Commit Graph
1078 Commits
Author SHA1 Message Date
Marc-André Bombeck dcbac63fdf feat(coach): goal / if-then plan memory + locale-keyed dashboard snapshot
Add a CoachPlan model: durable, user-scoped if-then implementation plans
the Coach proposes and the user confirms. The background memory-refresh
pass extracts plans the user agreed to and writes them as "proposed"; only
the user-facing PATCH on /api/coach/plans/[id] activates one. The free-text
cue / action / target are encrypted at rest with the shared Bytes codec;
metric, status and dates stay plain so the injection picker ranks without a
per-row decrypt. Active plans fold into the Coach snapshot's memory block
and survive the prompt-budget drop alongside facts. List, confirm and
soft-delete routes are Coach-gated and narrow the owner from the session.

Key the dashboard-snapshot query cell by locale so a locale switch reads
freshly localised tile copy on its own cell instead of serving the prior
locale's prose until the slot goes stale. The zero-arg key stays
byte-identical, so every prefix-based invalidation keeps reaching the
locale-keyed cells; the offline-banner and preloader match on the prefix.

Migration 0194 (additive CREATE TABLE). Encryption-rotation registry and
script cover the three new Bytes columns.
2026-06-27 01:25:35 +02:00
Marc-André Bombeck 2c33faa648 chore(insights): align the ambient-Coach file comments to the shipped version
The A1–A3 source files carried v1.22.0 docstrings from before the work was
re-cut as the v1.21.2 point release. Comment-only sweep so blame and the
in-file version tags read consistently with the shipped version.
2026-06-27 00:48:40 +02:00
Marc-André Bombeck 1de72d045e fix(insights): localise the Coach scope pill and clarify the return-to-baseline copy
The "the Coach is already on …" pill resolved its metric label through the
brand-free English domain vocabulary, so it read in English on every
non-English locale. It now prefers the existing localised measurements.type*
name for each measurement-backed source, keeping the English phrase only for
the gait/audio long tail that rarely becomes a scoped launch. Also rewords the
English return-to-baseline line so the day count reads as the in-range run
length rather than the time since.
2026-06-27 00:48:29 +02:00
Marc-André Bombeck 4d05bcf8d9 feat(insights): wire the score-card tension + return-to-baseline and the briefing recall
The hero score card and the daily briefing accept tension, returnToBand, and
memory props, but nothing populated them server-side, so the lines never rendered.

Resolve all three server-authoritatively on the dashboard snapshot DTO (iOS rides
the same payload):

- tension — fires when the readiness composite's contributors disagree (a strongly
  favourable one alongside a strongly unfavourable one), reusing the readiness
  components the wellness ring grades and the shared disagreement detector.
  Suppressed under a clinical red-flag (a coincident-deviation that no illness
  explains) so the red-flag path is never reconciled into a calm verdict.
- returnToBand — runs the streak detector across the salient vitals and surfaces
  at most one metric that has come back inside its own personal range after a
  genuine prior out-of-band run.
- memory — maps the machine-derived coach memory block to the briefing's recall +
  forward-look.

The blocks are locale-agnostic (contributor keys + a metric type); the insights
hero localises them through the existing readiness-contributor and metric-name
labels before handing the cards their resolved strings. Each value omits rather
than guesses. The snapshot cache key now carries the locale for the recall prose.
2026-06-27 00:28:56 +02:00
Marc-André Bombeck 6c1a4b7f45 merge: wave/v1220-narrative into release/v1.21.2 2026-06-26 23:58:51 +02:00
Marc-André Bombeck f5195cfcda merge: wave/v1220-coachui into release/v1.21.2 2026-06-26 23:58:51 +02:00
Marc-André Bombeck fe6c2e281b feat(insights): ambient narrative presence — callbacks, tension verdict, streaks, MI moves
Add a deterministic narrative layer the Coach narrates rather than invents:

- Briefing callbacks + forward-looks: the daily briefing recalls the prior
  period and points ahead, anchored to a figure, one callback per surface.
- Tension Verdict: when the readiness composite's contributors disagree
  (good sleep but a rising resting pulse), surface a brief honest internal
  read on the health-score card. A real clinical red-flag is never reconciled
  away into a calm verdict.
