` 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/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/lefthook.yml b/lefthook.yml
index d193d5b25..77a797709 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -16,13 +16,7 @@
# 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.
+# Reject tool attribution trailers.
commit-msg:
commands:
no-tooling-trailer:
diff --git a/messages/de.json b/messages/de.json
index 7e1b58038..735e7e288 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -774,7 +774,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",
@@ -1518,7 +1518,6 @@
"glp1NextInjectionToday": "heute",
"glp1NextInjectionTomorrow": "morgen",
"glp1RotationSuggested": "Empfohlen: {site}",
- "glp1LogSideEffect": "Nebenwirkung loggen",
"sideEffects": {
"section": "Nebenwirkungen",
"addCta": "Erfassen",
@@ -5680,7 +5679,7 @@
"title": "Krankheitstagebuch",
"description": "Listenansicht und Reihenfolge der Episoden."
},
- "vorsorge": {
+ "checkups": {
"title": "Vorsorge",
"description": "Listenansicht und Reihenfolge der Erinnerungen."
},
@@ -6105,7 +6104,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."
},
@@ -10210,6 +10209,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",
@@ -11054,7 +11054,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",
@@ -11143,14 +11143,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 e9686526d..b20c38494 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -1518,7 +1518,6 @@
"glp1NextInjectionToday": "today",
"glp1NextInjectionTomorrow": "tomorrow",
"glp1RotationSuggested": "Recommended next: {site}",
- "glp1LogSideEffect": "Log side effect",
"sideEffects": {
"section": "Side effects",
"addCta": "Log",
@@ -5680,7 +5679,7 @@
"title": "Illness journal",
"description": "List view and episode order."
},
- "vorsorge": {
+ "checkups": {
"title": "Checkups",
"description": "List view and reminder order."
},
@@ -6105,7 +6104,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."
},
@@ -10210,6 +10209,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 877ccf275..26b179ae2 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -1518,7 +1518,6 @@
"glp1NextInjectionToday": "hoy",
"glp1NextInjectionTomorrow": "mañana",
"glp1RotationSuggested": "Sugerido: {site}",
- "glp1LogSideEffect": "Registrar efecto secundario",
"sideEffects": {
"section": "Efectos secundarios",
"addCta": "Registrar",
@@ -1712,7 +1711,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"
},
@@ -5680,7 +5679,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."
},
@@ -6105,7 +6104,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."
},
@@ -10210,6 +10209,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",
@@ -11054,7 +11054,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",
@@ -11143,14 +11143,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 e0cec9458..c37e70b49 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -1518,7 +1518,6 @@
"glp1NextInjectionToday": "aujourd’hui",
"glp1NextInjectionTomorrow": "demain",
"glp1RotationSuggested": "Conseillé : {site}",
- "glp1LogSideEffect": "Consigner un effet secondaire",
"sideEffects": {
"section": "Effets secondaires",
"addCta": "Consigner",
@@ -1712,7 +1711,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"
},
@@ -5680,7 +5679,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."
},
@@ -6105,7 +6104,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."
},
@@ -10210,6 +10209,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",
@@ -11054,7 +11054,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",
@@ -11143,14 +11143,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 e4a3c7425..e18251ca4 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -1518,7 +1518,6 @@
"glp1NextInjectionToday": "oggi",
"glp1NextInjectionTomorrow": "domani",
"glp1RotationSuggested": "Suggerita: {site}",
- "glp1LogSideEffect": "Registra effetto collaterale",
"sideEffects": {
"section": "Effetti collaterali",
"addCta": "Registra",
@@ -1712,7 +1711,7 @@
"step1": {
"title": "Come si chiama il farmaco?",
"subline": "Esattamente come riportato sulla confezione.",
- "label": "Name",
+ "label": "Nome",
"placeholder": "ad es. Ramipril",
"short": "Nome"
},
@@ -5680,7 +5679,7 @@
"title": "Diario delle malattie",
"description": "Vista elenco e ordine degli episodi."
},
- "vorsorge": {
+ "checkups": {
"title": "Controlli",
"description": "Vista elenco e ordine dei promemoria."
},
@@ -6105,7 +6104,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."
},
@@ -10210,6 +10209,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",
@@ -11054,7 +11054,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",
@@ -11143,14 +11143,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 84a15bb40..84ad9f739 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -1518,7 +1518,6 @@
"glp1NextInjectionToday": "오늘",
"glp1NextInjectionTomorrow": "내일",
"glp1RotationSuggested": "다음 추천 부위: {site}",
- "glp1LogSideEffect": "부작용 기록",
"sideEffects": {
"section": "부작용",
"addCta": "기록",
@@ -5680,7 +5679,7 @@
"title": "아픈 날 일지",
"description": "목록 보기와 에피소드 순서."
},
- "vorsorge": {
+ "checkups": {
"title": "건강검진",
"description": "목록 보기와 알림 순서."
},
@@ -6105,7 +6104,7 @@
"moduleSettingsDescription": "순서와 보기 외의 설정은 모듈의 페이지에 있습니다."
}
},
- "gesundheitsakte": {
+ "health-record": {
"title": "건강 기록",
"subtitle": "임상의에게 바로 건넬 수 있는 완전한 건강 기록을 내보내요."
},
@@ -10210,6 +10209,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 feadb4d7a..a4aebd961 100644
--- a/messages/nl.json
+++ b/messages/nl.json
@@ -1518,7 +1518,6 @@
"glp1NextInjectionToday": "vandaag",
"glp1NextInjectionTomorrow": "morgen",
"glp1RotationSuggested": "Volgende aanbevolen plek: {site}",
- "glp1LogSideEffect": "Bijwerking vastleggen",
"sideEffects": {
"section": "Bijwerkingen",
"addCta": "Vastleggen",
@@ -5680,7 +5679,7 @@
"title": "Ziektedagboek",
"description": "Lijstweergave en volgorde van episodes."
},
- "vorsorge": {
+ "checkups": {
"title": "Preventieve controles",
"description": "Lijstweergave en volgorde van herinneringen."
},
@@ -6105,7 +6104,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."
},
@@ -10210,6 +10209,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 a2bb28063..15934f55d 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -1518,7 +1518,6 @@
"glp1NextInjectionToday": "dziś",
"glp1NextInjectionTomorrow": "jutro",
"glp1RotationSuggested": "Sugerowane: {site}",
- "glp1LogSideEffect": "Zapisz działanie niepożądane",
"sideEffects": {
"section": "Skutki uboczne",
"addCta": "Dodaj",
@@ -1712,7 +1711,7 @@
"step1": {
"title": "Jak nazywa się lek?",
"subline": "Dokładnie tak, jak na opakowaniu.",
- "label": "Name",
+ "label": "Nazwa",
"placeholder": "np. Ramipryl",
"short": "Nazwa"
},
@@ -5680,7 +5679,7 @@
"title": "Dziennik chorób",
"description": "Widok listy i kolejność epizodów."
},
- "vorsorge": {
+ "checkups": {
"title": "Badania kontrolne",
"description": "Widok listy i kolejność przypomnień."
},
@@ -6105,7 +6104,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."
},
@@ -10210,6 +10209,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",
@@ -11054,7 +11054,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ść",
@@ -11143,14 +11143,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 412d3efb4..6d9069711 100644
--- a/messages/pt-BR.json
+++ b/messages/pt-BR.json
@@ -1518,7 +1518,6 @@
"glp1NextInjectionToday": "hoje",
"glp1NextInjectionTomorrow": "amanhã",
"glp1RotationSuggested": "Próximo recomendado: {site}",
- "glp1LogSideEffect": "Registrar efeito colateral",
"sideEffects": {
"section": "Efeitos colaterais",
"addCta": "Registrar",
@@ -5680,7 +5679,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."
},
@@ -6105,7 +6104,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."
},
@@ -10210,6 +10209,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 9db3347b8..f260a981f 100644
--- a/messages/ru.json
+++ b/messages/ru.json
@@ -1518,7 +1518,6 @@
"glp1NextInjectionToday": "сегодня",
"glp1NextInjectionTomorrow": "завтра",
"glp1RotationSuggested": "Рекомендуется следующее: {site}",
- "glp1LogSideEffect": "Записать побочный эффект",
"sideEffects": {
"section": "Побочные эффекты",
"addCta": "Записать",
@@ -5680,7 +5679,7 @@
"title": "Дневник болезней",
"description": "Вид списка и порядок эпизодов."
},
- "vorsorge": {
+ "checkups": {
"title": "Профилактика",
"description": "Вид списка и порядок напоминаний."
},
@@ -6105,7 +6104,7 @@
"moduleSettingsDescription": "Всё, кроме порядка и вида, настраивается на странице модуля в разделе «Модули»."
}
},
- "gesundheitsakte": {
+ "health-record": {
"title": "Медицинская карта",
"subtitle": "Экспортируйте полную медицинскую карту, готовую для врача."
},
@@ -10210,6 +10209,7 @@
"BLOOD_PRESSURE_SYS": "АД",
"PULSE": "Пульс",
"BLOOD_GLUCOSE": "Глюкоза крови",
+ "BLOOD_KETONES": "Кетоны крови",
"OXYGEN_SATURATION": "Насыщение кислородом",
"BODY_FAT": "Процент жировой массы",
"BODY_TEMPERATURE": "Температура тела",
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/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/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-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 73a08bc66..97e043989 100644
--- a/src/__tests__/baselines/source-file-lines.json
+++ b/src/__tests__/baselines/source-file-lines.json
@@ -1,7 +1,7 @@
{
"$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
"count": 84,
- "total": 93830,
+ "total": 93802,
"entries": {
"app/medications/page-client.tsx": 839,
"components/charts/mood-chart.tsx": 1166,
@@ -13,7 +13,7 @@
"components/insights/health-score-card.tsx": 1023,
"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 598e55bff..246125b4a 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": 109328,
+ "total": 109312,
"entries": {
"app/achievements/page.tsx :: AchievementsPage": 206,
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 275,
@@ -145,10 +145,10 @@
"components/measurement-reminders/vorsorge-section.tsx :: VorsorgeSection": 352,
"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,
diff --git a/src/__tests__/baselines/test-as-never.json b/src/__tests__/baselines/test-as-never.json
index ef945bea3..f6803f372 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": 523,
- "total": 5459,
+ "total": 5458,
"entries": {
"__tests__/api/auth/login/mfa-branch.test.ts": 10,
"__tests__/api/auth/mfa/verify.test.ts": 9,
@@ -496,7 +496,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 ff3077a13..32735d50b 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": 337,
- "total": 337,
+ "count": 332,
+ "total": 332,
"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 13cee355d..bd8617a0b 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": 2883,
- "total": 9186,
+ "count": 2876,
+ "total": 9172,
"entries": {
"__tests__/account-payload-consumer-guard.test.ts": 2,
"__tests__/acting-account-boundary-guard.test.ts": 7,
@@ -68,7 +68,6 @@
"__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,
@@ -654,7 +653,6 @@
"app/mcp/__tests__/route.test.ts": 5,
"app/measurements/page.tsx": 5,
"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,
@@ -735,7 +733,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,
@@ -1001,7 +998,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,
@@ -1127,7 +1123,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,
@@ -1164,7 +1160,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,
@@ -2367,7 +2363,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,
@@ -2691,7 +2686,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,
@@ -2768,11 +2763,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__/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__/session-surface-guard.test.ts b/src/__tests__/session-surface-guard.test.ts
index d338495d4..e4c195931 100644
--- a/src/__tests__/session-surface-guard.test.ts
+++ b/src/__tests__/session-surface-guard.test.ts
@@ -43,7 +43,7 @@ const SRC = join(process.cwd(), "src");
* Every non-test `.ts` / `.tsx` under `src/app/`, which is the whole of the
* request-handling tree: API routes, RSC pages, layouts, route handlers.
* The generated Prisma client is excluded by construction (it is not under
- * `app/`) and never read — see CLAUDE.md.
+ * `app/`) and never read.
*/
function appFiles(): string[] {
return walkSourceFiles(SRC, { floor: 3000 })
diff --git a/src/__tests__/settings-legacy-routes.test.ts b/src/__tests__/settings-legacy-routes.test.ts
index 870b11045..1c764f5b5 100644
--- a/src/__tests__/settings-legacy-routes.test.ts
+++ b/src/__tests__/settings-legacy-routes.test.ts
@@ -50,6 +50,7 @@ const HISTORICAL_SETTINGS_PATHS: readonly string[] = [
"/settings/api",
"/settings/mcp",
"/settings/gesundheitsakte",
+ "/settings/health-record",
"/settings/sharing",
"/settings/export",
"/settings/advanced",
@@ -72,6 +73,7 @@ const HISTORICAL_SETTINGS_PATHS: readonly string[] = [
"/settings/layout/illness",
"/settings/layout/documents",
"/settings/layout/vorsorge",
+ "/settings/layout/checkups",
];
interface Redirect {
@@ -151,6 +153,22 @@ describe("historical Settings addresses", () => {
expect(unlisted).toEqual([]);
});
+ it("sends the old German addresses to their English ones", async () => {
+ const redirects = await settingsRedirects();
+ expect(resolve("/settings/gesundheitsakte", redirects)).toBe(
+ "/settings/health-record",
+ );
+ expect(resolve("/settings/sharing", redirects)).toBe(
+ "/settings/health-record",
+ );
+ expect(resolve("/settings/layout/vorsorge", redirects)).toBe(
+ "/settings/layout/checkups",
+ );
+ expect(resolve("/settings/vorsorge", redirects)).toBe(
+ "/settings/layout/checkups",
+ );
+ });
+
it("keeps the v1.44 moves addressable", async () => {
const redirects = await settingsRedirects();
expect(resolve("/settings/layout/mood", redirects)).toBe(
diff --git a/src/__tests__/sharing-surface-guard.test.ts b/src/__tests__/sharing-surface-guard.test.ts
index 4c5c12a33..d86d5e959 100644
--- a/src/__tests__/sharing-surface-guard.test.ts
+++ b/src/__tests__/sharing-surface-guard.test.ts
@@ -161,7 +161,7 @@ const BARE_REQUIRE_AUTH_FLOOR = 200;
* omits the three routes under `src/app/.well-known/` — three request handlers
* that a glob-based sweep reports as if they did not exist. Leg (a) pins one of
* them by name so this cannot quietly regress. Names only; nothing under
- * `generated/` is ever read (CLAUDE.md).
+ * `generated/` is ever read.
*/
function allPaths(): string[] {
return readdirSync(SRC, { recursive: true })
diff --git a/src/__tests__/v1341-locale-parity.test.ts b/src/__tests__/v1341-locale-parity.test.ts
deleted file mode 100644
index 4feaed2a2..000000000
--- a/src/__tests__/v1341-locale-parity.test.ts
+++ /dev/null
@@ -1,187 +0,0 @@
-import { readFileSync } from "node:fs";
-import { join } from "node:path";
-
-import { describe, expect, it } from "vitest";
-
-const ROOT = join(__dirname, "../..");
-const LOCALES = [
- "en",
- "de",
- "es",
- "fr",
- "it",
- "pl",
- "ko",
- "nl",
- "pt-BR",
-] as const;
-
-const REQUIRED_KEYS = [
- // Security: adding a passkey now requires an existing sign-in factor.
- "settings.passkeyReauth.title",
- "settings.passkeyReauth.description",
- "settings.passkeyReauth.methodLabel",
- "settings.passkeyReauth.methods.password",
- "settings.passkeyReauth.methods.totp",
- "settings.passkeyReauth.methods.passkey",
- "settings.passkeyReauth.methods.webauthn",
- "settings.passkeyReauth.currentPassword",
- "settings.passkeyReauth.authenticatorCode",
- "settings.passkeyReauth.authenticatorPlaceholder",
- "settings.passkeyReauth.verifyAndAdd",
- // Integrations/imports: operator policy and Apple ECG terminal outcome.
- "settings.nightscoutPrivateOperatorTitle",
- "settings.nightscoutPrivateOperatorDescription",
- "settings.sections.export.import.appleHealth.ecgResult",
- // Existing terminal contracts reused by the final v1.34.1 call sites.
- "insights.assessmentPreparing",
- "insights.noAnalysisYet",
- "common.loadFailed",
- "common.networkError",
- "settings.googleHealthSyncResult",
- "settings.googleHealthSyncFailed",
- // `settings.googleHealthBackfillInProgress` left this list in v1.42.4: the
- // history-loading line became a sentence variant of the shared integration
- // card (`settings.integrationCard.reason.dataArrivingHistory`).
- "settings.syncOutcome.empty",
- "settings.syncOutcome.partial",
- "settings.syncOutcome.failed",
- // Retained score, records, and navigation contracts.
- // `insights.healthScore.method` was listed obsolete in v1.34.1 because its
- // only reference was `METRIC_PROVENANCE.HEALTH_SCORE.methodKey`, which the
- // card then passed as `method={null}` — a dangling reference, not a dead
- // one. The score card now renders that method paragraph in its disclosure
- // footer, so the key is a live contract again.
- "insights.healthScore.method",
- "insights.healthScore.bandSetter",
- "insights.healthScore.versionAndNoise",
- "insights.healthScore.methodVersion",
- "insights.healthScore.source",
- "records.allergies.add",
- "records.allergies.addFirst",
- "records.family.add",
- "records.family.addFirst",
- "admin.shell.sectionsNav",
- "settings.shell.sectionsNav",
-] as const;
-
-const OBSOLETE_KEYS = [
- "insights.healthScore.algorithmChanged",
- "insights.healthScore.dismissAlgorithmChanged",
- "insights.healthScore.composition",
- "documents.detail.summary.regenerate",
- "records.profileFacts.correct",
- // v1.43 — the profile-facts card became the life-circumstances card, whose
- // sections save on their own.
- "records.profileFacts.save",
- "settings.nightscoutPrivateHost",
- "settings.nightscoutPrivateHostHelp",
-] as const;
-
-const NEW_CALL_SITES = {
- "src/components/settings/security-section/passkey-list-section.tsx": [
- "settings.passkeyReauth.title",
- "settings.passkeyReauth.description",
- "settings.passkeyReauth.methodLabel",
- "settings.passkeyReauth.methods.password",
- "settings.passkeyReauth.methods.totp",
- "settings.passkeyReauth.methods.passkey",
- "settings.passkeyReauth.methods.webauthn",
- "settings.passkeyReauth.currentPassword",
- "settings.passkeyReauth.authenticatorCode",
- "settings.passkeyReauth.authenticatorPlaceholder",
- "settings.passkeyReauth.verifyAndAdd",
- ],
- "src/components/settings/integrations/nightscout-card.tsx": [
- "settings.nightscoutPrivateOperatorTitle",
- "settings.nightscoutPrivateOperatorDescription",
- ],
- "src/components/settings/import-panel/apple-health-import-card.tsx": [
- "settings.sections.export.import.appleHealth.ecgResult",
- ],
-} as const;
-
-type Catalog = Record;
-
-function readCatalog(locale: (typeof LOCALES)[number]): Catalog {
- return JSON.parse(
- readFileSync(join(ROOT, "messages", `${locale}.json`), "utf8"),
- ) as Catalog;
-}
-
-function resolveLeaf(catalog: Catalog, dottedKey: string): string | undefined {
- let value: unknown = catalog;
- for (const segment of dottedKey.split(".")) {
- if (typeof value !== "object" || value === null || !(segment in value)) {
- return undefined;
- }
- value = (value as Record)[segment];
- }
- return typeof value === "string" ? value : undefined;
-}
-
-function placeholders(message: string): string[] {
- return [
- ...new Set(
- [...message.matchAll(/\{([A-Za-z_][A-Za-z0-9_]*)\b/g)].map(
- (match) => match[1],
- ),
- ),
- ].sort();
-}
-
-describe("v1.34.1 locale reconciliation", () => {
- it("wires every newly introduced user-facing state through a literal t() call", () => {
- for (const [relativePath, keys] of Object.entries(NEW_CALL_SITES)) {
- const source = readFileSync(join(ROOT, relativePath), "utf8");
- for (const key of keys) {
- expect(source, `${relativePath} must translate ${key}`).toMatch(
- new RegExp(`\\bt\\(\\s*["']${key.replaceAll(".", "\\.")}["']`),
- );
- }
- }
- });
-
- it("provides every final contract in all six locales with ICU parity", () => {
- const catalogs = Object.fromEntries(
- LOCALES.map((locale) => [locale, readCatalog(locale)]),
- ) as Record<(typeof LOCALES)[number], Catalog>;
-
- for (const key of REQUIRED_KEYS) {
- const english = resolveLeaf(catalogs.en, key);
- expect(english, `messages/en.json is missing ${key}`).toBeTruthy();
-
- for (const locale of LOCALES) {
- const translated = resolveLeaf(catalogs[locale], key);
- expect(
- translated,
- `messages/${locale}.json is missing ${key}`,
- ).toBeTruthy();
- expect(
- placeholders(translated ?? ""),
- `messages/${locale}.json changed ICU placeholders for ${key}`,
- ).toEqual(placeholders(english ?? ""));
- }
- }
- });
-
- it("removes only the v1.34.1 copy proven obsolete after final call-site inventory", () => {
- for (const locale of LOCALES) {
- const catalog = readCatalog(locale);
- for (const key of OBSOLETE_KEYS) {
- expect(
- resolveLeaf(catalog, key),
- `messages/${locale}.json still contains obsolete ${key}`,
- ).toBeUndefined();
- }
- }
-
- const productionSource = Object.keys(NEW_CALL_SITES)
- .map((relativePath) => readFileSync(join(ROOT, relativePath), "utf8"))
- .join("\n");
- for (const key of OBSOLETE_KEYS) {
- expect(productionSource).not.toContain(`t("${key}")`);
- expect(productionSource).not.toContain(`t('${key}')`);
- }
- });
-});
diff --git a/src/app/README.md b/src/app/README.md
index 889bc66c4..acce7939c 100644
--- a/src/app/README.md
+++ b/src/app/README.md
@@ -22,4 +22,4 @@ Every endpoint wraps its handler in `apiHandler` (`@/lib/api-handler`), parses a
## Conventions
-See [`../../CLAUDE.md`](../../CLAUDE.md). The request-flow walk in `.planning/codebase/arch.md` annotates each layer with file:line citations. Related dirs: handlers compose [`../lib/`](../lib/README.md); pages render [`../components/`](../components/README.md).
+Related dirs: handlers compose [`../lib/`](../lib/README.md); pages render [`../components/`](../components/README.md).
diff --git a/src/app/api/auth/me/glucose-unit/route.ts b/src/app/api/auth/me/glucose-unit/route.ts
index 719d3f45a..cd315578a 100644
--- a/src/app/api/auth/me/glucose-unit/route.ts
+++ b/src/app/api/auth/me/glucose-unit/route.ts
@@ -34,6 +34,7 @@ import { checkRateLimit, rateLimitHeaders } from "@/lib/rate-limit";
import { resolveGlucoseUnit, type GlucoseUnit } from "@/lib/glucose";
import { glucoseUnitPatchSchema } from "@/lib/validations/user-prefs";
import { refreshTextsAfterUnitChange } from "@/lib/insights/unit-change-refresh";
+import { invalidateUserProfile } from "@/lib/cache/invalidate";
export const dynamic = "force-dynamic";
@@ -103,9 +104,11 @@ export const PATCH = apiHandler(async (req: Request) => {
},
});
- // The status notes and narratives quote glucose in this unit; re-warm
- // them so today's stored text does not keep the old one.
+ // The targets payload and the status notes and narratives carry glucose in
+ // this unit: evict the cached payloads and re-warm today's stored text so
+ // neither keeps the old one.
if (resolveGlucoseUnit(previous?.glucoseUnit) !== next) {
+ invalidateUserProfile(user.id);
void refreshTextsAfterUnitChange(user.id);
}
diff --git a/src/app/api/version/__tests__/route.test.ts b/src/app/api/version/__tests__/route.test.ts
index 4803e2ab5..e7f23eb20 100644
--- a/src/app/api/version/__tests__/route.test.ts
+++ b/src/app/api/version/__tests__/route.test.ts
@@ -44,7 +44,10 @@ describe("GET /api/version", () => {
);
expect(body.data.license).toBe("PolyForm-Noncommercial-1.0.0");
expect(body.data.repository).toContain("github.com");
- expect(body.data.changelog).toContain("CHANGELOG");
+ // The release notes, not the multi-megabyte CHANGELOG.md.
+ expect(body.data.changelog).toBe(
+ "https://github.com/MBombeck/HealthLog/releases",
+ );
expect(body.data.docs).toContain("docs.healthlog.dev");
});
diff --git a/src/app/api/version/route.ts b/src/app/api/version/route.ts
index 953b653c7..c555db487 100644
--- a/src/app/api/version/route.ts
+++ b/src/app/api/version/route.ts
@@ -63,7 +63,7 @@ export const GET = apiHandler(async () => {
builtAt,
license: "PolyForm-Noncommercial-1.0.0",
repository: "https://github.com/MBombeck/HealthLog",
- changelog: "https://github.com/MBombeck/HealthLog/blob/main/CHANGELOG.md",
+ changelog: "https://github.com/MBombeck/HealthLog/releases",
docs: "https://docs.healthlog.dev",
offlineGeoEnabled,
geoProviderHost,
diff --git a/src/app/medications/[id]/history/page.tsx b/src/app/medications/[id]/history/page.tsx
index 948509a8d..fbb2eba00 100644
--- a/src/app/medications/[id]/history/page.tsx
+++ b/src/app/medications/[id]/history/page.tsx
@@ -21,7 +21,7 @@ export default function IntakeHistoryRedirectPage({
const router = useRouter();
useEffect(() => {
- router.replace(`/medications/${id}?tab=verlauf`);
+ router.replace(`/medications/${id}?tab=history`);
}, [id, router]);
return (
diff --git a/src/app/medications/__tests__/detail-page-history-surface.test.ts b/src/app/medications/__tests__/detail-page-history-surface.test.ts
deleted file mode 100644
index ce95075fd..000000000
--- a/src/app/medications/__tests__/detail-page-history-surface.test.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * v1.15.18 — the `/medications/[id]` detail page is the tabbed shell.
- *
- * The page owns auth + the medication read and delegates the rest to
- * `` (the tab strip, `?tab=` URL state, the hero
- * and the wizard / advanced affordances). Source-level guards (the page
- * is a client component with `use()` + hooks, so a full render is
- * heavier than the contract warrants):
- * - it renders `MedicationDetailTabs` and nothing tab-specific itself;
- * - it carries no `TodaysDoseCard` (lives only on the list page);
- * - it hosts neither the wizard nor the modal advanced sheet directly
- * (those live inside the tab component).
- */
-
-import { readFileSync } from "node:fs";
-import { join } from "node:path";
-import { describe, expect, it } from "vitest";
-
-const source = readFileSync(
- join(process.cwd(), "src/app/medications/[id]/page.tsx"),
- "utf8",
-);
-
-describe("medication detail page is the tabbed shell (v1.15.18)", () => {
- it("delegates to MedicationDetailTabs", () => {
- expect(source).toContain("MedicationDetailTabs");
- });
-
- it("does not import or render TodaysDoseCard", () => {
- expect(source).not.toContain("TodaysDoseCard");
- expect(source).not.toContain("todays-dose-card");
- });
-
- it("does not host the wizard or the modal advanced sheet directly", () => {
- expect(source).not.toContain("AdvancedSettingsSheet");
- expect(source).not.toContain("MedicationWizardDialog");
- });
-});
diff --git a/src/app/medications/page-client.tsx b/src/app/medications/page-client.tsx
index e197120d3..f35ecf09b 100644
--- a/src/app/medications/page-client.tsx
+++ b/src/app/medications/page-client.tsx
@@ -435,7 +435,7 @@ export default function MedicationsPageClient() {
function openHistory(med: Medication) {
seedDetail(med);
- router.push(`/medications/${med.id}?tab=verlauf`);
+ router.push(`/medications/${med.id}?tab=history`);
}
function closeDialog() {
diff --git a/src/components/README.md b/src/components/README.md
index eed13a68f..1537a3fae 100644
--- a/src/components/README.md
+++ b/src/components/README.md
@@ -21,4 +21,4 @@ React components. The UI primitives are shadcn/ui (new-york style, zinc base) ov
## Conventions
-Kebab-case filenames; React component exports stay PascalCase regardless. (A handful of PascalCase-filename outliers under `medications/` + `onboarding/` are pre-existing drift, noted in [`../../CLAUDE.md`](../../CLAUDE.md).) Every `t()` call needs a matching key in `messages/en.json` — guarded by `i18n-call-site-coverage.test.ts`. Data reads go through TanStack Query with keys from `@/lib/query-keys` and unwrap `(await res.json()).data`. Tests live in [`__tests__/`](__tests__/).
+Kebab-case filenames; React component exports stay PascalCase regardless. Every `t()` call needs a matching key in `messages/en.json` — guarded by `i18n-call-site-coverage.test.ts`. Data reads go through TanStack Query with keys from `@/lib/query-keys` and unwrap `(await res.json()).data`. Tests live in [`__tests__/`](__tests__/).
diff --git a/src/components/__tests__/delegated-write-affordances.test.tsx b/src/components/__tests__/delegated-write-affordances.test.tsx
index 7517ab847..623e22bde 100644
--- a/src/components/__tests__/delegated-write-affordances.test.tsx
+++ b/src/components/__tests__/delegated-write-affordances.test.tsx
@@ -320,20 +320,16 @@ describe("marking a dose", () => {
describe("the medication card menu", () => {
const node = (
- {}}
- onOpenHistory={() => {}}
- onLogSideEffect={() => {}}
- />
+ {}} onOpenHistory={() => {}} />
);
it("renders its trigger in the caller's own record", () => {
expect(render(OWN_RECORD, node)).toContain(" {
- // One item survives for them — noting a side effect — so the menu stays.
- expect(render(WRITABLE, node)).toContain(" {
+ // Editing and the history editor are owner work; nothing survives.
+ expect(render(WRITABLE, node)).toBe("");
});
it("disappears entirely for a read-only delegate", () => {
diff --git a/src/components/charts/__tests__/health-chart-target-link-removed.test.ts b/src/components/charts/__tests__/health-chart-target-link-removed.test.ts
deleted file mode 100644
index ea9102dd0..000000000
--- a/src/components/charts/__tests__/health-chart-target-link-removed.test.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * v1.16.8 — the in-header "adjust targets" link is gone from the chart
- * card. The affordance duplicated the page-level target-adjust control
- * in the insights sub-page header (``); the chart
- * stays a read surface. Pinned at the source level (same convention as
- * the auth-shell hoist contract) so the link doesn't quietly return on
- * a future header rework.
- */
-import { readFileSync } from "node:fs";
-import { resolve } from "node:path";
-
-import { describe, expect, it } from "vitest";
-
-import { readSource, readSourceFolder } from "@/__tests__/helpers/source-files";
-
-const WEIGHT_PAGE_PATH = resolve(
- __dirname,
- "../../../app/insights/weight/page.tsx",
-);
-const BP_PAGE_PATH = resolve(
- __dirname,
- "../../../app/insights/blood-pressure/page.tsx",
-);
-const EN_BUNDLE_PATH = resolve(__dirname, "../../../../messages/en.json");
-
-describe("v1.16.8 — chart-card target-adjust link removed", () => {
- it("ships no targetSettingsHref prop or header link in the chart", () => {
- // The component and every part it is built from (header included).
- const source = [
- readSource("src/components/charts/health-chart.tsx", "HealthChart"),
- ...readSourceFolder("src/components/charts/health-chart", {
- floor: 9,
- }).map(([, text]) => text),
- ].join("\n");
- expect(source).not.toContain("targetSettingsHref");
- expect(source).not.toContain("chart-target-settings-link");
- expect(source).not.toContain("charts.adjustTargets");
- });
-
- it("weight and blood-pressure pages no longer pass the link href", () => {
- for (const path of [WEIGHT_PAGE_PATH, BP_PAGE_PATH]) {
- const source = readFileSync(path, "utf8");
- expect(source).not.toContain("targetSettingsHref");
- }
- });
-
- it("the orphaned locale key is gone", () => {
- const bundle = JSON.parse(readFileSync(EN_BUNDLE_PATH, "utf8")) as {
- charts: Record;
- };
- expect(bundle.charts.adjustTargets).toBeUndefined();
- });
-});
diff --git a/src/components/day/__tests__/day-view.test.tsx b/src/components/day/__tests__/day-view.test.tsx
index e4b9685aa..175308929 100644
--- a/src/components/day/__tests__/day-view.test.tsx
+++ b/src/components/day/__tests__/day-view.test.tsx
@@ -38,7 +38,7 @@ const DAY: DayResponse = {
until: "2026-03-31",
dayIndex: 64,
dayCount: 151,
- href: "/medications/m1?tab=verlauf",
+ href: "/medications/m1?tab=history",
},
],
values: [
diff --git a/src/components/day/since-last-visit.tsx b/src/components/day/since-last-visit.tsx
index 488781784..a2ffe35be 100644
--- a/src/components/day/since-last-visit.tsx
+++ b/src/components/day/since-last-visit.tsx
@@ -321,7 +321,7 @@ export function SinceLastVisit({
className="min-h-11 sm:min-h-9"
>
diff --git a/src/components/illness/__tests__/illness-labels-i18n.test.ts b/src/components/illness/__tests__/illness-labels-i18n.test.ts
new file mode 100644
index 000000000..604871d47
--- /dev/null
+++ b/src/components/illness/__tests__/illness-labels-i18n.test.ts
@@ -0,0 +1,55 @@
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+
+import { describe, expect, it } from "vitest";
+
+import { resolveKey } from "@/lib/i18n/resolve-key";
+
+/**
+ * The illness screens name the vital that drives a recovery gap and label the
+ * day picker of the log sheet through these keys. They were once copied from
+ * English into five catalogs, so a German reader saw "Weight", "Pulse",
+ * "Recovery" and "Day" among translated neighbours. The English leak guard
+ * skips single words, so it cannot see this; the pin below can.
+ */
+const MESSAGES_DIR = join(__dirname, "../../../../messages");
+
+function catalog(locale: string): Record {
+ return JSON.parse(
+ readFileSync(join(MESSAGES_DIR, `${locale}.json`), "utf8"),
+ ) as Record;
+}
+
+const KEYS = [
+ "illness.vital.WEIGHT",
+ "illness.vital.PULSE",
+ "illness.vital.RECOVERY_SCORE",
+ "illness.sheet.day",
+] as const;
+
+const english = catalog("en");
+
+describe("illness labels", () => {
+ for (const locale of [
+ "de",
+ "fr",
+ "es",
+ "it",
+ "pl",
+ "nl",
+ "pt-BR",
+ "ko",
+ "ru",
+ ]) {
+ it(`are translated in ${locale}`, () => {
+ const messages = catalog(locale);
+ for (const key of KEYS) {
+ const value = resolveKey(messages, key);
+ expect(value, `${locale} ${key}`).toBeTruthy();
+ expect(value, `${locale} ${key} is still English`).not.toBe(
+ resolveKey(english, key),
+ );
+ }
+ });
+ }
+});
diff --git a/src/components/illness/__tests__/illness-view-nesting.test.tsx b/src/components/illness/__tests__/illness-view-nesting.test.tsx
index 547a4a20d..01f0d0c55 100644
--- a/src/components/illness/__tests__/illness-view-nesting.test.tsx
+++ b/src/components/illness/__tests__/illness-view-nesting.test.tsx
@@ -47,7 +47,10 @@ vi.mock("@/components/symptoms/symptoms-section", () => ({
SymptomsSection: () => null,
}));
vi.mock("../new-episode-sheet", () => ({ NewEpisodeSheet: () => null }));
-vi.mock("../log-day-sheet", () => ({ LogDaySheet: () => null }));
+vi.mock("../log-day-sheet", () => ({
+ LogDaySheet: () => null,
+ useLastLoggableDay: (today: string) => today,
+}));
vi.mock("@/components/day/day-link", () => ({
DAY_LINK_SLOT: "{date}",
DayLinkStated: ({ children }: { children: React.ReactNode }) => children,
diff --git a/src/components/illness/__tests__/log-day-sheet-last-day.test.ts b/src/components/illness/__tests__/log-day-sheet-last-day.test.ts
new file mode 100644
index 000000000..29f8bac3f
--- /dev/null
+++ b/src/components/illness/__tests__/log-day-sheet-last-day.test.ts
@@ -0,0 +1,48 @@
+import { describe, expect, it } from "vitest";
+
+import { lastLoggableDay } from "../log-day-sheet";
+
+/**
+ * "Log day" on an illness opens on the day it suggests and bounds the picker
+ * there. For a recovered episode that used to be today, so one tap on Save
+ * filed a sick day days after the recovery, and the day view then showed it
+ * with no illness running beside it.
+ */
+describe("lastLoggableDay", () => {
+ it("is today while the episode runs", () => {
+ expect(lastLoggableDay("2026-11-12", null, "Europe/Berlin")).toBe(
+ "2026-11-12",
+ );
+ });
+
+ it("is the recovery day once the episode ended before today", () => {
+ expect(
+ lastLoggableDay(
+ "2026-11-12",
+ "2026-11-09T08:00:00.000Z",
+ "Europe/Berlin",
+ ),
+ ).toBe("2026-11-09");
+ });
+
+ it("stays today when the episode ended today", () => {
+ expect(
+ lastLoggableDay(
+ "2026-11-12",
+ "2026-11-12T08:00:00.000Z",
+ "Europe/Berlin",
+ ),
+ ).toBe("2026-11-12");
+ });
+
+ it("reads the recovery day in the profile zone", () => {
+ // 23:30 UTC is already the next day in Berlin.
+ expect(
+ lastLoggableDay(
+ "2026-11-12",
+ "2026-11-09T23:30:00.000Z",
+ "Europe/Berlin",
+ ),
+ ).toBe("2026-11-10");
+ });
+});
diff --git a/src/components/illness/illness-episode-detail.tsx b/src/components/illness/illness-episode-detail.tsx
index 63a89e0e3..59c66a0c5 100644
--- a/src/components/illness/illness-episode-detail.tsx
+++ b/src/components/illness/illness-episode-detail.tsx
@@ -30,7 +30,7 @@ import { canReadBodySites } from "@/hooks/use-body-sites";
import { IllnessCorrelationCard } from "./illness-correlation-card";
import { IllnessDayTimeline } from "./illness-day-timeline";
-import { LogDaySheet } from "./log-day-sheet";
+import { LogDaySheet, useLastLoggableDay } from "./log-day-sheet";
import { NewEpisodeSheet } from "./new-episode-sheet";
import { EpisodeMenu } from "./episode-menu";
import { useIllnessEpisode, useResolveEpisode } from "./use-illness";
@@ -66,6 +66,7 @@ export function IllnessEpisodeDetail({ episodeId }: { episodeId: string }) {
const [logOpen, setLogOpen] = useState(false);
const [editOpen, setEditOpen] = useState(false);
const today = todayLocal();
+ const logLastDay = useLastLoggableDay(today, episode?.resolvedAt);
if (isError) {
return (
@@ -226,7 +227,7 @@ export function IllnessEpisodeDetail({ episodeId }: { episodeId: string }) {
open={logOpen}
onOpenChange={setLogOpen}
episodeId={episode.id}
- date={today}
+ date={logLastDay}
onsetDate={episode.onsetAt.slice(0, 10)}
/>
(null);
const logEpisode = episodes?.find((e) => e.id === logEpisodeId) ?? null;
+ const logLastDay = useLastLoggableDay(today, logEpisode?.resolvedAt);
/**
* Group the flat list into active / resolved, nesting flares and recurring
@@ -532,7 +533,7 @@ export function IllnessView() {
if (!open) setLogEpisodeId(null);
}}
episodeId={logEpisodeId}
- date={today}
+ date={logLastDay}
onsetDate={logEpisode?.onsetAt.slice(0, 10)}
/>
) : null}
diff --git a/src/components/illness/log-day-sheet.tsx b/src/components/illness/log-day-sheet.tsx
index 5284689ab..047f7aa21 100644
--- a/src/components/illness/log-day-sheet.tsx
+++ b/src/components/illness/log-day-sheet.tsx
@@ -24,7 +24,12 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
-import { useFormatters, useTranslations } from "@/lib/i18n/context";
+import {
+ useDisplayTimezone,
+ useFormatters,
+ useTranslations,
+} from "@/lib/i18n/context";
+import { userDayKey } from "@/lib/tz/format";
import { useUnitDisplay } from "@/hooks/use-unit-display";
import { ILLNESS_SYMPTOM_CATALOG } from "./symptom-catalog";
@@ -35,7 +40,11 @@ interface LogDaySheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
episodeId: string;
- /** The day to seed the picker with (usually today). */
+ /**
+ * The last day of the episode (`YYYY-MM-DD`): the day the picker opens on
+ * and the latest it offers. Today while the episode runs, its recovery day
+ * once it ended (`useLastLoggableDay`).
+ */
date: string;
/**
* Episode onset day (`YYYY-MM-DD`) — the earliest day the picker allows, so
@@ -45,6 +54,29 @@ interface LogDaySheetProps {
onsetDate?: string;
}
+/**
+ * The last day a log can belong to: today, or the recovery day when the
+ * episode ended before today. The recovery day is read in the profile zone,
+ * the zone the server reads it in.
+ */
+export function lastLoggableDay(
+ today: string,
+ resolvedAt: string | null | undefined,
+ timeZone: string,
+): string {
+ if (!resolvedAt) return today;
+ const resolvedDay = userDayKey(new Date(resolvedAt), timeZone);
+ return resolvedDay < today ? resolvedDay : today;
+}
+
+/** `lastLoggableDay` in the viewer's profile zone. */
+export function useLastLoggableDay(
+ today: string,
+ resolvedAt: string | null | undefined,
+): string {
+ return lastLoggableDay(today, resolvedAt, useDisplayTimezone());
+}
+
/** Mini toggle chip — the shared selection vocabulary. */
function Chip({
active,
@@ -147,8 +179,8 @@ export function LogDaySheet({
const { toDisplay, fromDisplay, unitFor } = useUnitDisplay();
const feverUnit = unitFor("BODY_TEMPERATURE");
- // The day being logged — seeded from `date` (today), but the picker lets the
- // user backdate within [onset, today] for the retrospective journal.
+ // The day being logged — seeded from `date` (the episode's last day), and
+ // the picker lets the user backdate within [onset, date] for the journal.
const [selectedDate, setSelectedDate] = useState(date);
// Re-seed the picker each time the sheet opens (the open transition).
const [pickerWasOpen, setPickerWasOpen] = useState(false);
diff --git a/src/components/insights/medication-efficacy-summary.tsx b/src/components/insights/medication-efficacy-summary.tsx
index 773bbccc7..cac643d01 100644
--- a/src/components/insights/medication-efficacy-summary.tsx
+++ b/src/components/insights/medication-efficacy-summary.tsx
@@ -114,7 +114,7 @@ export function MedicationEfficacySummary({
{t("medications.efficacy.insights.link")}
diff --git a/src/components/insights/mood/__tests__/mood-notes-timeline-removed.test.ts b/src/components/insights/mood/__tests__/mood-notes-timeline-removed.test.ts
deleted file mode 100644
index efa37cfba..000000000
--- a/src/components/insights/mood/__tests__/mood-notes-timeline-removed.test.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { describe, it, expect } from "vitest";
-import { existsSync, readFileSync } from "node:fs";
-import { join } from "node:path";
-
-const COMPONENT_DIR = join(__dirname, "..");
-const MOOD_LIST = join(__dirname, "../../../mood/mood-list.tsx");
-/**
- * The two modules the mood insights surface is rendered from. The breakdown
- * cluster lives behind `next/dynamic` in its own file; both are the surface.
- */
-const INSIGHTS_SURFACE_FILES = [
- join(COMPONENT_DIR, "mood-insights-sections.tsx"),
- join(COMPONENT_DIR, "mood-insights-breakdowns.tsx"),
-];
-
-/**
- * v1.8.6 — the notes timeline display is removed from the mood insights
- * surface (note capture on the form stays). These structural guards keep
- * the surface from regressing back to a notes feed and keep long notes
- * truncated in the entries table.
- */
-describe("mood notes timeline removal", () => {
- it("deletes the notes-timeline component file", () => {
- expect(existsSync(join(COMPONENT_DIR, "mood-notes-timeline.tsx"))).toBe(
- false,
- );
- });
-
- it("no longer renders the notes timeline from the insights sections", () => {
- // Both halves of the surface: the eager module that paints the calendar
- // and the assessment, and the deferred module that carries every
- // breakdown. A guard reading only one of them would go quiet the moment
- // the other grew a notes feed.
- for (const file of INSIGHTS_SURFACE_FILES) {
- const src = readFileSync(file, "utf8");
- expect(src).not.toContain("MoodNotesTimeline");
- expect(src).not.toContain("notesTimeline");
- expect(src).not.toContain("notesTimelineTitle");
- }
- });
-
- it("mounts the narrative feed in the insights sections", () => {
- // v1.12.7 — the narrative one-liners now ride the merged "What stands out"
- // card (`MoodWhatStandsOut`), which renders `MoodNarrativeFeed` internally.
- // The structural guard tracks the mount point on the sections surface.
- //
- // v1.38 — that mount point moved into `mood-insights-breakdowns.tsx` when
- // the below-the-fold cluster went behind `next/dynamic`. Read as the union
- // of the two files, so the guard asserts the card is mounted SOMEWHERE on
- // the surface rather than in one particular file — which is what it was
- // always about.
- const src = INSIGHTS_SURFACE_FILES.map((f) => readFileSync(f, "utf8")).join(
- "\n",
- );
- expect(src).toContain("MoodWhatStandsOut");
- expect(src).toContain("MoodInTargetTile");
- });
-
- it("renders the narrative feed inside the merged what-stands-out card", () => {
- const src = readFileSync(
- join(COMPONENT_DIR, "mood-what-stands-out.tsx"),
- "utf8",
- );
- expect(src).toContain("MoodNarrativeFeed");
- expect(src).toContain("MoodDiscoveredRelations");
- });
-});
-
-describe("mood entries table — long-note truncation", () => {
- const src = readFileSync(MOOD_LIST, "utf8");
-
- it("caps the note column width to prevent horizontal scroll", () => {
- expect(src).toContain("max-w-[18rem]");
- });
-
- it("clamps the note by default and exposes the full text via the row toggle", () => {
- // v1.16.8 — the desktop hover tooltip is replaced by an expandable
- // toggle (works on touch, preserves line breaks via pre-wrap). The
- // default presentation stays clamped so the table keeps its density.
- expect(src).toContain("line-clamp-2");
- expect(src).toContain("mood-note-toggle");
- expect(src).toContain("whitespace-pre-wrap");
- expect(src).not.toContain("TooltipContent");
- });
-});
diff --git a/src/components/measurement-reminders/__tests__/checkup-card-badges.test.tsx b/src/components/measurement-reminders/__tests__/checkup-card-badges.test.tsx
new file mode 100644
index 000000000..a506fa832
--- /dev/null
+++ b/src/components/measurement-reminders/__tests__/checkup-card-badges.test.tsx
@@ -0,0 +1,74 @@
+import { describe, expect, it, vi } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+
+import { I18nProvider } from "@/lib/i18n/context";
+import type { MeasurementReminder } from "@/hooks/use-measurement-reminders";
+
+/**
+ * A checkup that is switched off, or that the Coach created, says so in the
+ * card view. The list view used to drop both badges, so a disabled checkup
+ * looked active there; which view a person sees is a remembered preference,
+ * so the same checkup read differently depending on a toggle.
+ */
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({ replace: vi.fn(), push: vi.fn() }),
+}));
+
+import { VorsorgeCard } from "../checkups/checkup-card";
+
+const reminder = {
+ id: "coach-off",
+ label: "Check blood pressure",
+ measurementType: null,
+ intervalDays: 30,
+ rrule: null,
+ anchorDate: null,
+ endsOn: null,
+ origin: "COACH",
+ nextDueAt: null,
+ notifyHour: 9,
+ location: null,
+ lastSatisfiedAt: null,
+ enabled: false,
+ createdAt: "2030-01-01T00:00:00.000Z",
+ updatedAt: "2030-01-01T00:00:00.000Z",
+} as MeasurementReminder;
+
+function render(view: "cards" | "list"): string {
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false } },
+ });
+ const noop = () => {};
+ return renderToStaticMarkup(
+
+
+
+
+ ,
+ );
+}
+
+describe("checkup state badges", () => {
+ it.each(["cards", "list"] as const)(
+ "show the Coach and switched-off badges in the %s view",
+ (view) => {
+ const html = render(view);
+ expect(html).toContain("Check blood pressure");
+ expect(html).toContain(">Coach<");
+ expect(html).toContain(">Off<");
+ },
+ );
+});
diff --git a/src/components/measurement-reminders/__tests__/type-labels-i18n.test.ts b/src/components/measurement-reminders/__tests__/type-labels-i18n.test.ts
index 7643df2d2..4d7a2380f 100644
--- a/src/components/measurement-reminders/__tests__/type-labels-i18n.test.ts
+++ b/src/components/measurement-reminders/__tests__/type-labels-i18n.test.ts
@@ -28,6 +28,7 @@ const LOCALES = readdirSync(MESSAGES_DIR)
const REQUIRED_KEYS = [
// Newly added Vorsorge reminder type labels.
"measurementReminders.types.WAIST_CIRCUMFERENCE",
+ "measurementReminders.types.BLOOD_KETONES",
"measurementReminders.types.WHO5_SCORE",
"measurementReminders.types.SCI_SCORE",
// Pre-existing screening labels (regression floor).
diff --git a/src/components/measurement-reminders/checkups/checkup-card.tsx b/src/components/measurement-reminders/checkups/checkup-card.tsx
index 94777114b..744da0d82 100644
--- a/src/components/measurement-reminders/checkups/checkup-card.tsx
+++ b/src/components/measurement-reminders/checkups/checkup-card.tsx
@@ -244,6 +244,30 @@ function NextLastSlot({
);
}
+/**
+ * The Coach-origin and switched-off badges, shared by the card and the list
+ * view; `undefined` when neither applies, so no empty row is drawn.
+ */
+function checkupStateBadges(
+ reminder: MeasurementReminder,
+ t: (key: string) => string,
+): React.ReactNode {
+ const isCoach = reminder.origin === "COACH";
+ if (!isCoach && reminder.enabled) return undefined;
+ return (
+ <>
+ {isCoach && (
+ {t("measurementReminders.originCoach")}
+ )}
+ {!reminder.enabled && (
+
+ {t("measurementReminders.disabledBadge")}
+
+ )}
+ >
+ );
+}
+
/** The dense list variant of a checkup. */
function CheckupListRow({
reminder,
@@ -251,6 +275,7 @@ function CheckupListRow({
categoryLabel,
cadenceText,
meta,
+ stateBadges,
isLinked,
isScreening,
canManageMeasurements,
@@ -265,6 +290,8 @@ function CheckupListRow({
categoryLabel: string;
cadenceText: string | null;
meta: React.ReactNode;
+ /** The same Coach and disabled badges the card view shows. */
+ stateBadges: React.ReactNode;
isLinked: boolean;
isScreening: boolean;
canManageMeasurements: boolean;
@@ -283,6 +310,7 @@ function CheckupListRow({
{resolveReminderLabel(reminder, t)}
+ {stateBadges}
{/* Cadence rides on the metric line, styled like the
metric text (no shaded pill). */}
@@ -537,7 +565,6 @@ export function VorsorgeCard({
: reminder.rrule
? t("measurementReminders.cadence.custom")
: "";
- const isCoach = reminder.origin === "COACH";
const isLinked = reminder.measurementType != null;
// A screening reminder's action starts the check-in, never numeric entry.
const isScreening = isScreeningReminderType(reminder.measurementType);
@@ -558,20 +585,7 @@ export function VorsorgeCard({
? `/measurements?type=${encodeURIComponent(reminder.measurementType as string)}`
: undefined;
- // Provenance and disabled badges get a row only when one applies.
- const hasStateBadges = isCoach || !reminder.enabled;
- const stateBadges = hasStateBadges ? (
- <>
- {isCoach && (
- {t("measurementReminders.originCoach")}
- )}
- {!reminder.enabled && (
-
- {t("measurementReminders.disabledBadge")}
-
- )}
- >
- ) : undefined;
+ const stateBadges = checkupStateBadges(reminder, t);
const headerActions = canManageMeasurements ? (
diff --git a/src/components/medications/__tests__/detail-page-cascade.test.tsx b/src/components/medications/__tests__/detail-page-cascade.test.tsx
index 2f1cd6df9..7dc8f59c6 100644
--- a/src/components/medications/__tests__/detail-page-cascade.test.tsx
+++ b/src/components/medications/__tests__/detail-page-cascade.test.tsx
@@ -111,8 +111,6 @@ describe("medication detail compliance applicability", () => {
});
it("keeps PRN compliance disabled at the query boundary", () => {
- expect(source).toContain(
- 'enabled: activeTab === "uebersicht" && !asNeeded',
- );
+ expect(source).toContain('enabled: activeTab === "overview" && !asNeeded');
});
});
diff --git a/src/components/medications/__tests__/medication-card-glp1.test.tsx b/src/components/medications/__tests__/medication-card-glp1.test.tsx
index 5a405f085..b043d2e9e 100644
--- a/src/components/medications/__tests__/medication-card-glp1.test.tsx
+++ b/src/components/medications/__tests__/medication-card-glp1.test.tsx
@@ -465,49 +465,9 @@ describe(" — GLP-1 variant rendering", () => {
expect(html).not.toContain("Low stock");
});
- it("folds the side-effect quick-log into the same overflow kebab", () => {
- // v1.7.2 — the side-effect quick-log folds into the SAME overflow
- // menu as edit / history / advanced, so the GLP-1 header carries one
- // kebab whether or not the hand-off is wired (parity with the generic
- // card). SSR doesn't open Radix Portal content, so we assert the
- // single trigger surfaces and smoke-check the handler payload.
- const client = makeClient();
- seedCompliance(client, med7p5.id);
- seedGlp1Details(client, med7p5.id, {});
-
- const handler = vi.fn();
- const html = render(
- {}}
- onOpenHistory={() => {}}
- onLogSideEffect={handler}
- />,
- client,
- );
-
- // Exactly one kebab trigger with the localised aria-label.
- const triggers = html.match(/aria-label="More options"/g) ?? [];
- expect(triggers).toHaveLength(1);
- // The side-effect button must no longer ride alongside
- // Eingenommen / Übersprungen in the primary actions row.
- expect(html).not.toMatch(
- /class="[^"]*flex[^"]*gap-2[^"]*"[^>]*>[\s\S]*?Log side effect/,
- );
-
- // Invoke the supplied handler the way the menu item's onClick
- // would — pins the prefill payload contract for MoodEntry.
- handler(med7p5);
- expect(handler).toHaveBeenCalledWith(
- expect.objectContaining({ name: "Mounjaro", treatmentClass: "GLP1" }),
- );
- });
-
- it("still renders the single kebab when onLogSideEffect is not supplied", () => {
- // v1.7.2 — the kebab is unconditional now (it always carries edit
- // / history / advanced); the side-effect item is the only opt-in
- // part. Mounjaro and Ramipril therefore share the same one-kebab
- // header shape regardless of the hand-off prop.
+ it("renders the single overflow kebab", () => {
+ // The kebab carries edit and history, so Mounjaro and Ramipril share
+ // the same one-kebab header shape.
const client = makeClient();
seedCompliance(client, med7p5.id);
seedGlp1Details(client, med7p5.id, {});
diff --git a/src/components/medications/__tests__/medication-card-symmetry.test.tsx b/src/components/medications/__tests__/medication-card-symmetry.test.tsx
index 5b8ab213f..1343f7103 100644
--- a/src/components/medications/__tests__/medication-card-symmetry.test.tsx
+++ b/src/components/medications/__tests__/medication-card-symmetry.test.tsx
@@ -190,9 +190,8 @@ describe("medication card symmetry — Ramipril vs Mounjaro", () => {
}
});
- it("Mounjaro keeps the single kebab even when onLogSideEffect is supplied (GLP-1-only)", () => {
- // The side-effect quick-log folds into the SAME overflow menu, so the
- // GLP-1 header shape stays byte-symmetric with the generic card: one
+ it("Mounjaro carries the same single kebab as the generic card", () => {
+ // The GLP-1 header shape stays byte-symmetric with the generic card: one
// kebab trigger, no extra control.
const client = makeClient();
seedCompliance(client, mounjaro.id);
@@ -203,7 +202,6 @@ describe("medication card symmetry — Ramipril vs Mounjaro", () => {
medication={mounjaro}
onEdit={() => {}}
onOpenHistory={() => {}}
- onLogSideEffect={() => {}}
/>,
client,
);
@@ -294,7 +292,6 @@ describe("medication card symmetry — Ramipril vs Mounjaro", () => {
medication={mounjaro}
onEdit={() => {}}
onOpenHistory={() => {}}
- onLogSideEffect={() => {}}
/>,
client,
);
diff --git a/src/components/medications/__tests__/medication-tab-slugs.test.ts b/src/components/medications/__tests__/medication-tab-slugs.test.ts
new file mode 100644
index 000000000..219d803f6
--- /dev/null
+++ b/src/components/medications/__tests__/medication-tab-slugs.test.ts
@@ -0,0 +1,51 @@
+import { describe, expect, it } from "vitest";
+
+import {
+ resolveTabSlug,
+ TAB_SLUGS,
+} from "@/components/medications/detail/tab-slugs";
+
+/**
+ * The medication tabs carried German slugs in every language
+ * (`?tab=verlauf`). They are English now, and every old link (a bookmark,
+ * a report, a low-stock notification sent before the change) still lands on
+ * the tab it named.
+ */
+describe("medication tab slugs", () => {
+ const all = [...TAB_SLUGS];
+
+ it("opens a tab by its own slug", () => {
+ expect(resolveTabSlug("history", all)).toBe("history");
+ expect(resolveTabSlug("supply", all)).toBe("supply");
+ });
+
+ it.each([
+ ["uebersicht", "overview"],
+ ["wirkung", "effect"],
+ ["zeitplan", "schedule"],
+ ["bestand", "supply"],
+ ["verlauf", "history"],
+ ["injektion", "injection"],
+ ["erweitert", "advanced"],
+ ["erinnerung", "schedule"],
+ ["api", "advanced"],
+ ] as const)("sends the old ?tab=%s to %s", (legacy, tab) => {
+ expect(resolveTabSlug(legacy, all)).toBe(tab);
+ });
+
+ it("falls back to the overview for a missing, unknown or unavailable tab", () => {
+ expect(resolveTabSlug(null, all)).toBe("overview");
+ expect(resolveTabSlug("constructor", all)).toBe("overview");
+ expect(resolveTabSlug("nope", all)).toBe("overview");
+ expect(
+ resolveTabSlug(
+ "injektion",
+ all.filter((tab) => tab !== "injection"),
+ ),
+ ).toBe("overview");
+ });
+
+ it("keeps every slug URL-safe ASCII", () => {
+ for (const slug of TAB_SLUGS) expect(slug).toMatch(/^[a-z]+$/);
+ });
+});
diff --git a/src/components/medications/detail/medication-detail-tabs.tsx b/src/components/medications/detail/medication-detail-tabs.tsx
index 12f6c37de..8eecf84bc 100644
--- a/src/components/medications/detail/medication-detail-tabs.tsx
+++ b/src/components/medications/detail/medication-detail-tabs.tsx
@@ -6,12 +6,12 @@
* Promotes the detail page from a read-only history view to the
* canonical home for ONE medication. The modal ``
* is dissolved into the Erweitert tab (no more five-card stack in a
- * sheet) and the separate `/history` route folds into the Verlauf tab
- * (the route now redirects to `?tab=verlauf`).
+ * sheet) and the separate `/history` route folds into the history tab
+ * (the route now redirects to `?tab=history`).
*
* Tab state lives in the URL (`?tab=`), so a tab is deep-linkable,
* survives reload, and is reachable from the medications-list card kebab
- * (History→verlauf). The kebab's Edit arrives as `?edit=1`, which opens
+ * (History). The kebab's Edit arrives as `?edit=1`, which opens
* the wizard in edit mode on top of the landing tab. Slugs are ASCII /
* locale-independent; the visible labels are i18n.
*
@@ -20,7 +20,7 @@
* switch + the reminder grace), and Übersicht grew from a lone
* compliance bar into the status landing surface (next due dose,
* reminder state, supply runway — each with a jump link into its owner
- * tab). Stale `?tab=erinnerung` deep-links land on Zeitplan.
+ * tab). Stale `?tab=erinnerung` deep-links land on the schedule tab.
*
* Only the active tab body mounts (Radix `Tabs` default) so inactive tabs
* do not fetch. The hero stays above the tabs as the always-visible
@@ -106,6 +106,12 @@ import { MedicationRelatedSection } from "@/components/medications/detail/medica
import type { RelatedLink } from "@/lib/links/related";
import { InlineAction } from "@/components/ui/inline-action";
import { EmptyState } from "@/components/ui/empty-state";
+import {
+ resolveTabSlug,
+ TAB_LABEL_KEYS,
+ TAB_SLUGS,
+ type TabSlug,
+} from "@/components/medications/detail/tab-slugs";
// v1.15.20 — Recharts stays out of the detail page's initial bundle:
// both chart bodies load through `next/dynamic` (client-only) with the
@@ -202,20 +208,6 @@ export interface MedicationDetailSnapshot {
related?: RelatedLink[];
}
-// Slugs are ASCII / locale-independent so the URL stays stable across
-// locales; the visible labels resolve through i18n. The list drives both
-// the tab strip render and the `?tab=` round-trip validation.
-const TAB_SLUGS = [
- "uebersicht",
- "wirkung",
- "zeitplan",
- "bestand",
- "verlauf",
- "injektion",
- "erweitert",
-] as const;
-type TabSlug = (typeof TAB_SLUGS)[number];
-
/**
* Tabs whose entire content is owner-only work: the schedule editor and its
* plan history, the supply ledger, the external-token row, and the advanced
@@ -225,10 +217,10 @@ type TabSlug = (typeof TAB_SLUGS)[number];
const OWNER_ONLY_TABS: ReadonlySet = new Set([
// Schedule times are a MANAGE route, but the tab also carries the reminder
// and notification settings, which resolve the caller.
- "zeitplan",
+ "schedule",
// Phase config, export and the per-medication tokens (a credential
// surface, never delegated) resolve the caller.
- "erweitert",
+ "advanced",
]);
/**
@@ -237,20 +229,7 @@ const OWNER_ONLY_TABS: ReadonlySet = new Set([
* is one; the three above are not, because each mixes in a route that
* resolves the caller and would 403 under a switch.
*/
-const MANAGE_TABS: ReadonlySet = new Set(["bestand"]);
-
-/**
- * Legacy slugs that point at a dissolved tab. The Erinnerung tab folded
- * into Zeitplan in v1.15.20; old deep-links keep landing on the surface
- * that now owns the reminder controls instead of falling back to the
- * landing tab.
- */
-const LEGACY_TAB_SLUGS: Record = {
- erinnerung: "zeitplan",
- // The API tab folded back into Erweitert in v1.42: two tabs for the
- // rarely-opened owner settings made a strip that could not fit a phone.
- api: "erweitert",
-};
+const MANAGE_TABS: ReadonlySet = new Set(["supply"]);
function snapshotToWizardPayload(
med: MedicationDetailSnapshot,
@@ -364,26 +343,26 @@ export function MedicationDetailTabs({
// Only the active tab body mounts, but the tab REGISTRY must agree with
// whatever `?tab=` carries — Injektion is absent for non-injectable
- // medications, so a stale `?tab=injektion` deep-link falls back to the
+ // medications, so a stale `?tab=injection` deep-link falls back to the
// landing tab instead of rendering an empty surface.
// v1.16.11 — an as-needed medication has no schedule and never
// reminds, so the Zeitplan tab (times editor + reminder card +
// plan-history) does not apply and is dropped from the registry; a
- // stale `?tab=zeitplan` deep-link falls back to the landing tab.
+ // stale `?tab=schedule` deep-link falls back to the landing tab.
// v1.36.x — four tabs are nothing BUT owner work: the schedule editor, the
// supply ledger, the external-token row and the advanced surface that holds
// the wizard, the import and the destructive zone. None of it is delegated,
// so inside somebody else's record they leave the registry entirely rather
// than opening onto controls the server refuses — which also means a stale
- // `?tab=erweitert` deep link lands on the landing tab, the same way an
+ // `?tab=advanced` deep link lands on the landing tab, the same way an
// inapplicable tab already does.
const availableTabs = useMemo(
() =>
TAB_SLUGS.filter(
(slug) =>
- (slug !== "injektion" || isInjectable) &&
- (slug !== "zeitplan" || !asNeeded) &&
- (slug !== "wirkung" || hasEfficacyTarget) &&
+ (slug !== "injection" || isInjectable) &&
+ (slug !== "schedule" || !asNeeded) &&
+ (slug !== "effect" || hasEfficacyTarget) &&
(!inSharedRecord || !OWNER_ONLY_TABS.has(slug)) &&
(canManageMedications || !MANAGE_TABS.has(slug)),
),
@@ -396,14 +375,7 @@ export function MedicationDetailTabs({
],
);
- const requestedRaw = searchParams?.get("tab");
- const requested = (
- requestedRaw && requestedRaw in LEGACY_TAB_SLUGS
- ? LEGACY_TAB_SLUGS[requestedRaw]
- : requestedRaw
- ) as TabSlug | null;
- const activeTab: TabSlug =
- requested && availableTabs.includes(requested) ? requested : "uebersicht";
+ const activeTab = resolveTabSlug(searchParams?.get("tab"), availableTabs);
useEffect(() => {
if (shouldOpenEditFromUrl) {
@@ -438,7 +410,7 @@ export function MedicationDetailTabs({
},
// v1.16.11 — no compliance read for an as-needed medication (the
// section is hidden; there is no rate to fetch).
- enabled: activeTab === "uebersicht" && !asNeeded,
+ enabled: activeTab === "overview" && !asNeeded,
staleTime: 30_000,
});
@@ -468,7 +440,7 @@ export function MedicationDetailTabs({
return null;
}
},
- enabled: activeTab === "uebersicht",
+ enabled: activeTab === "overview",
staleTime: 30_000,
});
@@ -549,7 +521,7 @@ export function MedicationDetailTabs({
value={slug}
data-slot={`medication-tab-${slug}`}
>
- {t(`medications.detail.tabs.${slug}`)}
+ {t(TAB_LABEL_KEYS[slug])}
))}
@@ -558,7 +530,7 @@ export function MedicationDetailTabs({
reminder state and supply runway (each with a jump link into
its owner tab), then the compliance readout. All values come
from data the page already holds — no new endpoints. */}
-
+
onTabChange(asNeeded ? "verlauf" : "zeitplan")}
+ onJump={() => onTabChange(asNeeded ? "history" : "schedule")}
dataSlot="uebersicht-next-dose"
/>
{/* v1.16.11 — as-needed never reminds; the row would only
@@ -597,7 +569,7 @@ export function MedicationDetailTabs({
: t("medications.detail.uebersicht.reminderOff")
}
jumpLabel={t("medications.detail.uebersicht.jumpToZeitplan")}
- onJump={() => onTabChange("zeitplan")}
+ onJump={() => onTabChange("schedule")}
dataSlot="uebersicht-reminder"
/>
)}
@@ -639,7 +611,7 @@ export function MedicationDetailTabs({
>
}
jumpLabel={t("medications.detail.uebersicht.jumpToBestand")}
- onJump={() => onTabChange("bestand")}
+ onJump={() => onTabChange("supply")}
dataSlot="uebersicht-supply"
/>
)}
@@ -692,10 +664,10 @@ export function MedicationDetailTabs({
with a before/after comparison + adherence lane. Strictly
descriptive; hidden for one-shot + no-target medications. */}
{hasEfficacyTarget && (
-
+
+
{/* v1.39.1 (#1033) — the intake-tracking switch leads the tab:
it decides whether the times below are a dose plan or a
record. */}
@@ -827,7 +799,7 @@ export function MedicationDetailTabs({
{/* BESTAND — inventory readout + register / correct flows. */}
-
+
+
+
{/* Dose-escalation plan: completed + planned titration steps
with a "you are here" marker. Renders for every injectable
route (not GLP-1-only) since titration is a property of the
@@ -916,7 +888,7 @@ export function MedicationDetailTabs({
{/* ERWEITERT — Lifecycle → Daten → external API → Gefahrenzone.
The API tab folded back in here (v1.42); its group stays
collapsed like the other two, and the danger zone stays last. */}
-
+
= {
+ uebersicht: "overview",
+ wirkung: "effect",
+ zeitplan: "schedule",
+ bestand: "supply",
+ verlauf: "history",
+ injektion: "injection",
+ erweitert: "advanced",
+ erinnerung: "schedule",
+ api: "advanced",
+};
+
+/** The tab labels; the message keys predate the English slugs. */
+export const TAB_LABEL_KEYS: Record = {
+ overview: "medications.detail.tabs.uebersicht",
+ effect: "medications.detail.tabs.wirkung",
+ schedule: "medications.detail.tabs.zeitplan",
+ supply: "medications.detail.tabs.bestand",
+ history: "medications.detail.tabs.verlauf",
+ injection: "medications.detail.tabs.injektion",
+ advanced: "medications.detail.tabs.erweitert",
+};
+
+/**
+ * The tab a `?tab=` value opens: its own slug, or the one an old slug maps
+ * to, when that tab is available for this medication; the overview
+ * otherwise.
+ */
+export function resolveTabSlug(
+ raw: string | null | undefined,
+ available: readonly TabSlug[],
+): TabSlug {
+ const slug =
+ raw && Object.hasOwn(LEGACY_TAB_SLUGS, raw) ? LEGACY_TAB_SLUGS[raw] : raw;
+ return available.find((tab) => tab === slug) ?? "overview";
+}
diff --git a/src/components/medications/glp1-medication-card.tsx b/src/components/medications/glp1-medication-card.tsx
index 5024217cd..f85b491e7 100644
--- a/src/components/medications/glp1-medication-card.tsx
+++ b/src/components/medications/glp1-medication-card.tsx
@@ -174,11 +174,10 @@ interface Glp1MedicationCardProps {
onEdit: (med: Glp1Medication) => void;
/**
* v1.15.18 — navigates to the medication detail page's Verlauf tab
- * (`/medications/{id}?tab=verlauf`), mirroring the generic medication
+ * (`/medications/{id}?tab=history`), mirroring the generic medication
* card. The parent owns the navigation.
*/
onOpenHistory: (med: Glp1Medication) => void;
- onLogSideEffect?: (med: Glp1Medication) => void;
/**
* v1.29.x — briefly ringed when this card is the Today digest's "Log
* dose" deep-link target (`/medications?highlight=`).
@@ -215,7 +214,6 @@ export function Glp1MedicationCard({
medication,
onEdit,
onOpenHistory,
- onLogSideEffect,
highlighted = false,
}: Glp1MedicationCardProps) {
const queryClient = useQueryClient();
@@ -478,19 +476,11 @@ export function Glp1MedicationCard({
// The four former header icon-buttons (open / edit / history /
// advanced) collapse into a SINGLE overflow menu so the GLP-1 card
// stays symmetric with the generic medication card. The card header
- // itself links to the detail page (the former chevron target). The
- // optional side-effect quick-log would fold in as a last menu item
- // when `onLogSideEffect` is wired, but the medications list page does
- // not wire it — side-effect logging lives on the detail page's
- // SideEffectsSection — so the menu renders the same items as the
- // generic card.
+ // itself links to the detail page (the former chevron target).
const headerActions = (
onEdit(medication)}
onOpenHistory={() => onOpenHistory(medication)}
- onLogSideEffect={
- onLogSideEffect ? () => onLogSideEffect(medication) : undefined
- }
/>
);
diff --git a/src/components/medications/medication-card-menu.tsx b/src/components/medications/medication-card-menu.tsx
index b26c9c0b1..b9a75fbf1 100644
--- a/src/components/medications/medication-card-menu.tsx
+++ b/src/components/medications/medication-card-menu.tsx
@@ -14,13 +14,8 @@
* target as the name tap); the structural wizard lives under the detail
* page's Erweitert → Lebenszyklus row.
*
- * The optional `onLogSideEffect` slot exists for the GLP-1 card to fold a
- * "Log side effect" item into the SAME menu, but it is NOT wired on the
- * medications list page — both card variants there receive only the
- * edit / history / advanced handlers, so the item never renders. Side-
- * effect logging lives on the detail page's ``. When
- * the slot is left undefined the two card variants stay byte-symmetric in
- * the header (one kebab, identical items).
+ * Side-effect logging lives on the detail page's ``, so
+ * the two card variants carry the same menu (one kebab, identical items).
*
* The trigger is a 44px ghost icon-button (WCAG 2.5.5); every
* `DropdownMenuItem` already carries `min-h-11` from the shadcn
@@ -33,14 +28,13 @@
*/
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
-import { History, MoreVertical, Pencil, Stethoscope } from "lucide-react";
+import { History, MoreVertical, Pencil } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
- DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTranslations } from "@/lib/i18n/context";
@@ -48,25 +42,19 @@ import { useTranslations } from "@/lib/i18n/context";
export interface MedicationCardMenuProps {
onEdit: () => void;
onOpenHistory: () => void;
- /** GLP-1-only: folds the side-effect quick-log into the same menu. */
- onLogSideEffect?: () => void;
}
export function MedicationCardMenu({
onEdit,
onOpenHistory,
- onLogSideEffect,
}: MedicationCardMenuProps) {
const { t } = useTranslations();
- // v1.36.x — noting a side effect is an admitted delegated write; editing the
- // medication and opening its history editor are not. When nothing in the
- // menu survives, the trigger goes with it rather than opening onto an empty
- // sheet. The card itself is untouched: no wash, no badge, no colour change.
- const { canWriteDomain, canManageDomain } = useRecordCapabilities();
- const canAddSideEffect = canWriteDomain("medications");
- const canManageMedications = canManageDomain("medications");
- const showSideEffect = onLogSideEffect != null && canAddSideEffect;
- if (!canManageMedications && !showSideEffect) return null;
+ // Editing the medication and opening its history editor are owner work, not
+ // delegated writes. Without either, the trigger goes too rather than opening
+ // onto an empty menu. The card itself is untouched: no wash, no badge, no
+ // colour change.
+ const { canManageDomain } = useRecordCapabilities();
+ if (!canManageDomain("medications")) return null;
return (
@@ -89,27 +77,14 @@ export function MedicationCardMenu({
className="w-56"
data-slot="medication-card-menu"
>
- {canManageMedications && (
- <>
-
-
- {t("common.edit")}
-
-
-
- {t("medications.detail.header.historyLabel")}
-
- >
- )}
- {showSideEffect && onLogSideEffect && (
- <>
- {canManageMedications && }
-
-
- {t("medications.glp1LogSideEffect")}
-
- >
- )}
+
+
+ {t("common.edit")}
+
+
+
+ {t("medications.detail.header.historyLabel")}
+
);
diff --git a/src/components/medications/medication-card.tsx b/src/components/medications/medication-card.tsx
index d3033e08b..2a790b525 100644
--- a/src/components/medications/medication-card.tsx
+++ b/src/components/medications/medication-card.tsx
@@ -147,7 +147,7 @@ interface MedicationCardProps {
onEdit: (med: Medication) => void;
/**
* v1.15.18 — navigates to the medication detail page's Verlauf tab
- * (`/medications/{id}?tab=verlauf`). The parent owns the navigation.
+ * (`/medications/{id}?tab=history`). The parent owns the navigation.
*/
onOpenHistory: (med: Medication) => void;
/**
diff --git a/src/components/settings/__tests__/dashboard-layout-section.test.tsx b/src/components/settings/__tests__/dashboard-layout-section.test.tsx
index a350585bc..7c62ed175 100644
--- a/src/components/settings/__tests__/dashboard-layout-section.test.tsx
+++ b/src/components/settings/__tests__/dashboard-layout-section.test.tsx
@@ -111,7 +111,7 @@ describe(" — tile + chart split", () => {
it("renders both column headers in German", () => {
const html = render( , "de");
expect(html).toContain("Kachel");
- expect(html).toContain("Chart");
+ expect(html).toContain("Diagramm");
});
it("paints both switch slots per widget", () => {
diff --git a/src/components/settings/__tests__/gesundheitsakte-section.test.tsx b/src/components/settings/__tests__/gesundheitsakte-section.test.tsx
index 5fbc0e1dc..e8ee6efc3 100644
--- a/src/components/settings/__tests__/gesundheitsakte-section.test.tsx
+++ b/src/components/settings/__tests__/gesundheitsakte-section.test.tsx
@@ -10,7 +10,7 @@ import { renderToStaticMarkup } from "react-dom/server";
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
- usePathname: () => "/settings/gesundheitsakte",
+ usePathname: () => "/settings/health-record",
}));
vi.mock("@tanstack/react-query", () => ({
@@ -43,19 +43,19 @@ describe(" — SSR smoke", () => {
// v1.18.6 — the visible heading (with the historic id) comes from
// the shared frame the route wraps the section in.
const html = render(
-
+
,
);
- expect(html).toContain('id="settings-section-gesundheitsakte-title"');
+ expect(html).toContain('id="settings-section-health-record-title"');
expect(html).toContain('data-testid="health-record-export-panel"');
// Raw key never leaks past i18n.
- expect(html).not.toContain("settings.sections.gesundheitsakte.");
+ expect(html).not.toContain("settings.sections.health-record.");
});
it("German locale resolves without leaking the raw key", () => {
const html = render( , "de");
expect(html).toContain('data-testid="health-record-export-panel"');
- expect(html).not.toContain("settings.sections.gesundheitsakte.");
+ expect(html).not.toContain("settings.sections.health-record.");
});
});
diff --git a/src/components/settings/__tests__/glucose-unit-select.test.tsx b/src/components/settings/__tests__/glucose-unit-select.test.tsx
index 8893020e8..49584a291 100644
--- a/src/components/settings/__tests__/glucose-unit-select.test.tsx
+++ b/src/components/settings/__tests__/glucose-unit-select.test.tsx
@@ -134,6 +134,19 @@ describe("Settings — GlucoseUnitSelect", () => {
expect(componentSource).toMatch(/queryKey: queryKeys\.authMe\(\)/);
});
+ // The targets panel takes its unit from the targets payload, not from the
+ // account, so a save that only refreshed `/me` left it on the old unit
+ // until the server cache turned over (up to ten minutes). Both ends have
+ // to let go of the old payload.
+ it("refreshes the targets payload on both ends", () => {
+ expect(componentSource).toMatch(/queryKey: queryKeys\.insightsTargets\(\)/);
+ const route = readFileSync(
+ join(process.cwd(), "src/app/api/auth/me/glucose-unit/route.ts"),
+ "utf8",
+ );
+ expect(route).toMatch(/!== next\) \{\s*invalidateUserProfile\(user\.id\);/);
+ });
+
it("is actually mounted on the settings page", () => {
// A control nobody can reach is the same defect as no control.
const accountSection = readFileSync(
diff --git a/src/components/settings/__tests__/layout-groups.test.ts b/src/components/settings/__tests__/layout-groups.test.ts
index cb8e2b113..ec49eb6c9 100644
--- a/src/components/settings/__tests__/layout-groups.test.ts
+++ b/src/components/settings/__tests__/layout-groups.test.ts
@@ -38,7 +38,7 @@ describe("layout-groups registry", () => {
"labs",
"illness",
"documents",
- "vorsorge",
+ "checkups",
]);
// generateStaticParams() shape.
expect(LAYOUT_GROUP_IDS.map((module) => ({ module }))).toEqual([
@@ -48,13 +48,13 @@ describe("layout-groups registry", () => {
{ module: "labs" },
{ module: "illness" },
{ module: "documents" },
- { module: "vorsorge" },
+ { module: "checkups" },
]);
});
it("guards unknown segments", () => {
expect(isLayoutGroupId("medications")).toBe(true);
- expect(isLayoutGroupId("vorsorge")).toBe(true);
+ expect(isLayoutGroupId("checkups")).toBe(true);
expect(isLayoutGroupId("nope")).toBe(false);
expect(isLayoutGroupId("")).toBe(false);
});
@@ -68,10 +68,10 @@ describe("layout-groups registry", () => {
expect(gate("mood")).toBe("not-a-group");
expect(gate("labs")).toBe("labs");
expect(gate("illness")).toBe("illness");
- // dashboard / insights / vorsorge are never gated.
+ // dashboard / insights / checkups are never gated.
expect(gate("dashboard")).toBeUndefined();
expect(gate("insights")).toBeUndefined();
- expect(gate("vorsorge")).toBeUndefined();
+ expect(gate("checkups")).toBeUndefined();
});
it("maps every id to a distinct section body and the canonical i18n keys", () => {
diff --git a/src/components/settings/__tests__/layout-section.test.tsx b/src/components/settings/__tests__/layout-section.test.tsx
index 97d29dadd..59e32d182 100644
--- a/src/components/settings/__tests__/layout-section.test.tsx
+++ b/src/components/settings/__tests__/layout-section.test.tsx
@@ -56,7 +56,7 @@ describe(" hub", () => {
"medications",
"labs",
"illness",
- "vorsorge",
+ "checkups",
]) {
expect(html).toContain(`href="/settings/layout/${id}"`);
}
@@ -66,12 +66,12 @@ describe(" hub", () => {
const html = render({ medications: false, labs: false });
expect(html).not.toContain('href="/settings/layout/medications"');
expect(html).not.toContain('href="/settings/layout/labs"');
- // Other modules unaffected; Vorsorge is never gated.
+ // Other modules unaffected; checkups are never gated.
expect(html).toContain('href="/settings/layout/illness"');
- expect(html).toContain('href="/settings/layout/vorsorge"');
+ expect(html).toContain('href="/settings/layout/checkups"');
});
- it("always lists dashboard, insights, and vorsorge (no module gate)", () => {
+ it("always lists dashboard, insights, and checkups (no module gate)", () => {
const html = render({
medications: false,
mood: false,
@@ -80,7 +80,7 @@ describe(" hub", () => {
});
expect(html).toContain('href="/settings/layout/dashboard"');
expect(html).toContain('href="/settings/layout/insights"');
- expect(html).toContain('href="/settings/layout/vorsorge"');
+ expect(html).toContain('href="/settings/layout/checkups"');
// v1.44 — mood is tag management and left Arrangement for Modules.
expect(html).not.toContain('href="/settings/layout/mood"');
});
diff --git a/src/components/settings/__tests__/section-frame-harness.tsx b/src/components/settings/__tests__/section-frame-harness.tsx
index 24bbf656f..929633021 100644
--- a/src/components/settings/__tests__/section-frame-harness.tsx
+++ b/src/components/settings/__tests__/section-frame-harness.tsx
@@ -28,7 +28,7 @@ const LAYOUT_CHILD_SLUGS = new Set([
"mood",
"labs",
"illness",
- "vorsorge",
+ "checkups",
]);
export function SettingsSectionFrame({
diff --git a/src/components/settings/__tests__/settings-shell.test.tsx b/src/components/settings/__tests__/settings-shell.test.tsx
index 117aa294f..94f2359f1 100644
--- a/src/components/settings/__tests__/settings-shell.test.tsx
+++ b/src/components/settings/__tests__/settings-shell.test.tsx
@@ -120,7 +120,7 @@ describe("SETTINGS_SECTION_SLUGS", () => {
// 301-redirects to `/settings/notifications`.
// v1.18.0 — `modules` ("Was du trackst") sits right after `account` as
// the single front door for enabling/disabling secondary domains.
- // v1.18.0 — `gesundheitsakte` (the full health-record export) lifts
+ // v1.18.0 — `health-record` (the full health-record export) lifts
// out of Export & Import into its own top-level entry, before `export`;
// `coach` gathers the Coach preference cards out of the AI section and
// sits right after `ai`.
@@ -133,7 +133,7 @@ describe("SETTINGS_SECTION_SLUGS", () => {
// first-class sections, right after `mood` (the three were standalone
// `ModuleSettingsFrame` pages).
// v1.18.7 — `sharing` (clinician share links) sits directly after
- // `gesundheitsakte`, before `export`: minting a read-only link to the
+ // `health-record`, before `export`: minting a read-only link to the
// health record belongs next to the health-record export.
expect([...SETTINGS_SECTION_SLUGS]).toEqual([
"account",
@@ -164,7 +164,7 @@ describe("SETTINGS_SECTION_SLUGS", () => {
"coach",
"api",
"mcp",
- "gesundheitsakte",
+ "health-record",
"export",
"advanced",
// v1.23 — "Data & Privacy" assembles the export / deletion / retention /
@@ -246,7 +246,7 @@ describe("", () => {
api: "connections",
mcp: "connections",
export: "data",
- gesundheitsakte: "data",
+ "health-record": "data",
privacy: "data",
advanced: "data",
about: "system",
@@ -397,16 +397,16 @@ describe("", () => {
// reachable. The server-side `/api/export/health-record` route remains
// the hard enforcement of the opt-out.
expect(renderShell({ active: "account" })).toContain(
- 'href="/settings/gesundheitsakte"',
+ 'href="/settings/health-record"',
);
expect(renderShell({ active: "account", modules: {} })).toContain(
- 'href="/settings/gesundheitsakte"',
+ 'href="/settings/health-record"',
);
const optedOut = renderShell({
active: "account",
modules: { doctorReport: false },
});
- expect(optedOut).toContain('href="/settings/gesundheitsakte"');
+ expect(optedOut).toContain('href="/settings/health-record"');
});
describe("inside somebody else's record", () => {
@@ -507,7 +507,7 @@ describe("", () => {
// form so we don't accidentally match a parser that double-escapes.
expect(html).toContain("API & tokens");
// v1.18.0 — the health record is its own top-level entry.
- expect(html).toContain('href="/settings/gesundheitsakte"');
+ expect(html).toContain('href="/settings/health-record"');
expect(html).toContain("Health record");
// v1.4.16: the consolidated Export section is a top-level
// entry in the sidebar; the link must be present in both locales.
diff --git a/src/components/settings/gesundheitsakte-section.tsx b/src/components/settings/gesundheitsakte-section.tsx
index e68ba9e8e..ba7443889 100644
--- a/src/components/settings/gesundheitsakte-section.tsx
+++ b/src/components/settings/gesundheitsakte-section.tsx
@@ -20,7 +20,7 @@
* "Freigabe" section) folds in here as a labelled "Sharing" group below the
* export panel: a time-boxed read-only link is a sharing face of the SAME
* health record. `/settings/sharing` 301-redirects to
- * `/settings/gesundheitsakte#sharing`. The backing model, the
+ * `/settings/health-record#sharing`. The backing model, the
* `/api/share-links` routes, and the public `/c/[token]` view are unchanged.
*/
diff --git a/src/components/settings/glucose-unit-select.tsx b/src/components/settings/glucose-unit-select.tsx
index 496e0322c..e98274c0b 100644
--- a/src/components/settings/glucose-unit-select.tsx
+++ b/src/components/settings/glucose-unit-select.tsx
@@ -29,9 +29,10 @@ import { resolveGlucoseUnit, type GlucoseUnit } from "@/lib/glucose";
* locale.
*
* Persistence mirrors the unit-system select: a change PATCHes
- * `/api/auth/me/glucose-unit` immediately and invalidates `queryKeys.authMe()`
- * plus `queryKeys.userGlucoseUnit()`, so the dashboard tiles, the glucose
- * insight page and the targets panel re-render on the next /me refetch.
+ * `/api/auth/me/glucose-unit` immediately and invalidates `queryKeys.authMe()`,
+ * so the dashboard tiles and the glucose insight page re-render on the next
+ * /me refetch, and `queryKeys.insightsTargets()`, whose payload states the
+ * target ranges in the unit it was built with.
*/
export function GlucoseUnitSelect({
isAuthenticated,
@@ -66,9 +67,7 @@ export function GlucoseUnitSelect({
onSuccess: () => {
toastWrittenOutcome("success", t("settings.dashboard.glucoseUnit.saved"));
queryClient.invalidateQueries({ queryKey: queryKeys.authMe() });
- queryClient.invalidateQueries({
- queryKey: queryKeys.userGlucoseUnit(),
- });
+ queryClient.invalidateQueries({ queryKey: queryKeys.insightsTargets() });
setOptimistic(null);
},
onError: (err) => {
diff --git a/src/components/settings/layout-groups.ts b/src/components/settings/layout-groups.ts
index d778a9ecc..d490ac658 100644
--- a/src/components/settings/layout-groups.ts
+++ b/src/components/settings/layout-groups.ts
@@ -99,9 +99,10 @@ export const LAYOUT_GROUPS: ReadonlyArray = [
Body: DocumentsSection,
},
{
- id: "vorsorge",
- titleKey: "settings.sections.layout.vorsorge.title",
- descriptionKey: "settings.sections.layout.vorsorge.description",
+ // The address read `vorsorge` in every language; that one redirects.
+ id: "checkups",
+ titleKey: "settings.sections.layout.checkups.title",
+ descriptionKey: "settings.sections.layout.checkups.description",
Body: VorsorgeSection,
},
];
diff --git a/src/components/settings/settings-anchor-forward.tsx b/src/components/settings/settings-anchor-forward.tsx
index a2ebbb68d..b47cf0b9d 100644
--- a/src/components/settings/settings-anchor-forward.tsx
+++ b/src/components/settings/settings-anchor-forward.tsx
@@ -41,7 +41,7 @@ export const SETTINGS_ANCHOR_FORWARDS: Readonly<
"/settings/layout/medications": {
"injection-sites": "/settings/modules/medications#injection-sites",
},
- "/settings/layout/vorsorge": {
+ "/settings/layout/checkups": {
"screening-hints": "/settings/anamnesis#screening-hints",
"vorsorge-reminders": "/settings/notifications#vorsorge-reminders",
},
diff --git a/src/components/settings/settings-section-loader.tsx b/src/components/settings/settings-section-loader.tsx
index 2c0faae98..6f1e6b187 100644
--- a/src/components/settings/settings-section-loader.tsx
+++ b/src/components/settings/settings-section-loader.tsx
@@ -110,7 +110,7 @@ const LOADERS: Record<
import("@/components/settings/api-section").then((m) => m.ApiSection),
mcp: () =>
import("@/components/settings/mcp-section").then((m) => m.McpSection),
- gesundheitsakte: () =>
+ "health-record": () =>
import("@/components/settings/gesundheitsakte-section").then(
(m) => m.GesundheitsakteSection,
),
@@ -152,7 +152,7 @@ const SECTIONS: Record> = {
}),
api: dynamic(LOADERS.api, { loading: () => }),
mcp: dynamic(LOADERS.mcp, { loading: () => }),
- gesundheitsakte: dynamic(LOADERS.gesundheitsakte, {
+ "health-record": dynamic(LOADERS["health-record"], {
loading: () => ,
}),
export: dynamic(LOADERS.export, { loading: () => }),
diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx
index a2bcb5185..b5f48b217 100644
--- a/src/components/settings/settings-shell.tsx
+++ b/src/components/settings/settings-shell.tsx
@@ -257,8 +257,8 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
// Not module-gated: the health record is a flagship export, and its only
// entry point must not vanish behind the `doctorReport` opt-out.
{
- slug: "gesundheitsakte",
- titleKey: "settings.sections.gesundheitsakte.title",
+ slug: "health-record",
+ titleKey: "settings.sections.health-record.title",
icon: FileHeart,
group: "data",
},
diff --git a/src/components/settings/unit-preference-select.tsx b/src/components/settings/unit-preference-select.tsx
index 0ba6d4712..b8baf5b9c 100644
--- a/src/components/settings/unit-preference-select.tsx
+++ b/src/components/settings/unit-preference-select.tsx
@@ -23,9 +23,8 @@ type UnitPreference = "metric" | "imperial";
* picker rather than in a standalone card (it replaces the retired
* ``). Persistence is unchanged: a change PATCHes
* `/api/auth/me/unit-preference` immediately and invalidates
- * `queryKeys.authMe()` + `queryKeys.userUnitPreference()` so every chart
- * display transform (km/h vs mph, km vs mi) re-renders on the next /me
- * refetch — no manual reload, no contract change.
+ * `queryKeys.authMe()`, the account payload every chart display transform
+ * (km/h vs mph, km vs mi) reads, so they re-render on the next /me refetch.
*/
export function UnitPreferenceSelect({
isAuthenticated,
@@ -59,9 +58,6 @@ export function UnitPreferenceSelect({
onSuccess: () => {
toastWrittenOutcome("success", t("settings.dashboard.units.saved"));
queryClient.invalidateQueries({ queryKey: queryKeys.authMe() });
- queryClient.invalidateQueries({
- queryKey: queryKeys.userUnitPreference(),
- });
setOptimistic(null);
},
onError: (err) => {
diff --git a/src/lib/README.md b/src/lib/README.md
index fcffe12e9..50b41508a 100644
--- a/src/lib/README.md
+++ b/src/lib/README.md
@@ -30,4 +30,4 @@ The server and runtime toolkit. Everything that is not a page, component, or rou
## Conventions
-Kebab-case filenames. See [`../../CLAUDE.md`](../../CLAUDE.md) "Code conventions" and "Security-relevant patterns"; the critical-files map there cites the exact entry points. Most modules carry tests in a sibling `__tests__/`.
+Kebab-case filenames. Most modules carry tests in a sibling `__tests__/`.
diff --git a/src/lib/cache/invalidate.ts b/src/lib/cache/invalidate.ts
index 0081f1eea..4df20d5a0 100644
--- a/src/lib/cache/invalidate.ts
+++ b/src/lib/cache/invalidate.ts
@@ -242,10 +242,11 @@ export function invalidateUserHealthScore(userId: string): void {
* on the very next read.
*
* Callers: `/api/user/thresholds` PUT + DELETE, `applyProfileUpdate`
- * (only when heightCm / dateOfBirth / gender is in the update set), and
- * `/api/auth/me/source-priority` PUT. The unit-preference toggle is NOT
- * wired in: it only drives the client-side display transform, no
- * server-cached payload reads it.
+ * (only when heightCm / dateOfBirth / gender is in the update set),
+ * `/api/auth/me/source-priority` PUT, and `/api/auth/me/glucose-unit` PATCH
+ * (the targets payload states glucose in the account's unit). The
+ * unit-preference toggle is NOT wired in: it only drives the client-side
+ * display transform, no server-cached payload reads it.
*/
export function invalidateUserProfile(userId: string): void {
caches.insightsTargets.deleteByPrefix(userId);
diff --git a/src/lib/command-palette/settings-cards.ts b/src/lib/command-palette/settings-cards.ts
index cd8e71f23..45b4c7e05 100644
--- a/src/lib/command-palette/settings-cards.ts
+++ b/src/lib/command-palette/settings-cards.ts
@@ -171,17 +171,17 @@ export const SETTINGS_CARDS: ReadonlyArray = [
{ section: "integrations", anchor: "webhook", titleKey: "settings.webhook" },
// ── Health record ──
{
- section: "gesundheitsakte",
+ section: "health-record",
anchor: "health-record",
titleKey: "settings.healthRecord.title",
},
{
- section: "gesundheitsakte",
+ section: "health-record",
anchor: "share-link-create",
titleKey: "settings.sharing.createTitle",
},
{
- section: "gesundheitsakte",
+ section: "health-record",
anchor: "share-links-active",
titleKey: "settings.sharing.activeTitle",
},
@@ -459,7 +459,7 @@ export const SETTINGS_LAYOUT_PAGES: ReadonlyArray<{
titleKey: "settings.sections.layout.documents.title",
module: "inboundDocuments",
},
- { slug: "vorsorge", titleKey: "settings.sections.layout.vorsorge.title" },
+ { slug: "checkups", titleKey: "settings.sections.layout.checkups.title" },
];
/**
diff --git a/src/lib/export/backup-verdict.ts b/src/lib/export/backup-verdict.ts
deleted file mode 100644
index 6fbaf8ea6..000000000
--- a/src/lib/export/backup-verdict.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * The backup verdict for one schema model, read off the lists in
- * `./backup-plan.ts`. Kept beside it rather than in it so the plan stays the
- * lists and their reasons, and the one question asked of them lives here.
- */
-import {
- AUTH_MODELS_OUT_OF_SCOPE,
- BACKED_UP_MODELS,
- DERIVED_MODELS,
- NOT_IN_BACKUP_MODELS,
-} from "./backup-plan";
-
-export type BackupVerdict = "BACKED_UP" | "DERIVED" | "NOT_IN_BACKUP" | "AUTH";
-
-/**
- * The verdict for a model, or `null` when the schema carries a model this file
- * has never ruled on. The guard turns that `null` into a failing test, which
- * is the whole point: a new model gets a decision, not a default.
- */
-export 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;
-}
diff --git a/src/lib/http/retired-routes.ts b/src/lib/http/retired-routes.ts
index 22580e55c..d86cd6ccd 100644
--- a/src/lib/http/retired-routes.ts
+++ b/src/lib/http/retired-routes.ts
@@ -62,8 +62,7 @@
* other twenty-two are admin-console routes, operator diagnostics, aggregators
* superseded in place, and the moodLog bridge — surfaces reachable only from
* the operator's own browser, which ships with the server and reads no
- * contract to find out what it talks to. `moodlog-removal-guard.test.ts`
- * separately holds that bridge removed. Every entry after those five was
+ * contract to find out what it talks to. Every entry after those five was
* written by the release that removed the path, which is where a tombstone
* belongs.
*
diff --git a/src/lib/insights/__tests__/metric-catalog.test.ts b/src/lib/insights/__tests__/metric-catalog.test.ts
new file mode 100644
index 000000000..d62772cb3
--- /dev/null
+++ b/src/lib/insights/__tests__/metric-catalog.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, it } from "vitest";
+
+import { METRIC_CATALOG_ENTRIES } from "../metric-catalog";
+
+/**
+ * The metric catalog offers "Connect a source" for a metric without readings
+ * and the metric's own entry action for one only a person can log. Ketones
+ * shipped without joining the manual list, so their row sent people to the
+ * integrations page, where nothing delivers ketones.
+ */
+describe("metric catalog", () => {
+ it("offers ketones as a manual entry, not a source to connect", () => {
+ const ketones = METRIC_CATALOG_ENTRIES.find((e) => e.id === "ketones");
+ expect(ketones).toBeDefined();
+ expect(ketones?.connectable).toBe(false);
+ expect(ketones?.manualHref).toBe("/measurements?add=BLOOD_KETONES");
+ expect(ketones?.manualCtaKey).toBe("insights.bloodKetones.emptyState.cta");
+ });
+
+ it("gives every metric that cannot be connected a way to log it", () => {
+ const manualOnly = METRIC_CATALOG_ENTRIES.filter(
+ (e) => e.kind === "metric" && !e.connectable,
+ );
+ expect(manualOnly.length).toBeGreaterThan(0);
+ for (const entry of manualOnly) {
+ expect(entry.manualHref, entry.id).toBeTruthy();
+ expect(entry.manualCtaKey, entry.id).toBeTruthy();
+ }
+ });
+});
diff --git a/src/lib/insights/metric-catalog.ts b/src/lib/insights/metric-catalog.ts
index 5df3b6061..12b3611f7 100644
--- a/src/lib/insights/metric-catalog.ts
+++ b/src/lib/insights/metric-catalog.ts
@@ -109,12 +109,13 @@ const SOURCE_KEY_OVERRIDES: Partial> = {
/**
* Metrics with no device path at all — manual entry is the only source.
* Mood and medications are inherently self-report (no sensor produces a
- * mood score or an adherence event). Pain, grip strength, and waist-to-
- * height ratio are ALSO manual-only despite sitting beside device-fed
- * siblings in their group: none carries an Apple Health mapping
+ * mood score or an adherence event). Pain, grip strength, waist-to-height
+ * ratio and blood ketones are ALSO manual-only despite sitting beside
+ * device-fed siblings in their group: none carries an Apple Health mapping
* (`apple-health-mapping.ts`) or a Withings/wearable source — pain is a
- * 0–10 self-rating, grip strength is read off a hand dynamometer, and
- * waist-to-height is logged directly (not derived like BMI). Cross-
+ * 0–10 self-rating, grip strength is read off a hand dynamometer,
+ * waist-to-height is logged directly (not derived like BMI), and ketones
+ * come off a fingerstick meter that no integration reads. Cross-
* checked against every routed sub-page's `emptyStateCtaType` +
* `emptyState.description` copy so this list can't silently drift from
* what the metric's own empty state actually offers.
@@ -125,6 +126,7 @@ const MANUAL_ONLY_SLUGS: ReadonlySet = new Set([
"pain",
"grip-strength",
"waist-to-height",
+ "ketones",
]);
const MANUAL_CTA: Partial<
@@ -147,6 +149,10 @@ const MANUAL_CTA: Partial<
href: "/measurements?add=WAIST_TO_HEIGHT",
ctaKey: "insights.waistToHeight.emptyState.cta",
},
+ ketones: {
+ href: "/measurements?add=BLOOD_KETONES",
+ ctaKey: "insights.bloodKetones.emptyState.cta",
+ },
};
/**
diff --git a/src/lib/jobs/__tests__/medication-low-stock.test.ts b/src/lib/jobs/__tests__/medication-low-stock.test.ts
index 7e0f6b33e..674632eaf 100644
--- a/src/lib/jobs/__tests__/medication-low-stock.test.ts
+++ b/src/lib/jobs/__tests__/medication-low-stock.test.ts
@@ -310,7 +310,7 @@ describe("runMedicationLowStockTick", () => {
thresholdDays: 7,
unitsRemaining: 5,
// Deep link lands on the detail page's Bestand (supply) tab.
- url: "/medications/med-1?tab=bestand",
+ url: "/medications/med-1?tab=supply",
}),
}),
);
diff --git a/src/lib/jobs/__tests__/morning-digest-refresh.test.ts b/src/lib/jobs/__tests__/morning-digest-refresh.test.ts
index 64dd37bf6..6c365eb93 100644
--- a/src/lib/jobs/__tests__/morning-digest-refresh.test.ts
+++ b/src/lib/jobs/__tests__/morning-digest-refresh.test.ts
@@ -6,7 +6,7 @@ import {
COMPREHENSIVE_WARM_CEILING_MS,
comprehensiveWarmBudgetMs,
} from "../model-time";
-import { QUEUE_RUNTIME } from "../queue-runtime";
+import { QUEUE_RUNTIME } from "@/__tests__/helpers/queue-runtime";
import { COMPREHENSIVE_MODEL_CALLS } from "@/lib/ai/ai-budgets";
import { AI_RESPONSE_TIMEOUT_MAX_SECONDS } from "@/lib/ai/effective-timeout";
import type { GenerateOutcome } from "@/lib/insights/comprehensive-generate";
diff --git a/src/lib/jobs/__tests__/restore-drill.test.ts b/src/lib/jobs/__tests__/restore-drill.test.ts
index f72469026..fe6a05f0c 100644
--- a/src/lib/jobs/__tests__/restore-drill.test.ts
+++ b/src/lib/jobs/__tests__/restore-drill.test.ts
@@ -8,7 +8,7 @@ import {
RESTORE_DRILL_QUEUE,
RESTORE_DRILL_SEND_OPTIONS,
} from "@/lib/jobs/restore-drill-queue";
-import { QUEUE_RUNTIME } from "../queue-runtime";
+import { QUEUE_RUNTIME } from "@/__tests__/helpers/queue-runtime";
vi.mock("../report-worker-error", () => ({
reportWorkerError: vi.fn().mockResolvedValue(undefined),
diff --git a/src/lib/jobs/backup/__tests__/measurement-page.test.ts b/src/lib/jobs/backup/__tests__/measurement-page.test.ts
deleted file mode 100644
index fa668c0ac..000000000
--- a/src/lib/jobs/backup/__tests__/measurement-page.test.ts
+++ /dev/null
@@ -1,238 +0,0 @@
-/**
- * Round-trip + paging contract for the backup measurement reader.
- *
- * Runs without a database: the keyset pager takes a `fetchPage` callback,
- * so an in-memory fixture stands in for Prisma. This pins the two things
- * the heap-bounding refactor must not break:
- * 1. the keyset scan visits every row exactly once, in id order,
- * across page boundaries and the exact-multiple edge case;
- * 2. a measurement survives the full serialize → encrypt-blob shape →
- * `parseBackupPayload` → restore-mapping round-trip unchanged,
- * including a soft-deleted (tombstoned) row, which both writers keep.
- */
-import { describe, it, expect } from "vitest";
-import {
- collectPagedMeasurements,
- toWeeklyBackupMeasurement,
- sortWeeklyMeasurementsDesc,
- type WeeklyMeasurementRow,
-} from "@/lib/jobs/backup/measurement-page";
-import { parseBackupPayload } from "@/lib/validations/backup";
-
-/** Build an id-ascending fixture; `id` is zero-padded so string order == insert order. */
-function makeRows(n: number): WeeklyMeasurementRow[] {
- return Array.from({ length: n }, (_, i) => ({
- id: `m${String(i).padStart(4, "0")}`,
- type: "WEIGHT",
- value: 70 + i,
- unit: "kg",
- source: "MANUAL",
- measuredAt: new Date(Date.UTC(2026, 0, 1 + i, 8, 0, 0)),
- notes: i % 2 === 0 ? `note ${i}` : null,
- // notesEncrypted left null so readNote returns the plaintext fallback
- // and the pure test needs no encryption key.
- notesEncrypted: null,
- }));
-}
-
-/** In-memory keyset fetcher over an id-sorted fixture. */
-function fakeFetcher(rows: WeeklyMeasurementRow[]) {
- const sorted = [...rows].sort((a, b) =>
- a.id < b.id ? -1 : a.id > b.id ? 1 : 0,
- );
- return (afterId: string | null, take: number) => {
- const start = afterId ? sorted.findIndex((r) => r.id === afterId) + 1 : 0;
- return Promise.resolve(sorted.slice(start, start + take));
- };
-}
-
-describe("collectPagedMeasurements", () => {
- it("visits every row exactly once, in id order, across pages", async () => {
- const rows = makeRows(25);
- const out = await collectPagedMeasurements({
- fetchPage: fakeFetcher(rows),
- project: (r) => r.id,
- pageSize: 10,
- });
- expect(out).toEqual(rows.map((r) => r.id));
- expect(new Set(out).size).toBe(out.length);
- });
-
- it("terminates on the exact-multiple boundary without dropping rows", async () => {
- const rows = makeRows(20);
- const out = await collectPagedMeasurements({
- fetchPage: fakeFetcher(rows),
- project: (r) => r.id,
- pageSize: 10,
- });
- expect(out).toHaveLength(20);
- expect(out[out.length - 1]).toBe("m0019");
- });
-
- it("handles an empty set", async () => {
- const out = await collectPagedMeasurements({
- fetchPage: fakeFetcher([]),
- project: (r) => r.id,
- pageSize: 10,
- });
- expect(out).toEqual([]);
- });
-});
-
-describe("weekly backup measurement round-trip", () => {
- it("serialize → parseBackupPayload → restore-map equals originals", async () => {
- const rows: WeeklyMeasurementRow[] = [
- {
- id: "m0001",
- type: "WEIGHT",
- value: 82.4,
- unit: "kg",
- source: "MANUAL",
- measuredAt: new Date("2026-02-10T07:30:00.000Z"),
- notes: "morning",
- notesEncrypted: null,
- },
- {
- id: "m0002",
- type: "PULSE",
- value: 58,
- unit: "bpm",
- source: "WITHINGS",
- measuredAt: new Date("2026-02-11T06:00:00.000Z"),
- notes: null,
- notesEncrypted: null,
- },
- {
- // A tombstoned row: the weekly writer applies no deletedAt filter,
- // so a soft-deleted measurement is still backed up. The narrow
- // weekly projection does not carry deletedAt (the restore does not
- // recreate it), but the row must still appear in the payload.
- id: "m0003",
- type: "BLOOD_GLUCOSE",
- value: 95,
- unit: "mg/dL",
- source: "IMPORT",
- measuredAt: new Date("2026-02-12T12:00:00.000Z"),
- notes: "fasting",
- notesEncrypted: null,
- },
- ];
-
- const measurements = sortWeeklyMeasurementsDesc(
- await collectPagedMeasurements({
- fetchPage: fakeFetcher(rows),
- project: toWeeklyBackupMeasurement,
- pageSize: 2,
- }),
- );
-
- // Same envelope the writer produces, then parse it back exactly as the
- // admin restore route does.
- const backupJson = JSON.stringify({
- schemaVersion: "1",
- exportedAt: new Date("2026-02-13T00:00:00.000Z").toISOString(),
- userId: "user-1",
- measurements,
- medications: [],
- intakeEvents: [],
- moodEntries: [],
- });
- const payload = parseBackupPayload(backupJson);
-
- expect(payload.measurements).toHaveLength(rows.length);
-
- // measuredAt-desc ordering preserved (newest first).
- expect(payload.measurements.map((m) => m.measuredAt)).toEqual([
- "2026-02-12T12:00:00.000Z",
- "2026-02-11T06:00:00.000Z",
- "2026-02-10T07:30:00.000Z",
- ]);
-
- // Every original row survives the round-trip by value (the tombstoned
- // m0003 included).
- for (const original of rows) {
- const restored = payload.measurements.find(
- (m) => m.measuredAt === original.measuredAt.toISOString(),
- );
- expect(restored, `row ${original.id} missing from payload`).toBeDefined();
- expect(restored!.type).toBe(original.type);
- expect(restored!.value).toBe(original.value);
- expect(restored!.unit).toBe(original.unit);
- expect(restored!.source).toBe(original.source);
- expect(restored!.notes ?? null).toBe(original.notes ?? null);
- }
- });
-});
-
-describe("offhost backup measurement round-trip", () => {
- it("full-row JSON dump → parse preserves all columns incl. soft-deleted", async () => {
- // The off-host DR dump keeps the full row shape (identity projection).
- // Mirror the writer's select output for a live row and a tombstoned row.
- const liveRow = {
- id: "m0001",
- userId: "user-1",
- type: "WEIGHT",
- value: 80.1,
- valueMin: null,
- valueMax: null,
- unit: "kg",
- source: "MANUAL",
- measuredAt: new Date("2026-03-01T07:00:00.000Z"),
- notes: null,
- notesEncrypted: null,
- externalId: null,
- externalSourceVersion: null,
- glucoseContext: null,
- sleepStage: null,
- rhythmClassification: null,
- deviceType: "scale",
- syncVersion: 1,
- deletedAt: null,
- createdAt: new Date("2026-03-01T07:00:01.000Z"),
- updatedAt: new Date("2026-03-01T07:00:01.000Z"),
- };
- const tombstonedRow = {
- ...liveRow,
- id: "m0002",
- value: 81.2,
- measuredAt: new Date("2026-03-02T07:00:00.000Z"),
- deletedAt: new Date("2026-03-03T09:00:00.000Z"),
- };
-
- const measurements = await collectPagedMeasurements({
- fetchPage: fakeFetcher([
- liveRow,
- tombstonedRow,
- ] as unknown as WeeklyMeasurementRow[]),
- project: (row) => row,
- pageSize: 1,
- });
-
- const payload = JSON.parse(
- JSON.stringify({
- exportedAt: new Date("2026-03-04T00:00:00.000Z").toISOString(),
- userId: "user-1",
- measurements,
- medications: [],
- intakeEvents: [],
- moodEntries: [],
- cycleProfile: null,
- cycles: [],
- cycleDayLogs: [],
- }),
- ) as { measurements: Array> };
-
- expect(payload.measurements).toHaveLength(2);
-
- // The soft-deleted row survives with its tombstone instant intact —
- // the DR snapshot must include it.
- const restoredTombstone = payload.measurements.find(
- (m) => m.id === "m0002",
- )!;
- expect(restoredTombstone.deletedAt).toBe("2026-03-03T09:00:00.000Z");
-
- // Every scalar column the writer selected is present on the dump.
- const expectedKeys = Object.keys(liveRow).sort();
- expect(Object.keys(payload.measurements[0]).sort()).toEqual(expectedKeys);
- });
-});
diff --git a/src/lib/jobs/backup/measurement-page.ts b/src/lib/jobs/backup/measurement-page.ts
deleted file mode 100644
index abbf1ab95..000000000
--- a/src/lib/jobs/backup/measurement-page.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * Keyset-paged measurement reader shared by the two backup writers.
- *
- * Both writers — the weekly on-host `DataBackup` snapshot
- * (`reminder/backup-handlers.ts`) and the nightly off-host DR dump
- * (`offhost-backup.ts`) — used to load a user's ENTIRE measurement set
- * as full ORM rows into one array and then run a single
- * `JSON.stringify` over the whole payload. On a heavy multi-year tenant
- * that holds every measurement row plus the giant serialized string in
- * memory at the same instant, which is the dominant heap spike of either
- * job and scales linearly with history depth.
- *
- * This reader walks the set in id-ascending keyset pages: at most one
- * page of rows is resident at a time, and each page is projected to its
- * compact backup shape before the next page is fetched. Transient peak
- * heap is bounded by the page size, not by the size of the user's
- * history. The accumulated output is the compact projection the payload
- * needs anyway — not the wide ORM rows.
- *
- * Keyset (cursor on `id`) rather than `skip`/`take` offset paging so the
- * scan stays O(rows) instead of O(rows^2) as the offset grows, and so a
- * concurrent insert can't shift rows across a page boundary.
- */
-import { readNote } from "@/lib/crypto/note-cipher";
-
-/**
- * Page size for the backup keyset scan. Large enough that the per-page
- * round-trip overhead stays negligible, small enough that a single page
- * of rows is a bounded slice of any tenant's history.
- */
-export const MEASUREMENT_BACKUP_PAGE_SIZE = 2000;
-
-/**
- * Walk a keyset-paged result set, projecting each row to its compact
- * backup shape as it arrives. `fetchPage` returns the next page of rows
- * after `afterId` (exclusive, id-ascending), bounded to `take` rows;
- * returning fewer than `take` rows signals the final page. The generic
- * stays free of any Prisma dependency so the paging contract is unit
- * testable without a database.
- */
-export async function collectPagedMeasurements<
- Row extends { id: string },
- Out,
->(args: {
- fetchPage: (afterId: string | null, take: number) => Promise;
- project: (row: Row) => Out;
- pageSize?: number;
-}): Promise {
- const { fetchPage, project } = args;
- const pageSize = args.pageSize ?? MEASUREMENT_BACKUP_PAGE_SIZE;
- const out: Out[] = [];
- let afterId: string | null = null;
- for (;;) {
- const page = await fetchPage(afterId, pageSize);
- if (page.length === 0) break;
- for (const row of page) out.push(project(row));
- if (page.length < pageSize) break;
- afterId = page[page.length - 1].id;
- }
- return out;
-}
-
-/**
- * Narrow row shape the weekly snapshot reads — exactly the columns the
- * admin restore path recreates a measurement from
- * (`api/admin/backups/[id]/restore`): `type`, `value`, `unit`, `source`,
- * `measuredAt`, plus the note pair so the decrypted note rides into the
- * (whole-blob-encrypted) payload. `id` rides along only as the keyset
- * cursor.
- */
-export interface WeeklyMeasurementRow {
- id: string;
- type: string;
- value: number;
- unit: string;
- source: string;
- measuredAt: Date;
- notes: string | null;
- notesEncrypted: Uint8Array | null;
-}
-
-/** Compact measurement shape the weekly `DataBackup` JSON carries. */
-export interface WeeklyBackupMeasurement {
- type: string;
- value: number;
- unit: string;
- measuredAt: string;
- source: string;
- notes: string | null;
-}
-
-/**
- * Project a narrow measurement row into the weekly backup shape. The
- * note is decrypted here so the restore re-encrypts on re-insert — the
- * same contract the previous inline writer used. Shared with the writer
- * so the round-trip test exercises the real projection.
- */
-export function toWeeklyBackupMeasurement(
- row: WeeklyMeasurementRow,
-): WeeklyBackupMeasurement {
- return {
- type: row.type,
- value: row.value,
- unit: row.unit,
- measuredAt: row.measuredAt.toISOString(),
- source: row.source,
- notes: readNote(row.notesEncrypted, row.notes),
- };
-}
-
-/**
- * Order the collected weekly measurements newest-first to match the
- * pre-paging `orderBy: { measuredAt: "desc" }` output. The keyset scan
- * reads id-ascending for a stable cursor; this restores the historical
- * array order. Operates on the compact projections, so it does not
- * reintroduce the wide-row heap spike. ISO-8601 strings sort
- * lexicographically in chronological order, so a string compare is the
- * chronological compare.
- */
-export function sortWeeklyMeasurementsDesc(
- measurements: WeeklyBackupMeasurement[],
-): WeeklyBackupMeasurement[] {
- return measurements.sort((a, b) =>
- a.measuredAt < b.measuredAt ? 1 : a.measuredAt > b.measuredAt ? -1 : 0,
- );
-}
diff --git a/src/lib/jobs/insight-pregenerate.ts b/src/lib/jobs/insight-pregenerate.ts
index a56101dca..59f453448 100644
--- a/src/lib/jobs/insight-pregenerate.ts
+++ b/src/lib/jobs/insight-pregenerate.ts
@@ -46,7 +46,7 @@
* provider-less account costs one cheap chain-resolve and no LLM call.
*
* Recurring pg-boss task — never runs inside an HTTP request and never
- * shells out to `tsx` (CLAUDE.md DO-NOTs).
+ * shells out to `tsx` (the production image has none).
*/
import { activeAccount } from "@/lib/account-deletion/active-account";
import { createHash, randomUUID } from "node:crypto";
diff --git a/src/lib/jobs/insight-status-generate.ts b/src/lib/jobs/insight-status-generate.ts
index 33b525017..a3ea4fa23 100644
--- a/src/lib/jobs/insight-status-generate.ts
+++ b/src/lib/jobs/insight-status-generate.ts
@@ -16,7 +16,7 @@
* polls the read-only GET until it lands.
*
* Recurring/event pg-boss task — never runs inside an HTTP request and
- * never shells out to `tsx` (CLAUDE.md DO-NOTs). Registered in the
+ * never shells out to `tsx` (the production image has none). Registered in the
* reminder-worker `allQueues` loop + a `boss.work` handler; a queue built
* but never registered silently never drains (the v1.4.37 W10 incident),
* so a guard test pins the registration.
diff --git a/src/lib/jobs/medication-low-stock.ts b/src/lib/jobs/medication-low-stock.ts
index 16a8b2571..3d8f28435 100644
--- a/src/lib/jobs/medication-low-stock.ts
+++ b/src/lib/jobs/medication-low-stock.ts
@@ -621,7 +621,7 @@ export async function runMedicationLowStockTick(
// the service-worker payload's click target. Lands on
// the detail page's Bestand (supply) tab, the surface
// the alert is about.
- url: `/medications/${med.id}?tab=bestand`,
+ url: `/medications/${med.id}?tab=supply`,
},
});
if (!outcome.dispatched) {
diff --git a/src/lib/jobs/period-narrative-warm.ts b/src/lib/jobs/period-narrative-warm.ts
index f01162781..065154899 100644
--- a/src/lib/jobs/period-narrative-warm.ts
+++ b/src/lib/jobs/period-narrative-warm.ts
@@ -17,7 +17,7 @@
* so a provider-less / sparse account costs only the context build.
*
* Recurring pg-boss task — never runs inside an HTTP request, never shells out
- * to `tsx` (CLAUDE.md DO-NOTs).
+ * to `tsx` (the production image has none).
*/
import { ACTIVE_ACCOUNT } from "@/lib/account-deletion/active-account";
import type { PrismaClient } from "@/generated/prisma/client";
diff --git a/src/lib/labs/__tests__/homa-ir.test.ts b/src/lib/labs/__tests__/homa-ir.test.ts
deleted file mode 100644
index 205827936..000000000
--- a/src/lib/labs/__tests__/homa-ir.test.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { describe, it, expect } from "vitest";
-import { computeHomaIr, classifyHomaIr } from "@/lib/labs/homa-ir";
-
-describe("computeHomaIr", () => {
- it("computes the conventional-unit formula (glucose × insulin ÷ 405)", () => {
- // 90 mg/dL × 5 µIU/mL ÷ 405 = 1.111…
- expect(computeHomaIr(90, 5)).toBeCloseTo(1.1111, 3);
- });
-
- it("matches the SI form within rounding (mmol/L × insulin ÷ 22.5)", () => {
- // 100 mg/dL = 5.556 mmol/L; SI: 5.556 × 8 ÷ 22.5 ≈ 1.975; conv: 100×8÷405 ≈ 1.975
- const conv = computeHomaIr(100, 8)!;
- const si = (5.5556 * 8) / 22.5;
- expect(conv).toBeCloseTo(si, 2);
- });
-
- it("returns null for missing or non-positive inputs", () => {
- expect(computeHomaIr(null, 5)).toBeNull();
- expect(computeHomaIr(90, undefined)).toBeNull();
- expect(computeHomaIr(0, 5)).toBeNull();
- expect(computeHomaIr(90, -1)).toBeNull();
- expect(computeHomaIr(Number.NaN, 5)).toBeNull();
- });
-});
-
-describe("classifyHomaIr", () => {
- it("places values in the documented bands", () => {
- expect(classifyHomaIr(0.8)).toBe("optimal");
- expect(classifyHomaIr(1.5)).toBe("intermediate");
- expect(classifyHomaIr(2.5)).toBe("elevated");
- expect(classifyHomaIr(3.5)).toBe("high");
- });
-});
diff --git a/src/lib/labs/biomarker-catalog.ts b/src/lib/labs/biomarker-catalog.ts
index 3cb8058c1..40255cf41 100644
--- a/src/lib/labs/biomarker-catalog.ts
+++ b/src/lib/labs/biomarker-catalog.ts
@@ -104,8 +104,7 @@ export const BIOMARKER_CATALOG: readonly BiomarkerSeed[] = [
upperBound: 5.7,
},
// v1.25 — longevity panel: fasting insulin (metabolically-optimal often
- // cited < 8 µIU/mL; typical reference ~2.5–13). HOMA-IR is derived from
- // fasting glucose × insulin (see `homa-ir.ts`), not a stored catalog marker.
+ // cited < 8 µIU/mL; typical reference ~2.5–13).
{
slug: "fasting-insulin",
panel: "glucose",
diff --git a/src/lib/labs/homa-ir.ts b/src/lib/labs/homa-ir.ts
deleted file mode 100644
index 289f15e9d..000000000
--- a/src/lib/labs/homa-ir.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * v1.25 — HOMA-IR (Homeostatic Model Assessment of Insulin Resistance).
- *
- * A computed longevity marker derived from fasting glucose + fasting insulin —
- * NOT a stored biomarker catalog entry (BMI's derived-value precedent). Server-
- * authoritative per the parity rule: compute it here, never recompute on the
- * client. Pure + side-effect-free so it is trivially testable.
- *
- * Conventional-unit formula (fasting glucose mg/dL × fasting insulin µIU/mL ÷
- * 405) and the SI form (mmol/L × µIU/mL ÷ 22.5) are equivalent
- * (mg/dL ÷ 18 = mmol/L; 405 = 22.5 × 18). Population-dependent interpretive
- * bands (optimal < 1.0, early IR > 1.9, significant IR > 2.9) are applied at the
- * display edge, never asserted here. [HOMA-IR, PMC5033570].
- */
-
-/** Interpretive band for a HOMA-IR index value (population-dependent guidance). */
-export type HomaIrBand = "optimal" | "intermediate" | "elevated" | "high";
-
-/**
- * Compute HOMA-IR from fasting glucose (mg/dL) and fasting insulin (µIU/mL).
- * Returns `null` when either input is missing or non-positive (the index is
- * undefined for a zero/negative reading).
- */
-export function computeHomaIr(
- fastingGlucoseMgDl: number | null | undefined,
- fastingInsulinUiuMl: number | null | undefined,
-): number | null {
- if (
- fastingGlucoseMgDl == null ||
- fastingInsulinUiuMl == null ||
- !Number.isFinite(fastingGlucoseMgDl) ||
- !Number.isFinite(fastingInsulinUiuMl) ||
- fastingGlucoseMgDl <= 0 ||
- fastingInsulinUiuMl <= 0
- ) {
- return null;
- }
- return (fastingGlucoseMgDl * fastingInsulinUiuMl) / 405;
-}
-
-/** Place a HOMA-IR value in its descriptive band (general guidance, not a diagnosis). */
-export function classifyHomaIr(value: number): HomaIrBand {
- if (value < 1.0) return "optimal";
- if (value <= 1.9) return "intermediate";
- if (value <= 2.9) return "elevated";
- return "high";
-}
diff --git a/src/lib/logging/redact.ts b/src/lib/logging/redact.ts
index 9502be731..f00f13683 100644
--- a/src/lib/logging/redact.ts
+++ b/src/lib/logging/redact.ts
@@ -150,7 +150,7 @@ export function redactSecrets(input: string): string {
// calendar-feed link (#1350).
// Same boundary-aware shape as the `sk-` rule. The
// idempotency replay-cache already rejects bodies containing
- // these prefixes (CLAUDE.md headless-client-API patterns); this
+ // these prefixes; this
// is the matching egress guard for log/error surfaces.
.replace(/(^|[^A-Za-z0-9])hl[kpresvf]_[A-Fa-f0-9]{32,}/g, "$1[REDACTED]")
// v1.22.0 — the MCP OAuth bridge artifacts: the authorization code
diff --git a/src/lib/mood/context.ts b/src/lib/mood/context.ts
index a976428f3..b0c0ffb04 100644
--- a/src/lib/mood/context.ts
+++ b/src/lib/mood/context.ts
@@ -10,7 +10,7 @@
* both directions.
*
* Everything is built field by field from the parsed body. Nothing is spread
- * from a request (`CLAUDE.md`, no mass assignment), and the two multi-select
+ * from a request (no mass assignment), and the two multi-select
* lists go through the vocabulary module's encoder rather than a local
* `JSON.stringify`.
*/
diff --git a/src/lib/notifications/__tests__/admin-locale.test.ts b/src/lib/notifications/__tests__/admin-locale.test.ts
index a0b257125..d611c3ba9 100644
--- a/src/lib/notifications/__tests__/admin-locale.test.ts
+++ b/src/lib/notifications/__tests__/admin-locale.test.ts
@@ -204,26 +204,6 @@ describe("dispatchLocalisedNotification — pass-through plumbing", () => {
);
});
- it("annotates metadata with preferredChannel when channel is supplied", async () => {
- vi.mocked(prisma.user.findUnique).mockResolvedValueOnce({
- locale: "en",
- } as never);
-
- await dispatchLocalisedNotification({
- userId: "u-en",
- titleKey: "notifications.admin.testTitle",
- messageKey: "notifications.admin.testBody",
- params: { name: "Test" },
- channel: "telegram",
- metadata: { source: "test" },
- });
-
- expect(dispatchNotificationMock.mock.calls[0][0].metadata).toEqual({
- source: "test",
- preferredChannel: "telegram",
- });
- });
-
it("does not crash when the locale lookup throws (logs warning, falls back to default)", async () => {
vi.mocked(prisma.user.findUnique).mockRejectedValueOnce(
new Error("db down"),
diff --git a/src/lib/notifications/dispatch-localised.ts b/src/lib/notifications/dispatch-localised.ts
index ac3100ac2..5eea44434 100644
--- a/src/lib/notifications/dispatch-localised.ts
+++ b/src/lib/notifications/dispatch-localised.ts
@@ -21,10 +21,8 @@
*
* The default `eventType` is `SYSTEM_ALERT` — the only event type that
* predates F21 outside of the medication-reminder lane. Callers that
- * need a different event type pass it explicitly; the `channel` option
- * is reserved for a future per-channel routing pass and is currently
- * a no-op (the dispatcher fans the message across every enabled
- * channel as before).
+ * need a different event type pass it explicitly. The dispatcher fans
+ * the message across every enabled channel.
*
* v1.4.28 (R1.2 H5) — the locale lookup previously fired
* `prisma.user.findUnique` on every call. A 30-second TTL LRU keyed on
@@ -50,13 +48,6 @@ interface DispatchLocalisedOptions {
titleKey: string;
messageKey: string;
params?: Record;
- /**
- * Per-channel routing is forward-compat scaffolding. Today the
- * dispatcher fans out across every enabled channel for the user; the
- * option is accepted so call-sites can declare intent without
- * needing a second helper later.
- */
- channel?: "telegram" | "email";
/** Event type for preference gating. Defaults to SYSTEM_ALERT. */
eventType?: EventType;
/** Channel-specific extras forwarded to the dispatcher metadata. */
@@ -162,13 +153,6 @@ export async function dispatchLocalisedNotification(
);
}
- // `channel` is accepted for future per-channel routing; today it
- // is silently passed through. Suppress unused-var lint by referencing
- // it on the metadata if present.
- const metadata = opts.channel
- ? { ...(opts.metadata ?? {}), preferredChannel: opts.channel }
- : opts.metadata;
-
await dispatchNotification({
eventType: opts.eventType ?? "SYSTEM_ALERT",
userId: opts.userId,
@@ -177,7 +161,7 @@ export async function dispatchLocalisedNotification(
title,
message,
renderForRecipient,
- metadata,
+ metadata: opts.metadata,
managedFanoutEvent: opts.managedFanoutEvent,
urgent: opts.urgent,
});
diff --git a/src/lib/notifications/senders/apns.ts b/src/lib/notifications/senders/apns.ts
index 6f1cfb0ce..03c595a0e 100644
--- a/src/lib/notifications/senders/apns.ts
+++ b/src/lib/notifications/senders/apns.ts
@@ -1031,7 +1031,7 @@ const IOS_METADATA_ALLOWLIST = new Set([
"phase",
"date",
// v1.16.11 — MEDICATION_LOW_STOCK deep link + context: the in-app
- // path the push should open (`/medications/{id}?tab=bestand`) plus
+ // path the push should open (`/medications/{id}?tab=supply`) plus
// the runway / threshold figures the iOS handler can surface.
"url",
"runwayDays",
diff --git a/src/lib/onboarding/tour-state.ts b/src/lib/onboarding/tour-state.ts
index 26df070cd..4d772c047 100644
--- a/src/lib/onboarding/tour-state.ts
+++ b/src/lib/onboarding/tour-state.ts
@@ -177,7 +177,7 @@ const ALL_STOPS: readonly TourStop[] = [
id: "export",
targetId: "export-hero",
placement: "bottom",
- route: "/settings/gesundheitsakte",
+ route: "/settings/health-record",
titleKey: "onboarding.tour.steps.export.title",
bodyKey: "onboarding.tour.steps.export.body",
},
diff --git a/src/lib/query-keys/auth.ts b/src/lib/query-keys/auth.ts
index f5753c7b2..89c3fe0a4 100644
--- a/src/lib/query-keys/auth.ts
+++ b/src/lib/query-keys/auth.ts
@@ -23,23 +23,6 @@ export const authKeys = {
* settings/ai-section, targets/target-edit-sheet).
*/
userThresholds: () => ["user", "thresholds"] as const,
- /**
- * v1.7.0 — Settings → Display metric/imperial control reads its
- * current value from `GET /api/auth/me/unit-preference`. The PATCH
- * mutation also invalidates `authMe()` so `useAuth().unitPreference`
- * (and every chart display transform that keys off it) re-renders
- * without a manual reload.
- */
- userUnitPreference: () => ["user", "unit-preference"] as const,
- /**
- * The mg/dL vs mmol/L glucose display control behind
- * `GET /api/auth/me/glucose-unit`. A separate axis from the
- * metric/imperial preference above — metric countries are split on
- * which one they read glucose in — so it carries its own key. The
- * PATCH also invalidates `authMe()`, since every glucose surface
- * resolves the unit from the account payload rather than from here.
- */
- userGlucoseUnit: () => ["user", "glucose-unit"] as const,
passkeys: () => ["passkeys"] as const,
diff --git a/src/lib/record-settings/classification.ts b/src/lib/record-settings/classification.ts
index 3512c8204..fb9e794bb 100644
--- a/src/lib/record-settings/classification.ts
+++ b/src/lib/record-settings/classification.ts
@@ -94,7 +94,7 @@ export const SETTINGS_DESTINATION_INVENTORY = {
coach: MANAGED_GUARDIAN,
api: PERSONAL,
mcp: PERSONAL,
- gesundheitsakte: PERSONAL,
+ "health-record": PERSONAL,
export: PERSONAL,
advanced: PERSONAL,
privacy: PERSONAL,
diff --git a/src/lib/settings/section-slugs.ts b/src/lib/settings/section-slugs.ts
index 843203284..e8dff7301 100644
--- a/src/lib/settings/section-slugs.ts
+++ b/src/lib/settings/section-slugs.ts
@@ -44,9 +44,10 @@
// canonical editors; reminder TYPES now live centrally in `notifications`,
// each row gated on its module. `/settings/reminders` 301-redirects to
// `/settings/notifications` (next.config.ts).
-// v1.18.0 — per-submodule settings split. `gesundheitsakte` (the full
+// v1.18.0 — per-submodule settings split. `health-record` (the full
// health-record export, always available) lifts out of Export & Import into
-// its own top-level entry.
+// its own top-level entry. Its address was the German `gesundheitsakte` in
+// every language; that one redirects (next.config.ts).
export const SETTINGS_SECTION_SLUGS = [
"account",
// v1.23 — `security` is the dedicated account-security home: second-factor
@@ -75,7 +76,7 @@ export const SETTINGS_SECTION_SLUGS = [
// `insights`, `medications`, `mood`, `labs`, `illness`, `vorsorge`) and
// `sharing` left this list. Their URLs are owned by permanent redirects
// in `next.config.ts` (`/settings/` → `/settings/layout/`,
- // `/settings/sharing` → `/settings/gesundheitsakte#sharing`), so the
+ // `/settings/sharing` → `/settings/health-record#sharing`), so the
// dynamic route could never serve them — prerendering eight unreachable
// pages and keeping eight dead component wirings. The redirects, the
// section components, and the layout-hub subpages are unchanged.
@@ -106,7 +107,7 @@ export const SETTINGS_SECTION_SLUGS = [
// assistant (Claude.ai / ChatGPT). Sits next to `api` (both are programmatic
// access surfaces). Not nav-gated — the card carries its own enable toggle.
"mcp",
- "gesundheitsakte",
+ "health-record",
"export",
"advanced",
// v1.23 — "Data & Privacy": a single surface that assembles the already-
diff --git a/src/lib/targets/__tests__/mood-stability-label.test.ts b/src/lib/targets/__tests__/mood-stability-label.test.ts
deleted file mode 100644
index 9434a7fbc..000000000
--- a/src/lib/targets/__tests__/mood-stability-label.test.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { describe, expect, it } from "vitest";
-
-import { moodStabilityLabel } from "../mood-stability-label";
-
-/**
- * v1.4.25 — threshold contract for the verbal MOOD_STABILITY label.
- * The card switches from raw σ to one of three buckets; the rule lives
- * here so the boundaries can be unit-tested without React.
- */
-
-describe("moodStabilityLabel", () => {
- it("returns 'stable' for σ below 1.0", () => {
- expect(moodStabilityLabel(0)).toBe("stable");
- expect(moodStabilityLabel(0.42)).toBe("stable");
- expect(moodStabilityLabel(0.999)).toBe("stable");
- });
-
- it("returns 'variable' for σ in [1.0, 2.0)", () => {
- expect(moodStabilityLabel(1.0)).toBe("variable");
- expect(moodStabilityLabel(1.5)).toBe("variable");
- expect(moodStabilityLabel(1.999)).toBe("variable");
- });
-
- it("returns 'highlyVariable' for σ ≥ 2.0", () => {
- expect(moodStabilityLabel(2.0)).toBe("highlyVariable");
- expect(moodStabilityLabel(3.5)).toBe("highlyVariable");
- });
-});
diff --git a/src/lib/targets/mood-stability-label.ts b/src/lib/targets/mood-stability-label.ts
deleted file mode 100644
index b9b026855..000000000
--- a/src/lib/targets/mood-stability-label.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * v1.4.25 — verbal label for the MOOD_STABILITY target headline.
- *
- * The card used to render the raw σ value ("0.42 σ"), which most users
- * cannot interpret. Replace with a three-state verbal label keyed
- * against translation entries `targets.mood.stability.{stable,
- * variable,highlyVariable}`.
- *
- * Thresholds (maintainer-set, documented in the W3e implementation plan):
- * σ < 1.0 → "stable" (stabil / stable)
- * σ < 2.0 → "variable" (schwankend / variable)
- * σ ≥ 2.0 → "highly-variable" (sehr schwankend / highly variable)
- *
- * Pure helper so the threshold logic can be unit-tested in isolation
- * without React. Returns the stable English key — callers (the card,
- * the Coach prompt builder) own the locale resolution.
- */
-export type MoodStabilityLabel = "stable" | "variable" | "highlyVariable";
-
-export function moodStabilityLabel(sigma: number): MoodStabilityLabel {
- if (sigma < 1.0) return "stable";
- if (sigma < 2.0) return "variable";
- return "highlyVariable";
-}
diff --git a/src/lib/validations/__tests__/measurement-reminders.test.ts b/src/lib/validations/__tests__/measurement-reminders.test.ts
index 2ff187c10..5ad02edf0 100644
--- a/src/lib/validations/__tests__/measurement-reminders.test.ts
+++ b/src/lib/validations/__tests__/measurement-reminders.test.ts
@@ -22,6 +22,7 @@ describe("measurementReminderTypeEnum (V3 — completeness)", () => {
"BLOOD_PRESSURE_SYS",
"PULSE",
"BLOOD_GLUCOSE",
+ "BLOOD_KETONES",
"OXYGEN_SATURATION",
"BODY_TEMPERATURE",
// Body composition
diff --git a/src/lib/validations/measurement-reminders.ts b/src/lib/validations/measurement-reminders.ts
index 83ae6eb4f..19bce2c80 100644
--- a/src/lib/validations/measurement-reminders.ts
+++ b/src/lib/validations/measurement-reminders.ts
@@ -45,6 +45,8 @@ export const measurementReminderTypeEnum = z
"BLOOD_PRESSURE_SYS",
"PULSE",
"BLOOD_GLUCOSE",
+ // Ketones come off the same fingerstick meter as glucose.
+ "BLOOD_KETONES",
"OXYGEN_SATURATION",
"BODY_TEMPERATURE",
// Body composition (one scale reading produces the whole family)
diff --git a/src/lib/validations/mental-health.ts b/src/lib/validations/mental-health.ts
index 6cbbd0777..e2dc0304a 100644
--- a/src/lib/validations/mental-health.ts
+++ b/src/lib/validations/mental-health.ts
@@ -3,8 +3,7 @@
* (PHQ-9 / GAD-7; v1.27.9 adds WHO-5 + SCI). The item answers are small
* integers whose ceiling is instrument-specific (PHQ/GAD 0–3, SCI 0–4,
* WHO-5 0–5); the array length must match the instrument. `userId` is NEVER
- * a body field — it is narrowed from the session in the handler (CLAUDE.md
- * convention).
+ * a body field — it is narrowed from the session in the handler.
*/
import { z } from "zod/v4";
import {