Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion scripts/seed-registry-records.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -97,7 +97,43 @@ 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;
const hasReviewedGovernance =
Boolean(prior.last_reviewed_at) ||
prior.validation_status === "locally_reviewed" ||
prior.validation_status === "approved";
if (!hasReviewedGovernance) return row;
preserved += 1;
return {
...row,
source_status: prior.source_status,
validation_status: prior.validation_status,
Comment thread
BigSimmo marked this conversation as resolved.
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}`);
}
Expand Down
9 changes: 8 additions & 1 deletion src/app/api/registry/records/[slug]/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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,
Expand DownExpand Up@@ -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();
Expand Down
20 changes: 0 additions & 20 deletions src/app/api/registry/records/route.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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<string, string[]> = {};
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) {
Expand Down
1 change: 1 addition & 0 deletions src/components/ClinicalDashboard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,9 +62,9 @@
answerSurface,
chatMicroAction,
clinicalDivider,
clinicalNotesRow,

Check warning on line 65 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions/ verify

'clinicalNotesRow' is defined but never used
cn,
evidenceRow,

Check warning on line 67 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions/ verify

'evidenceRow' is defined but never used
EmptyState,
fieldControlPlain,
fieldControlWithIcon,
Expand DownExpand Up@@ -255,9 +255,9 @@
import {
type AnswerEvidenceMapRow,
type AnswerViewMode,
buildAnswerEvidenceMap,

Check warning on line 258 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions/ verify

'buildAnswerEvidenceMap' is defined but never used
buildClinicalOutputSections,

Check warning on line 259 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions/ verify

'buildClinicalOutputSections' is defined but never used
buildHighYieldClinicalOutputSections,

Check warning on line 260 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions/ verify

'buildHighYieldClinicalOutputSections' is defined but never used
shouldPollForUpdates,
} from "@/lib/ward-output";

Expand DownExpand Up@@ -478,7 +478,7 @@
return navigationHashes.includes(hash as (typeof navigationHashes)[number]) ? hash : "#search";
}

function WhyThisMatchedPanel({ sources }: { sources: SearchResult[] }) {

Check warning on line 481 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions/ verify

'WhyThisMatchedPanel' is defined but never used
const visibleSources = sources.slice(0, 3);
if (visibleSources.length === 0) return null;

Expand DownExpand Up@@ -1056,7 +1056,7 @@
function MobileEvidenceTabPanel({
tab,
renderModel,
query,

Check warning on line 1059 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions/ verify

'query' is defined but never used
visualEvidence,
answerEvidenceMapRows,
copiedQuotes,
Expand DownExpand Up@@ -1129,7 +1129,7 @@
return <EvidenceGapsPanel warnings={renderModel.warnings} />;
}

function UnifiedEvidenceDrawerContent({

Check warning on line 1132 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions/ verify

'UnifiedEvidenceDrawerContent' is defined but never used
answer,
renderModel,
query,
Expand DownExpand Up@@ -1322,8 +1322,8 @@
query,
safeAnswerText,
bestSource,
currentRelevance,

Check warning on line 1325 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions/ verify

'currentRelevance' is defined but never used
queryMode,

Check warning on line 1326 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions/ verify

'queryMode' is defined but never used
sourceGovernanceWarnings,
sourceSummary,
renderModel,
Expand DownExpand Up@@ -5589,6 +5589,7 @@
matches={documentMatches}
recordMatches={recordSearchMatches}
recordMode={recordSearchMode}
recordStatus={registryRecords.status}
showRecordMatches={searchMode === "services" || searchMode === "forms"}
query={query}
loading={loading}
Expand Down
43 changes: 42 additions & 1 deletion src/components/clinical-dashboard/document-search-results.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,6 +14,8 @@ import {
Filter,
FolderOpen,
ListChecks,
Loader2,
Shield,
ShieldAlert,
SlidersHorizontal,
Sparkles,
Expand DownExpand Up@@ -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 };
Expand DownExpand Up@@ -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 (
<p
data-testid="dashboard-registry-status-notice"
className={cn("flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-semibold", toneClass)}
>
<config.Icon className={cn("h-4 w-4 shrink-0", config.spin && "animate-spin")} aria-hidden />
{config.text}
</p>
);
}

export function DocumentSearchResultsPanel({
matches,
recordMatches = [],
recordMode = "services",
recordStatus = "ready",
showRecordMatches = false,
query,
loading,
Expand All@@ -965,6 +1000,7 @@ export function DocumentSearchResultsPanel({
matches: DocumentMatch[];
recordMatches?: SearchRecordMatch[];
recordMode?: SearchRecordMode;
recordStatus?: RegistryRequestStatus;
showRecordMatches?: boolean;
query: string;
loading: boolean;
Expand DownExpand Up@@ -1066,7 +1102,12 @@ export function DocumentSearchResultsPanel({
</div>
) : null}

{showRecordMatches ? <SearchRecordResults matches={recordMatches} query={query} mode={recordMode} /> : null}
{showRecordMatches ? (
<>
<RecordRegistryNotice status={recordStatus} mode={recordMode} />
<SearchRecordResults matches={recordMatches} query={query} mode={recordMode} />
</>
) : null}

{loading ? (
<LoadingPanel label="Finding matching documents" />
Expand Down
56 changes: 51 additions & 5 deletions src/components/forms/forms-home-page.tsx
Original file line numberDiff line numberDiff line change
@@ -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,
Expand DownExpand Up@@ -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" ? (
<ModeHomeStatusNotice
icon={Loader2}
title="Loading forms registry"
body="Form tasks will appear once your private registry is ready."
/>
) : registry.status === "unauthorized" ? (
<ModeHomeStatusNotice
icon={ShieldAlert}
title="Sign in required"
body="Sign in to open private form records and pathways."
actionHref="/"
actionLabel="Go to sign in"
/>
) : registry.status === "error" ? (
<ModeHomeStatusNotice
icon={ShieldAlert}
title="Could not load forms"
body="The forms registry could not be loaded. Try again shortly."
/>
) : !hasRegistryRecords ? (
<ModeHomeStatusNotice
icon={FileQuestion}
title="No forms seeded yet"
body="Seed your forms registry before opening form detail shortcuts."
/>
) : null;

return (
<ModeHomeMain testId="forms-home">
Expand All@@ -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 ? (
<ModeHomeVerificationFooter
icon={ShieldCheck}
label="Source verified"
body="MHA 2014 forms"
verifiedCount={verifiedCount}
totalCount={registry.total}
/>
) : null
) : (
registryNotice
)
}
/>
</ModeHomeMain>
Expand Down
27 changes: 21 additions & 6 deletions src/components/forms/forms-search-results-page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 =
Expand All@@ -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 (
<p
<div
data-testid="forms-registry-status-notice"
className={`flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-semibold ${toneClass}`}
className={`flex flex-wrap items-center gap-2 rounded-lg border px-3 py-2 text-sm font-semibold ${toneClass}`}
>
<Icon className={`h-4 w-4 shrink-0 ${notice.spin ? "animate-spin" : ""}`} aria-hidden />
{notice.text}
</p>
<span className="min-w-0 flex-1">{notice.text}</span>
{notice.action ? (
<Link
href={notice.action.href}
className="inline-flex min-h-8 items-center justify-center rounded-md bg-[color:var(--command)] px-3 text-xs font-bold text-[color:var(--command-contrast)] hover:bg-[color:var(--command-hover)]"
>
{notice.action.label}
</Link>
) : null}
</div>
);
}

Expand Down
40 changes: 38 additions & 2 deletions src/components/mode-home-template.tsx
Original file line numberDiff line numberDiff line change
@@ -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";

Expand DownExpand Up@@ -111,11 +111,13 @@ export function ModeHomeMain({
}

export function ModeHomeVerificationFooter({
icon: Icon,
label,
body,
verifiedCount,
totalCount,
}: {
icon: LucideIcon;
label: string;
body: string;
verifiedCount: number;
Expand All@@ -124,7 +126,7 @@ export function ModeHomeVerificationFooter({
return (
<p className="flex flex-wrap items-center justify-center gap-x-3 gap-y-2 pt-1 text-xs font-medium leading-5 text-[color:var(--text-muted)] sm:text-sm">
<span className="inline-flex items-center gap-2 font-semibold text-[color:var(--clinical-accent)]">
<ShieldCheck className="h-4 w-4" aria-hidden="true" />
<Icon className="h-4 w-4" aria-hidden="true" />
{label}
</span>
<span aria-hidden="true">•</span>
Expand All@@ -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 (
<div className="mx-auto grid max-w-xl gap-3 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] px-4 py-3 text-left shadow-[var(--shadow-inset)] sm:grid-cols-[2.25rem_minmax(0,1fr)_auto] sm:items-center">
<span className="grid h-9 w-9 place-items-center rounded-lg bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]">
<Icon className="h-5 w-5" aria-hidden="true" />
</span>
<span className="grid gap-1">
<span className="text-sm font-bold text-[color:var(--text-heading)]">{title}</span>
<span className="text-sm leading-5 text-[color:var(--text-muted)]">{body}</span>
</span>
{actionHref && actionLabel ? (
<Link
href={actionHref}
className="inline-flex min-h-9 items-center justify-center rounded-lg bg-[color:var(--command)] px-3 text-sm font-semibold text-[color:var(--command-contrast)] hover:bg-[color:var(--command-hover)]"
>
{actionLabel}
</Link>
) : null}
</div>
);
}

export function ModeHomeTemplate({
testId,
title,
Expand Down
Loading
Loading