Commit Graph
1347 Commits
Author SHA1 Message Date
Marc-André Bombeck a2bfa644e2 i18n: translate HealthKit metric labels and the generic load-error string
Fill the remaining English-fallback values in es/fr/it/pl for the Apple
Health / WHOOP measurement-type labels (irregular-rhythm, high/low heart
rate, walking steadiness, breathing disturbance, recovery/stress/strain
scores) and the shared common.loadFailed message.
2026-06-04 18:22:09 +02:00
Marc-André Bombeck 227dd2b73f fix(settings): show a pending state on the danger-zone confirm buttons
The data-reset and account-delete confirmations fired their mutation
from an uncontrolled AlertDialog, so the action dismissed the dialog
the instant it was clicked and the only spinner lived on the now-hidden
trigger. A user on a slow link saw no feedback and could re-open and
re-submit a destructive, irreversible action.

Drive both dialogs from controlled open state and add an in-dialog
pending treatment to the confirm button — disabled, aria-busy, and a
spinner — mirroring the bulk-delete confirmation in the medication
intake history. The dialog now holds open while the request is in
flight; the data-reset closes on success, the account-delete keeps its
pending state through the post-delete redirect.
2026-06-04 18:22:02 +02:00
Marc-André Bombeck c24f42848e feat(settings): add copy button and revoke error toast to API tokens
The one-time API token rendered in a bare code block with no way to
copy it short of selecting the text by hand — the worst place for copy
friction since the value is shown exactly once. Add a copy-to-clipboard
button beside the token with a success toast, reusing the
clipboard-plus-toast pattern already used by the per-medication token
row. Surface an inline check affordance for two seconds after a copy.

Token revoke previously swallowed every failure: a non-OK response or a
network error left the user with no signal the token was still live.
Add an error toast on both paths so a failed revoke is visible.
2026-06-04 18:22:02 +02:00
Marc-André Bombeck 4e2248dfff fix(insights): drop the empty tail on the signal and narrative cards
Both overview cards carried a min-h floor sized for their tallest state and
applied it to every state, so the calm everyday read left a large empty band
at the bottom.

- Today's signal: the min-h-48 floor now applies only to the tall `fired`
  state; all-clear / watch / insufficient size to their short content.
- Period overview: the resolved card drops the min-h-40 floor and the footer's
  mt-auto that pinned the timestamp to the bottom of the floored height.

The in-flight skeletons keep their min-h footprint, so the first paint still
reserves the row and does not shift as the read resolves.
2026-06-04 18:21:14 +02:00
Marc-André Bombeck 93bdbd8e0c fix(insights): match the header target gear to the Coach icon size
The target gear painted a heavier 44 px box on mobile against the Coach
icon's flat 40 px, so the header cluster read as two mismatched icons.
Settle the gear on a flat 40 px box that equals the Coach sparkle on every
breakpoint, and restore the 44 px WCAG 2.5.5 touch target through a
transparent ::before overlay rather than a larger painted box. The shared
icon-button size stays untouched, so other icon affordances are unaffected.
2026-06-04 18:20:38 +02:00
Marc-André Bombeck 252921b563 fix(insights): tidy the assessment card and gate its show-more toggle
Drop the top-right "cached" label from the assessment card — it surfaced
an implementation detail and read as if the insight were second-hand. The
warm cache still backs the text; the card just no longer announces it.

Mount the "show more" toggle only when the clamped prose actually
overflows three lines, measured against the live layout via a
ResizeObserver instead of a character-count guess. Short assessments that
fit no longer grow a toggle that reveals nothing, and text that wraps past
three lines on a narrow column now gets one.

Move the toggle copy off the inline locale-prefix fallback onto proper
`insights.assessmentShowMore` / `insights.assessmentShowLess` keys across
all six locales, and right-align the "last updated" line.
2026-06-04 18:20:38 +02:00
Marc-André Bombeck 4e6599d85f feat(mood-insights): promote mood to a discovery outcome and surface it
Two changes that turn the descriptive relations surfaces into a
statistically-gated discovery view on the mood page.

