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
12 changes: 6 additions & 6 deletions docs/privacy-impact-assessment.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,12 +193,12 @@ of the most important privacy items before real patient use (PIA-1).
All three log tables are **owner-stamped** and **RLS-enabled** (owner-read for authenticated users;
service-role for writes). Redaction is applied centrally at every write site.

| Table | Raw query stored? | Redaction mechanism | Other sensitive columns | RLS |
| -------------------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `rag_queries` | No (hash placeholder) | `queryTextForStorage` / `normalizedQueryTextForStorage` ([query-privacy.ts:33-39](src/lib/query-privacy.ts)); centralized write in `insertRagQuery` | `answer` is null by default and stored only with explicit `RAG_PERSIST_ANSWER_TEXT=true`; `source_chunk_ids` (own data) | owner-read, [schema.sql:3932](supabase/schema.sql) |
| `rag_query_misses` | No (hash placeholder) | same helpers; writes in [search/route.ts:558-559](src/app/api/search/route.ts), [interaction/route.ts:88-89](src/app/api/search/interaction/route.ts) | `metadata.query_hash` | owner-read, [schema.sql:3935](supabase/schema.sql) |
| `rag_retrieval_logs` | No (hash placeholder) | same helpers; write at [search/route.ts:556-559](src/app/api/search/route.ts) | retrieval telemetry only | owner-read, [schema.sql:3938](supabase/schema.sql) |
| `audit_logs` | N/A (no query text) | action/resource metadata only; error strings pass through `redactLogValue` ([privacy.ts:5-31](src/lib/privacy.ts)) | `owner_id`, `action`, `resource_id` | service-role-only, [schema.sql:3959](supabase/schema.sql) |
| Table | Raw query stored? | Redaction mechanism | Other sensitive columns | RLS |
| -------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `rag_queries` | No (hash placeholder) | `queryTextForStorage` / `normalizedQueryTextForStorage` ([query-privacy.ts:33-39](src/lib/query-privacy.ts)); centralized write in `insertRagQuery` | `answer` is null by default and stored only with explicit `RAG_PERSIST_ANSWER_TEXT=true`; `source_chunk_ids` (own data) | owner-read, [schema.sql:3932](supabase/schema.sql) |
| `rag_query_misses` | No (hash placeholder) | same helpers; writes in [search/route.ts:558-559](src/app/api/search/route.ts), [interaction/route.ts:88-89](src/app/api/search/interaction/route.ts) | `metadata.query_hash` | owner-read, [schema.sql:3935](supabase/schema.sql) |
| `rag_retrieval_logs` | No (hash placeholder) | same helpers; write at [search/route.ts:556-559](src/app/api/search/route.ts) | retrieval telemetry only | owner-read, [schema.sql:3938](supabase/schema.sql) |
| `audit_logs` | N/A (no query text) | action/resource metadata only; the write boundary allowlists operational metadata and excludes user-controlled filenames/titles/content hashes ([audit.ts](../src/lib/audit.ts)). Migration `20260717163000` minimizes existing rows on deployment. | `owner_id`, `action`, `resource_id` | service-role-only, [schema.sql:3959](supabase/schema.sql) |

### 5.1 M15 HMAC query-hash fix — verified present, enforced in production

