diff --git a/docs/audit/v1415-auto-deploy.md b/docs/audit/v1415-auto-deploy.md
index 26f2f9905..d3fe37e10 100644
--- a/docs/audit/v1415-auto-deploy.md
+++ b/docs/audit/v1415-auto-deploy.md
@@ -136,14 +136,14 @@ populated.
## Failure modes considered
-| Failure | What happens |
-| --------------------------------------------- | --------------------------------------------------------------------------------------------------- |
+| Failure | What happens |
+| --------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| Coolify deploy URL down | GH Actions step continues (`continue-on-error: true`); image stays on GHCR, deploy falls back to manual. |
-| Coolify notification fires before image pull | Coolify retries the pull internally; final status (success/failure) reaches our webhook regardless. |
-| Webhook secret leaked | Rotate `DEPLOY_WEBHOOK_SECRET`, redeploy. Audit-log entry on every received call shows source IP. |
-| Webhook handler down at deploy time | Coolify retries 3× per docs; final missed event is recoverable from Coolify's own deployment log. |
-| Coolify API rejects token (permission scope) | GH Actions step prints the response body; the next merge surfaces it as a workflow failure. |
-| GHCR push succeeds but webhook step is broken | Image still gets to GHCR; manual deploy still works as a fallback. |
+| Coolify notification fires before image pull | Coolify retries the pull internally; final status (success/failure) reaches our webhook regardless. |
+| Webhook secret leaked | Rotate `DEPLOY_WEBHOOK_SECRET`, redeploy. Audit-log entry on every received call shows source IP. |
+| Webhook handler down at deploy time | Coolify retries 3× per docs; final missed event is recoverable from Coolify's own deployment log. |
+| Coolify API rejects token (permission scope) | GH Actions step prints the response body; the next merge surfaces it as a workflow failure. |
+| GHCR push succeeds but webhook step is broken | Image still gets to GHCR; manual deploy still works as a fallback. |
## Watchtower — why not (revisited)
diff --git a/docs/audit/v1415-empty-states.md b/docs/audit/v1415-empty-states.md
index f84851de0..c273558fc 100644
--- a/docs/audit/v1415-empty-states.md
+++ b/docs/audit/v1415-empty-states.md
@@ -12,21 +12,21 @@ not a single grey sentence floating in a card.
## Findings
-| Route / surface | Component | State before | What was wrong | What changed |
-| ----------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| Route / surface | Component | State before | What was wrong | What changed |
+| ----------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `/admin/users` | `UserManagementSection` | Loading-only — when `users` resolved to `[]` the table simply rendered an empty `
` with the header still visible. | Empty case never specified; on a fresh deployment with one admin self-row it never reproduced for the live tenant, but the `admin/user` filter did regularly. | New `` mounts inside the card when `filteredUsers.length === 0`, with `Users` icon, filter-aware copy, and a "Reset filter" CTA when a filter is active. |
-| `/admin/backups` | `BackupsSection` | Plain `
No backups recorded yet.
`. | No icon, no CTA, no signal that the "Run backup now" button at the top is what to click. | Replaced with `` (`Database` icon) that includes a primary "Backup now" button — duplicates the header CTA but inside the empty card so the new admin doesn't need to look up. |
-| `/admin/login-overview` | `LoginOverviewSection` | Plain `
No entries found.
` (filter-dependent). | When "Failed only" is selected on a healthy installation the user sees nothing — looked like a bug. | `` with `ScrollText` icon, filter-aware description, and a "Show all events" CTA when the failed-only filter is active. |
-| `/admin/api-tokens` | `ApiTokenOverviewSection` | Plain `
No API tokens found.
`. | Doesn't tell admins that tokens are issued at login from native clients — they wonder if they need to create them somewhere. | `` with `Key` icon and a description pointing to native sign-in / Settings → Account API tokens. |
-| `/admin/feedback` | `FeedbackInboxSection` | Plain `
No feedback in this category.
` per tab. | OK as text but the tab strip already shows `0` badges; the inner panel deserves the same compactness as other admin sections. | `` with `Inbox` icon. Per-tab description. |
-| `/admin` overview | `RecentAuditPreview` | Plain `
No recent audit entries.
`. | Compact but icon-less — visually inconsistent with the audit-table fixes. | `` with `ScrollText` icon. |
-| `/measurements` | `MeasurementList` | `
{t("measurements.noMeasurements")}
`. | No CTA and the top-level "Add measurement" button is the only way out. | `` with `Activity` icon, filter-aware description, "Add your first measurement" CTA. |
-| `/mood` | `MoodList` | `
{t("mood.noEntries")}
`. | No CTA. | `` with `Smile` icon, filter-aware description, "Log your first mood" CTA. |
-| `/medications` | `MedicationsPage` | Custom inline icon + button block. | Already friendly but didn't share the new primitive — diverging styles. | Refactored to `` with `Pill` icon, "Add first medication" CTA. Visual parity with the rest. |
-| `/insights` (top-level) | `InsightsPage` | Plain `
{t("common.noData")}
` when `data` is undefined. | Looked like a hung page. | `` with `TrendingUp` icon, "Log a measurement" CTA → `/measurements`. |
-| `/insights` (BMI no-height) | Inline `
` | "no data" when `user.heightCm == null`. | The user might wonder why the BMI section is empty. | `` with `Ruler` icon, copy points to `/settings/account` and a "Set height" CTA. |
-| `/achievements` | `AchievementsPage` (no-grouped state) | Plain `
No achievements unlocked yet.
` inside a card. | Icon-less. The card after summary tiles needs the same visual cue we use elsewhere. | `` with `Trophy` icon — keeps the same `noneUnlockedYet` string to avoid C4 i18n churn. |
-| Dashboard tile-strip / charts | `DashboardPage` | Tile strip + chart row both hidden when every metric has zero data; only `` plus the welcome banner show. The strip itself stays a 0-px row. | Brand-new users see a near-blank page below the welcome banner — feels broken even though the checklist is technically present. | New `` block rendered when `trendCards.length === 0 && charts.length === 0`. Routed to "Add your first measurement" via the existing quick-entry dialog (no nav). Does not race the checklist (which has its own self-gating). |
+| `/admin/backups` | `BackupsSection` | Plain `
No backups recorded yet.
`. | No icon, no CTA, no signal that the "Run backup now" button at the top is what to click. | Replaced with `` (`Database` icon) that includes a primary "Backup now" button — duplicates the header CTA but inside the empty card so the new admin doesn't need to look up. |
+| `/admin/login-overview` | `LoginOverviewSection` | Plain `
No entries found.
` (filter-dependent). | When "Failed only" is selected on a healthy installation the user sees nothing — looked like a bug. | `` with `ScrollText` icon, filter-aware description, and a "Show all events" CTA when the failed-only filter is active. |
+| `/admin/api-tokens` | `ApiTokenOverviewSection` | Plain `
No API tokens found.
`. | Doesn't tell admins that tokens are issued at login from native clients — they wonder if they need to create them somewhere. | `` with `Key` icon and a description pointing to native sign-in / Settings → Account API tokens. |
+| `/admin/feedback` | `FeedbackInboxSection` | Plain `
No feedback in this category.
` per tab. | OK as text but the tab strip already shows `0` badges; the inner panel deserves the same compactness as other admin sections. | `` with `Inbox` icon. Per-tab description. |
+| `/admin` overview | `RecentAuditPreview` | Plain `
No recent audit entries.
`. | Compact but icon-less — visually inconsistent with the audit-table fixes. | `` with `ScrollText` icon. |
+| `/measurements` | `MeasurementList` | `
{t("measurements.noMeasurements")}
`. | No CTA and the top-level "Add measurement" button is the only way out. | `` with `Activity` icon, filter-aware description, "Add your first measurement" CTA. |
+| `/mood` | `MoodList` | `
{t("mood.noEntries")}
`. | No CTA. | `` with `Smile` icon, filter-aware description, "Log your first mood" CTA. |
+| `/medications` | `MedicationsPage` | Custom inline icon + button block. | Already friendly but didn't share the new primitive — diverging styles. | Refactored to `` with `Pill` icon, "Add first medication" CTA. Visual parity with the rest. |
+| `/insights` (top-level) | `InsightsPage` | Plain `
{t("common.noData")}
` when `data` is undefined. | Looked like a hung page. | `` with `TrendingUp` icon, "Log a measurement" CTA → `/measurements`. |
+| `/insights` (BMI no-height) | Inline `
` | "no data" when `user.heightCm == null`. | The user might wonder why the BMI section is empty. | `` with `Ruler` icon, copy points to `/settings/account` and a "Set height" CTA. |
+| `/achievements` | `AchievementsPage` (no-grouped state) | Plain `
No achievements unlocked yet.
` inside a card. | Icon-less. The card after summary tiles needs the same visual cue we use elsewhere. | `` with `Trophy` icon — keeps the same `noneUnlockedYet` string to avoid C4 i18n churn. |
+| Dashboard tile-strip / charts | `DashboardPage` | Tile strip + chart row both hidden when every metric has zero data; only `` plus the welcome banner show. The strip itself stays a 0-px row. | Brand-new users see a near-blank page below the welcome banner — feels broken even though the checklist is technically present. | New `` block rendered when `trendCards.length === 0 && charts.length === 0`. Routed to "Add your first measurement" via the existing quick-entry dialog (no nav). Does not race the checklist (which has its own self-gating). |
## Out of scope (deferred)
diff --git a/docs/audit/v1416-settings-audit.md b/docs/audit/v1416-settings-audit.md
index 31a304480..b78b2303d 100644
--- a/docs/audit/v1416-settings-audit.md
+++ b/docs/audit/v1416-settings-audit.md
@@ -151,7 +151,7 @@ bottom. Audit:
| Notifications | **Borderline** | `` lists per-channel status at the top; the per-channel config cards (``, ``, ``) follow below. The status card has `Re-enable` + `Send test` actions per channel that arguably duplicate functionality in the config cards. **Not a top/bottom-split-of-one-concept** — the status card is a read-only summary, the config cards are mutators. Keeping it. |
| Dashboard | No | One layout card. |
| Thresholds | No | One editor card with per-metric rows. |
-| AI | **YES (out of scope, B2)** | Provider dropdown at the top selects between OpenAI/Anthropic/Local; key/credential inputs for each provider live further down. This is the case the maintainer flagged explicitly. |
+| AI | **YES (out of scope, B2)** | Provider dropdown at the top selects between OpenAI/Anthropic/Local; key/credential inputs for each provider live further down. This is the case the maintainer flagged explicitly. |
| API | No | Endpoints reference card + tokens card. |
| Export | No | 5 cards, each self-contained. |
| Advanced | No | Single danger-zone card (post-B7 cleanup). |
diff --git a/docs/audit/v1416-summary.md b/docs/audit/v1416-summary.md
index 48f03f9e2..32d6cba07 100644
--- a/docs/audit/v1416-summary.md
+++ b/docs/audit/v1416-summary.md
@@ -124,20 +124,20 @@ the maintainer's "leave it" call.
### Cleanup E — Release
-| Item | Result |
-| --------------------------------- | ------------------------------------------------------------------------------------ |
-| Pre-release verify | `pnpm typecheck` clean; `pnpm test` 1540/1540; integration 59/59 |
-| Release commit | `d443c22 chore(release): v1.4.16` |
-| Tag | `v1.4.16` (annotated) |
-| GHCR — tag run | `25616783583` success, digest `2f1a0d6b381d…` |
-| GHCR — main run | `25616782255` success, digest `4841bef396ad…` |
-| Coolify auto-deploy | NO — same race as v1.4.14/v1.4.15; retag-on-host fallback used |
-| Live image digest | `sha256:05f8a126d63962d9a4af4769de830d3fee022d634787e811b4339ee464420daa` |
-| `/api/version` transition | `1.4.15` → `1.4.16` at `2026-05-10T03:45:58+02:00` |
-| Smoke (14 routes, live session) | 13/14 200; `/dashboard` 404 expected (root is dashboard) |
-| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.16 |
-| Docs site | `8addef4` + `2a5802b` on `healthlog-docs/main` (44 pages, three new deep-dive pages) |
-| Landing site | `3d17207` on `healthlog-landing/main` (1.4.15 → 1.4.16, AI cards updated) |
+| Item | Result |
+| ------------------------------- | ------------------------------------------------------------------------------------ |
+| Pre-release verify | `pnpm typecheck` clean; `pnpm test` 1540/1540; integration 59/59 |
+| Release commit | `d443c22 chore(release): v1.4.16` |
+| Tag | `v1.4.16` (annotated) |
+| GHCR — tag run | `25616783583` success, digest `2f1a0d6b381d…` |
+| GHCR — main run | `25616782255` success, digest `4841bef396ad…` |
+| Coolify auto-deploy | NO — same race as v1.4.14/v1.4.15; retag-on-host fallback used |
+| Live image digest | `sha256:05f8a126d63962d9a4af4769de830d3fee022d634787e811b4339ee464420daa` |
+| `/api/version` transition | `1.4.15` → `1.4.16` at `2026-05-10T03:45:58+02:00` |
+| Smoke (14 routes, live session) | 13/14 200; `/dashboard` 404 expected (root is dashboard) |
+| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.16 |
+| Docs site | `8addef4` + `2a5802b` on `healthlog-docs/main` (44 pages, three new deep-dive pages) |
+| Landing site | `3d17207` on `healthlog-landing/main` (1.4.15 → 1.4.16, AI cards updated) |
---
diff --git a/docs/audit/v1418-summary.md b/docs/audit/v1418-summary.md
index 63510ca89..6e07ed7be 100644
--- a/docs/audit/v1418-summary.md
+++ b/docs/audit/v1418-summary.md
@@ -54,10 +54,10 @@ v1.4.19 backlog: see `.planning/v1419-backlog.md`.
### Cleanup A — Quick fixes
-| Bucket | Headline commit | Subject |
-| ------ | --------------- | ------- |
-| A1 | `23363ca` | fix(analytics): wire BD-Zielbereich tile 7T/30T sub-values via `computeBpInTargetWindows()` |
-| A2 | `3e16074` | fix(admin): AdminShell mobile section strip — `.no-scrollbar` utility on both AdminShell + SettingsShell pillstrips |
+| Bucket | Headline commit | Subject |
+| ------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
+| A1 | `23363ca` | fix(analytics): wire BD-Zielbereich tile 7T/30T sub-values via `computeBpInTargetWindows()` |
+| A2 | `3e16074` | fix(admin): AdminShell mobile section strip — `.no-scrollbar` utility on both AdminShell + SettingsShell pillstrips |
| A3 | (6 commits in `agent/a3-charts-revert`) | feat(charts): clean-line revert + per-chart overlay toggles (`useChartOverlayPrefs` + `PUT /api/dashboard/chart-overlay-prefs`) |
A3's six atomic commits: gradient-fill module deleted, mood-chart
@@ -105,20 +105,20 @@ TypeError on cached pre-strict insight payloads. Three commits on
### Cleanup E — Release
-| Item | Result |
-| --------------------------------- | ---------------------------------------------------------------------------- |
-| Pre-release verify | `pnpm typecheck` clean; `pnpm test` 1605/1605; integration 66/66 |
-| Release commit | `0243e20 chore(release): v1.4.18` |
-| Tag | `v1.4.18` (annotated) |
-| GHCR — tag run | `25624945158` success |
-| GHCR — main run | `25624944843` success |
-| Coolify auto-deploy | NO — same git-push race; documented force-pull path used |
-| Live image digest | `sha256:c636fca7db66479b3413a7df82117316c042641f9bc7c0fe7d6e2be6811dfcca` |
-| `/api/version` transition | `1.4.17` → `1.4.18` within first 5-second poll cycle |
-| Smoke (15 routes, live session) | 14/14 real routes 200; `/dashboard` 404 expected (root is dashboard) |
-| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.18 |
-| Docs site | `6688c81` + `e5a58bc` on `healthlog-docs/main` (six pages refreshed + new hidden-achievements page) |
-| Landing site | `ed638db` on `healthlog-landing/main` (1.4.16 → 1.4.18, clean-line phrasing) |
+| Item | Result |
+| ------------------------------- | --------------------------------------------------------------------------------------------------- |
+| Pre-release verify | `pnpm typecheck` clean; `pnpm test` 1605/1605; integration 66/66 |
+| Release commit | `0243e20 chore(release): v1.4.18` |
+| Tag | `v1.4.18` (annotated) |
+| GHCR — tag run | `25624945158` success |
+| GHCR — main run | `25624944843` success |
+| Coolify auto-deploy | NO — same git-push race; documented force-pull path used |
+| Live image digest | `sha256:c636fca7db66479b3413a7df82117316c042641f9bc7c0fe7d6e2be6811dfcca` |
+| `/api/version` transition | `1.4.17` → `1.4.18` within first 5-second poll cycle |
+| Smoke (15 routes, live session) | 14/14 real routes 200; `/dashboard` 404 expected (root is dashboard) |
+| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.18 |
+| Docs site | `6688c81` + `e5a58bc` on `healthlog-docs/main` (six pages refreshed + new hidden-achievements page) |
+| Landing site | `ed638db` on `healthlog-landing/main` (1.4.16 → 1.4.18, clean-line phrasing) |
---
@@ -132,8 +132,8 @@ Full backlog: `.planning/v1419-backlog.md`. Highlights:
`messages/de.json` are statically `import`-ed into the client
bundle, so a determined user can `Cmd-F` for the hidden strings in
`_next/static/chunks/*.js`. Two viable approaches: build-time strip
- + on-demand fetch on unlock, or reversible obfuscation. Approach 1
- is the v1.5 fix, approach 2 the v1.4.19 stopgap.
+ - on-demand fetch on unlock, or reversible obfuscation. Approach 1
+ is the v1.5 fix, approach 2 the v1.4.19 stopgap.
- **MED items**: 12 entries spanning code-review, design, and
senior-dev — see `.planning/v1419-backlog.md`. Notable:
`chart-overlay-prefs` route still lacks `withIdempotency()`
@@ -227,7 +227,7 @@ Full backlog: `.planning/v1419-backlog.md`. Highlights:
tag examples bumped 1.4.16 → 1.4.18, rollback example
1.4.15 → 1.4.17.
- **healthlog-landing** (Next.js) — `softwareVersion 1.4.16 →
- 1.4.18` in JSON-LD; featureList line rewritten as "Clean-line
+1.4.18` in JSON-LD; featureList line rewritten as "Clean-line
health charts with smooth animation, rich tooltips, and explicit
empty states — plus per-chart toggles for trend indicator, trend
arrow, and target-range overlay"; capability badges updated
diff --git a/docs/audit/v1419-summary.md b/docs/audit/v1419-summary.md
index 1287ddec4..9a2862323 100644
--- a/docs/audit/v1419-summary.md
+++ b/docs/audit/v1419-summary.md
@@ -38,15 +38,15 @@ v1.5 reserved for iOS app + Apple Health.
### Cleanup A — bugs + polish
-| Bucket | Headline commit | Subject |
-| ------ | --------------- | ------- |
-| A1 | `a856272` | fix(analytics): BD-Zielbereich headline routes through new `allTime` window in `computeBpInTargetWindows()` |
-| A2 | `77a3ad3` + `a739085` | fix(charts): mobile-first header stack + universal x-axis tick density helper |
-| A3 | `60a91af` / `98a3d10` / `335f288` / `fa91a73` | feat(insights): comparison toggle relocated to hero, single page refresh, BP/Weight tile strip removed, lowercase template-token leak fixed |
-| A4 | `b5e9a95` | fix(ai): GROUND RULE 7 — no default-positivity opener; `PROMPT_VERSION` 4.16.1 → 4.19.0 |
-| A5 | `ba0d6b8` / `0dcc91a` / `47a8fc7` | feat(settings): `` consolidates Withings + Mood Log status UI |
-| A6 | `957f8e9` / `9fda634` / `1075784` / `737b533` / `78f1f3f` | fix(settings): mobile consistency sweep — input heights, action button placement, language select row |
-| A7 | `088832a` / `dd8212e` / `7a70db6` / `6507646` / `90a109d` | fix(admin): feedback tab strip, api-tokens scrollbar 4th attempt, Einklappen removal, Zielwerte spacing + DE labels |
+| Bucket | Headline commit | Subject |
+| ------ | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
+| A1 | `a856272` | fix(analytics): BD-Zielbereich headline routes through new `allTime` window in `computeBpInTargetWindows()` |
+| A2 | `77a3ad3` + `a739085` | fix(charts): mobile-first header stack + universal x-axis tick density helper |
+| A3 | `60a91af` / `98a3d10` / `335f288` / `fa91a73` | feat(insights): comparison toggle relocated to hero, single page refresh, BP/Weight tile strip removed, lowercase template-token leak fixed |
+| A4 | `b5e9a95` | fix(ai): GROUND RULE 7 — no default-positivity opener; `PROMPT_VERSION` 4.16.1 → 4.19.0 |
+| A5 | `ba0d6b8` / `0dcc91a` / `47a8fc7` | feat(settings): `` consolidates Withings + Mood Log status UI |
+| A6 | `957f8e9` / `9fda634` / `1075784` / `737b533` / `78f1f3f` | fix(settings): mobile consistency sweep — input heights, action button placement, language select row |
+| A7 | `088832a` / `dd8212e` / `7a70db6` / `6507646` / `90a109d` | fix(admin): feedback tab strip, api-tokens scrollbar 4th attempt, Einklappen removal, Zielwerte spacing + DE labels |
### Cleanup B — A8 quality findings applied
@@ -103,20 +103,20 @@ Full backlog: `.planning/v1420-backlog.md`.
## Production deploy
-| Field | Value |
-| ----- | ----- |
-| URL | `https://healthlog.bombeck.io` |
-| `/api/version` | `1.4.19` (flipped at 2026-05-10T12:39:59Z) |
-| Image digest BEFORE | `sha256:c636fca7db66479b3413a7df82117316c042641f9bc7c0fe7d6e2be6811dfcca` (v1.4.18) |
-| Image digest AFTER | `sha256:b48f93874cdbcd6c2d729f1b8eeb63a6d1bbb90d56f629846ef6eab6cf272aa9` (v1.4.19) |
-| Release commit | `89f00cf chore(release): v1.4.19` |
-| Tag | `v1.4.19` (annotated) |
-| GHCR — tag run | `25628853202` success |
-| Coolify auto-deploy | NO — main-branch deploy hung and was canceled; host-side retag-on-host fallback used |
+| Field | Value |
+| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| URL | `https://healthlog.bombeck.io` |
+| `/api/version` | `1.4.19` (flipped at 2026-05-10T12:39:59Z) |
+| Image digest BEFORE | `sha256:c636fca7db66479b3413a7df82117316c042641f9bc7c0fe7d6e2be6811dfcca` (v1.4.18) |
+| Image digest AFTER | `sha256:b48f93874cdbcd6c2d729f1b8eeb63a6d1bbb90d56f629846ef6eab6cf272aa9` (v1.4.19) |
+| Release commit | `89f00cf chore(release): v1.4.19` |
+| Tag | `v1.4.19` (annotated) |
+| GHCR — tag run | `25628853202` success |
+| Coolify auto-deploy | NO — main-branch deploy hung and was canceled; host-side retag-on-host fallback used |
| Smoke (curl, live session) | `/` 200 · `/insights` 200 · `/admin/api-tokens` 200 · `/settings/integrations` 200 · `/achievements` 200 · `/dashboard` 404 (expected — root is dashboard, no `/dashboard` route in App Router) |
-| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.19 |
-| Docs site | `6e8840e` on `healthlog-docs/main` (six pages refreshed) |
-| Landing site | `dd5892f` on `healthlog-landing/main` (`softwareVersion` JSON-LD bumped) |
+| GH release | https://github.com/MBombeck/HealthLog/releases/tag/v1.4.19 |
+| Docs site | `6e8840e` on `healthlog-docs/main` (six pages refreshed) |
+| Landing site | `dd5892f` on `healthlog-landing/main` (`softwareVersion` JSON-LD bumped) |
The tag-build pipeline succeeded; the main-branch deploy stalled and
was canceled, so v1.4.19 was promoted via the documented host-side
diff --git a/docs/audit/v146-findings.md b/docs/audit/v146-findings.md
index 3d36c4241..0a2afe41b 100644
--- a/docs/audit/v146-findings.md
+++ b/docs/audit/v146-findings.md
@@ -349,7 +349,7 @@ After all fixes:
differ from current.
- Authenticated session: pull a current session id from prod DB
when needed (`SELECT id FROM sessions WHERE user_id='…'
- AND expires_at > now() ORDER BY created_at DESC LIMIT 1;`).
+AND expires_at > now() ORDER BY created_at DESC LIMIT 1;`).
- Hit `/api/ai/test` with fake key → still returns 422
- Hit `/api/insights/generate` with `forceRefresh=false` and a
fresh user → does not consume rate-limit token (P13 verify).
diff --git a/src/app/__tests__/insights-b3-wiring.test.ts b/src/app/__tests__/insights-b3-wiring.test.ts
index 90d13c64a..edb5920e4 100644
--- a/src/app/__tests__/insights-b3-wiring.test.ts
+++ b/src/app/__tests__/insights-b3-wiring.test.ts
@@ -14,10 +14,7 @@ import { join } from "node:path";
const ROOT = join(__dirname, "../../..");
const INSIGHTS_PATH = join(ROOT, "src/app/insights/page.tsx");
-const ANALYTICS_ROUTE_PATH = join(
- ROOT,
- "src/app/api/analytics/route.ts",
-);
+const ANALYTICS_ROUTE_PATH = join(ROOT, "src/app/api/analytics/route.ts");
const ADVISOR_HOOK_PATH = join(
ROOT,
"src/components/insights/use-insights-advisor.ts",
@@ -30,12 +27,8 @@ function load(path: string): string {
describe("v1.4.20 B3 — /insights mounts CorrelationRow + TrendsRow", () => {
it("imports both new components", () => {
const src = load(INSIGHTS_PATH);
- expect(src).toContain(
- 'from "@/components/insights/correlation-row"',
- );
- expect(src).toContain(
- 'from "@/components/insights/trends-row"',
- );
+ expect(src).toContain('from "@/components/insights/correlation-row"');
+ expect(src).toContain('from "@/components/insights/trends-row"');
});
it("mounts behind a non-null analytics.correlations guard", () => {
diff --git a/src/app/api/analytics/route.ts b/src/app/api/analytics/route.ts
index 9b30ab1a9..f6fb3d68b 100644
--- a/src/app/api/analytics/route.ts
+++ b/src/app/api/analytics/route.ts
@@ -222,7 +222,10 @@ async function computeCorrelationHypotheses(userId: string): Promise<{
dailySys.set(key, list);
}
- const dailyCompliance = new Map();
+ const dailyCompliance = new Map<
+ string,
+ { expected: number; taken: number }
+ >();
for (const event of intakeRows) {
const key = dayKey(event.scheduledFor);
const slot = dailyCompliance.get(key) ?? { expected: 0, taken: 0 };
@@ -275,8 +278,7 @@ async function computeCorrelationHypotheses(userId: string): Promise<{
for (const [key, moodScores] of dailyMood.entries()) {
const pulseValues = dailyPulse.get(key);
if (!pulseValues || pulseValues.length === 0) continue;
- const meanMood =
- moodScores.reduce((s, v) => s + v, 0) / moodScores.length;
+ const meanMood = moodScores.reduce((s, v) => s + v, 0) / moodScores.length;
const meanPulse =
pulseValues.reduce((s, v) => s + v, 0) / pulseValues.length;
moodPulsePairs.push({
@@ -449,9 +451,7 @@ async function computeUserHealthScore(
// anchor still captures the same logical 30 days.
const shifted = events.map((e) => ({
scheduledFor: new Date(e.scheduledFor.getTime() + 7 * DAY_MS),
- takenAt: e.takenAt
- ? new Date(e.takenAt.getTime() + 7 * DAY_MS)
- : null,
+ takenAt: e.takenAt ? new Date(e.takenAt.getTime() + 7 * DAY_MS) : null,
skipped: e.skipped,
}));
return calculateCompliance(shifted, med.schedules, 30, med.createdAt)
diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx
index de25296d6..360caaf43 100644
--- a/src/app/insights/page.tsx
+++ b/src/app/insights/page.tsx
@@ -907,9 +907,11 @@ export default function InsightsPage() {
// cached AI payload carries a weeklyReport block. The banner deep-
// links into `/insights/report/[week]` for Read / Share / Export PDF.
const weeklyReport = (
- advisor.payload?.insights as {
- weeklyReport?: { weekISO: string } | null;
- } | undefined
+ advisor.payload?.insights as
+ | {
+ weeklyReport?: { weekISO: string } | null;
+ }
+ | undefined
)?.weeklyReport;
const weeklyReportReady = weeklyReport
? {
@@ -930,11 +932,13 @@ export default function InsightsPage() {
// 4.20.2 simply produce an empty array.
const rawStoryboard = (
advisor.payload?.insights as
- | { storyboardAnnotations?: Array<{
- date: string;
- label: string;
- category: string;
- }> }
+ | {
+ storyboardAnnotations?: Array<{
+ date: string;
+ label: string;
+ category: string;
+ }>;
+ }
| undefined
)?.storyboardAnnotations;
const bpStoryboardAnnotations = (rawStoryboard ?? []).map((entry) => ({
@@ -991,9 +995,7 @@ export default function InsightsPage() {
with an inline AI-authored sentence below each. Annotations come
from `advisor.payload.trendAnnotations` (PROMPT_VERSION 4.20.1+);
legacy cached payloads simply paint the per-metric empty hint. */}
-
+
{/* v1.4.16 phase D reconcile (CRITICAL C1) — wire the polished
`` (severity-ordered recommendations grid +
diff --git a/src/components/charts/__tests__/health-chart-annotations.test.ts b/src/components/charts/__tests__/health-chart-annotations.test.ts
index e2db9a07c..3a2d9e38b 100644
--- a/src/components/charts/__tests__/health-chart-annotations.test.ts
+++ b/src/components/charts/__tests__/health-chart-annotations.test.ts
@@ -19,7 +19,10 @@ import { resolveAnnotationPositions } from "../health-chart";
const DAY_MS = 24 * 60 * 60 * 1000;
-function makeSeries(start: number, count: number): Array<{ timestamp: number }> {
+function makeSeries(
+ start: number,
+ count: number,
+): Array<{ timestamp: number }> {
return Array.from({ length: count }, (_, i) => ({
timestamp: start + i * DAY_MS,
}));
@@ -54,9 +57,7 @@ describe("resolveAnnotationPositions", () => {
it("pins an exact-day annotation to its visible bucket", () => {
const positions = resolveAnnotationPositions(
- [
- { date: "2026-04-15", label: "Started Ramipril", color: "#ff79c6" },
- ],
+ [{ date: "2026-04-15", label: "Started Ramipril", color: "#ff79c6" }],
dailySeries,
);
expect(positions).toHaveLength(1);
diff --git a/src/components/charts/health-chart.tsx b/src/components/charts/health-chart.tsx
index 13038f4c9..2a76abfe1 100644
--- a/src/components/charts/health-chart.tsx
+++ b/src/components/charts/health-chart.tsx
@@ -276,7 +276,9 @@ export interface ResolvedAnnotation {
}
export function resolveAnnotationPositions(
- annotations: Array<{ date: string; label: string; color: string }> | undefined,
+ annotations:
+ | Array<{ date: string; label: string; color: string }>
+ | undefined,
chartData: Array<{ timestamp: number }> | undefined,
): ResolvedAnnotation[] {
if (!annotations || !chartData || chartData.length === 0) return [];
@@ -906,7 +908,6 @@ export function HealthChart({
chartData,
);
-
const showContextDetails = showMA || showTrend || showBands;
const animationsEnabled = !prefersReducedMotion();
diff --git a/src/components/insights/__tests__/correlation-row.test.tsx b/src/components/insights/__tests__/correlation-row.test.tsx
index 2c910b306..4882dab18 100644
--- a/src/components/insights/__tests__/correlation-row.test.tsx
+++ b/src/components/insights/__tests__/correlation-row.test.tsx
@@ -94,8 +94,7 @@ describe("", () => {
}}
/>,
);
- const matches =
- html.match(/data-slot="correlation-row-disclaimer"/g) ?? [];
+ const matches = html.match(/data-slot="correlation-row-disclaimer"/g) ?? [];
expect(matches.length).toBe(1);
expect(html).toContain("Patterns are observational, not causal");
});
diff --git a/src/components/insights/__tests__/daily-briefing.test.tsx b/src/components/insights/__tests__/daily-briefing.test.tsx
index a87e577be..e60171a71 100644
--- a/src/components/insights/__tests__/daily-briefing.test.tsx
+++ b/src/components/insights/__tests__/daily-briefing.test.tsx
@@ -134,9 +134,7 @@ describe("", () => {
it("hides the keyFindings section when the array is empty", () => {
const html = render(
- ,
+ ,
);
expect(html).not.toContain('data-slot="daily-briefing-findings-title"');
expect(html).not.toContain('data-slot="daily-briefing-findings"');
@@ -165,15 +163,9 @@ describe("", () => {
it("disables the empty-state CTA while regenerating", () => {
const html = render(
- {}}
- regenerating
- />,
- );
- expect(html).toMatch(
- /data-slot="daily-briefing-empty-cta"[^>]*disabled/,
+ {}} regenerating />,
);
+ expect(html).toMatch(/data-slot="daily-briefing-empty-cta"[^>]*disabled/);
expect(html).toContain("Regenerating");
});
diff --git a/src/components/insights/__tests__/health-score-card.test.tsx b/src/components/insights/__tests__/health-score-card.test.tsx
index ca66f2b26..9d7d3ee3c 100644
--- a/src/components/insights/__tests__/health-score-card.test.tsx
+++ b/src/components/insights/__tests__/health-score-card.test.tsx
@@ -95,9 +95,7 @@ describe("", () => {
);
expect(html).toContain("No history yet");
// No delta chip in the corner.
- expect(html).not.toContain(
- 'data-slot="health-score-card-delta-chip"',
- );
+ expect(html).not.toContain('data-slot="health-score-card-delta-chip"');
});
it("renders the up-arrow + delta chip when delta > 0", () => {
@@ -190,9 +188,7 @@ describe("", () => {
delta={null}
/>,
);
- expect(html).not.toContain(
- 'data-slot="health-score-card-ask-coach"',
- );
+ expect(html).not.toContain('data-slot="health-score-card-ask-coach"');
});
it("renders the Ask-the-Coach button when onAskCoach is supplied", () => {
diff --git a/src/components/insights/__tests__/hero-strip.test.tsx b/src/components/insights/__tests__/hero-strip.test.tsx
index 9b5b8a79a..e2f9c4fcb 100644
--- a/src/components/insights/__tests__/hero-strip.test.tsx
+++ b/src/components/insights/__tests__/hero-strip.test.tsx
@@ -45,10 +45,7 @@ describe("", () => {
});
it("renders the morning greeting in German", () => {
- const html = render(
- ,
- "de",
- );
+ const html = render(, "de");
expect(html).toContain("Guten Morgen");
});
@@ -136,11 +133,7 @@ describe("", () => {
it("enables the ask-the-coach button when onAskCoach is supplied (B2b)", () => {
const html = render(
- {}}
- />,
+ {}} />,
);
const coachTag = html.match(
/