Commit Graph
90 Commits
Author SHA1 Message Date
Marc-André Bombeck 03058b0210 Merge branch 'feat/vault-w4-qa' into release/v1.27.17 2026-07-07 15:10:45 +02:00
Marc-André Bombeck b8009d2e73 feat(documents): the vault surface
The page becomes one wide, calm filing-cabinet view: PageHeader with an
Upload action, a drop-target upload zone whose accept list and per-file
cap come from the usage endpoint (quota bar only above 80 %), a sticky
filter rail (200 ms debounced search focused by '/', nine type chips,
condition chips from linked episodes, a year segmenter, one-tap clear),
and the virtualized timeline below. Filter state lives in the URL so
every view is shareable and back-button-safe; uploads filtered to an
episode arrive pre-linked. Hovering a card prefetches detail metadata —
never the blob. Designed states: a skeleton grid that matches the loaded
layout, a teaching empty state, a distinct no-matches state, and a
retryable error card. /documents joins the shell's wide-container list;
the shell stays the single owner of the page frame. Locale bundles gain
the vault vocabulary in all six languages and drop the retired
extraction-review strings.

Plan-Task: W2-T1, W2-T4 (zone), W2-T5 (prefetch), W2-T6
2026-07-07 12:26:06 +02:00
Marc-André Bombeck 85058716f6 fix(pwa): stop treating network failure on the auth probe as session end
A failed /api/auth/me now only counts as a session END on an explicit
401/403. Network-level failures (offline relaunch fetch TypeError, the
service worker's synthetic 503 offline envelope, abort/timeout) and 5xx
carry no session verdict: the hook holds the last-known auth state via a
non-sensitive localStorage marker instead of flipping unauthenticated.

Pre-fix, an airplane-mode relaunch of the installed PWA redirected to
/auth/login and ran clearCachesForSessionEnd(), wiping the SW data/page
caches and the IndexedDB query snapshot — the entire offline read path
was unreachable in exactly the scenario it exists for.

- use-auth: isAuthVerdictUnknown() classification, was-authenticated
  marker (set on every successful /me, cleared on session end),
  isAuthUnknown exposed for the shell
- auth-shell: the wipe runs only on a real verdict; a non-verdict
  failure without a prior-session marker redirects without wiping
- explicit logout and true 401/403 expiry wipe exactly as before

H1
2026-07-07 12:02:43 +02:00
Marc-André Bombeck 4846dcb478 fix(ui): remove sub-AA opacity dimming from informational text
The muted-foreground token is AA-tuned in both themes; the /70 and /80
opacity modifiers silently undid that (2.9-4.2:1 measured). Every
informational text node drops the modifier for the full token: sleep
hypnogram captions, learning-gate caveat, integration-card setup hints,
module lock notes, glucose panel and token-footer meta, invite-token
hint, wizard hint, notification channel labels and disabled notes (the
disabled state reads through the note and the disabled switches, not
through dimming), plus the resting state of the timestamp, suggestion
chip and sidebar-collapse controls. User-authored mood notes move to
foreground text-sm — user data is never muted. Decorative non-text
strokes (sparkline grid, calendar dot, titration connector) keep their
opacity; icons keep the /60 graphics floor.
2026-07-05 15:03:01 +02:00
Marc-André Bombeck 109866b5d0 fix(layout): apply the sidebar viewport default after hydration
Reading matchMedia (or localStorage) inside the initial render branches
the hydration render away from the SSR markup — the sidebar is only
CSS-hidden below md and still hydrates its DOM there, so the phone
viewport tripped React #418 in the dashboard console guard. The
collapsed state now gates on the mounted probe like the module filter
above it: SSR and hydration paint the expanded shell, the stored pref
or tablet default applies right after. useIsMobile learns the lg cut
for the tablet probe.
2026-07-04 16:13:15 +02:00
Marc-André Bombeck c10288d61b fix(layout): let the content column shrink below its children's min width
Without min-w-0 the shell's flex column adopted the measurements
table's intrinsic min width, widening main past the viewport and
putting the whole page on a horizontal scrollbar at tablet widths.
Wide children scroll inside their own overflow-x-auto containers.
2026-07-04 15:51:46 +02:00
Marc-André Bombeck ed5cadf25d fix(layout): start the sidebar on the icon rail at tablet widths
A 256 px expanded sidebar next to a 768 px viewport leaves the content
a 512 px column — an iPad held upright rendered every page squeezed.
With no stored preference, md–lg widths now start collapsed to the
icon rail; a manual toggle persists and wins on every later visit.
2026-07-04 15:51:46 +02:00
Marc-André Bombeck b5eb5274b3 fix(nav): stop disabled-module entries flickering into the nav on first paint
The resolved per-user module map rides the client-only `/api/auth/me`
query, so it is still undefined on SSR and the first client paint. The
nav model's module gate fails open (`modules?.[key] !== false`), so every
module-gated entry — cycle, mood, labs, coach, insights … — rendered for
one frame before the query resolved and filtered it out. A user without a
module saw its entry flash into the sidebar and the mobile bar, then
vanish.

Gate the module-dependent filtering behind `useMounted()` in both bars.
SSR and the first client paint now render only the core (non-module-gated)
destinations; the real module map applies once mounted. Before mount a
gated entry is treated as hidden — fail-closed — so SSR and first paint are
identical (no hydration mismatch) and a disabled module can never flicker
in. The pure nav-model helpers keep their fail-open default for callers
that pass a settled map; the bars thread the mounted flag in.

Rework the sidebar and bottom-nav SSR tests to assert the fail-closed
first-paint contract (core-only, gated entries absent regardless of the
map); the resolved post-mount set stays covered by the pure-helper tests
in nav-model.test.ts and by e2e.
2026-06-30 23:42:49 +02:00
Marc-André Bombeck 9c1c0d86a9 Merge v1.25.1: clear the in-memory query cache at every session boundary 2026-06-28 21:48:16 +02:00
Marc-André Bombeck ea380d6f68 fix(auth): clear the in-memory query cache at every session boundary
The root QueryClient is created once and survives every client-side
navigation, so logout then login on the same browser left the previous
account's non-user-scoped health-data queries in memory and the next
account read them before any refetch landed. Wipe the entire in-memory
cache on logout, on session expiry, and at the start of a fresh login,
alongside the existing IndexedDB and service-worker cache wipe, so no
session ever inherits another account's data.
2026-06-28 21:37:55 +02:00
Marc-André Bombeck dda75abf38 Merge v1.25.1: reset scroll to top on forward navigation app-wide 2026-06-28 21:31:00 +02:00
Marc-André Bombeck 37c977e3a4 feat(mental-health): make the wellbeing check-in a standalone module page
Move the PHQ-9 / GAD-7 screener off `/insights/mental-wellbeing` to a
top-level `/mental-wellbeing` route so it no longer borrows the Insights
tab strip and layout shell. The page now uses the standard module-page
frame at the wider content width the other module pages use.

Replace the standing screening disclaimer block and the opt-in/private
line in the header with an info icon after the page heading whose
hover/focus tooltip carries the screening-not-a-diagnosis note. Drop the
now-orphaned `disclaimer` / `optInNote` strings across all six locales
and add the `infoTooltip` string in their place.

The questionnaire, scoring, severity result, PHQ-9 item-9 crisis-resource
handling, focus-on-submit, and 44px touch targets are unchanged. Repoint
the module nav entry and the registry comments at the new route.
2026-06-28 20:48:39 +02:00
Marc-André Bombeck 65daf11be5 fix(ui): reset scroll to top on forward navigation app-wide
The authenticated shell keeps a single <main id="main-content"> mounted
across every route change, so its scroll offset persisted between routes:
scrolling down in one section (e.g. Settings) and navigating to another
opened the next view already scrolled.

Reset the scroll container to the top on each pathname change, with a
window.scrollTo fallback for the body-scrolled shells. Back/forward
navigation skips the reset so the browser's own scroll restoration stands:
popstate arms a one-shot skip cleared on a macrotask so it can never stick.
2026-06-28 20:40:23 +02:00
Marc-André Bombeck f62ba50ff0 feat(mental-health): gate the PHQ-9/GAD-7 check-in behind an opt-in module
The mental-wellbeing screener surface shipped built but unreachable: no nav
entry, no module key, ungated. Add a `mentalHealth` module key (opt-in,
default OFF — a depression/anxiety self-assessment is at least as sensitive
as mood) to the registry, the module-hub icon map, and the exhaustive module
maps. Give the check-in a nav home at /insights/mental-wellbeing, born-gated
on the module so it appears only once the account opts in, and redirect the
page home on a direct hit when the module is off.

Item answers stay encrypted and off the AI Coach / MCP regardless of the
toggle; this only controls whether the surface is visible and reachable.
2026-06-28 18:25:27 +02:00
Marc-André Bombeck 422778f679 feat(documents): module gate, nav, and review-then-confirm UI
Add the opt-in inboundDocuments module (off by default, like the MCP
endpoint): ingesting a clinical document egresses it to the OCR/vision
provider, so the surface ships dark and the user turns it on from the
Modules hub. The born-gated /documents page carries the prominent
non-diagnostic disclaimer (we read, we don't interpret; nothing is saved
until you approve it; only your clinician can interpret these results),
uploads a document, and walks each extracted fact: source provenance and
confidence are shown, low-confidence facts are blocked from approval until
edited, and the confirm step commits the approved facts.
2026-06-28 16:57:16 +02:00
Marc-André Bombeck de33f1ad4c chore: remove the bug-report integration
Drop the in-app bug-report surface end to end: the user submission page,
the feedback intake and admin inbox, the optional GitHub-issue
escalation, the operator config, and the dedicated achievement badges.

- delete the /bugreport page and the bugreport / feedback / admin-feedback API routes
- drop the Feedback model, its enums, and the AppSettings GitHub-issue and enable columns (migration 0205)
- remove the admin Feedback section, the Bug Report integration card, and the system-status row
- drop the bug-report nav entry and the app-settings provider that only carried its flag
- remove the bug-hunter achievement badges and their metric
- prune the related i18n keys across every locale and the encrypted-column and key-rotation registries
2026-06-27 20:37:35 +02:00
Marc-André Bombeck 3f19b13fab fix(nav): keep the More-menu tile labels fully visible on narrow screens
The long single-word labels (de "Benachrichtigungen") clipped their last
character at the tile edge on a 320 px phone. Trim the grid gutters and the
tile padding/gap to hand each label more horizontal room, and cap the bottom
sheet at max-w-md, so the label wraps cleanly onto a second line instead of
running off the tile. Tile height stays uniform.
2026-06-27 17:53:12 +02:00
Marc-André Bombeck 9cef089192 fix(nav): even out the profile menu row spacing
The Theme sub-trigger inherited the shared sub-trigger padding (py-1.5,
no min-height) while every sibling row carries min-h-11 py-2, so the
Theme row sat a few pixels shorter and the menu read as unevenly spaced.
Match the item height on this instance only, leaving other dropdowns'
sub-triggers untouched.
2026-06-27 17:53:05 +02:00
Marc-André Bombeck a9892e9586 refactor(checkups): rename the preventive-care slug to /checkups
Move the preventive-care surface from the German-derived slug /vorsorge to
the English /checkups and make the feature read consistently across locales.
The route directory, the nav entry, the dashboard card link, and the
onboarding tour step now point at /checkups. A permanent redirect keeps old
bookmarks, the PWA's cached navigation, and push deep-links resolving.

Display label is now "Checkups" in English and the equivalent term in
Spanish (Revisiones), French (Examens), Italian (Controlli), and Polish
(Badania); German keeps "Vorsorge". The internal id stays "vorsorge" so
saved layouts, module-order preferences, and tour state keep resolving
without a data migration.
2026-06-27 12:48:04 +02:00
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 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 8ef61b02fe fix(mobile): keep Coach composer above the bottom nav, restore full hub labels
Size the full-page Coach surface to clear the mobile BottomNav band plus the
iOS safe area so the docked composer and Stop control never sit under the nav;
desktop full-bleed height is unchanged (the nav is mobile-only).

Stop truncating the More-hub labels: shrink the label type and allow a
two-line clamp so long words (German Benachrichtigungen, and the long es/fr/it
labels) read in full instead of ellipsising.

Raise the lab reading-history edit/delete controls to 44px on touch
(compact on desktop), and drop the lab-detail Add button to icon-only on the
narrowest phones.
2026-06-20 17:15:35 +02:00
Marc-André Bombeck 6ee7df3098 fix(nav): keep mobile More-menu labels on one line
Trim the longest label ('Benachrichtigungen') with truncate (nowrap +
ellipsis) instead of break-words so every More-hub tile reads on a
single line on a phone, matching its neighbours. The full label stays
discoverable via the title attribute; the desktop layout is untouched.
2026-06-20 16:57:01 +02:00
Marc-André Bombeck 1f4ba5939d style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
2026-06-19 22:20:31 +02:00
Marc-André Bombeck f0c25cc275 feat(onboarding): rebuild the tour as a 15-stop cross-page module walk
Replace the five chrome-pointer stops with a module-by-module guided tour
in the data->intelligence->export order. Each stop teaches the module's
value, one hidden capability, and how it feeds the others. Stops carry a
route + module gate; the overlay router.pushes to the route, waits for the
anchor to mount, then measures, so a single tour walks the whole product.

Move the launcher to the app shell so the overlay survives navigation,
auto-open immediately after anamnesis for fresh users (24h path stays the
fallback for wizard-skippers), resume from the persisted checkpoint, and
gate stops + the step counter on the resolved module map. Add tour copy
for all 15 stops across the six locales.
2026-06-18 19:39:10 +02:00
Marc-André Bombeck b474ea1489 fix(pwa): stop the offline cache from persisting sensitive health data
The offline layer wrote far too much to disk and cleared it too rarely.
The query persister used a denylist (only auth/session/admin/tokens), so
every other family — coach, insights, illness, labs, mood, cycle — was
dehydrated to IndexedDB in plaintext; the service worker likewise
prefix-cached GET /api/insights, which serves decrypted Coach
conversations. Neither store carried an account marker and both were
wiped only on explicit logout, so a session that ended by expiry or a
401 left health data on disk for the next account on a shared profile.

Flip the persister to a strict allowlist (dashboard snapshot, per-chart
series, widget layout — the offline-dashboard set only) and stamp each
payload with the live account id, discarding on mismatch. Drop
/api/insights and /api/analytics from the worker allowlist. Wipe the
IndexedDB blob and the data/page caches on every session end — the
401/expiry redirect and the logout path both route through one helper.
2026-06-18 18:18:24 +02:00
Marc-André Bombeck bb08aa42dc feat(pwa): real offline read value via SW data cache + query persistence
An installed PWA opened offline rendered empty skeletons forever: the
service worker hard-excluded every /api/* path and there was no client
query persistence. Close both gaps for read value.

Service worker: add a stale-while-revalidate branch for an allowlist of
safe GET /api/* reads (dashboard snapshot + widgets, measurements,
medications, insights, analytics, version) in a release-versioned
healthlog-data cache. Two-layer refusal — a path deny guard for any
auth/token/session/login segment plus a body-level secret refusal for
hlk_/hlr_/sk- shapes and no-store responses — so a secret-shaped body or
an auth surface can never be persisted. Mutations stay network-only.

Client: add a dependency-free IndexedDB query persister that hydrates the
last successful reads before the first authenticated paint, build-version
and age gated, debounced on cache change, with an auth/session/admin/token
denylist. Cleared on logout alongside the SW page and data caches so a
shared device never leaks one account's data.

Offline banner now states it is showing last-synced data when a cached
snapshot is present. Bump release to v1.18.6.
2026-06-18 15:18:48 +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 559366e308 feat(settings): make medications a toggleable module
Medications graduated from the always-on core set (weight / blood
pressure / pulse stay core) to an opt-out module. The surface gates
off the resolved module map — the left-nav entry, the dashboard
medication widget, and the dedicated Medikamente settings entry hide
when the account turns the module off. The medication data routes stay
exempt from the gate (same data-layer reasoning as mood / labs): an
importer, a sync, and the user's ability to clean up entries keep
working while the surface is hidden, and re-enabling finds the
schedule and intake history intact.

Wire the module-route gate inventory, the dashboard widget/summary
maps, the PATCH allowlist, and the nav model; regenerate the OpenAPI
contract for the new module key.
2026-06-16 21:31:54 +02:00
Marc-André Bombeck b079431a8d feat(illness): add the condition-journal MVP behind a born-gated module
Introduce a retrospective illness/condition journal: episodes with a
lifecycle (acute / chronic-ongoing / recurring / flare) and a daily
symptom timeline forked structurally from the cycle log-day surface. The
journal is a record, not a medical device — no prediction or diagnosis UI.

- Register `illness` as a born-gated (opt-in / default-off) module. The
  gate resolver flips the disabled-allowlist default to off for born-gated
  keys, so the surface stays dark until the account opts in from the
  Modules hub. `isIllnessEnabled` + `requireIllnessEnabled` delegate to the
  module foundation (one source of truth) and re-stamp an illness errorCode.
- Episode CRUD under /api/illness: list / create / get / edit / resolve /
  delete, plus a per-episode-day day-log upsert on (episodeId, date). Every
  route wraps apiHandler, Zod-validates with returnAllZodIssues, narrows
  userId from auth, builds data field-by-field, and gates the module.
  Free-text notes encrypt at rest through the shared Bytes codec.
- Log-day sheet (cycle fork): ~8 Jackson/WURSS symptom chips 0-3 + a
  functional-impact group + fever + an encrypted note, one row per day.
- Episode history page + a born-gated nav entry, neutral palette, status
  via a neutral Badge, retrospective-only copy. i18n across all six locales.
- Cover the new routes in the module-route-gate inventory; unit tests for
  the day-log upsert, the gate enforcement, and the born-gated resolver.

The /api/auth/me module map now projects the `illness` flag (openapi.yaml
regenerated).
2026-06-16 21:09:35 +02:00
Marc-André Bombeck b2684a2e21 feat(nav): gate the Insights destination on the insights module
Mark the top-level `/insights` nav destination `requiresModule: "insights"`
so it drops from both bars when the account has the insights module off,
mirroring mood / labs / coach / achievements. The bottom-nav pinned
`/insights` as a fixed primary slot regardless of the module map; it now
filters that slot through the same per-user map the More hub honours
(fail-open on a missing key / unloaded /me payload). A disabled insights
module is hidden everywhere — the primary-slot filter keeps it out of the
More hub too, so it is hidden, not relocated.

Extend the nav-model + bottom-nav tests with insights on/off coverage.
2026-06-16 02:42:43 +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 f0a030f1ce refactor(nav): drop Workouts from the left nav (already an Insights tab)
Workouts already surfaces as an Insights tab-strip pill (the workouts
slug, gated on a logged workout row), so a parallel left-nav home was
redundant. Removing it from NAV_DESTINATIONS lets the Insights nav home
read active on /insights/workouts like any other /insights/* sub-route.
2026-06-15 21:05:24 +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 56c92e0cfe fix(nav): keep More-hub tile labels inside their tiles
The mobile "More" overflow sheet renders a two-column grid of icon-plus-label
tiles. The label span carried min-w-0 but no break handling, so a long
single-word label such as the German "Benachrichtigungen" — which has no
natural break opportunity — pushed past its grid track and overflowed the
tile box.

Add break-words to the label so an unbroken long word breaks inside the tile;
combined with the existing min-w-0 shrink and the auto-height tile (min-h-14),
the second line is absorbed cleanly. The fix is layout-level, so every label
in all six locales fits without special-casing the longest one.
2026-06-15 20:34:33 +02:00
Marc-André Bombeck 6a994cd1c3 feat(nav): give Vorsorge a nav home and fold the utility tail into one model
Vorsorge (preventive-care) was a top-level tracking surface reachable only
three taps deep through Settings → Reminders, while peer surfaces (Labs,
Recovery) already had a top-level nav home. Add it to the one shared
destination model in the clinical spine, so it renders on the desktop
sidebar and the mobile More hub like every other feature, with a
nav.vorsorge key across all six locales.

Bug Report, Settings and Notifications were still a second hand-curated
list on each bar — the exact drift the one-model contract set out to kill,
just pushed down a level. Promote them to a shared NAV_UTILITY_DESTINATIONS
list both bars consume: the sidebar footer + avatar menu and the bottom-nav
More-hub tail now read from one source and can no longer disagree. The
More hub itself is now computed by mobileMoreHubDestinations() so the
headline invariant (hub == feature list minus primary slots, plus the
utility tail) is a tested model function rather than inline bar logic.

Add a sidebar utility-parity guard and a mobile More-hub invariant test.
2026-06-15 01:23:50 +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 9bedd6b4f8 feat(nav): give the Coach a single primary nav home
The Coach is reachable from seven scattered entry points — a floating
button, the hero CTA, suggested-prompt chips, every metric empty state,
a per-metric icon, the conversations drawer — but it had no entry in the
nav. Power users found it instantly; a new user could miss the
differentiator entirely behind a 56px floating circle.

Add the Coach to the shared destination model as one labeled, primary
home, consistent with every other destination across desktop and mobile.
The existing entry points stay; this adds the front door they lacked.
2026-06-15 00:06:28 +02:00
Marc-André Bombeck c90459a7bd refactor(nav): drive desktop and mobile from one destination model
The desktop sidebar and the mobile bottom-nav each hand-curated their
own destination list, and the two had drifted: the sidebar listed
Measurements and Mood inline but hid Workouts; the bottom bar buried
Measurements and Mood in More but promoted Workouts. A user who learned
the product on one platform re-learned it on the other.

Extract one ordered destination model both bars consume. The sidebar
renders every entry in order; the bottom bar keeps its 5-slot shape and
derives its More hub from the same list, so the two surfaces tell one
story. Workouts is now first-class on both. A shared active-resolver
prefers the most-specific sibling so a sub-route no longer lights up its
parent.
2026-06-15 00:04:13 +02:00
Marc-André Bombeck 72fb51c257 feat(labs): add the Laborwerte page, navigation, and OpenAPI contract
A dedicated /labs page hosts manual entry and a per-analyte view: the
four essentials (biomarker, value, unit, date) stay visible, the
reference range, panel grouping and note sit under an optional section.
Results group by analyte with a small inline trend once an analyte has
two or more readings. The reference-range badge is deliberately neutral
and calm — an out-of-range value reads as a quiet directional marker, not
an alarming red.

Adds the labs section toggle to the health-record export panel, the /labs
nav entries, the /api/labs OpenAPI routes, and the labs copy across all
six locales.
2026-06-14 23:39:52 +02:00
Marc-André BombeckandClaude Opus 4.8 f846b0681d fix(demo): hide the Coach FAB in demo mode
The Coach floating button was mounted unconditionally, so a demo
visitor could tap it and send a message. /api/insights/chat is not in
the proxy's demo-mutation allowlist, so the send returned a raw 403.
Demo is read-only by design, so the FAB has no job there — gate its
mount on !demoMode and pin the contract in the hoist test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:52:44 +02:00
Marc-André BombeckandClaude Opus 4.8 4111124f24 fix(nav): keep long More-menu tile labels inside their tile
The More hub grid tile label was a flex child without min-w-0, so
long compound labels (e.g. "Benachrichtigungen") painted past the
tile's rounded border instead of wrapping. Give the span min-w-0
and tightened leading so it wraps cleanly within the tile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 10:52:33 +02:00
Marc-André Bombeck 3d95ace218 feat(ui): an always-there coach button and a tidied insights chrome
A round coach button sits fixed bottom-right on every authenticated
page, above the bottom nav and safe-area inset, with a small dot when a
nudge is unread — replacing the bubble that only existed while a nudge
was pending. The per-chart target-adjust link is gone (the page-level
control remains), the duplicated customize entry on insights sub-pages
collapses onto the tab strip, and the full-width show-all-values foot
button becomes an icon beside the target control. The tab strip's
scroll container keeps four pixels of paint room so pill borders and
focus rings stop clipping at fractional zoom, and the mood tag
breakdown bars adopt the muted opacity the other mood charts use.
2026-06-11 22:12:06 +02:00
Marc-André Bombeck 78fe82e348 perf(insights,api): always answer instantly and regenerate in the background
The insights read path served a cold cache by generating inline — up to
ten seconds on first visit — and the nightly warm pass structurally
excluded evening visitors: their visit stamped the cache, the half-four
run skipped it as fresh, and it expired exactly at the next evening's
visit. Stale content now serves immediately with its timestamp while
one background job revalidates, the warm threshold drops to an hour,
forced warming fills both message locales, the analytics caches serve
stale-while-revalidate, the profile endpoint parallelises its reads,
the bug-report status drops a write per call, the umami proxy degrades
to a no-op script instead of an error, the insights route sheds its
heaviest static imports, and the medication page prefetches on intent.
2026-06-11 19:05:49 +02:00
Marc-André Bombeck 3b480b9885 perf(i18n,shell): ship one locale and stop blocking the first paint
The client bundle carries only the active locale — resolved on the
server and handed to the provider with no hydration flash, halving the
dashboard route to under a megabyte parsed (288 KiB gzip). The auth
shell renders the app chrome immediately instead of a spinner while the
session resolves in parallel, the widgets request folds into the
snapshot it duplicated, and insight pills prefetch their sub-pages —
a first visit to a sub-page lands in about ninety milliseconds instead
of nearly a second.
2026-06-11 02:19:27 +02:00
Marc-André Bombeck bd78cd7f7a feat(medications): schedules remember their history
Editing a schedule's times no longer rewrites the past. Each material
change archives the outgoing cadence as a dated revision, and every
band consumer — history, compliance, write attribution, the auto-miss
cutoff — mints past periods from the schedule that was valid at the
time, so a dose era taken at its then-correct times stays on time
forever. The repair script can infer historical eras from consistent
old anchors behind an explicit flag. The welcome tour scrolls its
target into view, flips above it when space below runs out and falls
back to a centred sheet on any viewport, so no step can strand its
buttons below the fold. The avatar menu drops the about entry — that
lives in settings.
2026-06-10 22:21:44 +02:00
Marc-André Bombeck 04765ea8ac style: one spacing, title and skeleton language across the app
Icons inside buttons drop their manual margins in favour of the
button's built-in gap (two hundred call sites), form field stacks settle
on one spacing step, card titles on exactly two sizes, inline field
errors on one text size, and a dozen hand-rolled loading pulses move
onto the shared skeleton primitive while keeping their silhouettes. The
intake dialog wording joins the rest of the app on 'übersprungen', and
the UI guidelines now document the two deliberate footer and
touch-target systems.
2026-06-10 16:54:15 +02:00
Marc-André Bombeck b108ae7318 feat(ux): close the feedback gaps a new user runs into first
The Withings OAuth callback finally reports its outcome — connected or
a readable reason — instead of dropping the user on the settings page
without a word. The adherence number carries one name everywhere and an
explanation right where it is shown. Demo mode announces itself with a
banner instead of failing silently in English. Bug reports become
reachable from the mobile menu, the about section returns to the
settings navigation, charts link to the target settings they draw,
integrations and source priority point at each other, and the
medication wizard warns when reminders are switched on without a
configured channel. The longest explainer texts shrink to two
sentences.
2026-06-10 15:40:37 +02:00
Marc-André Bombeck 51a7d36366 Show the uploaded photo in the top-right avatar
Render the user's profile photo in the mobile header user button when
one is set, falling back to the initials avatar otherwise — matching
the sidebar user section. Also surface the admin-console link in the
header user menu for admins, mirroring the desktop sidebar entry.
2026-06-07 14:25:04 +02:00