diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 351f9d4f5..ca3f523d4 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -840,7 +840,7 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-09 | claude/document-viewer-phase-3-bj5k5v | 156db63f1b60f09791e426b043ea90d427b789ab | post-#1772 test simplification: replace the viewer perf source-text grep with behavioural coverage; de-literalise rail window and keyboard label assertions | PR #1777 opened. Self-review of #1772's own tests against an excessive-strictness challenge. Finding: the client-performance-boundaries grep for resolveLiveCanvasWindow / resolveRenderAheadPages / liveCanvasLimit / requestIdleCallback was not merely brittle, it was INEFFECTIVE - replacing the budget call with a hardcoded 3 leaves every identifier in the file, so it stayed green while the viewer retained three full-zoom canvases (measured both ways). Replaced by a DOM case that binds the budget (VIEWER_MAX_ZOOM at dpr 3 gives ~16.8M backing px against the 24M budget, window collapses to 1) and fails on exactly that substitution. Also exported RAIL_IMAGE_WINDOW so the rail test derives its counts (verified by tuning 6->8: all 7 still pass), and relaxed the keyboard aria-label assertions from exact prose to the key names. Pre-existing greps for disableAutoFetch / canvas.width = 0 / pageToCleanup left alone deliberately - two are now redundant but they are another author's guard. | verify:pr-local (1 pre-existing root-only failure: pr-handoff-stop #291; 5872 passed), build OK 80s + client bundle secret check, eval:rag:offline 36 golden cases / 574 tests, lint + typecheck clean. Sabotage-verified in both directions. Browser gates unrunnable here (#279) - unchanged by this diff. | | 2026-08-09 | claude/planning-build-intelligence-9ot0nm | 3df3cb3993f73cda4dbbc4ac7549f84b3c6ea7ed | Node 24.15 engine floor: engines.node, preinstall hook, check:runtime, session-start provisioning, codex-cloud assertion | Authored and handed off as PR #1771; closes #285; operationalRisk true, clinicalRisk/ragRanking false | test 5800 passed/1 pre-existing root-uid failure (pr-handoff-stop, confirmed on stashed clean tree); lint 0; typecheck 0; prettier --check . pass; check:runtime pass; check:codex-cloud pass; check:outstanding-issues pass; preinstall boundary proof 24.13/24.14.9 reject, 24.15/24.19 accept, 25.0.0 reject; contract test mutation-checked red | | 2026-08-09 | pull/1771 | 466ec4216272c31c5f754db213dbdc529583b167 | PR 1771 runtime floor enforcement | P2: Cloud and Desktop setup paths remain major-only; do not merge until range-aware | static review; check:runtime PASS; check:codex-cloud PASS; ledger PASS; outstanding issues PASS; focused Vitest blocked by active Playwright lease | -| 2026-08-09 | cursor/smarter-meds-search-9c1b | 74c3ea7706802925040b2c5603a7140a54dc9cd3 | medications-catalog-search typos brands | shipped catalog-local typo/brand search; no RAG | npm run test: 5899 passed | +| 2026-08-09 | cursor/therapy-card-densify-e975 | 3db839a6bb1f5b45fc55bb732d21b30551a506b0 | therapy search ResultCard densify (gap, tags, favourite, actions, match cells) | pass — denser cards; band gap fixed; single-row prioritized tags; heart top-right; 3-col actions; summarised cells | unit 35/35; verify:pr-local pass; ensure visual phone+desktop pass | | 2026-08-09 | claude/m3-token-debt-262-261 | c6e1fe7fc42ec6f286eb5a3d8f7ddad7dfad2724 | design-system contract: raw padding/radius/line-height ratchets + type-step selection gate (#262 parts 2/3); closed #218/#270 | Authored and self-verified; PR #1780 open, auto-merge deliberately not armed (gate change). Baseline additive: all 15 pre-existing metrics and every debtByPath entry byte-identical; 94/94 new findings verified present at their cited line. Mutation-tested both halves of part 3 and three failure modes of part 2. | check:design-system-contract, check:icon-scale, check:type-scale, check:outstanding-issues, vitest design-system-contract-utils (31 passed), format:check whole-tree, verify:cheap (exit 1 from 5 pre-existing failures, none in this diff; 3 cleared by merging main, remaining 2 byte-identical to origin/main) | | 2026-08-09 | claude/documentviewer-nav-convergence-oddhjx | 1395d533cb13eadc705e47f76aa9f39a7a11c058 | DocumentViewer / in-page-nav convergence (#288): non-adoption decision recorded in docs/search-chrome-behaviour.md; merged duplicated visible-element predicate into resolveVisibleElement; new convergence guard test | Converged what was duplicated; DocumentReviewer header adoption declined on the merits with four blocking reasons recorded. No contract test edited. | verify:pr-local (546/547 files, 5883 tests pass; sole failure tests/pr-handoff-stop.test.ts reproduced on pristine origin/main), verify:phone-chrome (contracts 123 pass; focused Chromium 7 pass), contract set 12 files/151 tests pass, lint, typecheck, format | | 2026-08-09 | claude/disabled-button-accessibility-piclvr | 722abdb780c715c0a89df268ed48f6c741ffd569 | disabled-placeholder buttons -> aria-disabled + inert handler (25 sites, 13 components); controlDisabled/therapy recipe aria-disabled styling; require-button-wiring redundantDisabledPair gate; wiring-conventions contract rewrite (settles #291) | authored — PR #1778 opened | lint (uncached, exit 0); typecheck; test 5878 passed/1 pre-existing root-env failure in pr-handoff-stop; build; check:rag:fixtures 36 golden cases; prettier --check clean; verify:ui not run (no browser in container) | @@ -850,20 +850,16 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-09 | claude/m3-token-debt-262-261 | 7bac3bd762b381cb25c9b2a15ef3bb7223d15b16 | PR #1780 review-and-fix | fixed P2 ratchet bypasses (arbitrary-property classes, CSS-consumer exemption anti-rot, modern CSS zero units); Bugbot clean; merge-tree clean; required CI was green on prior tip | vitest design-system-contract-utils 32/32; check:design-system-contract; mutation CSS-exemption fail→restore; verify:cheap PASS (549 files / 5933 tests); verify:pr-local stages PASS (test flake in design-system-adoption timed out once then 51/51 + full test 549/549 + check:rag:fixtures PASS); no provider gates | | 2026-08-09 | cursor/dsm-search-header-fix-15d6 | df088c766f1761496189ec09146aa54c23b1c012 | dsm-search-header | pass: removed catalogue page strip; ribbon + category filter match target | vitest dsm-search-empty-state; npm test 5857 passed; lint; typecheck; ensure phone /dsm/search?q=Delirium | | 2026-08-09 | claude/m3-token-debt-262-261 | fe75e6acade008e68f953e235cc035f2e5d9d216 | PR #1780 review-and-fix | fixed P2 ratchet bypasses; synced origin/main (#1775); Bugbot clean; merge-tree clean | vitest design-system-contract-utils 32/32; check:design-system-contract; mutation CSS-exemption; verify:cheap PASS 549/5933; verify:pr-local stages PASS after adoption flake retest; check:rag:fixtures PASS; no provider gates | -| 2026-08-09 | cursor/smarter-meds-search-9c1b | aced65e055892b0e2927b3999f95c6435102f610 | medications-catalog-search typos brands | main sync; catalog-local typo/brand search complete; no RAG | medications+route tests 49 passed; merge-tree clean | +| 2026-08-09 | cursor/therapy-card-densify-e975 | 52f07d49f89e6c786c624ccbd38ae552818a2071 | PR 1783 babysit | fixed review threads: TagRow +N clip, title/alias preview exclusion, preview field fallbacks; Copilot md grid kept; CI re-triggered after Copilot tip | npm test: 5958 passed / 4 skipped | | 2026-08-09 | claude/in-page-nav-pr-3-i6gi8n | 6651feef4fab63f1181fba57908cb22e2932df3c | in-page-nav PR 3: convert /medications/[slug] (panel-swap) and /factsheets/[slug] (anchors) onto InPageNavHeader; record the differentials-presentations exception; delete orphaned SecondaryNavigation (#271) | converted 2 of 3 routes, 3rd recorded as a reasoned lasting exception; tocFor and SecondaryNavigation deleted; route-sections contract 7 -> 12 routes plus a panel-swap suite | verify:pr-local (1 pre-existing root-permission failure in pr-handoff-stop.test.ts, all else green); test 5932 passed; in-page-nav-route-sections 29 passed; verify:phone-chrome 3/4 stages (focused-browser blocked by #255 Chromium 1194 vs 1234); build + bundle-budget + rag:fixtures green; verify:ui not run (#255, delegated to CI) | -| 2026-08-09 | cursor/smarter-meds-search-9c1b (PR #1785) | 94dd432c0f64fd0593ea68d15aaa612240e7cd1d | PR #1785 unblock/fix | synced origin/main (behind-but-clean); merge-tree clean; prior tip CI green except PR mergeability DIRTY; review threads already cleared | merge-tree clean; behind 0; prior abb827b1 PR required+Production UI green; focused meds tests to re-run after sync | | 2026-08-09 | PR #1782 / cursor/fix-document-open-scroll-e5bf | 5709f2cc7a954197e02107c96d7896d8d13445c3 | document-viewer open-at-top | ship: remove chunk mount scrollIntoView so document opens stay at overview top | document-viewer-shell.dom 7 pass; document-section-summary.dom 8 pass; verify:pr-local dry-run | | 2026-08-09 | cursor/fix-document-open-scroll-e5bf (PR #1782) | 98029875db7d640d3e699829249bb33892296bff | PR #1782 unblock | before: static-pr+coverage failed on stale adoption-manifest (document-viewer-shell testFiles drift), merge-tree clean 0 behind, auto-merge armed, 1 advisory CodeRabbit waitFor thread; after: regenerated adoption-manifest, hardened scroll negative assertion, pre-commit+handoff adoption sync to prevent recurrence; CodeRabbit dispositioned as fixed by sync assert | check:design-system-adoption PASS; vitest design-system-adoption+document-viewer-shell+docs-inventory 63/63 PASS; format; no provider-backed checks | | 2026-08-09 | cursor/fix-document-open-scroll-e5bf (PR #1782) | 86698228533ebe10452c10c1bd7a3e1610d891ae | PR #1782 unblock | merged origin/main (behind-but-clean); fixed static-pr TS2322 on document-viewer-shell chunk fixture; fixed Production UI DSM compare remove stall via location.assign + DOM proof; prior adoption-manifest drift already fixed | tsc clean for changed files; vitest document-viewer-shell+dsm-compare-remove+design-system-adoption 59/59 PASS; check:design-system-adoption PASS; format; no provider-backed checks | -| 2026-08-09 | cursor/smarter-meds-search-9c1b (PR #1785) | b722c628ca05eb32190ac6355e8ee0537817621c | PR #1785 unblock/fix | synced origin/main (#1782); behind-but-clean DIRTY cleared; merge-tree clean; review threads clear; prior tip product CI green | merge-tree clean; behind 0; test:focused meds after sync | -| 2026-08-09 | cursor/therapy-card-densify-e975 | 3db839a6bb1f5b45fc55bb732d21b30551a506b0 | therapy search ResultCard densify (gap, tags, favourite, actions, match cells) | pass — denser cards; band gap fixed; single-row prioritized tags; heart top-right; 3-col actions; summarised cells | unit 35/35; verify:pr-local pass; ensure visual phone+desktop pass | -| 2026-08-09 | cursor/therapy-card-densify-e975 | 52f07d49f89e6c786c624ccbd38ae552818a2071 | PR 1783 babysit | fixed review threads: TagRow +N clip, title/alias preview exclusion, preview field fallbacks; Copilot md grid kept; CI re-triggered after Copilot tip | npm test: 5958 passed / 4 skipped | -| 2026-08-10 | cursor/smarter-meds-search-9c1b (PR #1785) | a4f57500f6b16a4616e1f84c126c2f787a40766b | PR #1785 unblock/fix | before: DIRTY/CONFLICTING behind-but-clean (merge-tree clean, behind 3/ahead 8); prior tip a4f57500 CI green; 0 unresolved threads → after: merged origin/main once (sync-only); merge-tree clean; behind 0; no CI/thread code fixes; focused meds tests 201 passed | git merge-tree clean; npm run format; npm run test:focused meds/route/universal-search 201 passed; no provider-backed checks run | -| 2026-08-10 | cursor/smarter-meds-search-9c1b (PR #1785) | 88dbdd80ede81ec6062ffebae79244703d495a99 | PR #1785 unblock/fix | before: BEHIND/MERGEABLE behind-but-clean (merge-tree clean, behind 2/ahead 9); tip 88dbdd80 required CI green; → after: late merged origin/main once (#1793/#1794); merge-tree clean; behind 0; no required-CI code fixes; no provider-backed checks | git merge-tree clean; npm run format; prior tip CI green; no provider-backed checks run | +| 2026-08-10 | PR #1788 | cc64d8356ca964ef2e550f4493f17778965ada79 | Run PR sweep | merged origin/main (resolved in-page-nav ContextualBackLink+rail); fixed restore metadata/signature/guest clear, privacy copy, tap targets, medication pathname mock; threads dispositioned; tip follows | merge-tree clean; vitest answer-thread/privacy/nav/medication/in-page-nav pass; CI pending after push | +| 2026-08-10 | PR #1788 | aaeb54630fde2c05efe9a90eda13fbb92cc933c0 | Run PR sweep | Static PR maintainability: extracted useAnswerThreadBootstrap (ClinicalDashboard 4144→4106); merged origin/main (#1794/#1795) | check:maintainability-budgets pass; vitest bootstrap+storage 17; tsc clean; CI pending after push | | 2026-08-10 | PR #1797 / claude/codex-m4a-retire-dead-type-8wq9ta | 6bf3c7b2a0600021290e165302fd07d721af6592 | retire the dead --text-2xl-compact type step (ledger #297): globals.css @theme, twMerge config, two test lists, the design-system-contract exemption, TOKENS.md/GATES.md | Executed the recorded next action on outstanding-issues #297. The step had zero class-utility and zero var(--text-*) consumers, so the deletion renders identically; UNUSED_TYPE_STEP_EXEMPTIONS is now empty and the declared-but-unconsumed gate holds the line with no carve-out. One test fixture using the token as a synthetic var() consumer was repointed at --text-2xl-minus. GATES.md corrected to eight non-standard steps; the 705-consumer total is unchanged because this step contributed 0. No clinical, RAG-ranking or operational risk paths touched (classifyPullRequestFiles: all false). | check:design-system-contract PASS (705 production files); check:type-scale --strict PASS; lint exit 0; typecheck exit 0; npm run build after rm -rf .next exit 0 (Compiled successfully in 63s); check:outstanding-issues PASS; verify:pr-local completed through typecheck then failed at test on a PRE-EXISTING root-permission failure in tests/pr-handoff-stop.test.ts that reproduces on clean d812c76 (5993 passed, 1 failed); build and check:rag:fixtures run/assessed separately. No UI gate: no rendered output can change. No provider-backed check run. | | 2026-08-10 | codex/visual-baseline-advisory-pr | 6bc57714c36bc6d027561bb8f5f8b00bb92524b2 | PR #1791 babysit unblock | fixed Production UI formulation Clear→Draft flake settle; classified visual drift vs non-drift failures | test:ci-workflows 263; classify-visual-baseline-outcome+ci-cache-safety 40 | -| 2026-08-10 | cursor/smarter-meds-search-9c1b (PR #1785) | 5cb0e11e077a3aaf5b8e4ea37b26ac72b0328997 | PR #1785 unblock/fix | before: Production UI (3) failed on service-detail scroll endpoint (remaining 67px) at 38b3bd0c; GitHub DIRTY behind-but-clean vs #1791. after: merged origin/main + re-scroll toPass fix in ui-tools service-detail test; threads untouched; do not merge | CI Production UI (3) logs; git merge-tree clean; prettier ui-tools; product fix in same tip commit as this row | +| 2026-08-10 | PR #1788 / codex/chat-contextual-back-answer-cache-05ea-1 | 98dd877ab4bd41e169310004c3b91aa4780d3772 | Run PR sweep | before: Production UI (2) failed on Breadcrumb/Medications selector; after: use Back to medications aria-label + contract guard; disposition Codex/Sentry/CodeRabbit threads; merged origin/main | vitest in-page-nav-contract+answer-thread-storage 19p; format; merge-tree clean | | 2026-08-09 | origin/pr/1686 | a5cce760d73bd174dba200b53852568dcdb9be0d | PR #1686 CI testing perfection and merged rollout reconciliation | Merged required CI was green, but hosted evidence confirmed P2 shard imbalance, duplicated critical coverage, net-negative 1.09 GB cache transport, inactive container revision enforcement, duplicated workflow/build/browser work, and missing local npm-ci selection. Fixed locally on current main; no P0/P1. | Hosted run 31285952061 inspected; focused Vitest 55 passed plus browser-preflight 12 passed; CI workflow suite 256 passed; typecheck passed; CI scope, verification plan, shard parity, gate manifest, action pins, npm-ci dry-run, docs and outstanding-issues guards passed; no Playwright/browser run or provider mutation. | | 2026-08-10 | codex/ci-perfected-rollout-20260809 (PR #1789) | accbc7c6324b839112ff8df8f9b66d3557f2b98e | PR babysit | unblocked; merged origin/main (false-DIRTY behind-but-clean); fixed Codex P2 ui_changed for Playwright runner helpers; thread replied+resolved | ci-change-scope --self-test pass; merge-tree clean; no provider gates | | 2026-08-10 | codex/ci-perfected-rollout-20260809 (PR #1789) | bf437370441c43a35ec63353642b0180ba5beba6 | PR babysit | late sync: merged origin/main (#1793/#1794); behind-but-clean; prior tip CI green; no code fixes | merge-tree clean; format clean; prior tip PR required pass; no provider gates | @@ -874,3 +870,10 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-10 | cursor/same-mode-focus-no-steal-6df8 | a6a5e4cd59352244163a5d6d5439c2bc40a7ff95 | Run PR sweep | fix: Unit coverage tsconfig contract aligned to #1798 ignoreDeprecations; merged origin/main | vitest test-runner-safety+check-lighthouse-budget 84 passed; Unit coverage was FAIL on 3f2aae3a | | 2026-08-10 | cursor/same-mode-focus-no-steal-6df8 | f8d7ef3faa397de7c9aecc61a66ec5428a3eeed3 | Run PR sweep | fix: Unit coverage tsconfig contract aligned to #1798 ignoreDeprecations; merged origin/main | vitest test-runner-safety+check-lighthouse-budget 84 passed; Unit coverage was FAIL on 3f2aae3a | | 2026-08-10 | cursor/same-mode-focus-no-steal-6df8 | e6eea4d9b677de6919165753b9ceae059bac1190 | Run PR sweep | fix: Unit coverage tsconfig contract aligned to #1798 ignoreDeprecations; merged origin/main | vitest test-runner-safety+check-lighthouse-budget 84 passed; Unit coverage was FAIL on 3f2aae3a | +| 2026-08-09 | cursor/smarter-meds-search-9c1b | 74c3ea7706802925040b2c5603a7140a54dc9cd3 | medications-catalog-search typos brands | shipped catalog-local typo/brand search; no RAG | npm run test: 5899 passed | +| 2026-08-09 | cursor/smarter-meds-search-9c1b | aced65e055892b0e2927b3999f95c6435102f610 | medications-catalog-search typos brands | main sync; catalog-local typo/brand search complete; no RAG | medications+route tests 49 passed; merge-tree clean | +| 2026-08-09 | cursor/smarter-meds-search-9c1b (PR #1785) | 94dd432c0f64fd0593ea68d15aaa612240e7cd1d | PR #1785 unblock/fix | synced origin/main (behind-but-clean); merge-tree clean; prior tip CI green except PR mergeability DIRTY; review threads already cleared | merge-tree clean; behind 0; prior abb827b1 PR required+Production UI green; focused meds tests to re-run after sync | +| 2026-08-09 | cursor/smarter-meds-search-9c1b (PR #1785) | b722c628ca05eb32190ac6355e8ee0537817621c | PR #1785 unblock/fix | synced origin/main (#1782); behind-but-clean DIRTY cleared; merge-tree clean; review threads clear; prior tip product CI green | merge-tree clean; behind 0; test:focused meds after sync | +| 2026-08-10 | cursor/smarter-meds-search-9c1b (PR #1785) | a4f57500f6b16a4616e1f84c126c2f787a40766b | PR #1785 unblock/fix | before: DIRTY/CONFLICTING behind-but-clean (merge-tree clean, behind 3/ahead 8); prior tip a4f57500 CI green; 0 unresolved threads → after: merged origin/main once (sync-only); merge-tree clean; behind 0; no CI/thread code fixes; focused meds tests 201 passed | git merge-tree clean; npm run format; npm run test:focused meds/route/universal-search 201 passed; no provider-backed checks run | +| 2026-08-10 | cursor/smarter-meds-search-9c1b (PR #1785) | 88dbdd80ede81ec6062ffebae79244703d495a99 | PR #1785 unblock/fix | before: BEHIND/MERGEABLE behind-but-clean (merge-tree clean, behind 2/ahead 9); tip 88dbdd80 required CI green; → after: late merged origin/main once (#1793/#1794); merge-tree clean; behind 0; no required-CI code fixes; no provider-backed checks | git merge-tree clean; npm run format; prior tip CI green; no provider-backed checks run | +| 2026-08-10 | cursor/smarter-meds-search-9c1b (PR #1785) | 5cb0e11e077a3aaf5b8e4ea37b26ac72b0328997 | PR #1785 unblock/fix | before: Production UI (3) failed on service-detail scroll endpoint (remaining 67px) at 38b3bd0c; GitHub DIRTY behind-but-clean vs #1791. after: merged origin/main + re-scroll toPass fix in ui-tools service-detail test; threads untouched; do not merge | CI Production UI (3) logs; git merge-tree clean; prettier ui-tools; product fix in same tip commit as this row | diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index 94a1f18f2..f0a28bf62 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -250,7 +250,11 @@ "preview": ".design-sync/previews/Breadcrumb.tsx", "previewValid": true }, - "testFiles": ["tests/design-sync-visual-exports.test.ts", "tests/information-page-shell.dom.test.tsx"], + "testFiles": [ + "tests/design-sync-visual-exports.test.ts", + "tests/in-page-nav-playwright-contract.test.ts", + "tests/information-page-shell.dom.test.tsx" + ], "baseline": { "targetLayer": "v2", "liveLayer": "v2", diff --git a/src/app/privacy/page.tsx b/src/app/privacy/page.tsx index bbd2c26b6..31c5c864e 100644 --- a/src/app/privacy/page.tsx +++ b/src/app/privacy/page.tsx @@ -43,7 +43,9 @@ const SECTIONS: Section[] = [ <> Raw question text is not written to query logs by default; logs use a keyed one-way hash. Generated answer text is also omitted from durable query logs by default. A short-lived response cache can contain the answer while - its read TTL is valid. + its read TTL is valid. To make recent answers reappear quickly, completed answer threads may also remain in this + browser tab for up to 12 hours. That tab-only copy stays in this tab, is not shared across tabs or devices, and + is never sent to the application service. ), }, @@ -64,7 +66,7 @@ const SECTIONS: Section[] = [ }, { heading: "Retention", - body: "Repository migrations configure 30-day retention for RAG query records, 90-day retention for retrieval logs and query-miss telemetry, and a bounded hourly purge of expired response-cache rows when the database scheduler is available. The operator must verify that those scheduled jobs are active. Uploaded documents remain until removed under the applicable process. Safety-plan working content has no Clinical KB retention: it is discarded when the component is cleared or the tab is closed. Clipboard, print, and PDF copies are outside the app and must follow the organisation's approved record-handling process.", + body: "Repository migrations configure 30-day retention for RAG query records, 90-day retention for retrieval logs and query-miss telemetry, and a bounded hourly purge of expired response-cache rows when the database scheduler is available. The operator must verify that those scheduled jobs are active. Uploaded documents remain until removed under the applicable process. Completed answer threads in the current browser tab expire no later than 12 hours after the most recent answer and are also cleared by New chat, sign-out, or an account change. Safety-plan working content has no Clinical KB retention: it is discarded when the component is cleared or the tab is closed. Clipboard, print, and PDF copies are outside the app and must follow the organisation's approved record-handling process.", }, { heading: "Your responsibilities", diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 707ed02ba..4b1e30536 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -157,7 +157,7 @@ import { UploadPanel, } from "@/components/clinical-dashboard/clinical-dashboard-lazy"; -import { clearLegacyRecentQueries, demoRecentQueryOwnerId, recentQueryStorageKey } from "@/lib/recent-query-storage"; +import { clearLegacyRecentQueries, recentQueryStorageKey } from "@/lib/recent-query-storage"; import type { SearchFacets } from "@/components/clinical-dashboard/document-search-results"; import { isWeakRelevance } from "@/components/clinical-dashboard/relevance"; import { @@ -212,10 +212,15 @@ import { useDeferredRegistrySearch } from "@/components/clinical-dashboard/use-d import { buildAnswerFollowUpQuery, buildAnswerFollowUpSuggestions } from "@/lib/answer-follow-up"; import { clearPersistedAnswerThread, - loadPersistedAnswerThread, + createAnswerThreadSnapshotMetadata, maxStoredAnswerTurns, - savePersistedAnswerThread, } from "@/lib/answer-thread-storage"; +import { useAnswerThreadBootstrap } from "@/components/clinical-dashboard/use-answer-thread-bootstrap"; +import { + resolveDashboardAnswerThreadOwnerId, + usePersistedAnswerThread, + type AnswerThreadSnapshotMetadata, +} from "@/components/clinical-dashboard/use-persisted-answer-thread"; import { buildAnswerClipboardText } from "@/components/clinical-dashboard/answer-copy-payload"; import { buildAnswerRenderModel, isAnswerSourceBacked } from "@/lib/answer-render-policy"; import { @@ -342,6 +347,7 @@ export function ClinicalDashboard({ const threadRestoreScrolledRef = useRef(false); const restoredThreadFromStorageRef = useRef(false); const latestAnswerTurnRef = useRef | null>(null); + const latestAnswerSnapshotMetadataRef = useRef(null); const answerTurnSeqRef = useRef(0); const [documentMatches, setDocumentMatches] = useState([]); const [searchRelevance, setSearchRelevance] = useState(null); @@ -401,6 +407,7 @@ export function ClinicalDashboard({ setLatestAnswerQuery(null); setCollapsedTurnIds(new Set()); setShowEarlierTurns(false); + latestAnswerSnapshotMetadataRef.current = null; const ownerId = activeAnswerThreadOwnerIdRef.current; if (ownerId) clearPersistedAnswerThread(ownerId); }, []); @@ -656,57 +663,33 @@ export function ClinicalDashboard({ openAccountProfile, setSettingsOpen: settingsState.setSettingsOpen, }); - const answerThreadOwnerId = auth.session?.user.id ?? (clientDemoMode ? demoRecentQueryOwnerId : null); - const previousAnswerThreadOwnerIdRef = useRef(answerThreadOwnerId); - useEffect(() => { - const previousOwnerId = previousAnswerThreadOwnerIdRef.current; - previousAnswerThreadOwnerIdRef.current = answerThreadOwnerId; - activeAnswerThreadOwnerIdRef.current = answerThreadOwnerId; - if (!previousOwnerId || previousOwnerId === answerThreadOwnerId) return; - answerThreadBootstrappedRef.current = false; - queueMicrotask(() => { - setPriorAnswerTurns([]); - setLatestAnswerQuery(null); - setCollapsedTurnIds(new Set()); - setAnswer(null); - setSources([]); - latestAnswerTurnRef.current = null; - setAnswerThreadBootstrapped(false); - }); - }, [answerThreadOwnerId]); - useEffect(() => { - if (authStatus === "loading" || answerThreadBootstrappedRef.current) return; - queueMicrotask(() => { - const persisted = answerThreadOwnerId ? loadPersistedAnswerThread(answerThreadOwnerId) : null; - if (persisted) { - restoredThreadFromStorageRef.current = true; - setPriorAnswerTurns(persisted.priorTurns); - setLatestAnswerQuery(persisted.latestTurn?.query ?? null); - if (persisted.latestTurn) { - latestAnswerTurnRef.current = persisted.latestTurn; - setAnswer(persisted.latestTurn.answer); - setSources(persisted.latestTurn.sources); - setModeSearchSubmitted(true); - setQuery(""); - const restoredQuery = persisted.latestTurn.query.trim(); - if (restoredQuery) autoRunSearchSignatureRef.current = `answer:${restoredQuery}`; - } - answerTurnSeqRef.current = persisted.priorTurns.reduce((max, turn) => { - const match = /^answer-turn-(\d+)$/.exec(turn.id); - return match ? Math.max(max, Number(match[1])) : max; - }, 0); - setCollapsedTurnIds( - persisted.collapsedTurnIds.length - ? new Set(persisted.collapsedTurnIds) - : new Set(persisted.priorTurns.map((turn) => turn.id)), - ); - } else if (!answerThreadOwnerId) { - clearPersistedAnswerThread(); - } - answerThreadBootstrappedRef.current = true; - setAnswerThreadBootstrapped(true); - }); - }, [answerThreadOwnerId, authStatus]); + const answerThreadOwnerId = resolveDashboardAnswerThreadOwnerId(auth.session?.user.id, clientDemoMode, authStatus); + useAnswerThreadBootstrap({ + answerThreadOwnerId, + authStatus, + searchMode, + submittedUrlQuery, + expectedSubmissionSignature: + searchMode === "answer" && submittedUrlQuery + ? searchSubmissionSignature(searchMode, submittedUrlQuery, routedSearchContext) + : undefined, + activeAnswerThreadOwnerIdRef, + answerThreadBootstrappedRef, + restoredThreadFromStorageRef, + latestAnswerTurnRef, + latestAnswerSnapshotMetadataRef, + answerTurnSeqRef, + autoRunSearchSignatureRef, + setPriorAnswerTurns, + setLatestAnswerQuery, + setCollapsedTurnIds, + setShowEarlierTurns, + setAnswer, + setSources, + setModeSearchSubmitted, + setQuery, + setAnswerThreadBootstrapped, + }); // Local no-auth can still exercise public-read APIs, but administration is always // derived separately from the immutable account role claim. const uploadReadOnlyMode = resolveUploadReadOnlyMode({ @@ -839,29 +822,16 @@ export function ClinicalDashboard({ [answerThreadOwnerId], ); - useEffect(() => { - if (!answerThreadBootstrapped) return; - if (searchMode !== "answer") return; - if (!answer && priorAnswerTurns.length === 0) { - if (answerThreadOwnerId) clearPersistedAnswerThread(answerThreadOwnerId); - return; - } - if (!answerThreadOwnerId) return; - savePersistedAnswerThread(answerThreadOwnerId, { - version: 1, - priorTurns: priorAnswerTurns, - latestTurn: latestAnswerTurnRef.current, - collapsedTurnIds: [...collapsedTurnIds], - }); - }, [ - searchMode, + usePersistedAnswerThread({ + ownerId: answerThreadOwnerId, + enabled: answerThreadBootstrapped && searchMode === "answer", answer, - priorAnswerTurns, + priorTurns: priorAnswerTurns, + latestTurn: latestAnswerTurnRef.current, collapsedTurnIds, - latestAnswerQuery, - answerThreadBootstrapped, - answerThreadOwnerId, - ]); + showEarlierTurns, + metadata: latestAnswerSnapshotMetadataRef.current, + }); useEffect(() => { documentsRef.current = documents; @@ -1820,7 +1790,7 @@ export function ClinicalDashboard({ const priorTurn = archivePreviousAnswer ? latestAnswerTurnRef.current : null; if (priorTurn) { const turnId = `answer-turn-${++answerTurnSeqRef.current}`; - setPriorAnswerTurns((turns) => [...turns, { id: turnId, ...priorTurn }].slice(-maxStoredAnswerTurns)); + setPriorAnswerTurns((turns) => [...turns, { id: turnId, ...priorTurn }].slice(-(maxStoredAnswerTurns - 1))); setCollapsedTurnIds((current) => new Set(current).add(turnId)); } const committedQuery = displayQuery ?? payload.query; @@ -2100,6 +2070,15 @@ export function ClinicalDashboard({ // M10: discard a stale response — a newer search owns the UI state. if (requestIsCurrent()) { + if (successfulPayload.kind === "answer") { + latestAnswerSnapshotMetadataRef.current = createAnswerThreadSnapshotMetadata( + searchSubmissionSignature(targetMode, trimmedQuery, { + queryMode: targetQueryMode, + scopeFilters: filtersOverride, + scopeRef: privateScopeRef, + }), + ); + } applySearchResult(successfulPayload, trimmedQuery, !replaceExistingAnswer); if (isDifferentialsMode) setDifferentialEvidenceQuery(trimmedQuery); if (successfulPayload.kind === "answer") { @@ -2108,11 +2087,8 @@ export function ClinicalDashboard({ // effect. Seed their completed context so a later in-place route to // the same query with different intent/scope is recognized as a // replacement search instead of leaving the old answer on screen. - autoRunSearchSignatureRef.current = searchSubmissionSignature(targetMode, trimmedQuery, { - queryMode: targetQueryMode, - scopeFilters: filtersOverride, - scopeRef: privateScopeRef, - }); + autoRunSearchSignatureRef.current = + latestAnswerSnapshotMetadataRef.current?.latestSubmissionSignature ?? null; // The composer is a draft box in a conversation: clear it so the // user can type the next follow-up immediately. setQuery(""); diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index c8b41cf57..246fd1845 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -1,6 +1,5 @@ "use client"; -import Link from "next/link"; import { useRouter } from "next/navigation"; import { CircleAlert, @@ -17,6 +16,7 @@ import { } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { documentDisplayTitle } from "@/components/DocumentOrganizationBadges"; +import { ContextualBackLink } from "@/components/contextual-back-link"; import { PhoneFooterLayerPortal } from "@/components/clinical-dashboard/phone-footer-layer-portal"; import { useActiveScrollOwner } from "@/components/clinical-dashboard/use-active-scroll-owner"; import { PhoneHeaderCollapsePortal } from "@/components/clinical-dashboard/phone-header-collapse-portal"; @@ -1091,14 +1091,14 @@ export function DocumentViewer({ className="edge-glass-header relative z-30 border-b border-[color:var(--border)] py-2 shadow-[var(--shadow-tight)] backdrop-blur-xl max-sm:pt-2 sm:sticky sm:top-0 sm:pt-[max(0.5rem,env(safe-area-inset-top))]" >
-
- Search - + diff --git a/src/components/differentials/differential-presentation-workflow-page.tsx b/src/components/differentials/differential-presentation-workflow-page.tsx index de2df7b2b..4c175d7e2 100644 --- a/src/components/differentials/differential-presentation-workflow-page.tsx +++ b/src/components/differentials/differential-presentation-workflow-page.tsx @@ -21,6 +21,7 @@ import { import { DiagnosisTermChip, DiagnosisTermInlineList } from "@/components/differentials/diagnosis-term-link"; import { CopyAfterReviewButton } from "@/components/differentials/differential-presentation-actions"; import { PhoneFooterLayerPortal } from "@/components/clinical-dashboard/phone-footer-layer-portal"; +import { ContextualBackLink } from "@/components/contextual-back-link"; import { cn } from "@/components/ui-primitives"; import { isClinicalHingeLabel, resolveDiagnosisTermSegments } from "@/lib/differential-diagnosis-links"; import { @@ -741,25 +742,25 @@ export function DifferentialPresentationWorkflowPage({
- Back - +
- Back to differentials - +
diff --git a/src/components/differentials/differential-stream-workspace.tsx b/src/components/differentials/differential-stream-workspace.tsx index a9bdee9a0..ed2619d6b 100644 --- a/src/components/differentials/differential-stream-workspace.tsx +++ b/src/components/differentials/differential-stream-workspace.tsx @@ -6,6 +6,7 @@ import { useEffect, useId, useMemo, useRef, useState, type ReactNode } from "rea import { createPortal } from "react-dom"; import { ArrowLeft, ArrowRight, ChevronDown, ChevronUp, FileText, GitCompareArrows, ShieldAlert } from "lucide-react"; +import { ContextualBackLink } from "@/components/contextual-back-link"; import { appModeHomeHref } from "@/lib/app-modes"; import { normalizeSearchText } from "@/lib/catalog-search"; import { differentialRouteWithQuery, differentialSelectedCompareHref } from "@/lib/differentials-navigation"; @@ -685,13 +686,13 @@ export function DifferentialStreamWorkspace({ model, query, initialFocus = "" }:

- Back to differential home - + {formulationTemplates.map((template) => { const active = template.id === templateId; + const selectTemplate = () => { + if (template.id === templateId) return; + setTemplateId(template.id); + setEditedDraft(null); + }; return (