Expand scoped demo analysis access
This commit is contained in:
+48
-2
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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`
|
||||||
|
|
||||||
|
|||||||
@@ -12217,4 +12217,16 @@ Open:
|
|||||||
- Desktop, tablet en mobiel: documentoverflow `0`; themapaneeloverflow `0`.
|
- Desktop, tablet en mobiel: documentoverflow `0`; themapaneeloverflow `0`.
|
||||||
- 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.
|
||||||
|
|||||||
+13
-4
@@ -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
|
||||||
@@ -1065,4 +1066,12 @@ This file now starts with the current implementation status. Older preparation/b
|
|||||||
- [x] Compacte analysecontextbalk en rustige desktop/tablet/mobiele hiërarchie.
|
- [x] Compacte analysecontextbalk en rustige desktop/tablet/mobiele hiërarchie.
|
||||||
- [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
@@ -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">
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)}`),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> =>
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user