Extract workbench bootstrap hook
GeoIntel CI / docs-smoke (push) Has been cancelled
GeoIntel CI / contract-smoke (push) Has been cancelled

This commit is contained in:
Codex
2026-06-17 17:58:16 +02:00
parent 701a70a343
commit 8cf2dca44f
7 changed files with 176 additions and 33 deletions
+7
View File
@@ -7,6 +7,13 @@
# Changelog # Changelog
## Sprint 43 Workbench bootstrap hook decomposition (2026-06-17)
- Moved frontend bootstrap/project/result reload effects from `App.tsx` into `frontend/src/hooks/useWorkbenchBootstrap.ts`.
- `App.tsx` no longer imports or owns `useEffect`; it wires hook state into panels and delegates lifecycle loading to focused hooks.
- Extended orchestration regression tests so bootstrap loading and result refresh effects stay out of `App.tsx`.
- No behavior, API contracts, migrations, provider fetching or AI behavior changed.
## Sprint 42 App entrypoint cleanup (2026-06-17) ## Sprint 42 App entrypoint cleanup (2026-06-17)
- Removed the stale `FormEvent`/`useState` React imports from `frontend/src/App.tsx`. - Removed the stale `FormEvent`/`useState` React imports from `frontend/src/App.tsx`.
@@ -14,11 +14,13 @@ def test_app_uses_shared_orchestration_hooks() -> None:
assert "useProviderCapabilities" in app assert "useProviderCapabilities" in app
assert "useChangeDetectionWorkflow" in app assert "useChangeDetectionWorkflow" in app
assert "useMapWorkspaceState" in app assert "useMapWorkspaceState" in app
assert "useWorkbenchBootstrap" in app
assert "from './hooks/useProjectWorkspace'" in app assert "from './hooks/useProjectWorkspace'" in app
assert "from './hooks/useDemoWorkflow'" in app assert "from './hooks/useDemoWorkflow'" in app
assert "from './hooks/useProviderCapabilities'" in app assert "from './hooks/useProviderCapabilities'" in app
assert "from './hooks/useChangeDetectionWorkflow'" in app assert "from './hooks/useChangeDetectionWorkflow'" in app
assert "from './hooks/useMapWorkspaceState'" in app assert "from './hooks/useMapWorkspaceState'" in app
assert "from './hooks/useWorkbenchBootstrap'" in app
assert "projectsApi" not in app assert "projectsApi" not in app
assert "areasApi" not in app assert "areasApi" not in app
assert "datasetsApi" not in app assert "datasetsApi" not in app
@@ -33,8 +35,9 @@ def test_app_entrypoint_has_clean_encoding_and_react_imports() -> None:
app = app_path.read_text(encoding="utf-8") app = app_path.read_text(encoding="utf-8")
assert not app_bytes.startswith(b"\xef\xbb\xbf") assert not app_bytes.startswith(b"\xef\xbb\xbf")
assert "import { useEffect, useMemo } from 'react'" in app assert "import { useMemo } from 'react'" in app
assert "FormEvent" not in app assert "FormEvent" not in app
assert "useEffect" not in app
assert "useState" not in app assert "useState" not in app
@@ -52,6 +55,24 @@ def test_demo_workflow_hook_owns_demo_api_and_cross_module_selection() -> None:
assert "loadDatasetDetails(result.project_id, candidateDataset)" in hook assert "loadDatasetDetails(result.project_id, candidateDataset)" in hook
def test_workbench_bootstrap_hook_owns_entrypoint_effects() -> None:
app = (ROOT / "frontend" / "src" / "App.tsx").read_text(encoding="utf-8")
hook = (ROOT / "frontend" / "src" / "hooks" / "useWorkbenchBootstrap.ts").read_text(encoding="utf-8")
assert "loadProjects().catch(() => null)" not in app
assert "loadDetectionResults().catch(() => null)" not in app
assert "loadSegmentationResults().catch(() => null)" not in app
assert "loadProjects().catch(() => null)" in hook
assert "loadCapabilities().catch(() => null)" in hook
assert "loadDetectionModels().catch(() => null)" in hook
assert "loadSegmentationModels().catch(() => null)" in hook
assert "resetProjectData()" in hook
assert "resetDatasetForProject()" in hook
assert "loadProjectData(selectedProjectId).catch(() => null)" in hook
assert "loadDetectionResults().catch(() => null)" in hook
assert "loadSegmentationResults().catch(() => null)" in hook
def test_project_workspace_hook_owns_project_area_dataset_loading() -> None: def test_project_workspace_hook_owns_project_area_dataset_loading() -> None:
hook = (ROOT / "frontend" / "src" / "hooks" / "useProjectWorkspace.ts").read_text(encoding="utf-8") hook = (ROOT / "frontend" / "src" / "hooks" / "useProjectWorkspace.ts").read_text(encoding="utf-8")
+30
View File
@@ -1,3 +1,33 @@
## Sprint 43 Workbench bootstrap hook decomposition (2026-06-17)
Changed:
- Moved frontend bootstrap, project-change reload/reset and detection/segmentation result reload effects from `frontend/src/App.tsx` into `frontend/src/hooks/useWorkbenchBootstrap.ts`.
- Kept `App.tsx` as a composition root that wires hook outputs into panels; it no longer imports `useEffect`.
- Extended orchestration tests so lifecycle side effects stay in the bootstrap hook.
- Updated frontend README, changelog and TODO status.
Validation:
- `cd backend && python -m pytest 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: 193 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.
- `App.tsx` size audit after extraction: 621 lines; `useWorkbenchBootstrap.ts`: 81 lines.
Open:
- Tower deployment and runtime smoke should run before considering this pass deployed.
Limitations:
- No UX behavior, API contracts, migrations, provider fetching or AI behavior changed.
- `App.tsx` line count remains high because it explicitly wires many panel props; the remaining size is primarily composition.
Next recommended pass:
- Pick the next V1 stabilization focus: UI browser regression coverage, backend service contract audit, or golden dataset expansion.
## Sprint 42 App entrypoint cleanup (2026-06-17) ## Sprint 42 App entrypoint cleanup (2026-06-17)
Changed: Changed:
+2 -1
View File
@@ -55,7 +55,8 @@ This file now starts with the current implementation status. Older preparation/b
- [x] Project/area/dataset cross-load orchestration hook decomposition. - [x] Project/area/dataset cross-load orchestration hook decomposition.
- [x] Demo workflow orchestration hook decomposition. - [x] Demo workflow orchestration hook decomposition.
- [x] Final `App.tsx` import/encoding cleanup and size audit. - [x] Final `App.tsx` import/encoding cleanup and size audit.
- [ ] Optional final bootstrap-effect extraction if `App.tsx` orchestration needs another size reduction. - [x] Optional final bootstrap-effect extraction.
- [ ] Decide next V1 stabilization focus: UI browser regression coverage, backend service contract audit, or golden dataset expansion.
## Sprint 8 status ## Sprint 8 status
+1
View File
@@ -216,6 +216,7 @@ React + TypeScript + MapLibre foundation for project/area/dataset workflow.
- Project, area and dataset-list loading lives in `src/hooks/useProjectWorkspace.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`. - 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. - 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.
- Workbench bootstrap and reload effects live in `src/hooks/useWorkbenchBootstrap.ts`, keeping `App.tsx` focused on composing hooks into panels.
## Raster dependency visibility ## Raster dependency visibility
+27 -31
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo } from 'react' import { useMemo } from 'react'
import './styles/app.css' import './styles/app.css'
import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel' import { ChangeDetectionPanel } from './components/analysis/ChangeDetectionPanel'
import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel' import { DatasetDetailPanel } from './components/datasets/DatasetDetailPanel'
@@ -24,6 +24,7 @@ import { useProviderCapabilities } from './hooks/useProviderCapabilities'
import { useProjectWorkspace } from './hooks/useProjectWorkspace' import { useProjectWorkspace } from './hooks/useProjectWorkspace'
import { useQualityWorkflow } from './hooks/useQualityWorkflow' import { useQualityWorkflow } from './hooks/useQualityWorkflow'
import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow' import { useSegmentationWorkflow } from './hooks/useSegmentationWorkflow'
import { useWorkbenchBootstrap } from './hooks/useWorkbenchBootstrap'
function isVectorDatasetType(datasetType: string): boolean { function isVectorDatasetType(datasetType: string): boolean {
return datasetType === 'vector' || datasetType === 'geojson' return datasetType === 'vector' || datasetType === 'geojson'
@@ -343,36 +344,31 @@ function App(): JSX.Element {
setErrorMessage, setErrorMessage,
}) })
useEffect(() => { useWorkbenchBootstrap({
loadProjects().catch(() => null) selectedProjectId,
loadCapabilities().catch(() => null) selectedDetectionRunId,
loadDetectionModels().catch(() => null) detectionClassFilter,
loadSegmentationModels().catch(() => null) detectionMinConfidenceFilter,
}, []) selectedSegmentationRunId,
segmentationClassFilter,
useEffect(() => { segmentationMinConfidenceFilter,
if (!selectedProjectId) { loadProjects,
resetProjectData() loadCapabilities,
resetDatasetForProject() loadDetectionModels,
resetDetectionForProject() loadSegmentationModels,
resetSegmentationForProject() loadProjectData,
resetExportsForProject() loadDetectionRuns,
return loadSegmentationRuns,
} loadQualityChecks,
loadProjectData(selectedProjectId).catch(() => null) loadExports,
loadDetectionRuns(selectedProjectId).catch(() => null) loadDetectionResults,
loadSegmentationRuns(selectedProjectId).catch(() => null) loadSegmentationResults,
loadQualityChecks(selectedProjectId).catch(() => null) resetProjectData,
loadExports(selectedProjectId).catch(() => null) resetDatasetForProject,
}, [selectedProjectId]) resetDetectionForProject,
resetSegmentationForProject,
useEffect(() => { resetExportsForProject,
loadDetectionResults().catch(() => null) })
}, [selectedDetectionRunId, detectionClassFilter, detectionMinConfidenceFilter])
useEffect(() => {
loadSegmentationResults().catch(() => null)
}, [selectedSegmentationRunId, segmentationClassFilter, segmentationMinConfidenceFilter])
return ( return (
<div className="app-shell"> <div className="app-shell">
@@ -0,0 +1,87 @@
import { useEffect } from 'react'
type AsyncAction = () => Promise<unknown>
type ProjectAction = (projectId: string) => Promise<unknown>
interface WorkbenchBootstrapOptions {
selectedProjectId: string | null
selectedDetectionRunId: string
detectionClassFilter: string
detectionMinConfidenceFilter: number
selectedSegmentationRunId: string
segmentationClassFilter: string
segmentationMinConfidenceFilter: number
loadProjects: AsyncAction
loadCapabilities: AsyncAction
loadDetectionModels: AsyncAction
loadSegmentationModels: AsyncAction
loadProjectData: ProjectAction
loadDetectionRuns: ProjectAction
loadSegmentationRuns: ProjectAction
loadQualityChecks: ProjectAction
loadExports: ProjectAction
loadDetectionResults: AsyncAction
loadSegmentationResults: AsyncAction
resetProjectData: () => void
resetDatasetForProject: () => void
resetDetectionForProject: () => void
resetSegmentationForProject: () => void
resetExportsForProject: () => void
}
export function useWorkbenchBootstrap({
selectedProjectId,
selectedDetectionRunId,
detectionClassFilter,
detectionMinConfidenceFilter,
selectedSegmentationRunId,
segmentationClassFilter,
segmentationMinConfidenceFilter,
loadProjects,
loadCapabilities,
loadDetectionModels,
loadSegmentationModels,
loadProjectData,
loadDetectionRuns,
loadSegmentationRuns,
loadQualityChecks,
loadExports,
loadDetectionResults,
loadSegmentationResults,
resetProjectData,
resetDatasetForProject,
resetDetectionForProject,
resetSegmentationForProject,
resetExportsForProject,
}: WorkbenchBootstrapOptions): void {
useEffect(() => {
loadProjects().catch(() => null)
loadCapabilities().catch(() => null)
loadDetectionModels().catch(() => null)
loadSegmentationModels().catch(() => null)
}, [])
useEffect(() => {
if (!selectedProjectId) {
resetProjectData()
resetDatasetForProject()
resetDetectionForProject()
resetSegmentationForProject()
resetExportsForProject()
return
}
loadProjectData(selectedProjectId).catch(() => null)
loadDetectionRuns(selectedProjectId).catch(() => null)
loadSegmentationRuns(selectedProjectId).catch(() => null)
loadQualityChecks(selectedProjectId).catch(() => null)
loadExports(selectedProjectId).catch(() => null)
}, [selectedProjectId])
useEffect(() => {
loadDetectionResults().catch(() => null)
}, [selectedDetectionRunId, detectionClassFilter, detectionMinConfidenceFilter])
useEffect(() => {
loadSegmentationResults().catch(() => null)
}, [selectedSegmentationRunId, segmentationClassFilter, segmentationMinConfidenceFilter])
}