diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md
index 185d036a2..4583a93dd 100644
--- a/docs/branch-review-ledger.md
+++ b/docs/branch-review-ledger.md
@@ -622,5 +622,7 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-19 | all remote feature branches and registered worktrees against `origin/main` through PR #899 | 8242fa63d5f5b79fc770c9ae4f633e3a784b80e1 | branch/worktree cleanup, useful-work recovery, and protected-main merge closure | Deleted 122 stale or closed remote feature refs with exact SHA leases; four additional merged PR branches were removed by the protected-main PR workflow. Removed 32 obsolete, superseded, or merge-proven worktree registrations. Recovered useful dirty RAG work into PR #901 (deterministic and opt-in semantic reranking) and PR #902 (retrieval phase latency telemetry), preserved follow-up decisions in `docs/process-hardening.md`, and recovered four missing historical review rows. PRs #897, #899, #901, and #902 are merged with green exact-head checks and zero unresolved review threads. A detached full-repo-review worktree is deliberately retained because its ownership/activity could not be safely disproved; one unregistered `node_modules` junction residue is also retained because deletion was denied by local safety policy. | Fresh fetch/prune; full GitHub PR/check/thread inventory; `git worktree list --porcelain`; cherry-pick-aware right-only logs; exact leased remote deletes; exact-old-value local ref deletes; clean-worktree, path, and merged-PR proof before every removal. PR #899 local proof: focused Vitest 31/31, changed-file ESLint, `verify:cheap` 317 files / 2,879 tests, and `verify:ui` 239/239; exact-head hosted checks all passed. PR #901 local proof: `verify:cheap` 316 files / 2,870 tests; PR #902 focused Vitest 8/8 plus ESLint and typecheck. No OpenAI, Supabase, live clinical, deployment, or production-data workflow ran; provider-backed semantic canary evaluation remains approval-gated. |
| 2026-07-19 | main / `codex/supabase-database-review` | 4034d2e60ebb6616130ff17bf3cb69368f36f8f6 + reviewed working diff | live `Clinical KB Database` security, migration, schema-drift, integrity, and performance review against current repo | Confirmed and remediated a P1 privacy defect: 601 private-document title-vocabulary rows were reachable by the service-role query corrector; the live public-only sync/backfill now reports zero private or out-of-scope rows. Applied the committed retrieval-count bound, audit-metadata minimization, registry cleanup/index, public-title corrector, and atomic summary-rate-limit migrations. The missing FK and registry indexes are present and no invalid indexes remain. A second P1 was found in the untracked live `ingestion-worker`: gateway JWT verification accepted any project JWT before privileged direct-Postgres job processing. Recovered the deployed source into the repo, restricted it to POST plus a gateway-verified `service_role` claim, expanded the Deno checker to every tracked Edge Function, and deployed exact-matching v13 with JWT verification enabled. Review also exposed a repo mirror/test gap: the count-clamp migration was not reflected in `schema.sql`; the branch now mirrors it and locks both sources in the focused test. Remaining hosted blocker: `postgres` cannot assume managed `supabase_admin`, so the fail-closed default-ACL migrations and final title-word constraint/trigger migration remain unapplied; the intentional service-role-only table still produces one INFO no-policy advisor. | Supabase connector project identity, migration and Edge Function inventory, full drift snapshot comparison, security/performance advisors, catalog integrity/ACL/index queries, Vault JWT-role compatibility check, post-apply invariants, exact deployed-source hashes, and unauthenticated live rejection (401); focused retrieval/schema/drift Vitest 82/83 with only manifest freshness failing; Edge/retrieval auth 9/9; Deno check for both functions; offline RAG 36 cases / 294 tests; function-grant guard; scoped ESLint, Prettier, and `git diff --check`. `check:supabase-project` was attempted but stopped before provider contact because local project env vars are unset. `drift:manifest` was blocked because Docker Desktop could not start and was cleaned up. `verify:cheap`, `verify:pr-local`, production-readiness, OpenAI, hosted CI, broader deployment, and commit/push were not run. |
| 2026-07-19 | cursor/safari-edge-to-edge-f46b (PR #933) | 15061964dd2fdf9665f72b7282f5cc81c736e57f | final Safari edge-to-edge / phone dock reserve review + merge readiness | No high-confidence P0-P1. Confirmed implementation: shared reserve module collapses to 0.75rem when dock hides; shell uses block scrollport + inner mobile-composer-reserve-pad so clearance contributes to scrollHeight; child dock-sized env(safe-area) pads removed; DocumentViewer owns its dock pad. Review polish: formulation/specifier max-sm:min-h-0 alignment, document-route ownership simplification, hidden-pad CSS token guard. Residual P2/P3 only: differentials compare zero-inset backdrop margin, idle 2rem vs max(2rem,safe-area) ~2px, unused-looking #main-content padding transition still needed by ClinicalDashboard. Merge-ready. | Local: format/lint/typecheck/knip/budgets pass; unit 2952 passed with only pre-existing pdf-extraction-budget (python ENOENT, also fails on clean main); production build + client-bundle secret scan pass; focused Chromium composer suite 6/6 (forms hide, tablet/desktop clearance, differentials compare, service-detail endpoint, document-viewer hide, long-answer dock). Hosted CI on prior head fully green including Production UI; polish head re-checked before merge. No OpenAI/live Supabase/provider calls. |
+| 2026-07-19 | cursor/hide-favourites-guest-mode-a26d (PR #934) | 23d05539320427ea3f994eaec3c2e95daa481492 | Favourites guest mode-switcher/sidebar auth gate review | No P0/P1. Primary request correctly wired: MasterSearchHeader fails closed; ClinicalDashboard and global-search-shell both pass session favouritesAccessible to mode menu + desktop/mobile showAccountLibrary; selectSearchMode/changeMode guarded; /favourites stays on-page with signup for guests. Residual P2: guest discovery still possible via composer cross-mode Favourites chips and Tools catalog Saved workflows (content remains gated on /favourites). P2 test gap: wiring tests are source-string, not MasterSearchHeader DOM. P3: header passes precomputed access as authenticated plus demoMode (redundant OR, intentional demo reopen). | Focused Vitest favourites-auth-gate + app-modes 26/26; prior verify:cheap green on branch; browser guest check: sidebar omits Your library/Favourites, /favourites shows signup gate; mode-menu automation inconclusive. No OpenAI/Supabase/hosted CI. |
+| 2026-07-19 | cursor/hide-favourites-guest-mode-a26d (PR #934) | 04192653 | Final Favourites total guest-blackout review + merge readiness | No P0/P1. Fixed residual P2s: rankToolRecords now fails closed by default; mode/chip filters stop double-ORing demoMode; gated deep-link mode chrome falls back to Answer; MasterSearchHeader mode-menu DOM tests added. Guest discovery closed for mode menu, sidebar, cross-mode chips, Tools Saved, universal tools ranking, also-matches, prefetch. Intentional residual: direct /favourites signup gate; mockup routes. PR body appears overwritten by unrelated Sync PR policy content โ agent posted corrective comment (403 on body edit). | Focused Vitest 37/37; verify:cheap green; verify:pr-local green (build + client-secret scan + RAG fixtures); browser guest Tools/sidebar/favourites proof earlier. No OpenAI/Supabase writes. |
| 2026-07-19 | cursor/pr-policy-body-cleanup-f46b (PR #942) + PR #933 closeout | 7c8e6aadf0890b143372fb96f13d9de47a416db9 | post-merge CI triage for #933 PR-policy red check | PR #933 product merge (`bd864de0`) already on main with green post-merge main CI (Static/Unit/Build/Production UI/SAST/Docker). Sole remaining red check on #933 was post-ready PR policy against a stale synced body with unchecked governance boxes (from leftover `PR_POLICY_BODY.md` introduced by #932). Token cannot edit merged PR bodies (403). Removed the stale template via #942 so Sync PR policy body no longer reapplies unchecked governance. Local composer regression 6/6 on main; reserve unit 11/11. No product regression. | Hosted #933 pre-merge + main push green; #942 required checks green then squash-merged; focused Chromium composer 6/6; reserve Vitest 11/11. No OpenAI/Supabase provider calls. |
| 2026-07-19 | cursor/documents-search-header-3eab / PR #936 | a7feaa3033180b672cfafaaaf75dc75088ebf052 | documents search header redesign final review + merge readiness | No remaining high-confidence P0-P1. Implemented identity-first results chrome, unified Sort/type-filter/Library toolbar, removed documents Also-in-library strip, relocated ScopeAndGovernanceNotice under controls, fixed Prettier CI failure and memo-busting empty warnings default, synced accurate PR policy body then removed the stale leftover, and repeatedly merged origin/main so squash auto-merge is not blocked behind/dirty. Hosted required checks including Production UI passed on the integrated head. | Local: typecheck/lint/format; focused Playwright documents `@critical` + deferred source/admin + forms sort persistence; design-system/icon-scale/maintainability; build + RAG fixtures; verify:pr-local units with known pdf-extraction-budget env artifact also on clean main. Hosted: PR policy, Static, Unit, Build, Production UI, Advisory UI, PR required green. No OpenAI/live Supabase writes. |
diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx
index cea973079..1b2faa658 100644
--- a/src/components/ClinicalDashboard.tsx
+++ b/src/components/ClinicalDashboard.tsx
@@ -95,6 +95,8 @@ import {
resolveMobileComposerReserve,
} from "@/components/clinical-dashboard/mobile-composer-reserve";
import { UniversalSearchAlsoMatches } from "@/components/clinical-dashboard/universal-search-also-matches";
+import { FavouritesGuestGate } from "@/components/clinical-dashboard/favourites-guest-gate";
+import { useDashboardShellActions } from "@/components/clinical-dashboard/use-dashboard-shell-actions";
import { useScrollHideReporter } from "@/components/clinical-dashboard/use-hide-on-scroll";
import { SearchCommandProvider } from "@/components/clinical-dashboard/search-command-context";
import {
@@ -110,7 +112,6 @@ import {
setupNeedsSlowRecheck,
setupRecheckPollMs,
shorterPollDelay,
- sessionFavouritesAccessible,
} from "@/components/clinical-dashboard/clinical-dashboard-helpers";
import { answerRecovery, errorCopy } from "@/lib/ui-copy";
import {
@@ -675,7 +676,6 @@ export function ClinicalDashboard({
const [activeHash, setActiveHash] = useState("#search");
const [guideOpen, setGuideOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
- const [accountSetupOpen, setAccountSetupOpen] = useState(false);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [sidebarCollapsed, setSidebarCollapsed] = useSidebarCollapsed();
const [documentsDrawerOpen, setDocumentsDrawerOpen] = useState(false);
@@ -814,6 +814,31 @@ export function ClinicalDashboard({
authUnavailableFallback: browserAuthUnavailableDemoFallback,
localNoAuthMode,
});
+ const sidebarIdentity = useMemo(() => deriveSidebarIdentity(auth.session?.user.email), [auth.session?.user.email]);
+ const {
+ favouritesAccessible,
+ accountSetupOpen,
+ accountSetupIntent,
+ closeAccountSetup,
+ closeTransientSurfaces: closeDashboardTransientSurfaces,
+ openAccountSetup,
+ openGuide,
+ closeGuide,
+ openSettings,
+ closeSettings,
+ openAccountProfile,
+ prefetchApplications,
+ } = useDashboardShellActions({
+ authenticated: auth.status === "authenticated",
+ demoMode: clientDemoMode,
+ signedIn: sidebarIdentity.signedIn,
+ setGuideOpen,
+ setSettingsOpen,
+ setMobileSidebarOpen,
+ setDocumentsDrawerOpen,
+ setUploadDrawerOpen,
+ prefetch: (href) => router.prefetch(href),
+ });
const answerThreadOwnerId = auth.session?.user.id ?? (clientDemoMode ? demoRecentQueryOwnerId : null);
const previousAnswerThreadOwnerIdRef = useRef(answerThreadOwnerId);
useEffect(() => {
@@ -888,43 +913,6 @@ export function ClinicalDashboard({
canUseDegradedLocalSearchApis ||
canUseNonProductionDemoFallback ||
canAttemptDeployedPublicSearch;
- const closeDashboardTransientSurfaces = useCallback(
- (except?: "guide" | "settings" | "accountSetup" | "mobileSidebar" | "documents" | "upload") => {
- if (except !== "guide") setGuideOpen(false);
- if (except !== "settings") setSettingsOpen(false);
- if (except !== "accountSetup") setAccountSetupOpen(false);
- if (except !== "mobileSidebar") setMobileSidebarOpen(false);
- if (except !== "documents") setDocumentsDrawerOpen(false);
- if (except !== "upload") setUploadDrawerOpen(false);
- },
- [],
- );
- const openGuide = useCallback(() => {
- closeDashboardTransientSurfaces("guide");
- setGuideOpen(true);
- }, [closeDashboardTransientSurfaces]);
- const closeGuide = useCallback(() => setGuideOpen(false), []);
- const openSettings = useCallback(() => {
- closeDashboardTransientSurfaces("settings");
- setSettingsOpen(true);
- }, [closeDashboardTransientSurfaces]);
- const closeSettings = useCallback(() => setSettingsOpen(false), []);
- const sidebarIdentity = useMemo(() => deriveSidebarIdentity(auth.session?.user.email), [auth.session?.user.email]);
- const openAccountProfile = useCallback(() => {
- if (sidebarIdentity.signedIn) {
- closeDashboardTransientSurfaces("settings");
- setSettingsOpen(true);
- return;
- }
- closeDashboardTransientSurfaces("accountSetup");
- setAccountSetupOpen(true);
- }, [closeDashboardTransientSurfaces, sidebarIdentity.signedIn]);
- const closeAccountSetup = useCallback(() => setAccountSetupOpen(false), []);
- const prefetchApplications = useCallback(() => {
- router.prefetch("/?mode=tools");
- router.prefetch("/favourites");
- router.prefetch("/differentials");
- }, [router]);
const openLibraryHealthTarget = useCallback(
(target: LibraryHealthTarget) => {
if (!canUseAdministrativeApis) {
@@ -2482,6 +2470,10 @@ export function ClinicalDashboard({
}
function crossModeSearch(mode: AppModeId, crossQuery: string) {
+ if (mode === "favourites" && !favouritesAccessible) {
+ openAccountSetup("favourites");
+ return;
+ }
modeChangeFromUiRef.current = true;
if (mode === "differentials") clearDifferentialModeResultState();
setCommandScopes([]);
@@ -2763,6 +2755,10 @@ export function ClinicalDashboard({
}
function selectSearchMode(mode: AppModeId) {
+ if (mode === "favourites" && !favouritesAccessible) {
+ openAccountSetup("favourites");
+ return;
+ }
modeChangeFromUiRef.current = true;
if (mode === "differentials") clearDifferentialModeResultState();
setQuery("");
@@ -3476,7 +3472,7 @@ export function ClinicalDashboard({
theme={theme}
onToggleTheme={toggleTheme}
onPrefetchApplications={prefetchApplications}
- showAccountLibrary={sessionFavouritesAccessible(auth.status, clientDemoMode)}
+ showAccountLibrary={favouritesAccessible}
/>
@@ -3493,6 +3489,8 @@ export function ClinicalDashboard({
realDataReady={canRunSearch}
onQueryChange={setQuery}
onSearchModeChange={selectSearchMode}
+ canAccessFavourites={favouritesAccessible}
+ onRequestAccountSetup={() => openAccountSetup("favourites")}
onAsk={ask}
onClearQuery={() => {
setQuery("");
@@ -3848,7 +3846,7 @@ export function ClinicalDashboard({
/>
) : activeModeResultKind === "tools" ? (
- ) : activeModeResultKind === "favourites" ? (
+ ) : activeModeResultKind === "favourites" && favouritesAccessible ? (
+ ) : activeModeResultKind === "favourites" ? (
+
openAccountSetup("favourites")} />
) : activeModeResultKind === "documents" || activeModeResultKind === "services" ? (
searchMode === "prescribing" ? (
-
+
diff --git a/src/components/applications-launcher-page.tsx b/src/components/applications-launcher-page.tsx
index 71e52e1aa..7ecfd2e2a 100644
--- a/src/components/applications-launcher-page.tsx
+++ b/src/components/applications-launcher-page.tsx
@@ -26,11 +26,14 @@ import { type FormEvent, useMemo, useState } from "react";
import { ModeHomeHero, ModeHomeVerificationFooter } from "@/components/mode-home-template";
import { useSearchCommand } from "@/components/clinical-dashboard/search-command-context";
+import { useFavouritesAccess } from "@/components/clinical-dashboard/use-favourites-access";
import { cn, toneInfo, toneSuccess, toneWarning } from "@/components/ui-primitives";
import { Sheet } from "@/components/ui/sheet";
+import { isLocalNoAuthMode, resolveClientDemoMode } from "@/lib/client-env";
import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";
+import { useAuthSession } from "@/lib/supabase/client";
import {
- toolCatalogRecords,
+ toolCatalogRecordsForSession,
type ToolCatalogArea,
type ToolCatalogRecord,
type ToolCatalogStatus,
@@ -92,14 +95,19 @@ const launcherIconById: Record = {
favourites: Star,
};
-const launcherApps: LauncherApp[] = toolCatalogRecords.map((record) => ({
- ...record,
- icon: launcherIconById[record.id] ?? Sparkles,
-}));
+function launcherAppsForSession(canAccessFavourites: boolean): LauncherApp[] {
+ return toolCatalogRecordsForSession({
+ authenticated: canAccessFavourites,
+ demoMode: false,
+ }).map((record) => ({
+ ...record,
+ icon: launcherIconById[record.id] ?? Sparkles,
+ }));
+}
const toolsLauncherCopy = {
heading: "Tools",
- description: "Assessment, prescribing, documents, and saved work.",
+ description: "Assessment, prescribing, documents, and clinical workflows.",
allSectionLabel: "All tools",
countNoun: "tools",
emptyTitle: "No tools match",
@@ -109,7 +117,7 @@ const toolsLauncherCopy = {
openSelectedAriaLabel: "Open selected tool",
};
-const quickActions = [
+const quickActionsBase = [
{ label: "Ask", desktopLabel: "Ask evidence", icon: Search, id: "clinical-kb-search" },
{ label: "Compare", desktopLabel: "Compare", icon: Brain, id: "differentials" },
{ label: "Prescribe", desktopLabel: "Prescribe", icon: Pill, id: "medication-prescribing" },
@@ -120,7 +128,7 @@ const quickActions = [
{ label: "More", desktopLabel: "More", icon: Sparkles, id: "favourites" },
] as const;
-const desktopFilters: Array<{ id: LauncherFilter; label: string }> = [
+const desktopFiltersBase: Array<{ id: LauncherFilter; label: string }> = [
{ id: "all", label: "All tools" },
{ id: "assessment", label: "Assess" },
{ id: "reference", label: "Evidence" },
@@ -137,17 +145,18 @@ const mobileFilters: Array<{ id: LauncherFilter; label: string; hasMenu?: boolea
{ id: "more", label: "More", hasMenu: true },
];
-export const applicationsLauncherItemCount = launcherApps.length;
+/** Full catalog length (includes Favourites). Prefer session-filtered lists in UI. */
+export const applicationsLauncherItemCount = launcherAppsForSession(true).length;
-function appById(id: string) {
- return launcherApps.find((app) => app.id === id) ?? launcherApps[0];
+function appById(id: string, apps: LauncherApp[]) {
+ return apps.find((app) => app.id === id) ?? apps[0];
}
-function initialToolId(query: string | undefined) {
+function initialToolId(query: string | undefined, apps: LauncherApp[]) {
const normalized = query?.trim().toLowerCase();
if (!normalized) return "risk-safety";
return (
- launcherApps.find((app) =>
+ apps.find((app) =>
[app.title, app.mobileTitle, app.description, app.bestFor, app.detail, app.area, ...app.keywords]
.filter(Boolean)
.join(" ")
@@ -157,6 +166,14 @@ function initialToolId(query: string | undefined) {
);
}
+function quickActionsForSession(canAccessFavourites: boolean) {
+ return canAccessFavourites ? quickActionsBase : quickActionsBase.filter((action) => action.id !== "favourites");
+}
+
+function desktopFiltersForSession(canAccessFavourites: boolean) {
+ return canAccessFavourites ? desktopFiltersBase : desktopFiltersBase.filter((filter) => filter.id !== "saved");
+}
+
function appIconTone(app: LauncherApp) {
if (app.id === "risk-safety") return iconToneClasses.safety;
if (app.id === "medication-prescribing") return iconToneClasses.medication;
@@ -266,14 +283,25 @@ function ToolChips({ app, includeStatus = false }: { app: LauncherApp; includeSt
);
}
-function QuickActions({ onSelect, mobile }: { onSelect: (id: string) => void; mobile?: boolean }) {
+function QuickActions({
+ onSelect,
+ mobile,
+ apps,
+ canAccessFavourites,
+}: {
+ onSelect: (id: string) => void;
+ mobile?: boolean;
+ apps: LauncherApp[];
+ canAccessFavourites: boolean;
+}) {
+ const quickActions = quickActionsForSession(canAccessFavourites);
return (
{quickActions.slice(0, mobile ? 8 : 6).map((action) => {
- const app = appById(action.id);
+ const app = appById(action.id, apps);
const Icon = action.icon;
return (