Harden raster detection manifest handoff
This commit is contained in:
@@ -1301,3 +1301,12 @@ Added:
|
|||||||
- Surfaced the current YOLOv8s hard-negative benchmark candidate and recommended starting threshold `0.25` as operator guidance.
|
- Surfaced the current YOLOv8s hard-negative benchmark candidate and recommended starting threshold `0.25` as operator guidance.
|
||||||
- Added regression coverage for the no-auto-select behavior and UI guardrail copy.
|
- Added regression coverage for the no-auto-select behavior and UI guardrail copy.
|
||||||
- No backend API contracts, migrations, model downloads, provider fetching or model weight mutation behavior changed.
|
- No backend API contracts, migrations, model downloads, provider fetching or model weight mutation behavior changed.
|
||||||
|
|
||||||
|
## Sprint 123 Raster detection manifest handoff (2026-07-08)
|
||||||
|
|
||||||
|
- Added a structured raster tile manifest handoff from the Data workspace into Detection Lab.
|
||||||
|
- Raster controls now surface manifest tile count, tile size, overlap and tile-set provenance before handing the manifest to AI workflows.
|
||||||
|
- The Detection Lab handoff now selects the configured YOLO run path, keeps local model assets explicit, applies the current recommended `0.25` starting threshold and refreshes YOLO preflight for the linked manifest.
|
||||||
|
- Detection Lab now shows linked tile manifest provenance plus preflight manifest validation, tile count and `will_run_inference` state.
|
||||||
|
- Added regression coverage for the handoff contract and preserved the existing no-auto-select model guardrail.
|
||||||
|
- No backend API contracts, migrations, model downloads, provider fetching or model weight mutation behavior changed.
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
def test_raster_workflow_exposes_structured_tile_manifest_handoff() -> None:
|
||||||
|
hook = (ROOT / "frontend" / "src" / "hooks" / "useDatasetWorkflow.ts").read_text(encoding="utf-8")
|
||||||
|
types = (ROOT / "frontend" / "src" / "types.ts").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "interface RasterTileHandoff" in types
|
||||||
|
assert "latestRasterTileManifest" in hook
|
||||||
|
assert "toRasterTileHandoff(job)" in hook
|
||||||
|
assert "setLatestRasterTileManifest(manifest)" in hook
|
||||||
|
assert "manifest.manifest_path" in hook
|
||||||
|
|
||||||
|
|
||||||
|
def test_raster_controls_show_manifest_details_and_ai_handoff_action() -> None:
|
||||||
|
controls = (ROOT / "frontend" / "src" / "components" / "datasets" / "RasterControls.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "latestRasterTileManifest" in controls
|
||||||
|
assert "Tile count" in controls
|
||||||
|
assert "Tile size" in controls
|
||||||
|
assert "Overlap" in controls
|
||||||
|
assert "Use manifest in Detection Lab" in controls
|
||||||
|
assert "Use manifest in Segmentation Lab" in controls
|
||||||
|
|
||||||
|
|
||||||
|
def test_detection_handoff_opens_ai_lab_preflights_manifest_and_keeps_asset_explicit() -> None:
|
||||||
|
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
|
||||||
|
lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "setDetectionTileManifestPath(manifestPath)" in app
|
||||||
|
assert "setSelectedDetectionDatasetId(selectedDataset.id)" in app
|
||||||
|
assert "setSelectedDetectionModelId('yolo-configured')" in app
|
||||||
|
assert "setDetectionConfidenceThreshold(0.25)" in app
|
||||||
|
assert "loadYoloPreflight(manifestPath).catch(() => null)" in app
|
||||||
|
assert "setSelectedModelAssetId(" not in app[app.index("const useRasterTileManifestForDetection"):app.index("const {", app.index("const useRasterTileManifestForDetection"))]
|
||||||
|
assert "Linked tile manifest" in lab
|
||||||
|
assert "Tile manifest validation" in lab
|
||||||
|
assert "Tile count" in lab
|
||||||
|
assert "will_run_inference" in lab
|
||||||
@@ -5079,6 +5079,30 @@ Open:
|
|||||||
Next recommended pass:
|
Next recommended pass:
|
||||||
- Implement V1 model-catalog hardening: show available local model assets, make active model/threshold explicit, and prevent accidental silent default activation.
|
- Implement V1 model-catalog hardening: show available local model assets, make active model/threshold explicit, and prevent accidental silent default activation.
|
||||||
|
|
||||||
|
## Sprint 123 Raster detection manifest handoff (2026-07-08)
|
||||||
|
|
||||||
|
Changed:
|
||||||
|
- Added a typed `RasterTileHandoff` frontend contract so raster tile jobs can expose manifest path, tile count, tile size, overlap, tile-set id and source dataset provenance.
|
||||||
|
- Updated the dataset workflow hook to retain the latest structured raster tile manifest alongside the existing manifest path.
|
||||||
|
- Updated Raster Controls to show manifest details and explicit Detection/Segmentation Lab handoff actions.
|
||||||
|
- Updated the Detection Lab handoff so a raster manifest selects `yolo-configured`, selects the active raster dataset, applies threshold `0.25`, refreshes YOLO preflight and does not auto-select a local model asset.
|
||||||
|
- Added linked tile manifest, manifest validation, tile count and `will_run_inference` visibility to Detection Lab.
|
||||||
|
- Added regression coverage in `backend/tests/test_sprint123_raster_detection_handoff_operational.py`.
|
||||||
|
|
||||||
|
Tested:
|
||||||
|
- Red step: `python -m pytest backend\tests\test_sprint123_raster_detection_handoff_operational.py -q` failed while the typed handoff, Raster Controls details and Detection Lab preflight indicators were absent.
|
||||||
|
- `python -m pytest backend\tests\test_sprint123_raster_detection_handoff_operational.py -q` (`3 passed`)
|
||||||
|
- `python -m pytest backend\tests\test_sprint99_raster_ui_handoff.py backend\tests\test_sprint122_model_asset_activation_guardrails.py backend\tests\test_sprint120_model_asset_detection_workflow_smoke.py backend\tests\test_sprint123_raster_detection_handoff_operational.py -q` (`8 passed`)
|
||||||
|
- `python -m compileall backend/app`
|
||||||
|
- `cd frontend && npm run typecheck`
|
||||||
|
- `cd frontend && npm run build`
|
||||||
|
|
||||||
|
Limitations:
|
||||||
|
- This pass improves the operator handoff and preflight visibility only. It does not change backend inference contracts, migrations, model files, provider fetching or automatic model promotion.
|
||||||
|
|
||||||
|
Next recommended pass:
|
||||||
|
- Add threshold calibration comparison UX so an operator can compare candidate thresholds before promoting a local model.
|
||||||
|
|
||||||
## Sprint 117 Safe local YOLO model activation (2026-07-06)
|
## Sprint 117 Safe local YOLO model activation (2026-07-06)
|
||||||
|
|
||||||
Changed:
|
Changed:
|
||||||
|
|||||||
@@ -418,6 +418,7 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Train/evaluate a YOLOv8s hard-negative local building-detector candidate on Tower and keep it inactive because hard-negative false positives remain.
|
- [x] Train/evaluate a YOLOv8s hard-negative local building-detector candidate on Tower and keep it inactive because hard-negative false positives remain.
|
||||||
- [x] Add an operator-facing local model catalog/activation workflow with SHA256, active model status and explicit threshold guidance.
|
- [x] Add an operator-facing local model catalog/activation workflow with SHA256, active model status and explicit threshold guidance.
|
||||||
- [x] Block silent local model asset auto-selection in Detection Lab.
|
- [x] Block silent local model asset auto-selection in Detection Lab.
|
||||||
|
- [x] Add structured raster tile manifest handoff into Detection Lab with linked preflight visibility.
|
||||||
- [ ] Add full threshold calibration comparison UX so detection runs can compare candidate thresholds before promotion.
|
- [ ] Add full threshold calibration comparison UX so detection runs can compare candidate thresholds before promotion.
|
||||||
- [ ] Add more AOIs after the tile-level baseline so the next local model attempt is not limited to Geel/Mol/Turnhout.
|
- [ ] Add more AOIs after the tile-level baseline so the next local model attempt is not limited to Geel/Mol/Turnhout.
|
||||||
- [ ] Add negative/background AOIs so the next tile dataset is not all positive tiles.
|
- [ ] Add negative/background AOIs so the next tile dataset is not all positive tiles.
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ function App(): JSX.Element {
|
|||||||
jobs,
|
jobs,
|
||||||
rasterPreview,
|
rasterPreview,
|
||||||
latestRasterTileManifestPath,
|
latestRasterTileManifestPath,
|
||||||
|
latestRasterTileManifest,
|
||||||
selectedIntersectTargetId,
|
selectedIntersectTargetId,
|
||||||
selectedClipAreaId,
|
selectedClipAreaId,
|
||||||
rasterTileSize,
|
rasterTileSize,
|
||||||
@@ -279,6 +280,8 @@ function App(): JSX.Element {
|
|||||||
setDetectionTileManifestPath(manifestPath)
|
setDetectionTileManifestPath(manifestPath)
|
||||||
setSelectedDetectionDatasetId(selectedDataset.id)
|
setSelectedDetectionDatasetId(selectedDataset.id)
|
||||||
setSelectedDetectionModelId('yolo-configured')
|
setSelectedDetectionModelId('yolo-configured')
|
||||||
|
setDetectionConfidenceThreshold(0.25)
|
||||||
|
loadYoloPreflight(manifestPath).catch(() => null)
|
||||||
setActiveWorkspace('ai')
|
setActiveWorkspace('ai')
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
@@ -1088,6 +1091,7 @@ function App(): JSX.Element {
|
|||||||
selectedRasterStats,
|
selectedRasterStats,
|
||||||
rasterPreview,
|
rasterPreview,
|
||||||
latestRasterTileManifestPath,
|
latestRasterTileManifestPath,
|
||||||
|
latestRasterTileManifest,
|
||||||
rasterUnavailableMessage,
|
rasterUnavailableMessage,
|
||||||
selectedClipAreaId,
|
selectedClipAreaId,
|
||||||
selectedIntersectTargetId,
|
selectedIntersectTargetId,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type {
|
|||||||
RasterMetadataResponse,
|
RasterMetadataResponse,
|
||||||
RasterPreviewResponse,
|
RasterPreviewResponse,
|
||||||
RasterStatsResponse,
|
RasterStatsResponse,
|
||||||
|
RasterTileHandoff,
|
||||||
VectorSummary,
|
VectorSummary,
|
||||||
} from '../../types'
|
} from '../../types'
|
||||||
import { RasterControls } from './RasterControls'
|
import { RasterControls } from './RasterControls'
|
||||||
@@ -20,6 +21,7 @@ export interface DatasetDetailPanelProps {
|
|||||||
selectedRasterStats: RasterStatsResponse | null
|
selectedRasterStats: RasterStatsResponse | null
|
||||||
rasterPreview: RasterPreviewResponse | null
|
rasterPreview: RasterPreviewResponse | null
|
||||||
latestRasterTileManifestPath: string
|
latestRasterTileManifestPath: string
|
||||||
|
latestRasterTileManifest: RasterTileHandoff | null
|
||||||
rasterUnavailableMessage: string | null
|
rasterUnavailableMessage: string | null
|
||||||
selectedClipAreaId: string
|
selectedClipAreaId: string
|
||||||
selectedIntersectTargetId: string
|
selectedIntersectTargetId: string
|
||||||
@@ -104,6 +106,7 @@ export function DatasetDetailPanel({
|
|||||||
selectedRasterStats,
|
selectedRasterStats,
|
||||||
rasterPreview,
|
rasterPreview,
|
||||||
latestRasterTileManifestPath,
|
latestRasterTileManifestPath,
|
||||||
|
latestRasterTileManifest,
|
||||||
rasterUnavailableMessage,
|
rasterUnavailableMessage,
|
||||||
selectedClipAreaId,
|
selectedClipAreaId,
|
||||||
selectedIntersectTargetId,
|
selectedIntersectTargetId,
|
||||||
@@ -178,6 +181,7 @@ export function DatasetDetailPanel({
|
|||||||
selectedRasterStats={selectedRasterStats}
|
selectedRasterStats={selectedRasterStats}
|
||||||
rasterPreview={rasterPreview}
|
rasterPreview={rasterPreview}
|
||||||
latestRasterTileManifestPath={latestRasterTileManifestPath}
|
latestRasterTileManifestPath={latestRasterTileManifestPath}
|
||||||
|
latestRasterTileManifest={latestRasterTileManifest}
|
||||||
rasterUnavailableMessage={rasterUnavailableMessage}
|
rasterUnavailableMessage={rasterUnavailableMessage}
|
||||||
selectedClipAreaId={selectedClipAreaId}
|
selectedClipAreaId={selectedClipAreaId}
|
||||||
rasterTileSize={rasterTileSize}
|
rasterTileSize={rasterTileSize}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { AreaRead, RasterMetadataResponse, RasterPreviewResponse, RasterStatsResponse } from '../../types'
|
import type { AreaRead, RasterMetadataResponse, RasterPreviewResponse, RasterStatsResponse, RasterTileHandoff } from '../../types'
|
||||||
|
|
||||||
interface RasterControlsProps {
|
interface RasterControlsProps {
|
||||||
areas: AreaRead[]
|
areas: AreaRead[]
|
||||||
@@ -7,6 +7,7 @@ interface RasterControlsProps {
|
|||||||
selectedRasterStats: RasterStatsResponse | null
|
selectedRasterStats: RasterStatsResponse | null
|
||||||
rasterPreview: RasterPreviewResponse | null
|
rasterPreview: RasterPreviewResponse | null
|
||||||
latestRasterTileManifestPath: string
|
latestRasterTileManifestPath: string
|
||||||
|
latestRasterTileManifest: RasterTileHandoff | null
|
||||||
rasterUnavailableMessage: string | null
|
rasterUnavailableMessage: string | null
|
||||||
selectedClipAreaId: string
|
selectedClipAreaId: string
|
||||||
rasterTileSize: number
|
rasterTileSize: number
|
||||||
@@ -61,6 +62,7 @@ export function RasterControls({
|
|||||||
selectedRasterStats,
|
selectedRasterStats,
|
||||||
rasterPreview,
|
rasterPreview,
|
||||||
latestRasterTileManifestPath,
|
latestRasterTileManifestPath,
|
||||||
|
latestRasterTileManifest,
|
||||||
rasterUnavailableMessage,
|
rasterUnavailableMessage,
|
||||||
selectedClipAreaId,
|
selectedClipAreaId,
|
||||||
rasterTileSize,
|
rasterTileSize,
|
||||||
@@ -167,11 +169,41 @@ export function RasterControls({
|
|||||||
{latestRasterTileManifestPath ||
|
{latestRasterTileManifestPath ||
|
||||||
'No tile manifest generated yet. Generate tiles before handing this raster to Detection Lab.'}
|
'No tile manifest generated yet. Generate tiles before handing this raster to Detection Lab.'}
|
||||||
</p>
|
</p>
|
||||||
<button type="button" onClick={onUseTileManifestForDetection} disabled={!latestRasterTileManifestPath}>
|
{latestRasterTileManifest ? (
|
||||||
Use in Detection Lab
|
<dl className="raster-manifest-details">
|
||||||
|
<div>
|
||||||
|
<dt>Tile count</dt>
|
||||||
|
<dd>{latestRasterTileManifest.count ?? 'n/a'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Tile size</dt>
|
||||||
|
<dd>{latestRasterTileManifest.tile_size ?? 'n/a'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Overlap</dt>
|
||||||
|
<dd>{latestRasterTileManifest.overlap ?? 'n/a'}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Tile set</dt>
|
||||||
|
<dd>{latestRasterTileManifest.tile_set_id ?? 'n/a'}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
) : null}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Use in Detection Lab with current manifest"
|
||||||
|
onClick={onUseTileManifestForDetection}
|
||||||
|
disabled={!latestRasterTileManifestPath}
|
||||||
|
>
|
||||||
|
Use manifest in Detection Lab
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={onUseTileManifestForSegmentation} disabled={!latestRasterTileManifestPath}>
|
<button
|
||||||
Use in Segmentation Lab
|
type="button"
|
||||||
|
aria-label="Use in Segmentation Lab with current manifest"
|
||||||
|
onClick={onUseTileManifestForSegmentation}
|
||||||
|
disabled={!latestRasterTileManifestPath}
|
||||||
|
>
|
||||||
|
Use manifest in Segmentation Lab
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -311,11 +311,20 @@ export function DetectionLab({
|
|||||||
<span>CUDA</span>
|
<span>CUDA</span>
|
||||||
<strong>{yoloPreflight.runtime.cuda_available === true ? 'available' : yoloPreflight.runtime.cuda_available === false ? 'not available' : 'not checked'}</strong>
|
<strong>{yoloPreflight.runtime.cuda_available === true ? 'available' : yoloPreflight.runtime.cuda_available === false ? 'not available' : 'not checked'}</strong>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={yoloPreflight.checks.manifest_valid ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
|
||||||
|
<span>Tile manifest validation</span>
|
||||||
|
<strong>{yoloPreflight.checks.manifest_valid === true ? 'valid' : yoloPreflight.checks.manifest_path_set ? 'not valid' : 'not provided'}</strong>
|
||||||
|
</div>
|
||||||
|
<div className={yoloPreflight.tile_count > 0 ? 'lab-readiness-item lab-readiness-item-ready' : 'lab-readiness-item'}>
|
||||||
|
<span>Tile count</span>
|
||||||
|
<strong>{yoloPreflight.tile_count} / {yoloPreflight.max_tiles}</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="entity-meta">
|
<div className="entity-meta">
|
||||||
<span>torch_version: {yoloPreflight.runtime.torch_version ?? 'n/a'}</span>
|
<span>torch_version: {yoloPreflight.runtime.torch_version ?? 'n/a'}</span>
|
||||||
<span>ultralytics_version: {yoloPreflight.runtime.ultralytics_version ?? 'n/a'}</span>
|
<span>ultralytics_version: {yoloPreflight.runtime.ultralytics_version ?? 'n/a'}</span>
|
||||||
<span>cuda_available: {String(yoloPreflight.runtime.cuda_available ?? 'unknown')}</span>
|
<span>cuda_available: {String(yoloPreflight.runtime.cuda_available ?? 'unknown')}</span>
|
||||||
|
<span>will_run_inference: {String(yoloPreflight.will_run_inference)}</span>
|
||||||
<span>YOLO_CONFIG_DIR: {yoloPreflight.runtime.yolo_config_dir ?? 'n/a'}</span>
|
<span>YOLO_CONFIG_DIR: {yoloPreflight.runtime.yolo_config_dir ?? 'n/a'}</span>
|
||||||
<span>model directory: {yoloPreflight.runtime.model_directory ?? 'n/a'}</span>
|
<span>model directory: {yoloPreflight.runtime.model_directory ?? 'n/a'}</span>
|
||||||
<span>model_asset_id: {yoloPreflight.model_asset_id ?? 'n/a'}</span>
|
<span>model_asset_id: {yoloPreflight.model_asset_id ?? 'n/a'}</span>
|
||||||
@@ -437,6 +446,13 @@ export function DetectionLab({
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
) : null}
|
) : null}
|
||||||
|
{selectedDetectionModelId === 'yolo-configured' && detectionTileManifestPath.trim() ? (
|
||||||
|
<div className="linked-manifest-card">
|
||||||
|
<strong>Linked tile manifest</strong>
|
||||||
|
<p>{detectionTileManifestPath}</p>
|
||||||
|
<span>Refresh preflight after changing model asset or manifest path.</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<button className="primary-action" type="button" onClick={onRunDetection} disabled={runningDetection || !detectionRunReady}>
|
<button className="primary-action" type="button" onClick={onRunDetection} disabled={runningDetection || !detectionRunReady}>
|
||||||
Run detection
|
Run detection
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type {
|
|||||||
AreaRead,
|
AreaRead,
|
||||||
DatasetCreateResponse,
|
DatasetCreateResponse,
|
||||||
JobRead,
|
JobRead,
|
||||||
|
RasterTileHandoff,
|
||||||
RasterMetadataResponse,
|
RasterMetadataResponse,
|
||||||
RasterPreviewResponse,
|
RasterPreviewResponse,
|
||||||
RasterStatsResponse,
|
RasterStatsResponse,
|
||||||
@@ -28,12 +29,39 @@ function toRasterMetadata(metadata: Record<string, unknown> | null | undefined):
|
|||||||
}
|
}
|
||||||
|
|
||||||
function extractRasterTileManifestPath(job: JobRead | null | undefined): string {
|
function extractRasterTileManifestPath(job: JobRead | null | undefined): string {
|
||||||
|
const manifest = toRasterTileHandoff(job)
|
||||||
|
return manifest?.manifest_path ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionalString(value: unknown): string | null {
|
||||||
|
return typeof value === 'string' && value.trim().length > 0 ? value : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function optionalNumber(value: unknown): number | null {
|
||||||
|
return typeof value === 'number' && Number.isFinite(value) ? value : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRasterTileHandoff(job: JobRead | null | undefined): RasterTileHandoff | null {
|
||||||
const result = job?.result_json
|
const result = job?.result_json
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return ''
|
return null
|
||||||
}
|
}
|
||||||
const manifestPath = result.manifest_path
|
const manifestPath = result.manifest_path
|
||||||
return typeof manifestPath === 'string' ? manifestPath : ''
|
if (typeof manifestPath !== 'string' || manifestPath.trim().length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const manifest = typeof result.manifest === 'object' && result.manifest !== null
|
||||||
|
? result.manifest as Record<string, unknown>
|
||||||
|
: {}
|
||||||
|
return {
|
||||||
|
manifest_path: manifestPath,
|
||||||
|
tile_set_id: optionalString(result.tile_set_id) ?? optionalString(manifest.tile_set_id),
|
||||||
|
tile_size: optionalNumber(result.tile_size) ?? optionalNumber(manifest.tile_size),
|
||||||
|
overlap: optionalNumber(result.overlap) ?? optionalNumber(manifest.overlap),
|
||||||
|
count: optionalNumber(result.count) ?? optionalNumber(manifest.count),
|
||||||
|
source_dataset_id: optionalString(result.source_dataset_id) ?? optionalString(manifest.source_dataset_id),
|
||||||
|
created_at: optionalString(manifest.created_at),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDatasetWorkflow({
|
export function useDatasetWorkflow({
|
||||||
@@ -53,6 +81,7 @@ export function useDatasetWorkflow({
|
|||||||
const [jobs, setJobs] = useState<JobRead[]>([])
|
const [jobs, setJobs] = useState<JobRead[]>([])
|
||||||
const [rasterPreview, setRasterPreview] = useState<RasterPreviewResponse | null>(null)
|
const [rasterPreview, setRasterPreview] = useState<RasterPreviewResponse | null>(null)
|
||||||
const [latestRasterTileManifestPath, setLatestRasterTileManifestPath] = useState('')
|
const [latestRasterTileManifestPath, setLatestRasterTileManifestPath] = useState('')
|
||||||
|
const [latestRasterTileManifest, setLatestRasterTileManifest] = useState<RasterTileHandoff | null>(null)
|
||||||
const [selectedIntersectTargetId, setSelectedIntersectTargetId] = useState('')
|
const [selectedIntersectTargetId, setSelectedIntersectTargetId] = useState('')
|
||||||
const [selectedClipAreaId, setSelectedClipAreaId] = useState('')
|
const [selectedClipAreaId, setSelectedClipAreaId] = useState('')
|
||||||
const [rasterTileSize, setRasterTileSize] = useState(512)
|
const [rasterTileSize, setRasterTileSize] = useState(512)
|
||||||
@@ -113,7 +142,10 @@ export function useDatasetWorkflow({
|
|||||||
const latestRasterTileJob = response.items.find(
|
const latestRasterTileJob = response.items.find(
|
||||||
(job) => job.job_type === 'raster.tile' && extractRasterTileManifestPath(job),
|
(job) => job.job_type === 'raster.tile' && extractRasterTileManifestPath(job),
|
||||||
)
|
)
|
||||||
setLatestRasterTileManifestPath(extractRasterTileManifestPath(latestRasterTileJob))
|
const manifest = toRasterTileHandoff(latestRasterTileJob)
|
||||||
|
const manifestPath = manifest ? manifest.manifest_path : ''
|
||||||
|
setLatestRasterTileManifest(manifest)
|
||||||
|
setLatestRasterTileManifestPath(manifestPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadDatasetDetails = async (projectId: string, dataset: DatasetCreateResponse) => {
|
const loadDatasetDetails = async (projectId: string, dataset: DatasetCreateResponse) => {
|
||||||
@@ -126,6 +158,7 @@ export function useDatasetWorkflow({
|
|||||||
setDatasetContent(null)
|
setDatasetContent(null)
|
||||||
setRasterPreview(null)
|
setRasterPreview(null)
|
||||||
setLatestRasterTileManifestPath('')
|
setLatestRasterTileManifestPath('')
|
||||||
|
setLatestRasterTileManifest(null)
|
||||||
setSelectedDatasetId(dataset.id)
|
setSelectedDatasetId(dataset.id)
|
||||||
setJobs([])
|
setJobs([])
|
||||||
try {
|
try {
|
||||||
@@ -372,10 +405,13 @@ export function useDatasetWorkflow({
|
|||||||
overlap: rasterTileOverlap,
|
overlap: rasterTileOverlap,
|
||||||
output_name: rasterTileOutputName || undefined,
|
output_name: rasterTileOutputName || undefined,
|
||||||
})
|
})
|
||||||
const manifestPath = extractRasterTileManifestPath(job)
|
const manifest = toRasterTileHandoff(job)
|
||||||
|
const manifestPath = manifest?.manifest_path ?? ''
|
||||||
|
setLatestRasterTileManifest(manifest)
|
||||||
setLatestRasterTileManifestPath(manifestPath)
|
setLatestRasterTileManifestPath(manifestPath)
|
||||||
await refreshSelectedDatasetAfterJob()
|
await refreshSelectedDatasetAfterJob()
|
||||||
if (manifestPath) {
|
if (manifestPath) {
|
||||||
|
setLatestRasterTileManifest(manifest)
|
||||||
setLatestRasterTileManifestPath(manifestPath)
|
setLatestRasterTileManifestPath(manifestPath)
|
||||||
}
|
}
|
||||||
setDatasetDetailError(null)
|
setDatasetDetailError(null)
|
||||||
@@ -465,6 +501,7 @@ export function useDatasetWorkflow({
|
|||||||
setDatasetContent(null)
|
setDatasetContent(null)
|
||||||
setRasterPreview(null)
|
setRasterPreview(null)
|
||||||
setLatestRasterTileManifestPath('')
|
setLatestRasterTileManifestPath('')
|
||||||
|
setLatestRasterTileManifest(null)
|
||||||
setJobs([])
|
setJobs([])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,6 +515,7 @@ export function useDatasetWorkflow({
|
|||||||
jobs,
|
jobs,
|
||||||
rasterPreview,
|
rasterPreview,
|
||||||
latestRasterTileManifestPath,
|
latestRasterTileManifestPath,
|
||||||
|
latestRasterTileManifest,
|
||||||
selectedIntersectTargetId,
|
selectedIntersectTargetId,
|
||||||
selectedClipAreaId,
|
selectedClipAreaId,
|
||||||
rasterTileSize,
|
rasterTileSize,
|
||||||
|
|||||||
@@ -3702,6 +3702,63 @@ button.entity-card {
|
|||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.raster-manifest-details {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
|
||||||
|
gap: 0.42rem;
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0.2rem 0 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raster-manifest-details div {
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid #d8e3de;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.42rem 0.48rem;
|
||||||
|
background: #f8fbf9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raster-manifest-details dt {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 850;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raster-manifest-details dd {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0.12rem 0 0;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
font-weight: 750;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linked-manifest-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.22rem;
|
||||||
|
min-width: 0;
|
||||||
|
border: 1px solid rgba(47, 125, 86, 0.24);
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 0.6rem 0.65rem;
|
||||||
|
background: #f5fbf6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linked-manifest-card strong {
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linked-manifest-card p,
|
||||||
|
.linked-manifest-card span {
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.raster-readiness-item span,
|
.raster-readiness-item span,
|
||||||
.raster-manifest-handoff span {
|
.raster-manifest-handoff span {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
|||||||
@@ -259,6 +259,16 @@ export interface RasterTileResponse {
|
|||||||
manifest: RasterTileManifest
|
manifest: RasterTileManifest
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RasterTileHandoff {
|
||||||
|
manifest_path: string
|
||||||
|
tile_set_id?: string | null
|
||||||
|
tile_size?: number | null
|
||||||
|
overlap?: number | null
|
||||||
|
count?: number | null
|
||||||
|
source_dataset_id?: string | null
|
||||||
|
created_at?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export interface VectorBBoxResponse {
|
export interface VectorBBoxResponse {
|
||||||
dataset_id: string
|
dataset_id: string
|
||||||
bounds_json: Record<string, number> | null
|
bounds_json: Record<string, number> | null
|
||||||
|
|||||||
Reference in New Issue
Block a user