Polish export system density
This commit is contained in:
@@ -7,6 +7,14 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Sprint 89 Export/System density polish (2026-06-20)
|
||||||
|
|
||||||
|
- Grouped Export Center summary, handoff readiness, artifact actions, state cards and history into focused surfaces.
|
||||||
|
- Grouped Provider Capabilities into a system shell with registry state cards, capability cards and attribution/license provenance cards.
|
||||||
|
- Preserved existing export action, filter, preview/download and provider refresh workflows without API or persistence changes.
|
||||||
|
- Added static regression coverage for Export/System hierarchy and density contracts.
|
||||||
|
- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed.
|
||||||
|
|
||||||
## Sprint 88 AI Labs density polish (2026-06-20)
|
## Sprint 88 AI Labs density polish (2026-06-20)
|
||||||
|
|
||||||
- Grouped Detection Lab and Segmentation Lab model registry, run controls, result loading and QA controls into focused surfaces.
|
- Grouped Detection Lab and Segmentation Lab model registry, run controls, result loading and QA controls into focused surfaces.
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_center_exposes_final_handoff_surfaces() -> None:
|
||||||
|
export_center = (ROOT / "frontend" / "src" / "components" / "exports" / "ExportCenter.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'className="export-center"' in export_center
|
||||||
|
assert 'className="export-center-shell"' in export_center
|
||||||
|
assert 'className="export-summary-surface"' in export_center
|
||||||
|
assert 'aria-label="Export summary"' in export_center
|
||||||
|
assert 'className="export-handoff-surface"' in export_center
|
||||||
|
assert 'aria-label="Export handoff readiness"' in export_center
|
||||||
|
assert 'className="export-actions-surface"' in export_center
|
||||||
|
assert 'aria-label="Export artifact actions"' in export_center
|
||||||
|
assert 'className="export-state-stack"' in export_center
|
||||||
|
assert 'className="export-history-surface"' in export_center
|
||||||
|
assert 'aria-label="Export history"' in export_center
|
||||||
|
|
||||||
|
|
||||||
|
def test_provider_panel_exposes_final_system_surfaces() -> None:
|
||||||
|
provider_panel = (ROOT / "frontend" / "src" / "components" / "providers" / "ProviderPanel.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'className="system-provider-panel"' in provider_panel
|
||||||
|
assert 'className="system-provider-shell"' in provider_panel
|
||||||
|
assert 'className="system-provider-state-stack"' in provider_panel
|
||||||
|
assert 'className="system-provider-capability-surface"' in provider_panel
|
||||||
|
assert 'aria-label="Provider capability registry"' in provider_panel
|
||||||
|
assert 'className="provider-provenance-grid"' in provider_panel
|
||||||
|
assert 'className="provider-provenance-card"' in provider_panel
|
||||||
|
assert 'className="provider-detail-stack"' in provider_panel
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_system_preserves_existing_workflow_controls() -> None:
|
||||||
|
export_center = (ROOT / "frontend" / "src" / "components" / "exports" / "ExportCenter.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
provider_panel = (ROOT / "frontend" / "src" / "components" / "providers" / "ProviderPanel.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "onExportDataset" in export_center
|
||||||
|
assert "onExportDetectionRun" in export_center
|
||||||
|
assert "onExportSegmentationRun" in export_center
|
||||||
|
assert "onExportProjectMetadata" in export_center
|
||||||
|
assert "onExportProjectReport" in export_center
|
||||||
|
assert "onPreviewContent" in export_center
|
||||||
|
assert "onDownload" in export_center
|
||||||
|
assert "exportTypeFilter" in export_center
|
||||||
|
assert "exportStatusFilter" in export_center
|
||||||
|
assert "exportSearchQuery" in export_center
|
||||||
|
assert "system-provider-list" in provider_panel
|
||||||
|
assert "system-provider-card" in provider_panel
|
||||||
|
assert "provider-layer-chip" in provider_panel
|
||||||
|
assert "provider.not_configured_reason" in provider_panel
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_system_density_css_contracts() -> None:
|
||||||
|
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert ".export-center-shell" in css
|
||||||
|
assert ".export-summary-surface" in css
|
||||||
|
assert ".export-handoff-surface" in css
|
||||||
|
assert ".export-actions-surface" in css
|
||||||
|
assert ".export-state-stack" in css
|
||||||
|
assert ".export-history-surface" in css
|
||||||
|
assert ".export-center-shell .quality-summary-grid" in css
|
||||||
|
assert ".system-provider-shell" in css
|
||||||
|
assert ".system-provider-state-stack" in css
|
||||||
|
assert ".system-provider-capability-surface" in css
|
||||||
|
assert ".provider-detail-stack" in css
|
||||||
|
assert ".provider-provenance-grid" in css
|
||||||
|
assert ".provider-provenance-card" in css
|
||||||
|
assert "grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));" in css
|
||||||
|
assert "background: linear-gradient(180deg, #ffffff, #f7fbf8);" in css
|
||||||
@@ -3210,3 +3210,34 @@ Limitations:
|
|||||||
|
|
||||||
Next recommended pass:
|
Next recommended pass:
|
||||||
- Continue with export/system final visual consistency or live workflow guidance once this pass is deployed and visually checked.
|
- Continue with export/system final visual consistency or live workflow guidance once this pass is deployed and visually checked.
|
||||||
|
|
||||||
|
## Sprint 89 Export/System density polish (2026-06-20)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Wrapped Export Center content in `export-center-shell`.
|
||||||
|
- Grouped export summary, handoff readiness, artifact actions, current state cards and history into focused `export-*` surfaces.
|
||||||
|
- Wrapped Provider Capabilities content in `system-provider-shell`.
|
||||||
|
- Replaced loose provider loading/error/empty text with shared `result-state` cards.
|
||||||
|
- Grouped provider registry content into `system-provider-capability-surface`.
|
||||||
|
- Split provider limitation/reason text and attribution/license data into `provider-detail-stack` and provenance cards.
|
||||||
|
- Added shared Export/System density CSS for compact summary, handoff, action, history and provider provenance grids.
|
||||||
|
- Added `backend/tests/test_sprint89_export_system_density.py`.
|
||||||
|
- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`.
|
||||||
|
|
||||||
|
Tested:
|
||||||
|
- Red step: `python -m pytest backend/tests/test_sprint89_export_system_density.py -q` failed on missing Export/System shells, surfaces and CSS contracts.
|
||||||
|
- `python -m pytest backend/tests/test_sprint89_export_system_density.py backend/tests/test_sprint51_quality_export_polish.py backend/tests/test_sprint64_export_handoff_polish.py backend/tests/test_sprint76_export_system_mobile_polish.py backend/tests/test_sprint78_export_preview_readability.py -q` (`12 passed`)
|
||||||
|
- `cd frontend && npm run typecheck`
|
||||||
|
- `cd frontend && npm run build`
|
||||||
|
- In-app browser fallback note: the Browser click path timed out on the local workspace navigation CDP command, so rendered validation used local Chrome via Playwright.
|
||||||
|
- Local Chrome UI check against `http://127.0.0.1:5174` passed on desktop and mobile: Export/System shells and surfaces rendered with no horizontal overflow. Local console showed expected Vite proxy 500s because only the frontend server was running.
|
||||||
|
- Full readiness and Tower deploy pending in this pass.
|
||||||
|
|
||||||
|
Open:
|
||||||
|
- None known before full readiness/deploy validation.
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
- Frontend Export/System presentation hierarchy only; no export behavior, API contract, persistence, migration, provider fetching or AI/model changes.
|
||||||
|
|
||||||
|
Next recommended pass:
|
||||||
|
- Continue with end-to-end workflow guidance after this pass is deployed and visually checked.
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Add QA/QC workspace result hierarchy and filter density polish.
|
- [x] Add QA/QC workspace result hierarchy and filter density polish.
|
||||||
- [x] Add Change Detection panel hierarchy and analysis workspace density polish.
|
- [x] Add Change Detection panel hierarchy and analysis workspace density polish.
|
||||||
- [x] Add AI Labs Detection/Segmentation hierarchy and result density polish.
|
- [x] Add AI Labs Detection/Segmentation hierarchy and result density polish.
|
||||||
|
- [x] Add Export/System handoff hierarchy and provider registry density polish.
|
||||||
|
|
||||||
## Sprint 8 status
|
## Sprint 8 status
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ AI Labs uses the same mobile-density baseline for Detection and Segmentation mod
|
|||||||
|
|
||||||
Exports and System provider capabilities now use compact, mobile-safe cards for handoff actions, artifact filters and provider metadata. Long provider limitations, export ids and artifact paths wrap inside their cards instead of widening the workbench.
|
Exports and System provider capabilities now use compact, mobile-safe cards for handoff actions, artifact filters and provider metadata. Long provider limitations, export ids and artifact paths wrap inside their cards instead of widening the workbench.
|
||||||
|
|
||||||
|
Export Center now separates export summary, handoff readiness, artifact actions, current export states and export history into focused surfaces. Provider Capabilities uses the same system-workspace rhythm with loading/error/empty cards, provider capability cards and dedicated attribution/license provenance cards.
|
||||||
|
|
||||||
The right-side inspector also uses mobile-safe metadata and tool panels. Dataset filenames, checksums, bounds, job JSON, raster/vector operation inputs and inspector action groups wrap or scroll within the inspector instead of widening the workbench.
|
The right-side inspector also uses mobile-safe metadata and tool panels. Dataset filenames, checksums, bounds, job JSON, raster/vector operation inputs and inspector action groups wrap or scroll within the inspector instead of widening the workbench.
|
||||||
|
|
||||||
Export Preview shows a compact JSON summary before the payload and keeps large JSON/GeoJSON handoff artifacts inside a scroll-contained preview shell. HTML report artifacts remain download-only.
|
Export Preview shows a compact JSON summary before the payload and keeps large JSON/GeoJSON handoff artifacts inside a scroll-contained preview shell. HTML report artifacts remain download-only.
|
||||||
|
|||||||
@@ -105,238 +105,265 @@ export function ExportCenter({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="export-center" data-testid="export-center">
|
<section className="export-center" data-testid="export-center">
|
||||||
<div className="panel-title-row">
|
<div className="export-center-shell">
|
||||||
<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="panel-title-row">
|
<div className="panel-title-row">
|
||||||
<div>
|
<div>
|
||||||
<h3>Handoff readiness</h3>
|
<h2>Export Center</h2>
|
||||||
<p className="muted">Pick the artifact that matches the current review state before sharing data outside the workbench.</p>
|
<p className="muted">Create handoff artifacts from persisted datasets, AI runs and project metadata.</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="status-badge">{handoffStatus}</span>
|
<span className="count-pill">{exports.length} exports</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="handoff-readiness-grid">
|
|
||||||
<div>
|
<div className="export-summary-surface" aria-label="Export summary">
|
||||||
<span>Vector dataset</span>
|
<div className="quality-summary-grid">
|
||||||
<strong>{canExportDataset ? selectedDataset?.name : 'none ready'}</strong>
|
<div>
|
||||||
</div>
|
<span>GeoJSON</span>
|
||||||
<div>
|
<strong>{geojsonExports}</strong>
|
||||||
<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>
|
</div>
|
||||||
<p title={item.storage_path}>{compactPath(item.storage_path)}</p>
|
<div>
|
||||||
<div className="button-row">
|
<span>Reports</span>
|
||||||
{canPreviewJson(item) ? (
|
<strong>{reportExports}</strong>
|
||||||
<button type="button" className="secondary-action" onClick={() => onPreviewContent(item.id)}>
|
</div>
|
||||||
Preview JSON content
|
<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>
|
</button>
|
||||||
) : (
|
</div>
|
||||||
<span className="status-badge">download-only HTML artifact</span>
|
<div className="handoff-action-card handoff-action-card-primary">
|
||||||
)}
|
<span>Project report</span>
|
||||||
<button type="button" className="primary-action" onClick={() => onDownload(item.id)}>
|
<strong>HTML summary</strong>
|
||||||
Download artifact
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
) : null}
|
||||||
))}
|
{filteredExports.length > 10 ? (
|
||||||
</ul>
|
<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>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,62 +15,95 @@ export function ProviderPanel({
|
|||||||
}: ProviderPanelProps): JSX.Element {
|
}: ProviderPanelProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<section className="system-provider-panel">
|
<section className="system-provider-panel">
|
||||||
<div className="panel-title-row">
|
<div className="system-provider-shell">
|
||||||
<div>
|
<div className="panel-title-row">
|
||||||
<h2>Provider Capabilities</h2>
|
<div>
|
||||||
<p className="muted">Review configured local providers and honest not-configured external sources.</p>
|
<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>
|
</div>
|
||||||
<button type="button" className="secondary-action" onClick={onRefresh} disabled={loadingCapabilities}>
|
|
||||||
Refresh providers
|
|
||||||
</button>
|
|
||||||
</div>
|
</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>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1766,6 +1766,60 @@ button.entity-card {
|
|||||||
margin: 0.8rem 0;
|
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 {
|
.export-action-grid button {
|
||||||
min-height: 2.8rem;
|
min-height: 2.8rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
@@ -2045,6 +2099,43 @@ button.entity-card {
|
|||||||
font-weight: 800;
|
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 {
|
.export-preview-panel .job-result {
|
||||||
max-height: 38rem;
|
max-height: 38rem;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user