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
1 change: 1 addition & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -623,3 +623,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-19 | origin/main 24-hour merged window (d1937d78e..ef042cacd, ~30 PRs incl. #853/#859/#861/#865/#868/#871–#874/#879/#885–#888/#890–#894/#896) | ef042cacd | integrated post-merge regression audit of the full 24h window (code review + design/performance/defect angles) | No P0/P1 regression found at the merged tip. One confirmed P2: the new settings surface (`use-app-preferences.ts` + `settings-dialog.tsx`) presents jurisdiction, population, answer-style, landing, home-content, compact-citations, and all notification preferences as live controls, but no consumer reads them — only density/motion (html attributes + globals.css) and theme are functional; a clinician selecting "Conservative" answer style reasonably but wrongly believes generation changed. One plausible P2/P3: `AuthProvider.initializeSession` now requires a live `getUser()` round-trip, so a transient network failure on load resolves a valid stored session to signed_out (INITIAL_SESSION replay is also skipped); the deliberate stale/tampered-token defense does not distinguish retryable network errors. One P3: `/medications` legacy redirect drops the query string while `/applications` and `/differentials/presentations` preserve theirs. Cleared after inspection: worker image-placement dedupe (key symmetric on both sides), title-word purge/scope migration (matches its replayed review), RPC-layer-only cancellation consolidation (intentional per PR #861), services route-chunk fix intact with no other heavy client value-imports (type-only imports verified), forced-colors ButtonFace/ButtonText flip, codex-autofix/pr-policy workflow changes conform to AGENTS.md (pin retained, rename-aware routing, workflow_sha checkout), audit-metadata allowlist exhaustive over the closed AuditAction union, answer-stream merged abort signals. Environment note (not a repo defect): the session-start hook skips `npm install` when node_modules exists, so this window's dependency bumps left the container stale and `verify:cheap` failed at typecheck until `npm ci`; the hook should compare a lockfile hash. | `npm ci` then typecheck clean and full Vitest 2828 passed / 1 failed / 2 skipped — the sole failure is the long-baselined container-only `tests/pdf-extraction-budget.test.ts` artifact (hosted-CI-green through #826/#835/#872/#890). First `verify:cheap` run passed every static guard (runtime, actions-pin, ci-scope, ci-triage, pr-policy, sitemap, brand, type-scale, icon-scale, function-grants, owner-scope, lint) before the stale-deps typecheck stop. No OpenAI, Supabase, deployment, or provider-backed check ran. |
| 2026-07-19 | origin/main foreign merges post-#896 landing (541da7b #871 remediation + f4557ca #892 policy parsing; explicit user review request) | ef042ca6a34d33862936e77e4b71068978382c1e | Bug review of recent main changes (diff-review protocol) | One P2 confirmed and fixed in PR #905: #892's widened heading matcher ended a required PR-body section at ANY next heading, so `###` sub-structure inside `## Verification` truncated the section and false-rejected valid bodies (fail-closed; repo template unaffected; repro via direct evaluatePullRequestPolicy probe old-vs-new). No P0/P1. Cleared after verification: `src/lib/client-env.ts` (no env leakage; production demote-to-demo removal deliberate and fail-safe — upload gating still locked via canUsePrivateApis), applications/medications redirect routes (fixed targets, 307+HEAD alias, no open-redirect/header-injection), test infra (no weakened assertions; route-coverage spec added to all projects), and the three biggest #871 UI diffs read inline (ClinicalDashboard upload tablist roving-tabindex + hydration-safe useSyncExternalStore role switch; visual-evidence unavailable-source rows became real non-interactive elements; favourites library demo-gates prototype items with sound menu keyboard nav). Residual (report-only): pr-policy `section()` remains fence-unaware (headings inside fenced code can satisfy required-section checks — pre-existing class, author-controlled attestation surface); dev-only Turbopack persistent-cache staleness served an old globals.css compile across restarts twice this session (fixed by setting `.next` aside). | Reviewer fan-out: general lane completed with concrete probes (pr-policy self-test, node repro on old parser from f4557ca^, adversarial body probes); UI lane agent lost to session limit and re-done inline on the three biggest diffs, leaning on the merged tree's green gates (verify:ui 236-passed run in this session covers #871's own new specs). No provider-backed checks run. |
| 2026-07-19 | claude/clinical-kb-pwa-review-asi3wb (PR #905; commits b2afe66 visuals + 6531178 policy + this ledger follow-up) | 6531178c1a3427dfd58c4bfcf8e29020c5731179 | PWA install/update notice redesign (all breakpoints) + review-follow-up policy fix | Redesigned the five PWA notices (install, update, iOS hint, offline, restored) as glass lux cards: per-type semantic icon tiles, heading-ink titles, corner dismiss buttons, reduced-motion-safe 280ms entrance. Deliberate placement per screen size: phones keep the bottom card above the fixed composer (thumb zone, safe areas); ≥640px floats a 25rem card bottom-right; ≥1280px moves the stack to a top-right toast under the header (same 4.25rem+safe-area offset constant as the mode-menu popover) with the animation direction flipped. Copy, roles, and button names unchanged. Plus the outline-aware pr-policy section parser fix from the same-session review (see the review row above). | Focused vitest pwa-lifecycle.dom 9/9 + pwa-manifest 8/8; `check:pr-policy` self-test green incl. new sub-heading case; `verify:cheap` 2828/2831 (sole fail = known container-only pdf-extraction-budget artifact); `test:e2e:pwa` privacy/offline green (installability fail = known container `in-incognito` artifact); `verify:ui` 236 passed/2 failed (the two long-baselined container artifacts); production build + client-bundle secret scan + bundle budget within tolerance (1293.4 vs 1278.6 KiB baseline); visual evidence at 390/768/1440 light+dark+offline in session scratchpad. Dev caveat recorded: Turbopack persistent `.next` cache served stale globals.css across restarts twice; fixed by setting the cache aside. No provider-backed checks run. |
| 2026-07-19 | claude/audit-recent-changes-kde66i (audit-remediation follow-up to the ef042cacd audit row) | see PR head | remediation of the three 2026-07-19 audit findings | Fixed all three findings from the 24-hour merged-window audit. (1) Inert settings honesty (P2): every preference the app does not yet consume — jurisdiction, population, answer style, landing, both home-content toggles, compact citations, and all three notification toggles — now renders an explicit "Saved for later — not active yet" marker in `settings-dialog.tsx`; the functional appearance/density/motion rows stay unmarked. Wiring the preferences into answer generation was deliberately NOT done here (clinical-behavior change requiring governance review); the new dom test documents the contract for flipping a control live. (2) Auth offline resolution (P2/P3): `resolveInitialAuthState` gains `verificationUnavailable`, set from `isAuthRetryableFetchError(getUser().error)`, so an unreachable auth server keeps the stored session signed in while a reachable server that rejects the token still resolves signed_out; server-side bearer validation is unchanged. (3) `/medications` redirect (P3): now preserves the sanitized q/focus/run search context with the same allowlist as the root legacy-mode redirect. | Focused Vitest 30/30 across `settings-inert-preferences.dom` (new), `private-client-auth` (3 new cases), `audit-navigation-auth-regressions` (query-preservation case), `app-preferences`, and `site-map`. Full `verify:cheap` run recorded on the PR. No OpenAI, Supabase, deployment, or provider-backed check ran. |
14 changes: 12 additions & 2 deletions src/app/medications/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
export function GET() {
import { type NextRequest } from "next/server";

// Legacy prescribing home. Preserve the search context the same way the root
// legacy-mode redirect does (q plus the focus/run flags, sanitized) so a
// bookmarked /medications?q=lithium link keeps its query after the redirect.
export function GET(request: NextRequest) {
const params = new URLSearchParams({ mode: "prescribing" });
const query = request.nextUrl.searchParams.get("q")?.trim();
if (query) params.set("q", query);
if (request.nextUrl.searchParams.get("focus") === "1") params.set("focus", "1");
if (request.nextUrl.searchParams.get("run") === "1") params.set("run", "1");
return new Response(null, {
status: 307,
headers: { Location: "/?mode=prescribing" },
headers: { Location: `/?${params.toString()}` },
});
}

Expand Down
61 changes: 60 additions & 1 deletion src/components/clinical-dashboard/settings-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -517,10 +517,12 @@ export function SettingsDialog({
icon={Globe2}
label="Jurisdiction"
description="Prioritises guidance relevant to your region."
notYetActive
htmlFor="settings-jurisdiction"
>
<SettingsSelect
id="settings-jurisdiction"
describedBy={notYetActiveId("settings-jurisdiction")}
value={preferences.jurisdiction}
onChange={(value) => setPreference("jurisdiction", value)}
options={JURISDICTION_OPTIONS}
Expand All @@ -530,10 +532,12 @@ export function SettingsDialog({
icon={CircleUserRound}
label="Default population"
description="Frames answers for your usual patient group."
notYetActive
htmlFor="settings-population"
>
<SettingsSelect
id="settings-population"
describedBy={notYetActiveId("settings-population")}
value={preferences.population}
onChange={(value) => setPreference("population", value)}
options={POPULATION_OPTIONS}
Expand All @@ -545,11 +549,13 @@ export function SettingsDialog({
description={
ANSWER_STYLE_OPTIONS.find((option) => option.value === preferences.answerStyle)?.description
}
notYetActive
labelId="settings-answer-style-label"
stacked
>
<SegmentedControl
ariaLabelledBy="settings-answer-style-label"
ariaDescribedBy={notYetActiveId("settings-answer-style-label")}
value={preferences.answerStyle}
onChange={(value) => setPreference("answerStyle", value)}
options={ANSWER_STYLE_OPTIONS}
Expand Down Expand Up @@ -593,11 +599,13 @@ export function SettingsDialog({
icon={PanelTop}
label="Default landing view"
description="The mode shown when you open the app."
notYetActive
labelId="settings-landing-label"
stacked
>
<SegmentedControl
ariaLabelledBy="settings-landing-label"
ariaDescribedBy={notYetActiveId("settings-landing-label")}
value={preferences.landing}
onChange={(value) => setPreference("landing", value)}
options={LANDING_OPTIONS}
Expand All @@ -618,20 +626,23 @@ export function SettingsDialog({
<SettingsGroup>
<SettingsToggleField
icon={PanelTop}
notYetActive
label="Recent searches on home"
description="Surface your latest questions when you land."
checked={preferences.showRecentOnHome}
onChange={(checked) => setPreference("showRecentOnHome", checked)}
/>
<SettingsToggleField
icon={Sparkles}
notYetActive
label="Saved protocols on home"
description="Keep pinned protocols within easy reach."
checked={preferences.showProtocolsOnHome}
onChange={(checked) => setPreference("showProtocolsOnHome", checked)}
/>
<SettingsToggleField
icon={BookOpen}
notYetActive
label="Compact citations"
description="Show tighter inline source references."
checked={preferences.compactCitations}
Expand All @@ -645,20 +656,23 @@ export function SettingsDialog({
<SettingsGroup>
<SettingsToggleField
icon={Stethoscope}
notYetActive
label="Guideline updates"
description="When source guidance you rely on changes."
checked={preferences.notifyGuidelineUpdates}
onChange={(checked) => setPreference("notifyGuidelineUpdates", checked)}
/>
<SettingsToggleField
icon={Sparkles}
notYetActive
label="Product news"
description="Occasional updates about new features."
checked={preferences.notifyProductNews}
onChange={(checked) => setPreference("notifyProductNews", checked)}
/>
<SettingsToggleField
icon={Bell}
notYetActive
label="Saved item changes"
description="Alerts about items you have saved."
checked={preferences.notifySavedChanges}
Expand Down Expand Up @@ -780,6 +794,29 @@ function IconBadge({ icon: Icon }: { icon: LucideIcon }) {
);
}

/**
* Honesty marker for preference controls that persist a choice but are not yet
* consumed anywhere in the app (audit 2026-07-19 P2: inert settings presented as
* live). Remove the marker from a control only when something actually reads its
* preference and changes behavior. The badge carries an id so the control it
* describes can reference it via `aria-describedby` — the marker must be
* announced to assistive tech, not just rendered visually.
*/
function notYetActiveId(anchor: string) {
return `${anchor}-not-yet-active`;
}

function NotYetActiveBadge({ id }: { id?: string }) {
return (
<span
id={id}
className="mt-1 inline-flex w-fit items-center rounded-full border border-[color:var(--border)] bg-[color:var(--surface-inset)] px-2 py-0.5 text-xs font-semibold leading-4 text-[color:var(--text-muted)]"
>
Saved for later — not active yet
</span>
);
}

function SettingsField({
icon,
label,
Expand All @@ -788,6 +825,7 @@ function SettingsField({
htmlFor,
labelId,
stacked = false,
notYetActive = false,
children,
}: {
icon: LucideIcon;
Expand All @@ -797,6 +835,7 @@ function SettingsField({
htmlFor?: string;
labelId?: string;
stacked?: boolean;
notYetActive?: boolean;
children?: ReactNode;
}) {
const LabelTag = htmlFor ? "label" : "span";
Expand All @@ -821,6 +860,9 @@ function SettingsField({
{description ? (
<p className="mt-0.5 text-xs font-medium leading-5 text-[color:var(--text-muted)]">{description}</p>
) : null}
{notYetActive ? (
<NotYetActiveBadge id={notYetActiveId(htmlFor ?? labelId ?? settingsRowTestId(label))} />
) : null}
</div>
</div>
{children ? (
Expand All @@ -839,16 +881,19 @@ function SegmentedControl<T extends string>({
onChange,
options,
ariaLabelledBy,
ariaDescribedBy,
}: {
value: T;
onChange: (value: T) => void;
options: ReadonlyArray<{ value: T; label: string; icon?: LucideIcon }>;
ariaLabelledBy?: string;
ariaDescribedBy?: string;
}) {
return (
<div
role="radiogroup"
aria-labelledby={ariaLabelledBy}
aria-describedby={ariaDescribedBy}
// Segments size to their content and wrap onto a second row on narrow
// screens rather than truncating long labels ("Comprehensive"); each row's
// items grow to fill the width so the control still reads as a unit.
Expand Down Expand Up @@ -885,17 +930,20 @@ function SettingsSelect<T extends string>({
value,
onChange,
options,
describedBy,
}: {
id: string;
value: T;
onChange: (value: T) => void;
options: ReadonlyArray<{ value: T; label: string }>;
describedBy?: string;
}) {
return (
<div className="relative min-[420px]:w-56">
<select
id={id}
value={value}
aria-describedby={describedBy}
onChange={(event) => onChange(event.target.value as T)}
className="w-full appearance-none rounded-lg border border-[color:var(--border)] bg-[color:var(--surface-raised)] py-2 pl-3 pr-9 text-sm font-semibold text-[color:var(--text-heading)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
>
Expand All @@ -919,12 +967,14 @@ function SettingsToggleField({
description,
checked,
onChange,
notYetActive = false,
}: {
icon: LucideIcon;
label: string;
description?: string;
checked: boolean;
onChange: (checked: boolean) => void;
notYetActive?: boolean;
}) {
return (
<div
Expand All @@ -938,9 +988,15 @@ function SettingsToggleField({
{description ? (
<p className="mt-0.5 text-xs font-medium leading-5 text-[color:var(--text-muted)]">{description}</p>
) : null}
{notYetActive ? <NotYetActiveBadge id={notYetActiveId(settingsRowTestId(label))} /> : null}
</div>
</div>
<Switch checked={checked} onChange={onChange} ariaLabel={label} />
<Switch
checked={checked}
onChange={onChange}
ariaLabel={label}
describedBy={notYetActive ? notYetActiveId(settingsRowTestId(label)) : undefined}
/>
</div>
);
}
Expand All @@ -949,17 +1005,20 @@ function Switch({
checked,
onChange,
ariaLabel,
describedBy,
}: {
checked: boolean;
onChange: (checked: boolean) => void;
ariaLabel: string;
describedBy?: string;
}) {
return (
<button
type="button"
role="switch"
aria-checked={checked}
aria-label={ariaLabel}
aria-describedby={describedBy}
onClick={() => onChange(!checked)}
className={cn(
"relative inline-flex h-6 w-tap shrink-0 items-center rounded-full border transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]",
Expand Down
Loading