Mood as an outcome: the FDR-controlled all-pairs correlation discovery
engine treated mood only as a behaviour (a lag source). Promote it to an
outcome channel too, so the scan also surfaces "behaviour today → next-day
mood" relations (e.g. more daylight today → better mood tomorrow) alongside
the existing "mood today → next-day outcome" direction. The self-pair is
skipped and Benjamini-Hochberg already controls the larger pair family this
opens up. The discovery route now feeds the shared mood daily-mean series
into both roles.

Surface the discovery slice: the engine was computed but never rendered.
The mood Insights page now shows the mood-involving pairs that survive the
false-discovery correction, each phrased observationally with its paired-day
count, r, and BH q-value, plus the honest "X of N tested" footer. The
surface self-fetches the correlation route and degrades to nothing when the
operator has disabled it. Observational only, with the standing disclaimer.
2026-06-04 18:09:07 +02:00
Marc-André Bombeck f1edbd17d1 feat(mood-insights): add tag influence and the "better days" relations board
Two new observational surfaces on the mood Insights page, both computed in
the single mood-aggregates engine so the charts and the coach snapshot stay
in sync.

Influence on mood (per tag): for each frequent flat and structured tag,
compare the average daily mood on days the tag is present against the
counterfactual baseline of days it is absent, with the delta and a
confidence band. The band comes from a Welch two-sample t-test reusing the
exact Student-t p-value already behind the correlation cards, not a naive
approximation. Both groups must clear a five-day floor; rows rank by
absolute delta and cap per axis. Sparse tags, single-day tags,
all-same-value days, and zero-delta tags are dropped cleanly — never NaN.

Better days board: one ranked list folding the tag deltas and the
mood x health-metric correlations (sleep, steps, pulse, weight, blood
pressure) into a single effect-size-ranked, confidence-gated surface — the
headline answer to "what goes with my good days?". Near-zero and
under-sampled correlations are excluded; the list is capped and observational
only, with the standing "association, not cause" caption.

Both surfaces ride the mood-status snapshot too so the assessment prose
never contradicts the on-screen relations. New copy in all six locales.
2026-06-04 18:09:07 +02:00
Marc-André Bombeck 528ce93c71 feat(insights): move target-range editing to a header gear
The target reference card carried an inline "Adjust target range" link,
which read as a control buried in a read surface. Lift the action to a
gear button in the category-page header, to the left of the Coach launch
icon, so the two sit as a consistent icon cluster beside the heading.

A small page-scoped provider bridges the two: the reference card
registers its editable target (one per page, except blood glucose which
fans out to several contexts and registers each) and the header gear
opens the per-metric editor sheet the provider owns. Single-target pages
open that target directly; glucose opens the primary context, since
target editing lives only on these pages — there is no separate targets
management surface. The card itself is now a pure read surface.

The gear carries the action's existing label as its accessible name,
clears the 44 px touch-target floor on mobile, and exposes a stable
`data-slot` for end-to-end coverage.
2026-06-04 17:47:17 +02:00
Marc-André Bombeck 9396bfea46 fix(insights): align the target reference card to the shared card surface
The per-metric target reference panel rendered on a borderless
`bg-muted/30` inset, which read as a nested detail block rather than a
top-level card. Side-by-side with the assessment, trajectory, trend and
recommendation cards on the same category page it looked off-tone. Match
the compact card surface those siblings use (`bg-card/20` tint, a
`border-border/40` hairline, the shared `rounded-lg`) so the panel reads
as one family.
2026-06-04 17:47:17 +02:00
Marc-André Bombeck af040e7480 Merge pull request #255 from MBombeck/release/v1.11.4
release/v1.11.4 — sleep totals, trend captions, and a round of fixes
v1.11.4
2026-06-04 17:46:08 +02:00
Marc-André Bombeck 8a26548123 test(analytics): expect the per-night sleep total in the stage-aggregation integration test
The SLEEP_DURATION summary now reports one per-night asleep total (CORE +
DEEP + REM) instead of a raw per-stage row count, so the stage-aggregation
test expects one datapoint of 400 minutes rather than four rows. The
per-stage breakdown assertions are unchanged.
2026-06-04 17:39:39 +02:00
Marc-André Bombeck cb4fd14a8a chore(release): v1.11.4 — sleep totals, trend captions, and a round of fixes 2026-06-04 17:27:12 +02:00
Marc-André Bombeck 49a029e023 fix(sleep): group per-night totals by session, not per-stage calendar day
Per-stage SLEEP_DURATION rows carry the stage segment's END instant, so a
night where the user falls asleep before local midnight spread its stages
across two calendar days. Keying each stage by its own day split one
overnight sleep into two partial nights, and the headline last-night tile
showed only the post-midnight slice.

