Polish export handoff artifacts
This commit is contained in:
@@ -7,6 +7,14 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Sprint 93 export handoff artifact polish (2026-06-21)
|
||||||
|
|
||||||
|
- Added a latest handoff artifacts section to the Export Center for project reports, project metadata, dataset GeoJSON, detection GeoJSON and segmentation GeoJSON.
|
||||||
|
- Reused existing preview/download export actions from each latest artifact card without changing export API contracts or persistence.
|
||||||
|
- Added responsive styling for latest artifact cards, empty artifact states and compact artifact actions.
|
||||||
|
- Added regression coverage for grouped latest artifact surfaces and preserved preview/download controls.
|
||||||
|
- No API contracts, migrations, provider fetching or AI/model behavior changed.
|
||||||
|
|
||||||
## Sprint 92 workflow rail interaction polish (2026-06-21)
|
## Sprint 92 workflow rail interaction polish (2026-06-21)
|
||||||
|
|
||||||
- Audited the live Overview workflow rail click path from Overview to Data, Map, QA/QC and Exports.
|
- Audited the live Overview workflow rail click path from Overview to Data, Map, QA/QC and Exports.
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_center_groups_latest_handoff_artifacts_by_type() -> None:
|
||||||
|
export_center = (ROOT / "frontend" / "src" / "components" / "exports" / "ExportCenter.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "latestHandoffArtifacts" in export_center
|
||||||
|
assert "Latest handoff artifacts" in export_center
|
||||||
|
assert "Project report" in export_center
|
||||||
|
assert "Project metadata" in export_center
|
||||||
|
assert "Dataset GeoJSON" in export_center
|
||||||
|
assert "Detection GeoJSON" in export_center
|
||||||
|
assert "Segmentation GeoJSON" in export_center
|
||||||
|
assert "latest-artifact-grid" in export_center
|
||||||
|
assert "latest-artifact-card" in export_center
|
||||||
|
assert "No artifact yet" in export_center
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_center_latest_artifact_cards_keep_existing_actions() -> None:
|
||||||
|
export_center = (ROOT / "frontend" / "src" / "components" / "exports" / "ExportCenter.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "getLatestExportByType" in export_center
|
||||||
|
assert "const item = artifact.item" in export_center
|
||||||
|
assert "canPreviewJson(item)" in export_center
|
||||||
|
assert "onPreviewContent(item.id)" in export_center
|
||||||
|
assert "onDownload(item.id)" in export_center
|
||||||
|
assert "compactPath(item.storage_path)" in export_center
|
||||||
|
assert "formatExportCreated(item.created_at)" in export_center
|
||||||
|
|
||||||
|
|
||||||
|
def test_export_center_latest_artifact_styles_are_responsive() -> None:
|
||||||
|
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert ".latest-artifact-grid" in css
|
||||||
|
assert ".latest-artifact-card" in css
|
||||||
|
assert ".latest-artifact-card-empty" in css
|
||||||
|
assert ".latest-artifact-actions" in css
|
||||||
|
assert "grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));" in css
|
||||||
@@ -3342,3 +3342,30 @@ Limitations:
|
|||||||
|
|
||||||
Next recommended pass:
|
Next recommended pass:
|
||||||
- After deploy and live validation, continue with export artifact action ergonomics or QA result drill-down, depending on the next live friction point.
|
- After deploy and live validation, continue with export artifact action ergonomics or QA result drill-down, depending on the next live friction point.
|
||||||
|
|
||||||
|
## Sprint 93 export handoff artifact polish (2026-06-21)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Added a `Latest handoff artifacts` section to `frontend/src/components/exports/ExportCenter.tsx`.
|
||||||
|
- Grouped newest persisted artifacts for project report, project metadata, dataset GeoJSON, detection GeoJSON and segmentation GeoJSON.
|
||||||
|
- Reused existing JSON preview and artifact download callbacks from latest artifact cards.
|
||||||
|
- Added responsive latest artifact card styling in `frontend/src/styles/app.css`.
|
||||||
|
- Added `backend/tests/test_sprint93_export_handoff_completion.py` to lock the grouped handoff surface, actions and responsive CSS.
|
||||||
|
- Updated `CHANGELOG.md`, `docs/TODO.md` and `frontend/README.md`.
|
||||||
|
|
||||||
|
Tested:
|
||||||
|
- Red step: `python -m pytest backend\tests\test_sprint93_export_handoff_completion.py -q` failed on missing latest artifact grouping/actions/styles.
|
||||||
|
- `python -m pytest backend\tests\test_sprint93_export_handoff_completion.py -q` (`3 passed`)
|
||||||
|
- `python -m pytest backend\tests\test_sprint93_export_handoff_completion.py backend\tests\test_sprint64_export_handoff_polish.py backend\tests\test_sprint89_export_system_density.py backend\tests\test_sprint51_quality_export_polish.py backend\tests\test_sprint78_export_preview_readability.py -q` (`13 passed`)
|
||||||
|
- `cd frontend && npm run typecheck`
|
||||||
|
- `cd frontend && npm run build`
|
||||||
|
- `bash scripts/run_readiness_check.sh` (`311 passed`; frontend typecheck/build passed; Alembic head `202606120900`; live smoke syntax passed)
|
||||||
|
|
||||||
|
Open:
|
||||||
|
- Tower deploy and live browser validation still pending for this pass.
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
- Export Center frontend handoff polish only; no API contract, persistence, migration, provider fetching or AI/model behavior changes.
|
||||||
|
|
||||||
|
Next recommended pass:
|
||||||
|
- After deploy and live validation, continue with QA/QC drilldown and map evidence layers.
|
||||||
|
|||||||
@@ -364,3 +364,4 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Add Overview workflow guidance for the V1 project -> data -> map -> QA/AI -> export path.
|
- [x] Add Overview workflow guidance for the V1 project -> data -> map -> QA/AI -> export path.
|
||||||
- [x] Audit populated demo workflow and tighten complete-state Overview guidance copy.
|
- [x] Audit populated demo workflow and tighten complete-state Overview guidance copy.
|
||||||
- [x] Make Overview workflow rail Map/Export clicks preserve useful dataset context.
|
- [x] Make Overview workflow rail Map/Export clicks preserve useful dataset context.
|
||||||
|
- [x] Add latest handoff artifact cards to the Export Center for report, metadata and GeoJSON outputs.
|
||||||
|
|||||||
@@ -253,6 +253,7 @@ Detection Lab and Segmentation Lab now share the same AI workspace hierarchy: mo
|
|||||||
- Overview rail Map and Export clicks reuse the first ready vector/GeoJSON dataset when no layer or dataset is active, keeping the happy path connected without adding new API calls.
|
- Overview rail Map and Export clicks reuse the first ready vector/GeoJSON dataset when no layer or dataset is active, keeping the happy path connected without adding new API calls.
|
||||||
- The Data workspace surfaces selected project, AOI and dataset context before creation/upload forms, then separates form and catalog/list regions for faster scanning.
|
- The Data workspace surfaces selected project, AOI and dataset context before creation/upload forms, then separates form and catalog/list regions for faster scanning.
|
||||||
- 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.
|
- 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.
|
||||||
|
- The Export Center highlights the latest project report, project metadata, dataset GeoJSON, detection GeoJSON and segmentation GeoJSON artifacts with direct preview/download actions.
|
||||||
|
|
||||||
## Workbench shell refactor
|
## Workbench shell refactor
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,17 @@ function formatExportType(value: string): string {
|
|||||||
.join(' ')
|
.join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatExportCreated(value?: string | null): string {
|
||||||
|
if (!value) {
|
||||||
|
return 'created time unavailable'
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLatestExportByType(exports: ExportRead[], exportType: string): ExportRead | null {
|
||||||
|
return exports.find((item) => item.export_type === exportType) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -90,6 +101,41 @@ export function ExportCenter({
|
|||||||
const handoffStatus = selectedProjectId ? 'project selected' : 'select project'
|
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 latestHandoffArtifacts = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
key: 'project_report_html',
|
||||||
|
label: 'Project report',
|
||||||
|
detail: 'HTML report for human review',
|
||||||
|
item: getLatestExportByType(exports, 'project_report_html'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'project_metadata_json',
|
||||||
|
label: 'Project metadata',
|
||||||
|
detail: 'Machine-readable project inventory',
|
||||||
|
item: getLatestExportByType(exports, 'project_metadata_json'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dataset_geojson',
|
||||||
|
label: 'Dataset GeoJSON',
|
||||||
|
detail: 'Selected vector/reference layer',
|
||||||
|
item: getLatestExportByType(exports, 'dataset_geojson'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'detection_geojson',
|
||||||
|
label: 'Detection GeoJSON',
|
||||||
|
detail: 'Persisted detection geometries',
|
||||||
|
item: getLatestExportByType(exports, 'detection_geojson'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'segmentation_geojson',
|
||||||
|
label: 'Segmentation GeoJSON',
|
||||||
|
detail: 'Persisted segmentation polygons',
|
||||||
|
item: getLatestExportByType(exports, 'segmentation_geojson'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[exports],
|
||||||
|
)
|
||||||
const filteredExports = useMemo(
|
const filteredExports = useMemo(
|
||||||
() =>
|
() =>
|
||||||
exports.filter((item) => {
|
exports.filter((item) => {
|
||||||
@@ -161,6 +207,42 @@ export function ExportCenter({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="latest-artifact-surface" aria-label="Latest handoff artifacts">
|
||||||
|
<div>
|
||||||
|
<h3>Latest handoff artifacts</h3>
|
||||||
|
<p className="muted">Open the newest artifact of each handoff type without searching the full export history.</p>
|
||||||
|
</div>
|
||||||
|
<div className="latest-artifact-grid">
|
||||||
|
{latestHandoffArtifacts.map((artifact) => {
|
||||||
|
const item = artifact.item
|
||||||
|
return (
|
||||||
|
<div className={item ? 'latest-artifact-card' : 'latest-artifact-card latest-artifact-card-empty'} key={artifact.key}>
|
||||||
|
<span>{artifact.label}</span>
|
||||||
|
<strong>{item ? item.status : 'No artifact yet'}</strong>
|
||||||
|
<p>{item ? compactPath(item.storage_path) : artifact.detail}</p>
|
||||||
|
{item ? (
|
||||||
|
<>
|
||||||
|
<small>{formatExportCreated(item.created_at)}</small>
|
||||||
|
<div className="latest-artifact-actions">
|
||||||
|
{canPreviewJson(item) ? (
|
||||||
|
<button type="button" className="secondary-action" onClick={() => onPreviewContent(item.id)}>
|
||||||
|
Preview
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="status-badge">download-only</span>
|
||||||
|
)}
|
||||||
|
<button type="button" className="primary-action" onClick={() => onDownload(item.id)}>
|
||||||
|
Download
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="export-actions-surface" aria-label="Export artifact actions">
|
<div className="export-actions-surface" aria-label="Export artifact actions">
|
||||||
<div className="panel-title-row">
|
<div className="panel-title-row">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1831,6 +1831,7 @@ button.entity-card {
|
|||||||
|
|
||||||
.export-summary-surface,
|
.export-summary-surface,
|
||||||
.export-handoff-surface,
|
.export-handoff-surface,
|
||||||
|
.latest-artifact-surface,
|
||||||
.export-actions-surface,
|
.export-actions-surface,
|
||||||
.export-history-surface,
|
.export-history-surface,
|
||||||
.system-provider-capability-surface {
|
.system-provider-capability-surface {
|
||||||
@@ -1849,6 +1850,10 @@ button.entity-card {
|
|||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.latest-artifact-surface {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
.export-center-shell .quality-summary-grid {
|
.export-center-shell .quality-summary-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
|
||||||
@@ -1977,6 +1982,69 @@ button.entity-card {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.latest-artifact-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.latest-artifact-card {
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 0.36rem;
|
||||||
|
border: 1px solid rgba(15, 118, 110, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.72rem;
|
||||||
|
background: linear-gradient(180deg, #f8fcfa, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.latest-artifact-card-empty {
|
||||||
|
border-style: dashed;
|
||||||
|
border-color: var(--line);
|
||||||
|
background: var(--panel-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.latest-artifact-card span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 850;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.latest-artifact-card strong {
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.24;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.latest-artifact-card p,
|
||||||
|
.latest-artifact-card small {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.latest-artifact-actions {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 0.45rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0.18rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.latest-artifact-actions button,
|
||||||
|
.latest-artifact-actions .status-badge {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 2.1rem;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.export-type-badge {
|
.export-type-badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user