Commit Graph
339 Commits
Author SHA1 Message Date
Marc-André Bombeck 43388eabbf Merge wave-coach: Claude/ChatGPT-style Coach frontend redesign 2026-06-19 19:48:33 +02:00
Marc-André Bombeck 36adedad58 feat(coach): premium chat surface — narrower column, collapsible rail, voice dictation
Redesign the Coach frontend toward a calm, prompt-first reading
experience:

- Narrow the reading column to a centred max-w-2xl with a roomier
  vertical rhythm so the conversation reads like a document rather than
  a packed log.
- Make the page-surface conversation rail collapsible and collapsed by
  default. A single labelled toggle in the rail strip opens it (lg+),
  the rail heading carries a matching close control, and the column
  folds to zero width with a smooth grid transition. Below lg the
  bottom tray is unchanged. The fullscreen header keeps only the
  minimize control.
- Replace the boxy default scrollbar on the thread, history list, and
  composer with a thin, rounded, subtle bar across WebKit and Firefox,
  scoped to the components.
- Polish the pre-stream beat: the Thinking indicator now pairs bouncing
  dots with the label, and the assistant turn fades in so the hand-off
  to streamed tokens reads as one motion.
- Return voice dictation to the composer via the Web Speech API. The
  mic mounts only where the browser supports recognition, dictates into
  the textarea, and is keyboard- and screen-reader-accessible.

New labels land in all six locale bundles.
2026-06-19 19:48:03 +02:00
Marc-André Bombeck d92b3b9efd style(insights): calm and flatten the health-score rings
The score dials wore a token-tinted drop-shadow bloom, a conic specular
sheen, a breathing pulse on the green ring, and a dark-to-neon two-stop
arc gradient. Against the flat 2-D Recharts charts that read as a glowing
3-D gauge and made the overview restless.

Make the calm treatment the default everywhere, not only behind the `flat`
prop:

- Drop the bloom, the sheen, and the pulse for every ring. `.wellness-ring-arc`
  is now a plain flat stroke; the sheen overlay paints nothing. The shared
  cycle ring inherits the same quiet.
- Mute the arc colours toward the chart palette: the two gradient stops sit a
  hair apart for a whisper of depth instead of a neon sweep, pulled down and
  desaturated per hue in both the Dracula and Alucard sets. Band semantics
  (green/yellow/red thresholds) are unchanged.
- `flat` now only drops the entrance sweep + count-up, so the dashboard hero
  ring paints its value at once. The /insights strip keeps the subtle spring
  fill; prefers-reduced-motion still zeroes it.

Score and band logic, thresholds, and the data-band/aria contract are
untouched.
2026-06-19 19:45:23 +02:00
Marc-André Bombeck f931f2f5c2 feat(insights): render signals of the day and remove the overview ask-coach block
Render the new signalsOfDay list at the top of the daily-briefing card — a
present-focused row (headline + concrete nudge) sharing the tone bar, metric
icon, and sub-page routing of the key-finding row, with matching typography
(text-sm headline, text-xs muted body) so the card reads consistently with the
rest of the insight surfaces. Add the signalsTitle string across all six
locales.

Remove the "Ask the coach" action button and the guided-questions chip strip
from the overview hero band: the Coach stays the bottom-right drawer mounted by
the insights layout shell. The hero is now greeting + subtitle + baseline meta
+ the optional Health Score panel, and no longer reads the coach flags or the
launch context. Update the coach-cascade / disable-cascade fixtures and the
hero-strip slot tests to pin the affordances absent.
2026-06-19 19:37:01 +02:00
Marc-André Bombeck 6f866ff1c6 fix(coach): bound the conversation scroll region so the composer stays pinned
The message thread used h-full inside a plain-block wrapper, so its height
resolved against an auto-height parent: the scroll container grew with its
content instead of scrolling, pushing the composer down and leaving a stray
scrollbar on the page.

Make the thread wrapper a flex column and switch the thread (and its empty
state) from h-full to min-h-0 flex-1, so the scroll region takes a bounded
track from the flex parent. The conversation now scrolls cleanly inside the
card while the composer stays pinned to the bottom.
2026-06-18 23:58:43 +02:00
Marc-André Bombeck 3ba542f797 fix(onboarding): make the guided tour first-time-only, drop every re-entry trigger
The guided tour now exists only as the first-time auto-start after onboarding.
Remove every in-app trigger that re-opened it: the per-module "show this tour"
button on the dashboard, measurements, mood, medications, labs, illness, cycle,
achievements, insights, and coach surfaces; and the Settings replay card.

