From 655a1d65a5e9bf74408c1908ce1b010ee2c5616c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Wed, 11 Nov 2026 22:16:47 +0100
Subject: [PATCH 01/20] chore(scripts): remove one-off scripts nothing runs
Nothing in package.json, the workflows, the Dockerfile, the docs or the
other scripts named these: the bench result dumps of v1.44 and v1.45
that no document cites, the version-pinned release checks, the v1.42
correlation dry run, the read-only diagnostics and their cleanup and
conversion companions for defects fixed long ago, the one-off locale
builder from v1.4.25, and the four June seeders for the demo account.
Kept: the two bench results the benchmarks doc cites, the Google Health
sleep repair (the duplicate-night issue is still open), the demo
migration its password guard reads, the column-reader sweep its test
imports, and the message-bundle tools. The scripts README now lists the
kept tools that it did not name before and drops a reference to a file
that does not exist.
---
scripts/README.md | 40 +-
scripts/bench/results/v1.44.0-baseline.json | 254 -
scripts/bench/results/v1.44.0-baseline.md | 25 -
.../bench/results/v1.45.0-before-boot.json | 1351 -----
scripts/bench/results/v1.45.0-before.json | 254 -
scripts/bench/results/v1.45.0-before.md | 25 -
scripts/bench/results/v1.45.0-boot-after.json | 1648 ------
scripts/bench/results/v1.45.0-boot-after.md | 298 -
scripts/bench/results/v1.45.0-boot-final.json | 5073 -----------------
scripts/bench/results/v1.45.0-boot-final.md | 804 ---
scripts/bench/results/v1.45.0-fan-out.md | 22 -
scripts/bench/results/v1.45.0-final.json | 254 -
scripts/bench/results/v1.45.0-final.md | 25 -
scripts/bench/results/v1.45.0-read-path.md | 45 -
.../bench/results/v1.45.0-workout-clusters.md | 28 -
scripts/bench/results/v1.45.0-write-path.md | 26 -
scripts/cleanup-apple-health-phantom-meds.ts | 138 -
scripts/compare-correlation-findings.ts | 338 --
.../convert-noncanonical-measurement-units.ts | 104 -
scripts/diagnose-apple-health-meds.ts | 121 -
scripts/diagnose-cycle-state.ts | 222 -
scripts/diagnose-implausible-measurements.ts | 150 -
scripts/diagnose-manual-sleep-hours.ts | 103 -
...diagnose-noncanonical-measurement-units.ts | 76 -
scripts/i18n-key-tool.mjs | 2 +-
scripts/i18n/build-locale.py | 404 --
scripts/seed-demo-apps01.sql | 154 -
scripts/seed-demo-extend-1y.sql | 216 -
scripts/seed-demo-meds-apps01.sql | 65 -
scripts/seed-demo-scores-1y.sql | 153 -
scripts/v1341-audit-ledger-check.mjs | 198 -
scripts/v1341-performance-baseline.mjs | 356 --
scripts/verify-v137-adversarial.ts | 305 -
33 files changed, 23 insertions(+), 13254 deletions(-)
delete mode 100644 scripts/bench/results/v1.44.0-baseline.json
delete mode 100644 scripts/bench/results/v1.44.0-baseline.md
delete mode 100644 scripts/bench/results/v1.45.0-before-boot.json
delete mode 100644 scripts/bench/results/v1.45.0-before.json
delete mode 100644 scripts/bench/results/v1.45.0-before.md
delete mode 100644 scripts/bench/results/v1.45.0-boot-after.json
delete mode 100644 scripts/bench/results/v1.45.0-boot-after.md
delete mode 100644 scripts/bench/results/v1.45.0-boot-final.json
delete mode 100644 scripts/bench/results/v1.45.0-boot-final.md
delete mode 100644 scripts/bench/results/v1.45.0-fan-out.md
delete mode 100644 scripts/bench/results/v1.45.0-final.json
delete mode 100644 scripts/bench/results/v1.45.0-final.md
delete mode 100644 scripts/bench/results/v1.45.0-read-path.md
delete mode 100644 scripts/bench/results/v1.45.0-workout-clusters.md
delete mode 100644 scripts/bench/results/v1.45.0-write-path.md
delete mode 100644 scripts/cleanup-apple-health-phantom-meds.ts
delete mode 100644 scripts/compare-correlation-findings.ts
delete mode 100644 scripts/convert-noncanonical-measurement-units.ts
delete mode 100644 scripts/diagnose-apple-health-meds.ts
delete mode 100644 scripts/diagnose-cycle-state.ts
delete mode 100644 scripts/diagnose-implausible-measurements.ts
delete mode 100644 scripts/diagnose-manual-sleep-hours.ts
delete mode 100644 scripts/diagnose-noncanonical-measurement-units.ts
delete mode 100644 scripts/i18n/build-locale.py
delete mode 100644 scripts/seed-demo-apps01.sql
delete mode 100644 scripts/seed-demo-extend-1y.sql
delete mode 100644 scripts/seed-demo-meds-apps01.sql
delete mode 100644 scripts/seed-demo-scores-1y.sql
delete mode 100644 scripts/v1341-audit-ledger-check.mjs
delete mode 100644 scripts/v1341-performance-baseline.mjs
delete mode 100644 scripts/verify-v137-adversarial.ts
diff --git a/scripts/README.md b/scripts/README.md
index 18da03099..a2640cd17 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,23 +18,27 @@ 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-edge01.sh`**: copies the demo account from one instance to a separate demo instance; see its README.
+- **`__tests__/`**: tests for the scripts above.
## Conventions
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/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/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);
From fc1fadcdab0e0043c55fb5a107f99788a2286c40 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Wed, 11 Nov 2026 22:18:11 +0100
Subject: [PATCH 02/20] chore: remove production files that only tests reached
Four files under src had no production importer:
- the keyset measurement reader for backups: both backup writers moved
to the shared account copy and the streamed full backup, and its
header still claimed both used it;
- the HOMA-IR calculator and the mood stability label, never wired to a
surface;
- the backup verdict helper, which only the backup classification guard
called. It now lives in that guard.
The queue expiry table is what the job queue guards check the registry
against and nothing in the worker reads it, so it moves to the test
helpers. The version-stamp baseline drops the deleted files.
---
src/__tests__/ai-job-expiry-guard.test.ts | 2 +-
.../backup-plan-classification.test.ts | 18 +-
.../baselines/version-stamp-comments.json | 7 +-
.../helpers}/queue-runtime.ts | 0
src/__tests__/job-queue-runtime-guard.test.ts | 4 +-
src/lib/export/backup-verdict.ts | 28 ---
.../__tests__/morning-digest-refresh.test.ts | 2 +-
src/lib/jobs/__tests__/restore-drill.test.ts | 2 +-
.../backup/__tests__/measurement-page.test.ts | 238 ------------------
src/lib/jobs/backup/measurement-page.ts | 126 ----------
src/lib/labs/__tests__/homa-ir.test.ts | 33 ---
src/lib/labs/biomarker-catalog.ts | 3 +-
src/lib/labs/homa-ir.ts | 47 ----
.../__tests__/mood-stability-label.test.ts | 28 ---
src/lib/targets/mood-stability-label.ts | 24 --
15 files changed, 25 insertions(+), 537 deletions(-)
rename src/{lib/jobs => __tests__/helpers}/queue-runtime.ts (100%)
delete mode 100644 src/lib/export/backup-verdict.ts
delete mode 100644 src/lib/jobs/backup/__tests__/measurement-page.test.ts
delete mode 100644 src/lib/jobs/backup/measurement-page.ts
delete mode 100644 src/lib/labs/__tests__/homa-ir.test.ts
delete mode 100644 src/lib/labs/homa-ir.ts
delete mode 100644 src/lib/targets/__tests__/mood-stability-label.test.ts
delete mode 100644 src/lib/targets/mood-stability-label.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/version-stamp-comments.json b/src/__tests__/baselines/version-stamp-comments.json
index 642303b6b..0afbd80b7 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": 2884,
- "total": 9191,
+ "count": 2881,
+ "total": 9188,
"entries": {
"__tests__/account-payload-consumer-guard.test.ts": 2,
"__tests__/acting-account-boundary-guard.test.ts": 7,
@@ -2368,7 +2368,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,
@@ -2769,11 +2768,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/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/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/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/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/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";
-}
From 507d4e6facf3014a928783d000d44694cabeafce Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Wed, 11 Nov 2026 22:19:47 +0100
Subject: [PATCH 03/20] refactor(medications): drop the side-effect slot nobody
wires
The card menu took an optional side-effect item for the GLP-1 card, but
no page ever passed the handler, so the item never rendered; side
effects are logged on the detail page. The slot, its message key in ten
locales and the tests that passed it by hand go.
One test pinned behaviour only the dead slot produced: a delegate who
may write but not manage kept a menu trigger because the test supplied
the handler. In the app that delegate never had one, and the test now
says so.
---
messages/de.json | 1 -
messages/en.json | 1 -
messages/es.json | 1 -
messages/fr.json | 1 -
messages/it.json | 1 -
messages/ko.json | 1 -
messages/nl.json | 1 -
messages/pl.json | 1 -
messages/pt-BR.json | 1 -
messages/ru.json | 1 -
.../baselines/version-stamp-comments.json | 6 +-
.../delegated-write-affordances.test.tsx | 12 ++--
.../__tests__/medication-card-glp1.test.tsx | 46 +--------------
.../medication-card-symmetry.test.tsx | 7 +--
.../medications/glp1-medication-card.tsx | 12 +---
.../medications/medication-card-menu.tsx | 59 ++++++-------------
16 files changed, 30 insertions(+), 122 deletions(-)
diff --git a/messages/de.json b/messages/de.json
index c3db429d5..b44fe4387 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -1481,7 +1481,6 @@
"glp1NextInjectionToday": "heute",
"glp1NextInjectionTomorrow": "morgen",
"glp1RotationSuggested": "Empfohlen: {site}",
- "glp1LogSideEffect": "Nebenwirkung loggen",
"sideEffects": {
"section": "Nebenwirkungen",
"addCta": "Erfassen",
diff --git a/messages/en.json b/messages/en.json
index d897c04e6..8dee2871b 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -1481,7 +1481,6 @@
"glp1NextInjectionToday": "today",
"glp1NextInjectionTomorrow": "tomorrow",
"glp1RotationSuggested": "Recommended next: {site}",
- "glp1LogSideEffect": "Log side effect",
"sideEffects": {
"section": "Side effects",
"addCta": "Log",
diff --git a/messages/es.json b/messages/es.json
index c6c9875ea..34261c62a 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -1481,7 +1481,6 @@
"glp1NextInjectionToday": "hoy",
"glp1NextInjectionTomorrow": "mañana",
"glp1RotationSuggested": "Sugerido: {site}",
- "glp1LogSideEffect": "Registrar efecto secundario",
"sideEffects": {
"section": "Efectos secundarios",
"addCta": "Registrar",
diff --git a/messages/fr.json b/messages/fr.json
index 95779eaa4..63b352e78 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -1481,7 +1481,6 @@
"glp1NextInjectionToday": "aujourd’hui",
"glp1NextInjectionTomorrow": "demain",
"glp1RotationSuggested": "Conseillé : {site}",
- "glp1LogSideEffect": "Consigner un effet secondaire",
"sideEffects": {
"section": "Effets secondaires",
"addCta": "Consigner",
diff --git a/messages/it.json b/messages/it.json
index 1bc25ec4c..940aeb2e9 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -1481,7 +1481,6 @@
"glp1NextInjectionToday": "oggi",
"glp1NextInjectionTomorrow": "domani",
"glp1RotationSuggested": "Suggerita: {site}",
- "glp1LogSideEffect": "Registra effetto collaterale",
"sideEffects": {
"section": "Effetti collaterali",
"addCta": "Registra",
diff --git a/messages/ko.json b/messages/ko.json
index 5aeef7e5d..4d05bd352 100644
--- a/messages/ko.json
+++ b/messages/ko.json
@@ -1481,7 +1481,6 @@
"glp1NextInjectionToday": "오늘",
"glp1NextInjectionTomorrow": "내일",
"glp1RotationSuggested": "다음 추천 부위: {site}",
- "glp1LogSideEffect": "부작용 기록",
"sideEffects": {
"section": "부작용",
"addCta": "기록",
diff --git a/messages/nl.json b/messages/nl.json
index e0a3f8535..66e9ad010 100644
--- a/messages/nl.json
+++ b/messages/nl.json
@@ -1481,7 +1481,6 @@
"glp1NextInjectionToday": "vandaag",
"glp1NextInjectionTomorrow": "morgen",
"glp1RotationSuggested": "Volgende aanbevolen plek: {site}",
- "glp1LogSideEffect": "Bijwerking vastleggen",
"sideEffects": {
"section": "Bijwerkingen",
"addCta": "Vastleggen",
diff --git a/messages/pl.json b/messages/pl.json
index d35c7b385..a41d3296f 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -1481,7 +1481,6 @@
"glp1NextInjectionToday": "dziś",
"glp1NextInjectionTomorrow": "jutro",
"glp1RotationSuggested": "Sugerowane: {site}",
- "glp1LogSideEffect": "Zapisz działanie niepożądane",
"sideEffects": {
"section": "Skutki uboczne",
"addCta": "Dodaj",
diff --git a/messages/pt-BR.json b/messages/pt-BR.json
index 05e875766..36ea5e553 100644
--- a/messages/pt-BR.json
+++ b/messages/pt-BR.json
@@ -1481,7 +1481,6 @@
"glp1NextInjectionToday": "hoje",
"glp1NextInjectionTomorrow": "amanhã",
"glp1RotationSuggested": "Próximo recomendado: {site}",
- "glp1LogSideEffect": "Registrar efeito colateral",
"sideEffects": {
"section": "Efeitos colaterais",
"addCta": "Registrar",
diff --git a/messages/ru.json b/messages/ru.json
index c96b8a0b2..431bdbac2 100644
--- a/messages/ru.json
+++ b/messages/ru.json
@@ -1481,7 +1481,6 @@
"glp1NextInjectionToday": "сегодня",
"glp1NextInjectionTomorrow": "завтра",
"glp1RotationSuggested": "Рекомендуется следующее: {site}",
- "glp1LogSideEffect": "Записать побочный эффект",
"sideEffects": {
"section": "Побочные эффекты",
"addCta": "Записать",
diff --git a/src/__tests__/baselines/version-stamp-comments.json b/src/__tests__/baselines/version-stamp-comments.json
index 0afbd80b7..95b72ddd6 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": 2881,
- "total": 9188,
+ "total": 9185,
"entries": {
"__tests__/account-payload-consumer-guard.test.ts": 2,
"__tests__/acting-account-boundary-guard.test.ts": 7,
@@ -1128,7 +1128,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,
@@ -1165,7 +1165,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,
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/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/glp1-medication-card.tsx b/src/components/medications/glp1-medication-card.tsx
index 5024217cd..91502183c 100644
--- a/src/components/medications/glp1-medication-card.tsx
+++ b/src/components/medications/glp1-medication-card.tsx
@@ -178,7 +178,6 @@ interface Glp1MedicationCardProps {
* 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")}
+
);
From 5552a9b149209ac67c15aac20810c1b733a971ce Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Wed, 11 Nov 2026 22:20:09 +0100
Subject: [PATCH 04/20] refactor(notifications): drop the channel option that
routed nothing
The localised dispatch helper accepted a `channel` and copied it into
the metadata as `preferredChannel`, which no sender reads. No caller
passed it either; every message still fans out across the user's
enabled channels. The option, its comment and the test that pinned the
copy go.
---
.../__tests__/admin-locale.test.ts | 20 -----------------
src/lib/notifications/dispatch-localised.ts | 22 +++----------------
2 files changed, 3 insertions(+), 39 deletions(-)
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,
});
From 0c07764097591bf6ab61f8d040ff5a541b6e62b1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Wed, 11 Nov 2026 22:20:43 +0100
Subject: [PATCH 05/20] refactor(settings): drop two query keys nothing reads
The unit-system and glucose-unit selects invalidated
`userUnitPreference()` and `userGlucoseUnit()` after a save, but no
query was ever registered under either key; every surface reads the
units from the account payload, which both selects already refresh.
---
.../baselines/version-stamp-comments.json | 4 ++--
src/components/settings/glucose-unit-select.tsx | 9 +++------
.../settings/unit-preference-select.tsx | 8 ++------
src/lib/query-keys/auth.ts | 17 -----------------
4 files changed, 7 insertions(+), 31 deletions(-)
diff --git a/src/__tests__/baselines/version-stamp-comments.json b/src/__tests__/baselines/version-stamp-comments.json
index 95b72ddd6..e93abac30 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": 2881,
- "total": 9185,
+ "total": 9184,
"entries": {
"__tests__/account-payload-consumer-guard.test.ts": 2,
"__tests__/acting-account-boundary-guard.test.ts": 7,
@@ -2691,7 +2691,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,
diff --git a/src/components/settings/glucose-unit-select.tsx b/src/components/settings/glucose-unit-select.tsx
index 496e0322c..234ec0434 100644
--- a/src/components/settings/glucose-unit-select.tsx
+++ b/src/components/settings/glucose-unit-select.tsx
@@ -29,9 +29,9 @@ 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.
*/
export function GlucoseUnitSelect({
isAuthenticated,
@@ -66,9 +66,6 @@ export function GlucoseUnitSelect({
onSuccess: () => {
toastWrittenOutcome("success", t("settings.dashboard.glucoseUnit.saved"));
queryClient.invalidateQueries({ queryKey: queryKeys.authMe() });
- queryClient.invalidateQueries({
- queryKey: queryKeys.userGlucoseUnit(),
- });
setOptimistic(null);
},
onError: (err) => {
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/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,
From 08e6229142c77c30a2391fb968821b17659e2aae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Wed, 11 Nov 2026 22:21:47 +0100
Subject: [PATCH 06/20] fix(glucose): show targets in the new unit right after
a unit change
After switching between mg/dL and mmol/L, the target panels on the
glucose page kept the old unit for up to ten minutes. Their unit comes
from the cached targets payload, and the save only refreshed the
account payload. The select now invalidates the targets query, and the
route evicts the profile-derived server caches when the unit changes,
the same way the thresholds and source-priority writes do.
---
src/app/api/auth/me/glucose-unit/route.ts | 7 +++++--
.../settings/__tests__/glucose-unit-select.test.tsx | 13 +++++++++++++
src/components/settings/glucose-unit-select.tsx | 4 +++-
src/lib/cache/invalidate.ts | 9 +++++----
4 files changed, 26 insertions(+), 7 deletions(-)
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/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/glucose-unit-select.tsx b/src/components/settings/glucose-unit-select.tsx
index 234ec0434..e98274c0b 100644
--- a/src/components/settings/glucose-unit-select.tsx
+++ b/src/components/settings/glucose-unit-select.tsx
@@ -31,7 +31,8 @@ import { resolveGlucoseUnit, type GlucoseUnit } from "@/lib/glucose";
* Persistence mirrors the unit-system select: a change PATCHes
* `/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.
+ * /me refetch, and `queryKeys.insightsTargets()`, whose payload states the
+ * target ranges in the unit it was built with.
*/
export function GlucoseUnitSelect({
isAuthenticated,
@@ -66,6 +67,7 @@ export function GlucoseUnitSelect({
onSuccess: () => {
toastWrittenOutcome("success", t("settings.dashboard.glucoseUnit.saved"));
queryClient.invalidateQueries({ queryKey: queryKeys.authMe() });
+ queryClient.invalidateQueries({ queryKey: queryKeys.insightsTargets() });
setOptimistic(null);
},
onError: (err) => {
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);
From 408c2abb3900ec7f93c71296fe32aba8dc69fe91 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Wed, 11 Nov 2026 22:22:31 +0100
Subject: [PATCH 07/20] test: lower the as-never baseline after the channel
test went
The removed preferred-channel test carried one of the casts the
realism ratchet counts.
---
src/__tests__/baselines/test-as-never.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
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,
From 8f6081de89575f8fc35a201035caef0499056147 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Wed, 11 Nov 2026 22:23:07 +0100
Subject: [PATCH 08/20] test: delete text pins on things removed releases ago
Five tests only read source or message files to assert that something
long gone stays gone: the moodLog bridge (removed in July), the chart's
target-adjust link (v1.16.8), the mood notes timeline, the medication
detail page's old dose card and wizard host (v1.15.18), and the
obsolete v1.34.1 copy keys. They protect no behaviour, and the general
i18n guards already hold every key and placeholder across the ten
locales, which the v1.34.1 list did for nine. The source-read baseline
drops from 337 to 332.
---
.../baselines/test-source-reads.json | 9 +-
.../baselines/version-stamp-comments.json | 8 +-
src/__tests__/moodlog-removal-guard.test.ts | 114 -----------
src/__tests__/v1341-locale-parity.test.ts | 187 ------------------
.../detail-page-history-surface.test.ts | 38 ----
.../health-chart-target-link-removed.test.ts | 53 -----
.../mood-notes-timeline-removed.test.ts | 85 --------
src/lib/http/retired-routes.ts | 3 +-
8 files changed, 5 insertions(+), 492 deletions(-)
delete mode 100644 src/__tests__/moodlog-removal-guard.test.ts
delete mode 100644 src/__tests__/v1341-locale-parity.test.ts
delete mode 100644 src/app/medications/__tests__/detail-page-history-surface.test.ts
delete mode 100644 src/components/charts/__tests__/health-chart-target-link-removed.test.ts
delete mode 100644 src/components/insights/mood/__tests__/mood-notes-timeline-removed.test.ts
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 e93abac30..e56ebd105 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": 2881,
- "total": 9184,
+ "count": 2877,
+ "total": 9177,
"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,
@@ -655,7 +654,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,
@@ -736,7 +734,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,
@@ -1002,7 +999,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,
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__/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/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/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/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/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.
*
From 4a40a63656efdd0164baf50242af25a161ed9ace Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Wed, 11 Nov 2026 22:24:08 +0100
Subject: [PATCH 09/20] docs: remove the second contributing note and its dead
pointers
CONTRIBUTING-AI.md pointed contributors at a CLAUDE.md "checked in and
kept current"; that file is not in the repository, so the pointer and
the twenty references to it in folder READMEs, docs and comments led
nowhere. The note goes; its one lasting rule (whoever commits a change
stands behind it, no real names, no live health figures, no secrets)
moves into CONTRIBUTING.md. The folder READMEs keep their own content
and drop the dead link, and a reference to the unpublished planning
folder goes with it.
The checked-in .mcp.json was a local editor config no workflow reads;
it is untracked and ignored now. The translation tiers are described
as machine-translated first drafts reviewed by the community, and the
list names Russian, which it missed.
---
.dockerignore | 1 -
.gitignore | 7 +++----
.mcp.json | 8 --------
CONTRIBUTING-AI.md | 16 ----------------
CONTRIBUTING.md | 8 ++++----
docs/CONTRIBUTING-CLIENTS.md | 4 ++--
e2e/README.md | 2 +-
lefthook.yml | 8 +-------
prisma/README.md | 2 +-
scripts/README.md | 2 +-
src/README.md | 2 +-
src/__tests__/labs-module-ai-gate-guard.test.ts | 3 +--
src/__tests__/session-surface-guard.test.ts | 2 +-
src/__tests__/sharing-surface-guard.test.ts | 2 +-
src/app/README.md | 2 +-
src/components/README.md | 2 +-
src/lib/README.md | 2 +-
src/lib/jobs/insight-pregenerate.ts | 2 +-
src/lib/jobs/insight-status-generate.ts | 2 +-
src/lib/jobs/period-narrative-warm.ts | 2 +-
src/lib/logging/redact.ts | 2 +-
src/lib/mood/context.ts | 2 +-
src/lib/validations/mental-health.ts | 3 +--
23 files changed, 26 insertions(+), 60 deletions(-)
delete mode 100644 .mcp.json
delete mode 100644 CONTRIBUTING-AI.md
diff --git a/.dockerignore b/.dockerignore
index 1901f009e..b95584b3f 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -82,7 +82,6 @@ README.md
CHANGELOG.md
CLAUDE.md
CONTRIBUTING.md
-CONTRIBUTING-AI.md
CODE_OF_CONDUCT.md
SECURITY.md
# LICENSE / LICENSE.md are deliberately NOT ignored: the PolyForm
diff --git a/.gitignore b/.gitignore
index 655bb269e..55ff4b2ad 100644
--- a/.gitignore
+++ b/.gitignore
@@ -72,11 +72,10 @@ next-env.d.ts
.cursor/
.codex/
.aider*
+.mcp.json
-# Planning scratch — all internal per-release reports, QA findings, ad-hoc
-# backlog, and the iOS coordination channel. These are internal working
-# notes; everything user-facing lives in the public surfaces (README,
-# CHANGELOG, docs/). Kept on disk for the working session, never tracked.
+# Internal working notes (per-release reports, backlog, iOS coordination).
+# Everything user-facing lives in README, CHANGELOG and docs/.
/.planning/
# Docs scratch — temporary spec drafts that live in the repo only
diff --git a/.mcp.json b/.mcp.json
deleted file mode 100644
index 29585572e..000000000
--- a/.mcp.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "mcpServers": {
- "shadcn": {
- "command": "npx",
- "args": ["-y", "shadcn@latest", "mcp"]
- }
- }
-}
diff --git a/CONTRIBUTING-AI.md b/CONTRIBUTING-AI.md
deleted file mode 100644
index 5cd662a4e..000000000
--- a/CONTRIBUTING-AI.md
+++ /dev/null
@@ -1,16 +0,0 @@
-# Contributing with AI tooling
-
-HealthLog is built with AI assistance, and contributions made the same way are welcome. This file used to carry a copy of the internal conventions document; the copy aged badly and started contradicting the codebase, so it now points at the living sources instead.
-
-## Where the real conventions live
-
-- `CLAUDE.md` at the repo root is the maintainer's operating manual for this codebase: architecture walk, code conventions, security patterns, test commands, the do-not list. It is checked in and kept current; whatever an AI agent (or a human) needs to work on the server is in there.
-- `docs/CONTRIBUTING-CLIENTS.md` covers building a client app (Android, iOS, CLI) against the server: the API contract, auth from a client's point of view, sync semantics, and how coordination between repos works.
-- `docs/api/openapi.yaml` is the wire contract, generated from the server's Zod schemas. CI fails on drift, so it is always accurate for the commit it sits in.
-
-## Ground rules, whatever tooling you use
-
-- The human who commits a change has read it and stands behind it. The git history knows authors, not tools: no AI attribution trailers, no assistant vocabulary in commit messages, PR text, or release notes.
-- No real names of users or reporters in committed text, and no live health figures anywhere, including fixtures and examples. Invent data.
-- No secrets in code, fixtures, or docs, and nothing secret-shaped either.
-- Run the gates before pushing (`pnpm typecheck`, `pnpm lint`, `pnpm test`) and fix what they find rather than suppressing it. A guard that fails is telling you something; the fix goes in the code, not in the guard.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 78d3286d8..f3bf2d402 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -76,7 +76,7 @@ pnpm typecheck && pnpm lint && pnpm test && pnpm format:check && pnpm build
- **shadcn/ui** components (new-york style) for UI primitives.
- **Dracula theme** -- dark mode is the default; a `light`/`system` toggle is exposed in the topbar. Chart and accent colors use the `--dracula-*` CSS tokens so both themes stay coherent.
-See `CONTRIBUTING-AI.md` for detailed architecture and patterns.
+Whoever commits a change has read it and stands behind it. Committed text names no real users or reporters and carries no live health figures; fixtures and examples use invented data. Nothing secret, and nothing secret-shaped, goes into code, fixtures or docs.
## Database Changes
@@ -121,7 +121,7 @@ Contributors: branch off `main`; PRs target `main`.
## Translations
-HealthLog ships with English (`en`) and German (`de`) maintained by the project, plus French (`fr`), Spanish (`es`), Italian (`it`), Polish (`pl`), Korean (`ko`), Dutch (`nl`), and Brazilian Portuguese (`pt-BR`) that are **AI-initial, community-maintained**. The maintained locales are reviewed line-by-line; the community-maintained ones surface a small banner inviting users to improve them.
+HealthLog ships with English (`en`) and German (`de`) maintained by the project, plus French (`fr`), Spanish (`es`), Italian (`it`), Polish (`pl`), Korean (`ko`), Dutch (`nl`), Brazilian Portuguese (`pt-BR`) and Russian (`ru`), which started as machine-translated first drafts and are reviewed by the community. The maintained locales are reviewed line by line; the community-reviewed ones show a small banner inviting users to improve them.
### Improving an existing locale
@@ -168,9 +168,9 @@ To propose a validated version for a language that has none, or a better source
If you want to introduce a locale that isn't shipped (e.g. `sv`, `cs`):
-1. Open an issue first using the **Translation request** template. We'll confirm the locale is in scope and decide whether it joins the AI-initial-community-maintained tier or the maintained tier.
+1. Open an issue first using the **Translation request** template. We'll confirm the locale is in scope and decide whether it joins the community-reviewed tier or the maintained tier.
2. Copy `messages/en.json` to `messages/.json`. Questionnaire text is not in it; the new language needs a wording file per instrument instead (see Questionnaire wording above), which step 4 makes impossible to forget.
-3. Translate. AI assistance is welcome as a starting point but the PR description should note whether it's an AI-initial pass or a fully human-reviewed one — that information drives the banner state.
+3. Translate. A machine translation is fine as a starting point, but the PR description should say whether the pass is machine-translated or fully reviewed by someone who reads the language; that decides the banner state.
4. Add the locale to `locales` in `src/lib/i18n/config.ts` and to `locales` in `project.inlang/settings.json`, then run `pnpm typecheck`. Every per-locale table that still lacks the new locale fails to compile, and that list is the work: date and number formatting, the reply-language footers of the Coach and insights prompts, the outbound safety banks, the prose-grounding patterns, the no-key fallback copy and the questionnaire wording registry. Include those in the same PR.
5. Run `pnpm test` before submitting. The i18n guards hold the new bundle to the English keys and placeholders.
diff --git a/docs/CONTRIBUTING-CLIENTS.md b/docs/CONTRIBUTING-CLIENTS.md
index e2f627e8c..3f84d6276 100644
--- a/docs/CONTRIBUTING-CLIENTS.md
+++ b/docs/CONTRIBUTING-CLIENTS.md
@@ -1,6 +1,6 @@
# Building a native client against HealthLog
-Working notes for anyone building a client app (Android, iOS, CLI, anything else) against the HealthLog server, with or without AI tooling. Everything in here is a rule we actually follow between the server and the iOS app, written down so a second client does not have to rediscover it. Pair it with `CLAUDE.md` (server-side conventions) and `docs/api/openapi.yaml` (the wire contract).
+Working notes for anyone building a client app (Android, iOS, CLI, anything else) against the HealthLog server. Everything in here is a rule we actually follow between the server and the iOS app, written down so a second client does not have to rediscover it. Pair it with [`CONTRIBUTING.md`](../CONTRIBUTING.md) (server-side conventions) and `docs/api/openapi.yaml` (the wire contract).
## The contract, and where truth lives
@@ -43,7 +43,7 @@ The corollary: if a resolved value you need is missing from a payload, the fix i
## Ground rules for the repo itself
-The voice-and-privacy rules from `CLAUDE.md` apply to any repo under the project, including a client repo: commit messages and public text carry no tooling attribution and no assistant vocabulary, no real names of users or reporters, and no live health figures, ever. Fixtures use invented data. If an AI tool wrote part of a change, the human who commits it has read it and stands behind it; the git history knows only authors.
+The privacy rules apply to any repo under the project, including a client repo: commit messages and public text carry no real names of users or reporters and no live health figures, ever. Fixtures use invented data. Whoever commits a change has read it and stands behind it.
Secrets never appear in code, fixtures, or examples, not even placeholder-shaped real ones. Anything user-identifying stays out of logs on the client just as it does on the server.
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/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/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 a2640cd17..34949a93e 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -42,4 +42,4 @@ typecheck.
## 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/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__/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__/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__/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/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/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/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/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/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/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/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 {
From 9712238d3dc86c78e29063c092f5e58eaaca8820 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Wed, 11 Nov 2026 22:24:40 +0100
Subject: [PATCH 10/20] fix(about): link the release notes instead of the full
changelog
Settings, About sent every user to CHANGELOG.md on GitHub, a 1.4 MB
file with every release since the first. The link now opens the
releases page, which carries the same notes per version and loads.
The `changelog` field of /api/version keeps its name and stays a URL.
---
src/app/api/version/__tests__/route.test.ts | 5 ++++-
src/app/api/version/route.ts | 2 +-
2 files changed, 5 insertions(+), 2 deletions(-)
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,
From 9620b0116aab80a994e39a48a503d2f6bf45a11f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Wed, 11 Nov 2026 22:25:30 +0100
Subject: [PATCH 11/20] docs(readme): plain feature list, no dashes, release
notes link
The feature bullets opened with slogans; they now open with what the
feature is. Dashes and middle dots become commas, full stops and bars.
Health scores and correlations move out of the AI bullet, since no
model computes them, and that bullet is marked optional. "Ships
releases roughly weekly" did not match the release list; the README
now says it ships often and points at the release notes rather than
the full CHANGELOG.
The encryption and PWA bullets stay as they are for now: the PWA line
is being corrected on its own.
---
README.md | 76 +++++++++++++++++++++++++++----------------------------
1 file changed, 38 insertions(+), 38 deletions(-)
diff --git a/README.md b/README.md
index 4a1a137ee..6fa8591aa 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
HealthLog
- Self-hosted, privacy-first health tracking. Your vitals, your devices, your server.
+ Self-hosted health tracking that syncs your devices and keeps the data on your own server.
@@ -17,10 +17,10 @@
- Website ·
- Live Demo ·
- Documentation ·
- iOS App Store ·
+ Website |
+ Live Demo |
+ Documentation |
+ iOS App Store |
iOS TestFlight
@@ -36,25 +36,25 @@
-## Why HealthLog
+## What HealthLog is
-HealthLog is a self-hosted health tracking app (PWA + native iOS client) that runs from a single `docker compose up` on a NAS, homelab, or small VPS. It brings weight, blood pressure, pulse, blood glucose, body composition, sleep, mood, menstrual cycle, and medications onto one timeline, synced from the devices you already own, and keeps it in a Postgres database on hardware you control. No vendor cloud, no subscription, no usage telemetry. The few requests the server makes on its own (an update check, a password breach check, an IP geolocation lookup for logins) can each be switched off.
+HealthLog is a self-hosted health tracking app (PWA + native iOS client) that runs from a single `docker compose up` on a NAS, homelab, or small VPS. It brings weight, blood pressure, pulse, blood glucose, body composition, sleep, mood, menstrual cycle, and medications onto one timeline, synced from the devices you already own, and keeps it in a Postgres database on hardware you control. It needs no vendor cloud or subscription and sends no usage telemetry. The few requests the server makes on its own (an update check, a password breach check, an IP geolocation lookup for logins) can each be switched off.
-HealthLog is a personal health record-keeping and wellness tool. It is not a medical device and does not diagnose, treat, cure, or prevent any disease — it helps you log, visualise, and export your own data, and leaves clinical judgement to a clinician.
+HealthLog is a personal health record-keeping and wellness tool. It is not a medical device and does not diagnose, treat, cure, or prevent any disease. It helps you log, visualise, and export your own data, and leaves clinical judgement to a clinician.
-Try the [live demo](https://demo.healthlog.dev) to see a working install — sign in with `demo` / `demo123demo123`.
+Try the [live demo](https://demo.healthlog.dev) to see a working install. Sign in with `demo` / `demo123demo123`.
-## Highlights
+## Features
-- **Every vital on one timeline.** Weight, blood pressure, pulse, glucose, body composition, sleep, SpO₂, respiratory rate, mood, and cycle tracking — charted with trends, moving averages, and clinical reference ranges (ESH 2023, ADA 2024), overridable with the targets your clinician set.
-- **Clinical signals, opt-in and additive.** Validated self-checks for mental wellbeing and sleep (WHO-5, PHQ-9, GAD-7, Sleep Condition Indicator — official questionnaire wording, calm crisis-resource handling), grip strength, a 0–10 pain score, waist and body measurements, and a longevity lab panel — each added through one signal registry, so a new signal wires into charts, correlations, the coach, the FHIR map, and the API at once.
-- **Records, structured.** Allergies and family history as structured records that flow into the doctor report (reactions and notes are encrypted, the substance and condition names are not), plus lab values you key in or scan from a printed report — OCR extracts the stated values, units, and dates for you to review and confirm before anything is saved. HealthLog transcribes what a report says; it never interprets or diagnoses from it. A vaccination record keeps a lifetime immunization log, doses grouped under the disease they protect against, and prints in the doctor report.
-- **A document vault for the paper trail.** An opt-in module stores every letter, report, and scan encrypted at rest — PDFs and photos preview inline, Office and text files stay download-only, and nothing user-uploaded can ever execute in the app. One searchable timeline with type, condition, and year filters, drag-and-drop and paste-to-upload, bulk filing, undo-able delete with a 30-day grace, and per-file/per-account limits the operator tunes.
-- **Context that explains the numbers.** An environmental module folds weather, daylight, temperature, air quality, and pollen in as influence signals (winter darkness against mood, heat against blood pressure) using a home location you set, so a pattern has somewhere to point. It is on by default, and nothing leaves the host until someone sets that location.
-- **Your devices, one server.** Withings, WHOOP, Oura and Polar sync over OAuth2, Google Health connects Fitbit and Pixel Watch through your own Google Cloud client, Nightscout streams CGM glucose, an Apple Health `export.zip` folds your full history in, and the native iOS app streams HealthKit live. A per-metric source priority decides which reading is canonical when wearables overlap.
-- **Medication tracking that tells the truth.** Flexible schedules (weekly injections, weekday-only, intervals, PRN, cyclic), a configurable intake window per dose, and a traceable dose history where every slot reads taken, late, skipped, or missed — the same ledger the compliance rate is computed from, so the percentage can never disagree with the timeline.
-- **AI insights you own.** Daily briefing, health scores, correlations, and a conversational coach grounded in your own measurements and interpreted against cited guideline reference bands — where a value sits and what that means in plain words, never a diagnosis. The coach proposes concrete if-then plans you accept or decline, and suppresses proactive check-ins when there's nothing genuine to say. Bring your own OpenAI or Anthropic key, or point at a local endpoint (Ollama, LM Studio, vLLM) so AI requests stay on your network.
-- **Clinician-ready output.** A doctor-report PDF rendered on your server, a read-only HL7 FHIR R4 API, and scoped, time-limited share links you revoke after the visit.
+- **Vitals and trends.** Weight, blood pressure, pulse, glucose, body composition, sleep, SpO₂, respiratory rate, mood, and cycle tracking, charted with trends, moving averages, and clinical reference ranges (ESH 2023, ADA 2024) that you can override with the targets your clinician set. Health scores and correlations between your metrics are computed on your server without an AI model.
+- **Questionnaires and self-checks.** Optional validated self-checks for mental wellbeing and sleep (WHO-5, PHQ-9, GAD-7 and the Sleep Condition Indicator, in their official wording, with crisis resources when an answer calls for them), grip strength, a pain score from 0 to 10, waist and body measurements, and a longevity lab panel. Each comes from one signal registry, so a new signal reaches charts, correlations, the coach, the FHIR map, and the API together.
+- **Allergies, family history, labs and vaccinations.** Allergies and family history are structured records that flow into the doctor report (reactions and notes are encrypted, the substance and condition names are not). Lab values are keyed in or scanned from a printed report: OCR reads the stated values, units, and dates for you to review and confirm before anything is saved. HealthLog transcribes what a report says; it never interprets or diagnoses from it. A vaccination record keeps a lifetime immunization log, doses grouped under the disease they protect against, and prints in the doctor report.
+- **Documents.** An opt-in module stores letters, reports, and scans encrypted at rest. PDFs and photos preview inline, Office and text files stay download-only, and nothing a user uploads can execute in the app. One searchable timeline with type, condition, and year filters, drag-and-drop and paste-to-upload, bulk filing, a delete you can undo within 30 days, and per-file and per-account limits the operator sets.
+- **Weather and environment.** An environmental module adds weather, daylight, temperature, air quality, and pollen as possible influences (winter darkness and mood, heat and blood pressure), based on a home location you set. It is on by default, and nothing leaves the host until someone sets that location.
+- **Devices and integrations.** Withings, WHOOP, Oura and Polar sync over OAuth2, Google Health connects Fitbit and Pixel Watch through your own Google Cloud client, Nightscout streams CGM glucose, an Apple Health `export.zip` folds your full history in, and the native iOS app streams HealthKit live. A per-metric source priority decides which reading is canonical when wearables overlap.
+- **Medications.** Flexible schedules (weekly injections, weekday-only, intervals, PRN, cyclic), a configurable intake window per dose, and a dose history where every slot reads taken, late, skipped, or missed. The compliance rate is computed from the same ledger, so the percentage always matches the timeline.
+- **AI features (optional).** A daily briefing and a conversational coach grounded in your own measurements and read against cited guideline reference bands: where a value sits and what that means, never a diagnosis. The coach proposes concrete if-then plans you accept or decline, and skips a proactive check-in when there is nothing new to say. Bring your own OpenAI or Anthropic key, or point at a local endpoint (Ollama, LM Studio, vLLM) so AI requests stay on your network.
+- **Doctor report and FHIR.** A doctor-report PDF rendered on your server, a read-only HL7 FHIR R4 API, and scoped, time-limited share links you revoke after the visit.
- **Private by construction.** AES-256-GCM encryption at rest for notes and other free text, documents, questionnaire answers, AI-written text and credentials, with zero-downtime key rotation. Measurement values, lab values, medication names and mood scores are stored unencrypted in the database, so the database volume and its backups need the same protection as the host. Passkey login, server-side sessions, strict CSP, and no third-party tracking anywhere.
- **Built to be lived in.** Installable PWA with offline support, medication reminders over APNs, Telegram, ntfy, and Web Push, a sub-second dashboard on years of imported history — in English, German, Spanish, French, Italian, Polish, Korean, Dutch, and Brazilian Portuguese end to end.
@@ -71,13 +71,13 @@ echo "POSTGRES_PASSWORD=$(openssl rand -hex 32)" >> .env
echo "ENCRYPTION_KEY=$(openssl rand -hex 32)" >> .env
echo "API_TOKEN_HMAC_KEY=$(openssl rand -hex 32)" >> .env
-# LAN / plain-HTTP host (NAS, homelab, Tailscale) — required, or login fails:
+# Required on a LAN or plain-HTTP host (NAS, homelab, Tailscale), or login fails:
echo "SESSION_COOKIE_SECURE=false" >> .env
docker compose up -d
```
-Open **http://localhost:3000** (or `http://:3000`) — the first registered user becomes admin. The compose file pulls a pre-built multi-arch image (`amd64` + `arm64`) from [GHCR](https://github.com/MBombeck/HealthLog/pkgs/container/healthlog); no build step required.
+Open **http://localhost:3000** (or `http://:3000`). The first registered user becomes admin. The compose file pulls a pre-built multi-arch image (`amd64` + `arm64`) from [GHCR](https://github.com/MBombeck/HealthLog/pkgs/container/healthlog); no build step required.
Verify the running version:
@@ -85,7 +85,7 @@ Verify the running version:
curl -s http://localhost:3000/api/version # returns version + buildSha + builtAt
```
-`SESSION_COOKIE_SECURE=false` is the one setting plain-HTTP self-hosts must set: without it the session cookie carries the `Secure` flag, the browser drops it over HTTP, and login silently fails. Leave it unset (or `true`) only when a TLS reverse proxy serves HTTPS — and then set `NEXT_PUBLIC_APP_URL` and `APP_URL` to the public URL first (see the [self-hosting guide](https://docs.healthlog.dev/getting-started/self-host-in-10-minutes/)).
+`SESSION_COOKIE_SECURE=false` is the one setting plain-HTTP self-hosts must set: without it the session cookie carries the `Secure` flag, the browser drops it over HTTP, and login silently fails. Leave it unset (or `true`) only when a TLS reverse proxy serves HTTPS, and in that case set `NEXT_PUBLIC_APP_URL` and `APP_URL` to the public URL first (see the [self-hosting guide](https://docs.healthlog.dev/getting-started/self-host-in-10-minutes/)).
Persistent state lives in the database volume (`postgres-data`) and in `backups`, which holds the nightly database dumps; the app container is stateless. An install from v1.45 or earlier moves its Postgres 16 database from `pgdata` into the new volume on the first start ([details](docs/ops/postgres-major-upgrade.md)); recovering a lost server is in [disaster-recovery.md](docs/ops/disaster-recovery.md).
@@ -95,7 +95,7 @@ NAS and homelab users have ready-made paths: [Unraid Community Applications](doc
One container hosts both the web server and the job worker by default; split them via `HEALTHLOG_PROCESS_TYPE=web|worker` for horizontal scale. Encrypted nightly backups to any S3-compatible bucket are opt-in through five `BACKUP_*` environment variables (the admin panel shows their status), new users join by invite link or QR code, and the stack works out of the box behind Caddy, Traefik, Nginx, or [Coolify](https://coolify.io/).
-HealthLog ships releases roughly weekly. Pin a tag, back up before upgrades, and skim the [CHANGELOG](CHANGELOG.md) before pulling `latest`. The operator manual — reverse proxy, migrations, encryption-key rotation, backup and restore — lives at [docs.healthlog.dev](https://docs.healthlog.dev).
+HealthLog ships often. Pin a tag, back up before upgrades, and read the [release notes](https://github.com/MBombeck/HealthLog/releases) before pulling `latest`. The operator manual (reverse proxy, migrations, encryption-key rotation, backup and restore) lives at [docs.healthlog.dev](https://docs.healthlog.dev).
## Native iOS app
@@ -109,18 +109,18 @@ Next.js 16 (App Router, React Server Components), TypeScript strict, PostgreSQL
## Documentation
-| | |
-| ---------------------- | ------------------------------------------------------------ |
-| User and operator docs | [docs.healthlog.dev](https://docs.healthlog.dev) |
-| Release history | [CHANGELOG.md](CHANGELOG.md) |
-| API contract | [`docs/api/openapi.yaml`](docs/api/openapi.yaml) |
-| Security policy | [SECURITY.md](SECURITY.md) |
-| Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) |
-| Translating | [CONTRIBUTING.md#translations](CONTRIBUTING.md#translations) |
+| | |
+| ---------------------- | --------------------------------------------------------------- |
+| User and operator docs | [docs.healthlog.dev](https://docs.healthlog.dev) |
+| Release history | [Release notes](https://github.com/MBombeck/HealthLog/releases) |
+| API contract | [`docs/api/openapi.yaml`](docs/api/openapi.yaml) |
+| Security policy | [SECURITY.md](SECURITY.md) |
+| Contributing | [CONTRIBUTING.md](CONTRIBUTING.md) |
+| Translating | [CONTRIBUTING.md#translations](CONTRIBUTING.md#translations) |
## Status
-Actively developed — new releases roughly weekly, issue reports and PRs welcome. Behaviour and schema can change between versions; migrations are forward-only.
+Actively developed, with frequent releases. Issue reports and pull requests are welcome. Behaviour and schema can change between versions; migrations are forward-only.
## Thanks
@@ -143,15 +143,15 @@ including anyone who found the defect without sending the fix.
## License
-HealthLog is licensed under the [PolyForm Noncommercial License 1.0.0](LICENSE): free to use, self-host, and modify for noncommercial purposes. Commercial use requires a separate agreement — open an issue or reach out via [healthlog.dev](https://healthlog.dev). Releases up to and including v1.15.18 were published under AGPL-3.0 and remain available under that license.
+HealthLog is licensed under the [PolyForm Noncommercial License 1.0.0](LICENSE): free to use, self-host, and modify for noncommercial purposes. Commercial use requires a separate agreement; open an issue or reach out via [healthlog.dev](https://healthlog.dev). Releases up to and including v1.15.18 were published under AGPL-3.0 and remain available under that license.
---
- healthlog.dev ·
- Live Demo ·
- Docs ·
- iOS App Store ·
- iOS TestFlight ·
+ healthlog.dev |
+ Live Demo |
+ Docs |
+ iOS App Store |
+ iOS TestFlight |
Buy Me a Coffee
From 3ba5efb025b3c50abd69649ee0ccbd341435298c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Wed, 11 Nov 2026 22:27:22 +0100
Subject: [PATCH 12/20] chore(scripts): drop private host names from the demo
tooling
The demo migration was named after the host it targets, and its
README, the v1.5 demo seed and a contributing note named the
maintainer's servers, a container id, an app id and two account ids.
The script already takes every host, container and account from its
environment, so the prose now says source and target and lists the
variables instead. It is renamed to migrate-demo-account.sh; the
password guard follows the new name.
---
CONTRIBUTING.md | 2 +-
scripts/README.md | 2 +-
.../demo-published-password-guard.test.ts | 8 +-
...ADME.md => migrate-demo-account.README.md} | 85 ++++++-----
...demo-edge01.sh => migrate-demo-account.sh} | 144 +++++++++---------
scripts/seed-demo-v15.sql | 6 +-
6 files changed, 124 insertions(+), 123 deletions(-)
rename scripts/{migrate-demo-edge01.README.md => migrate-demo-account.README.md} (72%)
rename scripts/{migrate-demo-edge01.sh => migrate-demo-account.sh} (88%)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f3bf2d402..157eb8949 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -185,7 +185,7 @@ If you want to introduce a locale that isn't shipped (e.g. `sv`, `cs`):
### Long-term
-Fink covers browser editing today. It edits the files in this repository directly and keeps no copy of the translations anywhere else. If the community outgrows it, a self-hosted [Weblate](https://weblate.org/) instance remains the fallback. Tracking issue: `v1.6 — Weblate self-hosted on edge-01`.
+Fink covers browser editing today. It edits the files in this repository directly and keeps no copy of the translations anywhere else. If the community outgrows it, a self-hosted [Weblate](https://weblate.org/) instance remains the fallback.
## Code of Conduct
diff --git a/scripts/README.md b/scripts/README.md
index 34949a93e..4f7b49c39 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -37,7 +37,7 @@ typecheck.
- **`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-edge01.sh`**: copies the demo account from one instance to a separate demo instance; see its README.
+- **`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
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/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-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;
From aa29d1b43274e9664cafa2c6488732c0fb816680 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?=
Date: Wed, 11 Nov 2026 22:28:39 +0100
Subject: [PATCH 13/20] docs(ui): align the UI guidelines with the binding
standards
The guidelines still described the v1.4 app: a drawer nobody uses, six
languages instead of ten, a hover tooltip as the default inline help
although a tooltip never opens on a phone, Dracula utilities for status
colours, a page container and a Settings body gutter the standards
removed, and components that do not exist (a status callout, a
multi-select, honey-pot fields, a per-integration test log). Where the
UI standards own a topic, the page now points there; the rest (button
choice, form controls, settings persistence, filter bars, the dialog or
sheet choice, loading, empty and error states, accessibility, i18n,
motion) is kept and corrected, with the section numbers code comments
cite unchanged. Inline help is an InfoPopover that opens on a tap.
---
docs/ui-guidelines.md | 569 +++++++++++++++---------------------------
1 file changed, 196 insertions(+), 373 deletions(-)
diff --git a/docs/ui-guidelines.md b/docs/ui-guidelines.md
index 5b00604e2..c4176a17c 100644
--- a/docs/ui-guidelines.md
+++ b/docs/ui-guidelines.md
@@ -1,104 +1,30 @@
# HealthLog UI Guidelines
-> Visual & interaction patterns in the HealthLog PWA. The binding rules
-> for card anatomy, spacing, text colour, tokens and primitives live in
-> [`contributing/ui-standards.md`](./contributing/ui-standards.md); where
-> this document disagrees with them, the standards win. If a pattern
-> below doesn't fit your case, propose an addition in the same PR.
-
-Last updated: 2026-05-08 (v1.4 cycle)
+> Interaction patterns for the HealthLog web app. Card anatomy, spacing,
+> text colour, colour tokens, typography and the choice of primitive are
+> owned by [`contributing/ui-standards.md`](./contributing/ui-standards.md);
+> this page covers what those rules leave open. Where the two disagree, the
+> standards win. If a pattern below doesn't fit your case, propose an
+> addition in the same PR.
---
## 1. Foundations
-### 1.1 Theme
+Theme, spacing, typography, radius and colour tokens are defined in the
+standards (§1 to §5 there). Two things to add:
-HealthLog is a Dracula-themed app with a light counterpart (Alucard). Both
-themes share the same six-color signal palette (purple, pink, green, cyan,
-orange, red, yellow, lavender), exposed as CSS variables and as Tailwind
-`dracula-*` color tokens. Use the semantic tokens (`--primary`,
-`--destructive`, `--card`, …) for chrome and the `--dracula-*` tokens for
-data viz only.
-
-| Surface | Token |
-| ---------------------------- | ------------------ |
-| App background | `--background` |
-| Card / popover surface | `--card` |
-| Primary action | `--primary` |
-| Destructive action | `--destructive` |
-| Secondary / muted background | `--secondary` |
-| Border | `--border` |
-| Focus ring | `--ring` |
-| Chart series 1–5 | `--chart-1`…`-5` |
-| Brand signal: purple | `--dracula-purple` |
-| Brand signal: green (good) | `--dracula-green` |
-| Brand signal: orange (warn) | `--dracula-orange` |
-| Brand signal: red (alert) | `--dracula-red` |
-| Brand signal: cyan (info) | `--dracula-cyan` |
-
-Dark mode is the default. The theme switcher in the sidebar three-dot menu
-toggles `.dark` / `.light` on `:root`. Always test both modes before
-shipping a component.
-
-### 1.2 Spacing
-
-Tailwind's default 4-pixel scale. Reach for these stops first:
-
-| Use | Token | Pixels |
-| ------------------------------------ | ------------- | ------ |
-| Inline gap between icon and label | `gap-1` | 4 |
-| Tight stack inside a card | `gap-2` | 8 |
-| Label-to-field stack (form fields) | `space-y-2` | 8 |
-| Section block (heading + body) | `space-y-4` | 16 |
-| Page section separator | `space-y-6` | 24 |
-| Dashboard tile-row gap, hero spacing | `gap-4` | 16 |
-| Page-edge padding (mobile / desktop) | `px-4 / px-6` | |
-
-Avoid arbitrary `space-y-5`, `gap-3.5`, etc. unless aligning to a fixed
-external constraint.
-
-### 1.3 Typography
-
-The app uses the Tailwind defaults inherited via shadcn/ui. Reach for
-these classes first:
-
-| Use | Class |
-| ---------------------------------- | --------------------------------------- |
-| Page title (h1) | `text-2xl font-semibold tracking-tight` |
-| Section heading / section card | `text-lg font-semibold` |
-| Tile / inner card title | `text-base font-semibold` |
-| Body text | `text-sm` |
-| Muted helper / metadata | `text-sm text-muted-foreground` |
-| Microcopy / footer | `text-xs text-muted-foreground` |
-| Tabular data (latency, large nums) | `font-mono tabular-nums` |
-
-Card titles come in exactly two sizes: `text-lg font-semibold` for a
-section-level card (one per page region) and `text-base font-semibold`
-for tiles and inner cards. Don't introduce a third level — a smaller
-heading is a sign the content wants a list row, not a card.
-
-Never use `font-bold` for emphasis — bump to `font-semibold` instead and
-let the design system breathe.
-
-### 1.4 Radius and elevation
-
-`--radius: 0.625rem` (10px) is the default. The Tailwind tokens
-`rounded-md` (`--radius-md` ≈ 8px) and `rounded-lg` (`--radius` ≈ 10px)
-cover 95% of cases. Use `rounded-2xl` only for hero cards or large
-modals.
-
-Elevation is achieved via `shadow-xs`, `shadow-sm`, or
-`shadow-[0_1px_0_rgba(255,255,255,0.04)]` for very subtle separators on
-dark backgrounds. Avoid Tailwind's heavy shadows (`shadow-lg`+) in chrome.
+- Dark is the default theme, and the light theme remaps the same semantic
+ tokens. Check every component in both before shipping it.
+- Elevation stays light: `shadow-xs` or `shadow-sm` in chrome, never the
+ heavy `shadow-lg` and up.
---
## 2. Components
-This section is **prescriptive, not exhaustive**. Each entry pairs a
-Radix/shadcn primitive with the cases it's the right answer for and the
-cases where another primitive should win.
+This section is **prescriptive, not exhaustive**. Each entry names the
+primitive for a case and the case where another one wins.
### 2.1 Buttons
@@ -114,43 +40,35 @@ Every button comes from `@/components/ui/button`. Pick by purpose:
| Inline link to external help docs | `link` | `sm` |
| Toolbar / table row action when space is tight | `outline` | `xs` |
-**Position rules.**
+Where buttons sit in a card (one action row, last in the card, primary on
+the right) is standards §12. Two additions:
-- Forms (full-width or modal): primary on the **right**, cancel/back on
- the **left**. Match shadcn dialog defaults.
-- Settings sections: a single sticky `Save` button at the **right end**
- of the section header _or_ per-row inline `Save`. Never both in the
- same section.
-- Test buttons (Telegram test, AI ping, etc.): immediately to the right
- of the related Save / Apply control. Never below — keeps the eye
- motion horizontal.
-- Destructive actions live in their own confirm dialog with a
- type-to-confirm input when the action is irreversible (delete account,
- revoke API token, drop user data).
+- A Test button (Telegram test, AI ping and the like) sits in the same row
+ as the Save or Apply control it belongs to.
+- An irreversible action (delete account, revoke an API token, wipe data)
+ asks in its own confirm dialog, with a type-to-confirm field where the loss
+ is large.
-**Disabled state.** A disabled button must always come with a tooltip or
-helper text explaining why. Silent disabled buttons frustrate users.
+**Disabled state.** A disabled button always comes with helper text that
+says why. A tooltip is not enough: it never opens on a phone.
### 2.2 Inputs and forms
Forms keep a controlled draft in component state and validate it with the
Zod schemas from `src/lib/validations/`; the server's schema is the authority.
-Wrap inputs in `` (visible) plus `aria-describedby` for helper text.
+Every input has a visible ``, plus `aria-describedby` for helper text.
-- **Text / number / email**: ` `. Always set `autocomplete`. See §4.4.
-- **Multi-line**: shadcn `