Polish default demo project selection
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 18:17:05 +02:00
parent 7fa6c3c869
commit 95a978f01f
5 changed files with 93 additions and 14 deletions
+8
View File
@@ -7,6 +7,14 @@
# Changelog # 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) ## 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. - 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.
@@ -46,6 +46,7 @@ def test_demo_workflow_hook_owns_demo_api_and_cross_module_selection() -> None:
assert "demoApi.seedWorkflow" in hook assert "demoApi.seedWorkflow" in hook
assert "loadDemoWorkflow" in hook assert "loadDemoWorkflow" in hook
assert "loadProjects(result.project_id)" in hook
assert "setSelectedProjectId(result.project_id)" in hook assert "setSelectedProjectId(result.project_id)" in hook
assert "setSelectedDatasetId(result.candidate_dataset_id)" in hook assert "setSelectedDatasetId(result.candidate_dataset_id)" in hook
assert "setSelectedMapAreaId(result.area_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: 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")
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.list" in hook
assert "projectsApi.create" in hook assert "projectsApi.create" in hook
assert "setSelectedProjectId(createdProject.id)" in hook
assert "areasApi.list" in hook assert "areasApi.list" in hook
assert "areasApi.create" in hook assert "areasApi.create" in hook
assert "datasetsApi.list" in hook assert "datasetsApi.list" in hook
+30
View File
@@ -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) ## Sprint 44 Workbench UI polish pass (2026-06-17)
Changed: Changed:
+2 -2
View File
@@ -4,7 +4,7 @@ import type { DatasetCreateResponse, QualityCheckRead } from '../types'
import { formatError } from '../lib/formatError' import { formatError } from '../lib/formatError'
interface DemoWorkflowOptions { interface DemoWorkflowOptions {
loadProjects: () => Promise<void> loadProjects: (preferredProjectId?: string | null) => Promise<void>
loadProjectData: (projectId: string) => Promise<{ datasets: DatasetCreateResponse[] } | null> loadProjectData: (projectId: string) => Promise<{ datasets: DatasetCreateResponse[] } | null>
loadDatasetDetails: (projectId: string, dataset: DatasetCreateResponse) => Promise<void> loadDatasetDetails: (projectId: string, dataset: DatasetCreateResponse) => Promise<void>
loadDetectionRuns: (projectId?: string | null) => Promise<void> loadDetectionRuns: (projectId?: string | null) => Promise<void>
@@ -58,7 +58,7 @@ export function useDemoWorkflow({
setDetectionReferenceDatasetId(result.reference_dataset_id) setDetectionReferenceDatasetId(result.reference_dataset_id)
setSegmentationReferenceDatasetId(result.reference_dataset_id) setSegmentationReferenceDatasetId(result.reference_dataset_id)
setDemoWorkflowMessage(result.message) setDemoWorkflowMessage(result.message)
await loadProjects() await loadProjects(result.project_id)
const [projectData] = await Promise.all([ const [projectData] = await Promise.all([
loadProjectData(result.project_id), loadProjectData(result.project_id),
loadDetectionRuns(result.project_id), loadDetectionRuns(result.project_id),
+47 -12
View File
@@ -12,6 +12,12 @@ import type {
ProjectRead, ProjectRead,
} from '../types' } from '../types'
const DEMO_PROJECT_NAME = 'GeoIntel Demo - Building QA'
function isDemoProject(project: ProjectRead): boolean {
return project.name === DEMO_PROJECT_NAME
}
export function useProjectWorkspace() { export function useProjectWorkspace() {
const [projects, setProjects] = useState<ProjectRead[]>([]) const [projects, setProjects] = useState<ProjectRead[]>([])
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null) const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null)
@@ -39,14 +45,45 @@ export function useProjectWorkspace() {
[projects, selectedProjectId], [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) setLoadingProjects(true)
setErrorMessage(null) setErrorMessage(null)
try { try {
const response = await projectsApi.list() const response = await projectsApi.list()
setProjects(response.items) setProjects(response.items)
if (!selectedProjectId && response.items.length > 0) { const nextProjectId = await pickInitialProjectId(response.items, preferredProjectId)
setSelectedProjectId(response.items[0].id) if (nextProjectId && nextProjectId !== selectedProjectId) {
setSelectedProjectId(nextProjectId)
} }
} catch (error) { } catch (error) {
setErrorMessage(error instanceof Error ? error.message : 'Failed to load projects') setErrorMessage(error instanceof Error ? error.message : 'Failed to load projects')
@@ -60,13 +97,10 @@ export function useProjectWorkspace() {
setLoadingDatasets(true) setLoadingDatasets(true)
setErrorMessage(null) setErrorMessage(null)
try { try {
const [areaResponse, datasetResponse]: [AreaListResponse, DatasetListResponse] = await Promise.all([ const projectData = await fetchProjectData(projectId)
areasApi.list(projectId), setAreas(projectData.areas)
datasetsApi.list(projectId), setDatasets(projectData.datasets)
]) return projectData
setAreas(areaResponse.items)
setDatasets(datasetResponse.items)
return { areas: areaResponse.items, datasets: datasetResponse.items }
} catch (error) { } catch (error) {
setErrorMessage(error instanceof Error ? error.message : 'Failed to load project data') setErrorMessage(error instanceof Error ? error.message : 'Failed to load project data')
return null return null
@@ -83,13 +117,14 @@ export function useProjectWorkspace() {
return return
} }
try { try {
await projectsApi.create({ const createdProject = await projectsApi.create({
name: projectForm.name.trim(), name: projectForm.name.trim(),
description: projectForm.description?.trim() || undefined, description: projectForm.description?.trim() || undefined,
region: projectForm.region?.trim() || 'Kempen', region: projectForm.region?.trim() || 'Kempen',
}) })
setProjectForm((previous) => ({ ...previous, name: '', description: '' })) setProjectForm((previous) => ({ ...previous, name: '', description: '' }))
await loadProjects() setSelectedProjectId(createdProject.id)
await loadProjects(createdProject.id)
} catch (error) { } catch (error) {
setErrorMessage(error instanceof Error ? error.message : 'Failed to create project') setErrorMessage(error instanceof Error ? error.message : 'Failed to create project')
} }