Polish change detection density
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 20:37:46 +02:00
parent c941b2619a
commit 335d5d1869
7 changed files with 255 additions and 70 deletions
+8
View File
@@ -7,6 +7,14 @@
# Changelog
## Sprint 87 Change Detection density polish (2026-06-20)
- Grouped Change Detection heading, input controls, result states, summary and warnings into focused surfaces.
- Reused shared result-state cards for not-enough-data and error states.
- Added compact desktop/mobile grids for vector inputs and change summary metrics.
- Added static regression coverage for Change Detection hierarchy and density contracts.
- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed.
## Sprint 86 QA/QC workspace density polish (2026-06-20)
- Grouped QA/QC summary, dataset evidence, refresh/filter controls and result history into focused surfaces.
@@ -0,0 +1,53 @@
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_change_detection_panel_exposes_structured_surfaces() -> None:
panel = (ROOT / "frontend" / "src" / "components" / "analysis" / "ChangeDetectionPanel.tsx").read_text(
encoding="utf-8"
)
assert 'className="panel change-detection-shell"' in panel
assert 'className="panel-header change-detection-heading"' in panel
assert 'className="change-detection-input-surface"' in panel
assert 'aria-label="Change detection input controls"' in panel
assert 'className="change-detection-state-stack"' in panel
assert 'className="change-detection-result-surface"' in panel
assert 'aria-label="Change detection result summary"' in panel
assert 'className="change-detection-warning-surface"' in panel
def test_change_detection_panel_preserves_existing_compare_controls() -> None:
panel = (ROOT / "frontend" / "src" / "components" / "analysis" / "ChangeDetectionPanel.tsx").read_text(
encoding="utf-8"
)
assert "onSourceDatasetChange" in panel
assert "onTargetDatasetChange" in panel
assert "onIouThresholdChange" in panel
assert "onIncludeUnchangedChange" in panel
assert "onRun" in panel
assert "Compare vectors" in panel
assert "Upload at least two vector datasets to compare." in panel
assert "result.added_count" in panel
assert "result.removed_count" in panel
assert "result.unchanged_count" in panel
def test_change_detection_density_css_contracts() -> None:
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
assert ".change-detection-shell" in css
assert ".change-detection-heading" in css
assert ".change-detection-input-surface" in css
assert ".change-detection-state-stack" in css
assert ".change-detection-result-surface" in css
assert ".change-detection-warning-surface" in css
assert ".change-detection-shell .form-grid" in css
assert ".change-detection-shell .summary-grid" in css
assert "grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));" in css
assert "grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));" in css
+32
View File
@@ -3143,3 +3143,35 @@ Limitations:
Next recommended pass:
- Continue with Change Detection panel hierarchy and analysis workspace balance after this pass is deployed and visually checked.
## Sprint 87 Change Detection density polish (2026-06-20)
Changed:
- Wrapped Change Detection input controls in `change-detection-input-surface`.
- Replaced loose error/empty text with shared `result-state` cards inside `change-detection-state-stack`.
- Wrapped result metrics in `change-detection-result-surface`.
- Wrapped warning output in `change-detection-warning-surface`.
- Added compact responsive CSS contracts for Change Detection form and summary grids.
- Added `backend/tests/test_sprint87_change_detection_density.py`.
- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`.
Tested:
- Live browser pre-check attempt against `http://192.168.10.150:1202` hit a transient browser automation click timeout on the QA/QC workspace nav; source review showed Change Detection was still the older header/form/summary stack.
- Red step: `python -m pytest backend/tests/test_sprint87_change_detection_density.py -q` failed on missing Change Detection surface and density CSS contracts.
- `python -m pytest backend/tests/test_sprint87_change_detection_density.py backend/tests/test_sprint18_change_detection.py backend/tests/test_sprint39_frontend_orchestration_hooks.py backend/tests/test_sprint86_quality_workspace_density.py backend/tests/test_sprint47_workbench_interaction_smoke.py -q` (`20 passed`)
- `cd frontend && npm run typecheck`
- `cd frontend && npm run build`
- `bash scripts/run_readiness_check.sh` (`295 passed`; frontend typecheck/build passed; Alembic head `202606120900`; live smoke syntax passed)
- Tower deploy via `scripts/deploy_tower.ps1` rebuilt and restarted the all-in-one container on `http://192.168.10.150:1202`.
- Tower deploy live migration smoke passed with PostGIS `3.6 USE_GEOS=1 USE_PROJ=1 USE_STATS=1` and required runtime schema objects present.
- Browser runtime verification passed for frontend, proxied API and icon.
- Direct Chrome live UI check passed on desktop `1440x1000` and mobile `390x844`: Change Detection shell/input surface rendered, no horizontal overflow and no console warnings/errors.
Open:
- None known after full readiness, Tower deploy and live browser validation.
Limitations:
- Frontend Change Detection presentation hierarchy only; no change-detection behavior, API contract, persistence, migration, provider fetching or AI/model changes.
Next recommended pass:
- Continue with AI Labs run-form hierarchy and detection/segmentation result density after this pass is deployed and visually checked.
+1
View File
@@ -76,6 +76,7 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Add Data workspace selected-summary and panel density polish.
- [x] Add Map workspace panel hierarchy and layer-control density polish.
- [x] Add QA/QC workspace result hierarchy and filter density polish.
- [x] Add Change Detection panel hierarchy and analysis workspace density polish.
## Sprint 8 status
+2
View File
@@ -10,6 +10,8 @@ Map workspace now surfaces the selected AOI, active layer and rendered feature s
QA/QC Results now separates persisted check summary, candidate/reference evidence, refresh/filter controls and result history into focused surfaces. Existing filters, refresh behavior, metric cards and stored result rendering are unchanged, with denser mobile grids for the same controls.
Change Detection now follows the same analysis workspace hierarchy: vector input controls, error/empty states, result metrics and warnings are separated into focused surfaces while the existing compare action and map overlay result flow remain unchanged.
AI Labs uses the same mobile-density baseline for Detection and Segmentation model cards, run forms, QA/result summaries and scroll-safe result tables. This keeps long model ids, tile paths and artifact paths from widening the workbench on phones.
Exports and System provider capabilities now use compact, mobile-safe cards for handoff actions, artifact filters and provider metadata. Long provider limitations, export ids and artifact paths wrap inside their cards instead of widening the workbench.
@@ -37,8 +37,8 @@ export function ChangeDetectionPanel({
onRun,
}: ChangeDetectionPanelProps): JSX.Element {
return (
<section className="panel">
<div className="panel-header">
<section className="panel change-detection-shell">
<div className="panel-header change-detection-heading">
<div>
<p className="eyebrow">Analysis</p>
<h2>Change Detection</h2>
@@ -48,80 +48,99 @@ export function ChangeDetectionPanel({
</button>
</div>
<div className="form-grid">
<label>
Source vector
<select value={sourceDatasetId} onChange={(event) => onSourceDatasetChange(event.target.value)}>
<option value="">Select source</option>
{vectorDatasets.map((dataset) => (
<option key={dataset.id} value={dataset.id}>
{datasetLabel(dataset)}
</option>
))}
</select>
</label>
<label>
Target vector
<select value={targetDatasetId} onChange={(event) => onTargetDatasetChange(event.target.value)}>
<option value="">Select target</option>
{vectorDatasets.map((dataset) => (
<option key={dataset.id} value={dataset.id}>
{datasetLabel(dataset)}
</option>
))}
</select>
</label>
<label>
IoU threshold
<input
max="1"
min="0"
step="0.05"
type="number"
value={iouThreshold}
onChange={(event) => onIouThresholdChange(Number(event.target.value))}
/>
</label>
<label className="checkbox-row">
<input
checked={includeUnchanged}
type="checkbox"
onChange={(event) => onIncludeUnchangedChange(event.target.checked)}
/>
Include unchanged geometry
</label>
<div className="change-detection-input-surface" aria-label="Change detection input controls">
<div className="form-grid">
<label>
Source vector
<select value={sourceDatasetId} onChange={(event) => onSourceDatasetChange(event.target.value)}>
<option value="">Select source</option>
{vectorDatasets.map((dataset) => (
<option key={dataset.id} value={dataset.id}>
{datasetLabel(dataset)}
</option>
))}
</select>
</label>
<label>
Target vector
<select value={targetDatasetId} onChange={(event) => onTargetDatasetChange(event.target.value)}>
<option value="">Select target</option>
{vectorDatasets.map((dataset) => (
<option key={dataset.id} value={dataset.id}>
{datasetLabel(dataset)}
</option>
))}
</select>
</label>
<label>
IoU threshold
<input
max="1"
min="0"
step="0.05"
type="number"
value={iouThreshold}
onChange={(event) => onIouThresholdChange(Number(event.target.value))}
/>
</label>
<label className="checkbox-row">
<input
checked={includeUnchanged}
type="checkbox"
onChange={(event) => onIncludeUnchangedChange(event.target.checked)}
/>
Include unchanged geometry
</label>
</div>
</div>
{error ? <p className="error">{error}</p> : null}
{!error && vectorDatasets.length < 2 ? <p className="muted">Upload at least two vector datasets to compare.</p> : null}
<div className="change-detection-state-stack">
{error ? (
<div className="result-state result-state-error">
<strong>Change detection failed.</strong>
<p>{error}</p>
</div>
) : null}
{!error && vectorDatasets.length < 2 ? (
<div className="result-state result-state-empty">
<strong>Not enough vector datasets</strong>
<p>Upload at least two vector datasets to compare.</p>
</div>
) : null}
</div>
{result ? (
<div className="summary-grid">
<div>
<span className="metric">{result.added_count}</span>
<span>Added</span>
</div>
<div>
<span className="metric">{result.removed_count}</span>
<span>Removed</span>
</div>
<div>
<span className="metric">{result.unchanged_count}</span>
<span>Unchanged</span>
</div>
<div>
<span className="metric">{result.geojson.features.length}</span>
<span>Map features</span>
<div className="change-detection-result-surface" aria-label="Change detection result summary">
<div className="summary-grid">
<div>
<span className="metric">{result.added_count}</span>
<span>Added</span>
</div>
<div>
<span className="metric">{result.removed_count}</span>
<span>Removed</span>
</div>
<div>
<span className="metric">{result.unchanged_count}</span>
<span>Unchanged</span>
</div>
<div>
<span className="metric">{result.geojson.features.length}</span>
<span>Map features</span>
</div>
</div>
</div>
) : null}
{result?.warnings.length ? (
<ul className="compact-list">
{result.warnings.map((warning) => (
<li key={warning}>{warning}</li>
))}
</ul>
<div className="change-detection-warning-surface">
<strong>Warnings</strong>
<ul className="compact-list">
{result.warnings.map((warning) => (
<li key={warning}>{warning}</li>
))}
</ul>
</div>
) : null}
</section>
)
+73 -3
View File
@@ -1465,6 +1465,71 @@ button.entity-card {
margin: 0;
}
.change-detection-shell {
display: grid;
gap: 0.75rem;
}
.change-detection-heading {
margin-bottom: 0;
}
.change-detection-input-surface,
.change-detection-result-surface,
.change-detection-warning-surface {
min-width: 0;
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.72rem;
background: #ffffff;
}
.change-detection-input-surface {
background: linear-gradient(180deg, #ffffff, #f7fbf8);
}
.change-detection-state-stack {
display: grid;
gap: 0.55rem;
}
.change-detection-shell .form-grid {
grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
gap: 0.62rem;
margin: 0;
}
.change-detection-shell .form-grid label {
min-width: 0;
}
.change-detection-shell .form-grid input,
.change-detection-shell .form-grid select {
width: 100%;
min-width: 0;
}
.change-detection-shell .summary-grid {
grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
gap: 0.55rem;
margin: 0;
}
.change-detection-warning-surface {
border-color: rgba(180, 83, 9, 0.24);
background: rgba(180, 83, 9, 0.08);
}
.change-detection-warning-surface strong {
display: block;
color: #92400e;
font-size: 0.88rem;
}
.change-detection-warning-surface .compact-list {
margin-top: 0.45rem;
}
.quality-results-shell .quality-summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
@@ -2798,12 +2863,16 @@ button.entity-card {
.quality-summary-surface,
.quality-evidence-surface,
.quality-control-surface,
.quality-history-surface {
.quality-history-surface,
.change-detection-input-surface,
.change-detection-result-surface,
.change-detection-warning-surface {
padding: 0.58rem;
}
.quality-results-shell .quality-summary-grid,
.quality-results-shell .quality-history-controls {
.quality-results-shell .quality-history-controls,
.change-detection-shell .form-grid {
grid-template-columns: repeat(auto-fit, minmax(8.25rem, 1fr));
gap: 0.5rem;
}
@@ -2811,7 +2880,8 @@ button.entity-card {
.quality-results-shell .quality-handoff-grid,
.quality-results-shell .quality-score-row,
.quality-results-shell .quality-metric-grid,
.quality-results-shell .metric-list {
.quality-results-shell .metric-list,
.change-detection-shell .summary-grid {
grid-template-columns: repeat(auto-fit, minmax(7.5rem, 1fr));
gap: 0.5rem;
}