From 4f09c3f862826ea8ad2f1bf412d3ca2f7d5217d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 15 May 2026 22:59:10 +0200 Subject: [PATCH] chore(release): v1.4.27 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v1.4.27 — Mobile capability and maintainer-finding cleanup. ResponsiveSheet primitive (bottom-sheet on narrow viewports, centred dialog on desktop) applied to BD, Weight, Glucose, Mood, Activity forms. Maintainer-finding cleanups across Coach, Insights, Reports, Privacy. GeoLite2 offline geocoding with resilience layer (graceful fallback when EULA not yet propagated). E2E gap-fill coverage. 450-line release notes attached to the GitHub Release. --- .github/workflows/docker-publish.yml | 28 + .gitignore | 6 + .planning/research/v1427-r1-backlog-sweep.md | 218 +++++ .planning/research/v1427-r1-coach-insights.md | 342 ++++++++ .planning/research/v1427-r1-dashboard.md | 203 +++++ .planning/research/v1427-r1-ios-offline.md | 200 +++++ .planning/research/v1427-r1-notifications.md | 241 ++++++ .planning/research/v1427-r1-settings-admin.md | 286 +++++++ .../research/v1427-r3c-mobile-auth-public.md | 360 +++++++++ .planning/research/v1427-r3c-mobile-coach.md | 196 +++++ .../research/v1427-r3c-mobile-dashboard.md | 187 +++++ .../research/v1427-r3c-mobile-insights.md | 402 ++++++++++ .../v1427-r3c-mobile-measurements-workouts.md | 171 ++++ .../research/v1427-r3c-mobile-medications.md | 189 +++++ .../v1427-r3c-mobile-settings-admin.md | 198 +++++ .planning/research/v1427-r4-code-review.md | 226 ++++++ .planning/research/v1427-r4-dead-code.md | 307 +++++++ .planning/research/v1427-r4-design.md | 758 ++++++++++++++++++ .planning/research/v1427-r4-i18n.md | 305 +++++++ .planning/research/v1427-r4-product-lead.md | 364 +++++++++ .planning/research/v1427-r4-security.md | 206 +++++ .planning/research/v1427-r4-senior-dev.md | 619 ++++++++++++++ .planning/research/v1427-r4-simplifier.md | 418 ++++++++++ .planning/research/v1427-r4-ui-conformity.md | 639 +++++++++++++++ .planning/round-3-b1-dashboard-report.md | 98 +++ .planning/round-3-b2-settings-report.md | 68 ++ .planning/round-3-b3-geoip-report.md | 71 ++ .planning/round-3-b4-coach-insights-report.md | 176 ++++ .planning/round-3-b5-notifications-report.md | 176 ++++ .planning/round-3-b6-i18n-report.md | 263 ++++++ .../round-3-b7-symmetry-cleanup-report.md | 172 ++++ .planning/round-3-bios-handoff-report.md | 39 + .../round-3d-mb1-responsive-sheet-report.md | 172 ++++ .planning/round-3d-mb2-tap-target-report.md | 116 +++ .planning/round-3d-mb3-input-attrs-report.md | 146 ++++ .../round-3d-mb4-coach-reachability-report.md | 111 +++ .../round-3d-mb5-tables-cardlist-report.md | 110 +++ .../round-3d-mb6-regressions-auth-report.md | 198 +++++ .../round-3d-mb7-polish-residue-report.md | 103 +++ ...reconcile-rc1-ci-i18n-convention-report.md | 115 +++ ...-4-reconcile-rc2-responsivesheet-report.md | 120 +++ ...rc3-workouts-ssr-polish-deadcode-report.md | 68 ++ .../round-5-blocker-maxmind-license-key.md | 126 +++ .planning/round-5-e2e-gapfill-report.md | 151 ++++ .planning/v1425-codex-audit-prompt.md | 339 ++++++++ .planning/v1426-backlog.md | 209 +++++ .planning/v1427-b6-dead-key-probe.mjs | 139 ++++ .planning/v1427-b6-dead-key-strip.mjs | 82 ++ .planning/v1427-b6-format-keys.mjs | 62 ++ .planning/v1427-b6-new-keys.mjs | 404 ++++++++++ .planning/v1427-fix-plan.md | 526 ++++++++++++ .planning/v1427-handoff-session-2.md | 146 ++++ .planning/v1427-mobile-fix-plan.md | 682 ++++++++++++++++ .planning/v1427-mobile-plan.md | 173 ++++ .planning/v1427-plan.md | 117 +++ .planning/v1428-backlog.md | 138 ++++ .planning/v15-apns-key-handling.md | 97 +++ .../22-offline-first-architecture.md | 246 ++++++ .../22-standalone-and-server-pairing.md | 256 ++++++ CHANGELOG.md | 457 +++++++++++ Dockerfile | 15 + assets/geolite2/README.md | 20 + docs/apple-store-connect-checklist.md | 160 ++++ docs/audit/v1425-summary.md | 463 +++++++++++ docs/audit/v1427-summary.md | 109 +++ e2e/insights-card-preview.spec.ts | 170 ---- e2e/settings-mobile-consistency.spec.ts | 52 +- e2e/v1427-admin-login-overview.spec.ts | 115 +++ e2e/v1427-coach-launch.spec.ts | 189 +++++ e2e/v1427-glp1-tile.spec.ts | 205 +++++ e2e/v1427-insights-empty-state.spec.ts | 155 ++++ e2e/v1427-measurements-add-param.spec.ts | 91 +++ e2e/v1427-public-pages.spec.ts | 123 +++ e2e/v1427-responsive-sheet.spec.ts | 92 +++ messages/de.json | 269 ++----- messages/en.json | 269 ++----- messages/es.json | 293 +++---- messages/fr.json | 293 +++---- messages/it.json | 293 +++---- messages/pl.json | 293 +++---- package.json | 3 +- pnpm-lock.yaml | 9 + .../0061_audit_log_carrier/migration.sql | 20 + prisma/schema.prisma | 7 + scripts/fetch-geolite2.sh | 117 +++ src/__tests__/i18n-drift-guard.test.ts | 194 +++++ src/app/__tests__/insights-polish.test.ts | 35 +- src/app/about/page.tsx | 189 +++++ src/app/achievements/page.tsx | 2 +- src/app/api/admin/audit-log/route.ts | 5 + .../notifications/reminder-check/route.ts | 34 +- src/app/api/admin/notifications/test/route.ts | 27 +- src/app/api/audit-log/route.ts | 46 -- .../insights/generate/__tests__/route.test.ts | 1 - .../deploy-webhook/__tests__/route.test.ts | 44 +- src/app/api/internal/deploy-webhook/route.ts | 32 +- .../__tests__/telegram-test-locale.test.ts | 202 +++++ src/app/api/settings/telegram/test/route.ts | 20 +- src/app/api/version/__tests__/route.test.ts | 21 + src/app/api/version/route.ts | 24 +- .../__tests__/canonical-dedup.test.ts | 211 +++++ src/app/api/workouts/batch/route.ts | 54 +- src/app/api/workouts/route.ts | 155 ++++ src/app/auth/login/page.tsx | 33 +- src/app/auth/register/page.tsx | 45 +- src/app/bugreport/page.tsx | 2 +- src/app/global-error.tsx | 23 +- src/app/insights/blutdruck/page.tsx | 65 +- src/app/insights/bmi/page.tsx | 63 ++ src/app/insights/gewicht/page.tsx | 56 ++ src/app/insights/layout.tsx | 22 +- src/app/insights/medikamente/page.tsx | 37 +- src/app/insights/page.tsx | 41 +- src/app/insights/puls/page.tsx | 44 + src/app/insights/schlaf/page.tsx | 66 ++ src/app/insights/stimmung/page.tsx | 19 +- src/app/measurements/page.tsx | 106 ++- src/app/medications/[id]/history/page.tsx | 2 +- src/app/medications/page.tsx | 37 +- src/app/mood/page.tsx | 48 +- src/app/not-found.tsx | 48 ++ src/app/notifications/page.tsx | 2 +- src/app/page.tsx | 146 ++-- src/app/privacy/page.tsx | 142 +++- src/app/targets/page.tsx | 2 +- .../app-log-preview-summary-position.test.tsx | 100 +++ .../__tests__/login-overview-csv.test.ts | 135 +++- ...ogin-overview-pagination-position.test.tsx | 125 +++ src/components/admin/_shared.tsx | 60 +- src/components/admin/admin-shell.tsx | 8 +- src/components/admin/ai-quality-section.tsx | 2 +- .../admin/app-log-preview-section.tsx | 110 +-- src/components/admin/backups-section.tsx | 2 +- src/components/admin/bug-report-section.tsx | 2 +- .../admin/coach-feedback-section.tsx | 2 +- src/components/admin/danger-zone-section.tsx | 2 +- .../admin/feedback-inbox-section.tsx | 251 +++--- .../admin/general-settings-section.tsx | 27 +- src/components/admin/glitchtip-section.tsx | 4 +- .../admin/login-overview-section.tsx | 202 +++-- src/components/admin/recent-audit-preview.tsx | 2 +- src/components/admin/reminders-section.tsx | 10 +- .../admin/system-status-section.tsx | 41 + .../admin/system-status-summary.tsx | 24 +- src/components/admin/umami-section.tsx | 4 +- .../admin/user-management-section.tsx | 12 +- .../admin/web-push-vapid-section.tsx | 2 +- src/components/charts/compliance-heatmap.tsx | 171 ++-- src/components/charts/health-chart.tsx | 18 +- .../charts/medication-compliance-chart.tsx | 12 +- src/components/charts/mood-chart.tsx | 35 +- .../charts/scatter-correlation-chart.tsx | 13 +- src/components/charts/trend-card.tsx | 69 +- .../dashboard/__tests__/glp1-tile.test.tsx | 138 +++- src/components/dashboard/glp1-tile.tsx | 245 +++++- src/components/error-details.tsx | 13 +- .../__tests__/maintainership-banner.test.tsx | 27 +- .../__tests__/daily-briefing.test.tsx | 18 +- .../__tests__/health-score-card.test.tsx | 59 +- .../insights/__tests__/hero-strip.test.tsx | 40 +- .../__tests__/insight-status-card.test.tsx | 92 +++ .../insights/__tests__/insights-card.test.tsx | 159 ---- .../__tests__/insights-tab-strip.test.tsx | 117 +++ .../insights/coach-launch-button.tsx | 93 +++ .../coach-drawer-mobile-trays.test.tsx | 30 +- .../__tests__/coach-input.test.tsx | 14 +- .../__tests__/coach-settings-sheet.test.tsx | 8 +- .../coach-panel/coach-drawer-body.tsx | 68 +- .../insights/coach-panel/coach-drawer.tsx | 124 ++- .../insights/coach-panel/coach-input.tsx | 90 ++- .../coach-panel/coach-settings-sheet.tsx | 74 +- .../insights/coach-panel/history-rail.tsx | 12 +- .../insights/coach-panel/message-thread.tsx | 80 +- .../insights/coach-panel/source-chips.tsx | 4 + .../insights/coach-panel/sources-rail.tsx | 20 +- src/components/insights/daily-briefing.tsx | 80 +- src/components/insights/health-score-card.tsx | 63 +- src/components/insights/hero-strip.tsx | 30 +- .../insights/insight-advisor-card.tsx | 16 +- .../insights/insight-status-card.tsx | 70 +- src/components/insights/insights-card.tsx | 118 --- .../insights/insights-layout-shell.tsx | 55 ++ .../insights/insights-tab-strip.tsx | 81 +- .../insights/layout-coach-mount.tsx | 26 + .../insights/recommendation-feedback.tsx | 4 +- .../insights/sleep-stage-stacked-bar.tsx | 6 +- src/components/insights/sub-page-shell.tsx | 36 +- src/components/insights/suggested-prompts.tsx | 2 +- src/components/insights/therapy-timeline.tsx | 7 + src/components/insights/trends-row.tsx | 14 +- .../insights/use-insights-advisor.ts | 6 +- src/components/insights/vo2-max-chart-row.tsx | 9 +- .../insights/weekly-report-view.tsx | 2 +- src/components/layout/auth-shell.tsx | 22 +- src/components/layout/top-bar.tsx | 2 +- .../measurements/measurement-form.tsx | 144 ++-- .../measurements/measurement-list.tsx | 208 +++-- src/components/medications/DrugLevelChart.tsx | 188 +++-- .../ResearchModeAcknowledgmentDialog.tsx | 92 +-- .../medications/SchedulingSection.tsx | 11 +- .../medications/SideEffectsSection.tsx | 78 +- .../medications/TitrationSection.tsx | 14 +- .../ResearchModeAcknowledgmentDialog.test.tsx | 53 +- .../medications/intake-history-list.tsx | 257 +++--- .../medications/inventory-section.tsx | 138 ++-- .../medications/medication-card.tsx | 4 +- .../medications/medication-form.tsx | 327 +++++--- .../medications/phase-config-dialog.tsx | 110 +-- src/components/mood/mood-form.tsx | 126 +-- src/components/mood/mood-list.tsx | 171 ++-- src/components/onboarding/BaselineForm.tsx | 2 +- .../onboarding/getting-started-checklist.tsx | 18 +- .../__tests__/api-section-responsive.test.tsx | 148 ++++ src/components/settings/about-section.tsx | 2 +- src/components/settings/account-section.tsx | 111 +-- src/components/settings/advanced-section.tsx | 2 +- src/components/settings/ai-section.tsx | 61 +- src/components/settings/api-section.tsx | 295 +++++-- .../settings/dashboard-layout-section.tsx | 18 +- src/components/settings/export-section.tsx | 8 +- .../settings/integrations-section.tsx | 87 +- .../settings/notification-status-card.tsx | 6 +- .../settings/notifications-section.tsx | 11 +- src/components/settings/ntfy-card.tsx | 6 +- src/components/settings/password-input.tsx | 36 - src/components/settings/settings-shell.tsx | 8 +- src/components/settings/sources-section.tsx | 147 ++-- src/components/settings/telegram-card.tsx | 16 +- .../settings/test-connection-button.tsx | 2 +- .../settings/thresholds-editor-section.tsx | 54 +- src/components/settings/timezone-picker.tsx | 15 +- src/components/settings/web-push-card.tsx | 6 +- src/components/targets/target-edit-sheet.tsx | 177 ++-- src/components/ui/__tests__/input.test.tsx | 40 + .../ui/__tests__/responsive-sheet.test.tsx | 223 ++++++ src/components/ui/button.tsx | 8 +- src/components/ui/checkbox.tsx | 47 ++ src/components/ui/dialog.tsx | 15 +- src/components/ui/dropdown-menu.tsx | 2 +- src/components/ui/empty-state.tsx | 29 +- src/components/ui/input.tsx | 32 +- src/components/ui/native-select.tsx | 57 ++ src/components/ui/password-input.tsx | 51 ++ src/components/ui/popover.tsx | 55 ++ src/components/ui/responsive-sheet.tsx | 194 +++++ src/components/ui/select.tsx | 2 +- src/components/ui/sheet.tsx | 13 +- src/components/ui/sonner.tsx | 2 +- src/hooks/use-is-mobile.ts | 69 ++ src/lib/__tests__/dashboard-layout.test.ts | 12 +- src/lib/__tests__/geo-asn.test.ts | 267 ++++++ .../__tests__/geo-offline-detection.test.ts | 186 +++++ src/lib/ai/coach/__tests__/snapshot.test.ts | 18 + src/lib/ai/coach/glp1-snapshot.ts | 9 - src/lib/ai/coach/persistence.ts | 7 - src/lib/ai/coach/snapshot.ts | 15 + src/lib/ai/coach/types.ts | 9 +- src/lib/ai/prompts/general-status.ts | 38 - src/lib/ai/prompts/insight-generator.ts | 3 +- src/lib/ai/prompts/safety-contracts.ts | 20 - src/lib/analytics/classifications.ts | 7 - src/lib/analytics/trends.ts | 51 -- src/lib/api-handler.ts | 15 +- src/lib/auth/__tests__/audit.test.ts | 99 ++- src/lib/auth/audit.ts | 40 +- src/lib/charts/constants.ts | 35 + src/lib/dashboard-layout.ts | 18 +- src/lib/format.ts | 4 - src/lib/geo.ts | 298 ++++++- .../__tests__/format-locale-order.test.ts | 69 ++ src/lib/i18n/context.tsx | 36 +- src/lib/i18n/server-translator.ts | 32 +- src/lib/i18n/shared-resolve.ts | 59 ++ .../__tests__/blood-pressure-status.test.ts | 44 + src/lib/insights/__tests__/bmi-status.test.ts | 36 + .../__tests__/coach-launch-context.test.tsx | 71 ++ .../insights/__tests__/general-status.test.ts | 42 + .../medication-compliance-status.test.ts | 62 ++ .../__tests__/metric-availability.test.ts | 171 ++++ .../insights/__tests__/mood-status.test.ts | 34 + .../insights/__tests__/pulse-status.test.ts | 39 + .../insights/__tests__/weight-status.test.ts | 36 + src/lib/insights/blood-pressure-status.ts | 3 +- src/lib/insights/bmi-status.ts | 3 +- src/lib/insights/coach-launch-context.tsx | 103 +++ src/lib/insights/general-status.ts | 8 +- .../insights/medication-compliance-status.ts | 3 +- src/lib/insights/metric-availability.ts | 89 ++ src/lib/insights/mood-status.ts | 3 +- src/lib/insights/prompt.ts | 71 -- src/lib/insights/pulse-status.ts | 3 +- src/lib/insights/sub-page-metric.ts | 3 - src/lib/insights/weight-status.ts | 3 +- .../__tests__/admin-alert.test.ts | 151 +++- src/lib/integrations/status.ts | 74 ++ src/lib/jobs/__tests__/geo-backfill.test.ts | 219 +++++ src/lib/jobs/geo-backfill.ts | 120 +++ src/lib/jobs/worker-status.ts | 5 - src/lib/medications/glp1-knowledge.ts | 25 +- src/lib/medications/scheduling/cadence.ts | 21 +- src/lib/medications/titration/ladder.ts | 18 +- src/lib/mood/labels.ts | 70 ++ .../__tests__/admin-locale.test.ts | 247 ++++++ src/lib/notifications/dispatch-localised.ts | 116 +++ src/lib/notifications/types.ts | 9 - src/lib/query-keys.ts | 10 +- src/lib/rate-limit.ts | 10 - src/lib/validations/measurement.ts | 9 - src/lib/validations/source-priority.ts | 102 +-- .../withings/__tests__/sync-activity.test.ts | 110 ++- src/lib/withings/__tests__/sync-sleep.test.ts | 100 ++- src/lib/withings/sync-activity.ts | 58 +- src/lib/withings/sync-sleep.ts | 42 +- src/proxy.ts | 4 + 314 files changed, 26801 insertions(+), 4419 deletions(-) create mode 100644 .planning/research/v1427-r1-backlog-sweep.md create mode 100644 .planning/research/v1427-r1-coach-insights.md create mode 100644 .planning/research/v1427-r1-dashboard.md create mode 100644 .planning/research/v1427-r1-ios-offline.md create mode 100644 .planning/research/v1427-r1-notifications.md create mode 100644 .planning/research/v1427-r1-settings-admin.md create mode 100644 .planning/research/v1427-r3c-mobile-auth-public.md create mode 100644 .planning/research/v1427-r3c-mobile-coach.md create mode 100644 .planning/research/v1427-r3c-mobile-dashboard.md create mode 100644 .planning/research/v1427-r3c-mobile-insights.md create mode 100644 .planning/research/v1427-r3c-mobile-measurements-workouts.md create mode 100644 .planning/research/v1427-r3c-mobile-medications.md create mode 100644 .planning/research/v1427-r3c-mobile-settings-admin.md create mode 100644 .planning/research/v1427-r4-code-review.md create mode 100644 .planning/research/v1427-r4-dead-code.md create mode 100644 .planning/research/v1427-r4-design.md create mode 100644 .planning/research/v1427-r4-i18n.md create mode 100644 .planning/research/v1427-r4-product-lead.md create mode 100644 .planning/research/v1427-r4-security.md create mode 100644 .planning/research/v1427-r4-senior-dev.md create mode 100644 .planning/research/v1427-r4-simplifier.md create mode 100644 .planning/research/v1427-r4-ui-conformity.md create mode 100644 .planning/round-3-b1-dashboard-report.md create mode 100644 .planning/round-3-b2-settings-report.md create mode 100644 .planning/round-3-b3-geoip-report.md create mode 100644 .planning/round-3-b4-coach-insights-report.md create mode 100644 .planning/round-3-b5-notifications-report.md create mode 100644 .planning/round-3-b6-i18n-report.md create mode 100644 .planning/round-3-b7-symmetry-cleanup-report.md create mode 100644 .planning/round-3-bios-handoff-report.md create mode 100644 .planning/round-3d-mb1-responsive-sheet-report.md create mode 100644 .planning/round-3d-mb2-tap-target-report.md create mode 100644 .planning/round-3d-mb3-input-attrs-report.md create mode 100644 .planning/round-3d-mb4-coach-reachability-report.md create mode 100644 .planning/round-3d-mb5-tables-cardlist-report.md create mode 100644 .planning/round-3d-mb6-regressions-auth-report.md create mode 100644 .planning/round-3d-mb7-polish-residue-report.md create mode 100644 .planning/round-4-reconcile-rc1-ci-i18n-convention-report.md create mode 100644 .planning/round-4-reconcile-rc2-responsivesheet-report.md create mode 100644 .planning/round-4-reconcile-rc3-workouts-ssr-polish-deadcode-report.md create mode 100644 .planning/round-5-blocker-maxmind-license-key.md create mode 100644 .planning/round-5-e2e-gapfill-report.md create mode 100644 .planning/v1425-codex-audit-prompt.md create mode 100644 .planning/v1426-backlog.md create mode 100644 .planning/v1427-b6-dead-key-probe.mjs create mode 100644 .planning/v1427-b6-dead-key-strip.mjs create mode 100644 .planning/v1427-b6-format-keys.mjs create mode 100644 .planning/v1427-b6-new-keys.mjs create mode 100644 .planning/v1427-fix-plan.md create mode 100644 .planning/v1427-handoff-session-2.md create mode 100644 .planning/v1427-mobile-fix-plan.md create mode 100644 .planning/v1427-mobile-plan.md create mode 100644 .planning/v1427-plan.md create mode 100644 .planning/v1428-backlog.md create mode 100644 .planning/v15-apns-key-handling.md create mode 100644 .planning/v15-ios-handoff/22-offline-first-architecture.md create mode 100644 .planning/v15-ios-handoff/22-standalone-and-server-pairing.md create mode 100644 assets/geolite2/README.md create mode 100644 docs/apple-store-connect-checklist.md create mode 100644 docs/audit/v1425-summary.md create mode 100644 docs/audit/v1427-summary.md delete mode 100644 e2e/insights-card-preview.spec.ts create mode 100644 e2e/v1427-admin-login-overview.spec.ts create mode 100644 e2e/v1427-coach-launch.spec.ts create mode 100644 e2e/v1427-glp1-tile.spec.ts create mode 100644 e2e/v1427-insights-empty-state.spec.ts create mode 100644 e2e/v1427-measurements-add-param.spec.ts create mode 100644 e2e/v1427-public-pages.spec.ts create mode 100644 e2e/v1427-responsive-sheet.spec.ts create mode 100644 prisma/migrations/0061_audit_log_carrier/migration.sql create mode 100755 scripts/fetch-geolite2.sh create mode 100644 src/__tests__/i18n-drift-guard.test.ts create mode 100644 src/app/about/page.tsx delete mode 100644 src/app/api/audit-log/route.ts create mode 100644 src/app/api/settings/__tests__/telegram-test-locale.test.ts create mode 100644 src/app/api/workouts/__tests__/canonical-dedup.test.ts create mode 100644 src/app/api/workouts/route.ts create mode 100644 src/app/not-found.tsx create mode 100644 src/components/admin/__tests__/app-log-preview-summary-position.test.tsx create mode 100644 src/components/admin/__tests__/login-overview-pagination-position.test.tsx create mode 100644 src/components/insights/__tests__/insight-status-card.test.tsx delete mode 100644 src/components/insights/__tests__/insights-card.test.tsx create mode 100644 src/components/insights/__tests__/insights-tab-strip.test.tsx create mode 100644 src/components/insights/coach-launch-button.tsx delete mode 100644 src/components/insights/insights-card.tsx create mode 100644 src/components/insights/layout-coach-mount.tsx create mode 100644 src/components/settings/__tests__/api-section-responsive.test.tsx delete mode 100644 src/components/settings/password-input.tsx create mode 100644 src/components/ui/__tests__/responsive-sheet.test.tsx create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/components/ui/native-select.tsx create mode 100644 src/components/ui/password-input.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/responsive-sheet.tsx create mode 100644 src/hooks/use-is-mobile.ts create mode 100644 src/lib/__tests__/geo-asn.test.ts create mode 100644 src/lib/__tests__/geo-offline-detection.test.ts create mode 100644 src/lib/charts/constants.ts create mode 100644 src/lib/i18n/__tests__/format-locale-order.test.ts create mode 100644 src/lib/i18n/shared-resolve.ts create mode 100644 src/lib/insights/__tests__/coach-launch-context.test.tsx create mode 100644 src/lib/insights/__tests__/metric-availability.test.ts create mode 100644 src/lib/insights/coach-launch-context.tsx create mode 100644 src/lib/insights/metric-availability.ts create mode 100644 src/lib/jobs/__tests__/geo-backfill.test.ts create mode 100644 src/lib/jobs/geo-backfill.ts create mode 100644 src/lib/mood/labels.ts create mode 100644 src/lib/notifications/__tests__/admin-locale.test.ts create mode 100644 src/lib/notifications/dispatch-localised.ts diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index c327d1d30..d50242e34 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -98,6 +98,34 @@ jobs: org.opencontainers.image.licenses=AGPL-3.0-only org.opencontainers.image.source=https://github.com/${{ github.repository }} + # v1.4.27 B3 — GeoLite2-City + GeoLite2-ASN databases are too large + # (~80 MB combined) to vendor in git. `scripts/fetch-geolite2.sh` + # downloads both editions into `assets/geolite2/` so the Dockerfile + # COPY at line 78 picks them up. + # + # The build is intentionally resilient to a missing + # `MAXMIND_LICENSE_KEY` secret: when the key is unset we emit a + # `::warning::` line, drop a `.empty` marker into the assets + # directory so the Dockerfile COPY still has a non-empty source, + # and continue cleanly. The runtime resolver in `src/lib/geo.ts` + # detects the marker on first lookup and emits a one-shot + # admin notification — the maintainer hears about the gap from + # the running app rather than from a red CI badge that blocks + # every release. To enable the offline feature, set the secret + # under repo Settings → Secrets and variables → Actions and + # redeploy. + - name: Fetch GeoLite2 databases + env: + MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }} + run: | + if [ -z "$MAXMIND_LICENSE_KEY" ]; then + echo "::warning::MAXMIND_LICENSE_KEY secret is unset. The image will ship without the offline GeoLite2 databases; the runtime resolver continues to fall back to the online ipwho.is provider and will send a one-shot admin notification on first use. To enable the offline tier, set MAXMIND_LICENSE_KEY under repo Settings → Secrets and variables → Actions." + mkdir -p assets/geolite2 + touch assets/geolite2/.empty + exit 0 + fi + ./scripts/fetch-geolite2.sh + # Per-ref + per-platform gha cache scope. Tag builds warm-start # off main's last good layer set for the same platform; tag and # main builds at the same SHA do not contend on writes, and diff --git a/.gitignore b/.gitignore index 7aa41ee3b..e37e7cb9e 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,12 @@ next-env.d.ts # prisma generated /src/generated/prisma +# v1.4.27 B3 — MaxMind GeoLite2 databases are fetched at build time +# via `scripts/fetch-geolite2.sh`. Too large for git; license key +# required. The folder + a `.gitkeep` are tracked so the Dockerfile +# COPY target always exists. +/assets/geolite2/*.mmdb + # AI session artefacts (kept locally, not committed) /docs/superpowers/ .claude/ diff --git a/.planning/research/v1427-r1-backlog-sweep.md b/.planning/research/v1427-r1-backlog-sweep.md new file mode 100644 index 000000000..09aaca786 --- /dev/null +++ b/.planning/research/v1427-r1-backlog-sweep.md @@ -0,0 +1,218 @@ +--- +file: .planning/research/v1427-r1-backlog-sweep.md +purpose: R1.6 — v1.4.26 backlog triage for v1.4.27 inclusion +created: 2026-05-15 +predecessor: .planning/v1426-backlog.md +companion: .planning/phase-W21-reconcile-plan.md (defer matrix lines 220-279) +--- + +# v1.4.27 R1.6 — backlog sweep + +The v1.4.26 release shipped as a small hotfix that contained only the +privacy-policy page. Every backlog item the v1.4.25 marathon parked +under `.planning/v1426-backlog.md` is therefore still open — plus the +17 Medium-deferred entries from the W21 reconcile defer matrix and the +40 Low-deferred entries from the same plan. + +Each entry below was verified against `develop @ c74a0c98`. Items that +were quietly closed by post-tag fixes (`v1.4.25..HEAD` = 47 commits) +are routed to the Rejected section with the resolving commit named. + +## Summary + +- Total items reviewed: **48** (16 from `v1426-backlog.md` after collapsing nested R1-R16/S1-S15 sub-entries, 17 Medium-deferred from W21 reconcile, 15 Low-deferred from W21 reconcile) +- Pulled into v1.4.27: **17** +- Deferred to v1.4.28: **20** +- Rejected as resolved or stale: **11** + +--- + +## Pulled-into-v1.4.27 items + +### Item — P1-1: 414 dead i18n keys cleanup +- Source: `.planning/v1426-backlog.md` line 27 +- Why pull: W9e race risk closed (no concurrent locale-bundle edits planned in v1.4.27 fix-surfaces). Drops ~414 keys × 6 locales = ~2.5k strings from every client bundle. Catches dead-M8 (132 `insights.*Status*` strings, line 264) in the same pass. +- Touch surface: `messages/{de,en,fr,es,it,pl}.json`, locale-integrity test fixtures +- LOC estimate: -2 500 / +0 (deletions only); one-line test threshold update. +- Risk: low. Locale-integrity test fails on one-sided touches; spot-check `classifications.alerts.*` + `targets.*` survives the pass. + +### Item — P1-2: `BASE_SYSTEM_PROMPT` / `INSIGHTS_SYSTEM_PROMPT` removal +- Source: `.planning/v1426-backlog.md` line 30 +- Why pull: zero consumers verified (`grep` returns only locale-suffixed forms `_DE` / `_EN`). The bare symbols + `@deprecated` markers can drop in one commit. +- Touch surface: `src/lib/ai/prompts/base-system.ts`, `src/lib/insights/prompt.ts`, plus the matching test fixtures. +- LOC estimate: -20 / +0. +- Risk: trivial. + +### Item — P1-3: W7d hardening +- Source: `.planning/v1426-backlog.md` line 33 +- Why pull: `safeRequestProp` still catches-all (`src/lib/api-handler.ts:73-87`); two-line narrow plus a `console.warn` on every fallback. Co-locates with the `globals.css` `@source` path fix. +- Touch surface: `src/lib/api-handler.ts`, `src/app/globals.css`. +- LOC estimate: +15 / -8. +- Risk: low. Existing telemetry catches misfires. + +### Item — P1-4: Cat-C stale-comment typo +- Source: `.planning/v1426-backlog.md` line 36 +- Why pull: confirmed still present at `src/app/api/insights/targets/route.ts:807` — stray brace + half-quoted reference to a deleted route. Trivial. +- Touch surface: one file, one line. +- LOC estimate: -1 / +1. +- Risk: nil. + +### Item — P2-3: Workout cross-source dedup +- Source: `.planning/v1426-backlog.md` line 49 +- Why pull: `src/lib/sources/pick-canonical-workout.ts` already exists with a `pickCanonicalWorkout()` companion (verified line 126). Wiring the canonical-picker into the workout read path closes the M-3 TODO without waiting for observable dupes. +- Touch surface: `src/lib/sources/pick-canonical-workout.ts` (already present), the workouts list/read route, regression test for MANUAL + HK dupes. +- LOC estimate: +60 / -10 (wiring + test). +- Risk: low. Test surface bounded. + +### Item — P3-2: Withings Sleep v2 routine — REFRAMED +- Source: `.planning/v1426-backlog.md` line 65 +- Why pull: `syncUserSleep` already shipped (W17b/c). Backlog entry is now stale framing; the actionable remainder is the 403 scope-skip hardening's twin (`syncUserActivity` shipped 2026-05-14 at `fec02aa5`). Cover with parity test cleanup + i18n suffix on the reauth banner. +- Touch surface: `src/lib/withings/sync-sleep.ts` (verify scope-guard parity), parity test. +- LOC estimate: +30 / -5. +- Risk: low. The 403 hotfix already landed. + +### Item — P4-2: Chart-x-axis-tick timezone audit +- Source: `.planning/v1426-backlog.md` line 78 +- Why pull: cosmetic axis-label shift; `sleep-stage-stacked-bar.tsx:117` builds `new Date(y, m-1, d)` in SSR-server tz. Single-pass audit across chart tick helpers. +- Touch surface: `src/components/insights/sleep-stage-stacked-bar.tsx` + 4-6 sibling chart files. +- LOC estimate: +25 / -25 (replacements). +- Risk: low. Visual diff bounded. + +### Item — P4-5: `__testables.WEEKDAY_KEYS` cleanup +- Source: `.planning/v1426-backlog.md` line 87 +- Why pull: confirmed exported, zero consumers (`src/lib/ai/coach/glp1-snapshot.ts:72,415,420`). Trivial. +- Touch surface: one file, two lines. +- LOC estimate: -5. +- Risk: nil. + +### Item — P4-6: `/api/audit-log` surface decision +- Source: `.planning/v1426-backlog.md` line 90 +- Why pull: endpoint exists (`src/app/api/audit-log/route.ts:13`), zero UI consumer. v1.4.27 is the polishing release before iOS — clean dead surfaces before they ship in the OpenAPI spec. +- Touch surface: either the new settings page or delete the route + DTO + test fixture. +- LOC estimate: depending on direction; either side ≤ 80 LOC. +- Risk: low. + +### Item — P4-8: i18n drift-guard for PR + Workout strings +- Source: `.planning/v1426-backlog.md` line 96 +- Why pull: pairs naturally with P1-1 dead-key cleanup. Adds a single drift-guard assertion so newly-introduced PR/Workout strings stay in lockstep across six locales. +- Touch surface: `src/__tests__/i18n-drift-guard.test.ts` (or co-locate with locale-integrity). +- LOC estimate: +40. +- Risk: nil. Test-only. + +### Item — P4-9: Design M7 + L1-L4 polish +- Source: `.planning/v1426-backlog.md` line 99 +- Why pull: bounded polish surface. M7 (medication-form Dialog inline-control sweep), L1 (`motion-reduce:animate-none` consistency), L2 (Health Score disclaimer 10 px borderline), L3 (`
` `aria-controls`), L4 (therapy-timeline `

