mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
chore: strip planning tags from comments and enable the rule
Output of `node scripts/strip-planning-tags.mjs --write`: 3,157 comment lines in 1,308 files lose the work-item identifier at their head. Release versions and issue references stay; nothing outside comment text changes, which the script checks per file against a comment-free printout. healthlog/no-planning-tags is enabled at error level with this commit, so the tree it lands on is clean.
This commit is contained in:
1 parent
892a819060
commit
fea1d1e0b5
1308 files changed
+3163
-3157
No files matched your search
@@ -3,7 +3,7 @@ import { expect, test } from "./setup/test";
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* v1.4.15 phase-B4 — /achievements navigation smoke.
|
||||
* v1.4.15 — /achievements navigation smoke.
|
||||
*
|
||||
* The page itself has unit-test coverage for the locked-vs-unlocked
|
||||
* card render; this spec answers the higher-level "can the user reach
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "./setup/test";
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* v1.4.16 A3 — `/admin/api-tokens` must not produce horizontal page
|
||||
* v1.4.16 — `/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 the maintainer still saw a
|
||||
* scrollbar on prod. The v1.4.16 fix swaps the desktop `<table>` for a
|
||||
@@ -75,7 +75,7 @@ test.describe("/admin/api-tokens mobile no-overflow", () => {
|
||||
).toBeLessThanOrEqual(dims.innerWidth + 1);
|
||||
});
|
||||
|
||||
// v1.4.18 phase A2 — production probe at Pixel-5 viewport pinned the
|
||||
// v1.4.18 — production probe at Pixel-5 viewport pinned 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)
|
||||
@@ -119,7 +119,7 @@ test.describe("/admin/api-tokens mobile no-overflow", () => {
|
||||
expect(stripDims.scrollWidth).toBeGreaterThan(stripDims.clientWidth);
|
||||
});
|
||||
|
||||
// v1.4.19 phase A7 (4th attempt) — even with the mobile-strip
|
||||
// v1.4.19 (4th attempt) — even with the mobile-strip
|
||||
// `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
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "./setup/test";
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* v1.4.16 phase B2 — AI provider settings UX with single dropdown
|
||||
* v1.4.16 — AI provider settings UX with single dropdown
|
||||
* driving the form below. Three things to lock in here:
|
||||
*
|
||||
* 1. /settings/ai exposes the active-provider Pulldown
|
||||
|
||||
@@ -4,7 +4,7 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
import { mockDashboardSnapshot } from "./utils/mock-dashboard-snapshot";
|
||||
|
||||
/**
|
||||
* v1.4.19 A2 — mobile-viewport regression spec for chart cards.
|
||||
* v1.4.19 — mobile-viewport regression spec for chart cards.
|
||||
*
|
||||
* Two assertions per chart visible in the dashboard's initial paint:
|
||||
*
|
||||
|
||||
@@ -98,7 +98,7 @@ test.describe("authenticated dashboard render", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
// S2 — the Today hero reads the unified daily digest from
|
||||
// The Today hero reads the unified daily digest from
|
||||
// `/api/daily/digest`. Mock it with a deterministic digest (a score +
|
||||
// one worth-a-look item) so the hero paints without reaching the real
|
||||
// route, and so a slow/absent digest can never hang the render or
|
||||
@@ -276,7 +276,7 @@ test.describe("authenticated dashboard render", () => {
|
||||
const tileCount = Number(await strip.getAttribute("data-tile-count"));
|
||||
expect(tileCount).toBeGreaterThan(0);
|
||||
|
||||
// S2 — the Today hero paints above the tile strip from the mocked
|
||||
// The Today hero paints above the tile strip from the mocked
|
||||
// digest: its score ring and its worth-a-look rail render.
|
||||
const todayHero = page.locator('[data-slot="today-hero"]');
|
||||
await expect(todayHero).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "./setup/test";
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* v1.4.28 R3a FB-D3 — `/insights` scroll restoration + tab-strip
|
||||
* v1.4.28 — `/insights` scroll restoration + tab-strip
|
||||
* gesture handling.
|
||||
*
|
||||
* Two regressions the maintainer hit walking through v1.4.27:
|
||||
|
||||
@@ -132,7 +132,7 @@ test.describe("add measurement flow", () => {
|
||||
|
||||
await openMenu(page, addTrigger);
|
||||
|
||||
// v1.5 phase-5: the menu items now have distinct labels — the
|
||||
// v1.5: the menu items now have distinct labels — the
|
||||
// measurement entry says "Measurement" / "Messung" instead of "Add",
|
||||
// so we can target it directly.
|
||||
// The desktop rail opens a dropdown, the phone shell the capture sheet
|
||||
|
||||
@@ -41,7 +41,7 @@ test.describe("mobile-viewport smoke", () => {
|
||||
// mocks below stay for the reversible `=false` path.
|
||||
await mockDashboardSnapshot(page, { summaries: WEIGHT_ONLY_SUMMARIES });
|
||||
|
||||
// v1.4.37 W-CI — match `/api/analytics` AND any sliced variant
|
||||
// v1.4.37 — match `/api/analytics` AND any sliced variant
|
||||
// (`?slice=summaries`). The previous string glob `**/api/analytics`
|
||||
// didn't match the query-string form so the IW1 slim-slice fetch
|
||||
// landed on the unmocked real route in CI.
|
||||
@@ -149,7 +149,7 @@ test.describe("mobile-viewport smoke", () => {
|
||||
// scroll to interact, which is its own UX problem the design
|
||||
// review handles.
|
||||
if (box.y < 0 || box.y > viewportH) continue;
|
||||
// v1.4.37 W-CI — enforce the WCAG 2.5.5 height floor only.
|
||||
// v1.4.37 — enforce the WCAG 2.5.5 height floor only.
|
||||
// The original `width < 44 || height < 44` check flagged
|
||||
// grouped compact controls (chart-range tabs like "7T"/"30T"
|
||||
// /"90T" sitting at 44 px tall but ~30-40 px wide) as
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "./setup/test";
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* v1.4.15 phase-A3 fix #2 — mood-list mobile card was rendering the score
|
||||
* v1.4.15 #2 — mood-list mobile card was rendering the score
|
||||
* twice: once in the left badge (large bold digit) and once in the title
|
||||
* line as "{score} ({label})". On desktop the table view only ever showed
|
||||
* one. The fix collapses the title line down to just the localized label
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "./utils/mock-dashboard-snapshot";
|
||||
|
||||
/**
|
||||
* v1.4.15 phase-A3 fix #3 — onboarding card flicker on dashboard load.
|
||||
* v1.4.15 #3 — onboarding card flicker on dashboard load.
|
||||
*
|
||||
* The previous version of `<GettingStartedChecklist>` rendered against a
|
||||
* default-true `shouldShowChecklist` decision while `measurementCount`
|
||||
@@ -64,7 +64,7 @@ test.describe("dashboard onboarding card flicker guard", () => {
|
||||
// works by NOT rendering until this resolves, so even with the
|
||||
// delay the card stays invisible the whole time.
|
||||
//
|
||||
// v1.4.37 W-CI — match `/api/analytics` AND any sliced variant
|
||||
// v1.4.37 — match `/api/analytics` AND any sliced variant
|
||||
// (`?slice=summaries`). The previous string glob `**/api/analytics`
|
||||
// missed the IW1 slim-slice URL so the checklist hook's analytics
|
||||
// query fell through to the real route in CI; on a fresh seed user
|
||||
@@ -161,7 +161,7 @@ test.describe("dashboard onboarding card flicker guard", () => {
|
||||
// card mounts collapsed: header + progress meter visible, full row
|
||||
// list hidden until the user clicks the chevron.
|
||||
//
|
||||
// v1.4.22 C4 — the onboarding redirect now lives in `proxy.ts`,
|
||||
// v1.4.22 — the onboarding redirect now lives in `proxy.ts`,
|
||||
// not in `<AuthShell>`. The proxy reads the `hl_onboarding` cookie
|
||||
// (which the auth routes mirror from the DB). Our storageState
|
||||
// belongs to a fully-onboarded user (no `hl_onboarding` cookie),
|
||||
|
||||
@@ -56,7 +56,7 @@ test.describe("Settings → Export & Import", () => {
|
||||
|
||||
test("renders all export surfaces with stable testids", async ({ page }) => {
|
||||
await page.goto("/settings/export", { waitUntil: "domcontentloaded" });
|
||||
// v1.18.0 (S5) — the full health-record export moved to its own
|
||||
// v1.18.0 — the full health-record export moved to its own
|
||||
// `/settings/gesundheitsakte` section. The Export & Import page keeps
|
||||
// the four CSV/backup tiles with the `export-card-*` shape.
|
||||
for (const id of [
|
||||
@@ -72,7 +72,7 @@ test.describe("Settings → Export & Import", () => {
|
||||
test("renders the health-record export panel on its own section", async ({
|
||||
page,
|
||||
}) => {
|
||||
// v1.18.0 (S5) — the health-record export is the hero of the
|
||||
// v1.18.0 — the health-record export is the hero of the
|
||||
// dedicated, module-gated Gesundheitsakte section.
|
||||
await openGesundheitsakte(page);
|
||||
await expect(page.getByTestId("health-record-export-panel")).toBeVisible({
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
WEIGHT_ONLY_SUMMARIES,
|
||||
} from "./utils/mock-dashboard-snapshot";
|
||||
/**
|
||||
* v1.4.19 A6 — Settings consistency snapshot at Pixel-5 (393 CSS px).
|
||||
* v1.4.19 — Settings consistency snapshot at Pixel-5 (393 CSS px).
|
||||
* v1.4.27 — input height floor lifted from 36 px (`h-9`) to 40 px
|
||||
* (`h-10`) on `Input`, `Select` trigger, and the native `<select>`
|
||||
* primitives per the WCAG 2.5.5 tap-target sweep (MB2). The
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.39 (C2) — axe over every setup screen, at both viewports, plus one
|
||||
* v1.39 — axe over every setup screen, at both viewports, plus one
|
||||
* keyboard-only pass through a question.
|
||||
*
|
||||
* The pattern is `a11y.spec.ts`: a scan waits for something the SCREEN
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.39 (C2) — the setup flow, path 1: blood pressure and weight, typed in.
|
||||
* v1.39 — the setup flow, path 1: blood pressure and weight, typed in.
|
||||
*
|
||||
* From the welcome screen to the dashboard on one account of its own: the
|
||||
* questions, leaving after Q3 and coming back to Q4, the units question that
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.39 (C2) — the setup flow, path 4: a child's managed profile.
|
||||
* v1.39 — the setup flow, path 4: a child's managed profile.
|
||||
*
|
||||
* Q1 "someone I look after" keeps the questions in the guardian's own record
|
||||
* and creates the profile on the confirm screen, after the questions — the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.39 (C2) — driving the setup flow in a browser, shared by its journeys.
|
||||
* v1.39 — driving the setup flow in a browser, shared by its journeys.
|
||||
*
|
||||
* Everything here addresses stable `data-slot` / `data-*` attributes, never
|
||||
* viewport text: the copy is i18n-driven and one of the specs runs every
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.39 (C2) — every setup screen, in every locale, with 30% longer strings.
|
||||
* v1.39 — every setup screen, in every locale, with 30% longer strings.
|
||||
*
|
||||
* The design spec asks for 30–50% string headroom in every layout, and the
|
||||
* seven bundles are the floor, not the ceiling: a German label that fits
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.39 (C2) — the setup flow, path 2: a daily medication with a reminder.
|
||||
* v1.39 — the setup flow, path 2: a daily medication with a reminder.
|
||||
*
|
||||
* Q3 "yes" is the one answer that makes the first medication the task the
|
||||
* flow ends on, ahead of a reading; every other question is passed, which is
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.39 (C2) — the setup flow, path 3: a doctor's visit within a month, with
|
||||
* v1.39 — the setup flow, path 3: a doctor's visit within a month, with
|
||||
* lab results.
|
||||
*
|
||||
* The visit answer is what switches the doctor report on and puts "prepare
|
||||
|
||||
@@ -585,7 +585,7 @@ export const SEAM_BANNERS_STORAGE_STATE_PATH = resolve(
|
||||
);
|
||||
|
||||
/**
|
||||
* v1.39 (C2) — the setup flow's own accounts, one per journey.
|
||||
* v1.39 — the setup flow's own accounts, one per journey.
|
||||
*
|
||||
* Each journey walks the questions from the welcome screen to the dashboard
|
||||
* and asserts what the walk wrote: the module map, the tile order, the
|
||||
@@ -1550,7 +1550,7 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
|
||||
// can reuse them via `test.use({ storageState })`. This is the documented
|
||||
// Playwright pattern (`docs/auth.md`) and the only way to avoid the
|
||||
// per-spec login + rate-limit dance.
|
||||
// v1.39 (C2) — the setup flow's accounts. `onboarding_completed_at` NULL
|
||||
// v1.39 — the setup flow's accounts. `onboarding_completed_at` NULL
|
||||
// is the whole point: it is what lands them on `/onboarding`. The tour
|
||||
// flag stays true for the reason every other fixture sets it. Whatever a
|
||||
// previous run wrote — the answers row, the module map, the seeded
|
||||
@@ -1693,7 +1693,7 @@ export default async function globalSetup(config: FullConfig): Promise<void> {
|
||||
`UPDATE users SET totp_confirmed_at = $2 WHERE username = $1`,
|
||||
[E2E_GUARDIAN.username, now],
|
||||
);
|
||||
// v1.39 (C2) — the setup flow's jars, one per journey and one per sweep.
|
||||
// v1.39 — the setup flow's jars, one per journey and one per sweep.
|
||||
// The child journey's factor is stamped after its capture, for the reason
|
||||
// the guardian's is: the password-only capture cannot pass a TOTP login.
|
||||
await capture(E2E_SETUP_BP, SETUP_BP_STORAGE_STATE_PATH);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.39 (C2) — the state the setup-flow journeys start from, and nothing else.
|
||||
* v1.39 — the state the setup-flow journeys start from, and nothing else.
|
||||
*
|
||||
* Each journey owns one account and walks it from the welcome screen to the
|
||||
* dashboard. What a walk leaves behind — the answers row, the completion
|
||||
|
||||
@@ -199,7 +199,7 @@ export const CONTENT_DOC_ID = "e2evaultcontent00000001";
|
||||
export const CONTENT_BODY_WORD = "pneumothorax";
|
||||
|
||||
/**
|
||||
* v1.28 (Phase 3) — a fixed trio the clinician-share e2e attaches to a link and
|
||||
* v1.28 — a fixed trio the clinician-share e2e attaches to a link and
|
||||
* serves through the public `/c/<token>/d/<id>` route:
|
||||
* - a PDF (Class A, inline preview via <iframe>),
|
||||
* - a JPEG carrying an Exif GPS marker (Class A image, inline <img>; the
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "./setup/test";
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* v1.4.16 A1 — Sidebar Admin entry must mirror the Settings entry
|
||||
* v1.4.16 — Sidebar Admin entry must mirror the Settings entry
|
||||
* exactly: a single link with no sub-item expansion in the global
|
||||
* 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
|
||||
|
||||
@@ -396,7 +396,7 @@ test.describe.serial("FENCE record-session fence in the browser", () => {
|
||||
const held = deferred();
|
||||
// The commit is observable: flag once the server round-trip inside the
|
||||
// route handler finishes, and poll for it instead of a fixed sleep
|
||||
// (C3) — the tab must close AFTER the write landed, and this waits
|
||||
// The tab must close AFTER the write landed, and this waits
|
||||
// exactly that long.
|
||||
let switchCommitted = false;
|
||||
await initiator.route("**/api/account/switch", async (route) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "./setup/test";
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* v1.4.27 B3 — admin login-overview carrier chip + CSV column.
|
||||
* v1.4.27 — admin login-overview carrier chip + CSV column.
|
||||
*
|
||||
* Two contracts under test:
|
||||
*
|
||||
|
||||
@@ -11,7 +11,7 @@ import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
|
||||
* 1. The floating Coach FAB (`data-slot="coach-fab"`) is the single
|
||||
* Coach launcher on every authenticated page. The per-metric inline
|
||||
* launch icon the sub-page header used to mount was retired in
|
||||
* CCH-04 — the FAB is now the only entry. An unread Coach-initiated
|
||||
* The FAB is now the only entry. An unread Coach-initiated
|
||||
* nudge (`GET /api/insights/coach/nudge-status`) paints an unread
|
||||
* dot (`data-slot="coach-fab-unread"`) on its corner; with nothing
|
||||
* unread the FAB renders without the dot.
|
||||
|
||||
@@ -4,7 +4,7 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
|
||||
|
||||
/**
|
||||
* v1.4.27 F17 — insights metric-availability gating.
|
||||
* v1.4.27 — insights metric-availability gating.
|
||||
*
|
||||
* When a metric has zero observations the routed sub-page early-
|
||||
* returns an empty-state component with TWO mandatory affordances:
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "./setup/test";
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* v1.4.27 MB6 — `/measurements?add=<TYPE>` consumer.
|
||||
* v1.4.27 — `/measurements?add=<TYPE>` consumer.
|
||||
*
|
||||
* Insight metric actions link here with `?add=<TYPE>` and an optional,
|
||||
* allowlisted `returnTo`. The page opens the responsive capture primitive,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { expect, test } from "./setup/test";
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* v1.4.27 MB6 — public surfaces.
|
||||
* v1.4.27 — public surfaces.
|
||||
*
|
||||
* Three contracts under test, all reachable without a session:
|
||||
*
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from "./setup/test";
|
||||
import { STORAGE_STATE_PATH } from "./setup/global-setup";
|
||||
|
||||
/**
|
||||
* v1.4.27 R3d MB1 — `<ResponsiveSheet>` primitive branch coverage.
|
||||
* v1.4.27 — `<ResponsiveSheet>` primitive branch coverage.
|
||||
*
|
||||
* The primitive renders as a bottom-anchored `<Sheet>` on `<md` (768
|
||||
* px) viewports and as a centred `<Dialog>` on `>=md`. Every primary
|
||||
|
||||
@@ -115,6 +115,12 @@ const eslintConfig = defineConfig([
|
||||
// carries `eslint-disable-next-line ... -- <reason>`; the rule refuses
|
||||
// a directive without one. See the rule header.
|
||||
"healthlog/no-utc-day-key": "error",
|
||||
// A comment does not open with the identifier of the work item that
|
||||
// produced it: those point into notes that never ship. A release
|
||||
// version and an issue reference may stay. The matcher is shared with
|
||||
// scripts/strip-planning-tags.mjs; see eslint-plugins/healthlog/
|
||||
// planning-tags.js for the shape and its limits.
|
||||
"healthlog/no-planning-tags": "error",
|
||||
},
|
||||
},
|
||||
// v1.28.17 — every recharts-rendering component funnels through the
|
||||
|
||||
+8
-8
@@ -45,7 +45,7 @@ const nextConfig: NextConfig = {
|
||||
NEXT_PUBLIC_APP_VERSION: PKG_VERSION,
|
||||
NEXT_PUBLIC_I18N_CATALOG_VERSION: catalogVersion(),
|
||||
},
|
||||
// v1.4.33 IW2 — strip `console.*` calls from the production bundle
|
||||
// v1.4.33 — strip `console.*` calls from the production bundle
|
||||
// (keep `console.error` + `console.warn` so the GlitchTip reporter
|
||||
// and prod-side debug rails still surface). The Lighthouse audit
|
||||
// flagged ~211 KiB of bundled JS as "unminified" — Turbopack already
|
||||
@@ -60,7 +60,7 @@ const nextConfig: NextConfig = {
|
||||
? { exclude: ["error", "warn"] }
|
||||
: false,
|
||||
},
|
||||
// v1.4.33 IW2 — bfcache hygiene. `Permissions-Policy: unload=()`
|
||||
// v1.4.33 — bfcache hygiene. `Permissions-Policy: unload=()`
|
||||
// tells the browser the page does not need the `unload` event,
|
||||
// which Chromium uses as a hint to admit the page to the
|
||||
// back/forward cache on navigation away. Pair with the absence of
|
||||
@@ -69,7 +69,7 @@ const nextConfig: NextConfig = {
|
||||
// headers already live on the response via the standalone runtime;
|
||||
// we add only the bfcache hint here.
|
||||
//
|
||||
// v1.4.34 IW-A — second rule layers the bfcache-friendly
|
||||
// v1.4.34 — second rule layers the bfcache-friendly
|
||||
// `Cache-Control` directive onto every authenticated HTML page
|
||||
// response (the source negative-lookahead excludes `/api/*` and
|
||||
// `/_next/*` so static assets keep their immutable caching and API
|
||||
@@ -151,10 +151,10 @@ const nextConfig: NextConfig = {
|
||||
destination: "/coach",
|
||||
permanent: true,
|
||||
},
|
||||
// v1.18.1 (D4) — Sources is a standalone `/settings/sources` route again
|
||||
// v1.18.1 — Sources is a standalone `/settings/sources` route again
|
||||
// (split back out of the Integrations sub-tabs), so the v1.18.0 redirect
|
||||
// to `/settings/integrations` was dropped.
|
||||
// v1.18.0 (S4) — the standalone "Erinnerungen" hub at
|
||||
// v1.18.0 — the standalone "Erinnerungen" hub at
|
||||
// `/settings/reminders` was a link-only page that duplicated the
|
||||
// canonical editors. Reminder TYPES now live in Notifications, each
|
||||
// gated on its module. 301-redirect so bookmarks, the PWA's cached
|
||||
@@ -275,7 +275,7 @@ const nextConfig: NextConfig = {
|
||||
"pdfjs-dist",
|
||||
"@napi-rs/canvas",
|
||||
],
|
||||
// v1.4.25 Fix-G — `src/lib/ai/prompts/safety-contracts.ts` reads its
|
||||
// v1.4.25 — `src/lib/ai/prompts/safety-contracts.ts` reads its
|
||||
// sibling YAML files at runtime via `__dirname + readFileSync`. The
|
||||
// standalone bundler ships the JS but not the YAML, so the build
|
||||
// crashed during page-data collection with ENOENT once the Turbopack
|
||||
@@ -309,7 +309,7 @@ const nextConfig: NextConfig = {
|
||||
"./node_modules/.pnpm/@napi-rs+canvas-linux-arm64-musl@*/node_modules/@napi-rs/canvas-linux-arm64-musl/**",
|
||||
],
|
||||
},
|
||||
// v1.4.34 IW-A — silence the Turbopack NFT trace warnings emitted
|
||||
// v1.4.34 — silence the Turbopack NFT trace warnings emitted
|
||||
// during `next build`. The tracer follows the `MAXMIND_LICENSE_KEY`
|
||||
// env access in `src/lib/geo.ts` back into the config file, then
|
||||
// emits "cannot be traced" warnings for paths it tried to walk
|
||||
@@ -336,7 +336,7 @@ const nextConfig: NextConfig = {
|
||||
};
|
||||
|
||||
/**
|
||||
* v1.4.28 R3d — opt-in bundle analyzer behind `ANALYZE=1`.
|
||||
* v1.4.28 — opt-in bundle analyzer behind `ANALYZE=1`.
|
||||
*
|
||||
* `pnpm analyze` (defined in `package.json`) sets the env var and
|
||||
* runs `next build`; the analyzer writes static HTML reports to
|
||||
|
||||
@@ -251,7 +251,7 @@ export default defineConfig({
|
||||
// through stable attributes and the account's own API, not a mobile
|
||||
// layout, so it runs in one project.
|
||||
"notification-dispatch-journey.spec.ts",
|
||||
// v1.39 (C2) — the setup-flow journeys each own one account and walk
|
||||
// v1.39 — the setup-flow journeys each own one account and walk
|
||||
// it from the welcome screen to the dashboard, resetting it first. A
|
||||
// second project would walk the same account from a second browser
|
||||
// and each would find the other's answers. The two sweeps set their
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
// and triggers an SW-unregister + cache-wipe + hard reload when the
|
||||
// server moves ahead of the running shell.
|
||||
//
|
||||
// v1.4.43 QoL (L3) — read the active version from `self.__APP_VERSION__`,
|
||||
// v1.4.43 — read the active version from `self.__APP_VERSION__`,
|
||||
// which the build step writes to `/public/sw-version.js` (loaded
|
||||
// synchronously below via `importScripts`). Pre-fix the literal was
|
||||
// hand-bumped per release and quietly drifted four releases stale, so
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.4.42 W6 — env-check sanity tests.
|
||||
* v1.4.42 — env-check sanity tests.
|
||||
*
|
||||
* The script's two pure helpers (`parseEnvFile` + `checkEnv`) are
|
||||
* pinned here. The renderResults / main() entrypoint is intentionally
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.4.43 QoL (L3) — verify the SW build step + the SW source contract.
|
||||
* v1.4.43 — verify the SW build step + the SW source contract.
|
||||
*
|
||||
* Pre-fix `public/sw.js` hard-coded `const CACHE_VERSION = "v1.4.38.4"`
|
||||
* which drifted four releases stale. The current shape:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* v1.4.43 QoL (L3) — write the active package.json version into
|
||||
* v1.4.43 — write the active package.json version into
|
||||
* `public/sw-version.js`. The Service Worker (`public/sw.js`) loads
|
||||
* this file via `importScripts('/sw-version.js')` and reads
|
||||
* `self.__APP_VERSION__` to key its `CACHE_VERSION`.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.4.39 W-SINCE — defense-in-depth row cap on the live-fallback
|
||||
* v1.4.39 — defense-in-depth row cap on the live-fallback
|
||||
* per-type read.
|
||||
*
|
||||
* The v1.4.38.7 perf audit (`.planning/round-v1438-perf-analysis.md`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.4.40 W-INSIGHTS SB-7 — pin the four discovery branches of
|
||||
* v1.4.40 — pin the four discovery branches of
|
||||
* `GET /api/auth/registration-status`.
|
||||
*
|
||||
* The route is read-only; the iOS sign-in flow + the web onboarding
|
||||
|
||||
@@ -282,7 +282,7 @@ describe("data-wipe completeness", () => {
|
||||
});
|
||||
|
||||
it("freezes the set of user-linked tables that carry no foreign key", () => {
|
||||
// v1.37.20 (A6-19) — NO_FK_BY_DESIGN, as an assertion instead of a pair
|
||||
// v1.37.20 — NO_FK_BY_DESIGN, as an assertion instead of a pair
|
||||
// of FK migrations. The two Telegram plumbing tables link to an account
|
||||
// by a bare `userId` string on purpose: both hold a bounded, short-lived
|
||||
// window of chat state (scheduled message deletions, prompt contexts)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.4.27 B6 / BL-P4-8 — i18n drift-guard.
|
||||
* v1.4.27 — i18n drift-guard.
|
||||
*
|
||||
* Locks down the bundled key sets that ship across all six locales so
|
||||
* a future copy-paste regression can't silently drop a translation
|
||||
@@ -103,7 +103,7 @@ const REQUIRED_KEY_SETS: Array<{
|
||||
"notifications.admin.reminderCheckMissedBody",
|
||||
"notifications.admin.reminderCheckOverdueTitle",
|
||||
"notifications.admin.reminderCheckOverdueBody",
|
||||
// v1.4.27 R5 — offline-geo fallback notifications.
|
||||
// v1.4.27 — offline-geo fallback notifications.
|
||||
"notifications.admin.offlineGeoUnavailableTitle",
|
||||
"notifications.admin.offlineGeoUnavailableBody",
|
||||
],
|
||||
|
||||
@@ -59,7 +59,7 @@ describe("proxy.ts DEMO_MODE mutation allowlist", () => {
|
||||
});
|
||||
|
||||
it("permits the three setup-flow writes so the demo can finish onboarding", () => {
|
||||
// v1.39 (C1) — the design spec requires the demo to complete the same flow
|
||||
// v1.39 — the design spec requires the demo to complete the same flow
|
||||
// with preset answers, and none of the three creates health data: closed
|
||||
// enum answers, the step ledger, and the module map derived from them.
|
||||
for (const [path, method] of [
|
||||
@@ -86,7 +86,7 @@ describe("proxy.ts DEMO_MODE mutation allowlist", () => {
|
||||
});
|
||||
|
||||
it("admits every mutating onboarding route, read off the route files", () => {
|
||||
// v1.39 (C2) — the demo has to be able to walk the whole flow, and the
|
||||
// v1.39 — the demo has to be able to walk the whole flow, and the
|
||||
// flow's welcome and exit make writes the C1 allowlist did not carry: the
|
||||
// disclaimer acknowledgment (both welcome buttons await it) and the tour
|
||||
// checkpoint. A hand-kept list of three paths could not see the other
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
/**
|
||||
* v1.4.22 C4 — onboarding redirect moved from `<AuthShell>` useEffect
|
||||
* v1.4.22 — onboarding redirect moved from `<AuthShell>` useEffect
|
||||
* to `src/proxy.ts` so it lands on the first server response instead
|
||||
* of post-hydration. The previous client-side approach caused a brief
|
||||
* dashboard flash for users with `onboardingCompletedAt === null`.
|
||||
@@ -101,7 +101,7 @@ describe("proxy.ts onboarding redirect (v1.4.22 C4)", () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.4.22 W5 reconcile (Sec-MED-2) — exact-match `/onboarding`
|
||||
* v1.4.22 — exact-match `/onboarding`
|
||||
* plus subroutes. A hypothetical `/onboarding-export` route must
|
||||
* NOT inherit the public-pass-through that the literal
|
||||
* `/onboarding` carries.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* G3 — the success affordance is owned by the outcome module, everywhere.
|
||||
* The success affordance is owned by the outcome module, everywhere.
|
||||
*
|
||||
* #640 established the rule inside the import panel: a write that wrote
|
||||
* nothing must not read as a success. #650 widened it to the medication
|
||||
@@ -160,7 +160,7 @@ const PINNED_AFFORDANCES: Record<
|
||||
"toast.success": 1,
|
||||
},
|
||||
"src/components/custom-metrics/custom-metric-history-list.tsx": {
|
||||
// v1.37.20 (A3-11) — the third success is the restore confirmation
|
||||
// v1.37.20 — the third success is the restore confirmation
|
||||
// behind the delete toast's Undo action.
|
||||
"toast.success": 3,
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export function GET(request: Request): Response {
|
||||
// M1 — fail closed when no origin is pinned; never derive the issuer /
|
||||
// Fail closed when no origin is pinned; never derive the issuer /
|
||||
// endpoint URLs from the attacker-influenceable Host header.
|
||||
if (!isMcpOriginConfigured()) {
|
||||
return Response.json(
|
||||
@@ -22,7 +22,7 @@ export function GET(request: Request): Response {
|
||||
);
|
||||
}
|
||||
return Response.json(authorizationServerMetadata(request.url), {
|
||||
// M1 — `private, no-store`: the document is host-derived, so a shared cache
|
||||
// `private, no-store`: the document is host-derived, so a shared cache
|
||||
// keyed on path (ignoring Host) must never serve it cross-tenant.
|
||||
headers: { "Cache-Control": "private, no-store" },
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export function GET(request: Request): Response {
|
||||
// M1 — fail closed when no origin is pinned; never derive the `resource` /
|
||||
// Fail closed when no origin is pinned; never derive the `resource` /
|
||||
// authorization-server URLs from the attacker-influenceable Host header.
|
||||
if (!isMcpOriginConfigured()) {
|
||||
return Response.json(
|
||||
@@ -23,7 +23,7 @@ export function GET(request: Request): Response {
|
||||
);
|
||||
}
|
||||
return Response.json(protectedResourceMetadata(request.url), {
|
||||
// M1 — `private, no-store`: the document is host-derived, so a shared cache
|
||||
// `private, no-store`: the document is host-derived, so a shared cache
|
||||
// keyed on path (ignoring Host) must never serve it cross-tenant.
|
||||
headers: { "Cache-Control": "private, no-store" },
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ import { join } from "node:path";
|
||||
* waterfall regresses to the v1.4.39.1 behaviour, so the test pins the
|
||||
* two call shapes.
|
||||
*
|
||||
* v1.7.0 W6 — both calls now carry an `enabled` gate so the unified
|
||||
* v1.7.0 — both calls now carry an `enabled` gate so the unified
|
||||
* snapshot rollout flag can disable them in favour of a single
|
||||
* `/api/dashboard/snapshot` cell. With the flag OFF (default) the
|
||||
* slim/thick split still drives the page, so the protective intent
|
||||
|
||||
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* v1.4.40 W-RSC — dashboard Suspense boundaries.
|
||||
* v1.4.40 — dashboard Suspense boundaries.
|
||||
*
|
||||
* Audit-H2 flagged "no `<Suspense>` boundaries anywhere in the app".
|
||||
* The v1.4.40 W-RSC wave wraps every dashboard tile cell and every
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "@/lib/dashboard-layout";
|
||||
|
||||
/**
|
||||
* v1.4.43 W11-M5 — dashboard tile-strip skeleton during slow first
|
||||
* v1.4.43 — dashboard tile-strip skeleton during slow first
|
||||
* paint.
|
||||
*
|
||||
* Pre-fix the strip only painted once `trendCards.length > 0`. When the
|
||||
|
||||
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* v1.4.20 phase B3 — guards for the `/insights` wiring of the
|
||||
* v1.4.20 — guards for the `/insights` wiring of the
|
||||
* Trends row + the analytics correlations payload.
|
||||
*
|
||||
* The page-source scan pattern mirrors `insights-polish.test.ts` —
|
||||
@@ -65,7 +65,7 @@ describe("v1.4.20 B3 — /insights mounts TrendsRow", () => {
|
||||
});
|
||||
|
||||
describe("v1.4.20 B3 — /api/analytics surfaces correlations", () => {
|
||||
// v1.4.37 W2 — the inline correlation builder relocated to
|
||||
// v1.4.37 — the inline correlation builder relocated to
|
||||
// `src/lib/analytics/correlations-fast-path.ts`. The route now
|
||||
// delegates through `computeCorrelationHypothesesFastPath`; the
|
||||
// three Pearson runner imports live alongside the new dispatcher.
|
||||
|
||||
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* v1.4.19 A3 — guards for the polish pass on `/insights` and the
|
||||
* v1.4.19 — guards for the polish pass on `/insights` and the
|
||||
* dashboard:
|
||||
*
|
||||
* 1. The dashboard page does NOT mount `<CompareToggle />`. The
|
||||
@@ -59,7 +59,7 @@ describe("v1.4.19 A3 — dashboard polish", () => {
|
||||
});
|
||||
|
||||
describe("v1.4.27 R3d MB4 — the layout mounts the Coach drawer", () => {
|
||||
// v1.4.34 IW-B — `<CoachLaunchProvider>` + `<LayoutCoachMount>` moved
|
||||
// v1.4.34 — `<CoachLaunchProvider>` + `<LayoutCoachMount>` moved
|
||||
// up to the global `<AuthShell>` so the dashboard hero (and every
|
||||
// other authed surface) can call `askCoach()` from the same context.
|
||||
// The insights layout keeps the FAB but no longer wraps the provider
|
||||
@@ -113,7 +113,7 @@ describe("v1.4.19 A3 — /insights polish", () => {
|
||||
});
|
||||
|
||||
it("tab strip owns the always-visible page-level refresh affordance (v1.4.25 W4)", () => {
|
||||
// v1.4.25 W4 — the regenerate handler moved out of the mother page
|
||||
// v1.4.25 — the regenerate handler moved out of the mother page
|
||||
// body entirely and now lives on the layout-mounted
|
||||
// `<InsightsLayoutShell>` (which renders `<InsightsTabStrip>` with
|
||||
// the advisor query's regenerate wired in). The mother-page hero
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.4.43 QoL (L1) — pin the tightened 404 / global-error copy so a
|
||||
* v1.4.43 — pin the tightened 404 / global-error copy so a
|
||||
* future translator does not re-introduce the v1.4.27 marketing
|
||||
* paragraph. The 404 page now ships with i18n keys (`notFound.*`)
|
||||
* across all six locales; the global-error boundary remains static
|
||||
|
||||
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
/**
|
||||
* v1.4.15 phase-A3 fix #1 — the dashboard "Hinzufügen" / "Add" dropdown used
|
||||
* v1.4.15 #1 — the dashboard "Hinzufügen" / "Add" dropdown used
|
||||
* to render two menu items whose visible labels both resolved to the bare
|
||||
* verb ("Hinzufügen" in DE, "Add" in EN). The dropdown trigger ALSO says
|
||||
* "Hinzufügen", so a screen-reader user heard "Hinzufügen, Hinzufügen,
|
||||
@@ -34,7 +34,7 @@ interface Messages {
|
||||
quickAddMeasurement: string;
|
||||
quickAddMood: string;
|
||||
/**
|
||||
* v1.4.37 W7b — third quick-add row: medication intake. Same
|
||||
* v1.4.37 — third quick-add row: medication intake. Same
|
||||
* contract as the other two: distinct from siblings and from the
|
||||
* trigger label, otherwise the screen-reader hears "Hinzufügen,
|
||||
* Hinzufügen, Hinzufügen, Hinzufügen" with no way to discriminate.
|
||||
@@ -48,7 +48,7 @@ function load(path: string): Messages {
|
||||
}
|
||||
|
||||
describe("dashboard quick-add submenu labels", () => {
|
||||
// v1.4.38 W-D P1-4 — extend the collision guard from en+de to all
|
||||
// v1.4.38 — extend the collision guard from en+de to all
|
||||
// six locales so a future translation that accidentally reuses the
|
||||
// trigger word (or a sibling row's verb) is caught before shipping.
|
||||
// es/fr/it/pl currently still hold the literal English block; the
|
||||
|
||||
@@ -6,7 +6,7 @@ import { resolveServerLocale } from "@/lib/i18n/server-locale";
|
||||
import { getServerTranslator } from "@/lib/i18n/server-translator";
|
||||
|
||||
/**
|
||||
* v1.4.27 B3 — Public about / credits page.
|
||||
* v1.4.27 — Public about / credits page.
|
||||
*
|
||||
* Two reasons this page exists:
|
||||
*
|
||||
@@ -102,7 +102,7 @@ export default async function AboutPage() {
|
||||
</header>
|
||||
|
||||
{/*
|
||||
v1.4.33 IW9 — `max-w-3xl` is intentional (same rationale as
|
||||
v1.4.33 — `max-w-3xl` is intentional (same rationale as
|
||||
`/privacy`): long-form column reads better around 70-80
|
||||
chars per line. The dashboard / settings / admin shells use
|
||||
`max-w-screen-xl` (1280 px); legal pages stay at 768 px.
|
||||
|
||||
@@ -5,7 +5,7 @@ import { I18nProvider } from "@/lib/i18n/context";
|
||||
import type { AchievementProgress } from "@/lib/gamification/achievements";
|
||||
|
||||
/**
|
||||
* v1.4.15 phase-B4 — /achievements page contract.
|
||||
* v1.4.15 — /achievements page contract.
|
||||
*
|
||||
* Two surfaces under test:
|
||||
*
|
||||
@@ -158,7 +158,7 @@ const mockData = {
|
||||
},
|
||||
};
|
||||
|
||||
// v1.4.34 IW-F-Perf — page now reads through the shared
|
||||
// v1.4.34 — page now reads through the shared
|
||||
// `useAchievementsQuery` hook; mock it directly so the test stays
|
||||
// scoped to the page render and bypasses the TanStack provider.
|
||||
vi.mock("@/lib/queries/use-achievements-query", () => ({
|
||||
|
||||
@@ -230,7 +230,7 @@ function AchievementCard({ achievement, t, tCount }: AchievementCardProps) {
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
{/* v1.4.33 IW9 — aria-label so the bar carries an accessible name. */}
|
||||
{/* v1.4.33 — aria-label so the bar carries an accessible name. */}
|
||||
<Progress
|
||||
value={achievement.progressPercent}
|
||||
className="h-1.5"
|
||||
@@ -289,7 +289,7 @@ export default function AchievementsPage() {
|
||||
// hides on an explicit `false`.
|
||||
const achievementsEnabled = user?.modules?.achievements !== false;
|
||||
|
||||
// v1.4.34 IW-F-Perf — mother page rides the shared cache slot
|
||||
// v1.4.34 — mother page rides the shared cache slot
|
||||
// alongside `<RecentAchievementsCard>` and
|
||||
// `<AchievementUnlockNotifier>` so the three consumers never trigger
|
||||
// more than one network call on a cold dashboard mount.
|
||||
@@ -432,7 +432,7 @@ export default function AchievementsPage() {
|
||||
</div>
|
||||
|
||||
{grouped.length === 0 ? (
|
||||
// v1.4.15 phase-C5: upgrade to the EmptyState primitive so the
|
||||
// v1.4.15: upgrade to the EmptyState primitive so the
|
||||
// surface matches the rest of the app. Trophy icon makes the
|
||||
// intent obvious before the user reads the copy. Reuses the
|
||||
// existing `noneUnlockedYet` string to avoid C4 i18n churn.
|
||||
|
||||
@@ -103,7 +103,7 @@ export function AdminSectionRenderer({
|
||||
// v1.18.1 — the former ai-quality, assistant, and coach-feedback
|
||||
// sections fold into one Coach area: surface toggles + operator
|
||||
// config first, then the two feedback-quality tables.
|
||||
// v1.18.6 (W9) — server-wide module availability moved out to its own
|
||||
// v1.18.6 — server-wide module availability moved out to its own
|
||||
// `module-availability` section: it gates EVERY module, not just the
|
||||
// coach, so it no longer belongs stacked under Coach.
|
||||
return (
|
||||
@@ -118,7 +118,7 @@ export function AdminSectionRenderer({
|
||||
</SectionFrame>
|
||||
);
|
||||
case "module-availability":
|
||||
// v1.18.6 (W9) — operator-side server-wide module on/off, its own
|
||||
// v1.18.6 — operator-side server-wide module on/off, its own
|
||||
// admin section (was stacked under Coach). Reconciled name: the
|
||||
// user-facing "Module" settings pick from what is available here.
|
||||
return (
|
||||
@@ -187,8 +187,8 @@ export function AdminSectionRenderer({
|
||||
</SectionFrame>
|
||||
);
|
||||
case "about":
|
||||
// v1.4.36 W4e — About section reused as-is from the settings surface.
|
||||
// v1.18.6 (W9) — route it through `SectionFrame` like every other admin
|
||||
// v1.4.36 — About section reused as-is from the settings surface.
|
||||
// v1.18.6 — route it through `SectionFrame` like every other admin
|
||||
// section so it gets the same visible heading + subtitle (the one admin
|
||||
// page that was bypassing the frame and showing no heading).
|
||||
return (
|
||||
|
||||
@@ -139,7 +139,7 @@ const MODULE_ROUTE_TREES_BY_KEY: Readonly<
|
||||
recovery: [],
|
||||
// v1.18.1 — the user-scoped Biomarker catalog backs the Labs feature.
|
||||
labs: ["src/app/api/labs", "src/app/api/biomarkers"],
|
||||
// v1.18.1 (W-B) — the illness/condition journal. Every `/api/illness/*`
|
||||
// v1.18.1 — the illness/condition journal. Every `/api/illness/*`
|
||||
// route gates on the `illness` module via the thin
|
||||
// `requireIllnessEnabled(...)` wrapper (which re-stamps the
|
||||
// illness-specific errorCode over `requireModuleEnabled("illness")`),
|
||||
@@ -157,19 +157,19 @@ const MODULE_ROUTE_TREES_BY_KEY: Readonly<
|
||||
// the data routes that live here (ECG, scores, statistics, the Today
|
||||
// digest) are EXEMPT below because a computation never depends on AI.
|
||||
insights: ["src/app/api/insights", "src/app/api/daily"],
|
||||
// v1.18.1 (D3) — medications graduated from CORE to a toggleable module.
|
||||
// v1.18.1 — medications graduated from CORE to a toggleable module.
|
||||
// SURFACE-gated (nav entry, dashboard widget, the dedicated Medikamente
|
||||
// settings entry), not data-layer-gated: every `/api/medications/*` route is
|
||||
// raw CRUD / intake / inventory / compliance over the user's own rows, so
|
||||
// they are EXEMPT below under the same data-layer reasoning as mood/labs.
|
||||
medications: ["src/app/api/medications"],
|
||||
// v1.18.0 B3 — the legacy `/api/doctor-report` tree was orphaned dead code
|
||||
// v1.18.0 — the legacy `/api/doctor-report` tree was orphaned dead code
|
||||
// and removed. The doctor-report surface is `/api/export/health-record` plus
|
||||
// the FHIR REST face; both gate on `doctorReport` directly, and both are
|
||||
// key-pinned below so a future route cannot satisfy the inventory by gating
|
||||
// on some other, more permissive module.
|
||||
doctorReport: ["src/app/api/fhir", "src/app/api/export/health-record"],
|
||||
// v1.25.0 (W-ENV) — the environmental-context tree. Every route calls
|
||||
// v1.25.0 — the environmental-context tree. Every route calls
|
||||
// `requireModuleEnabled(user.id, "environment")`, including the geocode
|
||||
// lookup and the backfill, because each one either reads the module's data
|
||||
// or spends the outbound weather/geocode budget on the user's behalf.
|
||||
@@ -178,7 +178,7 @@ const MODULE_ROUTE_TREES_BY_KEY: Readonly<
|
||||
// `/api` tree, and it gates on the module itself. What lives under
|
||||
// `/api/mcp` is the credential-management surface behind it, exempt below.
|
||||
mcp: ["src/app/api/mcp"],
|
||||
// v1.25.0 (W-DOCS-IN) — the inbound clinical-document vault. Every route
|
||||
// v1.25.0 — the inbound clinical-document vault. Every route
|
||||
// gates on `inboundDocuments`; the AI READ of a document is a separate
|
||||
// per-user opt-in on top.
|
||||
inboundDocuments: ["src/app/api/documents/inbound"],
|
||||
@@ -267,7 +267,7 @@ const EXEMPT_ROUTES: ReadonlyArray<string> = [
|
||||
"src/app/api/labs/ocr/capability/route.ts",
|
||||
"src/app/api/labs/ocr/commit/route.ts",
|
||||
// ── DATA LAYER (medications) ──────────────────────────────────────
|
||||
// v1.18.1 (D3) — medications graduated from CORE to a toggleable module,
|
||||
// v1.18.1 — medications graduated from CORE to a toggleable module,
|
||||
// but it is SURFACE-gated (nav / dashboard widget / settings entry), not
|
||||
// data-layer-gated. Every `/api/medications/*` route is raw CRUD over the
|
||||
// user's own medication / intake / inventory / compliance / side-effect
|
||||
|
||||
@@ -4,7 +4,7 @@ import { annotate } from "@/lib/logging/context";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
/**
|
||||
* v1.4.16 phase B5e — admin AI quality preview.
|
||||
* v1.4.16 — admin AI quality preview.
|
||||
*
|
||||
* Returns the latest aggregator-written summary from
|
||||
* `AppSettings.adminAiInsightsFeedbackSummary`. The pg-boss
|
||||
|
||||
@@ -9,7 +9,7 @@ import { z } from "zod/v4";
|
||||
import { isoDateTime } from "@/lib/validations/datetime";
|
||||
|
||||
/**
|
||||
* v1.4.16 phase B4: parse the union of legacy + new query params. Legacy
|
||||
* v1.4.16: parse the union of legacy + new query params. Legacy
|
||||
* `limit`/`offset`/`filter` keys still work so the existing
|
||||
* `<RecentAuditPreview>` and `<LoginOverviewSection>` callers don't break;
|
||||
* new `page`/`perPage`/`actor`/`action`/`target`/`since`/`until` drive
|
||||
@@ -152,7 +152,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
action: true,
|
||||
ipAddress: true,
|
||||
location: true,
|
||||
// v1.4.27 B3 — surface the carrier chip data alongside the
|
||||
// v1.4.27 — surface the carrier chip data alongside the
|
||||
// existing location string. Both nullable so older rows render
|
||||
// with the same dash that `location` falls back to.
|
||||
asn: true,
|
||||
@@ -167,7 +167,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
prisma.auditLog.count({ where }),
|
||||
]);
|
||||
|
||||
// v1.4.16 phase D reconcile (security H2) — `details` is a JSON string
|
||||
// v1.4.16 (security H2) — `details` is a JSON string
|
||||
// column written by `auditLog()` calls across the app. A user-controlled
|
||||
// value can land in here (e.g. `auth.login.failed → details.identifier`)
|
||||
// and a typo'd "Bearer hlk_..." in a username field would otherwise be
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
OFFHOST_BACKUP_PERIOD_HOURS,
|
||||
type OffhostBackupFreshness,
|
||||
} from "@/lib/jobs/offhost-backup-freshness";
|
||||
// v1.4.41 W-ORG — `BackupRow` / `BackupsList` moved to `src/types/backups.ts`
|
||||
// v1.4.41 — `BackupRow` / `BackupsList` moved to `src/types/backups.ts`
|
||||
// so callers (in particular `components/admin/backups-section.tsx`) don't
|
||||
// have to reach across the component → route-handler layer boundary.
|
||||
import type {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NextRequest } from "next/server";
|
||||
import { z } from "zod/v4";
|
||||
|
||||
/**
|
||||
* v1.4.16 phase B3: serve the host-load chart that sits above
|
||||
* v1.4.16: serve the host-load chart that sits above
|
||||
* `/admin/system-status`. The pg-boss `host-metric-sample` worker writes
|
||||
* one row per minute; this endpoint returns the last `?since=` window
|
||||
* (default 2h, max 24h) ordered by `capturedAt` ascending so the chart
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.4.48 H-APNs-1 — coverage for the admin notification diagnostic
|
||||
* v1.4.48 — coverage for the admin notification diagnostic
|
||||
* endpoint. The route surfaces what the dispatcher would observe when
|
||||
* targeting the calling admin's account so an operator can debug push
|
||||
* delivery without DB-shell access; these tests pin:
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { ChannelType } from "@/lib/notifications/types";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* v1.4.48 H-APNs-1 — admin-only diagnostic endpoint for the
|
||||
* v1.4.48 — admin-only diagnostic endpoint for the
|
||||
* notification subsystem. Surfaces what the dispatcher would see when
|
||||
* targeting the calling admin's account so an operator can debug an
|
||||
* iOS / Web Push / Telegram / ntfy issue without needing DB shell
|
||||
|
||||
@@ -194,7 +194,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
const timeWindow = `${schedule.windowStart}–${schedule.windowEnd}`;
|
||||
|
||||
try {
|
||||
// v1.4.27 F21 — each notification renders in the affected
|
||||
// v1.4.27 — each notification renders in the affected
|
||||
// user's `User.locale` (the recipient is the user whose dose
|
||||
// is overdue, not the admin running the check).
|
||||
// `dispatchLocalisedNotification` resolves the locale from
|
||||
|
||||
@@ -149,7 +149,7 @@ export const POST = apiHandler(async () => {
|
||||
});
|
||||
}
|
||||
|
||||
// v1.4.27 F21 — resolve the admin's persisted locale so the test
|
||||
// v1.4.27 — resolve the admin's persisted locale so the test
|
||||
// notification arrives in their preferred language. The test route
|
||||
// talks to the senders directly (not the dispatcher) because it
|
||||
// needs per-channel success / error feedback for the UI, so we
|
||||
|
||||
@@ -377,7 +377,7 @@ describe("PUT /api/admin/settings", () => {
|
||||
expect(res.status).toBe(415);
|
||||
});
|
||||
|
||||
// v1.4.25 W7 — server-default timezone.
|
||||
// v1.4.25 — server-default timezone.
|
||||
it("persists a valid defaultUserTimezone", async () => {
|
||||
vi.mocked(prisma.appSettings.upsert).mockResolvedValue({
|
||||
id: "singleton",
|
||||
|
||||
@@ -187,7 +187,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
const parsed = assistantFlagsSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
// v1.4.43 W6 — multi-issue 422.
|
||||
// v1.4.43 — multi-issue 422.
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ export const GET = apiHandler(async () => {
|
||||
settings?.documentQuotaBytes !== undefined
|
||||
? Number(settings.documentQuotaBytes)
|
||||
: 1_073_741_824,
|
||||
// v1.4.25 W7 — null means "fall back to Europe/Berlin in the
|
||||
// v1.4.25 — null means "fall back to Europe/Berlin in the
|
||||
// resolver"; surfacing the raw value lets the admin UI render
|
||||
// an empty picker placeholder until they opt in.
|
||||
defaultUserTimezone: settings?.defaultUserTimezone ?? null,
|
||||
@@ -116,7 +116,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = adminSettingsSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
// v1.4.43 W6 — multi-issue 422.
|
||||
// v1.4.43 — multi-issue 422.
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
auditDetails.documentQuotaBytes = data.documentQuotaBytes;
|
||||
}
|
||||
|
||||
// v1.4.25 W7 — server-default timezone for new signups.
|
||||
// v1.4.25 — server-default timezone for new signups.
|
||||
// Empty string clears the override (resolver falls back to
|
||||
// Europe/Berlin); a non-empty string must pass Intl validation
|
||||
// upstream of the column write.
|
||||
|
||||
@@ -68,7 +68,7 @@ vi.mock("@/lib/jobs/job-failures", () => ({
|
||||
readFailingQueues: vi.fn(),
|
||||
}));
|
||||
|
||||
// v1.4.25 W21 Fix-K — `/api/admin/status` now reads the legacy-form
|
||||
// v1.4.25 — `/api/admin/status` now reads the legacy-form
|
||||
import { GET } from "../route";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { requireAdmin, HttpError } from "@/lib/api-handler";
|
||||
|
||||
@@ -48,7 +48,7 @@ export const GET = apiHandler(async () => {
|
||||
appSettings,
|
||||
] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
// v1.4.41 W-DELETED-2 — exclude soft-deleted measurements from the
|
||||
// v1.4.41 — exclude soft-deleted measurements from the
|
||||
// admin status count so the dashboard tile reflects live data only.
|
||||
prisma.measurement.count({ where: { deletedAt: null } }),
|
||||
prisma.medication.count(),
|
||||
|
||||
@@ -56,7 +56,7 @@ export const PUT = apiHandler(
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = updateUserSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
// v1.4.43 W6 — multi-issue 422.
|
||||
// v1.4.43 — multi-issue 422.
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@ describe("POST /api/ai/test — provider error leak guard + non-5xx contract", (
|
||||
});
|
||||
});
|
||||
|
||||
// v1.4 fix: the dropdown in /settings was not honoured — the test always
|
||||
// v1.4: the dropdown in /settings was not honoured — the test always
|
||||
// ran against the SAVED provider (the maintainer reported "wirft was Komisches raus").
|
||||
// The route now accepts a JSON override body and forwards the user's
|
||||
// unsaved selection to resolveProviderForTest().
|
||||
|
||||
@@ -119,7 +119,7 @@ export const PATCH = apiHandler(
|
||||
await auditLog("allergy.update", {
|
||||
userId: user.id,
|
||||
ipAddress: getClientIp(request),
|
||||
// C4 — the replaced fields. The reaction and the note are named and
|
||||
// The replaced fields. The reaction and the note are named and
|
||||
// never quoted; both are encrypted at rest and the audit table is not a
|
||||
// second store for them.
|
||||
details: {
|
||||
@@ -196,7 +196,7 @@ export const DELETE = apiHandler(
|
||||
await auditLog("allergy.delete", {
|
||||
userId: user.id,
|
||||
ipAddress: getClientIp(request),
|
||||
// C3 — the allergen, its severity and its recorded date, and never the
|
||||
// The allergen, its severity and its recorded date, and never the
|
||||
// encrypted reaction text.
|
||||
details: {
|
||||
allergyId: id,
|
||||
|
||||
@@ -19,7 +19,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/lib/db", () => ({
|
||||
prisma: {
|
||||
// v1.4.37 W2 — `ensureUserRollupsFresh` pokes `measurement.findFirst`
|
||||
// v1.4.37 — `ensureUserRollupsFresh` pokes `measurement.findFirst`
|
||||
// for the newest-measurement watermark; mock both shapes.
|
||||
measurement: {
|
||||
findMany: vi.fn(),
|
||||
@@ -38,7 +38,7 @@ vi.mock("@/lib/db", () => ({
|
||||
// v1.38 — the composition note reads the account's last stored score
|
||||
// day. Nothing stored in these fixtures, so no note is raised.
|
||||
healthScoreRecord: { findFirst: vi.fn().mockResolvedValue(null) },
|
||||
// v1.4.33 C1 — slim summaries slice runs through `$queryRaw`. The
|
||||
// v1.4.33 — slim summaries slice runs through `$queryRaw`. The
|
||||
// v1.4.36 per-type coverage probe and the v1.4.37 default-slice
|
||||
// probe also ride `$queryRaw`. Default to an empty coverage map so
|
||||
// the route falls back to the live aggregator branches and the
|
||||
@@ -134,7 +134,7 @@ const SESSION_OK = {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
// v1.4.34 IW-G — reset the analytics LRU between tests so each case
|
||||
// v1.4.34 — reset the analytics LRU between tests so each case
|
||||
// observes a cold cache (otherwise tests sharing a userId would land
|
||||
// on the prior test's cached response).
|
||||
__resetAllCachesForTests();
|
||||
@@ -169,7 +169,7 @@ beforeEach(() => {
|
||||
count: 0,
|
||||
} as never);
|
||||
vi.mocked(prisma.measurementRollup.upsert).mockResolvedValue({} as never);
|
||||
// v1.4.37 W2 — `ensureUserRollupsFresh` reads `measurement.findFirst`;
|
||||
// v1.4.37 — `ensureUserRollupsFresh` reads `measurement.findFirst`;
|
||||
// the per-type coverage probe + the rollup-recompute aggregator ride
|
||||
// `$queryRaw` / `$queryRawUnsafe`. Default to empty so the route
|
||||
// falls back to the live fast-path branches and the assertions stay
|
||||
@@ -294,7 +294,7 @@ describe("GET /api/analytics", () => {
|
||||
expect("avg30LastMonth" in (body.data.summaries.WEIGHT ?? {})).toBe(true);
|
||||
});
|
||||
|
||||
// v1.4.33 C1 — slim summaries slice. The route branches on
|
||||
// v1.4.33 — slim summaries slice. The route branches on
|
||||
// `?slice=summaries` BEFORE any chunked findMany; the two `$queryRaw`
|
||||
// passes carry the per-type DataSummary shape with the same
|
||||
// contract the dashboard tile strip reads.
|
||||
@@ -344,7 +344,7 @@ describe("GET /api/analytics", () => {
|
||||
req,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
// v1.4.34 IW-B — bfcache-friendly directive rides on the slim
|
||||
// v1.4.34 — bfcache-friendly directive rides on the slim
|
||||
// slice too.
|
||||
expect(res.headers.get("Cache-Control")).toBe(
|
||||
"private, max-age=0, must-revalidate",
|
||||
@@ -378,7 +378,7 @@ describe("GET /api/analytics", () => {
|
||||
expect(body.data.summaries.WEIGHT.slope30?.direction).toBe("down");
|
||||
// Slim slice never carries BMI — the consumer re-derives.
|
||||
expect(body.data.bmi).toBeNull();
|
||||
// v1.4.34 IW-B — slim slice surfaces lastSeenByType too so the
|
||||
// v1.4.34 — slim slice surfaces lastSeenByType too so the
|
||||
// tile-strip caption works regardless of which slice the client
|
||||
// read.
|
||||
expect(body.data.lastSeenByType.WEIGHT?.daysAgo).toBeGreaterThanOrEqual(4);
|
||||
@@ -386,7 +386,7 @@ describe("GET /api/analytics", () => {
|
||||
expect(body.data.lastSeenByType.PULSE).toBeNull();
|
||||
});
|
||||
|
||||
// v1.18.0 B1 — the default slice serves the glucose-only surfaces
|
||||
// v1.18.0 — the default slice serves the glucose-only surfaces
|
||||
// (per-context summaries + the TIR/GMI/eA1C clinical panel) only when
|
||||
// the `glucose` module is enabled. A disabled-glucose account nulls
|
||||
// both fields rather than 403-ing the whole core-metrics payload.
|
||||
|
||||
@@ -45,7 +45,7 @@ import {
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* v1.4.33 C1 — pull `?slice=…` from either a NextRequest (the
|
||||
* v1.4.33 — pull `?slice=…` from either a NextRequest (the
|
||||
* route's production wrapper) or a plain `Request` (the integration
|
||||
* tests instantiate `new Request("http://localhost/api/analytics")`
|
||||
* and cast through). Falls back to the raw `URL(request.url)` parse
|
||||
@@ -83,7 +83,7 @@ export const GET = apiHandler(async (request?: Request) => {
|
||||
});
|
||||
}
|
||||
|
||||
// v1.4.33 C1 — slim summaries slice for the dashboard tile strip.
|
||||
// v1.4.33 — slim summaries slice for the dashboard tile strip.
|
||||
// The default path runs 30+ chunked findMany reads to feed
|
||||
// `summarize()` per type; under `?slice=summaries` the route
|
||||
// resolves the same per-type DataSummary shape from 2 SQL passes
|
||||
@@ -96,7 +96,7 @@ export const GET = apiHandler(async (request?: Request) => {
|
||||
// tile's `<InView>` boundary. Consumers that need only the headline
|
||||
// tile values land on this branch and skip the heavy chain.
|
||||
if (parsedQuery.data.slice === "summaries") {
|
||||
// v1.4.34 IW-G — read-through the analytics cache keyed on
|
||||
// v1.4.34 — read-through the analytics cache keyed on
|
||||
// (userId, slice). The slim slice is the dashboard tile strip's hot
|
||||
// path; multiple dashboard mounts inside a 60-second TTL all hit a
|
||||
// warm cache.
|
||||
@@ -123,7 +123,7 @@ export const GET = apiHandler(async (request?: Request) => {
|
||||
slim.lastSeenByType,
|
||||
slimModules,
|
||||
);
|
||||
// v1.4.34 IW-B — bfcache-friendly directive on the slim slice too
|
||||
// v1.4.34 — bfcache-friendly directive on the slim slice too
|
||||
// so a back-forward navigation that landed on the dashboard tile
|
||||
// strip can restore from memory instead of paying a full reload.
|
||||
const slimRes = apiSuccess({
|
||||
@@ -138,7 +138,7 @@ export const GET = apiHandler(async (request?: Request) => {
|
||||
return slimRes;
|
||||
}
|
||||
|
||||
// v1.4.34 IW-G — wrap the heavy default-slice body in the analytics
|
||||
// v1.4.34 — wrap the heavy default-slice body in the analytics
|
||||
// cache keyed on (userId, "default"). The dashboard, the checklist
|
||||
// mount, and the Coach drawer all hit this endpoint within seconds
|
||||
// of each other; the 60s TTL converts the 7.99s combined dashboard
|
||||
@@ -172,7 +172,7 @@ export const GET = apiHandler(async (request?: Request) => {
|
||||
const body = enrichLastSeenDaysAgo(cachedBody);
|
||||
|
||||
const response = apiSuccess(body);
|
||||
// v1.4.34 IW-B — bfcache-friendly directive so back-forward navigation
|
||||
// v1.4.34 — bfcache-friendly directive so back-forward navigation
|
||||
// restores the dashboard from memory. Per `src/lib/http/cache-headers.ts`:
|
||||
// `private` keeps shared caches out of personal data, `max-age=0` forces
|
||||
// revalidation on every navigation so session swaps detect on the wire,
|
||||
@@ -224,14 +224,14 @@ function enrichLastSeenDaysAgo<
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.4.34 IW-G — the heavy default-slice body, lifted out of the route
|
||||
* v1.4.34 — the heavy default-slice body, lifted out of the route
|
||||
* handler so `cached()` can wrap it. Returns the raw JSON payload; the
|
||||
* route handler attaches `Cache-Control` headers afterward. The split
|
||||
* matches the v1.4.33 snapshot LRU's `buildCoachSnapshot` →
|
||||
* `buildCoachSnapshotImpl` shape.
|
||||
*/
|
||||
async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
// v1.4.37.1 hotfix — fire-and-forget the rollup-fresh check.
|
||||
// v1.4.37.1 — fire-and-forget the rollup-fresh check.
|
||||
// Awaiting it on the read path can stall the Node event loop for
|
||||
// 30–60 s on a power-user account whose iOS step samples keep the
|
||||
// 90-day DAY window slightly stale, which starves /api/health,
|
||||
@@ -244,7 +244,7 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
// wraps its own try/catch, so the void call cannot reject.
|
||||
void ensureUserRollupsFresh(user.id);
|
||||
|
||||
// v1.4.37 W2 — single per-type coverage probe shared by the
|
||||
// v1.4.37 — single per-type coverage probe shared by the
|
||||
// bp_in_target / healthScore / correlations branches below. The
|
||||
// probe is one indexed query against `measurement_rollups`; the
|
||||
// three downstream branches each reuse the resulting `coverage`
|
||||
@@ -253,7 +253,7 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
// cost flat across the three branches.
|
||||
const coverage = await probeRollupCoverage(user.id);
|
||||
|
||||
// v1.4.25 W7b — every day-bucket call inside this route honours the
|
||||
// v1.4.25 — every day-bucket call inside this route honours the
|
||||
// user's display timezone via `userDayKey(date, userTz)` (the legacy
|
||||
// `berlinDayKey()` helper was retired in v1.4.40).
|
||||
const userTz = user.timezone ?? DEFAULT_TIMEZONE;
|
||||
@@ -329,7 +329,7 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
let bpInTargetPct30d: number | null = null;
|
||||
let bpInTargetPctAllTime: number | null = null;
|
||||
/**
|
||||
* v1.4.22 W5 reconcile (Code-H2) — period-aligned prior-window
|
||||
* v1.4.22 — period-aligned prior-window
|
||||
* pcts so the BD-Zielbereich tile's comparison-overlay caption
|
||||
* stops mismatching its math with its label. The tile's
|
||||
* `compareDelta` is `last30Days - priorMonth` (or `… - priorYear`)
|
||||
@@ -338,12 +338,12 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
*/
|
||||
let bpInTargetPctPriorMonth: number | null = null;
|
||||
let bpInTargetPctPriorYear: number | null = null;
|
||||
// v1.17 W1b — 90-day pair count + effective label span for the tile's
|
||||
// v1.17 — 90-day pair count + effective label span for the tile's
|
||||
// thin-data gate. Surfaced so the BD-Zielbereich tile renders "collecting
|
||||
// data" until the window holds enough readings, with a dynamic span label.
|
||||
let bpInTargetCount90: number | null = null;
|
||||
let bpInTargetSpanDays90: number | null = null;
|
||||
// v1.17 W1b — hold the current + prior-week BP envelopes so the shared
|
||||
// v1.17 — hold the current + prior-week BP envelopes so the shared
|
||||
// Health-Score input builder grades the pillar off the identical shape the
|
||||
// dashboard snapshot uses (one builder, no per-surface assembly drift).
|
||||
// The graded score + prior-week BP figures the score consumes are read off
|
||||
@@ -362,7 +362,7 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
const clinicalBpTargets = getBpTargets(user.dateOfBirth);
|
||||
if (bpTargets) {
|
||||
const now = new Date();
|
||||
// v1.4.37 W2 — probe-gated dispatcher replaces the inline chunked
|
||||
// v1.4.37 — probe-gated dispatcher replaces the inline chunked
|
||||
// read. When BOTH BP types have DAY-bucket coverage the helper
|
||||
// pairs the per-day MEAN SYS + MEAN DIA from `measurement_rollups`
|
||||
// and counts in-target days against the five reporting windows,
|
||||
@@ -390,7 +390,7 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
targets: bpTargets,
|
||||
now,
|
||||
coverage,
|
||||
// v1.4.38 W-A — cross-tz runtime guard. The helper falls back
|
||||
// v1.4.38 — cross-tz runtime guard. The helper falls back
|
||||
// to the live SQL path when the user is more than 3 hours
|
||||
// from UTC, where the rollup table's UTC-midnight day-key
|
||||
// would slip a calendar day relative to the live aggregator's
|
||||
@@ -454,7 +454,7 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
}),
|
||||
]);
|
||||
}
|
||||
// v1.17 W1d — the BD-Zielbereich headline standardises on the
|
||||
// v1.17 — the BD-Zielbereich headline standardises on the
|
||||
// trailing-90-day window (labelled "· 90 T" in the tile). All-time
|
||||
// remains carried below for the BP detail page's long view only.
|
||||
bpInTargetPct = windows.last90Days?.pct ?? null;
|
||||
@@ -463,7 +463,7 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
bpInTargetPctAllTime = windows.allTime?.pct ?? null;
|
||||
bpInTargetPctPriorMonth = windows.priorMonth?.pct ?? null;
|
||||
bpInTargetPctPriorYear = windows.priorYear?.pct ?? null;
|
||||
// v1.17 W1b — count + effective span for the BD-Zielbereich tile's
|
||||
// v1.17 — count + effective span for the BD-Zielbereich tile's
|
||||
// confidence gate (thin-data → "collecting data"; dynamic span label).
|
||||
// Shared helper keeps this identical to the dashboard snapshot.
|
||||
const bpWindow = deriveBpWindow90(
|
||||
@@ -477,7 +477,7 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
|
||||
// Per-context glucose summaries (canonical mg/dL).
|
||||
//
|
||||
// v1.4.29 H2 — bound to the trailing 30 days. The dashboard tile
|
||||
// v1.4.29 — bound to the trailing 30 days. The dashboard tile
|
||||
// path only reads the trailing window; pre-bound this query
|
||||
// walked every persisted BG row a multi-year user has written.
|
||||
const glucoseSince = new Date(
|
||||
@@ -518,8 +518,8 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
{ windowDays: GLUCOSE_PANEL_WINDOW_DAYS, now: new Date() },
|
||||
);
|
||||
|
||||
// v1.4.20 phase B3 — three pre-defined correlation hypotheses.
|
||||
// v1.4.37 W2 — probe-gated helper. The 28-day scan window (down
|
||||
// v1.4.20 — three pre-defined correlation hypotheses.
|
||||
// v1.4.37 — probe-gated helper. The 28-day scan window (down
|
||||
// from 30 to keep the cold critical path tight while still
|
||||
// satisfying the n >= 20 surface gate) reads SYS / PULSE / WEIGHT
|
||||
// per-day means from `measurement_rollups` when the user has full
|
||||
@@ -631,7 +631,7 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
bpEnvelopePriorTwoWeeks,
|
||||
});
|
||||
|
||||
// v1.18.1 P4 — Rest Mode annotation. When an illness/condition episode is
|
||||
// v1.18.1 — Rest Mode annotation. When an illness/condition episode is
|
||||
// active we frame the score (not penalise it): the number above is left
|
||||
// exactly as computed and we attach a value-free "you were unwell" context.
|
||||
// Resolved server-side so iOS mirrors the same fact, never recomputing it.
|
||||
@@ -668,7 +668,7 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
// sleep tile marks the headline with a discreet "sources disagree"
|
||||
// hint; clients that don't read the field stay unchanged.
|
||||
sleepSourceDiscrepancy: slim.sleepSourceDiscrepancy,
|
||||
// v1.4.34 IW-B — per-type freshness map drives the dashboard's
|
||||
// v1.4.34 — per-type freshness map drives the dashboard's
|
||||
// staleness caption on each `<TrendCard>`. Additive: clients that
|
||||
// don't read the field stay unchanged.
|
||||
lastSeenByType,
|
||||
@@ -684,7 +684,7 @@ async function buildAnalyticsResponse(user: AuthedUser, locale: Locale) {
|
||||
* case. Returns the sum-per-stage AND the day count covered so the
|
||||
* UI can render an "averaged across N nights" caption truthfully.
|
||||
*
|
||||
* v1.4.25 W3f — also returns a `perNight` array: one entry per
|
||||
* v1.4.25 — also returns a `perNight` array: one entry per
|
||||
* Berlin-tz day inside the window, with minutes-per-stage for the
|
||||
* stacked-bar chart. Days with zero stage-tagged rows are omitted
|
||||
* from `perNight` (the chart treats them as gaps).
|
||||
@@ -735,7 +735,7 @@ async function computeSleepStageBreakdown(
|
||||
return buildSleepStageComposition(rows, userTz, priorityJson, WINDOW_DAYS);
|
||||
}
|
||||
|
||||
// v1.4.37 W2 — `computeCorrelationHypotheses` body relocated to
|
||||
// v1.4.37 — `computeCorrelationHypotheses` body relocated to
|
||||
// `src/lib/analytics/correlations-fast-path.ts` so the probe-gated
|
||||
// rollup / live dispatcher can be unit-tested independently of the
|
||||
// route. The new helper also tightens the scan window to 28 days and
|
||||
@@ -751,7 +751,7 @@ async function computeSleepStageBreakdown(
|
||||
// concurrent slim slice and prevents future regressions from
|
||||
// accidentally reintroducing the fan-out.
|
||||
|
||||
// v1.4.37 W2 — `WEEKDAY_FORMATTER_CACHE`, `getWeekdayFormatter`,
|
||||
// v1.4.37 — `WEEKDAY_FORMATTER_CACHE`, `getWeekdayFormatter`,
|
||||
// `dateFromDayKey`, `ISO_WEEKDAY`, and `isoWeekdayInTz` relocated
|
||||
// alongside the correlation runner in
|
||||
// `src/lib/analytics/correlations-fast-path.ts`. They were only ever
|
||||
|
||||
@@ -48,7 +48,7 @@ vi.mock("@/lib/rate-limit", () => ({
|
||||
checkRateLimit: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ allowed: true, remaining: 5, reset: 0 }),
|
||||
// v1.4.43 W13 M-4 — auth surfaces now route through the wrapper that
|
||||
// v1.4.43 — auth surfaces now route through the wrapper that
|
||||
// returns `{ip, ...rate-limit-result}`. Default to a clean per-IP
|
||||
// result so the existing tests don't need to set every IP up.
|
||||
checkAuthSurfaceRateLimit: vi.fn().mockResolvedValue({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.4.43 W3-SECURITY (H-1) — `auth.login.failed` audit row must not
|
||||
* v1.4.43 — `auth.login.failed` audit row must not
|
||||
* leak the typed identifier (email / username) into operator
|
||||
* artefacts. Per the v1.4.20 PII directive, the `reason` field is the
|
||||
* only signal operators need; the identifier itself is PII and stays
|
||||
|
||||
@@ -39,7 +39,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
});
|
||||
}
|
||||
|
||||
// v1.4.43 W13 M-4 — `checkAuthSurfaceRateLimit` swaps to a tighter
|
||||
// v1.4.43 — `checkAuthSurfaceRateLimit` swaps to a tighter
|
||||
// global bucket when the trust chain is misconfigured; otherwise it
|
||||
// is byte-equivalent to the previous per-IP `auth:login:{ip}` key.
|
||||
const rl = await checkAuthSurfaceRateLimit(
|
||||
@@ -154,7 +154,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
if (!user || !user.passwordHash) {
|
||||
await attempt.failed();
|
||||
// v1.4.43 W3-SECURITY (H-1): never write the typed identifier into
|
||||
// v1.4.43: never write the typed identifier into
|
||||
// the audit row — `reason` already tells the operator what
|
||||
// happened, and PII must not land in operator artefacts. The
|
||||
// HMAC anchor below is one-way — recoverable only with the HMAC
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.32.22 (M4) — `PUT /api/auth/me/coach-prefs` optimistic concurrency.
|
||||
* v1.32.22 — `PUT /api/auth/me/coach-prefs` optimistic concurrency.
|
||||
*
|
||||
* A full-replace endpoint hydrated from a stale GET (two tabs / two devices)
|
||||
* clobbers silently; the base token 409s that. The guarded write covers BOTH
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.4.23 H4 — per-user Coach prompt-tuning preferences.
|
||||
* v1.4.23 — per-user Coach prompt-tuning preferences.
|
||||
*
|
||||
* GET /api/auth/me/coach-prefs — returns the parsed prefs (defaults
|
||||
* when the row is null).
|
||||
@@ -46,7 +46,7 @@ export const GET = apiHandler(async () => {
|
||||
select: { coachPrefsJson: true, updatedAt: true },
|
||||
});
|
||||
const prefs = parseCoachPrefs(row?.coachPrefsJson);
|
||||
// v1.32.22 (M4) — a never-saved user gets the PURE default with NO token
|
||||
// v1.32.22 — a never-saved user gets the PURE default with NO token
|
||||
// (mirrors the insights-layout never-saved GET). There is nothing to guard
|
||||
// against yet, and the first save is just the tokenless unconditional write.
|
||||
// A saved row carries `updatedAt` (additive sibling key; the prefs shape is a
|
||||
@@ -68,7 +68,7 @@ export const PUT = apiHandler(async (req: Request) => {
|
||||
});
|
||||
}
|
||||
|
||||
// v1.32.22 (M4) — pull the optimistic-concurrency base token off before the
|
||||
// v1.32.22 — pull the optimistic-concurrency base token off before the
|
||||
// parse. This is a full-replace endpoint, so the classic two-tab / two-device
|
||||
// stale-replace clobbers silently: the form was hydrated from a GET minutes
|
||||
// ago. The token closes that window.
|
||||
@@ -98,12 +98,12 @@ export const PUT = apiHandler(async (req: Request) => {
|
||||
// Persist the canonical defaulted form so the column shape stays
|
||||
// stable regardless of which subset of keys the caller supplied.
|
||||
//
|
||||
// v1.4.36 W3 T3 — mirror the excludeMetrics list onto the dedicated
|
||||
// v1.4.36 — mirror the excludeMetrics list onto the dedicated
|
||||
// `insightsExcludeMetrics` column so the Insights generator and
|
||||
// Coach snapshot share a single source of truth. Keeps the user's
|
||||
// privacy contract symmetric across the two surfaces without
|
||||
// requiring a separate Insights settings sheet.
|
||||
// v1.32.22 (M4) — the guarded write covers BOTH columns in one conditional
|
||||
// v1.32.22 — the guarded write covers BOTH columns in one conditional
|
||||
// update so the mirror-column contract (v1.4.36 W3 T3) stays unsplittable: a
|
||||
// 409 leaves `coachPrefsJson` AND `insightsExcludeMetrics` untouched together.
|
||||
const guarded = await guardedUserUpdate({
|
||||
|
||||
@@ -14,7 +14,7 @@ vi.mock("@/lib/db", () => ({
|
||||
apiToken: {
|
||||
updateMany: vi.fn(),
|
||||
},
|
||||
// v1.4.23 W6 — the cascade now runs through `prisma.$transaction`
|
||||
// v1.4.23 — the cascade now runs through `prisma.$transaction`
|
||||
// (HIGH 6 fix). The mock returns the resolved batch payload so the
|
||||
// route handler walks past the transaction step.
|
||||
$transaction: vi.fn().mockResolvedValue([] as never),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* DELETE /api/auth/me/devices/[id]
|
||||
*
|
||||
* v1.4.23 W4 F7b — revoke a single device.
|
||||
* v1.4.23 — revoke a single device.
|
||||
*
|
||||
* Behaviour:
|
||||
* 1. Confirms the device row belongs to the current user (ownership
|
||||
@@ -17,7 +17,7 @@
|
||||
* keep using /api/auth/logout. The two surfaces address different
|
||||
* blast radii.
|
||||
*
|
||||
* v1.4.23 W6 — the four-write cascade now runs through the shared
|
||||
* v1.4.23 — the four-write cascade now runs through the shared
|
||||
* `revokeDeviceCascade` helper so the writes commit atomically inside
|
||||
* a single `prisma.$transaction`. Previously a partial failure could
|
||||
* leave the device row alive with all its tokens revoked.
|
||||
|
||||
@@ -157,7 +157,7 @@ describe("GET /api/auth/me/devices", () => {
|
||||
expect(where).toEqual({ userId: "user-1" });
|
||||
});
|
||||
|
||||
// v1.4.23 W6 reconcile (Sr-MED-5) — a forged X-Device-Id header
|
||||
// v1.4.23 — a forged X-Device-Id header
|
||||
// pointing at a device the caller doesn't actually hold a credential
|
||||
// for must NOT flip `isCurrent: true`. The route now cross-checks the
|
||||
// header against an unrevoked RefreshToken row owned by the same
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* GET /api/auth/me/devices
|
||||
*
|
||||
* v1.4.23 W4 F7b — list active devices for the current user. Drives the
|
||||
* v1.4.23 — list active devices for the current user. Drives the
|
||||
* settings → security → devices surface in the v1.5 iOS app and the
|
||||
* matching web view planned for a follow-up. The list ships
|
||||
* device-identifying metadata only (label, lastSeen, channels) — no
|
||||
* tokens, no APNs identifiers, no IPs.
|
||||
*
|
||||
* v1.4.23 W6 reconcile (Sr-MED-5) — the "current device" marker is no
|
||||
* v1.4.23 — the "current device" marker is no
|
||||
* longer derived from the unauthenticated `X-Device-Id` request header
|
||||
* alone. The header value is now cross-checked against an unrevoked
|
||||
* RefreshToken row owned by the authenticated user; only when both
|
||||
@@ -40,7 +40,7 @@ export const GET = apiHandler(async (request: NextRequest) => {
|
||||
const { user } = await requireAuth();
|
||||
const headerDeviceId = request.headers.get("x-device-id");
|
||||
|
||||
// Sr-MED-5 — anchor `isCurrent` against a server-trusted credential.
|
||||
// Anchor `isCurrent` against a server-trusted credential.
|
||||
// The header is only honoured when the user actually holds a live
|
||||
// refresh-token issued for that device. Otherwise, ignore it.
|
||||
let trustedCurrentDeviceId: string | null = null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.4.47 W3 — per-user Coach opt-out endpoint.
|
||||
* v1.4.47 — per-user Coach opt-out endpoint.
|
||||
*
|
||||
* GET /api/auth/me/disable-coach — current flag.
|
||||
* PATCH /api/auth/me/disable-coach — body `{ disableCoach: boolean }`.
|
||||
|
||||
@@ -254,7 +254,7 @@ describe("PATCH /api/auth/me/modules", () => {
|
||||
vi.mocked(getSession).mockResolvedValue(SESSION_OK as never);
|
||||
primeUser();
|
||||
|
||||
// v1.18.1 (D3) — medications graduated to a toggleable module, so only
|
||||
// v1.18.1 — medications graduated to a toggleable module, so only
|
||||
// weight / blood pressure / pulse stay refused as core domains.
|
||||
for (const core of ["weight", "bloodPressure", "pulse"]) {
|
||||
const res = await (PATCH as (r: Request) => Promise<Response>)(
|
||||
@@ -371,7 +371,7 @@ describe("PATCH /api/auth/me/modules — the score caches", () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.32.22 (M3) — optimistic concurrency. The base token is stripped BEFORE
|
||||
* v1.32.22 — optimistic concurrency. The base token is stripped BEFORE
|
||||
* the `.strict()` schema parse, so it never trips strict validation; a stale
|
||||
* token 409s and writes nothing; a tokenless PATCH keeps the unconditional
|
||||
* write.
|
||||
|
||||
@@ -56,7 +56,7 @@ export const GET = apiHandler(async () => {
|
||||
annotate({ action: { name: "auth.me.modules.get" } });
|
||||
|
||||
const modules = await resolveModuleMap(user.id);
|
||||
// v1.32.22 (M3) — surface the optimistic-concurrency token. `resolveModuleMap`
|
||||
// v1.32.22 — surface the optimistic-concurrency token. `resolveModuleMap`
|
||||
// returns no row metadata, so read `updatedAt` on its own.
|
||||
const row = await prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
@@ -90,7 +90,7 @@ export const PATCH = apiHandler(async (req: Request) => {
|
||||
});
|
||||
}
|
||||
|
||||
// v1.32.22 (M3) — strip the optimistic-concurrency base token BEFORE the
|
||||
// v1.32.22 — strip the optimistic-concurrency base token BEFORE the
|
||||
// parse: `modulePrefsPatchSchema` is `.strict()` and must stay strict, so
|
||||
// the transport token can never reach it (it would 422 as an unknown key).
|
||||
const taken = takeBaseToken(body ?? {});
|
||||
@@ -127,7 +127,7 @@ export const PATCH = apiHandler(async (req: Request) => {
|
||||
changed.push(key);
|
||||
}
|
||||
|
||||
// v1.32.22 (M3) — guard the write on the client's base token; a stale base
|
||||
// v1.32.22 — guard the write on the client's base token; a stale base
|
||||
// 409s and writes nothing. Tokenless requests keep the unconditional write.
|
||||
const guarded = await guardedUserUpdate({
|
||||
userId: user.id,
|
||||
|
||||
@@ -236,7 +236,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
clientManaged: false,
|
||||
},
|
||||
content: { detail: "generic" },
|
||||
// v1.32.22 (M1) — the write echoes the fresh optimistic-concurrency token.
|
||||
// v1.32.22 — the write echoes the fresh optimistic-concurrency token.
|
||||
updatedAt: expect.any(String),
|
||||
});
|
||||
|
||||
@@ -441,7 +441,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
clientManaged: false,
|
||||
},
|
||||
content: { detail: "generic" },
|
||||
// v1.32.22 (M1) — the write echoes the fresh optimistic-concurrency token.
|
||||
// v1.32.22 — the write echoes the fresh optimistic-concurrency token.
|
||||
updatedAt: expect.any(String),
|
||||
});
|
||||
|
||||
@@ -644,7 +644,7 @@ describe("PATCH /api/auth/me/notification-prefs", () => {
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.32.22 (M1) — optimistic concurrency (issue #581 family). A PATCH carrying
|
||||
* v1.32.22 — optimistic concurrency (issue #581 family). A PATCH carrying
|
||||
* the `baseUpdatedAt` it was based on writes CONDITIONALLY on the stored row
|
||||
* still carrying that token. The headline case: a concurrent web PATCH of a
|
||||
* DIFFERENT category can no longer silently revert the iOS `clientManaged`
|
||||
|
||||
@@ -56,7 +56,7 @@ export const GET = apiHandler(async () => {
|
||||
|
||||
const row = await prisma.user.findUnique({
|
||||
where: { id: user.id },
|
||||
// v1.32.22 (M1) — `updatedAt` rides the response as the optimistic-
|
||||
// v1.32.22 — `updatedAt` rides the response as the optimistic-
|
||||
// concurrency token so a client can echo it on its next PATCH.
|
||||
select: { notificationPrefs: true, updatedAt: true },
|
||||
});
|
||||
@@ -91,7 +91,7 @@ export const PATCH = apiHandler(async (req: Request) => {
|
||||
});
|
||||
}
|
||||
|
||||
// v1.32.22 (M1) — pull the optimistic-concurrency base token off the body
|
||||
// v1.32.22 — pull the optimistic-concurrency base token off the body
|
||||
// BEFORE the Zod parse so it never pollutes `changedCategories`
|
||||
// (`Object.keys(parsed.data)`) or the schema. The race that matters: iOS
|
||||
// sets `medication.clientManaged = true`, a web card PATCH based on a
|
||||
@@ -125,7 +125,7 @@ export const PATCH = apiHandler(async (req: Request) => {
|
||||
parsed.data,
|
||||
);
|
||||
|
||||
// v1.32.22 (M1) — guard the deep-merge write on the client's base token. A
|
||||
// v1.32.22 — guard the deep-merge write on the client's base token. A
|
||||
// stale base 409s and writes NOTHING, so a concurrent web PATCH can't revert
|
||||
// a category (e.g. `medication.clientManaged`) another writer just set from a
|
||||
// fresher read. A tokenless request keeps the prior unconditional write.
|
||||
|
||||
@@ -175,7 +175,7 @@ export const GET = apiHandler(async () => {
|
||||
where: { id: recordId },
|
||||
select: {
|
||||
gender: true,
|
||||
// v1.39 (C1) — the record's own unit columns, for the setup
|
||||
// v1.39 — the record's own unit columns, for the setup
|
||||
// flow's "never re-ask a value the account holds" rule.
|
||||
glucoseUnit: true,
|
||||
unitPreference: true,
|
||||
@@ -199,7 +199,7 @@ export const GET = apiHandler(async () => {
|
||||
// Server-WIDE, so it is the same map for every record and is not
|
||||
// re-scoped here.
|
||||
getOperatorModuleAvailability(),
|
||||
// v1.39 (C1) — the needs-based setup state for the RECORD, like the two
|
||||
// v1.39 — the needs-based setup state for the RECORD, like the two
|
||||
// module fields above and for the same reason: it says what that record
|
||||
// tracks, and its only consumer reads it beside that record's reading
|
||||
// count, medication count and integration state. Ordering a record's rows
|
||||
@@ -249,7 +249,7 @@ export const GET = apiHandler(async () => {
|
||||
// exactly as it was, and `moduleAccess` is the same answer with the reason
|
||||
// attached — the record's own switch, the grant's edge, or the operator's.
|
||||
// Every client that only wants "paint it or not" keeps reading the boolean.
|
||||
// v1.39 (C1) — the published steps carry the Q6 resolution: a record whose
|
||||
// v1.39 — the published steps carry the Q6 resolution: a record whose
|
||||
// account already holds both unit preferences reads `units` as `done`, so
|
||||
// the flow never re-asks a value the account holds.
|
||||
const recordOnboarding = toOnboardingStateDto(
|
||||
@@ -308,7 +308,7 @@ export const GET = apiHandler(async () => {
|
||||
timezone: await resolveStoredTimezone(user.timezone),
|
||||
onboardingCompletedAt: user.onboardingCompletedAt,
|
||||
onboardingTourCompleted: user.onboardingTourCompleted,
|
||||
// v1.39 (C1) — the needs-based setup flow, published additively:
|
||||
// v1.39 — the needs-based setup flow, published additively:
|
||||
// `steps` (the nine stable ids and their `pending | done | skipped`
|
||||
// status), `needs` (the answers as given), `completedAt` (the flow's OWN
|
||||
// completion, distinct from `onboardingCompletedAt` above, which keeps its
|
||||
@@ -326,7 +326,7 @@ export const GET = apiHandler(async () => {
|
||||
// a step id it does not know skips it — the list is the contract, not the
|
||||
// client's own copy of it.
|
||||
onboarding,
|
||||
// v1.18.6 (DISC-02) — one-time medical-disclaimer acknowledgment. Null
|
||||
// v1.18.6 — one-time medical-disclaimer acknowledgment. Null
|
||||
// when never acknowledged; the onboarding welcome gate reads this to
|
||||
// decide whether to require the acknowledgment before "Get started".
|
||||
disclaimerAcknowledgedAt: user.disclaimerAcknowledgedAt,
|
||||
@@ -376,7 +376,7 @@ export const GET = apiHandler(async () => {
|
||||
// defaults rather than to `undefined`, so the switch is what renders.
|
||||
// No extra query — the session already loads the row this column is on.
|
||||
notificationPrefs: parseNotificationPrefs(user.notificationPrefs ?? null),
|
||||
// v1.4.47 W3 — the caller's own stored Coach opt-out, published for the
|
||||
// v1.4.47 — the caller's own stored Coach opt-out, published for the
|
||||
// switch that edits it. Default `false` if the column is absent
|
||||
// (partial-deploy rollback safety, see migration 0078 commentary). It is
|
||||
// not a gate: whether the Coach shows for the record on screen is the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.4.25 W5e — per-user, per-metric-class source priority.
|
||||
* v1.4.25 — per-user, per-metric-class source priority.
|
||||
*
|
||||
* GET /api/auth/me/source-priority — returns the fully-defaulted
|
||||
* shape (missing keys filled
|
||||
@@ -87,7 +87,7 @@ export const PUT = apiHandler(async (req: Request) => {
|
||||
);
|
||||
}
|
||||
|
||||
// v1.4.25 W10 reconcile (security M-3): capture the previous shape
|
||||
// v1.4.25 (security M-3): capture the previous shape
|
||||
// before overwriting so the audit-log entry records the actual
|
||||
// delta. Source-priority drives every aggregator's source pick,
|
||||
// so a silent compromise (or accidental client write) was
|
||||
|
||||
@@ -23,7 +23,7 @@ vi.mock("@/lib/db-compat", () => ({
|
||||
ensureDbCompatibility: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
|
||||
// v1.37.19 (A6-12) — the zone change enqueues a compliance re-fold.
|
||||
// v1.37.19 — the zone change enqueues a compliance re-fold.
|
||||
const refoldMock = vi.hoisted(() =>
|
||||
vi.fn(async () => ({ enqueued: true, error: null })),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* PUT /api/auth/me/timezone
|
||||
*
|
||||
* v1.4.25 W7 — per-user timezone setter. Lifts the timezone write
|
||||
* v1.4.25 — per-user timezone setter. Lifts the timezone write
|
||||
* out of `PUT /api/auth/profile` so the picker UI can post the
|
||||
* single field without round-tripping email/heightCm/etc, and so
|
||||
* the change pipeline (validate → write → invalidate-cache) is
|
||||
@@ -62,7 +62,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
invalidateUserTimezone(user.id);
|
||||
|
||||
// v1.37.19 (A6-12) — the medication-compliance rollup keys its rows by
|
||||
// v1.37.19 — the medication-compliance rollup keys its rows by
|
||||
// the LOCAL day the dose fell on, minted under the timezone in force at
|
||||
// write time. A zone change re-buckets those days, so the stored keys go
|
||||
// stale (and the coverage probe compares counts, not keys — it cannot
|
||||
|
||||
@@ -15,7 +15,7 @@ export const POST = apiHandler(async (request: Request) => {
|
||||
});
|
||||
}
|
||||
|
||||
// v1.4.43 W13 M-4 — tighter shared bucket on trust-chain misconfig.
|
||||
// v1.4.43 — tighter shared bucket on trust-chain misconfig.
|
||||
const rl = await checkAuthSurfaceRateLimit(
|
||||
request,
|
||||
"auth:passkey-login-options",
|
||||
|
||||
@@ -32,7 +32,7 @@ vi.mock("@/lib/auth/login-alert", () => ({
|
||||
|
||||
vi.mock("@/lib/rate-limit", () => ({
|
||||
checkRateLimit: vi.fn(),
|
||||
// v1.4.43 W13 M-4 — passkey/login-verify now uses the auth-surface
|
||||
// v1.4.43 — passkey/login-verify now uses the auth-surface
|
||||
// wrapper. Default to a clean per-IP result.
|
||||
checkAuthSurfaceRateLimit: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -72,7 +72,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
|
||||
await ensureDbCompatibility();
|
||||
|
||||
// v1.4.43 W13 M-4 — tighter shared bucket on trust-chain misconfig.
|
||||
// v1.4.43 — tighter shared bucket on trust-chain misconfig.
|
||||
const rl = await checkAuthSurfaceRateLimit(
|
||||
request,
|
||||
"auth:passkey-verify",
|
||||
@@ -139,7 +139,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
return apiError("User not found", 404);
|
||||
}
|
||||
|
||||
// v1.4.22 W5 reconcile (Sr-H1) — `createSession` anchors the
|
||||
// v1.4.22 — `createSession` anchors the
|
||||
// `hl_onboarding` cookie itself; pass the user's onboarding state
|
||||
// through so the proxy short-circuits the redirect before
|
||||
// hydration.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* v1.4.43 W6 — multi-issue 422 envelope on POST /api/auth/password.
|
||||
* v1.4.43 — multi-issue 422 envelope on POST /api/auth/password.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
@@ -53,7 +53,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
if (jsonError) return jsonError;
|
||||
const parsed = changePasswordSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
// v1.4.43 W6 — multi-issue 422.
|
||||
// v1.4.43 — multi-issue 422.
|
||||
return returnAllZodIssues(parsed.error, 422);
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
await revokeStepUpElevations(user.id);
|
||||
|
||||
// Invalidate all existing sessions and create a fresh one.
|
||||
// v1.4.22 W5 reconcile (Sr-H1) — `createSession` re-anchors the
|
||||
// v1.4.22 — `createSession` re-anchors the
|
||||
// `hl_onboarding` cookie itself.
|
||||
await destroyAllSessions(user.id);
|
||||
await createSession(
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "@/lib/auth/refresh-token";
|
||||
|
||||
export const POST = apiHandler(async (request: NextRequest) => {
|
||||
// v1.4.43 W13 M-4 — tighter shared bucket on trust-chain misconfig.
|
||||
// v1.4.43 — tighter shared bucket on trust-chain misconfig.
|
||||
// Refresh is a high-value rotation endpoint; align it with the
|
||||
// passkey-verify tier (10/15min) rather than the looser 60/15min. A
|
||||
// legitimate device rotates well under this — the 24h access token only
|
||||
|
||||
Loaded 100 of 1308 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user