Reconstruct nights by clustering stage segments into sessions: a new session
starts only when the next segment's start (end minus its minute duration) is
more than three hours after the running session end. A single long stage
block no longer splits a night, a daytime nap stays separable from the
following overnight block, and the gap is measured on the absolute instant so
the clustering is DST-immune. Each session is keyed by the local wake day of
its latest segment end, matching the convention that a sleep session belongs
to the morning you wake up.

Collapse a multi-source night to one canonical source before summing, using
the user's sleep source-priority ladder (default WHOOP > Apple Health >
Withings), so a dual-source account no longer double-counts the total. Sources
are never blended within a night.

Bound the series sleep read to one year even on the 'Alle' range, matching the
slim slice's sleep window, so a multi-year dual-source account does not walk a
six-figure stage-row set into JS.

Add fixtures straddling local midnight, crossing a DST spring-forward, keeping
a nap separable, and collapsing a dual-source night.
2026-06-04 17:25:19 +02:00
Marc-André Bombeck 4666359470 feat(insights): compute a deterministic trend caption when the AI annotation is absent
Each Trends-row card carries a one-line caption below its mini-chart.
The first choice is the AI advisor's per-metric sentence, but on a cold
briefing — a web-only account, a pre-briefing cached payload, or the
advisor still generating — that sentence is null and every card fell
back to a static "Awaiting more data" hint, even with a full 30-day
series painted right above it.

Add a deterministic, rule-based descriptor derived from the same series
the mini-chart plots, and apply a three-tier caption precedence:

  1. advisor annotation present → the AI sentence (unchanged).
  2. else, a computable series   → the deterministic descriptor:
     direction (rising / falling / stable) plus the first-vs-last
     magnitude over the window, e.g. "Rising over 30 days (+8 mmHg)",
     "Falling over 30 days (−1.4 kg)", "Stable over the last 30 days".
     Mood reads on a categorical 1–5 scale, so it is phrased in plain
     terms ("Mood trended slightly higher over 30 days") with no raw
     point delta.
  3. else (genuinely < 2 points)  → the real "Awaiting more data" hint.

The descriptor is observational and neutral by construction: direction
and magnitude only, with no causal, diagnostic, or medical claim and no
value judgement — a rising weight and a rising step count read the same
way here. A per-metric stability floor (the larger of an absolute and a
relative bound) keeps day-to-day noise from reading as a trend.

