Commit Graph
2438 Commits
Author SHA1 Message Date
Marc-André Bombeck 876e94b5c4 Merge wave-med-staleness: reflect supply changes on the next read
The medication card and table render dose-derived stock from the cached
medications list, but the inventory write routes evicted no server cache
and the client invalidated only the per-item inventory key, so a new
container's stock kept serving the pre-write value for the stale window.
Hard-evict the user's medication caches on every supply write and
invalidate the list key alongside the inventory key on the client.
2026-06-19 21:09:08 +02:00
Marc-André Bombeck 6609ce0dc7 fix(medications): evict supply caches so the card reflects stock at once
The /medications card and table render the dose-derived stock
(stockUnitsRemaining / stockDosesRemaining) straight from the
GET /api/medications list payload, which reads through cachedSwr on the
60s-fresh + 10-min-stale medications bucket. Registering, adjusting, or
deleting an inventory container changes that stock, but the inventory
routes never invalidated any cache — so the list served the pre-write
stock for the rest of the stale window. On the client the add / adjust /
delete supply mutations only invalidated the per-medication inventory
query, never the medications-list query that the card reads, so a mounted
list never refetched even once the server caught up.

Hard-evict the per-user medications + compliance buckets from every
supply-affecting write (inventory POST, the per-item PATCH / DELETE, and
the deprecated glp1 POST dose-change + ledger branches) so the next read
reflects the change rather than a stale SWR value. On the client,
centralise the supply mutations on a shared invalidateSupplyQueries helper
that drops both the medications-list key and the per-medication inventory
key. The card's neutral styling is unchanged.
2026-06-19 21:08:35 +02:00
Marc-André Bombeck eedfd9bff1 Merge wave-settings-ia: Labs, Vorsorge, and Illness as Settings sections
Fold the three standalone module settings pages into the Settings shell so
they open in-context with the same left-nav, heading, and chrome as every
other section. Remove the standalone pages and the now-unused
ModuleSettingsFrame, and drop the orphaned settings i18n blocks.
2026-06-19 21:06:54 +02:00
Marc-André Bombeck 43d9610f03 refactor(settings): fold Labs, Vorsorge, and Illness into the shell
Promote the three per-module customise surfaces from standalone
ModuleSettingsFrame pages to first-class Settings sections. They now
render inside the Settings shell — same left nav, heading, and chrome as
account / ai / integrations / notifications — instead of their own
back-button-and-title frame outside it.

Add labs, illness, and vorsorge to the slug registry (after mood) and to
SETTINGS_SECTIONS with FlaskConical / Thermometer / CalendarCheck icons.
labs and illness gate on their toggleable modules; vorsorge has no
module, so its entry is always shown. Three thin section components wrap
the existing LabsSettings / IllnessSettings / VorsorgeSettings content,
mirroring MedicationsSection / CoachSection.

Delete the three standalone /settings/{labs,vorsorge,illness} routes so
the wrench deep links resolve through the dynamic [section] route, and
drop ModuleSettingsFrame now that nothing renders it. Add the section
title / subtitle / description keys across all six locales and remove the
dead per-module *.settings.* blocks the old pages consumed.
2026-06-19 21:06:25 +02:00
Marc-André Bombeck 56abdac9fc Merge wave-demo-seed: reshape the demo tenant for current, healthy data
Recent in-band BP, stable healthy weight, a resting-heart-rate series, a
full week of per-stage sleep nights, on-track medication doses, two
preventive-care reminders, a healthy lab panel, one resolved illness
episode, and a sample Coach conversation — all dated relative to now so
the public demo reads current for screenshots.
2026-06-19 21:04:32 +02:00
Marc-André Bombeck a4b7ae5966 feat(apns): forward reminderId to iOS on measurement reminders
The measurement-reminder push already carries the reminder id in its
metadata, but the iOS allowlist stripped it before it reached the device.
Forward the key so a foreground or handled notification can post the
completion directly instead of routing through the capture deep link.
2026-06-19 21:03:59 +02:00
Marc-André Bombeck 431259bb06 feat(demo): reshape demo seed to read current and healthy
Tune the public demo tenant for App-Store screenshots:

