Expand scoped demo analysis access
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Jens
2026-08-01 16:00:13 +02:00
parent 96db4c966d
commit dfcc11b0d5
19 changed files with 206 additions and 112 deletions
+48 -2
View File
@@ -210,6 +210,12 @@ def create_app() -> FastAPI:
guest_safe_read_paths = { guest_safe_read_paths = {
f"{settings.api_prefix}/projects", f"{settings.api_prefix}/projects",
f"{settings.api_prefix}/external/providers", f"{settings.api_prefix}/external/providers",
f"{settings.api_prefix}/assistant/status",
f"{settings.api_prefix}/assistant/models",
f"{settings.api_prefix}/detection/models",
f"{settings.api_prefix}/detection/model-assets",
f"{settings.api_prefix}/detection/yolo/preflight",
f"{settings.api_prefix}/segmentation/models",
} }
normalized_path = raw_path.rstrip("/") or "/" normalized_path = raw_path.rstrip("/") or "/"
guest_project_read = ( guest_project_read = (
@@ -218,7 +224,21 @@ def create_app() -> FastAPI:
) )
is_read_request = request.method in {"GET", "HEAD", "OPTIONS"} is_read_request = request.method in {"GET", "HEAD", "OPTIONS"}
if is_read_request: if is_read_request:
if normalized_path not in guest_safe_read_paths and not guest_project_read: guest_scoped_analysis_read = (
query_project_id == str(principal.project_id)
and normalized_path.startswith(
(
f"{settings.api_prefix}/detection/",
f"{settings.api_prefix}/segmentation/",
f"{settings.api_prefix}/exports/",
)
)
)
if (
normalized_path not in guest_safe_read_paths
and not guest_project_read
and not guest_scoped_analysis_read
):
response = JSONResponse( response = JSONResponse(
status_code=403, status_code=403,
content=_to_error_payload( content=_to_error_payload(
@@ -234,6 +254,15 @@ def create_app() -> FastAPI:
f"{settings.api_prefix}/demo/workflow", f"{settings.api_prefix}/demo/workflow",
f"{settings.api_prefix}/external/coverage/resolve", f"{settings.api_prefix}/external/coverage/resolve",
} }
guest_scoped_analysis_post_paths = {
f"{settings.api_prefix}/detection/run",
f"{settings.api_prefix}/segmentation/run",
f"{settings.api_prefix}/qa/detections-vs-reference",
f"{settings.api_prefix}/exports/geojson",
f"{settings.api_prefix}/exports/metadata",
f"{settings.api_prefix}/exports/report",
f"{settings.api_prefix}/exports/map-result",
}
guest_safe_post_suffixes = ( guest_safe_post_suffixes = (
"/vector/select", "/vector/select",
"/raster/bathymetry/select", "/raster/bathymetry/select",
@@ -247,9 +276,26 @@ def create_app() -> FastAPI:
) )
is_guest_safe_post = request.method == "POST" and ( is_guest_safe_post = request.method == "POST" and (
raw_path in guest_safe_post_paths raw_path in guest_safe_post_paths
or (
raw_path in guest_scoped_analysis_post_paths
and query_project_id == str(principal.project_id)
)
or (
query_project_id == str(principal.project_id)
and raw_path.startswith(
(
f"{settings.api_prefix}/detection/runs/",
f"{settings.api_prefix}/segmentation/runs/",
)
)
and raw_path.endswith("/qa/reference")
)
or ( or (
raw_path.startswith(project_path_prefix) raw_path.startswith(project_path_prefix)
and raw_path.endswith(guest_safe_post_suffixes) and (
raw_path.endswith(guest_safe_post_suffixes)
or raw_path.endswith("/assistant/query")
)
) )
) )
if not is_guest_safe_post: if not is_guest_safe_post:
+11 -4
View File
@@ -126,7 +126,7 @@ def test_login_uses_http_only_session_cookie_and_logout_revokes_browser_access(m
assert protected_after_logout.status_code == 401 assert protected_after_logout.status_code == 401
def test_guest_login_seeds_scoped_demo_and_rejects_mutating_or_cross_project_requests(monkeypatch) -> None: def test_guest_login_exposes_models_but_rejects_management_and_cross_project_requests(monkeypatch) -> None:
project_id = UUID("00000000-0000-0000-0000-000000000123") project_id = UUID("00000000-0000-0000-0000-000000000123")
demo = DemoWorkflowResponse( demo = DemoWorkflowResponse(
project_id=project_id, project_id=project_id,
@@ -152,7 +152,11 @@ def test_guest_login_seeds_scoped_demo_and_rejects_mutating_or_cross_project_req
guest_session = client.get("/api/v1/auth/session") guest_session = client.get("/api/v1/auth/session")
mutation = client.post("/api/v1/projects", json={"name": "Not allowed"}) mutation = client.post("/api/v1/projects", json={"name": "Not allowed"})
other_project = client.get("/api/v1/projects/00000000-0000-0000-0000-000000000999") other_project = client.get("/api/v1/projects/00000000-0000-0000-0000-000000000999")
unscoped_read = client.get("/api/v1/detection/models") detection_models = client.get("/api/v1/detection/models")
segmentation_models = client.get("/api/v1/segmentation/models")
cross_project_runs = client.get(
"/api/v1/detection/runs?project_id=00000000-0000-0000-0000-000000000999"
)
cross_project_coverage = client.post( cross_project_coverage = client.post(
"/api/v1/external/coverage/resolve", "/api/v1/external/coverage/resolve",
json={ json={
@@ -172,8 +176,11 @@ def test_guest_login_seeds_scoped_demo_and_rejects_mutating_or_cross_project_req
assert mutation.json()["error"] == "GUEST_READ_ONLY" assert mutation.json()["error"] == "GUEST_READ_ONLY"
assert other_project.status_code == 403 assert other_project.status_code == 403
assert other_project.json()["error"] == "GUEST_PROJECT_SCOPE_REQUIRED" assert other_project.json()["error"] == "GUEST_PROJECT_SCOPE_REQUIRED"
assert unscoped_read.status_code == 403 assert detection_models.status_code == 200
assert unscoped_read.json()["error"] == "GUEST_ROUTE_NOT_AVAILABLE" assert detection_models.json()["data"]["models"]
assert segmentation_models.status_code == 200
assert cross_project_runs.status_code == 403
assert cross_project_runs.json()["error"] == "GUEST_PROJECT_SCOPE_REQUIRED"
assert cross_project_coverage.status_code == 403 assert cross_project_coverage.status_code == 403
assert cross_project_coverage.json()["error"] == "GUEST_PROJECT_SCOPE_REQUIRED" assert cross_project_coverage.json()["error"] == "GUEST_PROJECT_SCOPE_REQUIRED"
+13 -7
View File
@@ -64,8 +64,12 @@ one client/username combination within five minutes temporarily return HTTP
Optional guest access is a configuration-gated demonstration mode. It creates Optional guest access is a configuration-gated demonstration mode. It creates
a shorter signed session with role `guest`, scopes that session to the a shorter signed session with role `guest`, scopes that session to the
idempotently seeded demo project and blocks mutating operator routes. Project idempotently seeded demo project and blocks mutating operator routes. Project
listing is filtered to the bound demo project. The frontend exposes only the listing is filtered to the bound demo project. The frontend exposes the same
map and the already calculated quality evidence. This is deliberately **not** exploration, assistant, model-selection, analysis, QA and export workspaces as
an operator. Model catalogs are globally readable; every run, result and export
request remains explicitly bound to the demo-project UUID. Project and area
management, uploads, source/runtime configuration, evidence review and other
administrative mutations remain unavailable. This is deliberately **not**
a substitute for user accounts, authorization or tenant isolation; expose it a substitute for user accounts, authorization or tenant isolation; expose it
only on a dedicated demo installation without private or operational data. only on a dedicated demo installation without private or operational data.
@@ -119,11 +123,13 @@ Disabled guest access returns HTTP 403 `GUEST_ACCESS_DISABLED`. A guest request
for a different project returns HTTP 403 `GUEST_PROJECT_SCOPE_REQUIRED`; a for a different project returns HTTP 403 `GUEST_PROJECT_SCOPE_REQUIRED`; a
blocked mutation returns HTTP 403 `GUEST_READ_ONLY`. Unscoped read routes that blocked mutation returns HTTP 403 `GUEST_READ_ONLY`. Unscoped read routes that
are not needed by the demo return HTTP 403 `GUEST_ROUTE_NOT_AVAILABLE`. are not needed by the demo return HTTP 403 `GUEST_ROUTE_NOT_AVAILABLE`.
Guest reads are limited to the filtered project list, provider metadata and the Guest reads are limited to the filtered project list, provider/model metadata,
bound project tree. A small, explicit set of `POST` selection/read-analysis the bound project tree and project-scoped detection, segmentation and export
routes remains available because those routes query persisted evidence without results. An explicit set of `POST` selection, assistant, AI/QA and export routes
exposing operator administration. Coverage resolution additionally verifies is available for that bound demo project. Unscoped analysis routes require the
the `project_id` in the request body against the guest-session scope. same UUID as a `project_id` query parameter; cross-project values fail before
route execution. Coverage resolution additionally verifies the `project_id` in
the request body against the guest-session scope.
### POST `/api/v1/auth/logout` ### POST `/api/v1/auth/logout`
+12
View File
@@ -12218,3 +12218,15 @@ Open:
- Mobiele hoofdflow: thema zoeken, kiezen, volledig werkgebied selecteren, expliciet analyseren, resultatenlade openen en sluiten geslaagd. - Mobiele hoofdflow: thema zoeken, kiezen, volledig werkgebied selecteren, expliciet analyseren, resultatenlade openen en sluiten geslaagd.
- Browserconsole: `0` waarschuwingen en `0` fouten. - Browserconsole: `0` waarschuwingen en `0` fouten.
- Bewijsbeelden: `docs/screenshots/ui-ux-map-desktop-2026-08-01.jpg` en `docs/screenshots/ui-ux-map-mobile-results-2026-08-01.jpg`. - Bewijsbeelden: `docs/screenshots/ui-ux-map-desktop-2026-08-01.jpg` en `docs/screenshots/ui-ux-map-mobile-results-2026-08-01.jpg`.
## 2026-08-01 - Sprint 237 volwaardige demo-analysetoegang
### Gewijzigd
- Demo-navigatie omvat nu status, bronnen, kaart, AI-vragen, kwaliteit, beeldanalyse en downloads; alleen de systeem-/beheerwerkruimte blijft verborgen.
- De demo laadt dezelfde lokale assistent-, detectie- en segmentatiemodellen, runs, resultaten en exports als de operator binnen het gebonden demoproject.
- Gastverzoeken voor AI-runs, QA, assistent en exports zijn server-side toegestaan met een verplichte en gecontroleerde `project_id`; andere projecten blijven vóór route-uitvoering geblokkeerd.
- Project- en gebiedbeheer, uploads, bron-/runtimeconfiguratie, bewijsreviews en overige mutaties blijven operator-only.
### Verificatie
- TypeScript- en Vite-productiebuild geslaagd.
- Gerichte frontend-, backend-, browser- en productieverificatie volgen hieronder na de releasegate.
+12 -3
View File
@@ -21,9 +21,10 @@ Uitvoeringsbord: `docs/PYTORCH_TRAINING_ROADMAP_BELGIUM.md`.
Professionaliseringspass (2026-07-27): Professionaliseringspass (2026-07-27):
- [x] Voeg een expliciete gastknop toe aan de toegangspoort en open daarmee - [x] Voeg een expliciete gastknop toe aan de toegangspoort en open daarmee
een korte, projectgebonden, alleen-lezen demowerkruimte. een korte, projectgebonden demowerkruimte.
- [x] Beperk de gastinterface tot kaartverkenning en bestaand kwaliteitsbewijs; - [x] Geef de demo dezelfde kaart-, bron-, assistent-, model-, analyse-, QA- en
blokkeer operatoracties en toegang tot andere projecten ook server-side. downloadfuncties als de operator, maar blokkeer beheer, instellingen,
uploads, reviews en toegang tot andere projecten ook server-side.
- [x] Herwerk de landingspagina, aanmeldhiërarchie, mobiele navigatie en - [x] Herwerk de landingspagina, aanmeldhiërarchie, mobiele navigatie en
workbenchcontext tot één rustigere en professionelere productervaring. workbenchcontext tot één rustigere en professionelere productervaring.
- [ ] Consolideer na visuele regressiesnapshots de vier historische - [ ] Consolideer na visuele regressiesnapshots de vier historische
@@ -1066,3 +1067,11 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Uitschuifbare inzichten behouden; analyse blijft uitsluitend expliciet na themakeuze. - [x] Uitschuifbare inzichten behouden; analyse blijft uitsluitend expliciet na themakeuze.
- [x] 51 frontendtests en productiebuild groen. - [x] 51 frontendtests en productiebuild groen.
- [ ] 19 verouderde broncode-stringtests herijken; meerdere eisen daarin (automatische analyse) conflicteren bewust met de actuele productbeslissing. - [ ] 19 verouderde broncode-stringtests herijken; meerdere eisen daarin (automatische analyse) conflicteren bewust met de actuele productbeslissing.
## Sprint 237 - Volwaardige, projectgebonden demo (2026-08-01)
- [x] Maak alle niet-administratieve werkruimtes zichtbaar voor demo-gebruikers.
- [x] Laad dezelfde assistent-, detectie- en segmentatiemodellen en bewaarde resultaten.
- [x] Sta projectgebonden analyse-, QA-, assistent- en exportacties toe.
- [x] Behoud server-side blokkades op instellingen, beheer, uploads, reviews en cross-projectverzoeken.
- [ ] Verifieer en redeploy de exacte commit naar Tower `/mnt/user/appdata/geointel`.
+38 -25
View File
@@ -90,9 +90,21 @@ const workspaceNavGroups: WorkspaceNavigationGroup[] = [
{ label: 'Beheer', keys: ['overview', 'system'] }, { label: 'Beheer', keys: ['overview', 'system'] },
] ]
const guestWorkspaceKeys = new Set<WorkspaceKey>(['map', 'analysis']) const guestWorkspaceKeys = new Set<WorkspaceKey>([
'overview',
'data',
'map',
'assistant',
'analysis',
'ai',
'exports',
])
const guestWorkspaceGroups: WorkspaceNavigationGroup[] = [ const guestWorkspaceGroups: WorkspaceNavigationGroup[] = [
{ label: 'Demowerkruimte', keys: ['map', 'analysis'] }, { label: 'Verkennen', keys: ['map', 'data'] },
{ label: 'Vragen', keys: ['assistant'] },
{ label: 'Analyseren', keys: ['analysis', 'ai'] },
{ label: 'Afronden', keys: ['exports'] },
{ label: 'Demo', keys: ['overview'] },
] ]
interface WorkbenchAppProps { interface WorkbenchAppProps {
@@ -589,7 +601,7 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA
demoWorkflowMessage, demoWorkflowMessage,
loadDemoWorkflow, loadDemoWorkflow,
} = useDemoWorkflow({ } = useDemoWorkflow({
restrictedMode: isGuest, restrictedMode: false,
loadProjects, loadProjects,
loadProjectData, loadProjectData,
loadDatasetDetails, loadDatasetDetails,
@@ -618,7 +630,7 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA
}, [isGuest, loadDemoWorkflow]) }, [isGuest, loadDemoWorkflow])
useWorkbenchBootstrap({ useWorkbenchBootstrap({
restrictedMode: isGuest, restrictedMode: false,
selectedProjectId, selectedProjectId,
selectedDetectionRunId, selectedDetectionRunId,
detectionClassFilter, detectionClassFilter,
@@ -720,7 +732,7 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA
} }
const openWorkflowGuidanceStep = (target: WorkspaceKey) => { const openWorkflowGuidanceStep = (target: WorkspaceKey) => {
if (isGuest && !guestWorkspaceKeys.has(target)) { if (isGuest && !guestWorkspaceKeys.has(target)) {
setActiveWorkspace(target === 'exports' ? 'analysis' : 'map') setActiveWorkspace('map')
return return
} }
if (target === 'map' && availableMapDatasets.length > 0 && !mapFeatureCollection) { if (target === 'map' && availableMapDatasets.length > 0 && !mapFeatureCollection) {
@@ -894,9 +906,9 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA
<ShieldCheck aria-hidden="true" /> <ShieldCheck aria-hidden="true" />
<div> <div>
<strong>Tijdelijke demowerkruimte</strong> <strong>Tijdelijke demowerkruimte</strong>
<span>U verkent vooraf geladen voorbeelddata. Wijzigingen, nieuwe analyses en operatorfuncties zijn uitgeschakeld.</span> <span>Alle analysemodellen en werkfuncties zijn beschikbaar. Beheer, instellingen en blijvende gegevenswijzigingen blijven afgeschermd.</span>
</div> </div>
<span className="guest-mode-badge">Alleen-lezen</span> <span className="guest-mode-badge">Analyse-toegang</span>
</div> </div>
) : null} ) : null}
@@ -924,7 +936,7 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA
<WorkspaceSignal workspace={activeWorkspace} /> <WorkspaceSignal workspace={activeWorkspace} />
<div className="workspace-heading-actions"> <div className="workspace-heading-actions">
<p>{activeWorkspaceItem.description}</p> <p>{activeWorkspaceItem.description}</p>
{!isGuest && activeWorkspace !== 'overview' ? ( {activeWorkspace !== 'overview' ? (
<button <button
type="button" type="button"
className={inspectorOpen ? 'inspector-toggle inspector-toggle-active' : 'inspector-toggle'} className={inspectorOpen ? 'inspector-toggle inspector-toggle-active' : 'inspector-toggle'}
@@ -960,6 +972,15 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA
projectId={selectedProjectId ?? undefined} projectId={selectedProjectId ?? undefined}
loading={loadingDatasets} loading={loadingDatasets}
/> />
{isGuest ? (
<div className="guest-readonly-card">
<ShieldCheck aria-hidden="true" />
<div>
<strong>Bronnen bekijken zonder beheerrechten</strong>
<span>De demo gebruikt dezelfde beschikbare databronnen. Projecten, gebieden en uploads worden alleen door de operator beheerd.</span>
</div>
</div>
) : <>
<ProjectPanel <ProjectPanel
projects={projects} projects={projects}
selectedProjectId={selectedProjectId} selectedProjectId={selectedProjectId}
@@ -1002,12 +1023,13 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA
onOpenDatasetInMap={openDatasetInMap} onOpenDatasetInMap={openDatasetInMap}
onOpenDatasetExport={openDatasetExport} onOpenDatasetExport={openDatasetExport}
/> />
</>}
</div> </div>
) : null} ) : null}
<div className="workspace-persistent-map" hidden={activeWorkspace !== 'map'}> <div className="workspace-persistent-map" hidden={activeWorkspace !== 'map'}>
<MapWorkspace <MapWorkspace
readOnly={isGuest} readOnly={false}
selectedProjectId={selectedProjectId} selectedProjectId={selectedProjectId}
projects={projects} projects={projects}
areas={areas} areas={areas}
@@ -1103,8 +1125,8 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA
onRefreshProjectData={() => ( onRefreshProjectData={() => (
selectedProjectId ? loadProjectData(selectedProjectId) : Promise.resolve(null) selectedProjectId ? loadProjectData(selectedProjectId) : Promise.resolve(null)
)} )}
onOpenAssistant={() => setActiveWorkspace(isGuest ? 'analysis' : 'assistant')} onOpenAssistant={() => setActiveWorkspace('assistant')}
onOpenExports={() => setActiveWorkspace(isGuest ? 'analysis' : 'exports')} onOpenExports={() => setActiveWorkspace('exports')}
/> />
</div> </div>
@@ -1121,10 +1143,9 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA
evidenceLoading={qualityEvidenceLoading} evidenceLoading={qualityEvidenceLoading}
evidenceError={qualityEvidenceError} evidenceError={qualityEvidenceError}
onOpenMapWorkspace={() => setActiveWorkspace('map')} onOpenMapWorkspace={() => setActiveWorkspace('map')}
onOpenAnalysisWorkspace={() => setActiveWorkspace(isGuest ? 'map' : 'ai')} onOpenAnalysisWorkspace={() => setActiveWorkspace('ai')}
/> />
{!isGuest ? ( <details className="secondary-analysis-disclosure">
<details className="secondary-analysis-disclosure">
<summary> <summary>
<span>Historische vectorlagen vergelijken</span> <span>Historische vectorlagen vergelijken</span>
<strong>Geavanceerd</strong> <strong>Geavanceerd</strong>
@@ -1144,16 +1165,7 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA
onIncludeUnchangedChange={setChangeIncludeUnchanged} onIncludeUnchangedChange={setChangeIncludeUnchanged}
onRun={runChangeDetection} onRun={runChangeDetection}
/> />
</details> </details>
) : (
<div className="guest-readonly-card">
<ShieldCheck aria-hidden="true" />
<div>
<strong>Controleerbaar voorbeeldresultaat</strong>
<span>Deze kwaliteitsweergave gebruikt de vooraf berekende demo. Nieuwe vergelijkingen zijn alleen beschikbaar voor operators.</span>
</div>
</div>
)}
</div> </div>
) : null} ) : null}
@@ -1171,6 +1183,7 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA
{activeWorkspace === 'ai' ? ( {activeWorkspace === 'ai' ? (
<div className="workspace-grid workspace-grid-ai"> <div className="workspace-grid workspace-grid-ai">
<DetectionLab <DetectionLab
managementLocked={isGuest}
detectionModels={detectionModels} detectionModels={detectionModels}
modelAssets={modelAssets} modelAssets={modelAssets}
loadingDetectionModels={loadingDetectionModels} loadingDetectionModels={loadingDetectionModels}
@@ -1318,7 +1331,7 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA
) : null} ) : null}
</main> </main>
{inspectorOpen && !isGuest ? ( {inspectorOpen ? (
<aside className="workbench-inspector" id="workbench-inspector" aria-label="Details van de huidige selectie"> <aside className="workbench-inspector" id="workbench-inspector" aria-label="Details van de huidige selectie">
<WorkbenchInspector <WorkbenchInspector
selectedProject={selectedProject} selectedProject={selectedProject}
@@ -78,6 +78,7 @@ interface CalibrationRow {
} }
interface DetectionLabProps { interface DetectionLabProps {
managementLocked?: boolean
detectionModels: DetectionModelCapability[] detectionModels: DetectionModelCapability[]
modelAssets: ModelAssetRead[] modelAssets: ModelAssetRead[]
loadingDetectionModels: boolean loadingDetectionModels: boolean
@@ -138,6 +139,7 @@ interface DetectionLabProps {
} }
export function DetectionLab({ export function DetectionLab({
managementLocked = false,
detectionModels, detectionModels,
modelAssets, modelAssets,
loadingDetectionModels, loadingDetectionModels,
@@ -332,7 +334,7 @@ export function DetectionLab({
</div> </div>
) : null} ) : null}
<DetectionModelManagement {!managementLocked ? <DetectionModelManagement
detectionModels={detectionModels} detectionModels={detectionModels}
modelAssets={modelAssets} modelAssets={modelAssets}
loadingDetectionModels={loadingDetectionModels} loadingDetectionModels={loadingDetectionModels}
@@ -347,7 +349,7 @@ export function DetectionLab({
onRefreshYoloPreflight={onRefreshYoloPreflight} onRefreshYoloPreflight={onRefreshYoloPreflight}
onSelectModelAsset={onSelectModelAsset} onSelectModelAsset={onSelectModelAsset}
onApplyOperatorProfile={onApplyOperatorProfile} onApplyOperatorProfile={onApplyOperatorProfile}
/> /> : null}
<div className="lab-block"> <div className="lab-block">
<div className="ai-lab-run-surface" aria-label="Gebouwdetectie starten"> <div className="ai-lab-run-surface" aria-label="Gebouwdetectie starten">
@@ -414,7 +416,7 @@ export function DetectionLab({
<p>Voeg hieronder een gegeorefereerde GeoTIFF toe. GeoIntel controleert de projectie en bewaart het bronbestand als dataset.</p> <p>Voeg hieronder een gegeorefereerde GeoTIFF toe. GeoIntel controleert de projectie en bewaart het bronbestand als dataset.</p>
</div> </div>
) : null} ) : null}
<div className="guided-raster-input" aria-label="Luchtbeeld toevoegen"> {!managementLocked ? <div className="guided-raster-input" aria-label="Luchtbeeld toevoegen">
<div> <div>
<strong>Eigen luchtbeeld toevoegen</strong> <strong>Eigen luchtbeeld toevoegen</strong>
<p>Gebruik een GeoTIFF met geldige CRS en georeferentie. Een bestaand luchtbeeld kan meteen in de keuzelijst worden gebruikt.</p> <p>Gebruik een GeoTIFF met geldige CRS en georeferentie. Een bestaand luchtbeeld kan meteen in de keuzelijst worden gebruikt.</p>
@@ -444,7 +446,7 @@ export function DetectionLab({
> >
{detectionWorkflowStage === 'uploading' ? 'Luchtbeeld toevoegen...' : 'Luchtbeeld toevoegen'} {detectionWorkflowStage === 'uploading' ? 'Luchtbeeld toevoegen...' : 'Luchtbeeld toevoegen'}
</button> </button>
</div> </div> : null}
<div className="lab-form-grid"> <div className="lab-form-grid">
<label> <label>
Luchtbeeld Luchtbeeld
@@ -499,7 +501,7 @@ export function DetectionLab({
{detectionWorkflowActionLabel(detectionWorkflowStage)} {detectionWorkflowActionLabel(detectionWorkflowStage)}
</button> </button>
<details className="ai-lab-model-surface technical-manifest-surface" aria-label="Technische tegelinstellingen"> {!managementLocked ? <details className="ai-lab-model-surface technical-manifest-surface" aria-label="Technische tegelinstellingen">
<summary> <summary>
<span>Technische tegelinstellingen</span> <span>Technische tegelinstellingen</span>
<strong>{detectionHasTileManifest ? 'manifest beschikbaar' : 'automatisch'}</strong> <strong>{detectionHasTileManifest ? 'manifest beschikbaar' : 'automatisch'}</strong>
@@ -528,7 +530,7 @@ export function DetectionLab({
Bestaande beeldtegels analyseren Bestaande beeldtegels analyseren
</button> </button>
</div> </div>
</details> </details> : null}
</div> </div>
</div> </div>
@@ -556,7 +558,7 @@ export function DetectionLab({
) : null} ) : null}
</div> </div>
<details className="ai-lab-model-surface guided-calibration-surface" aria-label="Modelkalibratie voor beheerders"> {!managementLocked ? <details className="ai-lab-model-surface guided-calibration-surface" aria-label="Modelkalibratie voor beheerders">
<summary> <summary>
<span>Modelkalibratie voor beheerders</span> <span>Modelkalibratie voor beheerders</span>
<strong>{detectionCalibrationRows.length > 0 ? `${detectionCalibrationRows.length} drempels getest` : 'gesloten'}</strong> <strong>{detectionCalibrationRows.length > 0 ? `${detectionCalibrationRows.length} drempels getest` : 'gesloten'}</strong>
@@ -677,7 +679,7 @@ export function DetectionLab({
</div> </div>
)} )}
</div> </div>
</details> </details> : null}
<div className="ai-lab-results-surface" aria-label="Resultaten van de beeldanalyse"> <div className="ai-lab-results-surface" aria-label="Resultaten van de beeldanalyse">
<div className="panel-title-row"> <div className="panel-title-row">
+6 -8
View File
@@ -67,17 +67,15 @@ export function useDemoWorkflow({
setSegmentationReferenceDatasetId(result.reference_dataset_id) setSegmentationReferenceDatasetId(result.reference_dataset_id)
setDemoWorkflowMessage(result.message) setDemoWorkflowMessage(result.message)
await loadProjects(result.project_id) await loadProjects(result.project_id)
const operatorOnlyLoads = restrictedMode const analysisLoads = Promise.all([
? Promise.resolve() loadDetectionRuns(result.project_id),
: Promise.all([ loadSegmentationRuns(result.project_id),
loadDetectionRuns(result.project_id), loadExports(result.project_id),
loadSegmentationRuns(result.project_id), ]).then(() => undefined)
loadExports(result.project_id),
]).then(() => undefined)
const [projectData] = await Promise.all([ const [projectData] = await Promise.all([
loadProjectData(result.project_id), loadProjectData(result.project_id),
loadQualityChecks(result.project_id), loadQualityChecks(result.project_id),
operatorOnlyLoads, analysisLoads,
]) ])
const candidateDataset = projectData?.datasets.find((dataset) => dataset.id === result.candidate_dataset_id) const candidateDataset = projectData?.datasets.find((dataset) => dataset.id === result.candidate_dataset_id)
const rasterDataset = projectData?.datasets.find((dataset) => dataset.id === result.raster_dataset_id) const rasterDataset = projectData?.datasets.find((dataset) => dataset.id === result.raster_dataset_id)
+3 -2
View File
@@ -207,6 +207,7 @@ export function useDetectionWorkflow({
setDetectionRunError(null) setDetectionRunError(null)
try { try {
const params = { const params = {
project_id: selectedProjectId ?? '',
class_name: detectionClassFilter || null, class_name: detectionClassFilter || null,
min_confidence: detectionMinConfidenceFilter > 0 ? detectionMinConfidenceFilter : null, min_confidence: detectionMinConfidenceFilter > 0 ? detectionMinConfidenceFilter : null,
} }
@@ -415,7 +416,7 @@ export function useDetectionWorkflow({
setDetectionQaResult(null) setDetectionQaResult(null)
setRunningDetectionQa(true) setRunningDetectionQa(true)
try { try {
const result = await detectionApi.compareWithReference(analysisRunId, { const result = await detectionApi.compareWithReference(analysisRunId, selectedProjectId!, {
reference_dataset_id: referenceDatasetId, reference_dataset_id: referenceDatasetId,
iou_threshold: iouThresholdOverride ?? qaIouThreshold, iou_threshold: iouThresholdOverride ?? qaIouThreshold,
class_name: useCurrentFilters ? detectionClassFilter || null : null, class_name: useCurrentFilters ? detectionClassFilter || null : null,
@@ -486,7 +487,7 @@ export function useDetectionWorkflow({
parameters_json: { calibration: true, calibration_thresholds: thresholds }, parameters_json: { calibration: true, calibration_thresholds: thresholds },
}) })
setSelectedDetectionRunId(result.analysis_run_id) setSelectedDetectionRunId(result.analysis_run_id)
const qa = await detectionApi.compareWithReference(result.analysis_run_id, { const qa = await detectionApi.compareWithReference(result.analysis_run_id, selectedProjectId, {
reference_dataset_id: detectionReferenceDatasetId, reference_dataset_id: detectionReferenceDatasetId,
iou_threshold: qaIouThreshold, iou_threshold: qaIouThreshold,
class_name: detectionClassFilter || null, class_name: detectionClassFilter || null,
+10 -8
View File
@@ -59,7 +59,7 @@ export function useExportWorkflow({
setExporting(true) setExporting(true)
setExportError(null) setExportError(null)
try { try {
const response = await exportsApi.exportGeojson({ const response = await exportsApi.exportGeojson(selectedDataset.project_id, {
dataset_id: selectedDataset.id, dataset_id: selectedDataset.id,
export_kind: 'dataset', export_kind: 'dataset',
name: selectedDataset.name.replace(/\.(geo)?json$/i, ''), name: selectedDataset.name.replace(/\.(geo)?json$/i, ''),
@@ -74,14 +74,14 @@ export function useExportWorkflow({
} }
const exportSelectedDetectionRunGeoJson = async () => { const exportSelectedDetectionRunGeoJson = async () => {
if (!selectedDetectionRunId) { if (!selectedDetectionRunId || !selectedProjectId) {
setExportError('Select a detection run before exporting GeoJSON.') setExportError('Select a detection run before exporting GeoJSON.')
return return
} }
setExporting(true) setExporting(true)
setExportError(null) setExportError(null)
try { try {
const response = await exportsApi.exportGeojson({ const response = await exportsApi.exportGeojson(selectedProjectId, {
analysis_run_id: selectedDetectionRunId, analysis_run_id: selectedDetectionRunId,
export_kind: 'detection_run', export_kind: 'detection_run',
}) })
@@ -95,14 +95,14 @@ export function useExportWorkflow({
} }
const exportSelectedSegmentationRunGeoJson = async () => { const exportSelectedSegmentationRunGeoJson = async () => {
if (!selectedSegmentationRunId) { if (!selectedSegmentationRunId || !selectedProjectId) {
setExportError('Select a segmentation run before exporting GeoJSON.') setExportError('Select a segmentation run before exporting GeoJSON.')
return return
} }
setExporting(true) setExporting(true)
setExportError(null) setExportError(null)
try { try {
const response = await exportsApi.exportGeojson({ const response = await exportsApi.exportGeojson(selectedProjectId, {
analysis_run_id: selectedSegmentationRunId, analysis_run_id: selectedSegmentationRunId,
export_kind: 'segmentation_run', export_kind: 'segmentation_run',
}) })
@@ -123,7 +123,7 @@ export function useExportWorkflow({
setSelectionExporting(true) setSelectionExporting(true)
setSelectionExportError(null) setSelectionExportError(null)
try { try {
const response = await exportsApi.exportGeojson({ const response = await exportsApi.exportGeojson(selectedDataset.project_id, {
dataset_id: selectedDataset.id, dataset_id: selectedDataset.id,
area_id: areaId, area_id: areaId,
export_kind: 'vector_selection', export_kind: 'vector_selection',
@@ -199,7 +199,8 @@ export function useExportWorkflow({
const previewExportContent = async (exportId: string) => { const previewExportContent = async (exportId: string) => {
setExportError(null) setExportError(null)
try { try {
const response = await exportsApi.getContent(exportId) if (!selectedProjectId) return
const response = await exportsApi.getContent(selectedProjectId, exportId)
setExportPreview(response.content) setExportPreview(response.content)
} catch (error) { } catch (error) {
setExportError(formatError(error, 'Failed to load export content')) setExportError(formatError(error, 'Failed to load export content'))
@@ -207,7 +208,8 @@ export function useExportWorkflow({
} }
const downloadExportArtifact = (exportId: string) => { const downloadExportArtifact = (exportId: string) => {
window.open(exportsApi.downloadUrl(exportId), '_blank', 'noopener,noreferrer') if (!selectedProjectId) return
window.open(exportsApi.downloadUrl(selectedProjectId, exportId), '_blank', 'noopener,noreferrer')
} }
const resetExportsForProject = () => { const resetExportsForProject = () => {
+1 -1
View File
@@ -51,7 +51,7 @@ export function useMapSelectionQa({
iou_threshold: 0.5, iou_threshold: 0.5,
area_id: candidateDataset.area_id ?? null, area_id: candidateDataset.area_id ?? null,
} }
const job: JobRead = await qaApi.runQa(request) const job: JobRead = await qaApi.runQa(selectedProjectId, request)
if (job.status === 'failed') { if (job.status === 'failed') {
setMapSelectionQaError(job.error_message || 'Map selection QA/QC failed') setMapSelectionQaError(job.error_message || 'Map selection QA/QC failed')
return null return null
+1 -1
View File
@@ -92,7 +92,7 @@ export function useQualityWorkflow({ selectedProjectId, loadProjectData }: Quali
iou_threshold: qaIouThreshold, iou_threshold: qaIouThreshold,
area_id: qaAreaId || null, area_id: qaAreaId || null,
} }
const job: JobRead = await qaApi.runQa(request) const job: JobRead = await qaApi.runQa(selectedProjectId, request)
if (job.status === 'failed') { if (job.status === 'failed') {
setQaError(job.error_message || 'QA comparison failed') setQaError(job.error_message || 'QA comparison failed')
return return
@@ -104,6 +104,7 @@ export function useSegmentationWorkflow({
setSegmentationRunError(null) setSegmentationRunError(null)
try { try {
const params = { const params = {
project_id: selectedProjectId ?? '',
class_name: segmentationClassFilter || null, class_name: segmentationClassFilter || null,
min_confidence: segmentationMinConfidenceFilter > 0 ? segmentationMinConfidenceFilter : null, min_confidence: segmentationMinConfidenceFilter > 0 ? segmentationMinConfidenceFilter : null,
} }
@@ -175,7 +176,7 @@ export function useSegmentationWorkflow({
setSegmentationQaResult(null) setSegmentationQaResult(null)
setRunningSegmentationQa(true) setRunningSegmentationQa(true)
try { try {
const result = await segmentationApi.compareWithReference(selectedSegmentationRunId, { const result = await segmentationApi.compareWithReference(selectedSegmentationRunId, selectedProjectId!, {
reference_dataset_id: segmentationReferenceDatasetId, reference_dataset_id: segmentationReferenceDatasetId,
iou_threshold: qaIouThreshold, iou_threshold: qaIouThreshold,
class_name: segmentationClassFilter || null, class_name: segmentationClassFilter || null,
@@ -65,19 +65,19 @@ describe('useWorkbenchBootstrap', () => {
expect(state.loadExports).toHaveBeenCalledWith('project-1') expect(state.loadExports).toHaveBeenCalledWith('project-1')
}) })
it('keeps the guest bootstrap inside the read-only demo surface', async () => { it('loads the same analysis catalog and project results for a guest', async () => {
const state = { ...options('project-1'), restrictedMode: true } const state = { ...options('project-1'), restrictedMode: true }
renderHook(() => useWorkbenchBootstrap(state)) renderHook(() => useWorkbenchBootstrap(state))
await waitFor(() => expect(state.loadProjectData).toHaveBeenCalledWith('project-1')) await waitFor(() => expect(state.loadProjectData).toHaveBeenCalledWith('project-1'))
expect(state.loadCapabilities).toHaveBeenCalledOnce() expect(state.loadCapabilities).toHaveBeenCalledOnce()
expect(state.loadQualityChecks).toHaveBeenCalledWith('project-1') expect(state.loadQualityChecks).toHaveBeenCalledWith('project-1')
expect(state.loadDetectionModels).not.toHaveBeenCalled() expect(state.loadDetectionModels).toHaveBeenCalledOnce()
expect(state.loadSegmentationModels).not.toHaveBeenCalled() expect(state.loadSegmentationModels).toHaveBeenCalledOnce()
expect(state.loadDetectionRuns).not.toHaveBeenCalled() expect(state.loadDetectionRuns).toHaveBeenCalledWith('project-1')
expect(state.loadSegmentationRuns).not.toHaveBeenCalled() expect(state.loadSegmentationRuns).toHaveBeenCalledWith('project-1')
expect(state.loadExports).not.toHaveBeenCalled() expect(state.loadExports).toHaveBeenCalledWith('project-1')
expect(state.loadDetectionResults).not.toHaveBeenCalled() expect(state.loadDetectionResults).toHaveBeenCalledOnce()
expect(state.loadSegmentationResults).not.toHaveBeenCalled() expect(state.loadSegmentationResults).toHaveBeenCalledOnce()
}) })
}) })
+5 -11
View File
@@ -59,10 +59,8 @@ export function useWorkbenchBootstrap({
useEffect(() => { useEffect(() => {
loadProjects().catch(() => null) loadProjects().catch(() => null)
loadCapabilities().catch(() => null) loadCapabilities().catch(() => null)
if (!restrictedMode) { loadDetectionModels().catch(() => null)
loadDetectionModels().catch(() => null) loadSegmentationModels().catch(() => null)
loadSegmentationModels().catch(() => null)
}
}, [restrictedMode]) }, [restrictedMode])
useEffect(() => { useEffect(() => {
@@ -81,20 +79,16 @@ export function useWorkbenchBootstrap({
resetExportsForProject() resetExportsForProject()
loadProjectData(selectedProjectId).catch(() => null) loadProjectData(selectedProjectId).catch(() => null)
loadQualityChecks(selectedProjectId).catch(() => null) loadQualityChecks(selectedProjectId).catch(() => null)
if (!restrictedMode) { loadDetectionRuns(selectedProjectId).catch(() => null)
loadDetectionRuns(selectedProjectId).catch(() => null) loadSegmentationRuns(selectedProjectId).catch(() => null)
loadSegmentationRuns(selectedProjectId).catch(() => null) loadExports(selectedProjectId).catch(() => null)
loadExports(selectedProjectId).catch(() => null)
}
}, [restrictedMode, selectedProjectId]) }, [restrictedMode, selectedProjectId])
useEffect(() => { useEffect(() => {
if (restrictedMode) return
loadDetectionResults().catch(() => null) loadDetectionResults().catch(() => null)
}, [restrictedMode, selectedDetectionRunId, detectionClassFilter, detectionMinConfidenceFilter]) }, [restrictedMode, selectedDetectionRunId, detectionClassFilter, detectionMinConfidenceFilter])
useEffect(() => { useEffect(() => {
if (restrictedMode) return
loadSegmentationResults().catch(() => null) loadSegmentationResults().catch(() => null)
}, [restrictedMode, selectedSegmentationRunId, segmentationClassFilter, segmentationMinConfidenceFilter]) }, [restrictedMode, selectedSegmentationRunId, segmentationClassFilter, segmentationMinConfidenceFilter])
} }
+5 -5
View File
@@ -29,21 +29,21 @@ export const detectionApi = {
getYoloPreflight: (params: { tile_manifest_path?: string | null; check_model_load?: boolean | null; model_asset_id?: string | null } = {}): Promise<YoloPreflightResponse> => getYoloPreflight: (params: { tile_manifest_path?: string | null; check_model_load?: boolean | null; model_asset_id?: string | null } = {}): Promise<YoloPreflightResponse> =>
apiGet<YoloPreflightResponse>(`/api/v1/detection/yolo/preflight${queryString(params)}`), apiGet<YoloPreflightResponse>(`/api/v1/detection/yolo/preflight${queryString(params)}`),
run: (payload: DetectionRunRequest): Promise<DetectionRunResponse> => run: (payload: DetectionRunRequest): Promise<DetectionRunResponse> =>
apiPost<DetectionRunResponse>('/api/v1/detection/run', payload), apiPost<DetectionRunResponse>(`/api/v1/detection/run?project_id=${encodeURIComponent(payload.project_id)}`, payload),
listRuns: (params: { project_id?: string | null; dataset_id?: string | null } = {}): Promise<DetectionRunListResponse> => listRuns: (params: { project_id?: string | null; dataset_id?: string | null } = {}): Promise<DetectionRunListResponse> =>
apiGet<DetectionRunListResponse>(`/api/v1/detection/runs${queryString(params)}`), apiGet<DetectionRunListResponse>(`/api/v1/detection/runs${queryString(params)}`),
getRun: (analysisRunId: string): Promise<DetectionRunRead> => getRun: (analysisRunId: string): Promise<DetectionRunRead> =>
apiGet<DetectionRunRead>(`/api/v1/detection/runs/${analysisRunId}`), apiGet<DetectionRunRead>(`/api/v1/detection/runs/${analysisRunId}`),
listDetections: ( listDetections: (
analysisRunId: string, analysisRunId: string,
params: { dataset_id?: string | null; class_name?: string | null; min_confidence?: number | null } = {}, params: { project_id: string; dataset_id?: string | null; class_name?: string | null; min_confidence?: number | null },
): Promise<DetectionListResponse> => ): Promise<DetectionListResponse> =>
apiGet<DetectionListResponse>(`/api/v1/detection/runs/${analysisRunId}/detections${queryString(params)}`), apiGet<DetectionListResponse>(`/api/v1/detection/runs/${analysisRunId}/detections${queryString(params)}`),
getRunGeoJson: ( getRunGeoJson: (
analysisRunId: string, analysisRunId: string,
params: { class_name?: string | null; min_confidence?: number | null } = {}, params: { project_id: string; class_name?: string | null; min_confidence?: number | null },
): Promise<GeoJSON.FeatureCollection> => ): Promise<GeoJSON.FeatureCollection> =>
apiGet<GeoJSON.FeatureCollection>(`/api/v1/detection/runs/${analysisRunId}/geojson${queryString(params)}`), apiGet<GeoJSON.FeatureCollection>(`/api/v1/detection/runs/${analysisRunId}/geojson${queryString(params)}`),
compareWithReference: (analysisRunId: string, payload: DetectionQaRequest): Promise<DetectionQaResult> => compareWithReference: (analysisRunId: string, projectId: string, payload: DetectionQaRequest): Promise<DetectionQaResult> =>
apiPost<DetectionQaResult>(`/api/v1/detection/runs/${analysisRunId}/qa/reference`, payload), apiPost<DetectionQaResult>(`/api/v1/detection/runs/${analysisRunId}/qa/reference?project_id=${encodeURIComponent(projectId)}`, payload),
} }
+12 -9
View File
@@ -11,6 +11,7 @@ import type {
export const exportsApi = { export const exportsApi = {
exportGeojson: ( exportGeojson: (
projectId: string,
payload: payload:
| { | {
dataset_id?: string dataset_id?: string
@@ -24,18 +25,20 @@ export const exportsApi = {
| string, | string,
): Promise<ExportCreateResponse> => { ): Promise<ExportCreateResponse> => {
const body = typeof payload === 'string' ? { dataset_id: payload, export_kind: 'dataset' } : payload const body = typeof payload === 'string' ? { dataset_id: payload, export_kind: 'dataset' } : payload
return apiPost<ExportCreateResponse>(`/api/v1/exports/geojson`, body) return apiPost<ExportCreateResponse>(`/api/v1/exports/geojson?project_id=${encodeURIComponent(projectId)}`, body)
}, },
exportProjectMetadata: (projectId: string, name?: string): Promise<ExportCreateResponse> => exportProjectMetadata: (projectId: string, name?: string): Promise<ExportCreateResponse> =>
apiPost<ExportCreateResponse>(`/api/v1/exports/metadata`, { project_id: projectId, name }), apiPost<ExportCreateResponse>(`/api/v1/exports/metadata?project_id=${encodeURIComponent(projectId)}`, { project_id: projectId, name }),
exportProjectReport: (projectId: string, name?: string): Promise<ExportCreateResponse> => exportProjectReport: (projectId: string, name?: string): Promise<ExportCreateResponse> =>
apiPost<ExportCreateResponse>(`/api/v1/exports/report`, { project_id: projectId, name }), apiPost<ExportCreateResponse>(`/api/v1/exports/report?project_id=${encodeURIComponent(projectId)}`, { project_id: projectId, name }),
exportMapResult: (payload: MapResultExportRequest): Promise<ExportCreateResponse> => exportMapResult: (payload: MapResultExportRequest): Promise<ExportCreateResponse> =>
apiPost<ExportCreateResponse>(`/api/v1/exports/map-result`, payload), apiPost<ExportCreateResponse>(`/api/v1/exports/map-result?project_id=${encodeURIComponent(payload.project_id)}`, payload),
listProjectExports: (projectId: string): Promise<ExportListResponse> => listProjectExports: (projectId: string): Promise<ExportListResponse> =>
apiGet<ExportListResponse>(`/api/v1/exports/projects/${projectId}/exports`), apiGet<ExportListResponse>(`/api/v1/exports/projects/${projectId}/exports?project_id=${encodeURIComponent(projectId)}`),
getExport: (exportId: string): Promise<ExportRead> => apiGet<ExportRead>(`/api/v1/exports/${exportId}`), getExport: (projectId: string, exportId: string): Promise<ExportRead> =>
getContent: (exportId: string): Promise<ExportContentResponse> => apiGet<ExportRead>(`/api/v1/exports/${exportId}?project_id=${encodeURIComponent(projectId)}`),
apiGet<ExportContentResponse>(`/api/v1/exports/${exportId}/content`), getContent: (projectId: string, exportId: string): Promise<ExportContentResponse> =>
downloadUrl: (exportId: string): string => apiUrl(`/api/v1/exports/${exportId}/download`), apiGet<ExportContentResponse>(`/api/v1/exports/${exportId}/content?project_id=${encodeURIComponent(projectId)}`),
downloadUrl: (projectId: string, exportId: string): string =>
apiUrl(`/api/v1/exports/${exportId}/download?project_id=${encodeURIComponent(projectId)}`),
} }
+2 -2
View File
@@ -10,8 +10,8 @@ import type {
} from '../../types' } from '../../types'
export const qaApi = { export const qaApi = {
runQa: (payload: QaComparisonRequest): Promise<JobRead> => runQa: (projectId: string, payload: QaComparisonRequest): Promise<JobRead> =>
apiPost<JobRead>('/api/v1/qa/detections-vs-reference', payload), apiPost<JobRead>(`/api/v1/qa/detections-vs-reference?project_id=${encodeURIComponent(projectId)}`, payload),
listQualityChecks: (projectId: string): Promise<QualityCheckListResponse> => listQualityChecks: (projectId: string): Promise<QualityCheckListResponse> =>
apiGet<QualityCheckListResponse>(`/api/v1/projects/${projectId}/quality-checks`), apiGet<QualityCheckListResponse>(`/api/v1/projects/${projectId}/quality-checks`),
getQualityEvidenceGeoJson: (projectId: string, qualityCheckId: string): Promise<QualityEvidenceGeoJsonResponse> => getQualityEvidenceGeoJson: (projectId: string, qualityCheckId: string): Promise<QualityEvidenceGeoJsonResponse> =>
+5 -5
View File
@@ -24,21 +24,21 @@ function queryString(params: Record<string, string | number | null | undefined>)
export const segmentationApi = { export const segmentationApi = {
listModels: (): Promise<SegmentationModelsResponse> => apiGet<SegmentationModelsResponse>('/api/v1/segmentation/models'), listModels: (): Promise<SegmentationModelsResponse> => apiGet<SegmentationModelsResponse>('/api/v1/segmentation/models'),
run: (payload: SegmentationRunRequest): Promise<SegmentationRunResponse> => run: (payload: SegmentationRunRequest): Promise<SegmentationRunResponse> =>
apiPost<SegmentationRunResponse>('/api/v1/segmentation/run', payload), apiPost<SegmentationRunResponse>(`/api/v1/segmentation/run?project_id=${encodeURIComponent(payload.project_id)}`, payload),
listRuns: (params: { project_id?: string | null; dataset_id?: string | null } = {}): Promise<SegmentationRunListResponse> => listRuns: (params: { project_id?: string | null; dataset_id?: string | null } = {}): Promise<SegmentationRunListResponse> =>
apiGet<SegmentationRunListResponse>(`/api/v1/segmentation/runs${queryString(params)}`), apiGet<SegmentationRunListResponse>(`/api/v1/segmentation/runs${queryString(params)}`),
getRun: (analysisRunId: string): Promise<SegmentationRunRead> => getRun: (analysisRunId: string): Promise<SegmentationRunRead> =>
apiGet<SegmentationRunRead>(`/api/v1/segmentation/runs/${analysisRunId}`), apiGet<SegmentationRunRead>(`/api/v1/segmentation/runs/${analysisRunId}`),
listSegmentations: ( listSegmentations: (
analysisRunId: string, analysisRunId: string,
params: { dataset_id?: string | null; class_name?: string | null; min_confidence?: number | null } = {}, params: { project_id: string; dataset_id?: string | null; class_name?: string | null; min_confidence?: number | null },
): Promise<SegmentationListResponse> => ): Promise<SegmentationListResponse> =>
apiGet<SegmentationListResponse>(`/api/v1/segmentation/runs/${analysisRunId}/segmentations${queryString(params)}`), apiGet<SegmentationListResponse>(`/api/v1/segmentation/runs/${analysisRunId}/segmentations${queryString(params)}`),
getRunGeoJson: ( getRunGeoJson: (
analysisRunId: string, analysisRunId: string,
params: { class_name?: string | null; min_confidence?: number | null } = {}, params: { project_id: string; class_name?: string | null; min_confidence?: number | null },
): Promise<GeoJSON.FeatureCollection> => ): Promise<GeoJSON.FeatureCollection> =>
apiGet<GeoJSON.FeatureCollection>(`/api/v1/segmentation/runs/${analysisRunId}/geojson${queryString(params)}`), apiGet<GeoJSON.FeatureCollection>(`/api/v1/segmentation/runs/${analysisRunId}/geojson${queryString(params)}`),
compareWithReference: (analysisRunId: string, payload: SegmentationQaRequest): Promise<SegmentationQaResult> => compareWithReference: (analysisRunId: string, projectId: string, payload: SegmentationQaRequest): Promise<SegmentationQaResult> =>
apiPost<SegmentationQaResult>(`/api/v1/segmentation/runs/${analysisRunId}/qa/reference`, payload), apiPost<SegmentationQaResult>(`/api/v1/segmentation/runs/${analysisRunId}/qa/reference?project_id=${encodeURIComponent(projectId)}`, payload),
} }