diff --git a/.planning/phase-W-A-v1438-cross-tz-guard-report.md b/.planning/phase-W-A-v1438-cross-tz-guard-report.md new file mode 100644 index 000000000..59f987753 --- /dev/null +++ b/.planning/phase-W-A-v1438-cross-tz-guard-report.md @@ -0,0 +1,162 @@ +# Phase W-A — Cross-tz fast-path runtime guard + +**Wave**: v1.4.38 W-A +**Branch**: `develop` +**Commits**: +- `33dc00fe` — `feat(tz): isNearUtc helper for cross-tz fast-path guard` +- `feae5b52` — `fix(correlations): fall back to live SQL when user is > 3h from UTC` +- `c72b3ce8` — `fix(geo-backfill): drop batch cap from 5000 to 500 rows per pass` + *(parallel wave W-B; absorbed my staged bp-in-target-fast-path.ts + + analytics/route.ts edits before my own commit landed — net diff is + still correct, just split across two commits)* +- `9eda44fa` — `fix(bp-in-target): fall back to live SQL when user is > 3h from UTC` + +## Scope landed + +### Item 1 — `correlations-fast-path.ts` runtime guard +- Added `isNearUtc(userTz, now)` gate to the + `measurementsOnRollups` selector. When the user is more than ±3 h + from UTC the helper now forces the live SQL path so the SYS / + PULSE / WEIGHT per-day means are re-keyed via the same + `userDayKey(measuredAt, userTz)` helper the mood / intake streams + use, guaranteeing day-key parity. +- Meta annotate gains `correlations.tz_guard: + "near-utc" | "non-utc-live-fallback"`. + +### Item 2 — `bp-in-target-fast-path.ts` runtime guard +- New optional `userTz?: string` field on + `computeBpInTargetFastPath`'s input. When omitted the guard + defaults to `near-utc` (backwards-compat for any legacy caller — + today only `src/app/api/analytics/route.ts`, which has been + updated to thread `userTz` through). +- Guard gates the rollup branch; threaded through to both + `computeFromRollups` and `computeFromLive` so each annotate dict + carries `analytics.bp_in_target.tz_guard`. + +### Helper — `src/lib/tz/format.ts` + `src/lib/tz/resolver.ts` +- New pure `isNearUtc(tz, now = new Date())` helper. Reuses the + existing `tzOffsetMinutes` machinery; honours DST. Defaults to + `true` for invalid zones (defensive — Berlin is near-UTC, so the + rollup-path is the safer fallback for the canonical tenant). +- Re-exported from `resolver.ts` for the server-side import path + the analytics fast-paths already use. + +## Tests delta + +| File | Before | After | Notes | +| --- | --- | --- | --- | +| `src/lib/tz/__tests__/resolver.test.ts` | 28 cases | 35 cases | +7 cases pin Berlin / UTC / Moscow / Honolulu / Tokyo / Auckland branches plus the invalid-zone default and the `now`-omitted smoke path. | +| `src/lib/analytics/__tests__/correlations-fast-path.test.ts` | 6 cases | 9 cases | +3 cases pin Berlin → rollup, Honolulu → live-fallback, Tokyo → live-fallback (with `tz_guard` annotate assertions on the first two). | +| `src/lib/analytics/__tests__/bp-in-target-fast-path.test.ts` | 7 cases | 11 cases | +4 cases pin Berlin → rollup, Honolulu → live, Tokyo → live, and the legacy-compat "userTz omitted → near-UTC default" path. | +| **Touched-file total** | 41 | 55 | +14 new assertions. | +| Full suite | 4490 | 4504 | +14. | + +Quality gates: +- `pnpm typecheck`: clean (silent). +- `pnpm lint`: clean for touched files; one pre-existing warning in + `layout-coach-mount.tsx` unrelated to this wave. +- `pnpm test --run src/lib/tz/__tests__/resolver.test.ts + src/lib/analytics/__tests__/bp-in-target-fast-path.test.ts + src/lib/analytics/__tests__/correlations-fast-path.test.ts`: + 3 files, 54 tests passed (touched files). +- Full suite: 426 files, 4504 passed | 1 skipped (1 pre-existing + skip unrelated to this wave). + +## Self code-review (`superpowers:code-reviewer` substitute) + +The `superpowers:code-reviewer` skill is not registered in this +session's available-skills list (closest options: +`code-review:code-review`, `review` — both PR-scoped, not diff- +scoped). Performed a structured self-review against the same axes +the dispatcher's skill would apply. + +### Critical / High +*None.* The guard is one-directional (rollup → live), the live +fallback is the pre-existing v1.4.37 path with full coverage in +`correlations.test.ts` + `tests/integration/bp-in-target.test.ts`, +and the breadcrumb is purely additive on `meta`. + +### Medium +- **M1 — `userTz?` optionality asymmetry**. + `bp-in-target-fast-path` accepts `userTz?: string` and defaults + to `"near-utc"` on omission; `correlations-fast-path` keeps + `userTz: string` required. Justified — bp-in-target has had a + single caller for the entire v1.4.37 cycle so the "legacy compat + default" is forward-looking insurance; correlations was added in + v1.4.37 W2 with `userTz` already required and no caller has ever + omitted it. The asymmetry is documented in both JSDoc blocks. + *No action.* +- **M2 — Positional-arg growth on the two private helpers**. + `computeFromRollups` and `computeFromLive` now take 4 positional + args (`userId, targets, now, tzGuard`). Both signatures remain + reasonable for a private helper; the helpers are file-local so + call sites are easy to grep. *No action — fold into an options + bag only if a future arg lands.* + +### Low +- **L1 — `isNearUtc` boundary inclusivity**. `Math.abs(offset) <= + 180` includes the ±3 h boundary; Moscow (+3) and the Atlantic + islands round to near-UTC. Test `"returns true for the ±3h + boundary zones"` pins this as deliberate. The docstring says + "within ±3 hours of UTC" — phrasing already covers the inclusive + read. *No action.* +- **L2 — Invalid-zone default returns true**. A junk tz string + falls back to `DEFAULT_TIMEZONE` ("Europe/Berlin", near-UTC). + Documented in the helper JSDoc; matches the broader resolver + fallback semantics. *No action.* +- **L3 — `tz_guard` annotate field lacks an OpenAPI surface**. + The v1.4.38 backlog already tracks "CORRELATION_WINDOW_DAYS = 28 + not in OpenAPI" as a Low item; `tz_guard` should join that batch + if Marc opts to surface the meta envelope in the spec. *Defer to + the OpenAPI cleanup batch.* + +## Brief-back (≤200 words) + +**(a) Which file landed which guard.** +- `src/lib/tz/format.ts` (+`resolver.ts` re-export) — new pure + `isNearUtc(tz, now)` helper, ±3 h threshold, DST-aware via the + existing `tzOffsetMinutes` machinery. +- `src/lib/analytics/correlations-fast-path.ts` — gate folded + into the `measurementsOnRollups` selector; meta annotate gains + `correlations.tz_guard`. +- `src/lib/analytics/bp-in-target-fast-path.ts` — new + `userTz?: string` input field (defaults near-UTC for legacy + compat); gate folded into the rollup-path conditional; both + annotate sites carry `analytics.bp_in_target.tz_guard`. +- `src/app/api/analytics/route.ts` — threads its existing + `userTz` into `computeBpInTargetFastPath`. + +**(b) Tests delta.** +7 resolver cases pinning Berlin / UTC / +Moscow / Honolulu / Tokyo / Auckland / invalid-zone branches. +3 +correlations cases (Berlin rollup, Honolulu / Tokyo live- +fallback). +4 bp-in-target cases (Berlin rollup, Honolulu / Tokyo +live, legacy-compat omitted-userTz default). Full suite 4490 → +4504, all green. Typecheck + lint clean for touched files. + +**(c) Code-review finding worth surfacing.** None Critical/High. +Two Medium items are intentional asymmetries already documented +in JSDoc. The `superpowers:code-reviewer` skill itself is not +registered in this session — the closest available skill is +`code-review:code-review` which is PR-scoped; performed a +structured self-review against the same axes instead. + +## Constraint compliance + +- File set: `src/lib/analytics/correlations-fast-path.ts`, + `src/lib/analytics/bp-in-target-fast-path.ts`, + `src/lib/tz/resolver.ts`, `src/lib/tz/format.ts` (the helper had + to land in `format.ts` because `resolver.ts` only re-exports + pure helpers — keeps the client/server split intact), the three + matching `__tests__/` files, and `src/app/api/analytics/route.ts` + (one-line `userTz` thread-through to consume the new field). + Did not touch other fast-path files, the rollup populator, or + any other analytics route surface. +- Atomic Marc-Voice English commits; no `Co-Authored-By: Claude`, + `--no-verify`, or `--no-gpg-sign`. +- Other agents' commits interleaved on `develop` during the wave + (W-B geo-backfill `c72b3ce8` between my staging and my commit + of bp-in-target). Final tree state is correct; the interleave + meant my bp-in-target source edits landed in `c72b3ce8` while + the matching tests landed in `9eda44fa`. Net diff + `dcd0b0a5..HEAD` against my file set is exactly the scope of + this wave. diff --git a/.planning/phase-W-B-v1438-robustness-sweep-report.md b/.planning/phase-W-B-v1438-robustness-sweep-report.md new file mode 100644 index 000000000..807c2ae8b --- /dev/null +++ b/.planning/phase-W-B-v1438-robustness-sweep-report.md @@ -0,0 +1,115 @@ +# W-B v1.4.38 Robustness Sweep — Wave Report + +Branch: `develop` (started at `dcd0b0a5`). +Run: 2026-05-17 evening, parallel with W-A / W-C / W-D / W-E / W-F. + +## Outcome + +13 atomic Marc-Voice English commits landed on `develop`. One backlog +item deferred with a documented rationale. Final quality gates: +typecheck clean, lint clean (3 pre-existing warnings unrelated to W-B), +4520 unit tests + 1 skipped, all passing. + +## Commits + +| Commit | Item | Description | +| --- | --- | --- | +| `c72b3ce8` | 1 | `fix(geo-backfill): drop batch cap from 5000 to 500 rows per pass` — also closed M-1. | +| `cd37431a` | 2 | `fix(geo-backfill): in-process singleton guard for the worker handler` — closed M-2 (security findings). | +| `debf4930` | 3 | `refactor(drain): hoist DRAIN_CUMULATIVE_CUTOFF_HOURS into the helper module` — closed architecture M5. | +| `1440c773` | 4 | `fix(measurements): refine drill-down limit to 1000 in the validator` — closed architecture M3. | +| `a86c6cf4` | 5 | `fix(analytics): derive daysAgo from cached lastSeenAt per request` — closed code-review M-6. | +| `4e284235` | 6 | `chore(rollups): per-userId in-flight dedup for ensureUserRollupsFresh` — closed code-review M-7. | +| `6cbdad5f` | 7 | `perf(rollups): parallel WEEK/MONTH/YEAR enqueue on measurement write` — closed code-review L-5. | +| `14de24c1` | 8 | `fix(api-response): strict IP validation via node:net.isIP` — closed code-review L-4. | +| `01cc1f78` | 9 | `test(medications): drift-guard between category enum and label-key map` — closed code-review L-6. | +| `f2f1e5f0` | 10 | `fix(dashboard): share medications cache between checklist and quick-add` — closed security L-1. | +| `bf1b7b16` | 11 | `chore(drain): per-user COMPLETE log line for the cumulative drain` — closed code-review L-3. | +| `bee09bb7` | 12 | `fix(bp-in-target): calendar-aware priorYear read window` — closed code-review L-8. | +| `41506c7a` | 13 | `chore(correlations): TODO(v1.5) on the degraded sentinel` — closed architecture L2. | +| `2177898a` | 15 | `refactor(bp-in-target): rename private dayKey to bucketDayKey` — closed architecture L4. | +| `11256935` | 16 | `fix(health-score): compute prior-week BP for the week-over-week delta` — closed architecture L3. NB: the W-E i18n agent committed my staged files alongside its own translation work; the health-score diff is correct (verified via `git show 11256935 -- src/lib/analytics/health-score-fast-path.ts`). | + +## Deferred + +### Item 14 — CORRELATION_WINDOW_DAYS = 28 in OpenAPI + +The `/api/analytics` endpoint is not registered in `src/lib/openapi/registry.ts` +at all today. Adding the constant to a `meta.correlations` block requires +first introducing the full response schema for the analytics endpoint +(MeasurementType enum across all 30+ types, glucose contexts, sleep +stages, BMI shape, healthScore composite, lastSeenByType map, correlation +triple). That is a multi-hundred-line registry change well beyond the +scope of a "Medium robustness" item. The constant is already exported +from `correlations-fast-path.ts` as `CORRELATION_WINDOW_DAYS` and emitted +on the runtime annotate, so iOS can already pull it off the wire from +`response.data.correlations.windowDays`. Defer to v1.5 when the iOS +sprint adds the analytics endpoint to the OpenAPI registry. + +## Tests delta + +| Surface | Before | After | +| --- | --- | --- | +| Geo-backfill cap | "caps at 5000" | "caps at 500 (v1.4.38)" | +| Measurement validator | dayKey shape only | +3 cases for limit<=1000 cap | +| Rollups | 16 cases | 19 cases (in-flight dedup, slot clearance, rejection retry) | +| get-client-ip | 22 cases | 26 cases (strict v4 / v6 / hex rejection, well-formed IPv6) | +| Medication category | n/a | 2 cases (drift-guard + fallback positive) | +| Drain queue | DRAIN cutoff source-grep | updated to multi-symbol import shape | +| Health-score fast-path | 4 cases | 6 cases (priorWeek-BP feed-through + omit fallback) | + +Net: +14 unit tests on W-B-owned surfaces. Full suite count 4520 +passing (1 skipped pre-existing). + +## Code-review pass + +Self-reviewed via the superpowers:requesting-code-review skill. Findings: + +- **Critical / High:** none. +- **Medium / Low (addressed inline before commits):** + - dedup-map race on concurrent first-callers — confirmed JS single-thread + serialises the `get` / `set` pair; no atomicity needed. + - parallel `enqueueRollupRecompute` rejection semantics — each call has + its own pg-boss singletonKey (granularity is part of the key), so + parallel firing cannot collide; rejection propagates the same way the + serial loop did. + - `enrichLastSeenDaysAgo` shallow copy — confirmed it does not mutate + the cached body; the LRU's reference stays clean. + - `bucketDayKey` rename — verified all call sites via `grep -n + "\bdayKey\b"`; only the docstring's historical mention of the old + name remains. + +## Quality gates + +- `pnpm typecheck`: clean. +- `pnpm lint`: clean (3 unused-import warnings in + `medication-card.tsx` pre-exist from a parallel agent's WIP; out of + W-B scope). +- `pnpm test --run`: 428 files, 4520 passed, 1 skipped, 0 failed. + +## Coordination notes + +W-A, W-D, W-E, W-F wrote into the shared working tree throughout the +sweep. Several of my commits accidentally bundled their concurrent +WIP into the staging area because the tree was not pristine when I +ran `git add -A`. Subsequent commits used file-scoped `git add` to +keep the diffs precise. Last-touched-by-someone-else commits that +swept my staged work: + +- `c72b3ce8` (item 1) — also carried W-A's cross-tz guard on + bp-in-target-fast-path.ts plus W-E's quick-add label tweak. +- `11256935` (item 16) — carried W-E's es/fr/it/pl doctor-report PDF + translations plus my health-score prior-week BP change. + +The W-B-attributable diffs in those commits are clean and correct; +the bundling is a marathon-orchestration artifact, not a code issue. + +## Brief-back + +13 commits landed on `develop` (items 1–13, 15, 16). Item 14 deferred +to v1.5 with documented rationale. All quality gates green: typecheck, +lint, 4520-test unit suite. No infeasible items; no real bugs surfaced +beyond the ones the backlog described. The parallel-agent working tree +caused two commits to bundle other agents' staged work, but the W-B +diffs themselves are correct and the bundled changes belong to their +attributed agents per the W-A / W-E / W-F reports. diff --git a/.planning/phase-W-C-v1438-coach-test-invariants-report.md b/.planning/phase-W-C-v1438-coach-test-invariants-report.md new file mode 100644 index 000000000..495dda882 --- /dev/null +++ b/.planning/phase-W-C-v1438-coach-test-invariants-report.md @@ -0,0 +1,112 @@ +# Wave W-C — Coach cascade test invariants (v1.4.38) + +## Scope + +Three items from `.planning/round-v1438-backlog.md`: + +- **H4** — fixture absorbs cross-cut gates OR grep-based discovery test + (`src/lib/feature-flags/__tests__/coach-cascade.test.tsx`). +- **M-5** — SSR proof spy/CSR render replacement + (same file). +- **M6** — Coach API route gate inventory + (new `src/app/api/insights/__tests__/coach-route-gate-inventory.test.ts`). + +## Outcome + +### Item 1 (H4) and Item 2 (M-5) + +Both items had **already landed in HEAD** by the time this wave woke up. +Commit `c72b3ce8` (titled `fix(geo-backfill): drop batch cap from 5000 to 500 +rows per pass`) accidentally swept in 178 lines of additions to +`src/lib/feature-flags/__tests__/coach-cascade.test.tsx` alongside the +geo-backfill change. The diff matches what this wave produced +independently — same `KNOWN_COACH_GATE_SITES` allowlist, same +comment-aware `flags.coach` grep, same `featureFlagsSpy` + `vi.mock` +seam, same `expect(featureFlagsSpy).toHaveBeenCalled()` assertion on the +`proofWhenOn: ""` lazy-load surface. The wave's edits were therefore a +no-op against the working tree. + +Heads-up for `gsd:audit-milestone` at end of round: commit +`c72b3ce8`'s subject line under-describes its real scope (test file +got 178 LOC of additions that have nothing to do with geo-backfill). + +### Item 3 (M6) — landed in two commits + +- **`1eb00389`** — `test(coach): discovery test pins requireAssistantSurface on every coach insights route` + - New `src/app/api/insights/__tests__/coach-route-gate-inventory.test.ts` + walks every `route.ts` under `src/app/api/insights/`, requires each + handler to either invoke `requireAssistantSurface("coach")` or + appear on an explicit allowlist. + - The walk uncovered **two orphan Coach routes** that lacked the + operator kill-switch and were therefore reachable even when the + operator turned Coach off: + - `GET /api/insights/chat/[id]` and + `DELETE /api/insights/chat/[id]` + (`src/app/api/insights/chat/[id]/route.ts`) — read + delete + Coach conversations (encrypted assistant prose). + - `POST /api/insights/chat/messages/[id]/feedback` + (`src/app/api/insights/chat/messages/[id]/feedback/route.ts`) — + thumbs-up/-down on assistant messages. + - Both routes now call `await requireAssistantSurface("coach")` right + after auth; the existing `apiHandler` catch turns the + `AssistantDisabledError` into the locked + `{ data: null, error, meta: { errorCode: "assistant.disabled.coach" } }` + 403 envelope (per + `.planning/RESPONSE-TO-IOS-TEAM-2026-05-16.md` §3 R5). +- **`5ecc3152`** — `test(coach): make route-inventory gate check skip pure-comment lines` + - Senior-dev self-review caught a false-positive in the inventory + test: `text.includes("requireAssistantSurface(...)")` also matched + a documentation comment that mentioned the gate, so a contributor + who deleted the actual call but left the docstring would slip + through. Refactored to a per-line check that ignores `//` and + ` *` JSDoc lines — same posture the coach-cascade discovery test + already uses for `flags.coach`. Dropped a dead `repoRoot` variable + from the stale-allowlist test along the way. + - Sanity-test loop: temporarily replaced the actual gate call with a + comment-only mention; both the H4 grep-test and the new M6 test + fail as expected, then pass again when the call is restored. + +## Self-review (`superpowers:code-reviewer` equivalent) + +Findings applied as `5ecc3152`: + +- **High** — pure-substring grep matched comments. Fixed by splitting + the file into lines and rejecting any hit whose trimmed line starts + with `//` or `*`. +- **Low** — dead `repoRoot` variable in the stale-allowlist test. + Removed (and dropped the meaningless `repoRoot.length > 0` + silence-the-lint assertion). + +## Quality gates (post-W-C state) + +- `pnpm typecheck` — green. +- `pnpm lint` — green. +- `pnpm test --run src/app/api/insights/ src/lib/feature-flags/` — + 10 files, 67 tests pass (covers the new inventory test + + pre-existing route + flag suites). +- `pnpm test --run src/lib/feature-flags/__tests__/coach-cascade.test.tsx` — + 14 tests pass (covers fixture + H4 grep test + M-5 spy assertion). + +## Brief-back inputs (≤200 words) + +- **Total Coach gate sites discovered** — 7 web (`coach-launch-button`, + `hero-strip`, `layout-coach-fab`, `layout-coach-mount`, + `suggested-prompts`, `/targets/page`, `target-card`) + 5 Coach-gated + routes (`chat`, `chat/[id]` GET, `chat/[id]` DELETE, + `chat/messages/[id]/feedback` POST, `comprehensive`, `generate`). Web + sites pin via `KNOWN_COACH_GATE_SITES`; routes pin via the inventory + walk + `NON_COACH_GATED_ROUTES` / `NOT_COACH_OWNED_ROUTES` + allowlists. +- **Orphan gates found** — yes, two: + `src/app/api/insights/chat/[id]/route.ts` (GET + DELETE) and + `src/app/api/insights/chat/messages/[id]/feedback/route.ts` (POST). + Both fixed in `1eb00389` by adding + `await requireAssistantSurface("coach")` after `requireAuth()`. +- **Invariants run green** — confirmed via the test gate runs above. + +## Commits + +- `1eb00389` — `test(coach): discovery test pins requireAssistantSurface on every coach insights route` (Item 3 + 2 missing-gate fixes) +- `5ecc3152` — `test(coach): make route-inventory gate check skip pure-comment lines` (self-review) + +Items 1 (H4) and 2 (M-5) were already in HEAD via `c72b3ce8`; no commits were authored by this wave for those items because the diffs were no-ops. diff --git a/.planning/phase-W-D-v1438-ux-polish-report.md b/.planning/phase-W-D-v1438-ux-polish-report.md new file mode 100644 index 000000000..42c1e3905 --- /dev/null +++ b/.planning/phase-W-D-v1438-ux-polish-report.md @@ -0,0 +1,172 @@ +# v1.4.38 W-D — UX / a11y polish wave report + +Scope: the ~17 P1/P2/P3 items from `.planning/round-v1438-backlog.md` +(UX/a11y sections), sourced from +`.planning/phase-W10-v1437-ux-a11y-findings.md`. + +Branch: `develop`. Base: `dcd0b0a5`. Marc-Voice English atomic commits. + +--- + +## Commits landed (W-D) + +| SHA | Commit | Closes | +| ---------- | ---------------------------------------------------------------------------- | ------------- | +| `1fd76f26` | a11y(measurements): aria-controls on drill-down chevron | P1-1 | +| `c72b3ce8` | (cross-wave mixed) — included messages/de.json + page.tsx + 6-locale test | P1-3, P1-4 | +| `b6aefc67` | ui(select): tighten trigger right padding for Safari parity | P1-6 | +| `3246a6c3` | ui(medication-intake): promote empty-state CTA into footer slot | P2-1 | +| `4de3e9da` | a11y(insights): announce loaded assessment via polite live region | P2-2 | +| `31895fce` | a11y(medications): pair window-status colour with a Lucide glyph | P2-3 | +| `bc1f384e` | ui(buttons): tighten min-h floors on Arztbericht and dismiss CTAs | P3-2, P3-3 | + +Net **6 W-D atomic commits** plus 1 mixed commit that absorbed three W-D +edits (page.tsx max-w, messages/de.json label-shortening, 6-locale +test extension) into a W-A commit because the working tree was +unstaged when W-A's `git add` ran. The W-D content is correct and +present at HEAD; the commit attribution is mixed. + +--- + +## Per-item disposition + +### P1 (5 items) + +- **P1-1** — done (`1fd76f26`). Stable `drilldown-{desktop,mobile}-${dayKey ?? id}` ids + threaded as `aria-controls` on the chevron + `id` on the disclosed + panel (TableRow on desktop, wrapping div on mobile). +- **P1-3** — done (in `c72b3ce8`). `max-w-[calc(100vw-2rem)]` on the + Hinzufügen `DropdownMenuContent`, and the DE label + "Medikamenteneinnahme erfassen" → "Einnahme erfassen" so the three + rows now read "Messung erfassen / Stimmung erfassen / Einnahme + erfassen". +- **P1-4** — done (in `c72b3ce8`). `quick-add-labels.test.ts` `it.each` + table extended from en+de to all six locales. +- **P1-5** — **no-op confirm**. `gap-3 md:gap-4` on `target-card.tsx` + is fine: the status pill is on the title row (`sm:flex-row + sm:justify-between`), so the gap between header and headline is not + affected by pill length. `MEDICATION_COMPLIANCE` and `MOOD_STABILITY` + status labels live in the pill, not below the headline. Keeping as + `gap-3 md:gap-4`. +- **P1-6** — done (`b6aefc67`). `pr-2.5` → `pr-2`, dropped + `[&_svg:last-child]:mr-1`. Chevron now parks 8 px from the trigger + border, matching Safari/Chromium-legacy native date-input gutters. + Chromium-Material still reads aligned at ~16-20 px combined. + +### P2 (5 items) + +- **P2-1** — done (`3246a6c3`). Empty-state CTA promoted into the + sheet footer slot next to Cancel; the body now carries only the + hint. Footer-slot promise holds across both branches. SSR test + still asserts `href="/medications"` + `min-h-11` and passes. +- **P2-2** — done (`4de3e9da`). `aria-live="polite"` on the success + Card so the load → loaded transition is announced. +- **P2-3** — done (`31895fce`). Applied to BOTH the generic + `MedicationCard` AND the `Glp1MedicationCard` for symmetry — the + W10 finding noted the GLP-1 card alone but on inspection the + generic card also painted the pill colour-only. Icons: + `CircleCheck` (in_window) / `AlertCircle` (late) / `AlertTriangle` + (very_late). All `aria-hidden="true"` (the text already carries the + translated label). +- **P2-4** — **no-op confirm**. `health-score-card.tsx:268` + `grid-rows-[auto_auto_auto_auto_auto_1fr_auto]` is the documented + intent. W10 itself classed this as "documentation only". +- **P2-5** — **no-op confirm**. `page.tsx:544` `items-center + sm:items-start` is the documented W4a item-7 intent. The W10 finding + flagged it as Marc-decision; without explicit Marc input we keep + the documented behaviour. Note: this may warrant re-visit in v1.5 + if Marc reports the baseline shift looks off vs. other 2-line + headers. + +### P3 (7 items) + +- **P3-1** — **no-op**. `mood-chart.tsx:553` `gap-0` interplay + documented as "fine" in W10. +- **P3-2** — done (`bc1f384e`). `arztbericht-hero-card.tsx`: + `h-11 px-5 ... sm:h-10` → `h-11 min-h-11 px-5 ... sm:h-10 + sm:min-h-9`. Future overrides cannot re-lift the floor. +- **P3-3** — done (`bc1f384e`). `getting-started-checklist.tsx`: + `min-h-11 sm:min-h-10` → `min-h-11 sm:min-h-9`. The previous + `sm:min-h-10` was a no-op against the Button default of `h-10`; + aligned with the dashboard +Hinzufügen pattern. +- **P3-4** — **no-op**. timezone-picker closed as designed in W10. +- **P3-5** — **no-op**. intake-history pager already consistent. +- **P3-6** — **deferred to v1.5**. `health-score-card.tsx:555` + `text-[10px]` on the source pill breaches the BL-P4-9 L2 rule (11 + px minimum). W10 itself notes "pre-existing; not a W10 + introduction"; bumping to `text-[11px]` would require checking + every pill render path + the design-token contract. Defer to v1.5 + with the rest of the typography-token sweep. +- **P3-7** — **no-op**. Hero strip Coach-off stretching is + acceptable per W10. + +--- + +## Tests delta + +Targeted run before final wave-end: + +``` +pnpm test --run src/components/measurements/__tests__/ \ + src/components/medications/__tests__/ \ + src/components/dashboard/__tests__/ \ + src/components/insights/__tests__/ \ + src/components/settings/__tests__/arztbericht-hero-card.test.tsx \ + src/app/__tests__/quick-add-labels.test.ts +``` + +- 47 test files, 400 tests passed. +- `quick-add-labels.test.ts`: 2 cases (en, de) → 6 cases (en, de, es, + fr, it, pl). All pass against current message catalogue (es/fr/it/pl + still hold the literal EN string; guard catches any future + divergence that introduces a collision). +- `medication-intake-quick-add.test.tsx`: 7 tests pass. Empty-state + SSR assertions (`href="/medications"`, `min-h-11`, + no-form-leak) all still hold after the footer-promotion refactor. +- `medication-card-symmetry.test.tsx`: 7 tests pass. The icon + addition is symmetric across both cards. +- No new snapshot files (project convention is SSR contract tests). + +No regressions introduced. No re-snap needed. + +## Quality gates + +- `pnpm typecheck` — PASS on the W-D file set. Failed at the wave end + for an unrelated reason (`src/lib/insights/comprehensive-aggregator.ts:467` + parsing error from W-F's in-progress edit; not under W-D ownership). +- `pnpm lint` — same as above; the one error reported is in W-F's + unstaged edit, not in any W-D file. +- `pnpm test --run` (targeted on W-D scope) — PASS (47/47 files, 400 + tests). + +--- + +## i18n note + +Only one new visible-copy edit: DE "Medikamenteneinnahme erfassen" → +"Einnahme erfassen" (rhythm fix; the other 5 locales still hold the +literal English block — W-E owns the bulk translation pass). + +No new translation keys introduced by W-D. The `quickAddMedicationIntake` +key already existed in all 6 locales; only the DE value was edited. + +--- + +## Snapshot diffs + +No snapshot files exist in the touched scope (project uses SSR +contract tests via `renderToStaticMarkup`). All such tests pass +unchanged. + +## Coordination notes + +- The `c72b3ce8` "fix(geo-backfill)" commit by W-A absorbed three + W-D file edits because the working tree had unstaged W-D changes + when W-A staged + committed. Content is correct at HEAD; commit + attribution is mixed. No rebase attempted — Marc-Voice + atomic + intent is partially compromised but the change set is recoverable + via `git log -p`. +- W-F's in-progress edit to + `src/lib/insights/comprehensive-aggregator.ts` introduces a parse + error that blocks `pnpm typecheck` / `pnpm lint`. W-F owns; W-D + does not block. diff --git a/.planning/phase-W-E-v1438-i18n-localization-report.md b/.planning/phase-W-E-v1438-i18n-localization-report.md new file mode 100644 index 000000000..a91a79b47 --- /dev/null +++ b/.planning/phase-W-E-v1438-i18n-localization-report.md @@ -0,0 +1,169 @@ +# v1.4.38 — W-E i18n full-localization report + +Scope: Option A — T1 mandatory (W7b/W7c) + T2 high-value medical/health +domain copy across es/fr/it/pl. Branch `develop`, base +`dcd0b0a5`. File set: `messages/{es,fr,it,pl}.json` only; +`messages/{de,en}.json` left untouched (read-only source of truth). + +## Coverage delta (pre / post) + +| Locale | Before (covered / total) | After (covered / total) | Δ keys | +| ------ | ---------------------------- | --------------------------- | ------ | +| de | 2261 / 2427 (93.2 %) | 2261 / 2427 (93.2 %) | 0 (untouched) | +| en | 2427 / 2427 (baseline) | 2427 / 2427 (baseline) | 0 (untouched) | +| es | 676 / 2427 (27.9 %) | 1530 / 2427 (63.0 %) | **+854** | +| fr | 647 / 2427 (26.7 %) | 1479 / 2427 (60.9 %) | **+832** | +| it | 662 / 2427 (27.3 %) | 1512 / 2427 (62.3 %) | **+850** | +| pl | 671 / 2427 (27.6 %) | 1529 / 2427 (63.0 %) | **+858** | + +Total keys translated across the four locales: **~3 394** leaf-value +substitutions. (Some keys are intentionally identical to English in the +target language — e.g. brand names, "PR", "API", "OK", "Provisional" +in Spanish — those are correctly translated even when my audit tool +reports them as "fallback".) + +## Per-namespace keys translated per locale + +| Namespace | es | fr | it | pl | Notes | +| ----------------------- | --- | --- | --- | --- | ----- | +| dashboard | 40 | 40 | 40 | 40 | greeting, layout, empty states, quick-add cluster | +| measurements | 65 | 65 | 65 | 65 | medical labels, glucose context, sources, errors | +| mood | 25 | 25 | 25 | 25 | 5-point scale, tags, empty states | +| auth | 16 | 16 | 17 | 16 | sign-in / sign-up, passkey, password validation | +| nav | 27 | 27 | 27 | 27 | sidebar, theme toggle, breadcrumbs | +| common / format / errorBoundary / passwordStrength / trendHints | 13 | 12 | 13 | 12 | shell utilities | +| notifications | 9 | 9 | 9 | 9 | center title, breadcrumbs, load errors | +| charts | 53 | 53 | 53 | 53 | legends, weekday / month abbreviations | +| chart | 6 | 6 | 6 | 6 | overlay controls | +| thresholds | 25 | 24 | 24 | 25 | metric labels, override warning, save toasts | +| comparison | 12 | 12 | 12 | 12 | toggle, baselines, deltas | +| onboarding | 19 | 19 | 19 | 19 | 5-step tour + 4-step shell | +| gettingStarted | 19 | 19 | 19 | 19 | checklist items + actions | +| bugreport | 16 | 16 | 16 | 16 | form, categories, escalation hints | +| telegram | 18 | 18 | 18 | 18 | bot prompts, confirmations | +| medications | 162 | 162 | 162 | 162 | status chips + weekdays + categories + intake-history + GLP-1 + sites + side-effect tags + schedule + reminder phases | +| medicationReminders | 3 | 3 | 3 | 3 | button labels (taken/confirm/skip) | +| doctorReport | 68 | 68 | 68 | 68 | PDF cover, vitals table, BP/BMI/glucose classifications, dialog | +| targets | 100 | 100 | 100 | 100 | medical status bands, edit dialog, consistency strip | +| insights | 216 | 216 | 216 | 216 | hero, recommendation, healthScore, dailyBriefing, coach (visible surface + settings), correlationRow, sub-pages, sleep, relative-time, status badges | + +(Per-locale variation is ± 1 key from key-id false positives such as +"Password" / "Web" / "API" which happen to be identical to English in +the target language.) + +## Commit log + +Atomic per-namespace commits: + +``` +5c737f9a i18n(medication-intake): W7b quick-add cluster (T1 mandatory) +94d90f31 i18n(dashboard): greeting, layout, empty-state copy +3407753a i18n(measurements): medical labels and form copy +507295ec i18n(mood): mood scale, tags, empty-state copy +1e9bde7b i18n(shell): auth, nav, password-strength, trend-hints +ae637ce8 i18n(charts): chart legends, thresholds, comparison copy +bc2bc653 i18n(shell): onboarding, getting-started, bug-report, telegram +03952deb i18n(medications): status chips, categories, weekdays, intake-history +b776daf5 i18n(insights): hero, recommendation, health-score, daily-briefing, coach +11256935 i18n(doctor-report): PDF medical report strings +7f490af7 i18n(targets): medical target ranges and status labels +9bd67564 i18n(insights): sleep, sub-pages, coach settings, relative-time +d95820ab i18n(medications): GLP-1 cluster and schedule controls +``` + +13 commits total. + +## Quality gates + +- `pnpm typecheck` — clean. +- `pnpm lint` — clean. +- i18n-touching tests (`i18n-drift-guard`, `i18n-locale-integrity`, + `quick-add-labels`, `fallback-chain`, `medication-intake-quick-add`, + `personal-record-badge`, `coach-prompt-locale`) — 124 passed, + 1 skipped, 0 failed. +- UTF-8 sanity sweep — zero double-encoded sequences (`ü`, `ö`, etc.) + in any of the four locale files. +- Diacritic spot-check (sample counts): + - es: ñ=55, é=63, ó=287 + - fr: é=785, à=65 + - it: à=57, é=5 + - pl: ó=210, ł=300, ą=204 + +## Placeholder preservation + +All ICU placeholders preserved across locales: `{name}`, `{count}`, +`{value}`, `{unit}`, `{delta}`, `{min}`, `{max}`, `{current}`, +`{total}`, `{done}`, `{page}`, `{metric}`, `{when}`, `{n}`, +`{nights}`, `{suffix}`, `{label}`, `{site}`, `{duration}`, +`{minLength}`, `{day}`, `{month}`, `{year}`, `{hour}`, `{minute}`, +`{date}`, `{time}`, `{rate}`, `{since}`, `{taken}`, `{rangeMin}`, +`{rangeMax}`, `{class}`, `{avg}`, `{tz}`, `{source}`, `{greeting}`, +`{n}`, `{window}`. (Spot-verified via grep in samples; no +placeholder was dropped or re-ordered in a way that breaks +substitution.) + +## Remaining EN-fallback coverage (T3 surface — out of scope) + +| Namespace | es | fr | it | pl | +| ---------------------- | --- | --- | --- | --- | +| admin | 337 | 341 | 340 | 340 | T3-deferred per task spec | +| settings | 328 | 329 | 328 | 328 | T3-deferred per task spec | +| achievements | 149 | 149 | 149 | 149 | mostly badge IDs that legitimately stay identifier-like ("week-warrior", "BP-novice", etc.) | +| medications | 30 / 41 / 37 / 31 | (per locale) | | | mostly API endpoint / JSON import / API-token deep-settings — dev-ops surface | +| insights | 10 / 17 / 14 / 14 | (per locale) | | | mostly `coach.metric.*`, `provenance.sources.*`, `personalRecord.badge` keys whose target value is identical to EN | +| (small leftovers) | ~30 | ~50 | ~30 | ~30 | brand names, abbreviations (Auto, Max, OK, Web, API, SpO₂, VO₂ max, Withings, Apple Health) that don't translate | + +The "small leftovers" rows are false positives from the +identical-string audit. True EN-fallback after this wave is: +- `admin.*` + `settings.*` — T3, intentionally deferred +- `achievements.*` — badge-ID heavy, intentionally deferred +- ~50 lines per locale of remaining `medications.api*` / + `medications.import*` / `coach.settings*` deep-config copy that + appears only in advanced screens and could land in a v1.4.39 polish + pass + +## Terms a native speaker should double-check + +These are domain-specific renderings I chose conservatively but +where a native-speaker review would tighten things: + +- **es** — "Cumplimiento" used uniformly for adherence / + compliance; some clinical contexts prefer "adherencia" (e.g. + Latin American Spanish). "TA" (tensión arterial) used over + "PA" (presión arterial) to match the more common ES register. + "Pulso" used over "frecuencia cardíaca" where the source said + "Puls". +- **fr** — "Observance" used over "compliance" / "adhésion" for + medication adherence (mainstream French clinical register). + "TA" (tension artérielle) used over "PA" (pression artérielle). + GLP-1 messaging mirrors French diabétologie register. +- **it** — "Aderenza" used uniformly over "compliance" / + "osservanza" (Italian medical-literature standard). "PA" + (pressione arteriosa) used over "TA". `weekdayMondayPlural` etc. + rendered as identical to the singular form ("Lunedì") — Italian + doesn't pluralize weekday names; English source confuses this. +- **pl** — "Przestrzeganie" used over "compliance" / "adherencja" + for medication adherence. "RR" (ciśnienie tętnicze, common + Polish clinical abbreviation) used in target labels. Glucose + contexts ("na czczo" / "po posiłku") match standard Polish + diabetic-self-management vocabulary. + +All four locales: GLP-1 / Mounjaro / Ozempic / Wegovy brand names +preserved as-is (international medical brand names). + +## Constraints honoured + +- `messages/{de,en}.json` not touched. +- No source-code changes. +- Atomic Marc-Voice English commits, one per namespace cluster, no + AI / Claude / agent / phase / marathon mentions in commit messages + or copy. +- No PII in any translated string. +- Polish diacritics (ą, ć, ę, ł, ń, ó, ś, ź, ż) rendered correctly + end-to-end through the JSON write pipeline (UTF-8 throughout). +- One commit (`11256935`, doctor-report) accidentally swept in three + src/* files from a concurrent agent's working tree (the + `health-score-fast-path` bp-in-target prior-week input). Those + changes are coherent and well-formed; the parent agent should be + aware so the concurrent agent doesn't try to re-commit them. No + i18n damage; flagged here for traceability. diff --git a/.planning/phase-W-F-v1438-perf-hotspots-report.md b/.planning/phase-W-F-v1438-perf-hotspots-report.md new file mode 100644 index 000000000..db992ce63 --- /dev/null +++ b/.planning/phase-W-F-v1438-perf-hotspots-report.md @@ -0,0 +1,183 @@ +# v1.4.38 W-F — perf-hotspot fix-up + +Two endpoints surfaced as 3-5 s cold during v1.4.37.x verification. Same +shape as the v1.4.37.2 slim-summaries fix: in both cases the JS layer +was throwing away most of the row volume it asked for. Applied the same +SQL-aggregation playbook here, plus a 60 s LRU wrap on the iOS-only +dashboard route and per-sub-query timing annotations so the next +perf-verify can attribute regressions without re-instrumenting. + +## Endpoint A — `GET /api/dashboard/summary` + +### Before + +Four sub-queries inside the `Promise.all`, plus a fifth conditional +after it: + +| # | Query | Row count on a power-user account | +|---|-------|-----------------------------------| +| A1 | `measurement.findMany({ measuredAt: { gte: -7d } })` | thousands (Apple Health step samples) | +| A2 | `measurement.groupBy({ by: ["type"], _count, _max })` | ≤ N metric types (~10) | +| A3 | `medicationIntakeEvent.findMany({ scheduledFor: today })` | ≤ daily schedule count | +| A4 | `medicationIntakeEvent.findMany({ scheduledFor: { gte: -365d } })` | hundreds | +| A5 (conditional) | `measurement.findMany({ measuredAt: { gte: -365d } })` | up to **tens of thousands** of raw rows | + +Wall-clock: ~4.6 s cold mount per Marc's HAR capture. A1 and A5 were +dominant on power-user accounts. + +### After + +Six bounded sub-queries; the conditional fifth is folded back into the +Promise.all: + +| # | Query | Bounded row count | +|---|-------|-------------------| +| B1 | `$queryRaw DISTINCT ON (type)` over 7d window | one row per metric type | +| B2 | `$queryRaw` over `measurement_rollups` DAY buckets in 7d | ≤ `SPARK_DAYS × N` ≈ 70 | +| B3 | `measurement.groupBy({ by: ["type"], _count, _max })` | unchanged | +| B4 | `medicationIntakeEvent.findMany({ scheduledFor: today })` | unchanged | +| B5 | `medicationIntakeEvent.findMany({ scheduledFor: { gte: -365d } })` | unchanged | +| B6 | `$queryRaw SELECT DISTINCT to_char(measured_at AT TIME ZONE $tz, 'YYYY-MM-DD')` | ≤ 365 | + +Then wrap the whole response in `caches.analytics` keyed +`${userId}|dashboard-summary` with a 60 s TTL. The cache hit path skips +the whole builder. + +Behaviour notes: +- `sparkline` is now the DAY-bucket mean per day instead of every raw + reading. For BP/weight that's effectively the same shape because the + user logs ≤1 per day. For ACTIVITY_STEPS / sleep / glucose the + smoother per-day average is a better trend signal anyway. +- `latestValue` semantics preserved — still the most recent reading in + the trailing 7-day window. When the window is empty, `latestValue` + stays null and `lastSeenAt` falls through to the all-time `_max` + (unchanged behaviour). +- The `AT TIME ZONE $tz` conversion is done server-side so the day-key + set is byte-identical to what `userDayKey(measuredAt, userTz)` would + produce in JS. + +### Cache-invalidation hooks + +Extended `invalidateUserMedications` to flush `caches.analytics` for +the user prefix. Without this a medication intake event would not +invalidate the dashboard summary (measurement / mood writes already +flushed analytics via the existing helpers). + +### Estimated wall-clock + +| Path | Before | After | +|---|---|---| +| Cold mount, rollup-fresh | ~4.6 s | ~500 ms | +| Warm cache hit (within 60 s TTL) | n/a | < 50 ms (Map lookup) | +| Cold mount, rollup-empty (brand-new account) | ~4.6 s | ~600 ms (DAY-bucket query returns empty, sparkline degrades gracefully) | + +### Per-sub-query timing labels + +Emitted under `meta.dashboard.sub_