Expand Down
3 changes: 3 additions & 0 deletions docs/site-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ 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`.
Expand Down
4 changes: 2 additions & 2 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ const chromiumExecutablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;
// Tag-level filters keep production and prototype journeys disjoint even when
// they share a spec file.
const productionSpecPattern =
/.*ui-(smoke|stress|accessibility|tools|overlap|universal-search|specifiers|formulation|pwa)\.spec\.ts/;
/.*(?:answer-progress-ui-smoke|ui-(smoke|stress|accessibility|tools|overlap|universal-search|specifiers|formulation|pwa))\.spec\.ts/;
const mockupSpecPattern = /.*ui-(tools|tools-collapse|tools-task-directory)\.spec\.ts/;
const mockupTag = /@mockup/;

export default defineConfig({
testDir: "./tests",
testMatch:
/.*ui-(smoke|stress|accessibility|tools|tools-collapse|tools-task-directory|overlap|universal-search|specifiers|formulation|pwa)\.spec\.ts/,
/.*(?:answer-progress-ui-smoke|ui-(smoke|stress|accessibility|tools|tools-collapse|tools-task-directory|overlap|universal-search|specifiers|formulation|pwa))\.spec\.ts/,
timeout: 60_000,
retries: 0,
// Fail the run if a stray `test.only` is committed: otherwise it silently
Expand Down
11 changes: 11 additions & 0 deletions scripts/ci-change-scope.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ const uiPatterns = [
"src/components",
"src/styles",
"public",
// Answer progress is a production Playwright journey even though its
// historical filename does not start with `ui-`. Keep its CI trigger in
// lockstep with playwright.config.ts so an edited assertion cannot evade
// the required UI job (Vitest does not collect *.spec.ts files).
"tests/answer-progress-ui-smoke.spec.ts",
/^tests\/ui-.*\.spec\.ts$/,
/^tests\/playwright-.*\.ts$/,
/^playwright(?:\..*)?\.config\.ts$/,
Expand Down Expand Up @@ -420,6 +425,12 @@ function selfTest() {
ui_changed: true,
build_changed: true,
});
assertScope("answer-progress-playwright", ["tests/answer-progress-ui-smoke.spec.ts"], {
source_changed: true,
coverage_changed: true,
ui_changed: true,
build_changed: false,
});
assertScope("db", ["supabase/migrations/20260710000000_example.sql"], {
db_changed: true,
source_changed: true,
Expand Down
3 changes: 3 additions & 0 deletions scripts/generate-site-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ const routeDescriptions: Record<string, string> = {
"/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.",
Expand Down
8 changes: 6 additions & 2 deletions src/app/api/documents/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
action: "document_rename",
resourceType: "document",
resourceId: id,
metadata: { previousTitle: document.title, newTitle: body.title },
// Audit the rename event and resource without retaining user-controlled
// titles indefinitely in the service-role audit log.
metadata: {},
});
return NextResponse.json({ document: updated });
} catch (error) {
Expand Down Expand Up @@ -241,7 +243,9 @@ export async function DELETE(request: Request, { params }: { params: Promise<{ i
action: "document_delete",
resourceType: "document",
resourceId: id,
metadata: { title: result.document_title, storageRemoved: cleanup.storageRemoved },
// The deleted title can contain patient information. Retain only the
// operational cleanup result alongside the resource id and action.
metadata: { storageRemoved: cleanup.storageRemoved },
});
return NextResponse.json({
deleted: true,
Expand Down
5 changes: 4 additions & 1 deletion src/app/api/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,10 @@ export async function POST(request: Request) {
action: "document_upload",
resourceType: "document",
resourceId: documentId,
metadata: { fileName: file.name, fileType: file.type, fileSize: file.size, contentHash },
// `audit_logs` is retained indefinitely. Keep only operational facts there;
// the user-controlled filename and content hash remain on the scoped document
// record, not in the durable audit trail.
metadata: { fileType: file.type, fileSize: file.size },
});

return NextResponse.json({ document, job }, { status: 201 });
Expand Down
18 changes: 18 additions & 0 deletions src/app/factsheets/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";

import { FactsheetDetailPage } from "@/components/factsheets/factsheet-detail-page";
import { findFactsheet } from "@/components/factsheets/factsheets-data";

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const factsheet = findFactsheet(slug);
return { title: factsheet ? `${factsheet.title} | Patient Information` : "Factsheet not found" };
}

export default async function FactsheetInfoPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const factsheet = findFactsheet(slug);
if (!factsheet) notFound();
return <FactsheetDetailPage factsheet={factsheet} />;
}
7 changes: 7 additions & 0 deletions src/app/factsheets/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { ReactNode } from "react";

import { FactsheetShell } from "@/components/factsheets/factsheet-shell";

export default function FactsheetsLayout({ children }: { children: ReactNode }) {
return <FactsheetShell>{children}</FactsheetShell>;
}
12 changes: 12 additions & 0 deletions src/app/factsheets/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Metadata } from "next";

import { FactsheetsHomePage } from "@/components/factsheets/factsheets-home-page";

export const metadata: Metadata = {
title: "Patient Information Sheets | Clinical KB",
description: "Browse patient information sheet layouts and approved local resources.",
};

export default function FactsheetsPage() {
return <FactsheetsHomePage />;
}
9 changes: 9 additions & 0 deletions src/app/factsheets/search/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Metadata } from "next";

import { FactsheetsSearchPage } from "@/components/factsheets/factsheets-search-page";

export const metadata: Metadata = { title: "Search Patient Information | Clinical KB" };

export default function FactsheetsSearchRoute() {
return <FactsheetsSearchPage />;
}
1 change: 1 addition & 0 deletions src/app/global-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export default function GlobalError({ error, reset }: { error: Error & { digest?
<h1
ref={headingRef}
tabIndex={-1}
role="alert"
style={{
margin: "0 0 0.5rem",
fontSize: "1.125rem",
Expand Down
3 changes: 2 additions & 1 deletion src/components/clinical-dashboard/differentials-home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,6 @@ function SearchResultsView({
sortValue={sortValue}
onSortChange={setSortValue}
/>
<UniversalSearchAlsoMatches modeId="differentials" query={query} />
<p
data-testid="differentials-catalogue-notice"
className="flex items-start gap-2 rounded-lg border border-[color:var(--info-border)] bg-[color:var(--info-soft)]/50 px-3 py-1.5 text-xs font-semibold leading-5 text-[color:var(--info)] sm:py-2 sm:text-sm"
Expand Down Expand Up @@ -1142,6 +1141,8 @@ function SearchResultsView({
<DifferentialsMobileCompareBar selectedCount={selectedCount} selectedIds={comparisonIds} query={query} />
) : null}

<UniversalSearchAlsoMatches modeId="differentials" query={query} />

<p className="pb-3 text-center text-xs font-medium text-[color:var(--text-muted)] lg:hidden">
Clinical decision support only. Review before use.
</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -896,7 +896,6 @@ function DocumentSearchResultsPanelImpl({
onSortChange={setSortValue}
/>
</div>
<UniversalSearchAlsoMatches modeId="documents" query={trimmedQuery} />
</>
) : null}
{recordMatchCount > 0 ||
Expand Down Expand Up @@ -1140,6 +1139,8 @@ function DocumentSearchResultsPanelImpl({
</div>
</>
)}

{trimmedQuery && !shouldShowHome ? <UniversalSearchAlsoMatches modeId="documents" query={trimmedQuery} /> : null}
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,6 @@ export function FavouritesCommandLibraryPage({ query = "" }: { query?: string })
<div className="hidden lg:block">
<SearchResultsHeaderBand modeId="favourites" query={query} matchCount={scopedItems.length} />
</div>
<UniversalSearchAlsoMatches modeId="favourites" query={query} />

<FavouritesMobileQuickViews
items={items}
Expand Down Expand Up @@ -1116,6 +1115,8 @@ export function FavouritesCommandLibraryPage({ query = "" }: { query?: string })
onSelectItem={setSelectedItemId}
/>
)}

<UniversalSearchAlsoMatches modeId="favourites" query={query} />
</div>
</div>
{selectedItem ? <ItemWorkspace item={selectedItem} onClose={() => setSelectedItemId(null)} /> : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,6 @@ function MedicationResults({
<div className="hidden lg:block">
<SearchResultsHeaderBand modeId="prescribing" query={query} matchCount={resultCount} />
</div>
<UniversalSearchAlsoMatches modeId="prescribing" query={query} className="medication-also-matches" />
<div className="medication-results-inset min-w-0 space-y-2 sm:flex sm:items-end sm:justify-between sm:gap-4 sm:space-y-0">
<div className="min-w-0 space-y-1">
<p className="text-xs font-semibold uppercase text-[color:var(--text-soft)]">Medication search</p>
Expand Down Expand Up @@ -693,6 +692,8 @@ function MedicationResults({
})}
</div>

<UniversalSearchAlsoMatches modeId="prescribing" query={query} className="medication-also-matches" />

<StatusNotice
realDataReady={realDataReady}
authUnavailable={authUnavailable}
Expand Down
Loading
Loading