- Streaks + return-to-baseline: a pure detector over a DAY-bucket series
  closes a worry once a metric returns inside the personal MAD band after a
  genuine out-of-band run; a gap day breaks a streak.
- Coach prompt: add develop-discrepancy, roll-with-resistance, and an
  anti-persuasion clause; ratchet PROMPT_VERSION.

The tension flag and streak event are computed server-side; the card lines
render resolved fields and the model narrates them.
2026-06-26 23:57:37 +02:00
Marc-André Bombeck 9136a9bdba feat(coach): make launch scope visible and seed the blank state
Replace the hidden composer prefill with a visible scope affordance: when
the Coach is launched narrowed to a metric, the hero shows a scope pill
naming that metric plus a tappable seed question, and the drawer sources
rail surfaces the same scope line. When the Coach opens unscoped, resolve
today's most notable derived signal server-side and offer it as a tappable
opener; fall back to the neutral greeting when no signal crosses the gate,
so the blank box is never cold and never fabricated.

Add an internal GET /api/insights/coach/seeded-question that reuses the
briefing's derived-signal detector to select the opener server-side; the
client renders the resolved DTO.
2026-06-26 23:56:56 +02:00
Marc-André Bombeck cbf903841c feat(insights): add Coach-read strip above metric charts
Render a compact two-line "Coach read" above each metric sub-page chart:
an own-baseline line ("Your usual range is X–Y; today's Z sits within /
above / below") and one top lagged-association line from discovered
correlations, framed descriptively. It makes a metric read seen-in-context
rather than isolated.

The strip is server-authoritative: a new GET /api/insights/coach-read
resolves the band via the median ± k·MAD baseline engine (with its 7-day
history floor — below it the strip says "still learning your range" rather
than inventing a range) and the strongest on-metric driver from the
existing FDR-controlled, effect-size-floored correlation discovery (no
lowered floor; the line is omitted when nothing clears it). The client
component only renders the resolved DTO, so web and iOS read identical
numbers. DTO shapes and the pure selection logic live in a client-safe
sibling shared by the component and the unit test.

Wired through the HealthKit metric scaffold (every generic metric page) and
the bespoke weight, blood-pressure, and pulse pages.
2026-06-26 23:56:28 +02:00
Marc-André Bombeck d13dff93f2 fix(ui): pin dialog footer so the primary action stays in view
The desktop branch of ResponsiveSheet rendered DialogContent as a single
overflow-y-auto grid, so a tall form — or one made tall by browser zoom,
display scaling, or a longer-language label — scrolled its header, body,
and footer together and could push the primary button below the fold. The
branch now runs as a flex column capped at max-h-[calc(100dvh-2rem)], holds
the header and footer shrink-0, and scrolls only the body, matching the
bottom-sheet branch that already pinned its footer. Every form mounted
through the shared surface inherits the fix.

Add a desktop end-to-end guard at a short viewport asserting the body
scrolls and the footer's bottom edge stays within the viewport.
2026-06-26 22:55:31 +02:00
Marc-André Bombeck 8c5ef5df10 fix(medications): render intake datetime fields through the format-aware picker
The three intake dialogs (log, edit, dose-history add) still used a raw
datetime-local input, so they ignored the user's date-format preference.
Route them through DateTimeField like every other date surface. Also
correct a stale diastolic-floor comment in the dashboard verdict.
2026-06-26 14:03:39 +02:00
Marc-André Bombeck 03e1935c8a merge(LEARNUI): Learn-links registry, component, and surface wiring 2026-06-26 13:23:28 +02:00
Marc-André Bombeck 8c58b5b1c7 Wire Learn pointers into vitals, glucose, resilience, and lab surfaces
Add a single discreet LearnMoreLink to the per-vital baseline tiles
(via a new footer slot on the shared tile), the glucose clinical panel,
the resilience tile, and the lab biomarker detail. Each pointer is
registry-resolved and renders nothing when the concept has no mapped
guide, so only surfaces with a real article show one.
2026-06-26 13:22:33 +02:00
Marc-André Bombeck 6ed5b55f3d Add discreet LearnMoreLink pointer component
A small, calm anchor that links a concept out to its public /learn
guide. Fail-closed: the href is registry-backed (a closed-set value,
never user input) and an unmapped concept renders nothing. Plain
anchor, no markdown. Adds a common.learnMore label across all locales.
2026-06-26 13:22:27 +02:00
Marc-André Bombeck 92fc16448a chore(ui): drop the now-unused native date-input primitives
Every callsite moved to DateField/DateTimeField, which honour the
user's date-format preference; the native DateInput/DateTimeInput
wrappers have no remaining importers.
2026-06-26 13:19:44 +02:00
Marc-André Bombeck f854b16e86 merge(DF3): date-format rollout completion + DateTimeField 2026-06-26 13:18:56 +02:00
Marc-André Bombeck 01b24c4d73 feat(date): add DateTimeField and migrate datetime inputs
Add a dependency-free DateTimeField that paints the date part in the
user's date-order preference and the time part in the hour-cycle
preference over a hidden native datetime-local input, preserving the
local yyyy-MM-ddTHH:mm value contract. Migrate the seven DateTimeInput
callsites (measurement add/edit, mood add/edit, lab add/edit,
medication intake quick-add) so the chosen format is honoured there too,
and cover the field's value contract and per-preference rendering with a
component test.
2026-06-26 13:17:55 +02:00
Marc-André Bombeck 174c26afc9 refactor(coach): streamline the new-chat page composer
Reduce the full-page Coach new-chat surface to a calmer layout. Drop the
toolbar Conversations and New chat buttons and place the settings gear in
the top-right corner; both removed actions remain in the composer's + menu.
Collapse the greeting to one line and remove the gear from the composer so
its front is only the + actions menu, leaving a single baseline row of
[+] textarea [mic] [send].
2026-06-26 13:15:58 +02:00
Marc-André Bombeck 0816b3b0c0 feat(date): migrate remaining date inputs to DateField
Swap the four DateInput callsites (filter bar, export range, schedule
history, onboarding baseline) to DateField so the user's date-order
preference paints every date input. Adapt the onChange from a native
event to the direct ISO value DateField emits, and add an optional
autoComplete prop so the onboarding date-of-birth field keeps its bday
autofill hint.
2026-06-26 13:14:19 +02:00
Marc-André Bombeck 9a3049a6cf fix(coach): phrase the daily-limit reset as midnight UTC
The budget-exceeded copy named "00:00 UTC", which reads as a wrong local
clock for any non-UTC user (Berlin midnight UTC is 01:00/02:00 local) and
drove confused "the limit resets at the wrong time" reports. Reword
dailyLimitBody across all six locales to "midnight UTC", matching the
existing errorBudget wording, so the figure is honest regardless of the
reader's timezone.
2026-06-26 12:52:52 +02:00
Marc-André Bombeck 37dfce2104 merge(COACHUI): v1.21.0 2026-06-26 12:39:17 +02:00
Marc-André Bombeck 93433c3197 feat(coach): add "ask the Coach about this" to insight cards
Add a discreet, on-brand action to the high-value cards (briefing,
recommendation, correlation, status/assessment, health score, period
narrative) that opens the Coach pre-scoped to the card topic with a
seeded question. The action self-gates on the operator flag and the
per-user opt-out, never tints the card, and surfaces once per card.
2026-06-26 12:38:03 +02:00
Marc-André Bombeck 44796d1c0e feat(coach): carry metric scope into the Coach launch
Make the launch context's metric scope live instead of discarding it.
A conversation opened from a metric surface now narrows its snapshot to
that metric with a data-aware seed question, and the global FAB inherits
the page's ambient scope so drilling into a metric and tapping it no
longer opens a blank chat. The scope threads through the drawer into the
first turn of a fresh conversation.
2026-06-26 12:37:54 +02:00
Marc-André Bombeck bf58d5d0b3 feat(ui): render date inputs in the user's date-format preference
Swap the medication scheduling, inventory, illness, vorsorge, lab-OCR
and profile-birthdate date inputs from the native field to the
DateField primitive, which paints the chosen DMY/MDY/YMD/AUTO order
over an ISO yyyy-MM-dd value. The committed value contract, min/max
bounds and aria wiring are unchanged.
2026-06-26 12:26:09 +02:00
Marc-André Bombeck d6430528e9 feat(ui): add the DateField date input
Dependency-free date input that displays the value in the user's
date-format preference while editing rides the native picker behind a
calendar button. The committed value stays ISO yyyy-MM-dd, so it is a
drop-in for DateInput. Height/target-size parity, disabled/min/max/
placeholder, and progressive typed entry parsed against the order.
2026-06-26 12:15:22 +02:00
Marc-André Bombeck 9b7980d020 feat(settings): add the date-format profile control
Surface a Datumsformat select below Stundenformat with the four
preferences, in all six locales.
2026-06-26 12:15:16 +02:00
Marc-André Bombeck 86a9ca2e1e feat(profile): persist and expose the date-format preference
Accept dateFormat on the profile PATCH field-by-field, echo it from
the profile + /me reads, mirror it into the client auth user, and add
the field to the OpenAPI request/response schemas.
2026-06-26 12:15:09 +02:00
Marc-André Bombeck 1c93a670f0 feat(labs): surface low-confidence OCR rows in review
The extraction already self-scores each field; the review screen only
hinted at a shaky numeric value. Add a calm per-row low-confidence badge
when any tracked field scores below the threshold, including qualitative
rows that had no confidence cue. Informative secondary badge, no alarm
colour.
2026-06-26 09:26:59 +02:00
Marc-André Bombeck 2c112c3323 fix(dashboard): stop the chart-row render loop on tab resume
A user reported the dashboard crashing on returning to a backgrounded
tab. The on-focus snapshot refetch re-rendered the page, and each chart
received a fresh inline `() => markChartReady(id)` handler every render;
the chart's data-ready notify effect, keyed on that ever-changing prop,
re-fired on every commit. That per-commit passive effect kept the chart
row's anchored popovers re-committing until the browser tripped its
render-depth guard and the page fell back to its error card.

Latch the notify behind a ref so it fires once, when the chart's initial
query settles, and depend on the load flag alone. The handler is read
through a ref so a late-arriving callback still fires without re-arming
the effect. Extract the pure fire-once decision into a small helper with
direct unit coverage (no DOM runner, per project convention).
2026-06-23 21:45:42 +02:00
Marc-André Bombeck 8d6d63cd1a feat(illness): surface sleep as a context line on the recovery card
Adds a neutral sleep observation alongside the per-episode recovery gap: the
user's usual asleep minutes over their well baseline vs the mean over the
episode window, narrated as 'you slept more/less than usual' when the two
diverge. It is an observation only — never a recovery claim, never a return
track, and never part of the headline recovery-gap number.

Per-night asleep totals come from the canonical night reconstruction over the
baseline and episode windows, so a multi-source night (two writers for one
sleep) is counted once, matching every other sleep surface. The observation
withholds entirely on thin data — too few baseline or episode nights, or a
delta below a 30-minute floor — so day-to-day jitter is never narrated as a
finding.

Additive optional DTO field; no schema, no migration. Two locale keys across
the six bundles. Regenerate the OpenAPI contract from the Zod source.
2026-06-22 07:06:22 +02:00
Marc-André Bombeck 5a2d759e50 Merge symptom-severity recovery track into release/v1.19.2
Fold the illness journal's per-day functional-impact curve (falling back
to linked symptom severity) into the recovery-gap as a return track
against the known-constant healthy baseline. It is adverse by
construction, feeds the qualifying-days floor and the gap median, and
withholds on sparse logging. The card names the driver when symptoms
lead ('…before your logged symptoms eased').
2026-06-21 21:07:02 +02:00
Marc-André Bombeck 1352ba2aa6 feat(illness): name the symptom curve in the recovery-gap card
When the logged symptom curve drives the recovery gap, the card names it
('you felt better N days before your logged symptoms eased') instead of
the vital-driven phrasing. Mirror the new gapDriverType and the return
track's adverse flag into the client DTO, and add the driver-aware copy
across all six locales.
2026-06-21 21:06:14 +02:00
Marc-André Bombeck 38e10b9ca0 Merge multi-year chart range into release/v1.19.2
A multi-year All range was silently truncated to the most recent year by
the DAY-only 365-row cap. Route windows wider than the cap through a new
tiered rollup reader (WEEK/MONTH by span, finer on coverage miss,
uncapped by tier choice); narrow windows stay byte-identical. No silent
truncation — a coverage miss logs and falls through to the daily path.
2026-06-21 21:02:06 +02:00
Marc-André Bombeck 9ea258556d docs(charts): note the multi-year tier step-up in the chart data path
The chart still requests aggregate=daily for windows over 7 days, but
the server now steps the rollup tier up to WEEK / MONTH for ranges past
the DAY cap and returns whole-history coverage. Update the stale
comments that claimed the "All" range was still capped at ~365 daily
buckets; the client fold + bucketTimeSeries downsample the coarse
buckets to the visible range as before.
2026-06-21 21:01:10 +02:00
Marc-André Bombeck 559db472fc Merge Oura resilience tile into release/v1.19.2
Surface the captured Oura resilience band (limited…exceptional) as a calm
tile on the recovery surface, with a quiet above/in-line/below-average cue
and a learning state while the series is sparse. Server-authoritative read
of the existing summaries slice; neutral card chrome.
2026-06-21 20:58:42 +02:00
Marc-André Bombeck 7f40860891 feat(insights): surface the Oura resilience band on the recovery page
v1.19.0 ingests Oura's daily resilience level end-to-end (RESILIENCE,
ordinal-encoded limited=1 … exceptional=5) but no surface rendered it.
Add a calm resilience tile to the recovery page: it names the latest
band, gives a one-line description, and a quiet "vs the recent average"
cue once a few days exist, with a learning note while the series is
still sparse. Resilience is a categorical band, not a continuous score,
so it reads as a dedicated readout rather than a misleading line chart.

