195 Commits
Author SHA1 Message Date
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
Marc-André Bombeck 18462118b7 fix(apns): defensive PEM normalisation + ES256 verify (v1.4.47.2)
Coolify runtime logs surfaced
"secretOrPrivateKey must be an asymmetric key when using ES256"
on every push attempt. The channel auto-paused after 3 consecutive
failures.

Root cause: the .p8 PEM stored in the Coolify env-var arrived at
process.env.APNS_KEY without parseable newlines around the BEGIN/END
markers (env_file round-trip artefact). openssl pkey accepts it,
jsonwebtoken@9 used by node-apn is strict.

Normalise the PEM after the 12-factor \n unescape: strip whitespace
between markers, force 64-char line wrap, rebuild canonical PEM.
Idempotent on already-correct inputs. Verify via crypto.createPrivateKey
before handing to node-apn.
2026-05-22 11:17:20 +02:00
Marc-André Bombeck bf1d87499f perf(analytics): cap slim-slice narrows query at 90 days (v1.4.47.1)
The narrows query inside computeFromRollups scanned the user's full
measurements partition without an outer measured_at cap even though
every aggregate FILTER inside the SELECT restricted to 7/30/90 days.
On a ~450k-row tenant the cold slim slice paid ~9s TTFB to read rows
that were always aggregated to NULL.

Adding AND m.measured_at >= NOW() - INTERVAL '90 days' to the outer
WHERE turns the read into an index range scan on
(user_id, type, measured_at) and returns bit-identical output.
Expected: ~9s -> ~0.5-1s cold, sub-50ms warm.

No schema change, no migration, no env-var change.
2026-05-22 10:48:53 +02:00
Marc-André Bombeck 66a7f8b6fa chore(release): v1.4.47 — audit-backlog closure + drag-to-reorder + Coach disable + OAuth nonce ledger
Eight changes: W0 idempotency test pin, W1 legacy column drop, W2 Textarea primitive, W3 Coach disable toggle, W4 dnd-kit drag-to-reorder, W5 onboarding 24h gate, W6 Withings OAuth nonce ledger, W7+W8 legacy_form_total drop + Coach onLine pre-check. 6-axis QA reconciled. Migrations 0076/0077/0078 required pre-image. Tests 5093 → ~5170.
2026-05-22 09:18:52 +02:00
Marc-André Bombeck ac319c6160 chore(release): v1.4.46 — server reconcile (PR worker + auto-skip + APNS admin test)
Three discovery follow-ups from prior server reviews:

- REG-9: pr-detection-worker now aggregates cumulative HK kinds (steps, active energy, flights climbed, walking-running distance, time in daylight) by date_trunc('day', measured_at) SUM before picking the best day, instead of picking the largest single per-hour slice.
- New intake-auto-skip cron at :05 hourly flips MedicationIntakeEvent.skipped=true for events where skipped=false AND takenAt IS NULL AND scheduledFor < NOW() - INTERVAL '24 hours', so the compliance tile stops seeing forgotten doses as pending forever.
- /api/admin/notifications/test now handles the APNS channel (was falling through to "Unknown channel type"); surfaces the APNs sender's reason on failure.

5076 -> 5089 tests (+13). 4 pre-existing date-rollover flakes in dashboard/summary, medications/intake, and medication-card-symmetry tests are unchanged by this PR (verified against pre-fix baseline f18654f3).
2026-05-22 01:06:55 +02:00
Marc-André Bombeck 6c7fc7dc84 chore(release): v1.4.45 — analytics 9 s perf fix + audit-marathon closure + v1.4.44 hotfix merged
Audit-marathon close: 14 waves + W10 QA + reconcile. Perf fix (analytics 9 s -> 2-3 s), Zod multi-issue rollout (41 routes), Withings parked-state automation, mobile-UI WCAG 2.5.5 close, QoL i18n + offline banner + account delete, security PII drop + replay-injection scan. Migration 0075 required. Tests 4815 -> 5076.
2026-05-21 23:13:21 +02:00
Marc-André Bombeck 9b6e237f5f chore(release): v1.4.42 — Knip enforcing, queryKey factory closed, iOS Workouts dedup, Withings off-response classification
Six touch-disjoint waves landed on develop before this release:

- W1-KNIP — exports/types tiers flipped to enforcing-mode on main;
  35 unused exports + 52 unused types triaged to 0/0; shadcn surface
  + zod-infer contract types scoped via knip.json ignore-block.
