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( /]*data-slot="insights-hero-strip-action-coach"[^>]*>/, @@ -189,11 +182,7 @@ describe("", () => { it("forwards onPickPrompt down to the prompt strip without throwing", () => { const html = render( - {}} - />, + {}} />, ); // The chips render verbatim; SSR doesn't run click handlers. expect(html).toMatch(/data-slot="insights-suggested-prompts-chip"/); @@ -201,10 +190,7 @@ describe("", () => { // ── Misc ─────────────────────────────────────────────────────────── it("supports the German locale fallback subtitle", () => { - const html = render( - , - "de", - ); + const html = render(, "de"); expect(html).toContain("Täglicher Blick auf deine Trends"); }); @@ -259,7 +245,9 @@ describe("", () => { ); expect(html).toMatch(/data-slot="insights-hero-strip-weekly-banner-read"/); expect(html).toMatch(/data-slot="insights-hero-strip-weekly-banner-share"/); - expect(html).toMatch(/data-slot="insights-hero-strip-weekly-banner-export"/); + expect(html).toMatch( + /data-slot="insights-hero-strip-weekly-banner-export"/, + ); expect(html).toContain("Read"); expect(html).toContain("Share"); expect(html).toContain("Export PDF"); diff --git a/src/components/insights/__tests__/trend-annotation.test.tsx b/src/components/insights/__tests__/trend-annotation.test.tsx index 26792f9fa..4842d71fd 100644 --- a/src/components/insights/__tests__/trend-annotation.test.tsx +++ b/src/components/insights/__tests__/trend-annotation.test.tsx @@ -39,7 +39,10 @@ describe("", () => { }); it("renders the empty hint in German when locale=de", () => { - const html = render(, "de"); + const html = render( + , + "de", + ); expect(html).toContain("Mehr Daten"); }); @@ -57,14 +60,22 @@ describe("", () => { it("renders the moderate-confidence chip", () => { const html = render( - , + , ); expect(html).toContain("Moderate confidence"); }); it("renders the low-confidence chip", () => { const html = render( - , + , ); expect(html).toContain("Low confidence"); }); diff --git a/src/components/insights/coach-panel/__tests__/coach-input.test.tsx b/src/components/insights/coach-panel/__tests__/coach-input.test.tsx index d6d05d8b8..753e79c75 100644 --- a/src/components/insights/coach-panel/__tests__/coach-input.test.tsx +++ b/src/components/insights/coach-panel/__tests__/coach-input.test.tsx @@ -48,9 +48,7 @@ describe("", () => { // localised v1.5 voice tooltip on aria-label. Attribute order is // not guaranteed across React versions, so we check for both // independently inside the same tag. - const micTag = html.match( - /]*data-slot="coach-input-mic"[^>]*>/, - ); + const micTag = html.match(/]*data-slot="coach-input-mic"[^>]*>/); expect(micTag).not.toBeNull(); expect(micTag?.[0]).toContain("disabled"); expect(micTag?.[0]).toContain( @@ -120,9 +118,7 @@ describe("", () => { // SSR can't fire DOM events; smoke-check the contract by calling // the supplied handler directly. const handler = vi.fn(); - render( - {}} />, - ); + render( {}} />); handler("typed"); expect(handler).toHaveBeenCalledWith("typed"); }); @@ -131,8 +127,6 @@ describe("", () => { const html = render( {}} onSubmit={() => {}} />, ); - expect(html).toMatch( - /data-slot="coach-input-textarea"[^>]*rows="2"/, - ); + expect(html).toMatch(/data-slot="coach-input-textarea"[^>]*rows="2"/); }); }); diff --git a/src/components/insights/coach-panel/__tests__/history-rail.test.tsx b/src/components/insights/coach-panel/__tests__/history-rail.test.tsx index 3c099534c..d1e8d0678 100644 --- a/src/components/insights/coach-panel/__tests__/history-rail.test.tsx +++ b/src/components/insights/coach-panel/__tests__/history-rail.test.tsx @@ -23,7 +23,11 @@ function makeClientWithConversations(page: CoachConversationsPage) { return client; } -function render(node: React.ReactNode, client: QueryClient, locale: "en" | "de" = "en") { +function render( + node: React.ReactNode, + client: QueryClient, + locale: "en" | "de" = "en", +) { return renderToStaticMarkup( {node} @@ -81,9 +85,7 @@ describe("", () => { client, ); // c2 is active and gets the data-active flag. - expect(html).toMatch( - /data-slot="coach-history-item" data-active="true"/, - ); + expect(html).toMatch(/data-slot="coach-history-item" data-active="true"/); }); it("renders the empty-state copy when no conversations are cached", () => { diff --git a/src/components/insights/coach-panel/__tests__/message-thread.test.tsx b/src/components/insights/coach-panel/__tests__/message-thread.test.tsx index 1b45bb9cf..75cff0d1e 100644 --- a/src/components/insights/coach-panel/__tests__/message-thread.test.tsx +++ b/src/components/insights/coach-panel/__tests__/message-thread.test.tsx @@ -30,7 +30,8 @@ const baseConversation: CoachConversationDetailDTO = { { id: "m2", role: "assistant", - content: "Looking at your last 6 Mondays, the morning systolic averaged …", + content: + "Looking at your last 6 Mondays, the morning systolic averaged …", createdAt: "2026-05-10T09:00:30.000Z", metricSource: { windows: ["last30days"], @@ -84,9 +85,8 @@ describe("", () => { />, ); // 1 persisted user + 1 persisted assistant + 1 streaming assistant. - const userBubbles = ( - html.match(/data-slot="coach-bubble-user"/g) ?? [] - ).length; + const userBubbles = (html.match(/data-slot="coach-bubble-user"/g) ?? []) + .length; const assistantBubbles = ( html.match(/data-slot="coach-bubble-assistant"/g) ?? [] ).length; diff --git a/src/components/insights/coach-panel/__tests__/source-chips.test.tsx b/src/components/insights/coach-panel/__tests__/source-chips.test.tsx index de152a643..14fca24a2 100644 --- a/src/components/insights/coach-panel/__tests__/source-chips.test.tsx +++ b/src/components/insights/coach-panel/__tests__/source-chips.test.tsx @@ -71,9 +71,7 @@ describe("", () => { it("uses German labels when locale is 'de'", () => { const html = render( - , + , "de", ); expect(html).toContain("Blutdruck"); @@ -82,9 +80,7 @@ describe("", () => { it("renders a metric chip without window when windows are empty", () => { const html = render( - , + , ); expect(html).toContain('data-metric="general"'); expect(html).toContain("General"); diff --git a/src/components/insights/coach-panel/__tests__/use-coach.test.ts b/src/components/insights/coach-panel/__tests__/use-coach.test.ts index b8e5bb2cf..431eb34f9 100644 --- a/src/components/insights/coach-panel/__tests__/use-coach.test.ts +++ b/src/components/insights/coach-panel/__tests__/use-coach.test.ts @@ -104,8 +104,7 @@ describe("parseSseChunk", () => { }); it("ignores malformed frames silently", () => { - const chunk = - "data: not-json\n\n" + frame({ type: "token", token: "ok" }); + const chunk = "data: not-json\n\n" + frame({ type: "token", token: "ok" }); const { events } = parseSseChunk("", chunk); expect(events).toHaveLength(1); expect(events[0]).toEqual({ type: "token", token: "ok" }); @@ -184,8 +183,9 @@ describe("parseSseChunk", () => { } const tokenText = events - .filter((e): e is Extract => - e.type === "token", + .filter( + (e): e is Extract => + e.type === "token", ) .map((e) => e.token) .join(""); diff --git a/src/components/insights/coach-panel/coach-drawer-body.tsx b/src/components/insights/coach-panel/coach-drawer-body.tsx index 3b8e65a7c..b92e62727 100644 --- a/src/components/insights/coach-panel/coach-drawer-body.tsx +++ b/src/components/insights/coach-panel/coach-drawer-body.tsx @@ -70,7 +70,7 @@ export function CoachDrawerBody({ onClick={onOpenHistoryTray} data-slot="coach-drawer-history-tray-trigger" aria-label={t("insights.coach.historyTitle")} - className="bg-background/80 border-border/60 absolute left-2 top-2 z-10 h-7 gap-1 border px-2 text-[11px] backdrop-blur lg:hidden" + className="bg-background/80 border-border/60 absolute top-2 left-2 z-10 h-7 gap-1 border px-2 text-[11px] backdrop-blur lg:hidden" >