Delete the now-orphaned ModuleTourTrigger component, the tour-restart worker,
the healthlog:module-tour and healthlog:tour-restart event listeners, the
single-stop overlay path, and the force-launch session marker. The first-time
auto-launch gate and the tour overlay itself are unchanged. Prune the orphaned
i18n keys across all locales and update the affected tests.
2026-06-18 23:58:27 +02:00
Marc-André Bombeck 8ff3b51ba3 refactor: remove dead exports and an unused prop
Drop the health-score card's inert onAskCoach prop and its now-dead
pass-through from the hero strip, the buildDashboardBands re-export that no
caller reached through the snapshot module, an unused tour-progress status
type, and an orphan service-worker cache helper.
2026-06-18 22:42:27 +02:00
Marc-André Bombeck 9014203d8a fix(a11y): label per-reminder toggles and wire disclosure controls
The diabetes PATCH parsed its body without a size cap; bound it through
safeJson like the sibling routes. Label each reminder's enable/disable switch
by its own name span so screen readers hear the reminder rather than one
generic label repeated per row. Point the chronotype and illness disclosure
triggers at the regions they reveal via aria-controls.
2026-06-18 22:42:04 +02:00
Marc-André Bombeck 04a50b2702 refactor(insights): drop the per-page generic medical disclaimers
Remove the page-level Insights disclaimer footer and the health-score and
correlation card disclaimers now that the not-a-diagnosis acknowledgment
is shown once at onboarding and the legal text stays on the privacy page.
Device-provenance, contraception-safety, statistical-method and
shared-report notes stay where they convey specific information.
2026-06-18 22:11:45 +02:00
Marc-André Bombeck 6614e0646f merge: Coach v1.18.6 nudge and conversation continuity
Add the Coach proactive nudge-status surface and last-seen tracking
(migration renumbered to 0176) plus the seen endpoint.
2026-06-18 22:04:31 +02:00
Marc-André Bombeck 34fbfe1c67 feat(insights): link the overview Trends cards to their detail pages
The overview charted each trend but never offered a way into the metric's
detail page — the steps page in particular exists and is used, yet Insights
never linked to it. Give every Trends-row chart a routed detail target and a
discreet "view details" link below the caption, so a metric the row surfaces
(steps included, when the briefing flags it) drills into /insights/<slug>.
2026-06-18 21:24:02 +02:00
Marc-André Bombeck 71b29f5b6f refactor(recovery): drop the per-group sub-headings on the recovery page
Read next to the page's "Erholung" heading, the "Recharge" / "Strain & load" /
"Daily heart & energy" sub-headings were redundant chrome. Remove them and
render the present signal blocks as one flat, evenly-spaced stack — each block
already carries its own title + explainer, so nothing loses context. The group
structure stays internally to drive block order and the data gate. Promote the
block title to an h2 so the page heading outline stays nested (h1 then h2) with
no skipped level, and drop the now-orphaned group i18n keys.
2026-06-18 21:20:19 +02:00
Marc-André Bombeck 2535d45fc7 refactor(insights): drop the redundant per-metric Coach launcher
Every metric sub-page header carried a Coach launch icon. With the Coach
FAB anchored bottom-right on every authenticated page, that header icon
was a second entry point to the same surface. Remove it from the shared
sub-page shell so the FAB is the single Coach launcher app-wide. The
coachLaunch prop stays accepted on the shell (the metric pages still pass
it) but no longer renders a control, so no per-page churn is needed.
2026-06-18 21:19:42 +02:00
Marc-André Bombeck 96c5641787 fix(coach): give the chat page more width and fix the rail scrollbar
The Coach page read as cut off against other pages and the left
conversation scrollbar looked broken.

Width: the page sat inside the shared max-w-screen-xl content frame with
a tall pb-20 FAB gutter that does nothing on /coach (the FAB hides
there). Break out of the frame so the chat claims the full content width
and viewport height, edge-to-edge on small screens and a contained card
from sm up, and centre the thread on a comfortable reading measure.

Scrollbar: the page-surface history rail was wrapped in an overflow-auto
container while the rail itself is a flex column with its own
overflow-auto list — two nested scroll regions. The inner list's height
never resolved, so its scrollbar rendered against an indeterminate track
and the search field scrolled away. Drop the outer scroll: the rail
keeps a fixed search header and one internal list-scroll.
2026-06-18 21:19:36 +02:00
Marc-André Bombeck 3a3f88a80c feat(coach): mark an unread proactive nudge on the Coach launcher
Add a discreet unread dot to the Coach FAB when the Coach has something
the user has not opened yet, and clear it when they open the Coach.

