Commit Graph
684 Commits
Author SHA1 Message Date
Marc-André BombeckandClaude Opus 4.7 b094eaa2ae docs(planning): record Wave 5 reconcile + v1.4.22 backlog
Wave 5 reconcile pass complete: 7 HIGH applied inline, ~6 MEDs
applied inline, remaining MED + LOW deferred to
`.planning/v1422-backlog.md` under "Wave 5 reconcile carry-over".

- `.planning/phase-W5-v1422-reconcile-report.md`: 10 commit SHAs
  with cross-references to each finding, test count delta
  (+14 unit tests, +3 integration tests; final 2111 / 245 files),
  per-bucket HIGH / MED / LOW disposition, and items the
  maintainer should weigh in on (CSP scope, insights split, sentinel
  parser malformed-enum priority, /me resync stays as fall-back).
- `.planning/v1422-backlog.md`: every deferred MED + LOW with a
  one-line trace per item, organised by review bucket.
- `.planning/STATE.md`: Wave 5 ticked complete; tail updated to
  point at the W5 review files + reconcile report.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:26:21 +02:00
Marc-André BombeckandClaude Opus 4.7 6c0c69cbce fix: MED-cluster — SameSite=Strict, exact-match PUBLIC_PATHS, Berlin tz, streaming race
Six MEDs grouped per the W5 reconcile brief:

- Sec-MED-1: `hl_onboarding` cookie now `SameSite=Strict` (no
  cross-site flow depends on it; sibling `healthlog_session` stays
  `Lax` for the Withings OAuth callback).

- Sec-MED-2: `/onboarding` PUBLIC_PATHS match tightened from loose
  `startsWith("/onboarding")` to exact path + subroute guard
  (`pathname === "/onboarding" || pathname.startsWith("/onboarding/")`).
  A future `/onboarding-export` route can no longer inherit the
  silent-pass-through. Same guard mirrored on the redirect short-
  circuit. Two new proxy tests pin the contract.

- Code-MED-3: lift `berlinDayKey()` from
  `src/app/api/analytics/route.ts` to a shared
  `src/lib/analytics/berlin-day.ts` and route the targets
  sparkline-bucketing through it. The targets route used to bucket
  by UTC `YYYY-MM-DD` so a 23:30-Berlin reading on Tuesday landed
  in Wednesday's bucket. Four new unit tests pin DST + UTC-midnight
  edge cases.

- Code-MED (streaming race): on slow connections SSE `done` fires
  before the invalidate-refetch resolves, producing a 200-500ms
  duplicate-bubble render. Add a 150ms grace window on the
  message-thread that suppresses the persisted twin while the
  streaming bubble is still in flight.

- Code-MED-2: drop dead i18n keys `insights.coach.settings` and
  `insights.coach.settingsTooltip` (W3 removed the cog; the keys
  had zero call sites and would carry confusing legacy if v1.4.23's
  prompt-tuning panel picks a different key shape).

- Code-LOW-5: centralise `["user", "dashboardWidgets"]` query key
  in `queryKeys.dashboardWidgets()` and migrate the three call
  sites (dashboard, insights, settings/dashboard-layout-section).
  Same query-key-collision discipline as `queryKeys.analytics()`.

Closes Sec-MED-1, Sec-MED-2, Code-MED-2, Code-MED-3, Code-MED-3
(streaming race), Code-LOW-5.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:23:04 +02:00
Marc-André BombeckandClaude Opus 4.7 1631d8042c refactor: simplify-pass apply-yes cleanup
Five independent atomic edits from the W5 simplify review, grouped
in a single commit since none pull weight as a compound theme:

- coach-input: collapse `!disabled && value.trim().length > 0`
  triplication onto a single `canSubmit` (S-01).
- hero-strip: extract the duplicate weekly-report Button shell into
  a shared label fragment with the Link wrapping conditionally
  (S-02).
- api-token-overview-section: extract `<TokenStatusBadge size>` so
  the desktop table and mobile card list stay in lock-step on the
  revoked / expired / active 3-way ternary (S-03).
- trends-row: hoist the dynamic-load chart skeleton to a shared
  `ChartSkeleton` component (S-04). No chart-visual change —
  placeholder only.
- correlation-row: collapse the grid className ternary onto `cn()`
  with a conditional `md:grid-cols-2` (S-05).

