From 65523de158d0290827d9459e4a03bb787ffd1aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 10 May 2026 22:50:01 +0200 Subject: [PATCH 1/3] docs(audit): v1.4.22 release summary Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/audit/v1422-summary.md | 60 +++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 docs/audit/v1422-summary.md diff --git a/docs/audit/v1422-summary.md b/docs/audit/v1422-summary.md new file mode 100644 index 000000000..47a3ad393 --- /dev/null +++ b/docs/audit/v1422-summary.md @@ -0,0 +1,60 @@ +# HealthLog v1.4.22 — release summary + +## Release brief + +v1.4.22 is the polish release that closes loops the Insights redesign, the AI Coach, and the BD-Zielbereich tile have been carrying since v1.4.16. Two beats define it. The first is the Coach prose rewrite — the model used to open every reply with a number, which read clinical and database-cursor-shaped; v1.4.22 reframes the persona as warm, curious, and reserved, and moves every aggregate value out of the prose into a collapsible "Worauf bezieht sich das?" / "What I'm looking at" disclosure under each turn. The second is the long tail of small fixes that finally settle metrics, layout, and a11y where they should have landed two releases ago: the BD-Zielbereich headline re-anchored to the last-30-day window (with all-time as a sub-row), the Targets / Zielwerte page grew per-target sparklines and a Δ-vs-last-month caption, the sticky section navigation lifted above the Insights hero with proper `aria-current` + `motion-reduce` semantics, the comparison-overlay collapsed into a single global preference under Settings → Dashboard, and the admin / API-tokens horizontal scrollbar is gone on the fifth attempt. PROMPT_VERSION ratcheted 4.20.2 → 4.22.0 — the first minor-digit bump in the v1.4.x line, intentional because the persona, the sentinel-block contract, and the warmth of the prose all changed in one coherent step. v1.4.22 is the milestone that makes v1.5's iOS push start on a clean foundation instead of debt repayment. + +## Live state + +| Field | Value | +|---|---| +| URL | `https://healthlog.bombeck.io` | +| `/api/version` | `1.4.22` | +| Image digest | `sha256:865154614303fdc362ee3941776f73ec0f60e1f16112ec272a75cbbe28e2cffb` | +| Version transition | 2026-05-10T22:43:50+02:00 (host-side retag fallback) | +| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.22 | +| Branch model | second release through the `develop` → `main` model (introduced in v1.4.20 F1) | + +## Smoke (no session) + +`/api/version` → 200 (returns `1.4.22`). Every gated route (`/`, `/insights`, `/insights/report/2026-W19`, `/admin/api-tokens`, `/settings/integrations`, `/achievements`) returns 307 → `/auth/login`, confirming the proxy gate is alive on the new image. + +## What shipped + +**Insights surface polish.** Six changes that together move the page from "feature-rich and slightly cluttered" to "deliberate". The BD-Zielbereich tile got its fourth-attempt fix: the headline now reads `windows.last30Days?.pct` instead of the all-time aggregate, surfaces all-time as a sub-row, and gains a synthesised trend arrow plus a 7-day-trend chip. The on-surface comparison toggle retired from `/insights` because the canonical picker has lived in Settings → Dashboard since v1.4.16 (two surfaces violated the no-split-Settings rule). Three half-row layouts that left empty space got a row-fill rule. Trends-row card heights equalised. The "Muster" / "Patterns" row renamed to "Zusammenhänge" / "Relationships" because the row directly above already uses Trends. The sticky section navigation lifted above the hero with proper a11y semantics (`aria-current`, focus-visible ring, `motion-reduce`, observer-ratio sorting). And a single missing `stripChartTokens()` call upstream was producing raw `metric:WEIGHT` / `metric:BLOOD_PRESSURE_SYS` token leaks in recommendation prose; fix landed plus four DE Health-Score component labels normalised to German nouns. + +**Coach prose rewrite + collapsible evidence.** PROMPT_VERSION 4.20.2 → 4.22.0. The Coach used to open every reply with a number; the new persona is warm, curious, reserved — a partner sitting alongside the user, not pushing data. Numbers move out of the prose into a `---KEYVALUES---` … `---END---` sentinel block that the chat route parses out server-side and renders as a `
` disclosure under each assistant turn, closed by default and hidden when no key-values came back. Three-way hard caps on the sentinel (1 KB payload, 8 lines max, per-line Zod) so a prompt-injection attempt can't grow the persisted envelope. The user avatar reuses the existing Gravatar URL at the same dimensions as the Coach avatar. The medical disclaimer pinned at the bottom of the message thread on every viewport (clinical-adjacent UI must not gate the disclaimer behind a chevron tray). The settings cog removed because it was a dead button in v1.4.21 — per-user prompt-tuning is deferred to v1.4.23. A graceful fallback covers the sentinel-only-output edge case so the model never surfaces a raw marker. + +**Other surfaces + backlog cleanup.** The Targets / Zielwerte page stopped reading like a clinical cheat-sheet. Each `` grew a 30-day inline SVG sparkline beneath the range bar plus a localised "Δ −2.3 kg vs. last month" caption; both null when either window has fewer than 3 readings so cold-start accounts don't see a misleading flat trace. The admin / API-tokens horizontal scrollbar fell to a live Playwright probe that traced the residual culprit to `whitespace-nowrap` on the date ``s. The AuthShell post-hydration redirect on incomplete onboarding moved into `proxy.ts` so the dashboard flash is gone; the proxy reads a non-httpOnly `hl_onboarding` cookie that auth routes mirror from `onboardingCompletedAt`, with `SameSite=Strict` so cross-site requests can't see it. The `setOnboardingPendingCookie` write folded into `createSession()` so issuing a session without onboarding state is type-impossible. Twelve backlog items from `v1421-backlog.md` swept; hygiene continued (191 maintainer-name references in source comments swept, v1.4.14+v1.4.15 bilingual CHANGELOG entries normalised to English-only, the AI-vendor-specific contributor file retired in favour of `CONTRIBUTING.md`). + +**Multi-leg QA + reconcile.** Six-leg review (code, security, design, senior-dev, simplify, product-lead) found 0 CRITICAL across all reviews. Seven HIGH findings — all applied inline. About six MEDs applied inline (judgment-pick high-leverage); the rest documented in `.planning/v1422-backlog.md` for v1.4.23. Highlights of the inline applications: BD-Zielbereich tile compareDelta now matches caption (subtracts `bpInTargetPctPriorMonth` instead of `bpInTargetPctAllTime`); `createSession()` requires `onboardingPending` as a parameter so the cookie-write fan-out is one site; `createSseStream` extracted to `src/lib/sse/create-stream.ts` ahead of v1.5 iOS streaming endpoints; `berlinDayKey()` lifted to a shared `src/lib/analytics/berlin-day.ts` helper so the targets sparkline and analytics route share the same bucketing; BP tile collapses to a single secondary row at `` extracted from the duplicated desktop + mobile api-token surfaces. + +## Token-budget impact + +Coach token budget unchanged from v1.4.21. The prose rewrite swaps inline numbers for a sentinel block that the route strips before persisting; the per-turn cost remains ≈3 000 tokens for a full-scope snapshot, ≈600–700 for a single-source last-7-days narrowed turn. The 25 000 tokens-per-user-per-day cap is unchanged; ≈8 turns/day for a heavy full-scope user, plenty of room for a single-metric narrowed conversation. + +## Branch model + +The `develop` → `main` model (introduced in v1.4.20 F1) carried its second release cleanly. Long-lived `develop` accumulated 35 commits between v1.4.21 and v1.4.22 (W1 research, W2/W3/W4 implementation, W5 reconcile, release-prep). The release-merge into `main` was a single `--no-ff` with the full list of commits in the merge body. Future GHCR builds run on `main` only. + +## Quality + +- W1 research + W2/W3/W4 implementation + W5 reconcile across 35 commits on `develop` (`8606767` → `b094eaa`) +- 1-commit prettier pass on W5 touchpoints (`bbdc3fb`) +- 1-commit release-prep on `develop` (`d71e879`) +- Release-merge `005fed0` on `main` +- Tests: 2026 → 2111 unit (+85), 81 → 89 integration (+8), typecheck clean, lint baseline (15 warnings, no new) + +## Carry-overs + +The Coolify image-digest auto-deploy bug is still present and the workflow's auto-deploy step skipped because the `COOLIFY_WEBHOOK` / `COOLIFY_TOKEN` repo secrets aren't configured. The `.planning/coolify-auto-deploy-howto.md` runbook landed in W4 C3 documents the one-time UI toggle; bumping the secrets and flipping the toggle should drop the host-side retag fallback for v1.4.23. Worked around with the host-side retag procedure (`docker pull :1.4.22 → docker tag :latest → docker compose up -d --force-recreate`) on apps-01. Same fault that hit v1.4.19, v1.4.20, and v1.4.21. + +The full v1.4.23 carry-over list lives in `.planning/v1422-backlog.md`. Highlights: sentinel parser malformed-enum hardening (Sr-M5, ~30 LOC, highest signal-to-effort ratio of the deferred items); analytics-route unbounded `findMany` paging; targets-route 7-pass sparkline coalesce; `CoachDrawer key={prefill}` controlled-prop refactor (Sr-HIGH-4); per-user prompt-tuning surface; medication_schedules.days_of_week schema-drift cleanup. + +## Strategic next + +v1.5 plan in `.planning/phase-W5-v1422-product-lead-review.md`. Headline is the iOS native client + Apple Health ingest contract — six phases, P1 first launch (bearer + refresh-token end-to-end with one login page + one dashboard widget) through P6 cross-user feedback aggregator cron. New measurement types (HRV, Sleep, Resting HR, Steps, BodyFat, Glucose) extend the Coach to PROMPT_VERSION 5.0.0 with conservative phrasing rules around clinical sleep-stage conclusions and HRV claims. Per-metric APNs alerts add `src/lib/notifications/senders/apns.ts`. The Insights page split (the ~9-surface orchestrator) lands in v1.5 P5 alongside the Vitals tile row, not v1.4.23, so the split maps to the exact sub-trees the Apple Health work touches. + +## Deploy path used + +Host-side SSH retag fallback. The GHCR tag-build (workflow run `25639069964`) succeeded, but the auto-deploy step skipped because the `COOLIFY_WEBHOOK` / `COOLIFY_TOKEN` repo secrets aren't configured. A Coolify MCP `force=true` deploy was queued (`owrpyzoi1ijsvhge0hfv25bw`) but didn't refresh the digest within the 5-minute polling window — same `:latest` pinning bug from v1.4.21. The `ssh apps-01 'docker pull :1.4.22 → docker tag :latest → docker compose up -d --force-recreate'` procedure flipped `/api/version` from `1.4.21` to `1.4.22` at 22:43:50 +02:00. From ca03c6099f0b1d5d24b92bf66df465bc704ab6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 12 May 2026 22:33:35 +0200 Subject: [PATCH 2/3] chore(release): v1.4.24 Security + accessibility hardening release. Closes the highest-priority findings from a focused security audit of the v1.4.23 release: - Bearer auth fail-closed when a route declares no scope (api-handler) - Public /api/send Umami proxy now rate-limits 120 req/min/IP - Proxy nonce generation switched to btoa(...) for Edge portability - Session-expiry delete race uses deleteMany + catch (no more 500s) - POST /api/import returns the standard apiError envelope on 422 - Runtime Docker image pins Prisma 7.8 + worker deps to minors - Notifications page renders a real load-error alert - Accessibility: switch aria-labels, htmlFor pairing, mobile admin Overview link, deep-link anchors on notification settings - Chart overlays: per-chart comparisonBaseline pref, BMI/bodyFat/ sleep/steps overlay keys, TrendCard layout tolerates long labels Verification: pnpm typecheck && pnpm lint && pnpm test (2244/2244) && pnpm format:check all green. --- .planning/STATE.md | 7 +- .planning/phase-W6-v1423-reconcile-report.md | 30 ++-- .planning/v1423-backlog.md | 2 +- CHANGELOG.md | 153 ++++++++++++++++-- Dockerfile | 4 +- docs/api/openapi.yaml | 30 ++-- docs/audit/v1422-summary.md | 16 +- messages/de.json | 2 + messages/en.json | 2 + package.json | 2 +- scripts/check-openapi.ts | 4 - scripts/generate-openapi.ts | 1 - .../proxy-onboarding-redirect.test.ts | 23 +++ .../api/admin/monitoring/umami-test/route.ts | 8 +- src/app/api/analytics/route.ts | 3 +- src/app/api/auth/codex/device-start/route.ts | 3 +- src/app/api/bugreport/route.ts | 2 +- .../dashboard/chart-overlay-prefs/route.ts | 3 + src/app/api/import/__tests__/route.test.ts | 21 +++ src/app/api/import/route.ts | 5 +- .../insights/generate/__tests__/route.test.ts | 22 ++- .../api/integrations/moodlog/test/route.ts | 3 +- .../api/integrations/withings/test/route.ts | 3 +- src/app/api/medications/route.ts | 2 +- .../api/monitoring/glitchtip/test/route.ts | 3 +- src/app/api/monitoring/umami/test/route.ts | 3 +- .../api/notifications/preferences/route.ts | 2 +- .../api/notifications/web-push/test/route.ts | 3 +- src/app/api/send/__tests__/route.test.ts | 88 ++++++++++ src/app/api/send/route.ts | 6 + src/app/notifications/page.tsx | 28 +++- src/app/page.tsx | 8 +- src/components/admin/admin-shell.tsx | 15 ++ src/components/admin/reminders-section.tsx | 9 +- .../__tests__/chart-overlay-controls.test.tsx | 2 + .../__tests__/health-chart-mini.test.tsx | 12 ++ .../medication-overlay-toggles.test.tsx | 3 + .../charts/__tests__/trend-card.test.tsx | 26 +++ .../charts/chart-overlay-controls.tsx | 48 +++++- src/components/charts/health-chart.tsx | 44 +++-- .../charts/medication-compliance-chart.tsx | 6 +- src/components/charts/mood-chart.tsx | 75 +++++---- src/components/charts/trend-card.tsx | 72 ++++++--- .../insights/coach-panel/coach-drawer.tsx | 2 +- .../insights/coach-panel/message-thread.tsx | 7 +- .../measurements/measurement-list.tsx | 13 +- .../settings/integrations-section.tsx | 13 +- .../settings/notifications-section.tsx | 18 ++- src/lib/__tests__/dashboard-layout.test.ts | 32 ++++ src/lib/__tests__/require-auth-bearer.test.ts | 26 ++- src/lib/ai/provider.ts | 6 +- src/lib/api-handler.ts | 15 +- src/lib/auth/__tests__/session.test.ts | 65 ++++++++ src/lib/auth/session.ts | 4 +- src/lib/dashboard-layout.ts | 9 ++ src/lib/notifications/senders/apns.ts | 2 +- src/proxy.ts | 13 +- .../analytics-sleep-stages.test.ts | 17 +- vitest.config.mts | 1 + 59 files changed, 841 insertions(+), 206 deletions(-) create mode 100644 src/app/api/send/__tests__/route.test.ts create mode 100644 src/lib/auth/__tests__/session.test.ts diff --git a/.planning/STATE.md b/.planning/STATE.md index 75ffb8a0a..6b00989b4 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -216,9 +216,10 @@ MEDs, all LOWs, S-05 simplify, pre-existing `coach-prefs.test.ts` mock issue, sandbox `git commit` no-op). Test deltas: W5 ended at 2223 unit. Session A added the `revoke.test.ts` -+ `apns-dispatch` device tests landing at 2227. Session E ended at 2235. -Session F added the forged-X-Device-Id regression — final at 2236 unit -(+45 across the W5+W6 reconcile arc). + +- `apns-dispatch` device tests landing at 2227. Session E ended at 2235. + Session F added the forged-X-Device-Id regression — final at 2236 unit + (+45 across the W5+W6 reconcile arc). ## Wave 7 — Release v1.4.23 (next) diff --git a/.planning/phase-W6-v1423-reconcile-report.md b/.planning/phase-W6-v1423-reconcile-report.md index 370ad73d2..598fd2c89 100644 --- a/.planning/phase-W6-v1423-reconcile-report.md +++ b/.planning/phase-W6-v1423-reconcile-report.md @@ -6,13 +6,13 @@ six-review brief into shippable develop-branch state. ## Per-session SHA chain -| Session | Scope | Commit SHAs | -| --- | --- | --- | -| A | CRITICAL + HIGH 1-3 | `5486507`, `13977bc`, `d5202e5`, `a2dfc5e` | -| B | HIGH 4 + simplify (S-02 → S-04) + HIGH 6 | `3ee6dab`, `dfffe6b`, `d25e50e` | -| D | HIGH 5 (analytics chunked aggregate landed in W5; D session re-confirmed against the W6 senior-dev brief) | `deadc73` | -| E | HIGH 7 (Coach feedback referencing `coach_messages`, plaintext column drop) | `650f150` | -| F | MED cluster (Sec-MED-2 Coolify scrub, Sr-MED-5 isCurrent device-id, SLEEP_DURATION migration verification note) + planning record | `1a46bfe` (MED) + this commit (planning) | +| Session | Scope | Commit SHAs | +| ------- | --------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | +| A | CRITICAL + HIGH 1-3 | `5486507`, `13977bc`, `d5202e5`, `a2dfc5e` | +| B | HIGH 4 + simplify (S-02 → S-04) + HIGH 6 | `3ee6dab`, `dfffe6b`, `d25e50e` | +| D | HIGH 5 (analytics chunked aggregate landed in W5; D session re-confirmed against the W6 senior-dev brief) | `deadc73` | +| E | HIGH 7 (Coach feedback referencing `coach_messages`, plaintext column drop) | `650f150` | +| F | MED cluster (Sec-MED-2 Coolify scrub, Sr-MED-5 isCurrent device-id, SLEEP_DURATION migration verification note) + planning record | `1a46bfe` (MED) + this commit (planning) | ## Counts @@ -28,14 +28,14 @@ six-review brief into shippable develop-branch state. ## Test count delta -| Snapshot | Unit count | -| --- | --- | -| W5 close (commit `72829b1`) | 2223 | -| Session A close (CRIT + HIGH 1-3 added device + revoke tests) | 2227 | -| Session B close (HIGH 4 + simplify + HIGH 6) | ~2229 | -| Session D close (HIGH 5 reconfirm) | ~2229 | -| Session E close (HIGH 7 + coach-feedback updates) | 2235 | -| Session F close (forged X-Device-Id regression) | **2236** | +| Snapshot | Unit count | +| ------------------------------------------------------------- | ---------- | +| W5 close (commit `72829b1`) | 2223 | +| Session A close (CRIT + HIGH 1-3 added device + revoke tests) | 2227 | +| Session B close (HIGH 4 + simplify + HIGH 6) | ~2229 | +| Session D close (HIGH 5 reconfirm) | ~2229 | +| Session E close (HIGH 7 + coach-feedback updates) | 2235 | +| Session F close (forged X-Device-Id regression) | **2236** | Net delta over the W6 reconcile arc: **+13 unit tests**. diff --git a/.planning/v1423-backlog.md b/.planning/v1423-backlog.md index 3efc928f4..65183c8ea 100644 --- a/.planning/v1423-backlog.md +++ b/.planning/v1423-backlog.md @@ -12,7 +12,7 @@ without re-reading the full review pack. the settings cog) duplicates intent. Defer to v1.4.24 once W5 H7 surfaces enough thumbs data to know whether per-user prompt prefs drift from per-message ratings. Source: `phase-W6-v1423-design-review.md` - + `phase-W6-v1423-product-lead-review.md`. + - `phase-W6-v1423-product-lead-review.md`. ## Statistics diff --git a/CHANGELOG.md b/CHANGELOG.md index 64f9fc0f1..ac7ed9014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,130 @@ # Changelog +## [1.4.24] — 2026-05-12 + +Security + accessibility hardening release. Closes the highest-priority +findings from a focused security audit of the v1.4.23 release: a Bearer +token that omits a scope no longer authenticates routes which never +declared one, the public Umami proxy is rate-limited, the proxy nonce +is Edge-runtime portable, a session-expiry delete race can no longer +500 a parallel request, the import-validation envelope now matches the +standard error contract, and the runtime Docker image installs Prisma +7.8 alongside pinned worker dependencies. Accessibility passes on the +notifications surface, admin shell, chart overlays and notification +settings deep-link anchors round out the user-facing work. + +### Security + +- **Bearer auth is fail-closed when a route declares no scope.** + `requireAuth()` previously let any non-revoked, non-expired API token + through whenever a handler omitted the `requiredPermission` argument. + A narrowly-scoped token (e.g. `["medication:ingest"]`) could therefore + reach every handler that called `requireAuth()` bare — including + account-sensitive routes such as `/api/auth/profile`, + `/api/auth/password`, `/api/withings/credentials`, + `/api/settings/data`, `/api/export/full-backup` and `/api/tokens`. + The Bearer path now throws `HttpError(403)` with audit reason + `scope_required` unless the token carries the `["*"]` wildcard + (the iOS app login token), preserving the existing session-cookie + path and the explicit-scope path (`/api/ingest/medications`). The + admin surface is unaffected — `requireAdmin()` was already + cookie-only. +- **Public Umami analytics proxy now rate-limits by client IP.** + `POST /api/send` forwards browser analytics events to the configured + Umami origin. The SSRF allow-list and 64 KB body cap were already in + place, but the endpoint itself was unbounded. Added a 120-events-per- + minute-per-IP gate using the existing `checkRateLimit` infrastructure; + abuse returns the standard `429` envelope without invoking the + upstream fetch. +- **Edge-runtime portable nonce generation in the proxy.** The proxy + previously assembled the CSP nonce via `Buffer.from(...)`, which is + not guaranteed in the Edge runtime. Switched to + `btoa(String.fromCharCode(...new Uint8Array(16)))`. Same 128 bits of + entropy, Node-and-Edge compatible without a runtime declaration. +- **Session-expiry delete race no longer 500s a parallel request.** + `getSession()` reaped expired session rows with `prisma.session +.delete()`; two requests arriving at the same expired session would + race and the loser would surface a 500. Replaced with `deleteMany` + + a catch-all guard, matching the discipline already used in + `destroySession()`. + +### Fixed + +- **`POST /api/import` returns the standard `{ data: null, error }` + envelope on validation failure.** The handler previously returned + the validation message inside `apiSuccess(...)` with status 422, + which clients could not distinguish from a successful import. +- **Runtime Docker image pins Prisma 7.8 + worker dependencies.** The + multi-stage image was installing `prisma@7.4.0`, `@prisma/engines +@7.4.0`, unpinned `pg-boss@12`, `@prisma/adapter-pg@7` and `pg@8` + into the worker prefix while the app code shipped Prisma 7.8 from + the lockfile. Pinned to `prisma@7.8`, `@prisma/engines@7.8`, + `pg-boss@12.18`, `@prisma/adapter-pg@7.8` and `pg@8.20` so + migration engine and generated client cannot drift across a deploy. +- **Notifications page surfaces a load error instead of a silent + "no channels" empty state.** `useQuery` failures now render an + alert with `role="alert"` and `notifications.loadError` copy + (EN/DE), so a transient 500 on `/api/notifications/preferences` + stops looking like a misconfiguration. + +### Accessibility + +- **Notification-preference switches carry accessible names.** Each + switch in the per-event table now exposes an `aria-label` combining + event-type label and channel label; the mobile per-event layout pairs + each `` with its `