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

@@ -0,0 +1,353 @@
import { readFileSync } from "node:fs";
import { describe, expect, it, vi } from "vitest";
import {
createSeededFormDismissalController,
createSeededFormHistoryTraversalGuard,
safeInternalNavigationTarget,
} from "../use-seeded-form-dismissal";
describe("seeded form dismissal", () => {
const measurementSeed = {
value: "72.4",
measuredAt: "2026-07-21T08:30",
notes: "before breakfast",
};
it("closes a pristine edit immediately", () => {
const close = vi.fn();
const controller = createSeededFormDismissalController({
seed: measurementSeed,
value: measurementSeed,
});
controller.requestClose(close);
expect(close).toHaveBeenCalledOnce();
expect(controller.getState()).toEqual({
isDirty: false,
discardDialogOpen: false,
});
});
it("asks before closing an edit changed from its seeded values", () => {
const close = vi.fn();
const controller = createSeededFormDismissalController({
seed: measurementSeed,
value: { ...measurementSeed, value: "73.1" },
});
controller.requestClose(close);
expect(close).not.toHaveBeenCalled();
expect(controller.getState()).toEqual({
isDirty: true,
discardDialogOpen: true,
});
});
it("keeps the changed draft when discard is cancelled", () => {
const close = vi.fn();
const changed = { ...measurementSeed, notes: "after breakfast" };
const controller = createSeededFormDismissalController({
seed: measurementSeed,
value: changed,
});
controller.requestClose(close);
controller.cancelDiscard();
expect(close).not.toHaveBeenCalled();
expect(controller.getState()).toEqual({
isDirty: true,
discardDialogOpen: false,
});
});
it("closes and discards only after explicit confirmation", () => {
const close = vi.fn();
const controller = createSeededFormDismissalController({
seed: measurementSeed,
value: { ...measurementSeed, measuredAt: "2026-07-21T09:15" },
});
controller.requestClose(close);
controller.confirmDiscard();
expect(close).toHaveBeenCalledOnce();
expect(controller.getState().discardDialogOpen).toBe(false);
});
it("treats a successfully saved profile value as the new clean seed", () => {
const close = vi.fn();
const profileSeed = {
email: "person@example.test",
heightCm: "175",
timezone: "Europe/Berlin",
};
const savedProfile = { ...profileSeed, timezone: "Europe/London" };
const controller = createSeededFormDismissalController({
seed: profileSeed,
value: savedProfile,
});
expect(controller.getState().isDirty).toBe(true);
controller.sync({ seed: savedProfile, value: savedProfile });
controller.requestClose(close);
expect(controller.getState().isDirty).toBe(false);
expect(close).toHaveBeenCalledOnce();
});
it("does not dismiss while a write is pending", () => {
const close = vi.fn();
const controller = createSeededFormDismissalController({
seed: measurementSeed,
value: { ...measurementSeed, value: "73.1" },
blocked: true,
});
controller.requestClose(close);
controller.confirmDiscard();
expect(close).not.toHaveBeenCalled();
expect(controller.getState().discardDialogOpen).toBe(false);
});
});
describe("seeded form Back navigation", () => {
function createHistoryHarness() {
let index = 1;
let entries: unknown[] = [{}, {}];
let onPopState:
((event: { stopImmediatePropagation(): void }) => void) | null = null;
const events: Array<{ stopImmediatePropagation(): void }> = [];
const history = {
get state() {
return entries[index];
},
go(delta: number) {
const nextIndex = index + delta;
if (nextIndex < 0 || nextIndex >= entries.length) return;
index = nextIndex;
const event = { stopImmediatePropagation: vi.fn() };
events.push(event);
onPopState?.(event);
},
pushState(state: unknown, unused?: string, url?: string) {
void unused;
void url;
entries = entries.slice(0, index + 1);
entries.push(state);
index += 1;
},
replaceState(state: unknown, unused?: string, url?: string) {
void unused;
void url;
entries[index] = state;
},
};
return {
history,
events,
get index() {
return index;
},
getCurrentIndex: () => index,
listen(listener: typeof onPopState) {
onPopState = listener;
},
back() {
history.go(-1);
},
forward() {
history.go(1);
},
};
}
function createDirtyBackNavigation() {
const seed = { value: "72.4" };
const draft = { value: "73.1" };
const dismissal = createSeededFormDismissalController({
seed,
value: draft,
});
const historyHarness = createHistoryHarness();
const requestClose = vi.fn((close: () => void) => {
dismissal.requestClose(close);
return dismissal.getState().discardDialogOpen;
});
const historyGuard = createSeededFormHistoryTraversalGuard({
history: historyHarness.history,
getCurrentIndex: historyHarness.getCurrentIndex,
shouldBlock: () => dismissal.getState().isDirty,
requestClose,
});
historyHarness.listen((event) => historyGuard.handlePopState(event));
return {
dismissal,
draft,
historyGuard,
historyHarness,
requestClose,
};
}
it("restores the form route and dirty draft when Back is cancelled", () => {
const { dismissal, draft, historyGuard, historyHarness, requestClose } =
createDirtyBackNavigation();
historyHarness.back();
expect(historyHarness.index).toBe(1);
expect(dismissal.getState().discardDialogOpen).toBe(true);
expect(requestClose).toHaveBeenCalledOnce();
historyGuard.cancelPendingTraversal();
dismissal.cancelDiscard();
expect(historyHarness.index).toBe(1);
expect(draft).toEqual({ value: "73.1" });
expect(dismissal.getState()).toEqual({
isDirty: true,
discardDialogOpen: false,
});
historyHarness.back();
expect(historyHarness.index).toBe(1);
expect(requestClose).toHaveBeenCalledTimes(2);
});
it("does not prompt twice while the same Back dismissal is pending", () => {
const { dismissal, historyHarness, requestClose } =
createDirtyBackNavigation();
historyHarness.back();
historyHarness.back();
expect(historyHarness.index).toBe(1);
expect(historyHarness.events).toHaveLength(4);
expect(requestClose).toHaveBeenCalledOnce();
expect(dismissal.getState().discardDialogOpen).toBe(true);
});
it("allows the original Back traversal after discard is confirmed", () => {
const { dismissal, historyHarness } = createDirtyBackNavigation();
historyHarness.back();
dismissal.confirmDiscard();
expect(historyHarness.index).toBe(0);
expect(dismissal.getState().discardDialogOpen).toBe(false);
expect(
historyHarness.events.at(-1)?.stopImmediatePropagation,
).not.toHaveBeenCalled();
});
it("restores the form route when Forward follows a Back return and edit without the Navigation API", () => {
let dirty = false;
const historyHarness = createHistoryHarness();
const requestClose = vi.fn(() => true);
const historyGuard = createSeededFormHistoryTraversalGuard({
history: historyHarness.history,
shouldBlock: () => dirty,
requestClose,
});
historyHarness.listen((event) => historyGuard.handlePopState(event));
historyHarness.history.replaceState(
{ section: "profile" },
"",
"/settings?section=profile",
);
historyHarness.history.pushState({}, "", "/next");
historyHarness.back();
dirty = true;
historyHarness.forward();
expect(historyHarness.index).toBe(1);
expect(requestClose).toHaveBeenCalledOnce();
});
it("leaves clean-form Back traversal untouched", () => {
const historyHarness = createHistoryHarness();
const requestClose = vi.fn(() => true);
const historyGuard = createSeededFormHistoryTraversalGuard({
history: historyHarness.history,
getCurrentIndex: historyHarness.getCurrentIndex,
shouldBlock: () => false,
requestClose,
});
historyHarness.listen((event) => historyGuard.handlePopState(event));
historyHarness.back();
expect(historyHarness.index).toBe(0);
expect(requestClose).not.toHaveBeenCalled();
expect(
historyHarness.events[0]?.stopImmediatePropagation,
).not.toHaveBeenCalled();
});
});
describe("safeInternalNavigationTarget", () => {
it("keeps only same-origin path, query, and hash state", () => {
expect(
safeInternalNavigationTarget(
"https://healthlog.test/settings/security?from=account#passkeys",
"https://healthlog.test/settings/account",
),
).toBe("/settings/security?from=account#passkeys");
});
it("rejects external and credential-bearing navigation targets", () => {
expect(
safeInternalNavigationTarget(
"https://example.test/settings/security",
"https://healthlog.test/settings/account",
),
).toBeNull();
expect(
safeInternalNavigationTarget(
"https://user:secret@healthlog.test/settings/security",
"https://healthlog.test/settings/account",
),
).toBeNull();
});
});
describe("seeded dismissal migrations", () => {
it.each([
"measurements/measurement-list.tsx",
"mood/mood-list.tsx",
"labs/lab-history-list.tsx",
])(
"routes every %s edit exit through the shared contract",
(relativePath) => {
const source = readFileSync(
new URL(`../../${relativePath}`, import.meta.url),
"utf8",
);
expect(source).toContain("useSeededFormDismissal({");
expect(source).toContain("editDismissal.requestClose(dismissEdit)");
expect(source).toContain("<SeededFormDiscardDialog");
},
);
it("guards profile navigation and refreshes its seed after a successful save", () => {
const source = readFileSync(
new URL("../../settings/account-section/index.tsx", import.meta.url),
"utf8",
);
expect(source).toContain("guardNavigation: true");
expect(source).toContain("setProfileSeed(savedProfile)");
expect(source).toContain("<SeededFormDiscardDialog");
});
});
@@ -0,0 +1,456 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useTranslations } from "@/lib/i18n/context";
type CloseRequest = () => void;
type ControllerOptions<T> = {
seed: T;
value: T;
blocked?: boolean;
equals?: (seed: T, value: T) => boolean;
onStateChange?: () => void;
};
type ControllerSync<T> = Pick<
ControllerOptions<T>,
"seed" | "value" | "blocked"
>;
export type SeededFormDismissalController<T> = {
sync(options: ControllerSync<T>): void;
getState(): { isDirty: boolean; discardDialogOpen: boolean };
requestClose(close: CloseRequest): void;
confirmDiscard(): void;
cancelDiscard(): void;
};
function seededValuesEqual<T>(seed: T, value: T): boolean {
return JSON.stringify(seed) === JSON.stringify(value);
}
export function createSeededFormDismissalController<T>(
initial: ControllerOptions<T>,
): SeededFormDismissalController<T> {
let seed = initial.seed;
let value = initial.value;
let blocked = initial.blocked ?? false;
let discardDialogOpen = false;
let pendingClose: CloseRequest | null = null;
const equals = initial.equals ?? seededValuesEqual;
const notify = initial.onStateChange ?? (() => undefined);
return {
sync(next) {
seed = next.seed;
value = next.value;
blocked = next.blocked ?? false;
},
getState() {
return {
isDirty: !equals(seed, value),
discardDialogOpen,
};
},
requestClose(close) {
if (blocked) return;
if (equals(seed, value)) {
close();
return;
}
pendingClose = close;
discardDialogOpen = true;
notify();
},
confirmDiscard() {
if (blocked || !discardDialogOpen) return;
const close = pendingClose;
pendingClose = null;
discardDialogOpen = false;
notify();
close?.();
},
cancelDiscard() {
if (!discardDialogOpen) return;
pendingClose = null;
discardDialogOpen = false;
notify();
},
};
}
type HistoryTraversal = {
readonly state?: unknown;
go(delta: number): void;
pushState?(data: unknown, unused: string, url?: string | URL | null): void;
replaceState?(data: unknown, unused: string, url?: string | URL | null): void;
};
type HistoryTraversalGuardOptions = {
history: HistoryTraversal;
getCurrentIndex?: () => number | undefined;
shouldBlock: () => boolean;
requestClose: (close: CloseRequest) => boolean;
};
type InterceptablePopStateEvent = {
stopImmediatePropagation(): void;
};
export type SeededFormHistoryTraversalGuard = {
handlePopState(event: InterceptablePopStateEvent): void;
cancelPendingTraversal(): void;
dispose(): void;
};
const SEEDED_FORM_HISTORY_INDEX = "__healthlogSeededFormHistoryIndex";
function createHistoryEntryIndexTracker(history: HistoryTraversal) {
const getCurrentIndex = () => {
if (
history.state === null ||
typeof history.state !== "object" ||
Array.isArray(history.state)
) {
return undefined;
}
const index = (history.state as Record<string, unknown>)[
SEEDED_FORM_HISTORY_INDEX
];
return typeof index === "number" ? index : undefined;
};
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
if (originalPushState === undefined || originalReplaceState === undefined) {
return { getCurrentIndex, dispose: () => undefined };
}
const withIndex = (state: unknown, index: number) => ({
...(state !== null && typeof state === "object" && !Array.isArray(state)
? state
: {}),
[SEEDED_FORM_HISTORY_INDEX]: index,
});
if (getCurrentIndex() === undefined) {
originalReplaceState.call(history, withIndex(history.state, 0), "");
}
const trackedPushState: NonNullable<HistoryTraversal["pushState"]> = (
data,
unused,
url,
) => {
const nextIndex = (getCurrentIndex() ?? 0) + 1;
originalPushState.call(history, withIndex(data, nextIndex), unused, url);
};
const trackedReplaceState: NonNullable<HistoryTraversal["replaceState"]> = (
data,
unused,
url,
) => {
originalReplaceState.call(
history,
withIndex(data, getCurrentIndex() ?? 0),
unused,
url,
);
};
history.pushState = trackedPushState;
history.replaceState = trackedReplaceState;
return {
getCurrentIndex,
dispose() {
if (history.pushState === trackedPushState) {
history.pushState = originalPushState;
}
if (history.replaceState === trackedReplaceState) {
history.replaceState = originalReplaceState;
}
},
};
}
export function createSeededFormHistoryTraversalGuard({
history,
getCurrentIndex,
shouldBlock,
requestClose,
}: HistoryTraversalGuardOptions): SeededFormHistoryTraversalGuard {
const nativeIndex = getCurrentIndex?.();
const indexTracker =
nativeIndex === undefined
? createHistoryEntryIndexTracker(history)
: undefined;
const currentIndex =
indexTracker === undefined
? () => getCurrentIndex?.()
: indexTracker.getCurrentIndex;
const guardedIndex = currentIndex();
let allowNextTraversal = false;
let restoringRoute = false;
let promptOpen = false;
let confirmedWhileRestoring = false;
let confirmedTraversalDelta = -1;
const continueTraversal = () => {
promptOpen = false;
if (restoringRoute) {
confirmedWhileRestoring = true;
return;
}
allowNextTraversal = true;
history.go(confirmedTraversalDelta);
};
return {
handlePopState(event) {
if (allowNextTraversal) {
allowNextTraversal = false;
return;
}
if (restoringRoute) {
event.stopImmediatePropagation();
restoringRoute = false;
if (confirmedWhileRestoring) {
confirmedWhileRestoring = false;
allowNextTraversal = true;
history.go(confirmedTraversalDelta);
}
return;
}
if (!shouldBlock()) return;
event.stopImmediatePropagation();
const traversedIndex = currentIndex();
const restorationDelta =
guardedIndex === undefined ||
traversedIndex === undefined ||
guardedIndex === traversedIndex
? 1
: guardedIndex - traversedIndex;
confirmedTraversalDelta = -restorationDelta;
restoringRoute = true;
history.go(restorationDelta);
if (promptOpen) return;
promptOpen = true;
if (!requestClose(continueTraversal)) {
promptOpen = false;
}
},
cancelPendingTraversal() {
promptOpen = false;
confirmedWhileRestoring = false;
},
dispose() {
indexTracker?.dispose();
},
};
}
export function safeInternalNavigationTarget(
href: string,
currentHref: string,
): string | null {
try {
const current = new URL(currentHref);
const target = new URL(href, current);
if (
target.protocol !== current.protocol ||
target.origin !== current.origin ||
target.username !== "" ||
target.password !== ""
) {
return null;
}
return `${target.pathname}${target.search}${target.hash}`;
} catch {
return null;
}
}
type UseSeededFormDismissalOptions<T> = {
seed: T;
value: T;
blocked?: boolean;
guardNavigation?: boolean;
navigate?: (target: string) => void;
};
export function useSeededFormDismissal<T>({
seed,
value,
blocked = false,
guardNavigation = false,
navigate,
}: UseSeededFormDismissalOptions<T>) {
const [discardDialogOpen, setDiscardDialogOpen] = useState(false);
const pendingCloseRef = useRef<CloseRequest | null>(null);
const historyTraversalGuardRef =
useRef<SeededFormHistoryTraversalGuard | null>(null);
const isDirty = !seededValuesEqual(seed, value);
const requestClose = useCallback(
(close: CloseRequest) => {
if (blocked) return false;
if (!isDirty) {
close();
return true;
}
pendingCloseRef.current = close;
setDiscardDialogOpen(true);
return true;
},
[blocked, isDirty],
);
const confirmDiscard = useCallback(() => {
if (blocked || !discardDialogOpen) return;
const close = pendingCloseRef.current;
pendingCloseRef.current = null;
setDiscardDialogOpen(false);
close?.();
}, [blocked, discardDialogOpen]);
const cancelDiscard = useCallback(() => {
historyTraversalGuardRef.current?.cancelPendingTraversal();
pendingCloseRef.current = null;
setDiscardDialogOpen(false);
}, []);
useEffect(() => {
if (!guardNavigation) return;
const navigation = (
window as Window & {
navigation?: { currentEntry?: { index: number } };
}
).navigation;
const historyTraversalGuard = createSeededFormHistoryTraversalGuard({
history: window.history,
getCurrentIndex: () => navigation?.currentEntry?.index,
shouldBlock: () => isDirty,
requestClose,
});
historyTraversalGuardRef.current = historyTraversalGuard;
const handlePopState = (event: PopStateEvent) => {
historyTraversalGuard.handlePopState(event);
};
window.addEventListener("popstate", handlePopState, true);
return () => {
window.removeEventListener("popstate", handlePopState, true);
historyTraversalGuard.dispose();
if (historyTraversalGuardRef.current === historyTraversalGuard) {
historyTraversalGuardRef.current = null;
}
};
}, [guardNavigation, isDirty, requestClose]);
useEffect(() => {
if (!guardNavigation || !isDirty) return;
const handleBeforeUnload = (event: BeforeUnloadEvent) => {
event.preventDefault();
event.returnValue = "";
};
const handleDocumentClick = (event: MouseEvent) => {
if (
event.defaultPrevented ||
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey ||
!(event.target instanceof Element)
) {
return;
}
const anchor = event.target.closest<HTMLAnchorElement>("a[href]");
if (
!anchor ||
anchor.hasAttribute("download") ||
(anchor.target !== "" && anchor.target !== "_self")
) {
return;
}
const target = safeInternalNavigationTarget(
anchor.href,
window.location.href,
);
if (target === null || navigate === undefined) return;
const targetUrl = new URL(target, window.location.origin);
const currentDocument = `${window.location.pathname}${window.location.search}`;
if (`${targetUrl.pathname}${targetUrl.search}` === currentDocument)
return;
event.preventDefault();
requestClose(() => navigate(target));
};
window.addEventListener("beforeunload", handleBeforeUnload);
document.addEventListener("click", handleDocumentClick, true);
return () => {
window.removeEventListener("beforeunload", handleBeforeUnload);
document.removeEventListener("click", handleDocumentClick, true);
};
}, [guardNavigation, isDirty, navigate, requestClose]);
return {
isDirty,
discardDialogOpen,
requestClose,
confirmDiscard,
cancelDiscard,
};
}
export function SeededFormDiscardDialog({
open,
onConfirm,
onCancel,
}: {
open: boolean;
onConfirm: () => void;
onCancel: () => void;
}) {
const { t } = useTranslations();
return (
<AlertDialog open={open} onOpenChange={(next) => !next && onCancel()}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("dashboard.quickEntryDiscard.title")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("dashboard.quickEntryDiscard.description")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>
{t("dashboard.quickEntryDiscard.cancel")}
</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm}>
{t("dashboard.quickEntryDiscard.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}