diff --git a/CHANGELOG.md b/CHANGELOG.md index f9ec1cfb..ca27817c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,21 @@ # Changelog +## Sprint 234 Full audit closure and workspace lifecycle cleanup (2026-07-17) + +- Added reversible active/archived project lifecycle handling and made active + workspaces the default project-list contract. +- Added a dry-run-first, strict-allowlist operator command for archiving + benchmark, calibration and smoke-test projects while preserving all related + persistence and both canonical operational workspaces. +- Moved overview orchestration, detection model management and pure map helpers + into focused modules without changing API or persistence behavior. +- Reduced end-user noise across Map, Quality, Detection and Segmentation by + using Dutch task language and placing UUIDs, paths, checksums and runtime + diagnostics behind technical disclosures. +- Added widescreen layout guardrails for AI workspaces and regression coverage + for project lifecycle, packaged cleanup and the new component boundaries. + ## Sprint 233 Operational correctness and result completion (2026-07-17) - Fixed the map-first contract so drawn/manual selections use `bbox ∩ Area` diff --git a/backend/README.md b/backend/README.md index 7b1f9150..ef7eee0e 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1680,3 +1680,31 @@ DatasetService/VectorFeatureService, creates new temporal Datasets and retains all previous snapshots. Do not add `--force` to this coordinator; a source refetch remains a separate deliberate recovery action in the lower-level operators. + +## Safe project lifecycle cleanup + +Operational validation, calibration and benchmark runs can create technical +projects. The default project API now returns active workspaces only, while +archived workspaces remain queryable with `GET /api/v1/projects?status=archived`. +Use the packaged cleanup command to archive only the strict technical-name +allowlist: + +```bash +# Dry-run: inspect the number of matches without changing the database. +python scripts/archive_technical_projects.py + +# Apply the exact allowlisted plan. +python scripts/archive_technical_projects.py --apply +``` + +Inside the all-in-one Unraid container: + +```bash +docker exec geointel python /app/scripts/archive_technical_projects.py +docker exec geointel python /app/scripts/archive_technical_projects.py --apply +``` + +The command never deletes projects or related datasets, jobs, analyses, +quality checks and exports. It always preserves `Kempen Regional Workbench` +and `Mol Municipality Workbench`, defaults to dry-run and can print every +matched name with `--show-names`. diff --git a/backend/app/api/routes/projects.py b/backend/app/api/routes/projects.py index e76e508f..3947f824 100644 --- a/backend/app/api/routes/projects.py +++ b/backend/app/api/routes/projects.py @@ -1,5 +1,6 @@ from __future__ import annotations +from typing import Literal from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, status @@ -18,9 +19,16 @@ def list_projects( limit: int = Query(default=50, ge=1, le=200), offset: int = Query(default=0, ge=0), name: str | None = Query(default=None, min_length=1, max_length=255), + project_status: Literal["active", "archived", "all"] = Query(default="active", alias="status"), db: Session = Depends(get_db), ): - projects, total = ProjectService.list_projects(db, limit=limit, offset=offset, name=name) + projects, total = ProjectService.list_projects( + db, + limit=limit, + offset=offset, + name=name, + project_status=project_status, + ) return envelope({"items": [ProjectRead.model_validate(item).model_dump() for item in projects], "total": total, "limit": limit, "offset": offset}) @@ -41,6 +49,8 @@ def get_project(project_id: UUID, db: Session = Depends(get_db)): @router.patch("/{project_id}", response_model=dict) def update_project(project_id: UUID, payload: ProjectUpdate, db: Session = Depends(get_db)): project = ProjectService.update_project(db, project_id, payload) + if not project: + raise HTTPException(status_code=404, detail="Project not found") return envelope(ProjectRead.model_validate(project).model_dump()) diff --git a/backend/app/schemas/project.py b/backend/app/schemas/project.py index 4ec7d082..0469c5f3 100644 --- a/backend/app/schemas/project.py +++ b/backend/app/schemas/project.py @@ -1,6 +1,7 @@ from __future__ import annotations from datetime import datetime +from typing import Literal from uuid import UUID from pydantic import BaseModel @@ -16,6 +17,7 @@ class ProjectUpdate(BaseModel): name: str | None = None description: str | None = None region: str | None = None + status: Literal["active", "archived"] | None = None class ProjectRead(BaseModel): diff --git a/backend/app/services/project_service.py b/backend/app/services/project_service.py index 4090da47..57829541 100644 --- a/backend/app/services/project_service.py +++ b/backend/app/services/project_service.py @@ -1,6 +1,7 @@ from __future__ import annotations import uuid +from typing import Literal from sqlalchemy.orm import Session @@ -16,8 +17,11 @@ class ProjectService: limit: int = 50, offset: int = 0, name: str | None = None, + project_status: Literal["active", "archived", "all"] = "active", ) -> tuple[list[ProjectRead], int]: query = db.query(Project).filter(Project.status != "deleted") + if project_status != "all": + query = query.filter(Project.status == project_status) if name: query = query.filter(Project.name == name.strip()) query = query.order_by(Project.created_at.desc()) diff --git a/backend/tests/test_sprint100_segmentation_manifest_handoff.py b/backend/tests/test_sprint100_segmentation_manifest_handoff.py index 9256662f..0a4d6eb1 100644 --- a/backend/tests/test_sprint100_segmentation_manifest_handoff.py +++ b/backend/tests/test_sprint100_segmentation_manifest_handoff.py @@ -22,7 +22,7 @@ def test_raster_tile_manifest_can_handoff_to_segmentation_lab() -> None: assert "setSegmentationTileManifestPath(manifestPath)" in app assert "setSelectedSegmentationDatasetId(selectedDataset.id)" in app assert "onUseTileManifestForSegmentation" in detail_panel - assert "Use in Segmentation Lab" in raster_controls + assert "Gebruik voor segmentatie" in raster_controls assert "disabled={!latestRasterTileManifestPath}" in raster_controls - assert "Tile manifest" in segmentation_lab - assert "Raster tile manifest path" in segmentation_lab + assert "Beeldtegelmanifest" in segmentation_lab + assert "Beeldtegelmanifest" in segmentation_lab diff --git a/backend/tests/test_sprint103_ai_lab_run_readiness.py b/backend/tests/test_sprint103_ai_lab_run_readiness.py index 4c82459c..cc60134d 100644 --- a/backend/tests/test_sprint103_ai_lab_run_readiness.py +++ b/backend/tests/test_sprint103_ai_lab_run_readiness.py @@ -15,7 +15,7 @@ def test_detection_lab_exposes_run_readiness_contract() -> None: assert "detectionRequiresTileManifest = selectedDetectionModelId === 'yolo-configured'" in lab assert "detectionTileManifestPath.trim().length > 0" in lab assert "detectionRunReady" in lab - assert 'aria-label="Detection run readiness"' in lab + assert 'aria-label="Startklaar voor gebouwdetectie"' in lab assert "Wat is nog nodig?" in lab assert "Luchtbeeld" in lab assert "Analysemodel" in lab @@ -33,12 +33,12 @@ def test_segmentation_lab_exposes_run_readiness_contract() -> None: assert "segmentationRunReady" in lab assert "selectedSegmentationModelConfigured" in lab assert "selectedSegmentationModelLimitation" in lab - assert 'aria-label="Segmentation run readiness"' in lab - assert "Run readiness" in lab - assert "Raster dataset" in lab - assert "Model availability" in lab - assert "Tile manifest" in lab - assert "Ready to submit" in lab + assert 'aria-label="Startklaar voor segmentatie"' in lab + assert "Wat is nog nodig?" in lab + assert "Rasterbestand" in lab + assert "Analysemodel" in lab + assert "Beeldtegels" in lab + assert "Klaar om te starten" in lab def test_ai_lab_run_readiness_css_contract() -> None: diff --git a/backend/tests/test_sprint104_ai_lab_action_guardrails.py b/backend/tests/test_sprint104_ai_lab_action_guardrails.py index 92130b45..e8c37664 100644 --- a/backend/tests/test_sprint104_ai_lab_action_guardrails.py +++ b/backend/tests/test_sprint104_ai_lab_action_guardrails.py @@ -27,8 +27,8 @@ def test_segmentation_lab_distinguishes_configured_model_from_ui_runnable_action assert "segmentationModelUiRunnable" in lab assert "selectedSegmentationModelId !== 'fixture-segmenter'" in lab assert "segmentationRunBlockedReason" in lab - assert "Fixture segmenter is explicit test/demo-only" in lab - assert "Run action" in lab + assert "Het fixturemodel is alleen bedoeld voor expliciete tests" in lab + assert "Analyse" in lab assert "disabled={runningSegmentation || !segmentationRunReady}" in lab diff --git a/backend/tests/test_sprint105_map_feature_extract.py b/backend/tests/test_sprint105_map_feature_extract.py index f83e34c3..8bd0e230 100644 --- a/backend/tests/test_sprint105_map_feature_extract.py +++ b/backend/tests/test_sprint105_map_feature_extract.py @@ -11,13 +11,13 @@ def test_map_workspace_exposes_feature_extract_actions() -> None: encoding="utf-8" ) - assert "Selection & extract" in map_workspace + assert "Selectie en extractie" in map_workspace assert "downloadSelectedMapFeature" in map_workspace assert "copySelectedMapFeatureProperties" in map_workspace assert "selected-feature.geojson" in map_workspace - assert "Download selected GeoJSON" in map_workspace - assert "Copy selected properties" in map_workspace - assert "Clear selection" in map_workspace + assert "Geselecteerde GeoJSON downloaden" in map_workspace + assert "Eigenschappen kopiëren" in map_workspace + assert "Selectie wissen" in map_workspace assert "featureGeometrySummary" in map_workspace assert "featureExtractionEntries" in map_workspace diff --git a/backend/tests/test_sprint106_map_bbox_extract.py b/backend/tests/test_sprint106_map_bbox_extract.py index 25c8a4db..488c8fc3 100644 --- a/backend/tests/test_sprint106_map_bbox_extract.py +++ b/backend/tests/test_sprint106_map_bbox_extract.py @@ -268,9 +268,9 @@ def test_frontend_exposes_map_bbox_selection_contracts() -> None: assert "selectVectorFeatures" in api_client assert "Area selection" in map_workspace - assert "Start map bbox" in map_workspace - assert "Run area extract" in map_workspace - assert "Download area GeoJSON" in map_workspace + assert "Teken rechthoek" in map_workspace + assert "Objecten in gebied ophalen" in map_workspace + assert "Gebiedsdownload bewaren" in map_workspace assert "bboxSelectionMode" in geomap assert "selection-bbox" in geomap assert "selection-result" in geomap diff --git a/backend/tests/test_sprint107_map_selection_export.py b/backend/tests/test_sprint107_map_selection_export.py index 4771d406..9bfb15fd 100644 --- a/backend/tests/test_sprint107_map_selection_export.py +++ b/backend/tests/test_sprint107_map_selection_export.py @@ -258,7 +258,7 @@ def test_frontend_exposes_map_selection_export_action() -> None: assert "area_id?: string" in exports_api assert "exportMapSelectionGeoJson" in export_hook assert "vector_selection" in export_hook - assert "Save area export" in map_workspace + assert "Gebiedsdownload bewaren" in map_workspace assert "onExportMapSelection" in map_workspace assert "selectionExportError" in map_workspace assert "onExportMapSelection={exportMapSelectionGeoJson}" in app diff --git a/backend/tests/test_sprint108_map_selection_derived_dataset.py b/backend/tests/test_sprint108_map_selection_derived_dataset.py index bfbec9da..21f4ab78 100644 --- a/backend/tests/test_sprint108_map_selection_derived_dataset.py +++ b/backend/tests/test_sprint108_map_selection_derived_dataset.py @@ -1,4 +1,4 @@ -from __future__ import annotations +from __future__ import annotations import json from pathlib import Path @@ -220,5 +220,5 @@ def test_frontend_exposes_map_selection_derive_action() -> None: assert "VectorSelectionDeriveRequest" in types assert "deriveVectorSelection" in datasets_api assert "deriveMapSelectionDataset" in app - assert "Save as dataset" in map_workspace + assert "Als resultaatlaag bewaren" in map_workspace assert "selectionDatasetError" in map_workspace diff --git a/backend/tests/test_sprint109_map_selection_qa_shortcut.py b/backend/tests/test_sprint109_map_selection_qa_shortcut.py index d9bda168..22a17513 100644 --- a/backend/tests/test_sprint109_map_selection_qa_shortcut.py +++ b/backend/tests/test_sprint109_map_selection_qa_shortcut.py @@ -1,4 +1,4 @@ -from pathlib import Path +from pathlib import Path ROOT = Path(__file__).resolve().parents[2] @@ -19,7 +19,7 @@ def test_map_selection_qa_shortcut_uses_existing_qa_workflow_contract() -> None: assert "useMapSelectionQa" in app assert "mapQaReferenceDatasets={referenceDatasets}" in app assert "onRunMapSelectionQa={runMapSelectionQa}" in app - assert "Run QA on saved dataset" in map_workspace + assert "Bewaarde laag controleren" in map_workspace assert "mapSelectionQaError" in map_workspace assert "mapSelectionQaResult" in map_workspace diff --git a/backend/tests/test_sprint110_map_qa_evidence_drilldown.py b/backend/tests/test_sprint110_map_qa_evidence_drilldown.py index 6a2aa774..8c4fa4d4 100644 --- a/backend/tests/test_sprint110_map_qa_evidence_drilldown.py +++ b/backend/tests/test_sprint110_map_qa_evidence_drilldown.py @@ -24,13 +24,13 @@ def test_map_qa_result_exposes_quality_check_evidence_contract(): assert "latestMapSelectionQualityCheckId={latestMapSelectionQualityCheckId}" in app assert "onOpenMapSelectionQualityEvidence={openMapSelectionQualityEvidence}" in app - assert "QA evidence" in workspace - assert "Quality check id" in workspace - assert "Mean IoU" in workspace - assert "False positives" in workspace - assert "False negatives" in workspace - assert "Map selection QA warnings" in workspace - assert "Open QA/QC evidence" in workspace + assert "Kaartbewijs" in workspace + assert "Status bewijs" in workspace + assert "Gemiddelde overlap" in workspace + assert "Onterecht gevonden" in workspace + assert "Gemist" in workspace + assert "Aandachtspunten bij de kwaliteitscontrole" in workspace + assert "Kaartbewijs openen" in workspace def test_map_qa_evidence_keeps_backend_contract_unchanged(): diff --git a/backend/tests/test_sprint111_qa_feature_evidence.py b/backend/tests/test_sprint111_qa_feature_evidence.py index b8241de9..233de111 100644 --- a/backend/tests/test_sprint111_qa_feature_evidence.py +++ b/backend/tests/test_sprint111_qa_feature_evidence.py @@ -18,7 +18,7 @@ def test_qa_feature_evidence_contract_is_documented_and_rendered() -> None: assert field_name in quality_panel assert field_name in api_contracts - assert "Feature-level QA/QC evidence" in quality_panel + assert "Kaartbewijs per object" in quality_panel assert "Overeenkomende object-ID's" in quality_panel assert "ID's van onterecht gevonden objecten" in quality_panel assert "ID's van gemiste objecten" in quality_panel diff --git a/backend/tests/test_sprint116_operational_gis_map_workflow.py b/backend/tests/test_sprint116_operational_gis_map_workflow.py index 7d884482..4bcd8c00 100644 --- a/backend/tests/test_sprint116_operational_gis_map_workflow.py +++ b/backend/tests/test_sprint116_operational_gis_map_workflow.py @@ -24,24 +24,24 @@ def test_map_workspace_can_select_persisted_database_layer_and_run_query() -> No styles = (REPO_ROOT / "frontend/src/styles/app.css").read_text(encoding="utf-8") assert "map-database-layer-select" in map_workspace - assert "Select persisted vector layer" in map_workspace - assert "Basemap usage notice" in map_workspace + assert "Kies een bewaarde vectorlaag" in map_workspace + assert "Gebruik van de kaartondergrond" in map_workspace assert "VITE_MAP_STYLE_URL" in map_workspace - assert "Operational GIS run" in map_workspace - assert "Guided operational GIS workflow" in map_workspace - assert "vector_features" in map_workspace - assert "Run AOI/layer query" in map_workspace - assert "Save result dataset" in map_workspace - assert "Save GeoJSON export" in map_workspace - assert "Run QA/QC" in map_workspace - assert "Run full GIS workflow" in map_workspace + assert "Operationele GIS-controle" in map_workspace + assert "Begeleide operationele GIS-werkstroom" in map_workspace + assert "Bewaarde databankobjecten" in map_workspace + assert "Werkgebied of laag doorzoeken" in map_workspace + assert "Resultaatlaag bewaren" in map_workspace + assert "GeoJSON-download bewaren" in map_workspace + assert "Kwaliteit controleren" in map_workspace + assert "Volledige GIS-werkstroom uitvoeren" in map_workspace assert "runFullGisWorkflow" in map_workspace assert "fullWorkflowStatus" in map_workspace - assert "Query, save, QA and export" in map_workspace + assert "Selecteren, bewaren, controleren en downloaden" in map_workspace assert "fullWorkflowMode" in map_workspace - assert "Create new dataset/export" in map_workspace - assert "Reuse latest saved dataset for QA" in map_workspace - assert "Reusing latest saved dataset for QA/QC" in map_workspace + assert "Nieuwe resultaatlaag en download maken" in map_workspace + assert "Laatste resultaatlaag opnieuw controleren" in map_workspace + assert "Het laatste bewaarde resultaat is opnieuw gebruikt en gecontroleerd." in map_workspace assert "latestSelectionDataset" in map_workspace assert "selectedMapDatasetId=" in app_shell assert ".basemap-policy-notice" in styles diff --git a/backend/tests/test_sprint118_yolo_preflight_ui.py b/backend/tests/test_sprint118_yolo_preflight_ui.py index 268f2e8d..bb0163b8 100644 --- a/backend/tests/test_sprint118_yolo_preflight_ui.py +++ b/backend/tests/test_sprint118_yolo_preflight_ui.py @@ -5,15 +5,18 @@ ROOT = Path(__file__).resolve().parents[2] def test_detection_lab_surfaces_yolo_runtime_preflight() -> None: - lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text( - encoding="utf-8" + lab = "\n".join( + ( + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(encoding="utf-8"), + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionModelManagement.tsx").read_text(encoding="utf-8"), + ) ) hook = (ROOT / "frontend" / "src" / "hooks" / "useDetectionWorkflow.ts").read_text(encoding="utf-8") api = (ROOT / "frontend" / "src" / "services" / "api" / "detection.ts").read_text(encoding="utf-8") types = (ROOT / "frontend" / "src" / "types.ts").read_text(encoding="utf-8") app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") - assert "YOLO runtime preflight" in lab + assert "Technische YOLO-runtimecontrole" in lab assert "torch_version" in lab assert "ultralytics_version" in lab assert "cuda_available" in lab @@ -26,8 +29,11 @@ def test_detection_lab_surfaces_yolo_runtime_preflight() -> None: def test_detection_lab_surfaces_local_model_asset_selection() -> None: - lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text( - encoding="utf-8" + lab = "\n".join( + ( + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(encoding="utf-8"), + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionModelManagement.tsx").read_text(encoding="utf-8"), + ) ) hook = (ROOT / "frontend" / "src" / "hooks" / "useDetectionWorkflow.ts").read_text(encoding="utf-8") api = (ROOT / "frontend" / "src" / "services" / "api" / "detection.ts").read_text(encoding="utf-8") @@ -44,7 +50,7 @@ def test_detection_lab_surfaces_local_model_asset_selection() -> None: assert "modelAssets" in hook assert "selectedModelAssetId" in hook assert "model_asset_id: selectedModelAssetId || null" in hook - assert "Local model assets" in lab + assert "Lokaal modelbestand" in lab assert "onSelectModelAsset" in lab assert "modelAssets={modelAssets}" in app assert "Officiële referentiebronnen" in provider_panel diff --git a/backend/tests/test_sprint122_model_asset_activation_guardrails.py b/backend/tests/test_sprint122_model_asset_activation_guardrails.py index 548e4806..28884bb5 100644 --- a/backend/tests/test_sprint122_model_asset_activation_guardrails.py +++ b/backend/tests/test_sprint122_model_asset_activation_guardrails.py @@ -16,21 +16,27 @@ def test_detection_workflow_selects_only_the_active_runtime_model_asset_automati def test_detection_lab_explains_explicit_model_asset_and_threshold_selection() -> None: - lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text( - encoding="utf-8" + lab = "\n".join( + ( + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(encoding="utf-8"), + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionModelManagement.tsx").read_text(encoding="utf-8"), + ) ) assert "Lokaal modelbestand" in lab assert "GeoIntel kiest automatisch het actieve lokale model" in lab - assert "Gevalideerde profielen" in lab + assert "Gevalideerde YOLO-profielen" in lab assert "DETECTION_OPERATOR_PROFILES" in lab - assert "kandidaat · extra controle vereist" in lab + assert "kandidaat, extra controle vereist" in lab assert "will_download_models" in lab def test_detection_run_readiness_requires_explicit_asset_when_local_assets_exist() -> None: - lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text( - encoding="utf-8" + lab = "\n".join( + ( + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(encoding="utf-8"), + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionModelManagement.tsx").read_text(encoding="utf-8"), + ) ) assert "detectionHasExplicitModelAsset" in lab diff --git a/backend/tests/test_sprint123_raster_detection_handoff_operational.py b/backend/tests/test_sprint123_raster_detection_handoff_operational.py index f327ab22..45d1c8d7 100644 --- a/backend/tests/test_sprint123_raster_detection_handoff_operational.py +++ b/backend/tests/test_sprint123_raster_detection_handoff_operational.py @@ -21,17 +21,20 @@ def test_raster_controls_show_manifest_details_and_ai_handoff_action() -> None: ) assert "latestRasterTileManifest" in controls - assert "Tile count" in controls - assert "Tile size" in controls + assert "Aantal tegels" in controls + assert "Tegelgrootte" in controls assert "Overlap" in controls - assert "Use manifest in Detection Lab" in controls - assert "Use manifest in Segmentation Lab" in controls + assert "Gebruik voor gebouwdetectie" in controls + assert "Gebruik voor segmentatie" in controls def test_detection_handoff_opens_ai_lab_preflights_manifest_and_keeps_asset_explicit() -> None: app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") - lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text( - encoding="utf-8" + lab = "\n".join( + ( + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(encoding="utf-8"), + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionModelManagement.tsx").read_text(encoding="utf-8"), + ) ) assert "setDetectionTileManifestPath(manifestPath)" in app @@ -41,6 +44,6 @@ def test_detection_handoff_opens_ai_lab_preflights_manifest_and_keeps_asset_expl assert "loadYoloPreflight(manifestPath).catch(() => null)" in app assert "setSelectedModelAssetId(" not in app[app.index("const useRasterTileManifestForDetection"):app.index("const {", app.index("const useRasterTileManifestForDetection"))] assert "Gekoppelde beeldtegels" in lab - assert "Tile manifest validation" in lab - assert "Tile count" in lab - assert "will_run_inference" in lab + assert "Gekoppelde beeldtegels" in lab + assert "Aantal beeldtegels" in lab + assert "yoloPreflight.tile_count" in lab diff --git a/backend/tests/test_sprint133_detection_threshold_calibration_ux.py b/backend/tests/test_sprint133_detection_threshold_calibration_ux.py index f0a3e8c4..68b0ea5b 100644 --- a/backend/tests/test_sprint133_detection_threshold_calibration_ux.py +++ b/backend/tests/test_sprint133_detection_threshold_calibration_ux.py @@ -22,7 +22,7 @@ def test_detection_lab_exposes_persisted_threshold_calibration_comparison() -> N assert "Minste foutieve meldingen" in source assert "Drempel" in source assert "Precisie" in source - assert "Recall" in source + assert "Herkenningsgraad" in source assert "F1" in source assert "Fout positief" in source assert "Fout negatief" in source diff --git a/backend/tests/test_sprint134_guided_detection_calibration_runner.py b/backend/tests/test_sprint134_guided_detection_calibration_runner.py index 1169bb77..9ae2d43a 100644 --- a/backend/tests/test_sprint134_guided_detection_calibration_runner.py +++ b/backend/tests/test_sprint134_guided_detection_calibration_runner.py @@ -33,11 +33,11 @@ def test_detection_lab_has_guided_threshold_calibration_runner() -> None: assert "Configured YOLO calibration requires a tile manifest" in hook_source assert "Select a local model asset before calibration" in hook_source - assert "Guided calibration runner" in lab_source - assert "This runs real configured YOLO jobs" in lab_source - assert "Threshold set" in lab_source - assert "Run calibration sweep" in lab_source - assert "Calibration run progress" in lab_source + assert "Modelkalibratie voor beheerders" in lab_source + assert "Voert het lokale model en een kwaliteitscontrole uit" in lab_source + assert "Zekerheidsdrempels" in lab_source + assert "Drempels vergelijken" in lab_source + assert "Voortgang modelkalibratie" in lab_source assert "detectionCalibrationRows.map" in lab_source assert "runningDetectionCalibration" in lab_source assert "detectionCalibrationError" in lab_source diff --git a/backend/tests/test_sprint135_calibration_evidence_handoff.py b/backend/tests/test_sprint135_calibration_evidence_handoff.py index 83983536..98e3251e 100644 --- a/backend/tests/test_sprint135_calibration_evidence_handoff.py +++ b/backend/tests/test_sprint135_calibration_evidence_handoff.py @@ -14,7 +14,7 @@ def test_guided_calibration_rows_link_to_existing_qa_evidence_map() -> None: todo_source = todo.read_text(encoding="utf-8") assert "onOpenCalibrationEvidence" in lab_source - assert "Open evidence map" in lab_source + assert "Toon kaartbewijs" in lab_source assert "disabled={!row.quality_check_id || row.status !== 'success'}" in lab_source assert "onOpenCalibrationEvidence(row.quality_check_id)" in lab_source assert "Evidence" in lab_source diff --git a/backend/tests/test_sprint136_calibration_summary_export_ui.py b/backend/tests/test_sprint136_calibration_summary_export_ui.py index bfaae507..13199fe7 100644 --- a/backend/tests/test_sprint136_calibration_summary_export_ui.py +++ b/backend/tests/test_sprint136_calibration_summary_export_ui.py @@ -16,7 +16,7 @@ def test_guided_calibration_runner_exports_review_summary() -> None: assert "downloadJsonFile('detection-calibration-summary.json'" in lab_source assert "evidence_geojson_url" in lab_source assert "/api/v1/projects/${projectId}/quality-checks/${row.quality_check_id}/evidence/geojson" in lab_source - assert "Download calibration summary" in lab_source + assert "Samenvatting downloaden" in lab_source assert "disabled={detectionCalibrationRows.length === 0 || !selectedProjectId}" in lab_source assert "calibration_thresholds" in lab_source assert "quality_check_ids" in lab_source diff --git a/backend/tests/test_sprint155_detection_operator_profiles.py b/backend/tests/test_sprint155_detection_operator_profiles.py index c19a6fc3..476a0cb6 100644 --- a/backend/tests/test_sprint155_detection_operator_profiles.py +++ b/backend/tests/test_sprint155_detection_operator_profiles.py @@ -31,15 +31,18 @@ def test_detection_operator_profiles_define_explicit_yolo_candidates_and_promote def test_detection_lab_surfaces_profiles_as_deliberate_operator_actions() -> None: - lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text( - encoding="utf-8" + lab = "\n".join( + ( + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(encoding="utf-8"), + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionModelManagement.tsx").read_text(encoding="utf-8"), + ) ) assert "DETECTION_OPERATOR_PROFILES" in lab - assert "Gevalideerde profielen" in lab + assert "Gevalideerde YOLO-profielen" in lab assert "profile.displayName" in lab assert "profile.confidenceThreshold" in lab - assert "kandidaat · extra controle vereist" in lab + assert "kandidaat, extra controle vereist" in lab assert "standaardprofiel" in lab assert "Profiel gebruiken" in lab assert "onApplyOperatorProfile(profile)" in lab diff --git a/backend/tests/test_sprint175_detection_review_hardening.py b/backend/tests/test_sprint175_detection_review_hardening.py index ebc89be8..6e6d958d 100644 --- a/backend/tests/test_sprint175_detection_review_hardening.py +++ b/backend/tests/test_sprint175_detection_review_hardening.py @@ -99,7 +99,7 @@ def test_detection_results_table_uses_bounded_local_pagination() -> None: assert "DETECTION_PAGE_SIZE_OPTIONS" in lab assert "visibleDetectionItems" in lab assert "detectionItems.slice" in lab - assert "Detection result pagination" in lab + assert "Paginering van gevonden objecten" in lab assert "Vorige resultatenpagina" in lab assert "Volgende resultatenpagina" in lab assert "formatSourceTilePath(detection.source_tile_path)" in lab diff --git a/backend/tests/test_sprint180_premium_workbench.py b/backend/tests/test_sprint180_premium_workbench.py index 0d8be64d..5c97fc99 100644 --- a/backend/tests/test_sprint180_premium_workbench.py +++ b/backend/tests/test_sprint180_premium_workbench.py @@ -60,13 +60,18 @@ def test_map_prioritizes_controls_map_and_collapsed_diagnostics() -> None: def test_ai_workspaces_prioritize_runs_and_collapse_registry_detail() -> None: - detection = read("frontend/src/components/detection/DetectionLab.tsx") + detection = "\n".join( + ( + read("frontend/src/components/detection/DetectionLab.tsx"), + read("frontend/src/components/detection/DetectionModelManagement.tsx"), + ) + ) segmentation = read("frontend/src/components/segmentation/SegmentationLab.tsx") css = read("frontend/src/styles/premium.css") - assert '
' in detection - assert '
' in detection - assert '
' in segmentation + assert '
' in detection + assert '
' in detection + assert '
' in segmentation assert ".ai-lab-shell > .lab-block {\n order: 2;" in css assert ".ai-lab-shell > .ai-lab-model-surface {\n order: 7;" in css assert "details.ai-lab-model-surface > summary" in css diff --git a/backend/tests/test_sprint183_map_layer_source_mode.py b/backend/tests/test_sprint183_map_layer_source_mode.py index 3264aa1f..295211a3 100644 --- a/backend/tests/test_sprint183_map_layer_source_mode.py +++ b/backend/tests/test_sprint183_map_layer_source_mode.py @@ -11,7 +11,7 @@ def test_map_source_mode_keeps_database_layers_distinct_from_analysis_results() assert "const [mapContentMode, setMapContentMode]" in app assert "mapContentMode === 'analysis' && analysisMapLayerAvailable" in app assert "setMapContentMode('dataset')" in app - assert 'aria-label="Map content source"' in workspace + assert 'aria-label="Bron van de kaartinhoud"' in workspace assert "onSetMapContentMode('dataset')" in workspace assert "onSetMapContentMode('analysis')" in workspace assert "disabled={!analysisLayerAvailable}" in workspace diff --git a/backend/tests/test_sprint189_kempen_scope.py b/backend/tests/test_sprint189_kempen_scope.py index dbfd8c2b..4cd4c6a2 100644 --- a/backend/tests/test_sprint189_kempen_scope.py +++ b/backend/tests/test_sprint189_kempen_scope.py @@ -121,7 +121,12 @@ def test_scope_api_pagination_respects_canonical_limit() -> None: def test_kempen_scope_operator_is_packaged_and_exposed_in_map_flow() -> None: dockerfile = (ROOT / "deploy/unraid/Dockerfile.all-in-one").read_text(encoding="utf-8") readiness = (ROOT / "scripts/run_readiness_check.sh").read_text(encoding="utf-8") - workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8") + workspace = "\n".join( + ( + (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8"), + (ROOT / "frontend/src/components/map/mapWorkspaceUtils.ts").read_text(encoding="utf-8"), + ) + ) app = (ROOT / "frontend/src/App.tsx").read_text(encoding="utf-8") assert "COPY scripts/geographic_scopes.py" in dockerfile diff --git a/backend/tests/test_sprint19_map_workbench.py b/backend/tests/test_sprint19_map_workbench.py index 062e4189..67bd8a11 100644 --- a/backend/tests/test_sprint19_map_workbench.py +++ b/backend/tests/test_sprint19_map_workbench.py @@ -28,7 +28,7 @@ def test_app_wires_map_workbench_component() -> None: assert "selectedMapFeature" in app assert " None: assert "areaFeatureCollection" in app assert "areaFeatureCollection={areaFeatureCollection}" in app assert "areaData={areaFeatureCollection}" in map_workspace - assert "AOI" in map_workspace + assert "Werkgebied" in map_workspace assert "area-fill" in geomap assert "area-line" in geomap assert "onSelectMapArea" in area_panel diff --git a/backend/tests/test_sprint221_source_freshness_audit.py b/backend/tests/test_sprint221_source_freshness_audit.py index d5386083..caff5ed8 100644 --- a/backend/tests/test_sprint221_source_freshness_audit.py +++ b/backend/tests/test_sprint221_source_freshness_audit.py @@ -231,7 +231,7 @@ def test_source_freshness_operator_and_ui_contract_are_read_only() -> None: script = (root / "scripts" / "audit_source_freshness.py").read_text(encoding="utf-8") dockerfile = (root / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8") readiness = (root / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8") - app = (root / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + app = (root / "frontend" / "src" / "components" / "overview" / "OverviewWorkspace.tsx").read_text(encoding="utf-8") api = (root / "frontend" / "src" / "services" / "api" / "datasets.ts").read_text(encoding="utf-8") assert "Request(endpoint" in script diff --git a/backend/tests/test_sprint22_workbench_status_strip.py b/backend/tests/test_sprint22_workbench_status_strip.py index 2617abbb..99fd8a65 100644 --- a/backend/tests/test_sprint22_workbench_status_strip.py +++ b/backend/tests/test_sprint22_workbench_status_strip.py @@ -8,12 +8,13 @@ ROOT = Path(__file__).resolve().parents[2] def test_frontend_wires_v1_workbench_status_strip() -> None: app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + overview = (ROOT / "frontend" / "src" / "components" / "overview" / "OverviewWorkspace.tsx").read_text(encoding="utf-8") component = (ROOT / "frontend" / "src" / "components" / "WorkbenchStatusStrip.tsx").read_text(encoding="utf-8") css = (ROOT / "frontend" / "src" / "styles" / "app.css").read_text(encoding="utf-8") - assert "WorkbenchStatusStrip" in app - assert "selectedProject={selectedProject}" in app - assert "qualityChecks={qualityChecks}" in app + assert "WorkbenchStatusStrip" in overview + assert "selectedProject={selectedProject}" in overview + assert "qualityChecks={qualityChecks}" in overview assert "activeLayerFeatureCount={mapFeatureCount}" in app assert "selectedAreaHasGeometry={Boolean(areaFeatureCollection)}" in app assert "Platformstatus" in component diff --git a/backend/tests/test_sprint233_operational_completion.py b/backend/tests/test_sprint233_operational_completion.py index 1dbf6a0f..5958333d 100644 --- a/backend/tests/test_sprint233_operational_completion.py +++ b/backend/tests/test_sprint233_operational_completion.py @@ -248,8 +248,8 @@ def test_project_list_supports_exact_canonical_workspace_lookup(monkeypatch) -> project_id = uuid4() captured: dict = {} - def fake_list(_db, *, limit, offset, name): - captured.update(limit=limit, offset=offset, name=name) + def fake_list(_db, *, limit, offset, name, project_status): + captured.update(limit=limit, offset=offset, name=name, project_status=project_status) return [ ProjectRead( id=project_id, @@ -271,6 +271,7 @@ def test_project_list_supports_exact_canonical_workspace_lookup(monkeypatch) -> "limit": 1, "offset": 0, "name": "Kempen Regional Workbench", + "project_status": "active", } diff --git a/backend/tests/test_sprint234_project_lifecycle_cleanup.py b/backend/tests/test_sprint234_project_lifecycle_cleanup.py new file mode 100644 index 00000000..e5c2c5f1 --- /dev/null +++ b/backend/tests/test_sprint234_project_lifecycle_cleanup.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +import importlib.util +from pathlib import Path +import sys +from types import SimpleNamespace +from uuid import uuid4 + +import pytest +from fastapi import HTTPException +from fastapi.testclient import TestClient + +from app.api.routes import projects as project_routes +from app.main import app +from app.schemas.project import ProjectRead, ProjectUpdate +from app.services.project_service import ProjectService + + +ROOT = Path(__file__).resolve().parents[2] + + +def load_cleanup_module(): + script = ROOT / "scripts" / "archive_technical_projects.py" + spec = importlib.util.spec_from_file_location("archive_technical_projects_test", script) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def test_project_list_defaults_to_active_and_can_request_archived(monkeypatch) -> None: + captured: list[str] = [] + + def fake_list(_db, *, limit, offset, name, project_status): + del limit, offset, name + captured.append(project_status) + return [ + ProjectRead( + id=uuid4(), + name=f"{project_status} project", + region="Kempen", + status=project_status if project_status != "all" else "active", + ) + ], 1 + + monkeypatch.setattr(ProjectService, "list_projects", fake_list) + client = TestClient(app) + + assert client.get("/api/v1/projects").status_code == 200 + assert client.get("/api/v1/projects", params={"status": "archived"}).status_code == 200 + assert captured == ["active", "archived"] + + +def test_project_update_schema_allows_only_active_or_archived() -> None: + from pydantic import ValidationError + + from app.schemas.project import ProjectUpdate + + assert ProjectUpdate(status="archived").status == "archived" + try: + ProjectUpdate(status="deleted") + except ValidationError: + pass + else: + raise AssertionError("ProjectUpdate must not expose deleted as an ordinary lifecycle state") + + +def test_project_update_returns_404_when_project_is_missing(monkeypatch) -> None: + monkeypatch.setattr(ProjectService, "update_project", lambda *_args, **_kwargs: None) + + with pytest.raises(HTTPException) as exc_info: + project_routes.update_project(uuid4(), ProjectUpdate(status="archived"), db=SimpleNamespace()) + + assert exc_info.value.status_code == 404 + + +def test_cleanup_allowlist_preserves_real_workspaces() -> None: + module = load_cleanup_module() + + assert module.is_technical_project_name("GeoIntel Detection Quality Matrix 42") + assert module.is_technical_project_name("GeoIntel hard-negative Mol 20260709") + assert module.is_technical_project_name("GeoIntel Detection Calibration 0.15 20260709T090018Z") + assert module.is_technical_project_name("GeoIntel Real Data Validation 20260707T000620Z") + assert module.is_technical_project_name("GeoIntel Operational YOLO Geel Smoke 20260711T133656Z") + assert module.is_technical_project_name("GeoIntel Demo - Building QA") + assert not module.is_technical_project_name("Kempen Regional Workbench") + assert not module.is_technical_project_name("Mol Municipality Workbench") + assert not module.is_technical_project_name("Vrij project van een gebruiker") + + +def test_cleanup_plan_selects_active_allowlisted_projects_only() -> None: + module = load_cleanup_module() + rows = [ + SimpleNamespace( + id=uuid4(), + name="GeoIntel Detection Quality Matrix 1", + status="active", + ), + SimpleNamespace( + id=uuid4(), + name="Kempen Regional Workbench", + status="active", + ), + ] + + class Query: + def filter(self, *_args): + return self + + def order_by(self, *_args): + return self + + def all(self): + return rows + + class Session: + def query(self, _model): + return Query() + + plan = module.build_archive_plan(Session()) + + assert plan.count == 1 + assert plan.names == ("GeoIntel Detection Quality Matrix 1",) + + +def test_cleanup_script_is_packaged_and_readiness_checked() -> None: + dockerfile = (ROOT / "deploy/unraid/Dockerfile.all-in-one").read_text(encoding="utf-8") + readiness = (ROOT / "scripts/run_readiness_check.sh").read_text(encoding="utf-8") + + assert "COPY scripts/archive_technical_projects.py" in dockerfile + assert "py_compile scripts/archive_technical_projects.py" in readiness + + +def test_frontend_lifecycle_and_component_boundaries_are_wired() -> None: + app_source = (ROOT / "frontend/src/App.tsx").read_text(encoding="utf-8") + project_panel = (ROOT / "frontend/src/components/project/ProjectPanel.tsx").read_text(encoding="utf-8") + detection_lab = (ROOT / "frontend/src/components/detection/DetectionLab.tsx").read_text(encoding="utf-8") + map_workspace = (ROOT / "frontend/src/components/map/MapWorkspace.tsx").read_text(encoding="utf-8") + premium_css = (ROOT / "frontend/src/styles/premium.css").read_text(encoding="utf-8") + + assert "OverviewWorkspace" in app_source + assert "onArchiveProject={archiveProject}" in app_source + assert "DetectionModelManagement" in detection_lab + assert "from './mapWorkspaceUtils'" in map_workspace + assert "Werkruimte archiveren" in project_panel + assert "PROTECTED_PROJECT_NAMES" in project_panel + assert ".technical-inline-details" in premium_css + assert ".workspace-grid-ai" in premium_css diff --git a/backend/tests/test_sprint29_dataset_components.py b/backend/tests/test_sprint29_dataset_components.py index 50a9f9c9..9f84b96d 100644 --- a/backend/tests/test_sprint29_dataset_components.py +++ b/backend/tests/test_sprint29_dataset_components.py @@ -19,8 +19,8 @@ def test_app_uses_dataset_presentational_components() -> None: assert "from '../datasets/DatasetDetailPanel'" in inspector assert "" in inspector assert "
Raster operations" not in app - assert "

