From bab8b2ee737049d8f1dc47526eca5e4622c67805 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 1 Aug 2026 23:55:06 +0800 Subject: [PATCH 1/4] perf: keep document admin UI off initial dashboard --- docs/branch-review-ledger.md | 1 + src/components/ClinicalDashboard.tsx | 4 +- .../DocumentManagerPanel.tsx | 204 ++---------------- .../document-manager-contracts.ts | 63 ++++++ .../library-health-strip.tsx | 104 +++++++++ tests/client-performance-boundaries.test.ts | 3 + 6 files changed, 193 insertions(+), 186 deletions(-) create mode 100644 src/components/clinical-dashboard/document-manager-contracts.ts create mode 100644 src/components/clinical-dashboard/library-health-strip.tsx diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 614be0a351..edd7a5adc7 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -575,3 +575,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-07-31 | claude/top-search-design-mockups-w53znc | c09eb64918ae2956968d35f3253c405ed9b915d5 | therapy-compass filter sheet + documents review fixes (C2) | Therapy Compass's phone Topics/availability selects were faking multi-select (value pinned to "", literal check-prefix in option text, a Clear-filters action among the options); replaced with one sheet of aria-pressed toggles matching the wide viewport, in the tc- idiom. Also lands two CodeRabbit findings from merged PR #1542: sheet footer primary action to sm:min-h-12, and the dialog test now pins aria-expanded/aria-controls. Third finding (unmount skips focus restore) dispositioned no-change with a test pinning the scroll lock is not leaked. | verify:cheap exit 0 (458 files / 4785 tests); ui-accessibility.spec.ts 15 passed chromium incl. the rewritten Therapy Compass case; ui-smoke documents journey 1 passed chromium | | 2026-08-01 | claude/top-search-design-mockups-w53znc | 4f4440fd6776742f5de203ee15295f372205321d | search results bar: scope-system deletion, filter shelf, bar anatomy | Handoff for PR #1555. Deleted the inert command-scope system (voided props, six modes' scope config, three matchers, four no-op call sites, the original shelf) — behaviour-preserving because every matcher early-returned true on a permanently-empty array. Rebuilt the applied-filter shelf prop-driven on live facet data, scoped to documents and therapy-compass. Landed the bar anatomy: tile spinner and funnel states, Filter to the right edge, Sort inboard. Study step 6 (remove the library button) deliberately declined — the nav route clears the query via onSearchModeChange. Ledger #182 closed. | verify:pr-local exit 0 (460 files / 4796 tests, production build, client-bundle secret scan, RAG fixtures 36 cases / 23 suites); ui-tools 87 passed; ui-smoke + ui-accessibility 108 passed 1 failed (pre-existing PDF-canvas test, fails identically stashed, Chromium 1194 vs pinned 1228); mutation-tested the shelf's survives-loading guard | | 2026-08-01 | claude/sentry-agent-monitoring-eri94v | 86983f344b45e42310e9f167a5adb0a56e46ddb5 | pr-1551 | merge-ready-pending-ci: merged origin/main; fixed outstanding-issues blank-line/#183 orphan + renumbered npm row to #204; kept worker+wizard error-tracking sections; qodo claim-spam thread already fixed on prior tip and resolved | check:outstanding-issues pass; merge-tree clean vs origin/main; prior tip Static PR failed on outstanding-issues; push 86983f344 | +| 2026-08-01 | work | d4a4463172671cff3fd333feb685fa397e4f95d0 | latency, lazy loading, React and Next.js frontend performance | P2 eager document administration bundle boundary fixed; no P0/P1 findings | lint; typecheck; 463 files/4811 tests; production build; bundle budget | diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 8cafb75c60..2244941570 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -60,13 +60,13 @@ import { ClinicalMobileSidebar, } from "@/components/clinical-dashboard/ClinicalSidebar"; import { - LibraryHealthStrip, fallbackSetupChecks, hasReadyRequiredPublicSearchConfig, hasReadyPublicSearchSetup, type SetupCheck, type IngestionQualityReviewItem, -} from "@/components/clinical-dashboard/DocumentManagerPanel"; +} from "@/components/clinical-dashboard/document-manager-contracts"; +import { LibraryHealthStrip } from "@/components/clinical-dashboard/library-health-strip"; import { GuideDialog, GuideTrigger, UtilityDrawer } from "@/components/clinical-dashboard/dashboard-shell"; import { SystemNotice, DegradedNotice } from "@/components/clinical-dashboard/dashboard-notices"; import { sanitizeAnswerDisplayText, sanitizeDisplayText } from "@/components/clinical-dashboard/display-text"; diff --git a/src/components/clinical-dashboard/DocumentManagerPanel.tsx b/src/components/clinical-dashboard/DocumentManagerPanel.tsx index 3f7d81c4c2..ebd204b95e 100644 --- a/src/components/clinical-dashboard/DocumentManagerPanel.tsx +++ b/src/components/clinical-dashboard/DocumentManagerPanel.tsx @@ -22,100 +22,33 @@ import { emptyStates, errorCopy } from "@/lib/ui-copy"; import { exceedsClientUploadSize, getClientMaxUploadMb, uploadSizeLimitMessage } from "@/lib/upload-limits"; import { StatusBadge } from "@/components/clinical-dashboard/badges"; import { PrivacyInputNotice } from "@/components/privacy-input-notice"; -import type { ClinicalDocument, IngestionJob, ImportBatch } from "@/lib/types"; +import type { IngestionJob, ImportBatch } from "@/lib/types"; +import { + fallbackSetupChecks, + type IngestionQualityReviewItem, + type IngestionQualityReviewType, + type SetupCheck, + type SetupCheckStatus, +} from "@/components/clinical-dashboard/document-manager-contracts"; + +export { + fallbackSetupChecks, + hasReadyPublicSearchSetup, + hasReadyRequiredPublicSearchConfig, +} from "@/components/clinical-dashboard/document-manager-contracts"; +export type { + IngestionQualityReviewItem, + IngestionQualityReviewType, + SetupCheck, + SetupCheckStatus, +} from "@/components/clinical-dashboard/document-manager-contracts"; // Setup and quality types -export type SetupCheckStatus = "ready" | "needs_setup" | "unknown"; -export type SetupCheck = { - id: "env" | "project" | "schema" | "search" | "openai" | "worker"; - label: string; - status: SetupCheckStatus; - detail: string; -}; - const demoUploadReadOnlyMessage = "Demo mode is read-only. Configure Supabase, OpenAI, and the local worker before uploading private guideline files."; -export type LibraryHealthTarget = "documents" | "setup" | "indexing" | "failures"; export type IndexingMonitorFilter = "all" | "active" | "failed"; -export type IngestionQualityReviewType = - "failed_ocr" | "low_extraction_confidence" | "missing_tables" | "image_only_pages" | "failed_job" | "manual_review"; - -export type IngestionQualityReviewItem = { - id: string; - type: IngestionQualityReviewType; - severity: "danger" | "warning" | "info"; - title: string; - detail: string; - documentId: string; - documentTitle: string; - fileName: string; - jobId: string | null; - qualityScore: number | null; - extractionQuality: string | null; - reasons: string[]; - metrics: Record; - updatedAt: string | null; -}; - -export const fallbackSetupChecks: SetupCheck[] = [ - { - id: "env", - label: ".env.local configured", - status: "unknown", - detail: "Setup status has not loaded yet.", - }, - { - id: "project", - label: "Clinical KB Database target", - status: "unknown", - detail: "Setup status has not loaded yet.", - }, - { - id: "schema", - label: "supabase/schema.sql applied", - status: "unknown", - detail: "Setup status has not loaded yet.", - }, - { - id: "search", - label: "Search RPC and vector indexes", - status: "unknown", - detail: "Setup status has not loaded yet.", - }, - { - id: "openai", - label: "OpenAI API key available", - status: "unknown", - detail: "Setup status has not loaded yet.", - }, - { - id: "worker", - label: "npm run worker running", - status: "unknown", - detail: "Setup status has not loaded yet.", - }, -]; - -// OpenAI is intentionally excluded from both gates: browse/search only needs Supabase. -// The answer path validates OPENAI_API_KEY at request time (requireOpenAIEnv), so a -// missing key surfaces as a real API error there rather than blocking every mode here. -const publicSearchSetupCheckIds = new Set(["env", "project", "schema", "search"]); -const requiredPublicSearchConfigCheckIds = new Set(["env", "project", "schema"]); - -export function hasReadyPublicSearchSetup(checks: SetupCheck[]) { - return Array.from(publicSearchSetupCheckIds).every( - (id) => checks.find((check) => check.id === id)?.status === "ready", - ); -} - -export function hasReadyRequiredPublicSearchConfig(checks: SetupCheck[]) { - return Array.from(requiredPublicSearchConfigCheckIds).every( - (id) => checks.find((check) => check.id === id)?.status === "ready", - ); -} - function setupBadgeClasses(status: SetupCheckStatus) { if (status === "ready") { return toneSuccess; @@ -790,103 +723,6 @@ export function IngestionQualityConsole({ ); } -export function LibraryHealthStrip({ - documents, - jobs, - batches, - checks, - loading, - onSelectTarget, -}: { - documents: ClinicalDocument[]; - jobs: IngestionJob[]; - batches: ImportBatch[]; - checks: SetupCheck[]; - loading: boolean; - onSelectTarget?: (target: LibraryHealthTarget) => void; -}) { - const readyChecks = checks.filter((check) => check.status === "ready").length; - const indexedDocuments = documents.filter((document) => document.status === "indexed").length; - const activeJobs = jobs.filter((job) => job.status === "pending" || job.status === "processing").length; - const activeBatches = batches.filter((batch) => batch.status === "queued" || batch.status === "processing").length; - const failedWork = - jobs.filter((job) => job.status === "failed").length + batches.filter((batch) => batch.status === "failed").length; - const items = [ - { - target: "documents" as const, - label: "Documents", - value: loading ? "" : `${indexedDocuments} indexed`, - tone: loading ? toneNeutral : indexedDocuments ? toneSuccess : toneWarning, - actionLabel: "Show indexed document files", - }, - { - target: "setup" as const, - label: "Setup", - value: loading ? "" : `${readyChecks}/${checks.length || fallbackSetupChecks.length} ready`, - tone: loading - ? toneNeutral - : readyChecks === (checks.length || fallbackSetupChecks.length) - ? toneSuccess - : toneWarning, - actionLabel: "Show setup checks", - }, - { - target: "indexing" as const, - label: "Indexing", - value: loading ? "" : activeJobs + activeBatches ? `${activeJobs + activeBatches} active` : "Idle", - tone: loading ? toneNeutral : activeJobs + activeBatches ? toneInfo : toneNeutral, - actionLabel: "Show indexing progress", - }, - { - target: "failures" as const, - label: "Failures", - value: loading ? "" : failedWork ? `${failedWork} needs review` : "None", - tone: loading ? toneNeutral : failedWork ? toneDanger : toneNeutral, - actionLabel: "Show failed indexing work", - }, - ]; - - return ( -
-
-

