From 49e39c3a799c58a6883fa786dc797177c25b7b40 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 3 Jul 2026 04:11:34 +0800 Subject: [PATCH 1/3] fix(review): address PR #209 third-pass Codex findings (4x P2) - registry detail hook + [slug] API now carry governance; the loader reconciles the verified badge from the authoritative validation_status so a reviewed/downgraded record no longer shows the stale fixture verification state. - DocumentSearchResultsPanel takes recordStatus and renders a loading/ sign-in/error notice for Services/Forms record matches, so the main dashboard no longer silently shows zero cards when the registry fails. - seed-registry-records preserves existing governance columns (source_status/validation_status/last_reviewed_at/review_due_at) on reseed, so a fixture-copy reseed can't downgrade reviewed rows. - the 'apply default mode scope to shortcut searches' finding is moot: the mode-default scope was removed entirely in the prior commit, so executeSearch and shortcut searches are consistently unscoped. Co-Authored-By: Claude Fable 5 --- scripts/seed-registry-records.ts | 33 +++++++++++++- src/app/api/registry/records/[slug]/route.ts | 9 +++- src/components/ClinicalDashboard.tsx | 1 + .../document-search-results.tsx | 43 ++++++++++++++++++- src/components/registry-record-loader.tsx | 18 +++++++- src/lib/use-registry-records.ts | 30 ++++++++++--- 6 files changed, 122 insertions(+), 12 deletions(-) diff --git a/scripts/seed-registry-records.ts b/scripts/seed-registry-records.ts index 6375ae04d9..8b0ee4abfb 100644 --- a/scripts/seed-registry-records.ts +++ b/scripts/seed-registry-records.ts @@ -97,7 +97,38 @@ async function main() { } const supabase = await loadAdminClient(); - const { error } = await supabase.from("clinical_registry_records").upsert(rows, { onConflict: "owner_id,kind,slug" }); + + // Preserve governance that was reviewed after seeding: a reseed for fixture + // copy changes must not downgrade source_status / validation_status / + // last_reviewed_at / review_due_at back to the fixture-derived defaults. + const { data: existing, error: existingError } = await supabase + .from("clinical_registry_records") + .select("kind, slug, source_status, validation_status, last_reviewed_at, review_due_at") + .eq("owner_id", args.ownerId); + if (existingError) { + throw new Error(`Could not read existing governance: ${existingError.message}`); + } + const governanceByKey = new Map((existing ?? []).map((row) => [`${row.kind}:${row.slug}`, row] as const)); + let preserved = 0; + const upsertRows = rows.map((row) => { + const prior = governanceByKey.get(`${row.kind}:${row.slug}`); + if (!prior) return row; + preserved += 1; + return { + ...row, + source_status: prior.source_status, + validation_status: prior.validation_status, + last_reviewed_at: prior.last_reviewed_at, + review_due_at: prior.review_due_at, + }; + }); + if (preserved > 0) { + console.log(`[registry:seed] Preserving reviewed governance on ${preserved} existing record(s).`); + } + + const { error } = await supabase + .from("clinical_registry_records") + .upsert(upsertRows, { onConflict: "owner_id,kind,slug" }); if (error) { throw new Error(`Upsert failed: ${error.message}`); } diff --git a/src/app/api/registry/records/[slug]/route.ts b/src/app/api/registry/records/[slug]/route.ts index 18ef828948..a63d82ff0b 100644 --- a/src/app/api/registry/records/[slug]/route.ts +++ b/src/app/api/registry/records/[slug]/route.ts @@ -6,6 +6,7 @@ import { isDemoMode, isLocalNoAuthMode } from "@/lib/env"; import { jsonError } from "@/lib/http"; import { getFormRecord } from "@/lib/forms"; import { + deriveGovernanceColumns, normalizeRegistrySlug, rowGovernance, rowToServiceRecord, @@ -42,7 +43,13 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s if (isDemoMode()) { const record = kind === "form" ? getFormRecord(normalizedSlug) : getServiceRecord(normalizedSlug); if (!record) return notFoundResponse(normalizedSlug); - return registryResponse({ record, linkedDocuments: [], demoMode: true }); + const derived = deriveGovernanceColumns(record); + return registryResponse({ + record, + governance: { sourceStatus: derived.source_status, validationStatus: derived.validation_status }, + linkedDocuments: [], + demoMode: true, + }); } const supabase = createAdminClient(); diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 183755eff8..626508b726 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -5589,6 +5589,7 @@ export function ClinicalDashboard({ matches={documentMatches} recordMatches={recordSearchMatches} recordMode={recordSearchMode} + recordStatus={registryRecords.status} showRecordMatches={searchMode === "services" || searchMode === "forms"} query={query} loading={loading} diff --git a/src/components/clinical-dashboard/document-search-results.tsx b/src/components/clinical-dashboard/document-search-results.tsx index 1266fd2238..50f7552ec6 100644 --- a/src/components/clinical-dashboard/document-search-results.tsx +++ b/src/components/clinical-dashboard/document-search-results.tsx @@ -14,6 +14,8 @@ import { Filter, FolderOpen, ListChecks, + Loader2, + Shield, ShieldAlert, SlidersHorizontal, Sparkles, @@ -54,6 +56,7 @@ import { import type { ServiceSearchMatch } from "@/lib/services"; import type { FormSearchMatch } from "@/lib/forms"; import type { ClinicalDocument, DocumentMatch, SearchResult } from "@/lib/types"; +import type { RegistryRequestStatus } from "@/lib/use-registry-records"; import { documentRelevancePercent } from "./relevance-score"; type SearchFacet = { value: string; count: number }; @@ -939,10 +942,42 @@ function SearchRecordResults({ ); } +function RecordRegistryNotice({ status, mode }: { status: RegistryRequestStatus; mode: SearchRecordMode }) { + if (status === "ready") return null; + const noun = mode === "forms" ? "forms" : "services"; + const config = + status === "loading" + ? { Icon: Loader2, spin: true, tone: "info" as const, text: `Loading your ${noun} registry...` } + : status === "unauthorized" + ? { Icon: Shield, spin: false, tone: "warning" as const, text: `Sign in to search your ${noun} registry.` } + : { + Icon: ShieldAlert, + spin: false, + tone: "danger" as const, + text: `Couldn't load the ${noun} registry. Try again shortly.`, + }; + const toneClass = + config.tone === "danger" + ? "border-[color:var(--danger-border)] bg-[color:var(--danger-soft)]/50 text-[color:var(--danger)]" + : config.tone === "warning" + ? "border-[color:var(--warning-border)] bg-[color:var(--warning-soft)]/50 text-[color:var(--warning)]" + : "border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)]"; + return ( +