Vector operations

" not in app + assert "

Rasterbewerkingen

" not in app + assert "

Vectorbewerkingen

" not in app def test_dataset_panel_owns_upload_and_list_markup() -> None: @@ -46,9 +46,9 @@ def test_dataset_detail_panel_composes_raster_and_vector_controls() -> None: assert " None: assert "GeoMap" in map_workspace assert "map-toolbar" in map_workspace assert "Area" in map_workspace - assert "AOI" in map_workspace - assert "Active layer" in map_workspace - assert "Feature inspector" in map_workspace + assert "Werkgebied" in map_workspace + assert "Actieve kaartlaag" in map_workspace + assert "Objectinspectie" in map_workspace assert "onFeatureSelect={onSelectMapFeature}" in map_workspace assert "fetch(" not in map_workspace assert "api" not in map_workspace.lower() diff --git a/backend/tests/test_sprint50_workspace_usability_polish.py b/backend/tests/test_sprint50_workspace_usability_polish.py index a5611eda..edf4c7d9 100644 --- a/backend/tests/test_sprint50_workspace_usability_polish.py +++ b/backend/tests/test_sprint50_workspace_usability_polish.py @@ -31,8 +31,11 @@ def test_map_and_ai_workspaces_use_task_blocks_not_raw_stacks() -> None: map_workspace = (ROOT / "frontend" / "src" / "components" / "map" / "MapWorkspace.tsx").read_text( encoding="utf-8" ) - detection_lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text( - encoding="utf-8" + detection_lab = "\n".join( + ( + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(encoding="utf-8"), + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionModelManagement.tsx").read_text(encoding="utf-8"), + ) ) segmentation_lab = ( ROOT / "frontend" / "src" / "components" / "segmentation" / "SegmentationLab.tsx" @@ -41,7 +44,7 @@ def test_map_and_ai_workspaces_use_task_blocks_not_raw_stacks() -> None: assert "map-toolbar" in map_workspace assert "layer-control-card" in map_workspace - assert "Spatial review" in map_workspace + assert "Kwaliteit controleren" in map_workspace assert "model-list" in detection_lab assert "lab-block" in detection_lab assert "lab-form-grid" in detection_lab diff --git a/backend/tests/test_sprint52_workbench_inspector_tabs.py b/backend/tests/test_sprint52_workbench_inspector_tabs.py index e31dbae9..9410ace9 100644 --- a/backend/tests/test_sprint52_workbench_inspector_tabs.py +++ b/backend/tests/test_sprint52_workbench_inspector_tabs.py @@ -27,10 +27,10 @@ def test_workbench_inspector_exposes_context_dataset_quality_and_ai_tabs() -> No assert 'data-testid="workbench-inspector-panel"' in inspector assert "data-testid={`inspector-tab-${tab.key}`}" in inspector assert "" in inspector - assert "Latest QA/QC" in inspector - assert "Latest export" in inspector - assert "Detection run" in inspector - assert "Segmentation run" in inspector + assert "Laatste kwaliteitscontrole" in inspector + assert "Laatste download" in inspector + assert "Gebouwdetectie" in inspector + assert "Segmentatie" in inspector assert "fetch(" not in inspector diff --git a/backend/tests/test_sprint53_selection_ergonomics.py b/backend/tests/test_sprint53_selection_ergonomics.py index 60cecdf1..4569adc3 100644 --- a/backend/tests/test_sprint53_selection_ergonomics.py +++ b/backend/tests/test_sprint53_selection_ergonomics.py @@ -63,9 +63,9 @@ def test_inspector_exposes_navigation_actions_without_api_calls() -> None: assert "onOpenQualityWorkspace" in inspector assert "onOpenExportsWorkspace" in inspector assert "onOpenAiWorkspace" in inspector - assert "Data catalog" in inspector - assert "Map layer" in inspector - assert "Open QA/QC" in inspector - assert "Open exports" in inspector - assert "Open AI Labs" in inspector + assert "Gegevenscatalogus" in inspector + assert "Kaartlaag" in inspector + assert "Kwaliteit openen" in inspector + assert "Downloads openen" in inspector + assert "Beeldanalyse openen" in inspector assert "fetch(" not in inspector diff --git a/backend/tests/test_sprint63_map_overlay_ergonomics.py b/backend/tests/test_sprint63_map_overlay_ergonomics.py index 3df34a28..702b9adb 100644 --- a/backend/tests/test_sprint63_map_overlay_ergonomics.py +++ b/backend/tests/test_sprint63_map_overlay_ergonomics.py @@ -28,5 +28,5 @@ def test_map_workspace_has_clear_empty_result_layer_guidance() -> None: encoding="utf-8" ) - assert "No active vector or result layer" in map_workspace - assert "Open a dataset, detection run, segmentation run or change result to draw it here." in map_workspace + assert "Geen actieve vector- of resultaatlaag" in map_workspace + assert "Open een databron, beeldanalyse, segmentatie of veranderingsresultaat om het hier te tekenen." in map_workspace diff --git a/backend/tests/test_sprint67_map_empty_state_quick_actions.py b/backend/tests/test_sprint67_map_empty_state_quick_actions.py index 54bd8d6f..94ae6442 100644 --- a/backend/tests/test_sprint67_map_empty_state_quick_actions.py +++ b/backend/tests/test_sprint67_map_empty_state_quick_actions.py @@ -12,9 +12,9 @@ def test_map_empty_state_surfaces_ready_vector_dataset_actions() -> None: assert "availableMapDatasets" in map_workspace assert "onOpenDatasetInMap" in map_workspace assert "map-empty-action-grid" in map_workspace - assert "Open a ready vector dataset" in map_workspace - assert "Open in map" in map_workspace - assert "No ready vector datasets available yet" in map_workspace + assert "Open een beschikbare vectorlaag" in map_workspace + assert "Open op kaart" in map_workspace + assert "Nog geen gebruiksklare vectorlagen beschikbaar" in map_workspace def test_app_passes_available_vector_datasets_to_map_workspace() -> None: diff --git a/backend/tests/test_sprint75_ai_labs_mobile_polish.py b/backend/tests/test_sprint75_ai_labs_mobile_polish.py index 526bf0e3..223bb151 100644 --- a/backend/tests/test_sprint75_ai_labs_mobile_polish.py +++ b/backend/tests/test_sprint75_ai_labs_mobile_polish.py @@ -21,8 +21,11 @@ def test_ai_labs_mobile_density_css_contracts() -> None: def test_detection_and_segmentation_keep_ai_lab_workflow_markup() -> None: - detection_lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text( - encoding="utf-8" + detection_lab = "\n".join( + ( + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(encoding="utf-8"), + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionModelManagement.tsx").read_text(encoding="utf-8"), + ) ) segmentation_lab = ( ROOT / "frontend" / "src" / "components" / "segmentation" / "SegmentationLab.tsx" diff --git a/backend/tests/test_sprint79_accessibility_focus_polish.py b/backend/tests/test_sprint79_accessibility_focus_polish.py index 611563fd..8471af52 100644 --- a/backend/tests/test_sprint79_accessibility_focus_polish.py +++ b/backend/tests/test_sprint79_accessibility_focus_polish.py @@ -21,12 +21,12 @@ def test_global_focus_visible_contracts_are_defined() -> None: def test_primary_navigation_has_keyboard_labels() -> None: app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") - assert 'aria-label={`Open ${item.label} workspace: ${item.description}`}' in app - assert 'aria-label={`Switch to ${item.label} workspace`}' in app - assert 'aria-label="Open data setup workspace"' in app - assert 'aria-label="Inspect map workspace"' in app - assert 'aria-label="Review QA/QC workspace"' in app - assert 'aria-label="Manage exports workspace"' in app + assert 'aria-label={`Open ${item.label}: ${item.description}`}' in app + assert 'aria-label={`Ga naar ${item.label}`}' in app + assert "label: 'Bronnen'" in app + assert "label: 'Kaart'" in app + assert "label: 'Kwaliteit'" in app + assert "label: 'Downloads'" in app def test_inspector_tabs_are_bound_to_tab_panels() -> None: diff --git a/backend/tests/test_sprint80_operation_form_readability.py b/backend/tests/test_sprint80_operation_form_readability.py index 1eb41a6a..823179fc 100644 --- a/backend/tests/test_sprint80_operation_form_readability.py +++ b/backend/tests/test_sprint80_operation_form_readability.py @@ -30,8 +30,8 @@ def test_raster_controls_expose_readable_operation_groups() -> None: assert 'className="dataset-tool-label"' in raster_controls assert 'className="dataset-tool-action-row"' in raster_controls assert 'className="dataset-tool-error"' in raster_controls - assert "Target CRS for the derived raster artifact." in raster_controls - assert "Tile size must be > 0." in raster_controls + assert "Coördinatenstelsel van het afgeleide raster." in raster_controls + assert "De tegelgrootte moet groter zijn dan nul." in raster_controls def test_vector_controls_expose_readable_operation_groups() -> None: @@ -45,5 +45,5 @@ def test_vector_controls_expose_readable_operation_groups() -> None: assert 'className="dataset-tool-field"' in vector_controls assert 'className="dataset-tool-label"' in vector_controls assert 'className="dataset-tool-action-row"' in vector_controls - assert "Clip features to the selected project area." in vector_controls - assert "Intersect with another persisted vector dataset." in vector_controls + assert "Beperk objecten tot het gekozen werkgebied." in vector_controls + assert "Bereken de overlap met een andere bewaarde vectorlaag." in vector_controls diff --git a/backend/tests/test_sprint82_shell_density_polish.py b/backend/tests/test_sprint82_shell_density_polish.py index 1ea17115..1f2063f5 100644 --- a/backend/tests/test_sprint82_shell_density_polish.py +++ b/backend/tests/test_sprint82_shell_density_polish.py @@ -11,7 +11,7 @@ def test_workbench_shell_has_skip_link_and_main_focus_target() -> None: assert 'className="skip-link"' in app assert 'href="#workspace-main"' in app - assert 'nav aria-label="Primary workspaces"' in app + assert 'nav aria-label="Hoofdonderdelen"' in app assert 'id="workspace-main"' in app assert "tabIndex={-1}" in app diff --git a/backend/tests/test_sprint83_workspace_panel_hierarchy.py b/backend/tests/test_sprint83_workspace_panel_hierarchy.py index 023d20de..23c042ab 100644 --- a/backend/tests/test_sprint83_workspace_panel_hierarchy.py +++ b/backend/tests/test_sprint83_workspace_panel_hierarchy.py @@ -7,12 +7,12 @@ ROOT = Path(__file__).resolve().parents[2] def test_overview_uses_named_hierarchy_regions() -> None: - app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + app = (ROOT / "frontend" / "src" / "components" / "overview" / "OverviewWorkspace.tsx").read_text(encoding="utf-8") assert 'className="overview-action-copy"' in app assert 'className="quick-action-grid overview-quick-actions"' in app assert 'className="quick-action-button"' in app - assert 'aria-label="Recommended next workbench actions"' in app + assert 'aria-label="Aanbevolen acties"' in app def test_status_strip_has_compact_section_surface_contracts() -> None: diff --git a/backend/tests/test_sprint85_map_workspace_density.py b/backend/tests/test_sprint85_map_workspace_density.py index 7fc08ad3..7e1c5d11 100644 --- a/backend/tests/test_sprint85_map_workspace_density.py +++ b/backend/tests/test_sprint85_map_workspace_density.py @@ -13,9 +13,9 @@ def test_map_workspace_exposes_structured_surfaces() -> None: assert 'className="map-workspace-shell"' in map_workspace assert 'className="map-context-summary"' in map_workspace - assert 'aria-label="Map layer status"' in map_workspace + assert 'aria-label="Status van de kaartlagen"' in map_workspace assert 'className="map-control-surface"' in map_workspace - assert 'aria-label="Map workspace controls"' in map_workspace + assert 'aria-label="Bediening van de kaartwerkruimte"' in map_workspace assert 'className="map-frame-surface"' in map_workspace assert 'className="map-inspection-surface"' in map_workspace @@ -26,7 +26,7 @@ def test_map_workspace_summary_uses_current_layer_and_selection_state() -> None: ) assert "const selectedMapArea = areas.find" in map_workspace - assert "selectedMapArea?.name ?? 'No area selected'" in map_workspace + assert "selectedMapArea?.name ?? 'Geen gebied geselecteerd'" in map_workspace assert "mapLayerLabel" in map_workspace assert "mapLayerSourceLabel" in map_workspace assert "mapFeatureCount" in map_workspace diff --git a/backend/tests/test_sprint86_quality_workspace_density.py b/backend/tests/test_sprint86_quality_workspace_density.py index 7fff1450..925802f3 100644 --- a/backend/tests/test_sprint86_quality_workspace_density.py +++ b/backend/tests/test_sprint86_quality_workspace_density.py @@ -14,14 +14,14 @@ def test_quality_results_panel_exposes_structured_surfaces() -> None: assert "'quality-results-panel quality-results-panel-empty' : 'quality-results-panel'" in panel assert 'className="quality-results-shell"' in panel assert 'className="quality-summary-surface"' in panel - assert 'aria-label="QA/QC result summary"' in panel + assert 'aria-label="Samenvatting kwaliteitsresultaten"' in panel assert 'className="quality-evidence-surface"' in panel - assert 'aria-label="QA/QC dataset evidence"' in panel + assert 'aria-label="Databronnen van de kwaliteitscontrole"' in panel assert 'className="quality-control-surface"' in panel - assert 'aria-label="QA/QC refresh and filters"' in panel + assert 'aria-label="Kwaliteitsresultaten vernieuwen en filteren"' in panel assert 'className="quality-result-state-stack"' in panel assert 'className="quality-history-surface"' in panel - assert 'aria-label="QA/QC result history"' in panel + assert 'aria-label="Geschiedenis van kwaliteitsresultaten"' in panel assert 'className="quality-advanced-disclosure"' in panel diff --git a/backend/tests/test_sprint87_change_detection_density.py b/backend/tests/test_sprint87_change_detection_density.py index 99442e9c..daf1f436 100644 --- a/backend/tests/test_sprint87_change_detection_density.py +++ b/backend/tests/test_sprint87_change_detection_density.py @@ -14,10 +14,10 @@ def test_change_detection_panel_exposes_structured_surfaces() -> None: assert 'className="panel change-detection-shell"' in panel assert 'className="panel-header change-detection-heading"' in panel assert 'className="change-detection-input-surface"' in panel - assert 'aria-label="Change detection input controls"' in panel + assert 'aria-label="Instellingen voor veranderingsanalyse"' in panel assert 'className="change-detection-state-stack"' in panel assert 'className="change-detection-result-surface"' in panel - assert 'aria-label="Change detection result summary"' in panel + assert 'aria-label="Samenvatting veranderingsanalyse"' in panel assert 'className="change-detection-warning-surface"' in panel @@ -31,8 +31,8 @@ def test_change_detection_panel_preserves_existing_compare_controls() -> None: assert "onIouThresholdChange" in panel assert "onIncludeUnchangedChange" in panel assert "onRun" in panel - assert "Compare vectors" in panel - assert "Upload at least two vector datasets to compare." in panel + assert "Kaartlagen vergelijken" in panel + assert "Voeg minstens twee vectorlagen toe om periodes te vergelijken." in panel assert "result.added_count" in panel assert "result.removed_count" in panel assert "result.unchanged_count" in panel diff --git a/backend/tests/test_sprint88_ai_lab_density.py b/backend/tests/test_sprint88_ai_lab_density.py index 04439f38..3b2aa766 100644 --- a/backend/tests/test_sprint88_ai_lab_density.py +++ b/backend/tests/test_sprint88_ai_lab_density.py @@ -7,18 +7,21 @@ ROOT = Path(__file__).resolve().parents[2] def test_detection_lab_exposes_structured_surfaces() -> None: - lab = (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text( - encoding="utf-8" + lab = "\n".join( + ( + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionLab.tsx").read_text(encoding="utf-8"), + (ROOT / "frontend" / "src" / "components" / "detection" / "DetectionModelManagement.tsx").read_text(encoding="utf-8"), + ) ) assert 'className="workspace-panel ai-lab-shell detection-lab-shell"' in lab assert 'className="ai-lab-model-surface"' in lab - assert 'aria-label="Detection model capabilities"' in lab + assert 'aria-label="Technische modelmogelijkheden"' in lab assert 'className="ai-lab-run-surface"' in lab - assert 'aria-label="Detection run controls"' in lab + assert 'aria-label="Gebouwdetectie starten"' in lab assert 'className="ai-lab-state-stack"' in lab assert 'className="ai-lab-results-surface"' in lab - assert 'aria-label="Detection results"' in lab + assert 'aria-label="Resultaten van de beeldanalyse"' in lab assert 'className="ai-lab-qa-surface"' in lab assert 'aria-label="Kwaliteitscontrole gebouwdetectie"' in lab @@ -30,14 +33,14 @@ def test_segmentation_lab_exposes_structured_surfaces() -> None: assert 'className="workspace-panel ai-lab-shell segmentation-lab-shell"' in lab assert 'className="ai-lab-model-surface"' in lab - assert 'aria-label="Segmentation model capabilities"' in lab + assert 'aria-label="Technische informatie over segmentatiemodellen"' in lab assert 'className="ai-lab-run-surface"' in lab - assert 'aria-label="Segmentation run controls"' in lab + assert 'aria-label="Segmentatie starten"' in lab assert 'className="ai-lab-state-stack"' in lab assert 'className="ai-lab-results-surface"' in lab - assert 'aria-label="Segmentation results"' in lab + assert 'aria-label="Segmentatieresultaten"' in lab assert 'className="ai-lab-qa-surface"' in lab - assert 'aria-label="Segmentation QA controls and results"' in lab + assert 'aria-label="Kwaliteitscontrole voor segmentatie"' in lab def test_ai_lab_preserves_existing_detection_and_segmentation_controls() -> None: @@ -59,8 +62,8 @@ def test_ai_lab_preserves_existing_detection_and_segmentation_controls() -> None assert "onLoadResults" in segmentation assert "onRunQa" in segmentation assert "selectedSegmentationModelConfigured" in segmentation - assert "Segmentations loaded: {segmentationItems.length}" in segmentation - assert "Compare segmentations to reference" in segmentation + assert "{segmentationItems.length} vlakken geladen" in segmentation + assert "Vergelijk met referentielaag" in segmentation def test_ai_lab_density_css_contracts() -> None: diff --git a/backend/tests/test_sprint90_workflow_guidance.py b/backend/tests/test_sprint90_workflow_guidance.py index f7a5523b..9d6b7858 100644 --- a/backend/tests/test_sprint90_workflow_guidance.py +++ b/backend/tests/test_sprint90_workflow_guidance.py @@ -7,28 +7,29 @@ ROOT = Path(__file__).resolve().parents[2] def test_overview_exposes_end_to_end_workflow_guidance() -> None: - app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + overview = (ROOT / "frontend" / "src" / "components" / "overview" / "OverviewWorkspace.tsx").read_text(encoding="utf-8") - assert 'aria-label="V1 workflow guidance"' in app - assert 'className="workflow-guidance-panel"' in app - assert 'className="workflow-guidance-steps"' in app - assert "workflowGuidanceSteps.map" in app - assert "Project & AOI" in app - assert "Data" in app - assert "Map" in app - assert "QA / AI" in app - assert "Export" in app + assert 'aria-label="Voortgang van de werkstroom"' in overview + assert 'className="workflow-guidance-panel"' in overview + assert 'className="workflow-guidance-steps"' in overview + assert "steps.map" in overview + assert "Werkruimte en gebied" in overview + assert "Databronnen" in overview + assert "Kaart" in overview + assert "Controle en analyse" in overview + assert "Downloads" in overview def test_workflow_guidance_routes_to_existing_workspaces() -> None: app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + overview = (ROOT / "frontend" / "src" / "components" / "overview" / "OverviewWorkspace.tsx").read_text(encoding="utf-8") - assert "target: 'data'" in app - assert "target: 'map'" in app - assert "target: 'analysis'" in app - assert "target: 'exports'" in app - assert "onOpenAiWorkspace={() => setActiveWorkspace('ai')}" in app - assert "onClick={() => openWorkflowGuidanceStep(step.target)}" in app + assert "target: 'data'" in overview + assert "target: 'map'" in overview + assert "target: 'analysis'" in overview + assert "target: 'exports'" in overview + assert "onOpenWorkspace={openWorkflowGuidanceStep}" in app + assert "onClick={() => onOpenWorkspace(step.target)}" in overview assert "setActiveWorkspace(target)" in app @@ -44,21 +45,23 @@ def test_workflow_guidance_has_responsive_contracts() -> None: def test_workflow_guidance_complete_state_and_map_copy_are_precise() -> None: - app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + overview = (ROOT / "frontend" / "src" / "components" / "overview" / "OverviewWorkspace.tsx").read_text(encoding="utf-8") - assert "workflowGuidanceComplete" in app - assert "Ready for handoff" in app - assert "layer feature" in app - assert "+ AOI" in app - assert "AOI loaded" in app + assert "workflowComplete" in overview + assert "Klaar om te delen" in overview + assert "objecten op de kaart" in overview + assert "selectedAreaHasGeometry" in overview + assert "Werkgebied ingeladen" in overview def test_workflow_guidance_map_and_export_steps_reuse_dataset_context() -> None: app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + overview = (ROOT / "frontend" / "src" / "components" / "overview" / "OverviewWorkspace.tsx").read_text(encoding="utf-8") assert "openWorkflowGuidanceStep" in app assert "target === 'map'" in app assert "openDatasetInMap(availableMapDatasets[0])" in app assert "target === 'exports'" in app assert "openDatasetExport(availableMapDatasets[0])" in app - assert "onClick={() => openWorkflowGuidanceStep(step.target)}" in app + assert "onOpenWorkspace={openWorkflowGuidanceStep}" in app + assert "onClick={() => onOpenWorkspace(step.target)}" in overview diff --git a/backend/tests/test_sprint94_quality_drilldown.py b/backend/tests/test_sprint94_quality_drilldown.py index d4a435d7..fc32b1fb 100644 --- a/backend/tests/test_sprint94_quality_drilldown.py +++ b/backend/tests/test_sprint94_quality_drilldown.py @@ -11,7 +11,7 @@ def test_quality_results_panel_exposes_selected_check_drilldown() -> None: assert "selectedQualityCheckId" in panel assert "selectedQualityCheck" in panel - assert "QA/QC evidence drilldown" in panel + assert "Details van het kaartbewijs" in panel assert "Geselecteerde controle" in panel assert "Te controleren laag" in panel assert "Referentielaag" in panel diff --git a/backend/tests/test_sprint95_raster_pipeline_hardening.py b/backend/tests/test_sprint95_raster_pipeline_hardening.py index ad28bd60..602d72e9 100644 --- a/backend/tests/test_sprint95_raster_pipeline_hardening.py +++ b/backend/tests/test_sprint95_raster_pipeline_hardening.py @@ -11,13 +11,13 @@ def test_raster_controls_expose_pipeline_readiness_and_handoff() -> None: assert "rasterReadinessItems" in raster_controls assert "rasterGuardrailItems" in raster_controls - assert "Raster pipeline readiness" in raster_controls - assert "Metadata profile" in raster_controls - assert "CRS readiness" in raster_controls - assert "Preview artifact" in raster_controls - assert "Tile manifest handoff" in raster_controls - assert "Clip AOI" in raster_controls - assert "Processing guardrails" in raster_controls + assert "Gereedheid van rasterverwerking" in raster_controls + assert "Bestandsprofiel" in raster_controls + assert "Coördinatenstelsel" in raster_controls + assert "Voorbeeldweergave" in raster_controls + assert "Laatste beeldtegelmanifest" in raster_controls + assert "Werkgebied voor begrenzing" in raster_controls + assert "Veiligheidscontroles" in raster_controls assert "selectedRasterMetadata?.crs" in raster_controls assert "rasterPreview ?" in raster_controls assert "isRasterTileInputValid" in raster_controls diff --git a/backend/tests/test_sprint96_useful_default_context.py b/backend/tests/test_sprint96_useful_default_context.py index e4bf54f2..3fccd079 100644 --- a/backend/tests/test_sprint96_useful_default_context.py +++ b/backend/tests/test_sprint96_useful_default_context.py @@ -27,6 +27,6 @@ def test_ai_labs_explain_missing_raster_input_before_disabled_runs() -> None: assert 'aria-label="Luchtbeeld toevoegen"' in detection_lab assert "onUploadRaster" in detection_lab assert "rasterDatasets.length === 0" in detection_lab - assert "No raster datasets available for segmentation." in segmentation_lab - assert "Upload or select a raster dataset in Data before running segmentation." in segmentation_lab + assert "Geen rasterbestand beschikbaar voor segmentatie." in segmentation_lab + assert "Voeg eerst een rasterbestand toe onder Bronnen." in segmentation_lab assert "rasterDatasets.length === 0" in segmentation_lab diff --git a/backend/tests/test_sprint99_raster_ui_handoff.py b/backend/tests/test_sprint99_raster_ui_handoff.py index fa99e86f..1b0dea4f 100644 --- a/backend/tests/test_sprint99_raster_ui_handoff.py +++ b/backend/tests/test_sprint99_raster_ui_handoff.py @@ -20,6 +20,6 @@ def test_raster_tile_manifest_is_visible_and_can_handoff_to_detection_lab() -> N assert "setSelectedDetectionModelId('yolo-configured')" in app assert "latestRasterTileManifestPath" in detail_panel assert "onUseTileManifestForDetection" in detail_panel - assert "Latest tile manifest" in raster_controls - assert "Use in Detection Lab" in raster_controls + assert "Laatste beeldtegelmanifest" in raster_controls + assert "Gebruik voor gebouwdetectie" in raster_controls assert "disabled={!latestRasterTileManifestPath}" in raster_controls diff --git a/deploy/unraid/Dockerfile.all-in-one b/deploy/unraid/Dockerfile.all-in-one index 29b6836d..ade4667f 100644 --- a/deploy/unraid/Dockerfile.all-in-one +++ b/deploy/unraid/Dockerfile.all-in-one @@ -126,6 +126,7 @@ COPY scripts/build_detection_model_promotion_report.py /app/scripts/build_detect COPY scripts/build_mol_operational_benchmark_report.py /app/scripts/build_mol_operational_benchmark_report.py COPY scripts/run_split_background_promotion_workflow.sh /app/scripts/run_split_background_promotion_workflow.sh COPY scripts/activate_promoted_yolo_candidate.py /app/scripts/activate_promoted_yolo_candidate.py +COPY scripts/archive_technical_projects.py /app/scripts/archive_technical_projects.py COPY deploy/unraid/nginx-all-in-one.conf /etc/nginx/conf.d/default.conf COPY deploy/unraid/all-in-one-start.sh /usr/local/bin/geointel-all-in-one-start COPY --from=frontend-build /frontend/dist/ /usr/share/nginx/html/ diff --git a/docs/API_CONTRACTS.md b/docs/API_CONTRACTS.md index 731be852..c03f7f3d 100644 --- a/docs/API_CONTRACTS.md +++ b/docs/API_CONTRACTS.md @@ -90,12 +90,15 @@ Returns enabled feature flags and tool availability. ### GET `/api/v1/projects` -Returns active projects with `limit`/`offset` pagination. Optional exact -`name` filtering supports stable lookup of a canonical operational workspace -without depending on its position among newer operator or benchmark projects: +Returns active projects by default with `limit`/`offset` pagination. The +optional `status` query accepts `active`, `archived` or `all`; deleted projects +are never returned. Optional exact `name` filtering supports stable lookup of +a canonical operational workspace without depending on its position among +newer operator or benchmark projects: ```text GET /api/v1/projects?name=Kempen%20Regional%20Workbench&limit=1 +GET /api/v1/projects?status=archived&limit=50 ``` ### POST `/api/v1/projects` @@ -118,11 +121,16 @@ Returns one project with summary counts. ### PATCH `/api/v1/projects/{project_id}` -Updates name/description/region. +Updates name, description, region or the ordinary lifecycle status. The status +can only be `active` or `archived`. Archiving keeps datasets, jobs, analyses, +quality checks and exports intact while removing the workspace from the +default active-project list. ### DELETE `/api/v1/projects/{project_id}` -Soft-delete in V1 preferred. Hard-delete only if storage cleanup is also implemented. +Marks the project as deleted. This route does not remove storage artifacts or +related persistence and is not the ordinary workspace-cleanup path. Operators +and the UI use `PATCH` with `status: "archived"` for reversible cleanup. ## Areas diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 085190fb..deaee021 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -9963,3 +9963,30 @@ Final live acceptance: - Browser acceptance passed at 3440x1440 and 390x844 without horizontal page overflow. Workspace navigation reset the real scrolling main container and the browser console contained no warning or error. + +## Sprint 234 - Full audit closure and workspace lifecycle cleanup (2026-07-17) + +Implemented: +- Closed the remaining P1 project-pollution finding with reversible + `active`/`archived` lifecycle filtering. The default API and frontend load + active workspaces only; archived workspaces and all dependent persistence + remain available. +- Added `scripts/archive_technical_projects.py`. It is dry-run-first, uses a + strict technical-name allowlist and always preserves the canonical Kempen + and Mol workspaces. The all-in-one image packages it and readiness compiles + it. +- Added an explicit archive action for a selected non-canonical workspace. + The UI explains that data and results are retained and protects the two + canonical workspaces. +- Extracted Overview orchestration, Detection model management and pure Map + helpers from the largest frontend components. Shared state and API clients + remain unchanged. +- Reworded remaining visible operator terminology in the Map, Quality, + Detection, Segmentation and dataset controls. Technical UUIDs, file paths, + hashes and raw runtime states remain available only under labelled details. +- Added ultrawide AI-workspace constraints so controls use the available width + without creating multiple narrow nested columns. + +Validation evidence is recorded after the complete compile, pytest, typecheck, +build, readiness, migration, deployment, live cleanup and browser acceptance +chain has completed. diff --git a/docs/TODO.md b/docs/TODO.md index 016c56f8..25c10f86 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -35,6 +35,14 @@ geen open productroadmap meer. hoogte-, overstromings- of thematische beleidsrasters. - [x] Draai de volledige applicatie in de beheersbare Unraid-container op poort 1202 met PostGIS, backend, frontend, Ollama-koppeling en modelconfiguratie. +- [x] Archiveer allowlisted benchmark-, kalibratie- en rooktestwerkruimtes + om de actieve projectlijst beheersbaar te houden, zonder gegevens of + resultaten te verwijderen. +- [x] Verberg technische identifiers, modeldetails en interne statussen achter + expliciete technische details en gebruik uniforme Nederlandse taaktaal in + kaart-, kwaliteits-, detectie- en segmentatiewerkstromen. +- [x] Begrens de grootste frontendverantwoordelijkheden met afzonderlijke + overzichts-, modelbeheer- en kaarthelpermodules zonder gedrag te wijzigen. Bewuste, niet-blokkerende grenzen: diff --git a/frontend/README.md b/frontend/README.md index 5483e0cf..29e21e13 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -98,6 +98,27 @@ The workbench uses a task-based shell instead of a single long panel stack. `App The premium V1 presentation layer lives in `src/styles/premium.css`. It groups navigation by Workspace, Analyze and Deliver, removes the permanent inspector column, gives desktop/ultrawide workspaces stable readable widths and switches narrow screens to full-width content with a horizontally scrollable navigation rail. API calls and workflow state remain owned by the existing hooks. +## Current component boundaries + +`App.tsx` remains the shared workspace orchestrator, while focused surfaces +and pure map helpers are kept outside it: + +- `components/overview/OverviewWorkspace.tsx` owns status, source freshness, + workflow progress and overview actions. +- `components/detection/DetectionLab.tsx` owns the end-user detection flow; + `components/detection/DetectionModelManagement.tsx` contains the collapsed + operator-only model registry, profiles, assets and runtime diagnostics. +- `components/map/MapWorkspace.tsx` owns the map workflow; + `components/map/mapWorkspaceUtils.ts` owns pure bbox, metric, download and + display helpers. +- `components/project/ProjectPanel.tsx` owns explicit workspace management, + including reversible archiving for non-canonical workspaces. + +Normal screens use Dutch task language and friendly labels. UUIDs, file paths, +checksums, raw model states and job terminology stay behind labelled technical +disclosures. Archiving a workspace does not remove its data and the two +canonical regional workspaces cannot be archived through the UI. + Data and Map are organized around the core daily workflow. Data shows Project, AOI and Dataset columns together on normal desktop widths, bounds populated lists inside their own panels and keeps create/upload forms in explicit disclosures. Map keeps the layer/AOI command surface and MapLibre frame first, then exposes provenance, BBox controls and raw feature inspection only when requested. Existing selection, export and QA actions are unchanged. Wide and ultrawide screens keep a readable sidebar and centered work area, expand the MapLibre review frame and use extra horizontal space for Data, Analysis, AI and Export grids. The detail drawer overlays the work area only while open, so it does not permanently consume ultrawide canvas space. diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 56ce3a42..b39c86a4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -10,11 +10,10 @@ import { ExportCenter } from './components/exports/ExportCenter' import { ExportPreview } from './components/exports/ExportPreview' import { WorkbenchInspector } from './components/inspector/WorkbenchInspector' import { MapWorkspace } from './components/map/MapWorkspace' +import { OverviewWorkspace, type WorkspaceKey } from './components/overview/OverviewWorkspace' import { AreaPanel } from './components/project/AreaPanel' import { ProjectPanel } from './components/project/ProjectPanel' import { QualityResultsPanel } from './components/quality/QualityResultsPanel' -import { WorkbenchStatusStrip } from './components/WorkbenchStatusStrip' -import { SourceFreshnessPanel } from './components/status/SourceFreshnessPanel' import type { DatasetCreateResponse } from './types' import { ProviderPanel } from './components/providers/ProviderPanel' import { SegmentationLab } from './components/segmentation/SegmentationLab' @@ -43,8 +42,6 @@ function isVectorDatasetType(datasetType: string): boolean { return datasetType === 'vector' || datasetType === 'geojson' } -type WorkspaceKey = 'overview' | 'data' | 'map' | 'assistant' | 'analysis' | 'ai' | 'exports' | 'system' - const workspaceNavItems: Array<{ key: WorkspaceKey; label: string; description: string }> = [ { key: 'overview', label: 'Status', description: 'Beschikbaarheid en aandachtspunten' }, { key: 'data', label: 'Bronnen', description: 'Gebieden en ingeladen gegevens' }, @@ -83,6 +80,7 @@ function App(): JSX.Element { loadingProjects, loadingAreas, loadingDatasets, + archivingProjectId, errorMessage, projectForm, areaForm, @@ -90,6 +88,7 @@ function App(): JSX.Element { loadProjectData, createProject, createArea, + archiveProject, resetProjectData, setSelectedProjectId, setErrorMessage, @@ -586,24 +585,24 @@ function App(): JSX.Element { return 'Detectierun' } if ((datasetMapContent || viewportVectorLayer.enabled) && selectedDataset) { - return `${selectedDataset.dataset_type}-dataset` + return selectedDataset.dataset_type === 'raster' ? 'Rasterdatabron' : 'Vectordatabron' } return 'Geen actieve gegevens- of analyselaag' }, [analysisMapLayerActive, changeDetectionResult?.geojson, datasetMapContent, detectionGeoJson, segmentationGeoJson, selectedDataset, viewportVectorLayer.enabled]) const mapLayerProvenance = useMemo(() => { if (analysisMapLayerActive && changeDetectionResult?.geojson) { - return `source ${changeSourceDatasetId || 'n/a'} -> target ${changeTargetDatasetId || 'n/a'}` + return 'Vergelijking van twee bewaarde kaartlagen' } if (analysisMapLayerActive && segmentationGeoJson) { - return selectedSegmentationRunId ? `analysis run ${selectedSegmentationRunId}` : 'segmentation results loaded' + return selectedSegmentationRunId ? 'Bewaarde segmentatieronde' : 'Segmentatieresultaten geladen' } if (analysisMapLayerActive && detectionGeoJson) { - return selectedDetectionRunId ? `analysis run ${selectedDetectionRunId}` : 'detection results loaded' + return selectedDetectionRunId ? 'Bewaarde detectieronde' : 'Detectieresultaten geladen' } if ((datasetMapContent || viewportVectorLayer.enabled) && selectedDataset) { - return `${selectedDataset.dataset_role ?? 'source'} / ${selectedDataset.source_name ?? selectedDataset.source}` + return `Bewaarde ${selectedDataset.dataset_role === 'reference' ? 'referentielaag' : 'databron'}` } - return 'Open a dataset, detection run, segmentation run or change result to draw it here.' + return 'Open een databron, detectieronde, segmentatieronde of veranderingsresultaat om het hier te tekenen.' }, [ analysisMapLayerActive, changeDetectionResult?.geojson, @@ -658,73 +657,11 @@ function App(): JSX.Element { } setActiveWorkspace(target) } - const hasAnalysisOutput = qualityChecks.length > 0 || Boolean(changeDetectionResult) || detectionItems.length > 0 || segmentationItems.length > 0 - const hasMapContext = mapFeatureCount > 0 || areaFeatureCount > 0 - const workflowGuidanceComplete = Boolean(selectedProjectId) && datasets.length > 0 && hasMapContext && hasAnalysisOutput && exports.length > 0 - const recommendedWorkflowTarget: WorkspaceKey = !selectedProjectId - ? 'data' - : datasets.length === 0 - ? 'data' - : !hasMapContext - ? 'map' - : !hasAnalysisOutput - ? 'analysis' - : exports.length === 0 - ? 'exports' - : 'exports' - const workflowGuidanceSteps: Array<{ - step: string - title: string - detail: string - status: string - ready: boolean - target: WorkspaceKey - }> = [ - { - step: '1', - title: 'Project & AOI', - detail: selectedProjectId ? `${areas.length} AOI record${areas.length === 1 ? '' : 's'} available` : 'Create or load a project context', - status: selectedProjectId ? 'ready' : 'next', - ready: Boolean(selectedProjectId), - target: 'data', - }, - { - step: '2', - title: 'Data', - detail: datasets.length > 0 ? `${datasets.length} dataset${datasets.length === 1 ? '' : 's'} loaded` : 'Upload source and reference datasets', - status: datasets.length > 0 ? 'ready' : 'waiting', - ready: datasets.length > 0, - target: 'data', - }, - { - step: '3', - title: 'Map', - detail: hasMapContext - ? mapFeatureCount > 0 - ? `${mapFeatureCount} layer feature${mapFeatureCount === 1 ? '' : 's'}${areaFeatureCount > 0 ? ' + AOI' : ''}` - : 'AOI loaded' - : 'Inspect AOI and selected layer', - status: hasMapContext ? 'ready' : 'waiting', - ready: hasMapContext, - target: 'map', - }, - { - step: '4', - title: 'QA / AI', - detail: hasAnalysisOutput ? 'QA, change, detection or segmentation output exists' : 'Run checks after data is ready', - status: hasAnalysisOutput ? 'ready' : 'waiting', - ready: hasAnalysisOutput, - target: 'analysis', - }, - { - step: '5', - title: 'Export', - detail: exports.length > 0 ? `${exports.length} export artifact${exports.length === 1 ? '' : 's'} recorded` : 'Package validated project outputs', - status: exports.length > 0 ? 'ready' : 'waiting', - ready: exports.length > 0, - target: 'exports', - }, - ] + const hasAnalysisOutput = + qualityChecks.length > 0 + || Boolean(changeDetectionResult) + || detectionItems.length > 0 + || segmentationItems.length > 0 const projectContextLabel = selectedProject?.name === REGIONAL_WORKSPACE_PROJECT_NAME ? REGIONAL_WORKSPACE_LABEL : selectedProject?.name ?? 'Geen werkruimte' @@ -759,7 +696,7 @@ function App(): JSX.Element {

GeoAI-werkruimte · Mol en de Kempen

-
+
Werkruimte {projectContextLabel} @@ -782,8 +719,8 @@ function App(): JSX.Element { {errorMessage ?

{errorMessage}

: null}
-
: null} -
+
{workspaceNavItems.slice(0, 5).map((item) => ( @@ -852,112 +789,19 @@ function App(): JSX.Element {
{activeWorkspace === 'overview' ? ( -
- - { void sourceFreshness.refresh() }} - catalogReport={sourceFreshness.catalogReport} - catalogLoading={sourceFreshness.catalogLoading} - catalogError={sourceFreshness.catalogError} - onProbeCatalogs={(force) => { void sourceFreshness.probeCatalogs(force) }} - grbRefreshPlan={sourceFreshness.grbRefreshPlan} - grbRefreshPlanLoading={sourceFreshness.grbRefreshPlanLoading} - grbRefreshPlanError={sourceFreshness.grbRefreshPlanError} - /> -
- - Volledige workflowstatus - {workflowGuidanceComplete ? 'voltooid' : 'optionele stappen open'} - -
-
-
-

Workbench flow

-

Project to export path

-
- - {workflowGuidanceComplete - ? 'Ready for handoff' - : `Next: ${workspaceNavItems.find((item) => item.key === recommendedWorkflowTarget)?.label ?? 'Overview'}`} - -
-
- {workflowGuidanceSteps.map((step) => ( - - ))} -
-
-
-
-

Quick access

-

Continue working

-
-
- - - - -
-
-
-
+ ) : null} {activeWorkspace === 'data' ? ( @@ -967,12 +811,14 @@ function App(): JSX.Element { projects={projects} selectedProjectId={selectedProjectId} loadingProjects={loadingProjects} + archivingProjectId={archivingProjectId} projectForm={projectForm} loadingDemoWorkflow={loadingDemoWorkflow} demoWorkflowMessage={demoWorkflowMessage} onCreateProject={createProject} onUpdateProjectForm={setProjectForm} onSelectProject={selectProject} + onArchiveProject={archiveProject} onLoadDemoWorkflow={loadDemoWorkflow} /> @@ -1287,7 +1133,7 @@ function App(): JSX.Element { {inspectorOpen ? ( -