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: [