Polish workbench result states
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 03:10:45 +02:00
parent 8459b8bbd5
commit b3d1a1a5ee
10 changed files with 239 additions and 23 deletions
+8
View File
@@ -7,6 +7,14 @@
# Changelog
## Sprint 81 Result state consistency polish (2026-06-20)
- Added shared result-state styling for compact loading, error, empty and ready states.
- Applied consistent state blocks to QA/QC results, export history and AI lab model/result panels.
- Replaced loose text/error rows in Detection and Segmentation Labs with scan-friendly state cards.
- Added static regression coverage for result-state CSS and panel usage contracts.
- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed.
## Sprint 80 Operation form readability polish (2026-06-20)
- Added structured headings, helper text, field wrappers and action rows to dense raster operation controls.
@@ -0,0 +1,47 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_result_state_css_contracts() -> None:
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
assert ".result-state" in css
assert ".result-state-error" in css
assert ".result-state-empty" in css
assert ".result-state-loading" in css
assert ".result-state-ready" in css
assert ".result-state strong" in css
assert ".result-state p" in css
assert "overflow-wrap: anywhere;" in css
def test_quality_and_export_panels_use_result_state_blocks() -> None:
quality_panel = (ROOT / "frontend" / "src" / "components" / "quality" / "QualityResultsPanel.tsx").read_text(
encoding="utf-8"
)
export_center = (ROOT / "frontend" / "src" / "components" / "exports" / "ExportCenter.tsx").read_text(
encoding="utf-8"
)
assert 'className="result-state result-state-error"' in quality_panel
assert 'className="result-state result-state-empty"' in quality_panel
assert 'className="result-state result-state-error"' in export_center
assert 'className="result-state result-state-empty"' in export_center
assert 'className="result-state result-state-loading"' in export_center
def test_ai_labs_use_result_state_blocks() -> None:
detection_lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(
encoding="utf-8"
)
segmentation_lab = (
ROOT / "frontend" / "src" / "components" / "segmentation" / "SegmentationLab.tsx"
).read_text(encoding="utf-8")
for content in (detection_lab, segmentation_lab):
assert 'className="result-state result-state-loading"' in content
assert 'className="result-state result-state-error"' in content
assert 'className="result-state result-state-empty"' in content
assert 'className="result-state result-state-ready"' in content
+27
View File
@@ -2956,3 +2956,30 @@ Limitations:
Next recommended pass:
- Continue with compact empty/error-state polish across QA, exports and AI lab result panels.
## Sprint 81 Result state consistency polish (2026-06-20)
Changed:
- Added shared `result-state` CSS variants for loading, error, empty and ready states.
- Applied result-state blocks to QA/QC error/empty/filter-empty states.
- Applied result-state blocks to export loading, error and empty/filter-empty states.
- Applied result-state blocks to Detection and Segmentation model loading/errors, empty registries, result counts and QA/run errors.
- Added `backend/tests/test_sprint81_result_state_polish.py`.
- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`.
Tested:
- Red step: `python -m pytest backend/tests/test_sprint81_result_state_polish.py -q` failed on missing result-state CSS and panel markup contracts.
- `python -m pytest backend/tests/test_sprint81_result_state_polish.py -q` (`3 passed`)
- `python -m pytest backend/tests/test_sprint81_result_state_polish.py backend/tests/test_sprint80_operation_form_readability.py backend/tests/test_sprint75_ai_labs_mobile_polish.py backend/tests/test_sprint76_export_system_mobile_polish.py backend/tests/test_sprint73_quality_result_filtering.py backend/tests/test_sprint70_quality_handoff_polish.py backend/tests/test_sprint8c_detection_visualization_qa.py backend/tests/test_sprint9_segmentation_foundation.py -q` (`34 passed`)
- `cd frontend && npm run typecheck`
- `cd frontend && npm run build`
- `bash scripts/run_readiness_check.sh` (`276 passed`; frontend typecheck/build passed; Alembic single head `202606120900`)
Open:
- Run full readiness, redeploy Tower and verify live runtime smoke after deployment.
Limitations:
- Frontend presentation/state polish only; no workflow behavior, API contract, persistence, migration, provider fetching or AI/model changes.
Next recommended pass:
- Continue with visual density review for topbar/sidebar/responsive shell after another live browser pass.
+1
View File
@@ -70,6 +70,7 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Add export preview readability polish for large JSON/GeoJSON handoff artifacts.
- [x] Add accessibility focus polish for primary workbench keyboard navigation.
- [x] Add raster/vector operation form readability polish for dense tool panels.
- [x] Add compact loading/error/empty/result state polish across QA, exports and AI labs.
## Sprint 8 status
+2
View File
@@ -18,6 +18,8 @@ Primary workbench navigation, overview shortcuts, inspector tabs and dataset act
Raster and vector operation panels use structured group headings, compact helper text, field grids, action rows and inline error blocks so dense GIS controls remain scannable in the inspector.
QA/QC, exports and AI lab result panels use shared loading, error, empty and ready state cards. This keeps model registry failures, empty histories and result counts visually consistent across the workbench.
## Scope implemented
- API client layer (`src/services/api`)
- Project and area list/create flows
@@ -95,9 +95,24 @@ export function DetectionLab({
Refresh models
</button>
</div>
{loadingDetectionModels ? <p>Loading detection models...</p> : null}
{detectionModelError ? <p className="error">{detectionModelError}</p> : null}
{detectionModels.length === 0 && !loadingDetectionModels ? <p>No detection models reported by backend</p> : null}
{loadingDetectionModels ? (
<div className="result-state result-state-loading">
<strong>Loading detection models.</strong>
<p>Checking backend model registry availability.</p>
</div>
) : null}
{detectionModelError ? (
<div className="result-state result-state-error">
<strong>Detection model registry unavailable.</strong>
<p>{detectionModelError}</p>
</div>
) : null}
{detectionModels.length === 0 && !loadingDetectionModels ? (
<div className="result-state result-state-empty">
<strong>No detection models reported by backend.</strong>
<p>Refresh models after the backend is reachable.</p>
</div>
) : null}
<ul className="model-list">
{detectionModels.map((model) => (
<li className={model.configured ? 'model-card model-card-ready' : 'model-card'} key={model.model_id}>
@@ -164,7 +179,12 @@ export function DetectionLab({
Run detection
</button>
</div>
{detectionRunError ? <p className="error">{detectionRunError}</p> : null}
{detectionRunError ? (
<div className="result-state result-state-error">
<strong>Detection run failed.</strong>
<p>{detectionRunError}</p>
</div>
) : null}
{detectionRunResult ? (
<div className="result-summary-card">
<p>Status: {detectionRunResult.status}</p>
@@ -218,8 +238,16 @@ export function DetectionLab({
<button className="primary-action" type="button" onClick={onLoadResults} disabled={!selectedDetectionRunId || loadingDetectionResults}>
Load detections
</button>
{loadingDetectionResults ? <p>Loading detection results...</p> : null}
<p>Detections loaded: {detectionItems.length}</p>
{loadingDetectionResults ? (
<div className="result-state result-state-loading">
<strong>Loading detection results.</strong>
<p>Retrieving persisted detections for the selected run.</p>
</div>
) : null}
<div className="result-state result-state-ready">
<strong>Detections loaded: {detectionItems.length}</strong>
<p>{selectedDetectionRunId ? 'Loaded from persisted detection records.' : 'Select a detection run before loading results.'}</p>
</div>
{detectionItems.length > 0 ? (
<div className="table-scroll">
<table>
@@ -261,7 +289,12 @@ export function DetectionLab({
<button className="primary-action" type="button" onClick={onRunQa} disabled={runningDetectionQa || !selectedDetectionRunId || !detectionReferenceDatasetId}>
Compare detections to reference
</button>
{detectionQaError ? <p className="error">{detectionQaError}</p> : null}
{detectionQaError ? (
<div className="result-state result-state-error">
<strong>Detection QA failed.</strong>
<p>{detectionQaError}</p>
</div>
) : null}
{detectionQaResult ? (
<div className="result-summary-card">
<p>Status: {detectionQaResult.status}</p>
@@ -217,7 +217,18 @@ export function ExportCenter({
</div>
</div>
</div>
{exportError ? <p className="error">{exportError}</p> : null}
{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>
@@ -226,7 +237,7 @@ export function ExportCenter({
</div>
) : null}
{exports.length === 0 ? (
<div className="empty-state">
<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>
@@ -291,7 +302,7 @@ export function ExportCenter({
</div>
) : null}
{exports.length > 0 && filteredExports.length === 0 ? (
<div className="empty-state">
<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>
@@ -157,9 +157,14 @@ export function QualityResultsPanel({
Refresh QA/QC results
</button>
</div>
{qualityChecksError ? <p className="error">{qualityChecksError}</p> : null}
{qualityChecksError ? (
<div className="result-state result-state-error">
<strong>QA/QC results could not be loaded.</strong>
<p>{qualityChecksError}</p>
</div>
) : null}
{qualityChecks.length === 0 ? (
<div className="empty-state">
<div className="result-state result-state-empty">
<strong>No persisted QA/QC results yet</strong>
<p>Run dataset, detection or segmentation QA against a reference layer to populate this workspace.</p>
</div>
@@ -224,7 +229,7 @@ export function QualityResultsPanel({
</div>
) : null}
{qualityChecks.length > 0 && filteredQualityChecks.length === 0 ? (
<div className="empty-state">
<div className="result-state result-state-empty">
<strong>No QA/QC results match the current filters.</strong>
<p>Clear the search, status or check type filter to return to the latest quality results.</p>
</div>
@@ -95,9 +95,24 @@ export function SegmentationLab({
Refresh models
</button>
</div>
{loadingSegmentationModels ? <p>Loading segmentation models...</p> : null}
{segmentationModelError ? <p className="error">{segmentationModelError}</p> : null}
{segmentationModels.length === 0 && !loadingSegmentationModels ? <p>No segmentation models reported by backend</p> : null}
{loadingSegmentationModels ? (
<div className="result-state result-state-loading">
<strong>Loading segmentation models.</strong>
<p>Checking backend model registry availability.</p>
</div>
) : null}
{segmentationModelError ? (
<div className="result-state result-state-error">
<strong>Segmentation model registry unavailable.</strong>
<p>{segmentationModelError}</p>
</div>
) : null}
{segmentationModels.length === 0 && !loadingSegmentationModels ? (
<div className="result-state result-state-empty">
<strong>No segmentation models reported by backend.</strong>
<p>Refresh models after the backend is reachable.</p>
</div>
) : null}
<ul className="model-list">
{segmentationModels.map((model) => (
<li className={model.configured ? 'model-card model-card-ready' : 'model-card'} key={model.model_id}>
@@ -159,9 +174,17 @@ export function SegmentationLab({
</button>
</div>
{!selectedSegmentationModelConfigured ? (
<p>{selectedSegmentationModelLimitation ?? 'Select a configured segmentation model'}</p>
<div className="result-state result-state-empty">
<strong>Segmentation model is not ready.</strong>
<p>{selectedSegmentationModelLimitation ?? 'Select a configured segmentation model'}</p>
</div>
) : null}
{segmentationRunError ? (
<div className="result-state result-state-error">
<strong>Segmentation run failed.</strong>
<p>{segmentationRunError}</p>
</div>
) : null}
{segmentationRunError ? <p className="error">{segmentationRunError}</p> : null}
{segmentationRunResult ? (
<div className="result-summary-card">
<p>Status: {segmentationRunResult.status}</p>
@@ -215,8 +238,16 @@ export function SegmentationLab({
<button className="primary-action" type="button" onClick={onLoadResults} disabled={!selectedSegmentationRunId || loadingSegmentationResults}>
Load segmentations
</button>
{loadingSegmentationResults ? <p>Loading segmentation results...</p> : null}
<p>Segmentations loaded: {segmentationItems.length}</p>
{loadingSegmentationResults ? (
<div className="result-state result-state-loading">
<strong>Loading segmentation results.</strong>
<p>Retrieving persisted segmentation polygons for the selected run.</p>
</div>
) : null}
<div className="result-state result-state-ready">
<strong>Segmentations loaded: {segmentationItems.length}</strong>
<p>{selectedSegmentationRunId ? 'Loaded from persisted segmentation records.' : 'Select a segmentation run before loading results.'}</p>
</div>
{segmentationItems.length > 0 ? (
<div className="table-scroll">
<table>
@@ -262,7 +293,12 @@ export function SegmentationLab({
<button className="primary-action" type="button" onClick={onRunQa} disabled={runningSegmentationQa || !selectedSegmentationRunId || !segmentationReferenceDatasetId}>
Compare segmentations to reference
</button>
{segmentationQaError ? <p className="error">{segmentationQaError}</p> : null}
{segmentationQaError ? (
<div className="result-state result-state-error">
<strong>Segmentation QA failed.</strong>
<p>{segmentationQaError}</p>
</div>
) : null}
{segmentationQaResult ? (
<div className="result-summary-card">
<p>Status: {segmentationQaResult.status}</p>
+48 -2
View File
@@ -1070,7 +1070,8 @@ section th {
.quality-check-card,
.export-card,
.latest-export-card,
.empty-state {
.empty-state,
.result-state {
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel-soft);
@@ -1114,11 +1115,56 @@ button.entity-card {
.quality-check-card,
.export-card,
.latest-export-card,
.empty-state {
.empty-state,
.result-state {
min-width: 0;
padding: 0.85rem;
}
.result-state {
display: grid;
gap: 0.18rem;
background: #ffffff;
overflow-wrap: anywhere;
}
.result-state strong {
font-size: 0.92rem;
line-height: 1.25;
}
.result-state p {
margin: 0;
color: var(--muted);
font-size: 0.84rem;
line-height: 1.38;
}
.result-state-error {
border-color: rgba(153, 27, 27, 0.25);
background: rgba(153, 27, 27, 0.07);
}
.result-state-error strong,
.result-state-error p {
color: var(--danger);
}
.result-state-empty {
border-style: dashed;
background: var(--panel-soft);
}
.result-state-loading {
border-color: rgba(180, 83, 9, 0.24);
background: rgba(180, 83, 9, 0.08);
}
.result-state-ready {
border-color: rgba(15, 118, 110, 0.28);
background: var(--accent-soft);
}
.dataset-card,
.model-card,
.quality-check-card,