From dfcc11b0d5e2778c05a300feee9d064f9a87f8b5 Mon Sep 17 00:00:00 2001 From: Jens Date: Sat, 1 Aug 2026 16:00:13 +0200 Subject: [PATCH] Expand scoped demo analysis access --- backend/app/main.py | 50 ++++++++++++++- backend/tests/test_auth.py | 15 +++-- docs/API_CONTRACTS.md | 20 +++--- docs/CODEX_EXECUTION_LOG.md | 14 ++++- docs/TODO.md | 17 +++-- frontend/src/App.tsx | 63 +++++++++++-------- .../src/components/detection/DetectionLab.tsx | 18 +++--- frontend/src/hooks/useDemoWorkflow.ts | 14 ++--- frontend/src/hooks/useDetectionWorkflow.ts | 5 +- frontend/src/hooks/useExportWorkflow.ts | 18 +++--- frontend/src/hooks/useMapSelectionQa.ts | 2 +- frontend/src/hooks/useQualityWorkflow.ts | 2 +- frontend/src/hooks/useSegmentationWorkflow.ts | 3 +- .../src/hooks/useWorkbenchBootstrap.test.tsx | 16 ++--- frontend/src/hooks/useWorkbenchBootstrap.ts | 16 ++--- frontend/src/services/api/detection.ts | 10 +-- frontend/src/services/api/exports.ts | 21 ++++--- frontend/src/services/api/qa.ts | 4 +- frontend/src/services/api/segmentation.ts | 10 +-- 19 files changed, 206 insertions(+), 112 deletions(-) diff --git a/backend/app/main.py b/backend/app/main.py index 07b4bd20..952fbf58 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -210,6 +210,12 @@ def create_app() -> FastAPI: guest_safe_read_paths = { f"{settings.api_prefix}/projects", 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 "/" guest_project_read = ( @@ -218,7 +224,21 @@ def create_app() -> FastAPI: ) is_read_request = request.method in {"GET", "HEAD", "OPTIONS"} 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( status_code=403, content=_to_error_payload( @@ -234,6 +254,15 @@ def create_app() -> FastAPI: f"{settings.api_prefix}/demo/workflow", 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 = ( "/vector/select", "/raster/bathymetry/select", @@ -247,9 +276,26 @@ def create_app() -> FastAPI: ) is_guest_safe_post = request.method == "POST" and ( 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 ( 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: diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py index ccd82174..374b3352 100644 --- a/backend/tests/test_auth.py +++ b/backend/tests/test_auth.py @@ -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 -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") demo = DemoWorkflowResponse( 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") mutation = client.post("/api/v1/projects", json={"name": "Not allowed"}) 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( "/api/v1/external/coverage/resolve", 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 other_project.status_code == 403 assert other_project.json()["error"] == "GUEST_PROJECT_SCOPE_REQUIRED" - assert unscoped_read.status_code == 403 - assert unscoped_read.json()["error"] == "GUEST_ROUTE_NOT_AVAILABLE" + assert detection_models.status_code == 200 + 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.json()["error"] == "GUEST_PROJECT_SCOPE_REQUIRED" diff --git a/docs/API_CONTRACTS.md b/docs/API_CONTRACTS.md index d5997a85..f997c8bd 100644 --- a/docs/API_CONTRACTS.md +++ b/docs/API_CONTRACTS.md @@ -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 a shorter signed session with role `guest`, scopes that session to the idempotently seeded demo project and blocks mutating operator routes. Project -listing is filtered to the bound demo project. The frontend exposes only the -map and the already calculated quality evidence. This is deliberately **not** +listing is filtered to the bound demo project. The frontend exposes the same +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 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 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`. -Guest reads are limited to the filtered project list, provider metadata and the -bound project tree. A small, explicit set of `POST` selection/read-analysis -routes remains available because those routes query persisted evidence without -exposing operator administration. Coverage resolution additionally verifies -the `project_id` in the request body against the guest-session scope. +Guest reads are limited to the filtered project list, provider/model metadata, +the bound project tree and project-scoped detection, segmentation and export +results. An explicit set of `POST` selection, assistant, AI/QA and export routes +is available for that bound demo project. Unscoped analysis routes require the +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` diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index d867a96f..0dcc1375 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -12217,4 +12217,16 @@ Open: - Desktop, tablet en mobiel: documentoverflow `0`; themapaneeloverflow `0`. - Mobiele hoofdflow: thema zoeken, kiezen, volledig werkgebied selecteren, expliciet analyseren, resultatenlade openen en sluiten geslaagd. - 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`. \ No newline at end of file +- 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. diff --git a/docs/TODO.md b/docs/TODO.md index b858e39f..79f30278 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -21,9 +21,10 @@ Uitvoeringsbord: `docs/PYTORCH_TRAINING_ROADMAP_BELGIUM.md`. Professionaliseringspass (2026-07-27): - [x] Voeg een expliciete gastknop toe aan de toegangspoort en open daarmee - een korte, projectgebonden, alleen-lezen demowerkruimte. -- [x] Beperk de gastinterface tot kaartverkenning en bestaand kwaliteitsbewijs; - blokkeer operatoracties en toegang tot andere projecten ook server-side. + een korte, projectgebonden demowerkruimte. +- [x] Geef de demo dezelfde kaart-, bron-, assistent-, model-, analyse-, QA- en + 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 workbenchcontext tot één rustigere en professionelere productervaring. - [ ] 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] Uitschuifbare inzichten behouden; analyse blijft uitsluitend expliciet na themakeuze. - [x] 51 frontendtests en productiebuild groen. -- [ ] 19 verouderde broncode-stringtests herijken; meerdere eisen daarin (automatische analyse) conflicteren bewust met de actuele productbeslissing. \ No newline at end of file +- [ ] 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`. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b18bfa70..227afa61 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -90,9 +90,21 @@ const workspaceNavGroups: WorkspaceNavigationGroup[] = [ { label: 'Beheer', keys: ['overview', 'system'] }, ] -const guestWorkspaceKeys = new Set(['map', 'analysis']) +const guestWorkspaceKeys = new Set([ + 'overview', + 'data', + 'map', + 'assistant', + 'analysis', + 'ai', + 'exports', +]) 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 { @@ -589,7 +601,7 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA demoWorkflowMessage, loadDemoWorkflow, } = useDemoWorkflow({ - restrictedMode: isGuest, + restrictedMode: false, loadProjects, loadProjectData, loadDatasetDetails, @@ -618,7 +630,7 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA }, [isGuest, loadDemoWorkflow]) useWorkbenchBootstrap({ - restrictedMode: isGuest, + restrictedMode: false, selectedProjectId, selectedDetectionRunId, detectionClassFilter, @@ -720,7 +732,7 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA } const openWorkflowGuidanceStep = (target: WorkspaceKey) => { if (isGuest && !guestWorkspaceKeys.has(target)) { - setActiveWorkspace(target === 'exports' ? 'analysis' : 'map') + setActiveWorkspace('map') return } if (target === 'map' && availableMapDatasets.length > 0 && !mapFeatureCollection) { @@ -894,9 +906,9 @@ function WorkbenchApp({ username, accessMode, loggingOut, onLogout }: WorkbenchA