Polish export system 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 23:12:14 +02:00
parent 5544c92006
commit f50b25554b
8 changed files with 558 additions and 278 deletions
+8
View File
@@ -7,6 +7,14 @@
# Changelog # Changelog
## Sprint 89 Export/System density polish (2026-06-20)
- Grouped Export Center summary, handoff readiness, artifact actions, state cards and history into focused surfaces.
- Grouped Provider Capabilities into a system shell with registry state cards, capability cards and attribution/license provenance cards.
- Preserved existing export action, filter, preview/download and provider refresh workflows without API or persistence changes.
- Added static regression coverage for Export/System hierarchy and density contracts.
- No API contracts, migrations, backend behavior, provider fetching or AI model behavior changed.
## Sprint 88 AI Labs density polish (2026-06-20) ## Sprint 88 AI Labs density polish (2026-06-20)
- Grouped Detection Lab and Segmentation Lab model registry, run controls, result loading and QA controls into focused surfaces. - Grouped Detection Lab and Segmentation Lab model registry, run controls, result loading and QA controls into focused surfaces.
@@ -0,0 +1,83 @@
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
def test_export_center_exposes_final_handoff_surfaces() -> None:
export_center = (ROOT / "frontend" / "src" / "components" / "exports" / "ExportCenter.tsx").read_text(
encoding="utf-8"
)
assert 'className="export-center"' in export_center
assert 'className="export-center-shell"' in export_center
assert 'className="export-summary-surface"' in export_center
assert 'aria-label="Export summary"' in export_center
assert 'className="export-handoff-surface"' in export_center
assert 'aria-label="Export handoff readiness"' in export_center
assert 'className="export-actions-surface"' in export_center
assert 'aria-label="Export artifact actions"' in export_center
assert 'className="export-state-stack"' in export_center
assert 'className="export-history-surface"' in export_center
assert 'aria-label="Export history"' in export_center
def test_provider_panel_exposes_final_system_surfaces() -> None:
provider_panel = (ROOT / "frontend" / "src" / "components" / "providers" / "ProviderPanel.tsx").read_text(
encoding="utf-8"
)
assert 'className="system-provider-panel"' in provider_panel
assert 'className="system-provider-shell"' in provider_panel
assert 'className="system-provider-state-stack"' in provider_panel
assert 'className="system-provider-capability-surface"' in provider_panel
assert 'aria-label="Provider capability registry"' in provider_panel
assert 'className="provider-provenance-grid"' in provider_panel
assert 'className="provider-provenance-card"' in provider_panel
assert 'className="provider-detail-stack"' in provider_panel
def test_export_system_preserves_existing_workflow_controls() -> None:
export_center = (ROOT / "frontend" / "src" / "components" / "exports" / "ExportCenter.tsx").read_text(
encoding="utf-8"
)
provider_panel = (ROOT / "frontend" / "src" / "components" / "providers" / "ProviderPanel.tsx").read_text(
encoding="utf-8"
)
assert "onExportDataset" in export_center
assert "onExportDetectionRun" in export_center
assert "onExportSegmentationRun" in export_center
assert "onExportProjectMetadata" in export_center
assert "onExportProjectReport" in export_center
assert "onPreviewContent" in export_center
assert "onDownload" in export_center
assert "exportTypeFilter" in export_center
assert "exportStatusFilter" in export_center
assert "exportSearchQuery" in export_center
assert "system-provider-list" in provider_panel
assert "system-provider-card" in provider_panel
assert "provider-layer-chip" in provider_panel
assert "provider.not_configured_reason" in provider_panel
def test_export_system_density_css_contracts() -> None:
css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8")
assert ".export-center-shell" in css
assert ".export-summary-surface" in css
assert ".export-handoff-surface" in css
assert ".export-actions-surface" in css
assert ".export-state-stack" in css
assert ".export-history-surface" in css
assert ".export-center-shell .quality-summary-grid" in css
assert ".system-provider-shell" in css
assert ".system-provider-state-stack" in css
assert ".system-provider-capability-surface" in css
assert ".provider-detail-stack" in css
assert ".provider-provenance-grid" in css
assert ".provider-provenance-card" in css
assert "grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));" in css
assert "background: linear-gradient(180deg, #ffffff, #f7fbf8);" in css
+35
View File
@@ -3210,3 +3210,38 @@ Limitations:
Next recommended pass: Next recommended pass:
- Continue with export/system final visual consistency or live workflow guidance once this pass is deployed and visually checked. - Continue with export/system final visual consistency or live workflow guidance once this pass is deployed and visually checked.
## Sprint 89 Export/System density polish (2026-06-20)
Changed:
- Wrapped Export Center content in `export-center-shell`.
- Grouped export summary, handoff readiness, artifact actions, current state cards and history into focused `export-*` surfaces.
- Wrapped Provider Capabilities content in `system-provider-shell`.
- Replaced loose provider loading/error/empty text with shared `result-state` cards.
- Grouped provider registry content into `system-provider-capability-surface`.
- Split provider limitation/reason text and attribution/license data into `provider-detail-stack` and provenance cards.
- Added shared Export/System density CSS for compact summary, handoff, action, history and provider provenance grids.
- Added `backend/tests/test_sprint89_export_system_density.py`.
- Updated `frontend/README.md`, `docs/TODO.md` and `CHANGELOG.md`.
Tested:
- Red step: `python -m pytest backend/tests/test_sprint89_export_system_density.py -q` failed on missing Export/System shells, surfaces and CSS contracts.
- `python -m pytest backend/tests/test_sprint89_export_system_density.py backend/tests/test_sprint51_quality_export_polish.py backend/tests/test_sprint64_export_handoff_polish.py backend/tests/test_sprint76_export_system_mobile_polish.py backend/tests/test_sprint78_export_preview_readability.py -q` (`12 passed`)
- `cd frontend && npm run typecheck`
- `cd frontend && npm run build`
- In-app browser fallback note: the Browser click path timed out on the local workspace navigation CDP command, so rendered validation used local Chrome via Playwright.
- Local Chrome UI check against `http://127.0.0.1:5174` passed on desktop and mobile: Export/System shells and surfaces rendered with no horizontal overflow. Local console showed expected Vite proxy 500s because only the frontend server was running.
- `bash scripts/run_readiness_check.sh` (`303 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.
- Live Chrome UI check against `http://192.168.10.150:1202` passed on desktop and mobile: Export/System shells and surfaces rendered, four provider cards and eight provider provenance cards were visible, no horizontal overflow and no console warnings/errors.
Open:
- None known after full readiness, Tower deploy and live browser validation.
Limitations:
- Frontend Export/System presentation hierarchy only; no export behavior, API contract, persistence, migration, provider fetching or AI/model changes.
Next recommended pass:
- Continue with end-to-end workflow guidance after this pass is deployed and visually checked.
+1
View File
@@ -78,6 +78,7 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Add QA/QC workspace result hierarchy and filter density polish. - [x] Add QA/QC workspace result hierarchy and filter density polish.
- [x] Add Change Detection panel hierarchy and analysis workspace density polish. - [x] Add Change Detection panel hierarchy and analysis workspace density polish.
- [x] Add AI Labs Detection/Segmentation hierarchy and result density polish. - [x] Add AI Labs Detection/Segmentation hierarchy and result density polish.
- [x] Add Export/System handoff hierarchy and provider registry density polish.
## Sprint 8 status ## Sprint 8 status
+2
View File
@@ -16,6 +16,8 @@ AI Labs uses the same mobile-density baseline for Detection and Segmentation mod
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. 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.
Export Center now separates export summary, handoff readiness, artifact actions, current export states and export history into focused surfaces. Provider Capabilities uses the same system-workspace rhythm with loading/error/empty cards, provider capability cards and dedicated attribution/license provenance cards.
The right-side inspector also uses mobile-safe metadata and tool panels. Dataset filenames, checksums, bounds, job JSON, raster/vector operation inputs and inspector action groups wrap or scroll within the inspector instead of widening the workbench. The right-side inspector also uses mobile-safe metadata and tool panels. Dataset filenames, checksums, bounds, job JSON, raster/vector operation inputs and inspector action groups wrap or scroll within the inspector instead of widening the workbench.
Export Preview shows a compact JSON summary before the payload and keeps large JSON/GeoJSON handoff artifacts inside a scroll-contained preview shell. HTML report artifacts remain download-only. Export Preview shows a compact JSON summary before the payload and keeps large JSON/GeoJSON handoff artifacts inside a scroll-contained preview shell. HTML report artifacts remain download-only.
+251 -224
View File
@@ -105,238 +105,265 @@ export function ExportCenter({
return ( return (
<section className="export-center" data-testid="export-center"> <section className="export-center" data-testid="export-center">
<div className="panel-title-row"> <div className="export-center-shell">
<div>
<h2>Export Center</h2>
<p className="muted">Create handoff artifacts from persisted datasets, AI runs and project metadata.</p>
</div>
<span className="count-pill">{exports.length} exports</span>
</div>
<div className="quality-summary-grid">
<div>
<span>GeoJSON</span>
<strong>{geojsonExports}</strong>
</div>
<div>
<span>Reports</span>
<strong>{reportExports}</strong>
</div>
<div>
<span>Selected dataset</span>
<strong>{selectedDataset?.name ?? 'none'}</strong>
</div>
</div>
<div className="handoff-summary-card">
<div className="panel-title-row"> <div className="panel-title-row">
<div> <div>
<h3>Handoff readiness</h3> <h2>Export Center</h2>
<p className="muted">Pick the artifact that matches the current review state before sharing data outside the workbench.</p> <p className="muted">Create handoff artifacts from persisted datasets, AI runs and project metadata.</p>
</div> </div>
<span className="status-badge">{handoffStatus}</span> <span className="count-pill">{exports.length} exports</span>
</div> </div>
<div className="handoff-readiness-grid">
<div> <div className="export-summary-surface" aria-label="Export summary">
<span>Vector dataset</span> <div className="quality-summary-grid">
<strong>{canExportDataset ? selectedDataset?.name : 'none ready'}</strong> <div>
</div> <span>GeoJSON</span>
<div> <strong>{geojsonExports}</strong>
<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="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
type="button"
className="secondary-action"
onClick={onRefresh}
disabled={!selectedProjectId || loadingExports}
data-testid="refresh-exports"
>
Refresh exports
</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
type="button"
className="secondary-action"
onClick={onExportProjectMetadata}
disabled={!selectedProjectId || exporting}
data-testid="export-project-metadata"
>
Export project metadata JSON
</button>
</div>
<div className="handoff-action-card">
<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}>
Export selected vector GeoJSON
</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}>
Export selected detection run GeoJSON
</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}>
Export selected segmentation run GeoJSON
</button>
</div>
</div>
</div>
{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>
<strong>{formatExportType(latestExport.export_type)}</strong>
<p>{latestExport.path}</p>
</div>
) : null}
{exports.length === 0 ? (
<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>
) : null}
{exports.length > 0 ? (
<div className="export-history-controls" aria-label="Export history filters">
<label>
Search exports
<input
type="search"
placeholder="Type, id or path"
value={exportSearchQuery}
onChange={(event) => setExportSearchQuery(event.target.value)}
/>
</label>
<label>
Type
<select value={exportTypeFilter} onChange={(event) => setExportTypeFilter(event.target.value)}>
<option value="all">All types</option>
{exportTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</label>
<label>
Status
<select value={exportStatusFilter} onChange={(event) => setExportStatusFilter(event.target.value)}>
<option value="all">All statuses</option>
{exportStatuses.map((status) => (
<option key={status} value={status}>
{status}
</option>
))}
</select>
</label>
<button
type="button"
className="secondary-action"
onClick={() => {
setExportSearchQuery('')
setExportTypeFilter('all')
setExportStatusFilter('all')
setShowAllExports(false)
}}
disabled={!hasActiveExportFilters && !showAllExports}
>
Reset view
</button>
</div>
) : null}
{filteredExports.length > 10 ? (
<div className="list-limit-banner">
<span>
Showing {visibleExports.length} of {filteredExports.length} matching exports.
{hiddenExportCount > 0 ? ` ${hiddenExportCount} older artifacts hidden.` : ' All matching artifacts shown.'}
</span>
<button type="button" className="secondary-action" onClick={() => setShowAllExports((value) => !value)}>
{showAllExports ? 'Show latest 10' : 'Show all exports'}
</button>
</div>
) : null}
{exports.length > 0 && filteredExports.length === 0 ? (
<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>
) : null}
<ul className="export-list">
{visibleExports.map((item) => (
<li className="export-card" key={item.id}>
<div className="export-card-header">
<div>
<span className="export-type-badge">{formatExportType(item.export_type)}</span>
<div className="entity-meta">
<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>
<span className={item.status === 'ready' ? 'status-badge status-badge-ready' : 'status-badge'}>{item.status}</span>
</div> </div>
<p title={item.storage_path}>{compactPath(item.storage_path)}</p> <div>
<div className="button-row"> <span>Reports</span>
{canPreviewJson(item) ? ( <strong>{reportExports}</strong>
<button type="button" className="secondary-action" onClick={() => onPreviewContent(item.id)}> </div>
Preview JSON content <div>
<span>Selected dataset</span>
<strong>{selectedDataset?.name ?? 'none'}</strong>
</div>
</div>
</div>
<div className="export-handoff-surface" aria-label="Export handoff readiness">
<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>
<div className="export-actions-surface" aria-label="Export artifact actions">
<div className="panel-title-row">
<div>
<h3>Artifact actions</h3>
<p className="muted">Create review artifacts from the current project, dataset or selected AI analysis run.</p>
</div>
</div>
<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
type="button"
className="secondary-action"
onClick={onRefresh}
disabled={!selectedProjectId || loadingExports}
data-testid="refresh-exports"
>
Refresh exports
</button> </button>
) : ( </div>
<span className="status-badge">download-only HTML artifact</span> <div className="handoff-action-card handoff-action-card-primary">
)} <span>Project report</span>
<button type="button" className="primary-action" onClick={() => onDownload(item.id)}> <strong>HTML summary</strong>
Download artifact <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
type="button"
className="secondary-action"
onClick={onExportProjectMetadata}
disabled={!selectedProjectId || exporting}
data-testid="export-project-metadata"
>
Export project metadata JSON
</button>
</div>
<div className="handoff-action-card">
<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}>
Export selected vector GeoJSON
</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}>
Export selected detection run GeoJSON
</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}>
Export selected segmentation run GeoJSON
</button>
</div>
</div>
</div>
</div>
<div className="export-state-stack">
{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>
<strong>{formatExportType(latestExport.export_type)}</strong>
<p>{latestExport.path}</p>
</div>
) : null}
{exports.length === 0 ? (
<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>
) : null}
</div>
<div className="export-history-surface" aria-label="Export history">
<div>
<h3>Export history</h3>
<p className="muted">Review persisted artifacts and open JSON previews where supported.</p>
</div>
{exports.length > 0 ? (
<div className="export-history-controls" aria-label="Export history filters">
<label>
Search exports
<input
type="search"
placeholder="Type, id or path"
value={exportSearchQuery}
onChange={(event) => setExportSearchQuery(event.target.value)}
/>
</label>
<label>
Type
<select value={exportTypeFilter} onChange={(event) => setExportTypeFilter(event.target.value)}>
<option value="all">All types</option>
{exportTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</label>
<label>
Status
<select value={exportStatusFilter} onChange={(event) => setExportStatusFilter(event.target.value)}>
<option value="all">All statuses</option>
{exportStatuses.map((status) => (
<option key={status} value={status}>
{status}
</option>
))}
</select>
</label>
<button
type="button"
className="secondary-action"
onClick={() => {
setExportSearchQuery('')
setExportTypeFilter('all')
setExportStatusFilter('all')
setShowAllExports(false)
}}
disabled={!hasActiveExportFilters && !showAllExports}
>
Reset view
</button> </button>
</div> </div>
</li> ) : null}
))} {filteredExports.length > 10 ? (
</ul> <div className="list-limit-banner">
<span>
Showing {visibleExports.length} of {filteredExports.length} matching exports.
{hiddenExportCount > 0 ? ` ${hiddenExportCount} older artifacts hidden.` : ' All matching artifacts shown.'}
</span>
<button type="button" className="secondary-action" onClick={() => setShowAllExports((value) => !value)}>
{showAllExports ? 'Show latest 10' : 'Show all exports'}
</button>
</div>
) : null}
{exports.length > 0 && filteredExports.length === 0 ? (
<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>
) : null}
<ul className="export-list">
{visibleExports.map((item) => (
<li className="export-card" key={item.id}>
<div className="export-card-header">
<div>
<span className="export-type-badge">{formatExportType(item.export_type)}</span>
<div className="entity-meta">
<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>
<span className={item.status === 'ready' ? 'status-badge status-badge-ready' : 'status-badge'}>{item.status}</span>
</div>
<p title={item.storage_path}>{compactPath(item.storage_path)}</p>
<div className="button-row">
{canPreviewJson(item) ? (
<button type="button" className="secondary-action" onClick={() => onPreviewContent(item.id)}>
Preview JSON content
</button>
) : (
<span className="status-badge">download-only HTML artifact</span>
)}
<button type="button" className="primary-action" onClick={() => onDownload(item.id)}>
Download artifact
</button>
</div>
</li>
))}
</ul>
</div>
</div>
</section> </section>
) )
} }
@@ -15,62 +15,95 @@ export function ProviderPanel({
}: ProviderPanelProps): JSX.Element { }: ProviderPanelProps): JSX.Element {
return ( return (
<section className="system-provider-panel"> <section className="system-provider-panel">
<div className="panel-title-row"> <div className="system-provider-shell">
<div> <div className="panel-title-row">
<h2>Provider Capabilities</h2> <div>
<p className="muted">Review configured local providers and honest not-configured external sources.</p> <h2>Provider Capabilities</h2>
<p className="muted">Review configured local providers and honest not-configured external sources.</p>
</div>
<button type="button" className="secondary-action" onClick={onRefresh} disabled={loadingCapabilities}>
Refresh providers
</button>
</div>
<div className="system-provider-state-stack">
{loadingCapabilities ? (
<div className="result-state result-state-loading">
<strong>Loading provider capabilities.</strong>
<p>Checking local, fixture and external provider registry states.</p>
</div>
) : null}
{capabilitiesError ? (
<div className="result-state result-state-error">
<strong>Provider registry unavailable.</strong>
<p>{capabilitiesError}</p>
</div>
) : null}
{providers.length === 0 && !loadingCapabilities ? (
<div className="result-state result-state-empty">
<strong>No providers reported by backend.</strong>
<p>Refresh provider capabilities after the backend is reachable.</p>
</div>
) : null}
</div>
<div className="system-provider-capability-surface" aria-label="Provider capability registry">
<ul className="system-provider-list">
{providers.map((provider) => (
<li className="system-provider-card" key={provider.provider_name}>
<div className="system-provider-header">
<div>
<strong>{provider.display_name}</strong>
<span>{provider.provider_name}</span>
</div>
<span className={provider.configured ? 'status-badge status-badge-ready' : 'status-badge'}>{provider.status}</span>
</div>
<div className="system-provider-grid">
<div>
<span>Authority</span>
<strong>{provider.authority_level}</strong>
</div>
<div>
<span>Configured</span>
<strong>{provider.configured ? 'yes' : 'no'}</strong>
</div>
<div>
<span>Geometry</span>
<strong>{provider.supported_geometry_types.join(', ')}</strong>
</div>
<div>
<span>Query modes</span>
<strong>{provider.supported_query_modes.join(', ')}</strong>
</div>
</div>
<div className="provider-layer-list" aria-label={`${provider.provider_name} supported layers`}>
{provider.supported_layers.map((layer) => (
<span className="provider-layer-chip" key={layer}>
{layer}
</span>
))}
</div>
<div className="provider-detail-stack">
<div>limitation: {provider.limitation_message}</div>
{!provider.configured && provider.not_configured_reason ? (
<div>reason: {provider.not_configured_reason}</div>
) : null}
</div>
<div className="provider-provenance-grid">
<div className="provider-provenance-card">
<span>Attribution</span>
<strong>{provider.attribution}</strong>
</div>
<div className="provider-provenance-card">
<span>License</span>
<strong>{provider.license_note}</strong>
</div>
</div>
</li>
))}
</ul>
</div> </div>
<button type="button" className="secondary-action" onClick={onRefresh} disabled={loadingCapabilities}>
Refresh providers
</button>
</div> </div>
{loadingCapabilities ? <p>Loading provider capabilities...</p> : null}
{capabilitiesError ? <p className="error">{capabilitiesError}</p> : null}
{providers.length === 0 && !loadingCapabilities ? <p>No providers reported by backend</p> : null}
<ul className="system-provider-list">
{providers.map((provider) => (
<li className="system-provider-card" key={provider.provider_name}>
<div className="system-provider-header">
<div>
<strong>{provider.display_name}</strong>
<span>{provider.provider_name}</span>
</div>
<span className={provider.configured ? 'status-badge status-badge-ready' : 'status-badge'}>{provider.status}</span>
</div>
<div className="system-provider-grid">
<div>
<span>Authority</span>
<strong>{provider.authority_level}</strong>
</div>
<div>
<span>Configured</span>
<strong>{provider.configured ? 'yes' : 'no'}</strong>
</div>
<div>
<span>Geometry</span>
<strong>{provider.supported_geometry_types.join(', ')}</strong>
</div>
<div>
<span>Query modes</span>
<strong>{provider.supported_query_modes.join(', ')}</strong>
</div>
</div>
<div className="provider-layer-list" aria-label={`${provider.provider_name} supported layers`}>
{provider.supported_layers.map((layer) => (
<span className="provider-layer-chip" key={layer}>
{layer}
</span>
))}
</div>
<div>limitation: {provider.limitation_message}</div>
<div>attribution: {provider.attribution}</div>
<div>license: {provider.license_note}</div>
{!provider.configured && provider.not_configured_reason ? (
<div>reason: {provider.not_configured_reason}</div>
) : null}
</li>
))}
</ul>
</section> </section>
) )
} }
+91
View File
@@ -1766,6 +1766,60 @@ button.entity-card {
margin: 0.8rem 0; margin: 0.8rem 0;
} }
.export-center-shell,
.system-provider-shell {
display: grid;
gap: 0.85rem;
min-width: 0;
}
.export-summary-surface,
.export-handoff-surface,
.export-actions-surface,
.export-history-surface,
.system-provider-capability-surface {
display: grid;
gap: 0.68rem;
min-width: 0;
border: 1px solid var(--line);
border-radius: 8px;
padding: 0.78rem;
background: linear-gradient(180deg, #ffffff, #f7fbf8);
box-shadow: 0 1px 0 rgba(19, 32, 24, 0.03);
}
.export-history-surface,
.system-provider-capability-surface {
background: #ffffff;
}
.export-center-shell .quality-summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
gap: 0.58rem;
margin: 0;
}
.export-handoff-surface .handoff-summary-card {
margin-top: 0;
border: 0;
padding: 0;
background: transparent;
box-shadow: none;
}
.export-actions-surface .export-action-grid {
margin: 0;
}
.export-state-stack,
.system-provider-state-stack,
.provider-detail-stack {
display: grid;
gap: 0.5rem;
min-width: 0;
}
.export-action-grid button { .export-action-grid button {
min-height: 2.8rem; min-height: 2.8rem;
text-align: left; text-align: left;
@@ -2045,6 +2099,43 @@ button.entity-card {
font-weight: 800; font-weight: 800;
} }
.provider-detail-stack {
color: var(--muted);
font-size: 0.84rem;
line-height: 1.36;
}
.provider-provenance-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
gap: 0.5rem;
}
.provider-provenance-card {
min-width: 0;
border: 1px solid rgba(15, 118, 110, 0.16);
border-radius: 7px;
padding: 0.55rem;
background: #ffffff;
}
.provider-provenance-card span {
display: block;
color: var(--muted);
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.provider-provenance-card strong {
display: block;
margin-top: 0.18rem;
overflow-wrap: anywhere;
font-size: 0.88rem;
line-height: 1.25;
}
.export-preview-panel .job-result { .export-preview-panel .job-result {
max-height: 38rem; max-height: 38rem;
} }