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 `