docs(planning): W7c step-consolidation wave report

This commit is contained in:
Marc-André Bombeck committed 2026-05-17 14:39:47 +02:00
1 parent bca5298049
commit c824488895
1 file changed
+146
@@ -0,0 +1,146 @@
---
file: .planning/phase-W7c-v1437-step-consolidation-report.md
purpose: v1.4.37 W7c — Apple Health step consolidation wave report
created: 2026-05-17
wave: W7c
---
# Phase W7c — Apple Health step consolidation
Closes Marc's UAT directive ("Nach wie vor sehe ich unendlich viele
Apple-Health-Meldungen über die Schritte. […] Wie viele Schritte habe
ich am Tag? […] aber wir wollen eine tägliche Schrittmeldung haben,
die das einfach konsolidiert") by:
1. Painting one row per day per cumulative type on the measurements
list (with an expand chevron back to the per-sample chunks).
2. Scheduling the v1.4.30 drain helper nightly so storage converges
to one `stats:…` row per user-day per cumulative type with a 36 h
grace window for late watch syncs.
3. Pinning the new API surface + scheduler wiring with regression
tests so future refactors don't silently break the day-collapse
contract.
## Commits (chronological on develop)
| SHA | Subject |
| --- | --- |
| (riding 0af77230) | feat(measurements): groupBy=day mode for cumulative types (schema) |
| f6008e76 | feat(measurements): collapsed list and per-day drill-down for cumulative types (route) |
| 4e336f93 | feat(measurements): collapsed list view with day drill-down chevron (UI) |
| 9a08ad66 | feat(measurements): cutoffHours grace window for drainPerSampleCumulative |
| a1f61bb2 | chore(queue): schedule nightly drainPerSampleCumulative with 36h cutoff |
| (riding 2987b8c9) | test(measurements): pin groupBy=day, dayKey, and drain cutoff contracts |
| c7480e90 | fix(measurements): tighten W7c grouped-view pagination and timezone comment |
Two commits rode on sibling-agent commit messages (W2 / W7a)
because the index was raced during the marathon's parallel
dispatch — the actual file changes are mine and accurately
described in the surviving commits + this report. No work was
lost.
## File set
Owned (all committed):
- `src/lib/validations/measurement.ts` — `groupBy` + `dayKey` schema.
- `src/app/api/measurements/route.ts` — two new branches (groupBy=day, dayKey drill-down).
- `src/components/measurements/measurement-list.tsx` — collapsed view, chevron, lazy drill-down sub-component.
- `src/lib/measurements/drain-per-sample-cumulative.ts` — `cutoffHours` option.
- `src/lib/jobs/reminder-worker.ts` — new `drain-per-sample-cumulative` queue + cron.
- `messages/{en,de,fr,es,it,pl}.json` — `dailyTotalCaption`, `expandDay`, `collapseDay` keys (DE + EN translated, FR/ES/IT/PL English fallback per existing locale-fallback convention).
Tests (all committed, four new files):
- `src/lib/validations/__tests__/measurement.test.ts` (+6 cases)
- `src/app/api/measurements/__tests__/group-by-day.test.ts` (NEW, 5 cases)
- `src/components/measurements/__tests__/measurement-list-step-grouping.test.tsx` (NEW, 4 cases)
- `src/lib/measurements/__tests__/drain-per-sample-cumulative.test.ts` (+3 cases for the cutoff)
- `src/lib/jobs/__tests__/drain-cumulative-queue.test.ts` (NEW, 5 cases)
## Tests delta
| Layer | Before | After |
| --- | --- | --- |
| Unit (vitest run) | 4422 (pre-wave baseline) | 4445 (+23) |
| Files | 423 | 425 (+2) |
Full unit suite green (4436 → 4445 — minus 1 pre-existing W2-sibling
file with a vitest-4 signature deprecation that is NOT in my touch
set).
## Code-review findings (self-review applied)
The brief asked for `superpowers:code-reviewer` dispatch. The
subagent invocation skill returned guidance only (no Task tool
available from inside this agent), so I ran a manual self-review
against the focus areas the brief called out. Findings and
disposition:
| Severity | Finding | Disposition |
| --- | --- | --- |
| High | Grouped-view pagination was wired to PAGE_SIZE=25 but the route returns up to 5000 rows in one shot — `setPage(2)` would re-fire the same scan with offset=0 returning the same slice. | Fixed in c7480e90: pagination disabled on the grouped path; scan limit raised to 5000; offset pinned to 0. |
| High | The `dayKey` drill-down's "±12h around canonical noon" comment claimed support only for whole-hour and half-hour zones, but the maths is in fact correct for every IANA offset (the noon instant already encodes the zone's offset). | Comment corrected in c7480e90; an explicit Kathmandu/Chatham note added so future readers don't second-guess. |
| Medium | The grouped path's response carries `total = number of synthesised rows` and the count caption renders "X measurements" — semantically that's now "X days" in the cumulative case. | Deferred. New translation key would be needed; the integer is still informative. Tracked for v1.4.38 polish. |
| Medium | `expandedDayKeys` state persists across page changes — fine since the grouped path is single-page now, but if pagination is re-enabled it would carry over to other pages. | Deferred. Pagination is currently disabled on the grouped path so the state model is unreachable on it. |
| Low | Drain `cutoffAt` is computed once per invocation. A long-running drain could race a watch sync that lands just inside the cutoff. | Acceptable. The drain is idempotent — the next nightly run picks up anything missed. |
## Brief-back (≤200 words)
(a) **Sample compression**: post-drain steady state collapses each
day to one `stats:` row per cumulative type. With 5 cumulative
types × ~365 days/year = ~1 825 rows/year vs the pre-drain ~50–200
chunks/day per type → conservatively **50× compression on the
ingest stream**, more on chatty Apple-Watch days. Marc's account
should drop from ~300 k cumulative rows to a few thousand once the
36 h grace window has elapsed twice (two nightly runs cover the
full pre-drain backlog because the drain is unbounded by date —
only the new cutoffHours limits the recent window).
(b) **Drill-down chattiness**: the second-query pattern feels OK
because the drill-down is gated on a user click, has
`staleTime: 5 min`, and the result is bounded (~few hundred rows
worst case). For multi-day expansions in one session each day fetches
once and stays cached. If telemetry shows a hot drill-down loop a
follow-up could prefetch when the user hovers the chevron.
(c) **Edge case**: after the drain the multi-source same-day case
is resolved by construction (the drain only collapses APPLE_HEALTH;
Withings keeps its own one-row-per-day shape; `pickCumulativeDaySum`
sums per source independently). The latent double-count documented
in the research brief disappears once the drain has run.
## Quality gates
- `pnpm typecheck`: clean for owned files (the single pre-existing
failure is in `src/lib/insights/__tests__/features.test.ts` from
the W2 sibling wave's WIP).
- `pnpm lint`: clean for owned files (the two errors are in
`src/components/dashboard/medication-intake-quick-add.tsx` from a
sibling agent's WIP).
- `pnpm test --run` over the owned files: 78 / 78 pass.
- Full unit suite: 4445 pass, 1 file failing on the W2 sibling WIP.
## Deferred / out of scope
- Translation copy for FR / ES / IT / PL on the three new keys
(English fallback used, matches the existing locale-fallback
convention for those locales).
- "ALL"-filter mixed-collapse: the brief authorised collapse-always
but the practical interpretation is that the drain handles ALL
collapse at the storage layer. Pre-drain accounts still see
per-sample APPLE_HEALTH rows mixed with non-cumulative rows on
the ALL view; once the nightly drain catches up the ALL view is
naturally clean.
- Per-source priority for the day-grouped path (Withings vs
APPLE_HEALTH on the same day). Today the route sums all sources
inside a bucket — the existing `pickCanonicalSourceRows` ladder
remains the analytics-layer surface; the list view is a raw audit
view by design.
## Touch-disjoint verification
No file in my owned set overlaps with other waves' touch sets per
the marathon brief. Two marathon-collision events occurred where my
staged files rode on sibling commit messages (0af77230, 2987b8c9) —
content is intact and accurately described in this report.