From 8c3813d44273a9a2e093f7950e16460d111dfefd Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 17 Jun 2026 00:55:12 +0200 Subject: [PATCH] Add V1 workbench status strip --- CHANGELOG.md | 7 + .../test_sprint22_workbench_status_strip.py | 35 +++++ docs/CODEX_EXECUTION_LOG.md | 17 +++ docs/TODO.md | 1 + frontend/README.md | 7 + frontend/src/App.tsx | 11 ++ .../src/components/WorkbenchStatusStrip.tsx | 132 ++++++++++++++++++ frontend/src/styles/app.css | 83 +++++++++++ 8 files changed, 293 insertions(+) create mode 100644 backend/tests/test_sprint22_workbench_status_strip.py create mode 100644 frontend/src/components/WorkbenchStatusStrip.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c65ea4c..3bfaf10f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ # Changelog +## Sprint 22 V1 workbench status strip (2026-06-17) + +- Added a compact frontend status strip for project, AOI, datasets, active map layer, QA/QC and exports. +- The strip is driven by existing App state and suggests the next operator action in the V1 loop. +- Added regression coverage to ensure the strip remains wired without introducing new API calls. +- No backend behavior, migrations, API contracts, provider downloads, AI inference or new dependencies were introduced. + ## Sprint 21 V1 demo workflow smoke hardening (2026-06-17) - Hardened the browser-facing demo/export smoke to verify connected V1 state: project area GeoJSON, fixture datasets, vector FeatureCollection content, vector feature summary, persisted QA/QC metrics and export downloads. diff --git a/backend/tests/test_sprint22_workbench_status_strip.py b/backend/tests/test_sprint22_workbench_status_strip.py new file mode 100644 index 00000000..aa982046 --- /dev/null +++ b/backend/tests/test_sprint22_workbench_status_strip.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_frontend_wires_v1_workbench_status_strip() -> None: + app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + component = (ROOT / "frontend" / "src" / "components" / "WorkbenchStatusStrip.tsx").read_text(encoding="utf-8") + css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8") + + assert "WorkbenchStatusStrip" in app + assert "selectedProject={selectedProject}" in app + assert "qualityChecks={qualityChecks}" in app + assert "activeLayerFeatureCount={mapFeatureCount}" in app + assert "selectedAreaHasGeometry={Boolean(areaFeatureCollection)}" in app + assert "V1 readiness" in component + assert "Workbench status" in component + assert "Workbench has the core V1 loop populated." in component + assert "workbench-status-strip" in css + assert "status-tile-ready" in css + + +def test_workbench_status_strip_summarizes_existing_v1_loop_only() -> None: + component = (ROOT / "frontend" / "src" / "components" / "WorkbenchStatusStrip.tsx").read_text(encoding="utf-8") + + assert "ProjectRead" in component + assert "AreaRead" in component + assert "DatasetCreateResponse" in component + assert "QualityCheckRead" in component + assert "ExportRead" in component + assert "fetch(" not in component + assert "api" not in component.lower() diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index eb6015fd..49391cd5 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -1,3 +1,20 @@ +## Sprint 22 V1 workbench status strip (2026-06-17) + +Changed: +- Added `frontend/src/components/WorkbenchStatusStrip.tsx` to summarize existing V1 state for project, AOI, datasets, active map layer, QA/QC and exports. +- Wired the status strip into `frontend/src/App.tsx` using existing orchestration state only. +- Added compact status-strip styling and regression tests for the frontend wiring contract. +- Updated frontend README, TODO and changelog. + +Tested: +- Passed: backend compile, focused pytest, full backend pytest with DeprecationWarning as error, frontend typecheck/build, Alembic heads, Alembic SQL upgrade, readiness via Git Bash and live smoke syntax check via Git Bash. + +Known limitations: +- The strip is a read-only operator summary; it intentionally does not add new backend status APIs or product workflows. + +Next recommended pass: +- Add a compact project handoff summary in exports/report output if the browser-facing V1 workflow remains green. + ## Sprint 21 V1 demo workflow smoke hardening (2026-06-17) Changed: diff --git a/docs/TODO.md b/docs/TODO.md index 974514e5..9232e8d6 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -31,6 +31,7 @@ This file now starts with the current implementation status. Older preparation/b - [x] Persisted export foundation for vector/detection/segmentation GeoJSON and project metadata JSON. - [x] Lightweight HTML project report artifact export. - [x] Browser-facing demo/export workflow smoke script with connected V1 state checks. +- [x] Compact V1 workbench status strip for project, AOI, datasets, map, QA/QC and exports. - [x] Live Docker/PostGIS validation on Tower/Unraid. - [ ] Real YOLO compatibility smoke with optional AI extras and local model file. - [ ] Further frontend state/module decomposition beyond Sprint 10 extraction. diff --git a/frontend/README.md b/frontend/README.md index f544d6ca..616da9d9 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -156,6 +156,13 @@ React + TypeScript + MapLibre foundation for project/area/dataset workflow. - Loading the explicit demo workflow now opens the candidate vector fixture dataset directly, so the Map Workbench shows the demo vector layer without an extra manual dataset click. - The demo/export verification script now checks connected V1 state: area GeoJSON, fixture datasets, vector FeatureCollection content, vector feature summary, persisted QA/QC metrics and export downloads through the frontend proxy. +## Sprint 22 additions + +- Added a compact V1 Workbench status strip above the main panels. +- The strip summarizes existing connected state for project, AOI, datasets, active map layer, persisted QA/QC results and exports. +- It suggests the next operator action based on missing V1 loop state without calling new APIs or adding backend behavior. +- The status strip is implemented in `src/components/WorkbenchStatusStrip.tsx` and remains driven by `App.tsx` orchestration state. + ## Release hardening updates - Production builds split application code, React vendor code and MapLibre vendor code into separate chunks. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 57b61618..9be1951d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,6 +10,7 @@ import { DetectionLab } from './components/detection/DetectionLab' import { ExportCenter } from './components/exports/ExportCenter' import { AreaPanel } from './components/project/AreaPanel' import { ProjectPanel } from './components/project/ProjectPanel' +import { WorkbenchStatusStrip } from './components/WorkbenchStatusStrip' import type { ApiError, ChangeDetectionSummary, @@ -1374,6 +1375,16 @@ function App(): JSX.Element { {errorMessage ?

