` labels and `aria-label`.
+ quick actions): `min-h-11 sm:min-h-9`, a 44 px floor on a phone and the
+ compact `sm` tier on desktop. Plain `min-h-11` without the `sm:` reset
+ is legacy spelling; don't add new ones, and match a button to its
+ direct neighbours when editing one.
+- Focus is always visible: the primitives' `focus-visible:ring` stays on
+ every customised component.
+- Every interactive element has an accessible name. Icon-only buttons get
+ an `aria-label`.
- `prefers-reduced-motion: reduce` disables animations beyond a 200 ms
fade.
-- Charts include text-equivalent summaries (`aria-describedby` linked to
- a `` summary) and exposed data via the visible
- legend / table view toggle.
+- Charts carry a text summary (`aria-describedby` on a `sr-only`
+ paragraph) and expose their data through the visible legend or a table
+ view.
CI fails on any `serious` or `critical` axe-core violation.
---
-## 6. Slider audit and migration
+## 6. Sliders
-Sliders look pretty but are a poor fit for most numeric inputs because
-they sacrifice precision. The 1.4 audit catalogues every `` in
-the codebase and migrates the inappropriate ones:
-
-| Setting | Current control | Better control |
-| ----------------------------------- | --------------- | ------------------------------- |
-| Mood intensity (0–10) | Slider | SegmentedControl |
-| Notification quiet-hours range | Two sliders | Two ` ` |
-| Threshold-override numeric values | Slider | ` ` + steps |
-| AI insight depth (1–5 levels) | Slider | `` cards |
-| Export retention days (7/30/90/365) | Slider | `` with preset options |
-
-A slider stays appropriate for "scrub through history" or "set chart
-zoom range" — operations where the value is continuous and live preview
-is the point.
+A slider gives up precision, so it is the control only where the value is
+a bounded scale or the sweep is the point: a self-rating with anchors
+(`SliderField`, standards §6), scrubbing through history, a chart zoom
+range. Thresholds, counts and times take a number, select or time field.
---
## 7. Internationalization
-All user-facing strings come from `messages/en.json` (default) plus
-the `de`, `es`, `fr`, `it`, and `pl` bundles beside it. Guard tests
-(`i18n-call-site-coverage`, `i18n-locale-integrity`) enforce that every
-`t()` key resolves in EN and propagates across all six locales. Use
-`useTranslations()` on the client and
+All user-facing strings come from `messages/en.json` (the default) plus the
+`de`, `es`, `fr`, `it`, `pl`, `ko`, `nl`, `pt-BR` and `ru` bundles beside
+it. Guard tests (`i18n-call-site-coverage`, `i18n-locale-integrity`) enforce
+that every `t()` key resolves in English and exists, with the same
+placeholders, in all ten locales. Use `useTranslations()` on the client and
`getServerTranslator()` (`src/lib/i18n/server-translator.ts`) on the
-server. Numbers, dates, currencies, and units always go through
-`useFormatters()` — never hand-roll `Intl.NumberFormat(...)` with a
-fixed locale.
+server. Numbers, dates and units always go through `useFormatters()`; never
+hand-roll `Intl.NumberFormat(...)` with a fixed locale.
Server-rendered content (PDFs, notifications, server-issued category
-labels) must use the locale of the user the content is for, not the
-locale of the request that triggered the job.
+labels) uses the locale of the user it is for, not the locale of the
+request that triggered the job.
---
## 8. Motion
Default transitions: `transition-all duration-150 ease-out`. Heavier
-animations (insight cards, dialog enter/exit) use the shadcn defaults.
-Reduce-motion always wins.
+animations (dialog enter and exit) use the primitives' defaults. Reduced
+motion always wins.
-Loading skeletons pulse via Tailwind `animate-pulse`. Don't introduce
-custom keyframes without adding a `prefers-reduced-motion` fallback.
+Loading skeletons pulse via Tailwind `animate-pulse`. Don't add custom
+keyframes without a `prefers-reduced-motion` fallback.
---
## 9. Component checklist (for new features)
-Before opening a PR for a new component or page, walk through:
+Before opening a PR for a new component or page, walk through the PR
+checklist in the standards (§7) and:
-- [ ] Uses tokens from §1, not hard-coded hex/rgba.
-- [ ] Buttons follow §2.1 variant + position rules.
-- [ ] Inputs follow §2.2; correct `autocomplete`; honey-pots where needed.
-- [ ] Dialog vs Sheet vs Drawer per §2.3.
-- [ ] Loading, empty, and error states from §4.1–4.3 implemented.
-- [ ] All strings via `t()`; numbers via formatters.
-- [ ] Mobile viewport tested (Chrome DevTools 375×667 and 414×896).
-- [ ] Light + Dark mode tested.
-- [ ] Keyboard navigation: Tab order, Enter to submit, Esc to dismiss.
-- [ ] axe-core clean (no serious/critical).
-- [ ] Headless screenshot test added if UI is non-trivial.
-
----
-
-_Maintained as part of the v1.4 release cycle. Propose changes alongside
-the code that motivates them._
+- [ ] Buttons follow §2.1.
+- [ ] Inputs follow §2.2; correct `autoComplete`.
+- [ ] Dialog or sheet per §2.3.
+- [ ] Loading, empty and error states from §4.1 to §4.3.
+- [ ] All strings via `t()`; numbers via the formatters.
+- [ ] Tested at phone width (390 px) and on desktop.
+- [ ] Light and dark theme checked.
+- [ ] Keyboard: Tab order, Enter to submit, Esc to dismiss.
+- [ ] axe-core clean (no serious or critical findings).
+- [ ] A screenshot test added if the UI is non-trivial.
diff --git a/e2e/README.md b/e2e/README.md
index 202dd3dea..f6da63bf9 100644
--- a/e2e/README.md
+++ b/e2e/README.md
@@ -21,4 +21,4 @@ The Playwright config (`../playwright.config.ts`) builds the app (`next build`)
Import `test` and `expect` from `./setup/test`, never from `@playwright/test` — types (`Page`, `Route`, …) still come from the package. The entry point extends Playwright's `test` with an automatic fixture that drops route handlers and page listeners after every test, so a handler left in flight cannot fail the test it belonged to once the context closes. `src/__tests__/e2e-route-cleanup-guard.test.ts` enforces the import boundary.
-Assert against stable `data-*` attributes (`data-slot`, `data-display-step`, root display attrs), not viewport-dependent visible text. Responsive `sm:hidden` UI changes break `getByText` on desktop while the underlying element is still present — anchoring on data attributes keeps the specs stable across breakpoints. See [`../CLAUDE.md`](../CLAUDE.md).
+Assert against stable `data-*` attributes (`data-slot`, `data-display-step`, root display attrs), not viewport-dependent visible text. Responsive `sm:hidden` UI changes break `getByText` on desktop while the underlying element is still present — anchoring on data attributes keeps the specs stable across breakpoints.
diff --git a/e2e/admin-api-tokens-mobile.spec.ts b/e2e/admin-api-tokens-mobile.spec.ts
index 7c45949ab..d16dfbb14 100644
--- a/e2e/admin-api-tokens-mobile.spec.ts
+++ b/e2e/admin-api-tokens-mobile.spec.ts
@@ -137,7 +137,7 @@ test.describe("/admin/api-tokens mobile no-overflow", () => {
data: [
{
id: "tok1",
- name: "iOS auto-login 2026-05-05T19:42:11Z device-AABBCCDDEEFF-marathon",
+ name: "iOS auto-login 2026-05-05T19:42:11Z device-AABBCCDDEEFF-long-name",
permissions: [
"measurements:write:audit:legacy:extended",
"medications:read",
diff --git a/e2e/chrome-header-seam-banners.spec.ts b/e2e/chrome-header-seam-banners.spec.ts
index 87e470274..cb6a89165 100644
--- a/e2e/chrome-header-seam-banners.spec.ts
+++ b/e2e/chrome-header-seam-banners.spec.ts
@@ -221,7 +221,7 @@ async function enterSharedRecord(page: Page): Promise {
}
/**
- * Swap the context's pinned locale for one HealthLog ships as an AI-initial
+ * Swap the context's pinned locale for one HealthLog ships as a machine
* translation, so the maintainership strip has a reason to paint.
*
* The page is parked on `about:blank` for the swap. `I18nProvider` mirrors its
diff --git a/e2e/dashboard.spec.ts b/e2e/dashboard.spec.ts
index 0aa90fe5d..51d7ef289 100644
--- a/e2e/dashboard.spec.ts
+++ b/e2e/dashboard.spec.ts
@@ -21,12 +21,10 @@ test.describe("authenticated dashboard render", () => {
test.use({ storageState: STORAGE_STATE_PATH });
test.beforeEach(async ({ page }) => {
- // v1.7.2 — the dashboard snapshot flag is default-ON, so the page
- // reads every tile from `/api/dashboard/snapshot` and ignores the
- // legacy cells below. Mock the snapshot with the same populated
- // four-metric set so the tile strip + charts paint. The legacy
- // `/api/analytics` + `/api/mood/analytics` mocks stay for the
- // reversible `NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false` path.
+ // The page reads every tile from `/api/dashboard/snapshot`; mock it
+ // with the same populated four-metric set so the tile strip + charts
+ // paint. The dashboard ignores the `/api/analytics` and
+ // `/api/mood/analytics` mocks below.
await mockDashboardSnapshot(page);
// Mock analytics with a populated weight summary so the weight tile
diff --git a/e2e/doctor-report-delegate.spec.ts b/e2e/doctor-report-delegate.spec.ts
index 6cf5db593..28a69ca88 100644
--- a/e2e/doctor-report-delegate.spec.ts
+++ b/e2e/doctor-report-delegate.spec.ts
@@ -8,7 +8,7 @@
* record the delegate may only read, 200 inside the one it manages.
*
* The UI half proves something narrower, and the file says so rather than
- * implying the route's boundary twice. `/settings/gesundheitsakte` is a
+ * implying the route's boundary twice. `/settings/health-record` is a
* PERSONAL destination: inside SOMEBODY ELSE's record the shell answers it with
* the "not part of shared access" panel at every adult grant level — READ,
* WRITE and MANAGE alike — so the export card is never rendered for a delegate
@@ -149,7 +149,7 @@ async function leaveRecord(page: Page): Promise {
* card was never offered.
*/
async function openGesundheitsakte(page: Page): Promise {
- await page.goto("/settings/gesundheitsakte", {
+ await page.goto("/settings/health-record", {
waitUntil: "domcontentloaded",
});
await expect(
diff --git a/e2e/doctor-report.spec.ts b/e2e/doctor-report.spec.ts
index b8909cd2a..1f5f02e9e 100644
--- a/e2e/doctor-report.spec.ts
+++ b/e2e/doctor-report.spec.ts
@@ -247,7 +247,7 @@ async function openReportPanel(page: Page): Promise {
response.request().method() === "GET" &&
new URL(response.url()).pathname === "/api/share-links",
);
- await page.goto("/settings/gesundheitsakte", {
+ await page.goto("/settings/health-record", {
waitUntil: "domcontentloaded",
});
await boundaryHydrated;
diff --git a/e2e/medication-compliance-journey.spec.ts b/e2e/medication-compliance-journey.spec.ts
index 809f7120e..c9358f1d1 100644
--- a/e2e/medication-compliance-journey.spec.ts
+++ b/e2e/medication-compliance-journey.spec.ts
@@ -466,7 +466,7 @@ test.describe("medication adherence journey", () => {
// The 90-day ledger beside it is a view of the SCHEDULE and stops at now,
// so a dose skipped ahead of its window is not in it. The intake history
// is the view of what was recorded, and both gestures belong there.
- await page.goto(`/medications/${medicationId}?tab=verlauf`);
+ await page.goto(`/medications/${medicationId}?tab=history`);
await page
.locator('[data-slot="medication-verlauf-view-toggle-all"]')
.click();
diff --git a/e2e/mobile-route-overflow.spec.ts b/e2e/mobile-route-overflow.spec.ts
index 127ad3a3c..af82b1cc1 100644
--- a/e2e/mobile-route-overflow.spec.ts
+++ b/e2e/mobile-route-overflow.spec.ts
@@ -92,7 +92,7 @@ const DYNAMIC_ROUTES: Record = {
"labs",
"illness",
"documents",
- "vorsorge",
+ "checkups",
].map((group) => `/settings/layout/${group}`),
"/settings/modules/[module]": async () =>
MODULE_SETTINGS_PAGE_IDS.map((module) => `/settings/modules/${module}`),
diff --git a/e2e/notification-dispatch-journey.spec.ts b/e2e/notification-dispatch-journey.spec.ts
index ea8b68a90..97d583b91 100644
--- a/e2e/notification-dispatch-journey.spec.ts
+++ b/e2e/notification-dispatch-journey.spec.ts
@@ -661,7 +661,7 @@ test.describe("notification preferences drive the dispatch decision", () => {
await page.goto(`/medications/${medicationId}`, {
waitUntil: "domcontentloaded",
});
- await page.locator('[data-slot="medication-tab-zeitplan"]').click();
+ await page.locator('[data-slot="medication-tab-schedule"]').click();
await expect(
page.locator('[data-slot="notifications-client-managed-chip"]'),
).toBeVisible({ timeout: 30_000 });
diff --git a/e2e/settings-admin-one-piece.spec.ts b/e2e/settings-admin-one-piece.spec.ts
index 2b8e70726..f567665f8 100644
--- a/e2e/settings-admin-one-piece.spec.ts
+++ b/e2e/settings-admin-one-piece.spec.ts
@@ -53,7 +53,7 @@ const SETTINGS_ROUTES = [
"/settings/sources",
"/settings/environment",
"/settings/export",
- "/settings/gesundheitsakte",
+ "/settings/health-record",
"/settings/privacy",
"/settings/api",
"/settings/mcp",
@@ -76,7 +76,7 @@ const LAYOUT_CHILD_ROUTES = [
"/settings/layout/medications",
"/settings/layout/labs",
"/settings/layout/illness",
- "/settings/layout/vorsorge",
+ "/settings/layout/checkups",
"/settings/modules/medications",
"/settings/modules/mood",
"/settings/modules/labs",
diff --git a/e2e/settings-export.spec.ts b/e2e/settings-export.spec.ts
index 0fe1bae84..1bbcfab5c 100644
--- a/e2e/settings-export.spec.ts
+++ b/e2e/settings-export.spec.ts
@@ -14,7 +14,7 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup";
* 2. The import surfaces (v1.15.7, issue #281) render: the Apple Health
* and generic-JSON cards.
* 3. The full health-record export panel lives on its own top-level
- * `/settings/gesundheitsakte` section (v1.18.0 S5) — including the
+ * `/settings/health-record` section (v1.18.0 S5) — including the
* "included data" disclosure, which a first run opens and which stays
* operable in both directions.
* 4. The scope picker mounts twice on that page (the export panel and the
@@ -45,7 +45,7 @@ async function openGesundheitsakte(page: Page): Promise {
response.request().method() === "GET" &&
new URL(response.url()).pathname === "/api/share-links",
);
- await page.goto("/settings/gesundheitsakte", {
+ await page.goto("/settings/health-record", {
waitUntil: "domcontentloaded",
});
await boundaryHydrated;
@@ -57,7 +57,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 — the full health-record export moved to its own
- // `/settings/gesundheitsakte` section. The Export & Import page keeps
+ // `/settings/health-record` section. The Export & Import page keeps
// the four CSV/backup tiles with the `export-card-*` shape.
for (const id of [
"export-card-measurements-csv",
@@ -250,7 +250,7 @@ test.describe("Settings → Export & Import", () => {
page,
}) => {
// The disclosure that fronts the scope picker on
- // `/settings/gesundheitsakte`. An account with no saved selection is a
+ // `/settings/health-record`. An account with no saved selection is a
// first run, and a first run opens it: that is the only place the standard
// template is applied, and a template hidden behind a closed disclosure
// would be opt-out wearing a hat.
diff --git a/e2e/utils/mock-dashboard-snapshot.ts b/e2e/utils/mock-dashboard-snapshot.ts
index 8e3ae7574..9f3fec2a2 100644
--- a/e2e/utils/mock-dashboard-snapshot.ts
+++ b/e2e/utils/mock-dashboard-snapshot.ts
@@ -88,20 +88,16 @@ function mockTargetBands(): DashboardTargetBands {
/**
* v1.7.2 — shared mock for `GET /api/dashboard/snapshot`.
*
- * The dashboard snapshot flag flipped default-ON in v1.7.2, so
* `src/app/page.tsx` reads every above-the-fold tile from the single
- * `/api/dashboard/snapshot` cell unless `NEXT_PUBLIC_DASHBOARD_SNAPSHOT`
- * is `"false"`. The e2e build sets no such var, so the snapshot path is
- * live in CI. Specs that previously mocked only the legacy cells
- * (`/api/analytics` + `/api/mood/analytics` + `/api/dashboard/widgets`)
- * now hit the REAL snapshot route, which returns empty `summaries` for
- * the seed user (zero measurements) and blanks the tile strip.
+ * `/api/dashboard/snapshot` cell. Without a mock a spec hits the REAL
+ * snapshot route, which returns empty `summaries` for the seed user (zero
+ * measurements) and blanks the tile strip.
*
* `mockDashboardSnapshot` registers a `page.route` for the snapshot
- * endpoint returning a correctly-typed `DashboardSnapshot` so the
- * default-ON path paints deterministic tiles. Callers keep their legacy
- * mocks alongside it — harmless, and they protect the reversible
- * `NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false` path.
+ * endpoint returning a correctly-typed `DashboardSnapshot` so the tiles
+ * paint deterministically. Mocks of the older cells (`/api/analytics`,
+ * `/api/mood/analytics`, `/api/dashboard/widgets`) beside it are unused by
+ * the dashboard.
*/
/** Fill a `DataSummary` from a sparse partial, defaulting the rest. */
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 0a1d19700..db73867df 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -13,15 +13,11 @@ const eslintConfig = defineConfig([
"out/**",
"build/**",
"next-env.d.ts",
- // v1.31.4 — ignore local tool worktrees so a worktree on another
- // commit cannot poison `pnpm lint` locally.
- // CI never sees these paths; the rules protect the dev environment.
- // `.wt-*` is the convention actually in use; the two names below predate
- // it. Without the first pattern `pnpm lint` walks every worktree in the
- // root, which is both slow and exactly the poisoning this list prevents.
- ".wt-*/**",
- ".claude/**",
- ".worktrees/**",
+ // Hidden top-level folders hold local worktrees and caches, and no
+ // tracked code lives in one (`.github` and `.githooks` hold no JS or
+ // TS). Without this `pnpm lint` walks every worktree on another commit
+ // in the root, which is both slow and poisons the result.
+ ".*/**",
// The project-local ESLint rule plugin is CommonJS (require/module
// .exports) by ESLint convention; it is not application source and
// is not linted by the app's TypeScript ruleset.
diff --git a/lefthook.yml b/lefthook.yml
index d193d5b25..119054b6e 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -16,20 +16,22 @@
# Per project policy (`--no-verify` is banned), prefer fixing the
# finding over bypassing the hook.
-# Commit-message guard: the project voice rule (CLAUDE.md, "Voice and
-# privacy") states that commits are authored by the maintainer's git
-# identity alone and carry no tooling co-author trailer or session link.
-# Editors and CLIs add those trailers by default, and a message that
-# picks one up is only removable from the object store by a history
-# rewrite — so the check belongs here, before the commit exists, not in
-# a review pass afterwards.
+# Commit messages. Commits are authored by the maintainer's git identity;
+# a co-author trailer is only for a contributor whose work is in the
+# commit, under their GitHub noreply address. A message is refused when it
+# carries a "Generated with" / "Generated by" line, a "-Session:"
+# trailer, or a co-author trailer with any other address.
commit-msg:
commands:
- no-tooling-trailer:
+ message-trailers:
run: |
- if grep -qiE '^(Co-Authored-By:.*(claude|anthropic|copilot|gpt|gemini)|Claude-Session:|Generated with)' "{1}"; then
- echo "ERROR: the commit message carries a tooling co-author trailer or session link." >&2
- echo "Project policy: commits are authored by the maintainer's git identity alone." >&2
+ msg="{1}"
+ if grep -qiE '^[^[:alnum:]]*Generated (with|by)[[:space:]]' "$msg" ||
+ grep -qiE '^[[:alpha:]]+-Session:' "$msg" ||
+ grep -iE '^Co-authored-by:' "$msg" |
+ grep -qviE '<[^<>[:space:]]+@users\.noreply\.github\.com>[[:space:]]*$'; then
+ echo "ERROR: the commit message carries a generator line, a session trailer," >&2
+ echo "or a co-author trailer without a GitHub noreply address." >&2
echo "Remove the offending line(s) from the message and commit again." >&2
exit 1
fi
diff --git a/messages/de.json b/messages/de.json
index 41b64180e..77b23e200 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -764,7 +764,7 @@
"noneActionableCta": "Zu den Medikamenten"
},
"quickEntryDiscard": {
- "title": "Eintrag verwerfen?",
+ "title": "Eingaben verwerfen?",
"description": "Du hast nicht gespeicherte Eingaben. Formular schließen und das Getippte verwerfen?",
"confirm": "Verwerfen",
"cancel": "Weiter bearbeiten"
@@ -802,7 +802,7 @@
"moveUp": "Nach oben",
"moveDown": "Nach unten",
"layoutTileColumn": "Kachel",
- "layoutChartColumn": "Chart",
+ "layoutChartColumn": "Diagramm",
"layoutNativeOnlyTitle": "In der mobilen App sichtbar",
"layoutNativeOnlyHint": "Diese Widgets zeichnet die mobile App, nicht diese Seite. Sie liegen im selben gespeicherten Layout, hier wird ihre Sichtbarkeit gesetzt.",
"dragHandle": "Zum Verschieben ziehen",
@@ -1547,7 +1547,6 @@
"glp1NextInjectionToday": "heute",
"glp1NextInjectionTomorrow": "morgen",
"glp1RotationSuggested": "Empfohlen: {site}",
- "glp1LogSideEffect": "Nebenwirkung loggen",
"sideEffects": {
"section": "Nebenwirkungen",
"addCta": "Erfassen",
@@ -3583,7 +3582,8 @@
"emptyState": {
"title": "Noch keine HRV-Werte",
"description": "HRV-Werte erscheinen hier, sobald die Apple Watch (oder ein kompatibles Withings-Gerät) nächtliche Messungen synchronisiert."
- }
+ },
+ "coachPrefill": "Ich habe noch keine HRV-Daten erfasst. Was sagt mir die Herzfrequenzvariabilität, und wie erfasse ich sie?"
},
"restingHr": {
"title": "Ruhepuls",
@@ -3602,7 +3602,8 @@
"emptyState": {
"title": "Noch keine SpO₂-Werte",
"description": "SpO₂-Werte erscheinen, sobald ein kompatibles Wearable (Apple Watch, Withings ScanWatch) eine Messung erfasst."
- }
+ },
+ "coachPrefill": "Ich habe noch keine Sauerstoffsättigung erfasst. Was sagt mir die SpO₂, und welchen Bereich sollte ich anstreben?"
},
"bodyTemperature": {
"title": "Körpertemperatur",
@@ -3612,7 +3613,8 @@
"title": "Noch keine Körpertemperatur",
"description": "Erfasse eine Körpertemperatur oder lass dein Wearable einen Wert synchronisieren. Der Verlauf erscheint dann hier.",
"cta": "Körpertemperatur erfassen"
- }
+ },
+ "coachPrefill": "Ich habe noch keine Körpertemperatur erfasst. Was ist der gesunde Bereich, und was sollte ich tun, wenn sie abweicht?"
},
"steps": {
"title": "Schritte",
@@ -3621,7 +3623,8 @@
"emptyState": {
"title": "Noch keine Schrittdaten",
"description": "Tägliche Schritt-Summen erscheinen hier, sobald Apple Health oder ein Aktivitätsgerät einen Tagesumfang synchronisiert."
- }
+ },
+ "coachPrefill": "Ich habe noch keine Schritte erfasst. Was ist ein sinnvolles tägliches Schrittziel, und wie hilft mehr Gehen meiner Gesundheit?"
},
"activeEnergy": {
"title": "Aktive Energie",
@@ -3630,7 +3633,8 @@
"emptyState": {
"title": "Noch keine Aktive-Energie-Werte",
"description": "Aktive-Energie-Summen erscheinen hier, sobald Apple Health oder ein Withings-Aktivitätsgerät einen Tagesumfang synchronisiert."
- }
+ },
+ "coachPrefill": "Ich habe noch keine aktive Energie erfasst. Was ist ein sinnvolles Tagesziel, und wie verhält es sich zum gesamten Kalorienverbrauch?"
},
"subPage": {
"loadError": "Diese Messgröße konnte gerade nicht geladen werden.",
@@ -3725,7 +3729,8 @@
"valuesBack": "Zurück zu Insights",
"valuesBackToMetric": "Zurück zu {metric}",
"scoresBack": "Zurück zu Insights",
- "ecgDescription": "Einkanal-EKG-Aufzeichnungen deines Geräts, jeweils mit dem Ergebnis des Geräts."
+ "ecgDescription": "Einkanal-EKG-Aufzeichnungen deines Geräts, jeweils mit dem Ergebnis des Geräts.",
+ "emptyCoachPrefill": "{metric}: Ich habe hier noch nichts erfasst. Was sagt dieser Wert über meine Gesundheit aus, und wie verbessere ich ihn?"
},
"units": {
"steps": "Schritte",
@@ -3825,27 +3830,32 @@
"bloodPressure": {
"title": "Noch keine Blutdruckwerte",
"description": "Sobald du Blutdruck loggst, erscheinen hier Trend, Zielbereichs-Anteil und WHO-Klassifizierung.",
- "cta": "Blutdruck loggen"
+ "cta": "Blutdruck loggen",
+ "coachPrefill": "Ich habe noch keinen Blutdruck erfasst. Warum ist er wichtig, und was sollte ich wissen, bevor ich anfange?"
},
"weight": {
"title": "Noch keine Gewichtswerte",
"description": "Logge ein Gewicht, und Verlauf, gleitender Durchschnitt und Zielbereich erscheinen automatisch.",
- "cta": "Gewicht loggen"
+ "cta": "Gewicht loggen",
+ "coachPrefill": "Ich habe noch kein Gewicht erfasst. Warum ist es wichtig, und was sollte ich wissen, bevor ich mit dem Erfassen anfange?"
},
"pulse": {
"title": "Noch keine Pulswerte",
"description": "Logge einen Puls, dann erscheinen Ruhepuls, HRV und VO₂ max in dieser Ansicht.",
- "cta": "Puls loggen"
+ "cta": "Puls loggen",
+ "coachPrefill": "Ich habe noch keinen Ruhepuls erfasst. Warum ist er wichtig, und was sollte ich wissen, bevor ich anfange?"
},
"bmi": {
"title": "BMI noch nicht berechnet",
"description": "Sobald Größe und mindestens ein Gewichtswert vorliegen, erscheint hier der BMI-Verlauf samt WHO-Zonen.",
- "cta": "Gewicht loggen"
+ "cta": "Gewicht loggen",
+ "coachPrefill": "Ich habe noch kein Gewicht erfasst. Warum ist der BMI für mich wichtig, und was sollte ich wissen, bevor ich ihn verfolge?"
},
"mood": {
"title": "Noch keine Stimmungseinträge",
"description": "Logge eine Stimmung, dann erscheinen Verlauf, Stabilitätsband und Korrelationen.",
- "cta": "Stimmung loggen"
+ "cta": "Stimmung loggen",
+ "coachPrefill": "Ich habe noch keine Stimmung erfasst. Warum lohnt es sich, die Stimmung zu erfassen, und wie fange ich am besten an?"
},
"medication": {
"title": "Noch keine Medikamente angelegt",
@@ -3855,7 +3865,8 @@
"sleep": {
"title": "Noch keine Schlafwerte",
"description": "Sobald Schlafdaten vorliegen (manuell oder via Withings / Apple Health), erscheint hier der nächtliche Stadien-Verlauf.",
- "cta": "Quelle verbinden"
+ "cta": "Quelle verbinden",
+ "coachPrefill": "Ich habe noch keine Schlafdaten. Warum lohnt es sich, Schlaf zu erfassen, und was sollte ich wissen, bevor ich eine Quelle verbinde?"
}
},
"navWorkouts": "Workouts",
@@ -3865,7 +3876,8 @@
"description": "Letzte Läufe, Touren, Spaziergänge und Krafteinheiten, quellenübergreifend entdoppelt zwischen Apple Watch und Withings.",
"emptyState": {
"title": "Noch keine Workouts",
- "description": "Workouts kommen per Apple Health vom iPhone und von Withings. Du kannst eins auch von Hand erfassen."
+ "description": "Workouts kommen per Apple Health vom iPhone und von Withings. Du kannst eins auch von Hand erfassen.",
+ "coachPrefill": "Ich habe noch keine Workouts erfasst. Warum lohnt es sich, sie zu erfassen, und worauf sollte ich mich zuerst konzentrieren?"
},
"detail": {
"backToList": "Zurück zu Workouts",
@@ -4745,7 +4757,8 @@
"emptyState": {
"title": "Noch keine Sturzdaten",
"description": "Erkannte Stürze erscheinen hier, sobald Apple Health synchronisiert. Keine Stürze ist das Ziel."
- }
+ },
+ "coachPrefill": "Ich habe noch keine Sturzdaten erfasst. Was sagt dieser Wert über meine Gesundheit aus, und wie senke ich mein Sturzrisiko?"
},
"navFalls": "Stürze",
"sixMinuteWalk": {
@@ -5695,7 +5708,7 @@
"title": "Krankheitstagebuch",
"description": "Listenansicht und Reihenfolge der Episoden."
},
- "vorsorge": {
+ "checkups": {
"title": "Vorsorge",
"description": "Listenansicht und Reihenfolge der Erinnerungen."
},
@@ -6121,7 +6134,7 @@
"moduleSettingsDescription": "Alles außer Reihenfolge und Ansicht findest du auf der Seite des Moduls unter Module."
}
},
- "gesundheitsakte": {
+ "health-record": {
"title": "Gesundheitsakte",
"subtitle": "Exportiere eine vollständige, arzttaugliche Gesundheitsakte."
},
@@ -7058,7 +7071,7 @@
"builtAtLabel": "Erstellt",
"license": "Lizenz",
"repository": "Quellcode",
- "changelog": "Changelog",
+ "changelog": "Versionshinweise",
"docs": "Dokumentation",
"credits": "Credits & Datenquellen",
"linksHeading": "Quellen & Dokumentation",
@@ -10229,6 +10242,7 @@
"BLOOD_PRESSURE_SYS": "Blutdruck",
"PULSE": "Puls",
"BLOOD_GLUCOSE": "Blutzucker",
+ "BLOOD_KETONES": "Blutketone",
"OXYGEN_SATURATION": "Sauerstoffsättigung",
"BODY_FAT": "Körperfett",
"BODY_TEMPERATURE": "Körpertemperatur",
@@ -11073,7 +11087,7 @@
"notePlaceholder": "Wie sich der Tag anfühlte (verschlüsselt).",
"severityFor": "Stärke für {symptom}",
"saveError": "Der Tag konnte nicht gespeichert werden. Bitte erneut versuchen.",
- "day": "Day"
+ "day": "Tag"
},
"impact": {
"0": "Voll belastbar",
@@ -11162,14 +11176,14 @@
"RESTING_HEART_RATE": "Ruheherzfrequenz",
"HEART_RATE_VARIABILITY": "Herzfrequenzvariabilität",
"HRV_RMSSD": "HRV (RMSSD)",
- "RECOVERY_SCORE": "Recovery",
+ "RECOVERY_SCORE": "Erholung",
"OXYGEN_SATURATION": "Sauerstoffsättigung",
"BODY_TEMPERATURE": "Körpertemperatur",
"RESPIRATORY_RATE": "Atemfrequenz",
"BLOOD_PRESSURE_SYS": "Systolischer Blutdruck",
"BLOOD_PRESSURE_DIA": "Diastolischer Blutdruck",
- "PULSE": "Pulse",
- "WEIGHT": "Weight",
+ "PULSE": "Puls",
+ "WEIGHT": "Gewicht",
"FUNCTIONAL_IMPACT": "Funktionelle Beeinträchtigung"
},
"listLoadError": "Episoden konnten nicht geladen werden."
diff --git a/messages/en.json b/messages/en.json
index dea9402a2..6705917dc 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -764,7 +764,7 @@
"noneActionableCta": "Open medications"
},
"quickEntryDiscard": {
- "title": "Discard this entry?",
+ "title": "Discard your changes?",
"description": "You have unsaved input. Close the form and lose what you typed?",
"confirm": "Discard",
"cancel": "Keep editing"
@@ -1547,7 +1547,6 @@
"glp1NextInjectionToday": "today",
"glp1NextInjectionTomorrow": "tomorrow",
"glp1RotationSuggested": "Recommended next: {site}",
- "glp1LogSideEffect": "Log side effect",
"sideEffects": {
"section": "Side effects",
"addCta": "Log",
@@ -3583,7 +3582,8 @@
"emptyState": {
"title": "No HRV samples yet",
"description": "HRV samples land here once Apple Watch (or a compatible Withings device) starts syncing nightly readings."
- }
+ },
+ "coachPrefill": "I haven't logged any HRV data yet. What does heart-rate variability tell me, and how do I capture it?"
},
"restingHr": {
"title": "Resting heart rate",
@@ -3602,7 +3602,8 @@
"emptyState": {
"title": "No SpO₂ samples yet",
"description": "SpO₂ samples land here once a compatible wearable (Apple Watch, Withings ScanWatch) captures a reading."
- }
+ },
+ "coachPrefill": "I haven't logged any oxygen-saturation data yet. What does SpO₂ tell me, and what range should I aim for?"
},
"bodyTemperature": {
"title": "Body temperature",
@@ -3612,7 +3613,8 @@
"title": "No body temperature yet",
"description": "Log a body-temperature reading or let your wearable sync one, and the trend will appear here.",
"cta": "Log body temperature"
- }
+ },
+ "coachPrefill": "I haven't logged any body temperature yet. What's the healthy range, and what should I do if it drifts?"
},
"steps": {
"title": "Steps",
@@ -3621,7 +3623,8 @@
"emptyState": {
"title": "No step data yet",
"description": "Daily step totals appear here once Apple Health or an activity device syncs a day's worth of movement."
- }
+ },
+ "coachPrefill": "I haven't logged any steps yet. What's a reasonable daily step target, and how does walking more help my health?"
},
"activeEnergy": {
"title": "Active energy",
@@ -3630,7 +3633,8 @@
"emptyState": {
"title": "No active-energy data yet",
"description": "Active-energy totals land here once Apple Health or a Withings activity device syncs a day's worth of movement."
- }
+ },
+ "coachPrefill": "I haven't logged any active energy yet. What's a reasonable daily target, and how does it compare to total calories?"
},
"subPage": {
"loadError": "Could not load this metric right now.",
@@ -3725,7 +3729,8 @@
"valuesBack": "Back to insights",
"valuesBackToMetric": "Back to {metric}",
"scoresBack": "Back to insights",
- "ecgDescription": "Single-lead ECG recordings from your device, each with the device's own result."
+ "ecgDescription": "Single-lead ECG recordings from your device, each with the device's own result.",
+ "emptyCoachPrefill": "{metric}: I haven't logged anything here yet. What does this metric tell me about my health, and how do I improve it?"
},
"units": {
"steps": "steps",
@@ -3825,27 +3830,32 @@
"bloodPressure": {
"title": "No blood pressure entries yet",
"description": "Log a blood pressure reading and the trend chart, in-range share, and WHO classification will appear here.",
- "cta": "Log blood pressure"
+ "cta": "Log blood pressure",
+ "coachPrefill": "I haven't recorded any blood pressure yet. Why does it matter, and what should I know before I start?"
},
"weight": {
"title": "No weight entries yet",
"description": "Log a weight and the trend, moving average, and target band will appear automatically.",
- "cta": "Log weight"
+ "cta": "Log weight",
+ "coachPrefill": "I haven't recorded any weight yet. Why does it matter, and what should I know before I start tracking?"
},
"pulse": {
"title": "No pulse entries yet",
"description": "Log a pulse reading and resting pulse, HRV and VO₂ max will appear in this view.",
- "cta": "Log pulse"
+ "cta": "Log pulse",
+ "coachPrefill": "I haven't recorded any resting pulse yet. Why does it matter, and what should I know before I start?"
},
"bmi": {
"title": "BMI not yet computed",
"description": "Once height and at least one weight entry are present, the BMI trend with WHO zones will appear here.",
- "cta": "Log weight"
+ "cta": "Log weight",
+ "coachPrefill": "I haven't recorded any weight yet. Why does BMI matter for me, and what should I know before I start tracking it?"
},
"mood": {
"title": "No mood entries yet",
"description": "Log a mood entry and the trend, stability band and correlations will appear.",
- "cta": "Log mood"
+ "cta": "Log mood",
+ "coachPrefill": "I haven't logged any mood entries yet. Why does mood tracking matter, and how should I start?"
},
"medication": {
"title": "No medications set up yet",
@@ -3855,7 +3865,8 @@
"sleep": {
"title": "No sleep entries yet",
"description": "Once sleep data lands (manual or via Withings / Apple Health), the per-night stage breakdown will appear here.",
- "cta": "Connect a source"
+ "cta": "Connect a source",
+ "coachPrefill": "I don't have any sleep data yet. Why does sleep tracking matter, and what should I know before I connect a source?"
}
},
"navWorkouts": "Workouts",
@@ -3865,7 +3876,8 @@
"description": "Recent runs, rides, walks and strength sessions, deduped across Apple Watch and Withings sources.",
"emptyState": {
"title": "No workouts yet",
- "description": "Workouts sync from Apple Health on your iPhone and from Withings. You can also log one by hand."
+ "description": "Workouts sync from Apple Health on your iPhone and from Withings. You can also log one by hand.",
+ "coachPrefill": "I haven't logged any workouts yet. Why does tracking them matter, and what should I focus on first?"
},
"detail": {
"backToList": "Back to workouts",
@@ -4745,7 +4757,8 @@
"emptyState": {
"title": "No fall data yet",
"description": "Detected falls appear here once Apple Health syncs. No falls is the goal."
- }
+ },
+ "coachPrefill": "I haven't logged any fall data yet. What does this metric tell me about my health, and how do I lower my fall risk?"
},
"navFalls": "Falls",
"sixMinuteWalk": {
@@ -5695,7 +5708,7 @@
"title": "Illness journal",
"description": "List view and episode order."
},
- "vorsorge": {
+ "checkups": {
"title": "Checkups",
"description": "List view and reminder order."
},
@@ -6121,7 +6134,7 @@
"moduleSettingsDescription": "Everything beyond order and view lives on the module's page under Modules."
}
},
- "gesundheitsakte": {
+ "health-record": {
"title": "Health record",
"subtitle": "Export a complete, clinician-ready health record."
},
@@ -7058,7 +7071,7 @@
"builtAtLabel": "Built",
"license": "License",
"repository": "Source code",
- "changelog": "Changelog",
+ "changelog": "Release notes",
"docs": "Documentation",
"credits": "Credits & data attributions",
"linksHeading": "Sources & docs",
@@ -10229,6 +10242,7 @@
"BLOOD_PRESSURE_SYS": "Blood pressure",
"PULSE": "Pulse",
"BLOOD_GLUCOSE": "Blood glucose",
+ "BLOOD_KETONES": "Blood ketones",
"OXYGEN_SATURATION": "Oxygen saturation",
"BODY_FAT": "Body fat",
"BODY_TEMPERATURE": "Body temperature",
diff --git a/messages/es.json b/messages/es.json
index e4284f1e3..791a45a5b 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -764,7 +764,7 @@
"noneActionableCta": "Abrir medicamentos"
},
"quickEntryDiscard": {
- "title": "¿Descartar esta entrada?",
+ "title": "¿Descartar los cambios?",
"description": "Tienes datos sin guardar. ¿Cerrar el formulario y perder lo que escribiste?",
"confirm": "Descartar",
"cancel": "Seguir editando"
@@ -1547,7 +1547,6 @@
"glp1NextInjectionToday": "hoy",
"glp1NextInjectionTomorrow": "mañana",
"glp1RotationSuggested": "Sugerido: {site}",
- "glp1LogSideEffect": "Registrar efecto secundario",
"sideEffects": {
"section": "Efectos secundarios",
"addCta": "Registrar",
@@ -1741,7 +1740,7 @@
"step1": {
"title": "¿Cómo se llama el medicamento?",
"subline": "Exactamente como aparece en la caja.",
- "label": "Name",
+ "label": "Nombre",
"placeholder": "p. ej. Ramipril",
"short": "Nombre"
},
@@ -3583,7 +3582,8 @@
"emptyState": {
"title": "Aún no hay valores de VFC",
"description": "Los valores de VFC aparecerán aquí en cuanto Apple Watch (o un dispositivo Withings compatible) sincronice las mediciones nocturnas."
- }
+ },
+ "coachPrefill": "Aún no he registrado datos de VFC: ¿qué me dice la variabilidad de la frecuencia cardíaca y cómo la registro?"
},
"restingHr": {
"title": "FC en reposo",
@@ -3602,7 +3602,8 @@
"emptyState": {
"title": "Aún no hay muestras de SpO₂",
"description": "Las muestras de SpO₂ llegan aquí en cuanto un wearable compatible (Apple Watch, Withings ScanWatch) capture una lectura."
- }
+ },
+ "coachPrefill": "Aún no he registrado la saturación de oxígeno: ¿qué me dice la SpO₂ y a qué rango debería aspirar?"
},
"bodyTemperature": {
"title": "Temperatura corporal",
@@ -3612,7 +3613,8 @@
"title": "Aún no hay temperatura corporal",
"description": "Registra una lectura o deja que tu wearable sincronice una, y la tendencia aparecerá aquí.",
"cta": "Registrar temperatura corporal"
- }
+ },
+ "coachPrefill": "Aún no he registrado la temperatura corporal: ¿cuál es el rango saludable y qué debo hacer si se desvía?"
},
"steps": {
"title": "Pasos",
@@ -3621,7 +3623,8 @@
"emptyState": {
"title": "Aún no hay datos de pasos",
"description": "Los totales diarios de pasos aparecerán aquí en cuanto Apple Salud o un dispositivo de actividad sincronice un día completo."
- }
+ },
+ "coachPrefill": "Aún no he registrado pasos: ¿cuál es un objetivo diario de pasos razonable y cómo ayuda a mi salud caminar más?"
},
"activeEnergy": {
"title": "Energía activa",
@@ -3630,7 +3633,8 @@
"emptyState": {
"title": "Aún no hay datos de energía activa",
"description": "Los totales de energía activa aparecerán aquí en cuanto Apple Salud o un dispositivo de actividad Withings sincronice un día completo."
- }
+ },
+ "coachPrefill": "Aún no he registrado energía activa: ¿cuál es un objetivo diario razonable y cómo se compara con las calorías totales?"
},
"subPage": {
"loadError": "No se pudo cargar esta métrica ahora mismo.",
@@ -3725,7 +3729,8 @@
"valuesBack": "Volver a Insights",
"valuesBackToMetric": "Volver a {metric}",
"scoresBack": "Volver a Insights",
- "ecgDescription": "Registros de ECG de una derivación de tu dispositivo, cada uno con el resultado del propio dispositivo."
+ "ecgDescription": "Registros de ECG de una derivación de tu dispositivo, cada uno con el resultado del propio dispositivo.",
+ "emptyCoachPrefill": "{metric}: aún no he registrado nada aquí. ¿Qué dice esta métrica sobre mi salud y cómo la mejoro?"
},
"units": {
"steps": "pasos",
@@ -3825,27 +3830,32 @@
"bloodPressure": {
"title": "Aún no hay registros de presión arterial",
"description": "Registra una medición de presión y aquí aparecerán la tendencia, el porcentaje en rango y la clasificación de la OMS.",
- "cta": "Registrar presión"
+ "cta": "Registrar presión",
+ "coachPrefill": "Aún no he registrado la presión arterial: ¿por qué importa y qué debería saber antes de empezar?"
},
"weight": {
"title": "Aún no hay registros de peso",
"description": "Registra un peso y aparecerán automáticamente la tendencia, la media móvil y la zona objetivo.",
- "cta": "Registrar peso"
+ "cta": "Registrar peso",
+ "coachPrefill": "Aún no he registrado mi peso: ¿por qué importa y qué debería saber antes de empezar a registrarlo?"
},
"pulse": {
"title": "Aún no hay registros de pulso",
"description": "Registra un pulso y aquí aparecerán pulso en reposo, VFC y VO₂ máx.",
- "cta": "Registrar pulso"
+ "cta": "Registrar pulso",
+ "coachPrefill": "Aún no he registrado el pulso en reposo: ¿por qué importa y qué debería saber antes de empezar?"
},
"bmi": {
"title": "IMC aún no calculado",
"description": "Cuando haya altura y al menos un registro de peso, aquí aparecerá la tendencia del IMC con las zonas de la OMS.",
- "cta": "Registrar peso"
+ "cta": "Registrar peso",
+ "coachPrefill": "Aún no he registrado mi peso: ¿por qué me importa el IMC y qué debería saber antes de empezar a seguirlo?"
},
"mood": {
"title": "Aún no hay entradas de estado de ánimo",
"description": "Registra un estado de ánimo y aparecerán la tendencia, la banda de estabilidad y las correlaciones.",
- "cta": "Registrar ánimo"
+ "cta": "Registrar ánimo",
+ "coachPrefill": "Aún no he registrado mi estado de ánimo: ¿por qué importa registrarlo y cómo debería empezar?"
},
"medication": {
"title": "Aún no hay medicamentos configurados",
@@ -3855,7 +3865,8 @@
"sleep": {
"title": "Aún no hay registros de sueño",
"description": "Una vez disponibles los datos de sueño (manual o vía Withings / Apple Health), aquí aparecerá el desglose por fases.",
- "cta": "Conectar una fuente"
+ "cta": "Conectar una fuente",
+ "coachPrefill": "Aún no tengo datos de sueño: ¿por qué importa registrar el sueño y qué debería saber antes de conectar una fuente?"
}
},
"navWorkouts": "Entrenamientos",
@@ -3865,7 +3876,8 @@
"description": "Carreras, salidas en bici, paseos y sesiones de fuerza recientes, deduplicados entre Apple Watch y Withings.",
"emptyState": {
"title": "Aún no hay entrenamientos",
- "description": "Los entrenamientos llegan desde Apple Salud en el iPhone y desde Withings. También puedes registrar uno a mano."
+ "description": "Los entrenamientos llegan desde Apple Salud en el iPhone y desde Withings. También puedes registrar uno a mano.",
+ "coachPrefill": "Aún no he registrado entrenamientos: ¿por qué importa registrarlos y en qué debería centrarme primero?"
},
"detail": {
"backToList": "Volver a entrenamientos",
@@ -4745,7 +4757,8 @@
"emptyState": {
"title": "Aún no hay datos de caídas",
"description": "Las caídas detectadas aparecen aquí cuando Apple Health se sincroniza. Cero caídas es el objetivo."
- }
+ },
+ "coachPrefill": "Aún no he registrado datos de caídas: ¿qué dice esta métrica sobre mi salud y cómo reduzco mi riesgo de caídas?"
},
"navFalls": "Caídas",
"sixMinuteWalk": {
@@ -5695,7 +5708,7 @@
"title": "Diario de enfermedades",
"description": "Vista de lista y orden de episodios."
},
- "vorsorge": {
+ "checkups": {
"title": "Revisiones",
"description": "Vista de lista y orden de recordatorios."
},
@@ -6121,7 +6134,7 @@
"moduleSettingsDescription": "Todo lo demás está en la página del módulo, en Módulos."
}
},
- "gesundheitsakte": {
+ "health-record": {
"title": "Historial médico",
"subtitle": "Exporta un historial de salud completo, listo para el médico."
},
@@ -7058,7 +7071,7 @@
"builtAtLabel": "Compilado",
"license": "Licencia",
"repository": "Código fuente",
- "changelog": "Changelog",
+ "changelog": "Notas de la versión",
"docs": "Documentación",
"credits": "Créditos y atribuciones de datos",
"linksHeading": "Fuentes y documentación",
@@ -10206,7 +10219,7 @@
"snoozedUntil": "Aplazado hasta {date}",
"skippedOn": "Omitido el {date}",
"skipCount": "omitido {count} veces",
- "disabledBadge": "Apagado",
+ "disabledBadge": "Desactivado",
"nextDue": {
"none": "Sin programar",
"today": "Vence hoy",
@@ -10229,6 +10242,7 @@
"BLOOD_PRESSURE_SYS": "Presión arterial",
"PULSE": "Pulso",
"BLOOD_GLUCOSE": "Glucosa en sangre",
+ "BLOOD_KETONES": "Cetonas en sangre",
"OXYGEN_SATURATION": "Saturación de oxígeno",
"BODY_FAT": "Grasa corporal",
"BODY_TEMPERATURE": "Temperatura corporal",
@@ -11073,7 +11087,7 @@
"notePlaceholder": "Cómo te sentiste hoy (cifrado).",
"severityFor": "Gravedad de {symptom}",
"saveError": "No se pudo guardar el día. Inténtalo de nuevo.",
- "day": "Day"
+ "day": "Día"
},
"impact": {
"0": "Plenamente funcional",
@@ -11162,14 +11176,14 @@
"RESTING_HEART_RATE": "Frecuencia cardíaca en reposo",
"HEART_RATE_VARIABILITY": "Variabilidad de la frecuencia cardíaca",
"HRV_RMSSD": "VFC (RMSSD)",
- "RECOVERY_SCORE": "Recovery",
+ "RECOVERY_SCORE": "Recuperación",
"OXYGEN_SATURATION": "Saturación de oxígeno",
"BODY_TEMPERATURE": "Temperatura corporal",
"RESPIRATORY_RATE": "Frecuencia respiratoria",
"BLOOD_PRESSURE_SYS": "Presión arterial sistólica",
"BLOOD_PRESSURE_DIA": "Presión arterial diastólica",
- "PULSE": "Pulse",
- "WEIGHT": "Weight",
+ "PULSE": "Pulso",
+ "WEIGHT": "Peso",
"FUNCTIONAL_IMPACT": "Impacto funcional"
},
"listLoadError": "No se pudieron cargar tus episodios."
diff --git a/messages/fr.json b/messages/fr.json
index 0d5cddda6..574570c36 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -764,7 +764,7 @@
"noneActionableCta": "Ouvrir les médicaments"
},
"quickEntryDiscard": {
- "title": "Abandonner cette saisie ?",
+ "title": "Abandonner les modifications ?",
"description": "Vous avez des données non enregistrées. Fermer le formulaire et perdre ce que vous avez saisi ?",
"confirm": "Abandonner",
"cancel": "Continuer la saisie"
@@ -1547,7 +1547,6 @@
"glp1NextInjectionToday": "aujourd’hui",
"glp1NextInjectionTomorrow": "demain",
"glp1RotationSuggested": "Conseillé : {site}",
- "glp1LogSideEffect": "Consigner un effet secondaire",
"sideEffects": {
"section": "Effets secondaires",
"addCta": "Consigner",
@@ -1741,7 +1740,7 @@
"step1": {
"title": "Comment s’appelle le médicament ?",
"subline": "Exactement comme indiqué sur la boîte.",
- "label": "Name",
+ "label": "Nom",
"placeholder": "par exemple Ramipril",
"short": "Nom"
},
@@ -3583,7 +3582,8 @@
"emptyState": {
"title": "Aucune mesure de VFC",
"description": "Les mesures de VFC apparaîtront ici dès que l'Apple Watch (ou un appareil Withings compatible) synchronisera des relevés nocturnes."
- }
+ },
+ "coachPrefill": "Je n’ai pas encore enregistré de données de VFC. Que m’apprend la variabilité de la fréquence cardiaque, et comment la mesurer ?"
},
"restingHr": {
"title": "FC au repos",
@@ -3602,7 +3602,8 @@
"emptyState": {
"title": "Aucune mesure SpO₂",
"description": "Les mesures SpO₂ apparaîtront dès qu'un wearable compatible (Apple Watch, Withings ScanWatch) en capturera une."
- }
+ },
+ "coachPrefill": "Je n’ai pas encore enregistré de saturation en oxygène. Que m’apprend la SpO₂, et quelle plage viser ?"
},
"bodyTemperature": {
"title": "Température corporelle",
@@ -3612,7 +3613,8 @@
"title": "Aucune température corporelle",
"description": "Enregistrez une mesure ou laissez votre wearable en synchroniser une, et la tendance apparaîtra ici.",
"cta": "Enregistrer la température"
- }
+ },
+ "coachPrefill": "Je n’ai pas encore enregistré de température corporelle. Quelle est la plage normale, et que faire si elle s’en écarte ?"
},
"steps": {
"title": "Pas",
@@ -3621,7 +3623,8 @@
"emptyState": {
"title": "Aucune donnée de pas",
"description": "Les totaux de pas quotidiens apparaîtront ici dès qu'Apple Santé ou un appareil d'activité synchronise une journée complète."
- }
+ },
+ "coachPrefill": "Je n’ai pas encore enregistré de pas. Quel objectif quotidien de pas est raisonnable, et en quoi le fait de marcher davantage aide-t-il ma santé ?"
},
"activeEnergy": {
"title": "Énergie active",
@@ -3630,7 +3633,8 @@
"emptyState": {
"title": "Aucune donnée d'énergie active",
"description": "Les totaux d'énergie active apparaîtront dès qu'Apple Santé ou un appareil Withings activité synchronise une journée complète."
- }
+ },
+ "coachPrefill": "Je n’ai pas encore enregistré d’énergie active. Quel objectif quotidien est raisonnable, et comment se compare-t-il aux calories totales ?"
},
"subPage": {
"loadError": "Impossible de charger cette mesure pour le moment.",
@@ -3725,7 +3729,8 @@
"valuesBack": "Retour aux Insights",
"valuesBackToMetric": "Retour à {metric}",
"scoresBack": "Retour aux Insights",
- "ecgDescription": "Enregistrements ECG à dérivation unique de votre appareil, chacun accompagné du résultat de l'appareil."
+ "ecgDescription": "Enregistrements ECG à dérivation unique de votre appareil, chacun accompagné du résultat de l'appareil.",
+ "emptyCoachPrefill": "{metric} : je n’ai encore rien enregistré ici. Que dit cette mesure sur ma santé, et comment l’améliorer ?"
},
"units": {
"steps": "pas",
@@ -3825,27 +3830,32 @@
"bloodPressure": {
"title": "Aucune mesure de tension artérielle",
"description": "Enregistrez une mesure de tension : la tendance, la part dans la cible et la classification OMS s'afficheront ici.",
- "cta": "Enregistrer une tension"
+ "cta": "Enregistrer une tension",
+ "coachPrefill": "Je n’ai pas encore enregistré de tension artérielle. Pourquoi est-ce important, et que dois-je savoir avant de commencer ?"
},
"weight": {
"title": "Aucune mesure de poids",
"description": "Enregistrez un poids : la tendance, la moyenne glissante et la zone cible apparaîtront automatiquement.",
- "cta": "Enregistrer un poids"
+ "cta": "Enregistrer un poids",
+ "coachPrefill": "Je n’ai pas encore enregistré de poids. Pourquoi est-ce important, et que dois-je savoir avant de commencer le suivi ?"
},
"pulse": {
"title": "Aucune mesure de pouls",
"description": "Enregistrez un pouls : pouls au repos, VFC et VO₂ max apparaîtront dans cette vue.",
- "cta": "Enregistrer un pouls"
+ "cta": "Enregistrer un pouls",
+ "coachPrefill": "Je n’ai pas encore enregistré de pouls au repos. Pourquoi est-ce important, et que dois-je savoir avant de commencer ?"
},
"bmi": {
"title": "IMC pas encore calculé",
"description": "Dès qu'une taille et au moins un poids sont enregistrés, la tendance IMC avec les zones OMS s'affichera ici.",
- "cta": "Enregistrer un poids"
+ "cta": "Enregistrer un poids",
+ "coachPrefill": "Je n’ai pas encore enregistré de poids. Pourquoi l’IMC compte-t-il pour moi, et que dois-je savoir avant de le suivre ?"
},
"mood": {
"title": "Aucune entrée d'humeur",
"description": "Enregistrez une humeur : la tendance, la bande de stabilité et les corrélations s'afficheront.",
- "cta": "Enregistrer une humeur"
+ "cta": "Enregistrer une humeur",
+ "coachPrefill": "Je n’ai pas encore enregistré d’humeur. Pourquoi suivre mon humeur, et comment commencer ?"
},
"medication": {
"title": "Aucun médicament configuré",
@@ -3855,7 +3865,8 @@
"sleep": {
"title": "Aucune donnée de sommeil",
"description": "Dès que des données de sommeil sont disponibles (manuel ou via Withings / Apple Health), la répartition par stade apparaîtra ici.",
- "cta": "Connecter une source"
+ "cta": "Connecter une source",
+ "coachPrefill": "Je n’ai pas encore de données de sommeil. Pourquoi suivre mon sommeil, et que dois-je savoir avant de connecter une source ?"
}
},
"navWorkouts": "Entraînements",
@@ -3865,7 +3876,8 @@
"description": "Courses, sorties à vélo, marches et séances de musculation récentes, dédupliquées entre Apple Watch et Withings.",
"emptyState": {
"title": "Aucun entraînement",
- "description": "Vos entraînements arrivent d'Apple Santé sur l'iPhone et de Withings. Vous pouvez aussi en saisir un à la main."
+ "description": "Vos entraînements arrivent d'Apple Santé sur l'iPhone et de Withings. Vous pouvez aussi en saisir un à la main.",
+ "coachPrefill": "Je n’ai pas encore enregistré d’entraînement. Pourquoi les suivre, et sur quoi me concentrer en premier ?"
},
"detail": {
"backToList": "Retour aux entraînements",
@@ -4745,7 +4757,8 @@
"emptyState": {
"title": "Pas encore de données de chutes",
"description": "Les chutes détectées apparaissent ici dès qu'Apple Santé se synchronise. L'objectif est zéro chute."
- }
+ },
+ "coachPrefill": "Je n’ai pas encore enregistré de données de chute. Que dit cette mesure sur ma santé, et comment réduire mon risque de chute ?"
},
"navFalls": "Chutes",
"sixMinuteWalk": {
@@ -5695,7 +5708,7 @@
"title": "Journal des maladies",
"description": "Vue en liste et ordre des épisodes."
},
- "vorsorge": {
+ "checkups": {
"title": "Examens",
"description": "Vue en liste et ordre des rappels."
},
@@ -6121,7 +6134,7 @@
"moduleSettingsDescription": "Tout le reste se trouve sur la page du module, dans Modules."
}
},
- "gesundheitsakte": {
+ "health-record": {
"title": "Dossier médical",
"subtitle": "Exportez un dossier de santé complet, prêt pour le médecin."
},
@@ -7058,7 +7071,7 @@
"builtAtLabel": "Compilé",
"license": "Licence",
"repository": "Code source",
- "changelog": "Changelog",
+ "changelog": "Notes de version",
"docs": "Documentation",
"credits": "Crédits et attributions de données",
"linksHeading": "Sources et documentation",
@@ -10229,6 +10242,7 @@
"BLOOD_PRESSURE_SYS": "Tension artérielle",
"PULSE": "Pouls",
"BLOOD_GLUCOSE": "Glycémie",
+ "BLOOD_KETONES": "Cétones sanguines",
"OXYGEN_SATURATION": "Saturation en oxygène",
"BODY_FAT": "Masse grasse",
"BODY_TEMPERATURE": "Température corporelle",
@@ -11073,7 +11087,7 @@
"notePlaceholder": "Comment s'est passée la journée (chiffré).",
"severityFor": "Intensité de {symptom}",
"saveError": "Impossible d'enregistrer la journée. Veuillez réessayer.",
- "day": "Day"
+ "day": "Jour"
},
"impact": {
"0": "Pleinement fonctionnel",
@@ -11162,14 +11176,14 @@
"RESTING_HEART_RATE": "Fréquence cardiaque au repos",
"HEART_RATE_VARIABILITY": "Variabilité de la fréquence cardiaque",
"HRV_RMSSD": "VFC (RMSSD)",
- "RECOVERY_SCORE": "Recovery",
+ "RECOVERY_SCORE": "Récupération",
"OXYGEN_SATURATION": "Saturation en oxygène",
"BODY_TEMPERATURE": "Température corporelle",
"RESPIRATORY_RATE": "Fréquence respiratoire",
"BLOOD_PRESSURE_SYS": "Pression artérielle systolique",
"BLOOD_PRESSURE_DIA": "Pression artérielle diastolique",
- "PULSE": "Pulse",
- "WEIGHT": "Weight",
+ "PULSE": "Pouls",
+ "WEIGHT": "Poids",
"FUNCTIONAL_IMPACT": "Retentissement fonctionnel"
},
"listLoadError": "Impossible de charger vos épisodes."
diff --git a/messages/it.json b/messages/it.json
index 092346598..c0158d044 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -764,7 +764,7 @@
"noneActionableCta": "Apri i farmaci"
},
"quickEntryDiscard": {
- "title": "Scartare questa voce?",
+ "title": "Scartare le modifiche?",
"description": "Hai dati non salvati. Chiudere il modulo e perdere ciò che hai digitato?",
"confirm": "Scarta",
"cancel": "Continua a modificare"
@@ -1547,7 +1547,6 @@
"glp1NextInjectionToday": "oggi",
"glp1NextInjectionTomorrow": "domani",
"glp1RotationSuggested": "Suggerita: {site}",
- "glp1LogSideEffect": "Registra effetto collaterale",
"sideEffects": {
"section": "Effetti collaterali",
"addCta": "Registra",
@@ -1741,7 +1740,7 @@
"step1": {
"title": "Come si chiama il farmaco?",
"subline": "Esattamente come riportato sulla confezione.",
- "label": "Name",
+ "label": "Nome",
"placeholder": "ad es. Ramipril",
"short": "Nome"
},
@@ -3583,7 +3582,8 @@
"emptyState": {
"title": "Ancora nessun valore HRV",
"description": "I valori HRV appariranno qui non appena Apple Watch (o un dispositivo Withings compatibile) sincronizza le misurazioni notturne."
- }
+ },
+ "coachPrefill": "Non ho ancora registrato dati HRV. Cosa mi dice la variabilità della frequenza cardiaca e come la rilevo?"
},
"restingHr": {
"title": "FC a riposo",
@@ -3602,7 +3602,8 @@
"emptyState": {
"title": "Ancora nessun campione SpO₂",
"description": "I campioni SpO₂ appariranno qui non appena un wearable compatibile (Apple Watch, Withings ScanWatch) cattura una lettura."
- }
+ },
+ "coachPrefill": "Non ho ancora registrato la saturazione di ossigeno. Cosa mi dice la SpO₂ e a quale intervallo dovrei puntare?"
},
"bodyTemperature": {
"title": "Temperatura corporea",
@@ -3612,7 +3613,8 @@
"title": "Ancora nessuna temperatura corporea",
"description": "Registra una misura o lascia che il tuo wearable ne sincronizzi una: l'andamento apparirà qui.",
"cta": "Registra temperatura corporea"
- }
+ },
+ "coachPrefill": "Non ho ancora registrato la temperatura corporea. Qual è l’intervallo sano e cosa dovrei fare se si discosta?"
},
"steps": {
"title": "Passi",
@@ -3621,7 +3623,8 @@
"emptyState": {
"title": "Ancora nessun dato sui passi",
"description": "I totali giornalieri dei passi appariranno qui non appena Apple Salute o un dispositivo di attività sincronizza un'intera giornata."
- }
+ },
+ "coachPrefill": "Non ho ancora registrato passi. Qual è un obiettivo giornaliero di passi ragionevole e in che modo camminare di più aiuta la mia salute?"
},
"activeEnergy": {
"title": "Energia attiva",
@@ -3630,7 +3633,8 @@
"emptyState": {
"title": "Ancora nessun dato di energia attiva",
"description": "I totali di energia attiva appariranno qui non appena Apple Salute o un dispositivo di attività Withings sincronizza un'intera giornata."
- }
+ },
+ "coachPrefill": "Non ho ancora registrato energia attiva. Qual è un obiettivo giornaliero ragionevole e come si confronta con le calorie totali?"
},
"subPage": {
"loadError": "Impossibile caricare questa metrica al momento.",
@@ -3725,7 +3729,8 @@
"valuesBack": "Torna a Insights",
"valuesBackToMetric": "Torna a {metric}",
"scoresBack": "Torna a Insights",
- "ecgDescription": "Registrazioni ECG a singola derivazione dal tuo dispositivo, ciascuna con il risultato del dispositivo."
+ "ecgDescription": "Registrazioni ECG a singola derivazione dal tuo dispositivo, ciascuna con il risultato del dispositivo.",
+ "emptyCoachPrefill": "{metric}: non ho ancora registrato nulla qui. Cosa dice questo valore sulla mia salute e come lo miglioro?"
},
"units": {
"steps": "passi",
@@ -3825,27 +3830,32 @@
"bloodPressure": {
"title": "Nessuna misurazione della pressione",
"description": "Registra una misurazione e qui appariranno l'andamento, la percentuale in target e la classificazione OMS.",
- "cta": "Registra pressione"
+ "cta": "Registra pressione",
+ "coachPrefill": "Non ho ancora registrato la pressione arteriosa. Perché è importante e cosa dovrei sapere prima di iniziare?"
},
"weight": {
"title": "Nessuna misurazione del peso",
"description": "Registra un peso: andamento, media mobile e fascia obiettivo appariranno automaticamente.",
- "cta": "Registra peso"
+ "cta": "Registra peso",
+ "coachPrefill": "Non ho ancora registrato il peso. Perché è importante e cosa dovrei sapere prima di iniziare a monitorarlo?"
},
"pulse": {
"title": "Nessuna misurazione del polso",
"description": "Registra una misurazione: polso a riposo, HRV e VO₂ max appariranno in questa vista.",
- "cta": "Registra polso"
+ "cta": "Registra polso",
+ "coachPrefill": "Non ho ancora registrato il polso a riposo. Perché è importante e cosa dovrei sapere prima di iniziare?"
},
"bmi": {
"title": "BMI non ancora calcolato",
"description": "Una volta inseriti altezza e almeno un peso, qui apparirà l'andamento del BMI con le zone OMS.",
- "cta": "Registra peso"
+ "cta": "Registra peso",
+ "coachPrefill": "Non ho ancora registrato il peso. Perché il BMI conta per me e cosa dovrei sapere prima di iniziare a monitorarlo?"
},
"mood": {
"title": "Nessuna voce d'umore",
"description": "Registra un umore: appariranno andamento, fascia di stabilità e correlazioni.",
- "cta": "Registra umore"
+ "cta": "Registra umore",
+ "coachPrefill": "Non ho ancora registrato l’umore. Perché conta monitorarlo e come dovrei iniziare?"
},
"medication": {
"title": "Nessun farmaco configurato",
@@ -3855,7 +3865,8 @@
"sleep": {
"title": "Nessuna voce di sonno",
"description": "Quando saranno disponibili dati sul sonno (manuale o tramite Withings / Apple Health), qui apparirà la suddivisione per fase.",
- "cta": "Collega una fonte"
+ "cta": "Collega una fonte",
+ "coachPrefill": "Non ho ancora dati sul sonno. Perché conta monitorare il sonno e cosa dovrei sapere prima di collegare una fonte?"
}
},
"navWorkouts": "Allenamenti",
@@ -3865,7 +3876,8 @@
"description": "Corse, uscite in bici, camminate e sessioni di forza recenti, deduplicati tra Apple Watch e Withings.",
"emptyState": {
"title": "Nessun allenamento",
- "description": "Gli allenamenti arrivano da Salute di Apple sull'iPhone e da Withings. Puoi anche registrarne uno a mano."
+ "description": "Gli allenamenti arrivano da Salute di Apple sull'iPhone e da Withings. Puoi anche registrarne uno a mano.",
+ "coachPrefill": "Non ho ancora registrato allenamenti. Perché conta monitorarli e su cosa dovrei concentrarmi per primo?"
},
"detail": {
"backToList": "Torna agli allenamenti",
@@ -4745,7 +4757,8 @@
"emptyState": {
"title": "Nessun dato sulle cadute",
"description": "Le cadute rilevate compaiono qui quando Apple Health si sincronizza. Nessuna caduta è l'obiettivo."
- }
+ },
+ "coachPrefill": "Non ho ancora registrato dati sulle cadute. Cosa dice questo valore sulla mia salute e come riduco il rischio di cadute?"
},
"navFalls": "Cadute",
"sixMinuteWalk": {
@@ -5695,7 +5708,7 @@
"title": "Diario delle malattie",
"description": "Vista elenco e ordine degli episodi."
},
- "vorsorge": {
+ "checkups": {
"title": "Controlli",
"description": "Vista elenco e ordine dei promemoria."
},
@@ -6121,7 +6134,7 @@
"moduleSettingsDescription": "Tutto il resto è nella pagina del modulo, in Moduli."
}
},
- "gesundheitsakte": {
+ "health-record": {
"title": "Cartella clinica",
"subtitle": "Esporta una cartella clinica completa, pronta per il medico."
},
@@ -7058,7 +7071,7 @@
"builtAtLabel": "Compilato",
"license": "Licenza",
"repository": "Codice sorgente",
- "changelog": "Changelog",
+ "changelog": "Note di rilascio",
"docs": "Documentazione",
"credits": "Crediti e attribuzioni dei dati",
"linksHeading": "Fonti e documentazione",
@@ -10229,6 +10242,7 @@
"BLOOD_PRESSURE_SYS": "Pressione sanguigna",
"PULSE": "Battito",
"BLOOD_GLUCOSE": "Glicemia",
+ "BLOOD_KETONES": "Chetoni nel sangue",
"OXYGEN_SATURATION": "Saturazione di ossigeno",
"BODY_FAT": "Grasso corporeo",
"BODY_TEMPERATURE": "Temperatura corporea",
@@ -11073,7 +11087,7 @@
"notePlaceholder": "Com'è andata la giornata (cifrato).",
"severityFor": "Intensità di {symptom}",
"saveError": "Impossibile salvare la giornata. Riprova.",
- "day": "Day"
+ "day": "Giorno"
},
"impact": {
"0": "Pienamente funzionale",
@@ -11162,14 +11176,14 @@
"RESTING_HEART_RATE": "Frequenza cardiaca a riposo",
"HEART_RATE_VARIABILITY": "Variabilità della frequenza cardiaca",
"HRV_RMSSD": "HRV (RMSSD)",
- "RECOVERY_SCORE": "Recovery",
+ "RECOVERY_SCORE": "Recupero",
"OXYGEN_SATURATION": "Saturazione di ossigeno",
"BODY_TEMPERATURE": "Temperatura corporea",
"RESPIRATORY_RATE": "Frequenza respiratoria",
"BLOOD_PRESSURE_SYS": "Pressione arteriosa sistolica",
"BLOOD_PRESSURE_DIA": "Pressione arteriosa diastolica",
- "PULSE": "Pulse",
- "WEIGHT": "Weight",
+ "PULSE": "Polso",
+ "WEIGHT": "Peso",
"FUNCTIONAL_IMPACT": "Impatto funzionale"
},
"listLoadError": "Impossibile caricare gli episodi."
diff --git a/messages/ko.json b/messages/ko.json
index dc6f68aee..5ff7cafa4 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -764,7 +764,7 @@
"noneActionableCta": "약 목록 열기"
},
"quickEntryDiscard": {
- "title": "이 기록을 버릴까요?",
+ "title": "변경 내용을 버릴까요?",
"description": "저장하지 않은 입력이 있어요. 폼을 닫고 입력한 내용을 버릴까요?",
"confirm": "버리기",
"cancel": "계속 작성"
@@ -1547,7 +1547,6 @@
"glp1NextInjectionToday": "오늘",
"glp1NextInjectionTomorrow": "내일",
"glp1RotationSuggested": "다음 추천 부위: {site}",
- "glp1LogSideEffect": "부작용 기록",
"sideEffects": {
"section": "부작용",
"addCta": "기록",
@@ -3583,7 +3582,8 @@
"emptyState": {
"title": "아직 심박변이도 기록이 없어요",
"description": "Apple Watch(또는 호환 Withings 기기)가 밤마다 측정값을 동기화하기 시작하면 심박변이도 기록이 여기에 쌓여요."
- }
+ },
+ "coachPrefill": "아직 심박변이도 데이터를 기록한 적이 없어요. 심박변이도는 무엇을 알려주고, 어떻게 측정하나요?"
},
"restingHr": {
"title": "안정 시 심박수",
@@ -3602,7 +3602,8 @@
"emptyState": {
"title": "아직 SpO₂ 기록이 없어요",
"description": "호환 웨어러블(Apple Watch, Withings ScanWatch)이 측정하면 SpO₂ 기록이 여기에 쌓여요."
- }
+ },
+ "coachPrefill": "아직 산소포화도를 기록한 적이 없어요. SpO₂는 무엇을 알려주고, 어느 범위를 목표로 해야 하나요?"
},
"bodyTemperature": {
"title": "체온",
@@ -3612,7 +3613,8 @@
"title": "아직 체온 기록이 없어요",
"description": "체온을 기록하거나 웨어러블이 동기화하면 여기에 추이가 나타나요.",
"cta": "체온 기록"
- }
+ },
+ "coachPrefill": "아직 체온을 기록한 적이 없어요. 건강한 범위는 어느 정도이고, 벗어나면 어떻게 해야 하나요?"
},
"steps": {
"title": "걸음 수",
@@ -3621,7 +3623,8 @@
"emptyState": {
"title": "아직 걸음 수 데이터가 없어요",
"description": "Apple Health나 활동 기기가 하루치 움직임을 동기화하면 여기에 하루 걸음 수가 나타나요."
- }
+ },
+ "coachPrefill": "아직 걸음 수를 기록한 적이 없어요. 적당한 하루 걸음 목표는 얼마이고, 더 많이 걸으면 건강에 어떻게 도움이 되나요?"
},
"activeEnergy": {
"title": "활동 에너지",
@@ -3630,7 +3633,8 @@
"emptyState": {
"title": "아직 활동 에너지 데이터가 없어요",
"description": "Apple Health나 Withings 활동 기기가 하루치 움직임을 동기화하면 여기에 활동 에너지 합계가 나타나요."
- }
+ },
+ "coachPrefill": "아직 활동 에너지를 기록한 적이 없어요. 적당한 하루 목표는 얼마이고, 총 칼로리와는 어떻게 비교되나요?"
},
"subPage": {
"loadError": "지금은 이 지표를 불러오지 못했어요.",
@@ -3725,7 +3729,8 @@
"valuesBack": "인사이트로 돌아가기",
"valuesBackToMetric": "돌아가기: {metric}",
"scoresBack": "인사이트로 돌아가기",
- "ecgDescription": "기기에서 기록한 단일 유도 ECG로, 각각 기기가 낸 결과가 함께 있어요."
+ "ecgDescription": "기기에서 기록한 단일 유도 ECG로, 각각 기기가 낸 결과가 함께 있어요.",
+ "emptyCoachPrefill": "{metric}: 아직 여기에 기록한 게 없어요. 이 지표는 내 건강에 대해 무엇을 알려주고, 어떻게 개선하나요?"
},
"units": {
"steps": "걸음",
@@ -3825,27 +3830,32 @@
"bloodPressure": {
"title": "아직 혈압 기록이 없어요",
"description": "혈압을 기록하면 추이 차트, 정상 범위 비율, WHO 분류가 여기에 나타나요.",
- "cta": "혈압 기록"
+ "cta": "혈압 기록",
+ "coachPrefill": "아직 혈압을 기록한 적이 없어요. 혈압이 왜 중요하고, 시작하기 전에 무엇을 알아야 하나요?"
},
"weight": {
"title": "아직 체중 기록이 없어요",
"description": "체중을 기록하면 추이, 이동 평균, 목표 구간이 자동으로 나타나요.",
- "cta": "체중 기록"
+ "cta": "체중 기록",
+ "coachPrefill": "아직 체중을 기록한 적이 없어요. 체중이 왜 중요하고, 기록을 시작하기 전에 무엇을 알아야 하나요?"
},
"pulse": {
"title": "아직 맥박 기록이 없어요",
"description": "맥박을 기록하면 안정 시 맥박, 심박변이도, VO₂ max가 이 화면에 나타나요.",
- "cta": "맥박 기록"
+ "cta": "맥박 기록",
+ "coachPrefill": "아직 안정 시 맥박을 기록한 적이 없어요. 맥박이 왜 중요하고, 시작하기 전에 무엇을 알아야 하나요?"
},
"bmi": {
"title": "아직 BMI를 계산하지 못했어요",
"description": "키와 체중 기록이 하나 이상 있으면 WHO 구간과 함께 BMI 추이가 여기에 나타나요.",
- "cta": "체중 기록"
+ "cta": "체중 기록",
+ "coachPrefill": "아직 체중을 기록한 적이 없어요. BMI가 내게 왜 중요하고, 기록을 시작하기 전에 무엇을 알아야 하나요?"
},
"mood": {
"title": "아직 기분 기록이 없어요",
"description": "기분을 기록하면 추이, 안정성 구간, 상관관계가 나타나요.",
- "cta": "기분 기록"
+ "cta": "기분 기록",
+ "coachPrefill": "아직 기분을 기록한 적이 없어요. 기분 기록이 왜 중요하고, 어떻게 시작하면 좋을까요?"
},
"medication": {
"title": "아직 등록한 약이 없어요",
@@ -3855,7 +3865,8 @@
"sleep": {
"title": "아직 수면 기록이 없어요",
"description": "수면 데이터가 들어오면(직접 기록하거나 Withings / Apple Health를 통해) 밤별 단계 구성이 여기에 나타나요.",
- "cta": "소스 연결"
+ "cta": "소스 연결",
+ "coachPrefill": "아직 수면 데이터가 없어요. 수면 기록이 왜 중요하고, 데이터 소스를 연결하기 전에 무엇을 알아야 하나요?"
}
},
"navWorkouts": "운동",
@@ -3865,7 +3876,8 @@
"description": "최근 달리기, 자전거, 걷기, 근력 운동 세션이에요. Apple Watch와 Withings 소스 사이 중복은 정리했어요.",
"emptyState": {
"title": "아직 운동 기록이 없어요",
- "description": "운동은 iPhone의 Apple Health와 Withings에서 동기화돼요. 직접 기록할 수도 있어요."
+ "description": "운동은 iPhone의 Apple Health와 Withings에서 동기화돼요. 직접 기록할 수도 있어요.",
+ "coachPrefill": "아직 운동을 기록한 적이 없어요. 운동 기록이 왜 중요하고, 무엇부터 신경 써야 하나요?"
},
"detail": {
"backToList": "운동으로 돌아가기",
@@ -4745,7 +4757,8 @@
"emptyState": {
"title": "아직 낙상 데이터가 없어요",
"description": "Apple Health가 동기화하면 감지된 낙상이 여기에 표시돼요. 낙상이 없는 게 가장 좋아요."
- }
+ },
+ "coachPrefill": "아직 낙상 데이터를 기록한 적이 없어요. 이 지표는 내 건강에 대해 무엇을 알려주고, 낙상 위험은 어떻게 줄이나요?"
},
"navFalls": "낙상",
"sixMinuteWalk": {
@@ -5695,7 +5708,7 @@
"title": "아픈 날 일지",
"description": "목록 보기와 에피소드 순서."
},
- "vorsorge": {
+ "checkups": {
"title": "건강검진",
"description": "목록 보기와 알림 순서."
},
@@ -6121,7 +6134,7 @@
"moduleSettingsDescription": "순서와 보기 외의 설정은 모듈의 페이지에 있습니다."
}
},
- "gesundheitsakte": {
+ "health-record": {
"title": "건강 기록",
"subtitle": "임상의에게 바로 건넬 수 있는 완전한 건강 기록을 내보내요."
},
@@ -7058,7 +7071,7 @@
"builtAtLabel": "빌드 시각",
"license": "라이선스",
"repository": "소스 코드",
- "changelog": "변경 이력",
+ "changelog": "릴리스 노트",
"docs": "문서",
"credits": "크레딧 및 데이터 출처",
"linksHeading": "출처 및 문서",
@@ -10229,6 +10242,7 @@
"BLOOD_PRESSURE_SYS": "혈압",
"PULSE": "맥박",
"BLOOD_GLUCOSE": "혈당",
+ "BLOOD_KETONES": "혈중 케톤",
"OXYGEN_SATURATION": "산소포화도",
"BODY_FAT": "체지방",
"BODY_TEMPERATURE": "체온",
diff --git a/messages/nl.json b/messages/nl.json
index 51d74116f..6c21d8933 100644
--- a/messages/nl.json
+++ b/messages/nl.json
@@ -764,7 +764,7 @@
"noneActionableCta": "Medicatie openen"
},
"quickEntryDiscard": {
- "title": "Deze invoer weggooien?",
+ "title": "Wijzigingen weggooien?",
"description": "Je hebt niet-opgeslagen invoer. Het formulier sluiten en verliezen wat je hebt getypt?",
"confirm": "Weggooien",
"cancel": "Verder bewerken"
@@ -1547,7 +1547,6 @@
"glp1NextInjectionToday": "vandaag",
"glp1NextInjectionTomorrow": "morgen",
"glp1RotationSuggested": "Volgende aanbevolen plek: {site}",
- "glp1LogSideEffect": "Bijwerking vastleggen",
"sideEffects": {
"section": "Bijwerkingen",
"addCta": "Vastleggen",
@@ -3583,7 +3582,8 @@
"emptyState": {
"title": "Nog geen HRV-metingen",
"description": "HRV-metingen verschijnen hier zodra Apple Watch (of een compatibel Withings-apparaat) nachtelijke metingen begint te synchroniseren."
- }
+ },
+ "coachPrefill": "Ik heb nog geen HRV-gegevens vastgelegd. Wat zegt hartslagvariabiliteit me, en hoe leg ik die vast?"
},
"restingHr": {
"title": "Rusthartslag",
@@ -3602,7 +3602,8 @@
"emptyState": {
"title": "Nog geen SpO₂-metingen",
"description": "SpO₂-metingen verschijnen hier zodra een compatibele wearable (Apple Watch, Withings ScanWatch) een meting vastlegt."
- }
+ },
+ "coachPrefill": "Ik heb nog geen zuurstofverzadiging vastgelegd. Wat zegt SpO₂ me, en naar welk bereik moet ik streven?"
},
"bodyTemperature": {
"title": "Lichaamstemperatuur",
@@ -3612,7 +3613,8 @@
"title": "Nog geen lichaamstemperatuur",
"description": "Leg een temperatuurmeting vast of laat je wearable er een synchroniseren, dan verschijnt de trend hier.",
"cta": "Lichaamstemperatuur vastleggen"
- }
+ },
+ "coachPrefill": "Ik heb nog geen lichaamstemperatuur vastgelegd. Wat is het gezonde bereik, en wat moet ik doen als die afwijkt?"
},
"steps": {
"title": "Stappen",
@@ -3621,7 +3623,8 @@
"emptyState": {
"title": "Nog geen stappengegevens",
"description": "Dagtotalen van je stappen verschijnen hier zodra Apple Health of een activiteitsapparaat een volledige dag beweging synchroniseert."
- }
+ },
+ "coachPrefill": "Ik heb nog geen stappen vastgelegd. Wat is een redelijk dagelijks stappendoel, en hoe helpt meer lopen mijn gezondheid?"
},
"activeEnergy": {
"title": "Actieve energie",
@@ -3630,7 +3633,8 @@
"emptyState": {
"title": "Nog geen gegevens over actieve energie",
"description": "Dagtotalen van actieve energie verschijnen hier zodra Apple Health of een Withings-activiteitsapparaat een volledige dag beweging synchroniseert."
- }
+ },
+ "coachPrefill": "Ik heb nog geen actieve energie vastgelegd. Wat is een redelijk dagdoel, en hoe verhoudt het zich tot het totale aantal calorieën?"
},
"subPage": {
"loadError": "Deze meetwaarde kan nu niet worden geladen.",
@@ -3725,7 +3729,8 @@
"valuesBack": "Terug naar inzichten",
"valuesBackToMetric": "Terug naar {metric}",
"scoresBack": "Terug naar inzichten",
- "ecgDescription": "Eenkanaals-ECG-opnamen van je apparaat, elk met de eigen uitslag van het apparaat."
+ "ecgDescription": "Eenkanaals-ECG-opnamen van je apparaat, elk met de eigen uitslag van het apparaat.",
+ "emptyCoachPrefill": "{metric}: ik heb hier nog niets vastgelegd. Wat zegt deze waarde over mijn gezondheid, en hoe verbeter ik die?"
},
"units": {
"steps": "stappen",
@@ -3825,27 +3830,32 @@
"bloodPressure": {
"title": "Nog geen bloeddrukmetingen",
"description": "Leg een bloeddrukmeting vast, dan verschijnen hier de trendgrafiek, het aandeel binnen het bereik en de WHO-classificatie.",
- "cta": "Bloeddruk vastleggen"
+ "cta": "Bloeddruk vastleggen",
+ "coachPrefill": "Ik heb nog geen bloeddruk vastgelegd. Waarom is die belangrijk, en wat moet ik weten voordat ik begin?"
},
"weight": {
"title": "Nog geen gewichtsmetingen",
"description": "Leg je gewicht vast, dan verschijnen de trend, het voortschrijdend gemiddelde en de doelzone automatisch.",
- "cta": "Gewicht vastleggen"
+ "cta": "Gewicht vastleggen",
+ "coachPrefill": "Ik heb nog geen gewicht vastgelegd. Waarom is dat belangrijk, en wat moet ik weten voordat ik het ga bijhouden?"
},
"pulse": {
"title": "Nog geen hartslagmetingen",
"description": "Leg een hartslagmeting vast, dan verschijnen rusthartslag, HRV en VO₂max in deze weergave.",
- "cta": "Hartslag vastleggen"
+ "cta": "Hartslag vastleggen",
+ "coachPrefill": "Ik heb nog geen rusthartslag vastgelegd. Waarom is die belangrijk, en wat moet ik weten voordat ik begin?"
},
"bmi": {
"title": "BMI nog niet berekend",
"description": "Zodra je lengte en minstens één gewichtsmeting bekend zijn, verschijnt hier de BMI-trend met de WHO-zones.",
- "cta": "Gewicht vastleggen"
+ "cta": "Gewicht vastleggen",
+ "coachPrefill": "Ik heb nog geen gewicht vastgelegd. Waarom is BMI belangrijk voor mij, en wat moet ik weten voordat ik het ga bijhouden?"
},
"mood": {
"title": "Nog geen stemmingsregistraties",
"description": "Leg je stemming vast, dan verschijnen de trend, de stabiliteitszone en de correlaties.",
- "cta": "Stemming vastleggen"
+ "cta": "Stemming vastleggen",
+ "coachPrefill": "Ik heb nog geen stemming vastgelegd. Waarom is stemming bijhouden belangrijk, en hoe begin ik?"
},
"medication": {
"title": "Nog geen medicatie ingesteld",
@@ -3855,7 +3865,8 @@
"sleep": {
"title": "Nog geen slaapregistraties",
"description": "Zodra er slaapgegevens binnenkomen (handmatig of via Withings / Apple Health), verschijnt hier de verdeling over slaapfasen per nacht.",
- "cta": "Bron koppelen"
+ "cta": "Bron koppelen",
+ "coachPrefill": "Ik heb nog geen slaapgegevens. Waarom is slaap bijhouden belangrijk, en wat moet ik weten voordat ik een bron koppel?"
}
},
"navWorkouts": "Workouts",
@@ -3865,7 +3876,8 @@
"description": "Recente hardloop-, fiets-, wandel- en krachttrainingen, ontdubbeld over bronnen van Apple Watch en Withings.",
"emptyState": {
"title": "Nog geen workouts",
- "description": "Workouts worden gesynchroniseerd vanuit Apple Health op je iPhone en vanuit Withings. Je kunt er ook zelf een vastleggen."
+ "description": "Workouts worden gesynchroniseerd vanuit Apple Health op je iPhone en vanuit Withings. Je kunt er ook zelf een vastleggen.",
+ "coachPrefill": "Ik heb nog geen workouts vastgelegd. Waarom is bijhouden belangrijk, en waar moet ik me eerst op richten?"
},
"detail": {
"backToList": "Terug naar workouts",
@@ -4745,7 +4757,8 @@
"emptyState": {
"title": "Nog geen valgegevens",
"description": "Gedetecteerde valpartijen verschijnen hier zodra Apple Health synchroniseert. Geen valpartijen is het doel."
- }
+ },
+ "coachPrefill": "Ik heb nog geen valgegevens vastgelegd. Wat zegt deze waarde over mijn gezondheid, en hoe verklein ik mijn valrisico?"
},
"navFalls": "Valpartijen",
"sixMinuteWalk": {
@@ -5695,7 +5708,7 @@
"title": "Ziektedagboek",
"description": "Lijstweergave en volgorde van episodes."
},
- "vorsorge": {
+ "checkups": {
"title": "Preventieve controles",
"description": "Lijstweergave en volgorde van herinneringen."
},
@@ -6121,7 +6134,7 @@
"moduleSettingsDescription": "Al het andere staat op de pagina van de module onder Modules."
}
},
- "gesundheitsakte": {
+ "health-record": {
"title": "Gezondheidsdossier",
"subtitle": "Exporteer een volledig gezondheidsdossier dat klaar is voor je zorgverlener."
},
@@ -7058,7 +7071,7 @@
"builtAtLabel": "Gebouwd",
"license": "Licentie",
"repository": "Broncode",
- "changelog": "Wijzigingslogboek",
+ "changelog": "Releasenotes",
"docs": "Documentatie",
"credits": "Credits en bronvermeldingen",
"linksHeading": "Bronnen en documentatie",
@@ -10229,6 +10242,7 @@
"BLOOD_PRESSURE_SYS": "Bloeddruk",
"PULSE": "Hartslag",
"BLOOD_GLUCOSE": "Bloedglucose",
+ "BLOOD_KETONES": "Bloedketonen",
"OXYGEN_SATURATION": "Zuurstofsaturatie",
"BODY_FAT": "Vetpercentage",
"BODY_TEMPERATURE": "Lichaamstemperatuur",
diff --git a/messages/pl.json b/messages/pl.json
index ba7664f2a..d99f6a64d 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -764,7 +764,7 @@
"noneActionableCta": "Otwórz leki"
},
"quickEntryDiscard": {
- "title": "Odrzucić ten wpis?",
+ "title": "Odrzucić zmiany?",
"description": "Masz niezapisane dane. Zamknąć formularz i utracić to, co wpisano?",
"confirm": "Odrzuć",
"cancel": "Kontynuuj edycję"
@@ -1547,7 +1547,6 @@
"glp1NextInjectionToday": "dziś",
"glp1NextInjectionTomorrow": "jutro",
"glp1RotationSuggested": "Sugerowane: {site}",
- "glp1LogSideEffect": "Zapisz działanie niepożądane",
"sideEffects": {
"section": "Skutki uboczne",
"addCta": "Dodaj",
@@ -1741,7 +1740,7 @@
"step1": {
"title": "Jak nazywa się lek?",
"subline": "Dokładnie tak, jak na opakowaniu.",
- "label": "Name",
+ "label": "Nazwa",
"placeholder": "np. Ramipryl",
"short": "Nazwa"
},
@@ -3583,7 +3582,8 @@
"emptyState": {
"title": "Brak wartości HRV",
"description": "Wartości HRV pojawią się tutaj, gdy Apple Watch (lub zgodne urządzenie Withings) zsynchronizuje pomiary nocne."
- }
+ },
+ "coachPrefill": "Nie zapisałem jeszcze danych HRV. Co mówi mi zmienność rytmu serca i jak ją rejestrować?"
},
"restingHr": {
"title": "Tętno spoczynkowe",
@@ -3602,7 +3602,8 @@
"emptyState": {
"title": "Brak próbek SpO₂",
"description": "Próbki SpO₂ trafią tutaj, gdy zgodne wearable (Apple Watch, Withings ScanWatch) wykona pomiar."
- }
+ },
+ "coachPrefill": "Nie zapisałem jeszcze saturacji tlenu. Co mówi mi SpO₂ i do jakiego zakresu powinienem dążyć?"
},
"bodyTemperature": {
"title": "Temperatura ciała",
@@ -3612,7 +3613,8 @@
"title": "Brak temperatury ciała",
"description": "Zarejestruj pomiar lub pozwól wearable zsynchronizować jeden, a trend pojawi się tutaj.",
"cta": "Zarejestruj temperaturę ciała"
- }
+ },
+ "coachPrefill": "Nie zapisałem jeszcze temperatury ciała. Jaki zakres jest prawidłowy i co powinienem zrobić, gdy od niego odbiega?"
},
"steps": {
"title": "Kroki",
@@ -3621,7 +3623,8 @@
"emptyState": {
"title": "Brak danych o krokach",
"description": "Dzienne sumy kroków pojawią się tutaj, gdy Apple Zdrowie lub urządzenie aktywności zsynchronizuje pełny dzień."
- }
+ },
+ "coachPrefill": "Nie zapisałem jeszcze kroków. Jaki dzienny cel kroków jest rozsądny i jak więcej chodzenia pomaga mojemu zdrowiu?"
},
"activeEnergy": {
"title": "Energia czynna",
@@ -3630,7 +3633,8 @@
"emptyState": {
"title": "Brak danych o energii czynnej",
"description": "Sumy energii czynnej pojawią się tutaj, gdy Apple Zdrowie lub urządzenie Withings zsynchronizuje pełny dzień."
- }
+ },
+ "coachPrefill": "Nie zapisałem jeszcze energii czynnej. Jaki dzienny cel jest rozsądny i jak ma się on do łącznej liczby kalorii?"
},
"subPage": {
"loadError": "Nie udało się teraz wczytać tej metryki.",
@@ -3725,7 +3729,8 @@
"valuesBack": "Powrót do Insights",
"valuesBackToMetric": "Powrót do {metric}",
"scoresBack": "Powrót do Insights",
- "ecgDescription": "Zapisy EKG jednoodprowadzeniowe z Twojego urządzenia, każdy z wynikiem samego urządzenia."
+ "ecgDescription": "Zapisy EKG jednoodprowadzeniowe z Twojego urządzenia, każdy z wynikiem samego urządzenia.",
+ "emptyCoachPrefill": "{metric}: nie mam tu jeszcze żadnych wpisów. Co ten wskaźnik mówi o moim zdrowiu i jak mogę go poprawić?"
},
"units": {
"steps": "kroki",
@@ -3825,27 +3830,32 @@
"bloodPressure": {
"title": "Brak pomiarów ciśnienia krwi",
"description": "Zarejestruj pomiar ciśnienia, a pojawi się tu trend, udział w zakresie docelowym i klasyfikacja WHO.",
- "cta": "Zarejestruj ciśnienie"
+ "cta": "Zarejestruj ciśnienie",
+ "coachPrefill": "Nie zapisałem jeszcze ciśnienia krwi. Dlaczego ma ono znaczenie i co powinienem wiedzieć, zanim zacznę?"
},
"weight": {
"title": "Brak pomiarów wagi",
"description": "Zarejestruj wagę, a trend, średnia krocząca i zakres docelowy pojawią się automatycznie.",
- "cta": "Zarejestruj wagę"
+ "cta": "Zarejestruj wagę",
+ "coachPrefill": "Nie zapisałem jeszcze masy ciała. Dlaczego ma ona znaczenie i co powinienem wiedzieć, zanim zacznę ją śledzić?"
},
"pulse": {
"title": "Brak pomiarów tętna",
"description": "Zarejestruj tętno, a w tym widoku pojawi się tętno spoczynkowe, HRV i VO₂ max.",
- "cta": "Zarejestruj tętno"
+ "cta": "Zarejestruj tętno",
+ "coachPrefill": "Nie zapisałem jeszcze tętna spoczynkowego. Dlaczego ma ono znaczenie i co powinienem wiedzieć, zanim zacznę?"
},
"bmi": {
"title": "BMI jeszcze nie obliczone",
"description": "Po wpisaniu wzrostu i co najmniej jednej wagi pojawi się tu trend BMI z zakresami WHO.",
- "cta": "Zarejestruj wagę"
+ "cta": "Zarejestruj wagę",
+ "coachPrefill": "Nie zapisałem jeszcze masy ciała. Dlaczego BMI ma dla mnie znaczenie i co powinienem wiedzieć, zanim zacznę je śledzić?"
},
"mood": {
"title": "Brak wpisów nastroju",
"description": "Zarejestruj nastrój, a pojawi się trend, pasmo stabilności i korelacje.",
- "cta": "Zarejestruj nastrój"
+ "cta": "Zarejestruj nastrój",
+ "coachPrefill": "Nie zapisałem jeszcze nastroju. Dlaczego warto go śledzić i jak powinienem zacząć?"
},
"medication": {
"title": "Brak skonfigurowanych leków",
@@ -3855,7 +3865,8 @@
"sleep": {
"title": "Brak danych o śnie",
"description": "Gdy pojawią się dane o śnie (ręcznie lub przez Withings / Apple Health), wyświetli się tu rozkład faz na noc.",
- "cta": "Połącz źródło"
+ "cta": "Połącz źródło",
+ "coachPrefill": "Nie mam jeszcze danych o śnie. Dlaczego warto śledzić sen i co powinienem wiedzieć, zanim podłączę źródło?"
}
},
"navWorkouts": "Treningi",
@@ -3865,7 +3876,8 @@
"description": "Ostatnie biegi, jazdy, marsze i treningi siłowe, zdeduplikowane między Apple Watch a Withings.",
"emptyState": {
"title": "Brak treningów",
- "description": "Treningi trafiają tu z Apple Zdrowie na iPhonie i z Withings. Możesz też wpisać trening ręcznie."
+ "description": "Treningi trafiają tu z Apple Zdrowie na iPhonie i z Withings. Możesz też wpisać trening ręcznie.",
+ "coachPrefill": "Nie zapisałem jeszcze treningów. Dlaczego warto je śledzić i na czym powinienem się najpierw skupić?"
},
"detail": {
"backToList": "Powrót do treningów",
@@ -4745,7 +4757,8 @@
"emptyState": {
"title": "Brak danych o upadkach",
"description": "Wykryte upadki pojawią się tutaj, gdy Apple Health się zsynchronizuje. Celem jest brak upadków."
- }
+ },
+ "coachPrefill": "Nie zapisałem jeszcze danych o upadkach. Co ten wskaźnik mówi o moim zdrowiu i jak mogę zmniejszyć ryzyko upadku?"
},
"navFalls": "Upadki",
"sixMinuteWalk": {
@@ -5695,7 +5708,7 @@
"title": "Dziennik chorób",
"description": "Widok listy i kolejność epizodów."
},
- "vorsorge": {
+ "checkups": {
"title": "Badania kontrolne",
"description": "Widok listy i kolejność przypomnień."
},
@@ -6121,7 +6134,7 @@
"moduleSettingsDescription": "Wszystko poza kolejnością i widokiem jest na stronie modułu w Modułach."
}
},
- "gesundheitsakte": {
+ "health-record": {
"title": "Dokumentacja zdrowotna",
"subtitle": "Eksportuj kompletną dokumentację zdrowotną gotową dla lekarza."
},
@@ -7058,7 +7071,7 @@
"builtAtLabel": "Skompilowano",
"license": "Licencja",
"repository": "Kod źródłowy",
- "changelog": "Changelog",
+ "changelog": "Informacje o wersjach",
"docs": "Dokumentacja",
"credits": "Autorzy i źródła danych",
"linksHeading": "Źródła i dokumentacja",
@@ -10229,6 +10242,7 @@
"BLOOD_PRESSURE_SYS": "Ciśnienie krwi",
"PULSE": "Tętno",
"BLOOD_GLUCOSE": "Glukoza we krwi",
+ "BLOOD_KETONES": "Ketony we krwi",
"OXYGEN_SATURATION": "Saturacja tlenem",
"BODY_FAT": "Tkanka tłuszczowa",
"BODY_TEMPERATURE": "Temperatura ciała",
@@ -11073,7 +11087,7 @@
"notePlaceholder": "Jak minął dzień (zaszyfrowane).",
"severityFor": "Nasilenie: {symptom}",
"saveError": "Nie udało się zapisać dnia. Spróbuj ponownie.",
- "day": "Day"
+ "day": "Dzień"
},
"impact": {
"0": "Pełna sprawność",
@@ -11162,14 +11176,14 @@
"RESTING_HEART_RATE": "Spoczynkowe tętno",
"HEART_RATE_VARIABILITY": "Zmienność rytmu serca",
"HRV_RMSSD": "HRV (RMSSD)",
- "RECOVERY_SCORE": "Recovery",
+ "RECOVERY_SCORE": "Regeneracja",
"OXYGEN_SATURATION": "Saturacja tlenem",
"BODY_TEMPERATURE": "Temperatura ciała",
"RESPIRATORY_RATE": "Częstość oddechów",
"BLOOD_PRESSURE_SYS": "Ciśnienie skurczowe",
"BLOOD_PRESSURE_DIA": "Ciśnienie rozkurczowe",
- "PULSE": "Pulse",
- "WEIGHT": "Weight",
+ "PULSE": "Tętno",
+ "WEIGHT": "Waga",
"FUNCTIONAL_IMPACT": "Wpływ na funkcjonowanie"
},
"listLoadError": "Nie udało się załadować epizodów."
diff --git a/messages/pt-BR.json b/messages/pt-BR.json
index 9c4c36a1c..1ea7da102 100644
--- a/messages/pt-BR.json
+++ b/messages/pt-BR.json
@@ -764,7 +764,7 @@
"noneActionableCta": "Abrir medicamentos"
},
"quickEntryDiscard": {
- "title": "Descartar esta entrada?",
+ "title": "Descartar as alterações?",
"description": "Você tem entrada não salva. Fechar o formulário e perder o que digitou?",
"confirm": "Descartar",
"cancel": "Continuar editando"
@@ -1547,7 +1547,6 @@
"glp1NextInjectionToday": "hoje",
"glp1NextInjectionTomorrow": "amanhã",
"glp1RotationSuggested": "Próximo recomendado: {site}",
- "glp1LogSideEffect": "Registrar efeito colateral",
"sideEffects": {
"section": "Efeitos colaterais",
"addCta": "Registrar",
@@ -3583,7 +3582,8 @@
"emptyState": {
"title": "Ainda não há amostras de VFC",
"description": "As amostras de VFC chegam aqui assim que o Apple Watch (ou um dispositivo Withings compatível) começa a sincronizar leituras noturnas."
- }
+ },
+ "coachPrefill": "Ainda não registrei dados de VFC. O que a variabilidade da frequência cardíaca me diz, e como eu a registro?"
},
"restingHr": {
"title": "Frequência cardíaca em repouso",
@@ -3602,7 +3602,8 @@
"emptyState": {
"title": "Ainda não há amostras de SpO₂",
"description": "As amostras de SpO₂ chegam aqui assim que um wearable compatível (Apple Watch, Withings ScanWatch) captura uma leitura."
- }
+ },
+ "coachPrefill": "Ainda não registrei saturação de oxigênio. O que a SpO₂ me diz, e qual faixa devo buscar?"
},
"bodyTemperature": {
"title": "Temperatura corporal",
@@ -3612,7 +3613,8 @@
"title": "Ainda não há temperatura corporal",
"description": "Registre uma leitura de temperatura corporal ou deixe seu wearable sincronizar uma, e a tendência aparecerá aqui.",
"cta": "Registrar temperatura corporal"
- }
+ },
+ "coachPrefill": "Ainda não registrei temperatura corporal. Qual é a faixa saudável, e o que devo fazer se ela se desviar?"
},
"steps": {
"title": "Passos",
@@ -3621,7 +3623,8 @@
"emptyState": {
"title": "Ainda não há dados de passos",
"description": "Os totais diários de passos aparecem aqui assim que o Apple Health ou um dispositivo de atividade sincroniza um dia de movimento."
- }
+ },
+ "coachPrefill": "Ainda não registrei passos. Qual é uma meta diária de passos razoável, e como caminhar mais ajuda minha saúde?"
},
"activeEnergy": {
"title": "Energia ativa",
@@ -3630,7 +3633,8 @@
"emptyState": {
"title": "Ainda não há dados de energia ativa",
"description": "Os totais de energia ativa chegam aqui assim que o Apple Health ou um dispositivo de atividade Withings sincroniza um dia de movimento."
- }
+ },
+ "coachPrefill": "Ainda não registrei energia ativa. Qual é uma meta diária razoável, e como ela se compara ao total de calorias?"
},
"subPage": {
"loadError": "Não foi possível carregar esta métrica agora.",
@@ -3725,7 +3729,8 @@
"valuesBack": "Voltar aos insights",
"valuesBackToMetric": "Voltar para {metric}",
"scoresBack": "Voltar aos insights",
- "ecgDescription": "Registros de ECG de uma derivação do seu dispositivo, cada um com o resultado do próprio dispositivo."
+ "ecgDescription": "Registros de ECG de uma derivação do seu dispositivo, cada um com o resultado do próprio dispositivo.",
+ "emptyCoachPrefill": "{metric}: ainda não registrei nada aqui. O que essa métrica me diz sobre minha saúde, e como eu a melhoro?"
},
"units": {
"steps": "passos",
@@ -3825,27 +3830,32 @@
"bloodPressure": {
"title": "Ainda não há registros de pressão arterial",
"description": "Registre uma leitura de pressão arterial e o gráfico de tendência, a parcela dentro da faixa e a classificação da OMS aparecerão aqui.",
- "cta": "Registrar pressão arterial"
+ "cta": "Registrar pressão arterial",
+ "coachPrefill": "Ainda não registrei a pressão arterial. Por que ela importa, e o que devo saber antes de começar?"
},
"weight": {
"title": "Ainda não há registros de peso",
"description": "Registre um peso e a tendência, a média móvel e a faixa-alvo aparecerão automaticamente.",
- "cta": "Registrar peso"
+ "cta": "Registrar peso",
+ "coachPrefill": "Ainda não registrei meu peso. Por que ele importa, e o que devo saber antes de começar a acompanhá-lo?"
},
"pulse": {
"title": "Ainda não há registros de pulso",
"description": "Registre uma leitura de pulso e o pulso em repouso, a VFC e o VO₂ máx aparecerão nesta visão.",
- "cta": "Registrar pulso"
+ "cta": "Registrar pulso",
+ "coachPrefill": "Ainda não registrei o pulso em repouso. Por que ele importa, e o que devo saber antes de começar?"
},
"bmi": {
"title": "IMC ainda não calculado",
"description": "Assim que a altura e pelo menos um registro de peso estiverem presentes, a tendência do IMC com as zonas da OMS aparecerá aqui.",
- "cta": "Registrar peso"
+ "cta": "Registrar peso",
+ "coachPrefill": "Ainda não registrei meu peso. Por que o IMC importa para mim, e o que devo saber antes de começar a acompanhá-lo?"
},
"mood": {
"title": "Ainda não há registros de humor",
"description": "Registre uma entrada de humor e a tendência, a faixa de estabilidade e as correlações aparecerão.",
- "cta": "Registrar humor"
+ "cta": "Registrar humor",
+ "coachPrefill": "Ainda não registrei meu humor. Por que acompanhar o humor importa, e como devo começar?"
},
"medication": {
"title": "Ainda não há medicamentos configurados",
@@ -3855,7 +3865,8 @@
"sleep": {
"title": "Ainda não há registros de sono",
"description": "Assim que os dados de sono chegarem (manual ou via Withings / Apple Health), a divisão por estágio por noite aparecerá aqui.",
- "cta": "Conectar uma fonte"
+ "cta": "Conectar uma fonte",
+ "coachPrefill": "Ainda não tenho dados de sono. Por que acompanhar o sono importa, e o que devo saber antes de conectar uma fonte?"
}
},
"navWorkouts": "Treinos",
@@ -3865,7 +3876,8 @@
"description": "Corridas, pedaladas, caminhadas e sessões de força recentes, deduplicadas entre as fontes Apple Watch e Withings.",
"emptyState": {
"title": "Ainda não há treinos",
- "description": "Os treinos sincronizam do Apple Health no seu iPhone e da Withings. Você também pode registrar um manualmente."
+ "description": "Os treinos sincronizam do Apple Health no seu iPhone e da Withings. Você também pode registrar um manualmente.",
+ "coachPrefill": "Ainda não registrei treinos. Por que acompanhá-los importa, e no que devo focar primeiro?"
},
"detail": {
"backToList": "Voltar aos treinos",
@@ -4745,7 +4757,8 @@
"emptyState": {
"title": "Ainda não há dados de quedas",
"description": "As quedas detectadas aparecem aqui assim que o Apple Health sincroniza. Nenhuma queda é a meta."
- }
+ },
+ "coachPrefill": "Ainda não registrei dados de quedas. O que essa métrica me diz sobre minha saúde, e como reduzo meu risco de queda?"
},
"navFalls": "Quedas",
"sixMinuteWalk": {
@@ -5695,7 +5708,7 @@
"title": "Diário de doenças",
"description": "Visualização em lista e ordem dos episódios."
},
- "vorsorge": {
+ "checkups": {
"title": "Check-ups",
"description": "Visualização em lista e ordem dos lembretes."
},
@@ -6121,7 +6134,7 @@
"moduleSettingsDescription": "Tudo além da ordem e da visualização fica na página do módulo, em Módulos."
}
},
- "gesundheitsakte": {
+ "health-record": {
"title": "Registro de saúde",
"subtitle": "Exporte um registro de saúde completo, pronto para o clínico."
},
@@ -7058,7 +7071,7 @@
"builtAtLabel": "Compilado em",
"license": "Licença",
"repository": "Código-fonte",
- "changelog": "Changelog",
+ "changelog": "Notas de versão",
"docs": "Documentação",
"credits": "Créditos e atribuições de dados",
"linksHeading": "Fontes e documentação",
@@ -10229,6 +10242,7 @@
"BLOOD_PRESSURE_SYS": "Pressão arterial",
"PULSE": "Pulso",
"BLOOD_GLUCOSE": "Glicemia",
+ "BLOOD_KETONES": "Cetonas no sangue",
"OXYGEN_SATURATION": "Saturação de oxigênio",
"BODY_FAT": "Gordura corporal",
"BODY_TEMPERATURE": "Temperatura corporal",
diff --git a/messages/ru.json b/messages/ru.json
index 6f368e37c..0d451a0c5 100644
--- a/messages/ru.json
+++ b/messages/ru.json
@@ -764,9 +764,9 @@
"noneActionableCta": "Открыть лекарства"
},
"quickEntryDiscard": {
- "title": "Отменить эту запись?",
- "description": "У вас есть несохранённые данные. Закрыть форму и потерять введённое?",
- "confirm": "Отменить",
+ "title": "Закрыть без сохранения?",
+ "description": "Введённые данные не сохранены. Закрыть форму и потерять их?",
+ "confirm": "Не сохранять",
"cancel": "Продолжить ввод"
},
"compliance7d": "Приверженность (7 дней)",
@@ -1547,7 +1547,6 @@
"glp1NextInjectionToday": "сегодня",
"glp1NextInjectionTomorrow": "завтра",
"glp1RotationSuggested": "Рекомендуется следующее: {site}",
- "glp1LogSideEffect": "Записать побочный эффект",
"sideEffects": {
"section": "Побочные эффекты",
"addCta": "Записать",
@@ -3583,7 +3582,8 @@
"emptyState": {
"title": "Пока нет данных HRV",
"description": "Данные HRV появятся здесь, как только Apple Watch (или совместимое устройство Withings) начнёт синхронизировать ночные измерения."
- }
+ },
+ "coachPrefill": "Я ещё не записывал данные HRV. Что говорит вариабельность сердечного ритма и как её измерять?"
},
"restingHr": {
"title": "ЧСС в покое",
@@ -3602,7 +3602,8 @@
"emptyState": {
"title": "Пока нет данных SpO₂",
"description": "Данные SpO₂ появятся здесь, как только совместимое носимое устройство (Apple Watch, Withings ScanWatch) сделает измерение."
- }
+ },
+ "coachPrefill": "Я ещё не записывал насыщение кислородом. Что говорит SpO₂ и к какому диапазону стоит стремиться?"
},
"bodyTemperature": {
"title": "Температура тела",
@@ -3612,7 +3613,8 @@
"title": "Пока нет температуры тела",
"description": "Запишите температуру тела или дайте носимому устройству синхронизировать её, и динамика появится здесь.",
"cta": "Записать температуру тела"
- }
+ },
+ "coachPrefill": "Я ещё не записывал температуру тела. Какой диапазон считается нормальным и что делать, если она отклоняется?"
},
"steps": {
"title": "Шаги",
@@ -3621,7 +3623,8 @@
"emptyState": {
"title": "Пока нет данных о шагах",
"description": "Ежедневные итоги шагов появятся здесь, как только Apple Health или устройство активности синхронизирует день движения."
- }
+ },
+ "coachPrefill": "Я ещё не записывал шаги. Какая дневная цель по шагам разумна и как больше ходьбы помогает здоровью?"
},
"activeEnergy": {
"title": "Активная энергия",
@@ -3630,7 +3633,8 @@
"emptyState": {
"title": "Пока нет данных об активной энергии",
"description": "Итоги активной энергии появятся здесь, как только Apple Health или устройство активности Withings синхронизирует день движения."
- }
+ },
+ "coachPrefill": "Я ещё не записывал активную энергию. Какая дневная цель разумна и как она соотносится с общим расходом калорий?"
},
"subPage": {
"loadError": "Не удалось загрузить этот показатель прямо сейчас.",
@@ -3725,7 +3729,8 @@
"valuesBack": "Назад к аналитике",
"valuesBackToMetric": "Назад к {metric}",
"scoresBack": "Назад к аналитике",
- "ecgDescription": "Записи ЭКГ с одним отведением с вашего устройства, каждая с собственным результатом устройства."
+ "ecgDescription": "Записи ЭКГ с одним отведением с вашего устройства, каждая с собственным результатом устройства.",
+ "emptyCoachPrefill": "{metric}: здесь у меня ещё нет записей. Что этот показатель говорит о моём здоровье и как его улучшить?"
},
"units": {
"steps": "шаги",
@@ -3825,27 +3830,32 @@
"bloodPressure": {
"title": "Пока нет записей АД",
"description": "Запишите измерение АД, и здесь появятся график динамики, доля значений в диапазоне и классификация ВОЗ.",
- "cta": "Записать АД"
+ "cta": "Записать АД",
+ "coachPrefill": "Я ещё не записывал давление. Почему оно важно и что нужно знать, прежде чем начать?"
},
"weight": {
"title": "Пока нет записей веса",
"description": "Запишите вес, и динамика, скользящее среднее и целевой диапазон появятся автоматически.",
- "cta": "Записать вес"
+ "cta": "Записать вес",
+ "coachPrefill": "Я ещё не записывал вес. Почему он важен и что нужно знать, прежде чем начать его отслеживать?"
},
"pulse": {
"title": "Пока нет записей пульса",
"description": "Запишите измерение пульса, и в этом виде появятся пульс в покое, HRV и VO₂ max.",
- "cta": "Записать пульс"
+ "cta": "Записать пульс",
+ "coachPrefill": "Я ещё не записывал пульс в покое. Почему он важен и что нужно знать, прежде чем начать?"
},
"bmi": {
"title": "ИМТ ещё не рассчитан",
"description": "Как только появятся рост и хотя бы одна запись веса, здесь появится динамика ИМТ с зонами ВОЗ.",
- "cta": "Записать вес"
+ "cta": "Записать вес",
+ "coachPrefill": "Я ещё не записывал вес. Почему ИМТ важен для меня и что нужно знать, прежде чем начать его отслеживать?"
},
"mood": {
"title": "Пока нет записей настроения",
"description": "Запишите настроение, и появятся динамика, диапазон стабильности и корреляции.",
- "cta": "Записать настроение"
+ "cta": "Записать настроение",
+ "coachPrefill": "Я ещё не записывал настроение. Почему важно его отслеживать и с чего начать?"
},
"medication": {
"title": "Пока не настроено ни одного лекарства",
@@ -3855,7 +3865,8 @@
"sleep": {
"title": "Пока нет записей сна",
"description": "Как только появятся данные о сне (вручную или через Withings / Apple Health), здесь появится разбивка по фазам за каждую ночь.",
- "cta": "Подключить источник"
+ "cta": "Подключить источник",
+ "coachPrefill": "У меня ещё нет данных о сне. Почему важно отслеживать сон и что нужно знать, прежде чем подключить источник?"
}
},
"navWorkouts": "Тренировки",
@@ -3865,7 +3876,8 @@
"description": "Недавние пробежки, поездки на велосипеде, прогулки и силовые занятия, без дублей между источниками Apple Watch и Withings.",
"emptyState": {
"title": "Пока нет тренировок",
- "description": "Тренировки синхронизируются из Apple Health на вашем iPhone и из Withings. Вы также можете записать одну вручную."
+ "description": "Тренировки синхронизируются из Apple Health на вашем iPhone и из Withings. Вы также можете записать одну вручную.",
+ "coachPrefill": "Я ещё не записывал тренировки. Почему важно их отслеживать и на чём сосредоточиться в первую очередь?"
},
"detail": {
"backToList": "Назад к тренировкам",
@@ -4745,7 +4757,8 @@
"emptyState": {
"title": "Пока нет данных о падениях",
"description": "Обнаруженные падения появятся здесь, когда Apple Health синхронизирует их. Цель — ноль падений."
- }
+ },
+ "coachPrefill": "Я ещё не записывал данные о падениях. Что этот показатель говорит о моём здоровье и как снизить риск падений?"
},
"navFalls": "Падения",
"sixMinuteWalk": {
@@ -5695,7 +5708,7 @@
"title": "Дневник болезней",
"description": "Вид списка и порядок эпизодов."
},
- "vorsorge": {
+ "checkups": {
"title": "Профилактика",
"description": "Вид списка и порядок напоминаний."
},
@@ -6121,7 +6134,7 @@
"moduleSettingsDescription": "Всё, кроме порядка и вида, настраивается на странице модуля в разделе «Модули»."
}
},
- "gesundheitsakte": {
+ "health-record": {
"title": "Медицинская карта",
"subtitle": "Экспортируйте полную медицинскую карту, готовую для врача."
},
@@ -7058,7 +7071,7 @@
"builtAtLabel": "Собрано",
"license": "Лицензия",
"repository": "Исходный код",
- "changelog": "Список изменений",
+ "changelog": "Примечания к выпускам",
"docs": "Документация",
"credits": "Благодарности и источники данных",
"linksHeading": "Источники и документация",
@@ -10229,6 +10242,7 @@
"BLOOD_PRESSURE_SYS": "АД",
"PULSE": "Пульс",
"BLOOD_GLUCOSE": "Глюкоза крови",
+ "BLOOD_KETONES": "Кетоны в крови",
"OXYGEN_SATURATION": "Насыщение кислородом",
"BODY_FAT": "Процент жировой массы",
"BODY_TEMPERATURE": "Температура тела",
@@ -10278,7 +10292,7 @@
"pushBody": "Пора: {label}",
"pushLocation": "(в {location})",
"edit": "Изменить",
- "originCoach": "Тренер",
+ "originCoach": "Коуч",
"until": "До {date}",
"selfPlanned": "Запланировано самостоятельно",
"captureValue": "Записать значение",
diff --git a/next.config.ts b/next.config.ts
index 25c5c3724..1f78d2058 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -230,7 +230,19 @@ const nextConfig: NextConfig = {
},
{
source: "/settings/vorsorge",
- destination: "/settings/layout/vorsorge",
+ destination: "/settings/layout/checkups",
+ permanent: true,
+ },
+ // The health record and checkups pages had German addresses in every
+ // language; the English ones replace them, and the old ones redirect.
+ {
+ source: "/settings/gesundheitsakte",
+ destination: "/settings/health-record",
+ permanent: true,
+ },
+ {
+ source: "/settings/layout/vorsorge",
+ destination: "/settings/layout/checkups",
permanent: true,
},
// v1.25.7 — clinician share links fold into the Gesundheitsakte section
@@ -238,7 +250,7 @@ const nextConfig: NextConfig = {
// to the anchor there.
{
source: "/settings/sharing",
- destination: "/settings/gesundheitsakte#sharing",
+ destination: "/settings/health-record#sharing",
permanent: true,
},
// v1.22.0 — the preventive-care surface moved from the German slug
diff --git a/package.json b/package.json
index 4a741829c..87525da8e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "healthlog",
- "version": "1.47.4",
+ "version": "1.47.5",
"description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.",
"license": "PolyForm-Noncommercial-1.0.0",
"homepage": "https://healthlog.dev",
diff --git a/prisma/README.md b/prisma/README.md
index fdea762fa..7b11368e9 100644
--- a/prisma/README.md
+++ b/prisma/README.md
@@ -23,6 +23,6 @@ Number a new migration with the next free prefix. The production entrypoint runs
- Encryption is fail-closed: `*Encrypted` columns are written through `@/lib/crypto` (AES-256-GCM, versioned key ids); never store plaintext in an encrypted column. Key rotation is `scripts/rotate-encryption-key.ts` + `docs/ops/encryption-key-rotation.md`.
- The generated client under `src/generated/prisma` is build output — do not edit it.
-- Schema-touching changes that affect the API surface must regenerate `docs/api/openapi.yaml` (see [`../CLAUDE.md`](../CLAUDE.md)).
+- Schema-touching changes that affect the API surface must regenerate `docs/api/openapi.yaml` (`pnpm openapi:generate`; CI fails on drift).
Related: [`../src/lib/README.md`](../src/lib/README.md) (`db.ts`, `rollups/`), [`../docs/ops/`](../docs/ops/) (migrations + backup runbooks).
diff --git a/public/sw.js b/public/sw.js
index bf37050ea..fbb56e5c2 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -16,7 +16,7 @@ try {
}
const CACHE_VERSION =
(typeof self !== "undefined" && self.__APP_VERSION__) ||
- /* @sw-version-fallback */ "v1.47.4";
+ /* @sw-version-fallback */ "v1.47.5";
const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`;
const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`;
// Every HealthLog cache, current and legacy, and nothing another app on the
diff --git a/scripts/README.md b/scripts/README.md
index 18da03099..4f7b49c39 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -1,6 +1,6 @@
# `scripts/`
-One-shot operator and build scripts. These are not part of the running application — they are run by hand during a deploy, a backfill, or a release, or wired into CI. Recurring work belongs on pg-boss (`src/lib/jobs/`), not here.
+One-shot operator and build scripts. They are not part of the running application: they are run by hand during a deploy, a backfill, or a release, or wired into CI. Recurring work belongs on pg-boss (`src/lib/jobs/`), not here.
## How to run them
@@ -18,24 +18,28 @@ typecheck.
## What lives here
-- **`generate-openapi.ts`** — emits `docs/api/openapi.yaml` from the registered Zod schemas (`pnpm openapi:generate`).
-- **`check-openapi.ts`** — compares a freshly generated spec against the committed YAML; CI fails on drift (`pnpm openapi:check`).
-- **`check-env.ts`** — pre-deploy env-var sanity check against `env-manifest.json` (`pnpm check-env`); wired into CI.
-- **`assert-deploy.ts`** — post-deploy `/api/version` assertion.
-- **`rotate-encryption-key.ts`** — AES-256-GCM key rotation; see `docs/ops/encryption-key-rotation.md`.
-- **`restore-backup.ts`** — restore an off-host AES-GCM backup from an S3-compatible bucket.
-- **`repair-apple-health-distance.ts`** — repair walking-distance rows the `export.zip` import stored 1000x too small; dry-run by default, see `docs/ops/apple-health-distance-repair.md`.
-- **`repair-intake-anomalies.ts`** — repair historic medication-ledger defects (duplicate dose-slot rows, implausible `taken_at`); dry-run by default, see `docs/ops/intake-repair.md`.
-- **`backfill-rollups.ts`**, **`drain-per-sample-cumulative.ts`**, **`backfill-mood-note-column.ts`** — historical data backfills (note: the boot-time `rollup-full-backfill` queue handles new accounts automatically; the CLI is the manual fallback).
-- **`seed-demo.ts`** + **`seed-demo-v15.sql`** — populate a demo tenant.
-- **`bench/`** — `seed-large-account.ts` (a synthetic ten-year account of about 1.26 million measurement rows) and `run.ts` (latency and SQL statements per request against it); see `docs/contributing/benchmarks.md`. Results land in `bench/results/`.
-- **`test-notifications.ts`** — exercise the notification dispatcher + reminder check.
-- **`fetch-geolite2.sh`** — download the MaxMind GeoLite2 MMDBs (needs a licence key) before `docker build`.
-- **`build-pdf-fonts.py`** — rebuild the fonts the doctor-report PDF embeds (`src/lib/doctor-report-pdf/fonts/`) from the upstream Noto sources; needs `fonttools`. The source URLs are in its header.
-- **`install-hooks.sh`** — install the local git hooks.
-- **`generate-sw-version.mjs`**, **`print-bundle-report.mjs`** — service-worker version stamp and bundle-size report.
-- **`i18n/`** — locale tooling; **`__tests__/`** — tests for the scripts above.
+- **`generate-openapi.ts`**: emits `docs/api/openapi.yaml` from the registered Zod schemas (`pnpm openapi:generate`).
+- **`check-openapi.ts`**: compares a freshly generated spec against the committed YAML; CI fails on drift (`pnpm openapi:check`).
+- **`check-env.ts`**: pre-deploy env-var sanity check against `env-manifest.json` (`pnpm check-env`); wired into CI.
+- **`assert-deploy.ts`**: post-deploy `/api/version` assertion.
+- **`rotate-encryption-key.ts`**: AES-256-GCM key rotation; see `docs/ops/encryption-key-rotation.md`.
+- **`restore-backup.ts`**: restore an off-host AES-GCM backup from an S3-compatible bucket.
+- **`repair-apple-health-distance.ts`**: repair walking-distance rows the `export.zip` import stored 1000x too small; dry-run by default, see `docs/ops/apple-health-distance-repair.md`.
+- **`repair-intake-anomalies.ts`**: repair historic medication-ledger defects (duplicate dose-slot rows, implausible `taken_at`); dry-run by default, see `docs/ops/intake-repair.md`.
+- **`backfill-rollups.ts`**, **`drain-per-sample-cumulative.ts`**, **`backfill-mood-note-column.ts`**: historical data backfills (the boot-time `rollup-full-backfill` queue handles new accounts automatically; the CLI is the manual fallback).
+- **`seed-demo.ts`** + **`seed-demo-v15.sql`**: populate a demo tenant.
+- **`bench/`**: `seed-large-account.ts` (a synthetic ten-year account of about 1.26 million measurement rows) and `run.ts` (latency and SQL statements per request against it); see `docs/contributing/benchmarks.md`. Results land in `bench/results/`.
+- **`test-notifications.ts`**: exercise the notification dispatcher + reminder check.
+- **`fetch-geolite2.sh`**: download the MaxMind GeoLite2 MMDBs (needs a licence key) before `docker build`.
+- **`build-pdf-fonts.py`**: rebuild the fonts the doctor-report PDF embeds (`src/lib/doctor-report-pdf/fonts/`) from the upstream Noto sources; needs `fonttools`. The source URLs are in its header.
+- **`install-hooks.sh`**: install the local git hooks.
+- **`generate-sw-version.mjs`**, **`check-bundle-budget.mjs`**: service-worker version stamp and bundle-size report (`pnpm bundle-report`).
+- **`repair-google-health-sleep.ts`**: re-syncs every Google Health connection so nights stored twice before v1.28.18 collapse to one set; dry-run by default, `--run` to execute.
+- **`i18n-key-tool.mjs`**, **`split-description.mjs`**: edit one key across every `messages/*.json` (show, set, drop) and move an over-long card description's tail into its own key; **`measure-description-lengths.mjs`** measures the Settings and Admin description strings against the 120-character guideline.
+- **`audit-column-readers.ts`**: lists write-only schema columns and open `@pending-consumer` markers; a review tool, not a gate.
+- **`migrate-demo-account.sh`**: copies the demo account from one instance to a separate demo instance; see its README.
+- **`__tests__/`**: tests for the scripts above.
## Conventions
-See [`../CLAUDE.md`](../CLAUDE.md) "Tests and commands" + "Self-hosting gotchas". Operator runbooks that drive these scripts live in [`../docs/ops/`](../docs/ops/).
+Operator runbooks that drive these scripts live in [`../docs/ops/`](../docs/ops/).
diff --git a/scripts/__tests__/demo-published-password-guard.test.ts b/scripts/__tests__/demo-published-password-guard.test.ts
index e9167a684..eac5f20c4 100644
--- a/scripts/__tests__/demo-published-password-guard.test.ts
+++ b/scripts/__tests__/demo-published-password-guard.test.ts
@@ -8,7 +8,7 @@ import { describe, expect, it } from "vitest";
* demo.healthlog.dev. Its Argon2id hash lives in two places that must agree:
*
* - scripts/seed-demo.ts writes it when a demo tenant is seeded
- * - scripts/migrate-demo-edge01.sh overwrites it after a migration load
+ * - scripts/migrate-demo-account.sh overwrites it after a migration load
*
* Public issue #805: the migration used to carry the SOURCE account's hash
* across instead, so once the operator rotated their own demo password the
@@ -18,7 +18,7 @@ import { describe, expect, it } from "vitest";
const REPO_ROOT = join(__dirname, "..", "..");
const SEED = join(REPO_ROOT, "scripts", "seed-demo.ts");
-const MIGRATE = join(REPO_ROOT, "scripts", "migrate-demo-edge01.sh");
+const MIGRATE = join(REPO_ROOT, "scripts", "migrate-demo-account.sh");
/** Every Argon2id hash literal in a file, in source order. */
function argon2Hashes(path: string): string[] {
@@ -27,7 +27,7 @@ function argon2Hashes(path: string): string[] {
}
describe("published demo password", () => {
- it("is the same hash in the seed script and the edge01 migration", () => {
+ it("is the same hash in the seed script and the demo migration", () => {
const seeded = argon2Hashes(SEED);
const migrated = argon2Hashes(MIGRATE);
@@ -55,7 +55,7 @@ describe("published demo password", () => {
// ...and it must be proven, not merely attempted. Without the check a
// mangled hash would reach a visitor's login screen instead of the script.
expect(script).toContain(
- "ABORT/FAIL: demo password_hash on edge01 is not the published literal",
+ "ABORT/FAIL: demo password_hash on the target is not the published literal",
);
});
diff --git a/scripts/bench/results/v1.44.0-baseline.json b/scripts/bench/results/v1.44.0-baseline.json
deleted file mode 100644
index d6c5cc5dd..000000000
--- a/scripts/bench/results/v1.44.0-baseline.json
+++ /dev/null
@@ -1,254 +0,0 @@
-{
- "meta": {
- "label": "v1.44.0-baseline",
- "commit": "d640eea32",
- "date": "2026-10-22T03:54:05.746Z",
- "node": "v22.23.2",
- "postgres": "16.15",
- "cpus": 14,
- "loadAverage": [
- 9.4,
- 20.1,
- 20
- ],
- "measurements": 1266549,
- "iterations": 20,
- "warmup": 3,
- "heapMb": 792,
- "idleStatementsPerSecond": 0,
- "batch5k": {
- "totalMs": 1722,
- "totalStatements": 424
- }
- },
- "results": [
- {
- "name": "/api/auth/me",
- "mode": "cold",
- "request": "GET /api/auth/me",
- "n": 20,
- "p50Ms": 12.1,
- "p95Ms": 17.4,
- "maxMs": 18.4,
- "statementsMedian": 19,
- "statementsMax": 19,
- "failures": 0
- },
- {
- "name": "/api/auth/me",
- "mode": "warm",
- "request": "GET /api/auth/me",
- "n": 20,
- "p50Ms": 16.6,
- "p95Ms": 22.2,
- "maxMs": 24.4,
- "statementsMedian": 19,
- "statementsMax": 19,
- "failures": 0
- },
- {
- "name": "dashboard summary",
- "mode": "cold",
- "request": "GET /api/dashboard/summary",
- "n": 20,
- "p50Ms": 205.6,
- "p95Ms": 233.2,
- "maxMs": 275.3,
- "statementsMedian": 31,
- "statementsMax": 32,
- "failures": 0
- },
- {
- "name": "dashboard summary",
- "mode": "warm",
- "request": "GET /api/dashboard/summary",
- "n": 20,
- "p50Ms": 12.8,
- "p95Ms": 14.3,
- "maxMs": 27.5,
- "statementsMedian": 7,
- "statementsMax": 7,
- "failures": 0
- },
- {
- "name": "dashboard snapshot",
- "mode": "cold",
- "request": "GET /api/dashboard/snapshot",
- "n": 20,
- "p50Ms": 2157.8,
- "p95Ms": 2252.7,
- "maxMs": 2292.9,
- "statementsMedian": 158,
- "statementsMax": 161,
- "failures": 0
- },
- {
- "name": "dashboard snapshot",
- "mode": "warm",
- "request": "GET /api/dashboard/snapshot",
- "n": 20,
- "p50Ms": 15.6,
- "p95Ms": 23.5,
- "maxMs": 25.6,
- "statementsMedian": 13,
- "statementsMax": 16,
- "failures": 0
- },
- {
- "name": "series pulse 30 d",
- "mode": "cold",
- "request": "GET /api/measurements/series?kind=pulse&days=30",
- "n": 20,
- "p50Ms": 50.8,
- "p95Ms": 58.7,
- "maxMs": 58.9,
- "statementsMedian": 8,
- "statementsMax": 9,
- "failures": 0
- },
- {
- "name": "series pulse 30 d",
- "mode": "warm",
- "request": "GET /api/measurements/series?kind=pulse&days=30",
- "n": 20,
- "p50Ms": 52.3,
- "p95Ms": 59.5,
- "maxMs": 59.9,
- "statementsMedian": 8,
- "statementsMax": 8,
- "failures": 0
- },
- {
- "name": "series pulse 365 d",
- "mode": "cold",
- "request": "GET /api/measurements/series?kind=pulse&days=365",
- "n": 20,
- "p50Ms": 289,
- "p95Ms": 311.3,
- "maxMs": 363.7,
- "statementsMedian": 8,
- "statementsMax": 9,
- "failures": 0
- },
- {
- "name": "series pulse 365 d",
- "mode": "warm",
- "request": "GET /api/measurements/series?kind=pulse&days=365",
- "n": 20,
- "p50Ms": 274.3,
- "p95Ms": 355.8,
- "maxMs": 366.8,
- "statementsMedian": 8,
- "statementsMax": 8,
- "failures": 0
- },
- {
- "name": "series pulse 3650 d",
- "mode": "cold",
- "request": "GET /api/measurements/series?kind=pulse&days=3650",
- "n": 20,
- "p50Ms": 1502.6,
- "p95Ms": 2117.6,
- "maxMs": 2122.9,
- "statementsMedian": 8,
- "statementsMax": 9,
- "failures": 0
- },
- {
- "name": "series pulse 3650 d",
- "mode": "warm",
- "request": "GET /api/measurements/series?kind=pulse&days=3650",
- "n": 20,
- "p50Ms": 1490.3,
- "p95Ms": 1505.5,
- "maxMs": 1555.2,
- "statementsMedian": 8,
- "statementsMax": 11,
- "failures": 0
- },
- {
- "name": "day view",
- "mode": "cold",
- "request": "GET /api/day/2026-10-21",
- "n": 20,
- "p50Ms": 61.7,
- "p95Ms": 77.5,
- "maxMs": 86.1,
- "statementsMedian": 49,
- "statementsMax": 50,
- "failures": 0
- },
- {
- "name": "day view",
- "mode": "warm",
- "request": "GET /api/day/2026-10-21",
- "n": 20,
- "p50Ms": 49,
- "p95Ms": 55.6,
- "maxMs": 62.5,
- "statementsMedian": 48,
- "statementsMax": 48,
- "failures": 0
- },
- {
- "name": "insights health status",
- "mode": "cold",
- "request": "GET /api/insights/health-status",
- "n": 20,
- "p50Ms": 124.1,
- "p95Ms": 139.5,
- "maxMs": 141.5,
- "statementsMedian": 68,
- "statementsMax": 68,
- "failures": 0
- },
- {
- "name": "insights health status",
- "mode": "warm",
- "request": "GET /api/insights/health-status",
- "n": 20,
- "p50Ms": 124.7,
- "p95Ms": 157.8,
- "maxMs": 165.3,
- "statementsMedian": 68,
- "statementsMax": 68,
- "failures": 0
- },
- {
- "name": "insights correlations",
- "mode": "cold",
- "request": "GET /api/insights/correlations",
- "n": 20,
- "p50Ms": 95.4,
- "p95Ms": 105,
- "maxMs": 108.8,
- "statementsMedian": 45,
- "statementsMax": 45,
- "failures": 0
- },
- {
- "name": "insights correlations",
- "mode": "warm",
- "request": "GET /api/insights/correlations",
- "n": 20,
- "p50Ms": 13,
- "p95Ms": 15.4,
- "maxMs": 15.9,
- "statementsMedian": 8,
- "statementsMax": 8,
- "failures": 0
- },
- {
- "name": "batch ingest 5k (per 500)",
- "mode": "write",
- "request": "POST /api/measurements/batch ×10",
- "n": 10,
- "p50Ms": 163,
- "p95Ms": 220.7,
- "maxMs": 220.7,
- "statementsMedian": 42,
- "statementsMax": 45,
- "failures": 0
- }
- ]
-}
diff --git a/scripts/bench/results/v1.44.0-baseline.md b/scripts/bench/results/v1.44.0-baseline.md
deleted file mode 100644
index 09ecb185e..000000000
--- a/scripts/bench/results/v1.44.0-baseline.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Benchmark v1.44.0-baseline
-
-Commit d640eea32, 2026-10-22T03:54:05.746Z. 1266549 measurement rows on the bench account, 20 measured requests per row after 3 warm-up requests, one at a time. Server: production build, web process, heap ceiling 792 MB. Postgres 16.15, 14 cores, load average 9.4,20.1,20 at the end. Idle background: 0 statements/s.
-
-| Request | Mode | n | p50 ms | p95 ms | max ms | statements (median) | statements (max) |
-| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
-| /api/auth/me | cold | 20 | 12.1 | 17.4 | 18.4 | 19 | 19 |
-| /api/auth/me | warm | 20 | 16.6 | 22.2 | 24.4 | 19 | 19 |
-| dashboard summary | cold | 20 | 205.6 | 233.2 | 275.3 | 31 | 32 |
-| dashboard summary | warm | 20 | 12.8 | 14.3 | 27.5 | 7 | 7 |
-| dashboard snapshot | cold | 20 | 2157.8 | 2252.7 | 2292.9 | 158 | 161 |
-| dashboard snapshot | warm | 20 | 15.6 | 23.5 | 25.6 | 13 | 16 |
-| series pulse 30 d | cold | 20 | 50.8 | 58.7 | 58.9 | 8 | 9 |
-| series pulse 30 d | warm | 20 | 52.3 | 59.5 | 59.9 | 8 | 8 |
-| series pulse 365 d | cold | 20 | 289 | 311.3 | 363.7 | 8 | 9 |
-| series pulse 365 d | warm | 20 | 274.3 | 355.8 | 366.8 | 8 | 8 |
-| series pulse 3650 d | cold | 20 | 1502.6 | 2117.6 | 2122.9 | 8 | 9 |
-| series pulse 3650 d | warm | 20 | 1490.3 | 1505.5 | 1555.2 | 8 | 11 |
-| day view | cold | 20 | 61.7 | 77.5 | 86.1 | 49 | 50 |
-| day view | warm | 20 | 49 | 55.6 | 62.5 | 48 | 48 |
-| insights health status | cold | 20 | 124.1 | 139.5 | 141.5 | 68 | 68 |
-| insights health status | warm | 20 | 124.7 | 157.8 | 165.3 | 68 | 68 |
-| insights correlations | cold | 20 | 95.4 | 105 | 108.8 | 45 | 45 |
-| insights correlations | warm | 20 | 13 | 15.4 | 15.9 | 8 | 8 |
-| batch ingest 5k (per 500) | write | 10 | 163 | 220.7 | 220.7 | 42 | 45 |
diff --git a/scripts/bench/results/v1.45.0-before-boot.json b/scripts/bench/results/v1.45.0-before-boot.json
deleted file mode 100644
index 65e2ae420..000000000
--- a/scripts/bench/results/v1.45.0-before-boot.json
+++ /dev/null
@@ -1,1351 +0,0 @@
-{
- "meta": {
- "label": "v1.45.0-before-boot",
- "commit": "0f6d668df",
- "date": "2026-10-22T04:36:55.826Z",
- "node": "v22.23.2",
- "cpus": 14,
- "loadAverage": [
- 9.3,
- 9.4,
- 11.2
- ],
- "heapMb": 792,
- "idleSeconds": 300,
- "healthyAtMs": 1590,
- "readyAtMs": 5432,
- "stallMaxMs": 0
- },
- "bootLines": [],
- "stalls": [],
- "heapLines": [
- {
- "atMs": 5752,
- "heap_used_mb": 184,
- "heap_total_mb": 219,
- "heap_limit_mb": 840,
- "heap_used_ratio": 0.219,
- "new_space_mb": 1,
- "old_space_mb": 105,
- "code_space_mb": 4,
- "large_object_space_mb": 68,
- "new_large_object_space_mb": 0,
- "code_large_object_space_mb": 0,
- "trusted_space_mb": 6,
- "read_only_space_mb": 0,
- "other_spaces_mb": 0,
- "rss_mb": 267,
- "external_mb": 11,
- "array_buffers_mb": 0,
- "active_requests": 0,
- "active_jobs": 0,
- "in_flight_dropped": 0,
- "uptime_s": 6
- },
- {
- "atMs": 307706,
- "heap_used_mb": 164,
- "heap_total_mb": 170,
- "heap_limit_mb": 840,
- "heap_used_ratio": 0.196,
- "new_space_mb": 0,
- "old_space_mb": 99,
- "code_space_mb": 5,
- "large_object_space_mb": 58,
- "new_large_object_space_mb": 0,
- "code_large_object_space_mb": 0,
- "trusted_space_mb": 3,
- "read_only_space_mb": 0,
- "other_spaces_mb": 0,
- "rss_mb": 623,
- "external_mb": 11,
- "array_buffers_mb": 0,
- "active_requests": 0,
- "active_jobs": 1,
- "in_flight_dropped": 0,
- "uptime_s": 308
- }
- ],
- "markCompacts": [
- {
- "atMs": 307,
- "beforeMb": 21.4,
- "afterMb": 17.4
- },
- {
- "atMs": 560,
- "beforeMb": 49.9,
- "afterMb": 47.8
- },
- {
- "atMs": 685,
- "beforeMb": 79.8,
- "afterMb": 79.1
- },
- {
- "atMs": 1324,
- "beforeMb": 159.8,
- "afterMb": 154.8
- },
- {
- "atMs": 9449,
- "beforeMb": 189.6,
- "afterMb": 172.9
- },
- {
- "atMs": 10070,
- "beforeMb": 174.4,
- "afterMb": 170.7
- },
- {
- "atMs": 69903,
- "beforeMb": 179.9,
- "afterMb": 171.2
- },
- {
- "atMs": 151031,
- "beforeMb": 191.1,
- "afterMb": 174.3
- },
- {
- "atMs": 153639,
- "beforeMb": 193.8,
- "afterMb": 171.6
- },
- {
- "atMs": 158553,
- "beforeMb": 204.6,
- "afterMb": 161.5
- },
- {
- "atMs": 163529,
- "beforeMb": 191.2,
- "afterMb": 161.4
- },
- {
- "atMs": 167487,
- "beforeMb": 185.6,
- "afterMb": 161.5
- },
- {
- "atMs": 170381,
- "beforeMb": 187.3,
- "afterMb": 161.7
- },
- {
- "atMs": 173875,
- "beforeMb": 192.7,
- "afterMb": 162.2
- },
- {
- "atMs": 177580,
- "beforeMb": 185.9,
- "afterMb": 162.2
- },
- {
- "atMs": 183462,
- "beforeMb": 189.4,
- "afterMb": 162.3
- },
- {
- "atMs": 188340,
- "beforeMb": 181.6,
- "afterMb": 163.6
- },
- {
- "atMs": 193243,
- "beforeMb": 184.7,
- "afterMb": 163.7
- },
- {
- "atMs": 197708,
- "beforeMb": 186.5,
- "afterMb": 164.6
- },
- {
- "atMs": 201596,
- "beforeMb": 188.1,
- "afterMb": 164.7
- },
- {
- "atMs": 205007,
- "beforeMb": 182.3,
- "afterMb": 163.6
- },
- {
- "atMs": 208279,
- "beforeMb": 190.3,
- "afterMb": 164.3
- },
- {
- "atMs": 212210,
- "beforeMb": 184.4,
- "afterMb": 164.6
- },
- {
- "atMs": 217239,
- "beforeMb": 197.7,
- "afterMb": 163.6
- },
- {
- "atMs": 222139,
- "beforeMb": 190.6,
- "afterMb": 165
- },
- {
- "atMs": 226947,
- "beforeMb": 189.7,
- "afterMb": 164.5
- },
- {
- "atMs": 232566,
- "beforeMb": 194.2,
- "afterMb": 164.4
- },
- {
- "atMs": 235870,
- "beforeMb": 189.2,
- "afterMb": 164.9
- },
- {
- "atMs": 239409,
- "beforeMb": 182.9,
- "afterMb": 163.4
- },
- {
- "atMs": 243098,
- "beforeMb": 184.4,
- "afterMb": 163.7
- },
- {
- "atMs": 248670,
- "beforeMb": 193.1,
- "afterMb": 164.9
- },
- {
- "atMs": 252268,
- "beforeMb": 184.2,
- "afterMb": 164.2
- },
- {
- "atMs": 257945,
- "beforeMb": 190.5,
- "afterMb": 163.7
- },
- {
- "atMs": 261312,
- "beforeMb": 190.4,
- "afterMb": 166.8
- },
- {
- "atMs": 265221,
- "beforeMb": 188.2,
- "afterMb": 163.7
- },
- {
- "atMs": 269662,
- "beforeMb": 188.6,
- "afterMb": 164.2
- },
- {
- "atMs": 273624,
- "beforeMb": 187.7,
- "afterMb": 164.2
- },
- {
- "atMs": 277464,
- "beforeMb": 186.3,
- "afterMb": 163.8
- },
- {
- "atMs": 281950,
- "beforeMb": 196.7,
- "afterMb": 163.5
- },
- {
- "atMs": 285110,
- "beforeMb": 186.2,
- "afterMb": 164.6
- },
- {
- "atMs": 290497,
- "beforeMb": 193.9,
- "afterMb": 164.2
- },
- {
- "atMs": 295540,
- "beforeMb": 190.6,
- "afterMb": 164.4
- },
- {
- "atMs": 299084,
- "beforeMb": 183.5,
- "afterMb": 164.4
- },
- {
- "atMs": 305318,
- "beforeMb": 191.1,
- "afterMb": 164.4
- },
- {
- "atMs": 305585,
- "beforeMb": 175.3,
- "afterMb": 163.9
- },
- {
- "atMs": 305613,
- "beforeMb": 163.9,
- "afterMb": 163.9
- },
- {
- "atMs": 308911,
- "beforeMb": 174,
- "afterMb": 163.7
- }
- ],
- "tasks": [
- {
- "startMs": 5,
- "durationMs": 279,
- "byModule": [
- [
- "(native)",
- 122.3
- ],
- [
- "npm:next",
- 57.1
- ],
- [
- "node:internal/modules/cjs/loader",
- 41.7
- ],
- [
- "node:internal/bootstrap/realm",
- 24.7
- ],
- [
- "node:fs",
- 5.3
- ],
- [
- "node:internal/deps/undici/undici",
- 5.3
- ],
- [
- "node:internal/modules/package_json_reader",
- 4.5
- ],
- [
- "node:path",
- 4
- ],
- [
- "node:internal/streams/destroy",
- 1.5
- ],
- [
- "node:internal/deps/cjs-module-lexer/lexer",
- 1.5
- ],
- [
- "node:zlib",
- 1.5
- ],
- [
- "node:worker_threads",
- 1.5
- ],
- [
- "node:internal/net",
- 1.5
- ],
- [
- "node:events",
- 1.5
- ],
- [
- "node:internal/dns/callback_resolver",
- 1.3
- ]
- ],
- "byFunction": [
- [
- "readFileUtf8 (native)",
- 63.6
- ],
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 29.6
- ],
- [
- "compileForInternalLoader node:internal/bootstrap/realm",
- 24.7
- ],
- [
- "startServer npm:next",
- 22.8
- ],
- [
- "internalModuleStat (native)",
- 19.1
- ],
- [
- "dlopen (native)",
- 16.1
- ],
- [
- "(garbage collector) (native)",
- 10.5
- ],
- [
- "compileFunction (native)",
- 8.5
- ],
- [
- "(anonymous) npm:next",
- 8.2
- ],
- [
- "(anonymous) node:internal/modules/cjs/loader",
- 7.9
- ],
- [
- "stat node:internal/modules/cjs/loader",
- 4.2
- ],
- [
- "realpathSync node:fs",
- 4.1
- ],
- [
- "__require node:internal/deps/undici/undici",
- 4.1
- ],
- [
- "normalizeString node:path",
- 4
- ],
- [
- "__nccwpck_require__ npm:next",
- 3.9
- ]
- ],
- "byEntry": []
- },
- {
- "startMs": 339,
- "durationMs": 324,
- "byModule": [
- [
- "(native)",
- 87.6
- ],
- [
- "node:internal/modules/cjs/loader",
- 54.2
- ],
- [
- "npm:@prisma/client",
- 38
- ],
- [
- "wasm://wasm/00d1cb12",
- 36
- ],
- [
- "node:internal/modules/esm/utils",
- 26.7
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 24.9
- ],
- [
- "npm:next",
- 22.1
- ],
- [
- "npm:zod",
- 6
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/runtime-utils.ts",
- 3.1
- ],
- [
- "npm:@prisma/client-runtime-utils",
- 3
- ],
- [
- "node:fs",
- 2.9
- ],
- [
- "node:internal/bootstrap/realm",
- 2.8
- ],
- [
- "npm:pg-protocol",
- 1.8
- ],
- [
- "node:path",
- 1.5
- ],
- [
- "node:internal/deps/cjs-module-lexer/lexer",
- 1.5
- ]
- ],
- "byFunction": [
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 47.1
- ],
- [
- "(garbage collector) (native)",
- 40.1
- ],
- [
- "readFileUtf8 (native)",
- 27
- ],
- [
- "compileSourceTextModule node:internal/modules/esm/utils",
- 26.7
- ],
- [
- "#u npm:@prisma/client",
- 23.8
- ],
- [
- "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 23.3
- ],
- [
- "(anonymous) npm:next",
- 6
- ],
- [
- "(anonymous) npm:@prisma/client",
- 6
- ],
- [
- "(program) (native)",
- 5.2
- ],
- [
- "internalModuleStat (native)",
- 4.5
- ],
- [
- "wasm-function[1090] wasm://wasm/00d1cb12",
- 4.5
- ],
- [
- "wasm-function[2550] wasm://wasm/00d1cb12",
- 3.8
- ],
- [
- "readPackageJSON (native)",
- 3.1
- ],
- [
- "defineProp turbopack:///[turbopack]/shared/runtime/runtime-utils.ts",
- 3.1
- ],
- [
- "(anonymous) node:internal/modules/cjs/loader",
- 3
- ]
- ],
- "byEntry": [
- [
- "src/instrumentation.ts",
- 80.6
- ],
- [
- "src/lib/db-session.ts",
- 60.7
- ],
- [
- "src/lib/notifications/types.ts",
- 13.2
- ],
- [
- "src/lib/boot/readiness-summary.ts",
- 13.2
- ],
- [
- "src/lib/ai/local-host-allowlist.ts",
- 13.2
- ],
- [
- "src/generated/prisma/internal/class.ts",
- 7.3
- ],
- [
- "src/lib/logging/context.ts",
- 6
- ],
- [
- "src/lib/crypto.ts",
- 6
- ],
- [
- "src/lib/boot/key-check.ts",
- 6
- ],
- [
- "src/lib/validations/measurement.ts",
- 3
- ],
- [
- "src/lib/validations/entry-instant.ts",
- 1.5
- ],
- [
- "src/lib/medications/scheduling/hhmm.ts",
- 1.5
- ]
- ]
- },
- {
- "startMs": 698,
- "durationMs": 753,
- "byModule": [
- [
- "node:internal/modules/cjs/loader",
- 178.2
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 177.4
- ],
- [
- "(native)",
- 83.8
- ],
- [
- "npm:zod",
- 39.5
- ],
- [
- "npm:next",
- 34.6
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 33.9
- ],
- [
- "npm:mmdb-lib",
- 26.1
- ],
- [
- "node:fs",
- 23.5
- ],
- [
- "npm:yaml",
- 22.4
- ],
- [
- "src/lib/activity/goal-bounds.ts",
- 18.5
- ],
- [
- "node:internal/modules/esm/utils",
- 14.7
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/runtime-utils.ts",
- 9.3
- ],
- [
- "src/lib/daily/today-overview.ts",
- 4.6
- ],
- [
- "npm:undici",
- 4.5
- ],
- [
- "npm:semver",
- 4.2
- ]
- ],
- "byFunction": [
- [
- "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 177.4
- ],
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 172.5
- ],
- [
- "readFileUtf8 (native)",
- 37.3
- ],
- [
- "asyncModule turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 32.4
- ],
- [
- "(garbage collector) (native)",
- 29.3
- ],
- [
- "readFileSync node:fs",
- 22
- ],
- [
- "(anonymous) npm:mmdb-lib",
- 20.5
- ],
- [
- "(anonymous) src/lib/activity/goal-bounds.ts",
- 18.5
- ],
- [
- "compileSourceTextModule node:internal/modules/esm/utils",
- 14.7
- ],
- [
- "u npm:zod",
- 10.3
- ],
- [
- "a npm:next",
- 6.3
- ],
- [
- "(anonymous) npm:zod",
- 5.8
- ],
- [
- "module.exports.Object.defineProperty.value npm:mmdb-lib",
- 5.7
- ],
- [
- "(anonymous) npm:next",
- 5.4
- ],
- [
- "w src/lib/daily/today-overview.ts",
- 4.6
- ]
- ],
- "byEntry": [
- [
- "src/instrumentation.ts",
- 410
- ],
- [
- "src/lib/jobs/reminder/register-maintenance.ts",
- 279.5
- ],
- [
- "src/lib/sharing/acting-session.ts",
- 138.5
- ],
- [
- "src/lib/jobs/offhost-backup.ts",
- 137.1
- ],
- [
- "src/lib/integrations/oauth-refresh.ts",
- 136.3
- ],
- [
- "src/lib/jobs/morning-digest-refresh-shared.ts",
- 136.3
- ],
- [
- "src/lib/export/environment-backup.ts",
- 134.1
- ],
- [
- "src/lib/export/full-backup-payload.ts",
- 134.1
- ],
- [
- "src/app/api/health/route.ts",
- 127.5
- ],
- [
- "src/lib/insights/snapshot-hash.ts",
- 124.9
- ],
- [
- "src/lib/export/streamed-field.ts",
- 106.8
- ],
- [
- "src/lib/jobs/report-worker-error.ts",
- 93
- ],
- [
- "src/lib/labs/biomarker-resolve.ts",
- 93
- ],
- [
- "src/lib/jobs/reminder/google-health-sync.ts",
- 89.6
- ],
- [
- "src/lib/api-errors.ts",
- 77.5
- ]
- ]
- }
- ],
- "bootSelf": [
- [
- "(native)",
- 524.6
- ],
- [
- "node:internal/modules/cjs/loader",
- 274
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 202.3
- ],
- [
- "npm:next",
- 136.8
- ],
- [
- "wasm://wasm/00d1cb12",
- 108.1
- ],
- [
- "npm:@prisma/client",
- 76.1
- ],
- [
- "npm:pg",
- 56.5
- ],
- [
- "npm:zod",
- 45.5
- ],
- [
- "node:internal/modules/esm/utils",
- 41.4
- ],
- [
- "npm:luxon",
- 38.5
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 35.3
- ],
- [
- "node:fs",
- 31.7
- ],
- [
- "node:internal/bootstrap/realm",
- 28.9
- ],
- [
- "npm:pg-protocol",
- 27.7
- ],
- [
- "npm:mmdb-lib",
- 26.1
- ],
- [
- "npm:yaml",
- 22.4
- ],
- [
- "npm:pg-boss",
- 19.3
- ],
- [
- "src/lib/activity/goal-bounds.ts",
- 18.5
- ],
- [
- "node:internal/process/task_queues",
- 17.7
- ],
- [
- "node:internal/streams/readable",
- 15.5
- ],
- [
- "node:internal/async_hooks",
- 15.1
- ],
- [
- "npm:cron-parser",
- 14.6
- ],
- [
- "node:internal/timers",
- 12.8
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/runtime-utils.ts",
- 12.4
- ],
- [
- "node:buffer",
- 11.6
- ],
- [
- "node:internal/buffer",
- 10
- ],
- [
- "node:net",
- 9.8
- ],
- [
- "npm:pg-pool",
- 9.5
- ],
- [
- "node:internal/event_target",
- 7.5
- ],
- [
- "node:timers",
- 7.3
- ],
- [
- "node:path",
- 7
- ],
- [
- "node:internal/deps/undici/undici",
- 6.8
- ],
- [
- "npm:%40prisma",
- 5.8
- ],
- [
- "node:internal/streams/writable",
- 5.7
- ],
- [
- "node:internal/stream_base_commons",
- 5.2
- ],
- [
- "src/lib/daily/today-overview.ts",
- 4.6
- ],
- [
- "node:internal/modules/package_json_reader",
- 4.5
- ],
- [
- "npm:undici",
- 4.5
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 4.5
- ],
- [
- "node:events",
- 4.4
- ]
- ],
- "heap": {
- "probes": {
- "parsed locale catalogs": {
- "count": 32,
- "retainedMb": 14.1
- }
- },
- "totalMb": 172.3,
- "nodeCount": 1412114,
- "byType": [
- [
- "string",
- 79.1
- ],
- [
- "array",
- 27.6
- ],
- [
- "code",
- 18.6
- ],
- [
- "object",
- 16.7
- ],
- [
- "hidden",
- 9.3
- ],
- [
- "native",
- 8
- ],
- [
- "object shape",
- 5.8
- ],
- [
- "closure",
- 5
- ],
- [
- "concatenated string",
- 1.3
- ],
- [
- "number",
- 0.4
- ],
- [
- "regexp",
- 0.2
- ],
- [
- "sliced string",
- 0.2
- ],
- [
- "synthetic",
- 0
- ],
- [
- "symbol",
- 0
- ],
- [
- "bigint",
- 0
- ]
- ],
- "byConstructor": [
- [
- "array:(object elements)",
- 20.6,
- 24726
- ],
- [
- "object:Object",
- 11.9,
- 300844
- ],
- [
- "native:system / JSArrayBufferData",
- 7.5,
- 169
- ],
- [
- "array:(object properties)",
- 4.4,
- 6540
- ],
- [
- "array:",
- 2.7,
- 9578
- ],
- [
- "object:system / Context",
- 1.6,
- 28887
- ],
- [
- "closure:",
- 1.5,
- 27081
- ],
- [
- "object:Date1",
- 1,
- 10632
- ],
- [
- "closure:native_bind",
- 0.9,
- 20134
- ],
- [
- "object:Array",
- 0.6,
- 19550
- ],
- [
- "object:a",
- 0.4,
- 3880
- ],
- [
- "native:Node / histogram",
- 0.4,
- 1
- ],
- [
- "closure:get",
- 0.3,
- 4717
- ],
- [
- "object:Set",
- 0.2,
- 5102
- ],
- [
- "closure:set",
- 0.2,
- 3414
- ],
- [
- "object:d",
- 0.2,
- 3874
- ],
- [
- "object:Module",
- 0.1,
- 1907
- ],
- [
- "object:Promise",
- 0.1,
- 1590
- ],
- [
- "closure:n",
- 0.1,
- 1400
- ],
- [
- "closure:i",
- 0.1,
- 1390
- ],
- [
- "object:n",
- 0.1,
- 1900
- ],
- [
- "object:ZodOptional",
- 0.1,
- 236
- ],
- [
- "object:_TstNode",
- 0.1,
- 939
- ],
- [
- "native:Node / parser_buffer",
- 0.1,
- 1
- ],
- [
- "object:TCP",
- 0,
- 7
- ],
- [
- "closure:callbackTrampoline",
- 0,
- 1
- ],
- [
- "closure:promiseInitHook",
- 0,
- 1
- ],
- [
- "closure:promiseBeforeHook",
- 0,
- 1
- ],
- [
- "closure:promiseAfterHook",
- 0,
- 1
- ],
- [
- "object:global",
- 0,
- 4
- ]
- ],
- "largestStrings": [
- [
- 9.7,
- "module.exports=[425781,(e,a,i)=>{a.exports=JSON.parse('{\"common\":{\"listCapped\":\"Hier stehen die letz"
- ],
- [
- 9.7,
- "module.exports=[157893,(e,a,i)=>{a.exports=JSON.parse('{\"common\":{\"listCapped\":\"Hier stehen die letz"
- ],
- [
- 5.6,
- "module.exports=[17856,A=>{\"use strict\";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i,bgBlack:("
- ],
- [
- 5.6,
- "module.exports=[234494,A=>{\"use strict\";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i,bgBlack:"
- ],
- [
- 4.4,
- "const wasm = \"AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/fwBgBX9/f39/AGABfwF/YAZ/f3"
- ],
- [
- 4.4,
- "AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/fwBgBX9/f39/AGABfwF/YAZ/f39/f38AYAR/f39/"
- ],
- [
- 3.7,
- "module.exports=[799092,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),Objec"
- ],
- [
- 1.9,
- "lliADOAUngIEAACgKwAgBgAAoSsAIAcAAKErACAIAACiKwAgCQAAoysAIAoAAKQrACALAAClKwAgDAAApisAIA0AAKcrACARAACS"
- ],
- [
- 1.3,
- "(()=>{var e,t,r={\"./dist/build/webpack/alias/react-dom-server.js\"(e,t,r){\"use strict\";var n;function"
- ],
- [
- 1.2,
- "module.exports=[842865,(e,t,r)=>{let i=e.r(522734);t.exports=function(e){return e?/-----BEGIN ([A-Z\\"
- ],
- [
- 1.1,
- "module.exports=[834886,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0});var r"
- ],
- [
- 1.1,
- "// v1.4.34 web-freeze. Schema is locked additive-only until iOS launch // + v1.5.0 marker. See .plan"
- ],
- [
- 1.1,
- "module.exports=[971743,(A,e,t)=>{\"use strict\";let r=A.r(57328),{addAbortListener:s}=A.r(687769),{Rea"
- ],
- [
- 1,
- "module.exports=[835717,(A,e,t)=>{\"use strict\";let r=A.r(57328),{addAbortListener:s}=A.r(687769),{Rea"
- ],
- [
- 0.7,
- "module.exports=[594108,806176,674750,98746,t=>{\"use strict\";var i;let r;function n(t){return t}funct"
- ],
- [
- 0.7,
- "module.exports=[144056,t=>{\"use strict\";let i,n;function r(t){return t}function a(t){return t}functi"
- ],
- [
- 0.5,
- "'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); // these aren't reall"
- ],
- [
- 0.5,
- "import assert from 'node:assert'; import { indexKeysRaw, indexIncludeRaw, indexPredicateRaw, display"
- ],
- [
- 0.5,
- "module.exports=[21025,(e,t,r)=>{t.exports=e.x(\"next/dist/build/adapter/setup-node-env.external.js\",("
- ],
- [
- 0.4,
- "(()=>{var e,t,r={\"./dist/compiled/@edge-runtime/cookies/index.js\"(e){\"use strict\";var t=Object.defin"
- ],
- [
- 0.3,
- "(()=>{var e={975:(e,t,i)=>{const r=i(485);const{isObject:s,isArray:n}=i(648);const a=\"before\";const "
- ],
- [
- 0.3,
- "(()=>{var a={32:(a,e,i)=>{\"use strict\"; /*! * accepts * Copyright(c) 2014 Jonathan Ong * Copyrigh"
- ],
- [
- 0.2,
- "/* eslint-disable no-restricted-syntax -- everything above the v42 entry keeps its original SQL verb"
- ],
- [
- 0.2,
- "\"use strict\";var pu=Object.create;var xr=Object.defineProperty;var du=Object.getOwnPropertyDescripto"
- ],
- [
- 0.2,
- "\"use strict\"; Object.defineProperty(exports, \"__esModule\", { value: true }); 0 && (module.export"
- ]
- ],
- "topRetainers": [
- {
- "mb": 33.9,
- "name": "object:yr",
- "path": "[1] > [10] > 15 > extension > global > prisma > [1] > _engine"
- },
- {
- "mb": 10,
- "name": "object:Object",
- "path": "tX > onClose > onClose > context > system / Context > previous > system / Context > e > Context > __proto__ > c"
- },
- {
- "mb": 9.8,
- "name": "code:.next/server/chu",
- "path": "system / Context > timerListMap > Object > [120000] > TimersList > _idleNext > Timeout > _onTimeout > > shared > script"
- },
- {
- "mb": 9.7,
- "name": "code:.next/server/chu",
- "path": "Module > _cache > Object > .next/server/chunks/_09yxiz_._.js > Module > exports > Array > [1] > > shared > script"
- },
- {
- "mb": 7.4,
- "name": "object:system / Context",
- "path": "Node / PrincipalRealm > base_object_list > Node / BaseObjectList > [25] > Node / ModuleWrap > native_to_javascript > ModuleWrap > 2 > [4] > context"
- },
- {
- "mb": 5.6,
- "name": "code:.next/server/chu",
- "path": "global > prisma > [1] > t > _engineConfig > Object > compilerWasm > Object > getRuntime > getRuntime > shared > script"
- },
- {
- "mb": 5.6,
- "name": "code:.next/server/chu",
- "path": "Module > _cache > Object > .next/server/chunks/_0hfea7u._.js > Module > exports > Array > [1] > > shared > script"
- },
- {
- "mb": 4.4,
- "name": "object:Generator",
- "path": "[6] > principal_realm > Node / PrincipalRealm > base_object_list > Node / BaseObjectList > [26] > Node / ModuleWrap > native_to_javascript > ModuleWrap > 2 > [4]"
- },
- {
- "mb": 4.4,
- "name": "string:AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/",
- "path": "[1] > [21] > 59867"
- },
- {
- "mb": 3.7,
- "name": "code:.next/server/chu",
- "path": "[1] > [10] > 15 > extension > global > __healthlog_pgboss__ > PgBoss > _events > Object > error > > shared > script"
- },
- {
- "mb": 3.6,
- "name": "object:Client",
- "path": "Object > close > > context > system / Context > self > Connection > _events > Object > parseComplete > bound_this"
- }
- ],
- "sourceStringsMb": 44.2
- }
-}
diff --git a/scripts/bench/results/v1.45.0-before.json b/scripts/bench/results/v1.45.0-before.json
deleted file mode 100644
index 37365e6e9..000000000
--- a/scripts/bench/results/v1.45.0-before.json
+++ /dev/null
@@ -1,254 +0,0 @@
-{
- "meta": {
- "label": "v1.45.0-before",
- "commit": "1915f9152",
- "date": "2026-10-22T05:51:47.626Z",
- "node": "v22.23.2",
- "postgres": "16.15",
- "cpus": 14,
- "loadAverage": [
- 29.9,
- 21.9,
- 18.9
- ],
- "measurements": 1265561,
- "iterations": 20,
- "warmup": 3,
- "heapMb": 792,
- "idleStatementsPerSecond": 0,
- "batch5k": {
- "totalMs": 1730,
- "totalStatements": 534
- }
- },
- "results": [
- {
- "name": "/api/auth/me",
- "mode": "cold",
- "request": "GET /api/auth/me",
- "n": 20,
- "p50Ms": 9.2,
- "p95Ms": 11.6,
- "maxMs": 13.3,
- "statementsMedian": 19,
- "statementsMax": 19,
- "failures": 0
- },
- {
- "name": "/api/auth/me",
- "mode": "warm",
- "request": "GET /api/auth/me",
- "n": 20,
- "p50Ms": 8.6,
- "p95Ms": 10.5,
- "maxMs": 13,
- "statementsMedian": 19,
- "statementsMax": 19,
- "failures": 0
- },
- {
- "name": "dashboard summary",
- "mode": "cold",
- "request": "GET /api/dashboard/summary",
- "n": 20,
- "p50Ms": 170.2,
- "p95Ms": 220.5,
- "maxMs": 228.3,
- "statementsMedian": 31,
- "statementsMax": 34,
- "failures": 0
- },
- {
- "name": "dashboard summary",
- "mode": "warm",
- "request": "GET /api/dashboard/summary",
- "n": 20,
- "p50Ms": 6,
- "p95Ms": 6.7,
- "maxMs": 6.8,
- "statementsMedian": 7,
- "statementsMax": 7,
- "failures": 0
- },
- {
- "name": "dashboard snapshot",
- "mode": "cold",
- "request": "GET /api/dashboard/snapshot",
- "n": 20,
- "p50Ms": 1481.8,
- "p95Ms": 1526.2,
- "maxMs": 1530,
- "statementsMedian": 158,
- "statementsMax": 161,
- "failures": 0
- },
- {
- "name": "dashboard snapshot",
- "mode": "warm",
- "request": "GET /api/dashboard/snapshot",
- "n": 20,
- "p50Ms": 7,
- "p95Ms": 7.9,
- "maxMs": 10.2,
- "statementsMedian": 13,
- "statementsMax": 13,
- "failures": 0
- },
- {
- "name": "series pulse 30 d",
- "mode": "cold",
- "request": "GET /api/measurements/series?kind=pulse&days=30",
- "n": 20,
- "p50Ms": 24.4,
- "p95Ms": 30.8,
- "maxMs": 31.1,
- "statementsMedian": 8,
- "statementsMax": 8,
- "failures": 0
- },
- {
- "name": "series pulse 30 d",
- "mode": "warm",
- "request": "GET /api/measurements/series?kind=pulse&days=30",
- "n": 20,
- "p50Ms": 20.5,
- "p95Ms": 29.4,
- "maxMs": 30.2,
- "statementsMedian": 8,
- "statementsMax": 9,
- "failures": 0
- },
- {
- "name": "series pulse 365 d",
- "mode": "cold",
- "request": "GET /api/measurements/series?kind=pulse&days=365",
- "n": 20,
- "p50Ms": 184.1,
- "p95Ms": 192.6,
- "maxMs": 201.1,
- "statementsMedian": 8,
- "statementsMax": 8,
- "failures": 0
- },
- {
- "name": "series pulse 365 d",
- "mode": "warm",
- "request": "GET /api/measurements/series?kind=pulse&days=365",
- "n": 20,
- "p50Ms": 178.1,
- "p95Ms": 186.5,
- "maxMs": 187.6,
- "statementsMedian": 8,
- "statementsMax": 8,
- "failures": 0
- },
- {
- "name": "series pulse 3650 d",
- "mode": "cold",
- "request": "GET /api/measurements/series?kind=pulse&days=3650",
- "n": 20,
- "p50Ms": 1627,
- "p95Ms": 1664.5,
- "maxMs": 1675.7,
- "statementsMedian": 8,
- "statementsMax": 9,
- "failures": 0
- },
- {
- "name": "series pulse 3650 d",
- "mode": "warm",
- "request": "GET /api/measurements/series?kind=pulse&days=3650",
- "n": 20,
- "p50Ms": 2090.9,
- "p95Ms": 2435.4,
- "maxMs": 2529.7,
- "statementsMedian": 8,
- "statementsMax": 9,
- "failures": 0
- },
- {
- "name": "day view",
- "mode": "cold",
- "request": "GET /api/day/2026-10-21",
- "n": 20,
- "p50Ms": 76.7,
- "p95Ms": 93.6,
- "maxMs": 116.2,
- "statementsMedian": 49,
- "statementsMax": 49,
- "failures": 0
- },
- {
- "name": "day view",
- "mode": "warm",
- "request": "GET /api/day/2026-10-21",
- "n": 20,
- "p50Ms": 62.8,
- "p95Ms": 72,
- "maxMs": 73.5,
- "statementsMedian": 48,
- "statementsMax": 48,
- "failures": 0
- },
- {
- "name": "insights health status",
- "mode": "cold",
- "request": "GET /api/insights/health-status",
- "n": 20,
- "p50Ms": 244.7,
- "p95Ms": 401.2,
- "maxMs": 404.1,
- "statementsMedian": 68,
- "statementsMax": 69,
- "failures": 0
- },
- {
- "name": "insights health status",
- "mode": "warm",
- "request": "GET /api/insights/health-status",
- "n": 20,
- "p50Ms": 238.9,
- "p95Ms": 399.1,
- "maxMs": 476.6,
- "statementsMedian": 68,
- "statementsMax": 68,
- "failures": 0
- },
- {
- "name": "insights correlations",
- "mode": "cold",
- "request": "GET /api/insights/correlations",
- "n": 20,
- "p50Ms": 105.6,
- "p95Ms": 326.9,
- "maxMs": 349.7,
- "statementsMedian": 49,
- "statementsMax": 52,
- "failures": 0
- },
- {
- "name": "insights correlations",
- "mode": "warm",
- "request": "GET /api/insights/correlations",
- "n": 20,
- "p50Ms": 11.3,
- "p95Ms": 16.6,
- "maxMs": 20.3,
- "statementsMedian": 8,
- "statementsMax": 8,
- "failures": 0
- },
- {
- "name": "batch ingest 5k (per 500)",
- "mode": "write",
- "request": "POST /api/measurements/batch ×10",
- "n": 10,
- "p50Ms": 166.1,
- "p95Ms": 220.3,
- "maxMs": 220.3,
- "statementsMedian": 53,
- "statementsMax": 56,
- "failures": 0
- }
- ]
-}
diff --git a/scripts/bench/results/v1.45.0-before.md b/scripts/bench/results/v1.45.0-before.md
deleted file mode 100644
index 40bc7e19b..000000000
--- a/scripts/bench/results/v1.45.0-before.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Benchmark v1.45.0-before
-
-Commit 1915f9152 (the v1.44 runtime this branch starts from), 2026-10-22T05:51:47.626Z. 1265561 measurement rows on the bench account, 20 measured requests per row after 3 warm-up requests, one at a time. Server: production build, web process, heap ceiling 792 MB. Postgres 16.15, 14 cores, load average 29.9,21.9,18.9 at the end. Idle background: 0 statements/s.
-
-| Request | Mode | n | p50 ms | p95 ms | max ms | statements (median) | statements (max) |
-| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
-| /api/auth/me | cold | 20 | 9.2 | 11.6 | 13.3 | 19 | 19 |
-| /api/auth/me | warm | 20 | 8.6 | 10.5 | 13 | 19 | 19 |
-| dashboard summary | cold | 20 | 170.2 | 220.5 | 228.3 | 31 | 34 |
-| dashboard summary | warm | 20 | 6 | 6.7 | 6.8 | 7 | 7 |
-| dashboard snapshot | cold | 20 | 1481.8 | 1526.2 | 1530 | 158 | 161 |
-| dashboard snapshot | warm | 20 | 7 | 7.9 | 10.2 | 13 | 13 |
-| series pulse 30 d | cold | 20 | 24.4 | 30.8 | 31.1 | 8 | 8 |
-| series pulse 30 d | warm | 20 | 20.5 | 29.4 | 30.2 | 8 | 9 |
-| series pulse 365 d | cold | 20 | 184.1 | 192.6 | 201.1 | 8 | 8 |
-| series pulse 365 d | warm | 20 | 178.1 | 186.5 | 187.6 | 8 | 8 |
-| series pulse 3650 d | cold | 20 | 1627 | 1664.5 | 1675.7 | 8 | 9 |
-| series pulse 3650 d | warm | 20 | 2090.9 | 2435.4 | 2529.7 | 8 | 9 |
-| day view | cold | 20 | 76.7 | 93.6 | 116.2 | 49 | 49 |
-| day view | warm | 20 | 62.8 | 72 | 73.5 | 48 | 48 |
-| insights health status | cold | 20 | 244.7 | 401.2 | 404.1 | 68 | 69 |
-| insights health status | warm | 20 | 238.9 | 399.1 | 476.6 | 68 | 68 |
-| insights correlations | cold | 20 | 105.6 | 326.9 | 349.7 | 49 | 52 |
-| insights correlations | warm | 20 | 11.3 | 16.6 | 20.3 | 8 | 8 |
-| batch ingest 5k (per 500) | write | 10 | 166.1 | 220.3 | 220.3 | 53 | 56 |
diff --git a/scripts/bench/results/v1.45.0-boot-after.json b/scripts/bench/results/v1.45.0-boot-after.json
deleted file mode 100644
index cb435b3cb..000000000
--- a/scripts/bench/results/v1.45.0-boot-after.json
+++ /dev/null
@@ -1,1648 +0,0 @@
-{
- "meta": {
- "label": "v1.45.0-boot-after",
- "commit": "a859adb59",
- "date": "2026-10-22T04:49:50.419Z",
- "node": "v22.23.2",
- "cpus": 14,
- "loadAverage": [
- 8.6,
- 9.4,
- 10.7
- ],
- "heapMb": 792,
- "idleSeconds": 300,
- "healthyAtMs": 850,
- "readyAtMs": 3038,
- "stallMaxMs": 0
- },
- "bootLines": [
- "3038 ms: boot.steps {\"boot_queue_count\":122,\"boot_steps\":\"readiness_summary=2,key_check=187,time_zone_check=1,pulse_method_sync=25,monitors=3,worker_import=63,boss_start=266,load_worker_lifecycle=47,load_integration_sync=119,register_integration_sync=100,load_status=52,register_status=70,load_rollup=12,register_rollup=27,load_reminders=11,register_reminders=37,load_maintenance=50,register_maintena"
- ],
- "stalls": [],
- "heapLines": [
- {
- "atMs": 5524,
- "heap_used_mb": 166,
- "heap_total_mb": 192,
- "heap_limit_mb": 840,
- "heap_used_ratio": 0.197,
- "new_space_mb": 12,
- "old_space_mb": 91,
- "code_space_mb": 3,
- "large_object_space_mb": 53,
- "new_large_object_space_mb": 0,
- "code_large_object_space_mb": 0,
- "trusted_space_mb": 6,
- "read_only_space_mb": 0,
- "other_spaces_mb": 0,
- "rss_mb": 430,
- "external_mb": 12,
- "array_buffers_mb": 1,
- "active_requests": 0,
- "active_jobs": 0,
- "in_flight_dropped": 0,
- "uptime_s": 6
- },
- {
- "atMs": 305529,
- "heap_used_mb": 153,
- "heap_total_mb": 163,
- "heap_limit_mb": 840,
- "heap_used_ratio": 0.182,
- "new_space_mb": 1,
- "old_space_mb": 90,
- "code_space_mb": 4,
- "large_object_space_mb": 53,
- "new_large_object_space_mb": 0,
- "code_large_object_space_mb": 0,
- "trusted_space_mb": 5,
- "read_only_space_mb": 0,
- "other_spaces_mb": 0,
- "rss_mb": 599,
- "external_mb": 11,
- "array_buffers_mb": 0,
- "active_requests": 0,
- "active_jobs": 0,
- "in_flight_dropped": 0,
- "uptime_s": 306
- }
- ],
- "markCompacts": [
- {
- "atMs": 189,
- "beforeMb": 20.2,
- "afterMb": 17.1
- },
- {
- "atMs": 374,
- "beforeMb": 55.3,
- "afterMb": 52.6
- },
- {
- "atMs": 458,
- "beforeMb": 83.2,
- "afterMb": 81.1
- },
- {
- "atMs": 1036,
- "beforeMb": 148.4,
- "afterMb": 132.7
- },
- {
- "atMs": 16410,
- "beforeMb": 159.6,
- "afterMb": 153.2
- },
- {
- "atMs": 17036,
- "beforeMb": 153.4,
- "afterMb": 151
- },
- {
- "atMs": 61902,
- "beforeMb": 167.5,
- "afterMb": 157.5
- },
- {
- "atMs": 245952,
- "beforeMb": 183.4,
- "afterMb": 152.6
- },
- {
- "atMs": 303192,
- "beforeMb": 165.6,
- "afterMb": 151.8
- },
- {
- "atMs": 303207,
- "beforeMb": 151.8,
- "afterMb": 151.8
- }
- ],
- "tasks": [
- {
- "startMs": 6,
- "durationMs": 175,
- "byModule": [
- [
- "npm:next",
- 47.2
- ],
- [
- "node:internal/modules/cjs/loader",
- 34.8
- ],
- [
- "(native)",
- 34.5
- ],
- [
- "node:internal/bootstrap/realm",
- 25.7
- ],
- [
- "node:internal/deps/undici/undici",
- 8.8
- ],
- [
- "node:path",
- 6.3
- ],
- [
- "node:internal/modules/package_json_reader",
- 3
- ],
- [
- "node:internal/net",
- 3
- ],
- [
- "node:internal/modules/esm/resolve",
- 2.5
- ],
- [
- "node:fs",
- 1.5
- ],
- [
- "node:buffer",
- 1.5
- ],
- [
- "node:internal/deps/cjs-module-lexer/lexer",
- 1.5
- ],
- [
- "node:internal/util",
- 1.5
- ],
- [
- "node:events",
- 1.5
- ],
- [
- "node:internal/modules/helpers",
- 1.1
- ]
- ],
- "byFunction": [
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 32.2
- ],
- [
- "compileForInternalLoader node:internal/bootstrap/realm",
- 25.7
- ],
- [
- "startServer npm:next",
- 10.5
- ],
- [
- "internalModuleStat (native)",
- 9.5
- ],
- [
- "_export npm:next",
- 7.5
- ],
- [
- "__require node:internal/deps/undici/undici",
- 7.3
- ],
- [
- "(garbage collector) (native)",
- 5.8
- ],
- [
- "readFileUtf8 (native)",
- 5.8
- ],
- [
- "resolve node:path",
- 4.5
- ],
- [
- "__nccwpck_require__ npm:next",
- 4.5
- ],
- [
- "(anonymous) npm:next",
- 4.2
- ],
- [
- "compileFunction (native)",
- 3
- ],
- [
- "ZodType npm:next",
- 3
- ],
- [
- "dlopen (native)",
- 3
- ],
- [
- "(anonymous) node:internal/modules/cjs/loader",
- 2.6
- ]
- ],
- "byEntry": []
- },
- {
- "startMs": 203,
- "durationMs": 241,
- "byModule": [
- [
- "(native)",
- 52.7
- ],
- [
- "node:internal/modules/cjs/loader",
- 40.1
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 31.7
- ],
- [
- "wasm://wasm/00d1cb12",
- 28.7
- ],
- [
- "npm:@prisma/client",
- 28
- ],
- [
- "npm:next",
- 16.3
- ],
- [
- "node:internal/modules/esm/utils",
- 12
- ],
- [
- "node:fs",
- 4.5
- ],
- [
- "npm:zod",
- 3
- ],
- [
- "node:internal/modules/helpers",
- 3
- ],
- [
- "npm:pg",
- 3
- ],
- [
- "node:internal/fs/utils",
- 1.5
- ],
- [
- "src/lib/validations/entry-instant.ts",
- 1.5
- ],
- [
- "npm:undici",
- 1.5
- ],
- [
- "node:internal/bootstrap/realm",
- 1.5
- ]
- ],
- "byFunction": [
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 37.1
- ],
- [
- "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 31.7
- ],
- [
- "(garbage collector) (native)",
- 30.6
- ],
- [
- "#u npm:@prisma/client",
- 22.4
- ],
- [
- "compileSourceTextModule node:internal/modules/esm/utils",
- 12
- ],
- [
- "wasm-function[1454] wasm://wasm/00d1cb12",
- 6.1
- ],
- [
- "internalModuleStat (native)",
- 5.8
- ],
- [
- "readFileUtf8 (native)",
- 4.5
- ],
- [
- "js-to-wasm (native)",
- 4.5
- ],
- [
- "wasm-function[2550] wasm://wasm/00d1cb12",
- 4.5
- ],
- [
- "(anonymous) npm:next",
- 4.3
- ],
- [
- "readFileSync node:fs",
- 3
- ],
- [
- "toRealPath node:internal/modules/helpers",
- 3
- ],
- [
- "(anonymous) npm:pg",
- 3
- ],
- [
- "Module (native)",
- 3
- ]
- ],
- "byEntry": [
- [
- "src/instrumentation.ts",
- 80.8
- ],
- [
- "src/lib/db-session.ts",
- 49.5
- ],
- [
- "src/lib/boot/startup-state.ts",
- 25.6
- ],
- [
- "src/lib/boot/boot-steps.ts",
- 25.6
- ],
- [
- "src/lib/logging/background.ts",
- 25.6
- ],
- [
- "src/lib/process-type.ts",
- 25.5
- ],
- [
- "src/lib/logging/context.ts",
- 13.6
- ],
- [
- "src/lib/validations/measurement.ts",
- 12.1
- ],
- [
- "src/lib/logging/redact.ts",
- 10.5
- ],
- [
- "src/lib/validations/entry-instant.ts",
- 4.6
- ],
- [
- "src/generated/prisma/internal/class.ts",
- 3
- ],
- [
- "src/lib/medications/scheduling/hhmm.ts",
- 1.5
- ],
- [
- "src/lib/env.ts",
- 1.5
- ]
- ]
- },
- {
- "startMs": 501,
- "durationMs": 178,
- "byModule": [
- [
- "node:internal/modules/cjs/loader",
- 53.9
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 37.5
- ],
- [
- "npm:next",
- 18
- ],
- [
- "npm:zod",
- 12
- ],
- [
- "node:internal/modules/esm/utils",
- 10.5
- ],
- [
- "(native)",
- 8.7
- ],
- [
- "node:fs",
- 6
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 4.5
- ],
- [
- "npm:undici",
- 4.5
- ],
- [
- "node:internal/url",
- 1.8
- ],
- [
- "node:internal/modules/esm/resolve",
- 1.5
- ],
- [
- "node:internal/deps/cjs-module-lexer/lexer",
- 1.5
- ],
- [
- "node:internal/modules/esm/module_job",
- 1.5
- ],
- [
- "node:internal/deps/undici/undici",
- 1.5
- ],
- [
- "node:internal/promise_hooks",
- 1.5
- ]
- ],
- "byFunction": [
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 52.4
- ],
- [
- "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 37.5
- ],
- [
- "compileSourceTextModule node:internal/modules/esm/utils",
- 10.5
- ],
- [
- "(anonymous) npm:zod",
- 6.1
- ],
- [
- "(anonymous) npm:next",
- 6
- ],
- [
- "asyncModule turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 4.5
- ],
- [
- "readFileSync node:fs",
- 3
- ],
- [
- "(garbage collector) (native)",
- 3
- ],
- [
- "realpathSync node:fs",
- 2.9
- ],
- [
- "Z npm:zod",
- 2.9
- ],
- [
- "finalizeResolution node:internal/modules/esm/resolve",
- 1.5
- ],
- [
- "pathToFileURL node:internal/url",
- 1.5
- ],
- [
- "a npm:next",
- 1.5
- ],
- [
- "./dist/esm/server/app-render/manifests-singleton.js npm:next",
- 1.5
- ],
- [
- "a.d npm:next",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/app/api/health/route.ts",
- 61.4
- ],
- [
- "src/lib/api-errors.ts",
- 27
- ],
- [
- "src/lib/jobs/job-failures.ts",
- 14.9
- ],
- [
- "src/lib/jobs/offhost-backup.ts",
- 14.9
- ],
- [
- "src/lib/managed-profiles/handover-access.ts",
- 14.9
- ],
- [
- "src/lib/sharing/acting-session.ts",
- 14.9
- ],
- [
- "src/lib/db-session.ts",
- 13.4
- ],
- [
- "src/lib/export/environment-backup.ts",
- 13.4
- ],
- [
- "src/lib/export/full-backup-payload.ts",
- 13.4
- ],
- [
- "src/lib/logging/background.ts",
- 10.5
- ],
- [
- "src/lib/auth/secure-cookie.ts",
- 10.5
- ],
- [
- "src/lib/notifications/types.ts",
- 10.5
- ],
- [
- "src/lib/logging/redact.ts",
- 10.5
- ],
- [
- "src/lib/validations/measurement.ts",
- 9
- ],
- [
- "src/instrumentation.ts",
- 8.7
- ]
- ]
- },
- {
- "startMs": 833,
- "durationMs": 184,
- "byModule": [
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 38.9
- ],
- [
- "node:internal/modules/cjs/loader",
- 28.6
- ],
- [
- "(native)",
- 22.8
- ],
- [
- "src/lib/i18n/server-catalogs.ts",
- 15.1
- ],
- [
- "npm:yaml",
- 15
- ],
- [
- "node:fs",
- 12.1
- ],
- [
- "npm:zod",
- 9
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 5.9
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/runtime-utils.ts",
- 4.5
- ],
- [
- "npm:semver",
- 3
- ],
- [
- "npm:next",
- 2.3
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/runtime-base.ts",
- 1.5
- ],
- [
- "node:internal/bootstrap/realm",
- 1.5
- ],
- [
- "src/lib/labs/unit-conversion/registry.ts",
- 1.5
- ],
- [
- "src/lib/validations/user-prefs.ts",
- 1.5
- ]
- ],
- "byFunction": [
- [
- "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 38.9
- ],
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 28.6
- ],
- [
- "(garbage collector) (native)",
- 18.5
- ],
- [
- "(anonymous) src/lib/i18n/server-catalogs.ts",
- 15.1
- ],
- [
- "readFileSync node:fs",
- 12.1
- ],
- [
- "z npm:zod",
- 4.5
- ],
- [
- "asyncModule turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 4.5
- ],
- [
- "readFileUtf8 (native)",
- 4.3
- ],
- [
- "(anonymous) npm:zod",
- 3
- ],
- [
- "next npm:yaml",
- 3
- ],
- [
- "loadChunkAsync turbopack:///[turbopack]/nodejs/runtime/runtime-base.ts",
- 1.5
- ],
- [
- "compileForInternalLoader node:internal/bootstrap/realm",
- 1.5
- ],
- [
- "esmExport turbopack:///[turbopack]/shared/runtime/runtime-utils.ts",
- 1.5
- ],
- [
- "(anonymous) src/lib/labs/unit-conversion/registry.ts",
- 1.5
- ],
- [
- "(anonymous) src/lib/validations/user-prefs.ts",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/jobs/reminder-worker.ts",
- 129.1
- ],
- [
- "src/lib/jobs/reminder/google-health-sync.ts",
- 88.5
- ],
- [
- "src/lib/whoop/sync.ts",
- 79.5
- ],
- [
- "src/lib/whoop/sync-body.ts",
- 63
- ],
- [
- "src/lib/insights/bucket-series.ts",
- 60
- ],
- [
- "src/lib/whoop/oauth-descriptor.ts",
- 60
- ],
- [
- "src/lib/whoop/sync-core.ts",
- 60
- ],
- [
- "src/instrumentation.ts",
- 43.4
- ],
- [
- "src/lib/import/unzip-export-xml.ts",
- 31.4
- ],
- [
- "src/lib/jobs/integration-status-null-probe.ts",
- 31.4
- ],
- [
- "src/lib/ai/coach/memory/propose-plan.ts",
- 30.1
- ],
- [
- "src/lib/ai/coach/refusal.ts",
- 27.1
- ],
- [
- "src/lib/ai/coach/outbound-guard.ts",
- 25.6
- ],
- [
- "src/lib/i18n/server-catalogs.ts",
- 24.1
- ],
- [
- "src/lib/insights/snapshot-hash.ts",
- 23.9
- ]
- ]
- },
- {
- "startMs": 1099,
- "durationMs": 54,
- "byModule": [
- [
- "node:internal/modules/cjs/loader",
- 23.9
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 10.6
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 5.8
- ],
- [
- "src/lib/daily/today-overview.ts",
- 4.5
- ],
- [
- "src/lib/jobs/reminder-worker.ts",
- 3
- ],
- [
- "node:fs",
- 3
- ],
- [
- "npm:pg-boss",
- 1.5
- ],
- [
- "npm:zod",
- 1.5
- ]
- ],
- "byFunction": [
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 23.9
- ],
- [
- "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 10.6
- ],
- [
- "asyncModule turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 5.8
- ],
- [
- "w src/lib/daily/today-overview.ts",
- 4.5
- ],
- [
- "readFileSync node:fs",
- 3
- ],
- [
- "work npm:pg-boss",
- 1.5
- ],
- [
- "(anonymous) src/lib/jobs/reminder-worker.ts",
- 1.5
- ],
- [
- "d9 npm:zod",
- 1.5
- ],
- [
- "v src/lib/jobs/reminder-worker.ts",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/jobs/reminder-worker.ts",
- 52.3
- ],
- [
- "src/lib/jobs/reminder/insights-handlers.ts",
- 22.3
- ],
- [
- "src/lib/ai/prompts/arrival-reaction.ts",
- 12
- ],
- [
- "src/lib/daily/score-steady.ts",
- 10.5
- ],
- [
- "src/lib/dashboard/bands.ts",
- 6
- ],
- [
- "src/lib/jobs/data-arrival.ts",
- 4.5
- ],
- [
- "src/lib/dashboard/snapshot-read.ts",
- 4.5
- ],
- [
- "src/lib/daily/today-overview.ts",
- 4.5
- ],
- [
- "src/lib/daily/priority-item-key.ts",
- 4.5
- ],
- [
- "src/lib/ai/prompts/status-batch.ts",
- 3
- ],
- [
- "src/lib/jobs/reminder/registrar-shared.ts",
- 1.5
- ],
- [
- "src/lib/jobs/reminder/register-integration-sync.ts",
- 1.5
- ],
- [
- "src/lib/ai/prompts/base-system.ts",
- 1.5
- ],
- [
- "src/lib/insights/derived/derived-assessment-ai.ts",
- 1.5
- ],
- [
- "src/lib/measurements/list/sleep-nights.ts",
- 1.5
- ]
- ]
- },
- {
- "startMs": 1309,
- "durationMs": 51,
- "byModule": [
- [
- "node:internal/modules/cjs/loader",
- 28
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 6
- ],
- [
- "node:fs",
- 4.5
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 3
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/runtime-base.ts",
- 1.5
- ],
- [
- "src/lib/jobs/failure-condition.ts",
- 1.5
- ],
- [
- "src/lib/labs/parse-reference-range.ts",
- 1.5
- ],
- [
- "npm:zod",
- 1.5
- ],
- [
- "node:internal/linkedlist",
- 1.5
- ],
- [
- "(native)",
- 0.9
- ],
- [
- "node:buffer",
- 0.6
- ]
- ],
- "byFunction": [
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 28
- ],
- [
- "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 6
- ],
- [
- "readFileSync node:fs",
- 4.5
- ],
- [
- "asyncModule turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 3
- ],
- [
- "loadChunkAsync turbopack:///[turbopack]/nodejs/runtime/runtime-base.ts",
- 1.5
- ],
- [
- "(anonymous) src/lib/jobs/failure-condition.ts",
- 1.5
- ],
- [
- "r src/lib/labs/parse-reference-range.ts",
- 1.5
- ],
- [
- "u npm:zod",
- 1.5
- ],
- [
- "isEmpty node:internal/linkedlist",
- 1.5
- ],
- [
- "(garbage collector) (native)",
- 0.9
- ],
- [
- "getEncodingOps node:buffer",
- 0.6
- ]
- ],
- "byEntry": [
- [
- "src/lib/jobs/reminder-worker.ts",
- 48.2
- ],
- [
- "src/lib/jobs/reminder/ops-handlers.ts",
- 14.1
- ],
- [
- "src/lib/medications/ingredients/label-extract.ts",
- 3
- ],
- [
- "src/lib/export/restore-backup.ts",
- 2.1
- ],
- [
- "src/lib/jobs/failure-condition.ts",
- 1.5
- ],
- [
- "src/lib/documents/ai-runs/summary-run.ts",
- 1.5
- ],
- [
- "src/lib/documents/auto-stage-labs.ts",
- 1.5
- ],
- [
- "src/lib/documents/ai-runs/store.ts",
- 1.5
- ],
- [
- "src/lib/labs/parse-reference-range.ts",
- 1.5
- ],
- [
- "src/lib/labs/ocr-upload.ts",
- 1.5
- ],
- [
- "src/lib/jobs/document-content-index-backfill.ts",
- 1.5
- ],
- [
- "src/lib/labs/ocr-extract.ts",
- 1.5
- ],
- [
- "src/lib/dawarich/sync.ts",
- 1.5
- ],
- [
- "src/lib/validations/dawarich.ts",
- 1.5
- ],
- [
- "src/lib/jobs/auth-ledger-cleanup.ts",
- 1.5
- ]
- ]
- }
- ],
- "bootSelf": [
- [
- "(native)",
- 230.8
- ],
- [
- "node:internal/modules/cjs/loader",
- 223.6
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 124.7
- ],
- [
- "npm:next",
- 98.8
- ],
- [
- "wasm://wasm/00d1cb12",
- 62.6
- ],
- [
- "npm:@prisma/client",
- 56.2
- ],
- [
- "node:fs",
- 34.6
- ],
- [
- "node:internal/bootstrap/realm",
- 28.7
- ],
- [
- "npm:pg",
- 28.3
- ],
- [
- "npm:zod",
- 27
- ],
- [
- "npm:pg-boss",
- 24.6
- ],
- [
- "node:internal/modules/esm/utils",
- 22.5
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 20.6
- ],
- [
- "npm:luxon",
- 17
- ],
- [
- "src/lib/i18n/server-catalogs.ts",
- 15.1
- ],
- [
- "npm:yaml",
- 15
- ],
- [
- "npm:pg-pool",
- 13
- ],
- [
- "node:internal/deps/undici/undici",
- 10.4
- ],
- [
- "node:path",
- 7.8
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/runtime-utils.ts",
- 7.5
- ],
- [
- "node:internal/async_hooks",
- 7.5
- ],
- [
- "node:internal/process/task_queues",
- 7.3
- ],
- [
- "node:internal/modules/package_json_reader",
- 6
- ],
- [
- "npm:undici",
- 6
- ],
- [
- "npm:pg-protocol",
- 6
- ],
- [
- "npm:cron-parser",
- 6
- ],
- [
- "node:internal/stream_base_commons",
- 5.7
- ],
- [
- "node:events",
- 5.5
- ],
- [
- "node:internal/timers",
- 5.5
- ],
- [
- "node:internal/streams/writable",
- 5.3
- ],
- [
- "node:internal/async_local_storage/async_hooks",
- 5.3
- ],
- [
- "node:buffer",
- 5.2
- ],
- [
- "node:internal/util",
- 4.5
- ],
- [
- "npm:%40prisma",
- 4.5
- ],
- [
- "node:internal/crypto/random",
- 4.5
- ],
- [
- "src/lib/daily/today-overview.ts",
- 4.5
- ],
- [
- "node:internal/modules/helpers",
- 4.2
- ],
- [
- "node:internal/modules/esm/resolve",
- 4
- ],
- [
- "node:internal/deps/cjs-module-lexer/lexer",
- 3
- ],
- [
- "node:internal/net",
- 3
- ]
- ],
- "heap": {
- "probes": {
- "parsed locale catalogs": {
- "count": 8,
- "retainedMb": 7
- }
- },
- "totalMb": 160.1,
- "nodeCount": 1275329,
- "byType": [
- [
- "string",
- 68.7
- ],
- [
- "array",
- 26.4
- ],
- [
- "code",
- 21.1
- ],
- [
- "object",
- 14.1
- ],
- [
- "hidden",
- 8.9
- ],
- [
- "native",
- 8
- ],
- [
- "object shape",
- 5.9
- ],
- [
- "closure",
- 5.2
- ],
- [
- "concatenated string",
- 1.3
- ],
- [
- "regexp",
- 0.2
- ],
- [
- "sliced string",
- 0.2
- ],
- [
- "number",
- 0.1
- ],
- [
- "synthetic",
- 0
- ],
- [
- "symbol",
- 0
- ],
- [
- "bigint",
- 0
- ]
- ],
- "byConstructor": [
- [
- "array:(object elements)",
- 20.4,
- 23797
- ],
- [
- "object:Object",
- 10.2,
- 274231
- ],
- [
- "native:system / JSArrayBufferData",
- 7.6,
- 171
- ],
- [
- "array:(object properties)",
- 3.6,
- 6512
- ],
- [
- "array:",
- 2.4,
- 10502
- ],
- [
- "closure:",
- 1.6,
- 29042
- ],
- [
- "object:system / Context",
- 1.6,
- 28873
- ],
- [
- "closure:native_bind",
- 1,
- 20782
- ],
- [
- "object:Array",
- 0.6,
- 19679
- ],
- [
- "object:a",
- 0.4,
- 4438
- ],
- [
- "native:Node / histogram",
- 0.4,
- 1
- ],
- [
- "closure:get",
- 0.3,
- 4736
- ],
- [
- "object:d",
- 0.3,
- 4425
- ],
- [
- "object:Set",
- 0.2,
- 5673
- ],
- [
- "closure:set",
- 0.2,
- 3432
- ],
- [
- "object:Module",
- 0.2,
- 1958
- ],
- [
- "closure:i",
- 0.1,
- 1581
- ],
- [
- "closure:n",
- 0.1,
- 1572
- ],
- [
- "object:Promise",
- 0.1,
- 1277
- ],
- [
- "object:n",
- 0.1,
- 1881
- ],
- [
- "object:ZodOptional",
- 0.1,
- 236
- ],
- [
- "object:_TstNode",
- 0.1,
- 939
- ],
- [
- "native:Node / parser_buffer",
- 0.1,
- 1
- ],
- [
- "object:TCP",
- 0,
- 8
- ],
- [
- "closure:callbackTrampoline",
- 0,
- 1
- ],
- [
- "closure:promiseInitHook",
- 0,
- 1
- ],
- [
- "closure:promiseBeforeHook",
- 0,
- 1
- ],
- [
- "closure:promiseAfterHook",
- 0,
- 1
- ],
- [
- "object:Signal",
- 0,
- 4
- ],
- [
- "object:HTTPParser",
- 0,
- 2
- ]
- ],
- "largestStrings": [
- [
- 5.6,
- "module.exports=[234494,A=>{\"use strict\";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i,bgBlack:"
- ],
- [
- 5.6,
- "module.exports=[17856,A=>{\"use strict\";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i,bgBlack:("
- ],
- [
- 5.6,
- "module.exports=[17856,A=>{\"use strict\";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i,bgBlack:("
- ],
- [
- 4.4,
- "const wasm = \"AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/fwBgBX9/f39/AGABfwF/YAZ/f3"
- ],
- [
- 4.4,
- "AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/fwBgBX9/f39/AGABfwF/YAZ/f39/f38AYAR/f39/"
- ],
- [
- 1.9,
- "lliADOAUngIEAACgKwAgBgAAoSsAIAcAAKErACAIAACiKwAgCQAAoysAIAoAAKQrACALAAClKwAgDAAApisAIA0AAKcrACARAACS"
- ],
- [
- 1.3,
- "(()=>{var e,t,r={\"./dist/build/webpack/alias/react-dom-server.js\"(e,t,r){\"use strict\";var n;function"
- ],
- [
- 1.2,
- "module.exports=[842865,(e,t,r)=>{let i=e.r(522734);t.exports=function(e){return e?/-----BEGIN ([A-Z\\"
- ],
- [
- 1.2,
- "module.exports=[842865,(e,t,r)=>{let i=e.r(522734);t.exports=function(e){return e?/-----BEGIN ([A-Z\\"
- ],
- [
- 1.1,
- "module.exports=[799092,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),Objec"
- ],
- [
- 1.1,
- "// v1.4.34 web-freeze. Schema is locked additive-only until iOS launch // + v1.5.0 marker. See .plan"
- ],
- [
- 1.1,
- "module.exports=[971743,(A,e,t)=>{\"use strict\";let r=A.r(57328),{addAbortListener:s}=A.r(687769),{Rea"
- ],
- [
- 1,
- "module.exports=[835717,(A,e,t)=>{\"use strict\";let r=A.r(57328),{addAbortListener:s}=A.r(687769),{Rea"
- ],
- [
- 0.8,
- "module.exports=[767189,e=>e.a(async(t,a)=>{try{var n=e.i(64105),r=e.i(563616),i=t([n]);[n]=i.then?(a"
- ],
- [
- 0.7,
- "module.exports=[594108,806176,674750,98746,t=>{\"use strict\";var i;let r;function n(t){return t}funct"
- ],
- [
- 0.7,
- "module.exports=[144056,t=>{\"use strict\";let i,n;function r(t){return t}function a(t){return t}functi"
- ],
- [
- 0.6,
- "module.exports=[992657,e=>e.a(async(t,a)=>{try{var r=e.i(851258),n=t([r]);async function i(e,t,a,n){"
- ],
- [
- 0.6,
- "module.exports=[388046,e=>{\"use strict\";class t{value;next;constructor(e){this.value=e}}class n{#e;#"
- ],
- [
- 0.6,
- "module.exports=[388046,e=>{\"use strict\";class t{value;next;constructor(e){this.value=e}}class n{#e;#"
- ],
- [
- 0.5,
- "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def"
- ],
- [
- 0.5,
- "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def"
- ],
- [
- 0.5,
- "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def"
- ],
- [
- 0.5,
- "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def"
- ],
- [
- 0.5,
- "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def"
- ],
- [
- 0.5,
- "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def"
- ]
- ],
- "topRetainers": [
- {
- "mb": 33.9,
- "name": "object:t",
- "path": "[1] > [10] > 60 > extension > global > prisma > [1]"
- },
- {
- "mb": 7.9,
- "name": "object:global",
- "path": "[1] > [10] > 60 > extension"
- },
- {
- "mb": 7.4,
- "name": "object:system / Context",
- "path": "Node / PrincipalRealm > base_object_list > Node / BaseObjectList > [25] > Node / ModuleWrap > native_to_javascript > ModuleWrap > 2 > [4] > context"
- },
- {
- "mb": 5.6,
- "name": "object:Module",
- "path": "[1] > [10] > 97 > process > nextTick > patchedNextTick > context > system / Context > require > require > cache > .next/server/chunks/_1mg8fms._.js"
- },
- {
- "mb": 5.6,
- "name": "code:.next/server/chu",
- "path": "require > cache > Object > .next/server/chunks/_1kwfnjg._.js > Module > exports > Array > [1] > > shared > script"
- },
- {
- "mb": 5.6,
- "name": "code:.next/server/chu",
- "path": "global > prisma > [1] > t > _engineConfig > Object > compilerWasm > Object > getRuntime > getRuntime > shared > script"
- },
- {
- "mb": 4.4,
- "name": "object:Generator",
- "path": "[6] > principal_realm > Node / PrincipalRealm > base_object_list > Node / BaseObjectList > [26] > Node / ModuleWrap > native_to_javascript > ModuleWrap > 2 > [4]"
- },
- {
- "mb": 4.4,
- "name": "string:AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/",
- "path": "[1] > [21] > 62203"
- },
- {
- "mb": 3.3,
- "name": "object:Object",
- "path": "system / Context > require > require > cache > Object > .next/server/chunks/[turbopack]_runtime.js > Module > exports > > context > moduleCache"
- }
- ],
- "sourceStringsMb": 39.8
- }
-}
diff --git a/scripts/bench/results/v1.45.0-boot-after.md b/scripts/bench/results/v1.45.0-boot-after.md
deleted file mode 100644
index 4ec08b032..000000000
--- a/scripts/bench/results/v1.45.0-boot-after.md
+++ /dev/null
@@ -1,298 +0,0 @@
-# Boot profile v1.45.0-boot-after
-
-Commit a859adb59, 2026-10-22T04:49:50.419Z. Production build, every role, heap ceiling 792 MB, 300 s idle. 14 cores, load average 8.6,9.4,10.7 at the end.
-
-- first answer from /api/health: 850 ms; background role ready: 3038 ms
-- event-loop stalls logged: 0, max 0 ms
-
-## Stalls
-
-
-## Heap samples
-
-| at s | used MB | old | large obj | code | external | array buffers | rss |
-| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
-| 6 | 166 | 91 | 53 | 3 | 12 | 1 | 430 |
-| 306 | 153 | 90 | 53 | 4 | 11 | 0 | 599 |
-
-## Full collections (mark-compact)
-
-10 mark-compacts during the run. Used heap before the collection, and what survived it (the live heap):
-
-| at s | before MB | after MB |
-| ---: | ---: | ---: |
-| 0 | 20.2 | 17.1 |
-| 0 | 55.3 | 52.6 |
-| 0 | 83.2 | 81.1 |
-| 1 | 148.4 | 132.7 |
-| 16 | 159.6 | 153.2 |
-| 17 | 153.4 | 151 |
-| 62 | 167.5 | 157.5 |
-| 246 | 183.4 | 152.6 |
-| 303 | 165.6 | 151.8 |
-| 303 | 151.8 | 151.8 |
-
-## Main-thread busy stretches over 50 ms without an idle sample, first two minutes
-
-### at 6 ms, 175 ms
-
-| self time by module | ms |
-| --- | ---: |
-| npm:next | 47.2 |
-| node:internal/modules/cjs/loader | 34.8 |
-| (native) | 34.5 |
-| node:internal/bootstrap/realm | 25.7 |
-| node:internal/deps/undici/undici | 8.8 |
-| node:path | 6.3 |
-| node:internal/modules/package_json_reader | 3 |
-| node:internal/net | 3 |
-| node:internal/modules/esm/resolve | 2.5 |
-| node:fs | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-
-### at 203 ms, 241 ms
-
-| self time by module | ms |
-| --- | ---: |
-| (native) | 52.7 |
-| node:internal/modules/cjs/loader | 40.1 |
-| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 31.7 |
-| wasm://wasm/00d1cb12 | 28.7 |
-| npm:@prisma/client | 28 |
-| npm:next | 16.3 |
-| node:internal/modules/esm/utils | 12 |
-| node:fs | 4.5 |
-| npm:zod | 3 |
-| node:internal/modules/helpers | 3 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/instrumentation.ts | 80.8 |
-| src/lib/db-session.ts | 49.5 |
-| src/lib/boot/startup-state.ts | 25.6 |
-| src/lib/boot/boot-steps.ts | 25.6 |
-| src/lib/logging/background.ts | 25.6 |
-| src/lib/process-type.ts | 25.5 |
-| src/lib/logging/context.ts | 13.6 |
-| src/lib/validations/measurement.ts | 12.1 |
-| src/lib/logging/redact.ts | 10.5 |
-| src/lib/validations/entry-instant.ts | 4.6 |
-
-### at 501 ms, 178 ms
-
-| self time by module | ms |
-| --- | ---: |
-| node:internal/modules/cjs/loader | 53.9 |
-| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 37.5 |
-| npm:next | 18 |
-| npm:zod | 12 |
-| node:internal/modules/esm/utils | 10.5 |
-| (native) | 8.7 |
-| node:fs | 6 |
-| turbopack:///[turbopack]/shared/runtime/async-module.ts | 4.5 |
-| npm:undici | 4.5 |
-| node:internal/url | 1.8 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/app/api/health/route.ts | 61.4 |
-| src/lib/api-errors.ts | 27 |
-| src/lib/jobs/job-failures.ts | 14.9 |
-| src/lib/jobs/offhost-backup.ts | 14.9 |
-| src/lib/managed-profiles/handover-access.ts | 14.9 |
-| src/lib/sharing/acting-session.ts | 14.9 |
-| src/lib/db-session.ts | 13.4 |
-| src/lib/export/environment-backup.ts | 13.4 |
-| src/lib/export/full-backup-payload.ts | 13.4 |
-| src/lib/logging/background.ts | 10.5 |
-
-### at 833 ms, 184 ms
-
-| self time by module | ms |
-| --- | ---: |
-| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 38.9 |
-| node:internal/modules/cjs/loader | 28.6 |
-| (native) | 22.8 |
-| src/lib/i18n/server-catalogs.ts | 15.1 |
-| npm:yaml | 15 |
-| node:fs | 12.1 |
-| npm:zod | 9 |
-| turbopack:///[turbopack]/shared/runtime/async-module.ts | 5.9 |
-| turbopack:///[turbopack]/shared/runtime/runtime-utils.ts | 4.5 |
-| npm:semver | 3 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/jobs/reminder-worker.ts | 129.1 |
-| src/lib/jobs/reminder/google-health-sync.ts | 88.5 |
-| src/lib/whoop/sync.ts | 79.5 |
-| src/lib/whoop/sync-body.ts | 63 |
-| src/lib/insights/bucket-series.ts | 60 |
-| src/lib/whoop/oauth-descriptor.ts | 60 |
-| src/lib/whoop/sync-core.ts | 60 |
-| src/instrumentation.ts | 43.4 |
-| src/lib/import/unzip-export-xml.ts | 31.4 |
-| src/lib/jobs/integration-status-null-probe.ts | 31.4 |
-
-### at 1099 ms, 54 ms
-
-| self time by module | ms |
-| --- | ---: |
-| node:internal/modules/cjs/loader | 23.9 |
-| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 10.6 |
-| turbopack:///[turbopack]/shared/runtime/async-module.ts | 5.8 |
-| src/lib/daily/today-overview.ts | 4.5 |
-| src/lib/jobs/reminder-worker.ts | 3 |
-| node:fs | 3 |
-| npm:pg-boss | 1.5 |
-| npm:zod | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/jobs/reminder-worker.ts | 52.3 |
-| src/lib/jobs/reminder/insights-handlers.ts | 22.3 |
-| src/lib/ai/prompts/arrival-reaction.ts | 12 |
-| src/lib/daily/score-steady.ts | 10.5 |
-| src/lib/dashboard/bands.ts | 6 |
-| src/lib/jobs/data-arrival.ts | 4.5 |
-| src/lib/dashboard/snapshot-read.ts | 4.5 |
-| src/lib/daily/today-overview.ts | 4.5 |
-| src/lib/daily/priority-item-key.ts | 4.5 |
-| src/lib/ai/prompts/status-batch.ts | 3 |
-
-### at 1309 ms, 51 ms
-
-| self time by module | ms |
-| --- | ---: |
-| node:internal/modules/cjs/loader | 28 |
-| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 6 |
-| node:fs | 4.5 |
-| turbopack:///[turbopack]/shared/runtime/async-module.ts | 3 |
-| turbopack:///[turbopack]/nodejs/runtime/runtime-base.ts | 1.5 |
-| src/lib/jobs/failure-condition.ts | 1.5 |
-| src/lib/labs/parse-reference-range.ts | 1.5 |
-| npm:zod | 1.5 |
-| node:internal/linkedlist | 1.5 |
-| (native) | 0.9 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/jobs/reminder-worker.ts | 48.2 |
-| src/lib/jobs/reminder/ops-handlers.ts | 14.1 |
-| src/lib/medications/ingredients/label-extract.ts | 3 |
-| src/lib/export/restore-backup.ts | 2.1 |
-| src/lib/jobs/failure-condition.ts | 1.5 |
-| src/lib/documents/ai-runs/summary-run.ts | 1.5 |
-| src/lib/documents/auto-stage-labs.ts | 1.5 |
-| src/lib/documents/ai-runs/store.ts | 1.5 |
-| src/lib/labs/parse-reference-range.ts | 1.5 |
-| src/lib/labs/ocr-upload.ts | 1.5 |
-
-## Self time by module until the worker is ready
-
-| module | ms |
-| --- | ---: |
-| (native) | 230.8 |
-| node:internal/modules/cjs/loader | 223.6 |
-| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 124.7 |
-| npm:next | 98.8 |
-| wasm://wasm/00d1cb12 | 62.6 |
-| npm:@prisma/client | 56.2 |
-| node:fs | 34.6 |
-| node:internal/bootstrap/realm | 28.7 |
-| npm:pg | 28.3 |
-| npm:zod | 27 |
-| npm:pg-boss | 24.6 |
-| node:internal/modules/esm/utils | 22.5 |
-| turbopack:///[turbopack]/shared/runtime/async-module.ts | 20.6 |
-| npm:luxon | 17 |
-| src/lib/i18n/server-catalogs.ts | 15.1 |
-| npm:yaml | 15 |
-| npm:pg-pool | 13 |
-| node:internal/deps/undici/undici | 10.4 |
-| node:path | 7.8 |
-| turbopack:///[turbopack]/shared/runtime/runtime-utils.ts | 7.5 |
-| node:internal/async_hooks | 7.5 |
-| node:internal/process/task_queues | 7.3 |
-| node:internal/modules/package_json_reader | 6 |
-| npm:undici | 6 |
-| npm:pg-protocol | 6 |
-
-## Heap snapshot after 300 s idle (live heap after a full GC)
-
-160.1 MB in 1275329 nodes; module source strings over 50 KB: 39.8 MB.
-
-- parsed locale catalogs: 8 objects, 7 MB retained
-
-| node type | MB |
-| --- | ---: |
-| string | 68.7 |
-| array | 26.4 |
-| code | 21.1 |
-| object | 14.1 |
-| hidden | 8.9 |
-| native | 8 |
-| object shape | 5.9 |
-| closure | 5.2 |
-| concatenated string | 1.3 |
-| regexp | 0.2 |
-| sliced string | 0.2 |
-| number | 0.1 |
-| synthetic | 0 |
-| symbol | 0 |
-| bigint | 0 |
-
-| constructor | MB | count |
-| --- | ---: | ---: |
-| array:(object elements) | 20.4 | 23797 |
-| object:Object | 10.2 | 274231 |
-| native:system / JSArrayBufferData | 7.6 | 171 |
-| array:(object properties) | 3.6 | 6512 |
-| array: | 2.4 | 10502 |
-| closure: | 1.6 | 29042 |
-| object:system / Context | 1.6 | 28873 |
-| closure:native_bind | 1 | 20782 |
-| object:Array | 0.6 | 19679 |
-| object:a | 0.4 | 4438 |
-| native:Node / histogram | 0.4 | 1 |
-| closure:get | 0.3 | 4736 |
-| object:d | 0.3 | 4425 |
-| object:Set | 0.2 | 5673 |
-| closure:set | 0.2 | 3432 |
-| object:Module | 0.2 | 1958 |
-| closure:i | 0.1 | 1581 |
-| closure:n | 0.1 | 1572 |
-| object:Promise | 0.1 | 1277 |
-| object:n | 0.1 | 1881 |
-
-| largest retained subtrees | MB | retaining path |
-| --- | ---: | --- |
-| object:t | 33.9 | [1] > [10] > 60 > extension > global > prisma > [1] |
-| object:global | 7.9 | [1] > [10] > 60 > extension |
-| object:system / Context | 7.4 | Node / PrincipalRealm > base_object_list > Node / BaseObjectList > [25] > Node / ModuleWrap > native_to_javascript > ModuleWrap > 2 > [4] > context |
-| object:Module | 5.6 | [1] > [10] > 97 > process > nextTick > patchedNextTick > context > system / Context > require > require > cache > .n |
-| code:.next/server/chu | 5.6 | require > cache > Object > .next/server/chunks/_1kwfnjg._.js > Module > exports > Array > [1] > > shared > script |
-| code:.next/server/chu | 5.6 | global > prisma > [1] > t > _engineConfig > Object > compilerWasm > Object > getRuntime > getRuntime > shared > script |
-| object:Generator | 4.4 | [6] > principal_realm > Node / PrincipalRealm > base_object_list > Node / BaseObjectList > [26] > Node / ModuleWrap > native_to_javascript > ModuleWrap > 2 > [4 |
-| string:AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/ | 4.4 | [1] > [21] > 62203 |
-| object:Object | 3.3 | system / Context > require > require > cache > Object > .next/server/chunks/[turbopack]_runtime.js > Module > export |
-
-| largest strings | MB |
-| --- | ---: |
-| module.exports=[234494,A=>{"use strict";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=> | 5.6 |
-| module.exports=[17856,A=>{"use strict";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i | 5.6 |
-| module.exports=[17856,A=>{"use strict";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i | 5.6 |
-| const wasm = "AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/fwBgBX9/f39/AGAB | 4.4 |
-| AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/fwBgBX9/f39/AGABfwF/YAZ/f39/f3 | 4.4 |
-| lliADOAUngIEAACgKwAgBgAAoSsAIAcAAKErACAIAACiKwAgCQAAoysAIAoAAKQrACALAAClKwAgDAAApisAIA0AAK | 1.9 |
-| (()=>{var e,t,r={"./dist/build/webpack/alias/react-dom-server.js"(e,t,r){"use strict";var | 1.3 |
-| module.exports=[842865,(e,t,r)=>{let i=e.r(522734);t.exports=function(e){return e?/-----BE | 1.2 |
-| module.exports=[842865,(e,t,r)=>{let i=e.r(522734);t.exports=function(e){return e?/-----BE | 1.2 |
-| module.exports=[799092,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value: | 1.1 |
-| // v1.4.34 web-freeze. Schema is locked additive-only until iOS launch // + v1.5.0 marker. | 1.1 |
-| module.exports=[971743,(A,e,t)=>{"use strict";let r=A.r(57328),{addAbortListener:s}=A.r(68 | 1.1 |
-| module.exports=[835717,(A,e,t)=>{"use strict";let r=A.r(57328),{addAbortListener:s}=A.r(68 | 1 |
-| module.exports=[767189,e=>e.a(async(t,a)=>{try{var n=e.i(64105),r=e.i(563616),i=t([n]);[n] | 0.8 |
-| module.exports=[594108,806176,674750,98746,t=>{"use strict";var i;let r;function n(t){retu | 0.7 |
diff --git a/scripts/bench/results/v1.45.0-boot-final.json b/scripts/bench/results/v1.45.0-boot-final.json
deleted file mode 100644
index e2f378cce..000000000
--- a/scripts/bench/results/v1.45.0-boot-final.json
+++ /dev/null
@@ -1,5073 +0,0 @@
-{
- "meta": {
- "label": "v1.45.0-boot-final",
- "commit": "a57fa8335",
- "date": "2026-10-22T06:58:52.591Z",
- "node": "v22.23.2",
- "cpus": 14,
- "loadAverage": [
- 5.6,
- 6.4,
- 11.1
- ],
- "heapMb": 792,
- "idleSeconds": 300,
- "healthyAtMs": 1325,
- "readyAtMs": 3217,
- "stallMaxMs": 0
- },
- "bootLines": [
- "3217 ms: boot.steps {\"boot_queue_count\":122,\"boot_steps\":\"readiness_summary=3,key_check=172,time_zone_check=3,pulse_method_sync=18,monitors=6,worker_import=85,boss_start=186,load_worker_lifecycle=55,load_integration_sync=123,register_integration_sync=97,load_status=65,register_status=39,load_rollup=20,register_rollup=17,load_reminders=16,register_reminders=24,load_maintenance=58,register_maintenan"
- ],
- "stalls": [],
- "heapLines": [
- {
- "atMs": 5845,
- "heap_used_mb": 225,
- "heap_total_mb": 261,
- "heap_limit_mb": 840,
- "heap_used_ratio": 0.268,
- "new_space_mb": 3,
- "old_space_mb": 158,
- "code_space_mb": 4,
- "large_object_space_mb": 54,
- "new_large_object_space_mb": 0,
- "code_large_object_space_mb": 0,
- "trusted_space_mb": 6,
- "read_only_space_mb": 0,
- "other_spaces_mb": 0,
- "rss_mb": 500,
- "external_mb": 11,
- "array_buffers_mb": 0,
- "active_requests": 0,
- "active_jobs": 1,
- "in_flight_dropped": 0,
- "uptime_s": 6
- },
- {
- "atMs": 305875,
- "heap_used_mb": 158,
- "heap_total_mb": 179,
- "heap_limit_mb": 840,
- "heap_used_ratio": 0.188,
- "new_space_mb": 1,
- "old_space_mb": 96,
- "code_space_mb": 5,
- "large_object_space_mb": 53,
- "new_large_object_space_mb": 0,
- "code_large_object_space_mb": 0,
- "trusted_space_mb": 3,
- "read_only_space_mb": 0,
- "other_spaces_mb": 0,
- "rss_mb": 788,
- "external_mb": 11,
- "array_buffers_mb": 0,
- "active_requests": 0,
- "active_jobs": 1,
- "in_flight_dropped": 0,
- "uptime_s": 306
- }
- ],
- "markCompacts": [
- {
- "atMs": 423,
- "beforeMb": 21.5,
- "afterMb": 17.5
- },
- {
- "atMs": 712,
- "beforeMb": 52.4,
- "afterMb": 49.1
- },
- {
- "atMs": 814,
- "beforeMb": 85.1,
- "afterMb": 81.6
- },
- {
- "atMs": 1326,
- "beforeMb": 133,
- "afterMb": 127.6
- },
- {
- "atMs": 2476,
- "beforeMb": 199.1,
- "afterMb": 148.4
- },
- {
- "atMs": 5880,
- "beforeMb": 230.9,
- "afterMb": 147.8
- },
- {
- "atMs": 6847,
- "beforeMb": 200.4,
- "afterMb": 146.6
- },
- {
- "atMs": 8304,
- "beforeMb": 203.8,
- "afterMb": 146.5
- },
- {
- "atMs": 12026,
- "beforeMb": 226.7,
- "afterMb": 143.2
- },
- {
- "atMs": 12407,
- "beforeMb": 181.7,
- "afterMb": 150.2
- },
- {
- "atMs": 15846,
- "beforeMb": 231.8,
- "afterMb": 142.3
- },
- {
- "atMs": 16211,
- "beforeMb": 181.3,
- "afterMb": 142.3
- },
- {
- "atMs": 19993,
- "beforeMb": 223.6,
- "afterMb": 149.8
- },
- {
- "atMs": 20368,
- "beforeMb": 191.4,
- "afterMb": 154.8
- },
- {
- "atMs": 23910,
- "beforeMb": 236.5,
- "afterMb": 142.5
- },
- {
- "atMs": 24274,
- "beforeMb": 181.4,
- "afterMb": 142.5
- },
- {
- "atMs": 30373,
- "beforeMb": 222.8,
- "afterMb": 149.5
- },
- {
- "atMs": 31085,
- "beforeMb": 197,
- "afterMb": 149.2
- },
- {
- "atMs": 33099,
- "beforeMb": 210.8,
- "afterMb": 142.7
- },
- {
- "atMs": 36527,
- "beforeMb": 226.3,
- "afterMb": 143.8
- },
- {
- "atMs": 37227,
- "beforeMb": 189.6,
- "afterMb": 143.2
- },
- {
- "atMs": 40859,
- "beforeMb": 223.8,
- "afterMb": 144.9
- },
- {
- "atMs": 44753,
- "beforeMb": 220.7,
- "afterMb": 143.5
- },
- {
- "atMs": 45178,
- "beforeMb": 183.6,
- "afterMb": 150.9
- },
- {
- "atMs": 49163,
- "beforeMb": 242,
- "afterMb": 151.4
- },
- {
- "atMs": 52759,
- "beforeMb": 232,
- "afterMb": 144.7
- },
- {
- "atMs": 53218,
- "beforeMb": 185.2,
- "afterMb": 151.3
- },
- {
- "atMs": 56981,
- "beforeMb": 234.1,
- "afterMb": 145.1
- },
- {
- "atMs": 60028,
- "beforeMb": 225.8,
- "afterMb": 147.5
- },
- {
- "atMs": 63456,
- "beforeMb": 234.1,
- "afterMb": 150.7
- },
- {
- "atMs": 66640,
- "beforeMb": 231.3,
- "afterMb": 146.1
- },
- {
- "atMs": 68093,
- "beforeMb": 195,
- "afterMb": 146.1
- },
- {
- "atMs": 71676,
- "beforeMb": 232.2,
- "afterMb": 145.8
- },
- {
- "atMs": 75503,
- "beforeMb": 237.7,
- "afterMb": 159.5
- },
- {
- "atMs": 78075,
- "beforeMb": 248.5,
- "afterMb": 146.6
- },
- {
- "atMs": 84540,
- "beforeMb": 224.4,
- "afterMb": 147.7
- },
- {
- "atMs": 88825,
- "beforeMb": 200,
- "afterMb": 147.4
- },
- {
- "atMs": 93316,
- "beforeMb": 200.9,
- "afterMb": 147.3
- },
- {
- "atMs": 99358,
- "beforeMb": 219.8,
- "afterMb": 146.9
- },
- {
- "atMs": 104015,
- "beforeMb": 220,
- "afterMb": 147.8
- },
- {
- "atMs": 109364,
- "beforeMb": 221.8,
- "afterMb": 148.5
- },
- {
- "atMs": 118760,
- "beforeMb": 225.8,
- "afterMb": 147.2
- },
- {
- "atMs": 124489,
- "beforeMb": 225.4,
- "afterMb": 151.2
- },
- {
- "atMs": 131221,
- "beforeMb": 231.1,
- "afterMb": 150.8
- },
- {
- "atMs": 135350,
- "beforeMb": 215.3,
- "afterMb": 150.9
- },
- {
- "atMs": 139885,
- "beforeMb": 206.8,
- "afterMb": 149.6
- },
- {
- "atMs": 147072,
- "beforeMb": 223.8,
- "afterMb": 150.1
- },
- {
- "atMs": 150370,
- "beforeMb": 168.9,
- "afterMb": 149.9
- },
- {
- "atMs": 161654,
- "beforeMb": 203,
- "afterMb": 151.7
- },
- {
- "atMs": 166582,
- "beforeMb": 176.7,
- "afterMb": 151
- },
- {
- "atMs": 170653,
- "beforeMb": 173.8,
- "afterMb": 150.7
- },
- {
- "atMs": 173523,
- "beforeMb": 184.1,
- "afterMb": 150
- },
- {
- "atMs": 181372,
- "beforeMb": 224.9,
- "afterMb": 150.8
- },
- {
- "atMs": 187910,
- "beforeMb": 223.3,
- "afterMb": 149.9
- },
- {
- "atMs": 199166,
- "beforeMb": 210.8,
- "afterMb": 150.6
- },
- {
- "atMs": 203054,
- "beforeMb": 202.6,
- "afterMb": 151
- },
- {
- "atMs": 209332,
- "beforeMb": 230.1,
- "afterMb": 150.4
- },
- {
- "atMs": 214456,
- "beforeMb": 219,
- "afterMb": 151
- },
- {
- "atMs": 218065,
- "beforeMb": 193.1,
- "afterMb": 150.9
- },
- {
- "atMs": 221700,
- "beforeMb": 195.9,
- "afterMb": 150.9
- },
- {
- "atMs": 225906,
- "beforeMb": 210.4,
- "afterMb": 150.7
- },
- {
- "atMs": 227693,
- "beforeMb": 176.5,
- "afterMb": 150.9
- },
- {
- "atMs": 237405,
- "beforeMb": 204.1,
- "afterMb": 151.2
- },
- {
- "atMs": 241706,
- "beforeMb": 197.9,
- "afterMb": 151.5
- },
- {
- "atMs": 245651,
- "beforeMb": 210.7,
- "afterMb": 152.3
- },
- {
- "atMs": 249623,
- "beforeMb": 197.7,
- "afterMb": 151.4
- },
- {
- "atMs": 251604,
- "beforeMb": 185.1,
- "afterMb": 151.7
- },
- {
- "atMs": 253956,
- "beforeMb": 188.5,
- "afterMb": 152.1
- },
- {
- "atMs": 257864,
- "beforeMb": 205.5,
- "afterMb": 151.9
- },
- {
- "atMs": 262619,
- "beforeMb": 224.7,
- "afterMb": 151.3
- },
- {
- "atMs": 272867,
- "beforeMb": 224.8,
- "afterMb": 152.2
- },
- {
- "atMs": 280992,
- "beforeMb": 182.2,
- "afterMb": 151.9
- },
- {
- "atMs": 284638,
- "beforeMb": 169.3,
- "afterMb": 152
- },
- {
- "atMs": 291033,
- "beforeMb": 176.2,
- "afterMb": 152.2
- },
- {
- "atMs": 293457,
- "beforeMb": 174.5,
- "afterMb": 153.3
- },
- {
- "atMs": 297611,
- "beforeMb": 196.2,
- "afterMb": 151.9
- },
- {
- "atMs": 300538,
- "beforeMb": 184.5,
- "afterMb": 152.5
- },
- {
- "atMs": 302088,
- "beforeMb": 177.9,
- "afterMb": 152.5
- },
- {
- "atMs": 303258,
- "beforeMb": 176,
- "afterMb": 151.2
- },
- {
- "atMs": 303277,
- "beforeMb": 151.2,
- "afterMb": 151.2
- },
- {
- "atMs": 306059,
- "beforeMb": 165.5,
- "afterMb": 152
- },
- {
- "atMs": 306722,
- "beforeMb": 162.4,
- "afterMb": 152.2
- },
- {
- "atMs": 308561,
- "beforeMb": 172.2,
- "afterMb": 151.9
- },
- {
- "atMs": 309371,
- "beforeMb": 168.8,
- "afterMb": 151.8
- },
- {
- "atMs": 311582,
- "beforeMb": 170.5,
- "afterMb": 152.9
- },
- {
- "atMs": 312931,
- "beforeMb": 170.8,
- "afterMb": 152.5
- },
- {
- "atMs": 314178,
- "beforeMb": 171.1,
- "afterMb": 153.5
- },
- {
- "atMs": 316739,
- "beforeMb": 178.9,
- "afterMb": 152.9
- },
- {
- "atMs": 318728,
- "beforeMb": 179.7,
- "afterMb": 151.8
- },
- {
- "atMs": 321159,
- "beforeMb": 173.5,
- "afterMb": 152.3
- },
- {
- "atMs": 325180,
- "beforeMb": 178.5,
- "afterMb": 151.2
- },
- {
- "atMs": 326984,
- "beforeMb": 170.6,
- "afterMb": 152
- },
- {
- "atMs": 329437,
- "beforeMb": 177.2,
- "afterMb": 152.1
- },
- {
- "atMs": 331442,
- "beforeMb": 170.2,
- "afterMb": 153
- },
- {
- "atMs": 332715,
- "beforeMb": 180.7,
- "afterMb": 153
- },
- {
- "atMs": 334002,
- "beforeMb": 180.2,
- "afterMb": 153
- },
- {
- "atMs": 335414,
- "beforeMb": 173.5,
- "afterMb": 153.1
- },
- {
- "atMs": 337941,
- "beforeMb": 169.7,
- "afterMb": 151.8
- }
- ],
- "tasks": [
- {
- "startMs": 5,
- "durationMs": 396,
- "byModule": [
- [
- "(native)",
- 230.2
- ],
- [
- "node:internal/modules/cjs/loader",
- 57.8
- ],
- [
- "npm:next",
- 52.3
- ],
- [
- "node:internal/bootstrap/realm",
- 29.3
- ],
- [
- "node:internal/deps/undici/undici",
- 8.9
- ],
- [
- "node:internal/modules/esm/loader",
- 4.5
- ],
- [
- "node:fs",
- 3
- ],
- [
- "node:internal/net",
- 2.5
- ],
- [
- "node:internal/bootstrap/switches/is_main_thread",
- 2
- ],
- [
- "node:os",
- 1.8
- ],
- [
- "node:events",
- 1.5
- ],
- [
- "node:internal/url",
- 1.4
- ],
- [
- "node:path",
- 0.8
- ]
- ],
- "byFunction": [
- [
- "readFileUtf8 (native)",
- 120
- ],
- [
- "internalModuleStat (native)",
- 65.3
- ],
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 43.3
- ],
- [
- "compileForInternalLoader node:internal/bootstrap/realm",
- 29.3
- ],
- [
- "readPackageJSON (native)",
- 17
- ],
- [
- "dlopen (native)",
- 15.5
- ],
- [
- "__nccwpck_require__ npm:next",
- 15.4
- ],
- [
- "(anonymous) node:internal/modules/cjs/loader",
- 13
- ],
- [
- "startServer npm:next",
- 11
- ],
- [
- "(anonymous) npm:next",
- 10.7
- ],
- [
- "__require node:internal/deps/undici/undici",
- 6
- ],
- [
- "compileFunction (native)",
- 5
- ],
- [
- "(garbage collector) (native)",
- 4.5
- ],
- [
- "_interop_require_wildcard npm:next",
- 3.4
- ],
- [
- "realpathSync node:fs",
- 3
- ]
- ],
- "byEntry": []
- },
- {
- "startMs": 472,
- "durationMs": 336,
- "byModule": [
- [
- "(native)",
- 121.3
- ],
- [
- "wasm://wasm/00d1cb12",
- 43
- ],
- [
- "node:internal/modules/cjs/loader",
- 40.5
- ],
- [
- "npm:next",
- 31.7
- ],
- [
- "npm:@prisma/client",
- 23.8
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 23.4
- ],
- [
- "node:internal/modules/esm/utils",
- 17.3
- ],
- [
- "npm:zod",
- 5.9
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/runtime-utils.ts",
- 3
- ],
- [
- "node:vm",
- 1.7
- ],
- [
- "turbopack:///[turbopack]/shared-node/base-externals-utils.ts",
- 1.5
- ],
- [
- "npm:undici",
- 1.5
- ],
- [
- "src/instrumentation.ts",
- 1.5
- ],
- [
- "node:internal/modules/package_json_reader",
- 1.5
- ],
- [
- "npm:@prisma/client-runtime-utils",
- 1.5
- ]
- ],
- "byFunction": [
- [
- "readFileUtf8 (native)",
- 57.9
- ],
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 33.1
- ],
- [
- "(garbage collector) (native)",
- 25.4
- ],
- [
- "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 23.4
- ],
- [
- "internalModuleStat (native)",
- 22.4
- ],
- [
- "compileSourceTextModule node:internal/modules/esm/utils",
- 17.3
- ],
- [
- "#u npm:@prisma/client",
- 16.5
- ],
- [
- "(anonymous) npm:next",
- 12.2
- ],
- [
- "(anonymous) node:internal/modules/cjs/loader",
- 5.8
- ],
- [
- "wasm-function[1046] wasm://wasm/00d1cb12",
- 4.5
- ],
- [
- "js-to-wasm (native)",
- 4.4
- ],
- [
- "loadManifest npm:next",
- 3
- ],
- [
- "wasm-function[1286] wasm://wasm/00d1cb12",
- 3
- ],
- [
- "wasm-function[41] wasm://wasm/00d1cb12",
- 3
- ],
- [
- "wasm-function[915] wasm://wasm/00d1cb12",
- 3
- ]
- ],
- "byEntry": [
- [
- "src/instrumentation.ts",
- 95.8
- ],
- [
- "src/lib/db-session.ts",
- 47.3
- ],
- [
- "src/lib/process-type.ts",
- 27.1
- ],
- [
- "src/lib/boot/startup-state.ts",
- 25.5
- ],
- [
- "src/lib/boot/boot-steps.ts",
- 25.5
- ],
- [
- "src/lib/logging/background.ts",
- 25.5
- ],
- [
- "src/lib/logging/redact.ts",
- 13
- ],
- [
- "src/lib/logging/context.ts",
- 11
- ],
- [
- "src/lib/validations/measurement.ts",
- 10
- ],
- [
- "src/generated/prisma/internal/class.ts",
- 4.7
- ],
- [
- "src/lib/validations/entry-instant.ts",
- 4.5
- ],
- [
- "src/lib/env.ts",
- 2.8
- ],
- [
- "src/lib/logging/config.ts",
- 1.5
- ],
- [
- "src/lib/crypto.ts",
- 1.5
- ],
- [
- "src/lib/crypto/canary.ts",
- 1.5
- ]
- ]
- },
- {
- "startMs": 811,
- "durationMs": 225,
- "byModule": [
- [
- "node:internal/modules/cjs/loader",
- 52.3
- ],
- [
- "(native)",
- 48.2
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 37
- ],
- [
- "npm:next",
- 22.2
- ],
- [
- "npm:zod",
- 12.5
- ],
- [
- "node:internal/modules/esm/utils",
- 8.7
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 5.6
- ],
- [
- "node:fs",
- 4.5
- ],
- [
- "node:internal/async_hooks",
- 3
- ],
- [
- "node:internal/modules/esm/translators",
- 2.6
- ],
- [
- "node:internal/deps/cjs-module-lexer/lexer",
- 2
- ],
- [
- "node:internal/modules/package_json_reader",
- 1.7
- ],
- [
- "node:internal/bootstrap/realm",
- 1.5
- ],
- [
- "node:internal/deps/undici/undici",
- 1.5
- ],
- [
- "npm:rrule-temporal",
- 1.5
- ]
- ],
- "byFunction": [
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 52.3
- ],
- [
- "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 37
- ],
- [
- "readFileUtf8 (native)",
- 27.5
- ],
- [
- "compileSourceTextModule node:internal/modules/esm/utils",
- 8.7
- ],
- [
- "read (native)",
- 7.1
- ],
- [
- "asyncModule turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 5.6
- ],
- [
- "(garbage collector) (native)",
- 4.2
- ],
- [
- "(anonymous) npm:zod",
- 3.9
- ],
- [
- "(anonymous) npm:next",
- 3.8
- ],
- [
- "a npm:next",
- 3.2
- ],
- [
- "readFileSync node:fs",
- 3
- ],
- [
- "eJ npm:zod",
- 3
- ],
- [
- "u npm:zod",
- 2.6
- ],
- [
- "open (native)",
- 2.2
- ],
- [
- "compileFunctionForCJSLoader (native)",
- 1.8
- ]
- ],
- "byEntry": [
- [
- "src/app/api/health/route.ts",
- 62
- ],
- [
- "src/lib/api-errors.ts",
- 25.9
- ],
- [
- "src/lib/auth/secure-cookie.ts",
- 14.9
- ],
- [
- "src/lib/logging/background.ts",
- 13.4
- ],
- [
- "src/lib/sharing/acting-session.ts",
- 12.8
- ],
- [
- "src/lib/validations/measurement.ts",
- 11.9
- ],
- [
- "src/lib/db-session.ts",
- 11.3
- ],
- [
- "src/lib/export/environment-backup.ts",
- 11.3
- ],
- [
- "src/lib/export/full-backup-payload.ts",
- 11.3
- ],
- [
- "src/lib/jobs/job-failures.ts",
- 11.3
- ],
- [
- "src/lib/jobs/offhost-backup.ts",
- 11.3
- ],
- [
- "src/lib/managed-profiles/handover-access.ts",
- 11.3
- ],
- [
- "src/instrumentation.ts",
- 10.3
- ],
- [
- "src/lib/notifications/types.ts",
- 10.1
- ],
- [
- "src/lib/logging/redact.ts",
- 10.1
- ]
- ]
- },
- {
- "startMs": 1063,
- "durationMs": 246,
- "byModule": [
- [
- "(native)",
- 64.7
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 47.2
- ],
- [
- "node:internal/modules/cjs/loader",
- 31.6
- ],
- [
- "npm:luxon",
- 18
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 15.3
- ],
- [
- "npm:yaml",
- 15
- ],
- [
- "npm:zod",
- 10.2
- ],
- [
- "src/lib/i18n/server-catalogs.ts",
- 8.2
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/runtime-utils.ts",
- 4.5
- ],
- [
- "npm:cron-parser",
- 4
- ],
- [
- "npm:pg",
- 3.9
- ],
- [
- "node:fs",
- 3
- ],
- [
- "node:internal/process/task_queues",
- 3
- ],
- [
- "npm:semver",
- 2.9
- ],
- [
- "src/lib/ai/safety/outbound-screen.ts",
- 1.5
- ]
- ],
- "byFunction": [
- [
- "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 47.2
- ],
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 31.6
- ],
- [
- "(garbage collector) (native)",
- 30.3
- ],
- [
- "readFileUtf8 (native)",
- 24.8
- ],
- [
- "asyncModule turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 15.3
- ],
- [
- "(anonymous) src/lib/i18n/server-catalogs.ts",
- 8.2
- ],
- [
- "writev (native)",
- 5
- ],
- [
- "isValidZone npm:luxon",
- 4.5
- ],
- [
- "(anonymous) npm:zod",
- 3
- ],
- [
- "readFileSync node:fs",
- 3
- ],
- [
- "nextTick node:internal/process/task_queues",
- 3
- ],
- [
- "parse npm:yaml",
- 2.7
- ],
- [
- "partsOffset npm:luxon",
- 2
- ],
- [
- "bind npm:pg",
- 1.5
- ],
- [
- "#parseField npm:cron-parser",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/jobs/reminder-worker.ts",
- 137.6
- ],
- [
- "src/lib/jobs/reminder/google-health-sync.ts",
- 78
- ],
- [
- "src/lib/whoop/sync.ts",
- 65.9
- ],
- [
- "src/instrumentation.ts",
- 53.8
- ],
- [
- "src/lib/insights/bucket-series.ts",
- 47.1
- ],
- [
- "src/lib/whoop/oauth-descriptor.ts",
- 47.1
- ],
- [
- "src/lib/whoop/sync-core.ts",
- 47.1
- ],
- [
- "src/lib/whoop/sync-body.ts",
- 47.1
- ],
- [
- "src/lib/import/unzip-export-xml.ts",
- 35.3
- ],
- [
- "src/lib/jobs/integration-status-null-probe.ts",
- 35.3
- ],
- [
- "src/lib/insights/snapshot-hash.ts",
- 25.2
- ],
- [
- "src/lib/ai/prompts/safety-contracts.ts",
- 22.1
- ],
- [
- "src/lib/ai/prompts/native-prompts.ts",
- 22.1
- ],
- [
- "src/lib/ai/medical-references.ts",
- 22.1
- ],
- [
- "src/lib/notifications/delivery-identity.ts",
- 15.8
- ]
- ]
- },
- {
- "startMs": 1363,
- "durationMs": 68,
- "byModule": [
- [
- "node:internal/modules/cjs/loader",
- 29.2
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 6.7
- ],
- [
- "(native)",
- 5.9
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/runtime-utils.ts",
- 4.5
- ],
- [
- "src/lib/daily/today-overview.ts",
- 4.1
- ],
- [
- "node:fs",
- 3
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 3
- ],
- [
- "node:internal/streams/writable",
- 1.5
- ],
- [
- "src/lib/jobs/reminder/insights-handlers.ts",
- 1.5
- ],
- [
- "npm:next",
- 1.5
- ],
- [
- "npm:zod",
- 1.5
- ],
- [
- "src/lib/jobs/coach-nudge-ai.ts",
- 1.5
- ],
- [
- "npm:pg-pool",
- 1.5
- ],
- [
- "node:internal/bootstrap/node",
- 1.5
- ],
- [
- "node:internal/timers",
- 1.3
- ]
- ],
- "byFunction": [
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 29.2
- ],
- [
- "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 6.7
- ],
- [
- "readFileUtf8 (native)",
- 5.9
- ],
- [
- "defineProp turbopack:///[turbopack]/shared/runtime/runtime-utils.ts",
- 4.5
- ],
- [
- "w src/lib/daily/today-overview.ts",
- 4.1
- ],
- [
- "readFileSync node:fs",
- 3
- ],
- [
- "asyncModule turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 3
- ],
- [
- "onwrite node:internal/streams/writable",
- 1.5
- ],
- [
- "(anonymous) src/lib/jobs/reminder/insights-handlers.ts",
- 1.5
- ],
- [
- "(anonymous) npm:next",
- 1.5
- ],
- [
- "max npm:zod",
- 1.5
- ],
- [
- "module.exports.Object.freeze.yearly.id src/lib/jobs/coach-nudge-ai.ts",
- 1.5
- ],
- [
- "_acquireClient npm:pg-pool",
- 1.5
- ],
- [
- "get node:internal/bootstrap/node",
- 1.5
- ],
- [
- "Timeout node:internal/timers",
- 1.3
- ]
- ],
- "byEntry": [
- [
- "src/lib/jobs/reminder-worker.ts",
- 62.5
- ],
- [
- "src/lib/jobs/reminder/insights-handlers.ts",
- 24.4
- ],
- [
- "src/lib/ai/prompts/arrival-reaction.ts",
- 12.8
- ],
- [
- "src/lib/daily/score-steady.ts",
- 11.3
- ],
- [
- "src/lib/dashboard/bands.ts",
- 7.1
- ],
- [
- "src/lib/dashboard/snapshot-read.ts",
- 5.8
- ],
- [
- "src/lib/jobs/data-arrival.ts",
- 4.5
- ],
- [
- "src/lib/daily/today-overview.ts",
- 4.1
- ],
- [
- "src/lib/daily/priority-item-key.ts",
- 4.1
- ],
- [
- "src/lib/ai/prompts/status-batch.ts",
- 3
- ],
- [
- "src/lib/dashboard/score-narrative.ts",
- 2.7
- ],
- [
- "src/lib/jobs/ai-job-candidates.ts",
- 2.7
- ],
- [
- "src/lib/ai/prompts/general-status.ts",
- 1.5
- ],
- [
- "src/lib/insights/derived/derived-assessment-ai.ts",
- 1.5
- ],
- [
- "src/lib/insights/no-key-fallbacks.ts",
- 1.5
- ]
- ]
- },
- {
- "startMs": 1545,
- "durationMs": 57,
- "byModule": [
- [
- "node:internal/modules/cjs/loader",
- 25.3
- ],
- [
- "(native)",
- 12.9
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 7.2
- ],
- [
- "node:fs",
- 4.2
- ],
- [
- "node:buffer",
- 1.5
- ],
- [
- "npm:zod",
- 1.5
- ],
- [
- "src/lib/jobs/pr-detection.ts",
- 1.5
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 1.5
- ],
- [
- "src/lib/jobs/measurement-maintenance.ts",
- 1.3
- ]
- ],
- "byFunction": [
- [
- "wrapSafe node:internal/modules/cjs/loader",
- 25.3
- ],
- [
- "readFileUtf8 (native)",
- 11.4
- ],
- [
- "instantiateModule turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 7.1
- ],
- [
- "readFileSync node:fs",
- 4.2
- ],
- [
- "getEncodingOps node:buffer",
- 1.5
- ],
- [
- "dH npm:zod",
- 1.5
- ],
- [
- "(anonymous) src/lib/jobs/pr-detection.ts",
- 1.5
- ],
- [
- "(garbage collector) (native)",
- 1.5
- ],
- [
- "asyncModule turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 1.5
- ],
- [
- "(anonymous) src/lib/jobs/measurement-maintenance.ts",
- 1.3
- ],
- [
- "getOrInstantiateModuleFromParent turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 0
- ]
- ],
- "byEntry": [
- [
- "src/lib/jobs/reminder-worker.ts",
- 55.3
- ],
- [
- "src/lib/jobs/reminder/ops-handlers.ts",
- 13
- ],
- [
- "src/lib/export/restore-backup.ts",
- 3
- ],
- [
- "src/lib/medications/ingredients/label-extract.ts",
- 3
- ],
- [
- "src/lib/environment/service.ts",
- 1.5
- ],
- [
- "src/lib/export/backup-blob.ts",
- 1.5
- ],
- [
- "src/lib/jobs/pr-detection.ts",
- 1.5
- ],
- [
- "src/lib/jobs/document-content-index-backfill.ts",
- 1.5
- ],
- [
- "src/lib/labs/ocr-extract.ts",
- 1.5
- ],
- [
- "src/lib/jobs/reminder/backup-handlers.ts",
- 1.5
- ],
- [
- "src/lib/export/tenant-boundary.ts",
- 1.5
- ],
- [
- "src/lib/gamification/achievements-result.ts",
- 1.5
- ],
- [
- "src/lib/jobs/measurement-maintenance.ts",
- 1.3
- ],
- [
- "src/lib/documents/auto-stage-labs.ts",
- 1.2
- ]
- ]
- },
- {
- "startMs": 1820,
- "durationMs": 114,
- "byModule": [
- [
- "src/lib/tz/zone-offset.ts",
- 36
- ],
- [
- "npm:@prisma/client",
- 18.2
- ],
- [
- "src/lib/validations/source-priority.ts",
- 18.1
- ],
- [
- "(native)",
- 13.1
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 7.2
- ],
- [
- "node:buffer",
- 4.1
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 3.7
- ],
- [
- "npm:zod",
- 2.3
- ],
- [
- "npm:pg",
- 2
- ],
- [
- "src/lib/tz/format.ts",
- 1.9
- ],
- [
- "node:internal/async_local_storage/async_hooks",
- 1.5
- ],
- [
- "src/lib/tz/intl-cache.ts",
- 1.5
- ],
- [
- "node:internal/process/task_queues",
- 1.3
- ],
- [
- "npm:next",
- 1.2
- ],
- [
- "npm:%40prisma",
- 1.1
- ]
- ],
- "byFunction": [
- [
- "l src/lib/tz/zone-offset.ts",
- 30.4
- ],
- [
- "I src/lib/validations/source-priority.ts",
- 16.5
- ],
- [
- "(garbage collector) (native)",
- 7.4
- ],
- [
- "_ src/lib/personal-records/pr-detection-worker.ts",
- 7.2
- ],
- [
- "(anonymous) src/lib/tz/zone-offset.ts",
- 5.7
- ],
- [
- "runMicrotasks (native)",
- 4.3
- ],
- [
- "slice node:buffer",
- 4.1
- ],
- [
- "Zs npm:@prisma/client",
- 4
- ],
- [
- "(anonymous) src/lib/analytics/source-priority.ts",
- 3.7
- ],
- [
- "Tp npm:@prisma/client",
- 3
- ],
- [
- "(anonymous) npm:@prisma/client",
- 2.8
- ],
- [
- "ni npm:@prisma/client",
- 2.6
- ],
- [
- "Fp npm:@prisma/client",
- 1.5
- ],
- [
- "Ae npm:@prisma/client",
- 1.5
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 73.3
- ],
- [
- "src/lib/tz/format.ts",
- 40.7
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 40.7
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 38.8
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 38.1
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 28.1
- ],
- [
- "src/lib/validations/source-priority.ts",
- 20.4
- ],
- [
- "src/lib/tz/intl-cache.ts",
- 1.5
- ]
- ]
- },
- {
- "startMs": 2407,
- "durationMs": 50,
- "byModule": [
- [
- "(native)",
- 18.9
- ],
- [
- "src/lib/validations/source-priority.ts",
- 14.1
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 3.7
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 3
- ],
- [
- "npm:next",
- 1.8
- ],
- [
- "npm:%40prisma",
- 1.5
- ],
- [
- "src/lib/tz/format.ts",
- 1.5
- ],
- [
- "node:internal/async_local_storage/async_hooks",
- 1.5
- ],
- [
- "npm:pg",
- 1.5
- ],
- [
- "npm:@prisma/client",
- 1.3
- ],
- [
- "node:internal/async_hooks",
- 1.3
- ]
- ],
- "byFunction": [
- [
- "(garbage collector) (native)",
- 14.8
- ],
- [
- "I src/lib/validations/source-priority.ts",
- 11.1
- ],
- [
- "(anonymous) src/lib/analytics/source-priority.ts",
- 3.7
- ],
- [
- "_ src/lib/personal-records/pr-detection-worker.ts",
- 3
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 3
- ],
- [
- "(program) (native)",
- 3
- ],
- [
- "Date1 npm:next",
- 1.8
- ],
- [
- "D npm:%40prisma",
- 1.5
- ],
- [
- "d src/lib/tz/format.ts",
- 1.5
- ],
- [
- "_propagate node:internal/async_local_storage/async_hooks",
- 1.5
- ],
- [
- "_send npm:pg",
- 1.5
- ],
- [
- "Zs npm:@prisma/client",
- 1.3
- ],
- [
- "promiseInitHook node:internal/async_hooks",
- 1.3
- ],
- [
- "utf8Slice (native)",
- 1.2
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 26.6
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 19.3
- ],
- [
- "src/lib/validations/source-priority.ts",
- 14.1
- ],
- [
- "src/lib/tz/format.ts",
- 3
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 3
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 2.8
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 1.5
- ]
- ]
- },
- {
- "startMs": 18192,
- "durationMs": 59,
- "byModule": [
- [
- "(native)",
- 15.5
- ],
- [
- "npm:@prisma/client",
- 13.9
- ],
- [
- "src/lib/validations/source-priority.ts",
- 11.8
- ],
- [
- "npm:pg",
- 3.6
- ],
- [
- "node:events",
- 3
- ],
- [
- "node:buffer",
- 2.5
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 1.5
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 1.5
- ],
- [
- "node:internal/async_hooks",
- 1.5
- ],
- [
- "npm:pg-types",
- 1.4
- ],
- [
- "npm:next",
- 1.4
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 1.2
- ]
- ],
- "byFunction": [
- [
- "I src/lib/validations/source-priority.ts",
- 10.3
- ],
- [
- "(garbage collector) (native)",
- 5.8
- ],
- [
- "utf8Slice (native)",
- 4.1
- ],
- [
- "(program) (native)",
- 3.1
- ],
- [
- "emit node:events",
- 3
- ],
- [
- "Fp npm:@prisma/client",
- 3
- ],
- [
- "(anonymous) npm:@prisma/client",
- 2.9
- ],
- [
- "Zs npm:@prisma/client",
- 2.6
- ],
- [
- "slice node:buffer",
- 2.5
- ],
- [
- "runMicrotasks (native)",
- 2.5
- ],
- [
- "Ae npm:@prisma/client",
- 1.5
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 1.5
- ],
- [
- "m src/lib/measurements/consolidation-base.ts",
- 1.5
- ],
- [
- "_ src/lib/personal-records/pr-detection-worker.ts",
- 1.5
- ],
- [
- "promiseInitHook node:internal/async_hooks",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 18.9
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 12.9
- ],
- [
- "src/lib/validations/source-priority.ts",
- 11.8
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 4.5
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 2.6
- ],
- [
- "src/lib/tz/format.ts",
- 2.6
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 2.6
- ]
- ]
- },
- {
- "startMs": 19914,
- "durationMs": 65,
- "byModule": [
- [
- "src/lib/validations/source-priority.ts",
- 18.1
- ],
- [
- "(native)",
- 12.3
- ],
- [
- "npm:@prisma/client",
- 11.6
- ],
- [
- "npm:next",
- 5.9
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 3
- ],
- [
- "src/lib/tz/format.ts",
- 3
- ],
- [
- "npm:pg",
- 2.6
- ],
- [
- "npm:pg-protocol",
- 2.5
- ],
- [
- "node:internal/async_hooks",
- 1.5
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 1.5
- ],
- [
- "node:buffer",
- 1.5
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 1.3
- ]
- ],
- "byFunction": [
- [
- "I src/lib/validations/source-priority.ts",
- 14.1
- ],
- [
- "(garbage collector) (native)",
- 8.6
- ],
- [
- "Date1 npm:next",
- 5.9
- ],
- [
- "(program) (native)",
- 3.7
- ],
- [
- "(anonymous) npm:@prisma/client",
- 3
- ],
- [
- "Fp npm:@prisma/client",
- 3
- ],
- [
- "_ src/lib/personal-records/pr-detection-worker.ts",
- 3
- ],
- [
- "c src/lib/tz/format.ts",
- 3
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 2.9
- ],
- [
- "_parseRowAsArray npm:pg",
- 2.6
- ],
- [
- "request npm:@prisma/client",
- 2.5
- ],
- [
- "lookupPublicResource node:internal/async_hooks",
- 1.5
- ],
- [
- "parseDataRowMessage npm:pg-protocol",
- 1.5
- ],
- [
- "Zs npm:@prisma/client",
- 1.5
- ],
- [
- "Ae npm:@prisma/client",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 29.8
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 22.6
- ],
- [
- "src/lib/validations/source-priority.ts",
- 18.1
- ],
- [
- "src/lib/tz/format.ts",
- 5.8
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 5.8
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 2.8
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 2.8
- ]
- ]
- },
- {
- "startMs": 20296,
- "durationMs": 55,
- "byModule": [
- [
- "(native)",
- 22.4
- ],
- [
- "npm:@prisma/client",
- 14.8
- ],
- [
- "node:buffer",
- 3
- ],
- [
- "npm:zod",
- 3
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 3
- ],
- [
- "src/lib/tz/format.ts",
- 1.5
- ],
- [
- "node:internal/stream_base_commons",
- 1.5
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 1.5
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 1.5
- ],
- [
- "npm:pg-protocol",
- 1.3
- ],
- [
- "npm:pg",
- 1.1
- ]
- ],
- "byFunction": [
- [
- "(garbage collector) (native)",
- 15.3
- ],
- [
- "Fp npm:@prisma/client",
- 6.1
- ],
- [
- "(program) (native)",
- 4.1
- ],
- [
- "slice node:buffer",
- 3
- ],
- [
- "te npm:zod",
- 3
- ],
- [
- "Tp npm:@prisma/client",
- 3
- ],
- [
- "m src/lib/measurements/consolidation-base.ts",
- 3
- ],
- [
- "runMicrotasks (native)",
- 1.5
- ],
- [
- "(anonymous) npm:@prisma/client",
- 1.5
- ],
- [
- "Zs npm:@prisma/client",
- 1.5
- ],
- [
- "Ae npm:@prisma/client",
- 1.5
- ],
- [
- "(anonymous) src/lib/tz/format.ts",
- 1.5
- ],
- [
- "onStreamRead node:internal/stream_base_commons",
- 1.5
- ],
- [
- "utf8Slice (native)",
- 1.5
- ],
- [
- "(anonymous) src/lib/analytics/source-priority.ts",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 10.5
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 6
- ],
- [
- "src/lib/validations/source-priority.ts",
- 3
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 3
- ],
- [
- "src/lib/tz/format.ts",
- 1.5
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 1.5
- ]
- ]
- },
- {
- "startMs": 30289,
- "durationMs": 69,
- "byModule": [
- [
- "src/lib/validations/source-priority.ts",
- 18.3
- ],
- [
- "(native)",
- 12.9
- ],
- [
- "npm:@prisma/client",
- 8.8
- ],
- [
- "node:buffer",
- 6.8
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 4.5
- ],
- [
- "src/lib/tz/format.ts",
- 4
- ],
- [
- "npm:pg-protocol",
- 3
- ],
- [
- "npm:zod",
- 3
- ],
- [
- "node:timers",
- 1.5
- ],
- [
- "node:internal/async_hooks",
- 1.5
- ],
- [
- "npm:next",
- 1.5
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 1.5
- ],
- [
- "node:events",
- 1.2
- ]
- ],
- "byFunction": [
- [
- "I src/lib/validations/source-priority.ts",
- 12.4
- ],
- [
- "(garbage collector) (native)",
- 11.4
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 5.9
- ],
- [
- "slice node:buffer",
- 5.6
- ],
- [
- "_ src/lib/personal-records/pr-detection-worker.ts",
- 4.5
- ],
- [
- "parseDataRowMessage npm:pg-protocol",
- 3
- ],
- [
- "h src/lib/tz/format.ts",
- 3
- ],
- [
- "te npm:zod",
- 3
- ],
- [
- "setTimeout node:timers",
- 1.5
- ],
- [
- "(anonymous) npm:@prisma/client",
- 1.5
- ],
- [
- "getOrSetAsyncId node:internal/async_hooks",
- 1.5
- ],
- [
- "utf8Slice (native)",
- 1.5
- ],
- [
- "$a npm:@prisma/client",
- 1.5
- ],
- [
- "Fp npm:@prisma/client",
- 1.5
- ],
- [
- "Tp npm:@prisma/client",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 33
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 24.3
- ],
- [
- "src/lib/validations/source-priority.ts",
- 21.3
- ],
- [
- "src/lib/tz/format.ts",
- 5.6
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 5.6
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 2.6
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 1.5
- ]
- ]
- },
- {
- "startMs": 31004,
- "durationMs": 68,
- "byModule": [
- [
- "(native)",
- 17.8
- ],
- [
- "src/lib/validations/source-priority.ts",
- 14.9
- ],
- [
- "npm:@prisma/client",
- 7.6
- ],
- [
- "npm:next",
- 4.5
- ],
- [
- "node:internal/async_hooks",
- 4.5
- ],
- [
- "npm:pg-protocol",
- 4.1
- ],
- [
- "npm:pg",
- 3
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 3
- ],
- [
- "node:buffer",
- 2.7
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 1.5
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 1.4
- ],
- [
- "src/lib/tz/format.ts",
- 1.3
- ],
- [
- "npm:zod",
- 1.3
- ]
- ],
- "byFunction": [
- [
- "I src/lib/validations/source-priority.ts",
- 13.4
- ],
- [
- "(garbage collector) (native)",
- 10.7
- ],
- [
- "Date1 npm:next",
- 4.5
- ],
- [
- "(program) (native)",
- 4.5
- ],
- [
- "_parseRowAsArray npm:pg",
- 3
- ],
- [
- "(anonymous) npm:@prisma/client",
- 3
- ],
- [
- "(anonymous) src/lib/tz/zone-offset.ts",
- 3
- ],
- [
- "parseDataRowMessage npm:pg-protocol",
- 2.6
- ],
- [
- "slice node:buffer",
- 1.5
- ],
- [
- "handlePacket npm:pg-protocol",
- 1.5
- ],
- [
- "promiseBeforeHook node:internal/async_hooks",
- 1.5
- ],
- [
- "trackPromise node:internal/async_hooks",
- 1.5
- ],
- [
- "(anonymous) src/lib/analytics/source-priority.ts",
- 1.5
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 1.5
- ],
- [
- "Zs npm:@prisma/client",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 26.4
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 19
- ],
- [
- "src/lib/validations/source-priority.ts",
- 16.2
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 6.1
- ],
- [
- "src/lib/tz/format.ts",
- 5.8
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 5.8
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 4.5
- ]
- ]
- },
- {
- "startMs": 35350,
- "durationMs": 65,
- "byModule": [
- [
- "src/lib/validations/source-priority.ts",
- 17
- ],
- [
- "npm:@prisma/client",
- 11.8
- ],
- [
- "(native)",
- 8.6
- ],
- [
- "node:buffer",
- 6.5
- ],
- [
- "src/lib/tz/format.ts",
- 4.5
- ],
- [
- "npm:pg-protocol",
- 3
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 3
- ],
- [
- "npm:%40prisma",
- 1.6
- ],
- [
- "npm:pg",
- 1.5
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 1.5
- ],
- [
- "node:events",
- 1.5
- ],
- [
- "npm:next",
- 1.5
- ],
- [
- "node:internal/async_context_frame",
- 1.3
- ],
- [
- "npm:pg-types",
- 1.2
- ]
- ],
- "byFunction": [
- [
- "I src/lib/validations/source-priority.ts",
- 12.6
- ],
- [
- "slice node:buffer",
- 6.5
- ],
- [
- "d src/lib/tz/format.ts",
- 4.5
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 4.4
- ],
- [
- "(garbage collector) (native)",
- 4.3
- ],
- [
- "Zs npm:@prisma/client",
- 4.2
- ],
- [
- "parseDataRowMessage npm:pg-protocol",
- 3
- ],
- [
- "_ src/lib/personal-records/pr-detection-worker.ts",
- 3
- ],
- [
- "utf8Slice (native)",
- 2.8
- ],
- [
- "mapQueryEngineResult npm:@prisma/client",
- 1.7
- ],
- [
- "D npm:%40prisma",
- 1.6
- ],
- [
- "handleDataRow npm:pg",
- 1.5
- ],
- [
- "(anonymous) npm:@prisma/client",
- 1.5
- ],
- [
- "ki npm:@prisma/client",
- 1.5
- ],
- [
- "Ae npm:@prisma/client",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 26
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 20
- ],
- [
- "src/lib/validations/source-priority.ts",
- 17
- ],
- [
- "src/lib/tz/format.ts",
- 4.5
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 4.5
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 3
- ]
- ]
- },
- {
- "startMs": 36452,
- "durationMs": 57,
- "byModule": [
- [
- "(native)",
- 20
- ],
- [
- "src/lib/validations/source-priority.ts",
- 16
- ],
- [
- "npm:@prisma/client",
- 7.3
- ],
- [
- "node:internal/per_context/primordials",
- 2.8
- ],
- [
- "npm:%40prisma",
- 1.5
- ],
- [
- "npm:next",
- 1.5
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 1.5
- ],
- [
- "node:internal/async_hooks",
- 1.5
- ],
- [
- "node:internal/streams/readable",
- 1.5
- ],
- [
- "npm:pg-protocol",
- 1.2
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 1.2
- ],
- [
- "src/lib/tz/format.ts",
- 1
- ]
- ],
- "byFunction": [
- [
- "I src/lib/validations/source-priority.ts",
- 14.5
- ],
- [
- "(garbage collector) (native)",
- 13.1
- ],
- [
- "(anonymous) npm:@prisma/client",
- 3
- ],
- [
- "utf8Slice (native)",
- 3
- ],
- [
- "(program) (native)",
- 2.5
- ],
- [
- "D npm:%40prisma",
- 1.5
- ],
- [
- "Fp npm:@prisma/client",
- 1.5
- ],
- [
- "ni npm:@prisma/client",
- 1.5
- ],
- [
- "Date1 npm:next",
- 1.5
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 1.5
- ],
- [
- "runMicrotasks (native)",
- 1.5
- ],
- [
- "(anonymous) src/lib/tz/zone-offset.ts",
- 1.5
- ],
- [
- "promiseInitHook node:internal/async_hooks",
- 1.5
- ],
- [
- "next node:internal/per_context/primordials",
- 1.5
- ],
- [
- "Readable.push node:internal/streams/readable",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 21.2
- ],
- [
- "src/lib/validations/source-priority.ts",
- 16
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 16
- ],
- [
- "src/lib/tz/format.ts",
- 2.5
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 2.5
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 2.5
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 1.5
- ]
- ]
- },
- {
- "startMs": 38744,
- "durationMs": 56,
- "byModule": [
- [
- "src/lib/validations/source-priority.ts",
- 13.3
- ],
- [
- "(native)",
- 9.9
- ],
- [
- "npm:@prisma/client",
- 9.2
- ],
- [
- "node:buffer",
- 4.7
- ],
- [
- "npm:pg-protocol",
- 4.3
- ],
- [
- "node:events",
- 1.9
- ],
- [
- "npm:pg",
- 1.6
- ],
- [
- "npm:%40prisma",
- 1.5
- ],
- [
- "npm:next",
- 1.5
- ],
- [
- "src/lib/tz/format.ts",
- 1.5
- ],
- [
- "node:internal/async_local_storage/async_hooks",
- 1.5
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 1.5
- ],
- [
- "node:internal/async_hooks",
- 1.4
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 1.4
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 1.1
- ]
- ],
- "byFunction": [
- [
- "I src/lib/validations/source-priority.ts",
- 8.9
- ],
- [
- "(garbage collector) (native)",
- 8.4
- ],
- [
- "(anonymous) npm:@prisma/client",
- 4.8
- ],
- [
- "slice node:buffer",
- 4.7
- ],
- [
- "parseDataRowMessage npm:pg-protocol",
- 3.1
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 2.9
- ],
- [
- "emit node:events",
- 1.9
- ],
- [
- "cs npm:@prisma/client",
- 1.7
- ],
- [
- "_parseRowAsArray npm:pg",
- 1.6
- ],
- [
- "Zs npm:@prisma/client",
- 1.6
- ],
- [
- "writev (native)",
- 1.5
- ],
- [
- "D npm:%40prisma",
- 1.5
- ],
- [
- "Date1 npm:next",
- 1.5
- ],
- [
- "d src/lib/tz/format.ts",
- 1.5
- ],
- [
- "(anonymous) src/lib/validations/source-priority.ts",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 21.8
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 16
- ],
- [
- "src/lib/validations/source-priority.ts",
- 13.3
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 4.3
- ],
- [
- "src/lib/tz/format.ts",
- 3
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 3
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 1.4
- ]
- ]
- },
- {
- "startMs": 42458,
- "durationMs": 50,
- "byModule": [
- [
- "npm:@prisma/client",
- 13.5
- ],
- [
- "src/lib/validations/source-priority.ts",
- 10.1
- ],
- [
- "(native)",
- 7.6
- ],
- [
- "npm:pg-protocol",
- 3.2
- ],
- [
- "node:events",
- 3
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 3
- ],
- [
- "src/lib/tz/format.ts",
- 3
- ],
- [
- "npm:pg",
- 2.8
- ],
- [
- "node:internal/async_hooks",
- 1.5
- ],
- [
- "npm:%40prisma",
- 1.4
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 1.1
- ]
- ],
- "byFunction": [
- [
- "I src/lib/validations/source-priority.ts",
- 7
- ],
- [
- "Fp npm:@prisma/client",
- 4.5
- ],
- [
- "(garbage collector) (native)",
- 4.5
- ],
- [
- "utf8Slice (native)",
- 3.1
- ],
- [
- "emit node:events",
- 3
- ],
- [
- "Tp npm:@prisma/client",
- 3
- ],
- [
- "_ src/lib/personal-records/pr-detection-worker.ts",
- 3
- ],
- [
- "#a npm:@prisma/client",
- 1.8
- ],
- [
- "(anonymous) npm:pg-protocol",
- 1.6
- ],
- [
- "handlePacket npm:pg-protocol",
- 1.6
- ],
- [
- "_parseRowAsArray npm:pg",
- 1.5
- ],
- [
- "(anonymous) npm:@prisma/client",
- 1.5
- ],
- [
- "Zs npm:@prisma/client",
- 1.5
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 1.5
- ],
- [
- "promiseBeforeHook node:internal/async_hooks",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 17.3
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 12.7
- ],
- [
- "src/lib/validations/source-priority.ts",
- 10.1
- ],
- [
- "src/lib/tz/format.ts",
- 3
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 3
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 1.5
- ],
- [
- "src/lib/jobs/job-lock.ts",
- 1.3
- ],
- [
- "src/lib/jobs/long-pass.ts",
- 1.3
- ],
- [
- "src/lib/jobs/run-job.ts",
- 1.3
- ],
- [
- "src/lib/jobs/job-observer.ts",
- 1.3
- ],
- [
- "src/lib/observability/in-flight.ts",
- 1.3
- ]
- ]
- },
- {
- "startMs": 45451,
- "durationMs": 63,
- "byModule": [
- [
- "src/lib/validations/source-priority.ts",
- 15.8
- ],
- [
- "npm:@prisma/client",
- 10.5
- ],
- [
- "(native)",
- 8.4
- ],
- [
- "src/lib/tz/format.ts",
- 5.3
- ],
- [
- "node:internal/async_local_storage/async_hooks",
- 4.4
- ],
- [
- "node:buffer",
- 3
- ],
- [
- "npm:pg",
- 3
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 3
- ],
- [
- "npm:zod",
- 3
- ],
- [
- "npm:%40prisma",
- 1.5
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 1.5
- ],
- [
- "node:internal/stream_base_commons",
- 1.4
- ],
- [
- "npm:pg-protocol",
- 1.4
- ],
- [
- "node:internal/async_hooks",
- 1
- ]
- ],
- "byFunction": [
- [
- "I src/lib/validations/source-priority.ts",
- 13.1
- ],
- [
- "(garbage collector) (native)",
- 4.6
- ],
- [
- "Zs npm:@prisma/client",
- 4.5
- ],
- [
- "_propagate node:internal/async_local_storage/async_hooks",
- 4.4
- ],
- [
- "slice node:buffer",
- 3
- ],
- [
- "_ src/lib/personal-records/pr-detection-worker.ts",
- 3
- ],
- [
- "te npm:zod",
- 3
- ],
- [
- "h src/lib/tz/format.ts",
- 2.8
- ],
- [
- "utf8Slice (native)",
- 2.7
- ],
- [
- "d src/lib/tz/format.ts",
- 2.5
- ],
- [
- "D npm:%40prisma",
- 1.5
- ],
- [
- "_handleDataRow npm:pg",
- 1.5
- ],
- [
- "(anonymous) npm:@prisma/client",
- 1.5
- ],
- [
- "Tp npm:@prisma/client",
- 1.5
- ],
- [
- "(anonymous) src/lib/analytics/source-priority.ts",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 31.7
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 21.8
- ],
- [
- "src/lib/validations/source-priority.ts",
- 18.8
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 5.3
- ],
- [
- "src/lib/tz/format.ts",
- 5.3
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 5.3
- ]
- ]
- },
- {
- "startMs": 56854,
- "durationMs": 66,
- "byModule": [
- [
- "src/lib/validations/source-priority.ts",
- 15
- ],
- [
- "npm:@prisma/client",
- 13.7
- ],
- [
- "(native)",
- 6.6
- ],
- [
- "node:buffer",
- 6.6
- ],
- [
- "npm:next",
- 6
- ],
- [
- "npm:pg",
- 4.5
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 4.1
- ],
- [
- "src/lib/tz/format.ts",
- 3.1
- ],
- [
- "node:internal/process/task_queues",
- 1.5
- ],
- [
- "node:internal/async_hooks",
- 1.5
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 1.5
- ],
- [
- "npm:zod",
- 1.5
- ]
- ],
- "byFunction": [
- [
- "I src/lib/validations/source-priority.ts",
- 13.5
- ],
- [
- "slice node:buffer",
- 6.6
- ],
- [
- "Date1 npm:next",
- 6
- ],
- [
- "(anonymous) npm:@prisma/client",
- 4.6
- ],
- [
- "_ src/lib/personal-records/pr-detection-worker.ts",
- 4.1
- ],
- [
- "(program) (native)",
- 3.8
- ],
- [
- "_handleDataRow npm:pg",
- 3
- ],
- [
- "ni npm:@prisma/client",
- 3
- ],
- [
- "Zs npm:@prisma/client",
- 3
- ],
- [
- "d src/lib/tz/format.ts",
- 1.6
- ],
- [
- "nextTick node:internal/process/task_queues",
- 1.5
- ],
- [
- "utf8Slice (native)",
- 1.5
- ],
- [
- "_parseRowAsArray npm:pg",
- 1.5
- ],
- [
- "Dp npm:@prisma/client",
- 1.5
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 25.3
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 18.1
- ],
- [
- "src/lib/validations/source-priority.ts",
- 16.5
- ],
- [
- "src/lib/tz/format.ts",
- 3.1
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 3.1
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 3.1
- ]
- ]
- },
- {
- "startMs": 56923,
- "durationMs": 52,
- "byModule": [
- [
- "(native)",
- 20
- ],
- [
- "npm:@prisma/client",
- 10.1
- ],
- [
- "node:buffer",
- 3.7
- ],
- [
- "npm:pg",
- 3
- ],
- [
- "src/lib/tz/format.ts",
- 2.6
- ],
- [
- "npm:pg-protocol",
- 2.6
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 2.6
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 1.5
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 1.5
- ],
- [
- "npm:pg-types",
- 1.5
- ],
- [
- "npm:next",
- 1.4
- ],
- [
- "src/lib/validations/source-priority.ts",
- 1.1
- ]
- ],
- "byFunction": [
- [
- "(garbage collector) (native)",
- 20
- ],
- [
- "slice node:buffer",
- 3.7
- ],
- [
- "Zs npm:@prisma/client",
- 3
- ],
- [
- "(anonymous) npm:@prisma/client",
- 2.9
- ],
- [
- "(anonymous) src/lib/analytics/source-priority.ts",
- 2.6
- ],
- [
- "_handleDataRow npm:pg",
- 1.5
- ],
- [
- "_ src/lib/personal-records/pr-detection-worker.ts",
- 1.5
- ],
- [
- "h src/lib/tz/format.ts",
- 1.5
- ],
- [
- "(anonymous) npm:pg-protocol",
- 1.5
- ],
- [
- "(anonymous) src/lib/tz/zone-offset.ts",
- 1.5
- ],
- [
- "_parseRowAsArray npm:pg",
- 1.5
- ],
- [
- "noParse npm:pg-types",
- 1.5
- ],
- [
- "Fp npm:@prisma/client",
- 1.5
- ],
- [
- "Tp npm:@prisma/client",
- 1.4
- ],
- [
- "Date1 npm:next",
- 1.4
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 9.3
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 5.3
- ],
- [
- "src/lib/tz/format.ts",
- 4.1
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 4.1
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 2.6
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 1.5
- ],
- [
- "src/lib/validations/source-priority.ts",
- 1.1
- ]
- ]
- },
- {
- "startMs": 59508,
- "durationMs": 56,
- "byModule": [
- [
- "npm:@prisma/client",
- 15.7
- ],
- [
- "src/lib/validations/source-priority.ts",
- 9.7
- ],
- [
- "node:buffer",
- 7
- ],
- [
- "(native)",
- 6.1
- ],
- [
- "npm:pg-protocol",
- 4.5
- ],
- [
- "npm:next",
- 4.1
- ],
- [
- "src/lib/tz/format.ts",
- 3
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 3
- ],
- [
- "node:internal/async_hooks",
- 1.5
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 1.5
- ]
- ],
- "byFunction": [
- [
- "Fp npm:@prisma/client",
- 6.4
- ],
- [
- "I src/lib/validations/source-priority.ts",
- 6
- ],
- [
- "slice node:buffer",
- 5.5
- ],
- [
- "(anonymous) npm:@prisma/client",
- 4.6
- ],
- [
- "handlePacket npm:pg-protocol",
- 4.5
- ],
- [
- "Date1 npm:next",
- 4.1
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 3.8
- ],
- [
- "utf8Slice (native)",
- 3.1
- ],
- [
- "(garbage collector) (native)",
- 3
- ],
- [
- "(anonymous) src/lib/analytics/source-priority.ts",
- 3
- ],
- [
- "Zs npm:@prisma/client",
- 1.6
- ],
- [
- "request npm:@prisma/client",
- 1.5
- ],
- [
- "getEncodingOps node:buffer",
- 1.5
- ],
- [
- "Np npm:@prisma/client",
- 1.5
- ],
- [
- "d src/lib/tz/format.ts",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 21.7
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 15.8
- ],
- [
- "src/lib/validations/source-priority.ts",
- 9.7
- ],
- [
- "src/lib/tz/format.ts",
- 5.9
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 5.9
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 5.9
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 2.9
- ]
- ]
- },
- {
- "startMs": 61315,
- "durationMs": 62,
- "byModule": [
- [
- "src/lib/validations/source-priority.ts",
- 20.1
- ],
- [
- "npm:@prisma/client",
- 14.1
- ],
- [
- "(native)",
- 8.2
- ],
- [
- "npm:pg",
- 4.5
- ],
- [
- "node:buffer",
- 3.1
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 3
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 3
- ],
- [
- "npm:pg-protocol",
- 1.5
- ],
- [
- "npm:next",
- 1.5
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 1.4
- ],
- [
- "npm:zod",
- 1.3
- ]
- ],
- "byFunction": [
- [
- "I src/lib/validations/source-priority.ts",
- 15.6
- ],
- [
- "ni npm:@prisma/client",
- 5.7
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 4.5
- ],
- [
- "(program) (native)",
- 3.7
- ],
- [
- "utf8Slice (native)",
- 3
- ],
- [
- "Ae npm:@prisma/client",
- 3
- ],
- [
- "_ src/lib/personal-records/pr-detection-worker.ts",
- 3
- ],
- [
- "m src/lib/measurements/consolidation-base.ts",
- 3
- ],
- [
- "Zs npm:@prisma/client",
- 2.7
- ],
- [
- "getEncodingOps node:buffer",
- 1.5
- ],
- [
- "slice node:buffer",
- 1.5
- ],
- [
- "_parseRowAsArray npm:pg",
- 1.5
- ],
- [
- "(anonymous) npm:@prisma/client",
- 1.5
- ],
- [
- "(garbage collector) (native)",
- 1.5
- ],
- [
- "_handleDataRow npm:pg",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 30.3
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 22.8
- ],
- [
- "src/lib/validations/source-priority.ts",
- 21.4
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 4.5
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 1.5
- ],
- [
- "src/lib/tz/format.ts",
- 1.5
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 1.5
- ]
- ]
- },
- {
- "startMs": 72986,
- "durationMs": 57,
- "byModule": [
- [
- "src/lib/validations/source-priority.ts",
- 19.1
- ],
- [
- "npm:@prisma/client",
- 10.2
- ],
- [
- "(native)",
- 6.6
- ],
- [
- "npm:pg-protocol",
- 5.5
- ],
- [
- "npm:next",
- 3
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 3
- ],
- [
- "npm:pg",
- 2.5
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 1.5
- ],
- [
- "node:internal/async_local_storage/async_hooks",
- 1.5
- ],
- [
- "src/lib/tz/format.ts",
- 1.5
- ],
- [
- "node:buffer",
- 1.1
- ],
- [
- "node:events",
- 1.1
- ]
- ],
- "byFunction": [
- [
- "I src/lib/validations/source-priority.ts",
- 14.5
- ],
- [
- "handlePacket npm:pg-protocol",
- 4.5
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 4.5
- ],
- [
- "Zs npm:@prisma/client",
- 4.4
- ],
- [
- "(garbage collector) (native)",
- 4.1
- ],
- [
- "Date1 npm:next",
- 3
- ],
- [
- "Tp npm:@prisma/client",
- 3
- ],
- [
- "(anonymous) src/lib/analytics/source-priority.ts",
- 3
- ],
- [
- "_ src/lib/personal-records/pr-detection-worker.ts",
- 1.5
- ],
- [
- "_propagate node:internal/async_local_storage/async_hooks",
- 1.5
- ],
- [
- "addFields npm:pg",
- 1.5
- ],
- [
- "utf8Slice (native)",
- 1.5
- ],
- [
- "(anonymous) npm:@prisma/client",
- 1.5
- ],
- [
- "(anonymous) src/lib/tz/format.ts",
- 1.5
- ],
- [
- "Fp npm:@prisma/client",
- 1.2
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 29.6
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 23.5
- ],
- [
- "src/lib/validations/source-priority.ts",
- 19.1
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 4.6
- ],
- [
- "src/lib/tz/format.ts",
- 4.5
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 4.5
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 3
- ]
- ]
- },
- {
- "startMs": 73523,
- "durationMs": 51,
- "byModule": [
- [
- "src/lib/validations/source-priority.ts",
- 12
- ],
- [
- "npm:next",
- 8.9
- ],
- [
- "npm:@prisma/client",
- 6
- ],
- [
- "(native)",
- 4.5
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 4.2
- ],
- [
- "node:buffer",
- 4
- ],
- [
- "npm:pg",
- 2.1
- ],
- [
- "npm:pg-protocol",
- 1.5
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 1.5
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 1.5
- ],
- [
- "node:internal/async_hooks",
- 1.5
- ],
- [
- "npm:zod",
- 1.5
- ],
- [
- "src/lib/tz/format.ts",
- 1.1
- ],
- [
- "node:internal/event_target",
- 1
- ]
- ],
- "byFunction": [
- [
- "I src/lib/validations/source-priority.ts",
- 9
- ],
- [
- "Date1 npm:next",
- 7.5
- ],
- [
- "(anonymous) src/lib/analytics/source-priority.ts",
- 4.2
- ],
- [
- "slice node:buffer",
- 4
- ],
- [
- "(anonymous) npm:@prisma/client",
- 3
- ],
- [
- "(garbage collector) (native)",
- 3
- ],
- [
- "_parseRowAsArray npm:pg",
- 2.1
- ],
- [
- "handlePacket npm:pg-protocol",
- 1.5
- ],
- [
- "Fp npm:@prisma/client",
- 1.5
- ],
- [
- "_ src/lib/personal-records/pr-detection-worker.ts",
- 1.5
- ],
- [
- "(anonymous) src/lib/tz/zone-offset.ts",
- 1.5
- ],
- [
- "getOrSetAsyncId node:internal/async_hooks",
- 1.5
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 1.5
- ],
- [
- "te npm:zod",
- 1.5
- ],
- [
- "ni npm:@prisma/client",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 27.8
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 17.7
- ],
- [
- "src/lib/validations/source-priority.ts",
- 13.5
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 8.6
- ],
- [
- "src/lib/tz/format.ts",
- 7.1
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 7.1
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 6
- ]
- ]
- },
- {
- "startMs": 75393,
- "durationMs": 100,
- "byModule": [
- [
- "(native)",
- 32.6
- ],
- [
- "npm:@prisma/client",
- 17.2
- ],
- [
- "src/lib/validations/source-priority.ts",
- 14.6
- ],
- [
- "npm:next",
- 7.5
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 6.1
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 5.6
- ],
- [
- "npm:pg-protocol",
- 4.1
- ],
- [
- "npm:pg",
- 2.5
- ],
- [
- "src/lib/tz/format.ts",
- 2.3
- ],
- [
- "node:internal/async_local_storage/async_hooks",
- 1.5
- ],
- [
- "npm:zod",
- 1.5
- ],
- [
- "node:buffer",
- 1.5
- ],
- [
- "npm:%40prisma",
- 1.5
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 1.5
- ]
- ],
- "byFunction": [
- [
- "(garbage collector) (native)",
- 28
- ],
- [
- "I src/lib/validations/source-priority.ts",
- 10.4
- ],
- [
- "Date1 npm:next",
- 7.5
- ],
- [
- "(anonymous) npm:@prisma/client",
- 5.7
- ],
- [
- "Fp npm:@prisma/client",
- 4.5
- ],
- [
- "Zs npm:@prisma/client",
- 4.5
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 4.3
- ],
- [
- "(anonymous) src/lib/tz/zone-offset.ts",
- 4.1
- ],
- [
- "handlePacket npm:pg-protocol",
- 4.1
- ],
- [
- "_ src/lib/personal-records/pr-detection-worker.ts",
- 3
- ],
- [
- "(anonymous) src/lib/personal-records/pr-detection-worker.ts",
- 3
- ],
- [
- "_parseRowAsArray npm:pg",
- 2.5
- ],
- [
- "runMicrotasks (native)",
- 2
- ],
- [
- "_propagate node:internal/async_local_storage/async_hooks",
- 1.5
- ],
- [
- "runInChildSpan npm:@prisma/client",
- 1.5
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 34.6
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 24.3
- ],
- [
- "src/lib/validations/source-priority.ts",
- 16.1
- ],
- [
- "src/lib/tz/format.ts",
- 10.9
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 10.9
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 8.6
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 5.7
- ]
- ]
- },
- {
- "startMs": 78085,
- "durationMs": 54,
- "byModule": [
- [
- "src/lib/tz/zone-offset.ts",
- 26.6
- ],
- [
- "src/lib/validations/source-priority.ts",
- 13.5
- ],
- [
- "npm:@prisma/client",
- 4.2
- ],
- [
- "(native)",
- 4.1
- ],
- [
- "node:internal/async_local_storage/async_hooks",
- 1.5
- ],
- [
- "src/lib/tz/format.ts",
- 1.5
- ],
- [
- "src/lib/tz/intl-cache.ts",
- 1.5
- ],
- [
- "npm:pg",
- 1.1
- ]
- ],
- "byFunction": [
- [
- "l src/lib/tz/zone-offset.ts",
- 25.1
- ],
- [
- "I src/lib/validations/source-priority.ts",
- 10.6
- ],
- [
- "(garbage collector) (native)",
- 4.1
- ],
- [
- "ni npm:@prisma/client",
- 1.5
- ],
- [
- "e.s.T src/lib/validations/source-priority.ts",
- 1.5
- ],
- [
- "_propagate node:internal/async_local_storage/async_hooks",
- 1.5
- ],
- [
- "(anonymous) src/lib/tz/zone-offset.ts",
- 1.5
- ],
- [
- "c src/lib/tz/format.ts",
- 1.5
- ],
- [
- "r src/lib/tz/intl-cache.ts",
- 1.5
- ],
- [
- "Zs npm:@prisma/client",
- 1.5
- ],
- [
- "(anonymous) src/lib/validations/source-priority.ts",
- 1.4
- ],
- [
- "Tp npm:@prisma/client",
- 1.2
- ],
- [
- "_handleDataRow npm:pg",
- 1.1
- ]
- ],
- "byEntry": [
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 44.7
- ],
- [
- "src/lib/tz/format.ts",
- 29.6
- ],
- [
- "src/lib/measurements/consolidation-tz.ts",
- 29.6
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 29.6
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 28.1
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 15
- ],
- [
- "src/lib/validations/source-priority.ts",
- 13.5
- ],
- [
- "src/lib/tz/intl-cache.ts",
- 1.5
- ]
- ]
- }
- ],
- "bootSelf": [
- [
- "(native)",
- 853
- ],
- [
- "npm:@prisma/client",
- 295.9
- ],
- [
- "node:internal/modules/cjs/loader",
- 248.7
- ],
- [
- "src/lib/validations/source-priority.ts",
- 210.3
- ],
- [
- "npm:next",
- 188.4
- ],
- [
- "turbopack:///[turbopack]/nodejs/runtime/build-base.ts",
- 121.5
- ],
- [
- "src/lib/tz/zone-offset.ts",
- 83.2
- ],
- [
- "wasm://wasm/00d1cb12",
- 81.4
- ],
- [
- "npm:pg",
- 78.5
- ],
- [
- "node:buffer",
- 59.1
- ],
- [
- "src/lib/tz/format.ts",
- 55
- ],
- [
- "npm:zod",
- 50.7
- ],
- [
- "src/lib/personal-records/pr-detection-worker.ts",
- 48
- ],
- [
- "npm:pg-protocol",
- 37
- ],
- [
- "node:internal/bootstrap/realm",
- 32
- ],
- [
- "src/lib/analytics/source-priority.ts",
- 31.7
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/async-module.ts",
- 27.7
- ],
- [
- "node:internal/modules/esm/utils",
- 26
- ],
- [
- "npm:luxon",
- 22.1
- ],
- [
- "node:internal/async_hooks",
- 21.7
- ],
- [
- "node:internal/async_local_storage/async_hooks",
- 21.2
- ],
- [
- "node:fs",
- 18.8
- ],
- [
- "npm:pg-boss",
- 18.3
- ],
- [
- "npm:pg-pool",
- 18.1
- ],
- [
- "npm:yaml",
- 15
- ],
- [
- "src/lib/measurements/consolidation-base.ts",
- 13.8
- ],
- [
- "npm:%40prisma",
- 13.2
- ],
- [
- "node:internal/timers",
- 13.1
- ],
- [
- "node:internal/process/task_queues",
- 12.6
- ],
- [
- "turbopack:///[turbopack]/shared/runtime/runtime-utils.ts",
- 12
- ],
- [
- "node:internal/deps/undici/undici",
- 11.9
- ],
- [
- "node:events",
- 11.8
- ],
- [
- "npm:cron-parser",
- 10
- ],
- [
- "node:internal/streams/writable",
- 9
- ],
- [
- "node:net",
- 8.5
- ],
- [
- "src/lib/i18n/server-catalogs.ts",
- 8.2
- ],
- [
- "node:internal/stream_base_commons",
- 7.2
- ],
- [
- "src/lib/tz/intl-cache.ts",
- 5.6
- ],
- [
- "node:internal/url",
- 5
- ],
- [
- "node:internal/modules/package_json_reader",
- 4.6
- ]
- ],
- "heap": {
- "probes": {
- "parsed locale catalogs": {
- "count": 2,
- "retainedMb": 1.6
- }
- },
- "totalMb": 159.5,
- "nodeCount": 1302659,
- "byType": [
- [
- "string",
- 67.5
- ],
- [
- "array",
- 26.6
- ],
- [
- "code",
- 19.2
- ],
- [
- "object",
- 15.9
- ],
- [
- "hidden",
- 9.2
- ],
- [
- "native",
- 8
- ],
- [
- "object shape",
- 5.9
- ],
- [
- "closure",
- 5.2
- ],
- [
- "concatenated string",
- 1.3
- ],
- [
- "number",
- 0.5
- ],
- [
- "sliced string",
- 0.2
- ],
- [
- "regexp",
- 0.2
- ],
- [
- "synthetic",
- 0
- ],
- [
- "symbol",
- 0
- ],
- [
- "bigint",
- 0
- ]
- ],
- "byConstructor": [
- [
- "array:(object elements)",
- 20.8,
- 25651
- ],
- [
- "object:Object",
- 11.4,
- 293893
- ],
- [
- "native:system / JSArrayBufferData",
- 7.5,
- 170
- ],
- [
- "array:(object properties)",
- 3.3,
- 6517
- ],
- [
- "array:",
- 2.5,
- 10169
- ],
- [
- "closure:",
- 1.6,
- 29121
- ],
- [
- "object:system / Context",
- 1.6,
- 28886
- ],
- [
- "closure:native_bind",
- 1,
- 20804
- ],
- [
- "object:Array",
- 0.6,
- 20938
- ],
- [
- "object:Date1",
- 0.5,
- 5332
- ],
- [
- "object:a",
- 0.4,
- 4438
- ],
- [
- "native:Node / histogram",
- 0.4,
- 1
- ],
- [
- "object:d",
- 0.3,
- 4425
- ],
- [
- "closure:get",
- 0.3,
- 4732
- ],
- [
- "object:Set",
- 0.2,
- 5670
- ],
- [
- "object:Module",
- 0.2,
- 1963
- ],
- [
- "closure:set",
- 0.2,
- 3428
- ],
- [
- "closure:n",
- 0.1,
- 1572
- ],
- [
- "closure:i",
- 0.1,
- 1578
- ],
- [
- "object:Promise",
- 0.1,
- 1341
- ],
- [
- "object:n",
- 0.1,
- 1881
- ],
- [
- "object:ZodOptional",
- 0.1,
- 236
- ],
- [
- "object:_TstNode",
- 0.1,
- 939
- ],
- [
- "native:Node / parser_buffer",
- 0.1,
- 1
- ],
- [
- "object:TCP",
- 0,
- 9
- ],
- [
- "closure:callbackTrampoline",
- 0,
- 1
- ],
- [
- "closure:promiseInitHook",
- 0,
- 1
- ],
- [
- "closure:promiseBeforeHook",
- 0,
- 1
- ],
- [
- "closure:promiseAfterHook",
- 0,
- 1
- ],
- [
- "object:global",
- 0,
- 4
- ]
- ],
- "largestStrings": [
- [
- 5.6,
- "module.exports=[234494,A=>{\"use strict\";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i,bgBlack:"
- ],
- [
- 5.6,
- "module.exports=[17856,A=>{\"use strict\";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i,bgBlack:("
- ],
- [
- 5.6,
- "module.exports=[17856,A=>{\"use strict\";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i,bgBlack:("
- ],
- [
- 4.4,
- "const wasm = \"AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/fwBgBX9/f39/AGABfwF/YAZ/f3"
- ],
- [
- 4.4,
- "AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/fwBgBX9/f39/AGABfwF/YAZ/f39/f38AYAR/f39/"
- ],
- [
- 1.9,
- "lliADOAUngIEAACgKwAgBgAAoSsAIAcAAKErACAIAACiKwAgCQAAoysAIAoAAKQrACALAAClKwAgDAAApisAIA0AAKcrACARAACS"
- ],
- [
- 1.3,
- "(()=>{var e,t,r={\"./dist/build/webpack/alias/react-dom-server.js\"(e,t,r){\"use strict\";var n;function"
- ],
- [
- 1.2,
- "module.exports=[842865,(e,t,r)=>{let i=e.r(522734);t.exports=function(e){return e?/-----BEGIN ([A-Z\\"
- ],
- [
- 1.2,
- "module.exports=[842865,(e,t,r)=>{let i=e.r(522734);t.exports=function(e){return e?/-----BEGIN ([A-Z\\"
- ],
- [
- 1.1,
- "module.exports=[799092,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),Objec"
- ],
- [
- 1.1,
- "// v1.4.34 web-freeze. Schema is locked additive-only until iOS launch // + v1.5.0 marker. See .plan"
- ],
- [
- 1.1,
- "module.exports=[971743,(A,e,t)=>{\"use strict\";let r=A.r(57328),{addAbortListener:s}=A.r(687769),{Rea"
- ],
- [
- 1,
- "module.exports=[835717,(A,e,t)=>{\"use strict\";let r=A.r(57328),{addAbortListener:s}=A.r(687769),{Rea"
- ],
- [
- 0.8,
- "module.exports=[767189,e=>e.a(async(t,a)=>{try{var n=e.i(64105),r=e.i(563616),i=t([n]);[n]=i.then?(a"
- ],
- [
- 0.7,
- "module.exports=[594108,806176,674750,98746,t=>{\"use strict\";var i;let r;function n(t){return t}funct"
- ],
- [
- 0.7,
- "module.exports=[144056,t=>{\"use strict\";let i,n;function r(t){return t}function a(t){return t}functi"
- ],
- [
- 0.6,
- "module.exports=[992657,e=>e.a(async(t,a)=>{try{var r=e.i(851258),n=t([r]);async function i(e,t,a,n){"
- ],
- [
- 0.5,
- "module.exports=[388046,e=>{\"use strict\";class t{value;next;constructor(e){this.value=e}}class n{#e;#"
- ],
- [
- 0.5,
- "module.exports=[388046,e=>{\"use strict\";class t{value;next;constructor(e){this.value=e}}class n{#e;#"
- ],
- [
- 0.5,
- "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def"
- ],
- [
- 0.5,
- "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def"
- ],
- [
- 0.5,
- "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def"
- ],
- [
- 0.5,
- "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def"
- ],
- [
- 0.5,
- "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def"
- ],
- [
- 0.5,
- "module.exports=[120630,(e,t,n)=>{\"use strict\";Object.defineProperty(n,\"__esModule\",{value:!0}),n.def"
- ]
- ],
- "topRetainers": [
- {
- "mb": 34.6,
- "name": "object:yr",
- "path": "[1] > [10] > 22 > extension > global > prisma > [1] > _engine"
- },
- {
- "mb": 7.4,
- "name": "object:system / Context",
- "path": "Node / PrincipalRealm > base_object_list > Node / BaseObjectList > [25] > Node / ModuleWrap > native_to_javascript > ModuleWrap > 2 > [4] > context"
- },
- {
- "mb": 5.9,
- "name": "object:Promise",
- "path": "Object > executor > e > #r > gr > transactions > Map > table > > 6 > closing"
- },
- {
- "mb": 5.6,
- "name": "object:Module",
- "path": "[1] > [10] > 99 > process > getBuiltinModule > getBuiltinModule > context > system / Context > $Module > Module > _cache > .next/server/chunks/_1mg8fms._.js"
- },
- {
- "mb": 5.6,
- "name": "code:.next/server/chu",
- "path": "Module > _cache > Object > .next/server/chunks/_1kwfnjg._.js > Module > exports > Array > [1] > > shared > script"
- },
- {
- "mb": 5.6,
- "name": "code:.next/server/chu",
- "path": "global > prisma > [1] > t > _engineConfig > Object > compilerWasm > Object > getRuntime > getRuntime > shared > script"
- },
- {
- "mb": 4.4,
- "name": "object:Generator",
- "path": "[6] > principal_realm > Node / PrincipalRealm > base_object_list > Node / BaseObjectList > [26] > Node / ModuleWrap > native_to_javascript > ModuleWrap > 2 > [4]"
- },
- {
- "mb": 4.4,
- "name": "string:AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/",
- "path": "[1] > [21] > 60025"
- },
- {
- "mb": 3.4,
- "name": "object:Object",
- "path": "system / Context > $Module > Module > _cache > Object > .next/server/chunks/[turbopack]_runtime.js > Module > exports > > context > moduleCache"
- },
- {
- "mb": 2.6,
- "name": "object:global",
- "path": "[1] > [10] > 22 > extension"
- }
- ],
- "sourceStringsMb": 39.8
- }
-}
diff --git a/scripts/bench/results/v1.45.0-boot-final.md b/scripts/bench/results/v1.45.0-boot-final.md
deleted file mode 100644
index f1bdaed56..000000000
--- a/scripts/bench/results/v1.45.0-boot-final.md
+++ /dev/null
@@ -1,804 +0,0 @@
-# Boot profile v1.45.0-boot-final
-
-Commit a57fa8335, 2026-10-22T06:58:52.591Z. Production build, every role, heap ceiling 792 MB, 300 s idle. 14 cores, load average 5.6,6.4,11.1 at the end.
-
-- first answer from /api/health: 1325 ms; background role ready: 3217 ms
-- event-loop stalls logged: 0, max 0 ms
-
-## Stalls
-
-
-## Heap samples
-
-| at s | used MB | old | large obj | code | external | array buffers | rss |
-| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
-| 6 | 225 | 158 | 54 | 4 | 11 | 0 | 500 |
-| 306 | 158 | 96 | 53 | 5 | 11 | 0 | 788 |
-
-## Full collections (mark-compact)
-
-98 mark-compacts during the run. Used heap before the collection, and what survived it (the live heap):
-
-| at s | before MB | after MB |
-| ---: | ---: | ---: |
-| 0 | 21.5 | 17.5 |
-| 8 | 203.8 | 146.5 |
-| 24 | 236.5 | 142.5 |
-| 41 | 223.8 | 144.9 |
-| 60 | 225.8 | 147.5 |
-| 85 | 224.4 | 147.7 |
-| 124 | 225.4 | 151.2 |
-| 167 | 176.7 | 151 |
-| 209 | 230.1 | 150.4 |
-| 242 | 197.9 | 151.5 |
-| 273 | 224.8 | 152.2 |
-| 302 | 177.9 | 152.5 |
-| 312 | 170.5 | 152.9 |
-| 327 | 170.6 | 152 |
-| 338 | 169.7 | 151.8 |
-
-## Main-thread busy stretches over 50 ms without an idle sample, first two minutes
-
-### at 5 ms, 396 ms
-
-| self time by module | ms |
-| --- | ---: |
-| (native) | 230.2 |
-| node:internal/modules/cjs/loader | 57.8 |
-| npm:next | 52.3 |
-| node:internal/bootstrap/realm | 29.3 |
-| node:internal/deps/undici/undici | 8.9 |
-| node:internal/modules/esm/loader | 4.5 |
-| node:fs | 3 |
-| node:internal/net | 2.5 |
-| node:internal/bootstrap/switches/is_main_thread | 2 |
-| node:os | 1.8 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-
-### at 472 ms, 336 ms
-
-| self time by module | ms |
-| --- | ---: |
-| (native) | 121.3 |
-| wasm://wasm/00d1cb12 | 43 |
-| node:internal/modules/cjs/loader | 40.5 |
-| npm:next | 31.7 |
-| npm:@prisma/client | 23.8 |
-| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 23.4 |
-| node:internal/modules/esm/utils | 17.3 |
-| npm:zod | 5.9 |
-| turbopack:///[turbopack]/shared/runtime/runtime-utils.ts | 3 |
-| node:vm | 1.7 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/instrumentation.ts | 95.8 |
-| src/lib/db-session.ts | 47.3 |
-| src/lib/process-type.ts | 27.1 |
-| src/lib/boot/startup-state.ts | 25.5 |
-| src/lib/boot/boot-steps.ts | 25.5 |
-| src/lib/logging/background.ts | 25.5 |
-| src/lib/logging/redact.ts | 13 |
-| src/lib/logging/context.ts | 11 |
-| src/lib/validations/measurement.ts | 10 |
-| src/generated/prisma/internal/class.ts | 4.7 |
-
-### at 811 ms, 225 ms
-
-| self time by module | ms |
-| --- | ---: |
-| node:internal/modules/cjs/loader | 52.3 |
-| (native) | 48.2 |
-| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 37 |
-| npm:next | 22.2 |
-| npm:zod | 12.5 |
-| node:internal/modules/esm/utils | 8.7 |
-| turbopack:///[turbopack]/shared/runtime/async-module.ts | 5.6 |
-| node:fs | 4.5 |
-| node:internal/async_hooks | 3 |
-| node:internal/modules/esm/translators | 2.6 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/app/api/health/route.ts | 62 |
-| src/lib/api-errors.ts | 25.9 |
-| src/lib/auth/secure-cookie.ts | 14.9 |
-| src/lib/logging/background.ts | 13.4 |
-| src/lib/sharing/acting-session.ts | 12.8 |
-| src/lib/validations/measurement.ts | 11.9 |
-| src/lib/db-session.ts | 11.3 |
-| src/lib/export/environment-backup.ts | 11.3 |
-| src/lib/export/full-backup-payload.ts | 11.3 |
-| src/lib/jobs/job-failures.ts | 11.3 |
-
-### at 1063 ms, 246 ms
-
-| self time by module | ms |
-| --- | ---: |
-| (native) | 64.7 |
-| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 47.2 |
-| node:internal/modules/cjs/loader | 31.6 |
-| npm:luxon | 18 |
-| turbopack:///[turbopack]/shared/runtime/async-module.ts | 15.3 |
-| npm:yaml | 15 |
-| npm:zod | 10.2 |
-| src/lib/i18n/server-catalogs.ts | 8.2 |
-| turbopack:///[turbopack]/shared/runtime/runtime-utils.ts | 4.5 |
-| npm:cron-parser | 4 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/jobs/reminder-worker.ts | 137.6 |
-| src/lib/jobs/reminder/google-health-sync.ts | 78 |
-| src/lib/whoop/sync.ts | 65.9 |
-| src/instrumentation.ts | 53.8 |
-| src/lib/insights/bucket-series.ts | 47.1 |
-| src/lib/whoop/oauth-descriptor.ts | 47.1 |
-| src/lib/whoop/sync-core.ts | 47.1 |
-| src/lib/whoop/sync-body.ts | 47.1 |
-| src/lib/import/unzip-export-xml.ts | 35.3 |
-| src/lib/jobs/integration-status-null-probe.ts | 35.3 |
-
-### at 1363 ms, 68 ms
-
-| self time by module | ms |
-| --- | ---: |
-| node:internal/modules/cjs/loader | 29.2 |
-| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 6.7 |
-| (native) | 5.9 |
-| turbopack:///[turbopack]/shared/runtime/runtime-utils.ts | 4.5 |
-| src/lib/daily/today-overview.ts | 4.1 |
-| node:fs | 3 |
-| turbopack:///[turbopack]/shared/runtime/async-module.ts | 3 |
-| node:internal/streams/writable | 1.5 |
-| src/lib/jobs/reminder/insights-handlers.ts | 1.5 |
-| npm:next | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/jobs/reminder-worker.ts | 62.5 |
-| src/lib/jobs/reminder/insights-handlers.ts | 24.4 |
-| src/lib/ai/prompts/arrival-reaction.ts | 12.8 |
-| src/lib/daily/score-steady.ts | 11.3 |
-| src/lib/dashboard/bands.ts | 7.1 |
-| src/lib/dashboard/snapshot-read.ts | 5.8 |
-| src/lib/jobs/data-arrival.ts | 4.5 |
-| src/lib/daily/today-overview.ts | 4.1 |
-| src/lib/daily/priority-item-key.ts | 4.1 |
-| src/lib/ai/prompts/status-batch.ts | 3 |
-
-### at 1545 ms, 57 ms
-
-| self time by module | ms |
-| --- | ---: |
-| node:internal/modules/cjs/loader | 25.3 |
-| (native) | 12.9 |
-| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 7.2 |
-| node:fs | 4.2 |
-| node:buffer | 1.5 |
-| npm:zod | 1.5 |
-| src/lib/jobs/pr-detection.ts | 1.5 |
-| turbopack:///[turbopack]/shared/runtime/async-module.ts | 1.5 |
-| src/lib/jobs/measurement-maintenance.ts | 1.3 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/jobs/reminder-worker.ts | 55.3 |
-| src/lib/jobs/reminder/ops-handlers.ts | 13 |
-| src/lib/export/restore-backup.ts | 3 |
-| src/lib/medications/ingredients/label-extract.ts | 3 |
-| src/lib/environment/service.ts | 1.5 |
-| src/lib/export/backup-blob.ts | 1.5 |
-| src/lib/jobs/pr-detection.ts | 1.5 |
-| src/lib/jobs/document-content-index-backfill.ts | 1.5 |
-| src/lib/labs/ocr-extract.ts | 1.5 |
-| src/lib/jobs/reminder/backup-handlers.ts | 1.5 |
-
-### at 1820 ms, 114 ms
-
-| self time by module | ms |
-| --- | ---: |
-| src/lib/tz/zone-offset.ts | 36 |
-| npm:@prisma/client | 18.2 |
-| src/lib/validations/source-priority.ts | 18.1 |
-| (native) | 13.1 |
-| src/lib/personal-records/pr-detection-worker.ts | 7.2 |
-| node:buffer | 4.1 |
-| src/lib/analytics/source-priority.ts | 3.7 |
-| npm:zod | 2.3 |
-| npm:pg | 2 |
-| src/lib/tz/format.ts | 1.9 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 73.3 |
-| src/lib/tz/format.ts | 40.7 |
-| src/lib/measurements/consolidation-tz.ts | 40.7 |
-| src/lib/tz/zone-offset.ts | 38.8 |
-| src/lib/measurements/consolidation-base.ts | 38.1 |
-| src/lib/analytics/source-priority.ts | 28.1 |
-| src/lib/validations/source-priority.ts | 20.4 |
-| src/lib/tz/intl-cache.ts | 1.5 |
-
-### at 2407 ms, 50 ms
-
-| self time by module | ms |
-| --- | ---: |
-| (native) | 18.9 |
-| src/lib/validations/source-priority.ts | 14.1 |
-| src/lib/analytics/source-priority.ts | 3.7 |
-| src/lib/personal-records/pr-detection-worker.ts | 3 |
-| npm:next | 1.8 |
-| npm:%40prisma | 1.5 |
-| src/lib/tz/format.ts | 1.5 |
-| node:internal/async_local_storage/async_hooks | 1.5 |
-| npm:pg | 1.5 |
-| npm:@prisma/client | 1.3 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 26.6 |
-| src/lib/analytics/source-priority.ts | 19.3 |
-| src/lib/validations/source-priority.ts | 14.1 |
-| src/lib/tz/format.ts | 3 |
-| src/lib/measurements/consolidation-tz.ts | 3 |
-| src/lib/measurements/consolidation-base.ts | 2.8 |
-| src/lib/tz/zone-offset.ts | 1.5 |
-
-### at 18192 ms, 59 ms
-
-| self time by module | ms |
-| --- | ---: |
-| (native) | 15.5 |
-| npm:@prisma/client | 13.9 |
-| src/lib/validations/source-priority.ts | 11.8 |
-| npm:pg | 3.6 |
-| node:events | 3 |
-| node:buffer | 2.5 |
-| src/lib/measurements/consolidation-base.ts | 1.5 |
-| src/lib/personal-records/pr-detection-worker.ts | 1.5 |
-| node:internal/async_hooks | 1.5 |
-| npm:pg-types | 1.4 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 18.9 |
-| src/lib/analytics/source-priority.ts | 12.9 |
-| src/lib/validations/source-priority.ts | 11.8 |
-| src/lib/measurements/consolidation-base.ts | 4.5 |
-| src/lib/tz/zone-offset.ts | 2.6 |
-| src/lib/tz/format.ts | 2.6 |
-| src/lib/measurements/consolidation-tz.ts | 2.6 |
-
-### at 19914 ms, 65 ms
-
-| self time by module | ms |
-| --- | ---: |
-| src/lib/validations/source-priority.ts | 18.1 |
-| (native) | 12.3 |
-| npm:@prisma/client | 11.6 |
-| npm:next | 5.9 |
-| src/lib/personal-records/pr-detection-worker.ts | 3 |
-| src/lib/tz/format.ts | 3 |
-| npm:pg | 2.6 |
-| npm:pg-protocol | 2.5 |
-| node:internal/async_hooks | 1.5 |
-| src/lib/analytics/source-priority.ts | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 29.8 |
-| src/lib/analytics/source-priority.ts | 22.6 |
-| src/lib/validations/source-priority.ts | 18.1 |
-| src/lib/tz/format.ts | 5.8 |
-| src/lib/measurements/consolidation-tz.ts | 5.8 |
-| src/lib/tz/zone-offset.ts | 2.8 |
-| src/lib/measurements/consolidation-base.ts | 2.8 |
-
-### at 20296 ms, 55 ms
-
-| self time by module | ms |
-| --- | ---: |
-| (native) | 22.4 |
-| npm:@prisma/client | 14.8 |
-| node:buffer | 3 |
-| npm:zod | 3 |
-| src/lib/measurements/consolidation-base.ts | 3 |
-| src/lib/tz/format.ts | 1.5 |
-| node:internal/stream_base_commons | 1.5 |
-| src/lib/analytics/source-priority.ts | 1.5 |
-| src/lib/personal-records/pr-detection-worker.ts | 1.5 |
-| npm:pg-protocol | 1.3 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 10.5 |
-| src/lib/analytics/source-priority.ts | 6 |
-| src/lib/validations/source-priority.ts | 3 |
-| src/lib/measurements/consolidation-base.ts | 3 |
-| src/lib/tz/format.ts | 1.5 |
-| src/lib/measurements/consolidation-tz.ts | 1.5 |
-
-### at 30289 ms, 69 ms
-
-| self time by module | ms |
-| --- | ---: |
-| src/lib/validations/source-priority.ts | 18.3 |
-| (native) | 12.9 |
-| npm:@prisma/client | 8.8 |
-| node:buffer | 6.8 |
-| src/lib/personal-records/pr-detection-worker.ts | 4.5 |
-| src/lib/tz/format.ts | 4 |
-| npm:pg-protocol | 3 |
-| npm:zod | 3 |
-| node:timers | 1.5 |
-| node:internal/async_hooks | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 33 |
-| src/lib/analytics/source-priority.ts | 24.3 |
-| src/lib/validations/source-priority.ts | 21.3 |
-| src/lib/tz/format.ts | 5.6 |
-| src/lib/measurements/consolidation-tz.ts | 5.6 |
-| src/lib/measurements/consolidation-base.ts | 2.6 |
-| src/lib/tz/zone-offset.ts | 1.5 |
-
-### at 31004 ms, 68 ms
-
-| self time by module | ms |
-| --- | ---: |
-| (native) | 17.8 |
-| src/lib/validations/source-priority.ts | 14.9 |
-| npm:@prisma/client | 7.6 |
-| npm:next | 4.5 |
-| node:internal/async_hooks | 4.5 |
-| npm:pg-protocol | 4.1 |
-| npm:pg | 3 |
-| src/lib/tz/zone-offset.ts | 3 |
-| node:buffer | 2.7 |
-| src/lib/analytics/source-priority.ts | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 26.4 |
-| src/lib/analytics/source-priority.ts | 19 |
-| src/lib/validations/source-priority.ts | 16.2 |
-| src/lib/measurements/consolidation-base.ts | 6.1 |
-| src/lib/tz/format.ts | 5.8 |
-| src/lib/measurements/consolidation-tz.ts | 5.8 |
-| src/lib/tz/zone-offset.ts | 4.5 |
-
-### at 35350 ms, 65 ms
-
-| self time by module | ms |
-| --- | ---: |
-| src/lib/validations/source-priority.ts | 17 |
-| npm:@prisma/client | 11.8 |
-| (native) | 8.6 |
-| node:buffer | 6.5 |
-| src/lib/tz/format.ts | 4.5 |
-| npm:pg-protocol | 3 |
-| src/lib/personal-records/pr-detection-worker.ts | 3 |
-| npm:%40prisma | 1.6 |
-| npm:pg | 1.5 |
-| src/lib/analytics/source-priority.ts | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 26 |
-| src/lib/analytics/source-priority.ts | 20 |
-| src/lib/validations/source-priority.ts | 17 |
-| src/lib/tz/format.ts | 4.5 |
-| src/lib/measurements/consolidation-tz.ts | 4.5 |
-| src/lib/measurements/consolidation-base.ts | 3 |
-
-### at 36452 ms, 57 ms
-
-| self time by module | ms |
-| --- | ---: |
-| (native) | 20 |
-| src/lib/validations/source-priority.ts | 16 |
-| npm:@prisma/client | 7.3 |
-| node:internal/per_context/primordials | 2.8 |
-| npm:%40prisma | 1.5 |
-| npm:next | 1.5 |
-| src/lib/tz/zone-offset.ts | 1.5 |
-| node:internal/async_hooks | 1.5 |
-| node:internal/streams/readable | 1.5 |
-| npm:pg-protocol | 1.2 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 21.2 |
-| src/lib/validations/source-priority.ts | 16 |
-| src/lib/analytics/source-priority.ts | 16 |
-| src/lib/tz/format.ts | 2.5 |
-| src/lib/measurements/consolidation-tz.ts | 2.5 |
-| src/lib/measurements/consolidation-base.ts | 2.5 |
-| src/lib/tz/zone-offset.ts | 1.5 |
-
-### at 38744 ms, 56 ms
-
-| self time by module | ms |
-| --- | ---: |
-| src/lib/validations/source-priority.ts | 13.3 |
-| (native) | 9.9 |
-| npm:@prisma/client | 9.2 |
-| node:buffer | 4.7 |
-| npm:pg-protocol | 4.3 |
-| node:events | 1.9 |
-| npm:pg | 1.6 |
-| npm:%40prisma | 1.5 |
-| npm:next | 1.5 |
-| src/lib/tz/format.ts | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 21.8 |
-| src/lib/analytics/source-priority.ts | 16 |
-| src/lib/validations/source-priority.ts | 13.3 |
-| src/lib/measurements/consolidation-base.ts | 4.3 |
-| src/lib/tz/format.ts | 3 |
-| src/lib/measurements/consolidation-tz.ts | 3 |
-| src/lib/tz/zone-offset.ts | 1.4 |
-
-### at 42458 ms, 50 ms
-
-| self time by module | ms |
-| --- | ---: |
-| npm:@prisma/client | 13.5 |
-| src/lib/validations/source-priority.ts | 10.1 |
-| (native) | 7.6 |
-| npm:pg-protocol | 3.2 |
-| node:events | 3 |
-| src/lib/personal-records/pr-detection-worker.ts | 3 |
-| src/lib/tz/format.ts | 3 |
-| npm:pg | 2.8 |
-| node:internal/async_hooks | 1.5 |
-| npm:%40prisma | 1.4 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 17.3 |
-| src/lib/analytics/source-priority.ts | 12.7 |
-| src/lib/validations/source-priority.ts | 10.1 |
-| src/lib/tz/format.ts | 3 |
-| src/lib/measurements/consolidation-tz.ts | 3 |
-| src/lib/measurements/consolidation-base.ts | 1.5 |
-| src/lib/jobs/job-lock.ts | 1.3 |
-| src/lib/jobs/long-pass.ts | 1.3 |
-| src/lib/jobs/run-job.ts | 1.3 |
-| src/lib/jobs/job-observer.ts | 1.3 |
-
-### at 45451 ms, 63 ms
-
-| self time by module | ms |
-| --- | ---: |
-| src/lib/validations/source-priority.ts | 15.8 |
-| npm:@prisma/client | 10.5 |
-| (native) | 8.4 |
-| src/lib/tz/format.ts | 5.3 |
-| node:internal/async_local_storage/async_hooks | 4.4 |
-| node:buffer | 3 |
-| npm:pg | 3 |
-| src/lib/personal-records/pr-detection-worker.ts | 3 |
-| npm:zod | 3 |
-| npm:%40prisma | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 31.7 |
-| src/lib/analytics/source-priority.ts | 21.8 |
-| src/lib/validations/source-priority.ts | 18.8 |
-| src/lib/measurements/consolidation-base.ts | 5.3 |
-| src/lib/tz/format.ts | 5.3 |
-| src/lib/measurements/consolidation-tz.ts | 5.3 |
-
-### at 56854 ms, 66 ms
-
-| self time by module | ms |
-| --- | ---: |
-| src/lib/validations/source-priority.ts | 15 |
-| npm:@prisma/client | 13.7 |
-| (native) | 6.6 |
-| node:buffer | 6.6 |
-| npm:next | 6 |
-| npm:pg | 4.5 |
-| src/lib/personal-records/pr-detection-worker.ts | 4.1 |
-| src/lib/tz/format.ts | 3.1 |
-| node:internal/process/task_queues | 1.5 |
-| node:internal/async_hooks | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 25.3 |
-| src/lib/analytics/source-priority.ts | 18.1 |
-| src/lib/validations/source-priority.ts | 16.5 |
-| src/lib/tz/format.ts | 3.1 |
-| src/lib/measurements/consolidation-tz.ts | 3.1 |
-| src/lib/measurements/consolidation-base.ts | 3.1 |
-
-### at 56923 ms, 52 ms
-
-| self time by module | ms |
-| --- | ---: |
-| (native) | 20 |
-| npm:@prisma/client | 10.1 |
-| node:buffer | 3.7 |
-| npm:pg | 3 |
-| src/lib/tz/format.ts | 2.6 |
-| npm:pg-protocol | 2.6 |
-| src/lib/analytics/source-priority.ts | 2.6 |
-| src/lib/personal-records/pr-detection-worker.ts | 1.5 |
-| src/lib/tz/zone-offset.ts | 1.5 |
-| npm:pg-types | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 9.3 |
-| src/lib/analytics/source-priority.ts | 5.3 |
-| src/lib/tz/format.ts | 4.1 |
-| src/lib/measurements/consolidation-tz.ts | 4.1 |
-| src/lib/measurements/consolidation-base.ts | 2.6 |
-| src/lib/tz/zone-offset.ts | 1.5 |
-| src/lib/validations/source-priority.ts | 1.1 |
-
-### at 59508 ms, 56 ms
-
-| self time by module | ms |
-| --- | ---: |
-| npm:@prisma/client | 15.7 |
-| src/lib/validations/source-priority.ts | 9.7 |
-| node:buffer | 7 |
-| (native) | 6.1 |
-| npm:pg-protocol | 4.5 |
-| npm:next | 4.1 |
-| src/lib/tz/format.ts | 3 |
-| src/lib/analytics/source-priority.ts | 3 |
-| node:internal/async_hooks | 1.5 |
-| src/lib/measurements/consolidation-base.ts | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 21.7 |
-| src/lib/analytics/source-priority.ts | 15.8 |
-| src/lib/validations/source-priority.ts | 9.7 |
-| src/lib/tz/format.ts | 5.9 |
-| src/lib/measurements/consolidation-tz.ts | 5.9 |
-| src/lib/measurements/consolidation-base.ts | 5.9 |
-| src/lib/tz/zone-offset.ts | 2.9 |
-
-### at 61315 ms, 62 ms
-
-| self time by module | ms |
-| --- | ---: |
-| src/lib/validations/source-priority.ts | 20.1 |
-| npm:@prisma/client | 14.1 |
-| (native) | 8.2 |
-| npm:pg | 4.5 |
-| node:buffer | 3.1 |
-| src/lib/personal-records/pr-detection-worker.ts | 3 |
-| src/lib/measurements/consolidation-base.ts | 3 |
-| npm:pg-protocol | 1.5 |
-| npm:next | 1.5 |
-| src/lib/analytics/source-priority.ts | 1.4 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 30.3 |
-| src/lib/analytics/source-priority.ts | 22.8 |
-| src/lib/validations/source-priority.ts | 21.4 |
-| src/lib/measurements/consolidation-base.ts | 4.5 |
-| src/lib/tz/zone-offset.ts | 1.5 |
-| src/lib/tz/format.ts | 1.5 |
-| src/lib/measurements/consolidation-tz.ts | 1.5 |
-
-### at 72986 ms, 57 ms
-
-| self time by module | ms |
-| --- | ---: |
-| src/lib/validations/source-priority.ts | 19.1 |
-| npm:@prisma/client | 10.2 |
-| (native) | 6.6 |
-| npm:pg-protocol | 5.5 |
-| npm:next | 3 |
-| src/lib/analytics/source-priority.ts | 3 |
-| npm:pg | 2.5 |
-| src/lib/personal-records/pr-detection-worker.ts | 1.5 |
-| node:internal/async_local_storage/async_hooks | 1.5 |
-| src/lib/tz/format.ts | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 29.6 |
-| src/lib/analytics/source-priority.ts | 23.5 |
-| src/lib/validations/source-priority.ts | 19.1 |
-| src/lib/measurements/consolidation-base.ts | 4.6 |
-| src/lib/tz/format.ts | 4.5 |
-| src/lib/measurements/consolidation-tz.ts | 4.5 |
-| src/lib/tz/zone-offset.ts | 3 |
-
-### at 73523 ms, 51 ms
-
-| self time by module | ms |
-| --- | ---: |
-| src/lib/validations/source-priority.ts | 12 |
-| npm:next | 8.9 |
-| npm:@prisma/client | 6 |
-| (native) | 4.5 |
-| src/lib/analytics/source-priority.ts | 4.2 |
-| node:buffer | 4 |
-| npm:pg | 2.1 |
-| npm:pg-protocol | 1.5 |
-| src/lib/personal-records/pr-detection-worker.ts | 1.5 |
-| src/lib/tz/zone-offset.ts | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 27.8 |
-| src/lib/analytics/source-priority.ts | 17.7 |
-| src/lib/validations/source-priority.ts | 13.5 |
-| src/lib/measurements/consolidation-base.ts | 8.6 |
-| src/lib/tz/format.ts | 7.1 |
-| src/lib/measurements/consolidation-tz.ts | 7.1 |
-| src/lib/tz/zone-offset.ts | 6 |
-
-### at 75393 ms, 100 ms
-
-| self time by module | ms |
-| --- | ---: |
-| (native) | 32.6 |
-| npm:@prisma/client | 17.2 |
-| src/lib/validations/source-priority.ts | 14.6 |
-| npm:next | 7.5 |
-| src/lib/personal-records/pr-detection-worker.ts | 6.1 |
-| src/lib/tz/zone-offset.ts | 5.6 |
-| npm:pg-protocol | 4.1 |
-| npm:pg | 2.5 |
-| src/lib/tz/format.ts | 2.3 |
-| node:internal/async_local_storage/async_hooks | 1.5 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 34.6 |
-| src/lib/analytics/source-priority.ts | 24.3 |
-| src/lib/validations/source-priority.ts | 16.1 |
-| src/lib/tz/format.ts | 10.9 |
-| src/lib/measurements/consolidation-tz.ts | 10.9 |
-| src/lib/tz/zone-offset.ts | 8.6 |
-| src/lib/measurements/consolidation-base.ts | 5.7 |
-
-### at 78085 ms, 54 ms
-
-| self time by module | ms |
-| --- | ---: |
-| src/lib/tz/zone-offset.ts | 26.6 |
-| src/lib/validations/source-priority.ts | 13.5 |
-| npm:@prisma/client | 4.2 |
-| (native) | 4.1 |
-| node:internal/async_local_storage/async_hooks | 1.5 |
-| src/lib/tz/format.ts | 1.5 |
-| src/lib/tz/intl-cache.ts | 1.5 |
-| npm:pg | 1.1 |
-
-| inclusive, application modules | ms |
-| --- | ---: |
-| src/lib/personal-records/pr-detection-worker.ts | 44.7 |
-| src/lib/tz/format.ts | 29.6 |
-| src/lib/measurements/consolidation-tz.ts | 29.6 |
-| src/lib/measurements/consolidation-base.ts | 29.6 |
-| src/lib/tz/zone-offset.ts | 28.1 |
-| src/lib/analytics/source-priority.ts | 15 |
-| src/lib/validations/source-priority.ts | 13.5 |
-| src/lib/tz/intl-cache.ts | 1.5 |
-
-## Self time by module until the worker is ready
-
-| module | ms |
-| --- | ---: |
-| (native) | 853 |
-| npm:@prisma/client | 295.9 |
-| node:internal/modules/cjs/loader | 248.7 |
-| src/lib/validations/source-priority.ts | 210.3 |
-| npm:next | 188.4 |
-| turbopack:///[turbopack]/nodejs/runtime/build-base.ts | 121.5 |
-| src/lib/tz/zone-offset.ts | 83.2 |
-| wasm://wasm/00d1cb12 | 81.4 |
-| npm:pg | 78.5 |
-| node:buffer | 59.1 |
-| src/lib/tz/format.ts | 55 |
-| npm:zod | 50.7 |
-| src/lib/personal-records/pr-detection-worker.ts | 48 |
-| npm:pg-protocol | 37 |
-| node:internal/bootstrap/realm | 32 |
-| src/lib/analytics/source-priority.ts | 31.7 |
-| turbopack:///[turbopack]/shared/runtime/async-module.ts | 27.7 |
-| node:internal/modules/esm/utils | 26 |
-| npm:luxon | 22.1 |
-| node:internal/async_hooks | 21.7 |
-| node:internal/async_local_storage/async_hooks | 21.2 |
-| node:fs | 18.8 |
-| npm:pg-boss | 18.3 |
-| npm:pg-pool | 18.1 |
-| npm:yaml | 15 |
-
-## Heap snapshot after 300 s idle (live heap after a full GC)
-
-159.5 MB in 1302659 nodes; module source strings over 50 KB: 39.8 MB.
-
-- parsed locale catalogs: 2 objects, 1.6 MB retained
-
-| node type | MB |
-| --- | ---: |
-| string | 67.5 |
-| array | 26.6 |
-| code | 19.2 |
-| object | 15.9 |
-| hidden | 9.2 |
-| native | 8 |
-| object shape | 5.9 |
-| closure | 5.2 |
-| concatenated string | 1.3 |
-| number | 0.5 |
-| sliced string | 0.2 |
-| regexp | 0.2 |
-| synthetic | 0 |
-| symbol | 0 |
-| bigint | 0 |
-
-| constructor | MB | count |
-| --- | ---: | ---: |
-| array:(object elements) | 20.8 | 25651 |
-| object:Object | 11.4 | 293893 |
-| native:system / JSArrayBufferData | 7.5 | 170 |
-| array:(object properties) | 3.3 | 6517 |
-| array: | 2.5 | 10169 |
-| closure: | 1.6 | 29121 |
-| object:system / Context | 1.6 | 28886 |
-| closure:native_bind | 1 | 20804 |
-| object:Array | 0.6 | 20938 |
-| object:Date1 | 0.5 | 5332 |
-| object:a | 0.4 | 4438 |
-| native:Node / histogram | 0.4 | 1 |
-| object:d | 0.3 | 4425 |
-| closure:get | 0.3 | 4732 |
-| object:Set | 0.2 | 5670 |
-| object:Module | 0.2 | 1963 |
-| closure:set | 0.2 | 3428 |
-| closure:n | 0.1 | 1572 |
-| closure:i | 0.1 | 1578 |
-| object:Promise | 0.1 | 1341 |
-
-| largest retained subtrees | MB | retaining path |
-| --- | ---: | --- |
-| object:yr | 34.6 | [1] > [10] > 22 > extension > global > prisma > [1] > _engine |
-| object:system / Context | 7.4 | Node / PrincipalRealm > base_object_list > Node / BaseObjectList > [25] > Node / ModuleWrap > native_to_javascript > ModuleWrap > 2 > [4] > context |
-| object:Promise | 5.9 | Object > executor > e > #r > gr > transactions > Map > table > > 6 > closing |
-| object:Module | 5.6 | [1] > [10] > 99 > process > getBuiltinModule > getBuiltinModule > context > system / Context > $Module > Module > _cache > |
-| code:.next/server/chu | 5.6 | Module > _cache > Object > .next/server/chunks/_1kwfnjg._.js > Module > exports > Array > [1] > > shared > script |
-| code:.next/server/chu | 5.6 | global > prisma > [1] > t > _engineConfig > Object > compilerWasm > Object > getRuntime > getRuntime > shared > script |
-| object:Generator | 4.4 | [6] > principal_realm > Node / PrincipalRealm > base_object_list > Node / BaseObjectList > [26] > Node / ModuleWrap > native_to_javascript > ModuleWrap > 2 > [4 |
-| string:AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/ | 4.4 | [1] > [21] > 60025 |
-| object:Object | 3.4 | system / Context > $Module > Module > _cache > Object > .next/server/chunks/[turbopack]_runtime.js > Module > export |
-| object:global | 2.6 | [1] > [10] > 22 > extension |
-
-| largest strings | MB |
-| --- | ---: |
-| module.exports=[234494,A=>{"use strict";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=> | 5.6 |
-| module.exports=[17856,A=>{"use strict";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i | 5.6 |
-| module.exports=[17856,A=>{"use strict";var e,C,g,I,Q=Object.defineProperty,B={},s={$:()=>i | 5.6 |
-| const wasm = "AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/fwBgBX9/f39/AGAB | 4.4 |
-| AGFzbQEAAAABtgRLYAN/f38Bf2ACf38Bf2ACf38AYAF/AGADf39/AGAEf39/fwBgBX9/f39/AGABfwF/YAZ/f39/f3 | 4.4 |
-| lliADOAUngIEAACgKwAgBgAAoSsAIAcAAKErACAIAACiKwAgCQAAoysAIAoAAKQrACALAAClKwAgDAAApisAIA0AAK | 1.9 |
-| (()=>{var e,t,r={"./dist/build/webpack/alias/react-dom-server.js"(e,t,r){"use strict";var | 1.3 |
-| module.exports=[842865,(e,t,r)=>{let i=e.r(522734);t.exports=function(e){return e?/-----BE | 1.2 |
-| module.exports=[842865,(e,t,r)=>{let i=e.r(522734);t.exports=function(e){return e?/-----BE | 1.2 |
-| module.exports=[799092,(e,t,n)=>{"use strict";Object.defineProperty(n,"__esModule",{value: | 1.1 |
-| // v1.4.34 web-freeze. Schema is locked additive-only until iOS launch // + v1.5.0 marker. | 1.1 |
-| module.exports=[971743,(A,e,t)=>{"use strict";let r=A.r(57328),{addAbortListener:s}=A.r(68 | 1.1 |
-| module.exports=[835717,(A,e,t)=>{"use strict";let r=A.r(57328),{addAbortListener:s}=A.r(68 | 1 |
-| module.exports=[767189,e=>e.a(async(t,a)=>{try{var n=e.i(64105),r=e.i(563616),i=t([n]);[n] | 0.8 |
-| module.exports=[594108,806176,674750,98746,t=>{"use strict";var i;let r;function n(t){retu | 0.7 |
diff --git a/scripts/bench/results/v1.45.0-boot.md b/scripts/bench/results/v1.45.0-boot.md
index 1ed2ec776..f653e59a8 100644
--- a/scripts/bench/results/v1.45.0-boot.md
+++ b/scripts/bench/results/v1.45.0-boot.md
@@ -1,9 +1,9 @@
# v1.45.0: boot and idle memory
`scripts/bench/boot-profile.ts`, production build, every role, heap ceiling
-792 MB, against the seeded account, 300 s idle. `v1.45.0-before-boot` is the
-v1.44 runtime, `v1.45.0-boot-after` the branch after the boot changes and
-before the lazy match indexes, `v1.45.0-boot-final` the final tree.
+792 MB, against the seeded account, 300 s idle. "before" is the v1.44 runtime
+(`v1.45.0-before-boot.md`), "final" the v1.45.0 tree; its raw dumps are not
+kept.
| | before | final |
| --- | ---: | ---: |
diff --git a/scripts/bench/results/v1.45.0-fan-out.md b/scripts/bench/results/v1.45.0-fan-out.md
deleted file mode 100644
index 22fa259a6..000000000
--- a/scripts/bench/results/v1.45.0-fan-out.md
+++ /dev/null
@@ -1,22 +0,0 @@
-# v1.45.0: request fan-out
-
-Same runs as `v1.45.0-read-path.md`.
-
-| request | statements before | statements after | p50 ms before / after |
-| --- | ---: | ---: | ---: |
-| /api/auth/me | 19 | 14 | 9 / 8 |
-| day view, cold | 49 | 48 | 77 / 68 |
-
-- `/api/auth/me` read the AppSettings singleton six times (MFA policy,
- Assistant switches, module availability, provider presence, AI usage
- bounds, reasoning controls); one memoised read per request now serves all
- six (`src/lib/settings/operator-row.ts`). On the small budget fixture it
- went from 18 to 13 statements. The remaining reads are the session, the
- grant, cycle profile, onboarding, consent, coach usage and five user reads
- with different column sets.
-- The day view's record sections run six at a time instead of about
- twenty-five at once, so one day never holds more than a third of the pool.
- The statement count is the same; what changes is the peak of connections.
-- Keyset pagination for `/api/measurements` was not done: a cursor needs a
- new request parameter and a response field, a contract change for the
- native client.
diff --git a/scripts/bench/results/v1.45.0-final.json b/scripts/bench/results/v1.45.0-final.json
deleted file mode 100644
index 6b31e6c98..000000000
--- a/scripts/bench/results/v1.45.0-final.json
+++ /dev/null
@@ -1,254 +0,0 @@
-{
- "meta": {
- "label": "v1.45.0-final",
- "commit": "a57fa8335",
- "date": "2026-10-22T06:52:09.539Z",
- "node": "v22.23.2",
- "postgres": "16.15",
- "cpus": 14,
- "loadAverage": [
- 6.8,
- 8.9,
- 14.5
- ],
- "measurements": 1265561,
- "iterations": 20,
- "warmup": 3,
- "heapMb": 792,
- "idleStatementsPerSecond": 0,
- "batch5k": {
- "totalMs": 4448,
- "totalStatements": 504
- }
- },
- "results": [
- {
- "name": "/api/auth/me",
- "mode": "cold",
- "request": "GET /api/auth/me",
- "n": 20,
- "p50Ms": 8.3,
- "p95Ms": 27.2,
- "maxMs": 221,
- "statementsMedian": 14,
- "statementsMax": 14,
- "failures": 0
- },
- {
- "name": "/api/auth/me",
- "mode": "warm",
- "request": "GET /api/auth/me",
- "n": 20,
- "p50Ms": 7.9,
- "p95Ms": 10.1,
- "maxMs": 17.1,
- "statementsMedian": 14,
- "statementsMax": 17,
- "failures": 0
- },
- {
- "name": "dashboard summary",
- "mode": "cold",
- "request": "GET /api/dashboard/summary",
- "n": 20,
- "p50Ms": 86.6,
- "p95Ms": 100.8,
- "maxMs": 126.8,
- "statementsMedian": 30,
- "statementsMax": 31,
- "failures": 0
- },
- {
- "name": "dashboard summary",
- "mode": "warm",
- "request": "GET /api/dashboard/summary",
- "n": 20,
- "p50Ms": 9.2,
- "p95Ms": 17.1,
- "maxMs": 18.4,
- "statementsMedian": 6,
- "statementsMax": 6,
- "failures": 0
- },
- {
- "name": "dashboard snapshot",
- "mode": "cold",
- "request": "GET /api/dashboard/snapshot",
- "n": 20,
- "p50Ms": 545.3,
- "p95Ms": 592.5,
- "maxMs": 606.3,
- "statementsMedian": 153,
- "statementsMax": 156,
- "failures": 0
- },
- {
- "name": "dashboard snapshot",
- "mode": "warm",
- "request": "GET /api/dashboard/snapshot",
- "n": 20,
- "p50Ms": 8.1,
- "p95Ms": 17.9,
- "maxMs": 21.9,
- "statementsMedian": 10,
- "statementsMax": 10,
- "failures": 0
- },
- {
- "name": "series pulse 30 d",
- "mode": "cold",
- "request": "GET /api/measurements/series?kind=pulse&days=30",
- "n": 20,
- "p50Ms": 18.7,
- "p95Ms": 27.2,
- "maxMs": 36.1,
- "statementsMedian": 8,
- "statementsMax": 8,
- "failures": 0
- },
- {
- "name": "series pulse 30 d",
- "mode": "warm",
- "request": "GET /api/measurements/series?kind=pulse&days=30",
- "n": 20,
- "p50Ms": 15.9,
- "p95Ms": 21.8,
- "maxMs": 26,
- "statementsMedian": 8,
- "statementsMax": 8,
- "failures": 0
- },
- {
- "name": "series pulse 365 d",
- "mode": "cold",
- "request": "GET /api/measurements/series?kind=pulse&days=365",
- "n": 20,
- "p50Ms": 40.7,
- "p95Ms": 55.6,
- "maxMs": 56.2,
- "statementsMedian": 7,
- "statementsMax": 8,
- "failures": 0
- },
- {
- "name": "series pulse 365 d",
- "mode": "warm",
- "request": "GET /api/measurements/series?kind=pulse&days=365",
- "n": 20,
- "p50Ms": 38.3,
- "p95Ms": 59.6,
- "maxMs": 88.6,
- "statementsMedian": 7,
- "statementsMax": 7,
- "failures": 0
- },
- {
- "name": "series pulse 3650 d",
- "mode": "cold",
- "request": "GET /api/measurements/series?kind=pulse&days=3650",
- "n": 20,
- "p50Ms": 334.9,
- "p95Ms": 348.5,
- "maxMs": 351.2,
- "statementsMedian": 7,
- "statementsMax": 7,
- "failures": 0
- },
- {
- "name": "series pulse 3650 d",
- "mode": "warm",
- "request": "GET /api/measurements/series?kind=pulse&days=3650",
- "n": 20,
- "p50Ms": 328.1,
- "p95Ms": 346,
- "maxMs": 348.9,
- "statementsMedian": 7,
- "statementsMax": 8,
- "failures": 0
- },
- {
- "name": "day view",
- "mode": "cold",
- "request": "GET /api/day/2026-10-21",
- "n": 20,
- "p50Ms": 68.4,
- "p95Ms": 102.6,
- "maxMs": 104.7,
- "statementsMedian": 48,
- "statementsMax": 49,
- "failures": 0
- },
- {
- "name": "day view",
- "mode": "warm",
- "request": "GET /api/day/2026-10-21",
- "n": 20,
- "p50Ms": 46.4,
- "p95Ms": 66.1,
- "maxMs": 72.6,
- "statementsMedian": 47,
- "statementsMax": 47,
- "failures": 0
- },
- {
- "name": "insights health status",
- "mode": "cold",
- "request": "GET /api/insights/health-status",
- "n": 20,
- "p50Ms": 156,
- "p95Ms": 190,
- "maxMs": 197.7,
- "statementsMedian": 67,
- "statementsMax": 67,
- "failures": 0
- },
- {
- "name": "insights health status",
- "mode": "warm",
- "request": "GET /api/insights/health-status",
- "n": 20,
- "p50Ms": 173.6,
- "p95Ms": 1163.4,
- "maxMs": 1419.4,
- "statementsMedian": 67,
- "statementsMax": 67,
- "failures": 0
- },
- {
- "name": "insights correlations",
- "mode": "cold",
- "request": "GET /api/insights/correlations",
- "n": 20,
- "p50Ms": 106.1,
- "p95Ms": 229.1,
- "maxMs": 383.3,
- "statementsMedian": 48,
- "statementsMax": 49,
- "failures": 0
- },
- {
- "name": "insights correlations",
- "mode": "warm",
- "request": "GET /api/insights/correlations",
- "n": 20,
- "p50Ms": 18.7,
- "p95Ms": 626.6,
- "maxMs": 1136.5,
- "statementsMedian": 7,
- "statementsMax": 7,
- "failures": 0
- },
- {
- "name": "batch ingest 5k (per 500)",
- "mode": "write",
- "request": "POST /api/measurements/batch ×10",
- "n": 10,
- "p50Ms": 185.7,
- "p95Ms": 1269.2,
- "maxMs": 1269.2,
- "statementsMedian": 50,
- "statementsMax": 53,
- "failures": 0
- }
- ]
-}
diff --git a/scripts/bench/results/v1.45.0-final.md b/scripts/bench/results/v1.45.0-final.md
deleted file mode 100644
index dc26d0366..000000000
--- a/scripts/bench/results/v1.45.0-final.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Benchmark v1.45.0-final
-
-Commit a57fa8335 with the working tree of the final commits, 2026-10-22T06:52:09.539Z. 1265561 measurement rows on the bench account, 20 measured requests per row after 3 warm-up requests, one at a time. Server: production build, web process, heap ceiling 792 MB. Postgres 16.15, 14 cores, load average 6.8,8.9,14.5 at the end. Idle background: 0 statements/s.
-
-| Request | Mode | n | p50 ms | p95 ms | max ms | statements (median) | statements (max) |
-| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
-| /api/auth/me | cold | 20 | 8.3 | 27.2 | 221 | 14 | 14 |
-| /api/auth/me | warm | 20 | 7.9 | 10.1 | 17.1 | 14 | 17 |
-| dashboard summary | cold | 20 | 86.6 | 100.8 | 126.8 | 30 | 31 |
-| dashboard summary | warm | 20 | 9.2 | 17.1 | 18.4 | 6 | 6 |
-| dashboard snapshot | cold | 20 | 545.3 | 592.5 | 606.3 | 153 | 156 |
-| dashboard snapshot | warm | 20 | 8.1 | 17.9 | 21.9 | 10 | 10 |
-| series pulse 30 d | cold | 20 | 18.7 | 27.2 | 36.1 | 8 | 8 |
-| series pulse 30 d | warm | 20 | 15.9 | 21.8 | 26 | 8 | 8 |
-| series pulse 365 d | cold | 20 | 40.7 | 55.6 | 56.2 | 7 | 8 |
-| series pulse 365 d | warm | 20 | 38.3 | 59.6 | 88.6 | 7 | 7 |
-| series pulse 3650 d | cold | 20 | 334.9 | 348.5 | 351.2 | 7 | 7 |
-| series pulse 3650 d | warm | 20 | 328.1 | 346 | 348.9 | 7 | 8 |
-| day view | cold | 20 | 68.4 | 102.6 | 104.7 | 48 | 49 |
-| day view | warm | 20 | 46.4 | 66.1 | 72.6 | 47 | 47 |
-| insights health status | cold | 20 | 156 | 190 | 197.7 | 67 | 67 |
-| insights health status | warm | 20 | 173.6 | 1163.4 | 1419.4 | 67 | 67 |
-| insights correlations | cold | 20 | 106.1 | 229.1 | 383.3 | 48 | 49 |
-| insights correlations | warm | 20 | 18.7 | 626.6 | 1136.5 | 7 | 7 |
-| batch ingest 5k (per 500) | write | 10 | 185.7 | 1269.2 | 1269.2 | 50 | 53 |
diff --git a/scripts/bench/results/v1.45.0-read-path.md b/scripts/bench/results/v1.45.0-read-path.md
deleted file mode 100644
index 8894340d8..000000000
--- a/scripts/bench/results/v1.45.0-read-path.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# v1.45.0: read path
-
-Request benchmark on the seeded account (1,265,561 rows), production build,
-web process, heap ceiling 792 MB, 20 requests per row, one at a time.
-`v1.45.0-before` is the v1.44 runtime (1915f9152), `v1.45.0-final` this
-branch; both on the same machine within the hour, load average 7 to 30
-(other work on the host), so latency compares within a factor of noise and
-statements exactly. Cold means every request follows a one-row manual sync.
-
-| request | before p50 / p95 ms | after p50 / p95 ms | statements before / after |
-| --- | ---: | ---: | ---: |
-| dashboard snapshot, cold | 1482 / 1526 | 545 / 593 | 158 / 153 |
-| dashboard summary, cold | 170 / 221 | 87 / 101 | 31 / 30 |
-| series pulse 3650 d, cold | 1627 / 1665 | 335 / 349 | 8 / 7 |
-| series pulse 3650 d, warm | 2091 / 2435 | 328 / 346 | 8 / 7 |
-| series pulse 365 d, cold | 184 / 193 | 41 / 56 | 8 / 7 |
-| series pulse 30 d, cold | 24 / 31 | 19 / 27 | 8 / 8 |
-| insights health status, cold | 245 / 401 | 156 / 190 | 68 / 67 |
-
-What moved them:
-
-- **JIT off for app sessions.** Postgres compiled every fold whose estimated
- cost passed `jit_above_cost`; on the ten-year pulse pick that was 100 to
- 140 ms of a 220 ms statement (`EXPLAIN ANALYZE`: 395 ms with JIT, 220 ms
- without).
-- **The canonical-source pick groups first.** `DISTINCT ON` now sorts the
- distinct `(type, day, source)` triples instead of every raw reading (the
- ten-year pulse pick sorted 260,000 rows and spilled to disk); 220 ms to
- 175 ms on its own. Parity: `tests/integration/canonical-source-pick-parity.test.ts`
- runs the old statement beside the new one over five reader shapes and two
- ladders on a fixture with overlapping and off-ladder sources, tombstones,
- empty days and readings around midnight; identical rows.
-- **The dense series reads its rows once.** Buckets and strip figures come
- from one statement over a materialised window instead of two statements
- that each picked the canonical source and scanned the readings. Parity:
- `tests/integration/dense-series-read-parity.test.ts`, the two old
- statements as reference, pulse (two ranked sources), glucose and oxygen
- saturation, day and hour grains, UTC, Europe/Berlin, Asia/Kolkata and
- America/New_York; identical buckets and figures (relative 1e-9).
-- **Closed blocks survive a sync.** The slice's pre-fold remainder (every
- reading older than five years, the snapshot's largest statement at 357 ms)
- and the day view's past history blocks are cached under a key that names
- the instant their readings end before; a sync of newer readings no longer
- drops them (`src/lib/cache/closed-block.ts`). The values are the same
- statement's; only how long they are kept changed.
diff --git a/scripts/bench/results/v1.45.0-workout-clusters.md b/scripts/bench/results/v1.45.0-workout-clusters.md
deleted file mode 100644
index 18ab5a3b4..000000000
--- a/scripts/bench/results/v1.45.0-workout-clusters.md
+++ /dev/null
@@ -1,28 +0,0 @@
-# v1.45.0: workout duplicate window
-
-A correctness change, not a speed one; the request benchmark does not read
-workouts, so there is no latency row for it.
-
-Before: two copies of one session were collapsed only when both starts fell
-in the same fixed five-minute clock slot (`floor(start / 5 min)`). A pair 30 s
-apart that straddled a slot boundary stayed two workouts in the list, the
-batch write, the calendar, strain and the Coach.
-
-After: per user and sport, sorted by start, a workout joins the open cluster
-while it starts less than five minutes after the cluster's first start.
-
-Proof (`src/lib/measurements/__tests__/pick-canonical-workout-rows.property.test.ts`,
-against the previous picker kept as `pick-canonical-workout-rows.legacy.ts`):
-
-| property | inputs |
-| --- | --- |
-| same rows as the fixed slots wherever no pair within five minutes straddles a slot boundary, three ladders | 4,000 generated inputs, over 1,000 comparable |
-| same strain impulses as before on those inputs | 2,000 generated inputs, over 500 comparable |
-| two sources' copies collapse at any offset under five minutes (the old picker missed a share) | 3,000 random offsets |
-| five minutes or more apart never collapse | 2,000 |
-| a uniform shift of every start keeps the picked set | 1,500 |
-| a row of workouts four minutes apart does not chain | fixed case |
-
-One existing case changed its expectation: three runs at 08:04, 08:08 and
-08:12 used to stay three because they sat in three slots; 08:08 is now the
-08:04 run from a lower-ranked source (four minutes apart), and 08:12 stays.
diff --git a/scripts/bench/results/v1.45.0-write-path.md b/scripts/bench/results/v1.45.0-write-path.md
deleted file mode 100644
index c4c78a0c2..000000000
--- a/scripts/bench/results/v1.45.0-write-path.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# v1.45.0: write path
-
-Same runs as `v1.45.0-read-path.md`.
-
-| request | before p50 ms | after p50 ms | statements before / after |
-| --- | ---: | ---: | ---: |
-| batch ingest, 500 fresh rows per request | 166 | 186 | 53 / 50 |
-
-Fresh rows were already set-based (v1.44); the benchmark's ingest is all
-fresh rows, so it shows no gain, as expected. The changes are for re-posts:
-
-- **Unchanged `stats:` re-posts** write nothing and start nothing. One read
- over the batch's `stats:` ids replaces a reconciler transaction (about ten
- round trips) per row, and an all-unchanged batch no longer refolds, enqueues
- rollups or PR detection, or marks caches stale. The per-entry status stays
- `updated` and the summary's `updated` count is unchanged.
- `tests/integration/routes/measurements-batch-unchanged-stats.test.ts`: four
- unchanged re-posts take at most 12 statements in total (before: about 40)
- with zero rollup, invalidation and PR calls; a changed value, a changed
- device or a tombstoned row still goes through the reconciler.
-- **Apple Health re-import** skips the UPDATE of a row stored with exactly the
- values it would write; one read per chunk instead of one UPDATE per row.
- `tests/integration/apple-health-reimport-unchanged.test.ts`: a re-import of
- 20 stored rows reports the same per-type counts and sends no UPDATE.
-- **Narrower invalidation.** The batch route tells the cache how far back it
- wrote (`touchedFrom`), so closed blocks of older readings stay.
diff --git a/scripts/cleanup-apple-health-phantom-meds.ts b/scripts/cleanup-apple-health-phantom-meds.ts
deleted file mode 100644
index cd9f76a45..000000000
--- a/scripts/cleanup-apple-health-phantom-meds.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-/**
- * Remove the phantom Apple-Health mirror medications minted by the unstable
- * externalId bug (a fresh key per sync sweep => one duplicate row per sweep).
- *
- * SAFETY:
- * - Dry-run by default. Deletes ONLY when called with `--execute`.
- * - Targets ONLY rows that match ALL of: the given name, externalSource =
- * APPLE_HEALTH, ZERO intake events, and ZERO schedules. A row with any
- * intake history or any schedule is never touched. Real medications
- * (different name, or with intake/schedule history) can never match.
- * - Each delete replicates the DELETE /api/medications/{id} route exactly:
- * revoke medication-scoped tokens, deleteMedicationCategory, cascade
- * delete, audit row. Caches are busted per affected user at the end (and
- * otherwise clear on TTL / the next write).
- *
- * STOP THE SOURCE FIRST: while the iOS Apple-Health medication mirror is still
- * enabled on the device, every sync re-mints these rows and cleanup will undo
- * itself. Turn the toggle off (or ship the iOS fix) before running --execute.
- *
- * Run (inside the app container):
- * pnpm dlx tsx scripts/cleanup-apple-health-phantom-meds.ts # dry-run
- * pnpm dlx tsx scripts/cleanup-apple-health-phantom-meds.ts --execute # delete
- * pnpm dlx tsx scripts/cleanup-apple-health-phantom-meds.ts --name "Blutdruckmittel" --execute
- */
-import "dotenv/config";
-
-import { prisma } from "@/lib/db";
-import { auditLog } from "@/lib/auth/audit";
-import { invalidateUserMedications } from "@/lib/cache/invalidate";
-import { deleteMedicationCategory } from "@/lib/medication-category";
-
-function argValue(flag: string): string | undefined {
- const i = process.argv.indexOf(flag);
- return i >= 0 ? process.argv[i + 1] : undefined;
-}
-
-async function main(): Promise {
- const name = argValue("--name") ?? "Blutdruckmittel";
- const execute = process.argv.includes("--execute");
-
- // Candidate set: name + Apple-Health provenance only. Intake / schedule
- // counts are checked per row below and are the hard safety gate.
- const candidates = await prisma.medication.findMany({
- where: { name, externalSource: "APPLE_HEALTH" },
- select: { id: true, userId: true, externalId: true, createdAt: true },
- orderBy: { createdAt: "asc" },
- });
-
- const deletable: { id: string; userId: string }[] = [];
- const skipped: { id: string; reason: string }[] = [];
-
- for (const m of candidates) {
- const [intakes, schedules] = await Promise.all([
- prisma.medicationIntakeEvent.count({
- where: { medicationId: m.id, deletedAt: null },
- }),
- prisma.medicationSchedule.count({ where: { medicationId: m.id } }),
- ]);
- if (intakes > 0 || schedules > 0) {
- skipped.push({
- id: m.id,
- reason: `has ${intakes} intake(s) / ${schedules} schedule(s) — PRESERVED`,
- });
- continue;
- }
- deletable.push({ id: m.id, userId: m.userId });
- }
-
- console.log(
- `\n=== cleanup plan for name "${name}" (externalSource APPLE_HEALTH) ===`,
- );
- console.log(`candidates found: ${candidates.length}`);
- console.log(`safe to delete: ${deletable.length}`);
- console.log(`preserved (intake/schedule present): ${skipped.length}`);
- if (skipped.length > 0) {
- console.log("\nPreserved rows:");
- for (const s of skipped) console.log(` ${s.id} — ${s.reason}`);
- }
- console.log("\nRows to delete:");
- for (const d of deletable) console.log(` ${d.id}`);
-
- if (!execute) {
- console.log(
- "\nDRY RUN — nothing was deleted. Re-run with --execute to remove the rows above.",
- );
- return;
- }
-
- console.log(`\n--execute: deleting ${deletable.length} row(s)...`);
- const affectedUsers = new Set();
- let deleted = 0;
- for (const { id, userId } of deletable) {
- // Mirror the DELETE route: revoke medication-scoped tokens (none expected
- // on a phantom row, harmless if absent), category cleanup, cascade delete,
- // audit row.
- await prisma.apiToken.updateMany({
- where: {
- userId,
- revoked: false,
- permissions: { has: `medication:${id}:ingest` },
- },
- data: { revoked: true },
- });
- await deleteMedicationCategory(id);
- await prisma.medication.delete({ where: { id } });
- await auditLog("medication.delete", {
- userId,
- details: {
- medicationId: id,
- name,
- reason: "apple_health_phantom_cleanup",
- },
- });
- affectedUsers.add(userId);
- deleted += 1;
- }
-
- for (const userId of affectedUsers) {
- invalidateUserMedications(userId, { evict: true });
- }
-
- console.log(
- `\nDone. Deleted ${deleted} row(s) across ${affectedUsers.size} user(s).`,
- );
- console.log(
- "Caches were busted for the affected users; a page refresh reflects the change.",
- );
-}
-
-main()
- .then(() => process.exit(0))
- .catch((err) => {
- console.error(err);
- process.exit(1);
- })
- .finally(() => {
- void prisma.$disconnect();
- });
diff --git a/scripts/compare-correlation-findings.ts b/scripts/compare-correlation-findings.ts
deleted file mode 100644
index 886e5e0f4..000000000
--- a/scripts/compare-correlation-findings.ts
+++ /dev/null
@@ -1,338 +0,0 @@
-/**
- * Read-only before/after count of correlation findings per account, for the
- * release-candidate dry run of the v1.42 statistics change.
- *
- * v1.42 changes what the discovery engine accepts: trend and season are
- * removed from both series before correlating, p-values use the effective
- * sample size, shrinkage and the phrasing tier read that effective size, and
- * the weather channels are tested once over the averaged day-before-and-same-
- * day exposure instead of a next-day lag. Every one of those can make an
- * existing finding disappear. This script answers "how many, and where",
- * on a copy of production, before the release ships.
- *
- * For each account it builds the matrix exactly as the correlations route
- * does (same assembler, same 180-day window, same module map, same tiered
- * read) and scans it twice:
- *
- * - BEFORE: the pre-v1.42 rules, restated here from the engine's own
- * exported pieces (lag join at 1 day, Pearson with df = n − 2, BH-FDR,
- * shrinkage and tier by n), over the raw daily weather values;
- * - AFTER: the engine as it now ships.
- *
- * It prints counts only. No correlation, p-value, metric value or account
- * identifier leaves the database: accounts are listed by a short hash of
- * their id, channels by their stable key family. Nothing is written — the
- * pattern store is not synced, no cache is touched.
- *
- * Run (inside the app container, which has DATABASE_URL):
- * pnpm dlx tsx scripts/compare-correlation-findings.ts [--limit N]
- */
-import "dotenv/config";
-
-import { createHash } from "node:crypto";
-
-import { prisma } from "@/lib/db";
-import { resolveModuleMap } from "@/lib/modules/gate";
-import { assembleDiscoveryMatrix } from "@/lib/insights/discovery-matrix";
-import { MIN_PAIRED_N, pearson } from "@/lib/insights/correlations";
-import {
- benjaminiHochberg,
- confidenceTier,
- discoverCorrelations,
- discoverEmergingCorrelations,
- EARLY_FDR_Q,
- EARLY_MIN_PAIRS,
- EARLY_WINDOW_DAYS,
- FDR_Q,
- filterSeriesToWindow,
- lagJoin,
- metricFamily,
- shrinkEstimate,
- type DailySeriesPoint,
- type NamedSeries,
-} from "@/lib/insights/correlation-discovery";
-import { ENVIRONMENT_FIELDS } from "@/lib/environment/fields";
-import {
- isSurfaceVisible,
- correlationChannelSurfaceId,
-} from "@/lib/modules/surface";
-import { wallClockInTz } from "@/lib/tz/wall-clock";
-import { DEFAULT_TIMEZONE } from "@/lib/tz/format";
-
-const MS_PER_DAY = 86_400_000;
-/** The correlations route's window. */
-const WINDOW_DAYS = 180;
-
-interface Finding {
- behaviour: string;
- outcome: string;
-}
-
-interface LegacyFinding extends Finding {
- tier: ReturnType;
-}
-
-/** The pre-v1.42 discovery rules, restated from the engine's parts. */
-function legacyDiscover(
- series: NamedSeries[],
- opts: { minPairs: number; fdrQ: number },
-): { found: LegacyFinding[]; pairsTested: number } {
- const behaviours = series.filter((s) => s.role === "behaviour");
- const outcomes = series.filter((s) => s.role === "outcome");
- const tested: Array = [];
- for (const b of behaviours) {
- for (const o of outcomes) {
- if (metricFamily(b.key) === metricFamily(o.key)) continue;
- const { xs, ys } = lagJoin(b.points, o.points, 1);
- if (xs.length < opts.minPairs) continue;
- const result = pearson({ xs, ys, minPairs: opts.minPairs });
- if (result.status !== "ok") continue;
- tested.push({
- behaviour: b.key,
- outcome: o.key,
- n: result.n,
- r: result.r,
- pValue: result.pValue,
- });
- }
- }
- const q = benjaminiHochberg(tested.map((t) => t.pValue));
- const found = tested
- .filter((t, i) => t.pValue < 0.05 && q[i] <= opts.fdrQ)
- .map((t) => ({
- behaviour: t.behaviour,
- outcome: t.outcome,
- tier: confidenceTier(shrinkEstimate(t.r, t.n), t.n),
- }))
- .filter((t) => t.tier !== null);
- return { found, pairsTested: tested.length };
-}
-
-/** The pre-v1.42 emerging pass: same rules, trailing window, no faint tier. */
-function legacyEmerging(
- series: NamedSeries[],
- retrospective: Finding[],
- recentFromDayKey: string,
-): Finding[] {
- const recent = legacyDiscover(
- filterSeriesToWindow(series, recentFromDayKey),
- { minPairs: EARLY_MIN_PAIRS, fdrQ: EARLY_FDR_Q },
- );
- const established = new Set(retrospective.map(key));
- return recent.found
- .filter((f) => f.tier !== "faint")
- .filter((f) => !established.has(key(f)));
-}
-
-function key(f: Finding): string {
- return `${f.behaviour}␟${f.outcome}`;
-}
-
-function tzDayKey(at: Date, tz: string): string {
- const { year, month, day } = wallClockInTz(at, tz);
- return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
-}
-
-/**
- * The weather channels as the engine read them before v1.42: one raw daily
- * value per field, no averaging. Read here directly because the shipped
- * series now carries the averaged window.
- */
-async function legacyEnvironmentSeries(
- userId: string,
- since: Date,
-): Promise> {
- // Same UTC lower bound the engine's own environment read uses.
- const sinceKey = since.toISOString().slice(0, 10);
- const rows = await prisma.environmentContext.findMany({
- where: { userId, date: { gte: sinceKey } },
- orderBy: { date: "asc" },
- take: 1000,
- });
- const out = new Map();
- for (const field of ENVIRONMENT_FIELDS) {
- const points: DailySeriesPoint[] = [];
- for (const row of rows) {
- const raw = row[field.column];
- if (raw == null || !Number.isFinite(raw)) continue;
- const value =
- field.column === "sunshineSec" || field.column === "daylightSec"
- ? raw / 3600
- : raw;
- points.push({ day: row.date, value });
- }
- out.set(field.key, points);
- }
- return out;
-}
-
-/** Channel family label for the aggregate table (no user data). */
-function familyLabel(channel: string): string {
- if (channel.startsWith("ENV_")) return "ENVIRONMENT";
- if (channel.startsWith("CUSTOM_METRIC:")) return "CUSTOM_METRIC";
- if (channel.startsWith("SYMPTOM:")) return "SYMPTOM_EVENT";
- if (channel.startsWith("FACTOR:")) return "MOOD_FACTOR";
- return channel;
-}
-
-interface Tally {
- before: number;
- after: number;
-}
-
-function bump(map: Map, k: string, side: keyof Tally) {
- const t = map.get(k) ?? { before: 0, after: 0 };
- t[side]++;
- map.set(k, t);
-}
-
-function parseLimit(argv: string[]): number | undefined {
- const i = argv.indexOf("--limit");
- if (i === -1) return undefined;
- const n = Number(argv[i + 1]);
- return Number.isInteger(n) && n > 0 ? n : undefined;
-}
-
-async function main(): Promise {
- const limit = parseLimit(process.argv.slice(2));
- const users = await prisma.user.findMany({
- select: { id: true, timezone: true },
- orderBy: { createdAt: "asc" },
- ...(limit ? { take: limit } : {}),
- });
-
- const now = new Date();
- const since = new Date(now.getTime() - WINDOW_DAYS * MS_PER_DAY);
- const byBehaviour = new Map();
- const byOutcome = new Map();
- const totals = {
- accounts: 0,
- scanned: 0,
- retroBefore: 0,
- retroAfter: 0,
- recentBefore: 0,
- recentAfter: 0,
- kept: 0,
- lost: 0,
- gained: 0,
- };
-
- console.log(
- "account tested(b/a) retro b→a recent b→a kept lost new env b→a",
- );
- for (const user of users) {
- totals.accounts++;
- const tz = user.timezone ?? DEFAULT_TIMEZONE;
- const modules = await resolveModuleMap(user.id);
- const { series } = await assembleDiscoveryMatrix(user.id, {
- tz,
- since,
- fetchMode: "tiered",
- modules,
- });
- const envVisible = isSurfaceVisible(
- correlationChannelSurfaceId(ENVIRONMENT_FIELDS[0].key),
- modules,
- );
- const rawEnv = envVisible
- ? await legacyEnvironmentSeries(user.id, since)
- : new Map();
- const legacySeries: NamedSeries[] = series.map((s) =>
- s.key.startsWith("ENV_")
- ? { key: s.key, role: s.role, points: rawEnv.get(s.key) ?? [] }
- : { key: s.key, role: s.role, label: s.label, points: s.points },
- );
-
- const recentFromDayKey = tzDayKey(
- new Date(now.getTime() - EARLY_WINDOW_DAYS * MS_PER_DAY),
- tz,
- );
-
- const legacy = legacyDiscover(legacySeries, {
- minPairs: MIN_PAIRED_N,
- fdrQ: FDR_Q,
- });
- const before = legacy.found;
- const beforeRecent = legacyEmerging(legacySeries, before, recentFromDayKey);
- const after = discoverCorrelations(series, { locale: "en" });
- const afterRecent = discoverEmergingCorrelations(series, after, {
- recentFromDayKey,
- locale: "en",
- });
-
- if (legacy.pairsTested === 0 && after.pairsTested === 0) continue;
- totals.scanned++;
-
- const beforeKeys = new Set(before.map(key));
- const afterKeys = new Set(after.discovered.map(key));
- const kept = [...afterKeys].filter((k) => beforeKeys.has(k)).length;
- const lost = beforeKeys.size - kept;
- const gained = afterKeys.size - kept;
- const envBefore = before.filter((f) => f.behaviour.startsWith("ENV_"));
- const envAfter = after.discovered.filter((f) =>
- f.behaviour.startsWith("ENV_"),
- );
-
- for (const f of before) {
- bump(byBehaviour, familyLabel(f.behaviour), "before");
- bump(byOutcome, familyLabel(f.outcome), "before");
- }
- for (const f of after.discovered) {
- bump(byBehaviour, familyLabel(f.behaviour), "after");
- bump(byOutcome, familyLabel(f.outcome), "after");
- }
-
- totals.retroBefore += before.length;
- totals.retroAfter += after.discovered.length;
- totals.recentBefore += beforeRecent.length;
- totals.recentAfter += afterRecent.emerging.length;
- totals.kept += kept;
- totals.lost += lost;
- totals.gained += gained;
-
- const tag = createHash("sha256").update(user.id).digest("hex").slice(0, 8);
- console.log(
- [
- tag.padEnd(9),
- `${legacy.pairsTested}/${after.pairsTested}`.padEnd(12),
- `${before.length}→${after.discovered.length}`.padEnd(10),
- `${beforeRecent.length}→${afterRecent.emerging.length}`.padEnd(11),
- String(kept).padStart(4),
- String(lost).padStart(4),
- String(gained).padStart(3),
- ` ${envBefore.length}→${envAfter.length}`,
- ].join(" "),
- );
- }
-
- console.log("");
- console.log(
- `accounts ${totals.accounts}, with a testable matrix ${totals.scanned}`,
- );
- console.log(
- `retrospective findings ${totals.retroBefore} → ${totals.retroAfter} (kept ${totals.kept}, lost ${totals.lost}, new ${totals.gained})`,
- );
- console.log(
- `emerging findings ${totals.recentBefore} → ${totals.recentAfter}`,
- );
- const table = (title: string, map: Map) => {
- console.log("");
- console.log(`${title}: before → after`);
- for (const [k, t] of [...map.entries()].sort((a, b) =>
- a[0].localeCompare(b[0]),
- )) {
- console.log(` ${k.padEnd(24)} ${t.before} → ${t.after}`);
- }
- };
- table("by behaviour family", byBehaviour);
- table("by outcome family", byOutcome);
-}
-
-main()
- .catch((err: unknown) => {
- console.error(err);
- process.exitCode = 1;
- })
- .finally(() => {
- void prisma.$disconnect();
- });
diff --git a/scripts/convert-noncanonical-measurement-units.ts b/scripts/convert-noncanonical-measurement-units.ts
deleted file mode 100644
index 18a75cbda..000000000
--- a/scripts/convert-noncanonical-measurement-units.ts
+++ /dev/null
@@ -1,104 +0,0 @@
-/**
- * Convert measurement rows stored in a RECOGNISED non-canonical unit to
- * canonical SI, in place. Companion to
- * `diagnose-noncanonical-measurement-units.ts` — run the diagnose first.
- *
- * SAFETY:
- * - Dry-run by default. Writes ONLY when called with `--execute`.
- * - Touches ONLY rows whose stored `unit` differs from the type's canonical
- * unit AND resolves through the shared alias resolver (lb/lbs/pound(s),
- * in/inch(es), °F, mmol/L). A row whose unit is already canonical, or whose
- * unit is unrecognised, is never touched.
- * - Each row is rewritten to the canonical `{ value, unit }` the same
- * resolver the CSV importer + MCP write path use produces, so the number is
- * re-expressed, never re-labelled in place (which would corrupt it).
- * - Affected users' measurement caches are busted at the end.
- *
- * This repairs the storage LABEL + value for the recognised-alias legacy rows
- * (the MCP-leak class). It cannot repair a row whose unit was already canonical
- * but whose value was a foreign unit — that class is not mechanically
- * identifiable (see the diagnose script).
- *
- * Run (inside the app container):
- * pnpm dlx tsx scripts/convert-noncanonical-measurement-units.ts # dry-run
- * pnpm dlx tsx scripts/convert-noncanonical-measurement-units.ts --execute # write
- */
-import "dotenv/config";
-
-import { prisma } from "@/lib/db";
-import { getUnitForType } from "@/lib/validations/measurement";
-import { resolveToCanonicalUnit } from "@/lib/measurements/unit-aliases";
-import { invalidateUserMeasurements } from "@/lib/cache/invalidate";
-
-async function main(): Promise {
- const execute = process.argv.includes("--execute");
-
- const groups = await prisma.measurement.groupBy({
- by: ["type", "unit"],
- _count: { _all: true },
- });
- const convertibleGroups = groups.filter((g) => {
- if (g.unit === null || g.unit === getUnitForType(g.type)) return false;
- return resolveToCanonicalUnit(g.type, 1, g.unit) !== null;
- });
-
- if (convertibleGroups.length === 0) {
- console.log(
- "Nothing to convert — no recognised non-canonical units found.",
- );
- return;
- }
-
- const affectedUsers = new Set();
- let converted = 0;
-
- for (const g of convertibleGroups) {
- const canonical = getUnitForType(g.type);
- const rows = await prisma.measurement.findMany({
- where: { type: g.type, unit: g.unit },
- select: { id: true, userId: true, value: true, unit: true },
- });
- for (const row of rows) {
- const resolved = resolveToCanonicalUnit(
- g.type,
- row.value,
- row.unit as string,
- );
- if (!resolved) continue; // defensive; group already filtered
- converted += 1;
- affectedUsers.add(row.userId);
- console.log(
- ` ${execute ? "convert" : "would convert"} ${g.type} ${row.id}: ` +
- `${row.value} ${row.unit} -> ${resolved.value} ${resolved.unit}`,
- );
- if (execute) {
- await prisma.measurement.update({
- where: { id: row.id },
- data: { value: resolved.value, unit: canonical },
- });
- }
- }
- }
-
- if (execute) {
- for (const userId of affectedUsers) {
- invalidateUserMeasurements(userId, { evict: true });
- }
- console.log(
- `\nConverted ${converted} row(s) across ${affectedUsers.size} user(s). ` +
- "Rollups recompute on the next read/backfill.",
- );
- } else {
- console.log(
- `\nDry run: ${converted} row(s) across ${affectedUsers.size} user(s) ` +
- "would be converted. Re-run with --execute to write.",
- );
- }
-}
-
-main()
- .catch((err) => {
- console.error(err);
- process.exitCode = 1;
- })
- .finally(() => prisma.$disconnect());
diff --git a/scripts/diagnose-apple-health-meds.ts b/scripts/diagnose-apple-health-meds.ts
deleted file mode 100644
index ee1aa7269..000000000
--- a/scripts/diagnose-apple-health-meds.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-/**
- * Read-only diagnostic for the phantom Apple-Health medication mirror.
- *
- * Prints the shape of every medication matching a name (default
- * "Blutdruckmittel"): its provenance (externalSource / externalId), dose,
- * asNeeded flag, createdAt, and its intake-event + schedule counts. Nothing is
- * written. Use it to confirm the root cause before running any cleanup:
- *
- * - externalSource = APPLE_HEALTH with many DISTINCT externalId values
- * (address-shaped or otherwise mutually different) => the iOS mirror is
- * minting a fresh key per sync, so idempotency never matches.
- * - externalSource NULL => the rows are not mirror rows (a different path).
- * - createdAt spread => one row per sync sweep vs a single burst.
- *
- * Run (inside the app container, which has DATABASE_URL):
- * pnpm dlx tsx scripts/diagnose-apple-health-meds.ts
- * pnpm dlx tsx scripts/diagnose-apple-health-meds.ts --name "Blutdruckmittel"
- */
-import "dotenv/config";
-
-import { prisma } from "@/lib/db";
-
-function argValue(flag: string): string | undefined {
- const i = process.argv.indexOf(flag);
- return i >= 0 ? process.argv[i + 1] : undefined;
-}
-
-async function main(): Promise {
- const name = argValue("--name") ?? "Blutdruckmittel";
-
- const meds = await prisma.medication.findMany({
- where: { name },
- select: {
- id: true,
- userId: true,
- name: true,
- dose: true,
- asNeeded: true,
- externalSource: true,
- externalId: true,
- createdAt: true,
- },
- orderBy: { createdAt: "asc" },
- });
-
- if (meds.length === 0) {
- console.log(`No medications found with name "${name}".`);
- return;
- }
-
- const rows = [];
- for (const m of meds) {
- const [intakes, schedules] = await Promise.all([
- prisma.medicationIntakeEvent.count({
- where: { medicationId: m.id, deletedAt: null },
- }),
- prisma.medicationSchedule.count({ where: { medicationId: m.id } }),
- ]);
- rows.push({
- id: m.id,
- user: m.userId.slice(0, 8),
- source: m.externalSource ?? "NULL",
- externalId: m.externalId ?? "NULL",
- dose: m.dose,
- asNeeded: m.asNeeded,
- intakes,
- schedules,
- createdAt: m.createdAt.toISOString(),
- });
- }
-
- console.log(`\n=== ${rows.length} medication(s) named "${name}" ===\n`);
- console.table(rows);
-
- const distinctExternalIds = new Set(
- meds.map((m) => m.externalId).filter((v): v is string => v !== null),
- );
- const bySource = new Map();
- for (const m of meds) {
- const key = m.externalSource ?? "NULL";
- bySource.set(key, (bySource.get(key) ?? 0) + 1);
- }
- const created = meds.map((m) => m.createdAt.getTime());
- const withIntake = rows.filter((r) => r.intakes > 0).length;
- const withSchedule = rows.filter((r) => r.schedules > 0).length;
-
- console.log("\n=== summary ===");
- console.log(`total rows: ${rows.length}`);
- console.log(
- `externalSource breakdown: ${[...bySource.entries()]
- .map(([k, v]) => `${k}=${v}`)
- .join(", ")}`,
- );
- console.log(`distinct externalId: ${distinctExternalIds.size}`);
- console.log(`rows with >=1 intake: ${withIntake}`);
- console.log(`rows with >=1 schedule: ${withSchedule}`);
- console.log(
- `createdAt range: ${new Date(Math.min(...created)).toISOString()} .. ${new Date(Math.max(...created)).toISOString()}`,
- );
- console.log(
- "\nSample externalId values (first 5):\n" +
- meds
- .slice(0, 5)
- .map((m) => ` ${m.externalId ?? "NULL"}`)
- .join("\n"),
- );
- console.log(
- "\nRead-only. No rows were modified. If the shape confirms the phantom set,\n" +
- "run scripts/cleanup-apple-health-phantom-meds.ts (dry-run first).",
- );
-}
-
-main()
- .then(() => process.exit(0))
- .catch((err) => {
- console.error(err);
- process.exit(1);
- })
- .finally(() => {
- void prisma.$disconnect();
- });
diff --git a/scripts/diagnose-cycle-state.ts b/scripts/diagnose-cycle-state.ts
deleted file mode 100644
index 85856e998..000000000
--- a/scripts/diagnose-cycle-state.ts
+++ /dev/null
@@ -1,222 +0,0 @@
-/**
- * Read-only diagnostic for cycle records the module has stopped reading.
- *
- * Two defects put records into states that look, from the app, like no periods
- * were ever logged. Both are fixed going forward; neither repairs the rows it
- * already made, and neither is visible from the surface it broke.
- *
- * The first is a closed torso. Logging a period start closes the cycle before
- * it, and removing that start used to leave the closure standing. The previous
- * cycle keeps an end date that a cycle which no longer exists gave it, and a
- * record whose last cycle is closed has no open cycle for the engine to
- * forecast from, so the prediction disappears and does not come back. The
- * signature is exact and needs no audit trail: a live cycle carrying an end
- * date with no live cycle after it. A cycle is closed by its successor, so
- * closed with no successor means the successor is gone.
- *
- * The second is a period that only ever existed as flow. Cycle rows used to be
- * written by the one-tap period boundary alone, so bleeding entered through the
- * log sheet's chips, the bulk drain or an Apple Health import produced day-logs
- * and no cycle at all. The engine reads cycle rows, so the record holds periods
- * and reports none.
- *
- * The bleeding-day rule below is imported from the write path rather than
- * restated, so this report and the app cannot drift on what counts as a period
- * start.
- *
- * Nothing is written. The output is a per-record inventory plus the two flags,
- * so the state can be read before anything is decided about it.
- *
- * Run (inside the app container, which has DATABASE_URL):
- * pnpm dlx tsx scripts/diagnose-cycle-state.ts
- */
-import "dotenv/config";
-
-import { prisma } from "@/lib/db";
-import { bleedOpensCycle } from "@/lib/cycle/cycle-boundaries";
-import { dayDiff } from "@/lib/cycle/day-math";
-
-/** Below this many observed cycles the app withholds its phase band. */
-const STILL_LEARNING_BELOW = 3;
-
-interface CycleRow {
- id: string;
- startDate: string;
- endDate: string | null;
- lengthDays: number | null;
- periodEndDate: string | null;
- deletedAt: Date | null;
- isPredicted: boolean;
-}
-
-interface DayRow {
- date: string;
- flow: string | null;
- intermenstrualBleeding: boolean;
-}
-
-function describeCycle(c: CycleRow): string {
- const state = c.deletedAt
- ? `deleted ${c.deletedAt.toISOString().slice(0, 10)}`
- : c.endDate
- ? `closed ${c.endDate}`
- : "open";
- const length = c.lengthDays === null ? "-" : `${c.lengthDays}d`;
- const period = c.periodEndDate ? ` periodEnd=${c.periodEndDate}` : "";
- const predicted = c.isPredicted ? " PREDICTED" : "";
- return `${c.startDate} ${state.padEnd(20)} length=${length.padEnd(5)}${period}${predicted}`;
-}
-
-async function main(): Promise {
- // Every record that has ever touched the module, from either side: a cycle
- // row, or a day-log. A record with day-logs and no cycles is precisely one
- // of the cases this exists to find, so it cannot be found by cycles alone.
- const [cycleOwners, dayOwners] = await Promise.all([
- prisma.menstrualCycle.findMany({
- distinct: ["userId"],
- select: { userId: true },
- }),
- prisma.cycleDayLog.findMany({
- distinct: ["userId"],
- select: { userId: true },
- }),
- ]);
- const userIds = Array.from(
- new Set([
- ...cycleOwners.map((r) => r.userId),
- ...dayOwners.map((r) => r.userId),
- ]),
- ).sort();
-
- if (userIds.length === 0) {
- console.log("No cycle records on this instance. Nothing to report.");
- return;
- }
-
- let torsoTotal = 0;
- let flowOnlyTotal = 0;
- let staleBoundaryTotal = 0;
-
- for (const userId of userIds) {
- const [cycles, days] = await Promise.all([
- prisma.menstrualCycle.findMany({
- where: { userId },
- orderBy: { startDate: "asc" },
- select: {
- id: true,
- startDate: true,
- endDate: true,
- lengthDays: true,
- periodEndDate: true,
- deletedAt: true,
- isPredicted: true,
- },
- }) as Promise,
- prisma.cycleDayLog.findMany({
- where: { userId, deletedAt: null },
- orderBy: { date: "asc" },
- select: { date: true, flow: true, intermenstrualBleeding: true },
- }) as Promise,
- ]);
-
- const live = cycles.filter((c) => c.deletedAt === null);
- const liveStarts = live.map((c) => c.startDate);
- const startSet = new Set(liveStarts);
-
- console.log(`record ${userId}`);
- console.log(
- ` ${cycles.length} cycle row(s), ${live.length} live, ` +
- `${days.length} day-log(s)`,
- );
- for (const c of cycles) console.log(` ${describeCycle(c)}`);
-
- // A closed cycle with nothing after it: closed by a start that is gone.
- const torsos = live.filter(
- (c, i) => c.endDate !== null && i === live.length - 1,
- );
- // A closed cycle whose boundary does not match the successor it has.
- const stale = live.filter((c, i) => {
- const next = live[i + 1];
- if (!next) return false;
- const expectedLength = dayDiff(next.startDate, c.startDate);
- return c.endDate === null || c.lengthDays !== expectedLength;
- });
-
- // Bleeding days the write path would treat as a period start today, on a
- // date carrying no live cycle. Walked forward so each decision sees the
- // starts that already exist plus the ones this pass would infer, exactly
- // as a replay of the writes would.
- const inferred: string[] = [...liveStarts].sort();
- const flowOnly: string[] = [];
- for (const d of days) {
- if (startSet.has(d.date)) continue;
- const prior = inferred.filter((s) => s <= d.date).at(-1) ?? null;
- if (
- bleedOpensCycle(
- d.flow as never,
- d.intermenstrualBleeding,
- prior,
- d.date,
- )
- ) {
- flowOnly.push(d.date);
- inferred.push(d.date);
- inferred.sort();
- }
- }
-
- if (torsos.length > 0) {
- torsoTotal += torsos.length;
- console.log(
- ` CLOSED TORSO: ${torsos
- .map((c) => `${c.startDate} (ends ${c.endDate})`)
- .join(", ")}`,
- );
- console.log(
- " The last cycle is closed and has no successor, so the engine has " +
- "no open cycle to forecast from. Re-opening it means clearing its " +
- "endDate and lengthDays.",
- );
- }
- if (stale.length > 0) {
- staleBoundaryTotal += stale.length;
- console.log(
- ` STALE BOUNDARY: ${stale.map((c) => c.startDate).join(", ")}`,
- );
- console.log(
- " The stored end or length disagrees with the next cycle's start.",
- );
- }
- if (flowOnly.length > 0) {
- flowOnlyTotal += flowOnly.length;
- console.log(` PERIOD WITH NO CYCLE ROW: ${flowOnly.join(", ")}`);
- console.log(
- " Bleeding was logged on these days and no cycle opens on them. " +
- "Re-saving each day through the log sheet opens it now.",
- );
- }
- if (live.length > 0 && live.length - 1 < STILL_LEARNING_BELOW) {
- console.log(
- ` Only ${live.length - 1} observed cycle length(s): under the ` +
- `${STILL_LEARNING_BELOW}-cycle gate, so the phase band stays withheld.`,
- );
- }
- console.log("");
- }
-
- console.log(
- `${userIds.length} record(s) inspected. ` +
- `${torsoTotal} closed torso(s), ${staleBoundaryTotal} stale boundary(ies), ` +
- `${flowOnlyTotal} logged period(s) with no cycle row.`,
- );
- console.log("Nothing was changed.");
-}
-
-main()
- .catch((error) => {
- console.error(error);
- process.exitCode = 1;
- })
- .finally(() => {
- void prisma.$disconnect();
- });
diff --git a/scripts/diagnose-implausible-measurements.ts b/scripts/diagnose-implausible-measurements.ts
deleted file mode 100644
index 0be518a26..000000000
--- a/scripts/diagnose-implausible-measurements.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-/**
- * Read-only diagnostic for stored measurement rows outside the app's own
- * declared plausibility domain.
- *
- * `VALUE_RANGES` in `@/lib/validations/measurement` is the band the
- * application says a metric can occupy. Every path a person can drive has
- * always enforced it; the provider sync writers did not, so rows the
- * application itself calls impossible reached the table — and every mean,
- * median and personal band built from them inherited the impossibility. The
- * writers enforce it now, which stops new rows landing. It says nothing about
- * the rows already there.
- *
- * This reports them and repairs nothing. What to do with a stored impossible
- * row is a judgement the data cannot make: a pulse of 55 million is plainly a
- * decode slip and a candidate for deletion, while a weight of 0.9 kg might be
- * a neonatal record on a shared scale, or a unit mix-up, or a real reading the
- * band is simply too narrow for. Deleting somebody's health record on a
- * heuristic is not a thing a script gets to decide.
- *
- * Output is aggregate: one line per (metric, source) bucket with the row
- * count, the number of distinct records affected, the observed extremes, and
- * the window they fall in. Individual readings are not printed — the bucket
- * plus the metric's own declared bound is what a decision needs, and a log
- * file full of somebody's vitals is not.
- *
- * Run (inside the app container, which has DATABASE_URL):
- * pnpm dlx tsx scripts/diagnose-implausible-measurements.ts
- */
-import "dotenv/config";
-
-import { prisma } from "@/lib/db";
-import { VALUE_RANGES } from "@/lib/validations/measurement";
-
-interface BucketRow {
- type: string;
- source: string;
- row_count: bigint;
- record_count: bigint;
- min_value: number;
- max_value: number;
- first_at: Date;
- last_at: Date;
- tombstoned: bigint;
-}
-
-function ymd(date: Date): string {
- return date.toISOString().slice(0, 10);
-}
-
-async function main(): Promise {
- const types = Object.keys(VALUE_RANGES).sort();
- const mins = types.map((t) => VALUE_RANGES[t]!.min);
- const maxes = types.map((t) => VALUE_RANGES[t]!.max);
-
- // The band table rides in as three parallel bound arrays rather than a
- // spliced VALUES list, so the query carries no interpolated text at all.
- // `value <> value` is the NaN probe: a double-precision column can hold one,
- // and it compares false against every bound including itself.
- const buckets = await prisma.$queryRaw`
- WITH bands(type, min_value, max_value) AS (
- SELECT * FROM unnest(
- ${types}::text[],
- ${mins}::double precision[],
- ${maxes}::double precision[]
- )
- )
- SELECT
- m."type"::text AS type,
- m."source"::text AS source,
- COUNT(*)::bigint AS row_count,
- COUNT(DISTINCT m."user_id")::bigint AS record_count,
- MIN(m."value") AS min_value,
- MAX(m."value") AS max_value,
- MIN(m."measured_at") AS first_at,
- MAX(m."measured_at") AS last_at,
- COUNT(*) FILTER (WHERE m."deleted_at" IS NOT NULL)::bigint AS tombstoned
- FROM "measurements" m
- JOIN bands b ON b."type" = m."type"::text
- WHERE m."value" < b.min_value
- OR m."value" > b.max_value
- OR m."value" <> m."value"
- GROUP BY m."type", m."source"
- ORDER BY COUNT(*) DESC, m."type"::text ASC, m."source"::text ASC
- `;
-
- if (buckets.length === 0) {
- console.log(
- `Every stored measurement sits inside its metric's declared range ` +
- `(${types.length} metric bands checked). Nothing to review.`,
- );
- return;
- }
-
- const totalRows = buckets.reduce((sum, b) => sum + Number(b.row_count), 0);
- const affectedTypes = new Set(buckets.map((b) => b.type));
- const affectedSources = new Set(buckets.map((b) => b.source));
-
- console.log(
- `${totalRows} stored row(s) sit outside the declared plausibility band, ` +
- `across ${affectedTypes.size} metric(s) and ${affectedSources.size} source(s).\n` +
- `Nothing below is modified. Each line is a (metric, source) bucket; the ` +
- `band is the metric's own declared domain.\n`,
- );
-
- // Group by metric so the declared band is stated once per heading, and rank
- // the metrics by how much of the problem each one is. The query's own
- // ordering already ranks the sources inside a metric.
- const byType = new Map();
- for (const bucket of buckets) {
- const bucketsForType = byType.get(bucket.type);
- if (bucketsForType) bucketsForType.push(bucket);
- else byType.set(bucket.type, [bucket]);
- }
- const ranked = [...byType.entries()].sort(
- (a, b) =>
- b[1].reduce((sum, x) => sum + Number(x.row_count), 0) -
- a[1].reduce((sum, x) => sum + Number(x.row_count), 0),
- );
-
- for (const [type, rows] of ranked) {
- const band = VALUE_RANGES[type]!;
- console.log(`${type} — declared band ${band.min}…${band.max}`);
- for (const bucket of rows) {
- const live = Number(bucket.row_count) - Number(bucket.tombstoned);
- console.log(
- ` ${bucket.source.padEnd(14)} ` +
- `${String(bucket.row_count).padStart(7)} row(s) ` +
- `(${live} live, ${bucket.tombstoned} tombstoned) ` +
- `across ${bucket.record_count} record(s) — ` +
- `observed ${bucket.min_value}…${bucket.max_value}, ` +
- `${ymd(bucket.first_at)} → ${ymd(bucket.last_at)}`,
- );
- }
- }
-
- console.log(
- `\nThe sync writers refuse these values now, so the population above is ` +
- `closed unless a writer without the gate is still in play. Removing a ` +
- `row is a maintainer decision, per bucket.`,
- );
-}
-
-main()
- .catch((err) => {
- console.error(err);
- process.exitCode = 1;
- })
- .finally(() => {
- void prisma.$disconnect();
- });
diff --git a/scripts/diagnose-manual-sleep-hours.ts b/scripts/diagnose-manual-sleep-hours.ts
deleted file mode 100644
index 38a4b51a9..000000000
--- a/scripts/diagnose-manual-sleep-hours.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * Read-only diagnostic for hand-entered sleep rows stored in the wrong unit.
- *
- * `SLEEP_DURATION` is stored in minutes. The manual entry field asked for
- * hours and sent the typed number through unconverted, so a night entered as
- * 7.5 was filed as seven and a half MINUTES. Nothing objected: a single sleep
- * STAGE really can be that short, so the value sits inside the column's
- * plausibility band and looks like an ordinary row.
- *
- * That ambiguity is why this reports and does not repair. A stage segment of
- * eight minutes and a night typed as eight hours are the same number in the
- * same column, and only the person who entered it knows which it was. What CAN
- * be narrowed is the population: a MANUAL, whole-night row (no sleep stage) is
- * one somebody typed into the field, and a typed night shorter than the
- * longest plausible hours entry is almost certainly hours that were read as
- * minutes.
- *
- * The output lists the candidates with their date, stored value and the night
- * they would have been if the number was hours, so the maintainer can decide
- * per row. Nothing is written.
- *
- * Run (inside the app container, which has DATABASE_URL):
- * pnpm dlx tsx scripts/diagnose-manual-sleep-hours.ts
- */
-import "dotenv/config";
-
-import { prisma } from "@/lib/db";
-import { MINUTES_PER_HOUR } from "@/lib/measurements/entry-units";
-
-/**
- * Longest night anybody types into an hours field. A stored value at or below
- * this reads as hours; above it, the number can only ever have been minutes.
- */
-const MAX_PLAUSIBLE_HOURS = 24;
-
-async function main(): Promise {
- const suspects = await prisma.measurement.findMany({
- where: {
- type: "SLEEP_DURATION",
- source: "MANUAL",
- deletedAt: null,
- // Whole-night rows only — a staged row comes from a sync source, never
- // from the entry field.
- sleepStage: null,
- value: { gt: 0, lte: MAX_PLAUSIBLE_HOURS },
- },
- orderBy: [{ userId: "asc" }, { measuredAt: "asc" }],
- select: {
- id: true,
- userId: true,
- value: true,
- measuredAt: true,
- createdAt: true,
- },
- });
-
- if (suspects.length === 0) {
- console.log(
- "No hand-entered sleep rows in the hours-shaped band. Nothing to review.",
- );
- return;
- }
-
- const byUser = new Map();
- for (const row of suspects) {
- const bucket = byUser.get(row.userId);
- if (bucket) bucket.push(row);
- else byUser.set(row.userId, [row]);
- }
-
- console.log(
- `${suspects.length} hand-entered sleep row(s) across ${byUser.size} record(s) ` +
- `store a value of ${MAX_PLAUSIBLE_HOURS} or less.\n` +
- `Each is either a very short recorded sleep, or hours that were read as minutes.\n`,
- );
-
- for (const [userId, rows] of byUser) {
- console.log(`record ${userId} — ${rows.length} row(s)`);
- for (const row of rows) {
- const asHours = row.value * MINUTES_PER_HOUR;
- console.log(
- ` ${row.measuredAt.toISOString().slice(0, 10)} id=${row.id} ` +
- `stored=${row.value} min if it was hours=${asHours} min ` +
- `(${(asHours / MINUTES_PER_HOUR).toFixed(2)} h) ` +
- `entered=${row.createdAt.toISOString().slice(0, 10)}`,
- );
- }
- console.log("");
- }
-
- console.log(
- "Nothing was changed. Correct a row from the record list, or delete and re-enter it.",
- );
-}
-
-main()
- .catch((error) => {
- console.error(error);
- process.exitCode = 1;
- })
- .finally(() => {
- void prisma.$disconnect();
- });
diff --git a/scripts/diagnose-noncanonical-measurement-units.ts b/scripts/diagnose-noncanonical-measurement-units.ts
deleted file mode 100644
index 9c0667bda..000000000
--- a/scripts/diagnose-noncanonical-measurement-units.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-/**
- * Read-only diagnostic for measurement rows stored in a NON-canonical unit.
- *
- * Canonical storage is SI: every write path stamps `getUnitForType(type)`. The
- * one historical leak was the MCP `log_measurement` tool, which persisted a
- * caller-supplied `unit` verbatim (closed in v1.32.26). A row whose `unit`
- * column differs from its type's canonical unit is such a legacy row, and it
- * mislabels every display surface that trusts the column.
- *
- * This groups measurements by `(type, unit)` and prints every group whose
- * `unit` is not the canonical unit for its type, flagging whether the stored
- * unit is a RECOGNISED alias (mechanically convertible by the companion
- * convert script) or UNKNOWN (needs a manual look). Nothing is written.
- *
- * NOT auto-fixable and deliberately out of scope: a row whose stored unit IS
- * already canonical but whose VALUE was a foreign unit (e.g. a lb reading typed
- * into the old kg-labelled entry form). Those are indistinguishable from real
- * canonical readings; the reporter edits them via the record list.
- *
- * Run (inside the app container, which has DATABASE_URL):
- * pnpm dlx tsx scripts/diagnose-noncanonical-measurement-units.ts
- */
-import "dotenv/config";
-
-import { prisma } from "@/lib/db";
-import { getUnitForType } from "@/lib/validations/measurement";
-import { resolveToCanonicalUnit } from "@/lib/measurements/unit-aliases";
-
-async function main(): Promise {
- const groups = await prisma.measurement.groupBy({
- by: ["type", "unit"],
- _count: { _all: true },
- });
-
- const mismatches = groups.filter((g) => {
- if (g.unit === null) return false;
- return g.unit !== getUnitForType(g.type);
- });
-
- if (mismatches.length === 0) {
- console.log("No non-canonical measurement units found. Storage is clean.");
- return;
- }
-
- let convertible = 0;
- let unknown = 0;
- console.log(
- `Found ${mismatches.length} non-canonical (type, unit) group(s):\n`,
- );
- for (const g of mismatches.sort((a, b) => b._count._all - a._count._all)) {
- const canonical = getUnitForType(g.type);
- // Probe convertibility with a neutral value — only the recognition matters.
- const recognised = resolveToCanonicalUnit(g.type, 1, g.unit as string);
- const status = recognised ? "CONVERTIBLE" : "UNKNOWN — manual review";
- if (recognised) convertible += g._count._all;
- else unknown += g._count._all;
- console.log(
- ` ${g.type}: stored="${g.unit}" canonical="${canonical}" ` +
- `rows=${g._count._all} [${status}]`,
- );
- }
- console.log(
- `\nTotals: ${convertible} row(s) convertible, ${unknown} row(s) unknown.`,
- );
- console.log(
- "Convert the recognised aliases with:\n" +
- " pnpm dlx tsx scripts/convert-noncanonical-measurement-units.ts --execute",
- );
-}
-
-main()
- .catch((err) => {
- console.error(err);
- process.exitCode = 1;
- })
- .finally(() => prisma.$disconnect());
diff --git a/scripts/i18n-key-tool.mjs b/scripts/i18n-key-tool.mjs
index 4aa1ca599..26b4de22d 100644
--- a/scripts/i18n-key-tool.mjs
+++ b/scripts/i18n-key-tool.mjs
@@ -1,5 +1,5 @@
/**
- * One-shot helper for a text sweep across the six locale bundles.
+ * One-shot helper for a text sweep across every locale bundle.
*
* node scripts/i18n-key-tool.mjs drop [...]
* node scripts/i18n-key-tool.mjs set
diff --git a/scripts/i18n/build-locale.py b/scripts/i18n/build-locale.py
deleted file mode 100644
index e37905ca2..000000000
--- a/scripts/i18n/build-locale.py
+++ /dev/null
@@ -1,404 +0,0 @@
-#!/usr/bin/env python3
-"""
-v1.4.25 W9e — build messages/{fr,es,it,pl}.json from messages/en.json.
-
-These are AI-initial translations. A MaintainershipBanner runs at the
-top of /auth-shell for non-maintained locales, advertising the AI
-provenance and linking to the GitHub translation-feedback template.
-
-We translate via a layered lookup:
- 1. Full-path overrides (e.g. "auth.email" -> precise FR)
- 2. Last-segment lookups (covers Save/Cancel/Delete/etc.)
- 3. Whole-string lookups (covers common short phrases)
- 4. Token-by-token translation for sentences
- 5. Fallback: keep the English value verbatim (the banner says so)
-
-Placeholders ({foo}, {bar}, ) are preserved verbatim — the
-translator never touches text inside curly braces or angle brackets.
-
-Run: python3 scripts/i18n/build-locale.py
-"""
-import json
-import os
-import re
-import sys
-from pathlib import Path
-
-ROOT = Path(__file__).resolve().parents[2]
-MESSAGES = ROOT / "messages"
-EN = json.loads((MESSAGES / "en.json").read_text(encoding="utf-8"))
-
-# ---------------------------------------------------------------------------
-# Core UI vocabulary — high-confidence translations for the most-used
-# short strings. Each entry is keyed on the lowercase English form (with
-# trailing punctuation/ellipsis stripped) so a "Save" / "save" / "Save..."
-# all hit the same row.
-# ---------------------------------------------------------------------------
-# Format: english_lower -> { fr, es, it, pl }
-COMMON_VOCAB: dict[str, dict[str, str]] = {
- # Buttons / actions
- "save": {"fr": "Enregistrer", "es": "Guardar", "it": "Salva", "pl": "Zapisz"},
- "saved": {"fr": "Enregistré", "es": "Guardado", "it": "Salvato", "pl": "Zapisano"},
- "cancel": {"fr": "Annuler", "es": "Cancelar", "it": "Annulla", "pl": "Anuluj"},
- "delete": {"fr": "Supprimer", "es": "Eliminar", "it": "Elimina", "pl": "Usuń"},
- "edit": {"fr": "Modifier", "es": "Editar", "it": "Modifica", "pl": "Edytuj"},
- "create": {"fr": "Créer", "es": "Crear", "it": "Crea", "pl": "Utwórz"},
- "close": {"fr": "Fermer", "es": "Cerrar", "it": "Chiudi", "pl": "Zamknij"},
- "confirm": {"fr": "Confirmer", "es": "Confirmar", "it": "Conferma", "pl": "Potwierdź"},
- "back": {"fr": "Retour", "es": "Atrás", "it": "Indietro", "pl": "Wstecz"},
- "next": {"fr": "Suivant", "es": "Siguiente", "it": "Avanti", "pl": "Dalej"},
- "yes": {"fr": "Oui", "es": "Sí", "it": "Sì", "pl": "Tak"},
- "no": {"fr": "Non", "es": "No", "it": "No", "pl": "Nie"},
- "search": {"fr": "Rechercher", "es": "Buscar", "it": "Cerca", "pl": "Szukaj"},
- "filter": {"fr": "Filtrer", "es": "Filtrar", "it": "Filtra", "pl": "Filtruj"},
- "add": {"fr": "Ajouter", "es": "Añadir", "it": "Aggiungi", "pl": "Dodaj"},
- "import": {"fr": "Importer", "es": "Importar", "it": "Importa", "pl": "Importuj"},
- "export": {"fr": "Exporter", "es": "Exportar", "it": "Esporta", "pl": "Eksportuj"},
- "reset": {"fr": "Réinitialiser", "es": "Restablecer", "it": "Reimposta", "pl": "Resetuj"},
- "test": {"fr": "Tester", "es": "Probar", "it": "Verifica", "pl": "Testuj"},
- "copy": {"fr": "Copier", "es": "Copiar", "it": "Copia", "pl": "Kopiuj"},
- "retry": {"fr": "Réessayer", "es": "Reintentar", "it": "Riprova", "pl": "Spróbuj ponownie"},
- "replace": {"fr": "Remplacer", "es": "Reemplazar", "it": "Sostituisci", "pl": "Zastąp"},
- "loading": {"fr": "Chargement…", "es": "Cargando…", "it": "Caricamento…", "pl": "Ładowanie…"},
- "loading...": {"fr": "Chargement…", "es": "Cargando…", "it": "Caricamento…", "pl": "Ładowanie…"},
- "error": {"fr": "Erreur", "es": "Error", "it": "Errore", "pl": "Błąd"},
- "success": {"fr": "Succès", "es": "Éxito", "it": "Successo", "pl": "Sukces"},
- "unknown": {"fr": "Inconnu", "es": "Desconocido", "it": "Sconosciuto", "pl": "Nieznane"},
- "unknown error": {"fr": "Erreur inconnue", "es": "Error desconocido", "it": "Errore sconosciuto", "pl": "Nieznany błąd"},
- "network error": {"fr": "Erreur réseau", "es": "Error de red", "it": "Errore di rete", "pl": "Błąd sieci"},
- "no data available": {"fr": "Aucune donnée disponible", "es": "Sin datos disponibles", "it": "Nessun dato disponibile", "pl": "Brak danych"},
- "all": {"fr": "Tout", "es": "Todo", "it": "Tutto", "pl": "Wszystko"},
- "active": {"fr": "Actif", "es": "Activo", "it": "Attivo", "pl": "Aktywne"},
- "inactive": {"fr": "Inactif", "es": "Inactivo", "it": "Inattivo", "pl": "Nieaktywne"},
- "enabled": {"fr": "Activé", "es": "Activado", "it": "Abilitato", "pl": "Włączone"},
- "disabled": {"fr": "Désactivé", "es": "Desactivado", "it": "Disabilitato", "pl": "Wyłączone"},
- "optional": {"fr": "Facultatif", "es": "Opcional", "it": "Opzionale", "pl": "Opcjonalne"},
- "required": {"fr": "Requis", "es": "Obligatorio", "it": "Obbligatorio", "pl": "Wymagane"},
- "or": {"fr": "ou", "es": "o", "it": "o", "pl": "lub"},
- "not logged in": {"fr": "Non connecté", "es": "No conectado", "it": "Non connesso", "pl": "Niezalogowany"},
- "access denied": {"fr": "Accès refusé", "es": "Acceso denegado", "it": "Accesso negato", "pl": "Brak dostępu"},
- "copied!": {"fr": "Copié !", "es": "¡Copiado!", "it": "Copiato!", "pl": "Skopiowano!"},
- "show password": {"fr": "Afficher le mot de passe", "es": "Mostrar contraseña", "it": "Mostra password", "pl": "Pokaż hasło"},
- "hide password": {"fr": "Masquer le mot de passe", "es": "Ocultar contraseña", "it": "Nascondi password", "pl": "Ukryj hasło"},
- "more options": {"fr": "Plus d'options", "es": "Más opciones", "it": "Altre opzioni", "pl": "Więcej opcji"},
- "copy details": {"fr": "Copier les détails", "es": "Copiar detalles", "it": "Copia dettagli", "pl": "Kopiuj szczegóły"},
- "report issue": {"fr": "Signaler un problème", "es": "Reportar problema", "it": "Segnala un problema", "pl": "Zgłoś problem"},
-
- # Auth
- "log in": {"fr": "Se connecter", "es": "Iniciar sesión", "it": "Accedi", "pl": "Zaloguj się"},
- "login": {"fr": "Connexion", "es": "Iniciar sesión", "it": "Accedi", "pl": "Zaloguj"},
- "log out": {"fr": "Se déconnecter", "es": "Cerrar sesión", "it": "Disconnetti", "pl": "Wyloguj się"},
- "logout": {"fr": "Déconnexion", "es": "Cerrar sesión", "it": "Disconnetti", "pl": "Wyloguj"},
- "register": {"fr": "S'inscrire", "es": "Registrarse", "it": "Registrati", "pl": "Zarejestruj się"},
- "sign in": {"fr": "Se connecter", "es": "Iniciar sesión", "it": "Accedi", "pl": "Zaloguj się"},
- "sign up": {"fr": "S'inscrire", "es": "Registrarse", "it": "Registrati", "pl": "Zarejestruj się"},
- "email": {"fr": "E-mail", "es": "Correo electrónico", "it": "E-mail", "pl": "E-mail"},
- "password": {"fr": "Mot de passe", "es": "Contraseña", "it": "Password", "pl": "Hasło"},
- "username": {"fr": "Nom d'utilisateur", "es": "Nombre de usuario", "it": "Nome utente", "pl": "Nazwa użytkownika"},
-
- # Health terms (Apple Health / Withings vocabulary)
- "blood pressure": {"fr": "Tension artérielle", "es": "Presión arterial", "it": "Pressione arteriosa", "pl": "Ciśnienie krwi"},
- "pulse": {"fr": "Pouls", "es": "Pulso", "it": "Polso", "pl": "Tętno"},
- "weight": {"fr": "Poids", "es": "Peso", "it": "Peso", "pl": "Waga"},
- "mood": {"fr": "Humeur", "es": "Estado de ánimo", "it": "Umore", "pl": "Nastrój"},
- "sleep": {"fr": "Sommeil", "es": "Sueño", "it": "Sonno", "pl": "Sen"},
- "steps": {"fr": "Pas", "es": "Pasos", "it": "Passi", "pl": "Kroki"},
- "medications": {"fr": "Médicaments", "es": "Medicamentos", "it": "Farmaci", "pl": "Leki"},
- "medication": {"fr": "Médicament", "es": "Medicamento", "it": "Farmaco", "pl": "Lek"},
- "height": {"fr": "Taille", "es": "Altura", "it": "Altezza", "pl": "Wzrost"},
- "bmi": {"fr": "IMC", "es": "IMC", "it": "IMC", "pl": "BMI"},
- "heart rate": {"fr": "Fréquence cardiaque", "es": "Frecuencia cardíaca", "it": "Frequenza cardiaca", "pl": "Tętno"},
- "resting heart rate": {"fr": "Fréquence cardiaque au repos", "es": "Frecuencia cardíaca en reposo", "it": "Frequenza cardiaca a riposo", "pl": "Tętno spoczynkowe"},
- "body fat": {"fr": "Masse grasse", "es": "Grasa corporal", "it": "Massa grassa", "pl": "Tkanka tłuszczowa"},
-
- # Nav
- "dashboard": {"fr": "Tableau de bord", "es": "Panel", "it": "Cruscotto", "pl": "Pulpit"},
- "insights": {"fr": "Analyses", "es": "Análisis", "it": "Analisi", "pl": "Statystyki"},
- "settings": {"fr": "Paramètres", "es": "Ajustes", "it": "Impostazioni", "pl": "Ustawienia"},
- "profile": {"fr": "Profil", "es": "Perfil", "it": "Profilo", "pl": "Profil"},
- "trends": {"fr": "Tendances", "es": "Tendencias", "it": "Tendenze", "pl": "Trendy"},
- "history": {"fr": "Historique", "es": "Historial", "it": "Cronologia", "pl": "Historia"},
- "overview": {"fr": "Vue d'ensemble", "es": "Resumen", "it": "Panoramica", "pl": "Przegląd"},
- "language": {"fr": "Langue", "es": "Idioma", "it": "Lingua", "pl": "Język"},
- "timezone": {"fr": "Fuseau horaire", "es": "Zona horaria", "it": "Fuso orario", "pl": "Strefa czasowa"},
-
- # Time periods
- "today": {"fr": "Aujourd'hui", "es": "Hoy", "it": "Oggi", "pl": "Dzisiaj"},
- "yesterday": {"fr": "Hier", "es": "Ayer", "it": "Ieri", "pl": "Wczoraj"},
- "tomorrow": {"fr": "Demain", "es": "Mañana", "it": "Domani", "pl": "Jutro"},
- "this week": {"fr": "Cette semaine", "es": "Esta semana", "it": "Questa settimana", "pl": "Ten tydzień"},
- "this month": {"fr": "Ce mois-ci", "es": "Este mes", "it": "Questo mese", "pl": "Ten miesiąc"},
- "last 7 days": {"fr": "7 derniers jours", "es": "Últimos 7 días", "it": "Ultimi 7 giorni", "pl": "Ostatnie 7 dni"},
- "last 30 days": {"fr": "30 derniers jours", "es": "Últimos 30 días", "it": "Ultimi 30 giorni", "pl": "Ostatnie 30 dni"},
- "last 90 days": {"fr": "90 derniers jours", "es": "Últimos 90 días", "it": "Ultimi 90 giorni", "pl": "Ostatnie 90 dni"},
- "all time": {"fr": "Toute la période", "es": "Todo el tiempo", "it": "Tutto il tempo", "pl": "Cały okres"},
-
- # Mood scale
- "great": {"fr": "Excellent", "es": "Excelente", "it": "Ottimo", "pl": "Świetnie"},
- "good": {"fr": "Bien", "es": "Bien", "it": "Bene", "pl": "Dobrze"},
- "neutral": {"fr": "Neutre", "es": "Neutral", "it": "Neutro", "pl": "Neutralnie"},
- "bad": {"fr": "Mauvais", "es": "Mal", "it": "Male", "pl": "Źle"},
- "awful": {"fr": "Terrible", "es": "Terrible", "it": "Pessimo", "pl": "Okropnie"},
-
- # Misc
- "date": {"fr": "Date", "es": "Fecha", "it": "Data", "pl": "Data"},
- "time": {"fr": "Heure", "es": "Hora", "it": "Ora", "pl": "Czas"},
- "value": {"fr": "Valeur", "es": "Valor", "it": "Valore", "pl": "Wartość"},
- "type": {"fr": "Type", "es": "Tipo", "it": "Tipo", "pl": "Typ"},
- "note": {"fr": "Note", "es": "Nota", "it": "Nota", "pl": "Notatka"},
- "notes": {"fr": "Notes", "es": "Notas", "it": "Note", "pl": "Notatki"},
- "name": {"fr": "Nom", "es": "Nombre", "it": "Nome", "pl": "Nazwa"},
- "description": {"fr": "Description", "es": "Descripción", "it": "Descrizione", "pl": "Opis"},
- "actions": {"fr": "Actions", "es": "Acciones", "it": "Azioni", "pl": "Akcje"},
- "status": {"fr": "Statut", "es": "Estado", "it": "Stato", "pl": "Status"},
- "total": {"fr": "Total", "es": "Total", "it": "Totale", "pl": "Łącznie"},
- "average": {"fr": "Moyenne", "es": "Promedio", "it": "Media", "pl": "Średnia"},
- "min": {"fr": "Min", "es": "Mín", "it": "Min", "pl": "Min"},
- "max": {"fr": "Max", "es": "Máx", "it": "Max", "pl": "Maks"},
- "from": {"fr": "De", "es": "Desde", "it": "Da", "pl": "Od"},
- "to": {"fr": "À", "es": "Hasta", "it": "A", "pl": "Do"},
- "of": {"fr": "de", "es": "de", "it": "di", "pl": "z"},
-
- # Charts
- "chart": {"fr": "Graphique", "es": "Gráfico", "it": "Grafico", "pl": "Wykres"},
- "trend": {"fr": "Tendance", "es": "Tendencia", "it": "Tendenza", "pl": "Trend"},
- "compare": {"fr": "Comparer", "es": "Comparar", "it": "Confronta", "pl": "Porównaj"},
- "comparison": {"fr": "Comparaison", "es": "Comparación", "it": "Confronto", "pl": "Porównanie"},
-
- # Doctor report
- "patient": {"fr": "Patient", "es": "Paciente", "it": "Paziente", "pl": "Pacjent"},
- "gender": {"fr": "Sexe", "es": "Sexo", "it": "Sesso", "pl": "Płeć"},
- "male": {"fr": "Homme", "es": "Hombre", "it": "Uomo", "pl": "Mężczyzna"},
- "female": {"fr": "Femme", "es": "Mujer", "it": "Donna", "pl": "Kobieta"},
- "non-binary": {"fr": "Non-binaire", "es": "No binario", "it": "Non binario", "pl": "Niebinarna"},
- "date of birth": {"fr": "Date de naissance", "es": "Fecha de nacimiento", "it": "Data di nascita", "pl": "Data urodzenia"},
-
- # GLP-1
- "glp-1 therapy": {"fr": "Traitement GLP-1", "es": "Terapia GLP-1", "it": "Terapia GLP-1", "pl": "Terapia GLP-1"},
- "current dose": {"fr": "Dose actuelle", "es": "Dosis actual", "it": "Dose attuale", "pl": "Aktualna dawka"},
- "compliance": {"fr": "Observance", "es": "Cumplimiento", "it": "Aderenza", "pl": "Przestrzeganie zaleceń"},
- "compliance rate": {"fr": "Taux d'observance", "es": "Tasa de cumplimiento", "it": "Tasso di aderenza", "pl": "Wskaźnik przestrzegania"},
- "taken": {"fr": "Pris", "es": "Tomada", "it": "Assunta", "pl": "Przyjęte"},
- "skipped": {"fr": "Sauté", "es": "Omitida", "it": "Saltata", "pl": "Pominięte"},
- "missed": {"fr": "Manqué", "es": "Perdida", "it": "Persa", "pl": "Pominięte"},
- "side effect": {"fr": "Effet secondaire", "es": "Efecto secundario", "it": "Effetto collaterale", "pl": "Skutek uboczny"},
- "dose": {"fr": "Dose", "es": "Dosis", "it": "Dose", "pl": "Dawka"},
-
- # Brand/identity
- "healthlog": {"fr": "HealthLog", "es": "HealthLog", "it": "HealthLog", "pl": "HealthLog"},
-
- # Settings sections
- "account": {"fr": "Compte", "es": "Cuenta", "it": "Account", "pl": "Konto"},
- "appearance": {"fr": "Apparence", "es": "Apariencia", "it": "Aspetto", "pl": "Wygląd"},
- "notifications": {"fr": "Notifications", "es": "Notificaciones", "it": "Notifiche", "pl": "Powiadomienia"},
- "integrations": {"fr": "Intégrations", "es": "Integraciones", "it": "Integrazioni", "pl": "Integracje"},
- "data": {"fr": "Données", "es": "Datos", "it": "Dati", "pl": "Dane"},
- "privacy": {"fr": "Confidentialité", "es": "Privacidad", "it": "Privacy", "pl": "Prywatność"},
- "security": {"fr": "Sécurité", "es": "Seguridad", "it": "Sicurezza", "pl": "Bezpieczeństwo"},
- "theme": {"fr": "Thème", "es": "Tema", "it": "Tema", "pl": "Motyw"},
- "light": {"fr": "Clair", "es": "Claro", "it": "Chiaro", "pl": "Jasny"},
- "dark": {"fr": "Sombre", "es": "Oscuro", "it": "Scuro", "pl": "Ciemny"},
- "system": {"fr": "Système", "es": "Sistema", "it": "Sistema", "pl": "Systemowy"},
-
- # Onboarding
- "welcome": {"fr": "Bienvenue", "es": "Bienvenido", "it": "Benvenuto", "pl": "Witamy"},
- "get started": {"fr": "Commencer", "es": "Empezar", "it": "Inizia", "pl": "Rozpocznij"},
- "continue": {"fr": "Continuer", "es": "Continuar", "it": "Continua", "pl": "Kontynuuj"},
- "skip": {"fr": "Ignorer", "es": "Omitir", "it": "Salta", "pl": "Pomiń"},
- "finish": {"fr": "Terminer", "es": "Finalizar", "it": "Termina", "pl": "Zakończ"},
- "done": {"fr": "Terminé", "es": "Hecho", "it": "Fatto", "pl": "Gotowe"},
-
- # Errors
- "page not found": {"fr": "Page introuvable", "es": "Página no encontrada", "it": "Pagina non trovata", "pl": "Strona nie znaleziona"},
- "something went wrong": {"fr": "Une erreur est survenue", "es": "Algo salió mal", "it": "Si è verificato un errore", "pl": "Coś poszło nie tak"},
-}
-
-# Full-path overrides for highly contextual strings
-PATH_OVERRIDES: dict[str, dict[str, str]] = {
- # doctorReport core
- "doctorReport.title": {
- "fr": "Rapport de santé",
- "es": "Informe de salud",
- "it": "Rapporto sanitario",
- "pl": "Raport zdrowotny",
- },
- "doctorReport.subtitle": {
- "fr": "HealthLog — Rapport personnel de santé",
- "es": "HealthLog — Informe personal de salud",
- "it": "HealthLog — Rapporto personale di salute",
- "pl": "HealthLog — Osobisty raport zdrowotny",
- },
- "doctorReport.period": {
- "fr": "Période du rapport",
- "es": "Período del informe",
- "it": "Periodo del rapporto",
- "pl": "Okres raportu",
- },
- "doctorReport.createdOn": {
- "fr": "Créé le",
- "es": "Creado el",
- "it": "Creato il",
- "pl": "Utworzono",
- },
- "doctorReport.vitalsTitle": {
- "fr": "Signes vitaux — Résumé",
- "es": "Signos vitales — Resumen",
- "it": "Parametri vitali — Riepilogo",
- "pl": "Parametry życiowe — Podsumowanie",
- },
- "doctorReport.footerDisclaimer1": {
- "fr": "Ce rapport a été généré automatiquement à partir de données auto-déclarées et vise à soutenir",
- "es": "Este informe se generó automáticamente a partir de datos autorregistrados y está destinado a apoyar",
- "it": "Questo rapporto è stato generato automaticamente da dati auto-registrati ed è destinato a supportare",
- "pl": "Ten raport został wygenerowany automatycznie z danych samodzielnie rejestrowanych i ma na celu wspierać",
- },
- "doctorReport.footerDisclaimer2": {
- "fr": "la conversation médecin-patient. Il ne remplace pas un diagnostic médical. Les corrélations n'impliquent pas la causalité.",
- "es": "la conversación entre médico y paciente. No sustituye un diagnóstico médico. Las correlaciones no implican causalidad.",
- "it": "il dialogo medico-paziente. Non sostituisce una diagnosi medica. Le correlazioni non implicano causalità.",
- "pl": "rozmowę lekarza z pacjentem. Nie zastępuje diagnozy medycznej. Korelacje nie oznaczają związku przyczynowego.",
- },
- "doctorReport.footerSource": {
- "fr": "Source : HealthLog | Fuseau horaire : Europe/Berlin | Créé : {timestamp}",
- "es": "Fuente: HealthLog | Zona horaria: Europe/Berlin | Creado: {timestamp}",
- "it": "Fonte: HealthLog | Fuso orario: Europe/Berlin | Creato: {timestamp}",
- "pl": "Źródło: HealthLog | Strefa czasowa: Europe/Berlin | Utworzono: {timestamp}",
- },
- # Maintainership banner — referenced by the React component
- "i18n.maintainershipBanner.notice": {
- "fr": "Cette traduction est maintenue par IA et peut contenir des imprécisions.",
- "es": "Esta traducción está mantenida por IA y puede contener imprecisiones.",
- "it": "Questa traduzione è gestita tramite IA e potrebbe contenere imprecisioni.",
- "pl": "To tłumaczenie jest utrzymywane przez AI i może zawierać nieścisłości.",
- },
- "i18n.maintainershipBanner.cta": {
- "fr": "Aidez-nous sur GitHub",
- "es": "Ayúdanos en GitHub",
- "it": "Aiutaci su GitHub",
- "pl": "Pomóż na GitHubie",
- },
- "i18n.maintainershipBanner.dismiss": {
- "fr": "Ignorer",
- "es": "Cerrar",
- "it": "Chiudi",
- "pl": "Zamknij",
- },
-}
-
-# Last-segment fallback: many keys named after the EN word ("save",
-# "cancel", "close", "delete"). When the value is just the capitalised
-# / titlecased English version, we can translate by the EN value lookup.
-PLACEHOLDER_RE = re.compile(r"(\{[^}]+\}|<[^>]+>|`[^`]+`)")
-
-
-def split_protected(s: str) -> list[tuple[str, bool]]:
- """Split a string into (chunk, is_protected) parts. Protected chunks
- are placeholders/tags that must NEVER be translated."""
- parts: list[tuple[str, bool]] = []
- last = 0
- for m in PLACEHOLDER_RE.finditer(s):
- if m.start() > last:
- parts.append((s[last : m.start()], False))
- parts.append((m.group(0), True))
- last = m.end()
- if last < len(s):
- parts.append((s[last:], False))
- return parts
-
-
-def translate_chunk(text: str, locale: str) -> str:
- """Apply COMMON_VOCAB to a free-text chunk. Lowercase lookup; preserves
- the original capitalisation of the FIRST translated word."""
- if not text.strip():
- return text
- # Whole-string lookup first
- key = text.strip().lower().rstrip(".!?:,;")
- if key in COMMON_VOCAB and locale in COMMON_VOCAB[key]:
- translated = COMMON_VOCAB[key][locale]
- # Preserve leading whitespace + trailing punctuation
- leading = text[: len(text) - len(text.lstrip())]
- trailing_punct = ""
- stripped = text.strip()
- while stripped and stripped[-1] in ".!?:,;":
- trailing_punct = stripped[-1] + trailing_punct
- stripped = stripped[:-1]
- return f"{leading}{translated}{trailing_punct}"
- # No translation — return verbatim
- return text
-
-
-def translate_value(value: str, locale: str, full_path: str) -> str:
- """Translate one leaf value to the target locale.
-
- Priority:
- 1. PATH_OVERRIDES exact match
- 2. Whole-value lookup in COMMON_VOCAB
- 3. Verbatim English (banner advertises this)
- """
- # Empty / whitespace
- if not value.strip():
- return value
-
- # 1. Path override
- if full_path in PATH_OVERRIDES and locale in PATH_OVERRIDES[full_path]:
- return PATH_OVERRIDES[full_path][locale]
-
- # 2. Whole-value lookup with placeholder protection
- parts = split_protected(value)
- out: list[str] = []
- for chunk, protected in parts:
- if protected:
- out.append(chunk)
- else:
- out.append(translate_chunk(chunk, locale))
- return "".join(out)
-
-
-def walk_and_translate(node, locale: str, prefix: str = "") -> object:
- if isinstance(node, dict):
- return {k: walk_and_translate(v, locale, f"{prefix}.{k}" if prefix else k) for k, v in node.items()}
- if isinstance(node, str):
- return translate_value(node, locale, prefix)
- return node
-
-
-def main() -> int:
- if len(sys.argv) > 1:
- targets = sys.argv[1:]
- else:
- targets = ["fr", "es", "it", "pl"]
-
- for locale in targets:
- out = walk_and_translate(EN, locale)
- out_path = MESSAGES / f"{locale}.json"
- # Preserve EN's pretty-print style (2-space indent, sorted? — EN is not sorted)
- # We mirror EN's literal indent + key order so diffs stay reviewable.
- out_path.write_text(
- json.dumps(out, ensure_ascii=False, indent=2) + "\n",
- encoding="utf-8",
- )
- # Verify key parity
- new = json.loads(out_path.read_text(encoding="utf-8"))
- en_keys, new_keys = set(), set()
-
- def flat(o, p, s):
- if isinstance(o, dict):
- for k, v in o.items():
- flat(v, f"{p}.{k}" if p else k, s)
- elif isinstance(o, str):
- s.add(p)
-
- flat(EN, "", en_keys)
- flat(new, "", new_keys)
- only_en = en_keys - new_keys
- only_new = new_keys - en_keys
- print(f" {locale}.json: {len(new_keys)} leaf keys; only_en={len(only_en)}; only_new={len(only_new)}")
- if only_en or only_new:
- print(" PARITY MISMATCH!", file=sys.stderr)
- return 1
-
- return 0
-
-
-if __name__ == "__main__":
- sys.exit(main())
diff --git a/scripts/migrate-demo-edge01.README.md b/scripts/migrate-demo-account.README.md
similarity index 72%
rename from scripts/migrate-demo-edge01.README.md
rename to scripts/migrate-demo-account.README.md
index f21a6414d..dfb6a17dd 100644
--- a/scripts/migrate-demo-edge01.README.md
+++ b/scripts/migrate-demo-account.README.md
@@ -1,28 +1,30 @@
-# migrate-demo-edge01.sh
+# migrate-demo-account.sh
-Copies **exactly one** user — the demo account
-`usr_demo_cf31025295714ece8d91f5af13afd76d` — from the production apps01
-Postgres to the **separate** edge01 demo Postgres. No other user's rows are
-transferred under any circumstance.
+Copies **exactly one** user, the demo account named by `DEMO_USER_ID`, from a
+source instance's Postgres to the **separate** Postgres of a demo instance. No
+other user's rows are transferred under any circumstance.
> Review the script first, then run it deliberately. It performs destructive,
-> demo-scoped deletes on the edge01 demo DB, then loads the apps01 demo slice.
-> It never writes to apps01 (source is read-only).
+> demo-scoped deletes on the target demo DB, then loads the demo slice from the
+> source. It never writes to the source (read-only).
## How it connects
-- **Source (apps01):** SSH alias `apps-01`, container `db-pg8wggwogo8c4gc4ks0kk4ss`
- (stable), psql `healthlog`/`healthlog`.
-- **Target (edge01):** SSH alias `edge-01`. The demo Postgres is a separate
- Coolify resource whose container name is **not hardcoded** — the script
- resolves it at runtime via `docker ps | grep -iE 'postgres|^db-'` and aborts
- if it cannot pick a single match (override with `DST_DB_CONTAINER=db-xxxx`).
- The demo **app** uuid is `ck8cs4osswg8w440gskw08w8`; match its linked
- Postgres resource, not the docs/landing DB.
-- **Edge demo user id is resolved at runtime** by `username='demo'` (it may
- differ from the apps01 id). The wipe phase is scoped to that resolved id; the
- load then brings the apps01 row in, so afterwards the edge01 demo user
- carries the apps01 canonical id.
+Everything instance-specific comes from the environment:
+
+- `DEMO_USER_ID`: the user id to copy. `PROTECT_USER_ID`: an id that must
+ never appear on the target (the operator's own account).
+- `SRC_SSH` and `SRC_DB_CONTAINER_PATTERN`: the source host's ssh alias and a
+ `grep -E` pattern for its database container; psql `healthlog`/`healthlog`.
+- `DST_SSH`: the target host's ssh alias. The demo Postgres container is
+ **not hardcoded**: the script resolves it at runtime via
+ `docker ps | grep -iE 'postgres|^db-'` and aborts if it cannot pick a single
+ match (override with `DST_DB_CONTAINER=db-xxxx`). Match the demo app's own
+ Postgres, not a docs or landing-page database on the same host.
+- **The target demo user id is resolved at runtime** by `username='demo'` (it
+ may differ from the source id). The wipe phase is scoped to that resolved id;
+ the load then brings the source row in, so afterwards the target demo user
+ carries the source's canonical id.
Each table is transferred with a scoped, server-side `\copy ... TO STDOUT`
piped host-to-host into `\copy ... FROM STDIN`, using **explicit column lists
@@ -97,9 +99,9 @@ acknowledgement).
`*_oauth_states` / `*_connect_tickets`, telegram scheduled-deletions +
reminder messages, clinician_share_links, provider_health) — tied to live
credentials.
-- **Encrypted-at-rest payloads we can't decrypt on edge01:**
+- **Encrypted-at-rest payloads the target cannot decrypt:**
`insight_narratives` and all `coach_*` (coach_conversations, coach_messages,
- coach_facts, coach_usage). The edge01 instance does not hold the prod
+ coach_facts, coach_usage). The target does not hold the source's
encryption key, so the AES-256-GCM ciphertext would be unreadable. The same
reason drives NULLing `cycle_day_logs.sensitive_encrypted` /
`notes_encrypted` while still carrying that table's plaintext cycle fields.
@@ -109,16 +111,16 @@ acknowledgement).
import_jobs, host_metrics, feedback, recommendation_feedback, and all rollup
tables (measurement_rollups, mood_entry_rollups, strain_trimp_cache,
cycle_predictions) — these
- rebuild on edge01.
+ rebuild on the target.
- **No rows:** workouts / workout_routes / workout_samples (demo has none).
- **Instance / global:** app_settings; global catalogues (mood_tags,
- cycle_symptoms) — these pre-exist on edge01 and the carried link rows
+ cycle_symptoms) pre-exist on the target and the carried link rows
reference the global ids only.
## Encryption note
-The edge01 demo instance is assumed **not** to share the apps01 encryption
-key. Every column whose value is AES-256-GCM ciphertext under the prod key is
+The demo instance is assumed **not** to share the source's encryption key.
+Every column whose value is AES-256-GCM ciphertext under the source key is
either NULLed (secrets on `users`, `cycle_day_logs.*_encrypted`) or skipped
entirely (`insight_narratives`, `coach_messages.encryptedContent`). Carrying
that ciphertext would produce undecryptable rows that the app's fail-closed
@@ -128,38 +130,37 @@ loader would reject.
The demo user is loaded with `role='ADMIN'` so the admin surfaces are
viewable in the demo. This is **only safe because DEMO_MODE must stay ON** on
-edge01 — the proxy layer (`src/proxy.ts`) blocks every mutating request in
+the target: the proxy layer (`src/proxy.ts`) blocks every mutating request in
demo mode, so an admin demo session can browse admin pages but cannot change
-anything. If DEMO_MODE were ever off on edge01, this admin demo account would
+anything. If DEMO_MODE were ever off on the target, this admin demo account would
be a write-capable admin. Keep DEMO_MODE enabled.
## Safety mechanisms built into the script
- `set -euo pipefail` and `ON_ERROR_STOP=1` on every psql call.
-- **Pre-flight guard (apps01):** for each carried set, a query that must
+- **Pre-flight guard (source):** for each carried set, a query that must
return `0` foreign-owned rows — aborts on any non-zero. Includes a check
that the demo owns **0 custom mood_tags** (so every carried
- `mood_entry_tag_link` points at a global tag that pre-exists on edge01) plus
+ `mood_entry_tag_link` points at a global tag that pre-exists on the target) plus
a defensive "non-global tag" link count.
-- **Scoped wipe-first (edge01):** FK-child-first deletes, every one scoped to
- the runtime-resolved edge01 demo id. No unscoped delete anywhere. Refuses to
- proceed if `username='demo'` on edge01 resolves to the operator id.
-- **Post-load verification (edge01):** per-table demo row counts; a hard
- assertion that the operator id `cmlupy4tn000001rpzx1pxvz7` appears in zero
- rows; a check that user_id-bearing carried tables hold only the demo id; and
+- **Scoped wipe-first (target):** FK-child-first deletes, every one scoped to
+ the runtime-resolved target demo id. No unscoped delete anywhere. Refuses to
+ proceed if `username='demo'` on the target resolves to the operator id.
+- **Post-load verification (target):** per-table demo row counts; a hard
+ assertion that `PROTECT_USER_ID` appears in zero rows; a check that user_id-bearing carried tables hold only the demo id; and
a confirmation the demo user exists with `role=ADMIN`.
## Caveats / things to confirm before running
- **`FOREIGN_HITS` assertion** counts _all_ non-demo rows in the carried
- tables on edge01, not just what this run inserted. On a clean
- single-demo-user DB it is `0`. If edge01 legitimately hosts other demo-only
+ tables on the target, not just what this run inserted. On a clean
+ single-demo-user DB it is `0`. If the target legitimately hosts other demo-only
accounts, treat a non-zero value as a warning to inspect, not an automatic
failure (the script prints it as a WARNING, not an abort).
-- Confirm edge01 psql credentials are `healthlog`/`healthlog` (the script
+- Confirm the target's psql credentials are `healthlog`/`healthlog` (the script
verifies connectivity before any write; adjust `PG_USER` / `PG_DB` if the
demo Postgres resource uses different ones).
-- The script assumes edge01's schema is at the same (or a compatible) Prisma
- migration level as apps01. A column present on apps01 but missing on edge01
- fails the `\copy` loudly rather than silently corrupting data — run
- migrations on edge01 first if the two have drifted.
+- The script assumes the target's schema is at the same (or a compatible)
+ Prisma migration level as the source. A column present on the source but
+ missing on the target fails the `\copy` loudly rather than silently
+ corrupting data; run migrations on the target first if the two have drifted.
diff --git a/scripts/migrate-demo-edge01.sh b/scripts/migrate-demo-account.sh
similarity index 88%
rename from scripts/migrate-demo-edge01.sh
rename to scripts/migrate-demo-account.sh
index d623229f0..df958fc05 100755
--- a/scripts/migrate-demo-edge01.sh
+++ b/scripts/migrate-demo-account.sh
@@ -1,14 +1,14 @@
#!/usr/bin/env bash
#
-# migrate-demo-edge01.sh — copy EXACTLY ONE user (the demo account) from the
-# production apps01 Postgres to the SEPARATE edge01 demo Postgres.
+# migrate-demo-account.sh: copy EXACTLY ONE user (the demo account) from the
+# source Postgres to the SEPARATE target demo Postgres.
#
# ============================================================================
# WHAT THIS SCRIPT DOES (and the isolation guarantees it makes)
# ============================================================================
#
# It transfers a single, demo-scoped slice of health data so the public demo
-# instance on edge01 shows a realistic-looking account WITHOUT carrying any
+# instance on the target shows a realistic-looking account WITHOUT carrying any
# real-operator data. The ONLY user copied is:
#
# $DEMO_USER_ID (the single demo account, supplied via env)
@@ -26,7 +26,7 @@
# 3. No secret / credential column is transferred. The users SELECT NULLs
# every encrypted / token / integration-secret column explicitly. The
# cycle-day-log SELECT NULLs the two app-encrypted columns (they are
-# keyed to the prod encryption key, which is NOT present on edge01).
+# keyed to the prod encryption key, which is NOT present on the target).
#
# The transfer mechanism per table is a scoped server-side COPY piped
# host-to-host:
@@ -42,12 +42,12 @@
# - an added/dropped column on either side fails loudly rather than
# silently shifting data into the wrong column.
#
-# Tables load in FK-parent-first order. The edge01 demo rows are wiped first
-# (FK-child-first), scoped to the edge01 demo user id resolved at runtime.
+# Tables load in FK-parent-first order. The target demo rows are wiped first
+# (FK-child-first), scoped to the target demo user id resolved at runtime.
#
# THIS SCRIPT IS FOR REVIEW. Read it, then run it deliberately. It performs
-# DESTRUCTIVE deletes on the edge01 demo DB (scoped to the demo user) followed
-# by inserts. It NEVER writes to apps01 (source is read-only).
+# DESTRUCTIVE deletes on the target demo DB (scoped to the demo user) followed
+# by inserts. It NEVER writes to the source (source is read-only).
#
# ----------------------------------------------------------------------------
# EXCLUDED ENTIRELY (intentionally NOT copied)
@@ -59,16 +59,16 @@
# fitbit_connections, all *_oauth_states / *_connect_tickets,
# telegram_scheduled_deletions, telegram_reminder_messages,
# clinician_share_links, provider_health.
-# - Encrypted-at-rest payloads we cannot decrypt on edge01:
+# - Encrypted-at-rest payloads we cannot decrypt on the target:
# insight_narratives, coach_messages (coach_* entirely).
# - Coach: coach_conversations, coach_messages, coach_facts, coach_usage.
# - Ops / transient / regenerable: idempotency_keys, rate_limits,
# data_backups, import_jobs, host_metrics, feedback,
# recommendation_feedback, all rollup tables (measurement_rollups,
-# mood_entry_rollups, strain_trimp_cache, cycle_predictions) — these REGENERATE on edge01.
+# mood_entry_rollups, strain_trimp_cache, cycle_predictions) — these REGENERATE on the target.
# - workouts / workout_routes / workout_samples — demo has none.
# - app_settings — instance-level, not user data.
-# - Global catalogues (mood_tags, cycle_symptoms) — pre-exist on edge01;
+# - Global catalogues (mood_tags, cycle_symptoms) — pre-exist on the target;
# carried link rows reference these GLOBAL ids only (see guards below).
#
set -euo pipefail
@@ -129,21 +129,21 @@ psql_src() {
# so psql_dst is defined after resolution.
echo "=============================================================="
-echo " HealthLog demo-user migration apps01 -> edge01"
+echo " HealthLog demo-user migration source -> target"
echo " demo user : $DEMO_USER_ID"
echo " src host : $SRC_SSH (container $SRC_DB_CONTAINER)"
echo " dst host : $DST_SSH (container resolved below)"
echo "=============================================================="
# ============================================================================
-# RESOLVE the edge01 demo DB container (NOT hardcoded — it is a separate
+# RESOLVE the target demo DB container (NOT hardcoded — it is a separate
# Coolify Postgres resource whose container name can change on redeploy).
# ============================================================================
echo
if [[ -n "${DST_DB_CONTAINER:-}" ]]; then
echo "## Using DST_DB_CONTAINER from env: $DST_DB_CONTAINER"
else
- echo "## Resolving edge01 demo Postgres container ..."
+ echo "## Resolving target demo Postgres container ..."
DST_DB_CONTAINER="$(ssh "$DST_SSH" "docker ps --format '{{.Names}}' | grep -iE 'postgres|^db-' || true")"
echo "candidate containers on $DST_SSH:"
echo "$DST_DB_CONTAINER" | sed 's/^/ /'
@@ -159,7 +159,7 @@ else
DST_DB_CONTAINER="$(printf '%s\n' "$DST_DB_CONTAINER" | head -n1)"
fi
readonly DST_DB_CONTAINER
-echo "resolved edge01 demo DB container: $DST_DB_CONTAINER"
+echo "resolved target demo DB container: $DST_DB_CONTAINER"
# Confirm the psql user/db actually work on the resolved container before we
# touch anything (fail fast on a wrong container guess).
@@ -167,31 +167,31 @@ psql_dst() {
# Usage: psql_dst ""
ssh "$DST_SSH" "docker exec -i $DST_DB_CONTAINER psql -X -v ON_ERROR_STOP=1 -U $PG_USER -d $PG_DB $*"
}
-echo "## Verifying edge01 DB connectivity ..."
+echo "## Verifying target DB connectivity ..."
psql_dst "-c 'SELECT current_database(), current_user;'"
# ============================================================================
-# RESOLVE the edge01 demo user id (it MAY differ from the apps01 id!).
-# We resolve by username='demo'. After this migration the edge01 demo user
-# will carry the apps01 canonical id (we delete the old row + load the apps01
-# row). Scope EVERY wipe to whatever the edge01 id is right now.
+# RESOLVE the target demo user id (it MAY differ from the source id!).
+# We resolve by username='demo'. After this migration the target demo user
+# will carry the source canonical id (we delete the old row + load the source
+# row). Scope EVERY wipe to whatever the target id is right now.
# ============================================================================
echo
-echo "## Resolving edge01 demo user id (by username='demo') ..."
-EDGE_DEMO_ID="$(psql_dst "-tA -c \"SELECT id FROM users WHERE username = 'demo';\"" | tr -d '[:space:]')"
-if [[ -z "$EDGE_DEMO_ID" ]]; then
- echo "note: no existing demo user on edge01 (username='demo') — wipe phase is a no-op."
+echo "## Resolving target demo user id (by username='demo') ..."
+TARGET_DEMO_ID="$(psql_dst "-tA -c \"SELECT id FROM users WHERE username = 'demo';\"" | tr -d '[:space:]')"
+if [[ -z "$TARGET_DEMO_ID" ]]; then
+ echo "note: no existing demo user on the target (username='demo') — wipe phase is a no-op."
else
- echo "edge01 existing demo user id: $EDGE_DEMO_ID"
- if [[ "$EDGE_DEMO_ID" == "$REAL_USER_ID" ]]; then
- echo "ABORT: edge01 username='demo' resolves to the REAL operator id. Refusing."
+ echo "existing target demo user id: $TARGET_DEMO_ID"
+ if [[ "$TARGET_DEMO_ID" == "$REAL_USER_ID" ]]; then
+ echo "ABORT: target username='demo' resolves to the REAL operator id. Refusing."
exit 1
fi
fi
-readonly EDGE_DEMO_ID
+readonly TARGET_DEMO_ID
# ============================================================================
-# PRE-FLIGHT GUARD (on apps01 source): assert every carried set is demo-only.
+# PRE-FLIGHT GUARD (on the source): assert every carried set is demo-only.
# Each query MUST return 0. A non-zero result means a scope predicate is wrong
# and we would leak another user's rows — ABORT immediately.
# ============================================================================
@@ -243,13 +243,13 @@ preflight "mood_entry_tag_links(join)" \
# GUARD: mood_entry_tag_links must reference ONLY global mood_tags (user_id IS
# NULL on the tag). The demo is asserted to have 0 custom mood_tags; if a link
-# referenced a per-user tag id, the FK would break on edge01 (that tag row is
+# referenced a per-user tag id, the FK would break on the target (that tag row is
# not carried). Custom-tag count MUST be 0.
DEMO_CUSTOM_MOODTAGS="$(psql_src "-tA -c \"SELECT count(*) FROM mood_tags WHERE user_id = '$DEMO_USER_ID';\"" | tr -d '[:space:]')"
echo " demo custom mood_tags = $DEMO_CUSTOM_MOODTAGS (must be 0 — all links point to GLOBAL tags)"
if [[ "$DEMO_CUSTOM_MOODTAGS" != "0" ]]; then
echo "ABORT: demo user owns $DEMO_CUSTOM_MOODTAGS custom mood_tags. mood_entry_tag_links"
- echo " would reference non-global tag ids that are NOT carried -> FK break on edge01."
+ echo " would reference non-global tag ids that are NOT carried -> FK break on the target."
echo " Carry mood_tags (user-scoped) before mood_entry_tag_links, or skip the links."
exit 1
fi
@@ -260,55 +260,55 @@ preflight "mood_entry_tag_links(non-global tag)" \
echo "## PRE-FLIGHT guard passed — every carried set is demo-only."
# ============================================================================
-# WIPE-FIRST on edge01 (idempotent, demo-scoped ONLY). FK-child-first order.
-# Every DELETE is scoped to the resolved edge01 demo user id ($EDGE_DEMO_ID)
+# WIPE-FIRST on the target (idempotent, demo-scoped ONLY). FK-child-first order.
+# Every DELETE is scoped to the resolved target demo user id ($TARGET_DEMO_ID)
# or via a join back to it. NEVER an unscoped DELETE. Skipped entirely if no
# existing demo user was found.
# ============================================================================
echo
-if [[ -n "$EDGE_DEMO_ID" ]]; then
- echo "## WIPE edge01 demo rows (scoped to $EDGE_DEMO_ID), FK-child-first ..."
- # NOTE: edge01 FKs are ON DELETE CASCADE for most child rows; the explicit
+if [[ -n "$TARGET_DEMO_ID" ]]; then
+ echo "## WIPE target demo rows (scoped to $TARGET_DEMO_ID), FK-child-first ..."
+ # NOTE: target FKs are ON DELETE CASCADE for most child rows; the explicit
# child-first deletes below are belt-and-braces (some child tables hang off
# medications/mood_entries, not the user) and keep the operation transparent.
psql_dst < '$DEMO_USER_ID');
\"" | tr -d '[:space:]')"
echo " non-demo user_id rows across carried tables = $FOREIGN_HITS"
-echo " NOTE: this counts EVERY non-demo row in these tables on edge01, not just"
+echo " NOTE: this counts EVERY non-demo row in these tables on the target, not just"
echo " what this script loaded. On a clean demo DB it should be 0. If the"
-echo " edge01 demo DB legitimately holds other demo-only accounts, scope"
+echo " target demo DB legitimately holds other demo-only accounts, scope"
echo " this assertion to the rows this run inserted before treating >0 as fatal."
if [[ "$FOREIGN_HITS" != "0" ]]; then
- echo "WARNING: $FOREIGN_HITS non-demo rows exist in carried tables on edge01."
+ echo "WARNING: $FOREIGN_HITS non-demo rows exist in carried tables on the target."
echo " Inspect before serving the demo (see note above)."
fi
echo
-echo "HARD assertion 3: the demo user exists on edge01 with role=ADMIN and the apps01 id."
+echo "HARD assertion 3: the demo user exists on the target with role=ADMIN and the source id."
psql_dst "-c \"SELECT id, username, role, telegram_enabled FROM users WHERE id = '$DEMO_USER_ID';\""
echo
echo "=============================================================="
echo " DONE. Review the counts above. Demo user is role=ADMIN; the"
-echo " app's DEMO_MODE must remain ON on edge01 so all mutations are"
+echo " app's DEMO_MODE must remain ON on the target so all mutations are"
echo " blocked at the proxy layer while admin pages stay viewable."
echo "=============================================================="
diff --git a/scripts/seed-demo-apps01.sql b/scripts/seed-demo-apps01.sql
deleted file mode 100644
index 5fac887ea..000000000
--- a/scripts/seed-demo-apps01.sql
+++ /dev/null
@@ -1,154 +0,0 @@
--- HealthLog demo seed for apps01 — v1.15 (cycle-inclusive, FEMALE demo).
---
--- Creates / refreshes the `demo` account as a WOMAN with cycle tracking on,
--- NOW()-anchored + idempotent (re-runnable: wipes the demo user's rows first).
--- Showcases vitals + mood + the v1.15 cycle vertical (5 cycles, daily BBT with
--- a biphasic shift, mucus/OPK progression, period flow, phase-clustered
--- symptoms), plus phase-dependent RHR/HRV/sleep so the phase×vitals insights
--- board surfaces real findings. role=USER (login is open on prod; an
--- admin demo would be a risk). AI insights are generated separately afterwards.
---
--- Run: ssh apps-01 'docker exec -i psql -U healthlog -d healthlog' < scripts/seed-demo-apps01.sql
-
-BEGIN;
-
--- ── User (FEMALE, USER role) ──────────────────────────────────────
-INSERT INTO users (id, username, email, password_hash, role, height_cm, date_of_birth, gender, timezone, locale, onboarding_completed_at, created_at, updated_at)
-SELECT 'usr_demo_cf31025295714ece8d91f5af13afd76d',
- 'demo', 'demo@healthlog.app',
- '$argon2id$v=19$m=19456,t=2,p=1$3KvrEIuI5UnwG53GPbS71w$mL8ZQotIXn9iLFIw3yHXbh6SitX1w7TXKI5ahWcKkF8',
- 'USER', 168, TIMESTAMP '1992-03-15', 'FEMALE', 'Europe/Berlin', 'en', NOW(), NOW(), NOW()
-WHERE NOT EXISTS (SELECT 1 FROM users WHERE username = 'demo');
-
-UPDATE users SET
- gender = 'FEMALE', role = 'USER', height_cm = 168, date_of_birth = TIMESTAMP '1992-03-15',
- password_hash = '$argon2id$v=19$m=19456,t=2,p=1$3KvrEIuI5UnwG53GPbS71w$mL8ZQotIXn9iLFIw3yHXbh6SitX1w7TXKI5ahWcKkF8',
- onboarding_completed_at = NOW(), timezone = 'Europe/Berlin', locale = 'en', updated_at = NOW()
-WHERE username = 'demo';
-
-CREATE TEMP TABLE _demo AS SELECT id AS user_id FROM users WHERE username = 'demo' LIMIT 1;
-
-DO $$
-BEGIN
- IF NOT EXISTS (SELECT 1 FROM _demo) THEN
- RAISE EXCEPTION 'seed-demo-apps01: demo user not resolved';
- END IF;
-END $$;
-
--- ── Wipe prior demo rows (idempotent) ─────────────────────────────
-DELETE FROM cycle_symptom_links WHERE day_log_id IN (SELECT id FROM cycle_day_logs WHERE user_id IN (SELECT user_id FROM _demo));
-DELETE FROM cycle_day_logs WHERE user_id IN (SELECT user_id FROM _demo);
-DELETE FROM cycle_predictions WHERE user_id IN (SELECT user_id FROM _demo);
-DELETE FROM menstrual_cycles WHERE user_id IN (SELECT user_id FROM _demo);
-DELETE FROM mood_entries WHERE user_id IN (SELECT user_id FROM _demo);
-DELETE FROM measurements WHERE user_id IN (SELECT user_id FROM _demo);
-DELETE FROM measurement_rollups WHERE user_id IN (SELECT user_id FROM _demo);
-DELETE FROM mood_entry_rollups WHERE user_id IN (SELECT user_id FROM _demo);
-
--- ── Cycle profile (tracking ON) ───────────────────────────────────
-INSERT INTO cycle_profiles (id, user_id, goal, cycle_tracking_enabled, typical_cycle_length, typical_period_length, luteal_phase_length, prediction_enabled, raw_chart_mode, discreet_notifications, sensitive_category_encryption, created_at, updated_at)
-SELECT 'cp_' || replace(gen_random_uuid()::text, '-', ''), user_id, 'GENERAL_HEALTH', true, 28, 5, 14, true, false, false, true, NOW(), NOW()
-FROM _demo
-WHERE NOT EXISTS (SELECT 1 FROM cycle_profiles cp JOIN _demo d ON cp.user_id = d.user_id);
-
-UPDATE cycle_profiles SET
- cycle_tracking_enabled = true, goal = 'GENERAL_HEALTH', prediction_enabled = true,
- typical_cycle_length = 28, typical_period_length = 5, luteal_phase_length = 14, updated_at = NOW()
-WHERE user_id IN (SELECT user_id FROM _demo);
-
--- ── Measurements (90-day sliding window, phase-aware) ─────────────
--- cycleDay(d) = ((128 - d) % 28) + 1 ; luteal = cycleDay > 13.
-WITH demo AS (SELECT user_id FROM _demo), days AS (SELECT generate_series(0, 89) AS d)
-INSERT INTO measurements (id, user_id, type, value, unit, source, glucose_context, measured_at, created_at, updated_at)
-SELECT 'm_' || replace(gen_random_uuid()::text, '-', ''), (SELECT user_id FROM demo), v.type::measurement_type, v.value, v.unit, 'MANUAL'::measurement_source,
- v.gc::glucose_context,
- NOW() - (d || ' days')::interval - (random() * 5 || ' hours')::interval,
- NOW() - (d || ' days')::interval, NOW() - (d || ' days')::interval
-FROM days,
-LATERAL (
- SELECT ((128 - d) % 28) + 1 AS cd
-) c,
-LATERAL (VALUES
- ('WEIGHT', round((66.0 - (90 - d) * 0.011 + (random() - 0.5) * 0.5)::numeric, 1)::float8, 'kg', NULL),
- ('BLOOD_PRESSURE_SYS', round((116 + (random() - 0.5) * 8)::numeric, 0)::float8, 'mmHg', NULL),
- ('BLOOD_PRESSURE_DIA', round((74 + (random() - 0.5) * 6)::numeric, 0)::float8, 'mmHg', NULL),
- ('PULSE', round((68 + (random() - 0.5) * 8)::numeric, 0)::float8, 'bpm', NULL),
- ('RESTING_HEART_RATE', round((56 + (CASE WHEN c.cd > 13 THEN 3 ELSE 0 END) + (random() - 0.5) * 3)::numeric, 0)::float8, 'bpm', NULL),
- ('HEART_RATE_VARIABILITY', round((68 - (CASE WHEN c.cd > 13 THEN 7 ELSE 0 END) + (random() - 0.5) * 6)::numeric, 0)::float8, 'ms', NULL),
- ('SLEEP_DURATION', round((445 - (CASE WHEN c.cd > 13 THEN 18 ELSE 0 END) + (random() - 0.5) * 40)::numeric, 0)::float8, 'minutes', NULL),
- ('ACTIVITY_STEPS', round((8200 + (random() - 0.5) * 3500)::numeric, 0)::float8, 'steps', NULL),
- ('BODY_FAT', round((27.5 - (90 - d) * 0.006 + (random() - 0.5) * 0.4)::numeric, 1)::float8, '%', NULL),
- ('BODY_TEMPERATURE', round(((CASE WHEN c.cd <= 13 THEN 36.36 ELSE 36.73 END) + (random() - 0.5) * 0.08)::numeric, 2)::float8, 'celsius', NULL),
- ('BLOOD_GLUCOSE', round((92 + (random() - 0.5) * 18)::numeric, 0)::float8, 'mg/dL', 'FASTING')
-) AS v(type, value, unit, gc);
-
--- ── Mood (90 days, luteal dip) ────────────────────────────────────
-WITH demo AS (SELECT user_id FROM _demo), days AS (SELECT generate_series(0, 89) AS d)
-INSERT INTO mood_entries (id, user_id, date, mood, score, source, mood_logged_at, synced_at, tz, sync_version, created_at, updated_at)
-SELECT 'mo_' || replace(gen_random_uuid()::text, '-', ''), (SELECT user_id FROM demo),
- to_char(NOW() - (d || ' days')::interval, 'YYYY-MM-DD'),
- CASE s.score WHEN 1 THEN 'LAUSIG' WHEN 2 THEN 'SCHLECHT' WHEN 3 THEN 'OKAY' WHEN 4 THEN 'GUT' ELSE 'SUPER_GUT' END,
- s.score, 'MANUAL',
- NOW() - (d || ' days')::interval - (random() * 6 || ' hours')::interval,
- NOW() - (d || ' days')::interval, 'Europe/Berlin', 0,
- NOW() - (d || ' days')::interval, NOW() - (d || ' days')::interval
-FROM days,
-LATERAL (SELECT ((128 - d) % 28) + 1 AS cd) c,
-LATERAL (
- SELECT GREATEST(1, LEAST(5,
- round((CASE WHEN c.cd > 23 THEN 2.6 WHEN c.cd > 13 THEN 3.3 ELSE 3.9 END) + (random() - 0.5) * 1.6)::int
- )) AS score
-) s;
-
--- ── Menstrual cycles (5, NOW-anchored; last open) ─────────────────
-WITH offs AS (SELECT unnest(ARRAY[128, 100, 72, 44, 16]) AS off)
-INSERT INTO menstrual_cycles (id, user_id, start_date, end_date, period_end_date, length_days, ovulation_date, ovulation_confirmed, is_predicted, tz, created_at, updated_at)
-SELECT 'mc_' || replace(gen_random_uuid()::text, '-', ''), (SELECT user_id FROM _demo),
- to_char(NOW() - (off || ' days')::interval, 'YYYY-MM-DD'),
- CASE WHEN off = 16 THEN NULL ELSE to_char(NOW() - ((off - 27) || ' days')::interval, 'YYYY-MM-DD') END,
- to_char(NOW() - ((off - 4) || ' days')::interval, 'YYYY-MM-DD'),
- CASE WHEN off = 16 THEN NULL ELSE 28 END,
- to_char(NOW() - ((off - 13) || ' days')::interval, 'YYYY-MM-DD'),
- true, false, 'Europe/Berlin', NOW(), NOW()
-FROM offs;
-
--- ── Cycle day-logs (128 days) + phase-clustered symptom links ─────
-WITH demo AS (SELECT user_id FROM _demo),
-gen AS (
- SELECT d, ((128 - d) % 28) + 1 AS cd, to_char(NOW() - (d || ' days')::interval, 'YYYY-MM-DD') AS dstr
- FROM generate_series(0, 127) AS d
-),
-ins AS (
- INSERT INTO cycle_day_logs (id, user_id, date, flow, basal_body_temp_c, cervical_mucus, ovulation_test, source, tz, sync_version, created_at, updated_at)
- SELECT 'cdl_' || replace(gen_random_uuid()::text, '-', ''), (SELECT user_id FROM demo), g.dstr,
- (CASE g.cd WHEN 1 THEN 'MEDIUM' WHEN 2 THEN 'HEAVY' WHEN 3 THEN 'MEDIUM' WHEN 4 THEN 'LIGHT' WHEN 5 THEN 'SPOTTING' ELSE NULL END)::flow_level,
- round(((CASE WHEN g.cd <= 13 THEN 36.36 ELSE 36.73 END) + (random() - 0.5) * 0.08)::numeric, 2)::float8,
- (CASE WHEN g.cd BETWEEN 1 AND 5 THEN NULL WHEN g.cd BETWEEN 6 AND 9 THEN 'STICKY' WHEN g.cd BETWEEN 10 AND 12 THEN 'CREAMY' WHEN g.cd BETWEEN 13 AND 14 THEN 'EGG_WHITE' WHEN g.cd BETWEEN 15 AND 16 THEN 'WATERY' ELSE 'DRY' END)::cervical_mucus,
- (CASE WHEN g.cd = 13 THEN 'POSITIVE_LH_SURGE' WHEN g.cd IN (11, 12, 14) THEN 'NEGATIVE' ELSE NULL END)::ovulation_test,
- 'MANUAL'::measurement_source, 'Europe/Berlin', 0,
- NOW() - (g.d || ' days')::interval, NOW() - (g.d || ' days')::interval
- FROM gen g
- RETURNING id, date
-),
-link_menstrual AS (
- INSERT INTO cycle_symptom_links (day_log_id, symptom_id, severity, created_at)
- SELECT ins.id, s.id, (2 + floor(random() * 3))::int, NOW()
- FROM ins JOIN gen g ON g.dstr = ins.date
- CROSS JOIN LATERAL (SELECT id FROM cycle_symptoms WHERE user_id IS NULL AND key IN ('cramps', 'headache', 'fatigue')) s
- WHERE g.cd BETWEEN 1 AND 3
-)
-INSERT INTO cycle_symptom_links (day_log_id, symptom_id, severity, created_at)
-SELECT ins.id, s.id, (1 + floor(random() * 3))::int, NOW()
-FROM ins JOIN gen g ON g.dstr = ins.date
-CROSS JOIN LATERAL (SELECT id FROM cycle_symptoms WHERE user_id IS NULL AND key IN ('mood_swings', 'bloating', 'breast_tenderness')) s
-WHERE g.cd BETWEEN 24 AND 28;
-
-COMMIT;
-
--- ── Summary ───────────────────────────────────────────────────────
-SELECT 'demo user' AS what, id FROM users WHERE username = 'demo';
-SELECT 'measurements' AS what, count(*) FROM measurements WHERE user_id = (SELECT id FROM users WHERE username = 'demo');
-SELECT 'mood' AS what, count(*) FROM mood_entries WHERE user_id = (SELECT id FROM users WHERE username = 'demo');
-SELECT 'cycles' AS what, count(*) FROM menstrual_cycles WHERE user_id = (SELECT id FROM users WHERE username = 'demo');
-SELECT 'day_logs' AS what, count(*) FROM cycle_day_logs WHERE user_id = (SELECT id FROM users WHERE username = 'demo');
-SELECT 'symptom_links' AS what, count(*) FROM cycle_symptom_links l JOIN cycle_day_logs dl ON l.day_log_id = dl.id WHERE dl.user_id = (SELECT id FROM users WHERE username = 'demo');
diff --git a/scripts/seed-demo-extend-1y.sql b/scripts/seed-demo-extend-1y.sql
deleted file mode 100644
index d611aea68..000000000
--- a/scripts/seed-demo-extend-1y.sql
+++ /dev/null
@@ -1,216 +0,0 @@
--- HealthLog demo 1-year history extender for apps01.
---
--- WHAT THIS DOES
--- Extends the demo account's data BACKWARD from the existing ~90-day seed
--- (scripts/seed-demo-apps01.sql) to ~365 days of coverage, and adds ~365 days
--- of realistic medication-intake history for the two pre-existing demo meds.
--- It mirrors that seed EXACTLY: same column lists, enum values, NOW()-anchored
--- offset math (cycleDay(d) = ((128 - d) % 28) + 1), cuid/uuid-id style, and the
--- same phase-aware vitals / mood / cycle channels.
---
--- The seed covers offsets 0..89 (measurements + mood), 0..127 (cycle day-logs),
--- and 5 cycles at NOW-offsets {128,100,72,44,16}. This file ADDS ONLY the older
--- tail so nothing is duplicated:
--- - measurements / mood : offsets 90..364 (thinned to every 2nd day past ~180d)
--- - cycle day-logs : offsets 128..364
--- - menstrual cycles : 7 more 28-day cycles at offsets 156..352
--- - medication intakes : ~365d of due slots for both demo meds
---
--- SCOPE + SAFETY
--- - Demo-user-scoped: EVERY statement filters on the hardcoded demo user id.
--- - Idempotent: every INSERT guards on the live unique index via ON CONFLICT
--- DO NOTHING (or a date-offset window that begins past the seed's coverage),
--- so re-running adds nothing new.
--- - NO DDL (no CREATE/ALTER/DROP/TRUNCATE), no changes to any other user,
--- no table-wide DELETE/UPDATE.
---
--- Run: ssh apps-01 'docker exec -i psql -U healthlog -d healthlog' < scripts/seed-demo-extend-1y.sql
-
-\set uid 'usr_demo_cf31025295714ece8d91f5af13afd76d'
-\set medA 'cmq2lzjqh005x01nrys2crzg9' -- Mounjaro: weekly @ 08:00 (rolling 7d)
-\set medB 'cmq2m0mzk006101nrdpijbij0' -- Ramipril: twice daily @ 08:00 + 18:00
-
-BEGIN;
-
--- Refuse to run if the demo user does not exist.
-DO $$
-BEGIN
- IF NOT EXISTS (SELECT 1 FROM users WHERE id = 'usr_demo_cf31025295714ece8d91f5af13afd76d') THEN
- RAISE EXCEPTION 'seed-demo-extend-1y: demo user not found';
- END IF;
-END $$;
-
--- ── Measurements: offsets 90..364, phase-aware (mirrors the seed channels) ─────
--- Thin the deep tail: keep every day for 90..179, every 2nd day for 180..364, to
--- keep row volume sane while still covering the full year. Unique index
--- (user_id, type, measured_at, source, sleep_stage) NULLS NOT DISTINCT — but
--- measured_at carries a random sub-day jitter, so collisions are effectively
--- impossible on a fresh tail; ON CONFLICT DO NOTHING makes re-runs safe anyway.
-WITH days AS (
- SELECT d FROM generate_series(90, 364) AS d
- WHERE d < 180 OR (d % 2) = 0
-)
-INSERT INTO measurements (id, user_id, type, value, unit, source, glucose_context, measured_at, created_at, updated_at)
-SELECT 'm_' || replace(gen_random_uuid()::text, '-', ''),
- 'usr_demo_cf31025295714ece8d91f5af13afd76d',
- v.type::measurement_type, v.value, v.unit, 'MANUAL'::measurement_source,
- v.gc::glucose_context,
- NOW() - (d || ' days')::interval - (random() * 5 || ' hours')::interval,
- NOW() - (d || ' days')::interval, NOW() - (d || ' days')::interval
-FROM days,
-LATERAL (SELECT ((128 - d) % 28) + 1 AS cd) c,
-LATERAL (VALUES
- ('WEIGHT', round((66.0 - (90 - d) * 0.011 + (random() - 0.5) * 0.5)::numeric, 1)::float8, 'kg', NULL),
- ('BLOOD_PRESSURE_SYS', round((116 + (random() - 0.5) * 8)::numeric, 0)::float8, 'mmHg', NULL),
- ('BLOOD_PRESSURE_DIA', round((74 + (random() - 0.5) * 6)::numeric, 0)::float8, 'mmHg', NULL),
- ('PULSE', round((68 + (random() - 0.5) * 8)::numeric, 0)::float8, 'bpm', NULL),
- ('RESTING_HEART_RATE', round((56 + (CASE WHEN c.cd > 13 THEN 3 ELSE 0 END) + (random() - 0.5) * 3)::numeric, 0)::float8, 'bpm', NULL),
- ('HEART_RATE_VARIABILITY', round((68 - (CASE WHEN c.cd > 13 THEN 7 ELSE 0 END) + (random() - 0.5) * 6)::numeric, 0)::float8, 'ms', NULL),
- ('SLEEP_DURATION', round((445 - (CASE WHEN c.cd > 13 THEN 18 ELSE 0 END) + (random() - 0.5) * 40)::numeric, 0)::float8, 'minutes', NULL),
- ('ACTIVITY_STEPS', round((8200 + (random() - 0.5) * 3500)::numeric, 0)::float8, 'steps', NULL),
- ('BODY_FAT', round((27.5 - (90 - d) * 0.006 + (random() - 0.5) * 0.4)::numeric, 1)::float8, '%', NULL),
- ('BODY_TEMPERATURE', round(((CASE WHEN c.cd <= 13 THEN 36.36 ELSE 36.73 END) + (random() - 0.5) * 0.08)::numeric, 2)::float8, 'celsius', NULL),
- ('BLOOD_GLUCOSE', round((92 + (random() - 0.5) * 18)::numeric, 0)::float8, 'mg/dL', 'FASTING')
-) AS v(type, value, unit, gc)
-ON CONFLICT DO NOTHING;
-
--- ── Mood: offsets 90..364, luteal dip (mirrors the seed) ───────────────────────
--- Unique index (user_id, date, mood_logged_at). One entry per day, fresh tail.
-WITH days AS (SELECT generate_series(90, 364) AS d)
-INSERT INTO mood_entries (id, user_id, date, mood, score, source, mood_logged_at, synced_at, tz, sync_version, created_at, updated_at)
-SELECT 'mo_' || replace(gen_random_uuid()::text, '-', ''),
- 'usr_demo_cf31025295714ece8d91f5af13afd76d',
- to_char(NOW() - (d || ' days')::interval, 'YYYY-MM-DD'),
- CASE s.score WHEN 1 THEN 'LAUSIG' WHEN 2 THEN 'SCHLECHT' WHEN 3 THEN 'OKAY' WHEN 4 THEN 'GUT' ELSE 'SUPER_GUT' END,
- s.score, 'MANUAL',
- NOW() - (d || ' days')::interval - (random() * 6 || ' hours')::interval,
- NOW() - (d || ' days')::interval, 'Europe/Berlin', 0,
- NOW() - (d || ' days')::interval, NOW() - (d || ' days')::interval
-FROM days,
-LATERAL (SELECT ((128 - d) % 28) + 1 AS cd) c,
-LATERAL (
- SELECT GREATEST(1, LEAST(5,
- round((CASE WHEN c.cd > 23 THEN 2.6 WHEN c.cd > 13 THEN 3.3 ELSE 3.9 END) + (random() - 0.5) * 1.6)::int
- )) AS score
-) s
-ON CONFLICT DO NOTHING;
-
--- ── Menstrual cycles: 7 more 28-day cycles, offsets 156..352 ───────────────────
--- Continues the seed's cadence backward (seed had {128,100,72,44,16}; next is 156).
--- Unique index (user_id, start_date) → ON CONFLICT DO NOTHING. All closed cycles.
-WITH offs AS (SELECT unnest(ARRAY[156, 184, 212, 240, 268, 296, 324, 352]) AS off)
-INSERT INTO menstrual_cycles (id, user_id, start_date, end_date, period_end_date, length_days, ovulation_date, ovulation_confirmed, is_predicted, tz, created_at, updated_at)
-SELECT 'mc_' || replace(gen_random_uuid()::text, '-', ''),
- 'usr_demo_cf31025295714ece8d91f5af13afd76d',
- to_char(NOW() - (off || ' days')::interval, 'YYYY-MM-DD'),
- to_char(NOW() - ((off - 27) || ' days')::interval, 'YYYY-MM-DD'),
- to_char(NOW() - ((off - 4) || ' days')::interval, 'YYYY-MM-DD'),
- 28,
- to_char(NOW() - ((off - 13) || ' days')::interval, 'YYYY-MM-DD'),
- true, false, 'Europe/Berlin', NOW(), NOW()
-FROM offs
-ON CONFLICT (user_id, start_date) DO NOTHING;
-
--- ── Cycle day-logs: offsets 128..364 + phase-clustered symptom links ───────────
--- Mirrors the seed exactly (flow/BBT/mucus/OPK by cycleDay). Unique index
--- (user_id, date) → ON CONFLICT DO NOTHING. Symptom links only attach to rows
--- this run actually inserts (RETURNING), so re-runs add no duplicate links.
-WITH gen AS (
- SELECT d, ((128 - d) % 28) + 1 AS cd, to_char(NOW() - (d || ' days')::interval, 'YYYY-MM-DD') AS dstr
- FROM generate_series(128, 364) AS d
-),
-ins AS (
- INSERT INTO cycle_day_logs (id, user_id, date, flow, basal_body_temp_c, cervical_mucus, ovulation_test, source, tz, sync_version, created_at, updated_at)
- SELECT 'cdl_' || replace(gen_random_uuid()::text, '-', ''),
- 'usr_demo_cf31025295714ece8d91f5af13afd76d', g.dstr,
- (CASE g.cd WHEN 1 THEN 'MEDIUM' WHEN 2 THEN 'HEAVY' WHEN 3 THEN 'MEDIUM' WHEN 4 THEN 'LIGHT' WHEN 5 THEN 'SPOTTING' ELSE NULL END)::flow_level,
- round(((CASE WHEN g.cd <= 13 THEN 36.36 ELSE 36.73 END) + (random() - 0.5) * 0.08)::numeric, 2)::float8,
- (CASE WHEN g.cd BETWEEN 1 AND 5 THEN NULL WHEN g.cd BETWEEN 6 AND 9 THEN 'STICKY' WHEN g.cd BETWEEN 10 AND 12 THEN 'CREAMY' WHEN g.cd BETWEEN 13 AND 14 THEN 'EGG_WHITE' WHEN g.cd BETWEEN 15 AND 16 THEN 'WATERY' ELSE 'DRY' END)::cervical_mucus,
- (CASE WHEN g.cd = 13 THEN 'POSITIVE_LH_SURGE' WHEN g.cd IN (11, 12, 14) THEN 'NEGATIVE' ELSE NULL END)::ovulation_test,
- 'MANUAL'::measurement_source, 'Europe/Berlin', 0,
- NOW() - (g.d || ' days')::interval, NOW() - (g.d || ' days')::interval
- FROM gen g
- ON CONFLICT (user_id, date) DO NOTHING
- RETURNING id, date
-),
-link_menstrual AS (
- INSERT INTO cycle_symptom_links (day_log_id, symptom_id, severity, created_at)
- SELECT ins.id, s.id, (2 + floor(random() * 3))::int, NOW()
- FROM ins JOIN gen g ON g.dstr = ins.date
- CROSS JOIN LATERAL (SELECT id FROM cycle_symptoms WHERE user_id IS NULL AND key IN ('cramps', 'headache', 'fatigue')) s
- WHERE g.cd BETWEEN 1 AND 3
-)
-INSERT INTO cycle_symptom_links (day_log_id, symptom_id, severity, created_at)
-SELECT ins.id, s.id, (1 + floor(random() * 3))::int, NOW()
-FROM ins JOIN gen g ON g.dstr = ins.date
-CROSS JOIN LATERAL (SELECT id FROM cycle_symptoms WHERE user_id IS NULL AND key IN ('mood_swings', 'bloating', 'breast_tenderness')) s
-WHERE g.cd BETWEEN 24 AND 28;
-
--- ── Medication intakes: Med A (weekly @ 08:00 Berlin), ~365d ───────────────────
--- The existing demo rows store scheduled_for in UTC at 06:00 (= 08:00 Berlin DST).
--- We mint slots at the Berlin local 08:00 and convert to the stored UTC wall-time
--- with AT TIME ZONE so winter (07:00 UTC) vs summer (06:00 UTC) is honoured.
--- Live partial unique index: (user_id, medication_id, scheduled_for, source)
--- WHERE deleted_at IS NULL → ON CONFLICT on that predicate makes re-runs safe.
--- Compliance ~85-90%: deterministic per-slot pseudo-random skip ~12% of weeks.
--- Never future-dated: WHERE the slot timestamp <= NOW().
-WITH slots AS (
- SELECT
- ( (date_trunc('day', NOW() AT TIME ZONE 'Europe/Berlin') - (w * 7 || ' days')::interval + interval '8 hours')
- AT TIME ZONE 'Europe/Berlin' ) AT TIME ZONE 'UTC' AS sched_utc,
- -- deterministic skip decision per week offset (no random(): re-run stable)
- ( (hashtextextended('medA-week-' || w, 0) & 2147483647)::numeric / 2147483647.0 ) AS r
- FROM generate_series(0, 52) AS w -- 53 weekly slots = w*7 days back ≈ 371 days
-)
-INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at, updated_at, sync_version)
-SELECT 'cmq_demo_a_' || replace(gen_random_uuid()::text, '-', ''),
- 'usr_demo_cf31025295714ece8d91f5af13afd76d',
- 'cmq2lzjqh005x01nrys2crzg9',
- s.sched_utc,
- CASE WHEN s.r < 0.88 THEN s.sched_utc + (interval '5 minutes' + (s.r * interval '90 minutes')) ELSE NULL END,
- CASE WHEN s.r < 0.88 THEN false ELSE true END,
- 'WEB'::intake_source,
- s.sched_utc, s.sched_utc, 0
-FROM slots s
-WHERE s.sched_utc <= NOW()
- -- skip slots already covered by the seeded rows (06-06 06:00 + 06-13 06:00)
- AND s.sched_utc < date_trunc('day', NOW()) - interval '1 day'
-ON CONFLICT (user_id, medication_id, scheduled_for, source) WHERE deleted_at IS NULL DO NOTHING;
-
--- ── Medication intakes: Med B (twice daily @ 08:00 + 18:00 Berlin), ~365d ──────
--- One row per due slot per day. Compliance ~85-90%: deterministic per-slot skip
--- ~12% of individual slots. Same TZ + ON CONFLICT + no-future-date guards.
-WITH slots AS (
- SELECT
- ( (date_trunc('day', NOW() AT TIME ZONE 'Europe/Berlin') - (dd || ' days')::interval + (hh || ' hours')::interval)
- AT TIME ZONE 'Europe/Berlin' ) AT TIME ZONE 'UTC' AS sched_utc,
- ( (hashtextextended('medB-' || dd || '-' || hh, 0) & 2147483647)::numeric / 2147483647.0 ) AS r
- FROM generate_series(0, 364) AS dd
- CROSS JOIN (VALUES (8), (18)) AS h(hh)
-)
-INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at, updated_at, sync_version)
-SELECT 'cmq_demo_b_' || replace(gen_random_uuid()::text, '-', ''),
- 'usr_demo_cf31025295714ece8d91f5af13afd76d',
- 'cmq2m0mzk006101nrdpijbij0',
- s.sched_utc,
- CASE WHEN s.r < 0.88 THEN s.sched_utc + (interval '5 minutes' + (s.r * interval '120 minutes')) ELSE NULL END,
- CASE WHEN s.r < 0.88 THEN false ELSE true END,
- 'WEB'::intake_source,
- s.sched_utc, s.sched_utc, 0
-FROM slots s
-WHERE s.sched_utc <= NOW()
- -- skip slots already covered by the seeded rows (today's 06:00 + 16:00 UTC)
- AND s.sched_utc < date_trunc('day', NOW())
-ON CONFLICT (user_id, medication_id, scheduled_for, source) WHERE deleted_at IS NULL DO NOTHING;
-
-COMMIT;
-
--- ── Verification (demo-user-scoped totals after apply) ─────────────────────────
-\echo === post-apply totals (demo user) ===
-SELECT 'measurements' AS what, count(*), min(measured_at)::date AS oldest, max(measured_at)::date AS newest FROM measurements WHERE user_id = :'uid';
-SELECT 'mood' AS what, count(*), min(date) AS oldest, max(date) AS newest FROM mood_entries WHERE user_id = :'uid';
-SELECT 'cycles' AS what, count(*), min(start_date) AS oldest, max(start_date) AS newest FROM menstrual_cycles WHERE user_id = :'uid';
-SELECT 'day_logs' AS what, count(*), min(date) AS oldest, max(date) AS newest FROM cycle_day_logs WHERE user_id = :'uid';
-SELECT 'symptom_links' AS what, count(*) FROM cycle_symptom_links l JOIN cycle_day_logs dl ON l.day_log_id = dl.id WHERE dl.user_id = :'uid';
-SELECT 'intake_medA' AS what, count(*), count(*) FILTER (WHERE taken_at IS NOT NULL) AS taken, count(*) FILTER (WHERE skipped) AS skipped FROM medication_intake_events WHERE user_id = 'usr_demo_cf31025295714ece8d91f5af13afd76d' AND medication_id = 'cmq2lzjqh005x01nrys2crzg9';
-SELECT 'intake_medB' AS what, count(*), count(*) FILTER (WHERE taken_at IS NOT NULL) AS taken, count(*) FILTER (WHERE skipped) AS skipped FROM medication_intake_events WHERE user_id = 'usr_demo_cf31025295714ece8d91f5af13afd76d' AND medication_id = 'cmq2m0mzk006101nrdpijbij0';
diff --git a/scripts/seed-demo-meds-apps01.sql b/scripts/seed-demo-meds-apps01.sql
deleted file mode 100644
index b2fb1fedf..000000000
--- a/scripts/seed-demo-meds-apps01.sql
+++ /dev/null
@@ -1,65 +0,0 @@
--- HealthLog demo medications for apps01 — canonical ids the 1-year extender
--- (scripts/seed-demo-extend-1y.sql) references for its intake history.
--- Idempotent. Creates the two demo meds + schedules so the extender's
--- medication_intake_events FK resolves and the medication cards/compliance
--- surfaces populate. Demo user id is the canonical seeded id.
---
--- Run: ssh apps-01 'docker exec -i psql -U healthlog -d healthlog' < scripts/seed-demo-meds-apps01.sql
-
-BEGIN;
-
-DO $$
-BEGIN
- IF NOT EXISTS (SELECT 1 FROM users WHERE id = 'usr_demo_cf31025295714ece8d91f5af13afd76d') THEN
- RAISE EXCEPTION 'seed-demo-meds: demo user not found';
- END IF;
-END $$;
-
--- ── Med A — Mounjaro (GLP-1, weekly on Sundays @ 08:00) ────────────
-INSERT INTO medications
- (id, user_id, name, dose, treatment_class, doses_per_unit, active, notifications_enabled, starts_on, created_at, updated_at)
-VALUES
- ('cmq2lzjqh005x01nrys2crzg9', 'usr_demo_cf31025295714ece8d91f5af13afd76d',
- 'Mounjaro', '5 mg', 'GLP1', 4, true, true,
- (NOW() - INTERVAL '380 days')::date, NOW() - INTERVAL '380 days', NOW())
-ON CONFLICT (id) DO UPDATE SET
- name = EXCLUDED.name, dose = EXCLUDED.dose, treatment_class = EXCLUDED.treatment_class,
- doses_per_unit = EXCLUDED.doses_per_unit, active = true, starts_on = EXCLUDED.starts_on,
- updated_at = NOW();
-
-INSERT INTO medication_schedules
- (id, medication_id, window_start, window_end, label, times_of_day, days_of_week, rrule)
-VALUES
- ('sched_demo_medA_mounjaro', 'cmq2lzjqh005x01nrys2crzg9',
- '08:00', '10:00', 'Wöchentlich', ARRAY['08:00'], '7', 'FREQ=WEEKLY;BYDAY=SU')
-ON CONFLICT (id) DO UPDATE SET
- window_start = EXCLUDED.window_start, window_end = EXCLUDED.window_end,
- label = EXCLUDED.label, times_of_day = EXCLUDED.times_of_day,
- days_of_week = EXCLUDED.days_of_week, rrule = EXCLUDED.rrule;
-
--- ── Med B — Ramipril (twice daily @ 08:00 + 18:00) ─────────────────
-INSERT INTO medications
- (id, user_id, name, dose, treatment_class, active, notifications_enabled, starts_on, created_at, updated_at)
-VALUES
- ('cmq2m0mzk006101nrdpijbij0', 'usr_demo_cf31025295714ece8d91f5af13afd76d',
- 'Ramipril', '5 mg', 'GENERIC', true, true,
- (NOW() - INTERVAL '380 days')::date, NOW() - INTERVAL '380 days', NOW())
-ON CONFLICT (id) DO UPDATE SET
- name = EXCLUDED.name, dose = EXCLUDED.dose, treatment_class = EXCLUDED.treatment_class,
- active = true, starts_on = EXCLUDED.starts_on, updated_at = NOW();
-
-INSERT INTO medication_schedules
- (id, medication_id, window_start, window_end, label, times_of_day, days_of_week, rrule)
-VALUES
- ('sched_demo_medB_ramipril', 'cmq2m0mzk006101nrdpijbij0',
- '08:00', '18:00', 'Morgens & Abends', ARRAY['08:00','18:00'], NULL, 'FREQ=DAILY')
-ON CONFLICT (id) DO UPDATE SET
- window_start = EXCLUDED.window_start, window_end = EXCLUDED.window_end,
- label = EXCLUDED.label, times_of_day = EXCLUDED.times_of_day,
- days_of_week = EXCLUDED.days_of_week, rrule = EXCLUDED.rrule;
-
-COMMIT;
-
-\echo === demo meds ===
-SELECT id, name, dose, treatment_class, active FROM medications
-WHERE user_id = 'usr_demo_cf31025295714ece8d91f5af13afd76d' ORDER BY name;
diff --git a/scripts/seed-demo-scores-1y.sql b/scripts/seed-demo-scores-1y.sql
deleted file mode 100644
index d608b39b5..000000000
--- a/scripts/seed-demo-scores-1y.sql
+++ /dev/null
@@ -1,153 +0,0 @@
--- HealthLog demo computed-wellness-score seeder for apps01 (1 year).
---
--- WHAT THIS DOES
--- Seeds the three PERSISTED nightly wellness scores for the demo account so
--- all of the dashboard's "Deine Gesundheitswerte" rings render with a full
--- year of history. These three are normally written by the nightly score
--- crons (src/lib/jobs/{recovery,stress,strain}-score.ts) as COMPUTED
--- Measurement rows; the demo never had them computed, so only the two
--- compute-on-read composites (READINESS + SLEEP_SCORE — derived live from the
--- demo's RHR/HRV/sleep/mood, NOT stored) were showing.
---
--- channel type external_id unit value
--- Recovery RECOVERY_SCORE recovery:YYYY-MM-DD score 0..100 (55-90)
--- Strain STRAIN_SCORE strain:YYYY-MM-DD score 0..100 (20-75)
--- Stress STRESS_SCORE stress:YYYY-MM-DD score 0..100 (25-65)
---
--- READINESS and SLEEP_SCORE are NOT seeded here: neither exists in the
--- measurement_type enum (verified live) — both are recomputed on read from
--- the underlying signals the demo already carries, so they already render and
--- a stored row would be ignored. Seeding them would be dead data.
---
--- Row shape mirrors src/lib/insights/score-row.ts::upsertScoreRow exactly:
--- source = COMPUTED, unit = 'score', measured_at = noon UTC on the scored day,
--- external_id = ':YYYY-MM-DD', sleep_stage / device_type /
--- external_source_version / deleted_at all NULL.
---
--- Coverage: one row per (channel, UTC day) for the last 365 days (offsets
--- 1..365 — offset 0 is "today", which the nightly cron scores as the PREVIOUS
--- day, so the freshest persisted day is yesterday; this matches the job and
--- keeps every row strictly measured_at <= NOW()). 365 days x 3 channels =
--- 1,095 rows on a clean apply.
---
--- DETERMINISM
--- Values + ids are hash-derived from the day stamp (hashtextextended /
--- md5 — NO random()), so a re-run is byte-stable. Idempotent re-runs are
--- additionally guarded by ON CONFLICT DO NOTHING on the live unique index
--- (user_id, type, source, external_id) — the exact key the cron upserts on.
---
--- SCOPE + SAFETY
--- - Demo-user-scoped: EVERY statement hardcodes the demo user id.
--- - Idempotent: ON CONFLICT DO NOTHING; re-running adds nothing.
--- - No DDL (no CREATE/ALTER/DROP/TRUNCATE), no other-user writes, no
--- table-wide DELETE/UPDATE, no future-dated rows (measured_at <= NOW()).
---
--- Run: ssh apps-01 'docker exec -i psql -U healthlog -d healthlog' < scripts/seed-demo-scores-1y.sql
-
-\set uid 'usr_demo_cf31025295714ece8d91f5af13afd76d'
-
-BEGIN;
-
--- Refuse to run if the demo user does not exist.
-DO $$
-BEGIN
- IF NOT EXISTS (SELECT 1 FROM users WHERE id = 'usr_demo_cf31025295714ece8d91f5af13afd76d') THEN
- RAISE EXCEPTION 'seed-demo-scores-1y: demo user not found';
- END IF;
-END $$;
-
--- ── The three persisted score channels, one row per (channel, day) ─────────────
--- offsets 1..365 (the scored day is the day that just ended; offset 0 = today is
--- never persisted by the cron). day_key is the UTC calendar day; measured_at is
--- noon UTC on it, mirroring scoreMeasuredAt(). Deterministic values:
--- - week phase : a gentle 7-day sinusoid keyed to the day-of-year (weekly
--- rhythm — lighter mid-week, heavier toward the weekend).
--- - noise : per-(channel,day) hash in [0,1), no random().
--- - recovery : 55..90, dips after a "hard" (high-strain) day.
--- - strain : 20..75, weekend / mid-week training spikes (~25% of days).
--- - stress : 25..65, inversely coupled to recovery (recovery up → stress
--- down) so the two rings tell a coherent story.
-WITH days AS (
- SELECT
- d,
- (NOW() AT TIME ZONE 'UTC')::date - d AS day_key
- FROM generate_series(1, 365) AS d
-),
-calc AS (
- SELECT
- d,
- day_key,
- (day_key + 12)::timestamp AS measured_at, -- noon UTC on the scored day
- -- weekly rhythm in [-1, 1] from ISO day-of-week (Mon=1..Sun=7)
- sin(2 * pi() * (extract(isodow FROM day_key)::numeric - 1) / 7.0) AS week_phase,
- -- deterministic per-day "is this a training day" flag (~25%)
- ( (hashtextextended('score-train-' || day_key::text, 0) & 2147483647)::numeric / 2147483647.0 ) AS train_r,
- -- independent deterministic noise streams per channel, all in [0,1)
- ( (hashtextextended('score-rec-' || day_key::text, 0) & 2147483647)::numeric / 2147483647.0 ) AS rec_r,
- ( (hashtextextended('score-str-' || day_key::text, 0) & 2147483647)::numeric / 2147483647.0 ) AS str_r,
- ( (hashtextextended('score-stress-' || day_key::text, 0) & 2147483647)::numeric / 2147483647.0 ) AS stress_r
- FROM days
-),
-vals AS (
- SELECT
- d, day_key, measured_at, train_r,
- -- STRAIN 20..75: base + weekly rhythm + a spike on training days.
- LEAST(75, GREATEST(20, round(
- 40 + week_phase * 8
- + (CASE WHEN train_r < 0.25 THEN 22 ELSE 0 END)
- + (str_r - 0.5) * 14
- )))::int AS strain,
- -- RECOVERY 55..90: base + weekly rhythm, suppressed the day after a hard day.
- LEAST(90, GREATEST(55, round(
- 74 + week_phase * 6
- - (CASE WHEN train_r < 0.25 THEN 7 ELSE 0 END)
- + (rec_r - 0.5) * 16
- )))::int AS recovery
- FROM calc
-),
-final AS (
- SELECT
- v.d, v.day_key, v.measured_at, v.strain, v.recovery,
- -- STRESS 25..65: inverse to recovery (high recovery → low stress) + noise.
- LEAST(65, GREATEST(25, round(
- 90 - v.recovery * 0.5
- + (c.stress_r - 0.5) * 14
- )))::int AS stress
- FROM vals v JOIN calc c USING (d)
-),
-rows AS (
- SELECT day_key, measured_at, type, ext_prefix, value FROM final,
- LATERAL (VALUES
- ('RECOVERY_SCORE', 'recovery:', recovery),
- ('STRAIN_SCORE', 'strain:', strain),
- ('STRESS_SCORE', 'stress:', stress)
- ) AS ch(type, ext_prefix, value)
-)
-INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
-SELECT
- -- deterministic id from the per-row external_id → byte-stable re-runs.
- 'm_score_' || md5(ext_prefix || to_char(day_key, 'YYYY-MM-DD')),
- 'usr_demo_cf31025295714ece8d91f5af13afd76d',
- type::measurement_type,
- value::float8,
- 'score',
- 'COMPUTED'::measurement_source,
- measured_at,
- ext_prefix || to_char(day_key, 'YYYY-MM-DD'),
- measured_at,
- measured_at
-FROM rows
-WHERE measured_at <= NOW()
-ON CONFLICT (user_id, type, source, external_id) DO NOTHING;
-
-COMMIT;
-
--- ── Verification (demo-user-scoped, all five score channels) ───────────────────
-\echo === post-apply score coverage (demo user) ===
-SELECT type, count(*) AS rows, min(measured_at)::date AS oldest, max(measured_at)::date AS newest
-FROM measurements
-WHERE user_id = :'uid'
- AND type IN ('RECOVERY_SCORE', 'STRAIN_SCORE', 'STRESS_SCORE')
-GROUP BY type
-ORDER BY type;
-\echo (READINESS + SLEEP_SCORE are compute-on-read composites — not stored, render from underlying signals.)
diff --git a/scripts/seed-demo-v15.sql b/scripts/seed-demo-v15.sql
index a15ca2411..960a8d5ac 100644
--- a/scripts/seed-demo-v15.sql
+++ b/scripts/seed-demo-v15.sql
@@ -1,7 +1,7 @@
-- v1.5.0 demo seed — sliding-window, NOW()-anchored, full iOS+Withings story.
--
-- The pre-v1.5 demo seed shipped only WEIGHT / BLOOD_PRESSURE / PULSE /
--- BODY_FAT / SLEEP / STEPS — and the demo DB on edge-01 had drifted to
+-- BODY_FAT / SLEEP / STEPS, and the demo DB had drifted to
-- a static snapshot whose last reading was ten days old. For the iOS
-- public-beta cut we need a fresh picture: data through today, every
-- type the dashboard tiles can render, a source mix that shows the
@@ -17,8 +17,8 @@
-- falls back to live SQL (the slim rollup-tier reader) and the boot
-- backfill repopulates on the next entrypoint sweep.
--
--- Usage on edge-01:
--- docker exec -i agos0oo88gsgg88kcg4swosw \
+-- Usage on the demo host:
+-- docker exec -i \
-- psql -U healthlog -d healthlog < seed-demo-v15.sql
BEGIN;
diff --git a/scripts/v1341-audit-ledger-check.mjs b/scripts/v1341-audit-ledger-check.mjs
deleted file mode 100644
index 83e5724b5..000000000
--- a/scripts/v1341-audit-ledger-check.mjs
+++ /dev/null
@@ -1,198 +0,0 @@
-#!/usr/bin/env node
-
-import { readFileSync, statSync } from "node:fs";
-import { resolve } from "node:path";
-
-const ledgerArg = process.argv[2];
-if (!ledgerArg) {
- console.error("usage: node scripts/v1341-audit-ledger-check.mjs ");
- process.exit(2);
-}
-
-const ledgerPath = resolve(ledgerArg);
-const MAX_BYTES = 256 * 1024;
-const REQUIRED_FIELDS = [
- "Source",
- "Status",
- "Severity",
- "Category",
- "Evidence",
- "Affected path",
- "Attempted refutation",
- "Impact",
- "Requirement",
- "Planned verification",
- "Confidence",
- "Unresolved",
-];
-const ALLOWED_STATUS = new Set([
- "confirmed",
- "suspected",
- "blocked",
- "cleared",
- "refuted",
-]);
-const ALLOWED_SEVERITY = new Set(["critical", "high", "medium", "low", "info"]);
-const ALLOWED_CONFIDENCE = new Set(["high", "medium", "low"]);
-const REQUIRED_SOURCES = ["research:", "correctness:", "live:", "performance:"];
-const REQUIRED_CATEGORIES = [
- "day-boundary",
- "silent-success",
- "cache-freshness",
- "persistence",
- "outcome-honesty",
-];
-const PRIVACY_PATTERNS = [
- {
- name: "email address",
- pattern: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i,
- },
- {
- name: "authorization credential",
- pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]+/i,
- },
- {
- name: "secret assignment",
- pattern:
- /\b(?:token|secret|password|api[_-]?key|client[_-]?secret)\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{8,}/i,
- },
- {
- name: "URL or endpoint with authority",
- pattern: /\bhttps?:\/\/[^\s)>\]]+/i,
- },
- {
- name: "IPv4 address",
- pattern: /\b(?:\d{1,3}\.){3}\d{1,3}\b/,
- },
- {
- name: "UUID-like identifier",
- pattern:
- /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/i,
- },
- {
- name: "long hexadecimal identifier",
- pattern: /\b[0-9a-f]{32,}\b/i,
- },
- {
- name: "local user filesystem path",
- pattern: /(?:\/Users\/|\/home\/)[^\s`]+/,
- },
- {
- name: "explicit health value",
- pattern:
- /\b(?:glucose|blood pressure|heart rate|weight|bmi|hba1c)\s*(?:value\s*)?[:=]\s*-?\d+(?:\.\d+)?/i,
- },
-];
-
-const errors = [];
-let source;
-try {
- if (statSync(ledgerPath).size > MAX_BYTES) {
- errors.push(`ledger exceeds ${MAX_BYTES} bytes`);
- }
- source = readFileSync(ledgerPath, "utf8");
-} catch (error) {
- console.error(
- `v1341-audit-ledger-check: cannot read ledger (${error instanceof Error ? error.message : "unknown error"})`,
- );
- process.exit(2);
-}
-
-for (const check of PRIVACY_PATTERNS) {
- if (check.pattern.test(source))
- errors.push(`privacy-dangerous ${check.name} detected`);
-}
-
-const headingPattern = /^## (AUD-\d{3}) — (.+)$/gm;
-const headings = [...source.matchAll(headingPattern)];
-if (headings.length === 0) errors.push("no finding headings found");
-if (headings.length > 200) errors.push("finding count exceeds 200");
-
-const ids = new Set();
-const sourceRecords = new Set();
-const records = [];
-
-for (let index = 0; index < headings.length; index += 1) {
- const match = headings[index];
- const id = match[1];
- const title = match[2].trim();
- const bodyStart = match.index + match[0].length;
- const bodyEnd = headings[index + 1]?.index ?? source.length;
- const body = source.slice(bodyStart, bodyEnd);
- const fields = new Map();
-
- if (ids.has(id)) errors.push(`${id}: duplicate finding ID`);
- ids.add(id);
- if (!title) errors.push(`${id}: missing title`);
-
- for (const line of body.split("\n")) {
- const fieldMatch = line.match(/^- \*\*([^*]+):\*\* (.+)$/);
- if (!fieldMatch) continue;
- const [, name, value] = fieldMatch;
- if (fields.has(name)) errors.push(`${id}: duplicate field ${name}`);
- fields.set(name, value.trim());
- }
-
- for (const field of REQUIRED_FIELDS) {
- if (!fields.get(field)) errors.push(`${id}: missing ${field}`);
- }
-
- const status = fields.get("Status")?.toLowerCase();
- const severity = fields.get("Severity")?.toLowerCase();
- const confidence = fields.get("Confidence")?.toLowerCase();
- const unresolved = fields.get("Unresolved")?.toLowerCase();
- const sourceField = fields.get("Source");
-
- if (status && !ALLOWED_STATUS.has(status)) {
- errors.push(`${id}: invalid Status ${status}`);
- }
- if (severity && !ALLOWED_SEVERITY.has(severity)) {
- errors.push(`${id}: invalid Severity ${severity}`);
- }
- if (confidence && !ALLOWED_CONFIDENCE.has(confidence)) {
- errors.push(`${id}: invalid Confidence ${confidence}`);
- }
- if (unresolved && !["yes", "no"].includes(unresolved)) {
- errors.push(`${id}: Unresolved must be yes or no`);
- }
- if (["cleared", "refuted"].includes(status) && unresolved !== "no") {
- errors.push(`${id}: ${status} findings must set Unresolved to no`);
- }
- if (sourceField) {
- if (sourceRecords.has(sourceField))
- errors.push(`${id}: duplicate Source record`);
- sourceRecords.add(sourceField);
- }
-
- records.push({ id, fields });
-}
-
-for (let index = 0; index < records.length; index += 1) {
- const expected = `AUD-${String(index + 1).padStart(3, "0")}`;
- if (records[index].id !== expected) {
- errors.push(`${records[index].id}: expected sequential ID ${expected}`);
- }
-}
-
-for (const prefix of REQUIRED_SOURCES) {
- if (
- !records.some((record) => record.fields.get("Source")?.includes(prefix))
- ) {
- errors.push(`missing source family ${prefix}`);
- }
-}
-for (const category of REQUIRED_CATEGORIES) {
- if (!records.some((record) => record.fields.get("Category") === category)) {
- errors.push(`missing required category ${category}`);
- }
-}
-
-if (errors.length > 0) {
- console.error(`v1341-audit-ledger-check: FAIL (${errors.length} issue(s))`);
- for (const error of errors) console.error(`- ${error}`);
- process.exit(1);
-}
-
-console.log(
- `v1341-audit-ledger-check: PASS (${records.length} unique findings; privacy scan clean)`,
-);
diff --git a/scripts/v1341-performance-baseline.mjs b/scripts/v1341-performance-baseline.mjs
deleted file mode 100644
index 700fbd85c..000000000
--- a/scripts/v1341-performance-baseline.mjs
+++ /dev/null
@@ -1,356 +0,0 @@
-#!/usr/bin/env node
-
-import { execFileSync } from "node:child_process";
-import { writeFileSync } from "node:fs";
-import { arch, cpus, platform, release } from "node:os";
-import { resolve } from "node:path";
-import { performance } from "node:perf_hooks";
-
-const LIMITS = Object.freeze({
- deadlineMs: 30_000,
- warmSamples: 11,
- googlePages: 786,
- googleRowsPerPage: 32,
- reminderMedications: 2_000,
- reminderSlotsPerMedication: 4,
- reminderCleanupBacklog: 2_000,
- healthDenseDays: 42,
- healthSamplesPerDay: 1_440,
- healthSparseDays: 379,
- healthSparseSamplesPerDay: 8,
-});
-
-const args = process.argv.slice(2);
-const fixtureMode = args.includes("--fixture");
-const outputIndex = args.indexOf("--output");
-const outputPath =
- outputIndex >= 0 && args[outputIndex + 1]
- ? resolve(args[outputIndex + 1])
- : null;
-
-function fail(message) {
- console.error(`v1341-performance-baseline: ${message}`);
- process.exit(2);
-}
-
-if (!fixtureMode) {
- fail(
- "refusing to run without --fixture; live and production probes are unsupported",
- );
-}
-if (process.env.NODE_ENV === "production") {
- fail("refusing to run with NODE_ENV=production");
-}
-if (!outputPath) {
- fail("--output is required");
-}
-if (args.some((arg) => /^(--url|--host|--database|--token|--live)/.test(arg))) {
- fail(
- "network, database, credential, and live-target arguments are unsupported",
- );
-}
-
-const startedAt = performance.now();
-const deadlineAt = startedAt + LIMITS.deadlineMs;
-
-function assertWithinDeadline() {
- if (performance.now() > deadlineAt) {
- throw new Error(`fixture deadline exceeded (${LIMITS.deadlineMs} ms)`);
- }
-}
-
-function seededNumber(index, salt) {
- let value = (index + 1) ^ salt;
- value = Math.imul(value ^ (value >>> 16), 0x45d9f3b);
- value = Math.imul(value ^ (value >>> 16), 0x45d9f3b);
- return (value ^ (value >>> 16)) >>> 0;
-}
-
-function percentile(sorted, fraction) {
- return sorted[
- Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1)
- ];
-}
-
-function formatMs(value) {
- return value.toFixed(3);
-}
-
-function formatBytes(value) {
- const sign = value < 0 ? "-" : "";
- return `${sign}${(Math.abs(value) / 1024 / 1024).toFixed(2)} MiB`;
-}
-
-function measure(operation) {
- assertWithinDeadline();
- const beforeHeap = process.memoryUsage().heapUsed;
- const before = performance.now();
- const result = operation();
- const elapsedMs = performance.now() - before;
- const heapDeltaBytes = process.memoryUsage().heapUsed - beforeHeap;
- assertWithinDeadline();
- return { elapsedMs, heapDeltaBytes, result };
-}
-
-function benchmark(name, operation) {
- const cold = measure(operation);
- const warm = [];
- for (let index = 0; index < LIMITS.warmSamples; index += 1) {
- warm.push(measure(operation));
- }
- const elapsed = warm.map((sample) => sample.elapsedMs).sort((a, b) => a - b);
- const heaps = warm.map((sample) => sample.heapDeltaBytes);
- const checksums = new Set([
- cold.result.checksum,
- ...warm.map((sample) => sample.result.checksum),
- ]);
- if (checksums.size !== 1) {
- throw new Error(`${name} fixture produced a non-deterministic checksum`);
- }
- return {
- name,
- coldMs: cold.elapsedMs,
- coldHeapBytes: cold.heapDeltaBytes,
- medianMs: percentile(elapsed, 0.5),
- p95Ms: percentile(elapsed, 0.95),
- maxMs: elapsed.at(-1),
- maxWarmHeapBytes: Math.max(...heaps),
- checksum: cold.result.checksum,
- counters: cold.result.counters,
- };
-}
-
-function googleHistoryFixture() {
- const accumulated = [];
- let checksum = 0;
- let finalTokenPresent = false;
-
- for (let page = 0; page < LIMITS.googlePages; page += 1) {
- const encodedPage = JSON.stringify({
- rows: Array.from({ length: LIMITS.googleRowsPerPage }, (_, row) => ({
- t: page * LIMITS.googleRowsPerPage + row,
- v: seededNumber(page * LIMITS.googleRowsPerPage + row, 17) % 10_000,
- })),
- next: page + 1 < LIMITS.googlePages ? `fixture-page-${page + 1}` : null,
- });
- const decodedPage = JSON.parse(encodedPage);
- accumulated.push(...decodedPage.rows);
- finalTokenPresent = decodedPage.next !== null;
- }
-
- for (const row of accumulated) {
- checksum = (checksum + row.t * 17 + row.v) >>> 0;
- }
- return {
- checksum,
- counters: {
- pages: LIMITS.googlePages,
- rows: accumulated.length,
- concurrency: 1,
- finalTokenPresent,
- },
- };
-}
-
-function reminderCohortFixture() {
- let checksum = 0;
- let dueCandidates = 0;
- let modeledQueries = 1;
-
- for (let cleanup = 0; cleanup < LIMITS.reminderCleanupBacklog; cleanup += 1) {
- checksum = (checksum + seededNumber(cleanup, 31)) >>> 0;
- }
-
- for (
- let medication = 0;
- medication < LIMITS.reminderMedications;
- medication += 1
- ) {
- modeledQueries += 2;
- for (let slot = 0; slot < LIMITS.reminderSlotsPerMedication; slot += 1) {
- const phase = seededNumber(
- medication * LIMITS.reminderSlotsPerMedication + slot,
- 43,
- );
- if (phase % 5 === 0) {
- dueCandidates += 1;
- modeledQueries += 2;
- }
- checksum = (checksum + (phase & 0xffff)) >>> 0;
- }
- }
- return {
- checksum,
- counters: {
- medications: LIMITS.reminderMedications,
- slots: LIMITS.reminderMedications * LIMITS.reminderSlotsPerMedication,
- cleanupBacklog: LIMITS.reminderCleanupBacklog,
- dueCandidates,
- modeledQueries,
- concurrency: 1,
- },
- };
-}
-
-function healthScoreFixture() {
- const denseRows = Array.from(
- { length: LIMITS.healthDenseDays * LIMITS.healthSamplesPerDay },
- (_, index) => ({
- day: Math.floor(index / LIMITS.healthSamplesPerDay),
- source: index % 3,
- value: seededNumber(index, 59) % 1_000,
- }),
- );
- const sparseRows = Array.from(
- { length: LIMITS.healthSparseDays * LIMITS.healthSparseSamplesPerDay },
- (_, index) => ({
- day: Math.floor(index / LIMITS.healthSparseSamplesPerDay),
- domain: index % 8,
- value: seededNumber(index, 71) % 1_000,
- }),
- );
-
- const dailyCanonical = new Map();
- for (const row of denseRows) {
- const prior = dailyCanonical.get(row.day);
- if (!prior || row.source < prior.source) dailyCanonical.set(row.day, row);
- }
- const domainTotals = new Array(8).fill(0);
- const domainCounts = new Array(8).fill(0);
- for (const row of sparseRows) {
- domainTotals[row.domain] += row.value;
- domainCounts[row.domain] += 1;
- }
- let checksum = 0;
- for (const row of dailyCanonical.values())
- checksum = (checksum + row.value) >>> 0;
- for (let domain = 0; domain < domainTotals.length; domain += 1) {
- const normalized = Math.round(domainTotals[domain] / domainCounts[domain]);
- checksum = (checksum * 31 + normalized) >>> 0;
- }
- return {
- checksum,
- counters: {
- denseRows: denseRows.length,
- sparseRows: sparseRows.length,
- canonicalDays: dailyCanonical.size,
- domains: domainTotals.length,
- concurrency: 1,
- parity: "stable-checksum",
- },
- };
-}
-
-function commitSha() {
- try {
- return execFileSync("git", ["rev-parse", "--short", "HEAD"], {
- encoding: "utf8",
- stdio: ["ignore", "pipe", "ignore"],
- }).trim();
- } catch {
- return "unavailable";
- }
-}
-
-function counterSummary(result) {
- return Object.entries(result.counters)
- .map(([key, value]) => `${key}=${value}`)
- .join("; ");
-}
-
-try {
- const results = [
- benchmark("Google history pagination shape", googleHistoryFixture),
- benchmark("Medication reminder cohort shape", reminderCohortFixture),
- benchmark("Health Score read/canonicalization shape", healthScoreFixture),
- ];
- const durationMs = performance.now() - startedAt;
- const generatedAt = new Date().toISOString();
- const markdown = `# v1.34.1 Performance Baseline
-
-**Generated:** ${generatedAt}
-
-**Commit:** \`${commitSha()}\`
-
-**Exit status:** PASS
-
-**Mode:** deterministic local fixture only
-
-## Safety and interpretation
-
-- No network, database, provider, production, or staging endpoint was contacted.
-- Fixtures contain generated counters only. The report emits no tokens, identifiers, payloads, health values, hostnames, usernames, or working-directory paths.
-- Concurrency is fixed at one, warm sample count is ${LIMITS.warmSamples}, and the whole run has a ${LIMITS.deadlineMs / 1000}-second deadline.
-- Timing is a local code-shape baseline, not a production latency or capacity claim. Database, provider, network, framework, cache, and rendering costs remain unmeasured.
-- The Health Score fixture exercises dense/sparse row construction, source canonicalization, and a stable composite proxy. It does not replace production algorithm parity or database-backed performance tests.
-
-## Environment
-
-| Attribute | Value |
-| --- | --- |
-| Node | ${process.version} |
-| Platform | ${platform()} ${release()} |
-| Architecture | ${arch()} |
-| Logical CPUs | ${cpus().length} |
-| Process | single local Node process |
-| Total harness duration | ${formatMs(durationMs)} ms |
-
-## Fixture scale
-
-| Path shape | Fixed scale |
-| --- | --- |
-| Google history | ${LIMITS.googlePages} serial pages × ${LIMITS.googleRowsPerPage} generated rows; terminal page has no remaining token |
-| Reminder cohort | ${LIMITS.reminderMedications} medications × ${LIMITS.reminderSlotsPerMedication} slots plus ${LIMITS.reminderCleanupBacklog} cleanup records |
-| Health Score | ${LIMITS.healthDenseDays} dense days × ${LIMITS.healthSamplesPerDay} samples plus ${LIMITS.healthSparseDays} sparse days × ${LIMITS.healthSparseSamplesPerDay} samples |
-
-## Measurements
-
-| Local path shape | Cold | Warm median | Warm p95 | Warm max | Cold heap delta | Max warm heap delta |
-| --- | ---: | ---: | ---: | ---: | ---: | ---: |
-${results
- .map(
- (result) =>
- `| ${result.name} | ${formatMs(result.coldMs)} ms | ${formatMs(result.medianMs)} ms | ${formatMs(result.p95Ms)} ms | ${formatMs(result.maxMs)} ms | ${formatBytes(result.coldHeapBytes)} | ${formatBytes(result.maxWarmHeapBytes)} |`,
- )
- .join("\n")}
-
-### Determinism and bounded counters
-
-${results
- .map(
- (result) =>
- `- **${result.name}:** checksum \`${result.checksum.toString(16).padStart(8, "0")}\`; ${counterSummary(result)}.`,
- )
- .join("\n")}
-
-## Route/path interpretation
-
-| Representative path | What this fixture measures | What it does not measure |
-| --- | --- | --- |
-| Google full-sync pagination | Serial JSON page materialization and accumulation at the observed 786-page shape | Provider latency, DB writes, token refresh, rollups, or a safe universal page ceiling |
-| Medication reminder worker | Serial cohort/slot decisions, cleanup scan, and modeled query amplification | Prisma latency, locks, dispatch latency, or the live cohort size |
-| Dashboard / analytics Health Score | Dense/sparse row processing, canonical selection, and stable composite proxy | SQL plans, cache behavior, exact production score parity, or user-visible route latency |
-
-## Hypotheses and unmeasured risks
-
-- **Hypothesis:** Serial provider latency and write statements dominate the Google full-sync wall time; this local CPU result neither confirms nor refutes that hypothesis.
-- **Hypothesis:** Reminder query/dispatch amplification, not local loop cost, determines whether a high-water pass approaches its 15-minute cadence.
-- **Hypothesis:** Cold database row materialization and canonicalization dominate dense-user Health Score work; production impact remains unconfirmed.
-- **Unmeasured:** network and database tails, cache hit/miss ratios, cross-process invalidation, framework rendering, device/browser performance, and production cohort distributions.
-- **Do not infer:** a provider defect, a safe page cap, a production regression, or permission to optimize from this report alone.
-
-## Reproduction
-
-\`node scripts/v1341-performance-baseline.mjs --fixture --output .planning/phases/01-v1-34-1-dashboard-insights-records-admin-and-integration-reliability/01-PERFORMANCE-BASELINE.md\`
-`;
-
- writeFileSync(outputPath, markdown, { encoding: "utf8", mode: 0o600 });
- console.log(
- `v1341-performance-baseline: PASS (${results.length} fixtures, ${formatMs(durationMs)} ms)`,
- );
-} catch (error) {
- console.error(
- `v1341-performance-baseline: FAIL (${error instanceof Error ? error.message : "unknown error"})`,
- );
- process.exit(1);
-}
diff --git a/scripts/verify-v137-adversarial.ts b/scripts/verify-v137-adversarial.ts
deleted file mode 100644
index fab06f39c..000000000
--- a/scripts/verify-v137-adversarial.ts
+++ /dev/null
@@ -1,305 +0,0 @@
-/**
- * v1.37.0 integration verifier — the inventories, and whether any of them is empty.
- *
- * ## What this is for
- *
- * The release is proved by inventories: a migration allocation, a set of
- * admitted mutating handlers, a notification delivery matrix, a set of security
- * principals, five accessibility states and eight journeys. Every one of them is
- * consumed by a test that iterates it, and every one of those tests passes
- * trivially when the list it iterates is empty. That is the failure this script
- * exists to catch, and it is not hypothetical — three separate checks in this
- * repository have been found green because their matcher matched nothing.
- *
- * So the rule here is that a count is asserted before it is used, and asserted
- * against a number written down rather than against "more than zero" alone.
- * A list that lost half its entries is not caught by a non-empty check.
- *
- * ## What this is NOT
- *
- * It does not run the suites and it cannot tell whether the tests that consume
- * these inventories assert anything worth asserting. It answers one question —
- * "is there something in each of these, and is it the amount there is supposed
- * to be" — and leaves the rest to the tests themselves.
- *
- * ## Usage
- *
- * pnpm dlx tsx scripts/verify-v137-adversarial.ts # everything
- * pnpm dlx tsx scripts/verify-v137-adversarial.ts --check-inventories # counts only
- * pnpm dlx tsx scripts/verify-v137-adversarial.ts --check-migrations # allocation only
- *
- * Exits non-zero on the first failing assertion, and prints every check it ran
- * so a run that checked nothing is visible as a run that printed nothing.
- */
-import { readdirSync, readFileSync } from "node:fs";
-import { join } from "node:path";
-
-import {
- ACCESSIBILITY_STATES,
- SHARING_ACCESSIBILITY_STATES,
- V137_E2E_JOURNEYS,
-} from "../tests/fixtures/v137/e2e-journeys";
-import { NOTIFICATION_DELIVERY_MATRIX } from "../tests/fixtures/v137/notification-matrix";
-import { SECURITY_PRINCIPALS } from "../tests/fixtures/v137/security-principals";
-import { ADMITTED_MUTATING_HANDLERS } from "../tests/fixtures/v137/sharing-matrix";
-
-const ROOT = process.cwd();
-
-let failures = 0;
-let checks = 0;
-
-function check(label: string, ok: boolean, detail: string): void {
- checks += 1;
- if (ok) {
- console.log(` ok ${label} — ${detail}`);
- return;
- }
- console.error(` FAIL ${label} — ${detail}`);
- failures += 1;
-}
-
-function expectCount(label: string, actual: number, expected: number): void {
- check(label, actual === expected, `${actual} (expected ${expected})`);
-}
-
-/* -------------------------------------------------------------------------- */
-/* Migration allocation */
-/* -------------------------------------------------------------------------- */
-
-/**
- * The release's allocation, corrected. The synthesis said 0296-0299 and called
- * the range frozen; the release shipped seven. The list is here so a builder
- * inventing an eighth number, or a merge quietly renumbering one of these,
- * fails rather than being noticed at the release audit or not at all.
- */
-const MIGRATION_ALLOCATION = [
- "0296_account_grant_scope_and_manage",
- "0297_managed_profile",
- "0298_notification_delivery_identity",
- "0299_medication_intake_import_actor_attribution",
- "0300_notification_event_created_at_index",
- "0301_notification_egress_authorization",
- "0302_session_record_epoch",
-] as const;
-
-/**
- * Triggers this release installs. Neither is modelled in the Prisma schema, so
- * `migrate diff` has nothing to compare them against and cannot report one
- * going missing — the drift check is structurally blind here. Their presence in
- * the migration text is the cheapest honest signal; the behaviour is proved by
- * the integration suites named beside each one.
- */
-const MIGRATION_TRIGGERS: ReadonlyArray<{
- trigger: string;
- migration: string;
- provenBy: string;
-}> = [
- {
- trigger: "push_attempt_attribution_guard",
- migration: "0298_notification_delivery_identity",
- provenBy: "tests/integration/notification-attribution.test.ts",
- },
- {
- trigger: "sessions_record_epoch_bump",
- migration: "0302_session_record_epoch",
- provenBy: "tests/integration/record-session-epoch-trigger.test.ts",
- },
-];
-
-function checkMigrations(): void {
- console.log("migration allocation");
- const dir = join(ROOT, "prisma/migrations");
- const present = readdirSync(dir, { withFileTypes: true })
- .filter((entry) => entry.isDirectory())
- .map((entry) => entry.name);
-
- check(
- "the migrations directory really loaded",
- present.length > 100,
- `${present.length} migrations on disk`,
- );
-
- const release = present.filter((name) => /^(029[6-9]|030[0-2])_/.test(name));
- expectCount(
- "the release allocates exactly seven",
- release.length,
- MIGRATION_ALLOCATION.length,
- );
-
- for (const expected of MIGRATION_ALLOCATION) {
- check(
- `allocated ${expected.slice(0, 4)}`,
- present.includes(expected),
- expected,
- );
- }
-
- // Every number used once. A collision is the thing renumbering exists to
- // prevent, and two directories sharing a prefix apply in an order nobody
- // chose.
- const releaseNumbers = release.map((name) => name.slice(0, 4));
- const duplicated = releaseNumbers.filter(
- (n, i) => releaseNumbers.indexOf(n) !== i,
- );
- check(
- "no number is allocated twice in the release range",
- duplicated.length === 0,
- duplicated.length === 0 ? "none" : duplicated.join(", "),
- );
-
- for (const { trigger, migration, provenBy } of MIGRATION_TRIGGERS) {
- const sql = readFileSync(join(dir, migration, "migration.sql"), "utf8");
- check(
- `${migration} installs ${trigger}`,
- sql.includes(trigger),
- `behaviour proved by ${provenBy}`,
- );
- }
-}
-
-/* -------------------------------------------------------------------------- */
-/* Inventories */
-/* -------------------------------------------------------------------------- */
-
-function checkInventories(): void {
- console.log("inventories");
-
- // Handlers. Every admitted mutation is meant to run level, owner, actor,
- // effect and encrypted-field cases; an empty list runs none of them and the
- // matrix test reports the same green either way.
- check(
- "the admitted mutating handlers are non-empty",
- ADMITTED_MUTATING_HANDLERS.length > 0,
- `${ADMITTED_MUTATING_HANDLERS.length} handlers`,
- );
- // Keyed on route AND action, because one route legitimately appears more
- // than once: an `[id]` route carries an update and a delete, and they are
- // different contracts with different admitted levels. Keying on the route
- // alone reported eleven false collisions on the first run of this script.
- const handlerKeys = new Set(
- ADMITTED_MUTATING_HANDLERS.map((h) => `${h.action} ${h.route}`),
- );
- expectCount(
- "no route and action pair is admitted twice",
- handlerKeys.size,
- ADMITTED_MUTATING_HANDLERS.length,
- );
- check(
- "every handler names a module that exists",
- ADMITTED_MUTATING_HANDLERS.every((h) => {
- try {
- return (
- readFileSync(join(ROOT, "src", h.handlerModule), "utf8").length > 0
- );
- } catch {
- return false;
- }
- }),
- "each handlerModule resolves under src/",
- );
-
- // The notification matrix is two records by two Guardians by four channels.
- // Written as a product, so a dropped channel shows up here as sixteen minus
- // four rather than as a suite that simply ran fewer cases.
- expectCount(
- "the notification delivery matrix is 2 records x 2 Guardians x 4 channels",
- NOTIFICATION_DELIVERY_MATRIX.length,
- 16,
- );
- check(
- "no delivery in the matrix is interactive",
- NOTIFICATION_DELIVERY_MATRIX.every((row) => row.interactive === false),
- "a Guardian's notification offers no action buttons",
- );
-
- // Security principals. Each names an actor, a record and a recipient; a
- // principal missing one of the three would let a test assert against
- // undefined and pass.
- const principals = Object.entries(SECURITY_PRINCIPALS);
- check(
- "the security principals are non-empty",
- principals.length > 0,
- `${principals.length} principals`,
- );
- for (const [name, principal] of principals) {
- const { actorUserId, recordUserId, recipientUserId } = principal.identities;
- check(
- `${name} names all three identities`,
- Boolean(actorUserId && recordUserId && recipientUserId),
- `${actorUserId} / ${recordUserId} / ${recipientUserId}`,
- );
- }
-
- // Accessibility states and journeys. Both are enumerations the browser suite
- // is read back against, and both are exact rather than non-empty: a ninth
- // journey nobody counted and a fifth state quietly absorbed into a fourth are
- // the two failures the enumeration exists to make visible.
- expectCount(
- "exactly five accessibility states",
- ACCESSIBILITY_STATES.length,
- 5,
- );
- expectCount(
- "every state carries a case",
- SHARING_ACCESSIBILITY_STATES.length,
- 5,
- );
- expectCount("exactly eight journeys", V137_E2E_JOURNEYS.length, 8);
- expectCount(
- "every journey id is distinct",
- new Set(V137_E2E_JOURNEYS.map((j) => j.id)).size,
- 8,
- );
-
- // Each journey id must appear in a test title somewhere under e2e/ or tests/
- // or src/__tests__/. `sharing-accessibility-states.test.tsx` pins WHICH file
- // carries each one; this leg is the cheaper cross-check that none of the
- // eight has become a claim with no test anywhere at all.
- const searched = [
- ...readdirSync(join(ROOT, "e2e")).map((f) => join("e2e", f)),
- ...readdirSync(join(ROOT, "tests/integration")).map((f) =>
- join("tests/integration", f),
- ),
- ...readdirSync(join(ROOT, "src/__tests__")).map((f) =>
- join("src/__tests__", f),
- ),
- ].filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
- check(
- "the test tree really loaded",
- searched.length > 50,
- `${searched.length} files scanned`,
- );
- const corpus = searched
- .map((f) => readFileSync(join(ROOT, f), "utf8"))
- .join("\n");
- for (const journey of V137_E2E_JOURNEYS) {
- check(
- `${journey.id} is marked in a test title`,
- corpus.includes(`[${journey.id}]`),
- journey.claim.slice(0, 60) + "…",
- );
- }
-}
-
-/* -------------------------------------------------------------------------- */
-
-const argv = process.argv.slice(2);
-const only = argv.filter((a) => a.startsWith("--"));
-const runMigrations = only.length === 0 || only.includes("--check-migrations");
-const runInventories =
- only.length === 0 || only.includes("--check-inventories");
-
-if (runMigrations) checkMigrations();
-if (runInventories) checkInventories();
-
-// A run that asserted nothing is a run that proves nothing, and it would exit 0.
-if (checks === 0) {
- console.error("no checks ran — refusing to report success");
- process.exit(1);
-}
-
-console.log(
- `\n${checks} check(s), ${failures} failure(s)` +
- (failures === 0 ? " — inventories are non-empty and exact" : ""),
-);
-process.exit(failures === 0 ? 0 : 1);
diff --git a/src/README.md b/src/README.md
index cdbfccf46..7449a48d0 100644
--- a/src/README.md
+++ b/src/README.md
@@ -23,4 +23,4 @@ client → src/proxy.ts → src/app//route.ts → src/lib/api-handler.ts
## Conventions
-See [`../CLAUDE.md`](../CLAUDE.md) for the full set. The load-bearing ones: RSC by default (`"use client"` only when a hook/state/browser API needs it); every API route wraps `apiHandler` and parses its body with Zod `safeParse`; `userId` is always narrowed from session or Bearer, never accepted as a body field; TanStack Query keys come only from `src/lib/query-keys/`; outbound HTTP goes through `src/lib/safe-fetch.ts`.
+The load-bearing ones: RSC by default (`"use client"` only when a hook/state/browser API needs it); every API route wraps `apiHandler` and parses its body with Zod `safeParse`; `userId` is always narrowed from session or Bearer, never accepted as a body field; TanStack Query keys come only from `src/lib/query-keys/`; outbound HTTP goes through `src/lib/safe-fetch.ts`.
diff --git a/src/__tests__/ai-job-expiry-guard.test.ts b/src/__tests__/ai-job-expiry-guard.test.ts
index 65197af2d..e7b7cc5a5 100644
--- a/src/__tests__/ai-job-expiry-guard.test.ts
+++ b/src/__tests__/ai-job-expiry-guard.test.ts
@@ -51,7 +51,7 @@ import {
stripComments,
walkSourceFiles,
} from "./helpers/source-files";
-import { QUEUE_RUNTIME, type QueueRuntime } from "@/lib/jobs/queue-runtime";
+import { QUEUE_RUNTIME, type QueueRuntime } from "./helpers/queue-runtime";
import {
COMPLETION_CEILING_MS,
COMPREHENSIVE_WARM_CEILING_MS,
diff --git a/src/__tests__/backup-plan-classification.test.ts b/src/__tests__/backup-plan-classification.test.ts
index c62089f7d..9f93c188a 100644
--- a/src/__tests__/backup-plan-classification.test.ts
+++ b/src/__tests__/backup-plan-classification.test.ts
@@ -10,7 +10,23 @@ import {
NOT_IN_BACKUP_MODELS,
RAW_SQL_TABLES,
} from "@/lib/export/backup-plan";
-import { backupVerdict } from "@/lib/export/backup-verdict";
+
+type BackupVerdict = "BACKED_UP" | "DERIVED" | "NOT_IN_BACKUP" | "AUTH";
+
+/**
+ * The verdict for a model, or `null` when the schema carries a model the plan
+ * has never ruled on. The guard below turns that `null` into a failing test:
+ * a new model gets a decision, not a default.
+ */
+function backupVerdict(model: string): BackupVerdict | null {
+ if ((BACKED_UP_MODELS as readonly string[]).includes(model)) {
+ return "BACKED_UP";
+ }
+ if (model in DERIVED_MODELS) return "DERIVED";
+ if (model in NOT_IN_BACKUP_MODELS) return "NOT_IN_BACKUP";
+ if (AUTH_MODELS_OUT_OF_SCOPE.includes(model)) return "AUTH";
+ return null;
+}
/**
* Every model in the schema has a written verdict about the backup.
diff --git a/src/__tests__/baselines/source-file-lines.json b/src/__tests__/baselines/source-file-lines.json
index 59c4605ad..a4e3f316c 100644
--- a/src/__tests__/baselines/source-file-lines.json
+++ b/src/__tests__/baselines/source-file-lines.json
@@ -1,19 +1,19 @@
{
"$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
"count": 84,
- "total": 93840,
+ "total": 93816,
"entries": {
"app/medications/page-client.tsx": 839,
"components/charts/mood-chart.tsx": 1166,
- "components/cycle/log-day-sheet.tsx": 1639,
+ "components/cycle/log-day-sheet.tsx": 1646,
"components/insights/coach-panel/chat-bubble.tsx": 821,
"components/insights/coach-panel/coach-conversation.tsx": 1278,
"components/insights/coach-panel/use-coach.ts": 1486,
"components/insights/derived/vitals-dashboard.tsx": 1033,
"components/insights/health-score-card.tsx": 1023,
- "components/insights/insights-tab-strip.tsx": 1087,
+ "components/insights/insights-tab-strip.tsx": 1084,
"components/measurements/measurement-form.tsx": 815,
- "components/medications/detail/medication-detail-tabs.tsx": 1157,
+ "components/medications/detail/medication-detail-tabs.tsx": 1129,
"components/medications/medication-table.tsx": 853,
"components/medications/wizard/wizard-payload.ts": 1414,
"components/mood/mood-list.tsx": 1408,
diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json
index 5c72ec225..e785bbee3 100644
--- a/src/__tests__/baselines/source-function-lines.json
+++ b/src/__tests__/baselines/source-function-lines.json
@@ -1,7 +1,7 @@
{
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
- "count": 336,
- "total": 109290,
+ "count": 335,
+ "total": 109058,
"entries": {
"app/achievements/page.tsx :: AchievementsPage": 206,
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 275,
@@ -27,7 +27,7 @@
"app/api/mcp/oauth/token/route.ts :: POST": 250,
"app/api/mcp/oauth/token/route.ts :: POST > withBackgroundEvent(arg)": 248,
"app/api/measurement-reminders/[id]/route.ts :: apiHandler(arg) #2": 239,
- "app/api/medications/[id]/dose-history/route.ts :: apiHandler(arg)": 256,
+ "app/api/medications/[id]/dose-history/route.ts :: apiHandler(arg)": 241,
"app/api/medications/[id]/inventory/[itemId]/route.ts :: apiHandler(arg)": 204,
"app/api/medications/[id]/schedule-revisions/[revisionId]/route.ts :: apiHandler(arg)": 250,
"app/api/mental-health/assessments/route.ts :: postAssessment": 249,
@@ -38,14 +38,14 @@
"app/api/record-settings/[family]/route.ts :: apiHandler(arg) #2": 262,
"app/api/user/ai-provider/route.ts :: apiHandler(arg) #2": 224,
"app/api/withings/callback/route.ts :: apiHandler(arg)": 247,
- "app/auth/login/page.tsx :: LoginPage": 561,
+ "app/auth/login/page.tsx :: LoginPage": 559,
"app/auth/register/page.tsx :: RegisterPage": 283,
"app/coach/conversations/page.tsx :: CoachConversationsBody": 249,
"app/coach/plans/page.tsx :: CoachPlansBody": 216,
"app/insights/catalog/page.tsx :: MetricCatalogPage": 204,
"app/insights/medications/page.tsx :: InsightsMedikamentePage": 265,
"app/insights/page-client.tsx :: InsightsPageClient": 375,
- "app/labs/page.tsx :: LabsPage": 216,
+ "app/labs/page.tsx :: LabsPage": 217,
"app/medications/page-client.tsx :: MedicationsPageClient": 590,
"app/notifications/page.tsx :: NotificationsPage": 327,
"components/activity/activity-calendar-card.tsx :: ActivityCalendarCard": 470,
@@ -70,21 +70,21 @@
"components/charts/medication-compliance-chart.tsx :: MedicationComplianceChart": 543,
"components/charts/mood-chart.tsx :: MoodChart": 896,
"components/charts/mood-heatmap.tsx :: MoodHeatmap": 296,
- "components/charts/trend-card.tsx :: TrendCard": 365,
+ "components/charts/trend-card.tsx :: TrendCard": 351,
"components/clinician/clinician-view.tsx :: ClinicianView": 223,
"components/command-palette/command-palette.tsx :: PaletteBody": 333,
"components/custom-metrics/custom-metric-chart.tsx :: CustomMetricChart": 228,
- "components/custom-metrics/custom-metric-detail.tsx :: CustomMetricDetail": 277,
- "components/custom-metrics/custom-metric-form.tsx :: CustomMetricForm": 210,
+ "components/custom-metrics/custom-metric-detail.tsx :: CustomMetricDetail": 280,
+ "components/custom-metrics/custom-metric-form.tsx :: CustomMetricForm": 208,
"components/custom-metrics/custom-metric-history-list.tsx :: CustomMetricHistoryList": 298,
"components/cycle/bbt-chart.tsx :: BbtChart": 244,
"components/cycle/cycle-calendar.tsx :: CycleCalendar": 292,
"components/cycle/cycle-settings.tsx :: CycleSettings": 228,
- "components/cycle/cycle-view.tsx :: CycleView": 405,
- "components/cycle/log-day-sheet.tsx :: LogDaySheet": 753,
+ "components/cycle/cycle-view.tsx :: CycleView": 403,
+ "components/cycle/log-day-sheet.tsx :: LogDaySheet": 760,
"components/daily/today-hero.tsx :: TodayHero": 488,
"components/dashboard/dashboard-header.tsx :: DashboardHeader": 215,
- "components/dashboard/medication-intake-quick-add.tsx :: MedicationIntakeQuickAdd": 383,
+ "components/dashboard/medication-intake-quick-add.tsx :: MedicationIntakeQuickAdd": 384,
"components/day/day-layer.tsx :: DayLayer": 358,
"components/day/day-view.tsx :: DayView": 309,
"components/documents/document-ai-section.tsx :: DocumentAiSection": 262,
@@ -98,13 +98,13 @@
"components/documents/documents-view.tsx :: DocumentsView": 303,
"components/documents/sources/document-source-picker.tsx :: DocumentSourcePicker": 563,
"components/illness/log-day-sheet.tsx :: LogDaySheet": 265,
- "components/illness/new-episode-sheet.tsx :: NewEpisodeSheet": 264,
+ "components/illness/new-episode-sheet.tsx :: NewEpisodeSheet": 278,
"components/insights/coach-panel/chat-bubble.tsx :: ChatBubbleImpl": 417,
"components/insights/coach-panel/coach-conversation.tsx :: CoachConversation": 1002,
"components/insights/coach-panel/coach-input.tsx :: CoachInput": 259,
"components/insights/coach-panel/coach-model-picker.tsx :: CoachModelPicker": 208,
"components/insights/coach-panel/conversations-panel.tsx :: ConversationsPanel": 317,
- "components/insights/coach-panel/message-thread.tsx :: MessageThread": 390,
+ "components/insights/coach-panel/message-thread.tsx :: MessageThread": 389,
"components/insights/coach-panel/sources-rail.tsx :: SourcesRail": 203,
"components/insights/coach-panel/use-coach.ts :: useSendCoachMessage": 436,
"components/insights/coach-panel/use-coach.ts :: useSendCoachMessage > useCallback(arg) #2": 340,
@@ -138,18 +138,17 @@
"components/labs/ocr-row-editor.tsx :: OcrRowEditor": 378,
"components/layout/auth-shell.tsx :: AuthShell": 483,
"components/layout/bottom-nav.tsx :: BottomNav": 262,
- "components/layout/capture-picker.tsx :: CapturePicker": 235,
"components/layout/sidebar-nav.tsx :: SidebarNav": 356,
"components/layout/top-bar.tsx :: TopBar": 212,
"components/links/entity-link-picker.tsx :: EntityLinkPicker": 392,
"components/measurement-reminders/vorsorge-dashboard-card.tsx :: VorsorgeDashboardCard": 215,
- "components/measurement-reminders/vorsorge-section.tsx :: VorsorgeSection": 352,
+ "components/measurement-reminders/vorsorge-section.tsx :: VorsorgeSection": 354,
"components/measurements/measurement-form.tsx :: MeasurementForm": 496,
"components/medications/detail/efficacy/efficacy-chart.tsx :: EfficacyChart": 361,
- "components/medications/detail/medication-detail-tabs.tsx :: MedicationDetailTabs": 767,
+ "components/medications/detail/medication-detail-tabs.tsx :: MedicationDetailTabs": 760,
"components/medications/dose-history-add-dialog.tsx :: LedgerAddDialog": 221,
"components/medications/dose-strength-curve.tsx :: DoseStrengthCurve": 266,
- "components/medications/glp1-medication-card.tsx :: Glp1MedicationCard": 405,
+ "components/medications/glp1-medication-card.tsx :: Glp1MedicationCard": 396,
"components/medications/injection-site-picker.tsx :: InjectionSitePicker": 287,
"components/medications/intake-history-list-v2.tsx :: IntakeHistoryListV2": 403,
"components/medications/intake-import-dialog.tsx :: IntakeImportDialog": 201,
@@ -164,7 +163,7 @@
"components/medications/sections/inventory-dialogs.tsx :: AddInventoryDialog": 238,
"components/medications/sections/inventory-section.tsx :: InventorySection": 368,
"components/medications/side-effects-section.tsx :: SideEffectsSection": 518,
- "components/medications/wizard/medication-wizard-dialog.tsx :: WizardDialogShell": 516,
+ "components/medications/wizard/medication-wizard-dialog.tsx :: WizardDialogShell": 514,
"components/medications/wizard/steps/step3-dose.tsx :: Step3Dose": 222,
"components/medications/wizard/steps/step8-summary.tsx :: Step8Summary": 230,
"components/mood/manage/tag-groups-card.tsx :: TagGroupsCard": 252,
@@ -195,7 +194,7 @@
"components/settings/environment-section.tsx :: EnvironmentSection": 666,
"components/settings/import-panel/apple-health-import-card.tsx :: AppleHealthImportCard": 298,
"components/settings/import-panel/csv-import-card.tsx :: CsvImportCard": 212,
- "components/settings/import-panel/dose-history-import-card.tsx :: DoseHistoryImportCard": 222,
+ "components/settings/import-panel/dose-history-import-card.tsx :: DoseHistoryImportCard": 221,
"components/settings/import-panel/health-connect-import-card.tsx :: HealthConnectImportCard": 293,
"components/settings/import-panel/record-csv-import-card.tsx :: RecordCsvImportSheet": 458,
"components/settings/insights-pill-order-section.tsx :: InsightsPillOrderSection": 255,
@@ -220,18 +219,18 @@
"components/settings/thresholds-editor-section.tsx :: MetricRow": 241,
"components/symptoms/symptom-entry-form.tsx :: SymptomEntryForm": 317,
"components/symptoms/symptoms-section.tsx :: SymptomsSection": 205,
- "components/targets/range-bar.tsx :: RangeBar": 220,
- "components/targets/target-edit-sheet.tsx :: TargetEditSheetBody": 473,
- "components/timeline/life-event-form.tsx :: LifeEventForm": 281,
+ "components/targets/range-bar.tsx :: RangeBar": 215,
+ "components/targets/target-edit-sheet.tsx :: TargetEditSheetBody": 486,
+ "components/timeline/life-event-form.tsx :: LifeEventForm": 287,
"components/timeline/timeline-chart.tsx :: ChartBody": 236,
"components/timeline/timeline-chronicle.tsx :: TimelineChronicle": 246,
"components/timeline/timeline-view.tsx :: TimelineView": 478,
- "components/ui/responsive-sheet.tsx :: ResponsiveSheet": 239,
+ "components/ui/responsive-sheet.tsx :: ResponsiveSheet": 253,
"components/vaccinations/catalog-picker.tsx :: CatalogPicker": 221,
"components/vaccinations/custom-vaccine-sheet.tsx :: CustomVaccineSheet": 246,
"components/vaccinations/vaccination-form.tsx :: VaccinationForm": 218,
"components/workouts/manual-workout-form.tsx :: ManualWorkoutForm": 321,
- "hooks/use-sheet-drag.ts :: createSheetDrag": 312,
+ "hooks/use-sheet-drag.ts :: createSheetDrag": 319,
"lib/activity/calendar-math.ts :: buildActivityCalendar": 204,
"lib/ai/anthropic-client.ts :: generateCompletion": 294,
"lib/ai/coach/cycle-snapshot.ts :: buildCycleSnapshotBlock": 202,
@@ -322,7 +321,7 @@
"lib/medications/list-read.ts :: buildMedicationsList": 308,
"lib/medications/scheduling/dose-history.ts :: reconstructDoseHistory": 218,
"lib/multipart/stream-to-disk.ts :: streamMultipartToDisk": 272,
- "lib/notifications/dispatcher.ts :: dispatchNotification": 368,
+ "lib/notifications/dispatcher.ts :: dispatchNotification": 366,
"lib/notifications/senders/apns.ts :: sendViaApns": 299,
"lib/notifications/senders/web-push.ts :: sendViaWebPush": 255,
"lib/personal-records/pr-detection-worker.ts :: detectPersonalRecordsForUser": 239,
@@ -338,6 +337,6 @@
"lib/withings/sync-ecg.ts :: syncUserEcg": 202,
"lib/withings/sync-sleep.ts :: syncUserSleep": 414,
"lib/withings/sync.ts :: syncUserMeasurements": 337,
- "proxy.ts :: proxy": 470
+ "proxy.ts :: proxy": 467
}
}
diff --git a/src/__tests__/baselines/test-as-never.json b/src/__tests__/baselines/test-as-never.json
index 932401590..25d657b68 100644
--- a/src/__tests__/baselines/test-as-never.json
+++ b/src/__tests__/baselines/test-as-never.json
@@ -1,7 +1,7 @@
{
"$comment": "as never casts in tests, per test file under src/. Maintained by src/__tests__/test-realism-ratchet.test.ts.",
"count": 522,
- "total": 5448,
+ "total": 5447,
"entries": {
"__tests__/api/auth/login/mfa-branch.test.ts": 10,
"__tests__/api/auth/mfa/verify.test.ts": 9,
@@ -495,7 +495,7 @@
"lib/medications/scheduling/__tests__/dose-history.test.ts": 1,
"lib/medications/scheduling/__tests__/pin-conflict.test.ts": 2,
"lib/modules/__tests__/measurement-scope.test.ts": 3,
- "lib/notifications/__tests__/admin-locale.test.ts": 7,
+ "lib/notifications/__tests__/admin-locale.test.ts": 6,
"lib/notifications/__tests__/client-managed-apns.test.ts": 9,
"lib/notifications/__tests__/dispatch-localised-cache.test.ts": 5,
"lib/notifications/__tests__/dispatcher-global-channel-switch.test.ts": 8,
diff --git a/src/__tests__/baselines/test-source-reads.json b/src/__tests__/baselines/test-source-reads.json
index 638367e64..698898251 100644
--- a/src/__tests__/baselines/test-source-reads.json
+++ b/src/__tests__/baselines/test-source-reads.json
@@ -1,7 +1,7 @@
{
"$comment": "tests that read a source file by path, per test file under src/. Maintained by src/__tests__/test-realism-ratchet.test.ts.",
- "count": 336,
- "total": 336,
+ "count": 331,
+ "total": 331,
"entries": {
"__tests__/account-payload-consumer-guard.test.ts": 1,
"__tests__/admin-data-wipe-completeness.test.ts": 1,
@@ -87,7 +87,6 @@
"__tests__/module-gate-record-scoped.test.ts": 1,
"__tests__/mood-backup-completeness.test.ts": 1,
"__tests__/mood-prediction-write-surface-guard.test.ts": 1,
- "__tests__/moodlog-removal-guard.test.ts": 1,
"__tests__/next-image-optimizer-guard.test.ts": 1,
"__tests__/no-server-actions-guard.test.ts": 1,
"__tests__/oidc-native-error-reasons-documented.test.ts": 1,
@@ -141,7 +140,6 @@
"__tests__/unit-preference-display-guard.test.ts": 1,
"__tests__/unit-suite-browser-install-guard.test.ts": 1,
"__tests__/utc-day-key-allowlist-guard.test.ts": 1,
- "__tests__/v1341-locale-parity.test.ts": 1,
"__tests__/visit-link-guard.test.ts": 1,
"__tests__/workout-canonical-identity-guard.test.ts": 1,
"__tests__/write-rate-limit-guard.test.ts": 1,
@@ -174,7 +172,6 @@
"app/insights/__tests__/page-skeletons.test.ts": 1,
"app/insights/sleep/__tests__/empty-state-href.test.ts": 1,
"app/measurements/__tests__/add-token-resolution.test.ts": 1,
- "app/medications/__tests__/detail-page-history-surface.test.ts": 1,
"components/__tests__/design-consistency-guards.test.tsx": 1,
"components/__tests__/failed-read-never-empty.test.tsx": 1,
"components/__tests__/motion-reduce-spin-coverage.test.ts": 1,
@@ -182,7 +179,6 @@
"components/admin/__tests__/notification-test-failures.test.ts": 1,
"components/charts/__tests__/chart-error-state.test.tsx": 1,
"components/charts/__tests__/health-chart-overlay-defaults.test.tsx": 1,
- "components/charts/__tests__/health-chart-target-link-removed.test.ts": 1,
"components/charts/__tests__/mood-chart-polish.test.tsx": 1,
"components/charts/__tests__/touch-action-guard.test.ts": 1,
"components/cycle/__tests__/use-cycle-ring-has-dial.test.tsx": 1,
@@ -206,7 +202,6 @@
"components/insights/coach-panel/__tests__/open-at-end.test.ts": 1,
"components/insights/coach-panel/__tests__/panel-touch-targets.test.ts": 1,
"components/insights/coach-panel/__tests__/scroll-to-bottom-button.test.tsx": 1,
- "components/insights/mood/__tests__/mood-notes-timeline-removed.test.ts": 1,
"components/layout/__tests__/auth-shell-coach-hoist.test.ts": 1,
"components/layout/__tests__/auth-shell-shared-record-doors.test.tsx": 1,
"components/layout/__tests__/offline-banner.test.tsx": 1,
diff --git a/src/__tests__/baselines/version-stamp-comments.json b/src/__tests__/baselines/version-stamp-comments.json
index 6f553040c..6bbcd8aee 100644
--- a/src/__tests__/baselines/version-stamp-comments.json
+++ b/src/__tests__/baselines/version-stamp-comments.json
@@ -1,7 +1,7 @@
{
"$comment": "Comment lines that open with a release version, per file under src/. Maintained by src/__tests__/version-stamp-ratchet.test.ts.",
- "count": 2875,
- "total": 9162,
+ "count": 2863,
+ "total": 9122,
"entries": {
"__tests__/account-payload-consumer-guard.test.ts": 2,
"__tests__/acting-account-boundary-guard.test.ts": 7,
@@ -67,10 +67,8 @@
"__tests__/success-affordance-guard.test.ts": 8,
"__tests__/timezone-explicit-formatter-guard.test.ts": 1,
"__tests__/unit-preference-display-guard.test.ts": 2,
- "__tests__/v1341-locale-parity.test.ts": 1,
"__tests__/v1391-additive-contract.test.ts": 1,
"__tests__/write-rate-limit-guard.test.ts": 1,
- "app/__tests__/dashboard-analytics-split.test.ts": 2,
"app/__tests__/dashboard-chart-reveal.test.tsx": 2,
"app/__tests__/dashboard-suspense-boundaries.test.ts": 2,
"app/__tests__/dashboard-tile-strip-skeleton.test.ts": 4,
@@ -79,7 +77,6 @@
"app/__tests__/not-found-copy.test.ts": 1,
"app/__tests__/quick-add-labels.test.ts": 3,
"app/__tests__/root-loading-silhouette.test.tsx": 1,
- "app/__tests__/sheet-dirty-check.test.ts": 1,
"app/about/page.tsx": 1,
"app/achievements/__tests__/page.test.tsx": 2,
"app/achievements/page.tsx": 4,
@@ -174,9 +171,9 @@
"app/api/auth/refresh/route.ts": 2,
"app/api/auth/register/__tests__/route.test.ts": 5,
"app/api/auth/register/route.ts": 10,
+ "app/api/biomarkers/__tests__/error-contract.test.ts": 1,
"app/api/biomarkers/[id]/__tests__/route.test.ts": 2,
"app/api/biomarkers/[id]/route.ts": 6,
- "app/api/biomarkers/__tests__/error-contract.test.ts": 1,
"app/api/biomarkers/route.ts": 4,
"app/api/c/[token]/unlock/__tests__/route.test.ts": 1,
"app/api/c/[token]/unlock/route.ts": 1,
@@ -186,12 +183,12 @@
"app/api/coach/about-me/questions/__tests__/route-module-gate.test.ts": 1,
"app/api/coach/about-me/questions/route.ts": 3,
"app/api/coach/about-me/route.ts": 4,
- "app/api/coach/plans/[id]/route.ts": 2,
"app/api/coach/plans/__tests__/route.test.ts": 1,
+ "app/api/coach/plans/[id]/route.ts": 2,
"app/api/coach/plans/route.ts": 1,
"app/api/coach/reminder-suggestions/route.ts": 1,
- "app/api/coach/reminders/[id]/route.ts": 1,
"app/api/coach/reminders/__tests__/route.test.ts": 1,
+ "app/api/coach/reminders/[id]/route.ts": 1,
"app/api/coach/reminders/route.ts": 1,
"app/api/coach/suggested-actions/__tests__/route.test.ts": 1,
"app/api/coach/suggested-actions/route.ts": 1,
@@ -199,9 +196,9 @@
"app/api/consent/ai/latest/__tests__/route.test.ts": 3,
"app/api/consent/ai/latest/route.ts": 3,
"app/api/consent/ai/route.ts": 3,
- "app/api/custom-metrics/[id]/entries/[entryId]/route.ts": 2,
"app/api/custom-metrics/[id]/entries/__tests__/note-encryption.test.ts": 1,
"app/api/custom-metrics/[id]/entries/__tests__/soft-delete-restore.test.ts": 1,
+ "app/api/custom-metrics/[id]/entries/[entryId]/route.ts": 2,
"app/api/custom-metrics/[id]/entries/route.ts": 4,
"app/api/custom-metrics/[id]/route.ts": 1,
"app/api/custom-metrics/route.ts": 1,
@@ -211,8 +208,8 @@
"app/api/cycle/day-logs/[id]/route.ts": 4,
"app/api/cycle/day-logs/route.ts": 1,
"app/api/cycle/period/route.ts": 4,
- "app/api/cycle/symptoms/custom/[key]/route.ts": 1,
"app/api/cycle/symptoms/custom/__tests__/custom-crud.test.ts": 1,
+ "app/api/cycle/symptoms/custom/[key]/route.ts": 1,
"app/api/cycle/symptoms/custom/route.ts": 3,
"app/api/dashboard/chart-overlay-prefs/__tests__/route.test.ts": 1,
"app/api/dashboard/chart-overlay-prefs/route.ts": 2,
@@ -222,10 +219,11 @@
"app/api/dashboard/summary/route.ts": 5,
"app/api/dashboard/widgets/__tests__/route.test.ts": 4,
"app/api/dashboard/widgets/route.ts": 14,
+ "app/api/devices/__tests__/route.test.ts": 1,
"app/api/devices/[id]/__tests__/route.test.ts": 1,
"app/api/devices/[id]/route.ts": 1,
- "app/api/devices/__tests__/route.test.ts": 1,
"app/api/devices/route.ts": 4,
+ "app/api/documents/inbound/__tests__/route.test.ts": 1,
"app/api/documents/inbound/[id]/__tests__/route.test.ts": 1,
"app/api/documents/inbound/[id]/chat/__tests__/route.test.ts": 2,
"app/api/documents/inbound/[id]/chat/route.ts": 1,
@@ -240,7 +238,6 @@
"app/api/documents/inbound/[id]/suggest/route.ts": 2,
"app/api/documents/inbound/[id]/summary/route.ts": 2,
"app/api/documents/inbound/[id]/thumbnail/route.ts": 1,
- "app/api/documents/inbound/__tests__/route.test.ts": 1,
"app/api/documents/inbound/reindex/route.ts": 1,
"app/api/documents/inbound/route.ts": 2,
"app/api/documents/inbound/usage/route.ts": 1,
@@ -259,10 +256,10 @@
"app/api/export/mood/route.ts": 2,
"app/api/family-history/[id]/route.ts": 2,
"app/api/family-history/route.ts": 1,
- "app/api/fhir/Patient/$everything/route.ts": 1,
"app/api/fhir/__tests__/observation-series.test.ts": 1,
"app/api/fhir/__tests__/route.test.ts": 1,
"app/api/fhir/metadata/route.ts": 1,
+ "app/api/fhir/Patient/$everything/route.ts": 1,
"app/api/fitbit/callback/route.ts": 1,
"app/api/fitbit/connect/route.ts": 1,
"app/api/fitbit/credentials/route.ts": 1,
@@ -291,11 +288,6 @@
"app/api/insights/breathing-screening/route.ts": 2,
"app/api/insights/cards/__tests__/route.test.ts": 2,
"app/api/insights/cards/route.ts": 8,
- "app/api/insights/chat/[id]/attachments/[documentId]/route.ts": 1,
- "app/api/insights/chat/[id]/attachments/__tests__/route.test.ts": 1,
- "app/api/insights/chat/[id]/attachments/route.ts": 1,
- "app/api/insights/chat/[id]/messages/[messageId]/trail/__tests__/route.test.ts": 1,
- "app/api/insights/chat/[id]/route.ts": 1,
"app/api/insights/chat/__tests__/dialog-harness.ts": 1,
"app/api/insights/chat/__tests__/golden-transcripts.test.ts": 1,
"app/api/insights/chat/__tests__/route-activity.test.ts": 1,
@@ -307,14 +299,19 @@
"app/api/insights/chat/__tests__/route-snapshot-once.test.ts": 2,
"app/api/insights/chat/__tests__/route-tool-mode.test.ts": 4,
"app/api/insights/chat/__tests__/route-workout-scope.test.ts": 1,
+ "app/api/insights/chat/[id]/attachments/__tests__/route.test.ts": 1,
+ "app/api/insights/chat/[id]/attachments/[documentId]/route.ts": 1,
+ "app/api/insights/chat/[id]/attachments/route.ts": 1,
+ "app/api/insights/chat/[id]/messages/[messageId]/trail/__tests__/route.test.ts": 1,
+ "app/api/insights/chat/[id]/route.ts": 1,
"app/api/insights/chat/fenced/__tests__/route.test.ts": 1,
"app/api/insights/chat/fenced/route.ts": 1,
"app/api/insights/chat/messages/[id]/feedback/route.ts": 3,
"app/api/insights/chat/route.ts": 9,
"app/api/insights/coach-read/route.ts": 2,
- "app/api/insights/coach/facts/[id]/route.ts": 2,
"app/api/insights/coach/facts/__tests__/route-memory.test.ts": 1,
"app/api/insights/coach/facts/__tests__/route.test.ts": 1,
+ "app/api/insights/coach/facts/[id]/route.ts": 2,
"app/api/insights/coach/facts/route.ts": 1,
"app/api/insights/coach/nudge-status/route.ts": 1,
"app/api/insights/coach/seen/route.ts": 1,
@@ -325,9 +322,9 @@
"app/api/insights/derived/batch/__tests__/route.test.ts": 3,
"app/api/insights/derived/batch/route.ts": 6,
"app/api/insights/derived/route.ts": 5,
+ "app/api/insights/ecg/__tests__/route.test.ts": 1,
"app/api/insights/ecg/[id]/__tests__/route.test.ts": 1,
"app/api/insights/ecg/[id]/route.ts": 2,
- "app/api/insights/ecg/__tests__/route.test.ts": 1,
"app/api/insights/ecg/route.ts": 1,
"app/api/insights/feedback/route.ts": 2,
"app/api/insights/generate/__tests__/route.test.ts": 11,
@@ -366,8 +363,8 @@
"app/api/internal/deploy-webhook/route.ts": 1,
"app/api/internal/web-vitals/__tests__/route.test.ts": 1,
"app/api/internal/web-vitals/route.ts": 1,
- "app/api/labs/[id]/route.ts": 7,
"app/api/labs/__tests__/error-contract.test.ts": 1,
+ "app/api/labs/[id]/route.ts": 7,
"app/api/labs/ocr/capability/route.ts": 1,
"app/api/labs/ocr/commit/__tests__/route.test.ts": 2,
"app/api/labs/ocr/commit/route.ts": 4,
@@ -387,13 +384,13 @@
"app/api/measurement-reminders/[id]/skip/route.ts": 1,
"app/api/measurement-reminders/[id]/snooze/route.ts": 1,
"app/api/measurement-reminders/route.ts": 3,
- "app/api/measurements/[id]/__tests__/route.test.ts": 2,
- "app/api/measurements/[id]/route.ts": 16,
"app/api/measurements/__tests__/arrival-wiring.test.ts": 1,
"app/api/measurements/__tests__/group-by-day.test.ts": 8,
"app/api/measurements/__tests__/put-duplicate-timestamp.test.ts": 1,
"app/api/measurements/__tests__/range-aggregation-route.test.ts": 3,
"app/api/measurements/__tests__/zod-multi-issue.test.ts": 4,
+ "app/api/measurements/[id]/__tests__/route.test.ts": 2,
+ "app/api/measurements/[id]/route.ts": 16,
"app/api/measurements/batch/__tests__/aggregated-hr-wire.test.ts": 1,
"app/api/measurements/batch/__tests__/duplicate-prefilter.test.ts": 2,
"app/api/measurements/batch/__tests__/hr-bucket-observability.test.ts": 1,
@@ -410,6 +407,7 @@
"app/api/measurements/series-batch/route.ts": 1,
"app/api/measurements/series/__tests__/route.test.ts": 5,
"app/api/measurements/series/route.ts": 6,
+ "app/api/medications/__tests__/route.test.ts": 10,
"app/api/medications/[id]/__tests__/route.test.ts": 8,
"app/api/medications/[id]/api-endpoint/route.ts": 2,
"app/api/medications/[id]/cadence/route.ts": 10,
@@ -421,10 +419,10 @@
"app/api/medications/[id]/dose-history/route.ts": 14,
"app/api/medications/[id]/glp1/__tests__/route.test.ts": 3,
"app/api/medications/[id]/glp1/route.ts": 8,
- "app/api/medications/[id]/intake/[eventId]/__tests__/route.test.ts": 9,
- "app/api/medications/[id]/intake/[eventId]/route.ts": 4,
"app/api/medications/[id]/intake/__tests__/route.test.ts": 11,
"app/api/medications/[id]/intake/__tests__/source-provenance.test.ts": 1,
+ "app/api/medications/[id]/intake/[eventId]/__tests__/route.test.ts": 9,
+ "app/api/medications/[id]/intake/[eventId]/route.ts": 4,
"app/api/medications/[id]/intake/bulk-delete/route.ts": 7,
"app/api/medications/[id]/intake/import/__tests__/route.test.ts": 2,
"app/api/medications/[id]/intake/import/route.ts": 6,
@@ -432,12 +430,11 @@
"app/api/medications/[id]/inventory/[itemId]/route.ts": 8,
"app/api/medications/[id]/inventory/route.ts": 4,
"app/api/medications/[id]/phase-config/route.ts": 4,
- "app/api/medications/[id]/schedule-revisions/[revisionId]/route.ts": 9,
"app/api/medications/[id]/schedule-revisions/__tests__/route.test.ts": 1,
+ "app/api/medications/[id]/schedule-revisions/[revisionId]/route.ts": 9,
"app/api/medications/[id]/schedule-revisions/route.ts": 3,
"app/api/medications/[id]/side-effects/[logId]/route.ts": 2,
"app/api/medications/[id]/side-effects/route.ts": 5,
- "app/api/medications/__tests__/route.test.ts": 10,
"app/api/medications/categories/[key]/route.ts": 1,
"app/api/medications/categories/route.ts": 1,
"app/api/medications/compliance/__tests__/route.test.ts": 2,
@@ -454,9 +451,9 @@
"app/api/medications/route.ts": 2,
"app/api/mental-health/assessments/__tests__/route.test.ts": 2,
"app/api/mental-health/assessments/route.ts": 6,
+ "app/api/mood-entries/__tests__/route.test.ts": 7,
"app/api/mood-entries/[id]/__tests__/route.test.ts": 4,
"app/api/mood-entries/[id]/route.ts": 22,
- "app/api/mood-entries/__tests__/route.test.ts": 7,
"app/api/mood-entries/bulk-delete/__tests__/route.test.ts": 1,
"app/api/mood-entries/bulk-delete/route.ts": 4,
"app/api/mood-entries/bulk/__tests__/route.test.ts": 5,
@@ -546,12 +543,12 @@
"app/api/withings/connect/__tests__/route.test.ts": 1,
"app/api/withings/connect/route.ts": 3,
"app/api/withings/status/route.ts": 1,
- "app/api/withings/webhook/[token]/__tests__/route.test.ts": 1,
"app/api/withings/webhook/__tests__/route.test.ts": 1,
+ "app/api/withings/webhook/[token]/__tests__/route.test.ts": 1,
"app/api/withings/webhook/route.ts": 2,
- "app/api/workouts/[id]/route.ts": 2,
"app/api/workouts/__tests__/batch-create.test.ts": 3,
"app/api/workouts/__tests__/canonical-dedup.test.ts": 3,
+ "app/api/workouts/[id]/route.ts": 2,
"app/api/workouts/batch/route.ts": 3,
"app/api/workouts/route.ts": 2,
"app/auth/claim/page.tsx": 1,
@@ -653,20 +650,19 @@
"app/loading.tsx": 2,
"app/mcp/__tests__/route.test.ts": 5,
"app/measurements/page.tsx": 5,
- "app/medications/[id]/history/page.tsx": 1,
- "app/medications/[id]/page.tsx": 2,
"app/medications/__tests__/category-filter.test.tsx": 2,
- "app/medications/__tests__/detail-page-history-surface.test.ts": 1,
"app/medications/__tests__/list-view-toggle.test.tsx": 5,
"app/medications/__tests__/page-prefetch.test.tsx": 1,
"app/medications/__tests__/take-all-due-affordance.test.tsx": 1,
+ "app/medications/[id]/history/page.tsx": 1,
+ "app/medications/[id]/page.tsx": 2,
"app/medications/new/page.tsx": 1,
"app/medications/page-client.tsx": 50,
"app/mental-wellbeing/page.tsx": 1,
"app/mood/page-client.tsx": 2,
"app/not-found.tsx": 2,
- "app/onboarding/[step]/page.tsx": 1,
"app/onboarding/__tests__/page.test.tsx": 1,
+ "app/onboarding/[step]/page.tsx": 1,
"app/onboarding/handover/page.tsx": 1,
"app/onboarding/page.tsx": 2,
"app/privacy/__tests__/page.test.tsx": 1,
@@ -735,7 +731,6 @@
"components/charts/__tests__/health-chart-polish.test.tsx": 2,
"components/charts/__tests__/health-chart-preloaded-coverage.test.tsx": 1,
"components/charts/__tests__/health-chart-range.test.tsx": 3,
- "components/charts/__tests__/health-chart-target-link-removed.test.ts": 1,
"components/charts/__tests__/health-chart-vertical-markers.test.ts": 1,
"components/charts/__tests__/medication-chart-polish.test.tsx": 1,
"components/charts/__tests__/medication-compliance-chart-empty-state-gate.test.tsx": 1,
@@ -807,7 +802,7 @@
"components/dashboard/chart-reveal.tsx": 1,
"components/dashboard/dashboard-gates.ts": 9,
"components/dashboard/medication-intake-quick-add.tsx": 11,
- "components/dashboard/quick-entry-sheets.tsx": 6,
+ "components/dashboard/quick-entry-sheets.tsx": 3,
"components/dashboard/range-display.tsx": 1,
"components/dashboard/recent-workouts-tile.tsx": 3,
"components/data-list/__tests__/selection.test.ts": 1,
@@ -928,7 +923,7 @@
"components/insights/coach-panel/guided-questions-card.tsx": 1,
"components/insights/coach-panel/guided-questions-machine.ts": 2,
"components/insights/coach-panel/memory-note.tsx": 1,
- "components/insights/coach-panel/message-thread.tsx": 31,
+ "components/insights/coach-panel/message-thread.tsx": 28,
"components/insights/coach-panel/method-line.tsx": 1,
"components/insights/coach-panel/mobile-rail-tray.tsx": 2,
"components/insights/coach-panel/read-aloud.tsx": 8,
@@ -985,7 +980,7 @@
"components/insights/hero-strip.tsx": 3,
"components/insights/insight-status-card.tsx": 12,
"components/insights/insights-edit-mode.tsx": 5,
- "components/insights/insights-layout-shell.tsx": 11,
+ "components/insights/insights-layout-shell.tsx": 3,
"components/insights/insights-tab-strip.tsx": 35,
"components/insights/intraday-pulse-chart.tsx": 1,
"components/insights/labs-changes-card.tsx": 1,
@@ -1001,7 +996,6 @@
"components/insights/mood/__tests__/mood-discovered-relations.test.tsx": 3,
"components/insights/mood/__tests__/mood-insights-sections-in-target.test.tsx": 1,
"components/insights/mood/__tests__/mood-insights-sections-reveal.test.tsx": 1,
- "components/insights/mood/__tests__/mood-notes-timeline-removed.test.ts": 4,
"components/insights/mood/__tests__/mood-tag-bars-muted.test.tsx": 1,
"components/insights/mood/mood-better-days.tsx": 2,
"components/insights/mood/mood-correlation-cards.tsx": 3,
@@ -1094,7 +1088,7 @@
"components/layout/account-switcher-menu.tsx": 1,
"components/layout/auth-shell.tsx": 13,
"components/layout/bottom-nav.tsx": 8,
- "components/layout/capture-picker.tsx": 5,
+ "components/layout/capture-picker.tsx": 3,
"components/layout/nav-model.ts": 17,
"components/layout/record-session-fence-bridge.tsx": 1,
"components/layout/shared-record-banner.tsx": 1,
@@ -1125,7 +1119,7 @@
"components/medications/__tests__/inventory-section-actions.test.tsx": 1,
"components/medications/__tests__/inventory-section-summary.test.tsx": 2,
"components/medications/__tests__/log-injection-site-dialog.test.tsx": 1,
- "components/medications/__tests__/medication-card-glp1.test.tsx": 14,
+ "components/medications/__tests__/medication-card-glp1.test.tsx": 12,
"components/medications/__tests__/medication-card-header.test.tsx": 1,
"components/medications/__tests__/medication-card-next-due.test.tsx": 4,
"components/medications/__tests__/medication-card-symmetry.test.tsx": 4,
@@ -1162,7 +1156,7 @@
"components/medications/log-injection-site-dialog.tsx": 2,
"components/medications/log-intake-dialog.tsx": 4,
"components/medications/medication-card-header.tsx": 5,
- "components/medications/medication-card-menu.tsx": 3,
+ "components/medications/medication-card-menu.tsx": 2,
"components/medications/medication-card.tsx": 37,
"components/medications/medication-categories-sheet.tsx": 1,
"components/medications/medication-category-add-row.tsx": 1,
@@ -1793,7 +1787,6 @@
"lib/analytics/__tests__/effective-range.test.ts": 1,
"lib/analytics/__tests__/glucose-panel-coherence.test.ts": 1,
"lib/analytics/__tests__/glucose-panel-window.test.ts": 2,
- "lib/analytics/__tests__/merge-slim-thick.test.ts": 3,
"lib/analytics/__tests__/mood-series.test.ts": 1,
"lib/analytics/__tests__/range-delta.test.ts": 1,
"lib/analytics/__tests__/resting-pulse.test.ts": 1,
@@ -1826,7 +1819,6 @@
"lib/analytics/correlations.ts": 1,
"lib/analytics/intraday-pulse-io.ts": 2,
"lib/analytics/intraday-pulse.ts": 3,
- "lib/analytics/merge-slim-thick.ts": 2,
"lib/analytics/mood-series.ts": 2,
"lib/analytics/range-delta.ts": 1,
"lib/analytics/range-shared.ts": 1,
@@ -1931,7 +1923,6 @@
"lib/dashboard/__tests__/meds-today.test.ts": 1,
"lib/dashboard/__tests__/score-narrative.test.ts": 1,
"lib/dashboard/__tests__/score-rings.test.ts": 1,
- "lib/dashboard/__tests__/snapshot-flag.test.ts": 1,
"lib/dashboard/__tests__/snapshot.test.ts": 15,
"lib/dashboard/__tests__/target-bands.test.ts": 2,
"lib/dashboard/bands.ts": 1,
@@ -2318,7 +2309,7 @@
"lib/jobs/lab-biomarker-backfill.ts": 2,
"lib/jobs/mean-consolidation.ts": 1,
"lib/jobs/measurement-fold-repair.ts": 1,
- "lib/jobs/measurement-reminder.ts": 14,
+ "lib/jobs/measurement-reminder.ts": 13,
"lib/jobs/med-notes-encryption-backfill.ts": 2,
"lib/jobs/medication-intake-import.ts": 1,
"lib/jobs/medication-inventory-expire.ts": 1,
@@ -2365,7 +2356,6 @@
"lib/labs/__tests__/note-update.test.ts": 1,
"lib/labs/biomarker-catalog.ts": 10,
"lib/labs/biomarker-store.ts": 2,
- "lib/labs/homa-ir.ts": 1,
"lib/labs/lab-trend.ts": 1,
"lib/labs/local-ocr.ts": 1,
"lib/labs/note-update.ts": 1,
@@ -2683,7 +2673,7 @@
"lib/queries/use-profile-claim.ts": 4,
"lib/query-keys/admin.ts": 12,
"lib/query-keys/ai-runs.ts": 1,
- "lib/query-keys/auth.ts": 9,
+ "lib/query-keys/auth.ts": 8,
"lib/query-keys/coach.ts": 13,
"lib/query-keys/custom-metrics.ts": 1,
"lib/query-keys/cycle.ts": 1,
@@ -2760,11 +2750,9 @@
"lib/sync/cursor.ts": 1,
"lib/targets/__tests__/builders.test.ts": 1,
"lib/targets/__tests__/glucose-targets.test.ts": 1,
- "lib/targets/__tests__/mood-stability-label.test.ts": 1,
"lib/targets/__tests__/source-link.test.ts": 1,
"lib/targets/__tests__/target-unit-display.test.ts": 1,
"lib/targets/build-response.ts": 3,
- "lib/targets/mood-stability-label.ts": 1,
"lib/targets/target-unit-display.ts": 2,
"lib/targets/types.ts": 2,
"lib/targets/vitals-builder.ts": 2,
diff --git a/src/__tests__/client-poll-inventory-guard.test.ts b/src/__tests__/client-poll-inventory-guard.test.ts
index 48d16b993..95d17ae01 100644
--- a/src/__tests__/client-poll-inventory-guard.test.ts
+++ b/src/__tests__/client-poll-inventory-guard.test.ts
@@ -136,11 +136,6 @@ const INVENTORY: Readonly> = {
cadence:
"25 s, nextAdvisorPollInterval, attempts from the response timeout so the warm can land (advisor); 10 while settling",
},
- "components/insights/insights-layout-shell.tsx": {
- sites: 1,
- shape: "bounded",
- cadence: "25 s, nextAdvisorPollInterval, 10 attempts (briefing body)",
- },
};
const SITE = /\brefetchInterval\s*:/g;
diff --git a/src/__tests__/compose-env-whitelist-guard.test.ts b/src/__tests__/compose-env-whitelist-guard.test.ts
index 89f9a534e..676aa37f2 100644
--- a/src/__tests__/compose-env-whitelist-guard.test.ts
+++ b/src/__tests__/compose-env-whitelist-guard.test.ts
@@ -136,10 +136,6 @@ const NOT_OPERATOR_CONFIGURABLE: Readonly> = {
"CI-only knob for the same nightly eval workflow",
HEALTHLOG_MCP_TOKEN:
"read by the local stdio MCP bridge the operator runs on their own machine; the production image strips tsx so it cannot run there",
-
- // --- A whitelist entry would be obeyed by half the readers. ---
- NEXT_PUBLIC_DASHBOARD_SNAPSHOT:
- "NEXT_PUBLIC_* is inlined into the client bundle at build time and three of its four readers are client components, so a runtime value would steer the server half only — a split-brain worse than no knob. .env.production.example already documents it as a build-time toggle and says it is deliberately absent from the whitelist; adding it would make that note false.",
};
function sourceFiles(): string[] {
diff --git a/src/__tests__/customize-shortcut-guard.test.ts b/src/__tests__/customize-shortcut-guard.test.ts
index 4fe9624ea..1b5e4e1a6 100644
--- a/src/__tests__/customize-shortcut-guard.test.ts
+++ b/src/__tests__/customize-shortcut-guard.test.ts
@@ -215,7 +215,7 @@ describe("the customise shortcut is withheld inside somebody else's record", ()
export function Page() {
const { inSharedRecord } = useRecordCapabilities();
const wrench = !inSharedRecord ? (
- Customise
+ Customise
) : null;
return wrench;
}
diff --git a/src/lib/jobs/queue-runtime.ts b/src/__tests__/helpers/queue-runtime.ts
similarity index 100%
rename from src/lib/jobs/queue-runtime.ts
rename to src/__tests__/helpers/queue-runtime.ts
diff --git a/src/__tests__/job-queue-runtime-guard.test.ts b/src/__tests__/job-queue-runtime-guard.test.ts
index cfc4de732..854c980e0 100644
--- a/src/__tests__/job-queue-runtime-guard.test.ts
+++ b/src/__tests__/job-queue-runtime-guard.test.ts
@@ -8,7 +8,7 @@
* so could every other job that legitimately needs longer: the nightly status
* passes, the pre-generation, the record scan, the key rotation, the backups.
*
- * `src/lib/jobs/queue-runtime.ts` declares every queue `short`, `budgeted` or
+ * `helpers/queue-runtime.ts` declares every queue `short`, `budgeted` or
* `long`. This guard holds the tree to it:
*
* - every queue the worker registry declares (`jobs/registry/`) is in the
@@ -41,7 +41,7 @@ import {
DEFAULT_JOB_EXPIRE_SECONDS,
QUEUE_RUNTIME,
type QueueRuntime,
-} from "@/lib/jobs/queue-runtime";
+} from "./helpers/queue-runtime";
import { SCHEDULED_AND_SENT_QUEUES } from "@/lib/jobs/reminder/registrar-shared";
import { HEAVY_QUEUES } from "@/lib/jobs/heavy-lane";
import type { JobDefinition } from "@/lib/jobs/registry/define-job";
diff --git a/src/__tests__/labs-module-ai-gate-guard.test.ts b/src/__tests__/labs-module-ai-gate-guard.test.ts
index 70ca38c36..eaa74e844 100644
--- a/src/__tests__/labs-module-ai-gate-guard.test.ts
+++ b/src/__tests__/labs-module-ai-gate-guard.test.ts
@@ -22,8 +22,7 @@
* that file does, or is listed below by file and function with the reason
* its rows cannot reach a prompt. A new reader fails here until it does one
* of those. The data routes are not in scope: a module decides what a model
- * is given, not what the owner can read (`CLAUDE.md`, "An AI capability
- * gates model work").
+ * is given, not what the owner can read.
*
* The check is per read, not per file. It used to ask whether a file that
* reads the table mentions the gate anywhere, so one ungated read in a file
diff --git a/src/__tests__/moodlog-removal-guard.test.ts b/src/__tests__/moodlog-removal-guard.test.ts
deleted file mode 100644
index c012aed0f..000000000
--- a/src/__tests__/moodlog-removal-guard.test.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-/**
- * The moodLog bridge stays removed.
- *
- * It was retired once before, in pieces: the Settings card went in June, the
- * cron, webhook, credentials, ledger entry and roughly twenty i18n keys stayed
- * behind, and for a month the code ran hourly against an upstream that had
- * stopped answering while nothing on any screen said so. Half-removed was
- * worse than either finished state.
- *
- * This test pins the finished state. It is a structural guard, not a
- * behavioural one: it fails the moment a moodLog surface reappears, so a
- * reintroduction has to be a decision someone writes down rather than a merge
- * that slips through.
- *
- * What it deliberately does NOT forbid: the `MOODLOG` mood-entry source value
- * and its label. Rows the bridge imported are the user's own history, they
- * keep their provenance, and every read path must go on resolving it.
- */
-import { readFileSync, existsSync } from "node:fs";
-import path from "node:path";
-
-import { allJobDefinitions } from "@/lib/jobs/registry/index";
-import { describe, expect, it } from "vitest";
-
-import { INTEGRATION_CADENCE } from "@/lib/integrations/sync-verdict";
-import { INTEGRATION_DISPLAY_NAMES } from "@/components/settings/integrations/integration-fallback-row";
-
-const ROOT = path.resolve(__dirname, "../..");
-
-const LOCALES = [
- "de",
- "en",
- "es",
- "fr",
- "it",
- "pl",
- "ko",
- "nl",
- "pt-BR",
-] as const;
-
-/** Every path the retired bridge occupied. */
-const REMOVED_PATHS = [
- "src/lib/moodlog",
- "src/lib/moodlog-secret.ts",
- "src/lib/jobs/reminder/moodlog-sync.ts",
- "src/app/api/integrations/moodlog",
- "src/app/api/settings/moodlog",
- "src/lib/validations/moodlog.ts",
-];
-
-/** Columns migration 0271 dropped. */
-const REMOVED_SCHEMA_FIELDS = [
- "moodLogUrlEncrypted",
- "moodLogApiKeyEncrypted",
- "moodLogEnabled",
- "moodLogLastSyncedAt",
- "moodLogWebhookSecret",
- "moodLogGlobal",
-];
-
-describe("moodLog integration stays removed", () => {
- it("has no moodLog module, route or validation file", () => {
- for (const rel of REMOVED_PATHS) {
- expect(existsSync(path.join(ROOT, rel)), `${rel} is back`).toBe(false);
- }
- });
-
- it("declares no moodLog column on the Prisma schema", () => {
- const schema = readFileSync(
- path.join(ROOT, "prisma/schema.prisma"),
- "utf8",
- );
- for (const field of REMOVED_SCHEMA_FIELDS) {
- expect(schema, `${field} is back on the schema`).not.toContain(field);
- }
- });
-
- it("registers no moodlog queue, cron or handler binding", () => {
- const queues = allJobDefinitions().map((job) => job.queue);
- expect(queues.length).toBeGreaterThan(100);
- expect(queues.filter((queue) => /mood-?log/i.test(queue))).toEqual([]);
- });
-
- it("carries no moodlog integration key on the status envelope", () => {
- expect(Object.keys(INTEGRATION_CADENCE)).not.toContain("moodlog");
- expect(Object.keys(INTEGRATION_DISPLAY_NAMES)).not.toContain("moodlog");
- });
-
- it("keeps no orphaned moodLog message key in any locale", () => {
- for (const locale of LOCALES) {
- const bundle = JSON.parse(
- readFileSync(path.join(ROOT, `messages/${locale}.json`), "utf8"),
- ) as {
- settings: Record;
- admin: Record;
- };
- const orphans = [
- ...Object.keys(bundle.settings),
- ...Object.keys(bundle.admin),
- ].filter((key) => key.startsWith("moodLog"));
- expect(orphans, `${locale} still carries moodLog keys`).toEqual([]);
- }
- });
-
- it("still resolves the MOODLOG provenance of imported mood rows", () => {
- // The other half of the contract: removing the bridge must not erase the
- // history it wrote. The source badge label has to survive the removal.
- const en = JSON.parse(
- readFileSync(path.join(ROOT, "messages/en.json"), "utf8"),
- ) as { mood: Record };
- expect(en.mood.sourceMoodlog).toBeTruthy();
- });
-});
diff --git a/src/__tests__/openapi-route-coverage-guard.test.ts b/src/__tests__/openapi-route-coverage-guard.test.ts
index bafc8d560..6df4b68ff 100644
--- a/src/__tests__/openapi-route-coverage-guard.test.ts
+++ b/src/__tests__/openapi-route-coverage-guard.test.ts
@@ -339,7 +339,6 @@ const UNPUBLISHED: Readonly> = {
"/api/internal/web-vitals": { kind: "internalOps", methods: ["POST"] },
"/api/monitoring/csp-report": { kind: "internalOps", methods: ["POST"] },
"/api/monitoring/glitchtip": { kind: "internalOps", methods: ["POST"] },
- "/api/monitoring/settings": { kind: "internalOps", methods: ["GET"] },
"/api/monitoring/umami-script": { kind: "internalOps", methods: ["GET"] },
"/api/monitoring/umami/{path}": { kind: "internalOps", methods: ["POST"] },
"/api/mcp/oauth/authorize": {
diff --git a/src/__tests__/pwa-mobile-shell-guard.test.ts b/src/__tests__/pwa-mobile-shell-guard.test.ts
index 74da13d74..463402abd 100644
--- a/src/__tests__/pwa-mobile-shell-guard.test.ts
+++ b/src/__tests__/pwa-mobile-shell-guard.test.ts
@@ -9,17 +9,21 @@
* 5 px content box and pushed the logo across its bottom border).
* 2. The stylesheet carries the rules the shell and the keyboard bridge
* depend on.
- * 3. Every form-field primitive keeps 16 px type on a touch screen: iOS
- * Safari zooms the page into a focused field below 16 px, and a width
+ * 3. Every form field keeps 16 px type on a touch screen: iOS Safari
+ * zooms the page into a focused field below 16 px, and a width
* breakpoint alone (`md:text-sm`) handed 14 px to a phone held sideways
* and to every iPad. The small size is allowed only behind
- * `pointer-fine:`.
+ * `pointer-fine:`. That holds for the primitives and for every call
+ * site, whose `text-sm` replaces the primitive's size, and for a raw
+ * `