From 25f41f88fe0a5b82b89a603d5d65f9f2b58c34d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 11 Jun 2026 02:19:28 +0200 Subject: [PATCH] feat(ux,settings): undo where it was missing and settings that behave one way Deleting a measurement or mood entry can be undone from the toast via new restore endpoints, dialogs submit on enter with busy states, silent mutations gain error toasts, and the settings area finishes its consolidation: provider and integration cards live in focused subcomponents, every setting persists server-side at exactly one home, and the save conventions are written down. The remaining raw fetches in settings move onto the shared client. --- e2e/a11y.spec.ts | 4 ++-- e2e/admin-api-tokens-mobile.spec.ts | 16 ++++++++-------- e2e/chart-overlay-controls.spec.ts | 2 +- e2e/medications-wizard-daily.spec.ts | 2 +- e2e/medications-wizard-rolling.spec.ts | 2 +- e2e/sidebar-admin-no-expand.spec.ts | 2 +- eslint.config.mjs | 6 ++++++ src/__tests__/api/analytics/since-cap.test.ts | 4 ++-- src/__tests__/api/auth/check-user/route.test.ts | 6 +++--- src/__tests__/i18n-call-site-coverage.test.ts | 2 +- tests/integration/dashboard-widgets-save.test.ts | 2 +- tests/integration/doctor-report-sections.test.ts | 2 +- .../intake-scheduled-for-targeting.test.ts | 2 +- tests/integration/intake-slot-dedup.test.ts | 2 +- .../medication-cadence-worker.test.ts | 4 ++-- .../integration/medications-intake-bulk.test.ts | 2 +- vitest.config.mts | 4 ++++ 17 files changed, 37 insertions(+), 27 deletions(-) diff --git a/e2e/a11y.spec.ts b/e2e/a11y.spec.ts index 048c116fd..fa433f35a 100644 --- a/e2e/a11y.spec.ts +++ b/e2e/a11y.spec.ts @@ -116,7 +116,7 @@ test.describe("axe-core authenticated surfaces", () => { } // The keyboard-only "skip to content" link is fixed at the top-left - // corner of every authenticated page. Marc reported it intercepting + // corner of every authenticated page. The maintainer reported it intercepting // logo clicks in v1.4.14 — the fix combines `pointer-events-none` // with `-translate-y-full` so the element sits offscreen until // focused. Regression guard: with no keyboard focus, the logo's own @@ -127,7 +127,7 @@ test.describe("axe-core authenticated surfaces", () => { }) => { // The desktop sidebar (and its logo) is `hidden md:flex` — the // mobile profile (Pixel 5) hides it by design. The skip-link - // regression Marc originally reported in v1.4.14 was a desktop- + // regression the maintainer originally reported in v1.4.14 was a desktop- // only artifact (mobile uses the topbar logo, which is a // different DOM node). Skip the mobile project so we don't // flake on a contract that doesn't apply there. diff --git a/e2e/admin-api-tokens-mobile.spec.ts b/e2e/admin-api-tokens-mobile.spec.ts index 12fa90d04..08bef6dd2 100644 --- a/e2e/admin-api-tokens-mobile.spec.ts +++ b/e2e/admin-api-tokens-mobile.spec.ts @@ -5,7 +5,7 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup"; /** * v1.4.16 A3 — `/admin/api-tokens` must not produce horizontal page * overflow on a Pixel-5 viewport (393 CSS px). v1.4.15 added column- - * hide breakpoints + tighter card padding, but Marc still saw a + * hide breakpoints + tighter card padding, but the maintainer still saw a * scrollbar on prod. The v1.4.16 fix swaps the desktop `` for a * mobile card-list at `. * @@ -23,7 +23,7 @@ test.describe("/admin/api-tokens mobile no-overflow", () => { }) => { // Mock the admin tokens endpoint with a moderately wide payload — // a token with multiple permissions, a long name, and timestamps. - // This is the realistic worst case Marc hit on prod. + // This is the realistic worst case the maintainer hit on prod. await page.route("**/api/admin/tokens", (route) => route.fulfill({ status: 200, @@ -38,7 +38,7 @@ test.describe("/admin/api-tokens mobile no-overflow", () => { expiresAt: null, createdAt: "2026-05-01T08:00:00Z", revoked: false, - user: { id: "u1", username: "marc-the-very-long-username" }, + user: { id: "u1", username: "user-the-very-long-username" }, }, { id: "tok2", @@ -48,7 +48,7 @@ test.describe("/admin/api-tokens mobile no-overflow", () => { expiresAt: "2026-08-01T00:00:00Z", createdAt: "2026-04-15T08:00:00Z", revoked: false, - user: { id: "u1", username: "marc-the-very-long-username" }, + user: { id: "u1", username: "user-the-very-long-username" }, }, ], error: null, @@ -76,7 +76,7 @@ test.describe("/admin/api-tokens mobile no-overflow", () => { }); // v1.4.18 phase A2 — production probe at Pixel-5 viewport pinned the - // visible "scrollbar" Marc reported for the third time as the + // visible "scrollbar" the maintainer reported for the third time as the // `` mobile section strip, NOT the api-tokens table. The // strip carries 13 admin sections so its scrollWidth (~1700 CSS px) // permanently exceeds clientWidth (~360 CSS px). Adding the @@ -120,7 +120,7 @@ test.describe("/admin/api-tokens mobile no-overflow", () => { }); // v1.4.19 phase A7 (4th attempt) — even with the mobile-strip - // `no-scrollbar` fix from v1.4.18, Marc still reported a painted + // `no-scrollbar` fix from v1.4.18, the maintainer still reported a painted // scrollbar. The card body itself must not contribute any // horizontal-overflow culprit: every element inside the api-tokens // section must satisfy `scrollWidth <= clientWidth + 1`. Pads with @@ -149,7 +149,7 @@ test.describe("/admin/api-tokens mobile no-overflow", () => { revoked: false, user: { id: "u1", - username: "marc-the-very-long-username-that-overflows", + username: "user-the-very-long-username-that-overflows", }, }, ], @@ -166,7 +166,7 @@ test.describe("/admin/api-tokens mobile no-overflow", () => { // Walk every element inside the mobile card-list and assert no // child has a wider scrollWidth than its container. This is the - // Pixel-5 worst-case Marc kept hitting. + // Pixel-5 worst-case the maintainer kept hitting. // // Elements clamped via `truncate` (Tailwind's `overflow:hidden + // text-overflow:ellipsis + white-space:nowrap` shorthand) are by diff --git a/e2e/chart-overlay-controls.spec.ts b/e2e/chart-overlay-controls.spec.ts index ed7fa367a..2c4d5f8ef 100644 --- a/e2e/chart-overlay-controls.spec.ts +++ b/e2e/chart-overlay-controls.spec.ts @@ -9,7 +9,7 @@ import { /** * v1.4.18 — per-chart overlay-controls popover. * - * Marc reverted v1.4.16's always-on chart overlays (gradient fill, + * The maintainer reverted v1.4.16's always-on chart overlays (gradient fill, * personal-baseline line, target-zone shading) and asked for per-chart * switches that the user can flip on or off. This spec exercises the * end-to-end behaviour against the seeded test user: diff --git a/e2e/medications-wizard-daily.spec.ts b/e2e/medications-wizard-daily.spec.ts index 578ce5d89..65b23df62 100644 --- a/e2e/medications-wizard-daily.spec.ts +++ b/e2e/medications-wizard-daily.spec.ts @@ -22,7 +22,7 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup"; * * Walks the 7-step daily path through the modal dialog. The dialog * mounts when `/medications/new` redirects to `/medications?new=1`. - * The DE label surface (Marc's primary locale) is the assertion + * The DE label surface (the maintainer's primary locale) is the assertion * surface — "Schritt N von 7" tracks the visible counter. */ test.describe("medication wizard — daily", () => { diff --git a/e2e/medications-wizard-rolling.spec.ts b/e2e/medications-wizard-rolling.spec.ts index a720476d3..d98fad92b 100644 --- a/e2e/medications-wizard-rolling.spec.ts +++ b/e2e/medications-wizard-rolling.spec.ts @@ -23,7 +23,7 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup"; * Walks the 8-step recurring path with "Flexibel ab letzter * Einnahme" picked on Step 5 and rollingDays = 7 set on Step 6. * Emits `rollingIntervalDays: 7` (no rrule). The cadence row also - * surfaces Marc's literal "Counter startet neu" copy directly below + * surfaces the maintainer's literal "Counter startet neu" copy directly below * the row label — the assertion below pins the phrase so a future * copy edit can't silently drop the rolling explainer. */ diff --git a/e2e/sidebar-admin-no-expand.spec.ts b/e2e/sidebar-admin-no-expand.spec.ts index bc11d5e23..97051932c 100644 --- a/e2e/sidebar-admin-no-expand.spec.ts +++ b/e2e/sidebar-admin-no-expand.spec.ts @@ -5,7 +5,7 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup"; /** * v1.4.16 A1 — Sidebar Admin entry must mirror the Settings entry * exactly: a single link with no sub-item expansion in the global - * sidebar. Marc reported in v1.4.15 that the sidebar was auto-expanding + * sidebar. The maintainer reported in v1.4.15 that the sidebar was auto-expanding * admin sub-items on `/admin/*` and the gravatar dropdown felt linked to * the sidebar — both unwanted UX. The in-shell `` already * renders its own per-section nav inside the page itself. diff --git a/eslint.config.mjs b/eslint.config.mjs index a1d07e8eb..560d86560 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -38,6 +38,12 @@ const eslintConfig = defineConfig([ // requirePublicHost connect-time pin). The wrapper internals and // test files are exempt; see the rule file for the allowlist. "healthlog/safe-fetch-required": "error", + // v1.16.4 — same-origin `/api/...` calls in the client surface + // (src/components + src/app) must route through the typed apiFetch + // wrapper (`src/lib/api/api-fetch.ts`): one `.ok` check, one + // envelope unwrap, one ApiError shape. Wrapper internals and test + // files are exempt; see the rule file for the allowlist. + "healthlog/api-fetch-required": "error", }, }, ]); diff --git a/src/__tests__/api/analytics/since-cap.test.ts b/src/__tests__/api/analytics/since-cap.test.ts index 86259e984..e089b8aab 100644 --- a/src/__tests__/api/analytics/since-cap.test.ts +++ b/src/__tests__/api/analytics/since-cap.test.ts @@ -4,7 +4,7 @@ * * The v1.4.38.7 perf audit (`.planning/round-v1438-perf-analysis.md` * §1 A2) flagged the per-type `fetchMeasurementSeriesChunked` loop - * inside `/api/analytics` as the dominant cost of Marc's 74.6 s cold + * inside `/api/analytics` as the dominant cost of the maintainer's 74.6 s cold * full-slice mount. v1.4.38.8 (commit 8a8150d2) relaxed the * `isFullyCovered` gate inside the three downstream fast-paths so the * walk became unreachable in the common case — but the A2 loop itself @@ -129,7 +129,7 @@ const SESSION_OK = { session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) }, user: { id: "user-since-1", - username: "marc", + username: "testuser", role: "USER" as const, locale: "en", timezone: "Europe/Berlin", diff --git a/src/__tests__/api/auth/check-user/route.test.ts b/src/__tests__/api/auth/check-user/route.test.ts index b62d18636..7cdd433d0 100644 --- a/src/__tests__/api/auth/check-user/route.test.ts +++ b/src/__tests__/api/auth/check-user/route.test.ts @@ -119,7 +119,7 @@ describe("POST /api/auth/check-user", () => { passwordHash: null, _count: { passkeys: 2 }, } as never); - const res = await POST(makeRequest({ identifier: "marc" })); + const res = await POST(makeRequest({ identifier: "testuser" })); const body = (await res.json()) as CheckUserBody; expect(body.data.branch).toBe("passkey_only"); expect(body.data.hasPasskey).toBe(true); @@ -132,7 +132,7 @@ describe("POST /api/auth/check-user", () => { passwordHash: "argon2id$…", _count: { passkeys: 1 }, } as never); - const res = await POST(makeRequest({ identifier: "marc" })); + const res = await POST(makeRequest({ identifier: "testuser" })); const body = (await res.json()) as CheckUserBody; expect(body.data.branch).toBe("email_fallback"); expect(body.data.hasPasskey).toBe(true); @@ -145,7 +145,7 @@ describe("POST /api/auth/check-user", () => { passwordHash: null, _count: { passkeys: 0 }, } as never); - const res = await POST(makeRequest({ identifier: "marc" })); + const res = await POST(makeRequest({ identifier: "testuser" })); const body = (await res.json()) as CheckUserBody; expect(body.data.branch).toBe("exists"); expect(body.data.hasPasskey).toBe(false); diff --git a/src/__tests__/i18n-call-site-coverage.test.ts b/src/__tests__/i18n-call-site-coverage.test.ts index 3ef042678..8e461b4ae 100644 --- a/src/__tests__/i18n-call-site-coverage.test.ts +++ b/src/__tests__/i18n-call-site-coverage.test.ts @@ -1,7 +1,7 @@ /** * v1.4.49.3 — call-site i18n coverage guard. * - * Marc surfaced raw `insights.relativeHoursAgo`, `notifications.event + * The maintainer surfaced raw `insights.relativeHoursAgo`, `notifications.event * MoodReminder`, and `notifications.eventMoodReminderDesc` strings * leaking into the UI in v1.4.49. Audit found 28 keys called from real * code that never existed in any locale bundle — silent regressions diff --git a/tests/integration/dashboard-widgets-save.test.ts b/tests/integration/dashboard-widgets-save.test.ts index 1c774b107..12122f694 100644 --- a/tests/integration/dashboard-widgets-save.test.ts +++ b/tests/integration/dashboard-widgets-save.test.ts @@ -339,7 +339,7 @@ describe("PUT /api/dashboard/widgets — Save persists full layouts", () => { const { userId } = await seedUser(); const { PUT, GET } = await import("@/app/api/dashboard/widgets/route"); - // Marc flips the sleep widget's chart visibility on. + // The maintainer flips the sleep widget's chart visibility on. const body = buildClientPayload({ chartOverlayPrefs: { bp: { diff --git a/tests/integration/doctor-report-sections.test.ts b/tests/integration/doctor-report-sections.test.ts index a905ce4f6..f702eabd8 100644 --- a/tests/integration/doctor-report-sections.test.ts +++ b/tests/integration/doctor-report-sections.test.ts @@ -230,7 +230,7 @@ describe("doctor-report — per-section toggles", () => { const data = await collectDoctorReportData(userId, RANGE); - // Privacy default per Marc — mood is opt-in, not opt-out. + // Privacy default per the maintainer — mood is opt-in, not opt-out. expect(data.mood).toBeNull(); // Every other section keeps its data. expect(data.stats.WEIGHT).toBeDefined(); diff --git a/tests/integration/intake-scheduled-for-targeting.test.ts b/tests/integration/intake-scheduled-for-targeting.test.ts index 959747eeb..aa27a306e 100644 --- a/tests/integration/intake-scheduled-for-targeting.test.ts +++ b/tests/integration/intake-scheduled-for-targeting.test.ts @@ -24,7 +24,7 @@ import { applyCanonicalSlotWrite, resolveSlotInstantForWrite, } from "@/lib/medications/scheduling/slot-upsert"; -import { localHmAsUtc } from "@/lib/timezone"; +import { localHmAsUtc } from "@/lib/tz/local-day"; const TEST_USER_ID = "user-intake-scheduledfor-targeting"; diff --git a/tests/integration/intake-slot-dedup.test.ts b/tests/integration/intake-slot-dedup.test.ts index f04478697..c3f69246a 100644 --- a/tests/integration/intake-slot-dedup.test.ts +++ b/tests/integration/intake-slot-dedup.test.ts @@ -29,7 +29,7 @@ import { applyCanonicalSlotWrite, resolveSlotInstantForWrite, } from "@/lib/medications/scheduling/slot-upsert"; -import { localHmAsUtc } from "@/lib/timezone"; +import { localHmAsUtc } from "@/lib/tz/local-day"; const TEST_USER_ID = "user-intake-slot-dedup"; const OTHER_USER_ID = "user-intake-slot-dedup-clean"; diff --git a/tests/integration/medication-cadence-worker.test.ts b/tests/integration/medication-cadence-worker.test.ts index 3b9090e26..95fe6a847 100644 --- a/tests/integration/medication-cadence-worker.test.ts +++ b/tests/integration/medication-cadence-worker.test.ts @@ -41,7 +41,7 @@ import { buildRecurrenceContext, scheduleEmitsInWindow, } from "@/lib/medications/scheduling/worker-helpers"; -import { localHmAsUtc } from "@/lib/timezone"; +import { localHmAsUtc } from "@/lib/tz/local-day"; const TEST_USER_ID = "user-medication-cadence-worker"; @@ -131,7 +131,7 @@ async function loadCanonical(medicationId: string): Promise<{ /** * Compute the user-tz today bounds for an arbitrary "now" instant, - * matching `getUserTodayBounds` in `src/lib/timezone.ts` so the test + * matching `getUserTodayBounds` in `src/lib/tz/local-day.ts` so the test * window matches what the worker would build on that tick. */ // UTC midnight on the local calendar day that `now` falls within for `tz`. diff --git a/tests/integration/medications-intake-bulk.test.ts b/tests/integration/medications-intake-bulk.test.ts index 2986d521e..df18caf3f 100644 --- a/tests/integration/medications-intake-bulk.test.ts +++ b/tests/integration/medications-intake-bulk.test.ts @@ -12,7 +12,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { cookieJar } from "./mock-next-headers"; import { getPrismaClient, truncateAllTables } from "./setup"; -import { localHmAsUtc } from "@/lib/timezone"; +import { localHmAsUtc } from "@/lib/tz/local-day"; const TEST_USER_ID = "user-medications-intake-bulk"; const OTHER_USER_ID = "user-medications-intake-other"; diff --git a/vitest.config.mts b/vitest.config.mts index c0f5cfb44..6972701c9 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -9,6 +9,10 @@ export default defineConfig({ test: { globals: true, environment: "node", + // Primes the i18n locale cache so provider mounts resolve every + // locale synchronously (production gets the active bundle as an RSC + // prop instead — see src/lib/i18n/load-locale.ts). + setupFiles: ["./vitest.setup.ts"], // Default `pnpm test` keeps unit tests only — integration suite uses // testcontainers and runs separately via `pnpm test:integration`. exclude: [