Polish export system density
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-20 23:12:14 +02:00
parent 5544c92006
commit f50b25554b
8 changed files with 558 additions and 278 deletions
+251 -224
View File
@@ -105,238 +105,265 @@ export function ExportCenter({
return (
<section className="export-center" data-testid="export-center">
<div className="panel-title-row">
<div>
<h2>Export Center</h2>
<p className="muted">Create handoff artifacts from persisted datasets, AI runs and project metadata.</p>
</div>
<span className="count-pill">{exports.length} exports</span>
</div>
<div className="quality-summary-grid">
<div>
<span>GeoJSON</span>
<strong>{geojsonExports}</strong>
</div>
<div>
<span>Reports</span>
<strong>{reportExports}</strong>
</div>
<div>
<span>Selected dataset</span>
<strong>{selectedDataset?.name ?? 'none'}</strong>
</div>
</div>
<div className="handoff-summary-card">
<div className="export-center-shell">
<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>
<h2>Export Center</h2>
<p className="muted">Create handoff artifacts from persisted datasets, AI runs and project metadata.</p>
</div>
<span className="status-badge">{handoffStatus}</span>
<span className="count-pill">{exports.length} exports</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">
<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>
{loadingExports ? (
<div className="result-state result-state-loading">
<strong>Loading export registry.</strong>
<p>Retrieving persisted artifacts for the selected project.</p>
</div>
) : null}
{exportError ? (
<div className="result-state result-state-error">
<strong>Export action failed.</strong>
<p>{exportError}</p>
</div>
) : null}
{latestExport ? (
<div className="latest-export-card">
<span>Latest export</span>
<strong>{formatExportType(latestExport.export_type)}</strong>
<p>{latestExport.path}</p>
</div>
) : null}
{exports.length === 0 ? (
<div className="result-state result-state-empty">
<strong>No exports registered yet.</strong>
<p>Create metadata, GeoJSON or HTML report artifacts once the active project has data to hand off.</p>
</div>
) : null}
{exports.length > 0 ? (
<div className="export-history-controls" aria-label="Export history filters">
<label>
Search exports
<input
type="search"
placeholder="Type, id or path"
value={exportSearchQuery}
onChange={(event) => setExportSearchQuery(event.target.value)}
/>
</label>
<label>
Type
<select value={exportTypeFilter} onChange={(event) => setExportTypeFilter(event.target.value)}>
<option value="all">All types</option>
{exportTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</label>
<label>
Status
<select value={exportStatusFilter} onChange={(event) => setExportStatusFilter(event.target.value)}>
<option value="all">All statuses</option>
{exportStatuses.map((status) => (
<option key={status} value={status}>
{status}
</option>
))}
</select>
</label>
<button
type="button"
className="secondary-action"
onClick={() => {
setExportSearchQuery('')
setExportTypeFilter('all')
setExportStatusFilter('all')
setShowAllExports(false)
}}
disabled={!hasActiveExportFilters && !showAllExports}
>
Reset view
</button>
</div>
) : null}
{filteredExports.length > 10 ? (
<div className="list-limit-banner">
<span>
Showing {visibleExports.length} of {filteredExports.length} matching exports.
{hiddenExportCount > 0 ? ` ${hiddenExportCount} older artifacts hidden.` : ' All matching artifacts shown.'}
</span>
<button type="button" className="secondary-action" onClick={() => setShowAllExports((value) => !value)}>
{showAllExports ? 'Show latest 10' : 'Show all exports'}
</button>
</div>
) : null}
{exports.length > 0 && filteredExports.length === 0 ? (
<div className="result-state result-state-empty">
<strong>No exports match the current filters.</strong>
<p>Clear the search, type or status filter to return to the latest artifacts.</p>
</div>
) : null}
<ul className="export-list">
{visibleExports.map((item) => (
<li className="export-card" key={item.id}>
<div className="export-card-header">
<div>
<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>
<div className="export-summary-surface" aria-label="Export summary">
<div className="quality-summary-grid">
<div>
<span>GeoJSON</span>
<strong>{geojsonExports}</strong>
</div>
<p title={item.storage_path}>{compactPath(item.storage_path)}</p>
<div className="button-row">
{canPreviewJson(item) ? (
<button type="button" className="secondary-action" onClick={() => onPreviewContent(item.id)}>
Preview JSON content
<div>
<span>Reports</span>
<strong>{reportExports}</strong>
</div>
<div>
<span>Selected dataset</span>
<strong>{selectedDataset?.name ?? 'none'}</strong>
</div>
</div>
</div>
<div className="export-handoff-surface" aria-label="Export handoff readiness">
<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>
<div className="export-actions-surface" aria-label="Export artifact actions">
<div className="panel-title-row">
<div>
<h3>Artifact actions</h3>
<p className="muted">Create review artifacts from the current project, dataset or selected AI analysis run.</p>
</div>
</div>
<div className="export-action-grid">
<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>
) : (
<span className="status-badge">download-only HTML artifact</span>
)}
<button type="button" className="primary-action" onClick={() => onDownload(item.id)}>
Download artifact
</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>
</div>
<div className="export-state-stack">
{loadingExports ? (
<div className="result-state result-state-loading">
<strong>Loading export registry.</strong>
<p>Retrieving persisted artifacts for the selected project.</p>
</div>
) : null}
{exportError ? (
<div className="result-state result-state-error">
<strong>Export action failed.</strong>
<p>{exportError}</p>
</div>
) : null}
{latestExport ? (
<div className="latest-export-card">
<span>Latest export</span>
<strong>{formatExportType(latestExport.export_type)}</strong>
<p>{latestExport.path}</p>
</div>
) : null}
{exports.length === 0 ? (
<div className="result-state result-state-empty">
<strong>No exports registered yet.</strong>
<p>Create metadata, GeoJSON or HTML report artifacts once the active project has data to hand off.</p>
</div>
) : null}
</div>
<div className="export-history-surface" aria-label="Export history">
<div>
<h3>Export history</h3>
<p className="muted">Review persisted artifacts and open JSON previews where supported.</p>
</div>
{exports.length > 0 ? (
<div className="export-history-controls" aria-label="Export history filters">
<label>
Search exports
<input
type="search"
placeholder="Type, id or path"
value={exportSearchQuery}
onChange={(event) => setExportSearchQuery(event.target.value)}
/>
</label>
<label>
Type
<select value={exportTypeFilter} onChange={(event) => setExportTypeFilter(event.target.value)}>
<option value="all">All types</option>
{exportTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</label>
<label>
Status
<select value={exportStatusFilter} onChange={(event) => setExportStatusFilter(event.target.value)}>
<option value="all">All statuses</option>
{exportStatuses.map((status) => (
<option key={status} value={status}>
{status}
</option>
))}
</select>
</label>
<button
type="button"
className="secondary-action"
onClick={() => {
setExportSearchQuery('')
setExportTypeFilter('all')
setExportStatusFilter('all')
setShowAllExports(false)
}}
disabled={!hasActiveExportFilters && !showAllExports}
>
Reset view
</button>
</div>
</li>
))}
</ul>
) : null}
{filteredExports.length > 10 ? (
<div className="list-limit-banner">
<span>
Showing {visibleExports.length} of {filteredExports.length} matching exports.
{hiddenExportCount > 0 ? ` ${hiddenExportCount} older artifacts hidden.` : ' All matching artifacts shown.'}
</span>
<button type="button" className="secondary-action" onClick={() => setShowAllExports((value) => !value)}>
{showAllExports ? 'Show latest 10' : 'Show all exports'}
</button>
</div>
) : null}
{exports.length > 0 && filteredExports.length === 0 ? (
<div className="result-state result-state-empty">
<strong>No exports match the current filters.</strong>
<p>Clear the search, type or status filter to return to the latest artifacts.</p>
</div>
) : null}
<ul className="export-list">
{visibleExports.map((item) => (
<li className="export-card" key={item.id}>
<div className="export-card-header">
<div>
<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>
</div>
<p title={item.storage_path}>{compactPath(item.storage_path)}</p>
<div className="button-row">
{canPreviewJson(item) ? (
<button type="button" className="secondary-action" onClick={() => onPreviewContent(item.id)}>
Preview JSON content
</button>
) : (
<span className="status-badge">download-only HTML artifact</span>
)}
<button type="button" className="primary-action" onClick={() => onDownload(item.id)}>
Download artifact
</button>
</div>
</li>
))}
</ul>
</div>
</div>
</section>
)
}
@@ -15,62 +15,95 @@ export function ProviderPanel({
}: ProviderPanelProps): JSX.Element {
return (
<section className="system-provider-panel">
<div className="panel-title-row">
<div>
<h2>Provider Capabilities</h2>
<p className="muted">Review configured local providers and honest not-configured external sources.</p>
<div className="system-provider-shell">
<div className="panel-title-row">
<div>
<h2>Provider Capabilities</h2>
<p className="muted">Review configured local providers and honest not-configured external sources.</p>
</div>
<button type="button" className="secondary-action" onClick={onRefresh} disabled={loadingCapabilities}>
Refresh providers
</button>
</div>
<div className="system-provider-state-stack">
{loadingCapabilities ? (
<div className="result-state result-state-loading">
<strong>Loading provider capabilities.</strong>
<p>Checking local, fixture and external provider registry states.</p>
</div>
) : null}
{capabilitiesError ? (
<div className="result-state result-state-error">
<strong>Provider registry unavailable.</strong>
<p>{capabilitiesError}</p>
</div>
) : null}
{providers.length === 0 && !loadingCapabilities ? (
<div className="result-state result-state-empty">
<strong>No providers reported by backend.</strong>
<p>Refresh provider capabilities after the backend is reachable.</p>
</div>
) : null}
</div>
<div className="system-provider-capability-surface" aria-label="Provider capability registry">
<ul className="system-provider-list">
{providers.map((provider) => (
<li className="system-provider-card" key={provider.provider_name}>
<div className="system-provider-header">
<div>
<strong>{provider.display_name}</strong>
<span>{provider.provider_name}</span>
</div>
<span className={provider.configured ? 'status-badge status-badge-ready' : 'status-badge'}>{provider.status}</span>
</div>
<div className="system-provider-grid">
<div>
<span>Authority</span>
<strong>{provider.authority_level}</strong>
</div>
<div>
<span>Configured</span>
<strong>{provider.configured ? 'yes' : 'no'}</strong>
</div>
<div>
<span>Geometry</span>
<strong>{provider.supported_geometry_types.join(', ')}</strong>
</div>
<div>
<span>Query modes</span>
<strong>{provider.supported_query_modes.join(', ')}</strong>
</div>
</div>
<div className="provider-layer-list" aria-label={`${provider.provider_name} supported layers`}>
{provider.supported_layers.map((layer) => (
<span className="provider-layer-chip" key={layer}>
{layer}
</span>
))}
</div>
<div className="provider-detail-stack">
<div>limitation: {provider.limitation_message}</div>
{!provider.configured && provider.not_configured_reason ? (
<div>reason: {provider.not_configured_reason}</div>
) : null}
</div>
<div className="provider-provenance-grid">
<div className="provider-provenance-card">
<span>Attribution</span>
<strong>{provider.attribution}</strong>
</div>
<div className="provider-provenance-card">
<span>License</span>
<strong>{provider.license_note}</strong>
</div>
</div>
</li>
))}
</ul>
</div>
<button type="button" className="secondary-action" onClick={onRefresh} disabled={loadingCapabilities}>
Refresh providers
</button>
</div>
{loadingCapabilities ? <p>Loading provider capabilities...</p> : null}
{capabilitiesError ? <p className="error">{capabilitiesError}</p> : null}
{providers.length === 0 && !loadingCapabilities ? <p>No providers reported by backend</p> : null}
<ul className="system-provider-list">
{providers.map((provider) => (
<li className="system-provider-card" key={provider.provider_name}>
<div className="system-provider-header">
<div>
<strong>{provider.display_name}</strong>
<span>{provider.provider_name}</span>
</div>
<span className={provider.configured ? 'status-badge status-badge-ready' : 'status-badge'}>{provider.status}</span>
</div>
<div className="system-provider-grid">
<div>
<span>Authority</span>
<strong>{provider.authority_level}</strong>
</div>
<div>
<span>Configured</span>
<strong>{provider.configured ? 'yes' : 'no'}</strong>
</div>
<div>
<span>Geometry</span>
<strong>{provider.supported_geometry_types.join(', ')}</strong>
</div>
<div>
<span>Query modes</span>
<strong>{provider.supported_query_modes.join(', ')}</strong>
</div>
</div>
<div className="provider-layer-list" aria-label={`${provider.provider_name} supported layers`}>
{provider.supported_layers.map((layer) => (
<span className="provider-layer-chip" key={layer}>
{layer}
</span>
))}
</div>
<div>limitation: {provider.limitation_message}</div>
<div>attribution: {provider.attribution}</div>
<div>license: {provider.license_note}</div>
{!provider.configured && provider.not_configured_reason ? (
<div>reason: {provider.not_configured_reason}</div>
) : null}
</li>
))}
</ul>
</section>
)
}
+91
View File
@@ -1766,6 +1766,60 @@ button.entity-card {
margin: 0.8rem 0;
}
.export-center-shell,
.system-provider-shell {
display: grid;
gap: 0.85rem;
min-width: 0;
}
.export-summary-surface,
.export-handoff-surface,
.export-actions-surface,
.export-history-surface,
.system-provider-capability-surface {
display: grid;
gap: 0.68rem;
min-width: 0;
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.78rem;
background: linear-gradient(180deg, #ffffff, #f7fbf8);
box-shadow: 0 1px 0 rgba(19, 32, 24, 0.03);
}
.export-history-surface,
.system-provider-capability-surface {
background: #ffffff;
}
.export-center-shell .quality-summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
gap: 0.58rem;
margin: 0;
}
.export-handoff-surface .handoff-summary-card {
margin-top: 0;
border: 0;
padding: 0;
background: transparent;
box-shadow: none;
}
.export-actions-surface .export-action-grid {
margin: 0;
}
.export-state-stack,
.system-provider-state-stack,
.provider-detail-stack {
display: grid;
gap: 0.5rem;
min-width: 0;
}
.export-action-grid button {
min-height: 2.8rem;
text-align: left;
@@ -2045,6 +2099,43 @@ button.entity-card {
font-weight: 800;
}
.provider-detail-stack {
color: var(--muted);
font-size: 0.84rem;
line-height: 1.36;
}
.provider-provenance-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
gap: 0.5rem;
}
.provider-provenance-card {
min-width: 0;
border: 1px solid rgba(15, 118, 110, 0.16);
border-radius: 7px;
padding: 0.55rem;
background: #ffffff;
}
.provider-provenance-card span {
display: block;
color: var(--muted);
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.provider-provenance-card strong {
display: block;
margin-top: 0.18rem;
overflow-wrap: anywhere;
font-size: 0.88rem;
line-height: 1.25;
}
.export-preview-panel .job-result {
max-height: 38rem;
}