From 95a978f01f3b92f9edbe707e334865ed6a01d202 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 17 Jun 2026 18:17:05 +0200 Subject: [PATCH] Polish default demo project selection --- CHANGELOG.md | 8 +++ ...t_sprint39_frontend_orchestration_hooks.py | 6 ++ docs/CODEX_EXECUTION_LOG.md | 30 ++++++++++ frontend/src/hooks/useDemoWorkflow.ts | 4 +- frontend/src/hooks/useProjectWorkspace.ts | 59 +++++++++++++++---- 5 files changed, 93 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 34474cfb..6413a343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ # Changelog +## Sprint 45 Default demo selection polish (2026-06-17) + +- Improved frontend project selection so a cold start prefers a populated demo/workbench project over an empty first project. +- Preserved the current project selection when it still exists and selected newly created projects immediately after creation. +- Updated the demo workflow hook to pass the seeded project as the preferred project during refresh. +- Added static regression coverage for the smarter project selection and demo refresh behavior. +- No API contracts, migrations, provider fetching, AI behavior or product capabilities changed. + ## Sprint 44 Workbench UI polish pass (2026-06-17) - Reworked the frontend workbench styling into a cleaner operational GIS interface with compact panels, modern controls, restrained green/neutral accents and scroll-contained long sections. diff --git a/backend/tests/test_sprint39_frontend_orchestration_hooks.py b/backend/tests/test_sprint39_frontend_orchestration_hooks.py index 510cc0aa..900a90cc 100644 --- a/backend/tests/test_sprint39_frontend_orchestration_hooks.py +++ b/backend/tests/test_sprint39_frontend_orchestration_hooks.py @@ -46,6 +46,7 @@ def test_demo_workflow_hook_owns_demo_api_and_cross_module_selection() -> None: assert "demoApi.seedWorkflow" in hook assert "loadDemoWorkflow" in hook + assert "loadProjects(result.project_id)" in hook assert "setSelectedProjectId(result.project_id)" in hook assert "setSelectedDatasetId(result.candidate_dataset_id)" in hook assert "setSelectedMapAreaId(result.area_id)" in hook @@ -76,8 +77,13 @@ def test_workbench_bootstrap_hook_owns_entrypoint_effects() -> None: def test_project_workspace_hook_owns_project_area_dataset_loading() -> None: hook = (ROOT / "frontend" / "src" / "hooks" / "useProjectWorkspace.ts").read_text(encoding="utf-8") + assert "DEMO_PROJECT_NAME = 'GeoIntel Demo - Building QA'" in hook + assert "pickInitialProjectId" in hook + assert "preferredProjectId" in hook + assert "data.areas.length > 0 && data.datasets.length > 0" in hook assert "projectsApi.list" in hook assert "projectsApi.create" in hook + assert "setSelectedProjectId(createdProject.id)" in hook assert "areasApi.list" in hook assert "areasApi.create" in hook assert "datasetsApi.list" in hook diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 1d5a588c..260c457d 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -1,3 +1,33 @@ +## Sprint 45 Default demo selection polish (2026-06-17) + +Changed: +- Updated `frontend/src/hooks/useProjectWorkspace.ts` so cold-start project selection preserves an existing selected project, honors an explicit preferred project and otherwise prefers a populated demo/workbench project over an empty first project. +- New project creation now selects the newly created project immediately after creation. +- Updated `frontend/src/hooks/useDemoWorkflow.ts` so demo seed refresh passes the seeded project id to `loadProjects`. +- Extended orchestration tests to cover preferred demo selection and project creation selection behavior. + +Validation: +- `cd backend && python -m pytest tests/test_sprint39_frontend_orchestration_hooks.py tests/test_sprint21_demo_workflow_smoke.py -q` passed: 11 tests. +- `python -m compileall backend/app` passed. +- `cd backend && python -m pytest -W error::DeprecationWarning` passed: 194 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. +- Browser check against local Vite preview with live backend passed after async settle: selected project `GeoIntel Demo - Building QA`, status showed `1 area`, `2/2 ready` datasets and `2 checks`. + +Open: +- Tower deployment and live smoke should run before considering this pass deployed. + +Limitations: +- No API contracts, migrations, provider fetching, AI behavior or product capabilities changed. +- The heuristic probes up to eight project candidates on cold start; a future backend list endpoint with area/dataset counts would make this cleaner. + +Next recommended pass: +- Add a lightweight browser regression script for the demo start page once the UI state is stable enough to automate end to end. + ## Sprint 44 Workbench UI polish pass (2026-06-17) Changed: diff --git a/frontend/src/hooks/useDemoWorkflow.ts b/frontend/src/hooks/useDemoWorkflow.ts index ae1c0e6f..c4f3e650 100644 --- a/frontend/src/hooks/useDemoWorkflow.ts +++ b/frontend/src/hooks/useDemoWorkflow.ts @@ -4,7 +4,7 @@ import type { DatasetCreateResponse, QualityCheckRead } from '../types' import { formatError } from '../lib/formatError' interface DemoWorkflowOptions { - loadProjects: () => Promise + loadProjects: (preferredProjectId?: string | null) => Promise loadProjectData: (projectId: string) => Promise<{ datasets: DatasetCreateResponse[] } | null> loadDatasetDetails: (projectId: string, dataset: DatasetCreateResponse) => Promise loadDetectionRuns: (projectId?: string | null) => Promise @@ -58,7 +58,7 @@ export function useDemoWorkflow({ setDetectionReferenceDatasetId(result.reference_dataset_id) setSegmentationReferenceDatasetId(result.reference_dataset_id) setDemoWorkflowMessage(result.message) - await loadProjects() + await loadProjects(result.project_id) const [projectData] = await Promise.all([ loadProjectData(result.project_id), loadDetectionRuns(result.project_id), diff --git a/frontend/src/hooks/useProjectWorkspace.ts b/frontend/src/hooks/useProjectWorkspace.ts index b0d57cc0..ac3f7172 100644 --- a/frontend/src/hooks/useProjectWorkspace.ts +++ b/frontend/src/hooks/useProjectWorkspace.ts @@ -12,6 +12,12 @@ import type { ProjectRead, } from '../types' +const DEMO_PROJECT_NAME = 'GeoIntel Demo - Building QA' + +function isDemoProject(project: ProjectRead): boolean { + return project.name === DEMO_PROJECT_NAME +} + export function useProjectWorkspace() { const [projects, setProjects] = useState([]) const [selectedProjectId, setSelectedProjectId] = useState(null) @@ -39,14 +45,45 @@ export function useProjectWorkspace() { [projects, selectedProjectId], ) - const loadProjects = async () => { + const fetchProjectData = async (projectId: string): Promise<{ areas: AreaRead[]; datasets: DatasetCreateResponse[] }> => { + const [areaResponse, datasetResponse]: [AreaListResponse, DatasetListResponse] = await Promise.all([ + areasApi.list(projectId), + datasetsApi.list(projectId), + ]) + return { areas: areaResponse.items, datasets: datasetResponse.items } + } + + const pickInitialProjectId = async (items: ProjectRead[], preferredProjectId?: string | null) => { + if (preferredProjectId && items.some((project) => project.id === preferredProjectId)) { + return preferredProjectId + } + if (selectedProjectId && items.some((project) => project.id === selectedProjectId)) { + return selectedProjectId + } + const demoProjects = items.filter(isDemoProject) + const candidates = [...demoProjects, ...items.filter((project) => !isDemoProject(project))].slice(0, 8) + for (const project of candidates) { + try { + const data = await fetchProjectData(project.id) + if (data.areas.length > 0 && data.datasets.length > 0) { + return project.id + } + } catch { + // Project list should still render if a candidate's detail endpoints are temporarily unavailable. + } + } + return demoProjects[0]?.id ?? items[0]?.id ?? null + } + + const loadProjects = async (preferredProjectId?: string | null) => { setLoadingProjects(true) setErrorMessage(null) try { const response = await projectsApi.list() setProjects(response.items) - if (!selectedProjectId && response.items.length > 0) { - setSelectedProjectId(response.items[0].id) + const nextProjectId = await pickInitialProjectId(response.items, preferredProjectId) + if (nextProjectId && nextProjectId !== selectedProjectId) { + setSelectedProjectId(nextProjectId) } } catch (error) { setErrorMessage(error instanceof Error ? error.message : 'Failed to load projects') @@ -60,13 +97,10 @@ export function useProjectWorkspace() { setLoadingDatasets(true) setErrorMessage(null) try { - const [areaResponse, datasetResponse]: [AreaListResponse, DatasetListResponse] = await Promise.all([ - areasApi.list(projectId), - datasetsApi.list(projectId), - ]) - setAreas(areaResponse.items) - setDatasets(datasetResponse.items) - return { areas: areaResponse.items, datasets: datasetResponse.items } + const projectData = await fetchProjectData(projectId) + setAreas(projectData.areas) + setDatasets(projectData.datasets) + return projectData } catch (error) { setErrorMessage(error instanceof Error ? error.message : 'Failed to load project data') return null @@ -83,13 +117,14 @@ export function useProjectWorkspace() { return } try { - await projectsApi.create({ + const createdProject = await projectsApi.create({ name: projectForm.name.trim(), description: projectForm.description?.trim() || undefined, region: projectForm.region?.trim() || 'Kempen', }) setProjectForm((previous) => ({ ...previous, name: '', description: '' })) - await loadProjects() + setSelectedProjectId(createdProject.id) + await loadProjects(createdProject.id) } catch (error) { setErrorMessage(error instanceof Error ? error.message : 'Failed to create project') }