Server-authoritative — the tile reads the stored summaries slice (latest
+ mean, both computed server-side) and never re-derives the value. Calm
posture: one neutral card, no green-when-good / red-when-low tint. The
band label and trend cue carry the read in muted text only.

Self-gating: renders nothing without a reading, and counts toward the
recovery-page data gate so an Oura-only account no longer sees the empty
note. Copy lands in all six locales.
2026-06-21 20:57:30 +02:00
Marc-André Bombeck 1bf8f3d661 refactor(coach): drop the unread streaming startedAt and reasoning state
The thinking disclosure was removed, so the coach streaming bubble no
longer reads startedAt or reasoning. Both fields stayed populated by the
send hook but nothing consumed them. Remove them from
CoachStreamingMessage and every writer, accept the additive reasoning
SSE frame as an explicit no-op so the wire type stays recognised, and
update the thread-test fixture. Streaming is otherwise unchanged.
2026-06-21 20:54:39 +02:00
Marc-André Bombeck 638bc6c835 feat(illness): name the driving vital in the recovery-gap summary
The cross-episode summary already computes per-vital physiological returns
but the card surfaced only one bare line. Thread the dominant returning
vital through the aggregate and the DTO so the copy reads 'you tend to feel
better before your resting heart rate settles' instead of a context-free
gap figure.