`). Co-locates with the v1.4.27 finding-13 symmetry audit. +- Touch surface: 6-8 component files. +- LOC estimate: +30 / -10. +- Risk: low. + +### Item — P4-11 S1 / S10: simplifier apply-with-care +- Source: `.planning/v1426-backlog.md` line 105 + 108 +- Why pull: **S1** (`metricPriorityObjectSchema` derive from `SOURCE_PRIORITY_METRIC_KEYS`) bounds a class of future drift. **S10** (shared `allMessages` + `resolveKey` between `lib/i18n/context.tsx` and `lib/i18n/server-translator.ts`) — already verified duplicated identically; extract into a shared file. S3 (reorderLadder) and S12 (useInsightStatus) and S9 (ContributingSource) already landed pre-tag — see Rejected section. +- Touch surface: `src/lib/validations/source-priority.ts`, `src/lib/i18n/{context.tsx,server-translator.ts,shared-resolve.ts}` (new). +- LOC estimate: +60 / -50 (net wash; eliminates 25 LOC of duplication). +- Risk: low. Both surfaces have unit coverage. + +### Item — P6-4: Coach `lastYear` window option +- Source: `.planning/v1426-backlog.md` line 142 +- Why pull: W5 left the slot open; W7b timezone work settled. Enum extension + snapshot mapping is < 80 LOC including tests. +- Touch surface: `src/lib/ai/coach/types.ts`, `src/lib/ai/coach/snapshot.ts`, snapshot test. +- LOC estimate: +60 / -5. +- Risk: low. + +### Item — P6-7: Locale-native date format ordering +- Source: `.planning/v1426-backlog.md` line 151 +- Why pull: trivial. `format.dateShort` / `timeShort` / `dateTime` in `messages/{fr,es,it,pl}.json` swap to native ordering. Per locale: FR/ES/IT `{day}/{month}/{year}`; PL `{day}.{month}.{year}`. +- Touch surface: 4 JSON files, 3 keys each. +- LOC estimate: -12 / +12. +- Risk: nil. + +### Item — P6-11: Mood verbal labels follow-up +- Source: `.planning/v1426-backlog.md` line 163 +- Why pull: small carry-over from v1.4.25 polishing pass; co-locates with the symmetry audit in finding-13. `MOOD_LABEL_KEYS` already shipped (`src/components/mood/mood-list.tsx:75`), follow-up is i18n key audit + chart-label consistency. +- Touch surface: `src/components/charts/mood-chart.tsx`, `src/components/mood/mood-list.tsx`, locale bundles. +- LOC estimate: +20 / -10. +- Risk: low. + +### Item — code-M3 (W21 defer): manual-workout route attach uses serial `findFirst` +- Source: `.planning/phase-W21-reconcile-plan.md` line 241 +- Why pull: contract correctness for the workout-attach route. Even if no UI consumer exists today, the OpenAPI surface ships with v1.4.27 and the route is published as part of the iOS handshake foundation (finding-25). Single `findMany` collapses N round-trips. +- Touch surface: `src/app/api/workouts/[id]/route.ts` (or wherever the attach route lives). +- LOC estimate: +25 / -15. +- Risk: low. + +### Item — simp-M10 / dead-M6: `glp1-pk.ts` unused exports (`shotPhaseAt` et al.) +- Source: `.planning/phase-W21-reconcile-plan.md` line 256, 261 +- Why pull: maintainer decision was the gate. `shotPhaseAt` exists at `src/lib/medications/glp1-pk.ts:303`; the v1.4.27 GLP-1 dashboard tile work (finding-1, drug-level chart as secondary tile) will wire it. Treat as "internalise vs wire" → wire into the dashboard tile per finding-1 + GLP-1 R7 plan. +- Touch surface: `src/components/dashboard/glp1-card.tsx` (or equivalent), `src/lib/medications/glp1-pk.ts`. +- LOC estimate: +30 / -5. +- Risk: medium. Dashboard surface touched by finding-1 in parallel — coordinate via Round 2 file-touch matrix. + +--- + +## Deferred-to-v1.4.28 items + +| Item | Source (`v1426-backlog.md` or W21) | Why defer | Blocked-by | +|---|---|---|---| +| P0-1 OpenAPI drift-gate hard-fail flip | `v1426` line 14 | RESOLVED — see Rejected. | n/a | +| P0-2 Onboarding rebuild (M-L) | `v1426` line 17 | Multi-day effort. Marc-promoted from v1.5 but v1.4.27 is the QoS-pass release, not the onboarding-rebuild release. Park for v1.4.28 (post-iOS-foundation pass). | iOS native client handshake research (finding-25) lands first so onboarding can branch on "iOS-paired" vs "web-only" | +| P0-3 Native FR/ES/IT/PL Coach prompts (M per locale) | `v1426` line 20 | 4× ~500 LOC system prompts each requiring safety-contract re-validation. Out of QoS-pass scope. | translation hand-review (P6-9) ideally lands first so the native bodies do not duplicate translation drift | +| P2-1 Personal-record detection worker (sweep-on-insert + nightly cron) | `v1426` line 43 | Worker file exists at `src/lib/personal-records/pr-detection-worker.ts`; pg-boss queue lives. The remaining ask is sweep-on-insert + cron registration. Discuss-first: cron cadence + back-pressure policy. | finding-25 (iOS handshake) — back-pressure expectations differ once a fleet of native clients writes concurrently | +| P2-2 Workout ingest endpoint | `v1426` line 46 | RESOLVED — see Rejected. | n/a | +| P2-4 iOS-18 long-tail HK identifier mappings (M per wave) | `v1426` line 52 | The `HK_QUANTITY_TYPE_DEFERRED` set ships per-wave; v1.4.27 is the QoS-pass release, not an HK-coverage release. Next wave queued for v1.4.28 or whichever release pairs with the iOS app's first beta. | iOS Swift client release cadence | +| P2-5 VO2 max chart-row card | `v1426` line 55 | RESOLVED — see Rejected. | n/a | +| P3-1 Withings Activity sync routine | `v1426` line 62 | RESOLVED — see Rejected. | n/a | +| P4-1 Lazy-loaded locale JSON bundles (M) | `v1426` line 75 | Code-shape change; touches `src/lib/i18n/context.tsx:15-29` synchronous imports + hydration-flash mitigation. Risk + design pass before v1.4.28. | none, but pairs with onboarding rebuild (P0-2) since onboarding first-paint dominates the perceived hit | +| P4-4 `detectGlp1Plateau` direct test coverage | `v1426` line 84 | RESOLVED — see Rejected. | n/a | +| P4-7 `Measurement.deviceType` enum vs TEXT | `v1426` line 93 | Migration risk. v1.5 cleanup-pass note already in backlog; v1.4.27 keeps zero new migrations unless required. | v1.5 schema-cleanup pass | +| P4-12 simplifier discuss-first (S11/S14/S15) | `v1426` line 113 | All three require explicit maintainer decision (SSR-mismatch guard preserve, single-row fast-path drop, prisma guard removal). Discuss-first; not engineering. | maintainer decision per item | +| P5 GLP-1 component-test polish + `medication_schedules` consumer scrub | `v1426` line 125-126 | Integration tests for plateau-detection + therapy timeline + the `windowStart`/`windowEnd` consumer audit. Larger than QoS-pass touch. | none | +| P6-2 Sentinel parser observability for HealthKit envelopes | `v1426` line 136 | Pairs with PROMPT_VERSION 5.0.0 cut on the P3 schedule; not v1.4.27. | none | +| P6-3 Coolify auto-deploy maintainer toggle | `v1426` line 139 | The `vars.COOLIFY_AUTO_DEPLOY` toggle landed in v1.4.25 W11a (verified in `docker-publish.yml:251`). The remainder is operational (set the GitHub variable + secrets), not engineering. | maintainer action | +| P6-5 Coach prefill on health-score row tap | `v1426` line 145 | Callback plumbing through the hero strip. Health-score work in finding-8 (Health-Score card filling hero column) touches the same component; defer to v1.4.28 to avoid the collision. | finding-8 first | +| P6-6 Per-night sleep-stage stacked column chart (M) | `v1426` line 148 | New server endpoint required. Out of QoS-pass scope. | new analytics endpoint design pass | +| P6-9 Hand-review FR/ES/IT/PL prose on high-traffic surfaces (L per locale) | `v1426` line 157 | Translation-author work; outside engineering scope. Maintainership banner already discloses the structural-coverage state. | translator contributions (W21 i18n-M1) | +| P6-10 Cat-B endpoint triage (17 endpoints) | `v1426` line 160 | dead-M1/M2/M3/M4 below all cover subsets of this. v1.4.27 picks P4-6 (`/api/audit-log`) only; the rest park. | maintainer go/no-go per endpoint | +| simp-M2 (W21): three near-identical `async function advance()` | W21 line 248 | LOC > 50 across three callers; hook-extraction merits its own design pass. The three callers are settings-page sub-components Fix-L already touched at v1.4.25 tag — collision risk re-emerges if re-entered too soon. | design pass first | +| dead-M1/M2/M3/M4: orphan admin + monitoring endpoints | W21 line 257-260 | Five endpoints (`/api/admin/ai-settings`, `/api/admin/backup/test`, `/api/admin/status-overview`, `/api/monitoring/glitchtip/test`, `/api/monitoring/umami/test`) require maintainer decision per endpoint. Group into a v1.4.28 endpoint-triage commit. | maintainer go/no-go memo | +| dead-M5: `glp1-knowledge.ts` unused exports (10 symbols) | W21 line 261 | Module-surface narrowing; co-locates with the M5/M6/M7 cleanup phase. simp-M10/dead-M6 (`glp1-pk.ts`) ships in v1.4.27 via finding-1 wiring; the knowledge-layer narrowing follows. | finding-1 drug-level wiring (R3 of v1.4.27) | +| dead-M7: scheduling cadence + compliance unused type/helper exports | W21 line 263 | Module-surface narrowing; bundle with dead-M5. | none | +| simp-M6, simp-M7 (W21 Low-defer): pairDoses double-sort, escalationDue rename | W21 line 252-253 | Both already routed to v1.4.27 per the W21 reconcile defer matrix, but they are micro-polish / JSDoc-only — defer one more cycle in favour of the v1.4.27 fix-surface budget. Park for v1.4.28. | none | + +--- + +## Rejected-as-resolved items + +| Item | Source | What resolved it | +|---|---|---| +| P0-1 OpenAPI drift-gate hard-fail flip | `v1426` line 14 | Verified flipped in `.github/workflows/security.yml:44` — comment reads "hard-fail (flipped from warn-only in v1.4.25)". The hard-fail is live. Registry coverage completion remains a slow rollout but the gate itself is done. | +| P2-2 Workout ingest endpoint | `v1426` line 46 | Shipped pre-tag at `src/app/api/workouts/batch/route.ts` (`POST /api/workouts/batch`). Full test suite at `src/app/api/workouts/__tests__/batch-create.test.ts`. v1.4.25 W16b. | +| P2-5 VO2 max chart-row card | `v1426` line 55 | Shipped pre-tag at `src/app/insights/puls/page.tsx:65-121` ("v1.4.25 W16a — VO2 max chart-row consumes the same `/api/analytics`"). | +| P3-1 Withings Activity sync routine | `v1426` line 62 | `syncUserActivity()` shipped at `src/lib/withings/sync-activity.ts:192`, plus the 2026-05-14 403-scope-skip hardening (`fec02aa5`). Reconnect banner shipped W5d. The full sync routine is live. | +| P3-3 Withings webhook secret moved out of query string | `v1426` line 68 | Shipped pre-tag via W17a path-segment move (`src/lib/withings/sync.ts:25-47`). The header-form framing in the backlog was stale; path-segment + Fix-J `PATH_SECRET_PATHS` redaction closes the same hole. | +| P4-3 `coach.batch.too_large` errorCode rename | `v1426` line 81 | Verified `src/app/api/measurements/batch/route.ts:140` emits `measurement.batch.too_large` already. | +| P4-4 `detectGlp1Plateau` direct test coverage | `v1426` line 84 | Verified `src/lib/insights/__tests__/glp1-plateau.test.ts` exists with 8+ test cases covering window arithmetic, `meds[0]` pick, threshold comparison, plus security and timezone regressions. | +| P4-10 Pre-existing `hsl(var(--border))` anti-pattern | `v1426` line 102 | Verified zero remaining `hsl(...)` calls in `mood-chart.tsx` + `scatter-correlation-chart.tsx`. Cleaned during the Dracula-token sweep. | +| P4-11 S3 — `reorderLadder()` helper | `v1426` line 106 | Verified at `src/components/settings/sources-section.tsx:183` ("the previous moveSource + moveDeviceType pair drifted apart twice"). Helper extracted and consumed by both movers. | +| P4-11 S9 — shared `ContributingSource` union | `v1426` line 107 | Verified at `src/lib/analytics/health-score.ts:52` as `export type ContributingSource` consumed by `src/app/api/analytics/route.ts` and the dashboard. | +| P4-11 S12 — `useInsightStatus(metricSlug)` hook | `v1426` line 109 | Verified at `src/hooks/use-insight-status.ts`; consumed by 10 insight sub-pages (BMI, Stimmung, Blutdruck, Schlaf, Puls, Gewicht, Medikamente, etc.). | +| P6-1 Pearson incomplete-beta replacement | `v1426` line 132 | Verified at `src/lib/insights/correlations.ts:166-178` ("v1.4.26 P6-1 — replaced the normal-approx fallback with a rigorous regularised-incomplete-beta evaluation"). Shipped at commit `c80e0a08` per the post-v1.4.25 log. | +| P6-8 GitHub translation-feedback issue template | `v1426` line 153 | Verified at `.github/ISSUE_TEMPLATE/translation.yml`; banner link at `src/components/i18n/maintainership-banner.tsx:42` resolves correctly. | +| product-lead-M2 (W21): `User.onboardingGoals` column | W21 line 232 | Rejected from v1.4.27 with no resolution — Marc decision deferred; the storage block was dropped in Fix-L (`code-M4` / `simp-M9`). The column itself was never added. Reframe as part of the v1.4.28 onboarding-rebuild surface (P0-2). | +| product-lead-M3 (W21): W11a multi-arch lowercase fix | W21 line 233 | Operational note, already resolved in Fix-H + W20-rest. The `vars.COOLIFY_AUTO_DEPLOY` toggle is the surfaced switch. No code action remains. | +| product-lead-M4 (W21): Session-1 W12/W13 deliverables | W21 line 234 | Operational, not engineering. Maintainer-driven post-tag list. Closed at v1.4.25 tag. | + +--- + +## Round-2 coordination notes + +1. **finding-1 (GLP-1 drug-level tile)** and **simp-M10 wiring** touch the same dashboard component. Coordinate via Round 2 file-touch matrix; ideally one contributor owns both. +2. **finding-13 (symmetry audit)** overlaps with **P4-9 design polish** and **P6-11 mood verbal labels** on shared component files. Bundle into one fix-surface so the symmetry pass and the polish items land together. +3. **P1-1 dead i18n keys** must run before **P4-8 i18n drift-guard** — drift-guard asserts the new shape, so the cleanup needs to land first to avoid a chicken-and-egg test failure. +4. **simp-H1 readError**, **simp-H6 assertMedicationOwnership**, and the medication-detail wrapper extraction all shipped pre-tag in v1.4.25 W21 reconcile; do NOT re-litigate. +5. **P0-3 native Coach prompts** is deliberately not pulled. The v1.4.27 budget is a QoS-pass, not a prompt-system rewrite. The maintainership banner already discloses the structural-coverage state to FR/ES/IT/PL users. + +## Open uncertainties + +- **P4-6 `/api/audit-log` direction** — maintainer call needed: wire `/settings/audit-log` page (≤ 80 LOC) or delete the route. Either side ships clean; pick before Round 2. +- **P0-3 native prompts** — would benefit from a Round 1.6 follow-up commissioning a translation-side dry-run before engineering touches the system prompt. Out of v1.4.27 by maintainer directive; surfaced here so the v1.4.28 backlog inherits the framing rather than the stale v1426 wording. diff --git a/.planning/research/v1427-r1-coach-insights.md b/.planning/research/v1427-r1-coach-insights.md new file mode 100644 index 000000000..087d4ab12 --- /dev/null +++ b/.planning/research/v1427-r1-coach-insights.md @@ -0,0 +1,342 @@ +--- +file: .planning/research/v1427-r1-coach-insights.md +slot: R1.3 +purpose: v1.4.27 audit — Coach helper chips, textarea hint footer, metric token leak on Insights sub-pages, data-driven hiding of metrics with zero observations +findings_covered: 14, 15, 16, 17, 18, 19 +predecessor_plan: .planning/v1427-plan.md +mode: read-only +target_tag: v1.4.27 +--- + +# R1.3 — Coach and Insights data-driven audit + +Six findings, two concerns: the Coach reply-decoration surface bleeds raw values, and the Insights sub-pages render their metric scaffold regardless of whether a series carries any observations. The `metric:PULSE` leak on `/insights/puls` (plus its five siblings) is a producer-side gap — the per-metric status generators normalize whitespace but never strip the chart tokens the same prompt instructs the model to embed. + +Headline outcome — the `InsightStatusCard` text payload is the single source of the visible regression. Add `stripChartTokens` at the producer (six `*-status.ts` helpers) and at the consumer (`InsightStatusCard`) to close the defence-in-depth gap. The Coach helper row already gates raw values behind `
`; the bug Marc reported is that the row also surfaces a verbose prose preview underneath the assistant bubble — collapse to the disclosure only. + +--- + +## Finding 16 — `metric:PULSE` raw token leak (HIGH severity) + +### Root cause in one paragraph + +The Insights sub-page `` renders the status string at `src/components/insights/insight-status-card.tsx:97` with **no token strip**. The `text` payload comes from `generate*StatusForUser` in `src/lib/insights/{pulse,weight,bmi,mood,blood-pressure,medication-compliance}-status.ts`. Every one of those helpers parses the model reply, runs it through `normalizeSummaryText` (whitespace collapse only — `src/lib/insights/pulse-status.ts:51-53`), then persists it to `auditLog.details` and returns it verbatim. The pulse prompt at `src/lib/ai/prompts/pulse.ts:50` explicitly invites the model to "embed exactly one `metric:PULSE` token in the text to inline the pulse chart" — but the sub-page does not render that chart and never strips the token. The bilingual prompts at `src/lib/ai/prompts/{pulse,blood-pressure,weight,bmi,mood}.ts` all carry the same invitation. The result: every sub-page status card surfaces the literal `metric:PULSE` / `metric:WEIGHT` / `metric:BLOOD_PRESSURE_SYS` substring at the end of the prose. The mother-page `InsightAdvisorCard` already wraps every prose surface in `stripChartTokens` (`src/components/insights/insight-advisor-card.tsx:580/601/629/638`), which is why the leak only shows on the routed sub-pages. + +### Why the canonical regex didn't catch it + +`stripChartTokens` exists at `src/lib/insights/chart-tokens.ts:184` and already covers three permissive patterns (colon-form `metric:`, capitalised-Metric two-word form, orphan enum identifiers). The regex is correct — it just isn't called on the status-card render path. The leak is purely a wiring oversight on the consumer side and the absence of defence-in-depth on the producer side. + +### Proposed fix — two layers + +**Layer 1 — consumer side (`InsightStatusCard`)**: wrap `text` with `stripChartTokens` at the render site. One-line change in `src/components/insights/insight-status-card.tsx`: + +```tsx +import { stripChartTokens } from "@/lib/insights/chart-tokens"; +… +