The signal is server-authoritative: a new coach_last_seen_at column on
users records the last time the Coach was opened, and the launcher's
status endpoint reports unread when the newest assistant message is
newer than that stamp — so the dot works even with no push channel and
clears across every device, web and iOS. Opening the Coach (FAB or the
/coach page) stamps the column via a new /api/insights/coach/seen
endpoint; the launcher keeps a local stamp only for an instant paint.

The dot is a calm cyan, not an alarming red, per the card-status rule.
2026-06-18 21:19:27 +02:00
Marc-André Bombeck aa9b713a66 feat(sleep): ground the sleep-quality assessment in the user's own scores
The sleep-quality "Einschätzung" read "no assessment yet" until an AI provider
had run, and stayed blank on an account with no provider configured. Fill that
slot with a deterministic note built from the user's own quality averages —
efficiency, performance, consistency and the headline score graded against
recognised reference bands — so the assessment is always substantive. The
richer AI narrative still wins when present; the grounded note only steps in
once the assessment query settles with no narrative, so the two never stack.
2026-06-18 21:17:00 +02:00
Marc-André Bombeck d8c139698c feat(sleep): surface the chronotype as a labelled corner readout
Move the chronotype band out of a body badge into a labelled top-right corner
of the card — a small "Chronotype" label over the value — so the resolved
type is the first thing the eye lands on. Tighten the mid-sleep copy to name
it as the body's natural sleep midpoint on free days.
2026-06-18 21:16:43 +02:00
Marc-André Bombeck c69e6deb67 fix(sleep): drop the misleading last-night timeline for estimated stages
The last-night card drew a stepped stage timeline for every source. For a
source that reports per-stage totals without measured onsets (WHOOP / Polar),
the server lays the segments out in a fixed physiological order, so the bar
invents a phase progression that was never recorded — it reads as one Core
block then one Deep block, not a real hypnogram.

Suppress the timeline when the night is reconstructed and keep the per-stage
breakdown + totals, which are real. A calm note explains that stage timing is
estimated for that source. Real-series nights (Apple Health, Withings, Fitbit)
keep the measured timeline unchanged.
2026-06-18 21:11:18 +02:00
Marc-André Bombeck 9be30897fa feat(insights): add a cited reference-range backbone and richer metric explainers
Introduce src/lib/reference-ranges.ts: a structured, citation-backed
per-metric reference-range backbone (band + source body + year) with a
deterministic four-state placement helper, read by the UI surfaces. Each
band traces to the medical-citations registry, which gains the supporting
sources (ACC/AHA 2017 and ESC 2024 blood pressure, StatPearls pulse
pressure and mean arterial pressure, ESC/ESH 2018 pulse-wave velocity,
FDA 2024 and StatPearls pulse oximetry, J Gen Intern Med 2019 temperature,
RCP NEWS2 and American Lung Association respiratory rate, visceral-fat
imaging literature, AHA 2024 resting heart rate, ESC/NASPE 1996 HRV,
Wilcox 2000, ACOG 651, Phillips 2017 and CDC 2024 sleep).

Blood pressure keeps ESH 2023 as the labelled default; ACC/AHA 2017 and
ESC 2024 appear only as displayed placement context. Enrich the metric
explainers with concise cited reference bands and a general-guidance
tooltip on every metric page: a darker-skin pulse-oximeter over-read
caveat, device-proxy honesty for readiness/recovery/strain/stress, an
HRV day-to-day-noise note, and a resting-heart-rate clarification. Fill
the empty registry normal ranges, reconcile the steps green floor to
8,000, and tighten the body-temperature band below the fever line. Add
free pulse-pressure and mean-arterial-pressure context on the blood-
pressure detail page, plus cycle-physiology and sleep-hygiene copy across
all six locales. Cycle thresholds carry a verification marker pending an
ACOG source check.
2026-06-18 20:17:04 +02:00
Marc-André Bombeck 1d83e10764 merge(W4): module-by-module guided tour
Extend the in-house spotlight into a cross-page tour: 15 module stops,
each gated on its module, walking the app and teaching what each module
does and how it feeds the others. Auto-starts after the anamnesis,
resumes from onboarding_tour_progress_json, re-enters per-module via a
header trigger, and replays from one Settings card. Adds the tour
progress pref + migration 0173 and the onboarding tour checkpoint route.

