diff --git a/docs/process-hardening.md b/docs/process-hardening.md index b43cbbd37c..abbf4578cd 100644 --- a/docs/process-hardening.md +++ b/docs/process-hardening.md @@ -30,6 +30,19 @@ This document turns the current process review into phased, durable repo practic - Added `src/components/clinical-dashboard/` as the module boundary. - `src/app/page.tsx` now imports `ClinicalDashboard` from the module path (`@/components/clinical-dashboard`) while preserving the legacy source declaration file for AST and merge-guard compatibility. +- **2026-07-03:** extracted `AuthPanel` (+ its solely-consumed auth-email snapshot helpers) into `clinical-dashboard/auth-panel.tsx`. Monolith 7924 → 7800 lines. Per-module gate established: `npm run typecheck` + `npx vitest run tests/clinical-dashboard-merge-artifacts.test.ts tests/rendered-text-formatting.test.ts` + a `data-testid`/`aria-label` sha1 checksum over `ClinicalDashboard.tsx` + `clinical-dashboard/*.tsx` (must be byte-identical before/after each move) + lint + prettier. + +#### Remaining decomposition — hand-off (do on a stable `main`, one module per commit) + +The approved move map (`docs/redesign/04-deferred.md` §2) has 5 modules left. Unlike `auth-panel`, these are **interdependent** — they share a clinical-detail/notes helper family, so order matters and cross-module `export`s are required. Recommended order and the key dependency to resolve first: + +1. `answer-content.tsx` — `SourceImage`, `ScopeAndGovernanceNotice`, `SourcePreviewContent`, `NaturalLanguageAnswer` (**AST-pinned** — retarget `tests/clinical-dashboard-merge-artifacts.test.ts` to scan this file for `NaturalLanguageAnswer`), `UserQuestionBubble`, `KeyClinicalItems` + answer formatters. Widen `tests/rendered-text-formatting.test.ts` to also scan this file. +2. `evidence-panels.tsx` — the clinical-detail/notes helper family (`displayItemsForClinicalDetailSection`, `sortClinicalDetailSections`, `clinicalDetailSummaryItems`, and siblings) **plus** `ClinicalNotesChecklistPanel`, `SafetyFindingsPanel`, `EvidenceGapPanel`, `EvidenceCounts`, `AnswerSourceStatus`, `EvidenceSummaryCard`, `AnswerInsightBar`, `EvidenceVerificationStrip`, `AnswerFeedbackPanel`, `VerificationWorkspace`, `AnswerViewModeControl`, `EvidenceMapTable`, `AnswerSafetyNotice`, `QuoteCards`. **Export the helper family** so output-panel can import it. Must land before output-panel. +3. `output-panel.tsx` — `ClinicalOutputPanel` (**AST-pinned** — retarget `dashboardPath` in `tests/clinical-dashboard-merge-artifacts.test.ts` to resolve declarations across the monolith + this file). Imports the detail helpers from `evidence-panels`. +4. `visual-evidence.tsx` — `VisualEvidenceStrip`, `InlineTableCard`, `MobileEvidenceSheetContent`, `MobileEvidenceTabPanel`, `UnifiedEvidenceDrawerContent`. +5. `document-results.tsx` — `WhyThisMatchedPanel`, `RelatedDocumentsPanel`, `StagedAnswerResultSurface`. + +For each: trace which module-scope helpers/icons/types it uses; move solely-consumed ones with it, import shared ones; strip newly-orphaned monolith imports (lint flags them); run the per-module gate above; commit immediately. Keep the main `ClinicalDashboard` export in `ClinicalDashboard.tsx` (the barrel/bridge stays). Admin surfaces (`DocumentDrawer`, `SettingsDialog`, `ToolsHub`, `MobileSectionFab`) are out of the approved map — a later pass. ## Phase 4 - Release maturity diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index cb685f7379..f5722ea768 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -26,10 +26,8 @@ import { Layers, ListChecks, Loader2, - LogIn, LogOut, LockKeyhole, - Mail, Palette, PanelTop, Plus, @@ -53,7 +51,6 @@ import { } from "lucide-react"; import { type CSSProperties, - FormEvent, memo, type RefObject, useCallback, @@ -96,7 +93,6 @@ import { fieldIcon, floatingControl, iconTilePremium, - fieldLabel, metadataPill, panelSubtle, primaryControl, @@ -120,10 +116,11 @@ import { toneSuccess, toneWarning, } from "@/components/ui-primitives"; -import { AUTH_EMAIL_STORAGE_KEY, useAuthSession } from "@/lib/supabase/client"; +import { useAuthSession } from "@/lib/supabase/client"; import { SafeBoldText } from "@/components/SafeBoldText"; import { Sheet } from "@/components/ui/sheet"; import { AnswerEmptyState, AnswerSkeleton, CopyButton } from "@/components/clinical-dashboard/answer-status"; +import { AuthPanel } from "@/components/clinical-dashboard/auth-panel"; import { useSidebarCollapsed } from "@/components/clinical-dashboard/use-sidebar-collapsed"; import { useTheme } from "@/components/clinical-dashboard/use-theme"; import { StatusBadge, StrengthBadge } from "@/components/clinical-dashboard/badges"; @@ -297,7 +294,6 @@ function useMobilePreviewSheet() { return useSyncExternalStore(subscribeToMobilePreviewMedia, getMobilePreviewSnapshot, () => false); } -const authEmailChangeEvent = "clinical-kb-auth-email-change"; export const recentQueryStorageKey = "clinical-kb-recent-queries"; const documentPageSize = 150; const activeIndexingPollFallbackMs = 5_000; @@ -511,32 +507,6 @@ function normalizeNavigationHash(hash: string) { return navigationHashes.includes(hash as (typeof navigationHashes)[number]) ? hash : "#search"; } -function getAuthEmailSnapshot() { - if (typeof window === "undefined") return ""; - try { - return window.localStorage.getItem(AUTH_EMAIL_STORAGE_KEY) ?? ""; - } catch { - return ""; - } -} - -function getServerAuthEmailSnapshot() { - return ""; -} - -function subscribeAuthEmail(onStoreChange: () => void) { - if (typeof window === "undefined") return () => undefined; - const notify = () => onStoreChange(); - - window.addEventListener("storage", notify); - window.addEventListener(authEmailChangeEvent, notify); - - return () => { - window.removeEventListener("storage", notify); - window.removeEventListener(authEmailChangeEvent, notify); - }; -} - const SourceImage = memo(function SourceImage({ endpoint, caption, @@ -4525,100 +4495,6 @@ function StagedAnswerResultSurface({ ); } -function AuthPanel() { - const { status, error, isConfigured, signInWithEmail, signOut, session } = useAuthSession(); - const savedEmail = useSyncExternalStore(subscribeAuthEmail, getAuthEmailSnapshot, getServerAuthEmailSnapshot); - const [draftEmail, setDraftEmail] = useState(null); - const email = draftEmail ?? savedEmail; - const busy = status === "loading"; - const isExpired = status === "expired"; - - async function submit(event: FormEvent) { - event.preventDefault(); - if (!email.trim()) return; - await signInWithEmail(email.trim()); - } - - if (!isConfigured) { - return ( -
-
- -
-

