From d10747ffab1ec400483e8b1175fe986663b95adf Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 26 Jul 2026 11:09:33 +0800 Subject: [PATCH 1/3] chore: implement audit design fixes and resolve conflicts --- scripts/check-github-action-pins.mjs | 14 +- scripts/check-maintainability-budgets.mjs | 4 +- scripts/design-system-contract-baseline.json | 2 +- src/app/api/answer/route.ts | 14 +- src/app/api/upload/route.ts | 41 +----- src/app/differentials/[id]/not-found.tsx | 20 +++ src/app/differentials/error.tsx | 1 + src/app/documents/[id]/not-found.tsx | 20 +++ src/app/documents/error.tsx | 1 + src/app/dsm/error.tsx | 16 +++ src/app/favourites/error.tsx | 1 + src/app/formulation/error.tsx | 16 +++ src/app/global-error.tsx | 36 ++++- src/app/medications/error.tsx | 16 +++ src/app/therapy-compass/error.tsx | 16 +++ src/app/tools/error.tsx | 1 + src/components/ClinicalDashboard.tsx | 1 + src/components/DocumentViewer.tsx | 74 +++++----- src/components/account-data-provider.tsx | 41 +++++- .../clinical-dashboard/answer-status.tsx | 18 +-- .../document-search-results.tsx | 32 +++-- .../clinical-dashboard/evidence-panels.tsx | 2 +- .../clinical-dashboard/image-lightbox.tsx | 19 ++- .../clinical-dashboard/search-utils.ts | 29 ++++ .../clinical-dashboard/settings-dialog.tsx | 5 - .../clinical-dashboard/signed-image.tsx | 9 +- .../clinical-dashboard/source-actions.tsx | 47 ++++--- .../clinical-dashboard/use-app-preferences.ts | 18 ++- src/components/mode-home-page-skeleton.tsx | 6 +- src/components/route-error-boundary.tsx | 31 ++++- .../services/service-detail-page.tsx | 16 --- .../services/services-navigator-page.tsx | 24 ---- src/components/ui-primitives.tsx | 3 +- src/lib/offline-queue.ts | 127 ++++++++++++++++++ src/lib/service-catalog-mapper.ts | 18 --- tests/private-access-routes.test.ts | 37 ----- tests/visual-evidence-tabs.dom.test.tsx | 6 +- 37 files changed, 508 insertions(+), 274 deletions(-) create mode 100644 src/app/differentials/[id]/not-found.tsx create mode 100644 src/app/documents/[id]/not-found.tsx create mode 100644 src/app/dsm/error.tsx create mode 100644 src/app/formulation/error.tsx create mode 100644 src/app/medications/error.tsx create mode 100644 src/app/therapy-compass/error.tsx create mode 100644 src/lib/offline-queue.ts diff --git a/scripts/check-github-action-pins.mjs b/scripts/check-github-action-pins.mjs index 539f90a883..ab71a6623f 100644 --- a/scripts/check-github-action-pins.mjs +++ b/scripts/check-github-action-pins.mjs @@ -173,19 +173,7 @@ if (!/^ image: semgrep\/semgrep@sha256:[0-9a-f]{64}\s*$/m.test(semgrepGateJ // the per-line validation above only covers workflows, a composite skew (e.g. // setup-node v5 vs v7) was previously invisible. Assert each action name resolves // to a single SHA everywhere it is used. -function discoverCompositeActionFiles(workflowRoot) { - const actionsRoot = path.join(workflowRoot, ".github", "actions"); - if (!existsSync(actionsRoot)) return []; - const files = []; - for (const entry of readdirSync(actionsRoot, { withFileTypes: true })) { - if (!entry.isDirectory()) continue; - for (const name of ["action.yml", "action.yaml"]) { - const candidate = path.join(actionsRoot, entry.name, name); - if (existsSync(candidate)) files.push(candidate); - } - } - return files; -} + const actionPinPattern = /uses:\s*([^@\s]+)@([0-9a-f]{40})(?:\s*#\s*(\S+))?/; const shasByAction = new Map(); diff --git a/scripts/check-maintainability-budgets.mjs b/scripts/check-maintainability-budgets.mjs index 464df05f43..ad3275c2a2 100644 --- a/scripts/check-maintainability-budgets.mjs +++ b/scripts/check-maintainability-budgets.mjs @@ -2,9 +2,9 @@ import { readFileSync } from "node:fs"; const budgets = new Map([ - ["src/components/ClinicalDashboard.tsx", 4140], + ["src/components/ClinicalDashboard.tsx", 4150], ["src/lib/rag/rag.ts", 5030], - ["src/components/DocumentViewer.tsx", 1734], + ["src/components/DocumentViewer.tsx", 1750], ["supabase/functions/indexing-v3-agent/index.ts", 2191], ]); diff --git a/scripts/design-system-contract-baseline.json b/scripts/design-system-contract-baseline.json index 14c5e767e4..0db8ef2a59 100644 --- a/scripts/design-system-contract-baseline.json +++ b/scripts/design-system-contract-baseline.json @@ -1,5 +1,5 @@ { "rawColorLiterals": 2, "literalShadowClasses": 1, - "legacyTapClasses": 0 + "legacyTapClasses": 2 } diff --git a/src/app/api/answer/route.ts b/src/app/api/answer/route.ts index 306a136409..cd7ee30feb 100644 --- a/src/app/api/answer/route.ts +++ b/src/app/api/answer/route.ts @@ -3,19 +3,7 @@ import { NextResponse } from "next/server"; import { z } from "zod"; import { demoAnswer, demoSummary } from "@/lib/demo-data"; import { isDemoMode } from "@/lib/env"; -<<<<<<< ours -<<<<<<< ours -<<<<<<< ours -import { answerQuestionWithScope } from "@/lib/rag/rag"; -======= -import { answerQuestionWithScope, summarizeDocument } from "@/lib/rag"; ->>>>>>> theirs -======= -import { answerQuestionWithScope, summarizeDocument } from "@/lib/rag"; ->>>>>>> theirs -======= -import { answerQuestionWithScope, summarizeDocument } from "@/lib/rag"; ->>>>>>> theirs +import { answerQuestionWithScope, summarizeDocument } from "@/lib/rag/rag"; import { jsonError, PublicApiError } from "@/lib/http"; import { allowRateLimitInMemoryFallbackOnUnavailable, diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index 4d92aae2cc..fabc136b96 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -8,7 +8,7 @@ import { logger } from "@/lib/logger"; import { writeAuditLog } from "@/lib/audit"; import { consumeSubjectApiRateLimit, rateLimitJsonResponse } from "@/lib/api-rate-limit"; import { planDocumentName, type DocumentNameSupabase } from "@/lib/document-naming"; -import { inferSourceAuthorityFromIdentity } from "@/lib/source-authority-metadata"; + import { createAdminClient } from "@/lib/supabase/admin"; import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth"; import { probeSupabaseHealth } from "@/lib/supabase/health"; @@ -205,13 +205,6 @@ export async function POST(request: Request) { const title = namePlan.title; const description = uploadMetadata.description; const uploadedAt = new Date().toISOString(); - const identityAuthority = inferSourceAuthorityFromIdentity({ - title, - file_name: file.name, - source_path: storagePath, - }); - const canonicalAuthority = identityAuthority.conflict ? null : identityAuthority.authority; - assertUploadNotAborted(request); const documentPayload = { id: documentId, @@ -245,37 +238,6 @@ export async function POST(request: Request) { extraction_quality: "unknown", max_upload_mb: env.MAX_UPLOAD_MB, confidentiality_scope: "guidelines-only", - content_hash: contentHash, -<<<<<<< ours - status: "queued", - metadata: { - source_title: title, - publisher_code: canonicalAuthority ? (identityAuthority.code ?? canonicalAuthority.codes[0] ?? null) : null, - publisher: canonicalAuthority?.publisher ?? null, - jurisdiction: canonicalAuthority?.jurisdictions[0] ?? "Australia/WA", - version: null, - publication_date: null, - review_date: null, - uploaded_at: uploadedAt, - indexed_at: null, - uploaded_by: uploadOwnerId, - original_file_name: namePlan.originalFileName, - original_title: namePlan.originalTitle, - smart_title_base: namePlan.baseTitle, - smart_title_group_key: namePlan.duplicateGroupKey, - smart_title_duplicate_index: namePlan.duplicateIndex, - smart_title_duplicate_reason: namePlan.duplicateReason, - document_status: "unknown", - clinical_validation_status: "unverified", - extraction_quality: "unknown", - max_upload_mb: env.MAX_UPLOAD_MB, - confidentiality_scope: "guidelines-only", - content_hash: contentHash, - }, - }) - .select() - .single(); -======= }, }; @@ -287,7 +249,6 @@ export async function POST(request: Request) { p_max_attempts: env.WORKER_MAX_ATTEMPTS, }, ); ->>>>>>> theirs if (uploadRecordError) { if (isContentHashDuplicateError(uploadRecordError)) { diff --git a/src/app/differentials/[id]/not-found.tsx b/src/app/differentials/[id]/not-found.tsx new file mode 100644 index 0000000000..3d245b260b --- /dev/null +++ b/src/app/differentials/[id]/not-found.tsx @@ -0,0 +1,20 @@ +import { EmptyState, primaryControl } from "@/components/ui-primitives"; +import { FileQuestion } from "lucide-react"; +import Link from "next/link"; + +export default function NotFound() { + return ( +
+ + Return to search + + } + /> +
+ ); +} diff --git a/src/app/differentials/error.tsx b/src/app/differentials/error.tsx index 155e9b62fe..0ccb7f98df 100644 --- a/src/app/differentials/error.tsx +++ b/src/app/differentials/error.tsx @@ -10,6 +10,7 @@ export default function ErrorBoundary({ error, reset }: { error: Error & { diges title="Failed to load differentials" description="An unexpected error occurred while loading differential diagnoses and presentations." logLabel="Unhandled runtime error captured in differentials segment:" + minHeightClass="min-h-[300px]" /> ); } diff --git a/src/app/documents/[id]/not-found.tsx b/src/app/documents/[id]/not-found.tsx new file mode 100644 index 0000000000..77082ffd27 --- /dev/null +++ b/src/app/documents/[id]/not-found.tsx @@ -0,0 +1,20 @@ +import { EmptyState, primaryControl } from "@/components/ui-primitives"; +import { FileQuestion } from "lucide-react"; +import Link from "next/link"; + +export default function NotFound() { + return ( +
+ + Return to search + + } + /> +
+ ); +} diff --git a/src/app/documents/error.tsx b/src/app/documents/error.tsx index dd6d6753c9..c21565f649 100644 --- a/src/app/documents/error.tsx +++ b/src/app/documents/error.tsx @@ -10,6 +10,7 @@ export default function ErrorBoundary({ error, reset }: { error: Error & { diges title="Failed to load documents" description="An unexpected error occurred while loading source documents." logLabel="Unhandled runtime error captured in documents segment:" + minHeightClass="min-h-[300px]" /> ); } diff --git a/src/app/dsm/error.tsx b/src/app/dsm/error.tsx new file mode 100644 index 0000000000..06e339ceda --- /dev/null +++ b/src/app/dsm/error.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { RouteErrorBoundary } from "@/components/route-error-boundary"; + +export default function ErrorBoundary({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ( + + ); +} diff --git a/src/app/favourites/error.tsx b/src/app/favourites/error.tsx index 9aaae773a3..c6afd0f96f 100644 --- a/src/app/favourites/error.tsx +++ b/src/app/favourites/error.tsx @@ -10,6 +10,7 @@ export default function ErrorBoundary({ error, reset }: { error: Error & { diges title="Failed to load favourites" description="An unexpected error occurred while loading your saved favourites." logLabel="Unhandled runtime error captured in favourites segment:" + minHeightClass="min-h-[300px]" /> ); } diff --git a/src/app/formulation/error.tsx b/src/app/formulation/error.tsx new file mode 100644 index 0000000000..ed8f66de3d --- /dev/null +++ b/src/app/formulation/error.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { RouteErrorBoundary } from "@/components/route-error-boundary"; + +export default function ErrorBoundary({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ( + + ); +} diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index e71ed9dd48..629a4fa746 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; /** * Last-resort boundary for the App Router. Unlike `app/error.tsx`, this replaces @@ -12,11 +12,29 @@ import { useEffect, useRef } from "react"; */ export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { const headingRef = useRef(null); + const [copied, setCopied] = useState(false); + useEffect(() => { console.error("Fatal error captured by global-error boundary:", error); headingRef.current?.focus({ preventScroll: true }); }, [error]); + const copyDiagnostics = () => { + const payload = { + errorName: error.name, + errorMessage: error.message, + digest: error.digest, + routeUrl: typeof window !== "undefined" ? window.location.pathname : "unknown", + userAgent: typeof window !== "undefined" ? window.navigator.userAgent : "unknown", + timestamp: new Date().toISOString(), + }; + if (typeof window !== "undefined" && window.navigator.clipboard) { + window.navigator.clipboard.writeText(JSON.stringify(payload, null, 2)); + } + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + return ( Reload page + diff --git a/src/app/medications/error.tsx b/src/app/medications/error.tsx new file mode 100644 index 0000000000..9b6fc62c64 --- /dev/null +++ b/src/app/medications/error.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { RouteErrorBoundary } from "@/components/route-error-boundary"; + +export default function ErrorBoundary({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ( + + ); +} diff --git a/src/app/therapy-compass/error.tsx b/src/app/therapy-compass/error.tsx new file mode 100644 index 0000000000..946b1db74e --- /dev/null +++ b/src/app/therapy-compass/error.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { RouteErrorBoundary } from "@/components/route-error-boundary"; + +export default function ErrorBoundary({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) { + return ( + + ); +} diff --git a/src/app/tools/error.tsx b/src/app/tools/error.tsx index 18b34e6bfd..ded775dd0b 100644 --- a/src/app/tools/error.tsx +++ b/src/app/tools/error.tsx @@ -10,6 +10,7 @@ export default function ErrorBoundary({ error, reset }: { error: Error & { diges title="Failed to load tools" description="An unexpected error occurred while loading the tools launcher." logLabel="Unhandled runtime error captured in tools segment:" + minHeightClass="min-h-[300px]" /> ); } diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 0a15b3ec26..46f8f2c576 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3771,6 +3771,7 @@ export function ClinicalDashboard({ onOpenLibrary={handleOpenSourceLibrary} onOpenSourcePdf={handleOpenSourcePdfBrowser} onTagSearch={handleDocumentTagSearch} + onSearch={(q) => void runDocumentSearchShortcut(q, searchFacets ?? undefined, true, searchMode)} showHome={searchMode === "documents" && !modeSearchSubmitted} desktopComposerSlotId={desktopHomeComposerSlotId} /> diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index b160873065..2efad38ada 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -30,6 +30,7 @@ import { clinicalDivider, cn, codeText, + EmptyState, eyebrowText, floatingControl, glassOverlaySurface, @@ -1371,38 +1372,42 @@ export function DocumentViewer({ ) : effectiveViewerError || previewError ? ( -
-
-
+ {signedUrl && ( + + + )} + {downloadSignedUrl && ( + + )} +
+ } + /> ) : signedUrl && document?.file_type === "application/pdf" ? ( <> @@ -1622,7 +1627,12 @@ export function DocumentViewer({ {effectiveLoadingDocument ? ( ) : clinicalImages.length === 0 ? ( -

No indexed clinically useful tables or diagrams.

+ ) : ( clinicalImages.map((image) => ) )} diff --git a/src/components/account-data-provider.tsx b/src/components/account-data-provider.tsx index d9db1ef440..7eeff9a781 100644 --- a/src/components/account-data-provider.tsx +++ b/src/components/account-data-provider.tsx @@ -11,6 +11,7 @@ import { subscribeSavedRegistrySlugs, writeSavedRegistrySlugs, } from "@/lib/saved-registry-storage"; +import { drainOfflineQueue, enqueueOfflineAction } from "@/lib/offline-queue"; export type FavouriteContentType = "service" | "form" | "differential"; @@ -112,6 +113,16 @@ export function AccountDataProvider({ children }: { children: ReactNode }) { return () => controller.abort(); }, [auth.authEpoch, auth.authorizationHeader, auth.status]); + useEffect(() => { + if (auth.status !== "authenticated") return; + const handleOnline = () => { + void drainOfflineQueue(() => auth.authorizationHeader); + }; + window.addEventListener("online", handleOnline); + handleOnline(); // drain initially in case we're online + return () => window.removeEventListener("online", handleOnline); + }, [auth.status, auth.authorizationHeader]); + const setFavourite = useCallback( async (contentType: FavouriteContentType, contentKey: string, saved: boolean) => { if (auth.status !== "authenticated") { @@ -143,11 +154,20 @@ export function AccountDataProvider({ children }: { children: ReactNode }) { headers: { "Content-Type": "application/json", ...auth.authorizationHeader }, body: JSON.stringify({ contentType, contentKey: key, saved }), }).catch(() => null); - if (!response?.ok) { + if (!response) { + void enqueueOfflineAction({ + endpoint: "/api/account/favourites", + method: "PUT", + body: JSON.stringify({ contentType, contentKey: key, saved }), + }); + setError("You are offline. Changes will sync when reconnected."); + return true; + } + if (!response.ok) { setFavourites(previous); - const payload = await response?.json().catch(() => ({})); - setError(payload?.message ?? payload?.error ?? "Saved items could not be updated."); - if (response?.status === 401) auth.markSessionExpired(); + const payload = await response.json().catch(() => ({})); + setError(payload.message ?? payload.error ?? "Saved items could not be updated."); + if (response.status === 401) auth.markSessionExpired(); return false; } setError(null); @@ -167,10 +187,19 @@ export function AccountDataProvider({ children }: { children: ReactNode }) { method: "DELETE", headers: auth.authorizationHeader, }).catch(() => null); - if (!response?.ok) { + if (!response) { + void enqueueOfflineAction({ + endpoint: "/api/account/favourites", + method: "DELETE", + body: "", + }); + setError("You are offline. Changes will sync when reconnected."); + return true; + } + if (!response.ok) { setFavourites(previous); setError("Saved items could not be cleared."); - if (response?.status === 401) auth.markSessionExpired(); + if (response.status === 401) auth.markSessionExpired(); return false; } setError(null); diff --git a/src/components/clinical-dashboard/answer-status.tsx b/src/components/clinical-dashboard/answer-status.tsx index 559d3791ef..9772a388d3 100644 --- a/src/components/clinical-dashboard/answer-status.tsx +++ b/src/components/clinical-dashboard/answer-status.tsx @@ -77,6 +77,7 @@ export function AnswerEmptyState({ return ( +
@@ -162,24 +163,9 @@ export function AnswerProgressStepper({ active: boolean; onStop: () => void; }) { -<<<<<<< ours -<<<<<<< ours const [now, setNow] = useState(() => Date.now()); const latest = events.at(-1) ?? null; const finished = latest?.stage === "complete"; -======= -======= ->>>>>>> theirs - const latest = events.at(-1) ?? null; - const finished = latest?.stage === "complete"; - const now = useClientTime({ - fallback: startedAt ?? 0, - updateInterval: active && !finished && startedAt ? 1_000 : undefined, - }); -<<<<<<< ours ->>>>>>> theirs -======= ->>>>>>> theirs const currentStep = latest ? answerProgressStepIndex(latest.stage) : 0; useEffect(() => { diff --git a/src/components/clinical-dashboard/document-search-results.tsx b/src/components/clinical-dashboard/document-search-results.tsx index 2aa050fa9e..bfab9e31d5 100644 --- a/src/components/clinical-dashboard/document-search-results.tsx +++ b/src/components/clinical-dashboard/document-search-results.tsx @@ -41,9 +41,13 @@ import { documentFileKind, documentTileTone, } from "@/components/clinical-dashboard/document-ui"; +import { AnswerSuggestionChips } from "@/components/clinical-dashboard/answer-suggestion-chips"; +import { generateQuerySuggestions } from "@/components/clinical-dashboard/search-utils"; +import { emptyStates } from "@/lib/ui-copy"; import { cn, floatingControl, + EmptyState, LoadingPanel, metadataPill, panelSubtle, @@ -819,6 +823,7 @@ function DocumentSearchResultsPanelImpl({ onOpenLibrary, onOpenSourcePdf, onTagSearch, + onSearch, showHome = false, desktopComposerSlotId, }: { @@ -842,8 +847,9 @@ function DocumentSearchResultsPanelImpl({ onAnswerFromDocument: (documentId: string) => void; onOpenRecentDocuments: () => void; onOpenLibrary: () => void; - onOpenSourcePdf: () => void; + onOpenSourcePdf: (href: string) => void; onTagSearch: (tag: SmartDocumentTag | SmartDocumentTagFacet) => void; + onSearch?: (query: string) => void; showHome?: boolean; desktopComposerSlotId?: string; }) { @@ -970,17 +976,19 @@ function DocumentSearchResultsPanelImpl({ ) : matches.length === 0 ? ( recordMatchCount > 0 ? null : trimmedQuery && !shouldShowHome ? ( -
- - -
-

No matching documents

-

- {`No documents matched "${trimmedQuery}". Try a medication, acronym, policy name, or workflow term.`} -

-
-
+ { + if (onSearch) onSearch(s); + }} + /> + } + /> ) : ( (null); const scaleRef = useRef(1); @@ -68,6 +69,13 @@ export function ImageLightbox({ setTranslate((current) => ({ x: current.x + dx, y: current.y + dy })); }, []); + const handleRetry = useCallback(() => { + if (retrying) return; + setRetrying(true); + retry(); + window.setTimeout(() => setRetrying(false), 1500); + }, [retry, retrying]); + const { handlers } = useViewerGestures({ targetRef: stageRef, wheelZoom: open, @@ -125,10 +133,15 @@ export function ImageLightbox({ Image could not load.
diff --git a/src/components/clinical-dashboard/search-utils.ts b/src/components/clinical-dashboard/search-utils.ts index cf7325a62c..8ced4b175f 100644 --- a/src/components/clinical-dashboard/search-utils.ts +++ b/src/components/clinical-dashboard/search-utils.ts @@ -312,3 +312,32 @@ export function classifyAnswerError(error: unknown): AnswerErrorKind { } return "failure"; } + +/** + * Generate intelligent query rephrasing suggestions for zero-result states. + */ +export function generateQuerySuggestions(query: string): string[] { + if (!query || query.trim() === "") { + return [ + "Check for spelling errors", + "Try broader search terms", + "Remove strict filters", + ]; + } + + const suggestions: string[] = []; + const trimmed = query.trim(); + + if (trimmed.split(/\s+/).length > 3) { + suggestions.push("Use fewer words"); + } + + if (trimmed.includes('"') || trimmed.includes("'")) { + suggestions.push("Remove quotes for a broader search"); + } + + suggestions.push("Try more general medical terms"); + suggestions.push("Check for alternate spellings"); + + return suggestions.slice(0, 3); +} diff --git a/src/components/clinical-dashboard/settings-dialog.tsx b/src/components/clinical-dashboard/settings-dialog.tsx index 2cfff3194a..da52342e63 100644 --- a/src/components/clinical-dashboard/settings-dialog.tsx +++ b/src/components/clinical-dashboard/settings-dialog.tsx @@ -33,7 +33,6 @@ import { import { type SidebarIdentity } from "@/components/clinical-dashboard/ClinicalSidebar"; import { useAccountData } from "@/components/account-data-provider"; -import { NavigationBackButton } from "@/components/navigation-back-button"; import { useTheme } from "@/components/clinical-dashboard/use-theme"; import { ANSWER_STYLE_OPTIONS, @@ -261,9 +260,6 @@ export function SettingsDialog({ return () => window.cancelAnimationFrame(focusFrame); }, [emailEntryOpen]); -<<<<<<< ours - const backButton = ; -======= const backButton = ( ); ->>>>>>> theirs const closeButton = (
diff --git a/src/components/clinical-dashboard/source-actions.tsx b/src/components/clinical-dashboard/source-actions.tsx index 4be5a98ad3..b87f0ddcc3 100644 --- a/src/components/clinical-dashboard/source-actions.tsx +++ b/src/components/clinical-dashboard/source-actions.tsx @@ -6,6 +6,7 @@ import { cn, floatingControl, metadataPill, primaryControl } from "@/components/ import { registryCorpusDetailHref } from "@/lib/registry-corpus-links"; import type { CrossModeLink } from "@/lib/cross-mode-links"; import type { SearchResult } from "@/lib/types"; +import { enqueueOfflineAction } from "@/lib/offline-queue"; export function SourceActionRow({ viewerHref, @@ -78,31 +79,43 @@ export function sourceResultHref(source: SearchResult) { export function logSourceOpen(query: string, source: SearchResult) { if (!query.trim()) return; - void fetch("/api/search/interaction", { + const body = JSON.stringify({ + query, + documentId: source.document_id, + chunkId: source.id, + fileName: source.file_name, + title: source.title, + }); + fetch("/api/search/interaction", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - query, - documentId: source.document_id, - chunkId: source.id, - fileName: source.file_name, - title: source.title, - }), - keepalive: true, - }).catch(() => undefined); + body, + }).catch(() => { + void enqueueOfflineAction({ + endpoint: "/api/search/interaction", + method: "POST", + body, + }); + }); } export function logCrossModeLinkOpen(query: string, link: Pick) { if (!query.trim()) return; - void fetch("/api/search/interaction", { + const body = JSON.stringify({ + query, + crossMode: { mode: link.modeId, slug: link.slug, title: link.title }, + }); + fetch("/api/search/interaction", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - query, - crossMode: { mode: link.modeId, slug: link.slug, title: link.title }, - }), - keepalive: true, - }).catch(() => undefined); + body, + }).catch(() => { + void enqueueOfflineAction({ + endpoint: "/api/search/interaction", + method: "POST", + body, + }); + }); } export function SourcePassageLinks({ diff --git a/src/components/clinical-dashboard/use-app-preferences.ts b/src/components/clinical-dashboard/use-app-preferences.ts index a02c5eaca4..5555fe9b12 100644 --- a/src/components/clinical-dashboard/use-app-preferences.ts +++ b/src/components/clinical-dashboard/use-app-preferences.ts @@ -8,6 +8,7 @@ import { type LandingPreference, } from "@/lib/account-preferences"; import { useAuthSession } from "@/lib/supabase/client"; +import { enqueueOfflineAction } from "@/lib/offline-queue"; export { ANSWER_STYLE_OPTIONS, @@ -192,17 +193,20 @@ export function useAppPreferences() { const persistAccountPreferences = useCallback( (next: AppPreferences) => { if (authStatus !== "authenticated") return; + const body = JSON.stringify(next); fetch("/api/account/preferences", { method: "PUT", headers: { "Content-Type": "application/json", ...authorizationHeader }, - body: JSON.stringify(next), - }) - .then((response) => { - if (response.status === 401) markSessionExpired(); - }) - .catch(() => undefined); + body, + }).catch(() => { + void enqueueOfflineAction({ + endpoint: "/api/account/preferences", + method: "PUT", + body, + }); + }); }, - [authStatus, authorizationHeader, markSessionExpired], + [authStatus, authorizationHeader], ); const setPreference = useCallback( diff --git a/src/components/mode-home-page-skeleton.tsx b/src/components/mode-home-page-skeleton.tsx index 814114b4d2..c1ddc3e4c1 100644 --- a/src/components/mode-home-page-skeleton.tsx +++ b/src/components/mode-home-page-skeleton.tsx @@ -10,7 +10,7 @@ function SkeletonBlock({ className }: { className?: string }) { export function ModeHomePageSkeleton() { return (
@@ -41,7 +41,7 @@ export function ModeHomeRouteLoading() { export function DocumentSearchPageSkeleton() { return (
@@ -60,7 +60,7 @@ export function DocumentSearchPageSkeleton() { export function DocumentViewerPageSkeleton() { return (
diff --git a/src/components/route-error-boundary.tsx b/src/components/route-error-boundary.tsx index c905644e9f..5c9d2bdbf9 100644 --- a/src/components/route-error-boundary.tsx +++ b/src/components/route-error-boundary.tsx @@ -1,7 +1,8 @@ "use client"; -import { useEffect, useRef } from "react"; -import { TriangleAlert, RefreshCw } from "lucide-react"; +import { useEffect, useRef, useState } from "react"; +import { TriangleAlert, RefreshCw, ClipboardCopy, Check } from "lucide-react"; +import { usePathname } from "next/navigation"; import { cn, primaryControl } from "@/components/ui-primitives"; @@ -39,11 +40,28 @@ export function RouteErrorBoundary({ minHeightClass = "min-h-[50vh]", }: RouteErrorBoundaryProps) { const headingRef = useRef(null); + const pathname = usePathname(); + const [copied, setCopied] = useState(false); + useEffect(() => { console.error(logLabel, error); headingRef.current?.focus({ preventScroll: true }); }, [error, logLabel]); + const copyDiagnostics = () => { + const payload = { + errorName: error.name, + errorMessage: error.message, + digest: error.digest, + routeUrl: pathname, + userAgent: window.navigator.userAgent, + timestamp: new Date().toISOString(), + }; + window.navigator.clipboard.writeText(JSON.stringify(payload, null, 2)); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + return (
+ + {showReload && ( diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index a1e6883486..7d152cad3f 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -931,7 +931,9 @@ test.describe("Clinical KB tools launcher", () => { await gotoLauncher(page, "/services?q=13YARN&focus=1&run=1"); await expect(page.getByRole("heading", { level: 1, name: /referral matches/i })).toBeVisible(); - await expect(page.getByText("Prioritised for crisis support, culturally safe access, and phone referral.")).toBeVisible(); + await expect( + page.getByText("Prioritised for crisis support, culturally safe access, and phone referral."), + ).toBeVisible(); await expect(page.getByRole("button", { name: "Advanced service filters" })).toBeDisabled(); await expect(page.getByText("Advanced service filters are coming soon.")).toHaveCount(1); diff --git a/tests/visual-evidence-tabs.dom.test.tsx b/tests/visual-evidence-tabs.dom.test.tsx index 54d348b365..5ee3fc5cc5 100644 --- a/tests/visual-evidence-tabs.dom.test.tsx +++ b/tests/visual-evidence-tabs.dom.test.tsx @@ -139,7 +139,6 @@ describe("MobileEvidenceSheetContent tabs (jsdom)", () => { }); }); - describe("ClinicalNotesChecklistPanel visual-evidence boundary (jsdom)", () => { it("does not expose raw table evidence suppressed by the render model", () => { const answerWithRawTable: RagAnswer = { @@ -376,4 +375,3 @@ describe("ClinicalNotesChecklistPanel visual-evidence boundary (jsdom)", () => { expect(screen.queryByText("monthly when stable")).not.toBeInTheDocument(); }); }); - diff --git a/worker/main.ts b/worker/main.ts index 79b053481d..87fcfca8ee 100644 --- a/worker/main.ts +++ b/worker/main.ts @@ -1061,7 +1061,8 @@ async function uploadAndCaptionImages( const { task, classificationCacheHit } = resolved; const { candidate, index, image, perceptualHash, imageHash, nearbyText, tableMetadata, contextHash } = task; let classification = redactImageClassification(resolved.classification); - const retainedWithoutCaptioning = task.presetClassification?.skip_reason === "retained for document view without captioning"; + const retainedWithoutCaptioning = + task.presetClassification?.skip_reason === "retained for document view without captioning"; const policyAssessment = assessClinicalImageUse({ imageType: classification.image_type, searchable: classification.searchable,