# Conflicts:
#	src/components/onboarding/DoneScreen.tsx
2026-06-18 19:55:00 +02:00
Marc-André Bombeck 33fec28b29 feat(onboarding): per-module tour re-entry + consolidated replay
Add a 'Diese Tour zeigen' affordance to every module header that opens the
tour narrowed to that module's card in place, plus additive data-tour
anchors (measurements/medications/labs/illness/vorsorge/cycle/mood/
insights/coach/integrations/export/achievements) so the cross-page walk
has a real target on each page. Consolidate the full-tour replay onto a
single Settings -> Advanced card, relabel it, and drop the duplicate from
About. The replay reset clears the stored progress so it restarts cleanly.
2026-06-18 19:49:37 +02:00
Marc-André Bombeck e0d4d809dd feat(glucose): cross-link the clinical panel to Labs
Glucose has three front doors — measurements/CGM as readings over time,
the clinical panel as derived TIR/GMI/estimated-A1c, and Labs as dated
panel results — with no signpost between them. The clinical panel now
carries a one-line cross-link clarifying that a blood-draw HbA1c or
fasting glucose belongs in Labs, so the spot-estimate view and the
lab-result view reference each other instead of competing. Adds
insights.bloodGlucose.clinical.labsLink across all six locales.
2026-06-18 19:17:36 +02:00
Marc-André Bombeck ef1fbb8396 feat(insights): state Rest Mode on the health-score card
During an active illness episode the server suppresses (never penalises)
the health score, but the card gave no sign of it, so a held number read
as a silent decline. The card now carries an explicit 'paused during
illness — not being judged today' line when Rest Mode is active, driven
from the value-free annotation already on the analytics payload. Adds
insights.healthScore.restModePaused across all six locales.
2026-06-18 19:11:58 +02:00
Marc-André Bombeck 3d213d1fbf feat(insights): turn the no-provider dead-ends into guided setup
Coach, the daily briefing, and the per-metric assessment tiles all
default ON but need a connected AI provider that onboarding never
mentions, so a fresh user hits a bare error on the headline feature.

- The daily-briefing and per-metric no-provider states now explain what
  the AI does and why a provider is needed (your own key or a local
  model, data stays with you) rather than a one-line 'unavailable'.
- The per-metric tile gains a direct Settings link.
- The Coach 'no provider configured' turn maps to its own copy with a
  Settings link instead of the transient 'try again in a moment' that
  would 422 forever.

Adds insights.noProviderAction, insights.coach.errorNoProvider and
errorNoProviderAction across all six locales.
2026-06-18 19:07:38 +02:00
Marc-André Bombeck 1eb143fad4 fix(a11y): raise sub-44px tap targets to the mobile floor
Bring interactive elements that fell below the 44px iOS/WCAG touch
floor up to min-h-11 on phones (relaxing to the denser sm density on
larger screens), and add visible focus rings where they were missing:

- TimesOfDayChips remove button + API-token copy buttons gain an
  inset hit-area so the visual density stays tight while the tap
  target clears the floor.
- Dose-history row-actions menu trigger and the insights suggested-
  prompt chips raise to min-h-11 sm:min-h-9.
- Sortable table headers and the glucose advanced disclosure toggle
  gain a focus-visible ring and a phone tap floor.
2026-06-18 15:15:01 +02:00
Marc-André Bombeck cdc76620fb fix(insights,dashboard): rest-mode web banner, sleep per-chart assessments, steps explainer, recovery heading level 2026-06-16 23:20:41 +02:00
Marc-André Bombeck 201686f498 fix(insights): surface Rest Mode on the web and align sleep assessments
Add a calm, value-free Rest Mode indicator to the dashboard score card and
the recovery insight so the web matches what iOS already shows: when an
illness/condition episode is active the score and recovery are framed, never
penalised, and the surface now says so. The new banner is neutral and
untinted, self-gates on an active episode, and reveals only the onset date —
never a decrypted note or episode label. The dashboard reads the annotation
from the snapshot (resolved server-side alongside the score); the recovery
section reads it from the analytics payload.

Give the sleep page one assessment per distinct chart group: the
duration/architecture assessment now sits with its charts, and the
sleep-quality block carries its own SLEEP_SCORE assessment, matching the
"multiple charts, multiple texts" structure the recovery page meets.

