release: v1.32.0 — hardening and scalability (#580)

* release: prepare v1.32.0

* test: stabilize water capture re-entry
This commit is contained in:
Marc-André Bombeck authored and GitHub committed 2026-07-22 04:14:15 +02:00
1 parent b01822be48
commit 62eb431d66
372 files changed
+25178 -8561

No files matched your search

@@ -27,6 +27,17 @@ function render(node: React.ReactNode) {
const noop = () => {};
function buttonMarkupByName(html: string, name: string): string {
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const markup = html.match(
new RegExp(
`<button(?=[^>]*aria-label="${escapedName}")[^>]*>[\\s\\S]*?</button>`,
),
)?.[0];
expect(markup).toBeDefined();
return markup ?? "";
}
const suggestion = (
overrides: Partial<DocumentSuggestionDto> = {},
): DocumentSuggestionDto => ({
@@ -112,6 +123,27 @@ describe("<AssistSuggestionReview>", () => {
);
expect(html).toContain("Nothing could be read from this document");
});
it("keeps dismiss compact while providing a 44px mobile target", () => {
const html = render(
<AssistSuggestionReview
suggestion={suggestion()}
kindLabel="Lab result"
dateLabel="1 Mar 2026"
applied={{ title: false, kind: false, date: false }}
onUseTitle={noop}
onUseKind={noop}
onUseDate={noop}
onDismiss={noop}
/>,
);
const dismiss = buttonMarkupByName(html, "Dismiss");
expect(dismiss).toContain("size-11");
expect(dismiss).toContain("sm:size-7");
expect(dismiss).toContain('aria-label="Dismiss"');
expect(dismiss).toContain("<svg");
expect(dismiss).toContain("size-4");
});
});
describe("<DocumentSummaryPanel>", () => {
@@ -170,4 +202,22 @@ describe("<DocumentSummaryPanel>", () => {
expect(html).toContain('role="alert"');
expect(html).toContain("read the document");
});
it("keeps close compact while providing a 44px mobile target", () => {
const html = render(
<DocumentSummaryPanel
output="summary"
result={{ summary: "A lipid panel dated 1 March 2026." }}
isPending={false}
errorKey={null}
onClose={noop}
/>,
);
const close = buttonMarkupByName(html, "Hide");
expect(close).toContain("size-11");
expect(close).toContain("sm:size-7");
expect(close).toContain('aria-label="Hide"');
expect(close).toContain("<svg");
expect(close).toContain("size-4");
});
});
@@ -0,0 +1,176 @@
import { describe, expect, it } from "vitest";
import {
closeDocumentSelectionHistoryEntry,
closeDocumentSelectionAfterCoachHandoff,
documentSelectionHistoryState,
documentSelectionHref,
withoutDocumentSelectionHref,
} from "../documents-view";
class MemoryHistory {
private entries: { href: string; state: unknown }[];
private index: number;
constructor(entries: string[]) {
this.entries = entries.map((href) => ({ href, state: null }));
this.index = entries.length - 1;
}
get href() {
return this.entries[this.index]?.href;
}
get state() {
return this.entries[this.index]?.state;
}
pushState(state: unknown, _unused: string, href: string | URL) {
this.entries.splice(this.index + 1, Infinity, {
href: href.toString(),
state,
});
this.index += 1;
}
replaceState(state: unknown, _unused: string, href?: string | URL | null) {
this.entries[this.index] = {
href: href?.toString() ?? this.href!,
state,
};
}
back() {
if (this.index > 0) this.index -= 1;
}
}
describe("document vault URL selection", () => {
it("adds an ordinary card selection without dropping unrelated query state", () => {
expect(
documentSelectionHref(
"/documents",
"episode=episode-1&view=compact&q=MRT",
"doc_123",
),
).toBe("/documents?episode=episode-1&view=compact&q=MRT&doc=doc_123");
});
it("replaces an existing selection instead of appending a duplicate", () => {
expect(
documentSelectionHref(
"/documents",
"episode=episode-1&doc=old-document",
"new-document",
),
).toBe("/documents?episode=episode-1&doc=new-document");
});
it.each(["", "../coach", "doc?id=other", "doc id", "doc\n", "a".repeat(41)])(
"refuses to write an unsafe document id: %j",
(documentId) => {
expect(
documentSelectionHref(
"/documents",
"episode=episode-1&view=compact",
documentId,
),
).toBeNull();
},
);
it("removes only the document selection on close", () => {
expect(
withoutDocumentSelectionHref(
"/documents",
"episode=episode-1&doc=doc_123&view=compact&q=MRT",
),
).toBe("/documents?episode=episode-1&view=compact&q=MRT");
});
it("returns the pathname when doc was the only query parameter", () => {
expect(withoutDocumentSelectionHref("/documents", "doc=doc_123")).toBe(
"/documents",
);
});
it("consumes a card-open entry so the next Back reaches the prior distinct page", () => {
const history = new MemoryHistory(["/today", "/documents?episode=ep-1"]);
const openHref = documentSelectionHref(
"/documents",
"episode=ep-1",
"doc-1",
);
expect(openHref).not.toBeNull();
history.pushState(documentSelectionHistoryState("doc-1"), "", openHref!);
closeDocumentSelectionHistoryEntry(
history,
"/documents",
"episode=ep-1&doc=doc-1",
"doc-1",
);
expect(history.href).toBe("/documents?episode=ep-1");
history.back();
expect(history.href).toBe("/today");
});
it("consumes a dismissed Coach handoff and closes its card-owned detail entry", () => {
const history = new MemoryHistory(["/today", "/documents?episode=ep-1"]);
history.pushState(
documentSelectionHistoryState("doc-1"),
"",
"/documents?episode=ep-1&doc=doc-1",
);
const retainedHandoff = closeDocumentSelectionAfterCoachHandoff(
history,
"/documents",
"episode=ep-1&doc=doc-1",
"doc-1",
"doc-1",
"dismiss",
);
expect(retainedHandoff).toBeNull();
expect(history.href).toBe("/documents?episode=ep-1");
});
it("retains a navigating Coach handoff for maximize and browser Back", () => {
const history = new MemoryHistory(["/today", "/documents?episode=ep-1"]);
history.pushState(
documentSelectionHistoryState("doc-1"),
"",
"/documents?episode=ep-1&doc=doc-1",
);
const retainedHandoff = closeDocumentSelectionAfterCoachHandoff(
history,
"/documents",
"episode=ep-1&doc=doc-1",
"doc-1",
"doc-1",
"navigate",
);
expect(retainedHandoff).toBe("doc-1");
expect(history.href).toBe("/documents?episode=ep-1&doc=doc-1");
});
it("replaces a direct deep-link selection without navigating away", () => {
const history = new MemoryHistory([
"/today",
"/documents?episode=ep-1&doc=doc-1",
]);
history.replaceState({ __NA: true }, "", history.href);
closeDocumentSelectionHistoryEntry(
history,
"/documents",
"episode=ep-1&doc=doc-1",
"doc-1",
);
expect(history.href).toBe("/documents?episode=ep-1");
});
});
@@ -147,7 +147,7 @@ export function AssistSuggestionReview({
type="button"
variant="ghost"
size="icon"
className="size-7 shrink-0"
className="size-11 shrink-0 sm:size-7"
onClick={onDismiss}
aria-label={t("documents.assist.dismiss")}
>
@@ -233,7 +233,7 @@ export function DocumentSummaryPanel({
type="button"
variant="ghost"
size="icon"
className="size-7 shrink-0"
className="size-11 shrink-0 sm:size-7"
onClick={onClose}
aria-label={t("documents.summary.close")}
>
+165 -30
View File
@@ -42,6 +42,10 @@ import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
import { PullToRefreshIndicator } from "@/components/ui/pull-to-refresh-indicator";
import { apiGet, apiPost } from "@/lib/api/api-fetch";
import { useTranslations } from "@/lib/i18n/context";
import {
type CoachCloseIntent,
useCoachLaunch,
} from "@/lib/insights/coach-launch-context";
import { invalidateKeys, queryKeys } from "@/lib/query-keys";
import type { DocumentVaultFilters } from "@/lib/query-keys/documents";
import {
@@ -79,6 +83,99 @@ interface ListPage {
nextCursor: string | null;
}
// Strict end-of-input: `$` also accepts a trailing newline in JavaScript.
const DOCUMENT_QUERY_ID_PATTERN = /^[a-zA-Z0-9_-]{1,40}(?![\s\S])/;
const DOCUMENT_SELECTION_HISTORY_KEY = "__healthlogPushedDocumentSelection";
export function documentSelectionHistoryState(
documentId: string,
): Record<string, unknown> {
// Pass only application state here. Next's patched pushState copies its
// private router fields onto this object; copying history.state ourselves
// would include `__NA` and make that patch treat this as an internal write,
// skipping useSearchParams synchronization.
return { [DOCUMENT_SELECTION_HISTORY_KEY]: documentId };
}
export function documentSelectionHref(
pathname: string,
currentSearch: string,
documentId: string,
): string | null {
if (!DOCUMENT_QUERY_ID_PATTERN.test(documentId)) return null;
const next = new URLSearchParams(currentSearch);
next.set("doc", documentId);
const search = next.toString();
return search ? `${pathname}?${search}` : pathname;
}
export function withoutDocumentSelectionHref(
pathname: string,
currentSearch: string,
): string {
const next = new URLSearchParams(currentSearch);
next.delete("doc");
const search = next.toString();
return search ? `${pathname}?${search}` : pathname;
}
export function closeDocumentSelectionHistoryEntry(
history: {
readonly state: unknown;
back: () => void;
replaceState: (
data: unknown,
unused: string,
url?: string | URL | null,
) => void;
},
pathname: string,
currentSearch: string,
documentId: string,
): void {
const ownsCurrentEntry =
history.state !== null &&
typeof history.state === "object" &&
(history.state as Record<string, unknown>)[
DOCUMENT_SELECTION_HISTORY_KEY
] === documentId;
if (ownsCurrentEntry) {
// Card selection owns the current pushed entry. Returning to its base
// consumes it, so the user's next Back reaches the prior distinct page.
history.back();
return;
}
// A deep link did not create a disposable entry on this page. Keep the user
// here and remove only the selection from the current URL.
history.replaceState(
null,
"",
withoutDocumentSelectionHref(pathname, currentSearch),
);
}
export function closeDocumentSelectionAfterCoachHandoff(
history: Parameters<typeof closeDocumentSelectionHistoryEntry>[0],
pathname: string,
currentSearch: string,
documentId: string,
handedOffDocumentId: string | null,
closeIntent: CoachCloseIntent | null | undefined,
): string | null {
if (handedOffDocumentId === documentId && closeIntent === "navigate") {
return handedOffDocumentId;
}
closeDocumentSelectionHistoryEntry(
history,
pathname,
currentSearch,
documentId,
);
return handedOffDocumentId === documentId ? null : handedOffDocumentId;
}
export function DocumentsView() {
const { t } = useTranslations();
const { user, isLoading: authLoading, isAuthenticated } = useAuth();
@@ -86,6 +183,7 @@ export function DocumentsView() {
const pathname = usePathname();
const searchParams = useSearchParams();
const queryClient = useQueryClient();
const coachLaunch = useCoachLaunch();
const moduleEnabled = user?.modules?.inboundDocuments === true;
@@ -540,42 +638,79 @@ export function DocumentsView() {
// ── Detail sheet ──────────────────────────────────────────────────────
const [detailId, setDetailId] = useState<string | null>(null);
const [detailOpen, setDetailOpen] = useState(false);
const openDetail = useCallback((id: string) => {
setDetailId(id);
setDetailOpen(true);
}, []);
const handedOffDocumentRef = useRef<string | null>(null);
// `?doc=<id>` deep link (used by the illness page's document rows): open
// the detail sheet once per value; closing the sheet strips the param so
// back/forward and refresh behave. Render-phase derived-state adjustment
// (the file's established pattern), consumed only once auth has resolved
// so a deep link never burns before the module flag is known. The id is
// shape-checked before it is interpolated into an API path.
const docParam = searchParams.get("doc");
const [consumedDocParam, setConsumedDocParam] = useState<string | null>(null);
if (!authLoading && moduleEnabled && docParam !== consumedDocParam) {
setConsumedDocParam(docParam);
if (docParam !== null && /^[a-zA-Z0-9_-]{1,40}$/.test(docParam)) {
setDetailId(docParam);
setDetailOpen(true);
}
const rawDocParam = searchParams.get("doc");
const docParam =
rawDocParam !== null && DOCUMENT_QUERY_ID_PATTERN.test(rawDocParam)
? rawDocParam
: null;
const [observedDocParam, setObservedDocParam] = useState<string | null>(null);
// The query is the navigation owner: direct links, browser back, and browser
// forward all drive the same detail selection without remounting the route.
// Wait for auth so a deep link cannot be consumed before the module gate is
// known, and never interpolate an unvalidated id into the detail API path.
if (!authLoading && moduleEnabled && rawDocParam !== observedDocParam) {
setObservedDocParam(rawDocParam);
setDetailId(docParam);
setDetailOpen(docParam !== null);
}
const handleDetailOpenChange = useCallback(
(open: boolean) => {
setDetailOpen(open);
if (!open && searchParams.get("doc")) {
const sp = new URLSearchParams(searchParams.toString());
sp.delete("doc");
const search = sp.toString();
router.replace(search ? `${pathname}?${search}` : pathname, {
scroll: false,
});
}
const openDetail = useCallback(
(id: string) => {
const href = documentSelectionHref(pathname, searchParams.toString(), id);
if (href === null) return;
handedOffDocumentRef.current = null;
setDetailId(id);
setDetailOpen(true);
window.history.pushState(documentSelectionHistoryState(id), "", href);
},
[pathname, router, searchParams],
[pathname, searchParams],
);
const handleDetailOpenChange = useCallback((open: boolean) => {
setDetailOpen(open);
}, []);
const coachOwnsDocument =
docParam !== null &&
coachLaunch?.open === true &&
coachLaunch.documentId === docParam;
useEffect(() => {
if (authLoading || !moduleEnabled) return;
if (docParam === null) {
handedOffDocumentRef.current = null;
return;
}
if (detailOpen) return;
if (coachOwnsDocument) {
// "Ask Coach" intentionally closes the sheet while the drawer owns the
// same document. Keep the return URL intact so maximize → browser-back
// can reconstruct the sheet from `?doc=`.
handedOffDocumentRef.current = docParam;
return;
}
handedOffDocumentRef.current = closeDocumentSelectionAfterCoachHandoff(
window.history,
pathname,
searchParams.toString(),
docParam,
handedOffDocumentRef.current,
coachLaunch?.closeIntent,
);
}, [
authLoading,
coachOwnsDocument,
coachLaunch?.closeIntent,
detailOpen,
docParam,
moduleEnabled,
pathname,
searchParams,
]);
// Hover/focus intent prefetches the detail METADATA (never the blob —
// the blob fetch starts when the sheet mounts its preview element).
const prefetchDetail = useCallback(