diff --git a/CHANGELOG.md b/CHANGELOG.md index aae6e2bf0..c758d3ce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,16 @@ ## [Unreleased] +## [1.31.8] — 2026-07-21 + - **MCP search cursors keep lab results stable across pages.** Distinct analyte names are ordered before offset pagination, preventing repeated or skipped lab entries when a client follows `nextCursor`. +- **Opening the medication wizard no longer risks clearing entered fields.** + The one-shot URL flag is removed without starting a second page navigation, + so slower browsers keep the active wizard instance and its form state. + +No migrations. No breaking changes. ## [1.31.7] — 2026-07-20 diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 41a360d09..55d0cccfb 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.31.7 + version: 1.31.8 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/e2e/medications-wizard-daily.spec.ts b/e2e/medications-wizard-daily.spec.ts index 5bdb3855a..e6d773dd4 100644 --- a/e2e/medications-wizard-daily.spec.ts +++ b/e2e/medications-wizard-daily.spec.ts @@ -20,8 +20,9 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup"; /** * v1.5.4 — Medication wizard end-to-end — daily cadence. * - * Walks the 7-step daily path through the modal dialog. The dialog - * mounts when `/medications/new` redirects to `/medications?new=1`. + * Walks the 7-step daily path through the modal dialog. The shared helper + * opens `/medications?new=1` directly so cadence coverage is independent of + * the legacy `/medications/new` redirect. * The DE label surface (the maintainer's primary locale) is the assertion * surface — "Schritt N von 7" tracks the visible counter. */ diff --git a/e2e/medications-wizard-helpers.ts b/e2e/medications-wizard-helpers.ts index c2c236e17..1eba526d0 100644 --- a/e2e/medications-wizard-helpers.ts +++ b/e2e/medications-wizard-helpers.ts @@ -120,8 +120,9 @@ export async function stubDashboardAnalytics(page: Page): Promise { } /** - * Open the wizard dialog by hitting `/medications/new` (which - * redirects to `/medications?new=1` and opens the create dialog). + * Open the wizard directly on `/medications?new=1`. The redirect from the + * legacy `/medications/new` route is a separate concern; these cadence specs + * exercise the wizard without racing that server navigation. */ export async function openCreateWizard(page: Page): Promise { // Pin the German locale for the wizard flow. The app default locale is @@ -141,7 +142,11 @@ export async function openCreateWizard(page: Page): Promise { : "http://localhost:3000", }, ]); - await page.goto("/medications/new", { waitUntil: "domcontentloaded" }); + await page.goto("/medications?new=1", { waitUntil: "load" }); + await page.waitForURL( + (url) => url.pathname === "/medications" && !url.searchParams.has("new"), + { timeout: 10_000 }, + ); await expect( page.locator('[data-slot="medication-wizard-dialog"]'), ).toBeVisible({ timeout: 10_000 }); diff --git a/e2e/medications-wizard-oneshot.spec.ts b/e2e/medications-wizard-oneshot.spec.ts index e341811ba..d851f9a4a 100644 --- a/e2e/medications-wizard-oneshot.spec.ts +++ b/e2e/medications-wizard-oneshot.spec.ts @@ -33,6 +33,29 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup"; test.describe("medication wizard — one-shot", () => { test.use({ storageState: STORAGE_STATE_PATH }); + test("does not replay the one-shot URL flag after history traversal", async ({ + page, + }) => { + await stubDashboardAnalytics(page); + await openCreateWizard(page); + + await page.keyboard.press("Escape"); + await expect( + page.locator('[data-slot="medication-wizard-dialog"]'), + ).toHaveCount(0); + + await page.locator('a[href="/"]:visible').first().click(); + await page.waitForURL((url) => url.pathname === "/"); + await page.goBack(); + await page.waitForURL( + (url) => url.pathname === "/medications" && !url.searchParams.has("new"), + ); + + await expect( + page.locator('[data-slot="medication-wizard-dialog"]'), + ).toHaveCount(0); + }); + test("creates a single-dose medication end-to-end", async ({ page }) => { test.slow(); await stubDashboardAnalytics(page); diff --git a/package.json b/package.json index 83afa99f9..81abef708 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.31.7", + "version": "1.31.8", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "PolyForm-Noncommercial-1.0.0", "homepage": "https://healthlog.dev", diff --git a/public/sw.js b/public/sw.js index 0718f9aa0..3b2228f2f 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.31.7"; + /* @sw-version-fallback */ "v1.31.8"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*` diff --git a/src/app/medications/page-client.tsx b/src/app/medications/page-client.tsx index 63d2542a0..9e9483bca 100644 --- a/src/app/medications/page-client.tsx +++ b/src/app/medications/page-client.tsx @@ -198,12 +198,27 @@ export default function MedicationsPageClient() { const { layout, isLayoutLoading } = useMedicationListLayout(isAuthenticated); useEffect(() => { - if (shouldOpenFromUrl) { - // Drop the query param so a refresh after closing the dialog - // doesn't keep reopening it. - router.replace("/medications"); - } - }, [shouldOpenFromUrl, router]); + if (!shouldOpenFromUrl) return; + + // Child effects may run before the App Router installs its native-history + // integration, while the responsive sheet also settles its first client + // layout. Wait until the following painted frame so replaceState preserves + // Next's internals and synchronizes useSearchParams without starting a + // router navigation that can remount the open wizard and discard fields. + let cleanupFrame: number | undefined; + const layoutFrame = window.requestAnimationFrame(() => { + cleanupFrame = window.requestAnimationFrame(() => { + window.history.replaceState(null, "", "/medications"); + }); + }); + + return () => { + window.cancelAnimationFrame(layoutFrame); + if (cleanupFrame !== undefined) { + window.cancelAnimationFrame(cleanupFrame); + } + }; + }, [shouldOpenFromUrl]); const { data: medications,