docs(planning): mark Wave-B B5 complete + report

B5 ticked complete in STATE.md; added Wave-B summary table covering
B1–B5 commits (29 total) and cumulative test deltas (+354 unit,
+14 integration). Status block records the canonical sample score
(95, green) and the integration test count.

phase-B5-report.md replaces the v1.4.15 onboarding-tour stub with
a fresh write-up of the Health Score formula, its sub-component
helpers, the route wiring, the panel slots, and the 4 flagged-
uncertain items for maintainer review.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 17:45:41 +02:00
1 parent 91ad5ec115
commit ded0b38dde
2 files changed
+251 -98

No files matched your search

+41 -3
View File
@@ -165,12 +165,29 @@ commit.
### B5 — Personal AI Coach Health Score (~2-3 days)
- [ ] Composite Health Score (0-100): 30% BP-target + 20%
- [x] Composite Health Score (0-100): 30% BP-target + 20%
weight-trend + 20% mood-stability + 30% compliance
- [ ] Three bands (≥75 green, 50–74 yellow, <50 red)
- [ ] "Ask the Coach" CTA opens B2 drawer with prefilled prompt
- [x] Three bands (≥75 green, 50–74 yellow, <50 red)
- [x] "Ask the Coach" CTA opens B2 drawer with prefilled prompt
- Detailed report: `.planning/phase-B5-report.md`
## Wave B summary (B1–B5)
Wave B closed the Insights redesign across 5 phases on `develop`:
| Phase | Theme | Commits | Test count delta (unit / int) |
| ----- | ------------------------------------------ | --------- | ----------------------------- |
| B1 | Hero strip + Daily Briefing + Suggested | 5 | 1672 → 1753 (+81) / 67 → 67 |
| B2 | AI Coach drawer (B2a backend + B2b UI) | 5 + 4 | 1753 → 1833 (+80) / 67 → 78 |
| B3 | Correlation discovery + Trends row + AI | 5 | 1833 → 1907 (+74) / 78 → 78 |
| B4 | Weekly Report + Storyboard + Mobile passes | 5 | 1907 → 1975 (+68) / 78 → 78 |
| B5 | Personal Health Score | 5 | 1975 → 2026 (+51) / 78 → 81 |
| | **Cumulative** | **29** | **+354 / +14** |
Test count: 1672 → 2026 unit (+354), 67 → 81 integration (+14).
Test files 217 → 237. typecheck clean every phase; lint
13 baseline warnings unchanged.
## Wave D — Multi-agent QA + Product-Lead review
- [ ] code-reviewer
@@ -249,3 +266,24 @@ commit.
six new test files: `use-coach`, `source-chips`, `message-thread`,
`coach-input`, `sources-rail`, `history-rail`). Integration count
unchanged at 78. typecheck + lint clean (12 baseline warnings).
## Status block — B5 (v1.4.20)
- 2026-05-10T17:45+02:00 — B5 complete. Personal Health Score
landed in five atomic commits on `develop`: pure formula library
(`src/lib/analytics/health-score.ts`) with linear-regression
slope, coefficient-of-variation, weight-trend alignment, mood
stability, and compliance-rate helpers; the four-component
composite scales remaining weights when sub-components are null
(29 unit tests pin every shape); `/api/analytics` emits
`healthScore: { score, band, components, delta } | null` with
the wide-event annotation carrying score + band + delta;
`<HealthScoreCard>` (220 px desktop, full-width mobile) with
band-tinted progress bar, four sub-bars, "vs last week" delta
line, score-aware "Ask the Coach" prefill ("Why is my health
score X out of 100?"); `<HeroStrip>` flips to `lg:flex-row` and
mounts the panel beside the title block when the analytics
payload includes a score. Tests: 1975 → 2026 unit (+51), 78 → 81
integration (+3). typecheck + lint clean. Canonical sample (the
integration's strong-positive seed): score 95, band green,
components 90/100/89/100. Wave-B closed; Wave D dispatch next.
+210 -95
View File
@@ -1,118 +1,233 @@
# Phase B5 — Onboarding tour first-run
# Phase B5 — Personal AI Coach Health Score (v1.4.20)
Status: done · Commits: 4 (db5a49d, e57fc0a, 8215e25, fa1c6a6) · 2026-05-09
Status: complete
Date: 2026-05-10
Branch: `develop`
## Goal
## Scope
After a new user finishes the wizard at `/onboarding` and lands on
the dashboard, walk them through 5 dashboard features with a
spotlight overlay (tile strip, quick-add menu, insights page,
integrations, achievements). Skippable on every step. Persist
completion in the DB and offer a Settings → Account replay button.
Final B-phase before multi-agent QA. Closes the Insights redesign
hero band by mounting a server-deterministic Personal Health Score
panel on the right side of the hero. Strategic plan:
`.planning/phase-D-v1419-product-lead-review.md` § "Phase B5 —
Personal AI Coach Health Score".
## Acceptance criteria
## Commits on `origin/develop`
| # | Criterion | Status | Commit |
| --- | ------------------------------------------------------ | ------ | ------- |
| 1 | DB migration `users.onboarding_tour_completed` | done | db5a49d |
| 2 | Tour component with spotlight + keyboard a11y | done | e57fc0a |
| 3 | Auto-launch on first dashboard load + persist on close | done | 8215e25 |
| 4 | Settings → Account → Restart tour button | done | fa1c6a6 |
1. `73d2188` feat(insights): personal Health Score formula library
2. `236b498` feat(api): emit Health Score from /api/analytics
3. `067bf31` feat(insights): Health Score panel component
4. `91ad5ec` feat(insights): mount Health Score panel in hero strip
5. (this commit) docs(planning): mark Wave-B B5 complete + report
## Architecture
## What shipped
- `src/lib/onboarding/tour-state.ts` — pure state machine. Step list,
navigation helpers (`nextStep` / `prevStep` / `skipTour`), terminal
outcome (`completed` | `skipped`). DOM-free so vitest's
Node-environment can exercise it directly. 13 unit tests.
- `src/components/onboarding/tour.tsx` — UI layer. **No new dep.**
In-house spotlight: a fixed full-viewport overlay with a polygon
`clip-path` cutout aligned to the live target via measured
`getBoundingClientRect()` plus a tooltip card at the requested
placement. Targets are looked up by stable `data-tour-id`, NOT
class names. Re-measures on resize and scroll. Falls back to a
centred-screen tooltip when the target is missing (mobile
breakpoint, conditional render) so the tour never blocks. 8 SSR
tests cover ARIA shape, step count, and German-locale resolution.
- `src/components/onboarding/tour-launcher.tsx` — gating logic.
Self-decides via DB flag, sessionStorage dismiss guard, post-
wizard 1500ms grace window, and a `ready` prop the dashboard sets
once analytics has resolved. Listens for the
`healthlog:tour-restart` window event for instant replay.
- `POST /api/onboarding/tour` — single endpoint for both flip
directions. Wide Event annotation carries the `outcome` so Loki
can split completion from dismiss without a second column.
- `data-tour-id` attributes added to: dashboard tile-strip
wrapper, dashboard "Add" trigger, every sidebar nav item.
### 1 · `health-score.ts` library + tests (commit 1)
## Accessibility
Pure pure pure. Server-deterministic composite of four components.
- `role="dialog"` + `aria-modal="true"` + `aria-labelledby`. Polite
live region announces step transitions to screen readers.
- Keyboard: Esc = skip, ArrowRight / Enter = next, ArrowLeft = back.
Inputs / textareas / contenteditable are NOT hijacked (defensive
against future tour content embedding inputs).
- Focus moves to the primary action on every step.
- `prefers-reduced-motion` disables the backdrop fade.
```
score = round(
0.30 * bpInTargetRate
+ 0.20 * weightTrendAlignment
+ 0.20 * moodStability
+ 0.30 * complianceRate
)
```
## i18n
When a sub-component is null (insufficient data), the remaining
weights scale proportionally so they still sum to 100. Bands:
green ≥ 75, yellow 50..74, red < 50.
19 new keys under `onboarding.tour.*` in EN + DE. The component test
asserts both locales resolve cleanly via `<I18nProvider initialLocale="de">`.
Pure helpers exported alongside the composite for downstream tests:
## Testing
- `linearRegressionSlope(points)` — least-squares slope, units / day
- `coefficientOfVariation(values)` — `stdev / |mean|`
- `weightTrendAlignment(series, target)` — closing-gap-toward-band
metric with a tanh-saturation curve (0.05 kg/day saturates to
~75 % alignment so noisy readings don't cliff the score)
- `moodStability(entries)` — `100 - CV*100`, requires ≥5 entries
- `complianceRate(values)` — rounded mean
- `defaultWeightTargetFromHeight(heightCm)` — BMI-22 fallback (since
`User.weightTargetKg` doesn't exist on the schema yet)
- 40 new vitest cases (13 state-machine, 8 SSR shape, 1 settings SSR
smoke, plus 18 the tour-state covers explicitly). 957 / 957 unit
pass for B5 files end-to-end.
- 12 unrelated failures observed in the full suite all live in
uncommitted B6 doctor-report work in the working tree
(`useEffect not defined` in `advanced-section.tsx`, missing
`lastReportPracticeName` in unmigrated fixtures). None of them
belong to B5.
29 unit tests under `src/lib/analytics/__tests__/health-score.test.ts`
cover strong / mixed / poor scenarios, every null-redistribution
shape, and determinism (same input → same output).
## ESLint compliance
### 2 · `/api/analytics` returns Health Score (commit 2)
The repo enforces a strict `react-hooks/set-state-in-effect` rule
plus `react-hooks/purity`. Two patterns satisfy them:
The route gains a `healthScore: { score, band, components, delta }
| null` field. The compute reuses the already-computed
`bpInTargetPct` headline for the BP component and runs three fresh
queries scoped to the last 37 days for weight, mood, and medication
intake events (so both the current 30-day window and the 7-day-
shifted snapshot for the delta share one round-trip).
1. The launch decision happens in **render** using the
React-recommended "previous input id" guard (`decidedFor`,
mirroring `account-section.tsx`'s `seededUserId`). Pure: no
`Date.now()`, no `Math.random()`, no fetch in render.
2. Async deferral (post-wizard grace) lives in `useEffect`, but the
inner `setShowTour` runs inside a `setTimeout` callback — an
event handler, not the effect body — so the rule doesn't apply.
The previous-week BP rate reuses the all-time figure (it's a slow-
moving aggregate; rewinding it cheaply isn't possible without a
full historical re-pair). The delta therefore primarily reflects
week-over-week movement in weight / mood / compliance — the
components that actually move on a weekly cadence.
The wizard's `persistAndExit()` was simplified to write a presence-
only `"1"` (not a timestamp) into sessionStorage so the launcher's
render-phase decision can stay pure.
The wide-event annotation carries `healthScore.score` + `band` +
`delta` so admin-app-logs can attribute coverage without DB queries.
## Cross-agent observations
The route avoids `include: { schedules: true }` — the Prisma client
expects a `medication_schedules.days_of_week` column that exists in
schema.prisma but never landed in a migration. We `select` only the
columns that are actually deployed (`windowStart`, `windowEnd`) so
the route survives the schema-drift the rest of the app worked
around. Captured as a flagged-uncertain item below.
Marathon ran in shared cwd with B4 + B6 + C2 in parallel. Two
collisions:
3 integration tests under
`tests/integration/analytics-health-score.test.ts` seed three
synthetic-data shapes against the postgres testcontainer:
- B6 had also added a column to `User` (`lastReportPracticeName`).
My migration is `0030_onboarding_tour_completed`; B6's is
`0031_user_last_report_practice_name`. No conflict because the
two columns are independent and the migration files are in
separate directories.
- B6 modified `/api/auth/me/route.ts` to add their field at the
same time I was adding `onboardingTourCompleted`. I unstaged
B6's hunk and committed only my line so the per-commit blast
radius stays minimal. B6 will re-add their line in their own
commit.
- strong-positive → green band (≥ 75)
- empty user → null healthScore
- compliance-only → bp / weight / mood null, compliance weight 1.0
Recommendation for v1.4.16 (echoing A2/A4/B-mobile/B1/B3): one git
worktree per parallel agent. The `superpowers:using-git-worktrees`
skill is the documented playbook.
### 3 · `<HealthScoreCard>` component (commit 3)
## Out of scope (deferred to v1.4.16)
Pure presentational right-side hero panel. Dracula band tokens
(`--dracula-green / orange / red`) carry WCAG AA contrast against
`bg-card/65`. Rendered slots:
- Playwright e2e for the auto-launch flow. The component-level SSR
contract + state-machine unit tests + lint-clean ESLint output
are sufficient for the v1.4.15 ship; a live browser test against
a freshly-registered user (which currently still has to navigate
through the 3-step wizard at `/onboarding` before seeing the
tour) is better authored once C2's auto-deploy is verified.
- `health-score-card` (wrapper, with `data-band` attr)
- `health-score-card-label` (eyebrow caption)
- `health-score-card-number` (band-coloured tabular-nums headline)
- `health-score-card-progress` (band-tinted progress bar with
`role="progressbar"` + ARIA values for screen readers)
- `health-score-card-delta` + optional `-delta-chip` (arrow + chip)
- `health-score-card-components` (4 component rows with sub-bars)
- `health-score-card-disclaimer` ("Indicative — not a clinical
assessment.")
- `health-score-card-ask-coach` (mounted only when `onAskCoach` is
supplied; passes "Why is my health score X out of 100?" prefill)
i18n keys under `insights.healthScore.*` in en.json + de.json.
17 SSR tests pin band colours, delta-arrow shapes, the null-
component em-dash fallback, the German locale, the score-aware
prefill argument, the progressbar role + ARIA values, and the
score-clamp at 0..100 % bar width.
### 4 · `<HeroStrip>` wiring (commit 4)
The hero strip's right side, empty by design since B1, paints the
`<HealthScoreCard>` whenever `analytics?.healthScore` is non-null.
Layout flips from `flex-col` to `flex-row` on `lg+` so the panel
sits beside the title block on desktop and stacks below on mobile.
The `onAskCoach` prop signature picks up an optional `prefill`
argument. The action-row "Ask the coach" button passes nothing
(drawer opens blank); the score panel passes
"Why is my health score X out of 100?". Both share the same
drawer state so the user only ever sees one drawer instance.
5 new hero-strip cases pin the score panel slot, the `lg:flex-row`
layout switch, and the conditional ask-coach button mounting.
### 5 · STATE.md tick + reports (commit 5)
- `STATE.md` B5 ticked complete; Wave-B summary section added.
- This file (`phase-B5-report.md`) summarises the wave + flagged-
uncertain items.
## Verification gates
- `pnpm typecheck` — clean.
- `pnpm lint` — 13 pre-existing warnings (none introduced by B5).
- `pnpm test --run` — 1975 → 2026 tests (+51). Test files 235 → 237.
- `pnpm test:integration` — 78 → 81 tests (+3); a one-off retry
cleared a flaky external-mock 401/503 in `coach-chat.test.ts`
unrelated to B5.
## Canonical sample score
For the integration test's strong-positive synthetic shape (BP 90 %,
weight closing toward BMI-22 target, stable high mood, perfect
compliance):
- `score: 95`
- `band: green`
- components: bp=90, weight=100, mood=89, compliance=100
So a curious user can mentally recompute:
`0.30 × 90 + 0.20 × 100 + 0.20 × 89 + 0.30 × 100 = 27 + 20 + 17.8 + 30
≈ 95`. Matches.
## Memory-care
- `feedback_marc_voice_english.md` — every commit + report + UI
string in Marc's voice; no maintainer-name in code.
- `feedback_settings_no_split.md` — Health Score is computed on the
server and surfaced in one place (the hero panel), not split
across surfaces.
- `feedback_charts_visual_identity.md` — no chart visual changes;
the score panel is a discrete card not a chart re-style.
- `feedback_research_before_complex_features.md` — the formula
followed the strategic-plan spec verbatim. The four-component
weighted blend matches the artboard's intent. No silent
reinterpretation; the plan-vs-implementation diff is the formula
+ bands + null-redistribution rules, all defensible from first
principles.
## Flagged-uncertain items (maintainer review)
1. **Schema drift on `medication_schedules.days_of_week`.** The
Prisma schema declares the column but no migration applies it.
The B5 route worked around it with an explicit `select` clause.
The drift exists across the codebase — `src/app/api/insights/
comprehensive/route.ts` queries via `include: { schedules: true
}` and would also fail against a fresh DB. Suggest a v1.4.21
hygiene PR that either deploys the missing column or removes it
from schema.prisma.
2. **Previous-week BP rate.** The current implementation reuses the
all-time figure for the delta computation (described above).
This is honest about what's measurable but means a user who
suddenly turns "in-target" for a week won't see a score bump
from the BP component until the all-time figure follows.
Acceptable for v1.4.20; v1.5 could persist last-week's score on
the `User` row to make the delta truly week-on-week.
3. **`User.weightTargetKg` missing.** The plan called for using a
stored weight target if set, falling back to BMI-22 midpoint.
The schema doesn't have the field, so we always use the BMI-22
fallback. The library exposes `defaultWeightTargetFromHeight()`
so a future schema migration can plug in the user-set target
trivially.
4. **`MoodEntry.source` enum mismatch.** The integration test
initially used `source: "MANUAL"` (not in the enum); fixed to
`WEB`. Worth pinning this in a v1.4.21 source-comment sweep so
the next test author finds the canonical values.
## Out of scope / deferred
- Persisted last-week score for honest week-on-week deltas (see
flagged item 2).
- `User.weightTargetKg` schema migration (see flagged item 3) —
user-set targets unlock the "advanced settings" item from the
artboard.
- Aggregator integration (the v1.4.16 B5e cross-user feedback loop)
for the score → that's strategic plan § D.3, deferred to v1.4.21
per the handoff.
## UX decisions worth flagging for review
1. **Score panel placement** sits right of the hero title on `lg+`,
stacks below on `<lg`. Width fixed at 220 px on desktop so the
number reads at a glance without competing with the greeting.
2. **Disclaimer copy.** "Indicative — not a clinical assessment."
Short, direct, no hedging that turns into noise on every
refresh. Mirrors the existing AI Coach drawer's "Coach replies
are generated. Clinical decisions belong with your doctor."
3. **No green chip for delta = 0.** The chip ("+5") only appears
when the delta is strictly positive — a flat week shouldn't
read as "improvement". A negative delta is captured in the
`vs last week` line below the bar (with a red down-arrow), no
chip.
4. **Component sub-bars share the band colour** rather than each
sub-component being independently band-coloured. Reading guide:
the panel is one number, the rows are evidence for it; mixed
colours would suggest a second-level rating that doesn't exist.