{errorMessage}

: null} + +
dataset.dataset_type === datasetType).length +} + +function countReferenceDatasets(datasets: DatasetCreateResponse[]): number { + return datasets.filter((dataset) => dataset.dataset_role === 'reference').length +} + +function nextAction(items: StatusItem[]): string { + if (!items.some((item) => item.key === 'project' && item.state === 'ready')) { + return 'Create or open a project.' + } + if (!items.some((item) => item.key === 'area' && item.state === 'ready')) { + return 'Create an AOI or select an area with geometry.' + } + if (!items.some((item) => item.key === 'datasets' && item.state === 'ready')) { + return 'Upload a vector or raster dataset, or load the offline demo workflow.' + } + if (!items.some((item) => item.key === 'qa' && item.state === 'ready')) { + return 'Run QA/QC against a reference dataset when candidate and reference data are available.' + } + if (!items.some((item) => item.key === 'exports' && item.state === 'ready')) { + return 'Create a metadata, report or GeoJSON export for handoff.' + } + return 'Workbench has the core V1 loop populated.' +} + +export function WorkbenchStatusStrip({ + selectedProject, + areas, + datasets, + qualityChecks, + exports, + activeLayerFeatureCount, + selectedAreaHasGeometry, +}: WorkbenchStatusStripProps): JSX.Element { + const readyDatasets = datasets.filter((dataset) => dataset.status === 'ready').length + const vectorDatasets = countByDatasetType(datasets, 'vector') + countByDatasetType(datasets, 'geojson') + const rasterDatasets = countByDatasetType(datasets, 'raster') + const referenceDatasets = countReferenceDatasets(datasets) + const latestQualityCheck = qualityChecks[0] + const latestExport = exports[0] + + const items: StatusItem[] = [ + { + key: 'project', + label: 'Project', + value: selectedProject ? selectedProject.name : 'No project', + detail: selectedProject ? selectedProject.region : 'Open or create a project', + state: selectedProject ? 'ready' : 'waiting', + }, + { + key: 'area', + label: 'AOI', + value: `${areas.length} area${areas.length === 1 ? '' : 's'}`, + detail: selectedAreaHasGeometry ? 'selected area has map geometry' : 'no selected map geometry', + state: selectedAreaHasGeometry ? 'ready' : areas.length > 0 ? 'warning' : 'waiting', + }, + { + key: 'datasets', + label: 'Datasets', + value: `${readyDatasets}/${datasets.length} ready`, + detail: `${vectorDatasets} vector, ${rasterDatasets} raster, ${referenceDatasets} reference`, + state: datasets.length === 0 ? 'waiting' : readyDatasets === datasets.length ? 'ready' : 'warning', + }, + { + key: 'map', + label: 'Map', + value: `${activeLayerFeatureCount} feature${activeLayerFeatureCount === 1 ? '' : 's'}`, + detail: selectedAreaHasGeometry ? 'AOI overlay available' : 'AOI overlay missing', + state: selectedAreaHasGeometry || activeLayerFeatureCount > 0 ? 'ready' : 'waiting', + }, + { + key: 'qa', + label: 'QA/QC', + value: `${qualityChecks.length} check${qualityChecks.length === 1 ? '' : 's'}`, + detail: latestQualityCheck ? `${latestQualityCheck.check_type}: ${latestQualityCheck.status}` : 'no persisted QA/QC result', + state: qualityChecks.length > 0 ? 'ready' : 'waiting', + }, + { + key: 'exports', + label: 'Exports', + value: `${exports.length} export${exports.length === 1 ? '' : 's'}`, + detail: latestExport ? `${latestExport.export_type}: ${latestExport.status}` : 'no handoff artifact yet', + state: exports.length > 0 ? 'ready' : 'waiting', + }, + ] + + return ( +
+
+
+

V1 readiness

+

Workbench status

+
+

{nextAction(items)}

+
+
+ {items.map((item) => ( +
+
+ {item.label} + {item.state} +
+ {item.value} +

{item.detail}

+
+ ))} +
+
+ ) +} diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index 20639436..97428d4c 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -134,6 +134,89 @@ ul { padding: 0.6rem; } +.workbench-status-strip { + margin: 1rem 0; +} + +.status-strip-header { + display: flex; + gap: 1rem; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.status-strip-header h2 { + margin-bottom: 0; +} + +.status-next-action { + max-width: 32rem; + margin: 0; + color: var(--muted); + text-align: right; +} + +.status-strip-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 0.65rem; +} + +.status-tile { + min-height: 7rem; + border: 1px solid var(--line); + border-left: 4px solid #64748b; + border-radius: 8px; + padding: 0.65rem; + background: #ffffff; +} + +.status-tile strong { + display: block; + margin-top: 0.55rem; + font-size: 1.05rem; +} + +.status-tile p { + margin: 0.35rem 0 0; + color: var(--muted); + font-size: 0.88rem; +} + +.status-tile-ready { + border-left-color: #15803d; +} + +.status-tile-warning { + border-left-color: #b45309; +} + +.status-tile-waiting { + border-left-color: #64748b; +} + +.status-tile-topline { + display: flex; + gap: 0.5rem; + align-items: center; + justify-content: space-between; + color: var(--muted); + font-size: 0.78rem; + font-weight: 700; + text-transform: uppercase; +} + +.status-pill { + width: auto; + border: 1px solid var(--line); + border-radius: 999px; + padding: 0.1rem 0.45rem; + background: #f8fafc; + color: #334155; + text-transform: lowercase; +} + .metric { display: block; font-size: 1.4rem;