diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a33b0a..0bccff83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ # Changelog +## Sprint 49 Workbench shell UI refactor (2026-06-17) + +- Replaced the one-page workbench panel stack with a task-based UI shell. +- Added primary workspaces for Overview, Data, Map, QA/QC, AI Labs, Exports and System. +- Added a persistent top context bar for active project, AOI, dataset and layer state. +- Moved dataset details into a persistent right-side inspector instead of leaving them below the full workflow. +- Kept existing hooks, API contracts, backend behavior, migrations, provider behavior and AI configuration unchanged. +- Added static regression coverage for the new shell regions and workspace navigation anchors. + ## Sprint 48 Backend API contract audit (2026-06-17) - Added `scripts/audit_api_contracts.py` to compare the active FastAPI route surface with `docs/API_CONTRACTS.md`. diff --git a/backend/tests/test_sprint30_workbench_components.py b/backend/tests/test_sprint30_workbench_components.py index 91067fcb..1d059688 100644 --- a/backend/tests/test_sprint30_workbench_components.py +++ b/backend/tests/test_sprint30_workbench_components.py @@ -18,15 +18,18 @@ def test_app_uses_quality_and_map_presentational_components() -> None: assert " None: +def test_map_workspace_lives_in_task_based_workbench_shell() -> None: app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") styles = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8") - assert app.index("') - assert "grid-template-columns: repeat(12, minmax(0, 1fr));" in styles - assert ".workspace-grid > section" in styles - assert "max-height: 42rem;" in styles - assert "overflow: auto;" in styles + assert "activeWorkspace === 'map'" in app + assert app.index("activeWorkspace === 'map'") < app.index(" None: diff --git a/backend/tests/test_sprint39_frontend_orchestration_hooks.py b/backend/tests/test_sprint39_frontend_orchestration_hooks.py index 900a90cc..3a12f0e4 100644 --- a/backend/tests/test_sprint39_frontend_orchestration_hooks.py +++ b/backend/tests/test_sprint39_frontend_orchestration_hooks.py @@ -35,10 +35,10 @@ def test_app_entrypoint_has_clean_encoding_and_react_imports() -> None: app = app_path.read_text(encoding="utf-8") assert not app_bytes.startswith(b"\xef\xbb\xbf") - assert "import { useMemo } from 'react'" in app + assert "import { useMemo, useState } from 'react'" in app assert "FormEvent" not in app assert "useEffect" not in app - assert "useState" not in app + assert "const [activeWorkspace, setActiveWorkspace] = useState('overview')" in app def test_demo_workflow_hook_owns_demo_api_and_cross_module_selection() -> None: diff --git a/backend/tests/test_sprint49_workbench_shell_refactor.py b/backend/tests/test_sprint49_workbench_shell_refactor.py new file mode 100644 index 00000000..4f643f6e --- /dev/null +++ b/backend/tests/test_sprint49_workbench_shell_refactor.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] + + +def test_app_uses_task_based_workbench_shell() -> None: + app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + + assert "type WorkspaceKey" in app + assert "workspaceNavItems" in app + assert 'data-testid={`workspace-nav-${item.key}`}' in app + assert 'className="workbench-layout"' in app + assert 'className="workbench-sidebar"' in app + assert 'className="workbench-main"' in app + assert 'className="workbench-inspector"' in app + assert "activeWorkspace === 'data'" in app + assert "activeWorkspace === 'map'" in app + assert "activeWorkspace === 'analysis'" in app + assert "activeWorkspace === 'ai'" in app + assert "activeWorkspace === 'exports'" in app + assert "activeWorkspace === 'system'" in app + + +def test_workbench_shell_css_provides_navigation_main_and_inspector_regions() -> None: + css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8") + + assert ".workbench-topbar" in css + assert ".context-bar" in css + assert ".workbench-layout" in css + assert ".workbench-sidebar" in css + assert ".nav-item-active" in css + assert ".workbench-main" in css + assert ".workbench-inspector" in css + assert ".workspace-grid-data" in css + assert ".workspace-grid-ai" in css diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index d6e84306..f2aa1729 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -2042,3 +2042,28 @@ Limitations: Next recommended pass: - Run release readiness, deploy Tower and decide whether to manually acknowledge the current collation version. + +## Sprint 49 Workbench shell UI refactor (2026-06-17) + +Changed: +- Audited the live workbench UI and confirmed the main usability issue was information architecture: all V1 workflows were mounted as one long vertical panel stack. +- Refactored the frontend into a task-based workbench shell with Overview, Data, Map, QA/QC, AI Labs, Exports and System workspaces. +- Added a persistent top context bar for active project, AOI, dataset and layer state. +- Moved selected dataset details into a persistent right-side inspector while keeping the same dataset/raster/vector operation callbacks. +- Added stable primary navigation test anchors. +- Documented the new frontend shell structure and updated the changelog. + +Tested: +- `cd frontend && npm run typecheck` +- `cd frontend && npm run build` +- Local Vite visual audit on `http://127.0.0.1:5175` + +Open: +- Local Vite visual audit shows `Request failed (404)` when no local backend/proxy target is available. Docker/nginx same-origin proxy behavior remains the production path. +- The next pass should run full release readiness and deploy to Tower for browser-facing verification on `http://192.168.10.150:1202`. + +Limitations: +- This pass intentionally changes UI structure only. It does not add product capabilities, alter API contracts, change migrations, fetch live providers or enable new AI models. + +Next recommended pass: +- Run full readiness, rebuild/deploy Tower, then perform a live browser smoke through the new workbench navigation. diff --git a/docs/TODO.md b/docs/TODO.md index 15182b69..bde4e605 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -312,3 +312,10 @@ This file now starts with the current implementation status. Older preparation/b - [x] Add implementation ticket index and tickets. - [x] Add API example payloads. - [x] Add final pre-Codex checklist. + +# Current Workbench UI + +- [x] Replace the one-page workflow panel stack with a task-based workbench shell. +- [x] Add persistent project/AOI/dataset/layer context. +- [x] Move selected dataset details into a persistent inspector. +- [ ] Deploy the shell refactor to Tower and run live browser smoke on port 1202. diff --git a/frontend/README.md b/frontend/README.md index 64def3b6..62901cd0 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -2,7 +2,7 @@ React + TypeScript + MapLibre foundation for project/area/dataset workflow. -The workbench is intentionally map-first: `MapWorkspace` is promoted above the dense workflow grid so GIS context stays visible before lower-detail provider, AI, QA and export panels. Long workflow panels are scroll-contained by CSS rather than stretching every grid row. +The workbench now uses a task-based shell instead of a single long panel stack. `App.tsx` still owns shared orchestration state, but the UI is organized into Overview, Data, Map, QA/QC, AI Labs, Exports and System workspaces with a persistent top context bar and right-side dataset inspector. ## Scope implemented - API client layer (`src/services/api`) @@ -220,6 +220,17 @@ The workbench is intentionally map-first: `MapWorkspace` is promoted above the d - Offline demo workflow orchestration lives in `src/hooks/useDemoWorkflow.ts`, because it coordinates project, dataset, map, QA/QC, detection, segmentation and export state after the backend fixture seed. - Workbench bootstrap and reload effects live in `src/hooks/useWorkbenchBootstrap.ts`, keeping `App.tsx` focused on composing hooks into panels. +## Workbench shell refactor + +- The primary UI is organized around `workspaceNavItems` in `src/App.tsx`. +- The shell regions are: + - `workbench-topbar`: active project, AOI, dataset and layer context. + - `workbench-sidebar`: primary navigation between workflow workspaces. + - `workbench-main`: one active workflow at a time. + - `workbench-inspector`: persistent selected dataset details and raster/vector operation controls. +- Existing API calls, hooks, MapLibre rendering and QA/AI/export flows are unchanged. +- Stable navigation test anchors use `data-testid="workspace-nav-{workspace}"`. + ## Raster dependency visibility Raster metadata and raster ops may remain unavailable when backend raster stack is missing. In that case: diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 10db3b10..23170316 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import './styles/app.css' import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel' import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel' @@ -30,7 +30,20 @@ function isVectorDatasetType(datasetType: string): boolean { return datasetType === 'vector' || datasetType === 'geojson' } +type WorkspaceKey = 'overview' | 'data' | 'map' | 'analysis' | 'ai' | 'exports' | 'system' + +const workspaceNavItems: Array<{ key: WorkspaceKey; label: string; description: string }> = [ + { key: 'overview', label: 'Overview', description: 'V1 readiness and next action' }, + { key: 'data', label: 'Data', description: 'Projects, AOI and datasets' }, + { key: 'map', label: 'Map', description: 'Layer review and inspection' }, + { key: 'analysis', label: 'QA/QC', description: 'Quality checks and change detection' }, + { key: 'ai', label: 'AI Labs', description: 'Detection and segmentation' }, + { key: 'exports', label: 'Exports', description: 'Artifacts and downloads' }, + { key: 'system', label: 'System', description: 'Provider capabilities' }, +] + function App(): JSX.Element { + const [activeWorkspace, setActiveWorkspace] = useState('overview') const { projects, selectedProject, @@ -370,269 +383,370 @@ function App(): JSX.Element { resetExportsForProject, }) + const selectedArea = areas.find((area) => area.id === selectedMapAreaId) ?? null + const activeWorkspaceItem = workspaceNavItems.find((item) => item.key === activeWorkspace) ?? workspaceNavItems[0] + return ( -
-
-