Add the steps explainer (insights.subPage.explainer.steps*) and pass
explainerMetric="steps", and demote the recovery per-block title to an h3 so
the heading outline nests cleanly under the page heading and group heading.
2026-06-16 23:20:04 +02:00
Marc-André Bombeck 7451e886f1 fix(vorsorge,coach): align reminders + coach chrome with the gold standard
Twice-daily BP protocol (RRULE BYHOUR=7,19) now fires at every clock
hour: byHourTimesOfDay expands BYHOUR into the engine's timesOfDay so
the label, RRULE, and engine output agree instead of collapsing to one
nudge at notifyHour.

Vorsorge surface:
- move create/edit into a ResponsiveSheet with a pinned Cancel/Save
  footer, matching Labs/Illness/Medications
- wire the unused PATCH support: Edit affordance opens the form
  pre-filled; an enable/disable Switch drives the enabled flag
- translate COACH-origin labels (the row stores an i18n key, not prose)
  and render a neutral Coach provenance badge + an until-<date> line
- surface origin + endsOn on the hook type
- pull-to-refresh + always-on rationale documented on the nav entry
- NativeSelect-vs-Radix choice documented as deliberate

Coach:
- expired COACH course-window reminders self-clean: the cron tick
  soft-deletes finished finite cadences instead of leaving dead rows
- accept-dedup gains a partial unique index (user, metric) where
  origin=COACH and not deleted, with a P2002 catch in the route
- per-user rate-limit bucket on the suggestion-action endpoint
- SUGGEST-REMINDER parser keeps prose written after the block
- document POST /api/coach/reminder-suggestions in OpenAPI

Coach chrome:
- HistoryRail hideHeading prop drops the duplicate Conversations
  heading on the page surface
- align the rail-tray strip inset to px-3 sm:px-4 so the dividers line
  up; reword the FAB inset comment

satisfyReminder closes the forward-only TOCTOU via a conditional
updateMany (count===0 no-op). Migration headers corrected to v1.18.1.
2026-06-16 23:11:08 +02:00
Marc-André Bombeck b28fa4d4a5 merge: suppress-during-illness Rest Mode + FHIR Condition + doctor-report 2026-06-16 22:27:13 +02:00
Marc-André Bombeck c59cf0b652 feat(illness): Rest Mode — annotate score/recovery/streaks during an active episode + FHIR/doctor-report
An active illness/condition episode annotates, it never penalises: the
measured number is untouched, only the narrative changes.

- rest-mode.ts: one server-authoritative resolver (module-gated, fail-soft)
  for "is the account in Rest Mode + which episodes back it". iOS mirrors the
  resolved context, never recomputes the suppression.
- health-score: /api/analytics attaches a value-free Rest Mode annotation;
  computeHealthScore stays pure (score never adjusted).
- recovery-score: computeRecoveryScore carries the Rest Mode context as of the
  scored day; the persisted blend is unchanged.
- coach-nudge: pause cadence-nudges during an active episode (new
  skippedDuringIllness counter, gate sits before the trigger search).
- achievements: bridgeFrozenStreakGaps freezes (does not break) every
  day-streak across illness days; never invents a streak, a non-illness gap
  still breaks.
- coincident-deviation: reframe a fired flag as illness-explained; vital
  numbers untouched. Surfaced as a neutral line on the insights card.
- coach snapshot: illness history + active illness as load-bearing context so
  the coach knows about Rest Mode (no decrypted note reaches the prompt).
- FHIR: illness episodes map to Condition (generic disease root, label on
  code.text, patient-reported/unconfirmed) + a bounding Encounter.
- doctor-report: module-gated illness section in the aggregator + PDF table
  (retrospective, neutral palette); FHIR Condition section in the bundle.

i18n x6 for the report + reframe copy. Tests: score-annotation-not-
penalisation, streak-freeze, coach-nudge pause, FHIR Condition/Encounter
mapping, doctor-report inclusion, Rest Mode resolver. typecheck/lint/test
green, openapi in sync, knip clean.
2026-06-16 22:25:50 +02:00
Marc-André Bombeck 042510d1d4 merge: flatten the dashboard hero to match the chart-card design 2026-06-16 21:55:34 +02:00
Marc-André Bombeck 651aa65c28 fix(dashboard): flatten the hero band to match the tile + chart surfaces
The daily-verdict band wore a gradient surface and a health-score ring
with a drop-shadow bloom, a green-band breathing pulse, a conic sheen,
and a sweep entrance. It read louder than the chart cards and tiles it
sits among.