Library health

- Read-only status -
-
- {items.map((item) => ( - - ))} -
-
- ); -} - function indexingWorkMatchesFilter(item: Pick, filter: IndexingMonitorFilter) { if (filter === "all") return true; if (filter === "active") return item.status === "pending" || item.status === "processing" || item.status === "queued"; diff --git a/src/components/clinical-dashboard/document-manager-contracts.ts b/src/components/clinical-dashboard/document-manager-contracts.ts new file mode 100644 index 0000000000..626faf236a --- /dev/null +++ b/src/components/clinical-dashboard/document-manager-contracts.ts @@ -0,0 +1,63 @@ +// Kept in a dependency-free module so the initial dashboard does not pull the +// complete document administration UI into its eager client chunk. +export type SetupCheckStatus = "ready" | "needs_setup" | "unknown"; +export type SetupCheck = { + id: "env" | "project" | "schema" | "search" | "openai" | "worker"; + label: string; + status: SetupCheckStatus; + detail: string; +}; + +export type IngestionQualityReviewType = + "failed_ocr" | "low_extraction_confidence" | "missing_tables" | "image_only_pages" | "failed_job" | "manual_review"; + +export type IngestionQualityReviewItem = { + id: string; + type: IngestionQualityReviewType; + severity: "danger" | "warning" | "info"; + title: string; + detail: string; + documentId: string; + documentTitle: string; + fileName: string; + jobId: string | null; + qualityScore: number | null; + extractionQuality: string | null; + reasons: string[]; + metrics: Record; + updatedAt: string | null; +}; + +export const fallbackSetupChecks: SetupCheck[] = [ + { id: "env", label: ".env.local configured", status: "unknown", detail: "Setup status has not loaded yet." }, + { + id: "project", + label: "Clinical KB Database target", + status: "unknown", + detail: "Setup status has not loaded yet.", + }, + { id: "schema", label: "supabase/schema.sql applied", status: "unknown", detail: "Setup status has not loaded yet." }, + { + id: "search", + label: "Search RPC and vector indexes", + status: "unknown", + detail: "Setup status has not loaded yet.", + }, + { id: "openai", label: "OpenAI API key available", status: "unknown", detail: "Setup status has not loaded yet." }, + { id: "worker", label: "npm run worker running", status: "unknown", detail: "Setup status has not loaded yet." }, +]; + +const publicSearchSetupCheckIds = new Set(["env", "project", "schema", "search"]); +const requiredPublicSearchConfigCheckIds = new Set(["env", "project", "schema"]); + +export function hasReadyPublicSearchSetup(checks: SetupCheck[]) { + return Array.from(publicSearchSetupCheckIds).every( + (id) => checks.find((check) => check.id === id)?.status === "ready", + ); +} + +export function hasReadyRequiredPublicSearchConfig(checks: SetupCheck[]) { + return Array.from(requiredPublicSearchConfigCheckIds).every( + (id) => checks.find((check) => check.id === id)?.status === "ready", + ); +} diff --git a/src/components/clinical-dashboard/library-health-strip.tsx b/src/components/clinical-dashboard/library-health-strip.tsx new file mode 100644 index 0000000000..709cf6184b --- /dev/null +++ b/src/components/clinical-dashboard/library-health-strip.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { cn, textMuted, toneDanger, toneInfo, toneNeutral, toneSuccess, toneWarning } from "@/components/ui-primitives"; +import { fallbackSetupChecks, type SetupCheck } from "@/components/clinical-dashboard/document-manager-contracts"; +import type { ClinicalDocument, ImportBatch, IngestionJob } from "@/lib/types"; + +export type LibraryHealthTarget = "documents" | "setup" | "indexing" | "failures"; + +export function LibraryHealthStrip({ + documents, + jobs, + batches, + checks, + loading, + onSelectTarget, +}: { + documents: ClinicalDocument[]; + jobs: IngestionJob[]; + batches: ImportBatch[]; + checks: SetupCheck[]; + loading: boolean; + onSelectTarget?: (target: LibraryHealthTarget) => void; +}) { + const readyChecks = checks.filter((check) => check.status === "ready").length; + const indexedDocuments = documents.filter((document) => document.status === "indexed").length; + const activeJobs = jobs.filter((job) => job.status === "pending" || job.status === "processing").length; + const activeBatches = batches.filter((batch) => batch.status === "queued" || batch.status === "processing").length; + const failedWork = + jobs.filter((job) => job.status === "failed").length + batches.filter((batch) => batch.status === "failed").length; + const items = [ + { + target: "documents" as const, + label: "Documents", + value: loading ? "" : `${indexedDocuments} indexed`, + tone: loading ? toneNeutral : indexedDocuments ? toneSuccess : toneWarning, + actionLabel: "Show indexed document files", + }, + { + target: "setup" as const, + label: "Setup", + value: loading ? "" : `${readyChecks}/${checks.length || fallbackSetupChecks.length} ready`, + tone: loading + ? toneNeutral + : readyChecks === (checks.length || fallbackSetupChecks.length) + ? toneSuccess + : toneWarning, + actionLabel: "Show setup checks", + }, + { + target: "indexing" as const, + label: "Indexing", + value: loading ? "" : activeJobs + activeBatches ? `${activeJobs + activeBatches} active` : "Idle", + tone: loading ? toneNeutral : activeJobs + activeBatches ? toneInfo : toneNeutral, + actionLabel: "Show indexing progress", + }, + { + target: "failures" as const, + label: "Failures", + value: loading ? "" : failedWork ? `${failedWork} needs review` : "None", + tone: loading ? toneNeutral : failedWork ? toneDanger : toneNeutral, + actionLabel: "Show failed indexing work", + }, + ]; + + return ( +
+
+

Library health

+ Read-only status +
+
+ {items.map((item) => ( + + ))} +
+
+ ); +} diff --git a/tests/client-performance-boundaries.test.ts b/tests/client-performance-boundaries.test.ts index b60dd9f892..cb1a99b419 100644 --- a/tests/client-performance-boundaries.test.ts +++ b/tests/client-performance-boundaries.test.ts @@ -47,6 +47,9 @@ describe("fixture-free client performance boundaries", () => { it("loads administration data only after its source surface opens", () => { const dashboard = source("src/components/ClinicalDashboard.tsx"); + expect(dashboard).not.toContain('from "@/components/clinical-dashboard/DocumentManagerPanel"'); + expect(dashboard).toContain('from "@/components/clinical-dashboard/document-manager-contracts"'); + expect(dashboard).toContain('from "@/components/clinical-dashboard/library-health-strip"'); expect(dashboard).toContain("includeSetup: true, includeDashboardData: false"); expect(dashboard).toContain("dashboardDataSurfaceVisible && !dashboardDataLoadedRef.current"); expect(dashboard).toContain("administrationSurfaceVisible && !administrationDataLoadedRef.current"); From 2cd238e17f3d3150b2153642222205df804e2811 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 16:57:10 +0000 Subject: [PATCH 2/4] fix(chrome): stop overlay reserve publishing 0px on a transient miss Production UI failed when Services results kept chrome hidden after a viewport shrink but the result anchor jumped +131px with scrollTop unchanged. A zero-height stack measurement used `??`, so offsetHeight 0 published 0px over the CSS seed and the next successful measure restored the full stack height. Refuse zero phone measurements and fall through with `||`; tighten the Playwright poll to require a stable positive reserve. Co-authored-by: BigSimmo --- .../use-phone-overlay-chrome-reserve.ts | 34 ++++++-- tests/header-scroll-hide-contract.test.ts | 7 ++ .../phone-overlay-chrome-reserve.dom.test.ts | 63 +++++++++++++++ tests/ui-phone-scroll-page-owned.spec.ts | 81 ++++++++++++------- 4 files changed, 149 insertions(+), 36 deletions(-) create mode 100644 tests/phone-overlay-chrome-reserve.dom.test.ts diff --git a/src/components/clinical-dashboard/use-phone-overlay-chrome-reserve.ts b/src/components/clinical-dashboard/use-phone-overlay-chrome-reserve.ts index b6bc436cfe..9f26f1a7c9 100644 --- a/src/components/clinical-dashboard/use-phone-overlay-chrome-reserve.ts +++ b/src/components/clinical-dashboard/use-phone-overlay-chrome-reserve.ts @@ -8,6 +8,21 @@ const headerStackSelector = ".phone-sticky-header-stack"; const collapseSelector = '[data-testid="universal-header-collapse"]'; const reserveProperty = "--phone-overlay-chrome-h"; +/** + * Reads the phone overlay header stack height that `--phone-overlay-chrome-h` + * should publish. Returns 0 when the stack is temporarily unmeasurable + * (`display: contents`, mid-unmount) so callers can keep the previous value + * instead of publishing `0px` over the CSS seed. + */ +export function readPhoneOverlayChromeReservePx(root: ParentNode = document): number { + const collapse = root.querySelector(collapseSelector); + const stack = collapse?.closest(headerStackSelector) ?? null; + // Prefer `||` over `??`: a present stack with offsetHeight 0 (display:contents + // / empty mid-unmount) must fall through to the collapse row, and a 0 from + // both must stay 0 so the publisher can refuse to clobber the CSS seed. + return Math.round(stack?.offsetHeight || collapse?.offsetHeight || 0); +} + /** * Publishes the phone overlay header's stable height as `--phone-overlay-chrome-h` * so content can reserve a *constant* top clearance beneath it. @@ -28,6 +43,12 @@ const reserveProperty = "--phone-overlay-chrome-h"; * * Publishes `0px` above the phone breakpoint, where the sticky * [top bar | search] stack stays in flow and owns its own offsets. + * + * On phone, a transient `0` measurement must not overwrite the CSS seed or a + * previously published positive reserve. Doing so collapses content under the + * out-of-flow header and then jumps it back by the full stack height on the + * next successful measure — the Services result-anchor +131px failure under CI + * load after a viewport shrink (Production UI, PR #1562 / outstanding #146). */ export function usePhoneOverlayChromeReserve(): void { useLayoutEffect(() => { @@ -39,12 +60,13 @@ export function usePhoneOverlayChromeReserve(): void { root.style.setProperty(reserveProperty, "0px"); return; } - const collapse = document.querySelector(collapseSelector); - const stack = collapse?.closest(headerStackSelector) ?? null; - // Fall back to the collapse row alone when the overlay stack is absent - // (non-overlay hosts), rather than publishing a stale height. - const measured = stack?.offsetHeight ?? collapse?.offsetHeight ?? 0; - root.style.setProperty(reserveProperty, `${Math.round(measured)}px`); + const measured = readPhoneOverlayChromeReservePx(); + if (measured <= 0) { + // Keep the CSS seed (or the last positive inline value) rather than + // publishing 0px on a one-frame miss during resize / remount. + return; + } + root.style.setProperty(reserveProperty, `${measured}px`); }; sync(); diff --git a/tests/header-scroll-hide-contract.test.ts b/tests/header-scroll-hide-contract.test.ts index 94a3cc40d5..a5cee50757 100644 --- a/tests/header-scroll-hide-contract.test.ts +++ b/tests/header-scroll-hide-contract.test.ts @@ -177,6 +177,13 @@ describe("shared header hide/reveal wiring", () => { expect(reserveHookSource).not.toContain("dataset.scrollHidden"); expect(reserveHookSource).toContain("offsetHeight"); expect(reserveHookSource).toContain("ResizeObserver"); + // A present stack with offsetHeight 0 (display:contents / mid-unmount) must + // not publish 0px over the CSS seed via `??` — that collapses content under + // the out-of-flow header and jumps it back by the full stack height on the + // next measure (Services result-anchor +131px under CI, PR #1562 / #146). + expect(reserveHookSource).toContain("stack?.offsetHeight || collapse?.offsetHeight"); + expect(reserveHookSource).toContain("if (measured <= 0)"); + expect(reserveHookSource).toContain("readPhoneOverlayChromeReservePx"); }); it("portals the phone bottom dock out of the transformed overlay layer", () => { diff --git a/tests/phone-overlay-chrome-reserve.dom.test.ts b/tests/phone-overlay-chrome-reserve.dom.test.ts new file mode 100644 index 0000000000..ab9f5d0943 --- /dev/null +++ b/tests/phone-overlay-chrome-reserve.dom.test.ts @@ -0,0 +1,63 @@ +/** @vitest-environment jsdom */ + +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { readPhoneOverlayChromeReservePx } from "@/components/clinical-dashboard/use-phone-overlay-chrome-reserve"; + +function stubOffsetHeight(element: Element, height: number) { + Object.defineProperty(element, "offsetHeight", { + configurable: true, + get: () => height, + }); +} + +describe("readPhoneOverlayChromeReservePx", () => { + afterEach(() => { + document.body.innerHTML = ""; + vi.restoreAllMocks(); + }); + + it("falls through a zero-height stack to the collapse row", () => { + // A present `.phone-sticky-header-stack` with offsetHeight 0 (display:contents + // / mid-unmount) must not win via `??` and publish 0px over the CSS seed — + // that is the Services result-anchor jump under CI load (#146 / PR #1562). + document.body.innerHTML = ` +
+
+
+ `; + const stack = document.querySelector(".phone-sticky-header-stack"); + const collapse = document.querySelector('[data-testid="universal-header-collapse"]'); + expect(stack).toBeTruthy(); + expect(collapse).toBeTruthy(); + stubOffsetHeight(stack!, 0); + stubOffsetHeight(collapse!, 72); + expect(readPhoneOverlayChromeReservePx()).toBe(72); + }); + + it("returns 0 when neither stack nor collapse is measurable so the publisher can keep the seed", () => { + document.body.innerHTML = ` +
+
+
+ `; + const stack = document.querySelector(".phone-sticky-header-stack"); + const collapse = document.querySelector('[data-testid="universal-header-collapse"]'); + stubOffsetHeight(stack!, 0); + stubOffsetHeight(collapse!, 0); + expect(readPhoneOverlayChromeReservePx()).toBe(0); + }); + + it("prefers the stack height when the overlay stack is measurable", () => { + document.body.innerHTML = ` +
+
+
+ `; + const stack = document.querySelector(".phone-sticky-header-stack"); + const collapse = document.querySelector('[data-testid="universal-header-collapse"]'); + stubOffsetHeight(stack!, 131); + stubOffsetHeight(collapse!, 72); + expect(readPhoneOverlayChromeReservePx()).toBe(131); + }); +}); diff --git a/tests/ui-phone-scroll-page-owned.spec.ts b/tests/ui-phone-scroll-page-owned.spec.ts index 6cdb86d18a..56050119a3 100644 --- a/tests/ui-phone-scroll-page-owned.spec.ts +++ b/tests/ui-phone-scroll-page-owned.spec.ts @@ -719,6 +719,9 @@ test("Services results keep a continuous browser viewport after shared chrome re // Safari toolbar changes shrink and expand the visual viewport after a // scroll. Document ownership must keep the reading offset and its content // anchor stable without switching back to a fixed or nested canvas. + const overlayReserveBefore = await page.evaluate(() => + getComputedStyle(document.documentElement).getPropertyValue("--phone-overlay-chrome-h").trim(), + ); await page.setViewportSize({ width: phoneViewport.width, height: phoneViewport.height - 64 }); // Wait for chrome *and* the result anchor to settle rather than sleeping // (#146). A viewport-range / height change can spuriously re-show chrome under @@ -727,39 +730,57 @@ test("Services results keep a continuous browser viewport after shared chrome re // contract failed. While chrome is up the results anchor sits exactly // `collapseHeight + safe-area-top` lower — 72 + 59 = the 131 px jump that // failed this assertion on multiple heads, with `documentScrollTop` - // unchanged. Polling only `header.bottom <= 1` is a false settle: the bar can - // clear the top edge a frame before `data-scroll-hidden` and the content - // anchor finish recovering (reproduced on PR #1521 tip `061468e4`, Production - // UI shard 1). Keep the 0.5px anchor tolerance (#146 stop rule). + // unchanged. A related CI failure (PR #1562) kept chrome attribute-hidden and + // scrollTop stable while the overlay reserve briefly published `0px` then + // restored — same 131px anchor jump, different half of the contract. Polling + // only `header.bottom <= 1` is a false settle: the bar can clear the top edge + // a frame before `data-scroll-hidden` and the content anchor finish recovering + // (reproduced on PR #1521 tip `061468e4`, Production UI shard 1). Keep the + // 0.5px anchor tolerance (#146 stop rule) and require a positive overlay + // reserve so a 0px publish cannot read as settled chrome. await expect .poll( async () => { - return page.evaluate((expectedAnchorTop) => { - const header = document.querySelector("header#search"); - const collapse = document.querySelector('[data-testid="universal-header-collapse"]'); - const dock = document.querySelector(".answer-footer-search-dock"); - const resultList = document.querySelector('[data-testid="service-search-results"]'); - const scrollingElement = document.scrollingElement ?? document.documentElement; - const headerBottom = header?.getBoundingClientRect().bottom ?? -1; - const anchorTop = resultList?.getBoundingClientRect().top ?? Number.NaN; - const chromeHidden = - headerBottom <= 1 && - collapse?.getAttribute("data-scroll-hidden") === "true" && - dock?.getAttribute("data-scroll-hidden") === "true"; - const anchorStable = Number.isFinite(anchorTop) && Math.abs(anchorTop - expectedAnchorTop) < 0.5; - return { - ok: chromeHidden && anchorStable, - chromeHidden, - anchorStable, - headerBottom, - collapseHidden: collapse?.getAttribute("data-scroll-hidden"), - dockHidden: dock?.getAttribute("data-scroll-hidden"), - scrollSignal: collapse?.getAttribute("data-scroll-signal") ?? "missing", - anchorTop, - expectedAnchorTop, - documentScrollTop: scrollingElement.scrollTop, - }; - }, hidden.anchorTop); + return page.evaluate( + ({ expectedAnchorTop, expectedReserve }) => { + const header = document.querySelector("header#search"); + const collapse = document.querySelector('[data-testid="universal-header-collapse"]'); + const dock = document.querySelector(".answer-footer-search-dock"); + const resultList = document.querySelector('[data-testid="service-search-results"]'); + const scrollingElement = document.scrollingElement ?? document.documentElement; + const headerBottom = header?.getBoundingClientRect().bottom ?? -1; + const anchorTop = resultList?.getBoundingClientRect().top ?? Number.NaN; + const overlayReserve = getComputedStyle(document.documentElement) + .getPropertyValue("--phone-overlay-chrome-h") + .trim(); + const reservePx = Number.parseFloat(overlayReserve); + const chromeHidden = + headerBottom <= 1 && + collapse?.getAttribute("data-scroll-hidden") === "true" && + dock?.getAttribute("data-scroll-hidden") === "true"; + const anchorStable = Number.isFinite(anchorTop) && Math.abs(anchorTop - expectedAnchorTop) < 0.5; + const reserveStable = + Number.isFinite(reservePx) && + reservePx > 0 && + (!expectedReserve || Math.abs(reservePx - Number.parseFloat(expectedReserve)) < 1); + return { + ok: chromeHidden && anchorStable && reserveStable, + chromeHidden, + anchorStable, + reserveStable, + headerBottom, + collapseHidden: collapse?.getAttribute("data-scroll-hidden"), + dockHidden: dock?.getAttribute("data-scroll-hidden"), + scrollSignal: collapse?.getAttribute("data-scroll-signal") ?? "missing", + anchorTop, + expectedAnchorTop, + overlayReserve, + expectedReserve, + documentScrollTop: scrollingElement.scrollTop, + }; + }, + { expectedAnchorTop: hidden.anchorTop, expectedReserve: overlayReserveBefore }, + ); }, { timeout: 10_000, From 5fe0a55463ecd6094aa7aa0c7b83d30daee3c3db Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 18:01:42 +0000 Subject: [PATCH 3/4] fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit --- src/components/clinical-dashboard/library-health-strip.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/clinical-dashboard/library-health-strip.tsx b/src/components/clinical-dashboard/library-health-strip.tsx index 709cf6184b..82082e9982 100644 --- a/src/components/clinical-dashboard/library-health-strip.tsx +++ b/src/components/clinical-dashboard/library-health-strip.tsx @@ -80,7 +80,7 @@ export function LibraryHealthStrip({ type="button" onClick={() => onSelectTarget?.(item.target)} className={cn( - "rounded-md border px-2.5 py-2 text-left transition hover:-translate-y-px hover:shadow-[var(--shadow-soft)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] active:translate-y-0", + "min-h-12 rounded-md border px-2.5 py-2 text-left transition hover:-translate-y-px hover:shadow-[var(--shadow-soft)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] active:translate-y-0", item.tone, )} aria-label={item.actionLabel} From 26a3c4a76a419e7cffd9f583e63b20d713b553b0 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 1 Aug 2026 19:19:09 +0000 Subject: [PATCH 4/4] docs: record PR #1562 unblock after main sync Ledger the behind-but-clean sync that cleared GitHub DIRTY mergeability on the document-admin lazy-load PR tip. Co-authored-by: BigSimmo --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index da785da2ed..63d1f096ee 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -578,3 +578,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-01 | work | d4a4463172671cff3fd333feb685fa397e4f95d0 | latency, lazy loading, React and Next.js frontend performance | P2 eager document administration bundle boundary fixed; no P0/P1 findings | lint; typecheck; 463 files/4811 tests; production build; bundle budget | | 2026-08-01 | codex/cloud-connected-profile-boundary | 6fddcfc780b237b6b2cd524dbebd7b1de70d9701 | Cloud connected profile credential boundary | No high-confidence issues after least-privilege MCP hardening and portable Git fixture fix | Cloud static PASS; focused Vitest 15/15; full format PASS; Bash syntax PASS; PR-local dry-run | | 2026-08-01 | codex/cloud-connected-profile-boundary | 55b08496a5ee3495eed8a7436e8f69ae7b6612d8 | Cloud connected profile credential boundary | Reviewer findings fixed: cross-tenant service-role credential scrubbed and duplicate Supabase MCP parameters rejected | Cloud static PASS; focused Vitest 15/15; Bash syntax PASS; targeted Prettier PASS | +| 2026-08-01 | codex/review-latency-and-lazy-loading-optimizations | 5f069a7fec4e6ada47a0074aa5f2ea2c9dc97830 | pr-1562 unblock | unblocked: merge-tree was clean behind-by-4; merged origin/main; no unresolved threads; prior tip CI green including Production UI + PR required | merge-tree clean vs origin/main; gh mergeable was CONFLICTING/DIRTY (staleness); unresolved threads 0; auto-merge off |