The aggregate tallies which vital produced a real return across the
episodes that qualified for the typical-gap median and reports the most
frequent one (deterministic alphabetical tiebreak); the driver is named
only when a gap actually clears the magnitude gate. Zero new compute — the
returns were already in hand. Adds the localized copy in all six locales.
2026-06-21 14:11:20 +02:00
Marc-André Bombeck 36affd5daf fix(illness): gate the recovery-gap floor on adverse-direction days
The cross-episode typical-gap median counts an episode toward its
qualifying-days floor via the per-episode coverage `historyDays`, which
counts a day if ANY banded vital has a reading. A WEIGHT-only episode (no
illness-adverse direction) could therefore clear the floor and feed the
median despite carrying no illness-relevant signal, weakening the honest-
sample claim.

Compute a distinct-adverse-day count in the engine — days with at least
one notable reading in the metric's adverse direction — and feed that as
the qualifying-days floor instead. Reuses the engine's single adverse-
direction predicate (now exported as `isAdverseDeviation`); a neutral-
direction vital never counts a day. The insufficient arm contributes 0,
which is correct since its gap is null and never reaches the median.
2026-06-21 14:07:44 +02:00
Marc-André Bombeck 62a4dfdf7e Merge S1/S2 sleep average + mood saturation into release/v1.19.1
S1: add an average-per-night sleep card as a third peer beside debt and
chronotype, computed by a shared helper over the same scorable-night set
so the three cannot disagree; grid widens to three columns. S2: restore
full saturation on the two missed mood surfaces (calendar heatmap and the
time-of-day chart), keeping the quiet tint only for no-data cells.
2026-06-21 14:02:09 +02:00
Marc-André Bombeck bbff6ab143 fix(mood): full-saturate the time-of-day chart and mood calendar
The v1.19.0 saturation fix missed two surfaces: the mood-by-time-of-day
bars and the mood calendar still rendered populated values at 0.55
opacity, reading as a dimmed glitch. Drop the dimming on populated bars
and cells (and the legend swatches); keep the quiet tint only on the
no-data empty state.
2026-06-21 14:01:37 +02:00
Marc-André Bombeck c67bd942a2 feat(sleep): add average-per-night card beside sleep debt and chronotype
Compute the mean asleep minutes per night from the same canonical
per-night data the sleep-debt and chronotype cards read, exposed as a
new averagePerNight field on the sleep-rhythm DTO. Surface it as a third
peer card with identical chrome, so the three tiles sit side by side.
Calm partial state under a four-night floor; localised in six locales.
2026-06-21 14:01:30 +02:00
Marc-André Bombeck 430f7e81a1 Merge C1/C2/C3/C5 Coach UI rework into release/v1.19.1
C1: /coach defaults to the new-chat hero, auto-resuming only on an unread
coach-initiated message. C2: always-visible Conversations + New chat
toolbar. C3: replace the thinking disclosure with the typing-dots into
word-fade streaming animation. C5: the drawer Conversations handoff opens
the history list on arrival so the full view is never a blank pane.
2026-06-21 13:59:48 +02:00
Marc-André Bombeck c6f4772606 fix(coach): default to new chat and never dead-end on conversations
Reverse the resume-most-recent entry default: opening the Coach now lands
on the new-chat hero. The only exception is an unread coach-initiated
message (nudge-status `unread`), which auto-opens the most-recent thread so
the user sees what the Coach wrote. The `?c=<id>` deep-link is unchanged.

