# Changelog ## [1.4.22] — 2026-05-10 ### Added - **Per-target sparkline + Δ-vs-last-month caption on the Targets / Zielwerte page.** Each `` grew a 30-day inline SVG sparkline beneath the range bar plus a localised "Δ −2.3 kg vs. last month" caption. The API ships `points30d` and `deltaVsLastMonth` per target; both null when either window has fewer than 3 readings so cold-start accounts don't paint a misleading flat trace. BMI piggybacks on the weight series so its sparkline shares the range bar's y-axis. - **Sticky section navigation above the Insights hero.** A pinned strip (Allgemein / Blutdruck / Gewicht / Puls / Stimmung in DE; General / Blood Pressure / Weight / Pulse / Mood in EN) lifts the section tabs above the hero so they stay visible during scroll. Active section tracks the highest-intersection observed entry; `aria-current`, focus-visible ring, and `motion-reduce` gating ship from day one. - **Comparison-overlay as a single global preference under Settings → Dashboard.** The on-surface `` retired from `/insights`; the canonical picker has lived in Settings → Dashboard since v1.4.16 phase B8 and every chart already consumed it. Two surfaces for the same concept violated the no-split-Settings rule. - **Collapsible evidence disclosure under each Coach assistant message.** Numbers move out of the prose into a `---KEYVALUES---` … `---END---` sentinel block that the route parses out server-side and renders as a "Worauf bezieht sich das?" / "What I'm looking at" `
` disclosure under each assistant turn. Closed by default, hidden when no key-values came back. Hard caps on the sentinel (1 KB payload, 8 lines max, per-line Zod) so a prompt-injection attempt can't grow the persisted envelope. - **User avatar parity with the Coach avatar.** The Coach drawer's user-side bubble used a smaller initials avatar than the Coach's gradient one. The user avatar now reuses the existing `gravatarUrl` field from `/api/auth/me` at the same dimensions as the Coach avatar; initials fall back when no Gravatar is configured. ### Changed - **Coach prompt rewrite — warm, motivational-interviewing tone with prose-first responses + evidence collapsible.** PROMPT_VERSION ratchets 4.20.2 → 4.22.0 (first minor-digit bump in v1.4.x). The Coach used to open every reply with a number — clinical, database-cursor energy. The new persona is "warm, neugierig, zurückhaltend": a partner sitting alongside the user, not pushing data. Numbers move into the collapsible evidence block; the prose reads like a real conversation. Persona + sentinel land together because either alone is incomplete. - **BP-in-target headline re-anchored to the last-30-day window.** Fourth attempt at this metric. v1.4.19 routed the headline to `allTime` to fix the algorithmic 50/50/50 pin; v1.4.22 re-anchors to `windows.last30Days?.pct` and surfaces all-time as a sub-row, because v1.4.19's fix was emotionally wrong — the headline became the slowest-moving aggregate possible, punishing a user who put in real recent work. The tile also gets a synthesised trend arrow (slope from 7d/30d delta), a 7-day-trend chip, and a comparison-overlay caption. - **"Muster" renamed to "Zusammenhänge" (DE) / "Patterns" renamed to "Relationships" (EN).** The picked-bucket-of-correlations row read like an autopsy. The new label sits closer to what the row actually shows. Picked "Zusammenhänge" over "Trends" because the row directly above already uses Trends. - **Onboarding redirect for users with `null onboardingCompletedAt` moves to server-side enforcement in `proxy.ts`.** The previous post-hydration redirect inside `` `useEffect` produced a brief dashboard flash on incomplete onboarding. The proxy runs in the Edge runtime so the auth routes mirror `onboardingCompletedAt` into a non-httpOnly `hl_onboarding` cookie; the proxy short-circuits before hydration. Tampering only skips a UX hint and never bypasses a server check. - **`setOnboardingPendingCookie` folded into `createSession()`.** Issuing a session without onboarding state used to require two call sites to stay in sync; the helper now takes `onboardingPending` as a required parameter so the contract is type-impossible to break. ### Fixed - **Raw `metric:` token leaks in recommendation prose.** `` text is now wrapped in `stripChartTokens()`. The leak traced to a single missing call on the recommendation path; chart tokens never belonged in user copy. - **DE locale `componentMood`, `componentBp`, `componentCompliance` rendered English nouns in the German bundle.** Four Health-Score component labels normalised to `Stimmung`, `Blutdruck`, `Einnahmetreue`, `Gewicht`. The i18n-integrity test pins the contract. - **Admin / API tokens horizontal scrollbar at desktop + iPad-mini viewports (5th attempt).** A live Playwright probe confirmed `whitespace-nowrap` on the date ``s was the residual culprit. The two classes are gone; date + time wraps to two lines on narrow viewports. The earlier four fixes had targeted the wrong layout layer. - **Sentinel-only / malformed `---KEYVALUES---` block.** When the model emits a sentinel-only or malformed envelope, the fallback now produces a polite invitation ("I'd like to look at this with you — could you share which window you want me to focus on?") instead of surfacing the raw marker. A new integration test covers the empty-prose-after-strip branch. - **BD-Zielbereich tile delta math period-aligned with the comparison window.** The tile's compareDelta was subtracting `bpInTargetPctAllTime` while the caption said "vs last month", which produced numbers the caption couldn't justify. It now subtracts `bpInTargetPctPriorMonth` / `bpInTargetPctPriorYear` to match. Two new bp-in-target unit tests + two insights-polish guards. - **Coach drawer settings cog removed.** The cog was a dead button in v1.4.21; per-user prompt-tuning is deferred to v1.4.23. No dead buttons in this release. - **Coach disclaimer pinned at the bottom of the message thread.** Clinical-adjacent UI must not gate the disclaimer behind a chevron tray; the disclaimer now stays visible at the bottom of the message thread on every viewport, and the rail-footer duplicate kept for desktop redundancy. - **`hl_onboarding` UX-hint cookie now `SameSite=Strict`.** A cross-site request couldn't usefully exfiltrate the cookie (no auth value, only an onboarding state flag) but `Lax` was still over-permissive. Strict aligns with the cookie's same-site-only consumer. - **`PUBLIC_PATHS` exact-match guard against future subroute prefix bypasses.** `/onboarding` is now exact-match + explicit-subroute, not a `startsWith` check. Two new proxy guards pin the contract. - **`targets/route.ts` daily buckets keyed in Europe/Berlin.** The targets sparkline was the last analytics surface still bucketing in UTC, which produced a one-day-off trace for users in CEST. `berlinDayKey()` lifted to a shared `src/lib/analytics/berlin-day.ts` helper; four new DST + UTC- midnight edge-case unit tests. - **Streaming bubble vs persisted-twin race window.** The 150ms grace window suppresses the persisted twin while the in-flight streaming bubble is still rendering, so the thread never paints two copies of the same reply for a frame. - **Sticky section navigation a11y polish.** `aria-current` on the active section; focus-visible ring on keyboard navigation; `motion-reduce` honoured for smooth-scroll; the glow-bleed fixed via `bg-background/95 backdrop-blur`; the mobile cliff tightened from `scroll-mt-28` to `scroll-mt-16` so the strip no longer eats the heading at 280px viewports. - **BP tile mobile density at =sm`. ### Refactor - **`createSseStream` shared helper extracted from the chat route.** Preparation for v1.5 iOS streaming endpoints, which will reuse the SSE primitives. Three unit tests pin sync, async, and throw paths. Source: `src/lib/sse/create-stream.ts`. - **`` extracted from desktop + mobile api-token surfaces.** The badge logic was duplicated verbatim in two layouts; one component, two consumers. - **Five simplify apply-yes items in one commit.** `canSubmit` collapse, weekly-report `