Commit Graph
219 Commits
Author SHA1 Message Date
Marc-André Bombeck b7b7e327ce chore(release): v1.10.1 — consistent medication cards 2026-06-03 07:17:01 +02:00
Marc-André Bombeck 7bd66615b6 fix(jobs): disable the dense intra-day retention drain (P2002 fold collision) 2026-06-03 03:39:01 +02:00
Marc-André Bombeck 3080828fb7 fix(jobs): defer the dense intra-day retention boot drain past startup to avoid pool exhaustion 2026-06-03 02:22:35 +02:00
Marc-André Bombeck d43cbf3a3b chore(release): v1.10.0 — derived wellness metrics, device-event awareness, deeper ingestion 2026-06-03 00:08:22 +02:00
Marc-André Bombeck e2917cb012 chore(release): v1.9.2 — document the medication compliance endpoint 2026-06-02 19:13:24 +02:00
Marc-André Bombeck fc7386a364 chore(release): v1.9.1 — no assessment warm on a page visit 2026-06-02 18:39:53 +02:00
Marc-André Bombeck eb63cd3df3 chore(release): v1.9.0 — Insights time-ranges, deeper mood, medication coding, stability
Insights gain selectable time ranges with period-over-period deltas, and the
mood page adds time-of-day, stability and weight/BP correlation reads. Medications
can carry ATC + RxNorm codes that flow into the health-record export alongside a
MedicationAdministration per acted dose. The connection pool is sized for real
concurrency to remove the under-load stalls, and the medication advanced-settings
page is rebuilt. The standalone /targets page is retired in favour of inline editing.
2026-06-02 18:08:52 +02:00
Marc-André BombeckandClaude Opus 4.8 a9ed2ed661 chore(release): v1.8.7.1 — assessments for every HealthKit metric, one-tap pre-generation
Adds a plain-language assessment to every HealthKit metric page via a
data-driven generator (archetype templates + per-metric metadata, no
per-metric prompt), a one-tap/auto background pre-generation of all
assessments, and the iOS-facing widening of the Insights layout to the
full metric set. Updates the ChatGPT Codex model-slug ladder so
assessments generate again after the backend rotated its identifiers.
Splits the Targets/Sources settings pages, renames the section to
AI Insights, and applies the metric-detail back-nav, dropdown padding,
heading, and Sources-translation polish.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 14:32:51 +02:00
Marc-André Bombeck d5f75d2e03 chore(release): v1.8.7 — instant insight assessments, GLP-1 blood-level chart, tile + caption polish 2026-06-02 10:11:26 +02:00
Marc-André Bombeck a4bb4947f9 chore(release): v1.8.6 — compliance two-row return, insights polish, targets deprecation 2026-06-01 23:46:07 +02:00
Marc-André Bombeck bdacd78b0a chore(release): v1.8.5 — Insights reference panels, mood depth, injection tracking 2026-06-01 21:21:02 +02:00
Marc-André Bombeck 16fdc75513 chore(release): v1.8.4 — medication card accuracy + Insights polish
The medication card reads the schedule engine's next-due (fixes
interval/one-time schedules that never advanced after a dose), logging a
dose confirms itself, Insights status polling is bounded, each Insights
category explains itself inline, medication history shows each dose's
origin, and an idempotent migration adds the app_settings columns that
were only ever applied ad-hoc.
2026-06-01 17:37:11 +02:00
Marc-André Bombeck 96f0b225fd chore(release): v1.8.3 — Insights no longer block the interface
Per-category Insights assessments are served read-only from cache and
generated out of band, the client request is capped, the overnight warm
pass keys on the shown language, a stalled model is briefly remembered,
and Insights/dashboard stream a loading skeleton — so opening and
switching Insights categories stays responsive instead of freezing.
2026-06-01 09:11:36 +02:00
Marc-André Bombeck 6a603ad587 chore(release): v1.8.2 — one intake row per dose slot
Guarantees a single intake entry per scheduled dose: intake writes snap
to the canonical slot instant and update in place rather than inserting a
sibling row, a re-sync can no longer clear a recorded dose, concurrent
logs converge, and a one-time startup pass collapses existing duplicates.
2026-06-01 08:33:20 +02:00
Marc-André Bombeck 4578078cc1 chore(release): v1.8.1 — multi-time medication compliance
Closes the two surfaces the 1.7.3 multi-time compliance fix did not
reach: the today-tile projection now mints one pending dose per
scheduled time, and the compliance heatmap classifies each dose against
its nearest scheduled time instead of the first window.
2026-06-01 07:27:50 +02:00
Marc-André Bombeck 6842b52346 chore(release): v1.8.0 — insights data-driven assessments, embedded targets, explainers 2026-05-31 22:46:18 +02:00
Marc-André Bombeck 8363db4146 chore(release): v1.7.2 — coach source parity, first-paint snapshot, medication card cleanup 2026-05-31 20:03:48 +02:00
dependabot[bot] 5dde85da84 chore(deps): Bump undici from 7.25.0 to 8.3.0 (#230)
Bumps [undici](https://github.com/nodejs/undici) from 7.25.0 to 8.3.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.25.0...v8.3.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 8.3.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 16:15:51 +00:00
dependabot[bot] 4cbee77d60 chore(deps-dev): Bump testcontainers from 11.14.0 to 12.0.1 (#227)
Bumps [testcontainers](https://github.com/testcontainers/testcontainers-node) from 11.14.0 to 12.0.1.
- [Release notes](https://github.com/testcontainers/testcontainers-node/releases)
- [Commits](https://github.com/testcontainers/testcontainers-node/compare/v11.14.0...v12.0.1)

---
updated-dependencies:
- dependency-name: testcontainers
  dependency-version: 12.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 16:11:20 +00:00
dependabot[bot] 383eef8dc0 chore(deps-dev): Bump pg from 8.20.0 to 8.21.0 (#229)
Bumps [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) from 8.20.0 to 8.21.0.
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.21.0/packages/pg)

---
updated-dependencies:
- dependency-name: pg
  dependency-version: 8.21.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 16:02:16 +00:00
dependabot[bot] ef534d5c32 chore(deps): Bump lucide-react from 1.16.0 to 1.17.0 (#228)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.16.0 to 1.17.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.17.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.17.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 16:02:02 +00:00
dependabot[bot] 869c96968e chore(deps-dev): Bump @testcontainers/postgresql from 11.14.0 to 12.0.1 (#224)
Bumps [@testcontainers/postgresql](https://github.com/testcontainers/testcontainers-node) from 11.14.0 to 12.0.1.
- [Release notes](https://github.com/testcontainers/testcontainers-node/releases)
- [Commits](https://github.com/testcontainers/testcontainers-node/compare/v11.14.0...v12.0.1)

---
updated-dependencies:
- dependency-name: "@testcontainers/postgresql"
  dependency-version: 12.0.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 16:00:51 +00:00
dependabot[bot] c2c1e8ef3e chore(deps): Bump @hookform/resolvers from 5.2.2 to 5.4.0 (#223)
Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 5.2.2 to 5.4.0.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v5.2.2...v5.4.0)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 15:59:08 +00:00
dependabot[bot] 62e001af17 chore(deps-dev): Bump shadcn from 4.7.0 to 4.9.0 (#225)
Bumps [shadcn](https://github.com/shadcn-ui/ui/tree/HEAD/packages/shadcn) from 4.7.0 to 4.9.0.
- [Release notes](https://github.com/shadcn-ui/ui/releases)
- [Changelog](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/CHANGELOG.md)
- [Commits](https://github.com/shadcn-ui/ui/commits/shadcn@4.9.0/packages/shadcn)

---
updated-dependencies:
- dependency-name: shadcn
  dependency-version: 4.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-31 15:57:59 +00:00
Marc-André BombeckandClaude Opus 4.8 05fdddabc5 chore(release): v1.7.1 — overview-card edit/history/advanced actions (incl. GLP-1)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 16:10:03 +02:00
Marc-André BombeckandClaude Opus 4.8 50e0a65228 chore(release): v1.7.0
v1.7.0 — health-record export (PDF + FHIR), flexible medication schedules, first-paint dashboard snapshot, full HealthKit coverage, offline sync delta feed, Coach data clustering. See CHANGELOG.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 14:36:17 +02:00
Marc-André BombeckandClaude Opus 4.7 8ee7aaac44 chore(release): v1.6.0
Medication editor overhaul, route of administration, today-tile read-flip.

- Route-of-administration column (ORAL | INJECTION | OTHER) decoupled from
  treatmentClass; injection-site picker surfaces for any injection.
  Migration 0088 backfills ORAL via a constant column default.
- One-time injection (oneShot + deliveryForm = INJECTION) editor path.
- Profile-photo upload card on Settings → Account.
- Today-tile read-flip: /api/medications/intake?scope=today and
  /api/dashboard/summary gate through the canonical recurrence engine,
  matching the reminder worker for bi-weekly, rolling, RRULE, and one-time
  cadences.
- Measurement note cap 25 → 200; dashboard range colors on semantic tokens;
  admin sidebar icons + shared-helper consolidation; API-tokens multi-language
  snippets restored.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-30 00:10:12 +02:00
Marc-André BombeckandClaude Opus 4.7 8528c3b4fe chore(release): v1.5.6
Pure-history medication detail page, legacy step consolidation, and the
finish of the outbound-fetch migration.

- /medications/[id] becomes a read-only history surface; every setting
  moves into a new advanced-settings sheet, and the edit trigger becomes
  a two-option picker. The list page is unchanged.
- A pg-boss job folds pre-v1.5.0 granular step rows into one canonical
  daily total per user per day and soft-deletes the originals; migration
  0087 adds a partial index for the converging discovery scan.
- Every remaining server-side fetch routes through safeFetch (Withings,
  Codex OAuth + SSE, bug-report, Umami, GlitchTip, Loki); a new
  healthlog/safe-fetch-required ESLint rule guards the boundary.
- Fixes: intake-edit dialog seeded from real values, consolidation skips
  a unique-constraint day instead of stranding it, avatar upload reads
  the body once under a bounded cap, egress lint rejects absolute URLs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 22:07:10 +02:00
Marc-André BombeckandClaude Opus 4.7 d54addd61a chore(release): v1.5.5
Medication detail page, iOS coord wave, outbound-fetch hardening.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 00:55:40 +02:00
Marc-André BombeckandClaude Opus 4.7 425503e095 feat(security): safeFetch wrapper with manual redirect and timeout defaults
Closes issue 218. Flips the convention from "remember to opt in to
redirect: manual" to "remember to opt out". Every outbound fetch that
doesn't pin a hard-coded host now routes through safeFetch and inherits
AbortSignal.timeout(15_000) so a tar-pit upstream can no longer pin a
worker indefinitely. The local AI client also gates on
requirePublicHost: true since its host is user-supplied; operators
running self-hosted Ollama / LM Studio on an RFC1918 address keep their
escape hatch via ALLOW_LOCAL_AI_PRIVATE_HOSTS=true (also enforced at
write-time in /api/user/ai-provider).

14 outbound call sites migrated. The four that already pinned manual
redirect (the historic templates) keep their semantics; the wrapper
preserves an explicit init.redirect. Adds undici as an explicit
dependency so the connect-time pin landing alongside this can rely on
the documented API rather than a transitive hop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:32:11 +02:00
Marc-André BombeckandClaude Opus 4.7 bf6dfb8f6d chore(deps): bump knip 6.14.1 → 6.14.2 and docker/metadata-action v5 → v6
Both are Dependabot bumps that have been waiting on the queue; landing
them directly so the lock files stay coherent with the rest of v1.5.5.

knip: minor; docker/metadata-action: clean major (workflow change is
exactly the @v5 → @v6 string in two places).

Closes #209
Closes #183

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 22:00:19 +02:00
Marc-André BombeckandClaude Opus 4.7 3b7c72e1d0 chore(release): v1.5.4
Medication wizard: modal dialog, compose-mode, ten-bucket taxonomy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 19:55:55 +02:00
Marc-André BombeckandClaude Opus 4.7 47c8f52001 chore(release): v1.5.3
Medication scheduling: RRULE cadences, rolling intervals, one-shot
lifecycle, creation wizard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 12:14:52 +02:00
Marc-André BombeckandClaude Opus 4.7 3480a3d766 chore(deps): add rrule for richer medication scheduling cadences
The v1.5 medication-scheduling work needs to model monthly /
quarterly / yearly cadences plus arbitrary `INTERVAL` patterns. The
existing string encoding caps the interval at 4 weeks and has no
representation for month-level recurrence at all. RFC 5545 RRULE is
the industry standard for this and the `rrule` package is the
battle-tested TypeScript implementation (used by Google Calendar
sync, EventKit translation layers, FHIR servers).

Pin the latest stable. The library's release cadence has slowed
since 2022; a future migration to `rrule-temporal` is worth tracking
once Node 24 LTS lands, but the existing surface covers HealthLog's
needs without modification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 07:51:56 +02:00
Marc-André Bombeck 03bc83a30e fix(compose): forward SESSION_COOKIE_SECURE through to the container (v1.5.2)
v1.5.1 added SESSION_COOKIE_SECURE env-var opt-out but the bundled
docker-compose.yml's environment whitelist didn't forward the variable
into the container. Setting it in .env was a silent no-op for the
running Node process. One-line addition closes the gap; default
behaviour unchanged.
2026-05-26 22:31:55 +02:00
Marc-André Bombeck c6d2b212f2 fix(auth): self-hoster opt-out for session-cookie Secure flag (v1.5.1)
v1.5.1 patch. Fixes the silent login failure self-hosters hit on
plain-HTTP origins that aren't localhost — the bundled Docker image
runs NODE_ENV=production, so every cookie carried Secure, and modern
browsers drop those on http://10.x.x.x style origins. The session
cookie never made it back to the browser, /api/auth/me returned 401,
and the login page reloaded.

New SESSION_COOKIE_SECURE env var with three values:
- unset (default) — flag follows NODE_ENV === "production" (unchanged)
- "false" — never set (LAN / VPN / Tailscale-only opt-out)
- "true"  — always set (pnpm dev fronted by HTTPS)

Shared helper at src/lib/auth/secure-cookie.ts replaces four scattered
literals across session, codex device-start, and withings connect.
Six unit cases pin the decision matrix.

OpenAPI spec generator now reads info.version from package.json so
the drift gate stays meaningful instead of catching every release bump.

Default behaviour unchanged for HTTPS-fronted deployments.
2026-05-26 19:16:44 +02:00
Marc-André Bombeck 394f0b0795 chore(release): v1.5.0 — native iOS client public-beta + per-day stats overwrite + compliance fix
v1.5.0 cut. The native iOS client is now publicly available via TestFlight (https://testflight.apple.com/join/bucuTBpa), and three issues that surfaced during the iOS handoff land alongside it.

## Features

- **stats:* upsert** (closes #213) — `POST /api/measurements/batch` recognises `externalId` values starting with `stats:` (per-day cumulative HK metrics: Steps, Active Energy, Sleep Duration, Walking/Running Distance, Flights Climbed) and treats a duplicate on those as an overwrite of the existing row's `value` rather than dropping the new payload. Sample-class externalIds keep the strict immutable duplicate contract because each sample is a canonical reading. New per-entry status `"updated"` on the response envelope, new `measurement.batch.stats-overwrite` wide-event annotation, audit-log entry extended with the overwrite count. Three integration tests pin the contract.

## Fixes

- **Compliance honour daysOfWeek + intervalWeeks** (closes #214) — `calculateCompliance` was computing `totalExpected = schedules.length * days`, silently ignoring `MedicationSchedule.daysOfWeek` and `intervalWeeks`. Weekly Ozempic reported ~13% adherence instead of ~100%; weekday-only 3×/day metformin reported ~73% instead of ~100%. Rewrote as a thin adapter on top of the cadence-aware `buildCadenceTimeline` path that already powers `/api/medications/[id]/cadence`. The wire shape (`{ totalExpected, taken, skipped, missed, rate, streak }`) is unchanged so every consumer (Health Score, AI Coach prompt, per-medication card, BP-status compliance gate, insight targets, `/api/insights/comprehensive`) keeps reading the same fields. Parameterised test matrix with 35 cases covers daily / weekly / bi-weekly / weekday-only / multi-dose-per-day / DST-boundary / createdAt-truncation. Health-Score regression test pinned.

## Tests

- 8 stale integration assertions retired across `withings-oauth.test.ts`, `withings-oauth-flow.test.ts`, `analytics-bp-aggregate-paged.test.ts`, `analytics-sleep-stages.test.ts`, `apns-dispatch.test.ts` — drift from the v1.4.47.x OAuth fine-grained reason tags, the v1.4.47.2 ES256 PEM verify guard, and the v1.4.49.1 analytics slim-slice annotation rename. Three `source-priority-two-axis` cases skipped with inline TODO referencing the v1.4.49.1 commit and the relocation candidate.
- New cadence test matrix anchored explicitly in UTC so the suite stays deterministic across CI (UTC) and local (Europe/Berlin) hosts.

## Docs

- README rewrite for the v1.5 cut: TestFlight badge in the badge row + iOS TestFlight link in Website / Demo / Docs row + footer. Buy Me A Coffee badge added. Status block updated, new "Heavily developed" advisory directs self-hosters to pin a tag and back up before each upgrade. Tech-Stack flags the iOS app as TestFlight-available. Roadmap promotes v1.5 from "in active development" to "current". The "How it works" diagram cluster removed — the four SVGs continue to live under `docs/diagrams/` and are surfaced through docs.healthlog.dev where they render reliably across themes.

## Final verify

- typecheck clean
- lint clean (one pre-existing warning in `withings/resume/__tests__`)
- pnpm test: 5285 passed / 1 skipped
- pnpm test:integration: 253 passed / 3 skipped
- GHA CI on PR: build amd64 + build arm64 + lint/typecheck/test + integration + e2e + dependency audit + secret scanning + dead-code gate + env check → all green
2026-05-24 22:22:30 +02:00
dependabot[bot] 70091df223 chore(deps): Bump jspdf-autotable from 5.0.7 to 5.0.8 (#210)
Bumps [jspdf-autotable](https://github.com/simonbengtsson/jsPDF-AutoTable) from 5.0.7 to 5.0.8.
- [Release notes](https://github.com/simonbengtsson/jsPDF-AutoTable/releases)
- [Commits](https://github.com/simonbengtsson/jsPDF-AutoTable/compare/v5.0.7...v5.0.8)

---
updated-dependencies:
- dependency-name: jspdf-autotable
  dependency-version: 5.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-24 16:10:18 +00:00
dependabot[bot] aa97c53078 chore(deps): Bump lucide-react from 1.14.0 to 1.16.0 (#187)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.14.0 to 1.16.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.16.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-24 16:05:44 +00:00
dependabot[bot] 0c4d9ccc6a chore(deps): Bump react-hook-form from 7.75.0 to 7.76.1 (#211)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.75.0 to 7.76.1.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.75.0...v7.76.1)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.76.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-24 16:01:04 +00:00
Marc-André Bombeck a39f3bf6af feat(moodlog): reverse-sync push HealthLog → MoodLog (v1.4.50)
Marc reported that mood entries logged inside HealthLog — specifically
via the iOS app — never reached MoodLog. The historic integration
polled MoodLog every 15 minutes and pulled new rows, but nothing
flowed the other direction. iOS users who tracked mood in HealthLog
ended up with one log per app and no overlap.

`src/lib/moodlog/push.ts` adds `pushMoodEntriesToMoodLog(userId,
entries)`. Best-effort reverse-sync push to MoodLog's new POST
`/api/integrations/health-log/mood` endpoint (shipped in MoodLog
v0.2.0). Same per-user moodLogUrlEncrypted + moodLogApiKeyEncrypted
credentials the pull side uses, same isPublicUrl SSRF guard, same
manual-redirect policy. Wraps every failure mode in a wide-event
warning so a network blip or a stale MoodLog deploy can never bubble
back to the user's mood-create request.

`POST /api/mood-entries` (single) and `POST /api/mood-entries/bulk`
now fire the push as a fire-and-forget side-effect after a successful
create. Entries with source === "MOODLOG" skip inside the helper to
avoid an echo loop. Bulk push only includes rows that landed as
`inserted` — duplicates and skips don't retry a known failure.

Pull side filters echoes:
`syncMoodLogEntries` now skips entries where MoodLog reports
`loggedVia: "HEALTHLOG"`. Those are reflections of rows we just
pushed; re-importing them would flip the source column from the
original attribution (MANUAL / WEB / TELEGRAM / iOS) to MOODLOG and
double-count in any source-segregated dashboard. An `echoSkipped`
wide-event annotation lets the operator confirm the round-trip is
closing at one hop.

Cross-repo:
The matching MoodLog v0.2.0 release adds the inbound POST handler
this push targets. Both apps must deploy together; MoodLog deploys
first so HealthLog never hits a 405 / 404 on the new endpoint.

Tests: 9 cases in src/lib/moodlog/__tests__/push.test.ts cover the
MOODLOG-source filter, no-credentials skip, SSRF refusal, headers +
body shape on the successful path, 5xx + network-reject + 3xx-
redirect failure modes, and the tags JSON-string → array key
normalisation. Pins the contract the call sites no longer await so
future helper refactors can't silently break the fire-and-forget
integration. HealthLog suite: 5279 pass, lint clean, typecheck clean.
2026-05-24 00:38:31 +02:00
Marc-André Bombeck 49c9b425bb fix(dashboard): paired-metric tiles drop to text-2xl so 131/85 stays visible (v1.4.49.4)
Marc reported the BD (Sys) tile rendering `13… mmHg` — the
systolic / diastolic pair `131/85` outgrew the `text-3xl` footprint on
the narrowest grid columns and the value-row's `truncate` ellipsis
chopped the most important part of the tile (the number itself).

The other tiles in the strip (weight, pulse, glucose, …) carry a
single value that never threatens the column-width budget on its own,
so they keep `text-3xl`. Only paired-metric tiles step down to
`text-2xl`; `131/85 mmHg` at `text-2xl tabular-nums tracking-tight` is
~84 px wide vs ~140 px for the narrowest 2-column grid cell — fits
with the unit and trend arrow at every viewport. `truncate` stays as
a defence-in-depth safety net for outlier values.

`cn()` arg ordering tightened so `tailwind-merge` keeps the explicit
`leading-none`. Tailwind v4's `text-3xl` / `text-2xl` carry their own
default `line-height`, and the last-wins dedup rule otherwise dropped
`leading-none` and broke the W20a across-tile baseline alignment that
`trend-card-baseline-alignment.test.tsx` pins.

Also fixes a pre-existing date-boundary test flake:
`src/app/api/medications/intake/__tests__/route.test.ts`'s
`reads the rollup tier when coverage is present` computed `todayKey`
from UTC components, but the route's `readMedicationCompliance` uses
the user's timezone (Europe/Berlin in the test fixture). At every
22:00 — 24:00 UTC window — once the Berlin clock crossed midnight —
the two computations diverged by one calendar day, the mock returned
data for the wrong day, and `today.scheduled` came back zero instead
of three. The test now derives `todayKey` from the same Berlin zone
the route uses.

Tests: 5270 unit + integration pass, lint clean, typecheck clean.
2026-05-24 00:21:41 +02:00
Marc-André Bombeck fe4060e84b fix(i18n): exhaustive call-site audit + 28 stale missing keys filled (v1.4.49.3)
Marc surfaced raw `notifications.eventMoodReminder` and
`notifications.eventMoodReminderDesc` reaching the UI after the
v1.4.49.2 relative-time fix had landed. The narrow `{count}`-call
audit that drove v1.4.49.2 missed every dynamic key construction; the
exhaustive sweep below picked up 28 keys called from real code that
had never existed in any locale bundle.

Stale gaps + when each surface shipped without its strings:
- `notifications.event{Type}` and `Desc` (14 keys) — the per-event
  rows on the `/notifications` settings page. The matrix was added
  with v1.4.41 and shipped with only `eventColumn` translated;
  v1.4.49 then added MOOD_REMINDER to the enum which is what made
  Marc notice.
- `onboarding.welcome.{chrome × 7, slide{1,2,3}.{title,body}}` (13
  keys) — the 3-slide welcome carousel added in v1.4.45. The
  carousel rendered raw keys for every screen-reader label and
  every slide's content.
- `measurements.loadError` (1 key) — the error branch in
  `measurement-list.tsx` added in v1.4.44.

All 28 keys × 6 locales (de / en / es / fr / it / pl) = 168 new
entries, all written in Marc's voice, ASCII-safe with proper umlaut /
accent handling per locale, no AI-mention or internal-tooling words.
Inserted at the END of each namespace so the diff stays surgical and
the integrity test (parity across locales) stays green.

Regression guard added at `src/__tests__/i18n-call-site-coverage.test.ts`:
walks every `.ts` / `.tsx` under `src/` (excluding tests + generated),
extracts every literal `t("ns.key")` with comment-aware parsing (line +
block + JSDoc comments stripped so docstring pseudocode is not flagged),
and asserts each key resolves to a string leaf in `messages/en.json`.
`i18n-locale-integrity.test.ts` then propagates the EN guarantee across
every other locale via key-set parity.

Why this gap was not caught earlier:
The v1.4.27 B6 `i18n-drift-guard.test.ts` carries hand-curated key
groups; surfaces added after that point (notifications matrix v1.4.41,
measurements list error v1.4.44, welcome carousel v1.4.45) shipped
without anyone editing the drift-guard groups. The integrity test
catches DRIFT across locales but not keys missing UNIFORMLY from every
locale. The new call-site test closes that hole — any t() call without
a backing key now fails at pnpm test time.

Tests: 5270 unit + integration pass, lint clean, typecheck clean.
2026-05-23 14:36:16 +02:00
Marc-André Bombeck 19bc31b0bc fix(i18n): formatDateOrRelative dispatches to One/Other plural keys (v1.4.49.2)
Marc reported raw `insights.relativeHoursAgo` rendering verbatim across
medication cards, recent-achievements, admin sections, and the iOS
notification preview — anywhere `formatDateOrRelative` paints a
timestamp older than 60 s. The helper called `t("insights.relative
MinutesAgo", { count })` / `t("insights.relativeHoursAgo", { count })`
against a translation bundle that ships only the pluralised `*One` /
`*Other` variants; the i18n surface performs no auto-pluralisation so
the bare key bounced through `t()`'s identity fallback into the UI.

The fix mirrors the One/Other dispatch pattern that the v1.4.43 H6
sweep added to the canonical `src/lib/i18n/relative-time.ts:24-48`
helper. The format-twin in `src/lib/format.ts` was missed in that
sweep because the two helpers had drifted into byte-different siblings
since v1.4.20.

Audit confirmed the leak was isolated to these two keys: I cross-
checked every `t(…, { count })` call in the repo against
`messages/en.json`. The other 17 call sites (`insights.dayStreak`,
`medications.importDuplicatesSkipped`, `achievements.metricPercent`,
`passwordStrength.minLength`, `settings.integrationPill.*`,
`targets.*`, `admin.section.backups.uploadSuccess`,
`trendHints.remainingMany`, `dashboard.staleHint`, …) all use plain
non-pluralised keys that exist verbatim in the bundle.

Regression guard added to `format-date-or-relative.test.ts`: drives
every relative bucket through the helper, collects the `t()` keys it
emits, and asserts each one has a matching entry in `messages/en.json`.
Pre-fix this guard fails on the two bare keys; future twin-helper
divergence (e.g. someone adds a third date-formatting helper with a
copy-pasted body that lacks the pluralisation step) trips it before
it reaches prod.

Bundles v1.4.49.1's analytics rollup-tier delegation that the Coolify
auto-deploy never picked up.

Tests: 5269 unit + integration pass, lint clean, typecheck clean.
2026-05-23 13:53:04 +02:00
Marc-André Bombeck 6b7b8a7fdf perf(analytics): default slice delegates to slim rollup-tier reader (v1.4.49.1)
v1.4.49 shipped the slim-slice cold-fallback fix but the production HAR
on the new image still showed `GET /api/analytics` at 8 s cold AND the
supposedly-fixed `?slice=summaries` request hit the same 8 s when fired
concurrently. Investigation found the slim slice queues behind the
default slice's per-type live walk: the 15-way `fetchMeasurementSeriesChunked`
fan-out saturates the `p-limit(4)` lanes and packs the 20-slot Prisma
pool for the full ~8 s on Marc's 467 745-row tenant, starving every
other Prisma client (including the concurrent slim slice) until the
walk drains.

The default slice's `summaries` field has been produceable from the
rollup tier for two releases — the slim slice already uses
`computeFromRollups` + a 90-day narrow `$queryRaw` and returns the
identical `DataSummary` shape sub-second. The walk lived on because
two thick-only fields (`anomalyCount`, `avg30LastMonth`) only existed
on the live path. A grep of every consumer in the repo confirmed:

- `anomalyCount`: no consumer reads it from `/api/analytics`. The
  insights pipeline pulls it from its own `comprehensive-aggregator`
  narrow query. Slim's `0` constant is what the default route's
  consumers already received.
- `avg30LastMonth`: read by `src/app/page.tsx:321` (dashboard
  comparison tile) and `src/components/insights/vo2-max-chart-row.tsx`.
  Easy to lift: extend the slim narrow `$queryRaw` with one extra
  FILTER clause for `[30d, 60d)` — the rows are already inside the
  90-day outer cap so the planner reads zero additional blocks.

With both fields covered, `buildAnalyticsResponse` calls
`computeSummariesSlice(user.id)` once and uses its `summaries` +
`lastSeenByType` directly. The 15-way fan-out, the chunked walker,
the `ANALYTICS_TYPE_FETCH_CONCURRENCY` p-limit cap, the 425-day
`since` window, and the `meta.analytics.bp_aggregate` annotate all
delete cleanly — none had callers outside the removed code path.
Net diff: -418 LOC across the route + its tests.

Test updates:
- `route.test.ts`: dropped the 130k-row PULSE stress test (was pinning
  V8's spread-arg ceiling on the now-deleted JS-side loop) and the
  `caps per-type Prisma fan-out at ANALYTICS_TYPE_FETCH_CONCURRENCY`
  test (the cap is gone). Added a negative assertion that
  `prisma.measurement.findMany` is never called with the chunked-walk
  shape `(select.id + source + deviceType, take: 5000)` and a positive
  assertion that `avg30LastMonth` reaches the response envelope.
- `since-cap.test.ts`: dropped the `where.measuredAt.gte` per-type
  assertion and the `bp_aggregate.live_since` annotation assertion
  (both pinned the removed walk). The slim-slice no-per-type-loop
  invariant survives.

Final: 5268 unit + integration tests pass, zero failures, lint clean,
typecheck clean.
2026-05-23 13:25:07 +02:00
Marc-André Bombeck f92e644154 chore(release): v1.4.49 — audit-backlog closure + server reminder suppression + diagnostic endpoint + analytics cold-path fix
Bundles the deferred v1.4.47 Mediums + Lows with the v1.4.48-deferred items
into a single release. Headline: server-side suppression of MEDICATION_REMINDER
APNs for iOS clients that own their local reminders (closes GH #206), a
push_attempts table backing /api/admin/notifications/diagnostic, and a sweep
of v1.4.48 QA forward-findings.

Performance (the user-visible win):
- /api/analytics cold-fallback path splits the heavy single-statement
  aggregate into two parallel queries. The linearly composable columns
  (count / min / max / mean) keep the full-partition scan; the windowed
  7 / 30 / 90-day avg / slope / r² columns take a 90-day outer cap so
  the planner does an index range scan on (user_id, type, measured_at)
  instead of a full-partition sequential scan. Output is bit-identical;
  cold slim slice drops from multi-second to sub-second on tenants with
  large measurement partitions.

Server-side notifications:
- GET + PATCH /api/auth/me/notification-prefs with deep-merge semantics.
  Initial category medication.clientManaged: boolean (default false). Cron
  medicationReminderJob skips dose-due APNs when the flag is true,
  emitting a medication_reminder.suppressed_client_managed wide-event
  annotation per skip.
- push_attempts table backs the diagnostic endpoint. Every APNS /
  Web-Push / Telegram / NTFY sender writes a fire-and-forget row.
  Daily 03:35 Europe/Berlin cleanup prunes rows older than 90 days.
- /api/admin/notifications/diagnostic — admin-only endpoint surfacing
  device APNs token presence (masked), channel config presence, and
  recent push attempts. Registered in OpenAPI.
- Per-channel APNs "Send test" button now fires with
  eventType: "MEDICATION_REMINDER" so the dispatcher's time-sensitive +
  priority-10 branch runs. A successful self-test now exercises the
  exact path production reminders take; an unsuccessful one points the
  user at iOS Settings → Time Sensitive Notifications rather than
  appearing to be a server-side bug.

Withings reliability:
- OAuth callback consumes the nonce atomically (delete-first with
  P2025 catch for the replay branch). The previous read-then-delete
  pattern allowed a race between two concurrent callbacks.
- ?reason=state redirect differentiated into csrf1 / replay / expired /
  cross_user so operators can tell mismatch types apart without DB
  shell access.
- connect endpoint rate-limited to 10/60s/user; row-create failures
  redirect to a consistent error surface.

Observability hardening:
- redactSensitiveFields(body) denylist helper covers password / token /
  secret / apiKey / authorization / csrfState / nonce, recursive,
  case-insensitive. Caught: /api/devices invalid_format issues were
  echoing apnsToken values verbatim into the audit log.
- sanitiseZodIssues(issues, { stripValuesFromMessage: true }) overload
  applied to 14 audit-log sites.

Migrations: 0079 (notification_prefs), 0080 (push_attempts).

Tests: 4815 → 5272 unit + integration. Workout-batch integration suite
that had been red on main since v1.4.45 (three release commits shipped
through it) now green again — updated to match v1.4.42 cross-source
dedup semantics.

Resolves PR #207.
2026-05-23 12:51:41 +02:00
Marc-André Bombeck 30ab2112db fix(apns): wire per-channel Test senden button to a real endpoint (v1.4.47.6)
TEST_ENDPOINTS map was missing APNS. Add /api/notifications/apns/test route + map entry.
2026-05-22 15:05:37 +02:00
Marc-André Bombeck 5803612e4d fix(apns): retry opposite gateway on BadEnvironmentKeyInToken (v1.4.47.5)
On BadEnvironmentKeyInToken, retry once with the opposite gateway. On success, persist Device.apnsEnvironment correction. Two new unit tests cover retry+persist and no-retry-on-BadDeviceToken.
2026-05-22 14:38:29 +02:00
Marc-André Bombeck 4e1e2bed32 fix(apns): add APNS_KEY_B64 env-var for escape-free PEM transport (v1.4.47.4)
v1.4.47.2/.3 confirmed the production APNS_KEY env-var is corrupted along the docker-compose env_file pipeline. Add APNS_KEY_B64 — operator base64-encodes the .p8 file and stores that ASCII-safe blob; app decodes back to PEM. No \n escape gymnastics. Precedence: B64 > KEY > FILE.
2026-05-22 12:20:45 +02:00
Marc-André Bombeck 8d665db2df fix(apns): add diagnostic dump on PEM parse failure (v1.4.47.3)
v1.4.47.2 normalisation didn't recover the production PEM. Emit a structured diagnostic on the existing parse-failure branch (counts + sha256_prefix, no secret bytes) so the operator can compare the in-container shape against the source .p8.
2026-05-22 12:01:19 +02:00