- W2-ZOD-MULTI-ISSUE — `returnAllZodIssues` shared helper +
  `/api/dashboard/widgets` 422 now returns every Zod issue + worker
  audit-ledger breadcrumb. 41 sibling routes documented for v1.4.43.
- W3-QUERYKEY-LONGTAIL — 40 settings/medications/admin/hooks files
  migrated to the queryKeys factory; 9 new factory entries; ESLint
  rule + test-guard scope extended in lockstep.
- W4-ARCH-HYGIENE — BERLIN_DAY_FORMATTER consolidated across nine
  sites (seven status helpers + bucket-series + bp-in-target);
  Suspense double-comment consolidated; doctor-report sanitiser regex
  rewritten to escape-form so diffs are reviewable; pr-detection-
  worker soft-delete filter; offhost-backup DR-intent comment.
- W5-IOS-WORKOUTS — `dedupeWorkoutBatch` write-time cross-source
  dedup helper for the v1.5 iOS HealthKit observer ingest; dashboard
  tile-strip placeholder parity with the live TrendCard chrome.
- W6-WITHINGS-OFF + ENV-CHECK — Withings off-response classifier
  (transient/reauth/persistent) + WithingsApiError typed throw + the
  `pnpm check-env` CLI. APNs `allOrNone: true` closes the AP-2
  silent-disable gap the wave was conceived to close.

No migration, no env-var change, no API contract break for iOS v0.5.4.
The new `/api/dashboard/widgets` 422 envelope is additively extended.

pnpm test --run green at 4815 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip (no --include flag) green.
2026-05-21 19:33:07 +02:00
Marc-André Bombeck 67207d725e chore(release): v1.4.41 — iOS perf hotfix, soft-delete completeness, tree hygiene
Eight touch-disjoint waves landed on develop before this release:

- W-INSIGHTS-HOT + W-RECONCILE-INSIGHTS — extract the timeout-stub
  persist helper and wire all seven *-status routes through it, closing
  the recurring ~14 s warm response the iOS client sees on dashboard
  open once the AI provider has stalled once that day.
- W-DELETED-2 — close the remaining three soft-delete reader-tier gaps
  (export bundle, gamification achievements, doctor-report aggregator)
  with a five-route integration-test pin.
- W-ORG — types consolidation into src/types/analytics.ts and
  src/types/backups.ts; prompt directory unification.
- W-FRONTEND-FACTORY — queryKey factory expansion + ESLint rule lift.
- W-IOS-COORD — POST /api/auth/check-user four-branch onboarding
  discovery, identifier matched verbatim, per-IP rate-limited.
- W-SIMPLIFIER — today-intake projection helper extract; unused tx?
  params dropped; 13 knip-flagged dead exports trimmed.
- W-PERF-OPS — v1.4.39 W-SUM legacy-NULL UNION discovery arm retired.
- W-PROCESS-DOCS — pg.Pool sizing operator doc; ESLint rule
  formalisation; knip CI gate staged for v1.4.42 flip.

No migration, no env-var change, no API contract break for iOS v0.5.4.
The new check-user endpoint is an additive surface. AP-2 .p8 key is
installed; SB-5 time-sensitive APNs is live as of this deploy.

pnpm test --run green at 4732 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip green.
2026-05-21 18:36:17 +02:00
Marc-André Bombeck 1114cf7e63 chore(release): v1.4.40 — comprehensive Critical+High architecture closure plus iOS PB30 enablement
Eleven touch-disjoint waves landed on develop plus a six-axis QA
reconcile round before tagging. See CHANGELOG.md for the user-facing
prose; this commit message documents the wave + report inventory for
release-management traceability.

Waves shipped (each with a phase report under .planning/):

- phase-W-POOL-v1440-report.md — p-limit(4) wrapper around the
  analytics 15-way fan-out + pg.Pool max raised 10 -> 20. Closes the
  v1.4.39 empirical-trace finding #1 (Prisma pool starvation).
- phase-W-DELETED-v1440-report.md — 11 reader-tier helpers + 3
  integration contracts close audit Critical Finding #3 (soft-delete
  invisibility) across dashboard, analytics, summaries, rollup tiers,
  and the Coach snapshot path.
- phase-W-INSIGHTS-v1440-report.md — mood-rollup swap on the three
  /api/insights/{features,targets,cards} routes plus the
  compliance-rollup hook gap closure for bulk-projection mints; the
  post-QA reconcile patch closes the six remaining measurement.findMany
  sites in the cluster that still leaked tombstoned rows.
