Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
# generic-api-key rule on historical commit content scanned in PR history.
27121756b10df6d93841ebf48b0f1991e0144a1f:data/medications-snapshot.json:generic-api-key:22253
27121756b10df6d93841ebf48b0f1991e0144a1f:data/medications-snapshot.json:generic-api-key:47690
b65acddaa13e69e0ad2a49783116056b3a1a3639:data/medications-snapshot.json:generic-api-key:21520
b65acddaa13e69e0ad2a49783116056b3a1a3639:data/medications-snapshot.json:generic-api-key:46330
1 change: 1 addition & 0 deletions docs/clinical-governance.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,5 @@ Use the `.github/pull_request_template.md` clinical governance section for any c
- Supabase unused-index advisor items are a watchlist, not a removal queue. Keep search/RAG support indexes such as document-label, title, chunk, summary, RAG logging, and audit indexes unless production query evidence plus local verification shows they are genuinely dead.
- Document organization coverage is an operational invariant: after ingestion or generated-label reclassification, run `npm run check:document-label-coverage` and require zero indexed documents missing generated `site` or `document_type` labels.
- **Application-layer cross-owner denial** (service-role routes enforce `owner_id` scoping in code) is covered by `tests/private-access-routes.test.ts` and `tests/private-rag-access.test.ts` (unowned document detail/signed-url/rename rejected; listing and search scoped to the authenticated owner).
- **Public and anonymous read access** (2026-07-05): curated catalog routes (`/api/medications`, `/api/differentials`, `/api/registry/*`) serve static or seeded payloads to callers with no session signal; authenticated callers receive owner-scoped DB reads with separate rate-limit buckets (`medications`, `differentials`, `registry`). Document and image signed-url routes use `withOwnerReadScope` (public rows where `owner_id IS NULL`, plus owned rows when signed in), enforce `document_read` rate limits for anonymous callers, and return 404 for non-`indexed` documents or uncommitted image generations. Upload rejects unsafe local origins with HTTP 409 before Supabase access (`assertSafeLocalProjectRequest`).
- **Follow-up:** add a live DB-level RLS integration test that connects as two real authenticated users via the publishable (anon) key and asserts owner B cannot read owner A's rows. This needs a seeded test project/harness and is tracked as a remaining item.
5 changes: 2 additions & 3 deletions docs/site-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
## Main product pages

- `/` - Main Clinical KB shell. Source: `src/app/page.tsx`.
- `/applications` - Application and tool launcher. Source: `src/app/applications/page.tsx`.
- `/differentials` - Differentials home and search surface. Source: `src/app/differentials/page.tsx`.
- `/differentials/diagnoses` - Diagnosis stream. Source: `src/app/differentials/diagnoses/page.tsx`.
- `/differentials/presentations` - Presentation workflow stream. Source: `src/app/differentials/presentations/page.tsx`.
Expand Down Expand Up @@ -40,7 +39,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
| Favourites | `/favourites` | `/favourites?q=clozapine+set&focus=1&run=1` | Saved set and saved item detail render inside the favourites page surface. |
| Differentials | `/differentials` | `/differentials?q=acute+confusion&focus=1&run=1` | `/differentials/diagnoses`, `/differentials/diagnoses/[slug]`, and `/differentials/presentations`. |
| Medication | `/?mode=prescribing` | `/?mode=prescribing&q=acamprosate+renal+dose&focus=1&run=1` | `/medications/[slug]`; `/medications` redirects to medication mode. |
| Tools | `/?mode=tools` | `/?mode=tools&q=medications&focus=1&run=1` | `/applications` launcher and tool detail panels inside tools mode. |
| Tools | `/?mode=tools` | `/?mode=tools&q=medications&focus=1&run=1` | Tool launcher and detail panels inside dashboard tools mode (`/?mode=tools`). |

## Documents flow index

Expand Down Expand Up @@ -601,5 +600,5 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
| Differentials | `src/app/differentials, src/lib/differentials.ts` |
| Medications | `src/app/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx` |
| Documents | `src/app/documents, src/lib/document-flow-routes.ts` |
| Applications and tools | `src/app/applications, src/components/applications-launcher-page.tsx` |
| Tools | `src/components/applications-launcher-page.tsx` |
| Mockups | `src/app/mockups` |
9 changes: 9 additions & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ const nextConfig: NextConfig = {
},
];
},
async redirects() {
return [
{
source: "/applications",
destination: "/?mode=tools",
permanent: true,
},
];
},
};

