mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
fix(release): ssr-safe add-param, noon-anchored compliance days, localized wizard assertion
The measurements page consumed its add parameter during render, which crashed server rendering now that every route renders dynamically with real search params. Compliance day buckets anchored to the current wall-clock minus twelve hours, so before local noon the current day was missing from every bucket series — they anchor on local noon now. The wizard spec accepts the localized start date its summary deliberately shows, and the generated contract picks up the version bump.
This commit is contained in:
1 parent
d56cf70d7c
commit
5778dd0be0
4 files changed
+42
-15
No files matched your search
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: HealthLog API
|
||||
version: 1.16.3
|
||||
version: 1.16.4
|
||||
description: >-
|
||||
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
|
||||
|
||||
|
||||
@@ -78,7 +78,9 @@ test.describe("medication wizard — one-shot", () => {
|
||||
await expectStep(page, 5, 5);
|
||||
const summary = page.locator('[data-slot="wizard-summary"]');
|
||||
await expect(summary).toContainText(/Einmaldosis/i);
|
||||
await expect(summary).toContainText(/2026-10-15/);
|
||||
// The summary renders the date through the locale-aware formatter
|
||||
// (DE UI → "15.10.2026"), not the ISO value the input carries.
|
||||
await expect(summary).toContainText(/15\.10\.2026/);
|
||||
|
||||
await clickSave(page);
|
||||
|
||||
|
||||
@@ -278,12 +278,18 @@ async function buildScheduleAnchoredComplianceBuckets(
|
||||
});
|
||||
|
||||
// Pre-compute each local day's [start, end) bounds + key, oldest → newest.
|
||||
// Each day's representative instant anchors on the user's LOCAL NOON of
|
||||
// the current day and steps back in 24-hour increments: noon stays inside
|
||||
// the intended local day across DST shifts (23/25-hour days), and —
|
||||
// unlike the previous `now − 12 h` anchor — it cannot slip into
|
||||
// yesterday when the user's local time is before noon, which silently
|
||||
// dropped TODAY from the buckets for every pre-noon read.
|
||||
const { start: todayStart } = getUserTodayBounds(now, userTz);
|
||||
const todayNoonMs = todayStart.getTime() + 12 * 60 * 60 * 1000;
|
||||
const dayKeys: string[] = [];
|
||||
const dayBounds = new Map<string, { start: Date; end: Date }>();
|
||||
for (let i = days - 1; i >= 0; i--) {
|
||||
const representative = new Date(
|
||||
nowMs - i * 86_400_000 - 12 * 60 * 60 * 1000,
|
||||
);
|
||||
const representative = new Date(todayNoonMs - i * 86_400_000);
|
||||
const { start: dayStart, end: dayEndInclusive } = getUserTodayBounds(
|
||||
representative,
|
||||
userTz,
|
||||
|
||||
@@ -27,15 +27,27 @@ export default function MeasurementsPage() {
|
||||
|
||||
// v1.4.27 MB6 — read the `?add=<TYPE>` query param during render so
|
||||
// the dialog opens on the first paint (no flash of the page behind
|
||||
// it). The state-from-prop pattern follows `account-section.tsx`:
|
||||
// store the "addToken we acted on" sentinel and let render kick off
|
||||
// the open + replace transition once per param value. The lint rule
|
||||
// it). The first-load param is consumed in the `useState` lazy
|
||||
// initializers: they run identically on the server and the hydration
|
||||
// render (pure token resolution, no router call), so SSR — which sees
|
||||
// real search params now that the root layout renders every route
|
||||
// dynamically — stays side-effect free. Calling `router.replace`
|
||||
// during a server render throws (`location is not defined`); the
|
||||
// query-strip lives in the effect below instead. The render-driven
|
||||
// block then only handles LATER client-side navigations back to
|
||||
// `?add=` (e.g. the insights empty-state CTAs), following the
|
||||
// state-from-prop pattern in `account-section.tsx` — the lint rule
|
||||
// `react-hooks/set-state-in-effect` rejects setState from inside an
|
||||
// effect, so the open-on-param work is render-driven instead.
|
||||
// effect, so that path stays render-driven.
|
||||
const addParam = searchParams.get("add");
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [defaultType, setDefaultType] = useState<string | undefined>(undefined);
|
||||
const [consumedAddParam, setConsumedAddParam] = useState<string | null>(null);
|
||||
const initialAdd = addParam ? resolveAddToken(addParam) : null;
|
||||
const [dialogOpen, setDialogOpen] = useState(() => initialAdd != null);
|
||||
const [defaultType, setDefaultType] = useState<string | undefined>(
|
||||
() => initialAdd ?? undefined,
|
||||
);
|
||||
const [consumedAddParam, setConsumedAddParam] = useState<string | null>(
|
||||
() => addParam,
|
||||
);
|
||||
// v1.4.27 R4 RC2 — DOM handle the form portals its action row into so
|
||||
// the Sheet branch can sticky-pin Save / Cancel.
|
||||
const [footerEl, setFooterEl] = useState<HTMLDivElement | null>(null);
|
||||
@@ -53,6 +65,9 @@ export default function MeasurementsPage() {
|
||||
disabled: dialogOpen,
|
||||
});
|
||||
|
||||
// Later client-side navigation landed on `?add=` again (the first
|
||||
// load is consumed by the initializers above, so on the server this
|
||||
// is always a no-op: `consumedAddParam` starts as the param itself).
|
||||
if (addParam && addParam !== consumedAddParam) {
|
||||
setConsumedAddParam(addParam);
|
||||
// v1.4.34 IW-G — `resolveAddToken` is the single source of truth
|
||||
@@ -64,11 +79,15 @@ export default function MeasurementsPage() {
|
||||
setDefaultType(resolved);
|
||||
setDialogOpen(true);
|
||||
}
|
||||
// Drop the query string so the back-button leaves the user on
|
||||
// `/measurements` rather than re-opening the dialog.
|
||||
router.replace("/measurements");
|
||||
}
|
||||
|
||||
// Drop the query string so the back-button leaves the user on
|
||||
// `/measurements` rather than re-opening the dialog. Effect-driven
|
||||
// (client-only) because `router.replace` is not callable during SSR.
|
||||
useEffect(() => {
|
||||
if (addParam) router.replace("/measurements");
|
||||
}, [addParam, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isAuthenticated) {
|
||||
router.push("/auth/login");
|
||||
|
||||
Reference in new issue
Block a user