Drop the gradient: the band now wears the same bg-card + border + radius
+ padding as <TrendCard>, and the dose chip sits on a neutral muted
surface (no card tint, blur, or shadow). Add a flat treatment to
<ScoreRing> that suppresses the bloom, pulse, sheen, and entrance sweep,
and pass it on the hero ring only. The wellness strip and anatomy view
keep the full reveal. Reduced-motion handling is unchanged.

Information is untouched — only the visual treatment changes.
2026-06-16 21:54:25 +02:00
Marc-André Bombeck e51fb49b89 refactor(coach): rework the coach card layout for balance and breathing room
Rebuild the coach conversation surface so it uses the full width without
feeling cramped:

- Widen the inline history rail (260px -> 300px on lg, 340px on xl) and
  bump rail typography + padding off the cramped 10/11px scale so the
  conversation list and source toggles read at a comfortable size.
- Align the dividers across columns: the header, the history-rail
  heading, and the centre rail-tray strip now share one fixed h-14 band
  with identical borders, so the lines land on the same row instead of
  the restless two-height split.
- Redesign the header leading cluster: the surface-toggle sits in its
  own group behind a hairline divider rather than crowding the avatar,
  which steps up to size-9.
- Give the full-page coach card more vertical room and a soft shadow so
  it fills the standalone route instead of leaving a wasted band below.
- Even the coach FAB corner inset so the right gap matches the bottom
  gap at each breakpoint.
2026-06-16 21:54:08 +02:00
Marc-André Bombeck bc2ce702f1 fix(insights): rebuild the recovery page onto the canonical metric structure
The recovery page rendered a grid of compact device-score tiles plus a
redundant cross-link to the recovery-score anatomy at the top. It now
follows the same structure as every other metric page (blood pressure,
pulse, the HealthKit scaffold): each present device-native signal renders
a block carrying a short explainer, the max / median / mean stat strip,
the shared chart with the identical 7 / 30 / 90 / All toggle, and a
per-chart assessment. Multiple charts now carry multiple matching texts.

Register the seven device-native recovery / strain signals (ANS charge,
day / workout strain, cardio load, whole-day average / max heart rate,
energy expenditure) in the generic metric-status registry so each chart
gets a generated assessment, gate them on the recovery module, and give
each its own chart-overlay slot. Drop the duplicate top score-link block
(this surface is reached from the overview already).

Regenerate the OpenAPI contract for the widened metric-status enum.
2026-06-16 21:35:09 +02:00
Marc-André Bombeck 12f4bc24e8 feat(coach): evidence-based cadence suggestions with one-tap in-chat reminder
The Coach can append a ---SUGGEST-REMINDER--- sentinel to propose a
measurement cadence (weight daily, the ESH/AHA 7-2-2 home-BP protocol,
structured glucose). The server resolves the cadence from a closed catalog
and emits an additive 'suggestion' SSE frame the chat renders as a one-tap
action card; accepting it creates a MeasurementReminder with origin COACH
through the same engine the Vorsorge surface uses (server-authoritative
schedule, finite course window self-expires via endsOn).

Non-naggy by construction: a frequency-cap cooldown, dismissal memory, a
master opt-out under Coach settings, and an explicit 'I measure enough'
stop path, all in the coach-prefs blob. Triple dedup at suggest, render,
and create so a cadence is never double-suggested or double-created, and
suggestions for a disabled module never surface. RHR/HRV stay passive --
the Coach surfaces the baseline, it never nudges to 'go measure' them.

The reminder DTO now echoes origin and endsOn for the iOS client.
2026-06-16 21:17:51 +02:00
Marc-André BombeckandClaude Opus 4.8 3de53f549c fix(health-score): drop the mood pillar when the mood module is disabled
The Personal Health Score weighted the 0.20 mood-stability pillar
unconditionally — the fast-path read mood entries regardless of the
account's module state, so a user who turned the mood module off was
still scored (and silently penalised or rewarded) by a pillar hidden
everywhere else in the product. The dashboard already blanks the mood
tile and the coach snapshot unions mood out; the score ring and the
insights card pillar list did not.

Thread a default-on `moodEnabled` flag through the fast-path. When the
caller resolves the mood module as disabled the helper skips the mood
read entirely and leaves the series empty, so `moodStability` returns
null and the existing proportional null-redistribution re-scales the
surviving pillars to sum to 100. The analytics route resolves the
module server-side and threads the flag; the insights card hides the
Mood row via a new SSR-safe `useModuleEnabled` accessor so it never
advertises a pillar the account turned off.

