From 6d655a1f1ae4959e738169407ba557f99d910a53 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 17 Jun 2026 07:52:14 +0200 Subject: [PATCH] Extract demo workflow hook --- CHANGELOG.md | 7 ++ .../test_sprint21_demo_workflow_smoke.py | 8 +- ...t_sprint39_frontend_orchestration_hooks.py | 17 ++++ docs/CODEX_EXECUTION_LOG.md | 26 ++++++ docs/TODO.md | 3 +- frontend/README.md | 1 + frontend/src/App.tsx | 62 +++++--------- frontend/src/hooks/useDemoWorkflow.ts | 85 +++++++++++++++++++ 8 files changed, 165 insertions(+), 44 deletions(-) create mode 100644 frontend/src/hooks/useDemoWorkflow.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f73c19d..32a8ce3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ # Changelog +## Sprint 41 Demo workflow hook decomposition (2026-06-17) + +- Moved offline demo workflow orchestration from `App.tsx` into `frontend/src/hooks/useDemoWorkflow.ts`. +- The hook keeps the existing cross-module selection behavior for project, candidate/reference datasets, map AOI, QA/QC, detection, segmentation and export refresh state. +- Extended frontend orchestration regression tests so `demoApi` stays out of `App.tsx`. +- No API contracts, migrations, product features, provider fetching or AI behavior were introduced. + ## Sprint 40 Project workspace hook decomposition (2026-06-17) - Moved project listing/creation, area creation and project-scoped area/dataset loading from `App.tsx` into `frontend/src/hooks/useProjectWorkspace.ts`. diff --git a/backend/tests/test_sprint21_demo_workflow_smoke.py b/backend/tests/test_sprint21_demo_workflow_smoke.py index c4e5f513..9d75946c 100644 --- a/backend/tests/test_sprint21_demo_workflow_smoke.py +++ b/backend/tests/test_sprint21_demo_workflow_smoke.py @@ -22,8 +22,8 @@ def test_demo_workflow_browser_smoke_script_checks_connected_v1_state() -> None: def test_frontend_demo_action_loads_candidate_dataset_details_for_map_layer() -> None: - app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") + demo_hook = (ROOT / "frontend" / "src" / "hooks" / "useDemoWorkflow.ts").read_text(encoding="utf-8") - assert "const candidateDataset = projectData?.datasets.find" in app - assert "dataset.id === result.candidate_dataset_id" in app - assert "await loadDatasetDetails(result.project_id, candidateDataset)" in app + assert "const candidateDataset = projectData?.datasets.find" in demo_hook + assert "dataset.id === result.candidate_dataset_id" in demo_hook + assert "await loadDatasetDetails(result.project_id, candidateDataset)" in demo_hook diff --git a/backend/tests/test_sprint39_frontend_orchestration_hooks.py b/backend/tests/test_sprint39_frontend_orchestration_hooks.py index 3eca2a55..a394086a 100644 --- a/backend/tests/test_sprint39_frontend_orchestration_hooks.py +++ b/backend/tests/test_sprint39_frontend_orchestration_hooks.py @@ -10,20 +10,37 @@ def test_app_uses_shared_orchestration_hooks() -> None: app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8") assert "useProjectWorkspace" in app + assert "useDemoWorkflow" in app assert "useProviderCapabilities" in app assert "useChangeDetectionWorkflow" in app assert "useMapWorkspaceState" in app assert "from './hooks/useProjectWorkspace'" in app + assert "from './hooks/useDemoWorkflow'" in app assert "from './hooks/useProviderCapabilities'" in app assert "from './hooks/useChangeDetectionWorkflow'" in app assert "from './hooks/useMapWorkspaceState'" in app assert "projectsApi" not in app assert "areasApi" not in app assert "datasetsApi" not in app + assert "demoApi" not in app assert "analysisApi" not in app assert "externalApi" not in app +def test_demo_workflow_hook_owns_demo_api_and_cross_module_selection() -> None: + hook = (ROOT / "frontend" / "src" / "hooks" / "useDemoWorkflow.ts").read_text(encoding="utf-8") + + assert "demoApi.seedWorkflow" in hook + assert "loadDemoWorkflow" in hook + assert "setSelectedProjectId(result.project_id)" in hook + assert "setSelectedDatasetId(result.candidate_dataset_id)" in hook + assert "setSelectedMapAreaId(result.area_id)" in hook + assert "setQaCandidateDatasetId(result.candidate_dataset_id)" in hook + assert "setDetectionReferenceDatasetId(result.reference_dataset_id)" in hook + assert "setSegmentationReferenceDatasetId(result.reference_dataset_id)" in hook + assert "loadDatasetDetails(result.project_id, candidateDataset)" in hook + + def test_project_workspace_hook_owns_project_area_dataset_loading() -> None: hook = (ROOT / "frontend" / "src" / "hooks" / "useProjectWorkspace.ts").read_text(encoding="utf-8") diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 79d09142..70be9361 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -1,3 +1,29 @@ +## Sprint 41 Demo workflow hook decomposition (2026-06-17) + +Changed: +- Moved offline demo workflow orchestration from `frontend/src/App.tsx` into `frontend/src/hooks/useDemoWorkflow.ts`. +- Preserved the existing backend fixture seed flow and all cross-module selection updates for project, candidate/reference datasets, map AOI, QA/QC, detection, segmentation and exports. +- Extended static frontend orchestration tests so `demoApi.seedWorkflow` is owned by the new hook and not `App.tsx`. +- Updated frontend README, changelog and TODO status. + +Validation: +- `cd backend && python -m pytest tests/test_sprint21_demo_workflow_smoke.py tests/test_sprint39_frontend_orchestration_hooks.py -q` passed: 9 tests. +- `python -m compileall backend/app` passed. +- `cd backend && python -m pytest -W error::DeprecationWarning` passed: 191 tests. +- `bash scripts/run_readiness_check.sh` passed. +- `cd frontend && npm run typecheck` passed. +- `cd frontend && npm run build` passed. +- `cd backend && python -m alembic heads` passed: `202606120900 (head)`. +- `cd backend && python -m alembic upgrade head --sql` passed. +- `bash -n scripts/live_migration_smoke.sh` passed. + +Limitations: +- No UX behavior, API contracts, migrations, provider fetching or AI behavior changed. +- `App.tsx` still has a UTF-8 BOM and a stale React import cleanup opportunity; TypeScript build is unaffected. + +Next recommended pass: +- Commit, deploy to Tower and run browser/runtime smoke, then do a final `App.tsx` size/import cleanup pass. + ## Sprint 40 Project workspace hook decomposition (2026-06-17) Changed: diff --git a/docs/TODO.md b/docs/TODO.md index 92828d64..6e39281e 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -53,7 +53,8 @@ This file now starts with the current implementation status. Older preparation/b - [x] Export preview component decomposition and HTML report download-only UX hardening. - [x] Provider, change-detection and map-workspace orchestration hook decomposition. - [x] Project/area/dataset cross-load orchestration hook decomposition. -- [ ] Further demo workflow orchestration decomposition. +- [x] Demo workflow orchestration hook decomposition. +- [ ] Final `App.tsx` import/encoding cleanup and size audit. ## Sprint 8 status diff --git a/frontend/README.md b/frontend/README.md index 9166ce8c..7d833039 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -215,6 +215,7 @@ React + TypeScript + MapLibre foundation for project/area/dataset workflow. - Derived MapLibre workbench state lives in `src/hooks/useMapWorkspaceState.ts`. - Project, area and dataset-list loading lives in `src/hooks/useProjectWorkspace.ts`. - Area fallback selection now lives with the owning workflow hooks: clip-area selection in `useDatasetWorkflow.ts` and map-area selection in `useMapWorkspaceState.ts`. +- Offline demo workflow orchestration lives in `src/hooks/useDemoWorkflow.ts`, because it coordinates project, dataset, map, QA/QC, detection, segmentation and export state after the backend fixture seed. ## Raster dependency visibility diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ebc39fd0..dd03e019 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,5 @@ import { FormEvent, useEffect, useMemo, useState } from 'react' import './styles/app.css' -import { demoApi } from './services/api' import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel' import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel' import { DatasetPanel } from './components/datasets/DatasetPanel' @@ -16,6 +15,7 @@ import type { DatasetCreateResponse } from './types' import { ProviderPanel } from './components/providers/ProviderPanel' import { SegmentationLab } from './components/segmentation/SegmentationLab' import { useChangeDetectionWorkflow } from './hooks/useChangeDetectionWorkflow' +import { useDemoWorkflow } from './hooks/useDemoWorkflow' import { useDetectionWorkflow } from './hooks/useDetectionWorkflow' import { useDatasetWorkflow } from './hooks/useDatasetWorkflow' import { useExportWorkflow } from './hooks/useExportWorkflow' @@ -24,16 +24,12 @@ import { useProviderCapabilities } from './hooks/useProviderCapabilities' import { useProjectWorkspace } from './hooks/useProjectWorkspace' import { useQualityWorkflow } from './hooks/useQualityWorkflow' import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow' -import { formatError } from './lib/formatError' function isVectorDatasetType(datasetType: string): boolean { return datasetType === 'vector' || datasetType === 'geojson' } function App(): JSX.Element { - const [loadingDemoWorkflow, setLoadingDemoWorkflow] = useState(false) - const [demoWorkflowMessage, setDemoWorkflowMessage] = useState(null) - const { projects, selectedProject, @@ -324,6 +320,28 @@ function App(): JSX.Element { datasetContent, selectedDataset, }) + const { + loadingDemoWorkflow, + demoWorkflowMessage, + loadDemoWorkflow, + } = useDemoWorkflow({ + loadProjects, + loadProjectData, + loadDatasetDetails, + loadDetectionRuns, + loadSegmentationRuns, + loadQualityChecks, + loadExports, + setSelectedProjectId, + setSelectedDatasetId, + setSelectedMapAreaId, + setQaCandidateDatasetId, + setQaReferenceDatasetId, + setQaAreaId, + setDetectionReferenceDatasetId, + setSegmentationReferenceDatasetId, + setErrorMessage, + }) useEffect(() => { loadProjects().catch(() => null) @@ -356,40 +374,6 @@ function App(): JSX.Element { loadSegmentationResults().catch(() => null) }, [selectedSegmentationRunId, segmentationClassFilter, segmentationMinConfidenceFilter]) - const loadDemoWorkflow = async () => { - setLoadingDemoWorkflow(true) - setDemoWorkflowMessage(null) - setErrorMessage(null) - try { - const result = await demoApi.seedWorkflow() - setSelectedProjectId(result.project_id) - setSelectedDatasetId(result.candidate_dataset_id) - setSelectedMapAreaId(result.area_id) - setQaCandidateDatasetId(result.candidate_dataset_id) - setQaReferenceDatasetId(result.reference_dataset_id) - setQaAreaId(result.area_id) - setDetectionReferenceDatasetId(result.reference_dataset_id) - setSegmentationReferenceDatasetId(result.reference_dataset_id) - setDemoWorkflowMessage(result.message) - await loadProjects() - const [projectData] = await Promise.all([ - loadProjectData(result.project_id), - loadDetectionRuns(result.project_id), - loadSegmentationRuns(result.project_id), - loadQualityChecks(result.project_id), - loadExports(result.project_id), - ]) - const candidateDataset = projectData?.datasets.find((dataset) => dataset.id === result.candidate_dataset_id) - if (candidateDataset) { - await loadDatasetDetails(result.project_id, candidateDataset) - } - } catch (error) { - setErrorMessage(formatError(error, 'Failed to load demo workflow')) - } finally { - setLoadingDemoWorkflow(false) - } - } - return (
diff --git a/frontend/src/hooks/useDemoWorkflow.ts b/frontend/src/hooks/useDemoWorkflow.ts new file mode 100644 index 00000000..ae1c0e6f --- /dev/null +++ b/frontend/src/hooks/useDemoWorkflow.ts @@ -0,0 +1,85 @@ +import { useState } from 'react' +import { demoApi } from '../services/api' +import type { DatasetCreateResponse, QualityCheckRead } from '../types' +import { formatError } from '../lib/formatError' + +interface DemoWorkflowOptions { + loadProjects: () => Promise + loadProjectData: (projectId: string) => Promise<{ datasets: DatasetCreateResponse[] } | null> + loadDatasetDetails: (projectId: string, dataset: DatasetCreateResponse) => Promise + loadDetectionRuns: (projectId?: string | null) => Promise + loadSegmentationRuns: (projectId?: string | null) => Promise + loadQualityChecks: (projectId?: string | null) => Promise + loadExports: (projectId?: string | null) => Promise + setSelectedProjectId: (projectId: string) => void + setSelectedDatasetId: (datasetId: string | null) => void + setSelectedMapAreaId: (areaId: string) => void + setQaCandidateDatasetId: (datasetId: string) => void + setQaReferenceDatasetId: (datasetId: string) => void + setQaAreaId: (areaId: string) => void + setDetectionReferenceDatasetId: (datasetId: string) => void + setSegmentationReferenceDatasetId: (datasetId: string) => void + setErrorMessage: (message: string | null) => void +} + +export function useDemoWorkflow({ + loadProjects, + loadProjectData, + loadDatasetDetails, + loadDetectionRuns, + loadSegmentationRuns, + loadQualityChecks, + loadExports, + setSelectedProjectId, + setSelectedDatasetId, + setSelectedMapAreaId, + setQaCandidateDatasetId, + setQaReferenceDatasetId, + setQaAreaId, + setDetectionReferenceDatasetId, + setSegmentationReferenceDatasetId, + setErrorMessage, +}: DemoWorkflowOptions) { + const [loadingDemoWorkflow, setLoadingDemoWorkflow] = useState(false) + const [demoWorkflowMessage, setDemoWorkflowMessage] = useState(null) + + const loadDemoWorkflow = async () => { + setLoadingDemoWorkflow(true) + setDemoWorkflowMessage(null) + setErrorMessage(null) + try { + const result = await demoApi.seedWorkflow() + setSelectedProjectId(result.project_id) + setSelectedDatasetId(result.candidate_dataset_id) + setSelectedMapAreaId(result.area_id) + setQaCandidateDatasetId(result.candidate_dataset_id) + setQaReferenceDatasetId(result.reference_dataset_id) + setQaAreaId(result.area_id) + setDetectionReferenceDatasetId(result.reference_dataset_id) + setSegmentationReferenceDatasetId(result.reference_dataset_id) + setDemoWorkflowMessage(result.message) + await loadProjects() + const [projectData] = await Promise.all([ + loadProjectData(result.project_id), + loadDetectionRuns(result.project_id), + loadSegmentationRuns(result.project_id), + loadQualityChecks(result.project_id), + loadExports(result.project_id), + ]) + const candidateDataset = projectData?.datasets.find((dataset) => dataset.id === result.candidate_dataset_id) + if (candidateDataset) { + await loadDatasetDetails(result.project_id, candidateDataset) + } + } catch (error) { + setErrorMessage(formatError(error, 'Failed to load demo workflow')) + } finally { + setLoadingDemoWorkflow(false) + } + } + + return { + loadingDemoWorkflow, + demoWorkflowMessage, + loadDemoWorkflow, + } +}