Polish export handoff workflow
This commit is contained in:
@@ -695,3 +695,11 @@ Added:
|
|||||||
- Added regression coverage for AppError, HTTPException and validation-error envelopes.
|
- Added regression coverage for AppError, HTTPException and validation-error envelopes.
|
||||||
- Added static frontend parser coverage so API client error parsing does not drift silently.
|
- Added static frontend parser coverage so API client error parsing does not drift silently.
|
||||||
- No migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced.
|
- No migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced.
|
||||||
|
|
||||||
|
## Sprint 64 export/report handoff polish (2026-06-18)
|
||||||
|
|
||||||
|
- Added a handoff readiness summary to the Export Center for selected dataset, detection run, segmentation run and latest artifact context.
|
||||||
|
- Grouped existing export actions into scan-friendly artifact cards for project report, metadata, vector GeoJSON, detection GeoJSON and segmentation GeoJSON.
|
||||||
|
- Added clearer export history provenance with formatted export-type badges, analysis-run ids and created timestamps when available.
|
||||||
|
- Added regression coverage for the Export Center handoff structure and responsive styling.
|
||||||
|
- No API contracts, migrations, product capabilities, live provider fetching or AI/model dependency changes were introduced.
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_center_surfaces_handoff_readiness_context() -> None:
|
||||||
|
export_center = (ROOT / "frontend" / "src" / "components" / "exports" / "ExportCenter.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'className="handoff-summary-card"' in export_center
|
||||||
|
assert 'className="handoff-readiness-grid"' in export_center
|
||||||
|
assert 'className="handoff-action-grid"' in export_center
|
||||||
|
assert 'className="export-type-badge"' in export_center
|
||||||
|
assert "Latest handoff artifact" in export_center
|
||||||
|
assert "Project report" in export_center
|
||||||
|
assert "Detection run" in export_center
|
||||||
|
assert "Segmentation run" in export_center
|
||||||
|
assert "selectedDetectionRunId || 'none'" in export_center
|
||||||
|
assert "selectedSegmentationRunId || 'none'" in export_center
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_handoff_polish_has_responsive_styles() -> None:
|
||||||
|
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert ".handoff-summary-card" in css
|
||||||
|
assert ".handoff-readiness-grid" in css
|
||||||
|
assert ".handoff-action-grid" in css
|
||||||
|
assert ".export-type-badge" in css
|
||||||
|
assert ".handoff-action-card" in css
|
||||||
|
assert "grid-template-columns: repeat(4, minmax(0, 1fr));" in css
|
||||||
|
assert ".handoff-action-grid" in css and "@media (max-width: 980px)" in css
|
||||||
@@ -2460,3 +2460,30 @@ Limitations:
|
|||||||
|
|
||||||
Next recommended pass:
|
Next recommended pass:
|
||||||
- Continue with export/report handoff polish or add a live browser smoke that explicitly visits every workspace after deployment.
|
- Continue with export/report handoff polish or add a live browser smoke that explicitly visits every workspace after deployment.
|
||||||
|
|
||||||
|
## Sprint 64 Export/report handoff polish (2026-06-18)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Added a handoff readiness card to the Export Center using existing project, selected dataset, selected detection run, selected segmentation run and latest export state.
|
||||||
|
- Reworked existing export actions into artifact cards for refresh, project report, project metadata, selected vector GeoJSON, detection GeoJSON and segmentation GeoJSON.
|
||||||
|
- Added formatted export-type badges and extra export-card provenance for analysis-run ids and created timestamps when those fields are available.
|
||||||
|
- Added responsive styling for handoff summary/action cards.
|
||||||
|
- Added `backend/tests/test_sprint64_export_handoff_polish.py`.
|
||||||
|
- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`.
|
||||||
|
|
||||||
|
Tested:
|
||||||
|
- Red step: `python -m pytest backend/tests/test_sprint64_export_handoff_polish.py -q` failed on missing handoff summary/action structure and styles.
|
||||||
|
- `python -m pytest backend/tests/test_sprint64_export_handoff_polish.py backend/tests/test_sprint51_quality_export_polish.py -q` (`4 passed`)
|
||||||
|
- `cd frontend && npm run typecheck`
|
||||||
|
- `cd frontend && npm run build`
|
||||||
|
- Local Chrome/Playwright check against `http://127.0.0.1:5175` with live API proxy: Exports workspace opened, handoff summary/actions present, no console warnings/errors, no horizontal page overflow on desktop or mobile.
|
||||||
|
- `bash scripts/run_readiness_check.sh` (`233 passed`)
|
||||||
|
|
||||||
|
Open:
|
||||||
|
- Deploy Tower and run live browser verification after this pass.
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
- This is a frontend handoff usability pass only. It does not add export endpoints, change export persistence, introduce provider fetching or add AI/model behavior.
|
||||||
|
|
||||||
|
Next recommended pass:
|
||||||
|
- Run a live browser smoke across Exports and Overview after deployment, then continue with report artifact readability if the exported HTML itself needs visual polish.
|
||||||
|
|||||||
@@ -336,3 +336,4 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Expand golden datasets beyond the original single building QA fixture pair.
|
- [x] Expand golden datasets beyond the original single building QA fixture pair.
|
||||||
- [x] Add workbench visual polish pass for command bar, panel surfaces, empty states and mobile nav density.
|
- [x] Add workbench visual polish pass for command bar, panel surfaces, empty states and mobile nav density.
|
||||||
- [x] Add map/result overlay ergonomics for active layer provenance and feature property summaries.
|
- [x] Add map/result overlay ergonomics for active layer provenance and feature property summaries.
|
||||||
|
- [x] Add export/report handoff polish for artifact readiness, action grouping and export provenance.
|
||||||
|
|||||||
@@ -220,6 +220,7 @@ The workbench now uses a task-based shell instead of a single long panel stack.
|
|||||||
- 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.
|
- 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 bootstrap and reload effects live in `src/hooks/useWorkbenchBootstrap.ts`, keeping `App.tsx` focused on composing hooks into panels.
|
||||||
- The workbench shell includes a compact command bar, consistent raised/sunken surfaces, structured empty states and scroll-safe AI result tables to keep the V1 workflow usable across desktop and mobile widths.
|
- The workbench shell includes a compact command bar, consistent raised/sunken surfaces, structured empty states and scroll-safe AI result tables to keep the V1 workflow usable across desktop and mobile widths.
|
||||||
|
- The Export Center includes a handoff readiness summary, grouped artifact actions and provenance-rich export cards so report/GeoJSON handoff stays understandable in long-running demo projects.
|
||||||
|
|
||||||
## Workbench shell refactor
|
## Workbench shell refactor
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ function compactPath(value: string): string {
|
|||||||
return parts.slice(-2).join('/')
|
return parts.slice(-2).join('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatExportType(value: string): string {
|
||||||
|
return value
|
||||||
|
.split('_')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
function exportMatchesSearch(item: ExportRead, query: string): boolean {
|
function exportMatchesSearch(item: ExportRead, query: string): boolean {
|
||||||
const normalizedQuery = query.trim().toLowerCase()
|
const normalizedQuery = query.trim().toLowerCase()
|
||||||
if (!normalizedQuery) {
|
if (!normalizedQuery) {
|
||||||
@@ -79,6 +87,7 @@ export function ExportCenter({
|
|||||||
const canExportDataset = Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type))
|
const canExportDataset = Boolean(selectedDataset && isVectorDatasetType(selectedDataset.dataset_type))
|
||||||
const geojsonExports = exports.filter((item) => item.export_type.includes('geojson')).length
|
const geojsonExports = exports.filter((item) => item.export_type.includes('geojson')).length
|
||||||
const reportExports = exports.filter((item) => item.export_type === 'project_report_html').length
|
const reportExports = exports.filter((item) => item.export_type === 'project_report_html').length
|
||||||
|
const handoffStatus = selectedProjectId ? 'project selected' : 'select project'
|
||||||
const exportTypes = useMemo(() => Array.from(new Set(exports.map((item) => item.export_type))).sort(), [exports])
|
const exportTypes = useMemo(() => Array.from(new Set(exports.map((item) => item.export_type))).sort(), [exports])
|
||||||
const exportStatuses = useMemo(() => Array.from(new Set(exports.map((item) => item.status))).sort(), [exports])
|
const exportStatuses = useMemo(() => Array.from(new Set(exports.map((item) => item.status))).sort(), [exports])
|
||||||
const filteredExports = useMemo(
|
const filteredExports = useMemo(
|
||||||
@@ -117,7 +126,39 @@ export function ExportCenter({
|
|||||||
<strong>{selectedDataset?.name ?? 'none'}</strong>
|
<strong>{selectedDataset?.name ?? 'none'}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="handoff-summary-card">
|
||||||
|
<div className="panel-title-row">
|
||||||
|
<div>
|
||||||
|
<h3>Handoff readiness</h3>
|
||||||
|
<p className="muted">Pick the artifact that matches the current review state before sharing data outside the workbench.</p>
|
||||||
|
</div>
|
||||||
|
<span className="status-badge">{handoffStatus}</span>
|
||||||
|
</div>
|
||||||
|
<div className="handoff-readiness-grid">
|
||||||
|
<div>
|
||||||
|
<span>Vector dataset</span>
|
||||||
|
<strong>{canExportDataset ? selectedDataset?.name : 'none ready'}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Detection run</span>
|
||||||
|
<strong>{selectedDetectionRunId || 'none'}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Segmentation run</span>
|
||||||
|
<strong>{selectedSegmentationRunId || 'none'}</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Latest handoff artifact</span>
|
||||||
|
<strong>{latestExport ? formatExportType(latestExport.export_type) : 'none'}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="export-action-grid">
|
<div className="export-action-grid">
|
||||||
|
<div className="handoff-action-grid">
|
||||||
|
<div className="handoff-action-card">
|
||||||
|
<span>Refresh</span>
|
||||||
|
<strong>Export registry</strong>
|
||||||
|
<p>Reload the persisted artifact list for the selected project.</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="secondary-action"
|
className="secondary-action"
|
||||||
@@ -127,33 +168,60 @@ export function ExportCenter({
|
|||||||
>
|
>
|
||||||
Refresh exports
|
Refresh exports
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="handoff-action-card handoff-action-card-primary">
|
||||||
|
<span>Project report</span>
|
||||||
|
<strong>HTML summary</strong>
|
||||||
|
<p>Generate the human-readable V1 handoff report from persisted project state.</p>
|
||||||
|
<button type="button" className="primary-action" onClick={onExportProjectReport} disabled={!selectedProjectId || exporting}>
|
||||||
|
Export project report HTML
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="handoff-action-card">
|
||||||
|
<span>Metadata</span>
|
||||||
|
<strong>Project JSON</strong>
|
||||||
|
<p>Export project, dataset, QA and artifact metadata for machine-readable review.</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="primary-action"
|
className="secondary-action"
|
||||||
onClick={onExportProjectMetadata}
|
onClick={onExportProjectMetadata}
|
||||||
disabled={!selectedProjectId || exporting}
|
disabled={!selectedProjectId || exporting}
|
||||||
data-testid="export-project-metadata"
|
data-testid="export-project-metadata"
|
||||||
>
|
>
|
||||||
Export project metadata JSON
|
Export project metadata JSON
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="secondary-action" onClick={onExportProjectReport} disabled={!selectedProjectId || exporting}>
|
</div>
|
||||||
Export project report HTML
|
<div className="handoff-action-card">
|
||||||
</button>
|
<span>Vector dataset</span>
|
||||||
|
<strong>Selected GeoJSON</strong>
|
||||||
|
<p>Export the active vector/reference dataset when it is selected and ready.</p>
|
||||||
<button type="button" className="secondary-action" onClick={onExportDataset} disabled={!canExportDataset || exporting}>
|
<button type="button" className="secondary-action" onClick={onExportDataset} disabled={!canExportDataset || exporting}>
|
||||||
Export selected vector GeoJSON
|
Export selected vector GeoJSON
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="handoff-action-card">
|
||||||
|
<span>Detection run</span>
|
||||||
|
<strong>Detections GeoJSON</strong>
|
||||||
|
<p>Export persisted detection geometries for the selected analysis run.</p>
|
||||||
<button type="button" className="secondary-action" onClick={onExportDetectionRun} disabled={!selectedDetectionRunId || exporting}>
|
<button type="button" className="secondary-action" onClick={onExportDetectionRun} disabled={!selectedDetectionRunId || exporting}>
|
||||||
Export selected detection run GeoJSON
|
Export selected detection run GeoJSON
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="handoff-action-card">
|
||||||
|
<span>Segmentation run</span>
|
||||||
|
<strong>Segmentations GeoJSON</strong>
|
||||||
|
<p>Export persisted segmentation polygons for the selected analysis run.</p>
|
||||||
<button type="button" className="secondary-action" onClick={onExportSegmentationRun} disabled={!selectedSegmentationRunId || exporting}>
|
<button type="button" className="secondary-action" onClick={onExportSegmentationRun} disabled={!selectedSegmentationRunId || exporting}>
|
||||||
Export selected segmentation run GeoJSON
|
Export selected segmentation run GeoJSON
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{exportError ? <p className="error">{exportError}</p> : null}
|
{exportError ? <p className="error">{exportError}</p> : null}
|
||||||
{latestExport ? (
|
{latestExport ? (
|
||||||
<div className="latest-export-card">
|
<div className="latest-export-card">
|
||||||
<span>Latest export</span>
|
<span>Latest export</span>
|
||||||
<strong>{latestExport.export_type}</strong>
|
<strong>{formatExportType(latestExport.export_type)}</strong>
|
||||||
<p>{latestExport.path}</p>
|
<p>{latestExport.path}</p>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -233,9 +301,11 @@ export function ExportCenter({
|
|||||||
<li className="export-card" key={item.id}>
|
<li className="export-card" key={item.id}>
|
||||||
<div className="export-card-header">
|
<div className="export-card-header">
|
||||||
<div>
|
<div>
|
||||||
<strong>{item.export_type}</strong>
|
<span className="export-type-badge">{formatExportType(item.export_type)}</span>
|
||||||
<div className="entity-meta">
|
<div className="entity-meta">
|
||||||
<span>export id: {item.id}</span>
|
<span>export id: {item.id}</span>
|
||||||
|
{item.analysis_run_id ? <span>analysis run: {item.analysis_run_id}</span> : null}
|
||||||
|
{item.created_at ? <span>created: {item.created_at}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={item.status === 'ready' ? 'status-badge status-badge-ready' : 'status-badge'}>{item.status}</span>
|
<span className={item.status === 'ready' ? 'status-badge status-badge-ready' : 'status-badge'}>{item.status}</span>
|
||||||
|
|||||||
@@ -1202,6 +1202,105 @@ button.entity-card {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.handoff-summary-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.72rem;
|
||||||
|
margin-top: 0.8rem;
|
||||||
|
border: 1px solid rgba(15, 118, 110, 0.24);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.85rem;
|
||||||
|
background: linear-gradient(180deg, #f5fbf8, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-summary-card .panel-title-row {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-readiness-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-readiness-grid > div {
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 0.62rem;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-readiness-grid span,
|
||||||
|
.handoff-action-card span {
|
||||||
|
display: block;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-readiness-grid strong,
|
||||||
|
.handoff-action-card strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.22rem;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-action-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.6rem;
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-action-card {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 0.35rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.72rem;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-action-card-primary {
|
||||||
|
border-color: rgba(15, 118, 110, 0.28);
|
||||||
|
background: var(--accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-action-card p {
|
||||||
|
min-height: 2.55rem;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.84rem;
|
||||||
|
line-height: 1.36;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handoff-action-card button {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-type-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
max-width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid rgba(15, 118, 110, 0.22);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.18rem 0.54rem;
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent-strong);
|
||||||
|
font-size: 0.74rem;
|
||||||
|
font-weight: 850;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
.latest-export-card {
|
.latest-export-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.2rem;
|
gap: 0.2rem;
|
||||||
@@ -1575,8 +1674,10 @@ button.entity-card {
|
|||||||
.layer-provenance-rail,
|
.layer-provenance-rail,
|
||||||
.lab-form-grid,
|
.lab-form-grid,
|
||||||
.quality-summary-grid,
|
.quality-summary-grid,
|
||||||
|
.handoff-readiness-grid,
|
||||||
.quality-score-row,
|
.quality-score-row,
|
||||||
.export-action-grid,
|
.export-action-grid,
|
||||||
|
.handoff-action-grid,
|
||||||
.export-history-controls,
|
.export-history-controls,
|
||||||
.inspector-action-bar,
|
.inspector-action-bar,
|
||||||
.list-limit-banner {
|
.list-limit-banner {
|
||||||
|
|||||||
Reference in New Issue
Block a user