- Extend the measurement loop to include today so every headline metric
  has a current reading.
- Retarget blood pressure into the optimal band (~118/76), weight to a
  stable healthy BMI, and add a clean resting-heart-rate series so the
  resting-pulse tile scores it directly instead of the spot pulse.
- Write a full week of per-stage sleep nights (awake/REM/core/deep,
  ~7.5h, source APPLE_HEALTH) including last night, in minutes, and skip
  the aggregate row for those nights to avoid double-counting.
- Schedule today's doses on-track: morning meds taken, the evening dose
  pending and never auto-missed, so today never reads overdue or 0%.
- Add Vorsorge reminders (dental check-up, annual physical), one healthy
  lab panel, one resolved illness episode, and an AI-configured Coach
  with a short encrypted sample conversation.

All dates stay relative to now so the demo refreshes on every re-seed.
2026-06-19 21:02:42 +02:00
Marc-André Bombeck 572ff3502e Merge wave-qol-mechanical: route stray query keys through the factory
Fix the target-edit-sheet invalidation that used a bare ["insights"] array
not matching the factory prefix the reads nest under (a silent no-op), and
migrate 12 further bare queryKey call sites onto the centralised factory.
2026-06-19 20:58:16 +02:00
Marc-André Bombeck bda06c9ec4 refactor(query-keys): route remaining bare arrays through the factory
The target editor invalidated a bare ["insights"] array on save and reset.
That literal sat outside the centralised factory, so the broad insights
fan-out it intended never matched the reads, which key through
insightsTargets/insightsRoot. Route the editor's reads and invalidations
through userThresholds(), insightsRoot(), insightsTargets() and analytics()
so a saved target actually refreshes every insights surface.

Migrate the other call sites the lint rule does not reach onto the factory:
the Coach conversation rail + thread reads, the per-metric personal-records
badge, the per-day measurement drill-down, the dashboard intake
compliance-chart fan-out, the per-medication side-effects list, the
onboarding auth invalidations, the notification-preferences read, and the
PWA persister's auth-me lookup.

