diff --git a/next.config.ts b/next.config.ts index 7ad9f31bd..262feb75f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,7 @@ import type { NextConfig } from "next"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { buildSecurityHeaders, resolveRuntimeFlags } from "./src/lib/security-headers"; +import { expectedSupabaseProject } from "./src/lib/supabase/project"; const projectRoot = path.dirname(fileURLToPath(import.meta.url)); const requestedDistDir = process.env.NEXT_DIST_DIR?.trim(); @@ -47,6 +48,26 @@ const nextConfig: NextConfig = { proxyClientMaxBodySize: "151mb", }, poweredByHeader: false, + images: { + // Prefer AVIF (~20-30% smaller than WebP), falling back to WebP, for any + // next/image output. + formats: ["image/avif", "image/webp"], + // Permit optimizing Supabase Storage signed URLs (private document/image + // previews) through next/image. Scoped to this app's exact production and + // (when configured) staging project hostnames, not the wildcard *.supabase.co. + remotePatterns: (() => { + const allowedHostnames = [expectedSupabaseProject.ref + ".supabase.co"]; + const stagingRef = process.env.SUPABASE_STAGING_PROJECT_REF?.trim(); + if (stagingRef) { + allowedHostnames.push(stagingRef + ".supabase.co"); + } + return allowedHostnames.map((hostname) => ({ + protocol: "https" as const, + hostname, + pathname: "/storage/v1/object/**", + })); + })(), + }, turbopack: { root: projectRoot, }, diff --git a/src/app/forms/[slug]/page.tsx b/src/app/forms/[slug]/page.tsx index 3e8ddd973..8aef1678c 100644 --- a/src/app/forms/[slug]/page.tsx +++ b/src/app/forms/[slug]/page.tsx @@ -19,5 +19,9 @@ export async function generateMetadata({ params }: FormRouteProps): Promise; + // Hand the public fixture record to the client so the detail view paints real + // content on first load instead of a centered spinner; the owner-aware record + // refreshes in place. Absent from the fixtures (owner-only slug) → null. + const fallbackRecord = getFormRecord(slug); + return ; } diff --git a/src/app/globals.css b/src/app/globals.css index 241f1bf60..36c28d12b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -525,8 +525,9 @@ summary { transition-timing-function: var(--ease-out-soft); } +/* Tactile press only on button-like controls. Applying it to every `a` nudged + inline body/citation links down 1px on tap, which reads oddly for text links. */ button:active, -a:active, summary:active { transform: translateY(1px); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7d9a185a0..809791d96 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -17,6 +17,11 @@ const geistSans = Geist({ const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"], + // The mono face is only used deep in the UI (tabular figures, `kbd`, code) and + // never in initial/LCP text, so don't preload it on every route — it competes + // for the critical-path connection. It still loads on-demand via `swap` when + // first painted. The sans face keeps the default preload. + preload: false, }); const baseMetadata: Metadata = { diff --git a/src/app/medications/[slug]/page.tsx b/src/app/medications/[slug]/page.tsx index 4ee491c9a..e52cdbe36 100644 --- a/src/app/medications/[slug]/page.tsx +++ b/src/app/medications/[slug]/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { MedicationRecordPage } from "@/components/clinical-dashboard/medication-record-page"; +import { deriveGovernanceFromSections } from "@/lib/medication-records"; import { getMedicationRecord, loadMedicationSnapshot } from "@/lib/medication-snapshot"; type MedicationPageProps = { @@ -31,5 +32,21 @@ export async function generateMetadata({ params }: MedicationPageProps): Promise export default async function MedicationPage({ params }: MedicationPageProps) { const { slug } = await params; - return ; + // The public snapshot record is already in hand server-side. Pass it (plus the + // governance derived exactly as the public API does) so the page paints real + // clinical content on first load instead of a skeleton, while the client hook + // refreshes in place for authenticated owner records. Slugs absent from the + // snapshot (owner-only medications) pass `undefined` and keep the loading path. + const record = getMedicationRecord(slug); + const fallbackGovernance = record + ? (() => { + const derived = deriveGovernanceFromSections(record); + // Validation/review status is a governance decision that must come from + // the live/authoritative response, not a hard-coded guess used only for + // the pre-fetch content-first paint. + return { sourceStatus: derived.source_status, validationStatus: "unverified" as const }; + })() + : undefined; + + return ; } diff --git a/src/app/services/[slug]/page.tsx b/src/app/services/[slug]/page.tsx index 4c1986d41..0b17109fe 100644 --- a/src/app/services/[slug]/page.tsx +++ b/src/app/services/[slug]/page.tsx @@ -19,5 +19,9 @@ export async function generateMetadata({ params }: ServiceRouteProps): Promise; + // Hand the public fixture record to the client so the detail view paints real + // content on first load instead of a centered spinner; the owner-aware record + // refreshes in place. Absent from the fixtures (owner-only slug) → null. + const fallbackRecord = getServiceRecord(slug); + return ; } diff --git a/src/components/clinical-dashboard/DocumentManagerPanel.tsx b/src/components/clinical-dashboard/DocumentManagerPanel.tsx index 030e5e9aa..7927cb14d 100644 --- a/src/components/clinical-dashboard/DocumentManagerPanel.tsx +++ b/src/components/clinical-dashboard/DocumentManagerPanel.tsx @@ -429,8 +429,8 @@ export function UploadPanel({ className="h-1.5 w-full overflow-hidden rounded-full bg-[color:var(--surface-inset)]" >
diff --git a/src/components/clinical-dashboard/medication-record-page.tsx b/src/components/clinical-dashboard/medication-record-page.tsx index 1b4f02e85..e159a07df 100644 --- a/src/components/clinical-dashboard/medication-record-page.tsx +++ b/src/components/clinical-dashboard/medication-record-page.tsx @@ -475,8 +475,25 @@ function MedicationRecordDetail({ ); } -export function MedicationRecordPage({ slug }: { slug: string }) { +export function MedicationRecordPage({ + slug, + fallbackRecord, + fallbackGovernance, +}: { + slug: string; + fallbackRecord?: MedicationRecord; + fallbackGovernance?: MedicationGovernance; +}) { const { data, loading, error } = useMedicationDetail(slug); + // Content-first: render the SSR fallback immediately, then swap in the live + // (owner-aware) record once the hook resolves. Only fall back to the skeleton + // when there is no server record to show (owner-only slugs) and the fetch is + // still in flight; the error state applies only when nothing renderable exists. + const record = data?.record ?? fallbackRecord ?? null; + // Only trust the SSR fallback governance while the live fetch is still in + // flight. A failed request means the authoritative status is unknown, so + // don't keep presenting the fixture-derived guess as if it were confirmed. + const governance = data?.governance ?? (error ? undefined : fallbackGovernance); return (
@@ -491,19 +508,19 @@ export function MedicationRecordPage({ slug }: { slug: string }) {
- {loading ? ( + {record ? ( + + ) : loading ? (
- ) : error || !data?.record ? ( + ) : (
- ) : ( - )}