+ + {config.text} +

+ ); +} + export function DocumentSearchResultsPanel({ matches, recordMatches = [], recordMode = "services", + recordStatus = "ready", showRecordMatches = false, query, loading, @@ -965,6 +1000,7 @@ export function DocumentSearchResultsPanel({ matches: DocumentMatch[]; recordMatches?: SearchRecordMatch[]; recordMode?: SearchRecordMode; + recordStatus?: RegistryRequestStatus; showRecordMatches?: boolean; query: string; loading: boolean; @@ -1066,7 +1102,12 @@ export function DocumentSearchResultsPanel({ ) : null} - {showRecordMatches ? : null} + {showRecordMatches ? ( + <> + + + + ) : null} {loading ? ( diff --git a/src/components/registry-record-loader.tsx b/src/components/registry-record-loader.tsx index eb274eacd0..108e202976 100644 --- a/src/components/registry-record-loader.tsx +++ b/src/components/registry-record-loader.tsx @@ -57,7 +57,7 @@ export function RegistryRecordLoader({ slug: string; children: (record: ServiceRecord) => ReactNode; }) { - const { status, record } = useRegistryRecord(kind, slug); + const { status, record, governance } = useRegistryRecord(kind, slug); const copy = kindCopy[kind]; if (status === "loading") { @@ -108,5 +108,19 @@ export function RegistryRecordLoader({ ); } - return <>{children(record)}; + // Reconcile the verified badge with the authoritative governance column so a + // record reviewed/downgraded after seeding does not keep showing the stale + // fixture verification state. + const rendered = governance + ? { + ...record, + verification: { + ...record.verification, + locallyVerified: + governance.validationStatus === "locally_reviewed" || governance.validationStatus === "approved", + }, + } + : record; + + return <>{children(rendered)}; } diff --git a/src/lib/use-registry-records.ts b/src/lib/use-registry-records.ts index 3d050f8fbd..0cf8a09da9 100644 --- a/src/lib/use-registry-records.ts +++ b/src/lib/use-registry-records.ts @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; -import type { RegistryRecordKind, RegistryValidationStatus } from "@/lib/registry-records"; +import type { RegistryRecordKind, RegistrySourceStatus, RegistryValidationStatus } from "@/lib/registry-records"; import type { ServiceRecord } from "@/lib/services"; import { useAuthSession } from "@/lib/supabase/client"; @@ -18,14 +18,28 @@ export type RegistryRecordsState = { governance: Record; }; +export type RegistryRecordGovernance = { + sourceStatus: RegistrySourceStatus; + validationStatus: RegistryValidationStatus; +}; + export type RegistryRecordState = { status: RegistryRequestStatus; record: ServiceRecord | null; linkedDocuments: Array<{ id: string; title: string; file_name: string; status: string }>; demoMode: boolean; + /** Authoritative governance for the record from the API (null until ready), + * so detail pages can render current badges rather than the fixture copy. */ + governance: RegistryRecordGovernance | null; }; -const recordLoading: RegistryRecordState = { status: "loading", record: null, linkedDocuments: [], demoMode: false }; +const recordLoading: RegistryRecordState = { + status: "loading", + record: null, + linkedDocuments: [], + demoMode: false, + governance: null, +}; type RegistryRecordsKeyedState = RegistryRecordsState & { kind: RegistryRecordKind }; function recordsState( @@ -127,24 +141,25 @@ export function useRegistryRecord(kind: RegistryRecordKind, slug: string): Regis if (response.status === 401) { if (authStatus === "loading") return; if (authStatus === "authenticated") markSessionExpired(); - setState({ status: "unauthorized", record: null, linkedDocuments: [], demoMode: false }); + setState({ status: "unauthorized", record: null, linkedDocuments: [], demoMode: false, governance: null }); return; } if (response.status === 404) { - setState({ status: "not_found", record: null, linkedDocuments: [], demoMode: false }); + setState({ status: "not_found", record: null, linkedDocuments: [], demoMode: false, governance: null }); return; } if (!response.ok) { - setState({ status: "error", record: null, linkedDocuments: [], demoMode: false }); + setState({ status: "error", record: null, linkedDocuments: [], demoMode: false, governance: null }); return; } const payload = (await response.json()) as { record?: ServiceRecord; linkedDocuments?: Array<{ id: string; title: string; file_name: string; status: string }>; demoMode?: boolean; + governance?: RegistryRecordGovernance; }; if (!payload.record) { - setState({ status: "not_found", record: null, linkedDocuments: [], demoMode: false }); + setState({ status: "not_found", record: null, linkedDocuments: [], demoMode: false, governance: null }); return; } setState({ @@ -152,10 +167,11 @@ export function useRegistryRecord(kind: RegistryRecordKind, slug: string): Regis record: payload.record, linkedDocuments: payload.linkedDocuments ?? [], demoMode: Boolean(payload.demoMode), + governance: payload.governance ?? null, }); }) .catch(() => { - if (active) setState({ status: "error", record: null, linkedDocuments: [], demoMode: false }); + if (active) setState({ status: "error", record: null, linkedDocuments: [], demoMode: false, governance: null }); }); return () => { active = false; From eb37b76fad41050e7e71e5f10d131aa0ccffddfe Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 3 Jul 2026 04:30:35 +0800 Subject: [PATCH 2/3] fix(review): address late PR 209 registry states --- src/app/api/registry/records/route.ts | 20 ------- src/components/forms/forms-home-page.tsx | 56 +++++++++++++++++-- .../forms/forms-search-results-page.tsx | 27 +++++++-- src/components/mode-home-template.tsx | 40 ++++++++++++- .../services/services-home-page.tsx | 44 +++++++++++++-- 5 files changed, 149 insertions(+), 38 deletions(-) diff --git a/src/app/api/registry/records/route.ts b/src/app/api/registry/records/route.ts index 70f7cc79ef..6e33251361 100644 --- a/src/app/api/registry/records/route.ts +++ b/src/app/api/registry/records/route.ts @@ -96,31 +96,11 @@ export async function GET(request: Request) { const records = rows.map(rowToServiceRecord); const governanceBySlug = Object.fromEntries(rows.map((row) => [row.slug, rowGovernance(row)])); - const recordIds = rows.map((row) => row.id); - const linkedDocumentIdsByRecordId: Record = {}; - if (recordIds.length > 0) { - const { data: links, error: linksError } = await supabase - .from("clinical_registry_record_sources") - .select("record_id, document_id") - .eq("owner_id", user.id) - .in("record_id", recordIds); - if (linksError) throw new Error(linksError.message); - for (const link of links ?? []) { - const existing = linkedDocumentIdsByRecordId[link.record_id] ?? []; - existing.push(link.document_id); - linkedDocumentIdsByRecordId[link.record_id] = existing; - } - } - const linkedDocumentIdsBySlug = Object.fromEntries( - rows.map((row) => [row.slug, linkedDocumentIdsByRecordId[row.id] ?? []]), - ); - return registryResponse({ records, matches: q ? matchesPayload(rankRecords(kind, records, q, limit)) : undefined, total: rows.length, governance: governanceBySlug, - linkedDocumentIds: linkedDocumentIdsBySlug, }); } catch (error) { if (error instanceof AuthenticationError) { diff --git a/src/components/forms/forms-home-page.tsx b/src/components/forms/forms-home-page.tsx index 1d0bc5587d..eb3e040de3 100644 --- a/src/components/forms/forms-home-page.tsx +++ b/src/components/forms/forms-home-page.tsx @@ -1,9 +1,22 @@ "use client"; -import { ArrowLeftRight, ClipboardCheck, FileText, Route, Search, ShieldCheck, Truck, UserRound } from "lucide-react"; +import { + ArrowLeftRight, + ClipboardCheck, + FileQuestion, + FileText, + Loader2, + Route, + Search, + ShieldAlert, + ShieldCheck, + Truck, + UserRound, +} from "lucide-react"; import { ModeHomeMain, + ModeHomeStatusNotice, ModeHomeTemplate, ModeHomeVerificationFooter, type ModeHomeAction, @@ -65,6 +78,36 @@ const commonTasks: ModeHomePill[] = [ export function FormsHomePage() { const registry = useRegistryRecords("form"); const verifiedCount = countVerifiedRegistryRecords(registry); + const registryReady = registry.status === "ready"; + const hasRegistryRecords = registryReady && registry.total > 0; + const registryNotice = + registry.status === "loading" ? ( + + ) : registry.status === "unauthorized" ? ( + + ) : registry.status === "error" ? ( + + ) : !hasRegistryRecords ? ( + + ) : null; return ( @@ -75,18 +118,21 @@ export function FormsHomePage() { icon={FileText} desktopComposerSlotId={modeHomeDesktopComposerSlotId} actionsLabel="Forms tasks" - actions={taskCards} + actions={hasRegistryRecords ? taskCards : []} pillsTitle="Common tasks" - pills={commonTasks} + pills={hasRegistryRecords ? commonTasks : []} footer={ - registry.status === "ready" ? ( + hasRegistryRecords ? ( - ) : null + ) : ( + registryNotice + ) } /> diff --git a/src/components/forms/forms-search-results-page.tsx b/src/components/forms/forms-search-results-page.tsx index 641451b0bc..c4d7cf5dfc 100644 --- a/src/components/forms/forms-search-results-page.tsx +++ b/src/components/forms/forms-search-results-page.tsx @@ -841,14 +841,21 @@ function RegistryStatusNotice({ status }: { status: RegistryRequestStatus }) { if (status === "ready") return null; const notice = status === "loading" - ? { icon: Loader2, spin: true, tone: "info", text: "Loading your forms registry..." } + ? { icon: Loader2, spin: true, tone: "info", text: "Loading your forms registry...", action: null } : status === "unauthorized" - ? { icon: Shield, spin: false, tone: "warning", text: "Sign in to search your forms registry." } + ? { + icon: Shield, + spin: false, + tone: "warning", + text: "Sign in to search your forms registry.", + action: { href: "/", label: "Go to sign in" }, + } : { icon: ShieldAlert, spin: false, tone: "danger", text: "Couldn't load the forms registry. Try again shortly.", + action: null, }; const Icon = notice.icon; const toneClass = @@ -858,13 +865,21 @@ function RegistryStatusNotice({ status }: { status: RegistryRequestStatus }) { ? "border-[color:var(--warning-border)] bg-[color:var(--warning-soft)]/50 text-[color:var(--warning)]" : "border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)]"; return ( -

- {notice.text} -

+ {notice.text} + {notice.action ? ( + + {notice.action.label} + + ) : null} + ); } diff --git a/src/components/mode-home-template.tsx b/src/components/mode-home-template.tsx index 42241bf689..25ea533887 100644 --- a/src/components/mode-home-template.tsx +++ b/src/components/mode-home-template.tsx @@ -1,6 +1,6 @@ import Link from "next/link"; import { type ReactNode } from "react"; -import { ArrowRight, ShieldCheck, type LucideIcon } from "lucide-react"; +import { ArrowRight, type LucideIcon } from "lucide-react"; import { cn } from "@/components/ui-primitives"; @@ -111,11 +111,13 @@ export function ModeHomeMain({ } export function ModeHomeVerificationFooter({ + icon: Icon, label, body, verifiedCount, totalCount, }: { + icon: LucideIcon; label: string; body: string; verifiedCount: number; @@ -124,7 +126,7 @@ export function ModeHomeVerificationFooter({ return (

- @@ -136,6 +138,40 @@ export function ModeHomeVerificationFooter({ ); } +export function ModeHomeStatusNotice({ + icon: Icon, + title, + body, + actionHref, + actionLabel, +}: { + icon: LucideIcon; + title: string; + body: string; + actionHref?: string; + actionLabel?: string; +}) { + return ( +

+ + + + {title} + {body} + + {actionHref && actionLabel ? ( + + {actionLabel} + + ) : null} +
+ ); +} + export function ModeHomeTemplate({ testId, title, diff --git a/src/components/services/services-home-page.tsx b/src/components/services/services-home-page.tsx index 9eb5c6e745..657a488175 100644 --- a/src/components/services/services-home-page.tsx +++ b/src/components/services/services-home-page.tsx @@ -1,9 +1,10 @@ "use client"; -import { FileSearch, MapPinned, Route, Users } from "lucide-react"; +import { FileQuestion, FileSearch, Loader2, MapPinned, Route, ShieldAlert, ShieldCheck, Users } from "lucide-react"; import { ModeHomeMain, + ModeHomeStatusNotice, ModeHomeTemplate, ModeHomeVerificationFooter, type ModeHomeAction, @@ -79,6 +80,36 @@ const commonPathways: ModeHomePill[] = [ export function ServicesHomePage() { const registry = useRegistryRecords("service"); const verifiedCount = countVerifiedRegistryRecords(registry); + const registryReady = registry.status === "ready"; + const hasRegistryRecords = registryReady && registry.total > 0; + const registryNotice = + registry.status === "loading" ? ( + + ) : registry.status === "unauthorized" ? ( + + ) : registry.status === "error" ? ( + + ) : !hasRegistryRecords ? ( + + ) : null; return ( @@ -89,18 +120,21 @@ export function ServicesHomePage() { icon={Users} desktopComposerSlotId={modeHomeDesktopComposerSlotId} actionsLabel="Service tasks" - actions={taskCards} + actions={hasRegistryRecords ? taskCards : []} pillsTitle="Common pathways" - pills={commonPathways} + pills={hasRegistryRecords ? commonPathways : []} footer={ - registry.status === "ready" ? ( + hasRegistryRecords ? ( - ) : null + ) : ( + registryNotice + ) } /> From 9f3f873f2f2ddde2313a2f4398c31bb13f728468 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 3 Jul 2026 08:28:46 +0800 Subject: [PATCH 3/3] fix(review): preserve only reviewed registry governance --- scripts/seed-registry-records.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/seed-registry-records.ts b/scripts/seed-registry-records.ts index 8b0ee4abfb..c4468b6f16 100644 --- a/scripts/seed-registry-records.ts +++ b/scripts/seed-registry-records.ts @@ -113,6 +113,11 @@ async function main() { const upsertRows = rows.map((row) => { const prior = governanceByKey.get(`${row.kind}:${row.slug}`); if (!prior) return row; + const hasReviewedGovernance = + Boolean(prior.last_reviewed_at) || + prior.validation_status === "locally_reviewed" || + prior.validation_status === "approved"; + if (!hasReviewedGovernance) return row; preserved += 1; return { ...row,