MCP-2740 — Security scan UI stuck on "Scanning… N/N complete"

4/4 scenarios passed

Fix: terminal scan state is now derived from the authoritative backend status on every status fetch (poll tick + initial load + a render-independent safety-net watcher), independent of activeScanJobId / scan_pass bookkeeping. Decision logic lives in the pure, unit-tested reducer frontend/src/utils/scanState.ts.

Verified in a real Chromium against the built (embedded) Web UI; scan endpoints mocked via page.route to deterministically reproduce the sub-2s / mismatched-job-id race.

PASS 1 · Initial state — Security tab

Expected: Scan button reads "Scan Now" and is enabled; no "Scanning…" spinner.

Observed: data-test="scan-button" visible, text "Scan Now", enabled.

PASS 2 · Scan running — spinner up

Expected: After clicking, button reads "Scanning…" (disabled) and the progress panel (data-test="scan-progress") is visible.

Observed: Button shows "· Scanning…", "Initializing security scan…" progress bar + Cancel button render.

PASS 3 · THE FIX — completion poll reports a DIFFERENT job id

Expected: Start returns job-A; the completing /scan/status poll returns job-B (status=completed, scan_pass=1) — the exact race from the bug. The UI must still finalize: button back to "Scan Now" (enabled) and Report link enabled.

Observed: Spinner cleared, button "Scan Now" enabled, Risk Score 0/100, "View Full Report →" (data-test="scan-report-link") visible, "Scan Complete" toast. Pre-fix this poll hit the jobId≠activeScanJobId && scan_pass≠2 early-return and stayed stuck.

PASS 4 · Fresh mount on an already-completed scan

Expected: Reopening the tab when the backend job is already terminal shows the report, never "Scanning…".

Observed: Button "Scan Now", no scan-progress panel (count 0), report state reconciled on load.