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 # 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) ## Sprint 80 Operation form readability polish (2026-06-20)
- Added structured headings, helper text, field wrappers and action rows to dense raster operation controls. - 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: Next recommended pass:
- Continue with compact empty/error-state polish across QA, exports and AI lab result panels. - 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 export preview readability polish for large JSON/GeoJSON handoff artifacts.
- [x] Add accessibility focus polish for primary workbench keyboard navigation. - [x] Add accessibility focus polish for primary workbench keyboard navigation.
- [x] Add raster/vector operation form readability polish for dense tool panels. - [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 ## 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. 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 ## Scope implemented
- API client layer (`src/services/api`) - API client layer (`src/services/api`)
- Project and area list/create flows - Project and area list/create flows
@@ -95,9 +95,24 @@ export function DetectionLab({
Refresh models Refresh models
</button> </button>
</div> </div>
{loadingDetectionModels ? <p>Loading detection models...</p> : null} {loadingDetectionModels ? (
{detectionModelError ? <p className="error">{detectionModelError}</p> : null} <div className="result-state result-state-loading">
{detectionModels.length === 0 && !loadingDetectionModels ? <p>No detection models reported by backend</p> : null} <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"> <ul className="model-list">
{detectionModels.map((model) => ( {detectionModels.map((model) => (
<li className={model.configured ? 'model-card model-card-ready' : 'model-card'} key={model.model_id}> <li className={model.configured ? 'model-card model-card-ready' : 'model-card'} key={model.model_id}>
@@ -164,7 +179,12 @@ export function DetectionLab({
Run detection Run detection
</button> </button>
</div> </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 ? ( {detectionRunResult ? (
<div className="result-summary-card"> <div className="result-summary-card">
<p>Status: {detectionRunResult.status}</p> <p>Status: {detectionRunResult.status}</p>
@@ -218,8 +238,16 @@ export function DetectionLab({
<button className="primary-action" type="button" onClick={onLoadResults} disabled={!selectedDetectionRunId || loadingDetectionResults}> <button className="primary-action" type="button" onClick={onLoadResults} disabled={!selectedDetectionRunId || loadingDetectionResults}>
Load detections Load detections
</button> </button>
{loadingDetectionResults ? <p>Loading detection results...</p> : null} {loadingDetectionResults ? (
<p>Detections loaded: {detectionItems.length}</p> <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 ? ( {detectionItems.length > 0 ? (
<div className="table-scroll"> <div className="table-scroll">
<table> <table>
@@ -261,7 +289,12 @@ export function DetectionLab({
<button className="primary-action" type="button" onClick={onRunQa} disabled={runningDetectionQa || !selectedDetectionRunId || !detectionReferenceDatasetId}> <button className="primary-action" type="button" onClick={onRunQa} disabled={runningDetectionQa || !selectedDetectionRunId || !detectionReferenceDatasetId}>
Compare detections to reference Compare detections to reference
</button> </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 ? ( {detectionQaResult ? (
<div className="result-summary-card"> <div className="result-summary-card">
<p>Status: {detectionQaResult.status}</p> <p>Status: {detectionQaResult.status}</p>
@@ -217,7 +217,18 @@ export function ExportCenter({
</div> </div>
</div> </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 ? ( {latestExport ? (
<div className="latest-export-card"> <div className="latest-export-card">
<span>Latest export</span> <span>Latest export</span>
@@ -226,7 +237,7 @@ export function ExportCenter({
</div> </div>
) : null} ) : null}
{exports.length === 0 ? ( {exports.length === 0 ? (
<div className="empty-state"> <div className="result-state result-state-empty">
<strong>No exports registered yet.</strong> <strong>No exports registered yet.</strong>
<p>Create metadata, GeoJSON or HTML report artifacts once the active project has data to hand off.</p> <p>Create metadata, GeoJSON or HTML report artifacts once the active project has data to hand off.</p>
</div> </div>
@@ -291,7 +302,7 @@ export function ExportCenter({
</div> </div>
) : null} ) : null}
{exports.length > 0 && filteredExports.length === 0 ? ( {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> <strong>No exports match the current filters.</strong>
<p>Clear the search, type or status filter to return to the latest artifacts.</p> <p>Clear the search, type or status filter to return to the latest artifacts.</p>
</div> </div>
@@ -157,9 +157,14 @@ export function QualityResultsPanel({
Refresh QA/QC results Refresh QA/QC results
</button> </button>
</div> </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 ? ( {qualityChecks.length === 0 ? (
<div className="empty-state"> <div className="result-state result-state-empty">
<strong>No persisted QA/QC results yet</strong> <strong>No persisted QA/QC results yet</strong>
<p>Run dataset, detection or segmentation QA against a reference layer to populate this workspace.</p> <p>Run dataset, detection or segmentation QA against a reference layer to populate this workspace.</p>
</div> </div>
@@ -224,7 +229,7 @@ export function QualityResultsPanel({
</div> </div>
) : null} ) : null}
{qualityChecks.length > 0 && filteredQualityChecks.length === 0 ? ( {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> <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> <p>Clear the search, status or check type filter to return to the latest quality results.</p>
</div> </div>
@@ -95,9 +95,24 @@ export function SegmentationLab({
Refresh models Refresh models
</button> </button>
</div> </div>
{loadingSegmentationModels ? <p>Loading segmentation models...</p> : null} {loadingSegmentationModels ? (
{segmentationModelError ? <p className="error">{segmentationModelError}</p> : null} <div className="result-state result-state-loading">
{segmentationModels.length === 0 && !loadingSegmentationModels ? <p>No segmentation models reported by backend</p> : null} <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"> <ul className="model-list">
{segmentationModels.map((model) => ( {segmentationModels.map((model) => (
<li className={model.configured ? 'model-card model-card-ready' : 'model-card'} key={model.model_id}> <li className={model.configured ? 'model-card model-card-ready' : 'model-card'} key={model.model_id}>
@@ -159,9 +174,17 @@ export function SegmentationLab({
</button> </button>
</div> </div>
{!selectedSegmentationModelConfigured ? ( {!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} ) : null}
{segmentationRunError ? <p className="error">{segmentationRunError}</p> : null}
{segmentationRunResult ? ( {segmentationRunResult ? (
<div className="result-summary-card"> <div className="result-summary-card">
<p>Status: {segmentationRunResult.status}</p> <p>Status: {segmentationRunResult.status}</p>
@@ -215,8 +238,16 @@ export function SegmentationLab({
<button className="primary-action" type="button" onClick={onLoadResults} disabled={!selectedSegmentationRunId || loadingSegmentationResults}> <button className="primary-action" type="button" onClick={onLoadResults} disabled={!selectedSegmentationRunId || loadingSegmentationResults}>
Load segmentations Load segmentations
</button> </button>
{loadingSegmentationResults ? <p>Loading segmentation results...</p> : null} {loadingSegmentationResults ? (
<p>Segmentations loaded: {segmentationItems.length}</p> <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 ? ( {segmentationItems.length > 0 ? (
<div className="table-scroll"> <div className="table-scroll">
<table> <table>
@@ -262,7 +293,12 @@ export function SegmentationLab({
<button className="primary-action" type="button" onClick={onRunQa} disabled={runningSegmentationQa || !selectedSegmentationRunId || !segmentationReferenceDatasetId}> <button className="primary-action" type="button" onClick={onRunQa} disabled={runningSegmentationQa || !selectedSegmentationRunId || !segmentationReferenceDatasetId}>
Compare segmentations to reference Compare segmentations to reference
</button> </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 ? ( {segmentationQaResult ? (
<div className="result-summary-card"> <div className="result-summary-card">
<p>Status: {segmentationQaResult.status}</p> <p>Status: {segmentationQaResult.status}</p>
+48 -2
View File
@@ -1070,7 +1070,8 @@ section th {
.quality-check-card, .quality-check-card,
.export-card, .export-card,
.latest-export-card, .latest-export-card,
.empty-state { .empty-state,
.result-state {
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 8px; border-radius: 8px;
background: var(--panel-soft); background: var(--panel-soft);
@@ -1114,11 +1115,56 @@ button.entity-card {
.quality-check-card, .quality-check-card,
.export-card, .export-card,
.latest-export-card, .latest-export-card,
.empty-state { .empty-state,
.result-state {
min-width: 0; min-width: 0;
padding: 0.85rem; 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, .dataset-card,
.model-card, .model-card,
.quality-check-card, .quality-check-card,