From b6b3530e20e4476776abfcfc56e2ed11f31b0c6f Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 18 Jul 2026 21:20:57 +0800 Subject: [PATCH 01/21] fix: complete design accessibility audit remediation --- design-qa.md | 4 + docs/branch-review-ledger.md | 1 + docs/design-audit-2026-07-17.md | 94 ++++ playwright.config.ts | 7 +- src/app/favourites/page.tsx | 3 +- src/app/global-error.tsx | 7 +- src/app/globals.css | 1 - src/app/reference/colour-coding/page.tsx | 2 +- src/components/ClinicalDashboard.tsx | 64 ++- src/components/applications-launcher-page.tsx | 48 +- .../clinical-dashboard/evidence-panels.tsx | 27 +- .../favourites-command-library-page.tsx | 326 ++++++++------ .../clinical-dashboard/favourites-hub.tsx | 68 +-- .../global-search-shell.tsx | 8 +- .../master-search-header.tsx | 6 + .../universal-search-command-surface.tsx | 13 +- .../clinical-dashboard/visual-evidence.tsx | 113 +++-- ...ifferential-presentation-workflow-page.tsx | 31 +- .../differential-stream-page.tsx | 6 +- src/components/forms/form-detail-page.tsx | 4 - src/components/route-error-boundary.tsx | 6 +- .../services/services-navigator-page.tsx | 1 + src/components/therapy-compass/bindings.tsx | 1 - src/components/therapy-compass/nav.tsx | 2 +- .../therapy-compass/screens/brief-screen.tsx | 8 +- .../screens/compare-screen.tsx | 6 +- .../therapy-compass/screens/detail-screen.tsx | 7 +- .../therapy-compass/screens/home-screen.tsx | 24 +- .../screens/pathways-screen.tsx | 6 +- .../screens/recommend-screen.tsx | 1 + .../therapy-compass/screens/sheets-screen.tsx | 5 +- src/components/therapy-compass/styles.tsx | 28 +- tests/favourites-demo-boundary.test.ts | 41 ++ tests/forms-back-navigation.dom.test.tsx | 34 ++ tests/playwright-app-mode.ts | 15 + tests/playwright-project-isolation.test.ts | 17 + tests/route-error-boundary-focus.dom.test.tsx | 20 + tests/route-error-boundary.test.ts | 6 + ...therapy-compass-data-recovery.dom.test.tsx | 65 +++ ...herapy-compass-responsive-contract.test.ts | 134 ++++++ tests/ui-accessibility.spec.ts | 177 +++++++- tests/ui-overlay-css-contract.test.ts | 32 ++ tests/ui-route-coverage.spec.ts | 413 ++++++++++++++++++ tests/ui-smoke.spec.ts | 94 +++- tests/ui-tools.spec.ts | 62 +-- tests/ui-universal-search.spec.ts | 31 ++ tests/visual-evidence-tabs.dom.test.tsx | 139 ++++++ 47 files changed, 1875 insertions(+), 333 deletions(-) create mode 100644 docs/design-audit-2026-07-17.md create mode 100644 tests/favourites-demo-boundary.test.ts create mode 100644 tests/forms-back-navigation.dom.test.tsx create mode 100644 tests/playwright-app-mode.ts create mode 100644 tests/playwright-project-isolation.test.ts create mode 100644 tests/route-error-boundary-focus.dom.test.tsx create mode 100644 tests/therapy-compass-data-recovery.dom.test.tsx create mode 100644 tests/therapy-compass-responsive-contract.test.ts create mode 100644 tests/ui-overlay-css-contract.test.ts create mode 100644 tests/ui-route-coverage.spec.ts create mode 100644 tests/visual-evidence-tabs.dom.test.tsx diff --git a/design-qa.md b/design-qa.md index 2ec73430a..caa03d415 100644 --- a/design-qa.md +++ b/design-qa.md @@ -61,3 +61,7 @@ Final result: **blocked**. Blocker: an independent source visual target is required to certify concept fidelity. Implementation-level responsive, interaction, accessibility, and regression QA passed, but the design cannot honestly be declared pixel-faithful to an external approved concept that was not supplied. + +## Follow-up audit — 2026-07-17 + +The exhaustive line-level design, accessibility, UX, interaction, and route-coverage follow-up is documented in [`docs/design-audit-2026-07-17.md`](docs/design-audit-2026-07-17.md). It preserves the fixes above, avoids duplicating this pass, and records the new P1/P2/P3 fixes plus the exact local verification limitations. diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index c151ce902..9240bb2e0 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -602,3 +602,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-18 | claude/clinical-kb-pwa-review-asi3wb (PR #826) | 3d9ee5f44dea9edb1ef5af28f5f265d88d8b9f29 | PWA hardening implementation (plan Phase 1) | Implemented the three open findings from the 2026-07-17 PWA setup review with zero cache-semantics change: committed the rule-6 retirement worker `public/sw-kill-switch.js` with a five-test lock (`tests/pwa-kill-switch.test.ts`), bound the `offline.html` sha256 to the sw.js `CACHE_VERSION` pairing in `tests/pwa-manifest.test.ts` (drift trap closed), added the `?pwa-dev=0` local teardown to `pwa-lifecycle.tsx` with a dom test proving foreign workers and caches stay untouched, and updated `docs/pwa.md` rules 1 and 6 plus the local-dev cleanup step. Phase 0 of the approved plan (pr-policy `base_ref` checkout fix + the Set-Cookie worker-test case) was found already merged to main and skipped. | Focused Vitest 53/53. `verify:cheap` and the `verify:pr-local` unit stage green except `tests/pdf-extraction-budget.test.ts`, which fails identically on clean main in this container (child-process semantics; baselined twice). `verify:ui` 218 passed with 2 container-baselined pre-existing failures: the `ui-pwa` installability test (Chromium `in-incognito` artifact, reproduced from a clean-main detached worktree with its own server) and the `ui-smoke` document-viewer PDF-canvas mobile test (also fails on clean main `54229f0`; flagged as possible upstream regression). `format:check` clean for repo files. Conditional build/bundle stages deferred to the blocking hosted CI Build job on PR #826. No provider-backed checks run. | | 2026-07-18 | claude/clinical-kb-pwa-review-asi3wb (PR #835) | d46f381ac27b53b1bd5ac0ef77962fbd48cf3aa7 | PWA manifest and install-UX polish (plan Phase 2) | Implemented Phase 2 of the PWA plan with cache semantics untouched: `launch_handler` navigate-existing/auto and `display_override` standalone/minimal-ui in `manifest.ts`; `monochrome-192/512` icon variants rendered as a white alpha-only silhouette from the shared brand mark via `BRAND_MONOCHROME`; a one-time iOS/iPadOS Add to Home Screen hint in `pwa-lifecycle.tsx` (30-day dismissal key, never in standalone, timer-deferred eligibility for the set-state-in-effect lint rule). Docs Installability section updated; manifest screenshots remain deferred per the production-capture precondition. Phase 2 re-scout confirmed zero upstream drift before implementation. | Focused Vitest 55/55. `verify:cheap` 2773 passed/1 failed and `verify:pr-local` unit stage identical — the lone failure is the known container-only `pdf-extraction-budget` artifact (clean-main baselined; hosted CI green on #826). `test:e2e:pwa`: privacy journey passed; icon probes validated both new monochrome PNGs; sole installability error remains the container `in-incognito` artifact. `verify:ui` 219 passed/2 failed — exactly the two clean-main-baselined container artifacts, no new failures. `format:check` clean. Build/bundle stages deferred to the blocking hosted CI Build job on PR #835. No provider-backed checks run. | | 2026-07-18 | codex/rag-merge-final-20260718-655a | 20e5964bbfdcac0311b89c96931a43544be545a6 | RAG recovery, grounding, and latency merge closure | Reviewed the recovered historical RAG fix series against fresh `origin/main`. No additional source change was needed: current main already contains the stricter source-scoped numeric grounding, blocked-retrieval-only extractive recovery, generic LAI deterministic recovery, per-case answer latency diagnostics, and agitation lexical-path protections. The only residual branch diff was proven unreachable because every medication-chart trigger yields explicit search terms; it was intentionally not carried forward. | Full local Vitest: 301 files / 2,779 tests passed. Offline RAG fixture and production-contract validation: 36 golden cases, 21 suites, 294 tests passed. Prettier and `git diff --check` passed. `verify:cheap` reached its lint stage after all preceding static guards passed, but was blocked twice by active heavyweight jobs in other registered worktrees; it was not retried. No Supabase, OpenAI, live retrieval, production-readiness, deployment, or hosted CI command was run. | +| 2026-07-17 | codex/design-audit-20260716 | 47b58ddf95826231a6057b400e7f601c403a129d + reviewed working diff | exhaustive design, accessibility, UX, interaction, route, responsive, HTML/CSS/JS/TSX, and test-coverage audit with remediation | No P0. Fixed two P1 trust/responsive defects and the confirmed P2/P3 set: false Favourites provenance/demo leakage/dead controls, Therapy phone reflow/load recovery/contrast, semantic tabs/search/filters, error focus/theme, deterministic Forms back navigation, honest unavailable actions, application dialog semantics, duplicate differential keys, skip-link target, Playwright project isolation, and route coverage. External visual-target fidelity remains unavailable. | Static 261-file/~67,944-line inventory; independent combined-diff review; focused Vitest 21/21; jsdom 24/24; architecture 6/6; accessibility browser 5/5; focused route/browser recovery; lint; TypeScript; runtime/workflow/sitemap/brand/type/icon checks; production build with 1,043 pages and client-secret scan; `git diff --check`. Canonical aggregate Vitest was Windows-runner inconclusive and full UI was not repeated after focused remediation. No provider/API checks. | diff --git a/docs/design-audit-2026-07-17.md b/docs/design-audit-2026-07-17.md new file mode 100644 index 000000000..520b57590 --- /dev/null +++ b/docs/design-audit-2026-07-17.md @@ -0,0 +1,94 @@ +# Design, accessibility, and interaction audit — 2026-07-17 + +## Outcome + +The repository-wide design audit is complete. The implementation pass fixed the two P1 findings, the confirmed P2/P3 design and interaction defects, and three defects found by an independent adversarial review of the combined diff. No P0 was found. + +This pass deliberately built on the 2026-07-15 live design audit instead of repeating it. The earlier fixes for phone scroll ownership, app-mode Tab dismissal, decorative icon semantics, reduced-motion press feedback, Sheet backdrop tokens, and forced-colour tokens were treated as resolved. + +## Scope and method + +- Static line-level inventory: 261 user-facing TSX/CSS files under `src/app` and `src/components`, approximately 67,944 lines. +- Route and interaction inventory: 37 rendered/redirect route families and approximately 410 actionable controls. +- Parallel reviews: static product design, accessibility/UX, interaction/test coverage, plus an independent combined-diff review. +- Live proof: local demo-mode Next.js app only, with external requests blocked or provider variables removed. +- Breakpoints exercised by this and the inherited audit evidence: 320, 390, 639, 768, 1280/1440, 1536, and 1920 CSS pixels. +- Source visual target: unavailable. The existing product design system was the consistency baseline; this audit cannot certify pixel fidelity to an external Figma or approved mockup that was not supplied. + +## Findings and fixes + +| ID | Severity | Detailed issue and user impact | Implemented solution | Principal files | +| ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| DQA-01 | P1 | Favourites rendered the same acamprosate evidence and lithium note beneath unrelated selected items, creating false item-level provenance. | Evidence and notes now derive from the selected item; absent content has an explicit honest empty state. Copy actions use the item citation and return accessible status. | `favourites-command-library-page.tsx`, `ui-smoke.spec.ts` | +| DQA-02 | P1 | Therapy Compass used fixed desktop grid tracks wider than a phone while the global shell clipped page overflow, making several production screens unreachable at 320–390 px. | Added semantic responsive layout classes, one-column phone reflow, wrapping actions/metrics, and a contract test across all Therapy Compass screens. | `therapy-compass/*`, `therapy-compass-responsive-contract.test.ts` | +| DQA-03 | P2 | White text/icons on the dark clinical accent measured materially below accessible contrast and bypassed the existing contrast token. | Replaced hard-coded white foregrounds with `--clinical-accent-contrast`, including answer status and Therapy Compass controls. | `answer-status.tsx`, `therapy-compass/*` | +| DQA-04 | P2 | Prototype favourites appeared as genuine saved user content in normal mode, including through universal search. | Gated fixtures behind the effective client demo state on the route, hub, command library, and universal search surface. Normal mode now shows only real saved items. | `favourites/page.tsx`, `ClinicalDashboard.tsx`, `favourites-hub.tsx`, `global-search-shell.tsx`, `master-search-header.tsx`, `universal-search-command-surface.tsx` | +| DQA-05 | P2 | Favourites exposed invisible selection below `2xl`, nested/synthetic mobile card controls, no-op edit/sort actions, incomplete menu keyboard behavior, and clipboard focus loss. | Kept explicit native links and separate buttons on mobile, limited selection to the visible workspace, implemented real citation copy with status/focus restoration, added complete menu keyboard behavior, and disabled or relabelled unfinished actions honestly. | `favourites-command-library-page.tsx`, `favourites-hub.tsx` | +| DQA-06 | P2 | Differential presentation controls advertised editing, filtering, reorder/remove behavior, and a fixed `+2` count that did not exist. | Removed false instructions, derived the remaining count from state, and disabled unfinished controls with availability explanations while preserving the read-only comparison. | `differential-presentation-workflow-page.tsx` | +| DQA-07 | P2 | Services selection led to enabled-looking Edit/Review/View controls that did nothing, while Compare stayed unavailable without an accurate reason. A later diff briefly disabled the real Clear action. | Disabled unfinished actions with truthful titles, made Compare messaging state-aware, and restored Clear so selected services can be reset. | `services-navigator-page.tsx`, `ui-smoke.spec.ts` | +| DQA-08 | P2 | Evidence records with no URL were still keyboard-focusable `href="#"` links announced as “Open source”. | Missing-source rows now render as non-links with unavailable semantics; only valid destinations render a Link. | `visual-evidence.tsx`, `visual-evidence-tabs.dom.test.tsx` | +| DQA-09 | P2 | Therapy cards presented an enabled Favourite button with no handler or state change. | The unavailable feature is now disabled and labelled as coming soon, preventing false save confirmation. | `therapy-card.tsx` | +| DQA-10 | P2 | Route/global error fallbacks did not reliably move focus or announce a route failure; the global fallback was hard-coded light-only. | Added programmatic heading focus and alert announcement, and made the self-contained global fallback system-colour, dark-mode, and forced-colour aware. | `route-error-boundary.tsx`, `global-error.tsx`, route error tests | +| DQA-11 | P2 | Clinical Notes, Visual Evidence, and upload/index tab interfaces lacked complete roving keyboard behavior and reciprocal tab/panel relationships. | Added stable unique IDs, `aria-controls`/`aria-labelledby`, roving `tabIndex`, and Left/Right/Home/End auto-activation. | `ClinicalDashboard.tsx`, `answer-result-surface.tsx`, `visual-evidence.tsx`, UI/DOM tests | +| DQA-12 | P2 | Universal search visually highlighted an item without exposing it through `aria-activedescendant`. | The combobox now points to the active option and tests verify the relationship through keyboard navigation. | `master-search-header.tsx`, `universal-search-command-surface.tsx`, `ui-universal-search.spec.ts` | +| DQA-13 | P2 | Differential filters and several selector groups behaved as toggles but lacked grouped/pressed semantics. | Added labelled groups and stateful `aria-pressed` semantics, with browser accessibility assertions. | `differentials-home.tsx`, `evidence-panels.tsx`, `ui-accessibility.spec.ts` | +| DQA-14 | P2 | Therapy Compass silently swallowed data-load failure and still implied a fabricated `200+` catalogue count. | Added an explicit failure state and Retry action, removed fabricated totals, and retained real successful/empty states. | `use-therapy-data.ts`, `therapy-compass-page.tsx`, `therapy-compass-data-recovery.dom.test.tsx` | +| DQA-15 | P2 | Direct-entry Forms “Back to forms” used browser history and could leave the application. | The control now deterministically navigates to `/forms?focus=1`; a DOM regression test verifies the destination. | `form-detail-page.tsx`, `forms-back-navigation.dom.test.tsx` | +| DQA-16 | P2 | Firefox/WebKit production projects unintentionally collected advisory `@mockup` tests; two browser helpers also retried hydration inconsistently. | Applied production matchers and `grepInvert` to all required projects and introduced one shared app-mode hydration retry helper. | `playwright.config.ts`, `playwright-app-mode.ts`, `ui-stress.spec.ts`, `ui-tools.spec.ts` | +| DQA-17 | P2 | Application/tool cards looked like direct links but prevented navigation to open an internal detail state, obscuring the real action. | Cards are now honest dialog buttons with `aria-haspopup="dialog"`; the dialog contains the actual destination link. | `applications-launcher-page.tsx`, `ui-smoke.spec.ts` | +| DQA-18 | P2 | Differential diagnosis streams keyed repeated cards only by visible title, producing React duplicate-key errors and unstable list identity. | Cards now use a stable href/index composite key; the route matrix listens for console errors. | `differential-stream-page.tsx`, `ui-route-coverage.spec.ts` | +| DQA-19 | P2 | The colour-reference skip link targeted `#main-content`, but the route had no matching focus target. | Added the canonical focusable main-content target and browser proof. | `reference/colour-coding/page.tsx`, `ui-route-coverage.spec.ts` | +| DQA-20 | P3 | Answer sheets overrode the semantic backdrop token and duplicate mobile CSS retained obsolete layout overrides. | Removed call-site backdrop overrides and redundant CSS so theme, forced-colour, and responsive behavior come from the shared tokens/layout rules. | `answer-result-surface.tsx`, `globals.css`, `ui-overlay-css-contract.test.ts` | + +## Route and component interaction proof + +The new provider-free route matrix covers Therapy Compass, DSM home/compare/differentials, Specifier compare/map, differential diagnosis stream/detail, colour reference, Applications and Medications redirects, and both document-source aliases at desktop and 390 px. It asserts visible primary content, no document-level horizontal overflow, a representative action, blocked external/API leakage, and route-specific behavior. + +Additional focused browser proof covered: + +- Clinical Notes roving tabs and universal-search active descendant behavior. +- Favourites desktop workspace, item-specific evidence/notes, menu keyboard/copy/focus restoration, and native mobile link/menu separation. +- Applications detail-dialog semantics and real destination link. +- Tools mobile/desktop navigation and detail interactions. +- Differential comparison and the differential stream/detail console-error regression. +- Reduced motion, forced colours, 200% zoom, and axe WCAG A/AA in normal and forced-colour modes. + +The Services control test rendered the correct page and retained a screenshot showing the selected rail, working Clear action, and disabled unfinished controls. Its assertion was incorrectly scoped to a nonexistent test ID; that test now uses the semantic `main` root. Per the recovery instruction, it was not launched a third time after two failed attempts (the first was runner/port contention, the second exposed the selector defect). + +The valid `/documents/source` browser case issued the correct destination request but the cold document-detail compile (about 24 seconds plus source render) exceeded its 30-second browser poll. The exact local response contains `NEXT_REDIRECT;replace;/documents/11111111-1111-4111-8111-111111111111?page=2&chunk=safety+plan;307;`. The test allowance is now 60 seconds, but the same browser action was not retried a third time. + +## Verification results + +Passed: + +- `npm run build`: webpack production build compiled; TypeScript passed; 1,043/1,043 static pages generated; client-bundle secret scan passed. +- `npm run typecheck`. +- `npm run lint`. +- Runtime, GitHub Actions pin, sitemap, brand, strict type-scale, and strict icon-scale checks. +- Focused changed-surface Vitest: 9 files, 21/21 tests. +- Full jsdom project through the Windows worker-thread fallback: 10 files, 24/24 tests. +- Architecture boundary isolation: 6/6, including no runtime import cycles. +- Accessibility media/axe browser suite: 5/5. +- Focused route recovery: Specifier map 1/1; differential stream/detail 1/1; colour reference plus Applications, Medications, and invalid evidence alias 4/4. +- `git diff --check` before final documentation. + +Broad-gate limitations: + +- The canonical `npm run verify:cheap` reached the unit phase but Windows repeatedly failed to start Vitest fork workers. The worker-thread fallback exposed only the architecture scan exceeding the global 30-second timeout; that exact file passed 6/6 in isolation with diagnostic headroom. A later aggregate JSON run was terminated before writing its result. The changed-surface and jsdom suites are green, but the canonical aggregate unit command is not recorded as green. +- The first current-branch `npm run verify:ui` pass was 177/182. Each identified product/test issue was fixed and rerun narrowly; the full suite was not repeated after recovery to avoid repeating the large gate. The inherited prior design branch baseline remains 175/175. +- No OpenAI, Supabase, GitHub/GitLab, hosted CI, production-like, or paid-provider workflow was run. Provider variables were removed from local verification commands. + +## Residual decisions and risk + +- External design-target fidelity remains unprovable until an approved Figma file or screenshot set is supplied. +- Several future product features (service comparison/editing, therapy favourite persistence, differential editing) remain intentionally unavailable. They now present disabled or read-only states instead of false actions. +- Live authenticated data, provider failure behavior, and production clinical workflows remain outside this offline design audit and require explicit authorization before testing. +- No dependency, API contract, database schema, production data, or environment file was changed. + +## Audit execution state + +- Branch: `codex/design-audit-20260716` +- Isolated worktree: `C:\Dev\Apps\Database\worktrees\design-audit-20260716` +- Base HEAD: `47b58ddf95826231a6057b400e7f601c403a129d` +- The audit and verification finished before Git handoff; its reviewed diff is carried forward from this isolated worktree onto the final PR branch. +- No deployment or provider-backed call was made during the audit. diff --git a/playwright.config.ts b/playwright.config.ts index 25407cfd5..1400515cd 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -10,16 +10,17 @@ const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH; // Prototype /mockups journeys live in their own advisory project so a red // mockup can never mask a production-journey regression (PT-05). The two // Tag-level filters keep production and prototype journeys disjoint even when -// they share a spec file. +// they share a spec file. Every required browser project uses the same +// production matcher and tag exclusion. const productionSpecPattern = - /.*(?:answer-progress-ui-smoke|ui-(smoke|stress|accessibility|tools|overlap|universal-search|specifiers|formulation|pwa))\.spec\.ts/; + /.*(?:answer-progress-ui-smoke|ui-(smoke|stress|accessibility|tools|overlap|universal-search|specifiers|formulation|pwa|route-coverage))\.spec\.ts/; const mockupSpecPattern = /.*ui-(tools|tools-collapse|tools-task-directory)\.spec\.ts/; const mockupTag = /@mockup/; export default defineConfig({ testDir: "./tests", testMatch: - /.*(?:answer-progress-ui-smoke|ui-(smoke|stress|accessibility|tools|tools-collapse|tools-task-directory|overlap|universal-search|specifiers|formulation|pwa))\.spec\.ts/, + /.*(?:answer-progress-ui-smoke|ui-(smoke|stress|accessibility|tools|tools-collapse|tools-task-directory|overlap|universal-search|specifiers|formulation|pwa|route-coverage))\.spec\.ts/, timeout: 60_000, retries: 0, // Fail the run if a stray `test.only` is committed: otherwise it silently diff --git a/src/app/favourites/page.tsx b/src/app/favourites/page.tsx index 6048c35e0..872fac7c3 100644 --- a/src/app/favourites/page.tsx +++ b/src/app/favourites/page.tsx @@ -1,4 +1,5 @@ import { FavouritesCommandLibraryPage } from "@/components/clinical-dashboard/favourites-command-library-page"; +import { isDemoMode } from "@/lib/env"; type FavouritesPageProps = { searchParams?: Promise<{ @@ -16,5 +17,5 @@ export default async function FavouritesPage({ searchParams }: FavouritesPagePro // No key={query} remount: query is a pure prop, and remounting on query // change wiped the set/type/view/sort selections when clearing a search. - return ; + return ; } diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index aed84dad6..157a1b45c 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -14,7 +14,7 @@ export default function GlobalError({ error, reset }: { error: Error & { digest? const headingRef = useRef(null); useEffect(() => { console.error("Fatal error captured by global-error boundary:", error); - headingRef.current?.focus(); + headingRef.current?.focus({ preventScroll: true }); }, [error]); return ( @@ -27,6 +27,7 @@ export default function GlobalError({ error, reset }: { error: Error & { digest? alignItems: "center", justifyContent: "center", padding: "1rem", + colorScheme: "light dark", backgroundColor: "Canvas", color: "CanvasText", fontFamily: @@ -68,11 +69,11 @@ export default function GlobalError({ error, reset }: { error: Error & { digest? style={{ margin: "0 0 1.25rem", borderRadius: "0.5rem", - backgroundColor: "Canvas", + backgroundColor: "Field", padding: "0.5rem", fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace", fontSize: "0.75rem", - color: "CanvasText", + color: "FieldText", wordBreak: "break-all", }} > diff --git a/src/app/globals.css b/src/app/globals.css index fb181241c..4a440c2a9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1102,7 +1102,6 @@ summary::-webkit-details-marker { width: 1.1rem; } } - @media (prefers-reduced-motion: no-preference) { .mode-action-surface[data-placement="up"] { animation: mode-action-fold-up 170ms cubic-bezier(0.22, 1, 0.36, 1) both; diff --git a/src/app/reference/colour-coding/page.tsx b/src/app/reference/colour-coding/page.tsx index cb248a1dc..08eed8b00 100644 --- a/src/app/reference/colour-coding/page.tsx +++ b/src/app/reference/colour-coding/page.tsx @@ -31,7 +31,7 @@ const TONE_SAMPLE_LABEL: Record<(typeof SEMANTIC_TONES)[number], string> = { export default function ColourCodingReferencePage() { return ( -
+
diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index a36051777..d0594ef8b 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -23,7 +23,16 @@ import { WifiOff, Wrench, } from "lucide-react"; -import { type CSSProperties, useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react"; +import { + type CSSProperties, + type KeyboardEvent as ReactKeyboardEvent, + useCallback, + useEffect, + useMemo, + useReducer, + useRef, + useState, +} from "react"; import { type DocumentDeleteResult } from "@/components/DocumentManagementActions"; import { extractSafetyFindings } from "@/lib/clinical-safety"; import { isLocalNoAuthMode, publicUploadsEnabled } from "@/lib/client-env"; @@ -652,6 +661,7 @@ export function ClinicalDashboard({ const [documentsDrawerMode, setDocumentsDrawerMode] = useState("library"); const [uploadDrawerOpen, setUploadDrawerOpen] = useState(false); const [uploadMobileTab, setUploadMobileTab] = useState("upload"); + const uploadTabRefs = useRef(new Map()); const [documentDrawerStatusFilter, setDocumentDrawerStatusFilter] = useState("indexed"); const [indexingMonitorFilter, setIndexingMonitorFilter] = useState("all"); const [recentQueries, setRecentQueries] = useState([]); @@ -3226,6 +3236,7 @@ export function ClinicalDashboard({ id: UploadIndexingTab; label: string; summary: string; + tabId: string; panelId: string; icon: typeof UploadCloud; }> = [ @@ -3233,6 +3244,7 @@ export function ClinicalDashboard({ id: "setup", label: "Setup", summary: `${setupReadyCount}/${setupCheckCount} ready`, + tabId: "dashboard-upload-tab-setup", panelId: "dashboard-setup-section", icon: ListChecks, }, @@ -3240,6 +3252,7 @@ export function ClinicalDashboard({ id: "upload", label: "Upload", summary: uploadReadOnlyMode || !canUploadDocuments ? "Locked" : "Ready", + tabId: "dashboard-upload-tab-upload", panelId: "dashboard-upload-section", icon: UploadCloud, }, @@ -3251,6 +3264,7 @@ export function ClinicalDashboard({ : failedUploadWork ? `${failedUploadWork} failed` : "Idle", + tabId: "dashboard-upload-tab-jobs", panelId: "dashboard-indexing-section", icon: RefreshCw, }, @@ -3258,10 +3272,31 @@ export function ClinicalDashboard({ id: "quality", label: "Quality", summary: qualityItems.length ? `${qualityItems.length} review` : "Clear", + tabId: "dashboard-upload-tab-quality", panelId: "dashboard-quality-section", icon: ShieldAlert, }, ]; + + function handleUploadTabKeyDown(event: ReactKeyboardEvent) { + const order = uploadTabs.map((tab) => tab.id); + const index = order.indexOf(uploadMobileTab); + const next = + event.key === "ArrowRight" + ? order[(index + 1) % order.length] + : event.key === "ArrowLeft" + ? order[(index - 1 + order.length) % order.length] + : event.key === "Home" + ? order[0] + : event.key === "End" + ? order[order.length - 1] + : null; + if (!next) return; + event.preventDefault(); + if (next !== uploadMobileTab) setUploadMobileTab(next); + uploadTabRefs.current.get(next)?.focus(); + } + const handleUploadQueued = () => { setUserStartedIngestion(true); setIndexingActive(true); @@ -3385,6 +3420,7 @@ export function ClinicalDashboard({
{ setQuery(""); setModeSearchSubmitted(false); router.replace(appModeHomeHref("favourites", { focus: true, queryMode, scopeFilters })); }} - onAddFavourite={() => - setActionNotice({ tone: "success", message: "Favourite creation is ready to connect." }) - } desktopComposerSlotId={desktopHomeComposerSlotId} /> ) : activeModeResultKind === "documents" || activeModeResultKind === "services" ? ( @@ -3980,6 +4014,7 @@ export function ClinicalDashboard({
{uploadTabs.map((tab) => { @@ -3988,10 +4023,18 @@ export function ClinicalDashboard({ return ( @@ -4015,7 +4061,7 @@ export function ClinicalDashboard({
-
+
{desktopFilters.map((filter) => { const active = filter.id === activeFilter || (filter.id === "all" && activeFilter === "more"); return (
-
+
{mobileFilters.map((filter) => { const active = filter.id === activeFilter || (filter.id === "all" && activeFilter === "saved"); return ( ); } @@ -452,14 +450,12 @@ function MobileToolRow({ onSelect: (id: string) => void; }) { return ( - { - event.preventDefault(); - onSelect(app.id); - }} + onClick={() => onSelect(app.id)} className={cn( "grid min-h-[5.25rem] grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 rounded-lg border bg-[color:var(--surface-lux)] px-3 py-3 text-left shadow-[var(--shadow-inset)] transition hover:border-[color:var(--clinical-accent-border)]", selected @@ -478,10 +474,10 @@ function MobileToolRow({ - {app.actionLabel} + Details - + ); } diff --git a/src/components/clinical-dashboard/evidence-panels.tsx b/src/components/clinical-dashboard/evidence-panels.tsx index d12fdac01..47d3c984a 100644 --- a/src/components/clinical-dashboard/evidence-panels.tsx +++ b/src/components/clinical-dashboard/evidence-panels.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { type RefObject, useId, useState } from "react"; +import { type KeyboardEvent as ReactKeyboardEvent, type RefObject, useId, useRef, useState } from "react"; import { Activity, CircleAlert, @@ -626,6 +626,7 @@ export function ClinicalNotesChecklistPanel({ const tabs = clinicalNotesAvailableTabs(detailSections); const defaultTab = tabs.find((tab) => tab.id === "actions")?.id ?? tabs[0]?.id ?? "actions"; const [requestedTab, setRequestedTab] = useState(defaultTab); + const tabRefs = useRef(new Map()); const tabBaseId = useId(); const tabButtonId = (id: ClinicalNotesTabId) => `${tabBaseId}-tab-${id}`; const notesPanelId = `${tabBaseId}-panel`; @@ -644,6 +645,25 @@ export function ClinicalNotesChecklistPanel({ const showTabStrip = tabs.length > 1; + function handleTabKeyDown(event: ReactKeyboardEvent) { + const order = tabs.map((tab) => tab.id); + const index = order.indexOf(activeTab); + const next = + event.key === "ArrowRight" + ? order[(index + 1) % order.length] + : event.key === "ArrowLeft" + ? order[(index - 1 + order.length) % order.length] + : event.key === "Home" + ? order[0] + : event.key === "End" + ? order[order.length - 1] + : null; + if (!next) return; + event.preventDefault(); + if (next !== activeTab) setRequestedTab(next); + tabRefs.current.get(next)?.focus(); + } + return (
{showTabStrip ? ( @@ -651,6 +671,7 @@ export function ClinicalNotesChecklistPanel({
{ + if (element) tabRefs.current.set(tab.id, element); + else tabRefs.current.delete(tab.id); + }} type="button" role="tab" id={tabButtonId(tab.id)} diff --git a/src/components/clinical-dashboard/favourites-command-library-page.tsx b/src/components/clinical-dashboard/favourites-command-library-page.tsx index 4b84f5c53..b43e90e4c 100644 --- a/src/components/clinical-dashboard/favourites-command-library-page.tsx +++ b/src/components/clinical-dashboard/favourites-command-library-page.tsx @@ -6,7 +6,6 @@ import { ChevronDown, ChevronsRight, Copy, - Edit3, ExternalLink, FileText, Folder, @@ -76,20 +75,9 @@ type FavouriteSet = { meta?: string; }; -type SourceRecord = { - title: string; - type: string; -}; - const focusRing = "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; -const sourceRecords: SourceRecord[] = [ - { title: "NICE CKS - Alcohol dependence", type: "Guideline" }, - { title: "BNF - Acamprosate", type: "BNF" }, - { title: "Medsafe - Acamprosate data sheet", type: "Datasheet" }, -]; - const typeStyles: Record = { Medication: "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]", @@ -150,6 +138,42 @@ function isSourceBacked(item: FavouriteItem): boolean { return Boolean(item.evidence && item.evidence !== "Run" && item.evidence !== "Saved query"); } +function favouriteCitationText(item: FavouriteItem): string { + const evidenceLine = isSourceBacked(item) ? `Evidence: ${item.evidence}` : item.description; + return `${item.title}\n${evidenceLine}\n${item.href}`; +} + +async function copyFavouriteCitation(item: FavouriteItem): Promise { + const text = favouriteCitationText(item); + + if (navigator.clipboard?.writeText) { + try { + await navigator.clipboard.writeText(text); + return true; + } catch { + // Fall through to the local selection-based copy path. + } + } + + const previouslyFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null; + const textarea = document.createElement("textarea"); + textarea.value = text; + textarea.setAttribute("readonly", ""); + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; + document.body.appendChild(textarea); + textarea.select(); + + try { + return document.execCommand("copy"); + } catch { + return false; + } finally { + textarea.remove(); + previouslyFocused?.focus({ preventScroll: true }); + } +} + function toCommandItem(item: PrototypeFavouriteItem): FavouriteItem { const type = item.type === "sources" && item.primaryAction === "Run" @@ -328,7 +352,7 @@ function ActiveFilterChips({ ); } -function ContinueStrip({ item, onSelect }: { item: FavouriteItem; onSelect: (id: string) => void }) { +function ContinueStrip({ item }: { item: FavouriteItem }) { const Icon = item.icon; return (
- +
("idle"); const buttonRef = useRef(null); const menuRef = useRef(null); + const menuId = `favourite-actions-${item.id.replace(/[^a-zA-Z0-9_-]/g, "-")}`; + const triggerId = `${menuId}-trigger`; useDismissableLayer({ enabled: open, @@ -393,15 +417,60 @@ function RowActionsMenu({ item }: { item: FavouriteItem }) { const actionLabel = item.action === "Copy" ? "Open" : item.action; + function focusMenuItem(position: "first" | "last") { + window.requestAnimationFrame(() => { + const items = Array.from( + menuRef.current?.querySelectorAll('[role="menuitem"]:not([disabled])') ?? [], + ); + const target = position === "first" ? items[0] : items.at(-1); + target?.focus({ preventScroll: true }); + }); + } + + function openMenu(position: "first" | "last" = "first") { + setOpen(true); + setCopyStatus("idle"); + focusMenuItem(position); + } + + function handleMenuKeyDown(event: React.KeyboardEvent) { + const items = Array.from(menuRef.current?.querySelectorAll('[role="menuitem"]:not([disabled])') ?? []); + const currentIndex = items.findIndex((candidate) => candidate === document.activeElement); + + if (event.key === "Tab") { + setOpen(false); + return; + } + + let nextIndex: number | null = null; + if (event.key === "ArrowDown") nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % items.length; + if (event.key === "ArrowUp") + nextIndex = currentIndex < 0 ? items.length - 1 : (currentIndex - 1 + items.length) % items.length; + if (event.key === "Home") nextIndex = 0; + if (event.key === "End") nextIndex = items.length - 1; + + if (nextIndex !== null && items[nextIndex]) { + event.preventDefault(); + items[nextIndex].focus({ preventScroll: true }); + } + } + return (
) : null} + + {copyStatus === "copied" + ? `${item.title} citation copied` + : copyStatus === "failed" + ? "Unable to copy citation" + : ""} +
); } -function FavouriteMobileCard({ - item, - selected, - onSelect, -}: { - item: FavouriteItem; - selected: boolean; - onSelect: (id: string) => void; -}) { +function FavouriteMobileCard({ item }: { item: FavouriteItem }) { return ( -
- + + + {item.title} + + + {item.description} + + {item.type} @@ -713,6 +774,7 @@ function FavouritesTable({
{tableRows.map((item) => ( - + ))} {tableRows.length === 0 ? (
@@ -776,12 +833,13 @@ function FavouritesTable({ function ItemWorkspace({ item, onClose }: { item: FavouriteItem; onClose: () => void }) { const [activeTab, setActiveTab] = useState<"summary" | "evidence" | "notes">("summary"); + const [copyStatus, setCopyStatus] = useState<"idle" | "copied" | "failed">("idle"); const Icon = item.icon; const actionLabel = item.action === "Copy" ? "Open" : item.action; return (
); diff --git a/src/components/clinical-dashboard/favourites-hub.tsx b/src/components/clinical-dashboard/favourites-hub.tsx index 8b34efc70..e411c4931 100644 --- a/src/components/clinical-dashboard/favourites-hub.tsx +++ b/src/components/clinical-dashboard/favourites-hub.tsx @@ -1,18 +1,7 @@ "use client"; import Link from "next/link"; -import { - ArrowUpDown, - ChevronDown, - Filter, - Folder, - FolderInput, - Heart, - Plus, - Search, - ShieldCheck, - X, -} from "lucide-react"; +import { ArrowUpDown, ChevronDown, Filter, Folder, Heart, Plus, Search, ShieldCheck, X } from "lucide-react"; import { useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react"; import { useDismissableLayer } from "@/components/use-dismissable-layer"; import { ModeHomeHero, ModeHomeVerificationFooter } from "@/components/mode-home-template"; @@ -40,20 +29,24 @@ function favouriteMatchesQuery(value: { title: string; meta?: string; set?: stri export function FavouritesHub({ query, onClearQuery, - onAddFavourite, desktopComposerSlotId, + demoMode, headingLevel = 2, }: { query: string; onClearQuery: () => void; - onAddFavourite: () => void; desktopComposerSlotId?: string; + demoMode: boolean; headingLevel?: 1 | 2; }) { const savedRegistryFavourites = useSavedRegistryFavourites(); - const allFavouriteItems = useMemo(() => [...favouriteItems, ...savedRegistryFavourites], [savedRegistryFavourites]); + const allFavouriteItems = useMemo( + () => [...(demoMode ? favouriteItems : []), ...savedRegistryFavourites], + [demoMode, savedRegistryFavourites], + ); const allFavouriteSets = useMemo(() => { - const savedSetTitles = new Set(favouriteSets.map((set) => set.title)); + const prototypeSets = demoMode ? favouriteSets : []; + const savedSetTitles = new Set(prototypeSets.map((set) => set.title)); const dynamicSets: FavouriteSet[] = Array.from(new Set(savedRegistryFavourites.map((item) => item.set))) .filter((title): title is string => Boolean(title) && !savedSetTitles.has(title)) .map((title) => ({ @@ -67,13 +60,13 @@ export function FavouritesHub({ keywords: title.toLowerCase(), })); return [ - ...favouriteSets.map((set) => ({ + ...prototypeSets.map((set) => ({ ...set, count: allFavouriteItems.filter((item) => item.set === set.title).length, })), ...dynamicSets, ].filter((set) => set.count > 0); - }, [allFavouriteItems, savedRegistryFavourites]); + }, [allFavouriteItems, demoMode, savedRegistryFavourites]); const [selectedTab, setSelectedTab] = useState("all"); const [tabMenuOpen, setTabMenuOpen] = useState(false); const [selectedSetId, setSelectedSetId] = useState(null); @@ -312,14 +305,23 @@ export function FavouritesHub({ ) : null}
-
- + ); } diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 979dc30a5..14e631659 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -314,11 +314,8 @@ function GlobalStandaloneSearchShellClient({ // Recent queries are owner-scoped session state (2026-07-13 audit, finding 4): // the legacy unscoped localStorage value could resurface another account's // clinical queries on a shared workstation, so it is deleted, never read. - const recentQueriesOwnerId = - auth.session?.user.id ?? - (!auth.isConfigured || process.env.NEXT_PUBLIC_DEMO_MODE === "true" || isLocalNoAuthMode() - ? demoRecentQueryOwnerId - : null); + const clientDemoMode = !auth.isConfigured || process.env.NEXT_PUBLIC_DEMO_MODE === "true" || isLocalNoAuthMode(); + const recentQueriesOwnerId = auth.session?.user.id ?? (clientDemoMode ? demoRecentQueryOwnerId : null); useEffect(() => { clearLegacyRecentQueries(); @@ -512,6 +509,7 @@ function GlobalStandaloneSearchShellClient({
("up"); const [commandDropdownOpen, setCommandDropdownOpen] = useState(false); const [commandListboxId, setCommandListboxId] = useState(); + const [commandActiveItemId, setCommandActiveItemId] = useState(null); const [modeMenuOpen, setModeMenuOpen] = useState(false); const [usesScopeSheet, setUsesScopeSheet] = useState(false); const [usesPhoneSearchLayout, setUsesPhoneSearchLayout] = useState(false); @@ -1267,6 +1270,7 @@ export function MasterSearchHeader({ /> ) : null} onCommandScopesChange?.(scopes)} onListboxIdReady={setCommandListboxId} + onActiveItemIdChange={setCommandActiveItemId} onFocusSearchInput={() => queryInputRef?.current?.focus()} >
) => void; onFocusSearchInput?: () => void; onListboxIdReady?: (listboxId: string) => void; + onActiveItemIdChange?: (activeItemId: string | null) => void; placement?: CommandSurfacePlacement; children: ReactNode; }) { @@ -420,7 +424,10 @@ export function UniversalSearchCommandSurface({ contextMode: modeId, }); const savedRegistryFavourites = useSavedRegistryFavourites(); - const allFavouriteItems = useMemo(() => [...favouriteItems, ...savedRegistryFavourites], [savedRegistryFavourites]); + const allFavouriteItems = useMemo( + () => [...(demoMode ? favouriteItems : []), ...savedRegistryFavourites], + [demoMode, savedRegistryFavourites], + ); const favouriteMatches = useMemo( () => rankLocalFavourites(allFavouriteItems, trimmedQuery), [allFavouriteItems, trimmedQuery], @@ -929,6 +936,10 @@ export function UniversalSearchCommandSurface({ onListboxIdReady?.(listboxId); }, [listboxId, onListboxIdReady]); + useEffect(() => { + onActiveItemIdChange?.(dropdownOpen ? activeItemId : null); + }, [activeItemId, dropdownOpen, onActiveItemIdChange]); + useEffect(() => { if (!dropdownOpen) return; diff --git a/src/components/clinical-dashboard/visual-evidence.tsx b/src/components/clinical-dashboard/visual-evidence.tsx index 8f5deb79b..c6c48f9c4 100644 --- a/src/components/clinical-dashboard/visual-evidence.tsx +++ b/src/components/clinical-dashboard/visual-evidence.tsx @@ -1,7 +1,7 @@ "use client"; import Link from "next/link"; -import { useState } from "react"; +import { type KeyboardEvent, useId, useRef, useState } from "react"; import { CircleAlert, BookOpen, @@ -378,6 +378,8 @@ function EvidenceClaimsList({ rows, renderModel }: { rows: AnswerEvidenceMapRow[ const claimRows = claimRowsForEvidencePanel(rows, renderModel); const directCount = claimRows.filter((row) => supportLabel(row.supportLevel) === "Direct").length; const partialCount = claimRows.filter((row) => supportLabel(row.supportLevel) === "Partial").length; + const claimRowClassName = + "grid min-h-[76px] grid-cols-[auto_auto_minmax(0,1fr)_auto] items-center gap-3 border-b border-[color:var(--border)] px-3 py-3 text-left last:border-b-0"; if (!claimRows.length) { return ; @@ -409,29 +411,52 @@ function EvidenceClaimsList({ rows, renderModel }: { rows: AnswerEvidenceMapRow[
- {claimRows.map((row, index) => ( - - - - - {row.section} - - {row.detail || row.bestLinkedPassage || row.bestSourceLabel} + {claimRows.map((row, index) => { + const content = ( + <> + + + + {row.section} + + {row.detail || row.bestLinkedPassage || row.bestSourceLabel} + - -
); @@ -495,9 +520,40 @@ export function MobileEvidenceSheetContent({ const order = evidenceTabOrder(answer, renderModel); const [selectedTab, setSelectedTab] = useState(() => initialTab ?? null); const activeTab = selectedTab && order.includes(selectedTab) ? selectedTab : order[0]; - const panelIdFor = (tab: EvidenceTabName) => `mobile-evidence-panel-${tab.toLowerCase()}`; + const tabSetId = useId(); + const tabRefs = useRef>>({}); + const tabIdFor = (tab: EvidenceTabName) => `${tabSetId}-mobile-evidence-tab-${tab.toLowerCase()}`; + const panelIdFor = (tab: EvidenceTabName) => `${tabSetId}-mobile-evidence-panel-${tab.toLowerCase()}`; const [added, setAdded] = useState(false); const primarySourceHref = renderModel.primarySources[0]?.href; + + function handleTabKeyDown(event: KeyboardEvent, tab: EvidenceTabName) { + const currentIndex = order.indexOf(tab); + let nextIndex: number; + + switch (event.key) { + case "ArrowRight": + nextIndex = (currentIndex + 1) % order.length; + break; + case "ArrowLeft": + nextIndex = (currentIndex - 1 + order.length) % order.length; + break; + case "Home": + nextIndex = 0; + break; + case "End": + nextIndex = order.length - 1; + break; + default: + return; + } + + event.preventDefault(); + const nextTab = order[nextIndex]; + setSelectedTab(nextTab); + tabRefs.current[nextTab]?.focus(); + } + async function copyEvidence() { if (renderModel.quoteCards.length) { onCopyQuotes(); @@ -534,11 +590,16 @@ export function MobileEvidenceSheetContent({ key={tab} type="button" role="tab" + ref={(node) => { + tabRefs.current[tab] = node; + }} aria-selected={selected} aria-controls={panelIdFor(tab)} - id={`mobile-evidence-tab-${tab.toLowerCase()}`} + id={tabIdFor(tab)} + tabIndex={selected ? 0 : -1} data-testid={`mobile-evidence-tab-${tab.toLowerCase()}`} onClick={() => setSelectedTab(tab)} + onKeyDown={(event) => handleTabKeyDown(event, tab)} className={cn( "inline-flex min-h-11 items-center gap-1.5 rounded-md border px-3 text-xs font-semibold transition", selected @@ -563,7 +624,7 @@ export function MobileEvidenceSheetContent({ key={tab} id={panelIdFor(tab)} role="tabpanel" - aria-labelledby={`mobile-evidence-tab-${tab.toLowerCase()}`} + aria-labelledby={tabIdFor(tab)} data-testid={`mobile-evidence-panel-${tab.toLowerCase()}`} hidden={!selected} className="min-h-[220px]" diff --git a/src/components/differentials/differential-presentation-workflow-page.tsx b/src/components/differentials/differential-presentation-workflow-page.tsx index 0bb0aa870..001b4f76a 100644 --- a/src/components/differentials/differential-presentation-workflow-page.tsx +++ b/src/components/differentials/differential-presentation-workflow-page.tsx @@ -192,7 +192,9 @@ function DesktopComparisonTable({
diff --git a/src/components/differentials/differential-stream-page.tsx b/src/components/differentials/differential-stream-page.tsx index 6d93e25c1..9219f8d37 100644 --- a/src/components/differentials/differential-stream-page.tsx +++ b/src/components/differentials/differential-stream-page.tsx @@ -59,7 +59,7 @@ export function DifferentialStreamPage({ stream, query = "" }: DifferentialStrea Diagnosis-focused differential content
- {copy.cards.map((card) => ( + {copy.cards.map((card, index) => ( {card.title}

{card.description}

    - {card.examples.map((example) => ( -
  • + {card.examples.map((example, index) => ( +
  • {example}
  • diff --git a/src/components/forms/form-detail-page.tsx b/src/components/forms/form-detail-page.tsx index e2f321327..3799bc1fc 100644 --- a/src/components/forms/form-detail-page.tsx +++ b/src/components/forms/form-detail-page.tsx @@ -494,10 +494,6 @@ export function FormDetailPage({ form }: { form: FormRecord }) { const relatedTags = useMemo(() => [...(form.tags ?? []), ...(form.catchments ?? [])].slice(0, 8), [form]); function goBack() { - if (window.history.length > 1) { - router.back(); - return; - } router.push(appModeHomeHref("forms", { focus: true })); } diff --git a/src/components/route-error-boundary.tsx b/src/components/route-error-boundary.tsx index 04f053b3a..1ef72a98d 100644 --- a/src/components/route-error-boundary.tsx +++ b/src/components/route-error-boundary.tsx @@ -41,7 +41,7 @@ export function RouteErrorBoundary({ const headingRef = useRef(null); useEffect(() => { console.error(logLabel, error); - headingRef.current?.focus(); + headingRef.current?.focus({ preventScroll: true }); }, [error, logLabel]); return ( @@ -64,7 +64,9 @@ export function RouteErrorBoundary({ {title} -

    {description}

    +

    + {description} +

    {error.digest && (
    diff --git a/src/components/services/services-navigator-page.tsx b/src/components/services/services-navigator-page.tsx index 0459da34e..e664497bb 100644 --- a/src/components/services/services-navigator-page.tsx +++ b/src/components/services/services-navigator-page.tsx @@ -332,6 +332,7 @@ function RightRail({ type="button" onClick={clearSelectedServices} disabled={selected.length === 0} + title={selected.length === 0 ? "No selected services to clear" : "Clear selected services"} > Clear diff --git a/src/components/therapy-compass/bindings.tsx b/src/components/therapy-compass/bindings.tsx index 8b9491e17..eafee0256 100644 --- a/src/components/therapy-compass/bindings.tsx +++ b/src/components/therapy-compass/bindings.tsx @@ -229,7 +229,6 @@ export function TcProvider({ children }: { children: ReactNode }) { const router = useRouter(); const pathname = usePathname(); const searchParams = useSearchParams(); - const { data, loading, error, retry } = useTherapyData(); const therapies = useMemo(() => data?.therapies ?? [], [data]); const pathways = useMemo(() => data?.pathways ?? [], [data]); diff --git a/src/components/therapy-compass/nav.tsx b/src/components/therapy-compass/nav.tsx index c5161d859..6ba1eb2fd 100644 --- a/src/components/therapy-compass/nav.tsx +++ b/src/components/therapy-compass/nav.tsx @@ -36,7 +36,7 @@ export function TherapyCompassNav() { - + Therapy diff --git a/src/components/therapy-compass/screens/brief-screen.tsx b/src/components/therapy-compass/screens/brief-screen.tsx index e3ac9a866..3b2e1b3a8 100644 --- a/src/components/therapy-compass/screens/brief-screen.tsx +++ b/src/components/therapy-compass/screens/brief-screen.tsx @@ -75,7 +75,7 @@ export function BriefScreen() { Fast scripts and steps drawn from each record’s delivery fields.

    -
    +
    -
    +
    {/* decision summary */}
    {/* tabs */} -
    +
    diff --git a/src/components/therapy-compass/screens/detail-screen.tsx b/src/components/therapy-compass/screens/detail-screen.tsx index 04a479bfa..33788ada5 100644 --- a/src/components/therapy-compass/screens/detail-screen.tsx +++ b/src/components/therapy-compass/screens/detail-screen.tsx @@ -96,7 +96,7 @@ export function DetailScreen() {
    {/* QUICK TILES */} -
    +
    {/* RIGHT RAIL */} -
    +
    At a glance diff --git a/src/components/therapy-compass/screens/home-screen.tsx b/src/components/therapy-compass/screens/home-screen.tsx index 4b67e09d3..27455d74c 100644 --- a/src/components/therapy-compass/screens/home-screen.tsx +++ b/src/components/therapy-compass/screens/home-screen.tsx @@ -51,12 +51,15 @@ export function HomeScreen() { What therapy are you looking for?

    - Search {b.therapies.length || "200+"} source-grounded therapy records by problem, symptom, skill or population - — or jump into a clinical pathway. + Search {b.therapies.length} source-grounded therapy {b.therapies.length === 1 ? "record" : "records"} by + problem, symptom, skill or population — or jump into a clinical pathway.

    -
    +
    {/* quick tools */} -
    +
    -
    +
    {pathways.map((p) => (
    -
    +
    {featuredList.map((t) => (
    -
    +
    -
    +
    + + Additional sort options are coming soon. + + + Adding favourites from this screen is coming soon. +
    @@ -445,13 +454,19 @@ export function FavouritesHub({
    + + Creating favourite sets is coming soon. +
    diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 14e631659..d1b0469c1 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -43,7 +43,7 @@ import { visibleAppModeDefinitions, type AppModeId, } from "@/lib/app-modes"; -import { isLocalNoAuthMode } from "@/lib/client-env"; +import { isLocalNoAuthMode, resolveClientDemoMode } from "@/lib/client-env"; import { documentsSearchHref } from "@/lib/document-flow-routes"; import { differentialsMobileCompareAddonSlotId, modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer"; import { readSearchNavigationContext, type SearchNavigationOptions } from "@/lib/search-navigation-context"; @@ -314,7 +314,11 @@ function GlobalStandaloneSearchShellClient({ // Recent queries are owner-scoped session state (2026-07-13 audit, finding 4): // the legacy unscoped localStorage value could resurface another account's // clinical queries on a shared workstation, so it is deleted, never read. - const clientDemoMode = !auth.isConfigured || process.env.NEXT_PUBLIC_DEMO_MODE === "true" || isLocalNoAuthMode(); + const clientDemoMode = resolveClientDemoMode({ + explicitDemoMode: process.env.NEXT_PUBLIC_DEMO_MODE === "true", + authUnavailableFallback: !auth.isConfigured, + localNoAuthMode: isLocalNoAuthMode(), + }); const recentQueriesOwnerId = auth.session?.user.id ?? (clientDemoMode ? demoRecentQueryOwnerId : null); useEffect(() => { @@ -453,7 +457,11 @@ function GlobalStandaloneSearchShellClient({ if (!chromeVisible) { return (
    -
    +
    {children}
    @@ -579,7 +587,7 @@ function GlobalStandaloneSearchShellClient({ // auto, which turns #main-content into the sticky scrollport while the // window does the actual scrolling — silently disabling every // position:sticky descendant (e.g. the document viewer rail). - "min-w-0 focus:outline-none max-sm:flex max-sm:min-h-0 max-sm:flex-1 max-sm:flex-col max-sm:overflow-x-hidden max-sm:overflow-y-auto max-sm:overscroll-contain max-sm:[-webkit-overflow-scrolling:touch] sm:min-h-[calc(100dvh-4rem)] sm:overflow-x-clip", + "min-w-0 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)] max-sm:flex max-sm:min-h-0 max-sm:flex-1 max-sm:flex-col max-sm:overflow-x-hidden max-sm:overflow-y-auto max-sm:overscroll-contain max-sm:[-webkit-overflow-scrolling:touch] sm:min-h-[calc(100dvh-4rem)] sm:overflow-x-clip", !reservesFloatingComposer ? "max-sm:pb-[var(--mobile-composer-reserve)] sm:pb-8" : searchMode === "answer" diff --git a/src/lib/client-env.ts b/src/lib/client-env.ts index a23d5a66d..27009bae6 100644 --- a/src/lib/client-env.ts +++ b/src/lib/client-env.ts @@ -3,6 +3,20 @@ export function isLocalNoAuthMode() { return process.env.NODE_ENV !== "production" && process.env.NEXT_PUBLIC_LOCAL_NO_AUTH === "true"; } +export function resolveClientDemoMode({ + explicitDemoMode, + authUnavailableFallback, + localNoAuthMode, + environment = process.env.NODE_ENV, +}: { + explicitDemoMode: boolean; + authUnavailableFallback: boolean; + localNoAuthMode: boolean; + environment?: string; +}) { + return explicitDemoMode || (environment !== "production" && (authUnavailableFallback || localNoAuthMode)); +} + export function publicUploadsEnabled() { return process.env.NEXT_PUBLIC_PUBLIC_UPLOADS_ENABLED === "true"; } diff --git a/tests/favourites-demo-boundary.test.ts b/tests/favourites-demo-boundary.test.ts index a77acd413..0d2761950 100644 --- a/tests/favourites-demo-boundary.test.ts +++ b/tests/favourites-demo-boundary.test.ts @@ -1,6 +1,7 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; +import { resolveClientDemoMode } from "@/lib/client-env"; const routeSource = readFileSync(new URL("../src/app/favourites/page.tsx", import.meta.url), "utf8"); const librarySource = readFileSync( @@ -29,13 +30,40 @@ describe("favourites demo-data boundary", () => { expect(dashboardSource).toContain("demoMode={clientDemoMode}"); expect(hubSource).toContain("...(demoMode ? favouriteItems : [])"); expect(hubSource).not.toContain("[...favouriteItems, ...savedRegistryFavourites]"); - expect(hubSource).toContain('title="Additional sort options are coming soon"'); - expect(hubSource).toContain('title="Adding favourites from this screen is coming soon"'); - expect(hubSource).toContain('title="Creating favourite sets is coming soon"'); + expect(hubSource).toContain('aria-describedby="favourites-sort-unavailable"'); + expect(hubSource).toContain('aria-describedby="favourites-add-unavailable"'); + expect(hubSource).toContain('aria-describedby="favourites-new-set-unavailable"'); expect(hubSource).toContain("Browse sets"); expect(dashboardSource).not.toContain("Favourite creation is ready to connect."); expect(globalShellSource).toContain("demoMode={clientDemoMode}"); expect(universalSearchSource).toContain("...(demoMode ? favouriteItems : [])"); expect(universalSearchSource).not.toContain("[...favouriteItems, ...savedRegistryFavourites]"); }); + + it("fails closed in production while preserving explicit and non-production demo modes", () => { + expect( + resolveClientDemoMode({ + explicitDemoMode: false, + authUnavailableFallback: true, + localNoAuthMode: false, + environment: "production", + }), + ).toBe(false); + expect( + resolveClientDemoMode({ + explicitDemoMode: true, + authUnavailableFallback: true, + localNoAuthMode: false, + environment: "production", + }), + ).toBe(true); + expect( + resolveClientDemoMode({ + explicitDemoMode: false, + authUnavailableFallback: true, + localNoAuthMode: false, + environment: "development", + }), + ).toBe(true); + }); }); diff --git a/tests/favourites-hub-unavailable-controls.dom.test.tsx b/tests/favourites-hub-unavailable-controls.dom.test.tsx new file mode 100644 index 000000000..0bb48ecf4 --- /dev/null +++ b/tests/favourites-hub-unavailable-controls.dom.test.tsx @@ -0,0 +1,33 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import { FavouritesHub } from "@/components/clinical-dashboard/favourites-hub"; + +vi.mock("@/components/clinical-dashboard/use-saved-registry-favourites", () => ({ + useSavedRegistryFavourites: () => [], +})); + +describe("FavouritesHub unavailable controls", () => { + it("keeps unavailable actions focusable and exposes their reasons", () => { + render( undefined} demoMode={false} />); + + const recent = screen.getByRole("button", { name: "Recent" }); + const add = screen.getByRole("button", { name: /Add favourite/ }); + const newSet = screen.getByRole("button", { name: "New set" }); + + expect(recent).toBeEnabled(); + expect(recent).toHaveAttribute("aria-disabled", "true"); + expect(recent).toHaveAccessibleDescription("Additional sort options are coming soon."); + expect(add).toBeEnabled(); + expect(add).toHaveAttribute("aria-disabled", "true"); + expect(add).toHaveAccessibleDescription("Adding favourites from this screen is coming soon."); + expect(newSet).toBeEnabled(); + expect(newSet).toHaveAttribute("aria-disabled", "true"); + expect(newSet).toHaveAccessibleDescription("Creating favourite sets is coming soon."); + + for (const control of [recent, add, newSet]) { + control.focus(); + expect(document.activeElement).toBe(control); + } + }); +}); diff --git a/tests/ui-route-coverage.spec.ts b/tests/ui-route-coverage.spec.ts index 74b0276a6..e90cb6120 100644 --- a/tests/ui-route-coverage.spec.ts +++ b/tests/ui-route-coverage.spec.ts @@ -370,7 +370,11 @@ test.describe("previously uncovered production routes", () => { await skipLink.focus(); await expect(skipLink).toBeVisible(); await skipLink.press("Enter"); - await expect(currentPage.locator("#main-content")).toBeFocused(); + const mainContent = currentPage.locator("#main-content"); + await expect(mainContent).toBeFocused(); + await expect + .poll(() => mainContent.evaluate((element) => window.getComputedStyle(element).outlineStyle)) + .not.toBe("none"); }, ); }); From baeced6f238379411d9a78149b6d4e5f35470f8a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 18 Jul 2026 22:02:05 +0800 Subject: [PATCH 06/21] fix: restrict favourite selection to wide layouts --- .../favourites-command-library-page.tsx | 2 +- tests/favourites-demo-boundary.test.ts | 16 +++++++++ tests/ui-smoke.spec.ts | 33 +++++++++++++++++-- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/components/clinical-dashboard/favourites-command-library-page.tsx b/src/components/clinical-dashboard/favourites-command-library-page.tsx index b43e90e4c..dcec32318 100644 --- a/src/components/clinical-dashboard/favourites-command-library-page.tsx +++ b/src/components/clinical-dashboard/favourites-command-library-page.tsx @@ -698,7 +698,7 @@ function FavouritesTable({ className={cn( "relative h-14 transition hover:bg-[color:var(--surface-subtle)]", selected && - "bg-[color:var(--clinical-accent-soft)]/45 shadow-[inset_3px_0_0_var(--clinical-accent)]", + "xl:bg-[color:var(--clinical-accent-soft)]/45 xl:shadow-[inset_3px_0_0_var(--clinical-accent)]", )} > diff --git a/tests/favourites-demo-boundary.test.ts b/tests/favourites-demo-boundary.test.ts index 0d2761950..f87d049ad 100644 --- a/tests/favourites-demo-boundary.test.ts +++ b/tests/favourites-demo-boundary.test.ts @@ -21,6 +21,10 @@ const universalSearchSource = readFileSync( new URL("../src/components/clinical-dashboard/universal-search-command-surface.tsx", import.meta.url), "utf8", ); +const mobileCardSource = librarySource.slice( + librarySource.indexOf("function FavouriteMobileCard"), + librarySource.indexOf("function FavouritesTable"), +); describe("favourites demo-data boundary", () => { it("passes trusted server demo state and never merges prototype favourites into live mode unconditionally", () => { @@ -66,4 +70,16 @@ describe("favourites demo-data boundary", () => { }), ).toBe(true); }); + + it("limits item selection to xl tables and keeps mobile cards action-only", () => { + expect(mobileCardSource).toContain("function FavouriteMobileCard({ item }: { item: FavouriteItem })"); + expect(mobileCardSource).not.toContain("aria-pressed"); + expect(mobileCardSource).not.toContain("onSelect"); + expect(mobileCardSource).toContain(""); + expect(librarySource).toContain('"hidden min-w-0 max-w-full items-center gap-2.5 rounded-md text-left xl:flex"'); + expect(librarySource).toContain('"block min-w-0 max-w-full rounded-md text-left xl:hidden"'); + expect(librarySource).toContain( + '"xl:bg-[color:var(--clinical-accent-soft)]/45 xl:shadow-[inset_3px_0_0_var(--clinical-accent)]"', + ); + }); }); diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index e84f9c199..82a78d35b 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -2684,15 +2684,42 @@ test.describe("Clinical KB UI smoke coverage", () => { await expect(moreActions).toBeFocused(); }); - test("favourites mobile cards keep their links and menus as separate native controls", async ({ page }) => { + test("favourites disable item selection below xl while keeping navigation and actions", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await mockDemoApi(page); await gotoApp(page, "/favourites"); const hub = page.getByTestId("favourites-hub"); await expect(hub.locator('article[role="button"]')).toHaveCount(0); - await expect(hub.getByRole("link", { name: "Open Acamprosate renal screen" })).toBeVisible(); - await expect(hub.getByRole("button", { name: "More actions for Acamprosate renal screen" })).toBeVisible(); + const card = hub.locator("article").filter({ hasText: "Acamprosate renal screen" }); + const openItem = card.getByRole("link", { name: "Open Acamprosate renal screen" }); + const moreActions = card.getByRole("button", { name: "More actions for Acamprosate renal screen" }); + + await expect(card).toBeVisible(); + await expect(card.locator("button[aria-pressed]")).toHaveCount(0); + await expectMinTouchTarget(openItem); + await expectMinTouchTarget(moreActions); + await expectNoPageHorizontalOverflow(page); + + await page.setViewportSize({ width: 1180, height: 820 }); + const row = page.getByTestId("favourite-row-acamprosate-renal-screen"); + await expect(row).toBeVisible(); + await expect(row.locator("button[aria-pressed]")).toBeHidden(); + await expect(row.locator("td").first().getByRole("link")).toBeVisible(); + await expect(row.getByRole("link", { name: "Open Acamprosate renal screen" })).toBeVisible(); + await expect(row.getByRole("button", { name: "More actions for Acamprosate renal screen" })).toBeVisible(); + + await page.setViewportSize({ width: 1536, height: 900 }); + const selectItem = row.locator("button[aria-pressed]"); + await expect(selectItem).toBeVisible(); + await selectItem.click(); + await expect(page.getByTestId("favourites-item-workspace")).toBeVisible(); + + await page.setViewportSize({ width: 1180, height: 820 }); + await expect(page.getByTestId("favourites-item-workspace")).toBeHidden(); + await expect(selectItem).toBeHidden(); + await expect(row).not.toHaveClass(/(^|\s)bg-\[/); + await expectNoPageHorizontalOverflow(page); }); test("app mode menu supports keyboard navigation without removed prototype modes", async ({ page }) => { From 4b6b83bce33d2f5f4432367048b25f27ac63f2b1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 18 Jul 2026 15:55:36 +0000 Subject: [PATCH 07/21] fix: keep therapy count loading-safe and local no-auth uploads writable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate Therapy Compass home on an empty loading catalogue so it never advertises “0 records”, and resolve upload read-only without treating local no-auth as demo lockout. Co-authored-by: BigSimmo --- src/components/ClinicalDashboard.tsx | 8 ++++- .../therapy-compass/data/use-therapy-data.ts | 5 +++ .../therapy-compass/screens/home-screen.tsx | 13 +++++-- .../audit-navigation-auth-regressions.test.ts | 9 +++++ tests/favourites-demo-boundary.test.ts | 23 ++++++++++++ ...therapy-compass-data-recovery.dom.test.tsx | 36 +++++++++++++++++++ 6 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 643532a72..2919b29d5 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -838,7 +838,13 @@ export function ClinicalDashboard({ setAnswerThreadBootstrapped(true); }); }, [answerThreadOwnerId, authStatus]); - const uploadReadOnlyMode = clientDemoMode; + // Local no-auth still has private upload APIs (`canUsePrivateApis`); do not lock the + // upload drawer just because `resolveClientDemoMode` treats no-auth as demo for favourites. + const uploadReadOnlyMode = resolveClientDemoMode({ + explicitDemoMode, + authUnavailableFallback: browserAuthUnavailableDemoFallback, + localNoAuthMode: false, + }); const localDevCanAttemptPrivateApis = process.env.NODE_ENV !== "production" && hasReadyPublicSearchSetup(setupChecks); const canUsePublicSearchApis = localProjectReady && hasReadyPublicSearchSetup(setupChecks); const canUseDegradedLocalSearchApis = diff --git a/src/components/therapy-compass/data/use-therapy-data.ts b/src/components/therapy-compass/data/use-therapy-data.ts index f04d4a702..1b83416d4 100644 --- a/src/components/therapy-compass/data/use-therapy-data.ts +++ b/src/components/therapy-compass/data/use-therapy-data.ts @@ -13,6 +13,11 @@ const BASE = "/therapy-compass-data"; // shared across every screen. let cache: Promise | null = null; +/** Test helper: drop the session-scoped dataset cache between cases. */ +export function clearTherapyDataCache() { + cache = null; +} + async function fetchJson(path: string): Promise { const res = await fetch(path); if (!res.ok) throw new Error(`Failed to load ${path}: ${res.status}`); diff --git a/src/components/therapy-compass/screens/home-screen.tsx b/src/components/therapy-compass/screens/home-screen.tsx index 27455d74c..a7f1b0655 100644 --- a/src/components/therapy-compass/screens/home-screen.tsx +++ b/src/components/therapy-compass/screens/home-screen.tsx @@ -7,6 +7,7 @@ import { commandControl, linkButton } from "../controls"; import type { Therapy } from "../data/types"; import { ChevronRightIcon, CompassIcon, FileTextIcon, PathwayIcon, ScaleIcon, SearchIcon, SparkleIcon } from "../icons"; import { s } from "../style-utils"; +import { LoadingState } from "../ui"; const SUGGESTIONS = [ "Anxiety in outpatient care", @@ -28,10 +29,19 @@ export function HomeScreen() { const b = useTcBindings(); const [query, setLocalQuery] = useState(""); + // First paint / slow fetch: therapies is [] while loading — avoid advertising “0 records”. + if (b.loading && b.therapies.length === 0) { + return ; + } + const bySlug = new Map(b.therapies.map((t) => [t.slug, t])); const featured: Therapy[] = FEATURED_SLUGS.map((sl) => bySlug.get(sl)).filter((t): t is Therapy => Boolean(t)); const featuredList = (featured.length ? featured : b.therapies).slice(0, 6); const pathways = b.pathways.slice(0, 3); + const therapyCountCopy = + b.therapies.length === 0 + ? "Search source-grounded therapy records by problem, symptom, skill or population — or jump into a clinical pathway." + : `Search ${b.therapies.length} source-grounded therapy ${b.therapies.length === 1 ? "record" : "records"} by problem, symptom, skill or population — or jump into a clinical pathway.`; const submit = () => b.submitQuery(query); @@ -51,8 +61,7 @@ export function HomeScreen() { What therapy are you looking for?

    - Search {b.therapies.length} source-grounded therapy {b.therapies.length === 1 ? "record" : "records"} by - problem, symptom, skill or population — or jump into a clinical pathway. + {therapyCountCopy}

    diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts index 73973d114..0459ee460 100644 --- a/tests/audit-navigation-auth-regressions.test.ts +++ b/tests/audit-navigation-auth-regressions.test.ts @@ -90,6 +90,15 @@ describe("audit navigation and auth regressions", () => { }); it("gates private polling and mutations on local readiness plus authenticated status", () => { + const uploadReadOnlyContract = sourceSegment( + clinicalDashboardSource, + "const uploadReadOnlyMode =", + "const canUsePrivateApis =", + ); + expect(uploadReadOnlyContract).toContain("const uploadReadOnlyMode = resolveClientDemoMode({"); + expect(uploadReadOnlyContract).toContain("localNoAuthMode: false"); + expect(uploadReadOnlyContract).not.toMatch(/const uploadReadOnlyMode = clientDemoMode\b/); + const privateCapabilityContract = sourceSegment( clinicalDashboardSource, "const canUsePrivateApis =", diff --git a/tests/favourites-demo-boundary.test.ts b/tests/favourites-demo-boundary.test.ts index 0d2761950..85157fa8f 100644 --- a/tests/favourites-demo-boundary.test.ts +++ b/tests/favourites-demo-boundary.test.ts @@ -66,4 +66,27 @@ describe("favourites demo-data boundary", () => { }), ).toBe(true); }); + + it("keeps upload read-only independent of local no-auth demo treatment", () => { + // Local no-auth is demo for favourites/recent-query owners, but uploads must stay writable. + expect( + resolveClientDemoMode({ + explicitDemoMode: false, + authUnavailableFallback: false, + localNoAuthMode: true, + environment: "development", + }), + ).toBe(true); + expect( + resolveClientDemoMode({ + explicitDemoMode: false, + authUnavailableFallback: false, + localNoAuthMode: false, + environment: "development", + }), + ).toBe(false); + expect(dashboardSource).toContain("localNoAuthMode: false"); + expect(dashboardSource).toMatch(/const uploadReadOnlyMode = resolveClientDemoMode\(\{/); + expect(dashboardSource).not.toMatch(/const uploadReadOnlyMode = clientDemoMode\b/); + }); }); diff --git a/tests/therapy-compass-data-recovery.dom.test.tsx b/tests/therapy-compass-data-recovery.dom.test.tsx index d69965cff..32c14907b 100644 --- a/tests/therapy-compass-data-recovery.dom.test.tsx +++ b/tests/therapy-compass-data-recovery.dom.test.tsx @@ -2,6 +2,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { TherapyCompassWorkspace } from "@/components/therapy-compass"; +import { clearTherapyDataCache } from "@/components/therapy-compass/data/use-therapy-data"; import { HomeScreen } from "@/components/therapy-compass/screens/home-screen"; vi.mock("next/navigation", () => ({ @@ -28,10 +29,45 @@ function response(body: unknown, ok = true, status = 200) { } afterEach(() => { + clearTherapyDataCache(); vi.unstubAllGlobals(); }); describe("Therapy Compass required data recovery", () => { + it("does not advertise a zero therapy count while the catalogue is still loading", async () => { + let release!: (value: unknown) => void; + const therapiesGate = new Promise((resolve) => { + release = resolve; + }); + const fetchMock = vi.fn(async (input: RequestInfo | URL) => { + const path = String(input); + if (path.endsWith("/therapies.json")) { + await therapiesGate; + return response([therapy]); + } + if (path.endsWith("/pathways.json")) return response([]); + if (path.endsWith("/reference.json")) return response({}); + throw new Error(`Unexpected fetch: ${path}`); + }); + vi.stubGlobal("fetch", fetchMock); + + render( + + + , + ); + + expect(await screen.findByRole("status")).toHaveTextContent("Loading therapy library…"); + expect(screen.queryByText(/Search 0 source-grounded therapy/)).not.toBeInTheDocument(); + expect(screen.queryByRole("heading", { name: "What therapy are you looking for?" })).not.toBeInTheDocument(); + + release(undefined); + + expect(await screen.findByRole("heading", { name: "What therapy are you looking for?" })).toBeInTheDocument(); + expect(screen.getByText(/Search 1 source-grounded therapy record by/)).toBeInTheDocument(); + expect(screen.queryByText(/Search 0 source-grounded therapy/)).not.toBeInTheDocument(); + }); + it("shows an honest load error, retries all required files, and recovers", async () => { let failTherapies = true; const fetchMock = vi.fn(async (input: RequestInfo | URL) => { From 5a52cbb4cd83b49807c4422bc425f278fc253b83 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 18 Jul 2026 15:59:59 +0000 Subject: [PATCH 08/21] style: prettier therapy home loading-safe count copy Co-authored-by: BigSimmo --- src/components/therapy-compass/screens/home-screen.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/therapy-compass/screens/home-screen.tsx b/src/components/therapy-compass/screens/home-screen.tsx index a7f1b0655..8bc001ff6 100644 --- a/src/components/therapy-compass/screens/home-screen.tsx +++ b/src/components/therapy-compass/screens/home-screen.tsx @@ -60,9 +60,7 @@ export function HomeScreen() { > What therapy are you looking for? -

    - {therapyCountCopy} -

    +

    {therapyCountCopy}

    Date: Sun, 19 Jul 2026 00:03:28 +0800 Subject: [PATCH 09/21] fix: resolve final design audit regressions --- docs/branch-review-ledger.md | 2 +- docs/design-audit-2026-07-17.md | 8 +- src/components/ClinicalDashboard.tsx | 73 ++++++++++++++++--- src/components/applications-launcher-page.tsx | 11 +-- .../therapy-compass/screens/home-screen.tsx | 3 + .../audit-navigation-auth-regressions.test.ts | 8 +- ...therapy-compass-data-recovery.dom.test.tsx | 4 + tests/ui-smoke.spec.ts | 1 + tests/ui-tools.spec.ts | 2 + 9 files changed, 85 insertions(+), 27 deletions(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index fea8c3898..7c56dab8d 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -608,4 +608,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-17 | codex/design-audit-20260716 | 47b58ddf95826231a6057b400e7f601c403a129d + reviewed working diff | exhaustive design, accessibility, UX, interaction, route, responsive, HTML/CSS/JS/TSX, and test-coverage audit with remediation | No P0. Fixed two P1 trust/responsive defects and the confirmed P2/P3 set: false Favourites provenance/demo leakage/dead controls, Therapy phone reflow/load recovery/contrast, semantic tabs/search/filters, error focus/theme, deterministic Forms back navigation, honest unavailable actions, application dialog semantics, duplicate differential keys, skip-link target, Playwright project isolation, and route coverage. External visual-target fidelity remains unavailable. | Static 261-file/~67,944-line inventory; independent combined-diff review; focused Vitest 21/21; jsdom 24/24; architecture 6/6; accessibility browser 5/5; focused route/browser recovery; lint; TypeScript; runtime/workflow/sitemap/brand/type/icon checks; production build with 1,043 pages and client-secret scan; `git diff --check`. Canonical aggregate Vitest was Windows-runner inconclusive and full UI was not repeated after focused remediation. No provider/API checks. | | 2026-07-18 | codex/main-merge-51278-final-20260718-late | 45fa3c6c7, 14a0a898c | merge integration of 51278a70d onto fresh origin/main | Replayed the requested historical design-audit commit onto fresh `origin/main`. Kept current-main versions for five conflicts, including the regenerated drift manifest and current schema assertions. Fixed the duplicate sitemap-generator declaration, corrected redirect-section coverage, and restored the current `Clinical Guide` UI contract. | `git diff --check` and `npm run sitemap:check` passed. `npm run test` was attempted twice but blocked by the repository-wide heavy-command lock held by a separate Playwright worktree; no provider-backed checks ran. | | 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. | -| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | c5e7b96c3 + reviewed follow-up diff | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and disposition of all four CodeRabbit comments. The follow-up made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, and removed the sole zero-warning CI lint failure. External design-target fidelity remains unverified without an approved target. | Local PR plan: formatting, full ESLint, TypeScript, 309 files / 2,797 Vitest tests, webpack production build with 1,674 pages and client-secret scan, and 36 offline RAG fixtures across 21 suites. Initial hosted Production UI passed on `8071a6704`; focused review-fix Vitest 3/3 and zero-warning ESLint passed; formatting and diff checks passed. A post-fix local typecheck was deferred because another registered worktree held the heavyweight lock; exact-head hosted static/type/UI checks remain required after push. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. | +| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | c5e7b96c3 + baeced6f2 + reviewed follow-up diff | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. Initial exact-head hosted checks, including Production UI, passed before these final follow-ups; exact-head hosted static/type/UI checks remain required after the final push. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. | diff --git a/docs/design-audit-2026-07-17.md b/docs/design-audit-2026-07-17.md index 2eb0cfde8..a8654592f 100644 --- a/docs/design-audit-2026-07-17.md +++ b/docs/design-audit-2026-07-17.md @@ -29,10 +29,10 @@ This pass deliberately built on the 2026-07-15 live design audit instead of repe | DQA-08 | P2 | Evidence records with no URL were still keyboard-focusable `href="#"` links announced as “Open source”. | Missing-source rows now render as non-links with unavailable semantics; only valid destinations render a Link. | `visual-evidence.tsx`, `visual-evidence-tabs.dom.test.tsx` | | DQA-09 | P2 | Therapy cards presented an enabled Favourite button with no handler or state change. | The unavailable feature is now disabled and labelled as coming soon, preventing false save confirmation. | `therapy-card.tsx` | | DQA-10 | P2 | Route/global error fallbacks did not reliably move focus or announce a route failure; the global fallback was hard-coded light-only. | Added programmatic heading focus and alert announcement, and made the self-contained global fallback system-colour, dark-mode, and forced-colour aware. | `route-error-boundary.tsx`, `global-error.tsx`, route error tests | -| DQA-11 | P2 | Clinical Notes, Visual Evidence, and upload/index tab interfaces lacked complete roving keyboard behavior and reciprocal tab/panel relationships. | Added stable unique IDs, `aria-controls`/`aria-labelledby`, roving `tabIndex`, and Left/Right/Home/End auto-activation. | `ClinicalDashboard.tsx`, `answer-result-surface.tsx`, `visual-evidence.tsx`, UI/DOM tests | +| DQA-11 | P2 | Clinical Notes, Visual Evidence, and upload/index tab interfaces lacked complete roving keyboard behavior and reciprocal tab/panel relationships. | Added stable unique IDs, `aria-controls`/`aria-labelledby`, roving `tabIndex`, and Left/Right/Home/End auto-activation. Upload/indexing uses tab panels only with its visible mobile tablist and named regions when desktop renders every section. | `ClinicalDashboard.tsx`, `answer-result-surface.tsx`, `visual-evidence.tsx`, UI/DOM tests | | DQA-12 | P2 | Universal search visually highlighted an item without exposing it through `aria-activedescendant`. | The combobox now points to the active option and tests verify the relationship through keyboard navigation. | `master-search-header.tsx`, `universal-search-command-surface.tsx`, `ui-universal-search.spec.ts` | -| DQA-13 | P2 | Differential filters and several selector groups behaved as toggles but lacked grouped/pressed semantics. | Added labelled groups and stateful `aria-pressed` semantics, with browser accessibility assertions. | `differentials-home.tsx`, `evidence-panels.tsx`, `ui-accessibility.spec.ts` | -| DQA-14 | P2 | Therapy Compass silently swallowed data-load failure and still implied a fabricated `200+` catalogue count. | Added an explicit failure state and Retry action, removed fabricated totals, and retained real successful/empty states. The final integration tests the routed workspace that replaced the legacy single-page wrapper. | `use-therapy-data.ts`, `workspace.tsx`, `therapy-compass-data-recovery.dom.test.tsx` | +| DQA-13 | P2 | Differential filters and several selector groups behaved as toggles but lacked grouped/pressed semantics; the Tools result set retained orphaned tab-panel semantics after its controls became pressed filters. | Added labelled groups and stateful `aria-pressed` semantics, and exposed Tools results as a labelled group, with browser accessibility assertions. | `differentials-home.tsx`, `evidence-panels.tsx`, `applications-launcher-page.tsx`, `ui-accessibility.spec.ts`, `ui-tools.spec.ts` | +| DQA-14 | P2 | Therapy Compass silently swallowed data-load failure, rendered an empty `0`-record catalogue while loading, and still implied a fabricated `200+` catalogue count. | Added distinct loading and failure states, a Retry action, removed fabricated totals, and retained real successful/empty states. The final integration tests the routed workspace that replaced the legacy single-page wrapper. | `use-therapy-data.ts`, `workspace.tsx`, `home-screen.tsx`, `therapy-compass-data-recovery.dom.test.tsx` | | DQA-15 | P2 | Direct-entry Forms “Back to forms” used browser history and could leave the application. | The control now deterministically navigates to `/forms?focus=1`; a DOM regression test verifies the destination. | `form-detail-page.tsx`, `forms-back-navigation.dom.test.tsx` | | DQA-16 | P2 | Firefox/WebKit production projects unintentionally collected advisory `@mockup` tests; two browser helpers also retried hydration inconsistently. | Applied production matchers and `grepInvert` to all required projects and introduced one shared app-mode hydration retry helper. | `playwright.config.ts`, `playwright-app-mode.ts`, `ui-stress.spec.ts`, `ui-tools.spec.ts` | | DQA-17 | P2 | Application/tool cards looked like direct links but prevented navigation to open an internal detail state, obscuring the real action. | Cards are now honest dialog buttons with `aria-haspopup="dialog"`; the dialog contains the actual destination link. | `applications-launcher-page.tsx`, `ui-smoke.spec.ts` | @@ -43,7 +43,7 @@ This pass deliberately built on the 2026-07-15 live design audit instead of repe ## Route and component interaction proof -The new provider-free route matrix covers Therapy Compass, DSM home/compare/differentials, Specifier compare/map, differential diagnosis stream/detail, colour reference, Applications, Medications and differential-presentation redirects, and both document-source aliases at desktop and 390 px. It asserts visible primary content, no document-level horizontal overflow, a representative action, blocked external/API leakage, and route-specific behavior. +The new provider-free route matrix covers Therapy Compass, DSM home/compare/differentials, Specifier compare/map, differential diagnosis stream/detail, colour reference, Applications, Medications and differential-presentation redirects, and the invalid document-source alias at desktop and 390 px. It asserts visible primary content, no document-level horizontal overflow, a representative action, blocked external/API leakage, and route-specific behavior. The valid `/documents/source` case reached the expected redirect request but did not complete its final browser assertion, as recorded below. Additional focused browser proof covered: diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 643532a72..3e50847c1 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -32,6 +32,7 @@ import { useReducer, useRef, useState, + useSyncExternalStore, } from "react"; import { type DocumentDeleteResult } from "@/components/DocumentManagementActions"; import { extractSafetyFindings } from "@/lib/clinical-safety"; @@ -123,6 +124,23 @@ const FavouritesHub = dynamic( () => import("@/components/clinical-dashboard/favourites-hub").then((m) => m.FavouritesHub), { ssr: false }, ); + +const uploadDesktopMediaQuery = "(min-width: 1024px)"; + +function subscribeToUploadDesktopLayout(callback: () => void) { + if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => {}; + const media = window.matchMedia(uploadDesktopMediaQuery); + media.addEventListener("change", callback); + return () => media.removeEventListener("change", callback); +} + +function getUploadDesktopLayoutSnapshot() { + return ( + typeof window !== "undefined" && + typeof window.matchMedia === "function" && + window.matchMedia(uploadDesktopMediaQuery).matches + ); +} const MedicationPrescribingWorkspace = dynamic( () => import("@/components/clinical-dashboard/medication-prescribing-workspace").then( @@ -661,6 +679,11 @@ export function ClinicalDashboard({ const [documentsDrawerMode, setDocumentsDrawerMode] = useState("library"); const [uploadDrawerOpen, setUploadDrawerOpen] = useState(false); const [uploadMobileTab, setUploadMobileTab] = useState("upload"); + const uploadUsesDesktopRegions = useSyncExternalStore( + subscribeToUploadDesktopLayout, + getUploadDesktopLayoutSnapshot, + () => false, + ); const uploadTabRefs = useRef(new Map()); const [documentDrawerStatusFilter, setDocumentDrawerStatusFilter] = useState("indexed"); const [indexingMonitorFilter, setIndexingMonitorFilter] = useState("all"); @@ -4063,14 +4086,19 @@ export function ClinicalDashboard({
    -

    +

    Developer setup status

    @@ -4078,14 +4106,21 @@ export function ClinicalDashboard({
    -

    +

    Clinical upload

    -

    +

    Indexing progress

    -

    +

    Ingestion quality console

    filter.id === activeFilter)?.label ?? mobileFilters.find((filter) => filter.id === activeFilter)?.label; @@ -796,12 +796,7 @@ export function ApplicationsLauncherWorkspace({
    -
    +
    {filteredApps.length === 0 ? (

    {copy.emptyTitle}

    diff --git a/src/components/therapy-compass/screens/home-screen.tsx b/src/components/therapy-compass/screens/home-screen.tsx index 27455d74c..b2b917968 100644 --- a/src/components/therapy-compass/screens/home-screen.tsx +++ b/src/components/therapy-compass/screens/home-screen.tsx @@ -7,6 +7,7 @@ import { commandControl, linkButton } from "../controls"; import type { Therapy } from "../data/types"; import { ChevronRightIcon, CompassIcon, FileTextIcon, PathwayIcon, ScaleIcon, SearchIcon, SparkleIcon } from "../icons"; import { s } from "../style-utils"; +import { LoadingState } from "../ui"; const SUGGESTIONS = [ "Anxiety in outpatient care", @@ -28,6 +29,8 @@ export function HomeScreen() { const b = useTcBindings(); const [query, setLocalQuery] = useState(""); + if (b.loading) return ; + const bySlug = new Map(b.therapies.map((t) => [t.slug, t])); const featured: Therapy[] = FEATURED_SLUGS.map((sl) => bySlug.get(sl)).filter((t): t is Therapy => Boolean(t)); const featuredList = (featured.length ? featured : b.therapies).slice(0, 6); diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts index 73973d114..7041e4913 100644 --- a/tests/audit-navigation-auth-regressions.test.ts +++ b/tests/audit-navigation-auth-regressions.test.ts @@ -130,10 +130,14 @@ describe("audit navigation and auth regressions", () => { expect(clinicalDashboardSource).toContain("aria-selected={active}"); expect(clinicalDashboardSource).toContain("aria-controls={tab.panelId}"); expect(clinicalDashboardSource).toContain("tabIndex={active ? 0 : -1}"); - expect(clinicalDashboardSource).toContain('role="tabpanel"'); + expect(clinicalDashboardSource).toContain('role={uploadUsesDesktopRegions ? "region" : "tabpanel"}'); for (const tab of ["setup", "upload", "jobs", "quality"]) { - expect(clinicalDashboardSource).toContain(`aria-labelledby="dashboard-upload-tab-${tab}"`); + expect(clinicalDashboardSource).toContain(`"dashboard-upload-tab-${tab}"`); } + for (const section of ["setup", "upload", "indexing", "quality"]) { + expect(clinicalDashboardSource).toContain(`id="dashboard-${section}-section-heading"`); + } + expect(clinicalDashboardSource).toContain("subscribeToUploadDesktopLayout"); expect(clinicalDashboardSource).toContain('event.key === "ArrowRight"'); expect(clinicalDashboardSource).toContain('event.key === "ArrowLeft"'); expect(clinicalDashboardSource).toContain('event.key === "Home"'); diff --git a/tests/therapy-compass-data-recovery.dom.test.tsx b/tests/therapy-compass-data-recovery.dom.test.tsx index d69965cff..d964d39a6 100644 --- a/tests/therapy-compass-data-recovery.dom.test.tsx +++ b/tests/therapy-compass-data-recovery.dom.test.tsx @@ -51,6 +51,10 @@ describe("Therapy Compass required data recovery", () => { , ); + expect(screen.getByRole("status")).toHaveTextContent("Loading therapy catalogue"); + expect(screen.queryByText(/Search 0 source-grounded therapy records/)).not.toBeInTheDocument(); + expect(screen.queryByRole("heading", { name: "Frequently used therapies" })).not.toBeInTheDocument(); + expect(await screen.findByRole("alert")).toHaveTextContent("Therapy Compass could not load"); expect(screen.queryByRole("heading", { name: "What therapy are you looking for?" })).not.toBeInTheDocument(); diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 82a78d35b..d06284c2f 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -2728,6 +2728,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await gotoApp(page, "/?mode=answer"); const appModeButton = page.getByRole("button", { name: "Mode Answer" }); + await waitForReactEventHandler(appModeButton, "onClick"); await appModeButton.click(); const appModeMenu = page.getByRole("menu", { name: "Choose app mode" }); await expect(appModeMenu).toBeVisible(); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 27d4df2f0..994a414b6 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -364,6 +364,8 @@ test.describe("Clinical KB tools launcher", () => { await expect(page.getByRole("heading", { level: 1, name: "Tools" })).toBeVisible(); await expect(page.getByRole("region", { name: "Quick tool shortcuts" })).toBeVisible(); await expect(page.getByRole("heading", { name: "All tools" })).toBeVisible(); + await expect(page.locator("#launcher-results-panel")).toHaveAttribute("role", "group"); + await expect(page.locator("#launcher-results-panel")).toHaveAttribute("aria-label", "All tools"); if (viewport.name === "mobile") { await page.getByTestId("application-row-medication-prescribing").click(); const selectedSheet = page.getByRole("dialog", { name: "Medication Prescribing" }); From e5abc6b59572f146fb59d5440178035747ce7474 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:06:08 +0800 Subject: [PATCH 10/21] docs: record final design audit verification --- docs/branch-review-ledger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 248adf498..bbea51f7e 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -610,4 +610,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-17 | codex/design-audit-20260716 | 47b58ddf95826231a6057b400e7f601c403a129d + reviewed working diff | exhaustive design, accessibility, UX, interaction, route, responsive, HTML/CSS/JS/TSX, and test-coverage audit with remediation | No P0. Fixed two P1 trust/responsive defects and the confirmed P2/P3 set: false Favourites provenance/demo leakage/dead controls, Therapy phone reflow/load recovery/contrast, semantic tabs/search/filters, error focus/theme, deterministic Forms back navigation, honest unavailable actions, application dialog semantics, duplicate differential keys, skip-link target, Playwright project isolation, and route coverage. External visual-target fidelity remains unavailable. | Static 261-file/~67,944-line inventory; independent combined-diff review; focused Vitest 21/21; jsdom 24/24; architecture 6/6; accessibility browser 5/5; focused route/browser recovery; lint; TypeScript; runtime/workflow/sitemap/brand/type/icon checks; production build with 1,043 pages and client-secret scan; `git diff --check`. Canonical aggregate Vitest was Windows-runner inconclusive and full UI was not repeated after focused remediation. No provider/API checks. | | 2026-07-18 | codex/main-merge-51278-final-20260718-late | 45fa3c6c7, 14a0a898c | merge integration of 51278a70d onto fresh origin/main | Replayed the requested historical design-audit commit onto fresh `origin/main`. Kept current-main versions for five conflicts, including the regenerated drift manifest and current schema assertions. Fixed the duplicate sitemap-generator declaration, corrected redirect-section coverage, and restored the current `Clinical Guide` UI contract. | `git diff --check` and `npm run sitemap:check` passed. `npm run test` was attempted twice but blocked by the repository-wide heavy-command lock held by a separate Playwright worktree; no provider-backed checks ran. | | 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. | -| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | c5e7b96c3 + baeced6f2 + reviewed follow-up diff | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. Initial exact-head hosted checks, including Production UI, passed before these final follow-ups; exact-head hosted static/type/UI checks remain required after the final push. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. | +| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | c5e7b96c3 + baeced6f2 + reviewed follow-up diff | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. The full local Chromium sweep completed with one failure: the app-mode test clicked before React attached its handler. The existing handler-readiness pattern was applied; two focused rerun attempts were then lock-blocked by another registered worktree, so exact-head hosted Chromium remains the required proof. Initial exact-head hosted checks, including Production UI, passed before these final follow-ups. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. | From 4911bace36754e1a30cca47aaf7d66d4e7ba8f5f Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:07:25 +0800 Subject: [PATCH 11/21] fix: resolve final design audit findings --- docs/branch-review-ledger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index bbea51f7e..e9b66b7c6 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -610,4 +610,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-17 | codex/design-audit-20260716 | 47b58ddf95826231a6057b400e7f601c403a129d + reviewed working diff | exhaustive design, accessibility, UX, interaction, route, responsive, HTML/CSS/JS/TSX, and test-coverage audit with remediation | No P0. Fixed two P1 trust/responsive defects and the confirmed P2/P3 set: false Favourites provenance/demo leakage/dead controls, Therapy phone reflow/load recovery/contrast, semantic tabs/search/filters, error focus/theme, deterministic Forms back navigation, honest unavailable actions, application dialog semantics, duplicate differential keys, skip-link target, Playwright project isolation, and route coverage. External visual-target fidelity remains unavailable. | Static 261-file/~67,944-line inventory; independent combined-diff review; focused Vitest 21/21; jsdom 24/24; architecture 6/6; accessibility browser 5/5; focused route/browser recovery; lint; TypeScript; runtime/workflow/sitemap/brand/type/icon checks; production build with 1,043 pages and client-secret scan; `git diff --check`. Canonical aggregate Vitest was Windows-runner inconclusive and full UI was not repeated after focused remediation. No provider/API checks. | | 2026-07-18 | codex/main-merge-51278-final-20260718-late | 45fa3c6c7, 14a0a898c | merge integration of 51278a70d onto fresh origin/main | Replayed the requested historical design-audit commit onto fresh `origin/main`. Kept current-main versions for five conflicts, including the regenerated drift manifest and current schema assertions. Fixed the duplicate sitemap-generator declaration, corrected redirect-section coverage, and restored the current `Clinical Guide` UI contract. | `git diff --check` and `npm run sitemap:check` passed. `npm run test` was attempted twice but blocked by the repository-wide heavy-command lock held by a separate Playwright worktree; no provider-backed checks ran. | | 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. | -| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | c5e7b96c3 + baeced6f2 + reviewed follow-up diff | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. The full local Chromium sweep completed with one failure: the app-mode test clicked before React attached its handler. The existing handler-readiness pattern was applied; two focused rerun attempts were then lock-blocked by another registered worktree, so exact-head hosted Chromium remains the required proof. Initial exact-head hosted checks, including Production UI, passed before these final follow-ups. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. | +| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | bb85b546e + ledger closeout | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. The full local Chromium sweep completed 236/237 with one hydration-timing failure: the app-mode test clicked before React attached its handler. The existing handler-readiness pattern was applied; focused rerun attempts were then lock-blocked by another registered worktree, so exact-head hosted Chromium remains the required proof. Initial hosted checks, including Production UI, passed before these final follow-ups. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. | From 35d08a43b74828ec408a3d33f638059f59e0e96f Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:14:09 +0800 Subject: [PATCH 12/21] fix: keep prototype favourites out of live search --- src/components/ClinicalDashboard.tsx | 10 ++++-- .../universal-search-command-surface.tsx | 36 +++++++++++-------- src/lib/client-env.ts | 17 +++++++++ tests/favourites-demo-boundary.test.ts | 23 ++++++++++-- 4 files changed, 65 insertions(+), 21 deletions(-) diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 534bb507b..f9012e2cd 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -36,7 +36,12 @@ import { } from "react"; import { type DocumentDeleteResult } from "@/components/DocumentManagementActions"; import { extractSafetyFindings } from "@/lib/clinical-safety"; -import { isLocalNoAuthMode, publicUploadsEnabled, resolveClientDemoMode } from "@/lib/client-env"; +import { + isLocalNoAuthMode, + publicUploadsEnabled, + resolveClientDemoMode, + resolveUploadReadOnlyMode, +} from "@/lib/client-env"; import { readLocalProjectIdentity, unsafeLocalProjectMessage } from "@/lib/local-project-identity"; import { isDeployedClinicalKb } from "@/lib/deployed-app"; import { @@ -863,10 +868,9 @@ export function ClinicalDashboard({ }, [answerThreadOwnerId, authStatus]); // Local no-auth still has private upload APIs (`canUsePrivateApis`); do not lock the // upload drawer just because `resolveClientDemoMode` treats no-auth as demo for favourites. - const uploadReadOnlyMode = resolveClientDemoMode({ + const uploadReadOnlyMode = resolveUploadReadOnlyMode({ explicitDemoMode, authUnavailableFallback: browserAuthUnavailableDemoFallback, - localNoAuthMode: false, }); const localDevCanAttemptPrivateApis = process.env.NODE_ENV !== "production" && hasReadyPublicSearchSetup(setupChecks); const canUsePublicSearchApis = localProjectReady && hasReadyPublicSearchSetup(setupChecks); diff --git a/src/components/clinical-dashboard/universal-search-command-surface.tsx b/src/components/clinical-dashboard/universal-search-command-surface.tsx index 62d364d20..021c80f2b 100644 --- a/src/components/clinical-dashboard/universal-search-command-surface.tsx +++ b/src/components/clinical-dashboard/universal-search-command-surface.tsx @@ -76,7 +76,11 @@ type LocalFavouriteMatch = { score: number; }; -function rankLocalFavourites(items: FavouriteItem[], query: string): LocalFavouriteMatch[] { +function rankLocalFavourites( + items: FavouriteItem[], + query: string, + includePrototypeSets: boolean, +): LocalFavouriteMatch[] { const normalized = query.trim().toLowerCase(); if (!normalized) return []; const tokens = normalized.split(/\s+/).filter(Boolean); @@ -103,18 +107,20 @@ function rankLocalFavourites(items: FavouriteItem[], query: string): LocalFavour if ((byKey.get(key)?.score ?? -1) < score) byKey.set(key, match); } - for (const set of favouriteSets) { - const text = [set.title, set.meta, set.keywords].join(" ").toLowerCase(); - if (!tokens.every((token) => text.includes(token))) continue; - const score = set.title.toLowerCase().includes(normalized) ? 100 : 8; - byKey.set(`set:${set.id}`, { - id: `set:${set.id}`, - title: set.title, - subtitle: `${set.count} saved ${set.count === 1 ? "item" : "items"} · ${set.meta}`, - href: `/favourites?q=${encodeURIComponent(set.title)}&run=1`, - standalone: true, - score, - }); + if (includePrototypeSets) { + for (const set of favouriteSets) { + const text = [set.title, set.meta, set.keywords].join(" ").toLowerCase(); + if (!tokens.every((token) => text.includes(token))) continue; + const score = set.title.toLowerCase().includes(normalized) ? 100 : 8; + byKey.set(`set:${set.id}`, { + id: `set:${set.id}`, + title: set.title, + subtitle: `${set.count} saved ${set.count === 1 ? "item" : "items"} · ${set.meta}`, + href: `/favourites?q=${encodeURIComponent(set.title)}&run=1`, + standalone: true, + score, + }); + } } return [...byKey.values()].sort((left, right) => right.score - left.score || left.title.localeCompare(right.title)); @@ -429,8 +435,8 @@ export function UniversalSearchCommandSurface({ [demoMode, savedRegistryFavourites], ); const favouriteMatches = useMemo( - () => rankLocalFavourites(allFavouriteItems, trimmedQuery), - [allFavouriteItems, trimmedQuery], + () => rankLocalFavourites(allFavouriteItems, trimmedQuery, demoMode), + [allFavouriteItems, demoMode, trimmedQuery], ); const savedHrefs = useMemo(() => new Set(allFavouriteItems.map((item) => item.href)), [allFavouriteItems]); diff --git a/src/lib/client-env.ts b/src/lib/client-env.ts index 27009bae6..cea8c53cf 100644 --- a/src/lib/client-env.ts +++ b/src/lib/client-env.ts @@ -17,6 +17,23 @@ export function resolveClientDemoMode({ return explicitDemoMode || (environment !== "production" && (authUnavailableFallback || localNoAuthMode)); } +export function resolveUploadReadOnlyMode({ + explicitDemoMode, + authUnavailableFallback, + environment = process.env.NODE_ENV, +}: { + explicitDemoMode: boolean; + authUnavailableFallback: boolean; + environment?: string; +}) { + return resolveClientDemoMode({ + explicitDemoMode, + authUnavailableFallback, + localNoAuthMode: false, + environment, + }); +} + export function publicUploadsEnabled() { return process.env.NEXT_PUBLIC_PUBLIC_UPLOADS_ENABLED === "true"; } diff --git a/tests/favourites-demo-boundary.test.ts b/tests/favourites-demo-boundary.test.ts index 8b21bc5d7..43e74e432 100644 --- a/tests/favourites-demo-boundary.test.ts +++ b/tests/favourites-demo-boundary.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; -import { resolveClientDemoMode } from "@/lib/client-env"; +import { resolveClientDemoMode, resolveUploadReadOnlyMode } from "@/lib/client-env"; const routeSource = readFileSync(new URL("../src/app/favourites/page.tsx", import.meta.url), "utf8"); const librarySource = readFileSync( @@ -42,6 +42,8 @@ describe("favourites demo-data boundary", () => { expect(globalShellSource).toContain("demoMode={clientDemoMode}"); expect(universalSearchSource).toContain("...(demoMode ? favouriteItems : [])"); expect(universalSearchSource).not.toContain("[...favouriteItems, ...savedRegistryFavourites]"); + expect(universalSearchSource).toContain("if (includePrototypeSets)"); + expect(universalSearchSource).toContain("rankLocalFavourites(allFavouriteItems, trimmedQuery, demoMode)"); }); it("fails closed in production while preserving explicit and non-production demo modes", () => { @@ -101,8 +103,23 @@ describe("favourites demo-data boundary", () => { environment: "development", }), ).toBe(false); - expect(dashboardSource).toContain("localNoAuthMode: false"); - expect(dashboardSource).toMatch(/const uploadReadOnlyMode = resolveClientDemoMode\(\{/); + const browserAuthUnavailableDemoFallback = true; + expect( + resolveUploadReadOnlyMode({ + explicitDemoMode: false, + authUnavailableFallback: browserAuthUnavailableDemoFallback, + environment: "development", + }), + ).toBe(true); + expect( + resolveUploadReadOnlyMode({ + explicitDemoMode: false, + authUnavailableFallback: false, + environment: "development", + }), + ).toBe(false); + expect(dashboardSource).toMatch(/const uploadReadOnlyMode = resolveUploadReadOnlyMode\(\{/); + expect(dashboardSource).toContain("authUnavailableFallback: browserAuthUnavailableDemoFallback"); expect(dashboardSource).not.toMatch(/const uploadReadOnlyMode = clientDemoMode\b/); }); }); From 8050f59bc2bfe005a094af2bd8696e7fde1dbbb9 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:20:20 +0800 Subject: [PATCH 13/21] test: update design audit boundary contracts --- docs/branch-review-ledger.md | 2 +- docs/design-audit-2026-07-17.md | 5 +++-- tests/audit-navigation-auth-regressions.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index e9b66b7c6..a07ce57f7 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -610,4 +610,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-17 | codex/design-audit-20260716 | 47b58ddf95826231a6057b400e7f601c403a129d + reviewed working diff | exhaustive design, accessibility, UX, interaction, route, responsive, HTML/CSS/JS/TSX, and test-coverage audit with remediation | No P0. Fixed two P1 trust/responsive defects and the confirmed P2/P3 set: false Favourites provenance/demo leakage/dead controls, Therapy phone reflow/load recovery/contrast, semantic tabs/search/filters, error focus/theme, deterministic Forms back navigation, honest unavailable actions, application dialog semantics, duplicate differential keys, skip-link target, Playwright project isolation, and route coverage. External visual-target fidelity remains unavailable. | Static 261-file/~67,944-line inventory; independent combined-diff review; focused Vitest 21/21; jsdom 24/24; architecture 6/6; accessibility browser 5/5; focused route/browser recovery; lint; TypeScript; runtime/workflow/sitemap/brand/type/icon checks; production build with 1,043 pages and client-secret scan; `git diff --check`. Canonical aggregate Vitest was Windows-runner inconclusive and full UI was not repeated after focused remediation. No provider/API checks. | | 2026-07-18 | codex/main-merge-51278-final-20260718-late | 45fa3c6c7, 14a0a898c | merge integration of 51278a70d onto fresh origin/main | Replayed the requested historical design-audit commit onto fresh `origin/main`. Kept current-main versions for five conflicts, including the regenerated drift manifest and current schema assertions. Fixed the duplicate sitemap-generator declaration, corrected redirect-section coverage, and restored the current `Clinical Guide` UI contract. | `git diff --check` and `npm run sitemap:check` passed. `npm run test` was attempted twice but blocked by the repository-wide heavy-command lock held by a separate Playwright worktree; no provider-backed checks ran. | | 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. | -| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | bb85b546e + ledger closeout | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. The full local Chromium sweep completed 236/237 with one hydration-timing failure: the app-mode test clicked before React attached its handler. The existing handler-readiness pattern was applied; focused rerun attempts were then lock-blocked by another registered worktree, so exact-head hosted Chromium remains the required proof. Initial hosted checks, including Production UI, passed before these final follow-ups. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. | +| 2026-07-18 | PR #871 / codex/design-audit-final-pr-20260718 | bb85b546e + ledger closeout | final current-main design/accessibility audit integration, hosted review resolution, and PR-readiness review | No remaining high-confidence P0-P2 defect after current-main reconciliation, two independent read-only reviews, and remediation of every confirmed review finding. The follow-ups made client demo state fail closed in production for both prototype items and set suggestions, separated local no-auth upload capability from Favourites demo treatment, kept unavailable Favourites controls focusable with accessible reasons, restored visible skip-target focus, limited Favourites selection to the wide layout that exposes its workspace, aligned upload/index semantics with each responsive rendering mode, exposed filtered Tools results as a labelled group, and kept Therapy Compass in an honest loading state until its catalogue resolves. The audit also distinguishes completed browser assertions from an incomplete valid document-source redirect case. External design-target fidelity remains unverified without an approved target. | Canonical local PR verification completed through runtime, changed-file formatting, full ESLint, TypeScript, full Vitest, webpack production build/client-secret scan, and offline RAG fixtures. Focused Therapy, navigation/auth, demo-boundary, and unavailable-control regressions passed after correcting one test-order issue; the final demo/upload boundary selection passed 4/4; scoped zero-warning ESLint, changed-file formatting, and `git diff --check` passed. The full local Chromium sweep completed 236/237 with one hydration-timing failure: the app-mode test clicked before React attached its handler. The existing handler-readiness pattern was applied; focused rerun attempts were then lock-blocked by another registered worktree, so exact-head hosted Chromium remains the required proof. Initial hosted checks, including Production UI, passed before these final follow-ups. No OpenAI, Supabase, production-data, deployment, or live clinical workflow ran. | diff --git a/docs/design-audit-2026-07-17.md b/docs/design-audit-2026-07-17.md index a8654592f..6a2f386c2 100644 --- a/docs/design-audit-2026-07-17.md +++ b/docs/design-audit-2026-07-17.md @@ -22,7 +22,7 @@ This pass deliberately built on the 2026-07-15 live design audit instead of repe | DQA-01 | P1 | Favourites rendered the same acamprosate evidence and lithium note beneath unrelated selected items, creating false item-level provenance. | Evidence and notes now derive from the selected item; absent content has an explicit honest empty state. Copy actions use the item citation and return accessible status. | `favourites-command-library-page.tsx`, `ui-smoke.spec.ts` | | DQA-02 | P1 | Therapy Compass used fixed desktop grid tracks wider than a phone while the global shell clipped page overflow, making several production screens unreachable at 320–390 px. | Added semantic responsive layout classes, one-column phone reflow, wrapping actions/metrics, and a contract test across all Therapy Compass screens. | `therapy-compass/*`, `therapy-compass-responsive-contract.test.ts` | | DQA-03 | P2 | White text/icons on the dark clinical accent measured materially below accessible contrast and bypassed the existing contrast token. | Replaced hard-coded white foregrounds with `--clinical-accent-contrast`, including answer status and Therapy Compass controls. | `answer-status.tsx`, `therapy-compass/*` | -| DQA-04 | P2 | Prototype favourites appeared as genuine saved user content in normal mode, including through universal search; an unconfigured browser auth client could also fail open to demo state in production. | Gated fixtures behind the effective client demo state and introduced a shared resolver that permits auth-unavailable fallback only outside production while retaining explicit production demo opt-in. Normal mode now shows only real saved items. | `client-env.ts`, `favourites/page.tsx`, `ClinicalDashboard.tsx`, `favourites-hub.tsx`, `global-search-shell.tsx`, `master-search-header.tsx`, `universal-search-command-surface.tsx` | +| DQA-04 | P2 | Prototype favourites appeared as genuine saved user content in normal mode, including item and set suggestions through universal search; an unconfigured browser auth client could also fail open to demo state in production. | Gated fixture items and sets behind the effective client demo state and introduced a shared resolver that permits auth-unavailable fallback only outside production while retaining explicit production demo opt-in. Normal mode now shows only real saved items. | `client-env.ts`, `favourites/page.tsx`, `ClinicalDashboard.tsx`, `favourites-hub.tsx`, `global-search-shell.tsx`, `master-search-header.tsx`, `universal-search-command-surface.tsx` | | DQA-05 | P2 | Favourites exposed invisible selection below `2xl`, nested/synthetic mobile card controls, no-op edit/sort actions, incomplete menu keyboard behavior, clipboard focus loss, and title-only reasons on unfocusable unavailable controls. | Kept explicit native links and separate buttons on mobile, limited selection to the visible workspace, implemented real citation copy with status/focus restoration and complete menu keyboard behavior, and made unfinished actions honestly `aria-disabled`, focusable, and accessibly described. | `favourites-command-library-page.tsx`, `favourites-hub.tsx`, `favourites-hub-unavailable-controls.dom.test.tsx` | | DQA-06 | P2 | Differential presentation controls advertised editing, filtering, reorder/remove behavior, and a fixed `+2` count that did not exist. | Removed false instructions, derived the remaining count from state, and disabled unfinished controls with availability explanations while preserving the read-only comparison. | `differential-presentation-workflow-page.tsx` | | DQA-07 | P2 | Services selection led to enabled-looking Edit/Review/View controls that did nothing, while Compare stayed unavailable without an accurate reason. A later diff briefly disabled the real Clear action. | Removed the false actions and restored Clear. During current-main integration, the later functional checklist, confidence-detail, and selected-service comparison controls were preserved instead of replacing them with disabled placeholders. | `services-navigator-page.tsx`, `ui-smoke.spec.ts` | @@ -40,6 +40,7 @@ This pass deliberately built on the 2026-07-15 live design audit instead of repe | DQA-19 | P2 | The colour-reference skip link targeted `#main-content`, but the route had no matching focus target; app-shell targets also suppressed their focus outline. | Added canonical focusable main-content targets with an explicit visible focus outline and browser proof of both focus transfer and computed outline style. | `reference/colour-coding/page.tsx`, `ClinicalDashboard.tsx`, `global-search-shell.tsx`, `ui-route-coverage.spec.ts` | | DQA-20 | P3 | Answer sheets overrode the semantic backdrop token and duplicate mobile CSS retained obsolete layout overrides. | Removed call-site backdrop overrides and redundant CSS so theme, forced-colour, and responsive behavior come from the shared tokens/layout rules. | `answer-result-surface.tsx`, `globals.css`, `ui-overlay-css-contract.test.ts` | | DQA-21 | P2 | Route-handler redirects inherited the production server's `0.0.0.0` bind origin, so Chromium rejected Applications, Medications, and differential-presentation redirects with `ERR_ADDRESS_INVALID`. | Return same-origin relative `Location` headers, preserving allowed query state without leaking the server bind address into browser navigation. | `applications/route.ts`, `medications/route.ts`, `differentials/presentations/route.ts`, route tests | +| DQA-22 | P2 | Local no-auth mode reused the broad Favourites demo flag for uploads, so a development session with working private upload APIs incorrectly presented the upload workspace as read-only. | Added a dedicated upload-read-only resolver that retains explicit demo and unavailable-auth lockout while excluding local no-auth treatment; focused behavior and source contracts protect the boundary. | `client-env.ts`, `ClinicalDashboard.tsx`, `favourites-demo-boundary.test.ts`, `audit-navigation-auth-regressions.test.ts` | ## Route and component interaction proof @@ -99,5 +100,5 @@ Broad-gate limitations: - Final branch/worktree: `codex/design-audit-final-pr-20260718` in `C:\Dev\Apps\Database\worktrees\design-audit-final-pr-20260718`. - The current-main merge preserved the routed Therapy workspace, functional Services checklist/confidence/comparison controls, and dialog-first tool cards. Stale tests for the removed legacy wrappers/placeholders were reconciled without restoring inaccessible or false UI. - The local PR plan passed runtime, changed-file formatting, full ESLint, TypeScript, 309 Vitest files / 2,797 tests, a webpack production build with 1,674 generated pages, client-bundle secret scanning, and 36 offline RAG golden fixtures across 21 suites. The build was rerun narrowly after an empty `NEXT_PUBLIC_DEMO_MODE` test-shell value was removed; no source change was required for that environment-only failure. -- The exact-branch Chromium run passed 230/237 tests on its first pass. Focused production recovery passed Therapy navigation and all three relative redirect journeys; Services and Tools then passed against the identity-verified provider-free branch server. The guest upload journey was proven in production through the Source Library and exact admin-only notice; its final strict alert locator was narrowed to exclude Next's empty route announcer. A third duplicate production build was not launched under the recovery retry limit, so hosted exact-head UI remains the final confirmation for that selector. +- The exact-branch Chromium run passed 230/237 tests on its first pass. Focused production recovery passed Therapy navigation and all three relative redirect journeys; Services and Tools then passed against the identity-verified provider-free branch server. The guest upload journey was proven in production through the Source Library and exact admin-only notice; its final strict alert locator was narrowed to exclude Next's empty route announcer. A later full sweep passed 236/237; the sole failure clicked the app-mode trigger before React attached its handler, so the test now uses the existing handler-readiness guard. Two focused rerun attempts were shared-lock blocked, making hosted exact-head UI the final confirmation. - No OpenAI, Supabase, production-data, deployment, or live clinical workflow was invoked. Browser/API fixtures used the repository's offline environment and blocked external requests. diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts index 0dc1fb7b0..e9c50ae9d 100644 --- a/tests/audit-navigation-auth-regressions.test.ts +++ b/tests/audit-navigation-auth-regressions.test.ts @@ -95,8 +95,8 @@ describe("audit navigation and auth regressions", () => { "const uploadReadOnlyMode =", "const canUsePrivateApis =", ); - expect(uploadReadOnlyContract).toContain("const uploadReadOnlyMode = resolveClientDemoMode({"); - expect(uploadReadOnlyContract).toContain("localNoAuthMode: false"); + expect(uploadReadOnlyContract).toContain("const uploadReadOnlyMode = resolveUploadReadOnlyMode({"); + expect(uploadReadOnlyContract).toContain("authUnavailableFallback: browserAuthUnavailableDemoFallback"); expect(uploadReadOnlyContract).not.toMatch(/const uploadReadOnlyMode = clientDemoMode\b/); const privateCapabilityContract = sourceSegment( From 7565d818ddb1d5e592cb5fa79f5145fc293e9d6a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 18 Jul 2026 16:22:49 +0000 Subject: [PATCH 14/21] test: tighten uploadReadOnlyMode contract for local no-auth Assert the dashboard uses resolveUploadReadOnlyMode without localNoAuthMode and that the helper hardcodes localNoAuthMode: false so demo remains upload-locked while local no-auth stays writable. Co-authored-by: BigSimmo --- tests/audit-navigation-auth-regressions.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts index e9c50ae9d..d2aa7097e 100644 --- a/tests/audit-navigation-auth-regressions.test.ts +++ b/tests/audit-navigation-auth-regressions.test.ts @@ -95,9 +95,14 @@ describe("audit navigation and auth regressions", () => { "const uploadReadOnlyMode =", "const canUsePrivateApis =", ); + // Uploads stay writable in local no-auth; only explicit demo / auth-unavailable lock them. expect(uploadReadOnlyContract).toContain("const uploadReadOnlyMode = resolveUploadReadOnlyMode({"); + expect(uploadReadOnlyContract).toContain("explicitDemoMode,"); expect(uploadReadOnlyContract).toContain("authUnavailableFallback: browserAuthUnavailableDemoFallback"); + expect(uploadReadOnlyContract).not.toContain("localNoAuthMode"); expect(uploadReadOnlyContract).not.toMatch(/const uploadReadOnlyMode = clientDemoMode\b/); + expect(uploadReadOnlyContract).not.toMatch(/const uploadReadOnlyMode = resolveClientDemoMode\b/); + expect(source("src/lib/client-env.ts")).toContain("localNoAuthMode: false"); const privateCapabilityContract = sourceSegment( clinicalDashboardSource, From 364b414ab1781ec732d8c13b27f35c989c138e97 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:38:52 +0800 Subject: [PATCH 15/21] fix: align local no-auth favourites mode --- docs/design-audit-2026-07-17.md | 48 +++++++++++++------------- src/app/favourites/page.tsx | 10 ++++-- tests/favourites-demo-boundary.test.ts | 5 ++- 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/docs/design-audit-2026-07-17.md b/docs/design-audit-2026-07-17.md index 6a2f386c2..1c1fd1054 100644 --- a/docs/design-audit-2026-07-17.md +++ b/docs/design-audit-2026-07-17.md @@ -17,30 +17,30 @@ This pass deliberately built on the 2026-07-15 live design audit instead of repe ## Findings and fixes -| ID | Severity | Detailed issue and user impact | Implemented solution | Principal files | -| ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| DQA-01 | P1 | Favourites rendered the same acamprosate evidence and lithium note beneath unrelated selected items, creating false item-level provenance. | Evidence and notes now derive from the selected item; absent content has an explicit honest empty state. Copy actions use the item citation and return accessible status. | `favourites-command-library-page.tsx`, `ui-smoke.spec.ts` | -| DQA-02 | P1 | Therapy Compass used fixed desktop grid tracks wider than a phone while the global shell clipped page overflow, making several production screens unreachable at 320–390 px. | Added semantic responsive layout classes, one-column phone reflow, wrapping actions/metrics, and a contract test across all Therapy Compass screens. | `therapy-compass/*`, `therapy-compass-responsive-contract.test.ts` | -| DQA-03 | P2 | White text/icons on the dark clinical accent measured materially below accessible contrast and bypassed the existing contrast token. | Replaced hard-coded white foregrounds with `--clinical-accent-contrast`, including answer status and Therapy Compass controls. | `answer-status.tsx`, `therapy-compass/*` | -| DQA-04 | P2 | Prototype favourites appeared as genuine saved user content in normal mode, including item and set suggestions through universal search; an unconfigured browser auth client could also fail open to demo state in production. | Gated fixture items and sets behind the effective client demo state and introduced a shared resolver that permits auth-unavailable fallback only outside production while retaining explicit production demo opt-in. Normal mode now shows only real saved items. | `client-env.ts`, `favourites/page.tsx`, `ClinicalDashboard.tsx`, `favourites-hub.tsx`, `global-search-shell.tsx`, `master-search-header.tsx`, `universal-search-command-surface.tsx` | -| DQA-05 | P2 | Favourites exposed invisible selection below `2xl`, nested/synthetic mobile card controls, no-op edit/sort actions, incomplete menu keyboard behavior, clipboard focus loss, and title-only reasons on unfocusable unavailable controls. | Kept explicit native links and separate buttons on mobile, limited selection to the visible workspace, implemented real citation copy with status/focus restoration and complete menu keyboard behavior, and made unfinished actions honestly `aria-disabled`, focusable, and accessibly described. | `favourites-command-library-page.tsx`, `favourites-hub.tsx`, `favourites-hub-unavailable-controls.dom.test.tsx` | -| DQA-06 | P2 | Differential presentation controls advertised editing, filtering, reorder/remove behavior, and a fixed `+2` count that did not exist. | Removed false instructions, derived the remaining count from state, and disabled unfinished controls with availability explanations while preserving the read-only comparison. | `differential-presentation-workflow-page.tsx` | -| DQA-07 | P2 | Services selection led to enabled-looking Edit/Review/View controls that did nothing, while Compare stayed unavailable without an accurate reason. A later diff briefly disabled the real Clear action. | Removed the false actions and restored Clear. During current-main integration, the later functional checklist, confidence-detail, and selected-service comparison controls were preserved instead of replacing them with disabled placeholders. | `services-navigator-page.tsx`, `ui-smoke.spec.ts` | -| DQA-08 | P2 | Evidence records with no URL were still keyboard-focusable `href="#"` links announced as “Open source”. | Missing-source rows now render as non-links with unavailable semantics; only valid destinations render a Link. | `visual-evidence.tsx`, `visual-evidence-tabs.dom.test.tsx` | -| DQA-09 | P2 | Therapy cards presented an enabled Favourite button with no handler or state change. | The unavailable feature is now disabled and labelled as coming soon, preventing false save confirmation. | `therapy-card.tsx` | -| DQA-10 | P2 | Route/global error fallbacks did not reliably move focus or announce a route failure; the global fallback was hard-coded light-only. | Added programmatic heading focus and alert announcement, and made the self-contained global fallback system-colour, dark-mode, and forced-colour aware. | `route-error-boundary.tsx`, `global-error.tsx`, route error tests | -| DQA-11 | P2 | Clinical Notes, Visual Evidence, and upload/index tab interfaces lacked complete roving keyboard behavior and reciprocal tab/panel relationships. | Added stable unique IDs, `aria-controls`/`aria-labelledby`, roving `tabIndex`, and Left/Right/Home/End auto-activation. Upload/indexing uses tab panels only with its visible mobile tablist and named regions when desktop renders every section. | `ClinicalDashboard.tsx`, `answer-result-surface.tsx`, `visual-evidence.tsx`, UI/DOM tests | -| DQA-12 | P2 | Universal search visually highlighted an item without exposing it through `aria-activedescendant`. | The combobox now points to the active option and tests verify the relationship through keyboard navigation. | `master-search-header.tsx`, `universal-search-command-surface.tsx`, `ui-universal-search.spec.ts` | -| DQA-13 | P2 | Differential filters and several selector groups behaved as toggles but lacked grouped/pressed semantics; the Tools result set retained orphaned tab-panel semantics after its controls became pressed filters. | Added labelled groups and stateful `aria-pressed` semantics, and exposed Tools results as a labelled group, with browser accessibility assertions. | `differentials-home.tsx`, `evidence-panels.tsx`, `applications-launcher-page.tsx`, `ui-accessibility.spec.ts`, `ui-tools.spec.ts` | -| DQA-14 | P2 | Therapy Compass silently swallowed data-load failure, rendered an empty `0`-record catalogue while loading, and still implied a fabricated `200+` catalogue count. | Added distinct loading and failure states, a Retry action, removed fabricated totals, and retained real successful/empty states. The final integration tests the routed workspace that replaced the legacy single-page wrapper. | `use-therapy-data.ts`, `workspace.tsx`, `home-screen.tsx`, `therapy-compass-data-recovery.dom.test.tsx` | -| DQA-15 | P2 | Direct-entry Forms “Back to forms” used browser history and could leave the application. | The control now deterministically navigates to `/forms?focus=1`; a DOM regression test verifies the destination. | `form-detail-page.tsx`, `forms-back-navigation.dom.test.tsx` | -| DQA-16 | P2 | Firefox/WebKit production projects unintentionally collected advisory `@mockup` tests; two browser helpers also retried hydration inconsistently. | Applied production matchers and `grepInvert` to all required projects and introduced one shared app-mode hydration retry helper. | `playwright.config.ts`, `playwright-app-mode.ts`, `ui-stress.spec.ts`, `ui-tools.spec.ts` | -| DQA-17 | P2 | Application/tool cards looked like direct links but prevented navigation to open an internal detail state, obscuring the real action. | Cards are now honest dialog buttons with `aria-haspopup="dialog"`; the dialog contains the actual destination link. | `applications-launcher-page.tsx`, `ui-smoke.spec.ts` | -| DQA-18 | P2 | Differential diagnosis streams keyed repeated cards only by visible title, producing React duplicate-key errors and unstable list identity. | Cards now use a stable href/index composite key; the route matrix listens for console errors. | `differential-stream-page.tsx`, `ui-route-coverage.spec.ts` | -| DQA-19 | P2 | The colour-reference skip link targeted `#main-content`, but the route had no matching focus target; app-shell targets also suppressed their focus outline. | Added canonical focusable main-content targets with an explicit visible focus outline and browser proof of both focus transfer and computed outline style. | `reference/colour-coding/page.tsx`, `ClinicalDashboard.tsx`, `global-search-shell.tsx`, `ui-route-coverage.spec.ts` | -| DQA-20 | P3 | Answer sheets overrode the semantic backdrop token and duplicate mobile CSS retained obsolete layout overrides. | Removed call-site backdrop overrides and redundant CSS so theme, forced-colour, and responsive behavior come from the shared tokens/layout rules. | `answer-result-surface.tsx`, `globals.css`, `ui-overlay-css-contract.test.ts` | -| DQA-21 | P2 | Route-handler redirects inherited the production server's `0.0.0.0` bind origin, so Chromium rejected Applications, Medications, and differential-presentation redirects with `ERR_ADDRESS_INVALID`. | Return same-origin relative `Location` headers, preserving allowed query state without leaking the server bind address into browser navigation. | `applications/route.ts`, `medications/route.ts`, `differentials/presentations/route.ts`, route tests | -| DQA-22 | P2 | Local no-auth mode reused the broad Favourites demo flag for uploads, so a development session with working private upload APIs incorrectly presented the upload workspace as read-only. | Added a dedicated upload-read-only resolver that retains explicit demo and unavailable-auth lockout while excluding local no-auth treatment; focused behavior and source contracts protect the boundary. | `client-env.ts`, `ClinicalDashboard.tsx`, `favourites-demo-boundary.test.ts`, `audit-navigation-auth-regressions.test.ts` | +| ID | Severity | Detailed issue and user impact | Implemented solution | Principal files | +| ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| DQA-01 | P1 | Favourites rendered the same acamprosate evidence and lithium note beneath unrelated selected items, creating false item-level provenance. | Evidence and notes now derive from the selected item; absent content has an explicit honest empty state. Copy actions use the item citation and return accessible status. | `favourites-command-library-page.tsx`, `ui-smoke.spec.ts` | +| DQA-02 | P1 | Therapy Compass used fixed desktop grid tracks wider than a phone while the global shell clipped page overflow, making several production screens unreachable at 320–390 px. | Added semantic responsive layout classes, one-column phone reflow, wrapping actions/metrics, and a contract test across all Therapy Compass screens. | `therapy-compass/*`, `therapy-compass-responsive-contract.test.ts` | +| DQA-03 | P2 | White text/icons on the dark clinical accent measured materially below accessible contrast and bypassed the existing contrast token. | Replaced hard-coded white foregrounds with `--clinical-accent-contrast`, including answer status and Therapy Compass controls. | `answer-status.tsx`, `therapy-compass/*` | +| DQA-04 | P2 | Prototype favourites appeared as genuine saved user content in normal mode, including item and set suggestions through universal search; an unconfigured browser auth client could also fail open to demo state in production. | Gated fixture items and sets behind one production-safe demo decision across the direct server route and client shells. Auth-unavailable fallback is non-production only, local no-auth remains consistent, and explicit production demo opt-in is retained. Normal mode shows only real saved items. | `client-env.ts`, `favourites/page.tsx`, `ClinicalDashboard.tsx`, `favourites-hub.tsx`, `global-search-shell.tsx`, `master-search-header.tsx`, `universal-search-command-surface.tsx` | +| DQA-05 | P2 | Favourites exposed invisible selection below `2xl`, nested/synthetic mobile card controls, no-op edit/sort actions, incomplete menu keyboard behavior, clipboard focus loss, and title-only reasons on unfocusable unavailable controls. | Kept explicit native links and separate buttons on mobile, limited selection to the visible workspace, implemented real citation copy with status/focus restoration and complete menu keyboard behavior, and made unfinished actions honestly `aria-disabled`, focusable, and accessibly described. | `favourites-command-library-page.tsx`, `favourites-hub.tsx`, `favourites-hub-unavailable-controls.dom.test.tsx` | +| DQA-06 | P2 | Differential presentation controls advertised editing, filtering, reorder/remove behavior, and a fixed `+2` count that did not exist. | Removed false instructions, derived the remaining count from state, and disabled unfinished controls with availability explanations while preserving the read-only comparison. | `differential-presentation-workflow-page.tsx` | +| DQA-07 | P2 | Services selection led to enabled-looking Edit/Review/View controls that did nothing, while Compare stayed unavailable without an accurate reason. A later diff briefly disabled the real Clear action. | Removed the false actions and restored Clear. During current-main integration, the later functional checklist, confidence-detail, and selected-service comparison controls were preserved instead of replacing them with disabled placeholders. | `services-navigator-page.tsx`, `ui-smoke.spec.ts` | +| DQA-08 | P2 | Evidence records with no URL were still keyboard-focusable `href="#"` links announced as “Open source”. | Missing-source rows now render as non-links with unavailable semantics; only valid destinations render a Link. | `visual-evidence.tsx`, `visual-evidence-tabs.dom.test.tsx` | +| DQA-09 | P2 | Therapy cards presented an enabled Favourite button with no handler or state change. | The unavailable feature is now disabled and labelled as coming soon, preventing false save confirmation. | `therapy-card.tsx` | +| DQA-10 | P2 | Route/global error fallbacks did not reliably move focus or announce a route failure; the global fallback was hard-coded light-only. | Added programmatic heading focus and alert announcement, and made the self-contained global fallback system-colour, dark-mode, and forced-colour aware. | `route-error-boundary.tsx`, `global-error.tsx`, route error tests | +| DQA-11 | P2 | Clinical Notes, Visual Evidence, and upload/index tab interfaces lacked complete roving keyboard behavior and reciprocal tab/panel relationships. | Added stable unique IDs, `aria-controls`/`aria-labelledby`, roving `tabIndex`, and Left/Right/Home/End auto-activation. Upload/indexing uses tab panels only with its visible mobile tablist and named regions when desktop renders every section. | `ClinicalDashboard.tsx`, `answer-result-surface.tsx`, `visual-evidence.tsx`, UI/DOM tests | +| DQA-12 | P2 | Universal search visually highlighted an item without exposing it through `aria-activedescendant`. | The combobox now points to the active option and tests verify the relationship through keyboard navigation. | `master-search-header.tsx`, `universal-search-command-surface.tsx`, `ui-universal-search.spec.ts` | +| DQA-13 | P2 | Differential filters and several selector groups behaved as toggles but lacked grouped/pressed semantics; the Tools result set retained orphaned tab-panel semantics after its controls became pressed filters. | Added labelled groups and stateful `aria-pressed` semantics, and exposed Tools results as a labelled group, with browser accessibility assertions. | `differentials-home.tsx`, `evidence-panels.tsx`, `applications-launcher-page.tsx`, `ui-accessibility.spec.ts`, `ui-tools.spec.ts` | +| DQA-14 | P2 | Therapy Compass silently swallowed data-load failure, rendered an empty `0`-record catalogue while loading, and still implied a fabricated `200+` catalogue count. | Added distinct loading and failure states, a Retry action, removed fabricated totals, and retained real successful/empty states. The final integration tests the routed workspace that replaced the legacy single-page wrapper. | `use-therapy-data.ts`, `workspace.tsx`, `home-screen.tsx`, `therapy-compass-data-recovery.dom.test.tsx` | +| DQA-15 | P2 | Direct-entry Forms “Back to forms” used browser history and could leave the application. | The control now deterministically navigates to `/forms?focus=1`; a DOM regression test verifies the destination. | `form-detail-page.tsx`, `forms-back-navigation.dom.test.tsx` | +| DQA-16 | P2 | Firefox/WebKit production projects unintentionally collected advisory `@mockup` tests; two browser helpers also retried hydration inconsistently. | Applied production matchers and `grepInvert` to all required projects and introduced one shared app-mode hydration retry helper. | `playwright.config.ts`, `playwright-app-mode.ts`, `ui-stress.spec.ts`, `ui-tools.spec.ts` | +| DQA-17 | P2 | Application/tool cards looked like direct links but prevented navigation to open an internal detail state, obscuring the real action. | Cards are now honest dialog buttons with `aria-haspopup="dialog"`; the dialog contains the actual destination link. | `applications-launcher-page.tsx`, `ui-smoke.spec.ts` | +| DQA-18 | P2 | Differential diagnosis streams keyed repeated cards only by visible title, producing React duplicate-key errors and unstable list identity. | Cards now use a stable href/index composite key; the route matrix listens for console errors. | `differential-stream-page.tsx`, `ui-route-coverage.spec.ts` | +| DQA-19 | P2 | The colour-reference skip link targeted `#main-content`, but the route had no matching focus target; app-shell targets also suppressed their focus outline. | Added canonical focusable main-content targets with an explicit visible focus outline and browser proof of both focus transfer and computed outline style. | `reference/colour-coding/page.tsx`, `ClinicalDashboard.tsx`, `global-search-shell.tsx`, `ui-route-coverage.spec.ts` | +| DQA-20 | P3 | Answer sheets overrode the semantic backdrop token and duplicate mobile CSS retained obsolete layout overrides. | Removed call-site backdrop overrides and redundant CSS so theme, forced-colour, and responsive behavior come from the shared tokens/layout rules. | `answer-result-surface.tsx`, `globals.css`, `ui-overlay-css-contract.test.ts` | +| DQA-21 | P2 | Route-handler redirects inherited the production server's `0.0.0.0` bind origin, so Chromium rejected Applications, Medications, and differential-presentation redirects with `ERR_ADDRESS_INVALID`. | Return same-origin relative `Location` headers, preserving allowed query state without leaking the server bind address into browser navigation. | `applications/route.ts`, `medications/route.ts`, `differentials/presentations/route.ts`, route tests | +| DQA-22 | P2 | Local no-auth mode reused the broad Favourites demo flag for uploads, so a development session with working private upload APIs incorrectly presented the upload workspace as read-only. | Added a dedicated upload-read-only resolver that retains explicit demo and unavailable-auth lockout while excluding local no-auth treatment; focused behavior and source contracts protect the boundary. | `client-env.ts`, `ClinicalDashboard.tsx`, `favourites-demo-boundary.test.ts`, `audit-navigation-auth-regressions.test.ts` | ## Route and component interaction proof diff --git a/src/app/favourites/page.tsx b/src/app/favourites/page.tsx index 872fac7c3..eb3ff4715 100644 --- a/src/app/favourites/page.tsx +++ b/src/app/favourites/page.tsx @@ -1,5 +1,6 @@ import { FavouritesCommandLibraryPage } from "@/components/clinical-dashboard/favourites-command-library-page"; -import { isDemoMode } from "@/lib/env"; +import { resolveClientDemoMode } from "@/lib/client-env"; +import { isDemoMode, isLocalNoAuthMode } from "@/lib/env"; type FavouritesPageProps = { searchParams?: Promise<{ @@ -14,8 +15,13 @@ function firstSearchParam(value: string | string[] | undefined) { export default async function FavouritesPage({ searchParams }: FavouritesPageProps) { const params = searchParams ? await searchParams : {}; const query = firstSearchParam(params.q)?.trim() ?? ""; + const demoMode = resolveClientDemoMode({ + explicitDemoMode: isDemoMode(), + authUnavailableFallback: false, + localNoAuthMode: isLocalNoAuthMode(), + }); // No key={query} remount: query is a pure prop, and remounting on query // change wiped the set/type/view/sort selections when clearing a search. - return ; + return ; } diff --git a/tests/favourites-demo-boundary.test.ts b/tests/favourites-demo-boundary.test.ts index 43e74e432..3f987b51f 100644 --- a/tests/favourites-demo-boundary.test.ts +++ b/tests/favourites-demo-boundary.test.ts @@ -28,7 +28,10 @@ const mobileCardSource = librarySource.slice( describe("favourites demo-data boundary", () => { it("passes trusted server demo state and never merges prototype favourites into live mode unconditionally", () => { - expect(routeSource).toContain("demoMode={isDemoMode()}"); + expect(routeSource).toContain("const demoMode = resolveClientDemoMode({"); + expect(routeSource).toContain("explicitDemoMode: isDemoMode(),"); + expect(routeSource).toContain("localNoAuthMode: isLocalNoAuthMode(),"); + expect(routeSource).toContain("demoMode={demoMode}"); expect(librarySource).toContain("...(demoMode ? prototypeFavouriteItems : [])"); expect(librarySource).not.toContain("[...prototypeFavouriteItems, ...savedRegistryFavourites]"); expect(dashboardSource).toContain("demoMode={clientDemoMode}"); From 1e5b6a347f9fd92d7e07dc53f8001baba6ae4a07 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:04:24 +0000 Subject: [PATCH 16/21] Align therapy compass recovery test with merged home copy --- tests/therapy-compass-data-recovery.dom.test.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/therapy-compass-data-recovery.dom.test.tsx b/tests/therapy-compass-data-recovery.dom.test.tsx index 47c3d5f83..e27f35eaf 100644 --- a/tests/therapy-compass-data-recovery.dom.test.tsx +++ b/tests/therapy-compass-data-recovery.dom.test.tsx @@ -64,9 +64,8 @@ describe("Therapy Compass required data recovery", () => { release(undefined); await waitFor(() => - expect(screen.getByText(/Search 1 source-grounded therapy records? by/)).toBeInTheDocument(), + expect(screen.getByText(/Search 1 source-grounded therapy records by/)).toBeInTheDocument(), ); - expect(screen.getByText(/Search 1 source-grounded therapy records? by/)).toBeInTheDocument(); expect(screen.queryByText(/Search 0 source-grounded therapy/)).not.toBeInTheDocument(); }); @@ -101,7 +100,7 @@ describe("Therapy Compass required data recovery", () => { fireEvent.click(screen.getByRole("button", { name: "Retry" })); expect(await screen.findByRole("heading", { name: "What therapy are you looking for?" })).toBeInTheDocument(); - expect(screen.getByText(/Search 1 source-grounded therapy records? by/)).toBeInTheDocument(); + expect(screen.getByText(/Search 1 source-grounded therapy records by/)).toBeInTheDocument(); expect(screen.queryByRole("alert")).not.toBeInTheDocument(); await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(6)); }); From 7410fa79e7e26e3c026a363c42dc715c61f3e96a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:07:17 +0000 Subject: [PATCH 17/21] Format therapy compass recovery test --- tests/therapy-compass-data-recovery.dom.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/therapy-compass-data-recovery.dom.test.tsx b/tests/therapy-compass-data-recovery.dom.test.tsx index 95a60f205..67021ada5 100644 --- a/tests/therapy-compass-data-recovery.dom.test.tsx +++ b/tests/therapy-compass-data-recovery.dom.test.tsx @@ -63,9 +63,7 @@ describe("Therapy Compass required data recovery", () => { release(undefined); - await waitFor(() => - expect(screen.getByText(/Search 1 source-grounded therapy record by/)).toBeInTheDocument(), - ); + await waitFor(() => expect(screen.getByText(/Search 1 source-grounded therapy record by/)).toBeInTheDocument()); expect(screen.getByRole("heading", { name: "What therapy are you looking for?" })).toBeInTheDocument(); expect(screen.queryByText(/Search 0 source-grounded therapy/)).not.toBeInTheDocument(); }); From c4662f950c92b29efe8a5817eeef260d352d02d4 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 19 Jul 2026 01:16:36 +0800 Subject: [PATCH 18/21] test: preserve therapy home audit contracts --- tests/therapy-compass-data-recovery.dom.test.tsx | 2 ++ tests/therapy-compass-responsive-contract.test.ts | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/tests/therapy-compass-data-recovery.dom.test.tsx b/tests/therapy-compass-data-recovery.dom.test.tsx index 67021ada5..e08ef1a34 100644 --- a/tests/therapy-compass-data-recovery.dom.test.tsx +++ b/tests/therapy-compass-data-recovery.dom.test.tsx @@ -58,6 +58,7 @@ describe("Therapy Compass required data recovery", () => { ); expect(await screen.findByRole("status")).toHaveTextContent("Loading therapy library…"); + expect(screen.getAllByRole("main")).toHaveLength(1); expect(screen.queryByText(/Search 0 source-grounded therapy/)).not.toBeInTheDocument(); expect(screen.queryByRole("heading", { name: "What therapy are you looking for?" })).not.toBeInTheDocument(); @@ -65,6 +66,7 @@ describe("Therapy Compass required data recovery", () => { await waitFor(() => expect(screen.getByText(/Search 1 source-grounded therapy record by/)).toBeInTheDocument()); expect(screen.getByRole("heading", { name: "What therapy are you looking for?" })).toBeInTheDocument(); + expect(screen.getAllByRole("main")).toHaveLength(1); expect(screen.queryByText(/Search 0 source-grounded therapy/)).not.toBeInTheDocument(); }); diff --git a/tests/therapy-compass-responsive-contract.test.ts b/tests/therapy-compass-responsive-contract.test.ts index 4f12e8bcb..15e383c4c 100644 --- a/tests/therapy-compass-responsive-contract.test.ts +++ b/tests/therapy-compass-responsive-contract.test.ts @@ -8,6 +8,7 @@ const therapyPath = "src/components/therapy-compass"; const stylesSource = read(`${therapyPath}/styles.tsx`); const therapyCardSource = read(`${therapyPath}/therapy-card.tsx`); const homeSource = read(`${therapyPath}/screens/home-screen.tsx`); +const modeHomeTemplateSource = read("src/components/mode-home-template.tsx"); const detailSource = read(`${therapyPath}/screens/detail-screen.tsx`); const compareSource = read(`${therapyPath}/screens/compare-screen.tsx`); const recommendSource = read(`${therapyPath}/screens/recommend-screen.tsx`); @@ -53,6 +54,8 @@ describe("Therapy Compass responsive contract", () => { expect(responsiveStackCount(therapyCardSource)).toBeGreaterThanOrEqual(2); expect(homeSource).toContain("ModeHomeMain"); expect(homeSource).toContain("ModeHomeTemplate"); + expect(modeHomeTemplateSource).toContain("sm:grid-cols-[repeat(auto-fit,minmax(15rem,1fr))]"); + expect(modeHomeTemplateSource).toContain("sm:flex-wrap"); expect(homeSource).toContain("desktopComposerSlotId={modeHomeDesktopComposerSlotId}"); expect(homeSource).toContain("ModeHomeVerificationFooter"); expect(responsiveStackCount(detailSource)).toBeGreaterThanOrEqual(2); @@ -105,6 +108,9 @@ describe("clinical accent contrast contract", () => { expect(source).not.toMatch(/bg-\[color:var\(--clinical-accent\)\][^"\n]*\btext-white\b/); expect(source).toContain("clinical-accent-contrast"); } + expect(homeSource).toContain("ModeHomeTemplate"); + expect(homeSource).not.toMatch(/background:var\(--clinical-accent\);color:#(?:fff|ffffff)/i); + expect(homeSource).not.toMatch(/bg-\[color:var\(--clinical-accent\)\][^"\n]*\btext-white\b/); expect(pathwaysSource).not.toContain('? "#fff" : "var(--clinical-accent)"'); expect(briefSource).not.toContain('? "#fff" : "var(--clinical-accent)"'); }); From 664c72230766827ad44a18cb17be74da7b0df8bf Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 19 Jul 2026 01:18:19 +0800 Subject: [PATCH 19/21] fix: preserve therapy loading landmark --- src/components/therapy-compass/workspace.tsx | 12 ++++++------ tests/therapy-compass-mode-wiring.test.ts | 5 ++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/therapy-compass/workspace.tsx b/src/components/therapy-compass/workspace.tsx index b1a731419..88cbca7d4 100644 --- a/src/components/therapy-compass/workspace.tsx +++ b/src/components/therapy-compass/workspace.tsx @@ -73,14 +73,14 @@ function TherapyCompassMain({ asMain: boolean; }) { const b = useTcBindings(); - // Home normally leaves
    to ModeHomeMain. On catalogue load failure the - // home child (and its landmark) is replaced by TherapyCompassDataError, so - // promote this shell to
    for the error state only. - const useMainLandmark = asMain || Boolean(b.error); + // Home normally leaves
    to ModeHomeMain. Initial loading and load + // failure replace that child, so the workspace must own the landmark then. + const homeNeedsMainLandmark = Boolean(b.error) || (b.loading && b.therapies.length === 0); + const useMainLandmark = asMain || homeNeedsMainLandmark; const Tag = useMainLandmark ? "main" : "div"; // Home padding comes from ModeHomeMain; non-home routes keep the workspace gutter. - // Error-on-home still needs the non-home gutter so the alert isn't flush. - const style = asMain || b.error ? s(`min-width:0;padding:32px 40px 40px;`) : s(`min-width:0;`); + // Loading/error-on-home still need the non-home gutter so feedback isn't flush. + const style = asMain || homeNeedsMainLandmark ? s(`min-width:0;padding:32px 40px 40px;`) : s(`min-width:0;`); return ( {b.error ? : children} diff --git a/tests/therapy-compass-mode-wiring.test.ts b/tests/therapy-compass-mode-wiring.test.ts index 25a892be6..db1b8e1b9 100644 --- a/tests/therapy-compass-mode-wiring.test.ts +++ b/tests/therapy-compass-mode-wiring.test.ts @@ -59,7 +59,10 @@ describe("Therapy Compass production-mode wiring", () => { // Home uses ModeHomeMain; workspace must not wrap home in a second
    . expect(homeSrc).toMatch(/ModeHomeMain/); expect(workspaceSrc).toMatch(/asMain=\{!isHome\}/); - expect(workspaceSrc).toContain("const useMainLandmark = asMain || Boolean(b.error);"); + expect(workspaceSrc).toContain( + "const homeNeedsMainLandmark = Boolean(b.error) || (b.loading && b.therapies.length === 0);", + ); + expect(workspaceSrc).toContain("const useMainLandmark = asMain || homeNeedsMainLandmark;"); expect(workspaceSrc).toContain('const Tag = useMainLandmark ? "main" : "div"'); }); From 0be679447817b267e00ee1259b3018f6b46758da Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 19 Jul 2026 01:31:43 +0800 Subject: [PATCH 20/21] test: follow therapy home search action --- tests/ui-route-coverage.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ui-route-coverage.spec.ts b/tests/ui-route-coverage.spec.ts index e90cb6120..7f36c923e 100644 --- a/tests/ui-route-coverage.spec.ts +++ b/tests/ui-route-coverage.spec.ts @@ -224,8 +224,8 @@ test.describe("previously uncovered production routes", () => { }, async (currentPage) => { const search = currentPage - .getByRole("navigation", { name: "Therapy sections" }) - .getByRole("button", { name: "Search", exact: true }); + .getByRole("region", { name: "Common therapy searches" }) + .getByRole("button", { name: "Anxiety in outpatient care", exact: true }); await expect(search).toBeEnabled(); await search.click(); await expect(currentPage.getByRole("heading", { name: "Therapy Search", level: 1 })).toBeVisible(); From b54e589bd45c66197e861c8ae4677317618e73a9 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 19 Jul 2026 01:32:41 +0800 Subject: [PATCH 21/21] Initial plan (#895)