The descriptor logic lives in a pure, unit-tested helper. The caption
reads the same series the chart shows: mood reuses the existing
mood-analytics cache slot, and numeric metrics take a small dedicated
30-day daily-aggregate read keyed under a new query-key factory entry
that joins the measurement-write invalidation set. New descriptor copy
ships in all six locales. The pending shimmer while the advisor is in
flight is unchanged.
2026-06-04 17:05:35 +02:00
Marc-André Bombeck a2dff85fb8 fix(insights): contain the mood trend card so its axis labels stop overflowing
The Trends row renders three mini-chart cards (blood pressure, weight,
mood). The mood card alone overflowed: its categorical y-axis ("Super
gut/Gut/Okay/...") and date x-axis paint inside a Card shell that runs
taller than the flat HealthChart minis beside it, so the x-axis date
labels bled past the fixed chart slot and overlapped the caption below.

Bound the overflow on the slot itself, without touching the chart
components:

- `overflow-hidden` clips anything the inner chart paints past the
  180px envelope, so a stray axis label can never escape into the
  caption row.
- `[--chart-height:120px]` drives both mini charts' internal band down
  to a shared 120px (both honour `h-[var(--chart-height,140px)]`). The
  mood card's full envelope — band + header chrome + axis labels — now
  fits inside 180px, so all three tiles share one chart-band baseline.

Recharts and the chart tokens are untouched; the fix lives entirely on
the row's slot container.
2026-06-04 17:05:29 +02:00
Marc-André Bombeck 32ce6dfa18 fix(charts): render sparse metric data instead of withholding it
The metric chart withheld every reading behind a "more days needed"
card until three distinct daily points existed, so a user with one or
two days of real data (e.g. respiratory rate) saw a hint instead of
their measurements. Render the available points for any non-empty
window — a single marker for one day, a line for two — and add a
subtle inline caption that more days fill out the trend. Only a
genuinely empty window keeps the no-data card.

The trend, moving-average, and comparison overlays already self-gate
at two points, so relaxing the render gate cannot paint a misleading
line from too little data. Drop the now-unused raw-count plumbing the
old three-point copy split relied on.
2026-06-04 16:50:34 +02:00
Marc-André Bombeck ebeb16daa3 fix(sleep): emit the per-night total, not a single stage, on the sleep tile and series
SLEEP_DURATION is stored in minutes, one row per sleep stage per night
(IN_BED / AWAKE / ASLEEP / CORE / DEEP / REM) since the HealthKit
category-sample ingest landed. The value is not cumulative, so reading
the single most-recent row surfaced one stage's minutes — never the
night. The dashboard sleep tile and the sleep series therefore showed a
fragment of the night rather than the night.

Add a shared `summarizeSleepNights` helper that groups the per-stage
rows by the wake-day calendar date in the user's timezone and sums the
asleep stages (CORE + DEEP + REM, plus a legacy bare-duration row),
excluding IN_BED and AWAKE per the AASM convention already used by the
sleep score. Wire it into:

- the dashboard summary tile, which now emits last night's time asleep
  in hours with an explicit `unit: "h"` and a per-stage breakdown for a
  future detail view;
- the sleep series, which now returns one point per night (time asleep
  in hours) plus a top-level `unit` for every kind so clients never
  infer the unit;
- the slim summaries slice, so the web dashboard tile and the insights
  overview read per-night totals (kept in canonical minutes there; the
  web tile converts to hours) — strictly more correct than averaging
  across stage rows, and parity with the iOS value.
2026-06-04 16:44:53 +02:00
Marc-André Bombeck 6e241c16a4 fix(dashboard): stop the recent-achievements card flashing in then retracting
On a cold dashboard load the recent-achievements card briefly showed content
(or its empty state) and then vanished. Two causes:

- The layout-toggle gate read `DEFAULT_DASHBOARD_LAYOUT` while the real layout
  was still loading; achievements (and recent workouts) default to visible, so
  the card painted during the load window and was retracted once a layout that
  hid it arrived. Gate both layout-toggle-only cards on the resolved layout.
- The card itself rendered its empty / content branch before its query settled,
  so the "no achievements yet" state flashed ahead of the real unlocks. Render a
  skeleton while the query is pending and commit to a content shape only once it
  resolves.
2026-06-04 16:44:46 +02:00
Marc-André Bombeck 5e60eaf9c0 fix(insights): serve the daily briefing read-only so the overview never blocks on the LLM
The hero strip, daily-briefing card and trends row all read the advisor
payload through `useInsightsAdvisorQuery`, which POSTed `/api/insights/generate`
without `force`. That POST generates inline on a cache miss, pinning the
page-load path on the full provider chain (up to the client's 8 s abort) before
the briefing paints — even though a read-only lift of `User.insightsCachedText`
costs nothing.

Add a read-only GET to the route that serves the cached payload immediately and,
when the cache is stale or missing and a provider is configured, enqueues an
out-of-band warm on the existing `insight-pregenerate` queue. The read mount now
uses the GET; user-initiated regeneration keeps the POST force path. Mirrors the
stale-while-revalidate posture of the per-metric status and period-narrative
reads — no provider call ever sits on a navigation request.
2026-06-04 16:44:46 +02:00
Marc-André Bombeck accd7c8514 fix(measurements): merge MANUAL and Apple Health rows for the same reading
A measurement logged by hand on the iOS app while offline is mirrored
into HealthKit. On pairing a server, adopt-on-pair uploads it as a
MANUAL row while HealthKit background sync independently re-ingests the
same sample as an APPLE_HEALTH row. The two carry different external ids
and sources, so the composite unique index keeps both — doubling the
whole hand-logged history on first pair.

Collapse the pair at ingest: an incoming MANUAL or APPLE_HEALTH row is
dropped as a duplicate when a same-reading row of the opposite source
already exists (same type and value, measuredAt within two seconds).
The match is scoped to that one cross-source pair, so same-source rows
and rows from server-owned sources keep their existing contracts, and
two genuinely distinct readings at the same minute never collapse.
stats:* cumulative rows stay on their per-day overwrite lane.

The collapsed copy returns status "duplicate" with reason
"cross_source_merge" so the client cursor advances unchanged; no
schema, migration, or OpenAPI change.
2026-06-04 16:44:39 +02:00
Marc-André Bombeck b54b95dbc7 fix(insights): bound the mood time-of-day chart height
The "mood by time of day" card spans the full overview width, unlike its
distribution/weekday siblings which sit in a two-column grid. Sizing the
chart wrapper with aspect-[3/2] derived the height off the full card width,
so on a wide viewport the chart ballooned to roughly 800px tall and
overflowed its card. Swap the aspect ratio for a fixed bounded height so the
chart fits the card at the same size as the width-constrained sibling charts
regardless of viewport. Chart internals and tokens are unchanged.
2026-06-04 16:44:39 +02:00
Marc-André Bombeck 42945efa04 fix(insights): stop clipping long wellness-score ring labels
The wellness-score ring tiles passed the metric title into the ScoreRing
as an in-SVG centred <tspan>. A long localised title (Schlaf-Score,
Bereitschaft) was wider than the small ring's clear centre, and because
the text is SVG-centred inside the recharts overflow:hidden svg the leading
glyph was clipped (the missing leading "S" on Schlaf-Score). Render the
title as an HTML caption below the ring instead, where it wraps within the
tile and never truncates; the ring keeps just the number centred. The
in-SVG label slot stays for the anatomy detail view, which only ever passes
the short "/100" string.
2026-06-04 16:44:39 +02:00
Marc-André Bombeck 65b988c686 fix(settings): right-align danger-zone action buttons beside their labels
The data-reset and account-delete cards under Settings → Advanced
stacked their destructive button below the description. Move the
button to a right-aligned slot on the same row as the title and
description, matching the stack-on-mobile / right-align-on-desktop
contract the onboarding-tour and about cards already use
(flex-col gap-3 sm:flex-row sm:justify-between, button w-full sm:w-auto).
The AlertDialog confirmation, i18n keys, and destructive styling are
unchanged.
2026-06-04 16:44:39 +02:00
Marc-André Bombeck 979bfe4c01 Merge pull request #254 from MBombeck/release/v1.11.3
release/v1.11.3 — WHOOP body data, quality-of-life polish, fuller translations
v1.11.3
2026-06-04 16:06:19 +02:00
Marc-André Bombeck 3bc6124cf1 chore(release): v1.11.3 — WHOOP body data, quality-of-life polish, fuller translations 2026-06-04 15:58:51 +02:00
Marc-André Bombeck 41d0628a5f fix(i18n): use the formal register for the French feedback-error string 2026-06-04 15:57:14 +02:00
Marc-André Bombeck b99f2234a2 fix(whoop): reject non-positive or non-finite body-measurement values
WHOOP body fields are self-reported profile data; guard mapBody against
absent, NaN/Infinity, or non-positive values so a garbage reading never
seeds a WEIGHT measurement or a profile height of 0.
2026-06-04 15:57:14 +02:00
Marc-André Bombeck 1a88a3e6cf refactor(whoop): single-source the collection-403 soft-skip handling
Extract handleCollectionFetchError so the five resource syncs share one
soft-skip-vs-rethrow path instead of five copies. Behaviour is unchanged:
a per-resource 403 returns 0 and keeps the connection connected, anything
else records a classified failure and rethrows.
2026-06-04 15:57:14 +02:00
Marc-André Bombeck a8c0a30f29 fix(dashboard): skip prefilled date pickers in the quick-entry dirty check
A freshly-opened quick-entry sheet prefills its datetime-local field with
the current time, so the dismiss-time dirty check read every pristine
sheet as unsaved and always raised the discard confirm. Exclude
date/time picker types from the scan; typed text and notes still count.
2026-06-04 15:57:14 +02:00
Marc-André Bombeck 76426a2f2e fix(dashboard): match the tile skeleton to the resting tile height
The tile-strip skeleton and per-tile Suspense fallback reserved only 6rem,
shorter than a populated TrendCard (~8rem), so the strip grew when data
landed. Bump both to 8rem and mirror the card's overflow-hidden chrome on
the fallback to keep first-paint layout shift at zero.
2026-06-04 15:37:36 +02:00
Marc-André Bombeck e591137162 fix(dashboard): confirm before discarding a partly-filled quick-entry sheet
An accidental overlay tap, Escape, or mobile swipe-down on a quick-entry
sheet silently dropped any typed measurement, mood, or intake. Detect
unsaved input at dismiss time and ask before closing; an explicit Cancel
or a successful save still closes straight away.
2026-06-04 15:37:36 +02:00
Marc-André Bombeck ad3bbd5507 fix(settings): show a friendly message when an export download fails
The export cards rendered the raw "Download failed (500)" error string to
the user. Map the failure to a localized message across all six locales.
2026-06-04 15:37:36 +02:00
Marc-André Bombeck eddc06d135 style(settings): align the mobile passkey card padding with sibling surfaces
The mobile passkey list rows used a tighter p-3 while every other card
surface in the account section uses the standard padding step. Bump to p-4
so nested card surfaces read consistently; radius stays rounded-lg.
2026-06-04 15:37:36 +02:00
Marc-André Bombeck 51959a4737 fix(settings): give the API-token revoke button an accessible name and a larger target
The revoke icon button announced only "button" to screen readers and sat
at a 32px target. Add a localized aria-label and bump it to 36px.
2026-06-04 15:37:36 +02:00
Marc-André Bombeck 16b7306243 fix(insights): surface an error toast when recommendation feedback fails
The thumb-up / thumb-down POST used to fail silently — on error the
buttons just reappeared with no signal that the submission had not gone
through. Add a lightweight localized error toast on failure while keeping
the success path quiet (the inline confirmation row is signal enough
there).
2026-06-04 15:36:20 +02:00
Marc-André Bombeck 0baad1e736 refactor(insights): share one block skeleton across dynamic loaders, anchor the warm-assessments control
The six `next/dynamic` loaders on the insights overview each rendered a
bespoke `<div className="… h-[Xrem] animate-pulse …" />` with a guessed
fixed height. Those heights pinned each placeholder taller or shorter than
the resolved block, so the page shifted as each chunk landed. Route every
loader through one shared `BlockSkeleton` built on the `Skeleton` primitive
(which already honours reduced motion) and hold the row open with a
`min-h` floor rather than a hard-coded pixel guess, removing the
resolve-time layout shift. Decorative placeholders for the cards that can
un-mount stay `aria-hidden`.

Anchor the warm-assessments control too: it floated right-aligned with no
label, reading as a context-less affordance. Pair it with a left-aligned
caption that explains the nightly-refresh model so the button has a home.
2026-06-04 15:36:20 +02:00
Marc-André Bombeck e63fae564b test(insights): pin the reduced-motion guard for .animate-insight-in
The 400 ms `.animate-insight-in` entrance keyframe is applied across five
insight surfaces and is collapsed to `animation: none` once, centrally, in
a `@media (prefers-reduced-motion: reduce)` block in globals.css. Extend
the motion-reduce coverage test to assert that central guard so a future
edit cannot silently drop it and replay the entrance motion for
motion-sensitive users.
2026-06-04 15:36:20 +02:00
Marc-André Bombeck 60b86f1ec9 fix(insights): suppress recommendation-card hover lift under reduced motion
The recommendation cards carry `transition-all md:hover:-translate-y-0.5`
for a subtle lift on pointer hover. Motion-sensitive users got the full
transform with no opt-out. Pair the hover with
`motion-reduce:transition-none motion-reduce:hover:translate-y-0` so the
card stays put when the user asks for less motion.
2026-06-04 15:36:20 +02:00
Marc-André Bombeck fb4037ed96 fix(coach): tidy the message thread type scale, bubble width and empty state
Lift the disclaimer and feedback-thanks lines off the off-scale 10px
size onto the standard extra-small step. Unify the user and assistant
row gap, and budget the avatar column out of the bubble's 80% cap so a
bubble plus its avatar never overflow a comfortable width on a narrow
phone. Announce the empty-thread hero as a polite live region so screen
readers read the hint when the thread first mounts.
2026-06-04 15:36:16 +02:00
Marc-André Bombeck b12dc1e870 perf(coach): parallelise the cold-path snapshot reads
The snapshot builder ran its independent reads sequentially on a cache
miss: feature extraction, the shared measurement query, the mood,
compliance, sleep and workout table reads, and the GLP-1, derived,
trajectory and memory helper blocks each awaited in turn. None consumes
another's result, so they now fire concurrently and resolve in batched
hops — feature extraction and the measurement read first (both gate the
per-metric blocks), then the remaining reads in one Promise.all. The
source guards are unchanged, so a disabled source still issues no query,
and the synchronous block assembly keeps its original order, so the
provenance envelope is byte-for-byte the same. Cuts several round-trips
off the cold build; the warm path is cached and unaffected.
2026-06-04 15:36:16 +02:00
Marc-André Bombeck 2e3bfbb971 feat(coach): add a stop control and abort the stream on drawer close
While a reply streams the composer now swaps the send button for a
visible Stop control bound to the abort handler, so a long or off-track
reply can be interrupted instead of waited out. Closing the drawer
mid-stream also calls the cancel handler, so the SSE request no longer
keeps running in the background after the drawer is gone.

Add a localised "Stop" label across all six bundles.
2026-06-04 15:36:16 +02:00
Marc-André Bombeck fc6a55b91c fix(medications): give the wizard stepper jump buttons a 44px touch target
The first / last jump buttons rendered at 32px, below the minimum touch
target. Extend the hit area with an invisible inset pseudo-element so the
tappable region is 44px while the visual button stays 32px; the existing
aria-labels are unchanged.
2026-06-04 15:36:12 +02:00
Marc-André Bombeck 3c7d12f9c1 fix(medications): replace the list loading spinner with card skeletons
The bare centred spinner reserved a 32-unit-tall box that the resolved
card grid then displaced, jumping the layout. Render a grid of card
skeletons built on the real Card shell + shared Skeleton primitive so the
loading state matches the loaded footprint.
2026-06-04 15:36:12 +02:00
Marc-André Bombeck 0c3c80a4f6 fix(medications): show a pending spinner on the intake-edit save button
The intake-edit dialog already tracks a busy state and sets aria-busy, but
the Save button gave no visual feedback while the PUT was in flight, unlike
the wizard and quick-add. Render the same inline spinner during the
mutation.
2026-06-04 15:36:12 +02:00
Marc-André Bombeck 8434af40cb fix(medications): surface failed dose logging and add an undo affordance
A failed take/skip POST cleared the spinner without any feedback, so the
user believed the dose was logged when it was not. Show an error toast on
a non-ok response or a thrown request, and never show the success
confirmation in that case.

On a successful take/skip, attach an Undo action to the toast that
soft-deletes the just-recorded intake via the per-event delete route, so
a misclick no longer needs a trip through the intake history to correct.
2026-06-04 15:36:12 +02:00
Marc-André Bombeck 0cb8070a22 feat(i18n): translate Achievements copy for es/fr/it/pl
The achievements namespace — badge titles, descriptions, category
labels, and dashboard card copy — rendered verbatim English outside
German. Translate the surface for the four remaining locales, keeping
the format-token strings and the Engagement category as-is.
2026-06-04 15:36:06 +02:00
Marc-André Bombeck 06da3c44ee feat(i18n): translate Medications copy for es/fr/it/pl
Schedule, wizard, intake, and detail-page copy in the medication
surface rendered verbatim English for the four non-reference locales.
Translate the wizard steps, cadence options, intake history, reminder
controls, and danger-zone prose, leaving units, ATC/RxNorm codes, and
GLP-1 INN drug names in their language-appropriate form.
2026-06-04 15:36:06 +02:00
Marc-André Bombeck e8793c8212 feat(i18n): translate Settings copy for es/fr/it/pl
Large stretches of the Settings surface — account, passkeys,
notification channels, AI provider configuration, Withings, API
tokens, export, and the danger zone — rendered verbatim English in
the four non-reference locales. Translate the genuine prose and
labels, leaving brand names, units, and protocol tokens as-is.
2026-06-04 15:36:06 +02:00
Marc-André Bombeck 35264eaf94 feat(i18n): translate rhythm-event copy for es/fr/it/pl
The insights.rhythmEvents event labels and verdicts shipped verbatim
English in the four non-reference locales. Translate the 11 affected
keys so the device-health-notifications surface reads in the user's
language.
2026-06-04 15:36:06 +02:00