diff --git a/.env.example b/.env.example index 8e5ab0679..d809bbdee 100644 --- a/.env.example +++ b/.env.example @@ -151,6 +151,27 @@ API_TOKEN_HMAC_KEY="REPLACE-WITH-OUTPUT-OF-openssl-rand-hex-32" # BACKUP_ENCRYPTION_KEY="" # 64-hex-char or base64 32-byte; SEPARATE from ENCRYPTION_KEY +# ----------------------------------------------------------------------------- +# APNs — Apple Push Notification service (v1.4.23+, optional) +# ----------------------------------------------------------------------------- +# All four core APNS_* vars must be set together for APNs to fan out to iOS +# devices. Leaving them unset disables the channel silently — the dispatcher +# falls back to Telegram → ntfy → Web Push as before. +# +# Provision an APNs auth key (.p8) at https://developer.apple.com/account/ +# under Certificates, Identifiers & Profiles → Keys → APNs. +# +# APNS_KEY (preferred for 12-factor / Coolify env-block) and APNS_KEY_FILE +# (filesystem path) are mutually exclusive — pick one. APNS_KEY accepts the +# multi-line PEM body with literal newlines OR `\n` escapes. +# APNS_KEY_ID="" # 10-char Key ID from Apple Developer (e.g. ABCDE12345) +# APNS_TEAM_ID="" # 10-char Team ID from Apple Developer +# APNS_BUNDLE_ID="" # iOS app bundle id (e.g. dev.healthlog.ios) +# APNS_KEY="" # APNs auth key as PEM body (multi-line OK with \n escapes); preferred over APNS_KEY_FILE +# APNS_KEY_FILE="" # Alternate: filesystem path to .p8 file +# APNS_PRODUCTION="" # "true" to force the production gateway; default = mirror Device.apnsEnvironment + + # ----------------------------------------------------------------------------- # Auto-deploy webhook (v1.4.15 — phase C2) # ----------------------------------------------------------------------------- diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 1b73fccc1..e764ff85b 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -159,4 +159,5 @@ jobs: echo "::error::Coolify deploy webhook returned HTTP $status — image is published, but auto-deploy did not fire. Marc: deploy manually via SSH recipe in .planning/phase-8-report.md." exit 1 fi + echo "::notice::Coolify auto-deploy webhook fired at $(date -u +%FT%TZ) for ref ${{ github.ref_name }} (sha ${GITHUB_SHA::7}). If /api/version doesn't reach the new tag within ~60s, see .planning/coolify-auto-deploy-howto.md." echo "Coolify deploy queued — awaiting outgoing notification webhook to /api/internal/deploy-webhook for final status." diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 07f9a1cf1..4ba396512 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -41,6 +41,24 @@ jobs: - name: Unit tests run: pnpm test + # v1.4.23 W4 F5c — OpenAPI spec drift check. + # + # `continue-on-error: true` is intentional for v1.4.23: the + # registry covers only the iOS-touched routes today, and the + # legacy hand-maintained spec at docs/api/openapi-v1422-legacy.yaml + # carries the rest. Once the registry hits ~95% coverage in + # v1.4.24+, flip this to a hard gate by removing + # `continue-on-error`. + - name: OpenAPI spec drift check + continue-on-error: true + run: | + pnpm openapi:generate + if ! git diff --exit-code docs/api/openapi.yaml; then + echo "::warning::OpenAPI spec out of date. Run 'pnpm openapi:generate' locally and commit the changes." + exit 1 + fi + echo "::notice::OpenAPI spec in sync with Zod source schemas." + dependency-audit: name: Dependency Audit runs-on: ubuntu-latest diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 35ce1b64f..d746d64e4 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -1,55 +1,71 @@ -# HealthLog v1.4.22 — roadmap +# HealthLog v1.4.23 — roadmap -Milestone: **v1.4.22 — big polishing release before v1.5 iOS** (kicked off 2026-05-10, post-v1.4.21 patch) -Latest tag at start: v1.4.21 (live in prod, image digest -`sha256:4e818d44702c…`) - -Marc's directive: close ALL known bugs + apply ALL deferred items -from `.planning/v1421-backlog.md` so the iOS app builds on a clean -foundation. Marathon-style execution: research/probe first, then -parallel polish waves, then multi-agent QA, then release. - -| Wave | Goal | State | -| ---- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -| 1 | Research + probe — Playwright PROD probe (BD-Zielbereich 5th attempt, api-tokens 5th attempt, Metrik token leaks, Targets page brainstorm); health-coach prompt research | in flight | -| 2 | Insights surface polish — A1 BD framing, A2 BD-Kachel parity, A3 comparison-toggle global, A4 grid normalisation, A5 Muster rename + tabs above hero, A6 token-leak fix | pending | -| 3 | Coach polish — B1 prompt rewrite, B2 collapsible provenance, B3 Gravatar parity, B4 disclaimer move, B5 settings cog wire-or-remove | pending | -| 4 | Other surfaces + backlog cleanup — C1 Zielwerte upgrade, C2 api-tokens 5th attempt, C3 Coolify auto-deploy, C4 AuthShell flicker, C5 node-26-alpine, D backlog wave | pending | -| G | E2E fix wave (already on `develop`) — 7 stale-selector / layout regressions; release-merge brings them into main with v1.4.22 | done | -| 5 | Multi-agent QA + Product-Lead review | pending | -| 6 | Release v1.4.22 — bump, CHANGELOG, release-merge, tag, GHCR, Coolify deploy (validated by C3), smoke, GH release, docs+landing sync, brief | pending | +Milestone: **v1.4.23 — pre-iOS hygiene + sentinel hardening** (not +yet kicked off) +Latest tag at start: v1.4.22 (live in prod, image digest +`sha256:865154614303…`) Carry-over candidates already captured in -`.planning/v1421-backlog.md` (now serving as the v1.4.22 backlog -shim — the file name is historical): +`.planning/v1422-backlog.md`: -- 22 MED + 16 LOW + 4 simplify-apply-maybe items from the v1.4.20 - Phase-D pass (consolidate Pearson / linear-regression maths layer, - controlled `` prefill prop, transactional - `recordSpend()`, refusal lexicon expansion, …). +- Sentinel parser malformed-enum hardening (Sr-M5, ~30 LOC, + highest signal-to-effort ratio of the deferred MEDs). +- Analytics-route unbounded `findMany` paging. +- Targets-route 7-pass sparkline coalesce. +- `` controlled-prop refactor + (Sr-HIGH-4) — drop the React-key reset weaponisation before + the iOS Coach surface multiplies the pattern's footprint. +- Per-user prompt-tuning surface (the v1.4.22 settings cog was + removed pending this). - Schema drift on `medication_schedules.days_of_week` — either - deploy the column or drop it from `schema.prisma`. -- Split `src/app/insights/page.tsx` (now composes nine surfaces). -- Wire Coolify image-digest auto-deploy hook on apps-01 so the - host-side retag fallback isn't needed. -- Source-comment hygiene sweep (191 maintainer-name references in - `src/`); DE+EN bilingual CHANGELOG entry rewrite. + deploy the column or drop it from `schema.prisma`. Last call + before v1.5 P0. +- OpenAPI spec drift CI guard — pull forward from v1.5 D.5 if + possible. Cheaper than discovering drift via an iOS build + break. +- Pearson p-value normal-approx replacement (Code-MED-03) — + 30-LOC incomplete-beta or raise df ≥ 20 surfacing gate. Fix + before v1.5/v1.6 auto-discovery ships. +- Coach helpful/unhelpful first-week observation — does the new + warm persona land or does the disclosure-open rate stay below + ~30%? If low, tone pull-back before iOS multiplies the + audience. Reserved next strategic milestones: - **v1.5** — iOS app + Apple Health integration + per-metric APNs alerts. Strategic plan at - `.planning/phase-D-v1420-product-lead-review.md`. + `.planning/phase-W5-v1422-product-lead-review.md`. - **v1.6+** — Auto-correlation discovery (FDR-controlled), Coach full-page route at `/insights/coach`, conversation-driven goal setting. --- +## Previous milestone — v1.4.22 (completed 2026-05-10T22:43:50+02:00) + +| Wave | Goal | State | +| ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | +| 1 | Research + probe — Playwright PROD probe (BD-Zielbereich 5th attempt, api-tokens 5th attempt, metric token leaks, Targets page brainstorm); health-coach prompt research | done | +| 2 | Insights surface polish — A1 BD framing, A2 BD-Kachel parity, A3 comparison-toggle global, A4 grid normalisation, A5 Muster rename + tabs above hero, A6 token-leak fix | done | +| 3 | Coach polish — B1 prompt rewrite (PROMPT_VERSION 4.20.2 → 4.22.0), B2 collapsible evidence, B3 Gravatar parity, B4 disclaimer move, B5 settings cog removal | done | +| 4 | Other surfaces + backlog cleanup — C1 Zielwerte sparkline + Δ-vs-last-month, C2 api-tokens 5th attempt, C3 Coolify auto-deploy runbook, C4 AuthShell flicker → proxy.ts, C5 node-26-alpine deferred, D backlog wave (12 items) | done | +| 5 | Multi-agent QA + Product-Lead — code, security, design, senior-dev, simplify, product-lead; 0 CRIT, 7 HIGH all applied inline, ~6 MED applied, rest deferred | done | +| 6 | Release v1.4.22 — bump, CHANGELOG, release-merge, tag, GHCR, host-side retag deploy (Coolify secrets still missing), /api/version=1.4.22, smoke, GH release, docs+landing sync, brief | done | + +Milestone completed 2026-05-10T22:43:50+02:00 — v1.4.22 LIVE in prod. +Release brief: `docs/audit/v1422-summary.md`. +Image digest: +`sha256:865154614303fdc362ee3941776f73ec0f60e1f16112ec272a75cbbe28e2cffb`. +Backlog seeded to `.planning/v1422-backlog.md`. v1.5 strategic plan at +`.planning/phase-W5-v1422-product-lead-review.md`. + +--- + ## Previous patch — v1.4.21 (completed 2026-05-10T17:46+00:00) -| Phase | Goal | State | -| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| Phase | Goal | State | +| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- | | F | Same-day patch on top of v1.4.20 — Daily Briefing regenerate fix, Coach day-level snapshot, duplicate streaming bubble, drawer header, scope picker UI | done | | E | Release v1.4.21 — develop → main release-merge, bump, CHANGELOG, tag, GHCR, host-side retag fallback, /api/version=1.4.21, smoke, GH release, brief | done | @@ -62,21 +78,21 @@ Image digest: ## Previous milestone — v1.4.20 (completed 2026-05-10T16:49:25Z) -| Phase | Goal | State | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| F0 | Bootstrap — commit dangling v1.4.19 reports, scaffold STATE+ROADMAP | done | -| F1 | Branch model — long-lived `develop` from `main` HEAD; GHCR builds on `main` + `v*` only | done | -| F2 | Document branch + release model — CONTRIBUTING.md + docs site mirror page | done | +| Phase | Goal | State | +| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| F0 | Bootstrap — commit dangling v1.4.19 reports, scaffold STATE+ROADMAP | done | +| F1 | Branch model — long-lived `develop` from `main` HEAD; GHCR builds on `main` + `v*` only | done | +| F2 | Document branch + release model — CONTRIBUTING.md + docs site mirror page | done | | FX | User-facing artifact cleanup — PII (no real names, no health figures), internal jargon, German leaks; CHANGELOG + docs/audit + GH releases + docs + landing | done | -| F5 | Best-practice GitHub repo audit — CODE_OF_CONDUCT.md, issue + PR templates, dependabot expansion, package.json metadata | done | -| F6 | Multi-agent QA on new docs — 38 docs pages cross-checked vs deployed state, CRIT + HIGH inline | done | -| B1 | Hero strip + Daily Briefing + Suggested-prompts | done | -| B2 | AI Coach drawer + SSE streaming + encrypted persistence | done | -| B3 | Correlation discovery + Trends row with AI annotations | done | -| B4 | Weekly Report + Storyboard markers + Mobile passes | done | -| B5 | Personal Health Score (composite 0–100, 3 bands) | done | -| D | Multi-agent QA — code, security, design, senior-dev, simplify, product-lead; 13 HIGH + 6 MED + 5 simplify-yes inline; 22 MED + 16 LOW deferred | done | -| E | Release v1.4.20 — develop → main release-merge, bump, CHANGELOG, tag, GHCR, host-side retag deploy fallback, /api/version=1.4.20, smoke, docs+landing sync | done | +| F5 | Best-practice GitHub repo audit — CODE_OF_CONDUCT.md, issue + PR templates, dependabot expansion, package.json metadata | done | +| F6 | Multi-agent QA on new docs — 38 docs pages cross-checked vs deployed state, CRIT + HIGH inline | done | +| B1 | Hero strip + Daily Briefing + Suggested-prompts | done | +| B2 | AI Coach drawer + SSE streaming + encrypted persistence | done | +| B3 | Correlation discovery + Trends row with AI annotations | done | +| B4 | Weekly Report + Storyboard markers + Mobile passes | done | +| B5 | Personal Health Score (composite 0–100, 3 bands) | done | +| D | Multi-agent QA — code, security, design, senior-dev, simplify, product-lead; 13 HIGH + 6 MED + 5 simplify-yes inline; 22 MED + 16 LOW deferred | done | +| E | Release v1.4.20 — develop → main release-merge, bump, CHANGELOG, tag, GHCR, host-side retag deploy fallback, /api/version=1.4.20, smoke, docs+landing sync | done | Milestone completed 2026-05-10T16:49:25Z — v1.4.20 LIVE in prod. Release brief: `docs/audit/v1420-summary.md`. Backlog seeded to @@ -87,20 +103,20 @@ Release brief: `docs/audit/v1420-summary.md`. Backlog seeded to ## Previous milestone — v1.4.19 (completed 2026-05-10T12:39:59Z) -| Phase | Goal | State | -| ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | -| 0 | Bootstrap — STATE+ROADMAP for v1.4.19 | done | -| A1 | BD-Zielbereich constant 50% (4th attempt) — live-DB root-cause + integration test | done | -| A2 | Charts mobile audit — universal X-tick density helper, mobile-first stacked headers across HealthChart / MoodChart / MedicationComplianceChart | done | -| A3 | `/insights` polish — Comparison-toggle relocated, single page-level refresh, BP/Weight tile strip removed, raw-token leak guarded | done | -| A4 | AI prompt — GROUND RULE 7 forbids default-positivity opener, PROMPT_VERSION 4.16.1 → 4.19.0 | done | -| A5 | Settings/Integrations status-UI — `` consolidates Withings + Mood Log, divider parity, locale-aware relative-time | done | -| A6 | Settings mobile audit — input heights equalised at 36 px, action buttons standardised, Sprache select hoisted to its own row | done | -| A7 | Admin polish — feedback tab strip scrollbar, api-tokens 4th-attempt truncate+tooltip, Einklappen toggle removed, Zielwerte i18n | done | -| A8 | Quality-of-life audit — 78 findings prioritised CRITICAL / HIGH / MED / LOW | done | -| B | Apply A8 — 6/6 CRITICAL + 21/25 HIGH inline; 31 MED + 16 LOW carried to v1.4.20 backlog | done | -| D | Multi-agent QA (5 reviewers) + Product-Lead → v1.5 redesign plan filed | done | -| E1-E3 | Release v1.4.19 — tag, GHCR build green, host-side retag deploy via fallback (Coolify deploy hung, retried via SSH), GH release, docs+landing sync | done | +| Phase | Goal | State | +| ----- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ----- | +| 0 | Bootstrap — STATE+ROADMAP for v1.4.19 | done | +| A1 | BD-Zielbereich constant 50% (4th attempt) — live-DB root-cause + integration test | done | +| A2 | Charts mobile audit — universal X-tick density helper, mobile-first stacked headers across HealthChart / MoodChart / MedicationComplianceChart | done | +| A3 | `/insights` polish — Comparison-toggle relocated, single page-level refresh, BP/Weight tile strip removed, raw-token leak guarded | done | +| A4 | AI prompt — GROUND RULE 7 forbids default-positivity opener, PROMPT_VERSION 4.16.1 → 4.19.0 | done | +| A5 | Settings/Integrations status-UI — `` consolidates Withings + Mood Log, divider parity, locale-aware relative-time | done | +| A6 | Settings mobile audit — input heights equalised at 36 px, action buttons standardised, Sprache select hoisted to its own row | done | +| A7 | Admin polish — feedback tab strip scrollbar, api-tokens 4th-attempt truncate+tooltip, Einklappen toggle removed, Zielwerte i18n | done | +| A8 | Quality-of-life audit — 78 findings prioritised CRITICAL / HIGH / MED / LOW | done | +| B | Apply A8 — 6/6 CRITICAL + 21/25 HIGH inline; 31 MED + 16 LOW carried to v1.4.20 backlog | done | +| D | Multi-agent QA (5 reviewers) + Product-Lead → v1.5 redesign plan filed | done | +| E1-E3 | Release v1.4.19 — tag, GHCR build green, host-side retag deploy via fallback (Coolify deploy hung, retried via SSH), GH release, docs+landing sync | done | Milestone completed 2026-05-10T12:39:59Z — v1.4.19 LIVE in prod. Release brief: `docs/audit/v1419-summary.md`. Backlog seeded to @@ -111,20 +127,21 @@ Release brief: `docs/audit/v1419-summary.md`. Backlog seeded to ## Previous milestone — v1.4.18 (completed 2026-05-10T11:45+02:00) -| Phase | Goal | State | -| ----- | ---------------------------------------------------------------------------------------------------------- | ----- | -| 0 | Bootstrap — STATE+ROADMAP for v1.4.18 | done | -| A1 | BD-Zielbereich tile — 7T/30T sub-values render real numbers (currently "—" even with data) | done | +| Phase | Goal | State | +| ----- | ------------------------------------------------------------------------------------------------------------ | ----- | +| 0 | Bootstrap — STATE+ROADMAP for v1.4.18 | done | +| A1 | BD-Zielbereich tile — 7T/30T sub-values render real numbers (currently "—" even with data) | done | | A2 | `/admin/api-tokens` table scrollbar (3rd attempt) — Playwright live-verify against prod, fix actual offender | done | -| A3 | Chart visual revert + per-chart toggles — drop gradient/emoji/auto-mean, ship 3 opt-in overlay toggles | done | -| B1 | Achievements expansion — research + 15-25 new, hidden Easter-eggs, lock filter, hidden cards | done | -| D | Multi-agent QA + Product-Lead — code-reviewer, security, design, senior, simplify, product | done | -| E | Release v1.4.18 — bump, CHANGELOG, tag, GHCR, deploy, /api/version=1.4.18, smoke, docs+landing sync | done | +| A3 | Chart visual revert + per-chart toggles — drop gradient/emoji/auto-mean, ship 3 opt-in overlay toggles | done | +| B1 | Achievements expansion — research + 15-25 new, hidden Easter-eggs, lock filter, hidden cards | done | +| D | Multi-agent QA + Product-Lead — code-reviewer, security, design, senior, simplify, product | done | +| E | Release v1.4.18 — bump, CHANGELOG, tag, GHCR, deploy, /api/version=1.4.18, smoke, docs+landing sync | done | Marathon completed 2026-05-10T11:45+02:00 — v1.4.18 LIVE in prod. Full report: `docs/audit/v1418-summary.md`. Backlog seeded to `.planning/v1419-backlog.md` (tactical) and `.planning/v15-backlog.md` -+ `.planning/phase-D-v1418-product-lead-review.md` (strategic). + +- `.planning/phase-D-v1418-product-lead-review.md` (strategic). --- diff --git a/.planning/STATE.md b/.planning/STATE.md index fdd40589a..75ffb8a0a 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -1,198 +1,236 @@ -# v1.4.22 marathon — state log +# v1.4.23 marathon — state log -Status: Wave 5 — multi-agent QA + reconcile complete -Last update: 2026-05-10T22:30+02:00 +Status: Wave 5 shipped (7 hygiene items + 8 atomic commits) +Last update: 2026-05-11T02:00+02:00 -> Previous patch: v1.4.21 live (image digest -> `sha256:4e818d44702c…`, `/api/version=1.4.21`). -> v1.4.22 = big polishing release before v1.5 iOS. Closes ALL known -> bugs + applies ALL deferred items from `.planning/v1421-backlog.md` -> so the iOS app builds on a clean foundation. +> Previous milestone: v1.4.22 live (image digest +> `sha256:865154614303…`, `/api/version=1.4.22`). +> v1.4.23 = pre-iOS prep + hygiene. Backend foundation so the iOS +> Health app can ship in v1.5 without contract churn, plus 7 +> hygiene items from `.planning/v1422-backlog.md`. -## Wave 1 — Research + probe (parallel) +## Wave 1 — Research (single agent, three streams) -Per `feedback_research_before_complex_features.md`, deep-fix work on -4-attempt bugs (BD-Zielbereich, api-tokens scrollbar) + Coach prompt -rewrite needs research first. +- [ ] Apple Health `HKQuantityTypeIdentifier` → `MeasurementType` + mapping, with units + canonical aggregation rules +- [ ] APNs Node library decision (`apn` vs `@parse/node-apn` vs + raw HTTP/2) with maintenance / type-safety / footprint pros/cons +- [ ] OpenAPI 3.1 generator tooling for Next.js routes + (`@asteasolutions/zod-to-openapi`, `ts-rest`, `next-rest`, + hand-rolled) +- Detailed report: `.planning/phase-W1-v1423-research.md` -### W1a — Playwright PROD probe + token-leak hunt +## Wave 2 — Apple Health foundation (F1+F2+F3) -- [ ] BD-Zielbereich tile rendering against PROD with maintainer - session (4-attempt bug pattern: probe before patching) -- [ ] Admin / api-tokens scrollbar at multiple viewports (5th attempt) -- [ ] Raw `Metrik ` token leaks in Recommendations / Findings -- [ ] Targets / Zielwerte page screenshot for upgrade brainstorm -- [ ] Inventory `.planning/v1421-backlog.md` items by complexity -- Detailed report: `.planning/phase-W1a-report.md` +### F1 — `MeasurementType` enum extension -### W1b — Health-coach prompt research +- [x] Added `HEART_RATE_VARIABILITY`, `RESTING_HEART_RATE`, + `ACTIVE_ENERGY_BURNED`, `FLIGHTS_CLIMBED`, + `WALKING_RUNNING_DISTANCE`, `VO2_MAX`, `BODY_TEMPERATURE` + (W1 recommended reusing `ACTIVITY_STEPS` + `SLEEP_DURATION` + verbatim; the iOS DTO already shipped `BODY_TEMPERATURE` so + that joined the additive set — net new = 7) +- [x] Prisma migration `0036_apple_health_measurement_types` +- [x] Sleep unit shifted from hours to minutes; new `SleepStage` + enum + nullable `Measurement.sleepStage` column (CHECK + constraint scopes it to SLEEP_DURATION rows) +- [x] Composite unique index `(user_id, type, source, external_id)` + — Apple Health batch dedup key +- [x] Unit conversions in + `src/lib/measurements/apple-health-mapping.ts` (16 unit tests) -- [ ] Benchmark health-coach / motivational-interviewing prompt - patterns (Apple Health Coaching, Oura, Whoop, …) -- [ ] Define "natural prose first, evidence collapsible" structure - with concrete EN + DE drafts -- [ ] PROMPT_VERSION 4.20.2 → 4.22.0 (significant Coach tone shift) -- Detailed report: `.planning/phase-W1b-coach-prompt-research.md` +### F2 — `MeasurementSource` enum expansion -## Wave 2 — Insights surface polish +- [x] `APPLE_HEALTH` appended to the enum +- [x] UI badge in measurement-list (Dracula-pink chip) + bilingual + `measurements.sourceAppleHealth` keys + admin restore-route + enum guard sync -### A1 — BD-Zielbereich 5th attempt +### F3 — `POST /api/measurements/batch` -- [x] Headline re-anchored to `windows.last30Days?.pct`; all-time - surfaced as `bpInTargetPctAllTime` for the new sub-row -- [x] Integration test pins the route contract -- Detailed report: `.planning/phase-W2-report.md` +- [x] New route at `src/app/api/measurements/batch/route.ts`, + idempotent via `Idempotency-Key` +- [x] Validates batch ≤ 500 entries (returns 422 + `coach.batch.too_large`) +- [x] Dedupes via `(userId, type, source, externalId)` composite +- [x] Returns per-entry status (`inserted` / `duplicate` / + `skipped`) + idempotency-replay-safe +- [x] Integration test (`tests/integration/measurements-batch.test.ts`, + 6 cases including idempotency replay) +- [x] Sleep-stage aggregation in `/api/analytics`: + per-Berlin-day summary + `sleepStages` block + (`tests/integration/analytics-sleep-stages.test.ts`) +- Detailed report: `.planning/phase-W2-v1423-report.md` -### A2 — BD-Kachel feature parity +## Wave 3 — APNs scaffolding (F4) -- [x] Trend arrow + 7-day-trend chip + comparison overlay parity - (synthesised slope from 7d/30d delta); `` extended - with optional `avgAllTime` sub-value +- [x] Library install + lock-file (`@parse/node-apn@8.1.0`, + W1 stream-2 decision) +- [x] `src/lib/notifications/senders/apns.ts` — `sendApnsPush()` + one-shot helper plus `sendViaApns(userId, payload)` dispatcher + entry mirroring the web-push contract +- [x] Env-var contract: `APNS_KEY_ID`, `APNS_TEAM_ID`, + `APNS_BUNDLE_ID`, one of `APNS_KEY` / `APNS_KEY_FILE`, + optional `APNS_PRODUCTION` — documented in `.env.example`, + all-or-none guard with one-shot warning when partially set +- [x] `Device` Prisma model extension via migration + `0037_apns_device_columns`: nullable `apnsToken` + + `apnsEnvironment`, paired CHECK constraint, single-column + index on `apns_token` for the dispatcher fan-out lookup +- [x] Dispatcher wiring: `APNS` joins the cascade as channel-type + 4; cascade order is now deterministic + (APNs → Telegram → ntfy → Web Push) +- [x] Mock provider at `src/lib/notifications/senders/__mocks__/apns.ts` + with queued per-token responses + recorded calls +- [x] Integration test + `tests/integration/apns-dispatch.test.ts` — round-trip, + hard-reject + cleanup, cascade fall-through (3 tests) +- [x] `POST /api/devices` accepts paired `apnsToken` + + `apnsEnvironment`, enforces hex format, applies the + cross-user-hijack guard at the APNs-token layer + (409 `apns_token_owned_by_other_user`) +- Detailed report: `.planning/phase-W3-v1423-report.md` -### A3 — Comparison-Toggle → global Settings +## Wave 4 — OpenAPI + Coach schema + native auth + Coolify -- [x] On-surface `` removed from `/insights`; - Settings → Dashboard already carries the canonical picker -- [x] `User.dashboardWidgetsJson.comparisonBaseline` field already - exists (v1.4.16 phase B8) — every chart still consumes it +### F5 — OpenAPI 3.1 generator + CI gate -### A4 — Insights layout grid normalisation +- [x] Tool wired (W1 decision — `zod-openapi` (samchungy) + `yaml@^2`) +- [x] `pnpm openapi:generate` script emits + `docs/api/openapi.yaml` from Zod schemas +- [x] CI step compares generated vs committed; PR warns on drift + (`continue-on-error: true` — flips to hard-fail in v1.4.24+) +- [x] Legacy hand-maintained spec preserved at + `docs/api/openapi-v1422-legacy.yaml` during the incremental + migration -- [x] Row-fill rule: BP / weight / medications grids collapse to - full-width when only one card has data -- [x] Trends row equal heights via `min-h-[300px]` -- [x] `` drops insufficient-data tiles; zero ok - cards hides the row entirely +### F6 — Coach + Daily Briefing schema slot for new metrics -### A5 — Section heading + tab rename +- [x] `aiInsightResponseSchema.dailyBriefing.keyFindings[].sourceMetric` + enum extends to include the 9 Apple Health categories + (hrv, sleep, resting_hr, steps, active_energy, flights, + distance, vo2_max, body_temp) +- [x] `trendAnnotations` schema mirrors the same additive enum +- [x] `coach/snapshot.ts` queries the new measurement types when + scope toggles them on; web-only accounts pay zero extra SQL +- [x] `CoachProvenance.metrics` + `counts` extended symmetrically +- [x] PROMPT_VERSION 4.22.0 → 4.23.0 with new GROUND RULE 12 + ("treat Apple Health categories as silent when absent") +- [x] EN + DE prompt bodies, OUTPUT FORMAT block, and i18n strings + all updated -- [x] "Muster" → "Zusammenhänge" (DE) / "Patterns" → "Relationships" - (EN). Picked Zusammenhänge over Trends because the row above - already uses Trends. -- [x] `` lifted above the hero strip (sticky - scroll-anchored; tabs visible first) +### F7 — Native API hardening (refresh-token per-device) -### A6 — Raw `Metrik ` token leak fix +- [x] Refresh-token reuse-detection switches from "revoke all" to + per-device-token revocation (legacy null-deviceId tokens fall + back to user-wide revoke — safety hatch) +- [x] New route `GET /api/auth/me/devices` lists active devices + with last-seen, label, channels, isCurrent marker +- [x] New route `DELETE /api/auth/me/devices/[id]` revokes one device + (refresh + access tokens + Device row) +- [x] Alternate `DELETE /api/devices/[id]` for the iOS token-rotation + cleanup path (mirror of the auth/me variant) +- [x] Ownership-boundary 404 on cross-user attempts -- [x] `` text wrapped in `stripChartTokens()` -- [x] DE `componentMood`/`componentBp`/`componentCompliance` renamed - to German nouns; i18n-integrity test pins the contract +### F8 — Coolify auto-deploy fix for real -## Wave 3 — Coach polish +- [x] `COOLIFY_WEBHOOK` + `COOLIFY_TOKEN` GitHub repo secrets + documented (maintainer action; see + `.planning/coolify-auto-deploy-howto.md` for the verbatim + recipe) +- [x] Coolify "Watch image registry for new digests" UI toggle + documented as the load-bearing piece (maintainer action — can't + be flipped from CI) +- [x] Workflow step gains a `::notice::` line so future runs surface + the deploy timestamp + sha without opening the verbose log +- [x] Verification recipe (`curl /api/version | jq .data.version`) + host-side fallback documented inline -### B1 — Prompt rewrite +## Wave 5 — Hygiene (H1-H7) -- [x] Adopt W1b research output: natural prose-first, motivational, - conservative, evidence-grounded -- [x] PROMPT_VERSION 4.22.0; EN + DE -- [x] Coach-specific test suite covering tone + evidence-collapsible - pattern -- Detailed report: `.planning/phase-W3-report.md` +- [x] H1 sentinel parser malformed-enum hardening + (`SentinelParseResult.malformedEntries[]` + per-line typed + reasons; chat route splits `coach.keyvalues.parse_partial` from + the full-block `coach.keyvalues.parse_failed`) +- [x] H2 analytics-route unbounded `findMany` pagination + (cursor-paged 5 000-row chunks via `fetchBpSeriesChunked`, + `analytics.bp_in_target.row_count` wide-event meta for slow- + query attribution; integration test seeds 6 000 rows across a + chunk boundary) +- [x] H3 `` controlled-prop refactor + (`useResettableValue` hook + pure `nextResettableValue` helper; + `key={prefill}` removed from the parent mount) +- [x] H4 per-user prompt-tuning surface (Coach settings cog return) + (`User.coachPrefsJson` migration `0038_coach_prefs`; new + `GET/PUT /api/auth/me/coach-prefs`; settings cog opens a + right-edge ``; system-prompt prepends a per-user + OVERRIDE; snapshot reads prefs BEFORE measurement queries so + `excludeMetrics` filters before the snapshot lands) +- [x] H5 schema drift on `medication_schedules.days_of_week` + (decision: deploy — column is referenced by 9 source files + across 4 user-visible surfaces; migration + `0039_medication_schedule_days_of_week`, NULL = daily) +- [x] H6 Pearson p-value normal-approx replacement + (raised `MIN_PAIRED_N` from 14 → 20 — conservative patch; + rigorous incomplete-beta queued as a v1.4.24 candidate) +- [x] H7 Coach helpful/unhelpful first-week observation view + (polymorphic `RecommendationFeedback.target_type` migration + `0040_recommendation_feedback_target_type`; new POST + `/api/insights/chat/messages/:id/feedback`; aggregator slice + `buildCoachFeedbackBuckets` by (promptVersion, tone, verbosity); + admin section `/admin/coach-feedback` with the H7 i18n bundle + both EN + DE) -### B2 — Collapsible provenance + values +W5 commits (8): 58ae9bc, fa07748, 9413d29, 3f60c81, 0eda1de, 1faee95, +05c7f14, plus this STATE tick. -- [x] Move inline raw values out of prose into a - "Worauf bezieht sich das?" expandable below each assistant - message; `---KEYVALUES---` … `---END---` sentinel stripped - server-side, parsed into `provenance.keyValues` -- [x] Source chips stay visible; values reveal on click +Test deltas: 2191 → 2223 unit (+32). Integration suite added 3 +files: `coach-prefs`, `coach-feedback`, plus the 6000-row chunk +boundary regression in the existing `bp-in-target` test. -### B3 — User avatar = Gravatar (parity) +OpenAPI registry coverage: +3 routes (GET / PUT +`/api/auth/me/coach-prefs`, POST +`/api/insights/chat/messages/{id}/feedback`). -- [x] Same dimensions as Coach avatar -- [x] Reuses existing `gravatarUrl` from `/api/auth/me`; initials - fallback when no Gravatar configured +## Wave 6 — Multi-agent QA + Product-Lead review -### B4 — Disclaimer move +- [x] code-reviewer +- [x] security review +- [x] design / UX review +- [x] senior-dev review +- [x] simplify +- [x] Product Lead — v1.5 P1 (iOS first launch) plan refresh with + concrete file paths now that backend contracts exist +- [x] Reconcile — 1 CRITICAL + all 9 HIGH + 4 of 5 simplify + 3 MED + applied; remainder triaged into `.planning/v1423-backlog.md` -- [x] "Coach replies are generated. Clinical decisions belong with - your doctor." moves from below input → sources rail footer -- [x] "Coach reads only your connected data" caption replaced by the - disclaimer so the source picker stands alone +W6 reconcile commits (11): 5486507, 13977bc, d5202e5, a2dfc5e (Session A +— CRIT + HIGH 1-3); 3ee6dab, dfffe6b, d25e50e (Session B — HIGH 4 + +S-02-S-04 simplify + HIGH 6); deadc73 (Session D — HIGH 5); 650f150 +(Session E — HIGH 7); 1a46bfe (Session F — MED cluster); plus this +STATE tick. -### B5 — Settings cog wire-or-remove +Detailed reconcile report: `.planning/phase-W6-v1423-reconcile-report.md`. +Deferred items: `.planning/v1423-backlog.md` (settings-cog vs +per-message-controls debate, rigorous Pearson incomplete-beta, surplus +MEDs, all LOWs, S-05 simplify, pre-existing `coach-prefs.test.ts` mock +issue, sandbox `git commit` no-op). -- [x] Removed for v1.4.22; per-user prompt-tuning surface deferred - to v1.4.23 (no dead buttons in this release) +Test deltas: W5 ended at 2223 unit. Session A added the `revoke.test.ts` ++ `apns-dispatch` device tests landing at 2227. Session E ended at 2235. +Session F added the forged-X-Device-Id regression — final at 2236 unit +(+45 across the W5+W6 reconcile arc). -## Wave 4 — Other surfaces + backlog cleanup +## Wave 7 — Release v1.4.23 (next) -### C1 — Zielwerte page upgrade - -- [x] 30-day sparkline + Δ-vs-last-month caption per card (Direction - A from W1a §4). Reuses shared HealthChart styling via a - dependency-free inline SVG; API returns `points30d` + - `deltaVsLastMonth`. BMI derives both from the weight series. - -### C2 — Admin / api-tokens scrollbar 5th attempt - -- [x] Live Playwright probe confirmed `whitespace-nowrap` on the date - ``s was the residual culprit (W1a §2). Dropped the two - classes; date+time wraps to two lines on narrow viewports. - -### C3 — Coolify image-digest auto-deploy - -- [x] Workflow appends `?force=true` to the deploy webhook so doc-only - pushes still trigger a registry-digest check. Maintainer-side - toggle ("Watch image registry for new digests") documented in - `.planning/coolify-auto-deploy-howto.md` — one UI flip. - -### C4 — AuthShell post-hydration redirect flicker - -- [x] Redirect lives in `proxy.ts`. Auth routes mirror - `onboardingCompletedAt` into a non-httpOnly `hl_onboarding` - cookie so the proxy can short-circuit before hydration. The - e2e onboarding-flicker spec dropped its workaround. - -### C5 — node-26-alpine bump - -- [x] Dependabot PR #162 closed with reasoning. node:26-alpine ships - without corepack pre-installed; Next.js 16 also still pins - node 22 LTS as the supported minimum. Reopen when the framework - bumps its node-version requirement. - -### D — `.planning/v1421-backlog.md` cleanup wave - -Apply or confirm-defer every item: - -- [x] Phase D reconcile carry-over (22 MED + 16 LOW + 4 simplify-maybe) -- [x] FX carry-overs: - - [x] 191 `Marc` references swept across `src/` (test fixtures kept - as opaque test data per brief) - - [x] DE+EN bilingual CHANGELOG entries (v1.4.14 + v1.4.15) reduced - to English-only - - [x] `CLAUDE.md` renamed to `CONTRIBUTING-AI.md` so the filename - is not AI-vendor-specific; `AGENTS.md` stays for multi-agent - compatibility. `CONTRIBUTING.md` reference updated. - -### E2E fix wave (already on develop) - -- [x] 5 commits on develop from the v1.4.21 follow-up; release-merge - brings them into main when v1.4.22 ships - -## Wave 5 — Multi-agent QA + Product-Lead review - -- [x] code-reviewer (`.planning/phase-W5-v1422-code-review.md` — 0 CRIT, 2 HIGH, 4 MED, 5 LOW) -- [x] security review (`.planning/phase-W5-v1422-security-review.md` — 0 CRIT, 0 HIGH, 2 MED, 4 LOW) -- [x] design / UX review (`.planning/phase-W5-v1422-design-review.md` — 0 CRIT, 3 HIGH, 7 MED, 5 LOW) -- [x] senior-dev review (`.planning/phase-W5-v1422-senior-dev-review.md` — 0 CRIT, 2 HIGH, 5 MED, 4 LOW) -- [x] simplify (`.planning/phase-W5-v1422-simplify-review.md` — 5 apply-yes, 4 apply-maybe, 6 apply-no) -- [x] Product Lead — v1.5 plan refresh based on v1.4.22 state (`.planning/phase-W5-v1422-product-lead-review.md`) -- [x] Reconcile applied CRITICAL + HIGH; defer MED/LOW with reasoning (10 commits; report at `.planning/phase-W5-v1422-reconcile-report.md`; backlog at `.planning/v1422-backlog.md`) - -## Wave 6 — Release v1.4.22 - -- [ ] Pre-release verify (typecheck, lint, test, integration, - format, build) -- [ ] Bump `package.json` 1.4.21 → 1.4.22 + CHANGELOG +- [ ] Pre-release verify +- [ ] Bump `package.json` 1.4.22 → 1.4.23 + CHANGELOG - [ ] Release-merge develop → main -- [ ] Tag + push v1.4.22 +- [ ] Tag + push v1.4.23 - [ ] GHCR build green -- [ ] Coolify deploy (should work without host-side fallback after C3) -- [ ] /api/version=1.4.22 confirmed -- [ ] Production smoke + e2e workflow on main passes (e2e fix wave - from develop is included in the release-merge) +- [ ] Coolify deploy (auto, if F8 works; else host-side fallback) +- [ ] /api/version=1.4.23 confirmed +- [ ] Production smoke + e2e workflow on main passes - [ ] GH release - [ ] Docs site + landing site sync -- [ ] `docs/audit/v1422-summary.md` (release brief) -- [ ] v1.5 plan refreshed for the iOS push +- [ ] `docs/audit/v1423-summary.md` (release brief) +- [ ] v1.5 P1 plan refresh recorded diff --git a/.planning/coolify-auto-deploy-howto.md b/.planning/coolify-auto-deploy-howto.md index d35d7022b..40f987a22 100644 --- a/.planning/coolify-auto-deploy-howto.md +++ b/.planning/coolify-auto-deploy-howto.md @@ -1,65 +1,109 @@ -# Coolify image-digest auto-deploy — minimal maintainer action +# Coolify image-digest auto-deploy — maintainer runbook -Status: maintainer-task — the toggle lives in Coolify's UI; neither the -Coolify MCP nor the GitHub workflow can flip it. +Status: maintainer-task — the toggle lives in Coolify's UI; neither +the Coolify MCP nor the GitHub workflow can flip it. ## The problem -Releases v1.4.19, v1.4.20, and v1.4.21 all required a host-side `docker -tag :latest` + `docker compose up -d --no-deps` fallback after -GHCR pushed the new image. Coolify pulled `:latest` from its local -cache without re-checking the GHCR registry digest, so the freshly -published image never reached production until the maintainer SSH'd -into the host and forced the retag. +Releases v1.4.19, v1.4.20, and v1.4.21 all required a host-side +`docker tag :latest` + `docker compose up -d --no-deps` fallback +after GHCR pushed the new image. Coolify pulled `:latest` from its +local cache without re-checking the GHCR registry digest, so the +freshly published image never reached production until the maintainer +SSH'd into the host and forced the retag. Longer history: `docs/audit/v1416-auto-deploy-fix.md` documents the v1.4.15 partial fix (workflow-side webhook trigger) and why it -half-solved the problem. +half-solved the problem. v1.4.22 commit `b281c06` added the explicit +`?force=true` query parameter to the webhook call so Coolify skips +its image-cache on every workflow-triggered deploy. -## The minimal change +## Repo secrets (GitHub Settings → Secrets → Actions) -In the Coolify UI for the HealthLog application: +Set these once. Both required. -1. Open **Configuration → Source** (or equivalent tab depending on the - Coolify version — the option may also live under "General" or - "Deploy"). -2. Enable the **"Watch image registry for new digests"** / - **"Image-digest auto-deploy"** checkbox. Coolify v4 names it - slightly differently across point-releases; the one that triggers a - pull when the registry digest changes (not when a git push lands) - is the one we want. -3. Save. The next time GHCR pushes a new `:latest` digest, Coolify - will pull and recreate the container without a host-side retag. +1. **`COOLIFY_WEBHOOK`** + - Value: `https:///api/v1/deploy?uuid=&force=false` + - Find: Coolify UI → Application → **Webhooks** tab → "Deploy" URL. + The workflow appends `&force=true` if your stored URL doesn't + already carry a `force=` parameter, so either value is fine. + - The actual values live in repo secrets only. -## Why not solve it in the workflow +2. **`COOLIFY_TOKEN`** + - Value: a Bearer token from Coolify UI → **Keys & Tokens** → + "Create new token" → grant read + deploy scope. + - Treat as a long-lived secret; rotate alongside any host + credential rotation. -Two non-options were ruled out: +If either secret is missing the GitHub Actions step short-circuits +with a `::warning::` line in the workflow log — image is still +published to GHCR, but no Coolify call is made. -- **Push the explicit version tag in `docker-compose.yml`** — would - require a workflow edit on every release to bump `image:` to the new - tag. Worse than the current state (a manual host-side step) because - the release author has to remember the second commit. -- **Pre-deployment `docker pull` hook in the Coolify app config** — - duplicates what the registry-digest auto-deploy already does, but - worse: the pull runs on every deploy trigger (including doc-only - pushes) instead of only when the digest changed. +## Coolify UI toggle (one-time) -The workflow file (`.github/workflows/docker-publish.yml`) already -pings Coolify's webhook with `?force=true` after the GHCR push, so the -trigger half of the contract is in place — the missing half is the -registry-digest check, which is a Coolify-side feature. +Open Coolify UI → Application → **Configuration** tab → +**"Watch image registry for new digests"** → **ON**. + +This is the load-bearing piece. Without the toggle, `:latest` pulls +return the locally-cached digest even when the workflow webhook fires +with `force=true` — Coolify's own pull short-circuits before any +registry round-trip. Flip it once, save, never touch again. + +The toggle's exact wording shifts between Coolify v4 point-releases; +look for "image registry", "digest auto-deploy", or +"auto-update" in the same tab. + +## Pre-deploy data check (v1.4.23 onwards) + +Migration `0036_apple_health_measurement_types` documents a unit +semantics shift for `SLEEP_DURATION` (hours → minutes) and explicitly +relies on **zero** pre-existing rows of that type. Before tagging +v1.4.23 (or any future release that re-applies migration 0036 against +a fresh database): + +``` +psql "$DATABASE_URL" -c "select count(*) from measurements where type = 'SLEEP_DURATION'" +``` + +If the count is non-zero, write a one-shot data-migration multiplying +every existing `SLEEP_DURATION` row's `value` column by 60 (hours → +minutes) BEFORE running `prisma migrate deploy`. Skipping the check +will silently shrink displayed sleep duration by 60× without +rewriting the stored numeric. ## Verification -Push a new release tag. The expected sequence is: +Push a tag `vX.Y.Z` to main → wait for the GHCR build to finish → +wait ~30s → +`curl -s https://healthlog.bombeck.io/api/version | jq .data.version` +should return `X.Y.Z` automatically. If it doesn't: -1. `docker-publish.yml` builds, signs, pushes to GHCR. -2. Coolify webhook fires; Coolify pulls the new digest because - "Watch image registry" is enabled. -3. `https://healthlog.bombeck.io/api/version` returns the new version - string within ~60s of the GHCR push — without SSH. +1. Coolify UI → Application → **Deployments** tab → check the most + recent deploy says "Pulled fresh image" not "Image already up to + date". The latter means the registry-digest toggle isn't on. +2. If "already up to date" is the message, re-flip the toggle. +3. Last resort — host-side retag fallback: + ``` + ssh apps-01 + docker pull ghcr.io/mbombeck/healthlog:vX.Y.Z + docker tag ghcr.io/mbombeck/healthlog:vX.Y.Z ghcr.io/mbombeck/healthlog:latest + cd /path/to/coolify/app && docker compose up -d --no-deps + ``` + (see v1.4.21+ release summaries for the canonical command). -If step 3 still requires a host-side retag, the checkbox is in the -wrong place in the UI for that Coolify version — open the audit doc -above and the v1.5 deferral note for the alternative -(GHCR-tag-promoted-by-workflow path). +## Why not solve it in the workflow + +Two non-options were ruled out earlier: + +- **Push the explicit version tag in `docker-compose.yml`** — would + require a workflow edit on every release. Worse than the current + state (a one-time UI toggle). +- **Pre-deployment `docker pull` hook in the Coolify app config** — + duplicates the registry-digest auto-deploy feature but worse: the + pull runs on every deploy trigger (including doc-only pushes) + instead of only when the digest changed. + +The workflow file (`.github/workflows/docker-publish.yml`) already +pings Coolify's webhook with `?force=true` after the GHCR push, so +the trigger half of the contract is in place — the missing half is +the registry-digest check, which is a Coolify-side feature. diff --git a/.planning/phase-A3-report.md b/.planning/phase-A3-report.md index 6120bfce4..9b5aec3bb 100644 --- a/.planning/phase-A3-report.md +++ b/.planning/phase-A3-report.md @@ -13,15 +13,15 @@ Status: complete · 2026-05-09T13:18+02:00 ## Commits on origin/main -| SHA | Message | Concern | -| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | -| b5e9a95 | (A4 sweep) — `` removal in `src/app/page.tsx` was swept into A4's AI-prompt commit when our edits raced. Effectively covered. | 1 | -| 60a91af | feat(insights): comparison toggle on-surface in hero meta band — adds `metaSlot` prop to ``, mounts toggle inline. | 5 | -| 98a3d10 | refactor(insights): single page-level refresh and drop orphan subtitle — drops `onRegenerate` from the page's `` mount + the `aiOverviewTitle` placeholder. | 2 + 3 | -| 335f288 | refactor(insights): drop duplicate metric tile strip — removes 5-tile grid + dead helpers + `fmt`/`bf` locals. Net `-157` / `+20` lines. | 4 | -| fa91a73 | fix(insights): strip lowercase chart tokens from AI prose — splits `STRIP_TOKEN_REGEX` (`[A-Za-z0-9_]+`) from `PARSE_TOKEN_REGEX` (`[A-Z_]+`). | 6 | -| 5360d3c | test(insights): coverage for v1.4.19 polish changes — commit message landed but the test file was missing because a parallel agent's index race captured only the planning files. | -| f2b21a4 | test(insights): add insights-polish.test.ts (followup to 5360d3c) — actual test file (9 tests), pinning all of the above. | 7 | +| SHA | Message | Concern | +| ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| b5e9a95 | (A4 sweep) — `` removal in `src/app/page.tsx` was swept into A4's AI-prompt commit when our edits raced. Effectively covered. | 1 | +| 60a91af | feat(insights): comparison toggle on-surface in hero meta band — adds `metaSlot` prop to ``, mounts toggle inline. | 5 | +| 98a3d10 | refactor(insights): single page-level refresh and drop orphan subtitle — drops `onRegenerate` from the page's `` mount + the `aiOverviewTitle` placeholder. | 2 + 3 | +| 335f288 | refactor(insights): drop duplicate metric tile strip — removes 5-tile grid + dead helpers + `fmt`/`bf` locals. Net `-157` / `+20` lines. | 4 | +| fa91a73 | fix(insights): strip lowercase chart tokens from AI prose — splits `STRIP_TOKEN_REGEX` (`[A-Za-z0-9_]+`) from `PARSE_TOKEN_REGEX` (`[A-Z_]+`). | 6 | +| 5360d3c | test(insights): coverage for v1.4.19 polish changes — commit message landed but the test file was missing because a parallel agent's index race captured only the planning files. | +| f2b21a4 | test(insights): add insights-polish.test.ts (followup to 5360d3c) — actual test file (9 tests), pinning all of the above. | 7 | ## Verification @@ -40,13 +40,13 @@ Status: complete · 2026-05-09T13:18+02:00 ## File deltas -| File | Lines added | Lines removed | -| ----------------------------------------------------- | ----------- | ------------- | -| `src/app/insights/page.tsx` | +43 | -181 | -| `src/components/insights/insights-page-hero.tsx` | +22 | 0 | -| `src/components/insights/insight-advisor-card.tsx` | +12 | -5 | -| `src/lib/insights/chart-tokens.ts` | +16 | -7 | -| `src/app/__tests__/insights-polish.test.ts` | +140 | 0 | +| File | Lines added | Lines removed | +| -------------------------------------------------- | ----------- | ------------- | +| `src/app/insights/page.tsx` | +43 | -181 | +| `src/components/insights/insights-page-hero.tsx` | +22 | 0 | +| `src/components/insights/insight-advisor-card.tsx` | +12 | -5 | +| `src/lib/insights/chart-tokens.ts` | +16 | -7 | +| `src/app/__tests__/insights-polish.test.ts` | +140 | 0 | Net `-200` lines on the page itself, `+140` lines of guards; `/insights` is significantly leaner without losing any value. diff --git a/.planning/phase-A5-report.md b/.planning/phase-A5-report.md index 4457357ff..d96bf39bf 100644 --- a/.planning/phase-A5-report.md +++ b/.planning/phase-A5-report.md @@ -3,11 +3,11 @@ Completed 2026-05-10 ~13:05 CEST. Three atomic TDD-first commits on `origin/main`: -| # | SHA | What it ships | -| --- | --------- | ------------------------------------------------------------------------------ | -| 1 | `ba0d6b8` | `feat(integrations): IntegrationStatusPill component (single tag, mobile-safe)`| -| 2 | `0dcc91a` | `refactor(integrations): single status pill, drop redundant containers` | -| 3 | `47a8fc7` | `test(integrations): Pixel-5 e2e for consolidated status pill` | +| # | SHA | What it ships | +| --- | --------- | ------------------------------------------------------------------------------- | +| 1 | `ba0d6b8` | `feat(integrations): IntegrationStatusPill component (single tag, mobile-safe)` | +| 2 | `0dcc91a` | `refactor(integrations): single status pill, drop redundant containers` | +| 3 | `47a8fc7` | `test(integrations): Pixel-5 e2e for consolidated status pill` | Commit #2 absorbed Marc's planned commits #2 (Withings) and #3 (Mood Log) — the refactor was inherently atomic because both cards diff --git a/.planning/phase-A6-report.md b/.planning/phase-A6-report.md index ad9170f2f..a56bc57e5 100644 --- a/.planning/phase-A6-report.md +++ b/.planning/phase-A6-report.md @@ -36,11 +36,11 @@ human eye missed. ## What was fixed (5 commits) 1. `957f8e9` — `fix(settings): equalize input heights across all - settings sections`. AI active-provider select `h-10` → `h-9`, AI +settings sections`. AI active-provider select `h-10` → `h-9`, AI add-provider select `h-8` → `h-9`, Dashboard "Compare to" trigger loses `min-h-11` so the default `h-9` applies. 2. `9fda634` — `fix(settings): action-button placement consistent - across sections (right-side desktop, stacked mobile)`. Account → +across sections (right-side desktop, stacked mobile)`. Account → Password, Account → Restart onboarding tour and Dashboard → Reset to defaults all use `flex-col` on `=sm`, so the button stacks below the title on mobile (full-width) and @@ -48,21 +48,21 @@ human eye missed. pulled into the title cluster on the two account cards so each row is a clean `` flex pair. 3. `1075784` — `fix(settings): Sprache select position + width - consistent`. Language lifted out of the dob/language pair into +consistent`. Language lifted out of the dob/language pair into its own row at the bottom of the Profile card. `max-w-xs` on `>=sm` so it doesn't render heavier than other half-width fields. Drive-by: pulled the duplicated native-select class string into a single `NATIVE_SELECT_CLASS` constant so gender + language render identically. 4. `737b533` — `style(settings): uniform vertical spacing across all - sections`. Dashboard layout card `space-y-5` → `space-y-4`. AI +sections`. Dashboard layout card `space-y-5` → `space-y-4`. AI provider config forms `space-y-3` → `space-y-4`. Section roots keep `space-y-6` so distinct cards still read as distinct surfaces. Note: `.planning/STATE.md` got bundled into this commit accidentally via index race with A5; STATE content is correct, no semantic harm. 5. `78f1f3f` — `test(settings): mobile e2e snapshot verifies - consistency`. Pixel-5 Playwright spec +consistency`. Pixel-5 Playwright spec (`e2e/settings-mobile-consistency.spec.ts`) locks in five invariants: every form input on /settings/account is 36 px, neither password nor tour buttons overflow the card right edge, diff --git a/.planning/phase-A7-report.md b/.planning/phase-A7-report.md index cde2aedac..a1bedca64 100644 --- a/.planning/phase-A7-report.md +++ b/.planning/phase-A7-report.md @@ -16,6 +16,7 @@ Commits on `origin/main`: ## What landed ### 1. Feedback tab strip mini-scrollbar — fixed + Marc's "mini Scroll-Button rechts" on the admin feedback inbox tabs came from a CSS overflow-axis coupling: `tabsListVariants` set `overflow-x: auto` without `overflow-y`, so the browser silently @@ -27,6 +28,7 @@ short strips. Added `overflow-y-hidden` (with the matching component test that locks both axes in. ### 2. /admin/api-tokens 4th-attempt scrollbar — fixed + Three previous fixes (column-hide v1.4.15, mobile card-list v1.4.16, admin-shell mobile-strip `no-scrollbar` v1.4.18) cleared the obvious culprits but Marc still saw a residual painted bar. @@ -49,6 +51,7 @@ Added an e2e regression that walks every element inside the mobile card-list and asserts `scrollWidth <= clientWidth + 1` for each. ### 3. /admin/api-tokens "Einklappen" button — removed + Inherited from the v1.4 shared-admin page where 13 sections lived together. On the v1.5 dedicated `/admin/api-tokens` route the toggle hides the only card on the page. Marc called it "sinnlos". @@ -57,10 +60,12 @@ Dropped the `expanded` state + button. The shared `settings.collapse` `` still uses them. ### 4. Zielwerte page whitespace — tightened + Outer wrapper changed from `space-y-8` (32 px) to `space-y-6` (24 px), matching the admin / settings rhythm. ### 5. Zielwerte status labels translated + Added two new i18n key blocks under `targets.*`: - `targets.label.` (11 entries, EN + DE) — used by diff --git a/.planning/phase-B-quality-report.md b/.planning/phase-B-quality-report.md index 17857f2fa..803918f1e 100644 --- a/.planning/phase-B-quality-report.md +++ b/.planning/phase-B-quality-report.md @@ -6,44 +6,44 @@ defer the rest to v1.4.20. ## CRITICAL — 6 / 6 fixed -| Finding | Status | Commit | Notes | -| ------- | ------ | ------ | ----- | -| F-01 `formatTimeWindowRange` mixed-language `bis ... Uhr` | fixed | `180f46c` | Locale param threaded through medication card + form. TDD: 6 cases. | -| F-02 `/admin/login-overview` shows insights events | fixed | `dc75fe8` | Section now sets `filter=auth`; action dropdown filtered to `auth.*`. TDD: URL assertion. | -| F-03 Achievements `Idiot` / `Lazy Boy` titles | fixed | `7a6bc81` | Renamed in EN + DE; banned-word regression suite. | -| F-04 Date inputs render mm/dd/yyyy on DE | fixed | `ff6e184` | `lang={locale}` on every native `` in the app. | -| F-05 Admin "Pick a section from the sidebar" wrong on mobile | fixed | `4feacad` | Dropped sidebar reference in EN + DE welcome subtitle. | -| F-06 Insights legacy CTA verb mismatch | fixed | `d263125` | Legacy-payload button now says "Regenerate" in EN/DE; empty-state "Start analysis" untouched. | +| Finding | Status | Commit | Notes | +| ------------------------------------------------------------ | ------ | --------- | --------------------------------------------------------------------------------------------- | ----------------------------- | +| F-01 `formatTimeWindowRange` mixed-language `bis ... Uhr` | fixed | `180f46c` | Locale param threaded through medication card + form. TDD: 6 cases. | +| F-02 `/admin/login-overview` shows insights events | fixed | `dc75fe8` | Section now sets `filter=auth`; action dropdown filtered to `auth.*`. TDD: URL assertion. | +| F-03 Achievements `Idiot` / `Lazy Boy` titles | fixed | `7a6bc81` | Renamed in EN + DE; banned-word regression suite. | +| F-04 Date inputs render mm/dd/yyyy on DE | fixed | `ff6e184` | `lang={locale}` on every native `` in the app. | +| F-05 Admin "Pick a section from the sidebar" wrong on mobile | fixed | `4feacad` | Dropped sidebar reference in EN + DE welcome subtitle. | +| F-06 Insights legacy CTA verb mismatch | fixed | `d263125` | Legacy-payload button now says "Regenerate" in EN/DE; empty-state "Start analysis" untouched. | ## HIGH — 21 fixed / 4 deferred (target: 15) -| Finding | Status | Commit / reason | -| ------- | ------ | --------------- | -| F-07 thresholds three names | fixed | `1f4d6a2` | -| F-08 page+card title duplicate | fixed (partial) | `70ebe32` (danger-zone, feedback). Other affected pages deferred. | -| F-09 backup type raw enum | fixed | `1ae9b9d` | -| F-10 `WITHINGS` rendered ALL CAPS | fixed | `1ae9b9d` | -| F-11 `→ User` / `→ Admin` no label | fixed | `42b0f11` | -| F-12 mobile user cards no aria-labels | deferred | spot-check showed every icon button already has `aria-label` + `title`. *not-an-issue*; see backlog. | -| F-13 mobile sys/dia same heart icon | fixed | `876e074` | -| F-14 settings/integrations status duplicated | deferred | already shipped in v1.4.19 A5. | -| F-15 ntfy in subtitle, missing on board | fixed | `3f6e670` | -| F-16 "Configured" + "Enabled" double badge | fixed | `876e074` | -| F-17 raw `auth.token.autoissue.native` event-key | fixed | `bc81fd7` | -| F-18 ISO timestamp in token name | fixed | `713b494` | -| F-19 raw `*` permission badge | fixed | `3f6e670` | -| F-20 admin/feedback dupe subtitle | fixed | `3f6e670` | -| F-21 "system-wide" suffix repeated 4× | fixed | `3f6e670` | -| F-22 services subtitle misses API toggle | fixed | `3f6e670` | -| F-23 mood/achievements DE strings | deferred | spot-check showed every label already translated. *not-an-issue*; see backlog. | -| F-24 admin/integrations duplicate toggle label | fixed | `98cd13c` | -| F-25 `Glitchtip` casing | fixed | `3f6e670` | -| F-26 insights regenerate banner+button conflict | deferred | partial overlap with A3 + F-06; per-tile spinner removal owns by v1.4.20 Insights redesign. | -| F-27 awkward achievements subtitle | fixed | `3f6e670` | -| F-28 plural bug "for 1 consecutive days" | fixed | `98cd13c` | -| F-29 jargon "(severity x provider)" | fixed | `3f6e670` | -| F-30 "Inbox zero" empty-state idiom | fixed | `3f6e670` | -| F-31 audit-preview row no link | fixed | `bc81fd7` | +| Finding | Status | Commit / reason | +| ------------------------------------------------ | --------------- | ---------------------------------------------------------------------------------------------------- | +| F-07 thresholds three names | fixed | `1f4d6a2` | +| F-08 page+card title duplicate | fixed (partial) | `70ebe32` (danger-zone, feedback). Other affected pages deferred. | +| F-09 backup type raw enum | fixed | `1ae9b9d` | +| F-10 `WITHINGS` rendered ALL CAPS | fixed | `1ae9b9d` | +| F-11 `→ User` / `→ Admin` no label | fixed | `42b0f11` | +| F-12 mobile user cards no aria-labels | deferred | spot-check showed every icon button already has `aria-label` + `title`. _not-an-issue_; see backlog. | +| F-13 mobile sys/dia same heart icon | fixed | `876e074` | +| F-14 settings/integrations status duplicated | deferred | already shipped in v1.4.19 A5. | +| F-15 ntfy in subtitle, missing on board | fixed | `3f6e670` | +| F-16 "Configured" + "Enabled" double badge | fixed | `876e074` | +| F-17 raw `auth.token.autoissue.native` event-key | fixed | `bc81fd7` | +| F-18 ISO timestamp in token name | fixed | `713b494` | +| F-19 raw `*` permission badge | fixed | `3f6e670` | +| F-20 admin/feedback dupe subtitle | fixed | `3f6e670` | +| F-21 "system-wide" suffix repeated 4× | fixed | `3f6e670` | +| F-22 services subtitle misses API toggle | fixed | `3f6e670` | +| F-23 mood/achievements DE strings | deferred | spot-check showed every label already translated. _not-an-issue_; see backlog. | +| F-24 admin/integrations duplicate toggle label | fixed | `98cd13c` | +| F-25 `Glitchtip` casing | fixed | `3f6e670` | +| F-26 insights regenerate banner+button conflict | deferred | partial overlap with A3 + F-06; per-tile spinner removal owns by v1.4.20 Insights redesign. | +| F-27 awkward achievements subtitle | fixed | `3f6e670` | +| F-28 plural bug "for 1 consecutive days" | fixed | `98cd13c` | +| F-29 jargon "(severity x provider)" | fixed | `3f6e670` | +| F-30 "Inbox zero" empty-state idiom | fixed | `3f6e670` | +| F-31 audit-preview row no link | fixed | `bc81fd7` | ## MED + LOW — 31 + 16 deferred to v1.4.20 diff --git a/.planning/phase-B1-v1420-report.md b/.planning/phase-B1-v1420-report.md index d94aa6e31..0346e0ffc 100644 --- a/.planning/phase-B1-v1420-report.md +++ b/.planning/phase-B1-v1420-report.md @@ -13,28 +13,29 @@ full-width Daily Briefing card, and extend the AI insight pipeline with a `dailyBriefing` payload block (paragraph + 0-5 keyFindings). The 5 prior commits land each sub-deliverable atomically; this report -+ STATE.md tick complete in commit 6. + +- STATE.md tick complete in commit 6. ## What shipped (5 commits, all on `origin/develop`) -| # | SHA | Sub-deliverable | -|---|-----|------| -| 1 | `9873363` | Schema + prompt — `dailyBriefingSchema` (`paragraph`, `keyFindings[0..5]`, tone enum, sourceWindow / sourceMetric enums); PROMPT_VERSION bump 4.19.0 → 4.20.0; GROUND RULE 8 added in EN + DE prompts; new `daily-briefing-schema.test.ts` (+31 specs). | -| 2 | `9713bf1` | `` chip-strip component — 5-chip default ("Try asking" row), lucide `Quote` icon, Dracula purple chip styling, mobile-wrap; EN + DE keys (`insights.suggestedPrompts.*`); +9 specs. | -| 3 | `92f332e` | `` card — narrative paragraph, 0-5 finding rows with tone-coloured left bar (green/orange/cyan), metric-icon prefix, optional delta badge, empty-state CTA, `animate-pulse` skeleton, optional metaSlot; EN + DE keys (`insights.dailyBriefing.*`); +18 specs. | -| 4 | `59e63a0` | `` — locale-aware time-of-day greeting (4 buckets), briefing-paragraph subtitle with fallback, 3-button action row (weekly-report + ask-coach disabled with `title="Coming soon"`, regenerate wired), suggested-prompt strip below; old `` JSDoc-deprecated; new `globals.css` utilities `.hero-gradient`, `.glow-purple`, `pulseDot` keyframes; +22 specs. | -| 5 | `7d29596` | `/insights` page wire-up — `` + `` mounted above the existing Status / Advisor / Recommendations blocks; `useInsightsAdvisorQuery` lifts `dailyBriefing` off the cached payload via `dailyBriefingSchema.safeParse`; CompareToggle migrated from hero meta band into briefing metaSlot; v1.4.19 A3 polish-test loosened from string-count to JSX-block check (load-bearing "advisor card carries no onRegenerate" stays). | +| # | SHA | Sub-deliverable | +| --- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | `9873363` | Schema + prompt — `dailyBriefingSchema` (`paragraph`, `keyFindings[0..5]`, tone enum, sourceWindow / sourceMetric enums); PROMPT_VERSION bump 4.19.0 → 4.20.0; GROUND RULE 8 added in EN + DE prompts; new `daily-briefing-schema.test.ts` (+31 specs). | +| 2 | `9713bf1` | `` chip-strip component — 5-chip default ("Try asking" row), lucide `Quote` icon, Dracula purple chip styling, mobile-wrap; EN + DE keys (`insights.suggestedPrompts.*`); +9 specs. | +| 3 | `92f332e` | `` card — narrative paragraph, 0-5 finding rows with tone-coloured left bar (green/orange/cyan), metric-icon prefix, optional delta badge, empty-state CTA, `animate-pulse` skeleton, optional metaSlot; EN + DE keys (`insights.dailyBriefing.*`); +18 specs. | +| 4 | `59e63a0` | `` — locale-aware time-of-day greeting (4 buckets), briefing-paragraph subtitle with fallback, 3-button action row (weekly-report + ask-coach disabled with `title="Coming soon"`, regenerate wired), suggested-prompt strip below; old `` JSDoc-deprecated; new `globals.css` utilities `.hero-gradient`, `.glow-purple`, `pulseDot` keyframes; +22 specs. | +| 5 | `7d29596` | `/insights` page wire-up — `` + `` mounted above the existing Status / Advisor / Recommendations blocks; `useInsightsAdvisorQuery` lifts `dailyBriefing` off the cached payload via `dailyBriefingSchema.safeParse`; CompareToggle migrated from hero meta band into briefing metaSlot; v1.4.19 A3 polish-test loosened from string-count to JSX-block check (load-bearing "advisor card carries no onRegenerate" stays). | ## Test count -| Snapshot | Files | Tests | -|----|----|----| -| Baseline (start of B1) | 210 | 1672 | -| After commit 1 (schema) | 211 | 1703 (+31) | -| After commit 2 (suggested-prompts) | 212 | 1712 (+9) | -| After commit 3 (daily-briefing) | 213 | 1730 (+18) | -| After commit 4 (hero-strip) | 214 | 1752 (+22) | -| After commit 5 (page wire-up) | 214 | 1753 (+1 polish guard) | +| Snapshot | Files | Tests | +| ---------------------------------- | ----- | ---------------------- | +| Baseline (start of B1) | 210 | 1672 | +| After commit 1 (schema) | 211 | 1703 (+31) | +| After commit 2 (suggested-prompts) | 212 | 1712 (+9) | +| After commit 3 (daily-briefing) | 213 | 1730 (+18) | +| After commit 4 (hero-strip) | 214 | 1752 (+22) | +| After commit 5 (page wire-up) | 214 | 1753 (+1 polish guard) | Net delta: **+4 test files, +81 tests**, all green. `pnpm typecheck` clean across the chain. `pnpm lint` baseline preserved (12 pre- @@ -118,7 +119,7 @@ defer rather than a block — see above for the reasoning. 5. **5 default suggested prompts.** Pinned in i18n (`whyMonday / weightVsPulse / weekVsMonth / tellMyDoctor / - medicationWorking`). The list mirrors the artboard verbatim; if +medicationWorking`). The list mirrors the artboard verbatim; if "What should I tell my doctor?" reads too clinical, flipping the EN value is a one-line edit. @@ -134,6 +135,7 @@ i18n-locale-integrity ✓ EN + DE share the same key shape ## Files added / modified **Added:** + - `src/components/insights/hero-strip.tsx` - `src/components/insights/daily-briefing.tsx` - `src/components/insights/suggested-prompts.tsx` @@ -143,6 +145,7 @@ i18n-locale-integrity ✓ EN + DE share the same key shape - `src/lib/ai/__tests__/daily-briefing-schema.test.ts` **Modified:** + - `src/lib/ai/schema.ts` — `dailyBriefing` block added to response schema - `src/lib/ai/prompts/insight-generator.ts` — PROMPT_VERSION 4.20.0, GROUND RULE 8, JSON-shape examples in EN + DE - `src/components/insights/use-insights-advisor.ts` — `dailyBriefing` lifted off cached payload diff --git a/.planning/phase-B2a-report.md b/.planning/phase-B2a-report.md index aac58f0ce..bf0f5da37 100644 --- a/.planning/phase-B2a-report.md +++ b/.planning/phase-B2a-report.md @@ -11,13 +11,13 @@ endpoints. The drawer UI (B2b) dispatches separately. ## Commit timeline (five atomic commits on `develop`) -| SHA | Subject | -| --- | --- | +| SHA | Subject | +| --------- | --------------------------------------------------------------------- | | `5527001` | `feat(coach): add CoachConversation, CoachMessage, CoachUsage models` | -| `a68557d` | `feat(coach): types, persistence, budget, and refusal helpers` | -| `16f3ece` | `feat(coach): add POST /api/insights/chat SSE streaming endpoint` | -| `3168ba8` | `feat(coach): GET list, GET single, DELETE conversation endpoints` | -| _(this)_ | `docs(planning): tick B2a complete, record phase-B2a report` | +| `a68557d` | `feat(coach): types, persistence, budget, and refusal helpers` | +| `16f3ece` | `feat(coach): add POST /api/insights/chat SSE streaming endpoint` | +| `3168ba8` | `feat(coach): GET list, GET single, DELETE conversation endpoints` | +| _(this)_ | `docs(planning): tick B2a complete, record phase-B2a report` | ## Schema + migration @@ -49,9 +49,9 @@ every Coach row. read helpers, `summariseTitle()` for the rail, cursor-paginated `listConversations()`, `deleteConversation()` with ownership check. - `src/lib/ai/coach/budget.ts` — `MAX_TOKENS_PER_USER_PER_DAY = 25 - 000`. UTC `dateKey` so the meter aligns with the LLM provider's +000`. UTC `dateKey` so the meter aligns with the LLM provider's billing boundary. `enforceBudget()` throws `HttpError(429, - "coach.budget.exceeded")`. `recordSpend()` clamps non-finite token +"coach.budget.exceeded")`. `recordSpend()` clamps non-finite token figures to 0 so a misbehaving provider can't poison the ledger. - `src/lib/ai/coach/refusal.ts` — pattern-based detector for prompt injection (EN + DE wording, jailbreak variants, "reveal your @@ -87,10 +87,10 @@ every Coach row. ## Test coverage -| Suite | Before | After | Δ | -| --- | --- | --- | --- | -| Unit (vitest) | 1753 | 1781 | +28 | -| Integration | 67 | 78 | +11 | +| Suite | Before | After | Δ | +| ------------- | ------ | ----- | --- | +| Unit (vitest) | 1753 | 1781 | +28 | +| Integration | 67 | 78 | +11 | Unit tests cover refusal patterns (13 cases), title summarisation (6), and budget guard semantics (9). Integration tests pin five chat-route @@ -131,7 +131,7 @@ without the encryption key for future analytics. - The "summarise older half" pass is currently a placeholder (the history-window builder injects a synthetic `[summary placeholder - — N earlier turns elided]` line rather than calling a provider to +— N earlier turns elided]` line rather than calling a provider to produce a real summary). Cheap and deterministic; an opt-in real- summarisation pass can land alongside B2b once the drawer UI shows whether users actually push past 20 turns. diff --git a/.planning/phase-B2b-report.md b/.planning/phase-B2b-report.md index 479563073..0efe27f86 100644 --- a/.planning/phase-B2b-report.md +++ b/.planning/phase-B2b-report.md @@ -12,13 +12,13 @@ strip's "Ask the coach" button + suggested-prompt chips. ## Commit timeline (four atomic commits + this report on `develop`) -| SHA | Subject | -| --- | --- | -| `c42c0a5` | `feat(coach): TanStack Query hooks + SSE streaming client` | -| `195448a` | `feat(coach): drawer shell, message thread, source chips` | -| `8dac389` | `feat(coach): composer, history rail, sources rail` | +| SHA | Subject | +| --------- | ----------------------------------------------------------------- | +| `c42c0a5` | `feat(coach): TanStack Query hooks + SSE streaming client` | +| `195448a` | `feat(coach): drawer shell, message thread, source chips` | +| `8dac389` | `feat(coach): composer, history rail, sources rail` | | `e541799` | `feat(insights): wire Coach drawer into /insights via hero strip` | -| _(this)_ | `docs(planning): tick B2b complete, record phase-B2b report` | +| _(this)_ | `docs(planning): tick B2b complete, record phase-B2b report` | ## Streaming implementation @@ -88,10 +88,10 @@ the defaults so callers only need to pass `open` + `onOpenChange` + ## Test coverage -| Suite | Before | After | Δ | -| --- | --- | --- | --- | -| Unit (vitest) | 1781 | 1833 | +52 | -| Integration | 78 | 78 | 0 | +| Suite | Before | After | Δ | +| ------------- | ------ | ----- | --- | +| Unit (vitest) | 1781 | 1833 | +52 | +| Integration | 78 | 78 | 0 | Six new test files cover the streaming parser, every drawer column, the message thread state machine, locale-aware labels, and the @@ -110,8 +110,8 @@ New strings under `insights.coach.*`: - Sources rail: `sourcesTitle`, `sourcesFresh`, `sourcesStale`, `sourcesFooter` - Provenance chip namespaces: `metric.{bp,weight,pulse,mood, - compliance,general}` and `window.{last7days,last30days,last90days, - allTime}` +compliance,general}` and `window.{last7days,last30days,last90days, +allTime}` Both `messages/en.json` and `messages/de.json` updated; `i18n-locale-integrity.test.ts` keeps passing. diff --git a/.planning/phase-B3-report.md b/.planning/phase-B3-report.md index 5eaf38c1a..47a96a2c0 100644 --- a/.planning/phase-B3-report.md +++ b/.planning/phase-B3-report.md @@ -48,7 +48,7 @@ near-zero scatter that would imply a pattern that isn't there. `` wrapper), confidence chip, source-chip ("based on N paired readings · last 30 days"), interpretation prose, disabled "Try a 7-day experiment" CTA with `title="Coming - soon"`. Row mounts the three cards in 2-up grid on `>=md` and +soon"`. Row mounts the three cards in 2-up grid on `>=md` and single column on `` plus a native `title=` attribute. Neither surfaces @@ -117,6 +118,7 @@ Pages audited: truncated api-tokens cells works on touch (long-press on mobile, hover on desktop)". The hover path works; the touch path does not. + - **Recommendation:** On the mobile card-list (`md:hidden`, line 232), drop `truncate` and let long values wrap to two lines — vertical space is cheap on a card and reading is more important @@ -133,7 +135,7 @@ Pages audited: - **Issue:** When the comparison toggle (`metaSlot`) is mounted AND the regenerate button is wired (the live `/insights` config), the outer flex `flex-col gap-4 sm:flex-row sm:items-start - sm:justify-between` puts the regenerate button in a 2nd column on +sm:justify-between` puts the regenerate button in a 2nd column on desktop; on mobile (`` (3 buttons, each `min-h-11 px-3`). @@ -149,6 +151,7 @@ Pages audited: before any card body. Marc's A3 concern was "three stacked cards with separate controls" feel; folding the toggle into the hero removed the standalone band but made the hero itself heavy. + - **Recommendation:** On `>=sm`, inline the CompareToggle next to the Regenerate button (both in the right-side column) instead of inside the title block. On `API Tokens` (renderer line 184) AND `` body renders `
API Tokens
` (section @@ -190,7 +193,7 @@ Pages audited: is 1-column (correct), but on desktop the right cell renders empty without a placeholder — visually fine, semantically odd. - **Recommendation:** Replace the grid with a plain `
` so the DOB input still caps at half-card +class="sm:max-w-md">` so the DOB input still caps at half-card width on desktop. Drops one DOM node + one CSS class. - **Ship-blocker?** No. @@ -273,7 +276,7 @@ Pages audited: - **Severity:** LOW - **File:** `src/components/admin/api-token-overview-section.tsx:135-142, - 213-220` +213-220` - **Issue:** The colgroup widths sum to `18+28+24+10+12+8 = 100 %`. `lastUsedAt` cell is `whitespace-nowrap` rendering `formatDateTime(...)` (e.g. "10.05.2026, 13:18:42" — 19 chars + diff --git a/.planning/phase-D-reconcile-report.md b/.planning/phase-D-reconcile-report.md index 1ef4a344d..a410ffe8a 100644 --- a/.planning/phase-D-reconcile-report.md +++ b/.planning/phase-D-reconcile-report.md @@ -16,18 +16,18 @@ Agent: Phase D RECONCILE ## HIGH triage -| ID | Source | Decision | Commit / Backlog ref | -|---|---|---|---| -| H-01 (code) | UTC clock breaks Berlin convention | **fixed** | `1258b24` | -| H-02 (code) | useViewportWidth setState in effect | **fixed** | `5a8ad3d` | -| H-03 (code) | Orphan icon wrapper after card-title removal | **fixed via simplify F4** | `6b35cad` | -| H-04 (code) | Tabs ring clip on focus-visible | **fixed** | `977f124` | -| H-05 (code) | `/insights` `data?.` post-narrowing | deferred (D-CR-H-05) | v1.4.20 backlog | -| H-01 (design) | api-tokens truncate-tooltip on touch | deferred (D-DSGN-H-01) | v1.4.20 backlog | -| H-02 (design) | Insights hero density on Pixel-5 | deferred (D-DSGN-H-02) | folded into v1.4.20 redesign | -| H-1 (senior-dev) | DateInput wrapper | **fixed via simplify F3** | `6b35cad` | -| H-2 (senior-dev) | useAuthActionLabels lift-out | **fixed via simplify F1** | `6b35cad` | -| H-3 (senior-dev) | Withings/MoodLog card chrome | deferred (D-SR-H-3) | v1.4.20 backlog | +| ID | Source | Decision | Commit / Backlog ref | +| ---------------- | -------------------------------------------- | ------------------------- | ---------------------------- | +| H-01 (code) | UTC clock breaks Berlin convention | **fixed** | `1258b24` | +| H-02 (code) | useViewportWidth setState in effect | **fixed** | `5a8ad3d` | +| H-03 (code) | Orphan icon wrapper after card-title removal | **fixed via simplify F4** | `6b35cad` | +| H-04 (code) | Tabs ring clip on focus-visible | **fixed** | `977f124` | +| H-05 (code) | `/insights` `data?.` post-narrowing | deferred (D-CR-H-05) | v1.4.20 backlog | +| H-01 (design) | api-tokens truncate-tooltip on touch | deferred (D-DSGN-H-01) | v1.4.20 backlog | +| H-02 (design) | Insights hero density on Pixel-5 | deferred (D-DSGN-H-02) | folded into v1.4.20 redesign | +| H-1 (senior-dev) | DateInput wrapper | **fixed via simplify F3** | `6b35cad` | +| H-2 (senior-dev) | useAuthActionLabels lift-out | **fixed via simplify F1** | `6b35cad` | +| H-3 (senior-dev) | Withings/MoodLog card chrome | deferred (D-SR-H-3) | v1.4.20 backlog | Tally: **7 HIGH fixed, 3 HIGH deferred**. diff --git a/.planning/phase-D-security-findings.md b/.planning/phase-D-security-findings.md index 3c993b99b..fc0484898 100644 --- a/.planning/phase-D-security-findings.md +++ b/.planning/phase-D-security-findings.md @@ -7,12 +7,13 @@ Method: Read CLAUDE.md, .planning/STATE.md, full diff via `git log --oneline v1.4.18...HEAD` + targeted `git diff` per file, plus reads of every changed source file in the focus list, plus cross-checks of: - - the prompt revision against the existing server-side n<3 - confidence clamp (`src/lib/ai/confidence.ts`) - - the integration error decryption path (unchanged from v1.4.18) - - the admin tokens API selector (no `tokenHash`) - - new audit-log labels' semantics - - i18n placeholder interpolation (`{count}`, integer-only) + +- the prompt revision against the existing server-side n<3 + confidence clamp (`src/lib/ai/confidence.ts`) +- the integration error decryption path (unchanged from v1.4.18) +- the admin tokens API selector (no `tokenHash`) +- new audit-log labels' semantics +- i18n placeholder interpolation (`{count}`, integer-only) (Previous v1.4.18 review at `phase-D-v1418-security-findings.md` was overwritten — the v1.4.18 release shipped, those findings already @@ -97,11 +98,11 @@ filed in `.planning/v1419-backlog.md` / `.planning/v15-backlog.md`.) - **Severity**: LOW (existing v1.4.18 behaviour, surfaced by A5 refactor) - **File**: `src/components/settings/integrations-section.tsx:332-333, - 354, 630-631, 663` (``) +354, 630-631, 663` (``) - **Issue**: A5 consolidated the status UI but kept the inline actionable error message under the pill. `viewModel.lastError` is decrypted by the API in `src/lib/integrations/status.ts:112, - 376-383` from the `IntegrationStatus.lastError` ciphertext. The +376-383` from the `IntegrationStatus.lastError` ciphertext. The message is bounded to 1024 chars at encrypt time (`status.ts:367`) and originates from controlled callsites in `src/lib/withings/sync.ts` + `src/lib/moodlog/sync.ts` — neither @@ -118,7 +119,7 @@ filed in `.planning/v1419-backlog.md` / `.planning/v15-backlog.md`.) a token, never an Authorization header. Withings/moodLog OAuth error responses do not contain refresh tokens. - Pill viewer is the message owner only — `/api/integrations/ - status` is `requireAuth`-gated and returns the calling user's +status` is `requireAuth`-gated and returns the calling user's own row. - **Recommendation**: None for v1.4.19. If a future v1.5 sync helper passes raw HTTP body into `recordSyncFailure`, that's the @@ -172,6 +173,7 @@ purely Tailwind class swaps on `` / `` callsite carries it (verified via `grep -c - 'type="date(time-local)?"`). Future date inputs added without +'type="date(time-local)?"`). Future date inputs added without the prop will silently regress. - **Recommendation:** Introduce a thin wrapper `src/components/ui/date-input.tsx` (or `localized-date-input.tsx`) that wraps `` + reads `useTranslations()` internally to set `lang`. Replaces every `` with `` and `` with ``. Eliminates the +lang={locale} … />` with ``. Eliminates the 14× duplication and the parent-component `locale` import noise. - **Ship-blocker?** No. The pattern works correctly today; the refactor is hygiene. Recommend follow-up commit before v1.5 @@ -96,14 +96,14 @@ noted but is not v1.4.19's regression. - **File:** `src/components/settings/integrations-section.tsx` (`WithingsCard` lines 199-531, `MoodLogCard` lines 533-831) - **Issue:** A5 successfully introduced `IntegrationStatusPill` for - the status chip, but the *card chrome* itself + the status chip, but the _card chrome_ itself (`
` → header row with icon + title + pill → `

` muted description → `


` → `
` body) is duplicated verbatim between WithingsCard +className="border-border/60 mt-4">` → `
` body) is duplicated verbatim between WithingsCard and MoodLogCard. The Phase A5 report explicitly mentions - *"Reusable for v1.4.20 Apple Health card on iOS"* — but the + _"Reusable for v1.4.20 Apple Health card on iOS"_ — but the reusable surface today is only the pill, not the card. Adding Apple Health will paste the same chrome a third time. - **Recommendation:** Extract `` co-located with @@ -116,9 +116,9 @@ noted but is not v1.4.19's regression. - `errorMessage?: string` - `children` (the unique card body — credentials form, sync button, etc.) - Both existing cards become a thin shell + their unique body - forms. Apple Health card v1.5 ships as ~30 lines of unique - form, not ~200 lines of mostly chrome. + Both existing cards become a thin shell + their unique body + forms. Apple Health card v1.5 ships as ~30 lines of unique + form, not ~200 lines of mostly chrome. - **Ship-blocker?** No. Defer to v1.4.20 or v1.5 (would touch v1.4.19's tested surface, not worth the risk pre-release). @@ -135,7 +135,7 @@ noted but is not v1.4.19's regression. - `src/components/charts/medication-compliance-chart.tsx:269` - **Issue:** A2 introduced an identical mobile-first header layout (`flex flex-col gap-2 sm:flex-row sm:items-center - sm:justify-between`) plus the matching chip-hide rules +sm:justify-between`) plus the matching chip-hide rules (`hidden sm:inline-flex` on bucket chip + comparison chip) in three chart components. The same 4× ` +) : ( + +)} +``` + +**After** (≤8 lines): + +``` +const weeklyLabel = (<>