+ {stripChartTokens(text)} +

+``` + +**Layer 2 — producer side (six `*-status.ts` helpers)**: extend `normalizeSummaryText` to call `stripChartTokens` before whitespace collapse so the cached `auditLog.details.text` payload is already clean. This protects every future consumer of `result.text` (Telegram notifications, future PDF export, mobile JSON consumers). The signature stays a `string → string` pure function; the existing tests at `src/lib/insights/__tests__/*-status.test.ts` will catch any unintended cleave. + +```ts +function normalizeSummaryText(value: string): string { + return stripChartTokens(value).replace(/\s+/g, " ").trim(); +} +``` + +Touch surface (six files, identical 3-line patch): + +- `src/lib/insights/pulse-status.ts` +- `src/lib/insights/weight-status.ts` +- `src/lib/insights/bmi-status.ts` +- `src/lib/insights/mood-status.ts` +- `src/lib/insights/blood-pressure-status.ts` +- `src/lib/insights/medication-compliance-status.ts` + +`src/lib/insights/general-status.ts` should get the same treatment for parity; the mother-page consumer already strips so this is purely additive cache hygiene. + +### Cache-poisoning consideration + +Old cached `auditLog.details.text` rows already carry the leaked token. The Layer-1 consumer wrap means existing caches render clean immediately, no migration needed. New writes after Layer 2 lands will be clean on disk. The pulse-status route returns within 24h-rolled cache windows so the contaminated rows naturally expire — no Prisma migration required. + +### Regression test + +Add `src/components/insights/__tests__/insight-status-card.test.tsx`: + +```ts +it("strips metric: tokens from the rendered prose", () => { + render(); + expect(screen.getByRole("paragraph")).toHaveTextContent("Your pulse is stable."); + expect(screen.queryByText(/metric:PULSE/)).toBeNull(); +}); +``` + +Plus one symmetric helper-level test per status generator confirming the cached `text` field contains no `metric:` substring. + +### Sleep sub-page note + +`/insights/schlaf` renders `` which fetches its own chart data — there is no AI prose path on the sleep sub-page today, so finding 16's "Schlaf" mention does not apply to a status card. If the bug surfaces on `/insights` (mother page) sleep-related advisor copy, that path already strips via `InsightAdvisorCard`. Recommend: confirm the maintainer was referring to the cross-metric advisor mentioning sleep in prose rather than a sleep-specific status card. + +--- + +## Finding 14 — Coach helper chips with literal values + +### Current layout + +`src/components/insights/coach-panel/message-thread.tsx:386-486` composes the assistant bubble out of three stacked surfaces: + +1. The prose bubble (line 411) — `stripChartTokens(content)` already applied, fine. +2. `` (line 427) — the pill row showing metric+window+sample-count. **This is fine** — it's labels-only, no raw values. +3. The evidence disclosure `
` (lines 428-486) — opens to a `