Add the matching factory keys: coachConversations, coachConversation,
personalRecordsByMetric, measurementDrilldown, complianceChartInline and
medicationSideEffects. Each new key reuses the prefix its reads already use
so no invalidation shape drifts.
2026-06-19 20:57:18 +02:00
Marc-André Bombeck e956767371 Merge wave-backend-hardening: expiry indexes, Coach retention, crypto fail-closed
- Session + AuthChallenge @@index([expiresAt]) (migration 0178)
- CoachMessage daily retention job (365d default, 30d floor)
- crypto dev-key padding fails closed unless NODE_ENV is development/test
2026-06-19 20:57:17 +02:00
Marc-André Bombeck 288569297f fix(crypto): fail closed on weak dev key outside development/test
The dev-only SHA-256 key padding was gated on the absence of a production
marker, so a staging or preview container with NODE_ENV unset silently
derived a weak key the operator never recorded — rotating to a real key
later orphaned those rows (decrypt is fail-closed). Gate the padding on an
explicit NODE_ENV of development or test; every other case, including unset
NODE_ENV, throws the same hard error as in production.
2026-06-19 20:55:46 +02:00
Marc-André Bombeck 0278e22ed1 feat(jobs): add retention prune for stored Coach history
coach_messages is append-only and grows forever — a daily Coach user
accrues encrypted conversation rows with no lifecycle, while every
sibling ledger (push_attempts, audit_logs, host_metrics) already prunes.
Add a daily pg-boss cleanup that hard-deletes rows past a configurable
window (COACH_MESSAGE_RETENTION_DAYS, default 365, 30-day floor so a
misconfig cannot drop live in-context history). Wire it into the
maintenance registrar with the dead-queue contract guard and a focused
helper test.
2026-06-19 20:54:56 +02:00
Marc-André Bombeck b3d303217c perf(db): index session and challenge expiry columns
The sliding-30-day session reaper and the passkey-challenge sweep both
filter on expires_at, but neither sessions nor auth_challenges indexed
that column, so the cleanup degraded to a sequential scan on a busy
instance. Every sibling expiry table already indexes its expiry column;
add the two missing indexes via migration 0178.
2026-06-19 20:52:31 +02:00
Marc-André Bombeck 3e04035de8 fix(coach): paint the launcher glyph white on the gradient
The floating Coach launcher inherited the button's dark foreground token,
which left the sparkle glyph as a dark outline on the purple-pink
gradient. The glyph is decorative and the button carries its own
accessible name, so the white mark reads cleaner without affecting the
control's non-text contrast, which the offset focus ring already
provides.
2026-06-19 20:48:02 +02:00
Marc-André Bombeck f90ce8bace test(withings): scope the expired-nonce assertion to the token URL
The expired-nonce callback test asserted the global fetch spy was never
called. A preceding successful-connection test fires its webhook
subscriptions fire-and-forget, and those promises resolve into the next
test's freshly installed spy, so the broad assertion failed whenever the
background calls landed in time. Filter the recorded calls to the token
endpoint and assert none reached it, matching the replay and cross-user
tests in the same file and keeping the check true to its intent.
2026-06-19 20:45:46 +02:00
Marc-André Bombeck 2e27c3c441 refactor(insights): share the briefing row layout
KeyFindingRow and SignalRow rendered an identical tone bar + metric icon
+ headline/body/delta and the same link-when-routed behaviour, differing
only in their data-slot and body copy. Extract a single BriefingRow and
have both delegate to it.
2026-06-19 20:35:25 +02:00
Marc-André Bombeck f002a9d565 fix(bp): keep non-physiological readings out of the seed and the series
The demo seed's random-walk BP generator could drift below the
physiological floor and mint a systolic-0 row (iOS #33). Clamp the walk
to the same band the input validator enforces (systolic >= 40,
diastolic >= 20), reusing the VALUE_RANGES floors, so a fresh seed never
writes a sub-floor reading. Add a read-side selection guard on the blood
pressure series endpoint that excludes rows below those floors from both
the series and the implicit latest point, so a corrupt row that slipped
in before the floor was enforced can never surface as the latest
reading. Export VALUE_RANGES so both sites share one source of truth.
2026-06-19 20:34:20 +02:00
Marc-André Bombeck a3b274171c fix(a11y): announce coach history state and Vorsorge trend
Add aria-expanded to the coach drawer's history-collapse control so its
expanded state is exposed, matching the expand toggle. Give the Vorsorge
trend strip an sr-only summary of the latest value and seven-day
direction, and key the bars on measuredAt rather than the array index.
2026-06-19 20:31:58 +02:00
Marc-André Bombeck 1f8420a6d0 chore(i18n): drop orphaned integration help and overlap notes
Remove the integration keys left dead after the integrations refactor:
the WHOOP and Fitbit overlap notes, the Fitbit experimental note, the
per-provider credentials-help strings (WHOOP, Fitbit, Polar, Oura) and
the Nightscout help. None carry a t() call site any longer. Add the
Vorsorge trend-strip screen-reader summary strings (latest value plus
seven-day direction) across all locales.
2026-06-19 20:31:52 +02:00
Marc-André Bombeck d2007f2f37 perf(coach): read only coarse bands for the deep-history tail
Add a coarseOnly option to buildTieredSeries that skips the raw 0–14d
read and the DAY (14–30d) / WEEK (30–90d) band reads, returning only the
MONTH / YEAR bands plus the anomaly envelope. The Coach snapshot's
coarse timeline tail consumes only month/year + anomalies, so the
near-term bands were read and discarded every turn; it now passes
coarseOnly. The briefing path is unchanged. Look the learn-guide prompt
example up by slug rather than by array index so reordering the catalog
cannot silently change the example link.
2026-06-19 20:29:02 +02:00
Marc-André Bombeck ebb9d0c607 fix(insights): correct signals-of-day extraction and determinism
Drop steps from the signals-of-day set: Apple Health steps ingest as
many intraday stats-prefixed samples, so the newest raw row is a
partial-day fragment, not a daily total — same exclusion already applied
to per-night sleep. Add a glucose signal off the canonical stored value
(the model reads the snapshot, not display units) so the block matches
the priority comment that already claimed it; drop the dead sleep entry
from SIGNAL_PRIORITY. Track the raw extreme magnitude when keeping the
most extreme recent anomaly so a larger anomaly is no longer dropped by
comparing against an already-rounded value. Thread the injected clock
through trendSlope so the emerging-trend cutoff is deterministic under a
test clock. Rename the anomaly day-count to anomalyDaysAgo so the
snapshot hash staleness-tiers it instead of letting the raw count drive
the fingerprint. Cap the returned set at three to match the schema.
2026-06-19 20:27:36 +02:00
Marc-André Bombeck cdb76f1930 chore(release): v1.18.7 2026-06-19 20:03:28 +02:00
Marc-André Bombeck c29dae7ad9 Merge wave-coachctx: tiered + illness/article-aware Coach context 2026-06-19 20:00:33 +02:00
Marc-André Bombeck c334be22e9 feat(coach): tier history context + illness and learn-guide awareness
Wire the Coach snapshot's core-metric history onto the shared tiered
series so blood pressure, weight, and pulse carry a coarse tail the
in-window weekly fold could not produce — 90d-1y monthly and >1y yearly
buckets plus the preserved peak/trough envelope, so a spike from months
ago survives a coarse bucket. The tiered read skips the raw 0-14d band
the snapshot already holds, so no duplicate round-trip; the coarse tail
is bounded per band and sheds first under the prompt-budget degrader, so
token size holds.

Surface the active illness episodes and Rest Mode state to the Coach
prompt: the snapshot already carries the illness block, and the system
prompt now instructs the Coach to read an off-band vital as plausibly
illness-explained and avoid pushing a heavier cadence while the user is
in Rest Mode.

Add a static catalog of the public /learn guides and inject a compact
list into the system prompt. The Coach may reference at most one guide
by topic and URL when genuinely helpful, and only a slug the catalog
lists — a fabricated /learn URL is impossible by construction.
2026-06-19 20:00:04 +02:00
Marc-André Bombeck 675456eea3 Merge wave-integrations: unified integration-card skeleton + normalised copy 2026-06-19 19:59:12 +02:00
Marc-André Bombeck aa092df9f0 refactor(settings): unify the integration card skeleton
Every Settings → Integrations block now paints one consistent shape:
heading (icon + name + status pill) → a white primary description → a
grey secondary description that ends with the setup-guide link rendered
inline, instead of a standalone bottom-of-card element.

OAuth-only (WHOOP/Fitbit), BYO-credential (Withings/Oura/Polar) and
manual-key (Nightscout) cards share the same skeleton via the new
IntegrationCardDescription helper; the credential form stays as an extra
block where it applies. The "where to find these credentials" hint moves
above the Save button instead of sitting below it.

Rewrite the per-provider description strings across all six locales into a
parallel two-part shape with even length, and drop the now-orphaned
overlap/experimental sub-notes and the standalone setup-guide link.
2026-06-19 19:58:48 +02:00
Marc-André Bombeck d35c7660ea test(insights): assert the overview no longer wires the inline ask-Coach affordance
Wave A removed the inline ask-the-Coach / suggested-questions block from
the overview hero (Coach is reached via the FAB + drawer). Update the
stale assertion that still expected onAskCoach/onPickPrompt + the launch
context on the page.
2026-06-19 19:52:21 +02:00
Marc-André Bombeck 5fca70524d Merge wave-sleep: drop last-night card, big chronotype, fix AI-hint 2026-06-19 19:48:50 +02:00
Marc-André Bombeck 43388eabbf Merge wave-coach: Claude/ChatGPT-style Coach frontend redesign 2026-06-19 19:48:33 +02:00
Marc-André Bombeck 4bda26f47c fix(insights): rework the Sleep view — drop the redundant last-night card, promote chronotype, fix the assessment slot
Remove the single-night "Last night" hypnogram from the sleep overview: its
per-stage breakdown (time + %) duplicated the phase-distribution stacked bar
directly below it. The /api/sleep/night route stays (the dashboard reads it);
only the redundant surface is gone.

Promote the chronotype to the prominent bottom block of the view. Split it out
of the rhythm section (now the sleep-debt headline alone) into its own section:
a large band label with the natural sleep-midpoint clock beneath it and an
expandable disclosure for social jetlag + MSFsc. It reads the same
["sleep-rhythm"] cache, so it adds no round-trip, and keeps the calm
"still learning your rhythm — N of M nights" state.

Fix the sleep-quality assessment slot. It disabled the status fetch whenever the
AI narrative was absent, so the card could not read hasProvider and fell back to
its no-provider default — surfacing the "connect an AI provider" CTA even for
accounts that have a provider connected, stacked under the grounded note. The
card now stays enabled and reports the truthful state: no provider shows its
clean AI-settings CTA alone; a connected provider with no text yet shows the
grounded note; warm or preparing AI text wins. Exactly one block fills the slot.
2026-06-19 19:48:10 +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 0a4be95aba Merge wave-rings: flatten the health-score rings to the chart style 2026-06-19 19:45:44 +02:00
Marc-André Bombeck deccdaca0a Merge wave-vorsorge: 7-day strip + filtered-measurements deep-link 2026-06-19 19:45:36 +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 c7425ca426 feat(vorsorge): add 7-day trend strip + measurements deep link to reminder cards
Enrich the Vorsorge cards with a discreet 7-day trend strip and a path
into the underlying readings.

- A quiet seven-bar strip of the reminder metric's last seven readings
  sits under the metric context on both the dashboard summary card and
  the full-grid cards, mirroring how a dashboard tile carries a small
  trend. Neutral muted bars, no status colour. A free-text reminder or a
  metric with fewer than two readings shows nothing, so card height stays
  stable. The strip reads a bounded last-7 window per type through the
  existing measurements endpoint, cached by type under the measurements
  invalidation prefix.
- The card kebab gains a 'Show measurements' item for measurement-linked
  reminders that opens /measurements pre-filtered to that type.
- The measurements list accepts a ?type=<MEASUREMENT_TYPE> deep link that
  seeds the existing client-side type filter while leaving the rail
  interactive; unknown tokens are dropped. The ?add= behaviour is
  unchanged.
- New labels added across all six locales.
2026-06-19 19:45:14 +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 0b01bfb483 feat(insights): signals-of-the-day briefing and fix the stale-briefing gate
Compute a present-focused "signals of the day" block in extractFeatures from
the in-memory measurement set (no extra query): the freshest reading vs the
user's own 7d/30d windows, the normal-swing verdict, an emerging slope, and a
recent anomaly per salient metric, ranked by clinical priority and capped at
five. Add a signalsOfDay array to the dailyBriefing schema feeding the
briefing alongside the existing keyFindings cap, and rewrite the briefing
prompt (EN + DE) to lead with NOW: a warmer, motivating read that surfaces the
emerging signal and gives one concrete, doable nudge while still drawing on the
tiered history.

The signals block lands inside the compacted features payload, so it also
seeds insightsSnapshotHash — a fresh daily signal now changes the fingerprint
and forces the daily 04:30 warm to regenerate instead of re-stamping a
days-old narrative. Bump PROMPT_VERSION to 4.29.0.
2026-06-19 19:36:51 +02:00
Marc-André Bombeck 775d624228 feat(rollups): add progressive tiered-context builder for AI surfaces
Introduce buildTieredSeries(): a five-band progressive data-context helper —
0-14d raw daily, 14-30d DAY, 30-90d WEEK, 90d-1y MONTH, >1y YEAR — plus a
per-metric min/max-envelope anomaly list so a peak from months ago survives a
coarse bucket. Reuses probeRollupCoverage / ensureUserRollupsFresh with the
existing replace-fall-back-on-miss contract; readers never throw, so a
coverage gap simply yields an empty band. Caps each metric near 600 tokens to
stay within the existing context budget. Shared across the Coach snapshot and
the daily briefing.
2026-06-19 19:36:42 +02:00
Marc-André Bombeck 85272a956b Merge v1.18.6.1 into main
v1.18.6.1 — settings/admin alignment, first-run-only tour, preventive-care card, coach scroll, FHIR section.
v1.18.6.1
2026-06-19 00:13:49 +02:00
Marc-André Bombeck 155cedda50 chore(release): v1.18.6.1 2026-06-19 00:05:20 +02:00
Marc-André Bombeck dae7138fc6 fix(settings): keep the health-record entry reachable regardless of the doctorReport opt-out
The health-record section (PDF + FHIR R4 export) was the only entry-point
gated on the doctorReport module's nav toggle, so flipping that opt-out in
the Modules hub made the whole section vanish from Settings with no
in-context way back. Drop the nav gate: the entry-point now always shows.
The doctorReport module still governs the data layer — the server-side
/api/export/health-record route stays the hard enforcement (403
module.disabled over Bearer when opted out). Assert the entry shows for a
default, empty, and opted-out modules map.
2026-06-19 00:03:22 +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 8234427a94 fix(vorsorge): match the medication-card grammar on the reminder card
Mirror the medication card's name + cadence + status layout:
- the cadence ("alle 7 Tage") renders as a chip directly to the right of the
  metric name via a new MedicationCardHeader nameChip slot, instead of on the
  state-badge row below the category;
- the due / overdue status renders as plain text, not a chip;
- the "zuletzt erledigt" line always renders, falling back to an em dash when
  there is no last-done value, instead of being hidden.

The green action button is unchanged. Also drop the per-module tour trigger
from this surface (the tour is first-time-only now).
2026-06-18 23:58:35 +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 f67542d29e fix(settings): align section nav with the first card via the grid, not a spacer
The settings and admin shells reserved the page-heading height in the left
nav with a hidden replica block. The replica wrapped differently in the 220px
nav column than the real heading in the wide content column, so the first nav
item sat too low and drifted across locales.

Hoist the heading into the shell and place it in its own grid row spanning only
the content column: row 1 holds the heading, row 2 holds the nav and the cards.
The nav's first item and the first card now start the same grid row, so they
align by construction regardless of how the title or subtitle wraps. Drop the
spacer component.
2026-06-18 23:58:10 +02:00
Marc-André Bombeck ee16b05c93 Merge release/v1.18.6 into main
v1.18.6 — offline reads, grounded guidance, and one consistent app.
v1.18.6
2026-06-18 23:11:36 +02:00
Marc-André Bombeck 8b03fb450e fix(deps): patch undici and nodemailer high-severity advisories
Bump undici 8.4.1 -> 8.5.0 (GHSA-vmh5-mc38-953g, TLS certificate
validation bypass in the SOCKS5 ProxyAgent) and nodemailer 9.0.0 -> 9.0.1
(GHSA-p6gq-j5cr-w38f), both direct dependencies. Clears the production
high-severity dependency audit; no API change.
2026-06-18 23:04:27 +02:00
Marc-André Bombeck 742faffdda refactor(dashboard): drop the duplicate band-builder export
buildTargetBands was a same-file alias of buildDashboardBands, which the
dead-code gate flags as a duplicate export. Keep the historic name only as
a re-export alias from the snapshot module, so callers and the parity test
are unchanged while bands.ts exposes a single builder.
2026-06-18 22:55:22 +02:00
Marc-André Bombeck 1118d36cf8 docs: consolidate the v1.18.6 changelog for the full cycle
Cover the whole release — offline reads, cited reference ranges + Coach
grounding, responsible safety alerts + diabetes opt-in, the module tour,
the module-consistency and settings/admin IA pass, sleep/coach polish,
and the reminder completion route — and correct the additive migration
note (0173–0177).
2026-06-18 22:45:26 +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