Add a dedicated, always-visible Conversations button (plus a New chat
button) to the page surface so reaching past conversations no longer hides
in the composer menu. The drawer's Conversations handoff now routes to
`/coach?view=conversations`, which opens the history drawer on arrival, so
the side-view to full-view path always shows the list instead of a blank
pane.
2026-06-21 13:59:05 +02:00
Marc-André Bombeck a86abd3662 fix(coach): restore the typing animation for the live turn
Replace the elapsed-time thinking disclosure with the classic typing
indicator: while a turn is still thinking with no prose, the assistant
bubble shows the pulsing typing dots, then swaps to the streamed
word-by-word prose once tokens land. Drop the now-unused reasoning/
startedAt bubble props and the orphaned disclosure component.
2026-06-21 13:58:57 +02:00
Marc-André Bombeck de14238535 feat(nav): fix the sidebar order of the clinical and insight spine
Below the dashboard/measurements/mood/cycle head, the entries now follow one
fixed sequence: Medications, Vorsorge, Labs, Illness, Insights, Coach,
Achievements. A disabled module drops out; the rest keep this order.
2026-06-21 13:58:43 +02:00
Marc-André Bombeck 8d1c90b8b6 fix(settings): label the layout nav entry Dashboard to match where it leads
The left-nav entry read "Layout" but the editor it opens is the dashboard
layout home. Rename the label across all six locales; the route stays
/settings/layout.
2026-06-21 13:56:38 +02:00
Marc-André Bombeck 26230542ce fix(sleep): make the chronotype tile uniform with the sleep-debt tile
The chronotype card carried its own section heading on top of the title it
already renders, so 'Chronotype' appeared twice and the tile did not match
its sibling. Drop the standalone heading and bring the card to the same
shape as the sleep-debt tile: identical chrome and a bold primary value with
one secondary line. The chronotype band is the primary value at the same
scale the debt tile uses for its hours figure, with the natural sleep-
midpoint clock as the companion line.
2026-06-21 12:31:26 +02:00
Marc-André Bombeck 2760f6b327 Merge w11b/ouraresil into release/v1.19.0 2026-06-21 12:22:21 +02:00
Marc-André Bombeck 9680f318a3 Merge w11b/chartrange into release/v1.19.0 2026-06-21 11:00:24 +02:00