Test count unchanged (2105).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:17:29 +02:00
Marc-André BombeckandClaude Opus 4.7 65a8b5a8b6 fix(coach): pin medical disclaimer at bottom of message thread
v1.4.22 B4 moved the disclaimer ("Coach replies are generated.
Clinical decisions belong with your doctor.") into the sources-rail
footer. The rail only paints at `xl+` viewports; on `<xl` (every
laptop and every mobile) the disclaimer was reachable only by
opening the chevron tray. That dropped the medical-disclaimer reach
from "every Coach session" to "the subset of users who tap a
chevron" — material regression for clinical-adjacent UI.

Pin a small persistent line at the bottom of the message thread so
the disclaimer paints regardless of viewport. The rail footer stays
for desktop users; the redundancy is intentional.

Closes Design-H3, Design-M4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:15:27 +02:00
Marc-André BombeckandClaude Opus 4.7 651a529cdd fix(insights): collapse BP tile to single secondary row at <sm
The BD-Zielbereich tile is the only tile that ships an `avgAllTime`
sub-value (W2 A1+A2). Combined with the comparison-delta callout
above it, the tile rendered four stacked rows below the headline on
mobile: the comparison delta, then 7d/30d/all-time on a single
flex row that wrapped to three lines at 280 px (Galaxy Fold). The
BP tile read 2× as dense as every other trend card.

At `<sm`: hide the all-time span on the 7d/30d row, hide the
standalone comparison-delta callout, and render a single combined
secondary row beneath: "All-time 11% · Δ −0.4 last month". Full
layout returns at `>=sm` for users with the screen real-estate.

Closes Design-H2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:14:47 +02:00
Marc-André BombeckandClaude Opus 4.7 a2e24b1240 a11y(insights): polish sticky section nav (aria-current, focus, motion-reduce)
Multiple accessibility + visual defects on `<InsightsSectionNav>`:

- No `aria-current` on the active tab — screen readers had no signal
  the active section had changed.
- No `:focus-visible` ring — `transition-colors` only animated the
  background, leaving keyboard focus invisible against `bg-primary/10`.
- Unconditional `scroll-smooth` ignored `prefers-reduced-motion`.
- `bg-background/80` let the hero gradient bleed through.
- `-mx-4` carried the bar past the safe horizontal-scroll boundary
  on 280 px Galaxy Fold; combined with the inner `overflow-x-auto`
  it produced a ghost scrollbar.
- 112 px `scroll-mt-28` was ~2.5× the actual sticky-strip height
  (~40-44 px), pushing target headings well below the viewport top.
- IntersectionObserver picked the last entry in the batch instead of
  the highest intersectionRatio, so the active pill jumped during
  fast scrolls.

Add `aria-current="location"`, focus-visible ring, motion-reduce
gate around `scrollIntoView`, swap `bg-background/80` for `/95
backdrop-blur`, drop the negative margin (the parent handles
horizontal padding), hide the inner scrollbar, ratio-sort the
observer entries, and tune `scroll-mt-28` → `scroll-mt-16` on all
seven sections.

Add `insights.navAriaLabel` (EN: "Skip to section", DE: "Zu
Abschnitt springen").

Closes Design-H1, Design-H3, Code-MED-4, Design-LOW-4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:13:50 +02:00
Marc-André BombeckandClaude Opus 4.7 2075d392b1 refactor(api): extract createSseStream helper from chat route
The Coach chat route had three identical `new ReadableStream({
start(controller) { try { ... } finally { controller.close(); } } })`
blocks (refusal / provider-error / happy path), differing only in
which frames they enqueued. Extract the boilerplate into
`src/lib/sse/create-stream.ts` so the v1.5 iOS streaming endpoints
inherit the same try/finally close semantics without copy-paste.

The helper accepts sync or async emit callbacks; the controller is
always closed in `finally` so a thrown emit never hangs the response.

Three unit tests pin the sync, async, and throw paths.

Closes Sr-H2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:12:22 +02:00
Marc-André BombeckandClaude Opus 4.7 b1ce6ebe81 refactor(auth): fold onboarding-cookie write into createSession + destroySession
The `setOnboardingPendingCookie` helper was called from six different
auth routes (login, passkey-verify, register, password, /me,
/onboarding/complete) by convention. A future auth surface (SSO,
magic link, social login) added without remembering the helper would
silently re-introduce the dashboard flash that v1.4.22 C4 was
designed to fix.

Make `onboardingPending` a required parameter on `createSession`
(not optional) so issuing a session without anchoring the cookie is
type-impossible. The five primary auth surfaces now thread the
user's `onboardingCompletedAt == null` value through; the
`/api/auth/me` mirror remains as a fall-back for legacy sessions
that predate the cookie. The `/api/onboarding/complete` clear stays
where it is — it is a value flip after a DB write, not a session
issuance.

Three new integration tests pin: createSession sets the cookie when
pending, clears it when completed, and destroySession clears it
alongside the session cookie.

Closes Sr-H1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:11:07 +02:00
Marc-André BombeckandClaude Opus 4.7 ef597bfbbd fix(insights): align BD-Zielbereich delta math with comparison window
The tile computed `bp30 - bpAll` regardless of which baseline the
user picked under Settings → Dashboard, while rendering a caption
keyed on `comparisonBaseline` ("vs. last month" / "vs. last year").
The math therefore lied to the user. Every other trend tile uses
`tileCompareDelta()` against period-aligned `avg30LastMonth` /
`avg30LastYear` baselines.

Add `priorMonth` (now-60d…now-30d) and `priorYear` (now-395d…
now-365d) windows to `computeBpInTargetWindows()`. Ship the pcts on
the analytics envelope as `bpInTargetPctPriorMonth` /
`bpInTargetPctPriorYear`. Route the dashboard's `bpCompareDelta` to
subtract the prior window matching the user's baseline; fall back
to null for `none`.

Three new bp-in-target unit tests (priorMonth, priorYear, null
windows) and two insights-polish guards (envelope contains the
fields, page no longer subtracts `bpAll`).

Closes Code-H2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:08:03 +02:00
Marc-André BombeckandClaude Opus 4.7 75ccc01984 fix(coach): graceful fallback when sentinel-only output strips empty
When the model emits only a `---KEYVALUES---` block with no leading
prose (or a malformed block missing the closing marker), the
post-strip prose is empty. The previous fallback chained back to
`rawReply`, surfacing raw sentinel markers to the user. Replace with
a structured `coach.provider.empty` error frame so the UI renders
the localised provider-error copy instead of leaking the markers.

Add a regression integration test pinning that the streamed body
never contains `---KEYVALUES---` for a sentinel-only reply, and that
no assistant message is persisted in this branch.

Closes Code-H1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:05:14 +02:00
Marc-André BombeckandClaude Opus 4.7 363e73cbf2 chore: voice + filename hygiene + wave-4 state wrap
Maintainer-voice sweep across `src/`. ~190 references to the
maintainer by first name in code comments and JSDoc neutralised to
"the maintainer", "the live tenant", "users", or "the user" depending
on context. Three sed passes covering ~50 distinct patterns
(`Marc reported`, `Marc wants`, `Marc kept`, `Marc rolled back`, etc.).
Test fixtures (`displayName: "Marc B."`, `username: "marc"`,
`<HeroStrip userName="Marc">`) kept as opaque test data per the brief.

CHANGELOG normalisation. The v1.4.14 + v1.4.15 entries paired a
German bullet with its English translation by historical design — the
in-app copy is English-default and the user docs are English, so the
mixed-language style was a holdover from the early-marathon notes.
A one-off node script extracted the `_..._` English paragraph from
each bilingual bullet and rewrote the entry as that text. ~44 bullets
reduced; one Docs bullet that lost a leading word in the regex
fixed by hand.

CLAUDE.md → CONTRIBUTING-AI.md rename so the filename is no longer
AI-vendor-specific. `CONTRIBUTING.md` reference updated. `AGENTS.md`
stays in place for multi-agent compatibility (the conventions doc is
shared with Codex / Cursor / etc.).

Wave-4 state wrap. STATE.md now records C1–C5 + D + D2 as complete;
v1421-backlog.md grew a "Closed in v1.4.22" header listing the 20
items the marathon landed; `.planning/phase-W4-report.md` documents
the per-commit decisions, test deltas, and the 23 items deferred to
v1.4.23 with reasoning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:46:32 +02:00
Marc-André BombeckandClaude Opus 4.7 1499e6f83c i18n+ui: backlog quick-wins sweep (F-32/33/34/35/44/45/47/50/58/62 + 2 mediums)
Cluster of the v1.4.21 carry-over items that each landed in ≤5 LOC.

- F-32 — drop the duplicate help paragraph beneath the dashboard
  settings header; the page-level description already covers it
- F-33 / F-34 — replace `&` with "and"/"und" in the measurements and
  mood subtitles
- F-35 — unify the test-notification button label
  (notificationTestSend) with the user-facing notificationStatus.sendTest
- F-44 — drop the `LOKI_ENDPOINT` env-var name from the user-facing
  monitoring help text; reads as "Loki integration" instead
- F-45 — admin/users table column reads "Name" instead of repeating
  the page title "Users"
- F-47 — /notifications event-matrix first column reads "Event" instead
  of repeating the page title "Notifications" — new notifications.eventColumn key
- F-50 — soften every DE error-toast "Bitte erneut versuchen" to
  "Bitte versuche es erneut", matching the friendlier tone we already
  use in the Coach error path
- F-58 — admin/reminders section header swaps Clock for Bell so the
  card icon matches the admin-shell nav tile
- F-62 — DE dashboard `greetingSalutation` reads "Hallo, {name}"
  instead of the English "Hi, {name}"
- D-CR-M-03 — formatTokenName regex broadens to non-Z ISO offsets so a
  `+02:00` timestamp suffix renders as the locale-aware chunk like a
  `Z` suffix already does
- D-DSGN-M-04 — moodLog "Copy" button uses common.copy instead of
  stripping the `!` off common.copied

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:40:05 +02:00
Marc-André BombeckandClaude Opus 4.7 15796c148b fix(auth): move onboarding redirect from AuthShell useEffect to proxy
The post-hydration redirect for users with `onboardingCompletedAt ===
null` caused a brief dashboard flash before the client-side effect
could fire. The redirect now lives in `src/proxy.ts` so it lands on the
first server response.

The proxy runs in the Edge runtime and can't reach Prisma, so the auth
routes (login / passkey-verify / register / password / /api/auth/me)
mirror the DB `onboardingCompletedAt` column into a non-httpOnly
`hl_onboarding` cookie. The cookie is a UX hint, not a security
signal — a user editing it locally only skips the dashboard flash;
they still can't bypass any server check. `/api/onboarding/complete`
clears the cookie so the next navigation drops the redirect
immediately, and `destroySession` + expired-session purge wipe it
alongside the session cookie.

The e2e onboarding-flicker spec can return to testing the actual
incomplete-onboarding case: the proxy reads the cookie (absent for
the fully-onboarded storage-state user), not the mocked `/api/auth/me`
response, so the test now sets `onboardingCompletedAt: null` honestly
instead of pinning it to a non-null value as a workaround.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:29:17 +02:00
Marc-André BombeckandClaude Opus 4.7 b281c0655b ops(deploy): force=true on Coolify webhook + node-26-alpine investigation
C3 — every v1.4.19/20/21 release required a host-side `docker tag`
fallback because Coolify pulled `:latest` from its image cache without
re-checking the GHCR registry digest. The workflow now appends
`?force=true` (or `&force=true` if other params are present) to the
deploy webhook URL so a doc-only push still triggers a registry-digest
check. The complementary maintainer-side toggle ("Watch image registry
for new digests" in the Coolify application config) is documented in
`.planning/coolify-auto-deploy-howto.md` — one checkbox flip in the UI,
no code change required.

C5 — the dependabot PR bumping node:22-alpine → node:26-alpine fails
at build because node:26-alpine no longer ships with corepack
pre-installed (`/bin/sh: corepack: not found`, exit 127). Next.js 16
also still pins node 22 LTS as its supported minimum, so a corepack
workaround would put the runtime ahead of the framework. Closed PR
#162 with a comment; reopen when Next.js bumps its node-version pin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:24:00 +02:00
Marc-André BombeckandClaude Opus 4.7 1643a78f63 fix(admin): drop whitespace-nowrap on api-tokens date cells (5th attempt)
The /admin/api-tokens page has been painting a residual horizontal
scrollbar for four releases despite column-hide, mobile card-list, and
the admin section-strip `no-scrollbar` fixes. A fresh production probe
confirmed the actual culprit: the "Last used" and "Created" `<td>`s
carry `whitespace-nowrap`, and `formatDateTime` renders strings like
"05.05.2026, 21:46" (~110px) that exceed the 12% colgroup allotment
(~84px on a 700px content area). `whitespace-nowrap` wins over
`table-fixed`'s width contract, so the table's intrinsic width exceeds
100% and the wrapping `overflow-x-auto` paints the scrollbar.

Dropping the two `whitespace-nowrap` classes lets the date+time wrap
to two lines on narrow viewports. The card grows by one row but no
scrollbar — and at >1024px desktop the unwrapped string already fits
in the 12% allotment so the visual is unchanged for the common case.

Picked option 1 of the three W1a probe found (drop nowrap, switch to
formatDateShort, or hide "Created" at md): smallest two-line diff, no
i18n surface impact, no information loss.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:22:32 +02:00
Marc-André BombeckandClaude Opus 4.7 a22f77881e feat(targets): 30-day sparkline + Δ-vs-last-month caption per card
The Zielwerte page read as a static reference cheat-sheet — every card
surfaced a current value + range bar but no recent trajectory. Each
target now grows a dependency-free 30-day sparkline beneath the range
bar plus a localised "Δ −2.3 kg vs. last month" caption so the page
reads as a living health log rather than a guideline lookup.

The API computes both fields from a single 60-day query (one round-trip,
not two): `points30d` is the daily-mean series for the last 30 days,
`deltaVsLastMonth` is the signed difference between the current and
prior 30-day averages. Both are null when either window has fewer than
3 readings so cold-start accounts don't see a misleading flat trace
or a "± 0" comparison that's really "no comparison".

BMI piggybacks on the weight series (divided by height²) so the BMI
card's sparkline matches its range bar's y-axis. Derived metrics
(BP-in-target rate, mood stability, medication compliance,
blood-glucose-by-context) keep their previous chrome — the rich
status surface they already render is the right answer there.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:21:48 +02:00
Marc-André BombeckandClaude Opus 4.7 7c46cc9439 chore(planning): record Wave 3 Coach polish completion
Tick B1-B5 in STATE and capture the sentinel-parse design notes,
test count delta, and the synthesised prose example showing how the
new prompt handles "Meinst du, ich sollte mehr Sport machen?" with
the invitation pivot from rule 3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:13:19 +02:00
Marc-André BombeckandClaude Opus 4.7 b845cf5dae refactor(coach): drawer polish — Gravatar avatar, disclaimer move, cog removal
Three contained UX fixes that line up with the new prose-first
conversation feel:

- User bubble avatar pulls the user's Gravatar at the same 32-px
  dimensions as the Coach avatar; falls back to initials when no
  Gravatar is configured.
- Disclaimer ("Coach replies are generated. Clinical decisions belong
  with your doctor.") moves out of the composer and into the sources
  rail footer, replacing the now-redundant "Coach reads only your
  connected data" caption.
- Settings cog removed from the drawer header. The placeholder
  tooltip read as a dead button and a real settings surface lands in
  a later patch — better to drop the icon cleanly than wire it to
  nothing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:12:26 +02:00
Marc-André BombeckandClaude Opus 4.7 bc87fa3904 feat(coach): collapsible evidence-block disclosure under assistant bubbles
Render the v1.4.22 keyValues array as a click-to-expand "Worauf
bezieht sich das?" / "What I'm looking at" disclosure under each
assistant message. Closed by default; rows show label, value, unit,
and window with Dracula muted-foreground accents and a chevron that
rotates on open. Source chips stay above the disclosure so users see
both abstraction layers, and the whole block hides when keyValues is
empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:08:54 +02:00
Marc-André BombeckandClaude Opus 4.7 7b8efaae14 feat(coach): parse evidence-block sentinel out of streamed prose
Add a text-marker sentinel parser (`---KEYVALUES---` … `---END---`)
that strips the Coach's evidence block out of the prose before it
streams to the client, validates each line against a Zod schema, and
attaches the parsed entries to `provenance.keyValues`. The persisted
envelope round-trips so the disclosure renders on conversation
reload. Hard-capped at 8 lines and 1 KB to guard against
prompt-injection padding; malformed blocks log a wide-event and pass
the prose through unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:07:32 +02:00
Marc-André BombeckandClaude Opus 4.7 bea55eac5c feat(coach): warm motivational rewrite with evidence-block sentinel
Replace the v1.4.20 "conversational summariser" persona with a partner
who sits alongside the user — warm, curious, conservative, with seven
numbered ground rules that prefer invitation over refusal when data is
missing and reserve specific numbers for a structured evidence block
at the end of each reply. The day-level timeline section from v1.4.21
stays verbatim so weekday questions still resolve cleanly. Bumps the
shared PROMPT_VERSION from 4.20.2 to 4.22.0 and broadens existing
schema tests off the 4.20.x pin onto the 4.x train.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:02:28 +02:00
Marc-André BombeckandClaude Opus 4.7 1c0cd8c87e docs(planning): record Wave 2 — Insights surface polish
Six atomic commits landed for A1-A6: BP target headline re-anchor,
BP tile feature parity, comparison-toggle moved to Settings only,
layout grid normalisation, "Muster" rename + section tabs lifted
above the hero, and the recommendation-card token-leak fix plus DE
component-label normalisation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:55:27 +02:00
Marc-André BombeckandClaude Opus 4.7 59a4a89b62 fix(insights): strip metric tokens in recommendation prose + DE component labels
Two related leak fixes traced to the same root: every other AI-prose
surface ran user-visible text through `stripChartTokens()`, but the
recommendation card's always-visible text was rendered directly. The
model embeds `metric:WEIGHT` and friends in the rec text expecting an
inline mini-chart, but the chart slot lives in the EXPANDED rationale
block — so the token had nowhere to be consumed and leaked verbatim
to the DOM.

  - `<RecommendationCard>` now wraps `norm.text` in
    `stripChartTokens()`, matching the contract every other
    rec / finding / summary surface already uses.
  - DE Health-Score component labels surfaced English nouns ("Mood",
    "Medis", "BD") instead of full German. Rename to "Blutdruck",
    "Stimmung", "Einnahmetreue", "Gewicht" so the four sub-bars read
    natively German. The "Mut" the maintainer reported on prod was
    voice-to-text rendering "Mood".
  - i18n-integrity test pins the four component labels so a copy-paste
    regression can't reintroduce the leak.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:54:21 +02:00
Marc-André BombeckandClaude Opus 4.7 4d3b12936a feat(insights): rename "Muster" + lift section tabs above the hero
Two related polishing changes for the Insights surface:

a) Correlation row title rename. "Muster" / "Patterns" reads as a
   loose grouping; the row actually shows pre-defined statistical
   relationships between two metrics. Rename to "Zusammenhänge" /
   "Relationships" so the title carries the same conceptual weight as
   the surrounding "Trends" / "Daily Briefing" headers without
   borrowing the "Trends" word from the row above it. Picked
   "Zusammenhänge" over "Trends" because the Trends row already uses
   that word and a correlation is conceptually a relationship between
   two series rather than a single-series trend.

b) Section tabs lift above the hero strip. `<InsightsSectionNav>` was
   mounted below the advisor card so the user had to scroll past the
   hero before discovering the metric-tab nav. Lift it to the top of
   the page so the tabs are the first thing visible — the hero +
   daily briefing render directly below, and clicking a tab still
   scrolls to the matching section. Sticky scroll-anchored behaviour
   preserved (incremental over a full SPA-tab rewrite) so today's
   reactive surface keeps working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:52:07 +02:00
Marc-André BombeckandClaude Opus 4.7 36b9db34c5 fix(insights): row-fill rule + equal-height trends + hide-on-insufficient-data
Three layout problems landed together because they share the same
root: the `/insights` grids assumed every row would be full, so when a
section dropped a card the layout left a half-row of empty space.

  - Row-fill rule: every grid on `/insights` now picks `xl:grid-cols-2`
    only when 2+ cards actually render. With 1 card the grid collapses
    to a single full-width column. The BP-medication grid, the weight-
    correlation grid, and the medications-per-day grid all carried the
    half-empty-row bug; all three are normalised.

  - Trends row equal heights: each `<TrendsRow>` card is now a flex
    column with `min-h-[300px]` so a multi-line AI annotation under one
    chart doesn't push that card taller than its neighbours. The
    visual rhythm holds even when the prose varies in length.

  - Hide insufficient-data correlation tiles: `<CorrelationRow>` drops
    any hypothesis with `status !== "ok"` instead of painting a
    placeholder. When zero hypotheses qualify, the entire row hides
    itself (no header, no disclaimer). 1 ok card spans 100 % width, 2
    or 3 ok cards split 50/50 on `>=md`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:49:55 +02:00
Marc-André BombeckandClaude Opus 4.7 706aa5fd73 feat(settings): comparison-overlay becomes a global dashboard preference
Up to v1.4.21 the comparison-baseline toggle lived in two places: the
canonical Settings → Dashboard picker (where it's persisted) and an
on-surface duplicate above the Daily Briefing on `/insights`. Per the
"no split" rule the duplicate is removed: the toggle is a global
preference, so it belongs in Settings only.

Every chart on `/insights` still consumes the resolved
`comparisonBaseline` from the same dashboard-layout query the picker
writes to, so flipping the Settings toggle keeps propagating to every
overlay on next refetch — only the duplicate UI affordance is gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:46:55 +02:00
Marc-André BombeckandClaude Opus 4.7 b39d5f890b feat(insights): BP tile gains trend arrow + comparison overlay parity
Up to v1.4.21 the BD-Zielbereich tile was the only one in the strip
that didn't ship the standard tile feature set. Other tiles paint a
trend arrow next to the headline, a 7-day-trend chip on the `7d:`
sub-row, and a comparison-overlay caption when the dashboard's global
compare toggle is active. The BP-in-target tile shipped none of these
because we only had a single % series instead of a `DataSummary`.

Synthesise the slope from the difference between the 7-day and 30-day
in-target shares: a recent week above the recent month means the
metric is improving (up-good ⇒ green arrow); below means slipping.
The 7-day-trend chip uses the same delta. The comparison overlay uses
`bpInTargetPctAllTime` as the long-arc baseline so the caption reads
correctly when comparison is on.

Extend `<TrendCard>` with an optional `avgAllTime` sub-value so the
all-time aggregate stays visible alongside `7d:` and `30d:` after the
v1.4.22 A1 headline re-anchor. Other tiles leave the field undefined
so the third sub-row never renders for them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:45:50 +02:00
Marc-André BombeckandClaude Opus 4.7 8606767058 fix(analytics): BP target headline reads last-30d, all-time becomes sub-row
Up to v1.4.19 the BD-Zielbereich tile pinned 50/50/50 because the
route routed the headline through `windows.last30Days?.pct` — making
the headline a literal copy of the `30T` sub-value. v1.4.19 A1
re-anchored the headline to all-time so the three windows could
diverge. That fixed the algorithmic pin but introduced an emotional
problem: the headline became the slowest-moving aggregate possible,
punishing users who had recently dragged their compliance up. A user
seeing 7d 50 % / 30d 50 % alongside a stubborn 11 % all-time read it
as "your work doesn't count yet."

Re-anchor the headline to `windows.last30Days?.pct` and surface
all-time as a third sub-value (`bpInTargetPctAllTime`) so power users
still see the long-arc number without it dominating. The integration
test pins the new contract: with three legitimately divergent windows
the route's `bpInTargetPct` equals `last30Days.pct`, and the new
all-time field carries the historical figure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:42:09 +02:00
Marc-André BombeckandClaude Opus 4.7 e794f57bae docs(planning): v1.4.22 marathon scaffold — polishing before v1.5 iOS
Big-polishing release scope: BD-Zielbereich 5th attempt, BD-Kachel
parity, comparison-toggle global via Settings, Insights grid
normalisation, "Muster" rename + tabs above hero, raw Metrik token
leak, Coach prompt rewrite (research-driven), collapsible provenance,
Gravatar parity, disclaimer move, settings cog wire-or-remove,
Zielwerte upgrade, api-tokens 5th attempt, Coolify auto-deploy fix,
AuthShell flicker, node-26-alpine investigation, full v1421-backlog
cleanup. Marathon plan: research/probe → polish waves → QA → release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:20:01 +02:00
Marc-André BombeckandClaude Opus 4.7 16b6976c54 Merge main into develop — sync v1.4.21 brief
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:52:39 +02:00
Marc-André BombeckandClaude Opus 4.7 2dd4a3fe64 docs(audit): v1.4.21 release summary
Captures the same-day patch on top of v1.4.20: Daily Briefing
regenerate fix, Coach day-level snapshot, duplicate streaming bubble,
drawer header, scope picker UI. STATE + ROADMAP rolled forward; v1.4.22
slot scaffolded for the e2e fix wave already in flight on develop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:51:52 +02:00
Marc-André BombeckandClaude Opus 4.7 3785fec2ac fix(e2e): scroll first chart card into view before counting axis ticks
The Pixel-5 tick-density spec waited on `.recharts-xAxis` to be visible
but Playwright kept resolving the SVG axis as `hidden`. On a 393×851
viewport the dashboard ships several stacked tiles above the first
chart card, so the bottom-positioned axis sits below the initial fold
when the test starts probing.

Locate the first chart card by its range-tab data-slot, scroll it into
view so Recharts' ResponsiveContainer measures a non-zero parent, then
wait for an axis tick label (not the wrapper `<g>`) to be visible
before counting ticks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:49:44 +02:00
Marc-André BombeckandClaude Opus 4.7 e8d0660198 fix(e2e): exempt truncate-clamped elements from per-element overflow walk
The api-tokens mobile no-overflow spec walks every descendant of the
mobile card-list and asserts `scrollWidth <= clientWidth + 1`. The
v1.4.19 fix wrapped long token names / usernames / permissions in a
`<TruncatedCell>` (Tailwind `truncate` ⇒ `overflow:hidden +
text-overflow:ellipsis + white-space:nowrap`), which by design keeps
`clientWidth` clamped to the parent column while `scrollWidth` reflects
the full text — exactly the case the assertion was tripping on.

Skip elements whose computed `text-overflow: ellipsis` and clamped
`overflow-x` show they are intentionally truncating. The page-level
`document.documentElement.scrollWidth <= innerWidth + 1` assertion
remains the authoritative no-scrollbar guard, so the regression intent
is preserved without false-positives on the truncate helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:49:32 +02:00
Marc-André BombeckandClaude Opus 4.7 812b68a99d fix(e2e): scroll trigger into view + force-click on portalled toggle
The chart-overlay-controls spec timed out clicking the target-range
Switch — Playwright resolved the locator but reported "element is
outside of the viewport" after auto-scroll. The Radix dropdown content
is portalled to `document.body`, so `scrollIntoViewIfNeeded()` on the
toggle is a no-op (it lives outside the page scroll container).

Scroll the cog trigger into view BEFORE opening the popover so the
portalled content paints inside the viewport, then force the click on
the toggle to bypass the off-viewport guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:49:20 +02:00
Marc-André BombeckandClaude Opus 4.7 f181ab71fe fix(e2e): pin onboardingCompletedAt so AuthShell does not redirect
The "incomplete-onboarding" flicker spec mocked `/api/auth/me` with a
null `onboardingCompletedAt`, which makes `<AuthShell>` redirect every
authenticated route to `/onboarding` before the dashboard hydrates —
so the checklist card the test asserts on never paints.

Pin the timestamp to a non-null value while keeping every other
checklist signal empty (profile fields null, `summaries: {}` so
`measurementCount = 0`). The `shouldShowChecklist` gate still returns
true via its `measurementCount < 5` branch, so the assertion still
exercises the collapsed-by-default rendering path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:49:12 +02:00
Marc-André BombeckandClaude Opus 4.7 32cd18566d fix(e2e): adopt HeroStrip data-slot on insights advisor spec
v1.4.20 B1 retired `<InsightsPageHero>` in favour of the condensed
`<HeroStrip>` component, which exposes `data-slot="insights-hero-strip"`
instead of the legacy `insights-page-hero` marker. Update the advisor-
card spec selector so the hero check hits the live element on /insights.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:48:59 +02:00
Marc-André BombeckandClaude Opus 4.7 8e198e1b6b Release v1.4.21
9955288 chore(release): v1.4.21
52dd9ff docs(planning): record v1.4.20.1 hotfix wave
08fd411 feat(coach): per-source + window scope picker on sources rail
ddb2914 fix(coach): drawer header — close-X / cog collision
f07e35b fix(coach): clear streaming bubble after persisted twin lands
2143377 feat(coach): system prompt instructs model to use day-level data
ed61b17 feat(coach): snapshot includes day-level readings with weekday labels
7921ffc fix(insights): wire generate route to strict prompt so dailyBriefing emits

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.4.21
2026-05-10 19:36:22 +02:00
Marc-André BombeckandClaude Opus 4.7 995528801c chore(release): v1.4.21
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:36:05 +02:00
Marc-André BombeckandClaude Opus 4.7 52dd9ff9c3 docs(planning): record v1.4.20.1 hotfix wave
New `.planning/phase-v1421-hotfix-report.md` summarises the six
fixes (Daily Briefing regenerate · Coach day-level snapshot · Coach
prompt update · streaming-bubble dedup · drawer header · sources rail
scope picker), the test-count delta (2026 → 2036 unit), the
token-budget impact of the day-level snapshot (~190 → ~3000 tokens
per full-scope turn), and a recommendation to ship as a v1.4.20.1
hotfix.

STATE.md grows a Phase F section + status block tracking the six
commits on develop. Includes prettier polish across the touched
coach surfaces (sources-rail, drawer, message-thread, snapshot,
chat route).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:31:20 +02:00
Marc-André BombeckandClaude Opus 4.7 08fd411ff1 feat(coach): per-source + window scope picker on sources rail
Previously the Sources rail listed BP / Weight / Pulse / Mood /
Compliance as a static legend. Marc asked for a real "what I can
see" surface — per-source checkboxes (include/exclude) plus a
window selector (last 7d / 30d / 90d / all-time) that flow through
the Coach request body.

Wire the controls all the way down:
  - <SourcesRail> grows scope + onScopeChange props. Each row
    becomes a 36px tall flex row with a real checkbox; the row
    fades to 60% opacity when its source is excluded.
  - A Select trigger above the list drives the window. Both control
    surfaces share the existing insights.coach.window.* i18n keys;
    add a new windowLabel key so the dropdown has an EN+DE label.
  - <CoachDrawer> owns the scope state (resets on each mount per
    the v1.4.20.1 plan — no conversation-level persistence yet) and
    forwards the picked scope through useSendCoachMessage to the
    chat request body. The payload only ships when the user has
    narrowed away from the all-source last30days default so the
    server can tell "no opinion" from "intentionally narrow".
  - buildCoachSnapshot already accepts the optional CoachScope from
    the previous commit; it now flows end-to-end.

No new dependencies — checkboxes are plain inputs styled with
Tailwind + Dracula tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:27:28 +02:00
Marc-André BombeckandClaude Opus 4.7 ddb2914b65 fix(coach): drawer header — close-X / cog collision
Radix Sheet paints its default close-X at absolute top-4 right-4. The
v1.4.20 drawer header parked the settings cog inside the right-edge
button cluster (next to the New chat button), so the close-X overlay
visually swallowed the cog — the icon still rendered but read as
unclickable.

Move the cog to the left side, immediately after the gradient
avatar, so the right edge is reserved for Radix's close-X alone. Add
pr-12 / sm:pr-14 padding to the header so the New chat button can
never slide under the close-X on narrower viewports either.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:24:05 +02:00
Marc-André BombeckandClaude Opus 4.7 f07e35bb9a fix(coach): clear streaming bubble after persisted twin lands
After an SSE `done` frame the streaming hook keeps `streaming.content`
populated to support the in-flight render path, then fires a TanStack
invalidate that pulls the persisted assistant message into
`conversation.messages`. Until the next `send` reset the message
thread therefore rendered the assistant reply twice — once from the
streaming bubble and once from the persisted message — until the
drawer was closed and reopened (which is when Marc noticed the bug
went away).

Suppress the streaming bubble at render time as soon as the persisted
twin lands. The match keys on `streaming.messageId`, which the SSE
`done` frame fills in once the row is on disk; mid-stream the field
is null so the in-flight bubble stays visible.

Add two regression tests: one that asserts dedup-on-id works and one
that pins the still-streaming render path so the fix doesn't trade a
duplicate-bubble bug for a no-bubble bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:23:26 +02:00
Marc-André BombeckandClaude Opus 4.7 2143377844 feat(coach): system prompt instructs model to use day-level data
The previous Coach prompt only said "ground every number in the
SNAPSHOT" without telling the model that the snapshot now carries a
timeline.recent block of day-level rows with weekday labels.
Without that pointer the model kept defaulting to aggregate-only
phrasing on day-specific questions.

Add a DAY-LEVEL READINGS section to both EN and DE prompts:
  - explain the timeline.recent / timeline.weekly shape
  - tell the model to answer day-specific or weekday-specific
    questions out of timeline.recent and to cite the actual reading
    with date + weekday
  - when no row exists for the day named, say so plainly and offer
    the closest available day rather than the aggregate
  - older weekday questions fall back to timeline.weekly
  - the scope.sources field is the conversation's source-allowlist;
    a question about a non-listed metric should be acknowledged as
    out-of-scope rather than answered from the aggregate

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:22:38 +02:00
Marc-André BombeckandClaude Opus 4.7 ed61b17f57 feat(coach): snapshot includes day-level readings with weekday labels
The Coach snapshot used to ship only aggregated statistics (mean,
slope, SD, range, count) per metric. A turn that asked
"why was BP higher on Monday?" had no day-level rows to ground in
and the model fell back to apologetic aggregate-only phrasing
("from the snapshot I cannot isolate the specific Monday").

Extend buildCoachSnapshot to fold in:
  - timeline.recent: one row per UTC day for the last 14 days, each
    tagged with its weekday so the model can answer day or weekday
    questions directly. BP rows pair systolic + diastolic; days with
    only one side measured are dropped to avoid fabricating a
    complement.
  - timeline.weekly: ISO-week buckets covering days 15..N where N is
    the configured window. Keeps the prompt budget tight on 30/90-day
    windows while still letting the Coach cite older weeks.
  - per-day medication adherence rows from MedicationIntakeEvent so
    the Coach can answer "did I miss my dose Tuesday?" without
    inventing the schedule.

Also add the optional `scope` field to coachChatRequestSchema (sources
+ window) so the upcoming sources-rail picker can narrow the snapshot
without breaking older clients — missing fields fall through to
all-source last30days.

A typical 30-day, 5-source snapshot lands around 2.4-2.8 KB JSON
(≈700-900 prompt tokens) — comfortably inside the 25k/day Coach token
budget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:21:48 +02:00
Marc-André BombeckandClaude Opus 4.7 7921ffc8e6 fix(insights): wire generate route to strict prompt so dailyBriefing emits
The /api/insights/generate route was still calling the legacy
getInsightsSystemPrompt that returns the v1.4.5 {changed, stable,
drivers, …} shape and never asks the model for a dailyBriefing block.
GROUND RULE 8 (emit dailyBriefing when the snapshot has signal) lives
on getStrictInsightsSystemPrompt in src/lib/ai/prompts/insight-
generator.ts; the route never adopted it. Re-runs from the hero strip
therefore stored a payload with no briefing, the use-insights-advisor
lift saw null, and the briefing card painted its empty state forever.

Switch the route to getStrictInsightsSystemPrompt(locale). Existing
strict-shape tolerance is unchanged: insightResultSchema.safeParse
falls through to the raw parsed body on shape mismatch, and
aiInsightResponseSchema's passthrough() keeps any custom keys for
forward compat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:20:50 +02:00
Marc-André BombeckandClaude Opus 4.7 ddd0b27eaa docs(planning): close v1.4.20 milestone
Phase E ticked complete in STATE.md; v1.4.20 archived to ROADMAP
"Previous milestone" with completion timestamp. Top of ROADMAP
scaffolds v1.4.21 with carry-over candidates from
.planning/v1421-backlog.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:57:40 +02:00
Marc-André BombeckandClaude Opus 4.7 7f9ae2186d docs(audit): v1.4.20 release summary
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:56:07 +02:00
dependabot[bot] f4140d7e2c chore(deps): Bump tailwind-merge from 3.5.0 to 3.6.0 (#165)
Bumps [tailwind-merge](https://github.com/dcastil/tailwind-merge) from 3.5.0 to 3.6.0.
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v3.5.0...v3.6.0)

---
updated-dependencies:
- dependency-name: tailwind-merge
  dependency-version: 3.6.0
  dependency-type: direct:production
  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:29:47 +00:00
dependabot[bot] 1d3009d9ef chore(deps): Bump actions/checkout from 4 to 6 (#161)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-10 16:28:06 +00:00
Marc-André BombeckandClaude Opus 4.7 666f6eec78 Release v1.4.20
dd2ef57 Merge main into develop — sync Dependabot dep bumps before v1.4.20 tag
aac8b5e chore(release): v1.4.20
73e51d7 style: prettier on touched insights/coach surfaces
21d064c docs(planning): record Wave D reconcile + carry-over backlog
31fbf98 chore(i18n): drop duplicate heroGreetingNight key
9f3baf9 fix(insights): MED-bucket polish across coach + report surfaces
3402394 refactor(insights): consolidate relative-time and confidence-band tables
cfe6d1c chore(api): drop maintainer-name comment in analytics route
346867b fix(insights): isolate hero glow from sticky section nav
f9af100 fix(coach): cap drawer width on lg+ and route sources rail via tray
8479e05 a11y(coach): announce streaming assistant text to screen readers
3c353fe fix(insights): bump suggested-prompt chip touch target to 36px floor
046d788 feat(insights): enable Generate weekly report button on hero
249607d perf(coach): bound buildCoachSnapshot to last 90 days
e632e26 fix(coach): repair SSE idempotency and error-frame handling
ded0b38 docs(planning): mark Wave-B B5 complete + report
91ad5ec feat(insights): mount Health Score panel in hero strip
067bf31 feat(insights): Health Score panel component
236b498 feat(api): emit Health Score from /api/analytics
73d2188 feat(insights): personal Health Score formula library
54d3e92 docs(planning): mark Wave-B B4 complete + report
ebb2bc0 feat(coach): mobile rail trays for the AI Coach drawer
ff1e335 feat(charts): storyboard annotations on HealthChart
f186845 feat(insights): hero strip weekly-report banner card
8f18c7f feat(insights): /insights/report/[week] printable weekly report
6d4cba1 feat(insights): weeklyReport + storyboardAnnotations schema + prompt
bbd00ca docs(planning): mark Wave-B B3 complete + report
c83cb23 feat(insights): wire CorrelationRow + TrendsRow into /insights
42591c6 feat(insights): CorrelationCard + CorrelationRow components
2ae69d7 feat(insights): TrendAnnotation + TrendsRow components
f9afaf7 feat(insights): trendAnnotations schema + prompt rule
7261c36 feat(insights): correlation maths for B3 hypotheses
90d3a80 docs(planning): tick B2b complete, record phase B2b report
e541799 feat(insights): wire Coach drawer into /insights via hero strip
8dac389 feat(coach): composer, history rail, sources rail
195448a feat(coach): drawer shell, message thread, source chips
c42c0a5 feat(coach): TanStack Query hooks + SSE streaming client
e7287d3 docs(planning): tick B2a complete, record phase B2a report
3168ba8 feat(coach): GET list, GET single, DELETE conversation endpoints
16f3ece feat(coach): add POST /api/insights/chat SSE streaming endpoint
a68557d feat(coach): types, persistence, budget, and refusal helpers
5527001 feat(coach): add CoachConversation, CoachMessage, CoachUsage models
3bdb438 docs(planning): tick B1 complete, record v1.4.20 phase B1 report
7d29596 feat(insights): wire <HeroStrip> + <DailyBriefing> into /insights
59e63a0 feat(insights): add <HeroStrip> + Dracula gradient utilities
92f332e feat(insights): add <DailyBriefing> card component
9713bf1 feat(insights): add <SuggestedPrompts> chip-strip component
9873363 feat(insights): add Daily Briefing schema + prompt v4.20.0
81a40f7 docs(planning): F6 docs accuracy audit recorded
a2d959c docs(planning): F5 best-practice audit recorded, v1.4.21 backlog seeded
f7977d1 chore(package): add description, repository, license metadata
779e5a3 chore(github): expand dependabot to GitHub Actions + Docker
072941b chore(github): add issue + PR templates
e7b6b27 chore(community): add Code of Conduct (Contributor Covenant 2.1)
fe660d4 docs(contributing): document develop / main branch model
77f9741 docs(planning): F1 develop branch + FX user-facing cleanup recorded

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.4.20
2026-05-10 18:26:14 +02:00