Polish export handoff workflow
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-18 23:38:06 +02:00
parent 705b53fbfa
commit ad288df761
7 changed files with 273 additions and 32 deletions
+102 -32
View File
@@ -43,6 +43,14 @@ function compactPath(value: string): string {
return parts.slice(-2).join('/')
}
function formatExportType(value: string): string {
return value
.split('_')
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ')
}
function exportMatchesSearch(item: ExportRead, query: string): boolean {
const normalizedQuery = query.trim().toLowerCase()
if (!normalizedQuery) {
@@ -79,6 +87,7 @@ export function ExportCenter({
const canExportDataset = Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type))
const geojsonExports = exports.filter((item) => item.export_type.includes('geojson')).length
const reportExports = exports.filter((item) => item.export_type === 'project_report_html').length
const handoffStatus = selectedProjectId ? 'project selected' : 'select project'
const exportTypes = useMemo(() => Array.from(new Set(exports.map((item) => item.export_type))).sort(), [exports])
const exportStatuses = useMemo(() => Array.from(new Set(exports.map((item) => item.status))).sort(), [exports])
const filteredExports = useMemo(
@@ -117,43 +126,102 @@ export function ExportCenter({
<strong>{selectedDataset?.name ?? 'none'}</strong>
</div>
</div>
<div className="handoff-summary-card">
<div className="panel-title-row">
<div>
<h3>Handoff readiness</h3>
<p className="muted">Pick the artifact that matches the current review state before sharing data outside the workbench.</p>
</div>
<span className="status-badge">{handoffStatus}</span>
</div>
<div className="handoff-readiness-grid">
<div>
<span>Vector dataset</span>
<strong>{canExportDataset ? selectedDataset?.name : 'none ready'}</strong>
</div>
<div>
<span>Detection run</span>
<strong>{selectedDetectionRunId || 'none'}</strong>
</div>
<div>
<span>Segmentation run</span>
<strong>{selectedSegmentationRunId || 'none'}</strong>
</div>
<div>
<span>Latest handoff artifact</span>
<strong>{latestExport ? formatExportType(latestExport.export_type) : 'none'}</strong>
</div>
</div>
</div>
<div className="export-action-grid">
<button
type="button"
className="secondary-action"
onClick={onRefresh}
disabled={!selectedProjectId || loadingExports}
data-testid="refresh-exports"
>
Refresh exports
</button>
<button
type="button"
className="primary-action"
onClick={onExportProjectMetadata}
disabled={!selectedProjectId || exporting}
data-testid="export-project-metadata"
>
Export project metadata JSON
</button>
<button type="button" className="secondary-action" onClick={onExportProjectReport} disabled={!selectedProjectId || exporting}>
Export project report HTML
</button>
<button type="button" className="secondary-action" onClick={onExportDataset} disabled={!canExportDataset || exporting}>
Export selected vector GeoJSON
</button>
<button type="button" className="secondary-action" onClick={onExportDetectionRun} disabled={!selectedDetectionRunId || exporting}>
Export selected detection run GeoJSON
</button>
<button type="button" className="secondary-action" onClick={onExportSegmentationRun} disabled={!selectedSegmentationRunId || exporting}>
Export selected segmentation run GeoJSON
</button>
<div className="handoff-action-grid">
<div className="handoff-action-card">
<span>Refresh</span>
<strong>Export registry</strong>
<p>Reload the persisted artifact list for the selected project.</p>
<button
type="button"
className="secondary-action"
onClick={onRefresh}
disabled={!selectedProjectId || loadingExports}
data-testid="refresh-exports"
>
Refresh exports
</button>
</div>
<div className="handoff-action-card handoff-action-card-primary">
<span>Project report</span>
<strong>HTML summary</strong>
<p>Generate the human-readable V1 handoff report from persisted project state.</p>
<button type="button" className="primary-action" onClick={onExportProjectReport} disabled={!selectedProjectId || exporting}>
Export project report HTML
</button>
</div>
<div className="handoff-action-card">
<span>Metadata</span>
<strong>Project JSON</strong>
<p>Export project, dataset, QA and artifact metadata for machine-readable review.</p>
<button
type="button"
className="secondary-action"
onClick={onExportProjectMetadata}
disabled={!selectedProjectId || exporting}
data-testid="export-project-metadata"
>
Export project metadata JSON
</button>
</div>
<div className="handoff-action-card">
<span>Vector dataset</span>
<strong>Selected GeoJSON</strong>
<p>Export the active vector/reference dataset when it is selected and ready.</p>
<button type="button" className="secondary-action" onClick={onExportDataset} disabled={!canExportDataset || exporting}>
Export selected vector GeoJSON
</button>
</div>
<div className="handoff-action-card">
<span>Detection run</span>
<strong>Detections GeoJSON</strong>
<p>Export persisted detection geometries for the selected analysis run.</p>
<button type="button" className="secondary-action" onClick={onExportDetectionRun} disabled={!selectedDetectionRunId || exporting}>
Export selected detection run GeoJSON
</button>
</div>
<div className="handoff-action-card">
<span>Segmentation run</span>
<strong>Segmentations GeoJSON</strong>
<p>Export persisted segmentation polygons for the selected analysis run.</p>
<button type="button" className="secondary-action" onClick={onExportSegmentationRun} disabled={!selectedSegmentationRunId || exporting}>
Export selected segmentation run GeoJSON
</button>
</div>
</div>
</div>
{exportError ? <p className="error">{exportError}</p> : null}
{latestExport ? (
<div className="latest-export-card">
<span>Latest export</span>
<strong>{latestExport.export_type}</strong>
<strong>{formatExportType(latestExport.export_type)}</strong>
<p>{latestExport.path}</p>
</div>
) : null}
@@ -233,9 +301,11 @@ export function ExportCenter({
<li className="export-card" key={item.id}>
<div className="export-card-header">
<div>
<strong>{item.export_type}</strong>
<span className="export-type-badge">{formatExportType(item.export_type)}</span>
<div className="entity-meta">
<span>export id: {item.id}</span>
{item.analysis_run_id ? <span>analysis run: {item.analysis_run_id}</span> : null}
{item.created_at ? <span>created: {item.created_at}</span> : null}
</div>
</div>
<span className={item.status === 'ready' ? 'status-badge status-badge-ready' : 'status-badge'}>{item.status}</span>
+101
View File
@@ -1202,6 +1202,105 @@ button.entity-card {
text-align: left;
}
.handoff-summary-card {
display: grid;
gap: 0.72rem;
margin-top: 0.8rem;
border: 1px solid rgba(15, 118, 110, 0.24);
border-radius: 8px;
padding: 0.85rem;
background: linear-gradient(180deg, #f5fbf8, #ffffff);
}
.handoff-summary-card .panel-title-row {
margin-bottom: 0;
}
.handoff-readiness-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.55rem;
}
.handoff-readiness-grid > div {
min-width: 0;
border: 1px solid var(--line);
border-radius: 7px;
padding: 0.62rem;
background: #ffffff;
}
.handoff-readiness-grid span,
.handoff-action-card span {
display: block;
color: var(--muted);
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.handoff-readiness-grid strong,
.handoff-action-card strong {
display: block;
margin-top: 0.22rem;
overflow: hidden;
font-size: 0.88rem;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.handoff-action-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.6rem;
grid-column: 1 / -1;
width: 100%;
}
.handoff-action-card {
display: grid;
min-width: 0;
gap: 0.35rem;
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.72rem;
background: #ffffff;
}
.handoff-action-card-primary {
border-color: rgba(15, 118, 110, 0.28);
background: var(--accent-soft);
}
.handoff-action-card p {
min-height: 2.55rem;
margin: 0;
color: var(--muted);
font-size: 0.84rem;
line-height: 1.36;
}
.handoff-action-card button {
width: 100%;
margin-top: 0;
}
.export-type-badge {
display: inline-flex;
max-width: 100%;
align-items: center;
border: 1px solid rgba(15, 118, 110, 0.22);
border-radius: 999px;
padding: 0.18rem 0.54rem;
background: var(--accent-soft);
color: var(--accent-strong);
font-size: 0.74rem;
font-weight: 850;
line-height: 1.2;
}
.latest-export-card {
display: grid;
gap: 0.2rem;
@@ -1575,8 +1674,10 @@ button.entity-card {
.layer-provenance-rail,
.lab-form-grid,
.quality-summary-grid,
.handoff-readiness-grid,
.quality-score-row,
.export-action-grid,
.handoff-action-grid,
.export-history-controls,
.inspector-action-bar,
.list-limit-banner {