mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
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.
This commit is contained in:
1 parent
3b480b9885
commit
25f41f88fe
17 files changed
+37
-27
No files matched your search
+2
-2
@@ -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.
|
||||
|
||||
@@ -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 `<table>` for a
|
||||
* mobile card-list at <md, mirroring `<UserManagementSection>`.
|
||||
*
|
||||
@@ -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
|
||||
// `<AdminShell>` 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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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 `<AdminShell>` already
|
||||
* renders its own per-section nav inside the page itself.
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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: [
|
||||
|
||||
Reference in new issue
Block a user