- phase-W-INFRA-v1440-report.md — rollup helper umbrella consolidation
  under src/lib/rollups/* with zero orphan re-exports; lint, typecheck,
  knip, and 93/93 rollup tests green post-restructure.
- phase-W-RSC-v1440-report.md — per-tile Suspense boundaries plus the
  queryKey factory-bypass walker test (extended to comparison/ in the
  reconcile pass).
- phase-W-PRIVACY-v1440-report.md — bilingual /privacy page with 17
  paired-section tests pinning the 9 SB-3 disclosure requirements.
- phase-W-AASA-v1440-report.md — /.well-known/apple-app-site-association
  endpoint pinned to S8WDX4W5KX.dev.healthlog.app with app-ID parity
  assertions guarding against split rotation.
- phase-W-APNS-NOTIFY-v1440-report.md — MEDICATION_REMINDER
  time-sensitive APNs payload (priority 10 + interruption-level)
  scoped via a parameterised test that pins all 6 other event-types
  do not bypass Focus. AP-2 .p8 install gates real delivery.
- phase-W-CONSENT-v1440-report.md — AI consent receipts CRUD with
  append-only invariant pinned in test; revoke-then-regrant leaves
  both rows; idempotent DELETE returns 200 + receipt: null.
- phase-W-WMY-WIRE-v1440-report.md — three previously-dead WMY readers
  (week/month/year) wired through summaries-slice and
  health-score-fast-path with a linear-composability parity test
  pinning the cross-consumer routing contract.
- phase-W-GHOSTS-v1440-report.md — ~1 177 lines of dead code removed
  across 9 atomic commits; startOfUtcDay + wallClockInTz consolidation
  eliminates a sibling-drift risk; knip whitelist trimmed to three
  files pending follow-up.

QA reconcile patches applied per the v1.4.40 QA round (six reports
under .planning/round-v1440-QA-*-findings.md):

- senior C1 — dashboard-suspense-boundaries regex aligned with the
  post-W-INFRA Thread 2 userTimezone-local lift.
- senior H1 — soft-delete filter added to the six remaining
  insights measurement.findMany sites.
- specialist H1 — Promise.allSettled around bulk-projection
  compliance recompute on both /api/medications/intake and
  /api/dashboard/summary.
- ux H1 — compare-toggle migrated to queryKeys.dashboardWidgets
  factory; walker test extended to src/components/comparison/.
- security M1 — consent artefact 64 KB cap enforced via
  Buffer.byteLength (UTF-8 bytes, not UTF-16 code units).

Quality gates green at tag: pnpm typecheck, pnpm lint, pnpm knip
--include files,dependencies,binaries,unlisted, pnpm test --run
(4726 passing / 1 skipped). Tests 4525 -> 4726 unit + 1 skipped over
the v1.4.39 -> v1.4.40 span.

User-facing prose: see CHANGELOG.md.
2026-05-21 12:48:02 +02:00
Marc-André Bombeck f9996552ed chore(release): v1.4.39.4 — dashboard symmetry for daily-schedule intake projection 2026-05-21 11:20:24 +02:00
Marc-André Bombeck dbb5979a97 chore(release): v1.4.39.3 — dashboard slim/thick merge robustness and list-page precision
The post-deploy CI for v1.4.39.2 surfaced eight e2e failures across
chromium-desktop and chromium-mobile (chart-overlay-controls,
dashboard, charts-mobile, measurement-flow) that all routed through
two narrow regressions and one durable test-fixture gap.

### Fixed

- Dashboard slim/thick merge no longer blanks the tile strip when the
  slim slice resolves with an empty summaries record. The v1.4.39.2
  inline `slim?.summaries ?? thick?.summaries` short-circuited on
  JavaScript's truthy-empty-object even when thick carried the full
  payload. The merge moved to a pure `mergeSlimAndThickAnalytics`
  helper that uses object emptiness as the discriminator: populated
  slim still wins on overlapping fields (progressive-paint contract
  preserved), empty slim falls through to thick, both empty leaves
  the dashboard's data-floor gates to render the appropriate empty
  state. Eight new behaviour-level unit tests cover the edge cases.
- `MeasurementList` non-grouped rows now preserve decimal precision
  on both the desktop table and the mobile card grid. The v1.4.37 W7c
  collapsed-list view passed every value through `fmt.integer`, which
  kept the per-day step / activity aggregates correct but silently
  truncated single weight / body-fat / temperature readings — 78.4 kg
  rendered as "78 kg". Grouped rows (cumulative HK type day
  aggregates) stay on `fmt.integer` because their readings are
  integer-only; non-grouped rows now use `fmt.number` which honours
  up to three fraction digits without forcing a minimum.

### Changed

- Playwright route mocks for `/api/analytics` migrate from the
  literal `**/api/analytics` string glob to the regex form
  `/\/api\/analytics(\?|$)/` across every authenticated spec. The
  string glob does not match the sliced URL form
  `/api/analytics?slice=summaries` the v1.4.39.2 dashboard split
  fires alongside the thick request, so the slim request fell
  through to the real route on the seeded test user that returns
  empty summaries. Ten specs migrate: dashboard,
  chart-overlay-controls, measurement-flow, charts-mobile, a11y,
  v1427-responsive-sheet, v1427-coach-launch,
  v1427-measurements-add-param, v1427-insights-empty-state,
  onboarding-tour-passthrough. The regex form is the durable
  alignment with the v1.4.37 W-CI lesson that already swept
  `onboarding-flicker.spec.ts` and `mobile-viewport.spec.ts`.

### Notes

- No schema migration. Runtime + test-fixture-only changes.
- `pnpm test --run` green at 4662 / 4663 (1 long-standing skip).
- Full Playwright suite green at 116 passing / 36 skipped / 152 total
  on both `chromium-desktop` and `chromium-mobile` profiles, against
  the production Next.js build. Closes every failure from CI run
  26213226723.
2026-05-21 10:31:18 +02:00
Marc-André Bombeck d665a5db42 chore(release): v1.4.39.2 — per-request rollup convergence and progressive dashboard paint
v1.4.39.1 wired the rollup write hook into the Withings sync, /api/import,
and admin-restore paths and widened the boot-time backfill discovery to
per-(user, type, day). Two follow-ups Marc surfaced from the post-deploy
trace remained: the dashboard chart still painted its `< 3 daily points`
empty-state on the 30-point range for accounts whose historic rollup
partition had not yet been folded, and the cold-mount UX waterfall left
every per-type tile waiting on the heavy /api/analytics envelope.

### Fixed

- GET /api/measurements?source=rollup now reconciles the rollup table
  against the live measurements table when the read returns suspiciously
  few rows. A request that lands while the boot backfill is still folding
  the historic partition would previously short-circuit on the sparse
  rollup (`rollupRows.length > 0`) and never reconcile with the live
  table. The route now probes
  `COUNT(DISTINCT date_trunc('day', measured_at))` against measurements
  for the requested window when the rollup carries fewer than three
  rows on a window of at least seven days; if the live table holds more
  distinct days than the rollup, the route folds the
  (user, type, DAY, [from, to]) partition inline via
  `recomputeUserRollups` and re-reads. The chart paints the full window
  on the same request without paying the cost on subsequent requests.
  The probe is gated on rollup sparsity so covered-tenant hot paths
  stay single-read.

### Changed

- Dashboard analytics fan-out split across two parallel queries. The
  per-type tile strip now reads from the slim `?slice=summaries`
  branch and paints as soon as the slim slice resolves; the
  BD-Zielbereich and glucose tiles stream in from the thick branch
  afterwards. Pre-fix the single thick-slice query blocked every
  per-type tile until the heavy fan-out resolved, so the mood and
  medication tiles (separate routes) painted first and every other
  tile arrived as one delayed burst. Both queries share the
  `caches.analytics` 60 s LRU server-side so warm hits stay free.

### Notes

- No schema migration. Runtime-only fix on top of the v1.4.39.1
  write-path hooks.
- pnpm test --run green at 4654 / 4655 (1 long-standing skip);
  targeted suites (src/app/api/measurements, src/lib/measurements,
  src/app/__tests__) all clean.
- The inline rollup fold cost is bounded by the requested window and
  one measurement type. For the dashboard chart's 30-day BP_SYS window
  that is a single 30-row upsert against the composite primary key —
  well inside the request budget.
2026-05-21 09:56:24 +02:00
Marc-André Bombeck 4122c79d66 chore(release): v1.4.39.1 — rollup tier catches Withings + import + admin-restore write paths
Hotfix on top of v1.4.39. Three measurement write paths (Withings
sync, `/api/import`, admin backup restore) were bypassing the
persistent rollup tier's write hook, so the dashboard chart's
`source=rollup` fast-path painted "Noch nicht genug Daten" on the
30 / 90-point range for accounts whose recent data only came through
those paths. The 7-day window kept rendering because the chart's raw
fetch never consults the rollup tier.

Closes the read-coverage gap by wiring `recomputeBucketsForMeasurement`
into every remaining ingest path and re-anchors the boot-time backfill
discovery to per-day coverage so already-deployed instances re-converge
on the next worker boot. The legacy `sum_value IS NULL` branch stays
on the same UNION so the v1.4.39 column-backfill continues working.

Also re-anchors one date-bound test in the medications-intake route
suite so the green CI run no longer depends on the wall clock.

Tests 4647 → 4648 passing; one long-standing skip unchanged.
2026-05-21 09:03:33 +02:00
Marc-André Bombeck d84922cdc3 chore(release): v1.4.39 — mood, medication-compliance, and cumulative-sum rollup tiers
Squashed merge of the v1.4.39 wave work from develop. The release
extends the v1.4.38 fast-path posture to two more endpoints that
still walked the source table on every cold mount and folds a
cumulative `sum_value` column into the existing rollup tier so step /
flight / distance / daylight / active-energy sparklines no longer
re-derive their daily totals in Node.

Raw measurements / mood_entries / medication_intake_events tables are
byte-unchanged. Three new tiers (mood_entry_rollups,
medication_compliance_rollups, measurement_rollups.sum_value) are
derived caches that self-heal via boot-time backfill on first reach.

Migrations 0070, 0071, 0072 are additive only. The sum_value column
add is catalog-only DDL on PostgreSQL 11+ — no table rewrite, writes
resume in milliseconds. No environment-variable change. No API
contract break. No iOS contract change.

Expected wins (live perf-verify rides the post-deploy window):
- /api/mood/analytics cold 12.7 s -> ~200 ms
- /api/medications/intake?scope=compliance cold 3.2 s -> ~200 ms
- /api/dashboard/summary cumulative sparkline ~500 ms -> ~300 ms
- /api/analytics live-fallback row cap 347 k -> ~5 k (425 d cap
  preserves the year-ago baseline tile)

Tests 4 524 -> 4 640 (+116). Typecheck clean. Lint clean.

See CHANGELOG.md for the full v1.4.39 entry.
2026-05-21 07:39:10 +02:00
Marc-André Bombeck 8a8150d2bf perf(analytics): fast-path gates per-type only
`isFullyCovered(coverage) && specificTypes` poisoned every fast-
path with a single unrelated uncovered measurement type. v1.4.38.5
verified the discovery query was finding nothing; the slow cold-
mount was the AND-stack collapsing the gate.

- correlations-fast-path: gates on SYS + PULSE + WEIGHT only.
- bp-in-target-fast-path: gates on SYS + DIA only.
- health-score-fast-path: gates on WEIGHT only.

Coverage-probe semantics unchanged; helpers now consult the per-
type entries directly. Power-user cold-mount on /api/analytics
estimated 30-75 s → ~3-5 s.
2026-05-18 11:06:24 +02:00
Marc-André Bombeck 9cc5ececa7 chore(release): v1.4.38.7
Rollup recompute observability + admin trigger.

- POST /api/admin/rollups/recompute synchronously folds one user or
  re-runs the boot-discovery loop on demand. Admin-gated.
- slim_summaries live-fallback now annotates `fallback_reason` and
  `missing_types[]` so the operator can map a slow cold-mount to
  the type missing DAY-bucket coverage without DB shell access.
- Boot-backfill discovery now logs every result, including the
  silent "no users to backfill" case — the previous shape made it
  impossible to tell "discovery ran cleanly + found nothing" apart
  from "discovery never executed".

See CHANGELOG.md.
2026-05-18 06:27:02 +02:00
Marc-André Bombeck 80b1fcb8d0 fix(rollups): boot-backfill discovery SQL uses bucket_start as NULL sentinel
v1.4.38.5 referenced `r."id" IS NULL` on the LEFT JOIN — but
`measurement_rollups` has a composite primary key
`(user_id, type, granularity, bucket_start)` and no surrogate `id`
column. Postgres rejected the query with `column r.id does not
exist` (SQL state 42703); the worker boot swallowed the error per
the helper's best-effort contract and the queue stayed empty.

Net effect: v1.4.38.5 deployed cleanly but delivered none of its
promised fast-path recovery. Marc's account stayed on the live
aggregator.

The fix uses `r."bucket_start" IS NULL` — any column from the
unmatched right side of the LEFT JOIN serves the "no matching row"
filter, and `bucket_start` is part of the composite PK so the
planner already has the column. Bumps to v1.4.38.6.
2026-05-18 06:15:56 +02:00
Marc-André Bombeck 9c13cba3f7 chore(release): v1.4.38.5
Analytics fast-path restored on long-running accounts. The boot-time
rollup-backfill discovery query was matching only ZERO-rollup users;
any account that ever folded silently stranded the moment a brand-
new measurement type landed. Discovery now widens to per-type
missing coverage so the boot-backfill queue picks these accounts up
on the next deploy and folds them back into the fast-path.
2026-05-18 06:07:14 +02:00
Marc-André Bombeck 0c21d4c616 chore(release): bump package.json to 1.4.38.4
Mirrors the v1.4.38.4 tag (the previous release commit only carried
the CHANGELOG entry because the editor lost the version-line edit on
a merge race). `NEXT_PUBLIC_APP_VERSION` derives from this string at
build time, so the running shell's self-heal poll would otherwise
keep firing against itself.
2026-05-18 05:50:16 +02:00
Marc-André Bombeck a16bb4b753 chore(release): v1.4.38.3
CI green-up + chunk-load auto-recover. Closes the three pre-existing
red checks that had been failing on every push since v1.4.38 landed
(TODO marker in product code, rollup integration test racing the
fire-and-forget rollup populator, e2e drift after the v1.4.37
doctor-report hero refactor + the dashboard touch-target gate),
plus a new `AppError` branch that auto-reloads the SPA when the
cached shell hits a stale chunk after a deploy.

See CHANGELOG.md.
2026-05-17 23:56:39 +02:00
Marc-André Bombeck d224811a34 chore(release): v1.4.38.2
Mood-reminder hotfix bundle on top of v1.4.38.1 plus the missing
Settings toggle. Six-axis review of the v0.5.4 iOS-coord patch
surfaced enough blockers that the feature could not have been used
safely as shipped:

- Locale resolver demoted 4 of 6 locales (es/fr/it/pl) to English.
- Dedup ledger committed before delivery — a transient APNs blip
  silently nuked the day's nudge and blocked retries.
- DST-unsafe arithmetic drifted the local-22:00 baseline by an hour
  twice a year for both the mood-reminder push and the medication
  iOS-snooze action.
- APNs payload forwarded the Telegram `replyMarkup` inline-keyboard
  object and other channel-specific extras to Apple's servers and
  the iOS notification handler.
- No Settings UI for the per-user opt-in flag — the feature was
  unreachable for end users regardless of the other fixes.

v1.4.38.2 closes every item above plus adds the missing
`MoodReminderCard` Switch, a 90-day retention sweep for the dispatch
ledger, and a per-user `try` wrapper so one bad row cannot abort the
22:00 tick.

See CHANGELOG.md.
2026-05-17 23:46:09 +02:00
Marc-André Bombeck c4f2e0bc51 chore(release): v1.4.38.1
iOS v0.5.4 push-notification coordination — APNs category for
med-reminder action-buttons + optional MOOD_REMINDER daily event.
Additive only, migration 0069 idempotent.

See CHANGELOG.md.
2026-05-17 23:13:20 +02:00
Marc-André Bombeck a550031a84 chore(release): v1.4.38
Robustness sweep, perf hotspots, full localization, coach gate
cascade closure.

- /api/dashboard/summary cold-mount 4.6 s → ~500 ms via DAY-bucket
  sparkline + 60 s response cache.
- Coach API gate inventory test surfaced and closed two orphan
  routes (chat[id] GET+DELETE, chat-message feedback POST).
- Cross-tz fast-path runtime guard falls back to live for users
  more than ±3 h from UTC; proper bucket minting stays v1.5.
- es / fr / it / pl coverage 27 % → 63 % (~3 400 new strings) plus
  three medication-cluster placeholder restorations.
- IANA timezone validation closes the User.timezone self-DoS
  regression the new dashboard SQL would have surfaced.
- Unit suite 4520 → 4524.

See CHANGELOG.md for the full set.
2026-05-17 19:07:03 +02:00
dependabot[bot] fe64f66c7f chore(deps-dev): Bump @playwright/test from 1.59.1 to 1.60.0 (#189)
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.59.1 to 1.60.0.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.59.1...v1.60.0)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.60.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-17 16:03:18 +00:00
dependabot[bot] 55469fa56a chore(deps): Bump @tanstack/react-query from 5.100.9 to 5.100.10 (#188)
Bumps [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) from 5.100.9 to 5.100.10.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/HEAD/packages/react-query)

---
updated-dependencies:
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.100.10
  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-17 15:58:54 +00:00