Commit Graph
634 Commits
Author SHA1 Message Date
Marc-André BombeckandClaude Opus 4.7 dd2ef570d9 Merge main into develop — sync Dependabot dep bumps before v1.4.20 tag
Brings in 7 dependency PRs auto-merged on main while v1.4.20 was in
flight on develop:
- chore(deps-dev): @tailwindcss/postcss 4.2.4 → 4.3.0 (#156)
- chore(deps-dev): shadcn 4.6.0 → 4.7.0 (#157)
- chore(deps-dev): eslint-config-next 16.2.4 → 16.2.6 (#159)
- chore(deps): zod 4.4.2 → 4.4.3 (#160)
- chore(deps): next 16.2.4 → 16.2.6 (#158)
- chore(deps-dev): tailwindcss 4.2.4 → 4.3.0 (#154)
- chore(deps-dev): @types/node 25.6.0 → 25.6.2 (#153)

Resolves package.json conflict on the F5 metadata block + 1.4.20 bump
and pnpm-lock.yaml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:25:27 +02:00
Marc-André BombeckandClaude Opus 4.7 aac8b5ebdb chore(release): v1.4.20
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:24:37 +02:00
Marc-André BombeckandClaude Opus 4.7 73e51d785e style: prettier on touched insights/coach surfaces
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:23:20 +02:00
Marc-André BombeckandClaude Opus 4.7 21d064cfc6 docs(planning): record Wave D reconcile + carry-over backlog
11 commits landed on develop covering every Wave-D HIGH (13/13), the
design pushback on the disabled weekly-report button, 6 MED items,
and every simplify-apply-yes item. 22 MED + 16 LOW + 4 simplify-
apply-maybe deferred to v1421-backlog under a new "Phase D — v1.4.20
reconcile carry-over" section. Tests unchanged at 2026 unit + 81
integration; typecheck + lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:17:55 +02:00
Marc-André BombeckandClaude Opus 4.7 31fbf98c4a chore(i18n): drop duplicate heroGreetingNight key
Both EN + DE have heroGreetingNight pointing to the same string as
heroGreetingEvening (the night bucket maps to "Good evening" so 03:00
doesn't read as Morgen). Drop the duplicate key and fall through to
heroGreetingEvening in the resolver — the resolver decided the
collapse, the JSON catalog carried dead weight.

Fixes Design-M2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:14:59 +02:00
Marc-André BombeckandClaude Opus 4.7 9f3baf940d fix(insights): MED-bucket polish across coach + report surfaces
- weekly-report eyebrow chip drops uppercase + tracking-wide for the
  sentence-case rule the rest of the app follows; print stylesheet
  now neutralises bg + colour so monochrome output reads clean
  (Design-M4, Design-L3).

- correlation-card "Try a 7-day experiment" CTA flips from default to
  outline while disabled. Disabled-primary read as "the system is
  broken right now"; outline reads as "this is on the roadmap"
  (Design-M7).

- coach-drawer settings cog aria-label switches to the coming-soon
  tooltip copy so SR users hear the same "arrives in v1.4.21" message
  sighted users see (Design-M9).

- sources-rail static "fresh" dot becomes aria-hidden — v1.4.20 has
  no real freshness state behind it; SR users no longer hear "Fresh"
  five times in a row (Design-L4).

- summariseTitle slices on Unicode code points rather than UTF-16
  code units so a user message ending mid-emoji ("🩺") no longer
  produces a half-character on disk (Code-LOW-04).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:11:16 +02:00
Marc-André BombeckandClaude Opus 4.7 340239423b refactor(insights): consolidate relative-time and confidence-band tables
Three live byte-identical formatRelativeTime copies (hero-strip,
daily-briefing, coach history-rail) lifted to src/lib/i18n/relative-
time.ts. Both correlation-card and trend-annotation duplicated the
exact same low/moderate/high Dracula classnames; extracted to
src/components/insights/confidence-badge.ts (per-component i18n
labels stay local — the copy differs).

Drop the dead "null" placeholder branch in useCoachConversation's
queryKey: the ternary picked a different key when id was null, but
enabled: id !== null kept the query disabled so the placeholder never
fired. Key directly on id.

Hoist STORYBOARD_COLOR_BY_CATEGORY in /insights/page.tsx to module
scope so the map isn't recreated on every render.

Fixes Simplify-S-02, Simplify-S-03, Simplify-S-04, Simplify-S-05,
Code-LOW-02.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:09:45 +02:00
Marc-André BombeckandClaude Opus 4.7 cfe6d1c577 chore(api): drop maintainer-name comment in analytics route
The Phase D security review flagged a "Marc's data (572 paired
readings, …)" comment in the analytics route. It predates v1.4.20
but the file received new commits in this release, so neutralise the
example to a generic "busy account" wording that conveys the same
shape without leaking a maintainer name.

Fixes Security-LOW-4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:06:49 +02:00
Marc-André BombeckandClaude Opus 4.7 346867b468 fix(insights): isolate hero glow from sticky section nav
The hero band's glow-purple box-shadow bled through the sticky
section nav (bg-background/80 + backdrop-blur) so the nav painted
with a faint purple cast on scroll. Add `isolate` to the hero
container so the shadow stays z-trapped inside its own stacking
context.

Fixes Design-HIGH-4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:06:32 +02:00
Marc-André BombeckandClaude Opus 4.7 f9af100d9d fix(coach): cap drawer width on lg+ and route sources rail via tray
The drawer's lg:max-w-[1080px] cap left only ~200-286px of underlying
/insights visible on common laptop viewports (1280-1366px), turning
the "keep dashboard context behind it" sheet into a near-takeover.

Drop the lg cap to min(960px,75vw) with the inline sources rail
hidden below xl — the rail still surfaces on lg via the existing
mobile chevron tab. xl+ restores the full three-column layout. The
history rail stays visible inline on lg+ (it's the more frequently
referenced of the two).

Update the SSR test contract to match: history trigger hides on lg+,
sources trigger hides on xl+.

Fixes Design-HIGH-1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:06:18 +02:00
Marc-André BombeckandClaude Opus 4.7 8479e05a20 a11y(coach): announce streaming assistant text to screen readers
The streaming assistant bubble updated in place as the SSE tokens
landed but had no aria-live region — sighted users saw live text
while screen-reader users heard silence. Wrap the in-flight bubble
in role=log + aria-live=polite + aria-relevant=additions text so the
assistant prose announces progressively (matches the daily-briefing
pattern that already uses aria-live for its loading state).

Fixes Design-HIGH-3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:04:39 +02:00
Marc-André BombeckandClaude Opus 4.7 3c353fe012 fix(insights): bump suggested-prompt chip touch target to 36px floor
Chips painted at ~28px tall (px-3 py-1.5 text-xs). On a 375px viewport
the chip strip became a fingertip lottery. Bump to min-h-9 with
px-3.5 py-2 text-[13px] so chips clear the 36px mobile touch baseline
without the type-shrink that triggered the regression.

Fixes Design-HIGH-2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:04:22 +02:00
dependabot[bot] 6f43a6218f chore(deps-dev): Bump @tailwindcss/postcss from 4.2.4 to 4.3.0 (#156)
Bumps [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) from 4.2.4 to 4.3.0.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.0/packages/@tailwindcss-postcss)

---
updated-dependencies:
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-10 16:04:14 +00:00
Marc-André BombeckandClaude Opus 4.7 046d78862a feat(insights): enable Generate weekly report button on hero
Phase B4 shipped /insights/report/[week] but the hero action-row
button stayed disabled with a "Coming soon" tooltip. Wire it into the
current ISO week so a click opens this week's report. Older parents
that don't pass weeklyReportHref still get the disabled placeholder.

Fixes Design pushback (item 6).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:04:07 +02:00
Marc-André BombeckandClaude Opus 4.7 249607d0b2 perf(coach): bound buildCoachSnapshot to last 90 days
Every POST /api/insights/chat called extractFeatures(userId, false),
which ran prisma.measurement.findMany({ where: { userId } }) with no
date filter — fetching every measurement the user had ever recorded.
For power users with multi-year Withings imports that's tens of
thousands of rows on every Coach turn.

Add an optional sinceDays bound to extractFeatures (default unchanged
= full history; analytics dashboard + insights/generate keep their
existing behaviour) and have the Coach snapshot pass sinceDays: 90.
The Coach already only narrates the last7 / last30 / last90 windows,
so the 90-day floor preserves every metric the snapshot surfaces.

Fixes Senior-HIGH-3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:02:43 +02:00
dependabot[bot] 8e17f46d1f chore(deps-dev): Bump shadcn from 4.6.0 to 4.7.0 (#157)
Bumps [shadcn](https://github.com/shadcn-ui/ui/tree/HEAD/packages/shadcn) from 4.6.0 to 4.7.0.
- [Release notes](https://github.com/shadcn-ui/ui/releases)
- [Changelog](https://github.com/shadcn-ui/ui/blob/main/packages/shadcn/CHANGELOG.md)
- [Commits](https://github.com/shadcn-ui/ui/commits/shadcn@4.7.0/packages/shadcn)

---
updated-dependencies:
- dependency-name: shadcn
  dependency-version: 4.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-10 16:02:14 +00:00
Marc-André BombeckandClaude Opus 4.7 e632e26854 fix(coach): repair SSE idempotency and error-frame handling
- Drop the withIdempotency wrap on POST /api/insights/chat. The wrapper
  caches response bodies via cloned.text() then replays through
  NextResponse.json(JSON.parse(...)) — turning the SSE wire format into
  a null body on retry. The PWA never sends Idempotency-Key so the bug
  is hidden today; the iOS client would walk straight into it. Dedup
  still holds via the conversationId existence check + 20-turn cap.

- Read the SSE response body even on !response.ok when the content type
  is text/event-stream so the structured error frame the route emits
  surfaces its errorCode to the bubble. Without this the stream was
  read only on 2xx and the rich error channel was dead code.

- Stabilise the useSendCoachMessage opts reference via useRef so an
  inline-literal { onDone } from the drawer no longer re-memoises send
  on every render.

- Drop the unused snapshot parameter from streamProviderError (the
  ReturnType<typeof Object.assign> alias widened to any). Switch the
  error response to status 200 so the streaming client reads the SSE
  body and parses the error frame instead of branching on HTTP status.

Fixes Code-HIGH-1, Code-HIGH-2, Code-HIGH-3, Code-HIGH-4, Security-M-1,
Senior-HIGH-1, Simplify-S-01.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:01:40 +02:00
dependabot[bot] 6e5a8a7ba5 chore(deps-dev): Bump eslint-config-next from 16.2.4 to 16.2.6 (#159)
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 16.2.4 to 16.2.6.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v16.2.6/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 16.2.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-10 16:01:32 +00:00
dependabot[bot] f39249bdf0 chore(deps): Bump zod from 4.4.2 to 4.4.3 (#160)
Bumps [zod](https://github.com/colinhacks/zod) from 4.4.2 to 4.4.3.
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.4.2...v4.4.3)

---
updated-dependencies:
- dependency-name: zod
  dependency-version: 4.4.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-10 15:59:42 +00:00
dependabot[bot] 8ba5a70e8c chore(deps): Bump next from 16.2.4 to 16.2.6 (#158)
Bumps [next](https://github.com/vercel/next.js) from 16.2.4 to 16.2.6.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.2.4...v16.2.6)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-10 15:57:36 +00:00
dependabot[bot] 114b8311b4 chore(deps-dev): Bump tailwindcss from 4.2.4 to 4.3.0 (#154)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 4.2.4 to 4.3.0.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.0/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-10 15:55:03 +00:00
dependabot[bot] 60c501f49f chore(deps-dev): Bump @types/node from 25.6.0 to 25.6.2 (#153)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.6.0 to 25.6.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.6.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-10 15:54:37 +00:00
Marc-André BombeckandClaude Opus 4.7 ded0b38dde 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>
2026-05-10 17:45:41 +02:00
Marc-André BombeckandClaude Opus 4.7 91ad5ec115 feat(insights): mount Health Score panel in hero strip
The hero strip's right side, empty by design since B1, now paints the
<HealthScoreCard> when `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 so the same handler powers both surfaces — the action-row
button passes nothing (drawer opens blank), the score panel passes
"Why is my health score X out of 100?". One drawer instance, one
state pair, one consistent UX.

Tests: 5 new hero-strip cases pin the score panel slot, the lg-row
layout switch, and the conditional ask-coach button mounting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:42:53 +02:00
Marc-André BombeckandClaude Opus 4.7 067bf312c3 feat(insights): Health Score panel component
Pure presentational <HealthScoreCard> for the right-side hero
panel. Surfaces the composite 0..100 score with a band-coloured
number, a band-tinted progress bar, four sub-component rows
(BP / Weight / Mood / Compliance) with their individual scores, a
"vs last week" delta line with arrow + chip, the "Indicative — not
a clinical assessment" disclaimer, and an "Ask the Coach" button
that pre-fills the drawer with "Why is my health score X out of
100?".

Layout: 220 px wide on desktop, full-width on mobile so it stacks
below the hero title block on `<lg`. Dracula band tokens
(`--dracula-green/orange/red`) carry WCAG AA contrast against
`bg-card/65`.

Tests: 17 SSR cases covering each band, delta-arrow shapes, the
null-component em-dash fallback, the German locale, the score-aware
prefill argument format, the progressbar role + ARIA values, and
the score-clamp at 0..100 % bar width.

i18n keys land under `insights.healthScore.*` in en.json + de.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:40:15 +02:00
Marc-André BombeckandClaude Opus 4.7 236b4986a4 feat(api): emit Health Score from /api/analytics
The route now builds the four-component Health Score input from the
already-fetched BP figure plus a fresh weight + mood + medication
intake fetch (last 30 days, plus a 7-day-shifted snapshot for the
delta). The response gains a `healthScore: { score, band,
components, delta } | null` field; null when no component is
computable so the UI hero panel can hide cleanly.

The wide-event annotation carries `healthScore.score` + `band` +
`delta` so admin-app-logs can attribute coverage without DB queries.

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). Delta therefore primarily reflects week-
over-week movement in weight / mood / compliance — the components
that actually move on a weekly cadence.

Integration coverage seeds three synthetic-data shapes against the
postgres testcontainer:
- strong-positive → green band (>= 75)
- empty user → null healthScore
- compliance-only → bp/weight/mood null, compliance weight 1.0

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:37:20 +02:00
Marc-André BombeckandClaude Opus 4.7 73d21881a8 feat(insights): personal Health Score formula library
Composite 0-100 score with three bands (green >= 75, yellow 50..74,
red < 50). Server-deterministic so the same inputs always produce the
same output.

Sub-components:
- bpInTargetRate (30 %) — reuses the existing all-time figure
- weightTrendAlignment (20 %) — slope vs target band over 30 days
- moodStability (20 %) — `100 - CV*100`, requires >= 5 entries
- complianceRate (30 %) — mean of medication compliance30

Null components scale the remaining weights proportionally so the
score never lies about what was actually measured. Pure helpers
(linearRegressionSlope, coefficientOfVariation, weightTrendAlignment,
moodStability, complianceRate) live alongside the composite.

Tests cover strong/mixed/poor scenarios, every null-redistribution
shape (bp null, bp+mood null, all-but-compliance null, all null) and
determinism (same input -> same output).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:31:08 +02:00
Marc-André BombeckandClaude Opus 4.7 54d3e92acc docs(planning): mark Wave-B B4 complete + report
Five-commit Phase B4 lands the printable weekly report route, the
hero banner card surfacing fresh weekly reports, additive storyboard
annotations on the 90-day BP chart, and mobile rail trays for the AI
Coach drawer. PROMPT_VERSION 4.20.1 -> 4.20.2 with new GROUND RULES
10/11 covering both new schema blocks.

Tests 1902 -> 1975 (+73). Typecheck + lint clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:24:59 +02:00
Marc-André BombeckandClaude Opus 4.7 ebb2bc0602 feat(coach): mobile rail trays for the AI Coach drawer
The history + sources rails were desktop-only since B2b. B4 surfaces
them on `<lg` via two chevron-button triggers anchored to the inside
edges of the message thread. Tapping a trigger opens a side <Sheet>
that hosts the same HistoryRail / SourcesRail instance the desktop
layout uses, so the user can browse conversations or check what data
the Coach is reading without losing message-thread context.

Refactors the body grid into a separate <CoachDrawerBody> presentational
component so the SSR test harness can pin the trigger slots + their
lg:hidden Tailwind variant without rendering the outer Radix Sheet
portal (which is client-only).

Selecting a conversation from the mobile history tray closes the tray
automatically so the user lands back on the message thread.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:22:38 +02:00
Marc-André BombeckandClaude Opus 4.7 ff1e335c19 feat(charts): storyboard annotations on HealthChart
Add an additive annotations[] prop to <HealthChart> that paints a
vertical Recharts <ReferenceLine> per AI-narrated event on the
underlying timeline. The line + tooltip + comparison overlay remain
untouched — annotations sit between the cartesian grid and the data
lines so they read as orientation, never as data.

The /insights page wires advisor.payload.insights.storyboardAnnotations
into the 90-day BP chart, mapping the four canonical categories to
Dracula colours (medication=pink, event=cyan, milestone=green,
warning=orange).

Pure helper resolveAnnotationPositions() handles the date->pointIndex
mapping with a 7-day snap window (so annotations don't stack on
monthly buckets) and a 24-char truncation for the small-viewport
label. The unit suite pins exact-day pinning, near-day snapping,
out-of-window dropping, and label truncation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:17:45 +02:00
Marc-André BombeckandClaude Opus 4.7 f1868453af feat(insights): hero strip weekly-report banner card
Add a slim banner-card on <HeroStrip> that surfaces a fresh weeklyReport
between the title block and the action row. Three actions:

- Read   -> Next.js Link to /insights/report/[week]
- Share  -> navigator.share() with clipboard fallback + sonner toast
- Export PDF -> deep-link to the report URL with ?print=1 so the report
  page auto-fires window.print() after first paint

The banner only renders when the parent passes weeklyReportReady; the
insights page derives that prop from advisor.payload.insights.
weeklyReport so the banner appears the moment a fresh PROMPT_VERSION
4.20.2 generation lands.

EN + DE translations under insights.heroBanner.* (Read / Share / Export
PDF + share success/failure toasts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:14:27 +02:00
Marc-André BombeckandClaude Opus 4.7 8f18c7ff15 feat(insights): /insights/report/[week] printable weekly report
Add a newsletter-style report route at /insights/report/[week] that
parses the YYYY-Www slug, reads the cached weeklyReport block off the
AI advisor payload, and renders Summary / Going-well / Worth-watching /
Tips / Data-quality sections in a print-friendly layout.

Print export uses window.print() with Tailwind print: variants on the
toolbar (hidden) and article wrapper (full-width margins). The route
honours ?print=1 to deep-link the export PDF flow from the upcoming
hero banner card.

Includes:
- src/lib/insights/week-iso.ts — parseWeekISO / toWeekISO / weekISOToRange
- WeeklyReportView wrapper + WeeklyReportPresentation pure layer
- Empty-state with Generate CTA when the cached report's weekISO
  doesn't match the route param
- EN + DE i18n keys under insights.report.* and insights.heroBanner.*

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:11:45 +02:00
Marc-André BombeckandClaude Opus 4.7 6d4cba1aad feat(insights): weeklyReport + storyboardAnnotations schema + prompt
Extend aiInsightResponseSchema with weeklyReport (ISO week + summary +
goingWell/worthWatching/tips bullets + optional dataQualityNotes) and
storyboardAnnotations (date + label + category + detail; max 20).

Both blocks are nullable + optional so legacy 4.20.1 caches round-trip.

Bump PROMPT_VERSION 4.20.1 -> 4.20.2 and add GROUND RULE 10 (weekly
report sections match layout, conservative phrasing, no causal claims)
+ GROUND RULE 11 (storyboard annotations are factual user-logged events
with neutral prose) to both EN and DE prompts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 17:06:04 +02:00
Marc-André BombeckandClaude Opus 4.7 bbd00ca848 docs(planning): mark Wave-B B3 complete + report
Tick B3 in `.planning/STATE.md` against the six commit drops on
`develop` (correlation maths, schema + prompt rule,
TrendAnnotation + TrendsRow, CorrelationCard + CorrelationRow,
analytics route + page wiring, this report). Replace the stale
v1.4.16 host-load report with the v1.4.20 narrative — what
shipped, synthetic-data spot checks for each hypothesis, and
flagged-uncertain items for maintainer review (confidence-band
thresholds, eta-squared mapping, interpretation localisation
strategy).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:54:53 +02:00
Marc-André BombeckandClaude Opus 4.7 c83cb2311d feat(insights): wire CorrelationRow + TrendsRow into /insights
Compute the three pre-defined correlation hypotheses
(BP × compliance, mood × pulse, weight × weekday) on the trailing
30-day window in `/api/analytics` and surface the typed result
shapes on the response payload. Each runner is gated on n >= 14
+ p < 0.05; below the bar the result.status === "insufficient"
and the per-card EmptyState paints in place of a misleading
near-zero scatter.

The insights page now mounts:

- `<CorrelationRow>` between the Daily Briefing and the Advisor
  card — the layout slot the artboard reserves for the patterns
  surface.
- `<TrendsRow>` immediately below it, fed from
  `advisor.payload.trendAnnotations` (lifted client-side via
  `trendAnnotationsSchema.safeParse`). Legacy cached payloads
  predating PROMPT_VERSION 4.20.1 surface the per-metric
  empty-state hint instead of a half-rendered annotation.

The wide-event annotation captures per-hypothesis status so admin
observability can attribute coverage gaps without re-querying
the DB.

10 source-scan tests pin the load-bearing imports, the JSX mounts,
the analytics-route surfacing, and the malformed-payload null
fallback in the advisor hook.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:53:29 +02:00
Marc-André BombeckandClaude Opus 4.7 42591c6cec feat(insights): CorrelationCard + CorrelationRow components
Two cards-and-row primitives for the Insights redesign:

- `<CorrelationCard>` — single hypothesis. Tone-coloured left bar,
  scatter sparkline (deferred-bundled), confidence chip, source
  chip ("based on N paired readings · last 30 days"), interpretation
  prose, and a disabled "Try a 7-day experiment" CTA with a
  Coming-soon tooltip. Below-threshold results render the
  EmptyState in place of the chart so we never paint a misleading
  near-zero scatter.
- `<CorrelationRow>` — 2-up on `>= md`, single column on `< md`.
  Row-level correlation-disclaimer ("Patterns are observational,
  not causal — talk to your doctor if BP runs high.") sits below
  the card grid once per row, not per card.

13 tests cover OK + insufficient rendering, the disabled CTA's
tooltip wiring, the confidence chip variants, the disclaimer
position, the responsive grid, and the EN / DE locale switch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:49:00 +02:00
Marc-André BombeckandClaude Opus 4.7 2ae69d7e64 feat(insights): TrendAnnotation + TrendsRow components
Add two presentational pieces for the Insights redesign:

- `<TrendAnnotation>` — pure renderer for the AI-authored
  one-sentence annotation that sits below each Trends-row chart.
  Empty state is locale-aware ("Awaiting more data to annotate
  this trend"). Optional confidence chip surfaces a discrete
  low / moderate / high band.
- `<TrendsRow>` — 3-up responsive grid (1 column < md, 3 columns
  >= md) wrapping BP / Weight / Mood mini charts with their
  annotations. Recharts ships through the existing `next/dynamic`
  defer-load pattern so the bundle savings stay intact.

Adds the EN + DE i18n keys (trendsRow, trendAnnotation,
correlationRow). 15 unit tests cover the locale switch, the
empty-state hint per metric, the confidence chip variants, and
the row layout chrome.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:47:08 +02:00
Marc-André BombeckandClaude Opus 4.7 f9afaf77a0 feat(insights): trendAnnotations schema + prompt rule
Extend `aiInsightResponseSchema` with an optional, nullable
`trendAnnotations: { bp?, weight?, mood? }` block — one short
sentence per metric, capped at 200 characters. Each field is
independently optional so the model emits only the metrics it can
ground in the snapshot.

Insight-generator prompt picks up GROUND RULE 9 (EN + DE):
single-sentence, observational, ≤ 200 chars, banned causal
phrases enumerated. PROMPT_VERSION bumps 4.20.0 → 4.20.1 to
signal the format change so cached payloads from B1 still parse
through the `.nullable().optional()` field.

13 tests cover the boundary (200 chars), the empty-string
rejection, the legacy-payload round-trip, and the EN/DE prompt
rule presence.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:44:20 +02:00
Marc-André BombeckandClaude Opus 4.7 7261c36633 feat(insights): correlation maths for B3 hypotheses
Add `src/lib/insights/correlations.ts` with three pre-defined
hypothesis runners (BP × medication compliance, mood × resting
pulse, weight × weekday) plus pure Pearson + one-way weekday-ANOVA
helpers. Each runner gates the surface on n >= 14 paired rows AND
p < 0.05; below the bar a typed `insufficient` result lets the
card render an EmptyState rather than a misleading pattern.

Interpretations stay conservative ("a pattern worth watching" —
never "X causes Y"). 95 % confidence intervals come from a Fisher
z-transform on r and a discrete band-from-eta-squared mapping for
the weekday case. No new dependencies — the t-distribution +
F-statistic p-value approximations are inlined (Wilson-Hilferty +
Abramowitz-Stegun).

16 unit tests cover positive / negative / no-correlation / under-n /
non-significant / no-variance branches and the Monday-outlier
weekday flag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:41:34 +02:00
Marc-André BombeckandClaude Opus 4.7 90d3a80267 docs(planning): tick B2b complete, record phase B2b report
B2b drawer UI shipped in four atomic commits on develop:
- TanStack Query hooks + SSE streaming client
- Drawer shell + message thread + source chips
- Composer + history rail + sources rail
- /insights wires the drawer via the hero strip's "Ask the coach"
  button + suggested-prompt chips

Tests: 1781 → 1833 unit (+52). Integration unchanged at 78.
Six new test files cover the streaming parser, every drawer column,
the thread state machine, locale-aware labels, and the page wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:34:10 +02:00
Marc-André BombeckandClaude Opus 4.7 e54179997b feat(insights): wire Coach drawer into /insights via hero strip
Closes the loop between the hero strip's "Ask the coach" /
suggested-prompt chips and the Coach drawer:

- `<HeroStrip>` gains an `onAskCoach` prop. When supplied, the
  "Ask the coach" button drops its disabled state + Coming-soon
  tooltip and clicking calls the handler. Older parents that
  haven't adopted the drawer still get the disabled affordance —
  no breaking change.
- `<SuggestedPrompts>`'s `onPick` already routed up via
  `onPickPrompt`; the page wires it to a setter that pre-fills the
  drawer's input AND opens it in the same tick.
- `/insights` owns `coachOpen` + `coachPrefill` state. The drawer
  mounts with `key={prefill ?? "blank"}` so the lazy `useState`
  initialiser fires fresh on every chip transition, sidestepping
  the `react-hooks/set-state-in-effect` rule.

Tests:
- Updates the hero-strip ratchet to allow either disabled (no
  handler) or enabled (handler supplied) states.
- Adds three guards under `insights-polish.test.ts` that pin the
  page-side wiring (CoachDrawer import, hero handler hook-up,
  state ownership).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:32:14 +02:00
Marc-André BombeckandClaude Opus 4.7 8dac3891b8 feat(coach): composer, history rail, sources rail
Fills the three drawer slots from commit 2 with their full
implementations:

- `<CoachInput>` is a Dracula-styled `<textarea>` card. Plain Enter
  and Cmd/Ctrl+Enter both submit; Shift+Enter inserts a newline.
  The mic button is disabled with an "Voice input arrives with the
  iOS app in v1.5" tooltip — the natural shipper of voice is the
  native client. A footer disclaimer ("Coach replies are
  generated. Clinical decisions belong with your doctor.") sits
  beneath the box.
- `<HistoryRail>` reads from `useCoachConversations()`, exposes a
  substring filter, marks the active conversation, and offers a
  hover-revealed delete affordance with a confirm-then-go flow
  driven by `useDeleteCoachConversation()`.
- `<SourcesRail>` lists the five contracts the snapshot helper
  surfaces (BP, weight, pulse, mood, compliance) with a static
  fresh-dot indicator. A v1.4.21 follow-up swaps in
  `<IntegrationStatusPill>` once the rail decides whether to lean
  on `/api/auth/me` or a dedicated `/api/insights/data-sources`
  endpoint.
- `<CoachDrawer>` mounts these as the defaults so callers only
  need to pass open + onOpenChange + prefill; the slot props stay
  as escape hatches for tests.

Tests added: composer keyboard / disabled / streaming states (10),
sources-rail render + locale (5), history-rail render + locale +
empty / active states (7).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:30:04 +02:00
Marc-André BombeckandClaude Opus 4.7 195448ae19 feat(coach): drawer shell, message thread, source chips
Lays the visual scaffold for the AI Coach drawer (B2b):

- `<CoachDrawer>` wraps `<Sheet side="right">` and exposes the
  three-column layout from the artboard (history rail · message
  thread · sources rail) on `lg+`. Mobile (<lg) collapses to a
  single column with the message thread in focus and the rails
  hidden — they get reachable via tray menus in v1.4.21.
  History / sources / composer are slot props so the parent wires
  them in commit 3 without coupling them to the shell.
- `<MessageThread>` renders user / assistant bubbles, supports an
  in-flight streaming bubble fed by `useSendCoachMessage()`, and
  preserves a "user-pinned-to-bottom" auto-scroll heuristic so
  reading earlier turns doesn't yank the viewport when a new token
  lands.
- `<SourceChips>` renders the labels-only provenance envelope —
  metric + window + n-count, no raw values — matching the wire
  contract from B2a.
- i18n keys land under `insights.coach.*` for tagline, new chat,
  composer placeholder, history rail, sources rail, metric labels,
  and window labels. Both locales updated.

Drawer state (open / prefill) is consumed via a controlled prop +
lazy `useState` initialiser; the close path resets thread + composer
in the `onOpenChange` handler so we don't trip
`react-hooks/set-state-in-effect`.

Tests cover: source-chip render permutations (8), message-thread
empty / persisted / streaming / error states (8), provenance
locale-aware labels.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:34 +02:00
Marc-André BombeckandClaude Opus 4.7 c42c0a50cb feat(coach): TanStack Query hooks + SSE streaming client
Adds the data layer for the AI Coach drawer (B2b):

- `useCoachConversations()` reads `GET /api/insights/chat` for the
  history rail (default page; cursor pre-wired for v1.4.21 infinite
  scroll).
- `useCoachConversation(id)` reads the single decrypted thread.
- `useDeleteCoachConversation()` removes the thread with optimistic
  list cache update + rollback on failure.
- `useSendCoachMessage()` POSTs to `/api/insights/chat`, walks the
  body via `fetch().body.getReader()`, and decodes SSE frames
  (`data: <json>\n\n`) with `TextDecoder`. Token / provenance / done /
  error events update a `streaming` state object that the message
  thread will render as the live assistant bubble. On `done` we
  invalidate the conversation cache + the rail so the next mount sees
  the canonical persisted shape.
- `parseSseChunk()` is exported as a pure helper so the parser is
  unit-testable without a live network stream — 11 cases cover
  single-frame, multi-frame, byte-by-byte interleaving, malformed
  payload tolerance, and the full token + provenance + done round-trip.

No third-party SSE library introduced — `fetch` + `getReader()` is
enough.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:20:04 +02:00
Marc-André BombeckandClaude Opus 4.7 e7287d34e7 docs(planning): tick B2a complete, record phase B2a report
The Coach backend half of v1.4.20 wave B is complete; STATE.md
splits B2 into B2a (backend, done) + B2b (drawer UI, deferred to a
later dispatch) and records the test-count delta plus the migration
file in the status block. phase-B2a-report.md captures the five-
commit timeline, schema, helpers, route surface, token-budget
rationale, provenance JSON shape, and flagged-uncertain items.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:13:09 +02:00
Marc-André BombeckandClaude Opus 4.7 3168ba8e0a feat(coach): GET list, GET single, DELETE conversation endpoints
GET /api/insights/chat returns the caller's conversations newest-first
with cursor pagination (default limit 20, hard cap 50). Each row
carries id, title, timestamps, and a message count so the rail can
render without paying the per-message decrypt cost.

GET /api/insights/chat/[id] fetches one conversation with every
message decrypted in memory before serialisation. Provenance metadata
round-trips alongside the assistant turns.

DELETE /api/insights/chat/[id] hard-deletes the thread; the FK chain
cascades the message rows.

Cross-user lookups map to 404, never 403, so the existence channel
never leaks across accounts.

6 integration tests cover the list ordering, owner-only filter,
decryption round-trip, and the 404 boundary on both verbs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:11:12 +02:00
Marc-André BombeckandClaude Opus 4.7 16f3ece90f feat(coach): add POST /api/insights/chat SSE streaming endpoint
The route gates on requireAuth (cookie or bearer), enforces the daily
token budget, runs the prompt-injection / off-topic refusal scanner,
and only then walks the user's provider chain.

The conversation is created via withIdempotency() when no
conversationId is supplied, so a retry under the same Idempotency-Key
replays the original assistant reply rather than spinning up a second
thread. Existing-thread turns skip the wrap.

Replies stream as SSE: one "token" frame per word-sized chunk, then a
"provenance" frame describing the windows + metrics the assistant
could see, then a "done" frame carrying the persisted message id.
Provider failure cascades to a single "error" frame and the assistant
message is never persisted. Conversation history is capped at 20 turns
in the prompt window via a synthetic summary placeholder so cost stays
bounded.

`buildCoachSnapshot()` reuses the analytics features pipeline so the
Coach narrates the same numbers the dashboard tiles render.
`getCoachSystemPrompt()` extends the strict-insight ground rules with
conversational framing while keeping the zero-hallucination rule.

5 integration tests pin the round-trip, refusal short-circuit, budget
429, foreign-conversation 404, and validation 422 paths against the
postgres testcontainer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:07:47 +02:00
Marc-André BombeckandClaude Opus 4.7 a68557d04b feat(coach): types, persistence, budget, and refusal helpers
Wire the foundation layer for the AI Coach:

- `types.ts` defines the SSE event shapes (token / provenance / done /
  error), the chat-request zod schema, and the conversation/message
  DTOs the route + UI share.
- `persistence.ts` encrypts every message body with the active
  ENCRYPTION_KEYS entry before insert and decrypts on read.
  `summariseTitle()` keeps the rail snippet ≤80 chars with a smart
  word-boundary cut.
- `budget.ts` introduces a per-(user, UTC-day) ledger with a 25 000-
  token daily cap. `enforceBudget()` throws HttpError(429,
  "coach.budget.exceeded") so the route can short-circuit before
  spending. UTC alignment matches the LLM-provider billing cycle.
- `refusal.ts` runs prompt-injection and off-topic detection on inbound
  messages. Pattern bank covers EN+DE injection wording, jailbreak
  variants, and obvious off-topic asks (weather, code-help, recipes).
  Health-allow tokens override the off-topic deny list so mixed
  questions land on-topic.
- i18n keys `insights.coach.refusal*` + `insights.coach.error*`
  added to both locale bundles.
- 28 unit tests cover the helpers with mocked Prisma.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:01:15 +02:00
Marc-André BombeckandClaude Opus 4.7 5527001a01 feat(coach): add CoachConversation, CoachMessage, CoachUsage models
Persist Coach chat threads with AES-256-GCM-encrypted message bodies
and a per-user, per-day token-spend ledger. Encryption reuses the
active ENCRYPTION_KEYS entry so rotation continues to work without a
backfill. metric_source_json carries provenance labels only (window +
metric names, never raw values), keeping it queryable for analytics.

Cascade-on-user-delete is wired through the FK chain so GDPR delete
removes Coach data with the rest of the account. The integration-suite
truncation list picks up the new tables.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:54:28 +02:00
Marc-André BombeckandClaude Opus 4.7 3bdb4380ad docs(planning): tick B1 complete, record v1.4.20 phase B1 report
STATE.md: Wave B1 ticked (hero strip / daily briefing / suggested
prompts / schema / prompt bump). The Apple-Health vitals tile row
is annotated as descoped + deferred to B3 — the per-section status
cards already render the same numbers, so a tile row above them
would duplicate the surface.

New `phase-B1-v1420-report.md` captures the 5 SHAs (9873363 →
7d29596), the test-count delta (1672 → 1753, +81 tests), the
deferral list (Coach wiring → B2, weekly-report → B4, Health Score
→ B5, Briefing variants → backlog, source-chips → B3), and 5
prompt-copy decisions flagged for maintainer review.

The older `.planning/phase-B1-report.md` (v1.4.18 achievements
marathon) is left untouched; v1.4.20's report uses the v1420
suffix to avoid the collision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:47:56 +02:00