Real-data sign-in unavailable

-

- Configure the Supabase public URL and publishable key before using private documents. -

-
-
-
- ); - } - - if (status === "authenticated") { - return ( -
-
-
-

Signed in for private documents

-

{session?.user.email ?? "Authenticated session"}

-
- -
-
- ); - } - - return ( -
-
- -
-

- {isExpired ? "Sign-in link expired" : "Sign in for private documents"} -

-

- {isExpired - ? "Send a fresh link if this one failed or already timed out." - : "Real-data search, upload, and source previews require a Supabase Auth session."} -

-
-
- - - {error && ( -

- {error} -

- )} -
- ); -} - const tagQualityTone: Record = { noisy: toneDanger, duplicate: toneWarning, diff --git a/src/components/clinical-dashboard/auth-panel.tsx b/src/components/clinical-dashboard/auth-panel.tsx new file mode 100644 index 0000000000..c78fc806d3 --- /dev/null +++ b/src/components/clinical-dashboard/auth-panel.tsx @@ -0,0 +1,138 @@ +"use client"; + +import { type FormEvent, useState, useSyncExternalStore } from "react"; +import { Loader2, LogIn, LogOut, Mail, ShieldAlert } from "lucide-react"; + +import { AUTH_EMAIL_STORAGE_KEY, useAuthSession } from "@/lib/supabase/client"; +import { + cn, + fieldControlWithIcon, + fieldIcon, + fieldLabel, + floatingControl, + panelSubtle, + primaryControl, + textMuted, +} from "@/components/ui-primitives"; + +const authEmailChangeEvent = "clinical-kb-auth-email-change"; + +function getAuthEmailSnapshot() { + if (typeof window === "undefined") return ""; + try { + return window.localStorage.getItem(AUTH_EMAIL_STORAGE_KEY) ?? ""; + } catch { + return ""; + } +} + +function getServerAuthEmailSnapshot() { + return ""; +} + +function subscribeAuthEmail(onStoreChange: () => void) { + if (typeof window === "undefined") return () => undefined; + const notify = () => onStoreChange(); + + window.addEventListener("storage", notify); + window.addEventListener(authEmailChangeEvent, notify); + + return () => { + window.removeEventListener("storage", notify); + window.removeEventListener(authEmailChangeEvent, notify); + }; +} + +export function AuthPanel() { + const { status, error, isConfigured, signInWithEmail, signOut, session } = useAuthSession(); + const savedEmail = useSyncExternalStore(subscribeAuthEmail, getAuthEmailSnapshot, getServerAuthEmailSnapshot); + const [draftEmail, setDraftEmail] = useState(null); + const email = draftEmail ?? savedEmail; + const busy = status === "loading"; + const isExpired = status === "expired"; + + async function submit(event: FormEvent) { + event.preventDefault(); + if (!email.trim()) return; + await signInWithEmail(email.trim()); + } + + if (!isConfigured) { + return ( +
+
+ +
+

Real-data sign-in unavailable

+

+ Configure the Supabase public URL and publishable key before using private documents. +

+
+
+
+ ); + } + + if (status === "authenticated") { + return ( +
+
+
+

Signed in for private documents

+

{session?.user.email ?? "Authenticated session"}

+
+ +
+
+ ); + } + + return ( +
+
+ +
+

+ {isExpired ? "Sign-in link expired" : "Sign in for private documents"} +

+

+ {isExpired + ? "Send a fresh link if this one failed or already timed out." + : "Real-data search, upload, and source previews require a Supabase Auth session."} +

+
+
+ + + {error && ( +

+ {error} +

+ )} +
+ ); +}