GeoIntel Kempen V1 Workbench

-

Raster/vector workbench with QA/QC, exports, change detection and AI foundation layers.

+
+
+
+

GeoAI Workbench

+

GeoIntel Kempen

+
+
+
+ Project + {selectedProject?.name ?? 'No project'} +
+
+ AOI + {selectedArea?.name ?? (areas.length > 0 ? 'Select area' : 'No AOI')} +
+
+ Dataset + {selectedDataset?.name ?? (datasets.length > 0 ? 'Select dataset' : 'No dataset')} +
+
+ Layer + {mapFeatureCollection ? `${mapFeatureCount} features` : 'No active layer'} +
+
{errorMessage ?

{errorMessage}

: null} - +
+ - +
+
+
+

Workspace

+

{activeWorkspaceItem.label}

+
+

{activeWorkspaceItem.description}

+
-
- + {activeWorkspace === 'overview' ? ( +
+ +
+
+

Recommended flow

+

Build the project, load data, review on map, validate, export.

+

+ Keep the daily workflow focused: prepare data first, inspect it spatially, then run QA/QC or AI lab tasks only when the + required datasets are present. +

+
+
+ + + + +
+
+
+ ) : null} - + {activeWorkspace === 'data' ? ( +
+ - + - + +
+ ) : null} - + {activeWorkspace === 'map' ? ( + + ) : null} - loadDetectionRuns()} - onSelectRun={setSelectedDetectionRunId} - onSetClassFilter={setDetectionClassFilter} - onSetMinConfidenceFilter={setDetectionMinConfidenceFilter} - onLoadResults={() => loadDetectionResults()} - onSelectReferenceDataset={setDetectionReferenceDatasetId} - onRunQa={runDetectionQa} - /> + {activeWorkspace === 'analysis' ? ( +
+ - loadSegmentationRuns()} - onSelectRun={setSelectedSegmentationRunId} - onSetClassFilter={setSegmentationClassFilter} - onSetMinConfidenceFilter={setSegmentationMinConfidenceFilter} - onLoadResults={() => loadSegmentationResults()} - onSelectReferenceDataset={setSegmentationReferenceDatasetId} - onRunQa={runSegmentationQa} - /> + loadQualityChecks()} + /> +
+ ) : null} - loadQualityChecks()} - /> + {activeWorkspace === 'ai' ? ( +
+ loadDetectionRuns()} + onSelectRun={setSelectedDetectionRunId} + onSetClassFilter={setDetectionClassFilter} + onSetMinConfidenceFilter={setDetectionMinConfidenceFilter} + onLoadResults={() => loadDetectionResults()} + onSelectReferenceDataset={setDetectionReferenceDatasetId} + onRunQa={runDetectionQa} + /> - loadExports()} - onExportDataset={exportSelectedDatasetGeoJson} - onExportDetectionRun={exportSelectedDetectionRunGeoJson} - onExportSegmentationRun={exportSelectedSegmentationRunGeoJson} - onExportProjectMetadata={exportProjectMetadata} - onExportProjectReport={exportProjectReport} - onPreviewContent={previewExportContent} - onDownload={downloadExportArtifact} - /> - -
+ loadSegmentationRuns()} + onSelectRun={setSelectedSegmentationRunId} + onSetClassFilter={setSegmentationClassFilter} + onSetMinConfidenceFilter={setSegmentationMinConfidenceFilter} + onLoadResults={() => loadSegmentationResults()} + onSelectReferenceDataset={setSegmentationReferenceDatasetId} + onRunQa={runSegmentationQa} + /> +
+ ) : null} - runVectorIntersect(availableVectorTargets)} - onPickDerivedDataset={pickDerivedDataset} - /> + {activeWorkspace === 'exports' ? ( +
+ loadExports()} + onExportDataset={exportSelectedDatasetGeoJson} + onExportDetectionRun={exportSelectedDetectionRunGeoJson} + onExportSegmentationRun={exportSelectedSegmentationRunGeoJson} + onExportProjectMetadata={exportProjectMetadata} + onExportProjectReport={exportProjectReport} + onPreviewContent={previewExportContent} + onDownload={downloadExportArtifact} + /> + +
+ ) : null} + + {activeWorkspace === 'system' ? ( + + ) : null} + + + +
) } diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index 884c0b64..7f5ae88b 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -506,3 +506,355 @@ section li strong + div { margin-top: 0.6rem; } } + +.workbench-shell { + width: 100%; + max-width: none; + padding: 0; + background: #eef4f1; +} + +.workbench-topbar { + position: sticky; + top: 0; + z-index: 20; + display: grid; + grid-template-columns: minmax(16rem, 26rem) minmax(0, 1fr); + gap: 1rem; + align-items: center; + border-bottom: 1px solid var(--line); + padding: 0.85rem 1rem; + background: rgba(248, 251, 249, 0.96); + backdrop-filter: blur(12px); +} + +.brand-block h1 { + font-size: 1.45rem; + line-height: 1.1; +} + +.context-bar { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 0.65rem; +} + +.context-bar > div { + min-height: 4.2rem; + border: 1px solid var(--line); + border-radius: 8px; + padding: 0.62rem 0.72rem; + background: #ffffff; +} + +.context-bar span { + display: block; + color: var(--muted); + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.context-bar strong { + display: block; + margin-top: 0.3rem; + font-size: 0.92rem; + line-height: 1.25; +} + +.workbench-layout { + display: grid; + grid-template-columns: 14rem minmax(0, 1fr) 24rem; + gap: 0; + height: calc(100vh - 5.95rem); + min-height: 42rem; +} + +.workbench-sidebar, +.workbench-inspector { + min-height: 0; + overflow: auto; + border-color: var(--line); + background: #f8fbf9; +} + +.workbench-sidebar { + border-right: 1px solid var(--line); + padding: 0.85rem; +} + +.workbench-sidebar nav { + display: grid; + gap: 0.45rem; +} + +.nav-item { + display: block; + width: 100%; + min-height: 4.2rem; + border-color: transparent; + padding: 0.66rem 0.72rem; + background: transparent; + box-shadow: none; + text-align: left; +} + +.nav-item:hover:not(:disabled) { + background: #ffffff; +} + +.nav-item span, +.nav-item small { + display: block; +} + +.nav-item span { + font-size: 0.96rem; +} + +.nav-item small { + margin-top: 0.22rem; + color: var(--muted); + font-size: 0.77rem; + font-weight: 550; + line-height: 1.3; +} + +.nav-item-active { + border-color: rgba(15, 118, 110, 0.34); + background: #ffffff; + box-shadow: inset 4px 0 0 var(--accent); +} + +.workbench-main { + min-width: 0; + min-height: 0; + overflow: auto; + padding: 1rem; +} + +.workbench-inspector { + border-left: 1px solid var(--line); + padding: 1rem; +} + +.workbench-inspector > section { + border: 0; + padding: 0; + background: transparent; + box-shadow: none; +} + +.workspace-heading { + display: flex; + gap: 1rem; + align-items: end; + justify-content: space-between; + margin-bottom: 1rem; + border: 1px solid var(--line); + border-radius: 8px; + padding: 0.85rem 1rem; + background: #ffffff; +} + +.workspace-heading h2 { + margin: 0; +} + +.workspace-heading p:last-child { + max-width: 32rem; + margin: 0; + color: var(--muted); + text-align: right; +} + +.workspace-stack { + display: grid; + gap: 1rem; +} + +.workspace-grid { + margin-top: 0; +} + +.workspace-grid-data { + grid-template-columns: minmax(16rem, 0.75fr) minmax(18rem, 0.95fr) minmax(22rem, 1.35fr); +} + +.workspace-grid-analysis, +.workspace-grid-ai, +.workspace-grid-exports { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.workspace-grid-data > section, +.workspace-grid-analysis > section, +.workspace-grid-ai > section, +.workspace-grid-exports > section, +.workspace-grid-data > section:nth-child(n), +.workspace-grid-analysis > section:nth-child(n), +.workspace-grid-ai > section:nth-child(n), +.workspace-grid-exports > section:nth-child(n) { + grid-column: auto; + max-height: none; + overflow: visible; +} + +.workspace-grid-ai > section { + align-self: stretch; +} + +.overview-actions { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(16rem, 24rem); + gap: 1rem; + align-items: center; +} + +.overview-actions h2 { + max-width: 42rem; + margin-bottom: 0.5rem; +} + +.quick-action-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.65rem; +} + +.quick-action-grid button { + min-height: 4.25rem; +} + +section form, +section > div:not(.map-controls):not(.feature-inspector):not(.quick-action-grid) { + display: grid; + gap: 0.55rem; +} + +section button + button { + margin-top: 0.45rem; +} + +section table { + width: 100%; + margin-top: 0.75rem; + border-collapse: collapse; + font-size: 0.86rem; +} + +section th, +section td { + border-bottom: 1px solid var(--line); + padding: 0.5rem; + text-align: left; + vertical-align: top; +} + +section th { + color: var(--muted); + font-size: 0.72rem; + letter-spacing: 0.05em; + text-transform: uppercase; +} + +.workbench-inspector .job-result, +.workbench-inspector pre { + max-height: 16rem; +} + +.workbench-inspector h2 { + position: sticky; + top: 0; + z-index: 2; + margin: 0 -1rem 0.8rem; + border-bottom: 1px solid var(--line); + padding: 0.35rem 1rem 0.75rem; + background: #f8fbf9; +} + +.workbench-main .map-container { + height: calc(100vh - 23rem); + min-height: 28rem; +} + +@media (max-width: 1280px) { + .workbench-layout { + grid-template-columns: 12rem minmax(0, 1fr); + } + + .workbench-inspector { + grid-column: 1 / -1; + border-top: 1px solid var(--line); + border-left: 0; + } +} + +@media (max-width: 980px) { + .workbench-topbar, + .workbench-layout, + .workspace-grid-data, + .workspace-grid-analysis, + .workspace-grid-ai, + .workspace-grid-exports, + .overview-actions { + grid-template-columns: 1fr; + } + + .workbench-layout { + height: auto; + } + + .workbench-sidebar { + position: sticky; + top: 5.8rem; + z-index: 10; + border-right: 0; + border-bottom: 1px solid var(--line); + } + + .workbench-sidebar nav, + .context-bar, + .quick-action-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .nav-item { + min-height: 3.45rem; + } + + .workspace-heading, + .status-strip-header { + display: block; + } + + .workspace-heading p:last-child, + .status-next-action { + margin-top: 0.45rem; + text-align: left; + } + + .workbench-main .map-container { + height: 28rem; + } +} + +@media (max-width: 620px) { + .workbench-topbar, + .workbench-main, + .workbench-inspector, + .workbench-sidebar { + padding: 0.75rem; + } + + .context-bar, + .workbench-sidebar nav, + .quick-action-grid { + grid-template-columns: 1fr; + } + + .status-strip-grid { + grid-template-columns: 1fr; + } +}