Golden fixtures pin mood-on vs mood-off to different, correctly
re-weighted scores and assert the disabled read is never issued.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 00:39:55 +02:00
Marc-André Bombeck 5682055fe2 feat(modules): hide nav entries and Insights pills for disabled modules
Generalise the per-destination cycle gate into a requiresModule marker
on each nav destination and read it from the account's resolved module
map (the modules field on GET /api/auth/me). Fold the bespoke cycle
boolean into requiresModule: "cycle" so there is one gating mechanism;
cycle and coach are delegated keys already reflected in the map, so
reading them here is not a re-derivation.

Tag mood, cycle, labs, coach and achievements with their module keys;
core destinations (measurements, medications, vorsorge, insights, the
dashboard) carry none and always render. The sidebar, the mobile More
hub, and the Insights tab strip all filter on the same map.

Insights pills: a metric pill belonging to a toggleable module (mood,
sleep, glucose, workouts) hides when its module is disabled, on top of
the existing data + saved-layout gates; the Recovery pill gates on the
recovery module. Overview and core-metric pills always show. Every gate
fails open — a missing key or an absent map keeps the entry, matching
the gate's default-on contract, so a stale /me payload never blanks the
nav.
2026-06-15 21:18:47 +02:00
Marc-André Bombeck 640793a3e8 refactor(nav): surface Recovery as an Insights tab instead of a left-nav item
Recovery is a composite wearable surface (WHOOP / Polar / Oura recovery,
strain, ANS charge, cardio load), not a top-level tracking destination.
It moves off the left nav and onto the Insights tab strip as a pill at
/insights/recovery. The pill is always present (like Overview) since the
page self-gates every block and falls back to a calm empty note rather
than carrying its own availability signal. Adds the insights.navRecovery
label across all six locales.
2026-06-15 21:03:48 +02:00
Marc-André Bombeck aa27708ebc refactor(nav): move Coach to a standalone /coach page
The Coach lived under /insights/coach, rendering inside the Insights
layout's tab strip. It is the product's headline surface, not a metric
sub-page, so it now lives at the top-level /coach in the standard page
chrome without the Insights tab strip. The nav model points at /coach,
every in-app link and the proactive-nudge deep link follow, and a 301
redirect keeps /insights/coach (bookmarks, cached PWA navigation, older
push payloads) resolving.
2026-06-15 21:00:46 +02:00
Marc-André Bombeck dad0b89a8c fix(insights): unify device-score tiles onto the shared card and skeleton primitives
Route the recovery and sleep-quality device-score surfaces through the
canonical card and loading primitives so they read as one family:

- DeviceScoreTile titles itself with CardHeader/CardTitle + CardAction
  and drops its bespoke header row and padding override, so it matches
  the sibling sleep-debt and chronotype cards on the unified
  p-4 md:p-6 token.
- Add DeviceScoreTileSkeleton / DeviceScoreGridSkeleton, a shared
  tile-shaped placeholder that mirrors the loaded layout. Recovery and
  sleep-quality paint it while their analytics slice loads instead of
  popping in.
- Recovery's no-data branch now renders the shared EmptyState with an
  icon rather than a hand-rolled div; the sleep-quality sub-section
  keeps its calm collapse-to-null once data lands.
- New insights.recovery.emptyTitle key across all six locales.
2026-06-15 01:22:43 +02:00
Marc-André Bombeck 1b0c4b2b7d test(insights): pin the device-score data-gating across the new surfaces
Cover the contract that matters for the vendor-native score tiles: a tile or
section appears only when its metric has stored readings. Asserts the empty
tile collapses to nothing, the sparse series swaps to the learning state, the
Sleep quality block renders only the metrics with data, and the Recovery page
shows its calm empty note with no data and groups + cross-links the score once
present.
2026-06-15 00:39:34 +02:00
Marc-André Bombeck 866f3dc0e2 feat(insights): add a Recovery sub-page for the device-native strain scores
WHOOP and Polar record day strain, workout strain, ANS charge, cardio load, and
the day's whole-cycle average / max heart rate and kilojoule energy expenditure
— all stored end-to-end with no render host. The composite recovery score had
only its anatomy detail at /insights/scores/recovery; the raw signals around it
appeared nowhere.

Add /insights/recovery as the home for them, grouped into recharge, strain &
load, and the day's heart & energy. Each block is data-gated, so an account
without a strap or ring lands on a calm empty note rather than a broken surface.
When a recovery score is present the page cross-links to its anatomy view rather
than duplicating the ring. Server-authoritative throughout.

