Add V1 workbench status strip
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 ? <p className="error">{errorMessage}</p> : null}
|
||||
|
||||
<WorkbenchStatusStrip
|
||||
selectedProject={selectedProject}
|
||||
areas={areas}
|
||||
datasets={datasets}
|
||||
qualityChecks={qualityChecks}
|
||||
exports={exports}
|
||||
activeLayerFeatureCount={mapFeatureCount}
|
||||
selectedAreaHasGeometry={Boolean(areaFeatureCollection)}
|
||||
/>
|
||||
|
||||
<main className="workspace-grid">
|
||||
<ProjectPanel
|
||||
projects={projects}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
import type { AreaRead, DatasetCreateResponse, ExportRead, ProjectRead, QualityCheckRead } from '../types'
|
||||
|
||||
interface StatusItem {
|
||||
key: string
|
||||
label: string
|
||||
value: string
|
||||
detail: string
|
||||
state: 'ready' | 'warning' | 'waiting'
|
||||
}
|
||||
|
||||
interface WorkbenchStatusStripProps {
|
||||
selectedProject: ProjectRead | null
|
||||
areas: AreaRead[]
|
||||
datasets: DatasetCreateResponse[]
|
||||
qualityChecks: QualityCheckRead[]
|
||||
exports: ExportRead[]
|
||||
activeLayerFeatureCount: number
|
||||
selectedAreaHasGeometry: boolean
|
||||
}
|
||||
|
||||
function countByDatasetType(datasets: DatasetCreateResponse[], datasetType: string): number {
|
||||
return datasets.filter((dataset) => 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 (
|
||||
<section className="workbench-status-strip" aria-label="Workbench readiness">
|
||||
<div className="status-strip-header">
|
||||
<div>
|
||||
<p className="eyebrow">V1 readiness</p>
|
||||
<h2>Workbench status</h2>
|
||||
</div>
|
||||
<p className="status-next-action">{nextAction(items)}</p>
|
||||
</div>
|
||||
<div className="status-strip-grid">
|
||||
{items.map((item) => (
|
||||
<div className={`status-tile status-tile-${item.state}`} key={item.key}>
|
||||
<div className="status-tile-topline">
|
||||
<span>{item.label}</span>
|
||||
<span className="status-pill">{item.state}</span>
|
||||
</div>
|
||||
<strong>{item.value}</strong>
|
||||
<p>{item.detail}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user