export default nextConfig;
5 changes: 2 additions & 3 deletions scripts/generate-site-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ type SiteMapData = {

const routeDescriptions: Record<string, string> = {
"/": "Main Clinical KB shell.",
"/applications": "Application and tool launcher.",
"/differentials": "Differentials home and search surface.",
"/differentials/diagnoses": "Diagnosis stream.",
"/differentials/diagnoses/[slug]": "Differential diagnosis detail.",
Expand Down Expand Up @@ -97,7 +96,7 @@ const routeOwnershipRows = [
["Differentials", "src/app/differentials, src/lib/differentials.ts"],
["Medications", "src/app/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx"],
["Documents", "src/app/documents, src/lib/document-flow-routes.ts"],
["Applications and tools", "src/app/applications, src/components/applications-launcher-page.tsx"],
["Tools", "src/components/applications-launcher-page.tsx"],
["Mockups", "src/app/mockups"],
] as const;

Expand Down Expand Up @@ -279,7 +278,7 @@ function renderModePageIndex() {
mode: "Tools",
home: appModeHomeHref("tools"),
search: appModeHomeHref("tools", { query: "medications", focus: true, run: true }),
detail: "`/applications` launcher and tool detail panels inside tools mode.",
detail: "Tool launcher and detail panels inside dashboard tools mode (`/?mode=tools`).",
},
]);
}
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/differentials/[slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
const rateLimit = await consumeSubjectApiRateLimit({
supabase,
subject: access.rateLimitSubject,
bucket: "registry",
bucket: "differentials",
allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
});
if (rateLimit.limited) {
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/differentials/presentations/[slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
const rateLimit = await consumeSubjectApiRateLimit({
supabase,
subject: access.rateLimitSubject,
bucket: "registry",
bucket: "differentials",
allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
});
if (rateLimit.limited) {
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/differentials/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export async function GET(request: Request) {
const rateLimit = await consumeSubjectApiRateLimit({
supabase,
subject: access.rateLimitSubject,
bucket: "registry",
bucket: "differentials",
allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
});
if (rateLimit.limited) {
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/documents/[id]/reindex/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
isAtomicReindexCandidate,
isCommittedGenerationMetadata,
} from "@/lib/reindex-pipeline";
import { invalidateRagCachesForDocumentMutation } from "@/lib/rag";
import { createAdminClient } from "@/lib/supabase/admin";
import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth";
import { parseJsonBodyOrDefault } from "@/lib/validation/body";
Expand Down Expand Up @@ -174,6 +175,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ id:
images: committedImages,
summary: enrichment.summary.summary,
});
invalidateRagCachesForDocumentMutation(user.id);
return NextResponse.json({
mode,
enrichment,
Expand Down
5 changes: 4 additions & 1 deletion src/app/api/documents/[id]/signed-url/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,15 @@ export async function GET(_request: Request, { params }: { params: Promise<{ id:
const supabase = createAdminClient();
const access = await publicAccessContext(_request, supabase);
const { data: document, error } = await withOwnerReadScope(
supabase.from("documents").select("storage_path,file_type").eq("id", id),
supabase.from("documents").select("storage_path,file_type,status").eq("id", id),
access.ownerId,
).maybeSingle();

if (error) throw new Error(error.message);
if (!document) return NextResponse.json({ error: "Document not found." }, { status: 404 });
if (document.status && document.status !== "indexed") {
return NextResponse.json({ error: "Document not found." }, { status: 404 });
}

const storage = supabase.storage.from(env.SUPABASE_DOCUMENT_BUCKET);
const signed = shouldDownload
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/medications/[slug]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export async function GET(request: Request, context: { params: Promise<{ slug: s
const rateLimit = await consumeSubjectApiRateLimit({
supabase,
subject: access.rateLimitSubject,
bucket: "registry",
bucket: "medications",
allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
});
if (rateLimit.limited) {
Expand Down
2 changes: 1 addition & 1 deletion src/app/api/medications/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export async function GET(request: Request) {
const rateLimit = await consumeSubjectApiRateLimit({
supabase,
subject: access.rateLimitSubject,
bucket: "registry",
bucket: "medications",
allowInMemoryFallbackOnUnavailable: isLocalNoAuthMode(),
});
if (rateLimit.limited) {
Expand Down
15 changes: 10 additions & 5 deletions src/app/api/setup-status/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { env, isDemoMode } from "@/lib/env";
import { jsonError } from "@/lib/http";
import { localProjectRequestIdentityPayload, unsafeLocalProjectResponse } from "@/lib/local-project-guard";
import { createAdminClient } from "@/lib/supabase/admin";
import { formatSupabaseUnavailableError, isSupabaseUnavailableError, probeSupabaseHealth } from "@/lib/supabase/health";
Expand Down Expand Up @@ -409,10 +410,14 @@ async function readSetupStatusPayload() {
}

export async function GET(request: Request) {
const identity = localProjectRequestIdentityPayload(request);
if (!identity.localServer.safeLocalOrigin) {
return unsafeLocalProjectResponse(identity);
}
try {
const identity = localProjectRequestIdentityPayload(request);
if (!identity.localServer.safeLocalOrigin) {
return unsafeLocalProjectResponse(identity);
}

return setupStatusResponse(await readSetupStatusPayload());
return setupStatusResponse(await readSetupStatusPayload());
} catch (error) {
return jsonError(error);
}
}
5 changes: 5 additions & 0 deletions src/app/api/upload/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { writeAuditLog } from "@/lib/audit";
import { planDocumentName, type DocumentNameSupabase } from "@/lib/document-naming";
import { createAdminClient } from "@/lib/supabase/admin";
import { AuthenticationError, requireAuthenticatedUser, unauthorizedResponse } from "@/lib/supabase/auth";
import { assertSafeLocalProjectRequest, localProjectOriginErrorResponse, UnsafeLocalProjectOriginError } from "@/lib/local-project-guard";
import { probeSupabaseHealth } from "@/lib/supabase/health";
import { optionalFormText, parseFormDataFields } from "@/lib/validation/form-data";

Expand Down Expand Up @@ -82,6 +83,7 @@ export async function POST(request: Request) {
let insertedDocumentOwnerId: string | null = null;

try {
assertSafeLocalProjectRequest(request);
supabase = createAdminClient();
const adminSupabase = supabase;
const user = await requireAuthenticatedUser(request, adminSupabase);
Expand Down Expand Up @@ -298,6 +300,9 @@ export async function POST(request: Request) {
if (error instanceof AuthenticationError) {
return unauthorizedResponse();
}
if (error instanceof UnsafeLocalProjectOriginError) {
return localProjectOriginErrorResponse(error);
}

return jsonError(error);
}
Expand Down
11 changes: 0 additions & 11 deletions src/app/applications/layout.tsx

This file was deleted.

5 changes: 0 additions & 5 deletions src/app/applications/loading.tsx

This file was deleted.

12 changes: 0 additions & 12 deletions src/app/applications/page.tsx

This file was deleted.

23 changes: 15 additions & 8 deletions src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1101,14 +1101,14 @@ summary::-webkit-details-marker {

.answer-footer-search-action,
.answer-footer-search-send {
height: 2.05rem;
width: 2.05rem;
height: 2.75rem;
width: 2.75rem;
}

.answer-footer-search-action svg,
.answer-footer-search-send svg {
height: 1rem;
width: 1rem;
height: 1.1rem;
width: 1.1rem;
}
}

Expand Down Expand Up @@ -1172,14 +1172,21 @@ summary::-webkit-details-marker {
@media (max-width: 430px) {
.answer-footer-search-action,
.answer-footer-search-send {
height: 2.05rem !important;
width: 2.05rem !important;
height: 2.75rem !important;
width: 2.75rem !important;
min-height: 2.75rem;
min-width: 2.75rem;
}

.answer-footer-search-action svg,
.answer-footer-search-send svg {
height: 1rem;
width: 1rem;
<<<<<<< Updated upstream
height: 1.125rem;
width: 1.125rem;
=======
height: 1.1rem;
width: 1.1rem;
>>>>>>> Stashed changes
}
}

Expand Down
Loading