From ecb5855ec7167c717d37f659ea357a45ecac1815 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:21:19 +0800 Subject: [PATCH 1/4] perf: lazy-load document pdf viewer --- src/components/DocumentViewer.tsx | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index 05f21b30..d37baa3f 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -133,6 +133,34 @@ function PdfPreviewLoading() { ); } +// pdf-canvas-viewer is only needed after a source document has loaded and the +// user is viewing a PDF. Keeping it out of the document route's initial client +// chunk avoids parsing its reader controls for image, text, and download-only +// documents. pdf.js itself remains loaded on demand by that component. +const PdfCanvasViewer = dynamic( + () => import("@/components/document-viewer/pdf-canvas-viewer").then((module) => module.PdfCanvasViewer), + { + ssr: false, + loading: () => , + }, +); +const NativePdfEmbed = dynamic( + () => import("@/components/document-viewer/pdf-canvas-viewer").then((module) => module.NativePdfEmbed), + { ssr: false, loading: () => }, +); + +function PdfPreviewLoading() { + return ( +
+ Loading PDF reader… +
+ ); +} + type PageRow = { id: string; page_number: number; From b34d6aac51ca9fc595f354de5bd7e224f6a56540 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 18 Jul 2026 03:06:08 +0800 Subject: [PATCH 2/4] chore: merge design-audit remediation changes --- docs/site-map.md | 5 +- playwright.config.ts | 1 + scripts/generate-site-map.ts | 5 +- src/app/differentials/presentations/route.ts | 5 +- src/app/page.tsx | 4 +- src/components/ClinicalDashboard.tsx | 8 ++- .../master-search-header.tsx | 26 ++------ .../services/services-navigator-page.tsx | 8 +-- src/lib/rag-candidate-sources.ts | 22 ++----- src/lib/rag.ts | 49 +-------------- supabase/drift-manifest.json | 55 ++++------------- ...00_patch_rag_and_corrector_scalability.sql | 8 +-- ...14190000_document_table_facts_trgm_idx.sql | 20 ++++--- ...audit-content-services-regressions.test.ts | 60 ++++++++++--------- .../audit-navigation-auth-regressions.test.ts | 25 +++----- tests/site-map.test.ts | 34 +++++++++++ tests/supabase-schema.test.ts | 23 +++---- tests/ui-accessibility.spec.ts | 2 +- tests/ui-smoke.spec.ts | 4 +- tests/ui-tools.spec.ts | 19 +++++- 20 files changed, 158 insertions(+), 225 deletions(-) diff --git a/docs/site-map.md b/docs/site-map.md index 0a82b758..986979ab 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -14,9 +14,6 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` - `/dsm` - DSM-5 Diagnosis home. Source: `src/app/dsm/page.tsx`. - `/dsm/compare` - DSM diagnosis comparison. Source: `src/app/dsm/compare/page.tsx`. - `/dsm/search` - DSM diagnosis search and catalogue browser. Source: `src/app/dsm/search/page.tsx`. -- `/factsheets` - Patient information sheet library. Source: `src/app/factsheets/page.tsx`. -- `/factsheets/[slug]` - Patient information sheet detail. Source: `src/app/factsheets/[slug]/page.tsx`. -- `/factsheets/search` - Patient information sheet search. Source: `src/app/factsheets/search/page.tsx`. - `/favourites` - Saved clinical items and sets. Source: `src/app/favourites/page.tsx`. - `/forms` - Forms home and search surface. Source: `src/app/forms/page.tsx`. - `/formulation` - Clinical formulation home and local mechanism search surface. Source: `src/app/formulation/page.tsx`. @@ -849,7 +846,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` ## Redirects - `/applications` - Redirects to `/tools`. Source: `src/app/applications/route.ts`. -- `/differentials/presentations` - Redirects to `/differentials/presentations/[slug]`. Source: `src/app/differentials/presentations/route.ts`. +- `/differentials/presentations` - Redirects to `/differentials/presentations/[workflow-slug]`. Source: `src/app/differentials/presentations/route.ts`. - `/documents/source` - Redirects to `/documents/search`. Source: `src/app/documents/source/page.tsx`. - `/medications` - Redirects to `/?mode=prescribing`. Source: `src/app/medications/route.ts`. - `/mockups/favourites-hub` - Redirects to `/favourites`. Source: `src/app/mockups/favourites-hub/page.tsx`. diff --git a/playwright.config.ts b/playwright.config.ts index 4195dd79..83c5452e 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -55,6 +55,7 @@ export default defineConfig({ grepInvert: mockupTag, use: { ...devices["Desktop Chrome"], + reducedMotion: "no-preference", ...(chromiumExecutablePath ? { launchOptions: { executablePath: chromiumExecutablePath } } : {}), }, }, diff --git a/scripts/generate-site-map.ts b/scripts/generate-site-map.ts index d1197981..87ddeb94 100644 --- a/scripts/generate-site-map.ts +++ b/scripts/generate-site-map.ts @@ -41,7 +41,7 @@ const productRouteHandlerPaths = new Set(["/applications", "/differentials/prese const documentedRedirectTargets: Record = { "/applications": "/tools", - "/differentials/presentations": "/differentials/presentations/[slug]", + "/differentials/presentations": "/differentials/presentations/[workflow-slug]", "/medications": "/?mode=prescribing", }; @@ -61,9 +61,6 @@ const routeDescriptions: Record = { "/documents/search": "Documents search command centre.", "/documents/source": "Compatibility redirect to the canonical live document viewer when a valid id is supplied.", "/documents/source/evidence": "Compatibility redirect sharing the canonical live document viewer handoff.", - "/factsheets": "Patient information sheet library.", - "/factsheets/[slug]": "Patient information sheet detail.", - "/factsheets/search": "Patient information sheet search.", "/favourites": "Saved clinical items and sets.", "/forms": "Forms home and search surface.", "/forms/[slug]": "Registry-backed form detail.", diff --git a/src/app/differentials/presentations/route.ts b/src/app/differentials/presentations/route.ts index f485954b..001912f0 100644 --- a/src/app/differentials/presentations/route.ts +++ b/src/app/differentials/presentations/route.ts @@ -3,10 +3,7 @@ import { type NextRequest, NextResponse } from "next/server"; import { getPresentationWorkflowSelectionForDiagnosisIds } from "@/lib/differentials"; export function GET(request: NextRequest) { - const rawQuery = request.nextUrl.searchParams.get("query"); - const legacyQuery = request.nextUrl.searchParams.get("q"); - // Prefer `query`, but fall through on empty/whitespace so `q` remains usable. - const query = (rawQuery?.trim() || legacyQuery?.trim())?.trim(); + const query = (request.nextUrl.searchParams.get("query") ?? request.nextUrl.searchParams.get("q"))?.trim(); const selectedIds = (request.nextUrl.searchParams.get("ids") ?? "") .split(",") .map((id) => id.trim()) diff --git a/src/app/page.tsx b/src/app/page.tsx index 0d6eeafa..6ce30c13 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,7 +5,9 @@ import { HomePageClient } from "@/app/home-page-client"; import { appModeHomeHref, isAppModeId, isAppModeVisible, type AppModeId } from "@/lib/app-modes"; type HomeProps = { - searchParams?: Promise>; + searchParams?: Promise<{ + mode?: string | string[]; + }>; }; function firstSearchParam(value: string | string[] | undefined) { diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 073fe88c..a3605177 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3648,7 +3648,7 @@ export function ClinicalDashboard({ ) : error ? ( ) : null} - {showUniversalAlsoMatches ? ( + {showUniversalAlsoMatches && activeModeResultKind !== "answer" ? ( + + ) : null} + + {showUniversalAlsoMatches && activeModeResultKind === "answer" ? ( ) : null} diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 45995577..adf00ebf 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -33,7 +33,7 @@ import { import { DocumentTagCloud } from "@/components/DocumentTagCloud"; import { PrivacyInputNotice } from "@/components/privacy-input-notice"; -import { restoreFocusUnlessMoved, useDismissableLayer } from "@/components/use-dismissable-layer"; +import { useDismissableLayer } from "@/components/use-dismissable-layer"; import { useHideOnScroll } from "@/components/clinical-dashboard/use-hide-on-scroll"; import { AnswerFollowUpSuggestions } from "@/components/clinical-dashboard/answer-follow-up-suggestions"; import { @@ -747,15 +747,13 @@ export function MasterSearchHeader({ if (scopeOpen || !restoreActionMenuFocusRef.current) return; restoreActionMenuFocusRef.current = false; window.requestAnimationFrame(() => { - restoreFocusUnlessMoved(actionMenuTriggerRef.current); + actionMenuTriggerRef.current?.focus({ preventScroll: true }); }); }, [scopeOpen]); const closeScopeSheet = useCallback(() => { setScopeSheetOpen(false); - window.requestAnimationFrame(() => { - restoreFocusUnlessMoved(actionMenuTriggerRef.current); - }); + window.requestAnimationFrame(() => actionMenuTriggerRef.current?.focus()); }, []); useEffect(() => { @@ -1440,7 +1438,7 @@ export function MasterSearchHeader({ closeButtonClassName="grid h-11 w-11 shrink-0 place-items-center rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]" contentClassName={cn( "bg-[color:var(--surface-lux)]", - scopeSheetFullscreen ? "max-h-full" : "max-h-[min(84dvh,42rem)]", + scopeSheetFullscreen ? "max-h-dvh" : "max-h-[min(84dvh,42rem)]", "sm:max-h-[min(88dvh,44rem)] sm:max-w-xl", )} bodyClassName={cn( @@ -1583,13 +1581,7 @@ export function MasterSearchHeader({ onBlur={(event) => { const nextFocusedElement = event.relatedTarget; if (nextFocusedElement instanceof Node && event.currentTarget.contains(nextFocusedElement)) return; - // Defer dismiss so a pointer activation on a menuitem can land before - // unmount; keyboard leave (Tab/Shift+Tab) still closes on the next frame. - const menuRoot = event.currentTarget; - window.requestAnimationFrame(() => { - if (menuRoot.contains(document.activeElement)) return; - setModeMenuOpen(false); - }); + setModeMenuOpen(false); }} className={cn("relative z-[60] min-w-0", isWorkflowHeader ? "justify-self-start" : "justify-self-center")} > @@ -1598,7 +1590,6 @@ export function MasterSearchHeader({ type="button" onClick={() => { setActionMenuOpen(false); - setCommandDropdownOpen(false); closeScope(false); setModeMenuOpen((open) => !open); }} @@ -1637,11 +1628,6 @@ export function MasterSearchHeader({ id="app-mode-menu" role="menu" aria-label="Choose app mode" - onMouseDown={(event) => { - // Keep focus on the mode trigger so blur-to-dismiss does not - // unmount options before the click lands. - event.preventDefault(); - }} className="polished-scroll fixed left-[max(0.5rem,var(--safe-area-left))] right-[max(0.5rem,var(--safe-area-right))] top-[calc(4.25rem+env(safe-area-inset-top))] z-50 max-h-[min(20rem,calc(100dvh-5.5rem))] overflow-y-auto rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] p-1.5 text-[color:var(--text)] shadow-[var(--shadow-lux)] ring-1 ring-white/25 backdrop-blur-md dark:ring-white/10 sm:absolute sm:left-0 sm:right-auto sm:top-[calc(100%+0.5rem)] sm:w-[min(21rem,calc(100vw-2rem))]" > {visibleAppModeOptions.map((mode, index) => { @@ -1695,7 +1681,7 @@ export function MasterSearchHeader({ ) : null} -
+
{isWorkflowHeader ? ( <>
)} - ); } diff --git a/src/lib/rag-candidate-sources.ts b/src/lib/rag-candidate-sources.ts index 80e30acd..effed08f 100644 --- a/src/lib/rag-candidate-sources.ts +++ b/src/lib/rag-candidate-sources.ts @@ -44,13 +44,6 @@ import type { DocumentIndexUnitMatch, DocumentMemoryCard, SearchResult } from "@ // the floor, which is how the live schema drift (42702) went unnoticed. Log it structurally and, // where telemetry is in scope, record the failing RPC + code so it shows up in rag_retrieval_logs. export type SupabaseRpcError = { message?: string; code?: string; details?: string; hint?: string } | null; -type RpcResult = Promise<{ data: T | null; error: SupabaseRpcError }>; -type AbortableRpc = RpcResult & { - abortSignal?: (signal: AbortSignal) => RpcResult; -}; -type SupabaseRpcClient = { - rpc: (name: string, rpcArgs: Record) => AbortableRpc | PromiseLike; -}; function legacyRankFields(versionedName: string) { if (versionedName === "match_document_chunks_v2") return ["similarity"]; @@ -88,20 +81,15 @@ export async function callVersionedRetrievalRpc versionedName: string, legacyName: string, args: Record, - signal?: AbortSignal, ): Promise<{ data: T | null; error: SupabaseRpcError }> { - const client = supabase as unknown as SupabaseRpcClient; - const executeRpc = async (name: string, rpcArgs: Record) => { - const pending = client.rpc(name, rpcArgs) as AbortableRpc; - const pendingWithAbort = - signal && typeof pending.abortSignal === "function" ? pending.abortSignal(signal) : pending; - return await pendingWithAbort; + const client = supabase as unknown as { + rpc: (name: string, rpcArgs: Record) => Promise<{ data: T | null; error: SupabaseRpcError }>; }; - const versioned = await executeRpc(versionedName, args); + const versioned = await client.rpc(versionedName, args); if (versioned && !isMissingRetrievalRpcError(versioned.error)) return versioned; const legacyArgs = { ...args }; delete legacyArgs.include_public; - const ownerResult = await executeRpc(legacyName, legacyArgs); + const ownerResult = await client.rpc(legacyName, legacyArgs); const ownerFilter = String(args.owner_filter ?? ""); if ( ownerResult.error || @@ -111,7 +99,7 @@ export async function callVersionedRetrievalRpc ) { return ownerResult; } - const publicResult = await executeRpc(legacyName, { + const publicResult = await client.rpc(legacyName, { ...legacyArgs, owner_filter: PUBLIC_OWNER_FILTER_SENTINEL, }); diff --git a/src/lib/rag.ts b/src/lib/rag.ts index 3e9e5a76..5cd3c449 100644 --- a/src/lib/rag.ts +++ b/src/lib/rag.ts @@ -2,7 +2,6 @@ import { createAdminClient } from "@/lib/supabase/admin"; import { retrievalAccessScopeForArgs, retrievalRpcScopeArgs } from "@/lib/owner-scope"; import { callVersionedRetrievalRpc, - createChunkLoadCache, memoryCardChunkScore, mergeSearchResults, recordHybridRpcError, @@ -120,11 +119,6 @@ export { retrievalPlanCacheQuery, } from "@/lib/rag-cache"; import { classifySearchCacheOutcome, recordCacheLookup } from "@/lib/observability/cache-metrics"; -import { - recordAnswerOrigination, - recordAnswerOriginationFinished, - recordCoalescedAnswerWaiter, -} from "@/lib/observability/answer-coalescing-metrics"; import { buildRagSourceBlock, compactContextText, neutralizeIdentityField } from "@/lib/rag-source-block"; export { buildRagSourceBlock, truncateForModel } from "@/lib/rag-source-block"; import { @@ -428,26 +422,6 @@ function throwIfAborted(signal?: AbortSignal) { } } -function awaitWithCallerSignal(pending: Promise, signal?: AbortSignal): Promise { - if (!signal) return pending; - if (signal.aborted) throw signal.reason ?? new DOMException("The operation was aborted.", "AbortError"); - - return new Promise((resolve, reject) => { - const onAbort = () => reject(signal.reason ?? new DOMException("The operation was aborted.", "AbortError")); - signal.addEventListener("abort", onAbort, { once: true }); - pending.then( - (value) => { - signal.removeEventListener("abort", onAbort); - resolve(value); - }, - (error) => { - signal.removeEventListener("abort", onAbort); - reject(error); - }, - ); - }); -} - export type AnswerProgressEvent = { stage: | "retrieved" @@ -1315,7 +1289,6 @@ export async function analyzeQueryWithClassifierFallback( // owner_filter retrieval will use so grounding can never see documents retrieval cannot. corpusGrounding?: { supabase: ReturnType; ownerFilter: string | null }; ownerId?: string | null; - signal?: AbortSignal; }, ) { if ( @@ -1387,16 +1360,10 @@ export async function analyzeQueryWithClassifierFallback( } try { - const verdict = await awaitWithCallerSignal(pending, opts?.signal); + const verdict = await pending; storeClassifierVerdictMemo(memoKey, verdict); return applyClassifierVerdict(analysis, verdict); - } catch (error) { - if ( - error && - (error instanceof DOMException || typeof error === "object") && - (error as { name?: string }).name === "AbortError" - ) - throw error; + } catch { // Transport/parse failures are deliberately NOT memoized: fall back to the deterministic // analysis for this request only, and let the next request retry the classifier. return analysis; @@ -2403,7 +2370,6 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) { // A3: shared across every withMemoryBoostedCandidates call in this request so the same // owner/query memory cards are fetched at most once per (query, embedding-present, count). const memoryCardCache: MemoryCardCache = new Map(); - const chunkLoadCache = createChunkLoadCache(); const documentRankingMetadataCache = createDocumentRankingMetadataCache(); const modeQueryClass = queryClassForClinicalMode(args.queryMode ?? "auto"); const documentFilterList = args.documentIds?.length @@ -2430,7 +2396,6 @@ export async function searchChunksWithTelemetry(args: SearchChunksArgs) { const queryAnalysis = await analyzeQueryWithClassifierFallback(retrievalQuery, analyzeClinicalQuery(retrievalQuery), { corpusGrounding: corpusGroundingScope, ownerId: args.ownerId, - signal: args.signal, }); throwIfAborted(args.signal); if (modeQueryClass) queryAnalysis.queryClass = modeQueryClass; @@ -3156,7 +3121,6 @@ export async function answerQuestionWithScope(args: AnswerQuestionWithScopeArgs) let existing = inflightKey ? answerInflight.get(inflightKey) : undefined; while (existing) { - recordCoalescedAnswerWaiter(); await args.onProgress?.({ stage: "cached", message: "Waiting for an identical cited answer request already in progress.", @@ -3188,15 +3152,8 @@ export async function answerQuestionWithScope(args: AnswerQuestionWithScopeArgs) } } - // Only coalescible requests belong in this process-local signal. Requests - // that intentionally bypass cache/coalescing must not make a replica look - // ineffective, and neither keys nor clinical content leave this function. - if (inflightKey) recordAnswerOrigination(); const pending = answerQuestionWithScopeUncoalesced(args, startedAt).finally(() => { - if (inflightKey) { - answerInflight.delete(inflightKey); - recordAnswerOriginationFinished(); - } + if (inflightKey) answerInflight.delete(inflightKey); }); if (inflightKey) answerInflight.set(inflightKey, pending); return pending; diff --git a/supabase/drift-manifest.json b/supabase/drift-manifest.json index 40ebc996..ed2fe468 100644 --- a/supabase/drift-manifest.json +++ b/supabase/drift-manifest.json @@ -1,9 +1,9 @@ { - "generated_at": "2026-07-17T22:10:14.366Z", + "generated_at": "2026-07-17T14:08:59.595Z", "generator": "scripts/generate-drift-manifest.ts", "postgres_image": "supabase/postgres:17.6.1.127", - "schema_sha256": "302a9ecec8e69564d50035be8480fa5e5686ca6136d96978f2188ecf5fb58be1", - "replay_seconds": 13, + "schema_sha256": "e93fe90cae38654e77b7be0f81e5a0c90b0c7db2fd37d76e32f4f52a089eff45", + "replay_seconds": 30, "snapshot": { "views": [ { @@ -5290,12 +5290,6 @@ "table": "document_table_facts", "def_hash": "9ac4c08564d3f549df99a1e543875cb8" }, - { - "def": "CREATE INDEX document_title_words_document_id_idx ON public.document_title_words USING btree (document_id)", - "name": "document_title_words_document_id_idx", - "table": "document_title_words", - "def_hash": "003cf77523b819a06ff50cf8df6fcf4b" - }, { "def": "CREATE UNIQUE INDEX document_title_words_pkey ON public.document_title_words USING btree (word, document_id)", "name": "document_title_words_pkey", @@ -5356,12 +5350,6 @@ "table": "documents", "def_hash": "fc27ae2194373897e2f316e1cac47fb0" }, - { - "def": "CREATE INDEX documents_registry_projection_lookup_idx ON public.documents USING btree (((metadata ->> 'registry_record_kind'::text)), ((metadata ->> 'registry_record_id'::text))) WHERE ((metadata ->> 'source_kind'::text) = 'registry_record'::text)", - "name": "documents_registry_projection_lookup_idx", - "table": "documents", - "def_hash": "2c19fee4cac85c85d9ec69f179a6cb79" - }, { "def": "CREATE INDEX documents_search_idx ON public.documents USING gin (search_tsv)", "name": "documents_search_idx", @@ -6340,11 +6328,6 @@ "name": "documents_require_publication_approval", "table": "documents" }, - { - "def": "CREATE TRIGGER documents_sync_title_words AFTER INSERT OR DELETE OR UPDATE OF title, status, owner_id ON public.documents FOR EACH ROW EXECUTE FUNCTION public.sync_document_title_words()", - "name": "documents_sync_title_words", - "table": "documents" - }, { "def": "CREATE TRIGGER documents_updated_at BEFORE UPDATE ON public.documents FOR EACH ROW EXECUTE FUNCTION public.set_updated_at()", "name": "documents_updated_at", @@ -6465,9 +6448,10 @@ }, { "acl": [ - "postgres=X/postgres" + "postgres=X/postgres", + "service_role=X/postgres" ], - "def_hash": "7104ee6e947fcca68fdebdf2fa878339", + "def_hash": "a0e9e41b95c9eeff7957dd292f3dd3fa", "signature": "public.cleanup_registry_corpus_document()" }, { @@ -6525,14 +6509,6 @@ "def_hash": "e263f3819b05177abcf2eedd3f468991", "signature": "public.consume_api_subject_rate_limit(text,text,integer,integer)" }, - { - "acl": [ - "postgres=X/postgres", - "service_role=X/postgres" - ], - "def_hash": "833c00fa1743026d9269b9af28e0b86f", - "signature": "public.consume_summary_rate_limits_atomic(uuid,text,integer,integer,integer,integer,integer,integer)" - }, { "acl": [ "postgres=X/postgres", @@ -6554,7 +6530,7 @@ "postgres=X/postgres", "service_role=X/postgres" ], - "def_hash": "cb65883a561cb1f5cd2247213f417a41", + "def_hash": "a5ebdd1ec14008cdf0c8601a32b4d24c", "signature": "public.correct_clinical_query_terms(text,real)" }, { @@ -6562,7 +6538,7 @@ "postgres=X/postgres", "service_role=X/postgres" ], - "def_hash": "a6f0e7c27da08e47e2c01dce4ab5d58d", + "def_hash": "675d521a0746befe09e68e47986c6355", "signature": "public.default_privileges_status(text,text)" }, { @@ -7080,9 +7056,10 @@ }, { "acl": [ - "postgres=X/postgres" + "postgres=X/postgres", + "service_role=X/postgres" ], - "def_hash": "6b51ece12494d136b39977d8532c013c", + "def_hash": "582d35a5082ff0e4879db461294404fd", "signature": "public.sync_document_title_words()" }, { @@ -7596,21 +7573,11 @@ "name": "document_title_words_document_id_fkey", "table": "document_title_words" }, - { - "def": "CHECK ((word = lower(word)))", - "name": "document_title_words_lowercase", - "table": "document_title_words" - }, { "def": "PRIMARY KEY (word, document_id)", "name": "document_title_words_pkey", "table": "document_title_words" }, - { - "def": "CHECK (((length(word) >= 4) AND (length(word) <= 40)))", - "name": "document_title_words_word_length", - "table": "document_title_words" - }, { "def": "FOREIGN KEY (import_batch_id) REFERENCES public.import_batches(id) ON DELETE SET NULL", "name": "documents_import_batch_id_fkey", diff --git a/supabase/migrations/20260714180000_patch_rag_and_corrector_scalability.sql b/supabase/migrations/20260714180000_patch_rag_and_corrector_scalability.sql index 1b37c57a..45d3e118 100644 --- a/supabase/migrations/20260714180000_patch_rag_and_corrector_scalability.sql +++ b/supabase/migrations/20260714180000_patch_rag_and_corrector_scalability.sql @@ -60,14 +60,12 @@ security definer set search_path = public, pg_catalog, pg_temp as $$ begin - if TG_OP = 'DELETE' or - (TG_OP = 'UPDATE' and OLD.status = 'indexed' and NEW.status is distinct from 'indexed') then + if TG_OP = 'DELETE' or (TG_OP = 'UPDATE' and OLD.status = 'indexed' and NEW.status <> 'indexed') then delete from public.document_title_words where document_id = OLD.id; end if; - if (TG_OP = 'INSERT' and NEW.status = 'indexed') or - (TG_OP = 'UPDATE' and NEW.status = 'indexed' and - (OLD.status is distinct from 'indexed' or OLD.title is distinct from NEW.title)) then + if (TG_OP = 'INSERT' and NEW.status = 'indexed') or + (TG_OP = 'UPDATE' and NEW.status = 'indexed' and (OLD.status <> 'indexed' or OLD.title <> NEW.title)) then if TG_OP = 'UPDATE' then delete from public.document_title_words where document_id = NEW.id; diff --git a/supabase/migrations/20260714190000_document_table_facts_trgm_idx.sql b/supabase/migrations/20260714190000_document_table_facts_trgm_idx.sql index 1bff910f..c0523bf6 100644 --- a/supabase/migrations/20260714190000_document_table_facts_trgm_idx.sql +++ b/supabase/migrations/20260714190000_document_table_facts_trgm_idx.sql @@ -1,9 +1,11 @@ --- Intentionally a no-op. --- --- An earlier revision of this migration created --- public.document_table_facts_text_trgm_idx, but --- 20260717010000_harden_rag_scalability_patch.sql always drops that index. --- Keeping a transactional CREATE here forced fresh replays to pay for a write- --- blocking build that is immediately discarded. Environments that already --- applied the CREATE still clean up via the harden migration's DROP INDEX IF EXISTS. -select 1; +-- Migration to add GIN trigram index on document_table_facts text fields for performance optimization +create index if not exists document_table_facts_text_trgm_idx + on public.document_table_facts using gin ( + lower( + coalesce(table_title, '') || ' ' || + coalesce(row_label, '') || ' ' || + coalesce(clinical_parameter, '') || ' ' || + coalesce(threshold_value, '') || ' ' || + coalesce(action, '') + ) extensions.gin_trgm_ops + ); diff --git a/tests/audit-content-services-regressions.test.ts b/tests/audit-content-services-regressions.test.ts index 3a248013..d86cfb4c 100644 --- a/tests/audit-content-services-regressions.test.ts +++ b/tests/audit-content-services-regressions.test.ts @@ -71,14 +71,10 @@ describe("content and services audit regressions", () => { expect(canCompareServices([])).toBe(false); expect(canCompareServices(records.slice(0, 1))).toBe(false); expect(canCompareServices(records.slice(0, 2))).toBe(true); - expect(normalizedServiceNavigatorSource).not.toContain( + expect(normalizedServiceNavigatorSource).toContain( 'key={selected.length === 0 ? "empty" : selected.length === 1 ? "single" : "multiple"}', ); expect(serviceNavigatorSource).not.toContain("useEffect("); - expect(serviceNavigatorSource).toContain("const checklistExpanded = showChecklistDetails && selected.length > 0"); - expect(serviceNavigatorSource).toContain("const comparisonExpanded = showComparison && comparisonAvailable"); - expect(serviceNavigatorSource).toContain("if (remainingCount === 0) setShowChecklistDetails(false)"); - expect(serviceNavigatorSource).toContain("if (remainingCount < 2) setShowComparison(false)"); expect(serviceNavigatorSource).toContain("aria-pressed={selected}"); expect(serviceNavigatorSource).toContain("Add ${service.title} to comparison"); expect(serviceNavigatorSource).toContain("Remove ${service.title} from comparison"); @@ -112,24 +108,28 @@ describe("content and services audit regressions", () => { expect(transport).not.toBeNull(); expect(transport).toMatchObject({ - verification: { locallyVerified: false, confidence: "Medium" }, + verification: { locallyVerified: false, confidence: "Unknown" }, source: { - label: "Office of the Chief Psychiatrist WA — approved MHA 2014 forms", - status: "Source checked", + label: "Transport form workflow entry", + status: "Local source confirmation required", }, }); - expect(transport?.source).toHaveProperty("url"); - expect(transport?.source).toHaveProperty("reviewed"); + expect(transport?.source).not.toHaveProperty("url"); + expect(transport?.source).not.toHaveProperty("published"); + expect(transport?.source).not.toHaveProperty("reviewed"); expect(transport?.source).not.toHaveProperty("pages"); expect(transport?.source).not.toHaveProperty("pageCount"); expect(transport?.source).not.toHaveProperty("reviewDue"); - expect(JSON.stringify(transport?.source)).not.toMatch(/\b\d+\s+pages?\b|\bstatutory\b/i); - expect(formDetailSource).not.toMatch(/\b\d+\s+pages?\b|\bReview due\b/i); + expect(JSON.stringify(transport?.source)).not.toMatch(/\.pdf\b|\b\d+\s+pages?\b|\bact sections?\b|\bstatutory\b/i); + expect(formDetailSource).not.toMatch(/\.pdf\b|\b\d+\s+pages?\b|\bAct sections?\b|\bReview due\b/i); expect(formDetailSource).not.toContain("01 May 2026"); - expect(formDetailSource).not.toMatch(/5\(2\)|Admission order|Treatment order/); - expect(formDetailSource).toContain("Full pathway unavailable"); + expect(formDetailSource).not.toMatch(/\b(?:1A|3A|4A|4B)\b|5\(2\)|Admission order|Treatment order/); + expect(formDetailSource).not.toMatch( + /Pathway navigation is not available yet|Full pathway unavailable|>Source info { expect(form.source?.reviewed, form.slug).toBeUndefined(); } - expect(formsSearchSource).toContain("Title or content match"); - expect(formsSearchSource).toContain("Content match in related pathway"); - expect(formsSearchSource).toContain("View all forms"); + expect(formsSearchSource).not.toMatch( + /\b(?:1A|3A|4A|4B)\b|Evidence 278|Pathways 12|Tasks 8|PSOLIS|Source verified|Official source|Aligned to MHA|Open account setup|View full pathway|Filter controls are coming soon/, + ); + expect(formsSearchSource).toContain("statusToneClass(chip.tone)"); + expect(formsSearchSource).toContain("Title or identifier match"); + expect(formsSearchSource).toContain("Match in form record details"); + expect(formsSearchSource).toContain("Browse all forms"); expect(formsHomeSource).not.toMatch(/Source verified|Open account setup/); expect(formsHomeSource).not.toMatch( /Number, pathway, clock|Maker, clock, copies|Browse pathways|Before, current, parallel, after|starter set of MHA 2014 forms|follow a pathway/, ); - expect(formsHomeSource).toContain("local confirmation"); - expect(formsHomeSource).toContain("Source catalogue reviewed"); + expect(formsHomeSource).toContain("Local confirmation required"); + expect(formsHomeSource).toContain("form records confirmed"); }); it("does not render negative or text-only source statuses as verified", () => { @@ -202,12 +206,14 @@ describe("content and services audit regressions", () => { }); it("claims and renders a form source link only when the record has a URL", () => { - expect(normalizedFormDetailSource).toContain("sourceHref={form.source?.url ?? null}"); - expect(normalizedFormDetailSource).toContain("href={form.source.url}"); - expect(normalizedFormDetailSource).toContain('target="_blank"'); - expect(normalizedFormDetailSource).toContain('rel="noopener noreferrer"'); - expect(normalizedFormDetailSource).toContain("inline-flex min-h-10"); - expect(formDetailSource).toContain("Source link pending"); - expect(formDetailSource).toContain("Official"); + expect(normalizedFormDetailSource).toContain( + '{form.source?.url ? "Source link available" : "No source link available"}', + ); + expect(normalizedFormDetailSource).toMatch(/\{form\.source\?\.url \? \( { "https://clinical-kb.test/differentials/presentations/acute-confusion-encephalopathy?q=acute+confusion&ids=delirium", ); - const presentationsEmptyQueryFallback = redirectPresentations( - new NextRequest("https://clinical-kb.test/differentials/presentations?query=%20%20&q=delirium"), - ); - expect(presentationsEmptyQueryFallback.status).toBe(307); - expect(presentationsEmptyQueryFallback.headers.get("location")).toBe( - "https://clinical-kb.test/differentials/presentations/acute-confusion-encephalopathy?q=delirium", - ); - const medications = redirectMedications(new NextRequest("https://clinical-kb.test/medications?ignored=1")); expect(medications.status).toBe(307); expect(medications.headers.get("location")).toBe("https://clinical-kb.test/?mode=prescribing"); @@ -100,22 +92,21 @@ describe("audit navigation and auth regressions", () => { it("gates private polling and mutations on local readiness plus authenticated status", () => { const privateCapabilityContract = sourceSegment( clinicalDashboardSource, - "const canUsePrivateApis =", + "// Local/demo guests can read the public library", "const canRunSearch =", ); - expect(privateCapabilityContract).toContain("const canUsePrivateApis ="); expect(privateCapabilityContract).toContain( - 'localNoAuthMode || localDevCanAttemptPrivateApis || authStatus === "authenticated"', + 'const canUsePrivateApis = localProjectReady && authStatus === "authenticated";', ); + expect(privateCapabilityContract).not.toMatch(/localNoAuth|clientDemoMode/); const pollingContract = sourceSegment( clinicalDashboardSource, - "if (!nextDemoMode && !canUsePrivateApis) {", "const shouldRefreshWorkState =", + "const [documentsResponse", ); - expect(pollingContract).toContain("if (!nextDemoMode && !canUsePrivateApis) {"); - expect(pollingContract).toContain("setDocuments([]);"); - expect(pollingContract).toContain("return;"); + expect(pollingContract).toContain("(canUsePrivateApis || serverDemoMode)"); + expect(pollingContract).not.toMatch(/localNoAuth|clientDemoMode/); const labelMutationContract = sourceSegment( clinicalDashboardSource, @@ -132,8 +123,8 @@ describe("audit navigation and auth regressions", () => { expect(uploadMutationContract).toContain("if (!canUsePrivateApis) {"); }); - it("keeps the root dashboard H1 as Clinical Guide", () => { + it("keeps the root dashboard H1 as Clinical KB", () => { expect(clinicalDashboardSource.match(/\s*Clinical Guide\s*<\/h1>/); + expect(clinicalDashboardSource).toMatch(/

\s*Clinical KB\s*<\/h1>/); }); }); diff --git a/tests/site-map.test.ts b/tests/site-map.test.ts index 71ea01db..4c28fe5d 100644 --- a/tests/site-map.test.ts +++ b/tests/site-map.test.ts @@ -97,6 +97,40 @@ describe("tracked sitemap", () => { } }); + it("keeps public redirect handlers in product routes and API handlers in the API section", () => { + const data = collectSiteMapData(); + const productSection = siteMap.slice( + siteMap.indexOf("## Main product routes"), + siteMap.indexOf("## Mode/query routes"), + ); + const apiSection = siteMap.slice(siteMap.indexOf("## API routes"), siteMap.indexOf("## Redirects")); + const expectedProductHandlers = [ + ["/applications", "src/app/applications/route.ts", "/tools"], + [ + "/differentials/presentations", + "src/app/differentials/presentations/route.ts", + "/differentials/presentations/[workflow-slug]", + ], + ["/medications", "src/app/medications/route.ts", "/?mode=prescribing"], + ] as const; + + expect(data.apiRoutes.every((route) => route.route === "/api" || route.route.startsWith("/api/"))).toBe(true); + expect(data.publicRouteHandlers.some((route) => route.route === "/auth/callback")).toBe(true); + expect(data.publicRouteHandlers).toContainEqual({ + route: "/icons/[variant]", + file: "src/app/icons/[variant]/route.tsx", + }); + expect(apiSection).not.toContain("`/icons/[variant]`"); + + for (const [route, file, target] of expectedProductHandlers) { + expect(data.publicRouteHandlers).toContainEqual({ route, file }); + expect(data.apiRoutes).not.toContainEqual({ route, file }); + expect(data.redirects).toContainEqual({ route, file, target }); + expect(productSection).toContain(`\`${route}\``); + expect(apiSection).not.toContain(`\`${route}\``); + } + }); + it("documents seeded dynamic slugs", () => { for (const service of serviceRecords) expectDocumentedRoute(service.slug); for (const form of formRecords) expectDocumentedRoute(form.slug); diff --git a/tests/supabase-schema.test.ts b/tests/supabase-schema.test.ts index 33a3f03a..c0f6dcfd 100644 --- a/tests/supabase-schema.test.ts +++ b/tests/supabase-schema.test.ts @@ -129,7 +129,7 @@ const deleteDocumentIfIdleMigration = readFileSync( "utf8", ).replace(/\s+/g, " "); const defaultAclAssertionMigration = readFileSync( - new URL("../supabase/migrations/20260717173000_reassert_supabase_admin_default_privileges.sql", import.meta.url), + new URL("../supabase/migrations/20260717161000_assert_supabase_admin_default_privileges.sql", import.meta.url), "utf8", ).replace(/\s+/g, " "); const defaultAclRoleBootstrap = readFileSync(new URL("../supabase/roles.sql", import.meta.url), "utf8").replace( @@ -1219,7 +1219,7 @@ describe("Supabase Preview replay guards", () => { const migrationFiles = readdirSync(migrationDirectoryUrl) .filter((fileName) => /^\d+_.+\.sql$/.test(fileName)) .sort(); - expect(migrationFiles.at(-1)).toBe("20260717173000_reassert_supabase_admin_default_privileges.sql"); + expect(migrationFiles.at(-1)).toBe("20260717161000_assert_supabase_admin_default_privileges.sql"); }); it("bootstraps safe default ACLs before fresh local and preview migration replay", () => { @@ -1369,10 +1369,9 @@ describe("Supabase Preview replay guards", () => { "create or replace function public.cleanup_registry_corpus_document()", "revoke execute on function public.cleanup_registry_corpus_document()", ); - const cleanupLower = cleanup.toLowerCase(); - expect(cleanupLower).toContain("metadata->>'registry_record_id' = old.id::text"); - expect(cleanupLower).toContain("metadata->>'registry_record_kind' = case tg_table_name"); - expect(cleanupLower).toMatch(/when 'clinical_registry_records' then (pg_catalog\.)?to_jsonb\(old\)->>'kind'/); + expect(cleanup).toContain("metadata->>'registry_record_id' = old.id::text"); + expect(cleanup).toContain("metadata->>'registry_record_kind' = case tg_table_name"); + expect(cleanup).toContain("when 'clinical_registry_records' then to_jsonb(old)->>'kind'"); expect(cleanup).toContain("when 'medication_records' then 'medication'"); expect(cleanup).toContain("when 'differential_records' then 'differential'"); expect(cleanup).not.toContain("registry_record_id')::uuid"); @@ -1396,10 +1395,8 @@ describe("Supabase Preview replay guards", () => { expect(corrector).toContain("lower(canonical) % tok"); expect(corrector).toContain("word % tok"); expect(corrector).toContain("limit 32"); - expect(corrector).toContain("best is not null and best_sim >= min_sim"); - if (corrector.includes("min_sim is null")) { - expect(corrector).toContain("min_sim is null or min_sim < 0.3 or min_sim > 1"); - } + expect(corrector).toContain("set pg_trgm.similarity_threshold = 0.3"); + expect(corrector).toContain("min_sim is null or min_sim < 0.3 or min_sim > 1"); expect(corrector).not.toContain("array_agg(distinct term)"); expect(corrector).not.toContain("unnest(vocab)"); expect(sql).toContain("rag_aliases_canonical_trgm_idx"); @@ -1417,11 +1414,7 @@ describe("Supabase Preview replay guards", () => { "$function$;", ); - // Fresh installs skip the write-blocking CREATE that harden immediately drops. - expect(documentTableFactsTrgmMigration).not.toContain( - "create index if not exists document_table_facts_text_trgm_idx", - ); - expect(documentTableFactsTrgmMigration).toMatch(/intentionally a no-op/i); + expect(documentTableFactsTrgmMigration).toContain("create index if not exists document_table_facts_text_trgm_idx"); expect(hardenRagScalabilityPatchMigration).toContain( "drop index if exists public.document_table_facts_text_trgm_idx", ); diff --git a/tests/ui-accessibility.spec.ts b/tests/ui-accessibility.spec.ts index e18ffd1c..6be58fad 100644 --- a/tests/ui-accessibility.spec.ts +++ b/tests/ui-accessibility.spec.ts @@ -48,7 +48,7 @@ async function expectNoPageHorizontalOverflow(page: Page) { } async function expectDashboardUsable(page: Page) { - await expect(page.getByRole("heading", { level: 1, name: "Clinical Guide" })).toHaveCount(1); + await expect(page.getByRole("heading", { level: 1, name: "Clinical KB" })).toHaveCount(1); await expect(page.getByRole("heading", { name: "Answer" })).toBeVisible(); await expect(page.locator('[aria-label^="Search indexed guidelines by question or keyword"]:visible')).toBeVisible(); await expect(page.getByRole("button", { name: "Open answer options" })).toBeVisible(); diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index f8468792..21cee432 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -873,7 +873,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await gotoApp(page, "/"); await waitForDemoDashboardReady(page); - await expect(page.getByRole("heading", { level: 1, name: "Clinical Guide" })).toHaveCount(1); + await expect(page.getByRole("heading", { level: 1, name: "Clinical KB" })).toHaveCount(1); await expect(page.getByRole("heading", { name: "Answer" })).toBeVisible(); await expect(visibleQuestionInput(page)).toBeVisible(); await expect(page.getByRole("button", { name: "Generate source-backed answer" })).toHaveText(/^\s*Ask\s*$/); @@ -1268,7 +1268,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await expect(page.getByRole("button", { name: "Generate source-backed answer" })).toBeEnabled(); await expect(page.getByTestId("answer-grounding-chip")).toHaveCount(0); expect(answerRequests).toEqual([]); - await expect(page.getByRole("heading", { level: 1, name: "Clinical Guide" })).toBeVisible(); + await expect(page.getByRole("heading", { level: 1, name: "Clinical KB" })).toBeVisible(); await expectDomIntegrity(page, { mobileNav: true, mobileFabReady: false }); await expectNoPageHorizontalOverflow(page); }); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 98dccb93..c20855b1 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -885,7 +885,7 @@ test.describe("Clinical KB tools launcher", () => { await expectNoPageHorizontalOverflow(page); }); - test("forms mode shows source-backed form records in search results", async ({ page }) => { + test("forms mode shows registry-backed form records without unsupported pathway claims", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); await mockAnswerDashboardApi(page); await gotoLauncher(page, "/forms?q=transport%20forms&focus=1&run=1"); @@ -905,6 +905,9 @@ test.describe("Clinical KB tools launcher", () => { await expect( page.getByTestId("form-search-result-transport-crisis-form").getByLabel("Open Transport order"), ).toHaveAttribute("href", "/forms/transport-crisis-form"); + await expect(page.getByRole("button", { name: "Refine" })).toHaveCount(0); + await expect(page.getByText(/Evidence 278|Pathways 12|Tasks 8|Source verified|Aligned to MHA 2014/)).toHaveCount(0); + await expect(page.getByText(/PSOLIS Transport|View full pathway/)).toHaveCount(0); await expect(page.getByTestId("service-search-results")).toHaveCount(0); await expectNoPageHorizontalOverflow(page); }); @@ -1005,6 +1008,7 @@ test.describe("Clinical KB tools launcher", () => { await expect(page.getByTestId("form-search-mobile-results")).toBeVisible(); await expect(page.getByTestId("form-search-mobile-result-transport-crisis-form")).toContainText("Transport order"); + await expect(page.getByText(/PSOLIS Transport|View full pathway|Source verified/)).toHaveCount(0); await expect(visibleGlobalSearchInput(page)).toHaveValue("transport"); await expectNoPageHorizontalOverflow(page); }); @@ -1017,6 +1021,19 @@ test.describe("Clinical KB tools launcher", () => { const dock = page.locator("form.answer-footer-search-dock"); await expect(dock).toBeVisible(); await expect(dock).not.toHaveAttribute("data-scroll-hidden", "true"); + const transition = await dock.evaluate((node) => { + const style = window.getComputedStyle(node); + const durationMs = Math.max( + ...style.transitionDuration.split(",").map((value) => { + const normalized = value.trim(); + const duration = Number.parseFloat(normalized); + return normalized.endsWith("ms") ? duration : duration * 1000; + }), + ); + return { durationMs, property: style.transitionProperty }; + }); + expect(transition.property).toMatch(/transform|all/); + expect(transition.durationMs).toBeGreaterThanOrEqual(100); // focus=1 leaves the composer focused; hide-on-scroll stays off while it has focus. const input = visibleGlobalSearchInput(page).first(); From fac9daaca38fc481cf4cc968e44219afd671ce56 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:08:13 +0800 Subject: [PATCH 3/4] fix: stop Escape focus restore from closing the app-mode menu (#822) * fix: stop Escape focus restore from closing the app-mode menu Scope dismiss deferred a focus restore onto the answer-options trigger. When a mode-menu open landed in the same frame window, that restore stole focus, blur-dismissed the menu, and flaked the Documents mode switch in Production UI CI. Skip restore when the mode menu is open or focus already moved, and wait for the scope popover to hide before opening the menu. Co-authored-by: BigSimmo * fix(test): fail closed when scope popover does not dismiss Only wait for the scope popover when it is visible, and let that wait throw if dismissal times out so the mode-menu open cannot recreate the Escape focus-restore race. Co-authored-by: BigSimmo --------- Co-authored-by: Cursor Agent Co-authored-by: BigSimmo --- .../clinical-dashboard/master-search-header.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 40e793b2..77670a7d 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -33,7 +33,7 @@ import { import { DocumentTagCloud } from "@/components/DocumentTagCloud"; import { PrivacyInputNotice } from "@/components/privacy-input-notice"; -import { useDismissableLayer } from "@/components/use-dismissable-layer"; +import { restoreFocusUnlessMoved, useDismissableLayer } from "@/components/use-dismissable-layer"; import { useHideOnScroll } from "@/components/clinical-dashboard/use-hide-on-scroll"; import { AnswerFollowUpSuggestions } from "@/components/clinical-dashboard/answer-follow-up-suggestions"; import { @@ -747,13 +747,15 @@ export function MasterSearchHeader({ if (scopeOpen || !restoreActionMenuFocusRef.current) return; restoreActionMenuFocusRef.current = false; window.requestAnimationFrame(() => { - actionMenuTriggerRef.current?.focus({ preventScroll: true }); + restoreFocusUnlessMoved(actionMenuTriggerRef.current); }); }, [scopeOpen]); const closeScopeSheet = useCallback(() => { setScopeSheetOpen(false); - window.requestAnimationFrame(() => actionMenuTriggerRef.current?.focus()); + window.requestAnimationFrame(() => { + restoreFocusUnlessMoved(actionMenuTriggerRef.current); + }); }, []); useEffect(() => { From 546e26ef7396ef27ec4c3d70185c183a1c2154f4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 18 Jul 2026 08:11:27 +0000 Subject: [PATCH 4/4] fix: remove duplicate lazy PDF viewer declarations A merge left PdfCanvasViewer/NativePdfEmbed/PdfPreviewLoading declared twice in DocumentViewer, which broke typecheck and the production build. Co-authored-by: BigSimmo --- src/components/DocumentViewer.tsx | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index d37baa3f..05f21b30 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -133,34 +133,6 @@ function PdfPreviewLoading() { ); } -// pdf-canvas-viewer is only needed after a source document has loaded and the -// user is viewing a PDF. Keeping it out of the document route's initial client -// chunk avoids parsing its reader controls for image, text, and download-only -// documents. pdf.js itself remains loaded on demand by that component. -const PdfCanvasViewer = dynamic( - () => import("@/components/document-viewer/pdf-canvas-viewer").then((module) => module.PdfCanvasViewer), - { - ssr: false, - loading: () => , - }, -); -const NativePdfEmbed = dynamic( - () => import("@/components/document-viewer/pdf-canvas-viewer").then((module) => module.NativePdfEmbed), - { ssr: false, loading: () => }, -); - -function PdfPreviewLoading() { - return ( -
- Loading PDF reader… -
- ); -} - type PageRow = { id: string; page_number: number;