diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 6c62b14c..81ece0ce 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -12091,3 +12091,36 @@ Open: - The guest screenshots intentionally show governed fixture/demo evidence, not a claim that the rejected V66 candidate labels or a new detector have been promoted. + +## 2026-08-01 - Live animated area-analysis workflow + +### Changed + +- Added a reusable `LiveAnalysisJourney` overlay to the real map canvas, using + the same dark glass, mint signal and four-stage language as the public + interactive project illustration. +- Bound Select, Sources, Process and Verify to persisted workbench state: + bounded AOI selection, source/coverage loading, extraction or image analysis, + QA validation, completed evidence and real error responses. +- Added a processing-only scan line, status pulse, responsive compact layout and + a complete `prefers-reduced-motion` fallback. The overlay uses + `pointer-events: none`, so drawing, panning and object inspection remain + available beneath it. +- Added a pure state resolver and component coverage for idle, loading, + processing, verification, completion and error transitions. + +### Tested before deployment + +- `npm run test:unit -- --run src/components/map/LiveAnalysisJourney.test.tsx` + passed: 3 tests. +- Complete frontend unit suite passed: 49 tests in 15 files. +- `npm run build` passed TypeScript compilation and the Vite production build. +- React review confirmed static step metadata is module-scoped, status is + derived during render, icons use direct imports and the overlay has an + accessible live region. + +### Known limitation and next gate + +- The local frontend preview cannot enter the workbench without its API runtime. + Tower deployment and browser validation against the real guest/operator data + remain the final acceptance gate for this pass. diff --git a/docs/TODO.md b/docs/TODO.md index 6ae6c410..cb9a7d6a 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1029,3 +1029,12 @@ This file now starts with the current implementation status. Older preparation/b - [x] Keep near-gate regional precision/recall stabilization inside a 0.03 sampling guard-band to prevent cross-region seesaw regressions. - [ ] Open protected test and pure-background evidence only after every calibration gate passes. - [ ] Queue final representative human sign-off only after all automated gates pass, then promote and redeploy the exact checksummed model. + +# Sprint 233 - Live animated gebiedsanalyse + +- [x] Vertaal de vier schakels van de welkomstillustratie naar de echte kaartworkflow. +- [x] Koppel Selecteer, Bronnen, Verwerk en Controleer uitsluitend aan bestaande API- en hookstatussen. +- [x] Toon verwerking, QA, fout en gereed als onderscheidbare live kaartstatussen zonder fictieve successen. +- [x] Voeg een responsieve desktop- en mobiele procesrail met reduced-motion fallback toe. +- [x] Dek de statusresolver en het geverifieerde-bewijslabel af met gerichte componenttests. +- [ ] Controleer na Tower-redeploy de operator- en gastworkflow visueel op desktop en mobiel. diff --git a/frontend/src/components/map/LiveAnalysisJourney.test.tsx b/frontend/src/components/map/LiveAnalysisJourney.test.tsx new file mode 100644 index 00000000..e1e87722 --- /dev/null +++ b/frontend/src/components/map/LiveAnalysisJourney.test.tsx @@ -0,0 +1,57 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { LiveAnalysisJourney, resolveLiveAnalysisJourney } from './LiveAnalysisJourney' + +const baseState = { + selectionMode: false, + hasSelection: false, + sourceLoading: false, + sourceReady: false, + processing: false, + validating: false, + hasResult: false, + verified: false, + error: null, +} + +describe('resolveLiveAnalysisJourney', () => { + it('follows real workflow state without assuming completion', () => { + expect(resolveLiveAnalysisJourney(baseState).currentLabel).toBe('Selecteer') + expect(resolveLiveAnalysisJourney({ ...baseState, hasSelection: true, sourceLoading: true }).currentLabel).toBe('Bronnen') + expect(resolveLiveAnalysisJourney({ ...baseState, hasSelection: true, sourceReady: true, processing: true }).currentLabel).toBe('Verwerk') + expect(resolveLiveAnalysisJourney({ ...baseState, hasSelection: true, sourceReady: true, hasResult: true }).currentLabel).toBe('Controleer') + expect(resolveLiveAnalysisJourney({ ...baseState, hasSelection: true, sourceReady: true, hasResult: true, verified: true }).status).toBe('complete') + }) + + it('marks the reached stage as failed', () => { + const view = resolveLiveAnalysisJourney({ + ...baseState, + hasSelection: true, + sourceReady: true, + processing: true, + error: 'Bron tijdelijk niet beschikbaar.', + }) + expect(view.status).toBe('error') + expect(view.steps[2]).toBe('error') + }) +}) + +describe('LiveAnalysisJourney', () => { + it('announces verified evidence only when verification exists', () => { + render( + , + ) + expect(screen.getAllByText('Kwaliteitsbewijs beschikbaar')).toHaveLength(2) + expect(screen.getByTestId('live-analysis-journey').getAttribute('data-stage')).toBe('controleer') + }) +}) diff --git a/frontend/src/components/map/LiveAnalysisJourney.tsx b/frontend/src/components/map/LiveAnalysisJourney.tsx new file mode 100644 index 00000000..f1ff2ad0 --- /dev/null +++ b/frontend/src/components/map/LiveAnalysisJourney.tsx @@ -0,0 +1,97 @@ +import { BadgeCheck, Database, MapPinned, ScanSearch, ShieldAlert } from 'lucide-react' + +export type LiveAnalysisStepStatus = 'waiting' | 'active' | 'complete' | 'error' + +export interface LiveAnalysisJourneyState { + selectionMode: boolean + hasSelection: boolean + sourceLoading: boolean + sourceReady: boolean + processing: boolean + validating: boolean + hasResult: boolean + verified: boolean + error: string | null +} + +export interface LiveAnalysisJourneyView { + currentIndex: number + currentLabel: string + status: 'idle' | 'running' | 'complete' | 'error' + steps: LiveAnalysisStepStatus[] +} + +const STEP_LABELS = ['Selecteer', 'Bronnen', 'Verwerk', 'Controleer'] as const + +export function resolveLiveAnalysisJourney(state: LiveAnalysisJourneyState): LiveAnalysisJourneyView { + let currentIndex = 0 + if (state.validating || state.verified || state.hasResult) currentIndex = 3 + else if (state.processing) currentIndex = 2 + else if (state.hasSelection) currentIndex = state.sourceLoading || !state.sourceReady ? 1 : 2 + + const status: LiveAnalysisJourneyView['status'] = state.error + ? 'error' + : state.verified + ? 'complete' + : state.sourceLoading || state.processing || state.validating + ? 'running' + : 'idle' + + const steps: LiveAnalysisStepStatus[] = [ + state.hasSelection ? 'complete' : 'active', + state.hasSelection && state.sourceReady ? 'complete' : state.hasSelection ? 'active' : 'waiting', + state.hasResult ? 'complete' : state.processing || (state.hasSelection && state.sourceReady) ? 'active' : 'waiting', + state.verified ? 'complete' : state.validating || state.hasResult ? 'active' : 'waiting', + ] + + if (state.error) steps[currentIndex] = 'error' + + return { currentIndex, currentLabel: STEP_LABELS[currentIndex], status, steps } +} + +interface LiveAnalysisJourneyProps extends LiveAnalysisJourneyState { + selectionLabel: string + sourceLabel: string + statusMessage: string + resultLabel: string +} + +const steps = [ + { label: 'Selecteer', icon: MapPinned }, + { label: 'Bronnen', icon: Database }, + { label: 'Verwerk', icon: ScanSearch }, + { label: 'Controleer', icon: BadgeCheck }, +] as const + +export function LiveAnalysisJourney({ selectionLabel, sourceLabel, statusMessage, resultLabel, ...state }: LiveAnalysisJourneyProps): JSX.Element { + const view = resolveLiveAnalysisJourney(state) + const detail = view.currentIndex === 0 + ? state.selectionMode ? 'Teken nu op de kaart' : selectionLabel + : view.currentIndex === 1 + ? sourceLabel + : view.currentIndex === 3 && state.verified + ? resultLabel + : statusMessage + + return ( +
+
+