Give the page a single nav home alongside Workouts in the unified destination
model so it is discoverable on both the sidebar and the mobile More hub.
2026-06-15 00:39:18 +02:00
Marc-André Bombeck e063f79353 feat(sleep): surface the nightly sleep-quality scores on the Sleep page
WHOOP and Oura write sleep efficiency, performance, consistency, need, the
disturbance count, and the Oura sleep score every night; until now none of them
rendered anywhere. Add a Sleep quality block after the rhythm section that
surfaces all six through the device-score tile.

Each tile is data-gated, so a non-wearable account never sees an empty card and
the whole block collapses metric-by-metric down to nothing when no source
writes them. It reads the same analytics summaries slice the page already
fetched for its duration gate, so it costs no extra round-trip.
2026-06-15 00:39:07 +02:00
Marc-André Bombeck 794192c089 feat(insights): add a calm device-score tile for vendor-native metrics
The WHOOP / Oura / Polar score family (sleep performance, day strain, ANS
charge, body-temperature deviation, …) is ingested end-to-end and lit in the
AI chart-token allowlist, but no page renders it. These scores sit outside the
metric-status registry by design — they are vendor-rolled-up values, not
signals the app re-derives — so they carry no assessment card.

DeviceScoreTile is the surface for them: a title, the latest reading, the
trailing-window mean, and a small sparkline, read straight from the stored
analytics summaries slice (server-authoritative, never recomputed). One neutral
card, never a status colour. When the series is still sparse the sparkline gives
way to the shared LearningGate so a two-night-old strap never paints a jagged
two-point line. The connected variant reads its own summary so it can drop onto
a page that already owns an analytics read.

Add the page-level copy in all six locales.
2026-06-15 00:38:59 +02:00
Marc-André Bombeck b1a81bb65e refactor(insights): unify the learning-state gates behind one primitive
Glucose, sleep debt, chronotype, the trajectory forecast, and the
composite-score anatomy each hand-rolled the warm 'still learning your X'
paragraph — five copies with different markup, markers, and no shared
accessibility contract. The calm voice held only because each author
copied the tone by hand.

Add a LearningGate primitive that owns the presentation and behaviour of
a learning state (the muted body, an optional caveat line, the learning
marker, and the polite live region) and repoint all five surfaces to it.
Each keeps its own card chrome and its metric-specific copy; only the
gate's look and behaviour are now shared, so they can no longer drift.
2026-06-14 23:57:15 +02:00
Marc-André Bombeck 3594b549d9 feat(sleep): place sleep-debt and rhythm cards side by side when both present
When both the sleep-debt and chronotype cards carry a settled readout, render
them two-up on large screens instead of stacked; they are compact and read
better paired. A lone data-bearing card — or either card in its learning /
partial state — keeps full width so it never leaves a half-width orphan. The
grid collapses to one column below the lg breakpoint and uses items-start so a
toggled-open chronotype card does not stretch its neighbour.
2026-06-14 23:02:37 +02:00
Marc-André Bombeck bd9f03805c feat(insights): surface ring/strap HRV and rank wearable activity sources 2026-06-14 21:22:41 +02:00
Marc-André Bombeck 1b1713dd12 fix(insights): align the sleep cards with the design system
Drop the bespoke vertical-rhythm override on the sleep-debt and chronotype
cards so they inherit the standard card spacing the glucose panel uses, give the
sleep-rhythm section a quiet error notice instead of an endless skeleton on a
failed read, and move the card accents onto the semantic info token.
2026-06-14 21:10:06 +02:00
Marc-André Bombeck 4807c3afd8 Align sleep rhythm cards with the house Card rhythm and states
Drop the compact gap/padding override on the sleep-debt and chronotype
cards so they inherit the standard Card rhythm, matching the glucose
panel. Switch the card icons to the semantic text-info token. Branch
SleepRhythmSection on a query error to a quiet inline notice instead of
an endless skeleton, keeping loading and settled states distinct.
2026-06-14 21:08:32 +02:00
Marc-André Bombeck 235e509333 fix(insights): point the briefing steps link at the dedicated page
Every other briefing destination links to its dedicated insight sub-page; the
step row alone pointed at the generic readings view. Route it to /insights/steps
so the briefing, the tab strip and the dashboard all open the same step page.
2026-06-14 21:05:37 +02:00