2415 Commits
Author SHA1 Message Date
Marc-André Bombeck ecb54a8d1b Merge branch 'fix/prompt-structure' into release/v1.27.5 2026-07-05 15:11:03 +02:00
Marc-André Bombeck 4b36d5ba52 fix(api): enforce range and source guards on the measurement edit path
PUT /api/measurements/[id] was the one write surface that skipped the
per-type plausibility bands — POST, batch, CSV import, Apple export,
Telegram and MCP all enforce VALUE_RANGES — so an implausible edited
value flowed unchecked into rollups, the health score, the BP gates
and the Coach snapshot. The route now validates the new value against
the row's own type after the lookup and returns the standard
multi-issue 422 envelope on violation.

Value edits on server-owned sources (anything outside
WRITABLE_MEASUREMENT_SOURCES: connector, import, computed rows) are
refused with 409 and the stable errorCode
measurement.update.server_owned_source — editing a provider-attributed
number forges a reading the server never received. Timestamp and note
edits stay allowed, and the edit sheet shows a localised message for
the new code.

The schema's generic min(0).max(500000) bound is dropped: the per-type
band is stricter where it matters, and min(0) blocked legitimate edits
of the signed types (ANS_CHARGE, BODY_TEMPERATURE_DEVIATION). Route
tests cover out-of-range 422, negative signed-type accept, the
server-owned 409, a timestamp-only edit on a server-owned row, and the
happy path. The PUT endpoint is not part of the published OpenAPI
surface — the generated contract is unchanged.
2026-07-05 15:07:40 +02:00
Marc-André Bombeck ba17ca34eb feat(insights): open the briefing card on the day's signals
The recall/forward-look block read as a second briefing paragraph
above the structured list and pulled the card down — the card now
starts straight at 'signals of the day'. The section titles read in
the regular foreground, and the card's Ask-the-Coach corner entry is
gone; the Coach keeps its own surfaces.
2026-07-05 15:03:22 +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 a20aa845d9 fix(ai): ask for blank-line paragraphs in the assessment and briefing output formats
The shared formatting contract asks every narrative surface for short
blank-line paragraphs, but two field-level instructions overrode it:
the assessment OUTPUT FORMAT pinned 'flowing prose (2-4 sentences)'
and the briefing paragraph asked for 'a connected 70-160 word read' —
so both consistently arrived as one unbroken block regardless of the
contract. Both now spell out 1-3 short paragraphs separated by a
blank line written as \n\n inside the JSON string.
2026-07-05 14:59:05 +02:00
Marc-André Bombeck dc963540b8 feat(insights): surface the recovery score's factor breakdown
The recovery detail page rendered only the ring — the one wellness
score without a composition view, although the COMPUTED score IS the
readiness blend persisted nightly. The wellness-score read now attaches
the blend's per-factor sub-scores (RHR, HRV, sleep, respiratory rate,
mood stability — same engine, same weights, resolved server-side on the
same request) when the canonical latest row is the COMPUTED proxy, and
the anatomy view renders them through the identical ranked contributor
rows the readiness and sleep-score composites use, reusing their
labels. A WHOOP-native percentage is not our blend, so it keeps the
row-less presentation; a gated blend simply omits the breakdown. The
new value field is optional and additive — existing clients decode
unchanged.
2026-07-05 14:58:49 +02:00
Marc-André Bombeck 75b0e618b4 fix(cycle): stop the wheel counting past a plausible cycle day
A user-visible "day 90" inside the cycle ring traced to the open
cycle's phase window: the engine labels every day up to the predicted
next start, and a confirmed or anchored ovulation signal (BBT trend,
symptothermal double-check, LH surge — all clamped to future next
starts) or a long estimated length can push that prediction into the
future while the last logged period start is months old. Every day of
the gap then carries a population-framed LUTEAL label, and the wheel's
back-walk counted the whole gap as one giant cycle.

deriveWheelState now applies an honest ceiling: beyond the profile's
typical cycle length plus a 14-day grace window the wheel drops the
day count and phase claim, draws the canonical four-phase dial, and
the view swaps the caption for a factual "period is later than usual —
the count pauses" line plus a log-period CTA. The dashboard ring tile
and the insights summary card share the same derivation, so neither
can show the inflated count. Calendar labels and the server DTO are
unchanged.
2026-07-05 14:53:35 +02:00
Marc-André Bombeck 0b2bb42625 fix(checkups): keep the measure button constant when a checkup is due
The card and list variants of the reminder action button washed green
(bg-success) once the next-due date arrived. A button that changes
colour by schedule state reads as a different control; the rule that
keeps the medication-card surface constant applies to actions too.
Due / overdue keeps reading through the discreet coloured next-due
text and the adherence meta line — the button itself never changes.
2026-07-05 14:50:41 +02:00
Marc-André Bombeck d323d01c93 fix(medications): drop the duplicated overdue line below the streak row
The injection/GLP-1 card showed an overdue dose twice: once on the
next-intake value line (the destructive escalation with the due time)
and again as the open-cycle status line below the compliance bars. The
cycle line's missed tier is retired; the due-today tier stays. The
reserved min-h slot in the shared card body is untouched, so card
heights stay symmetric across medication types.
2026-07-05 14:49:22 +02:00
Marc-André Bombeck ff27faf3ef docs(google-health): rewrite the mapping notes for the verified v4 contract
The previous notes carried two wrong premises: that every launch type
supports dataPoints.list, and that the response payload is snake_case.
Document the three per-type encodings (kebab path, snake_case filter,
camelCase payload union), the int64-as-string fields, the per-shape
filter grammar, the dailyRollUp read for the cumulative activity types,
the corrected daily-* data-type ids, the sleep end-time filter, the
exercise metricsSummary payload, the AIP-193 error surfacing and the
structure probe.
2026-07-05 13:59:31 +02:00
Marc-André Bombeck f92e9fd840 feat(google-health): surface the API error body and add a structure probe
- On a non-2xx data-point response, extract the AIP-193 error envelope
  into a redacted STATUS: message detail (bearer tokens stripped, URL
  query strings dropped, 200-char cap) and thread it into the thrown
  GoogleHealthApiError plus the external-call annotation, so a
  field-grammar 400 is diagnosable from operator logs. The OAuth token
  endpoint keeps its flat invalid_grant handling.
- POST /api/integrations/google-health/test with {"probe":"structure"}
  now fetches one recent page/window per data type and returns only the
  payload STRUCTURE (field names + typeof leaves, never values) plus the
  per-type error verdict — paste-safe diagnostics for payload-shape
  drift. Same auth and rate limit as the plain connection check.
2026-07-05 13:59:24 +02:00
Marc-André Bombeck 295a6a8050 fix(google-health): parse the camelCase v4 payloads and correct the read methods
The v4 client read none of its documented response shapes: the DataPoint
value is a camelCase union keyed by the camelCase type name with camelCase
nested times, proto3 int64 fields arrive as JSON strings, and four fetchers
targeted wrong data types or illegal filter fields. Every HTTP-200 type
parsed zero measurements and four fetchers returned HTTP 400.

- Read every value/time path in camelCase; keep snake_case only inside the
  filter parameter. Pin path/filter/payload-key triples per data type.
- Coerce int64 JSON strings in every numeric extractor.
- weight.weightGrams -> kg, height.heightMeters -> cm,
  distance millimetres -> m, bodyFat.percentage, heartRate.beatsPerMinute.
- Move SpO2, HRV, respiratory rate and VO2 max to their daily-* data types
  (the bare sample types reject a .date filter).
- Move steps, distance, active energy and floors to POST :dailyRollUp with
  windowSizeDays 1 (list returns minute-grain buckets, not daily totals;
  floors has no list method), chunked at 90 civil days, day-keyed on
  civilStartTime.date at the UTC-midday anchor.
- Filter sleep on interval.end_time (the only legal sleep time field) and
  anchor sessions on interval.endTime; filter exercise on
  interval.civil_start_time with an offset-less civil bound in the user's
  zone; read exercise metrics from metricsSummary and take the workout id
  from the DataPoint name.
- Seed height from max(sampleTime) explicitly - list order is descending,
  so last-row-wins picked the oldest sample.
- Rewrite the mapper fixtures to the documented shapes.
2026-07-05 13:58:50 +02:00
Marc-André Bombeck d83220a4e0 feat(insights): assessment and prose readability pass
- assessments render in full; the three-line clamp and its show-more
  toggle are gone
- AI prose (assessment, briefing, explainers, usual-range note) reads
  in the regular foreground; muted stays reserved for meta lines
- the usual-range note becomes a proper card with the shared TileHeader
  (icon + title, labelled in all six locales)
- the metric-page heading + explainer indent to the cards' text edge
- the health-score card drops its Ask-the-Coach corner button
2026-07-05 13:27:30 +02:00
Marc-André Bombeck 8579705fbf fix(insights): keep the briefing on a transport-failed grounding retry
When the corrective grounding retry dies on transport (provider outage,
timeout) the model never got its chance to self-correct — stripping the
briefing to null left the morning surface empty. The strip now falls
back to the previous payload's briefing, which passed this same gate
when it was written; a content-failed retry still strips hard so a
fabricated number never persists.
2026-07-05 13:27:30 +02:00
Marc-André Bombeck e754979abf feat(ai): allow lists and one bold takeaway in the formatting contract
The shared contract banned every structure beyond 1-3 paragraphs, so
longer replies read as unformatted blocks. It now asks for short
blank-line paragraphs (2-4 on longer replies), permits a plain "- "
list when a reply genuinely enumerates three or more parallel items,
and one bold key takeaway per reply. The Coach ground rule keeps
prose-first; a list is the exception, never the default shape.
2026-07-04 23:33:17 +02:00
Marc-André Bombeck a778a0b2f7 feat(insights): render plain lists and bold in the AI prose surfaces
The prose renderer knew only blank-line paragraphs, so a reply that
enumerated options read as dash-prefixed text lines and a bold marker
surfaced as literal asterisks. ProseBlocks now groups consecutive
"- " lines into a real list and renders **bold** spans as strong
text — pure string splitting over the trusted string, fail-closed on
anything unclosed or unknown, still no markdown library. The Coach
streaming renderer settles completed paragraphs through the same
block renderer.
2026-07-04 23:33:16 +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 5b63cb9ac9 Merge main into release/v1.27.0
Reconciles the v1.26.1 Fitbit setup-guide fix with the Google Health
release: google-health.md stays the full runbook (with a pointer to the
classic fitbit.md guide), the doc-parity guard now asserts the Google
Health page against the google-health client's endpoints and scopes,
and the changelog carries both sections.
2026-07-04 14:57:39 +02:00
Marc-André Bombeck 0d3067837e fix(google-health): surface the connection status and add the resume and test routes
Wire the settings card to the live connection so it reflects reality
after a connect.

- Emit a google-health entry on the consolidated integration-status
  envelope (configured, connected, last sync, backfill, and the
  needs-reconnect flag). Without it the card read an undefined status and
  showed a permanently disconnected connection even after a successful
  OAuth grant, hiding the sync, disconnect, and reconnect controls.
- Add the resume and test-connection routes the card calls, mirroring the
  classic Fitbit ones.
- Use the shared refresh icon on the reconnect action.
- Drop the experimental-scope opt-in from the runbook and describe ECG
  and irregular-rhythm as a planned addition rather than a current toggle.
2026-07-03 14:41:53 +02:00
Marc-André Bombeck 422a6a7df0 fix(google-health): correct day-keying, timezone, and skin-temperature handling
Tighten the transport so device data lands on the right day and in the
right slot.

- Cumulative daily totals (steps, distance, energy, floors) now key their
  measuredAt and stats:<tag>:<day> external id off the interval's civil
  start date anchored at UTC-midday, not the physical instant. Keying off
  the physical instant put a positive-UTC-offset user's day one calendar
  day early and split the same local day across the Google, Apple, and
  Fitbit sources so neither won the per-day source pick.
- Thread the user's timezone through the sleep, workout, and interval
  parses so an offset-less wall-clock timestamp resolves in the user's
  zone rather than the process zone — the same fix the classic Fitbit
  sleep path already carries.
- Drop skin temperature from the launch set. Google reports a signed
  nightly deviation from baseline, not an absolute reading, so mapping it
  into the absolute wrist-temperature slot stored a delta as a
  temperature and dropped every cold-night negative. It returns once a
  signed-delta model exists.
- Remove the unused daily-rollup read path and field-map, and the
  experimental ECG and irregular-rhythm scopes that had no reader — the
  connect flow now requests exactly the four core scopes it uses.
2026-07-03 14:41:44 +02:00
Marc-André Bombeck 9f7947a729 feat(google-health): add the settings card, runbook, and translations
Surface the Google Health integration to the operator and document the
setup honestly.

- A settings card matching the other integration cards: the BYO Google
  client id and secret form, connect and disconnect, a manual sync, and
  a distinct reconnect prompt when the connection needs fresh consent.
- Register the card in the connections panel and extend the integration
  key, the docs-link provider, and the query-key factory.
- Translations for the card copy across all six locales. The copy states
  plainly that this reads Fitbit, Pixel Watch, and Fitbit Air through the
  user's Google account, that it succeeds the Fitbit connection Google
  retires in September 2026, that the operator registers their own Google
  client, that stress and readiness are not available, and that a
  periodic reconnect may be needed.
- Rewrite the Google Health runbook into the real setup: a Google Cloud
  project, the Health API, an OAuth consent screen kept in Testing, a
  Web Server client, the callback redirect, the four Restricted scopes,
  the hundred-test-user ceiling, and the reconnect caveat.
- Regenerate the OpenAPI contract for the new measurement source.
2026-07-03 14:13:39 +02:00
Marc-André Bombeck b26a18d35e feat(google-health): wire the OAuth routes, sync jobs, and source priority
Add the request surface and the background sync for the Google Health
provider, all built on the shared integration plumbing.

- Routes under /api/google-health: connect (mint the PKCE pair and state
  nonce, redirect to Google), callback (timing-safe CSRF, atomic state
  consume, token exchange, connection upsert, clear the reauth flag,
  enqueue backfill), credentials (encrypt the operator's BYO client id
  and secret field-by-field), disconnect, status, and a rate-limited
  manual sync.
- Jobs: a boot-time backfill, an hourly poll cohort, and a daily
  oauth-state cleanup, registered under the existing dead-queue contract.
- Rank GOOGLE_HEALTH just after Fitbit in the per-metric source ladders;
  it is the successor API for the same devices.
- Add GOOGLE_HEALTH to the readable measurement-source enum but leave it
  out of the writable set — the provider is server-owned, so a client
  cannot attribute a write to it.
- Register the two new encrypted token columns and the two BYO-client
  columns in the encryption-rotation registry and the rotation script.
2026-07-03 14:13:28 +02:00
Marc-André Bombeck fb17a4e9c6 feat(google-health): add the Google Health API transport
Add the transport that reads a user's device data from the Google Health
API (health.googleapis.com/v4): Google OAuth with PKCE and offline
access, the paginated dataPoints reader with a daily-rollup fallback for
the types that lack a list method, the metric/activity/sleep/workout
mappers onto the shared measurement shape, and a response classifier
that separates retryable errors, hard rejects, and the re-consent case.

Access tokens live an hour and refresh tokens are reused, not rotated. A
refresh that comes back invalid_grant raises a distinct reauth-required
signal so the app can prompt a fresh consent rather than fail silently —
the periodic re-consent a Testing-mode Google client needs. The four
core readonly scopes ship by default; the Pixel Watch ECG and
irregular-rhythm scopes stay behind an opt-in flag.

Only the transport is new: the mapped-measurement output, the
upsert/dedup/rollup tail, and the source ladders are the shared ones.
2026-07-03 14:13:16 +02:00
Marc-André Bombeck 46db21dbeb fix(fitbit): correct the setup guide to the classic Fitbit Web API
The Fitbit integration connects through the classic Fitbit Web API, so a
self-hoster registers a Fitbit developer app at dev.fitbit.com — but the
setup guide walked users through creating a Google Cloud OAuth client,
whose client id the Fitbit sign-in rejects (unauthorized_client). Rewrite
the guide for the dev.fitbit.com flow (app type, HTTPS callback, scopes,
PKCE), leave the old page as a pointer, and add a test that keeps the
documented scopes and endpoints in sync with the client so the guide
cannot drift from the code again. Flags the September 2026 sunset of the
classic Fitbit Web API.
2026-07-02 09:10:38 +02:00
Marc-André Bombeck 88474adcd0 refactor(ui): finish the page-header adoption and align the demote label
Route the notifications main view and the documents header through the
canonical page-header vocabulary the rest of the release adopted, and
align the admin demote-role label to the standard Nutzer term.
2026-07-02 00:30:19 +02:00
Marc-André Bombeck f118b6f8c7 refactor(insights): route the dashboard chart colors through theme tokens
The authenticated dashboard's chart lines, reference bands, and blood-
pressure target zones still carried raw Dracula hex, so they rendered as
low-contrast neon on the light theme. Route them through the same tokens
the insights pages use; dark rendering is unchanged.
2026-07-02 00:30:19 +02:00
Marc-André Bombeck 7ca2657454 fix(api): keep logout resilient and drop the over-strict batch time bound
A transient database fault during logout must still clear the session
cookie; record the delete failure on the wide event instead of throwing
before the cookie is cleared, so a user can never stay authenticated
after logging out. Remove the batch measurement future/past instant
refine: cumulative HealthKit rows carry an end-of-day endDate that is
legitimately hours ahead for the current day, and a whole-array parse
would 422 the entire sync. Note the workout batch's deferred Withings
path must write its source directly.
2026-07-02 00:30:19 +02:00
Marc-André Bombeck ba8118546d build(lint): ban raw palette colors and guard the design contract
Add the healthlog/no-raw-palette-color rule so UI code routes color
through the semantic tokens instead of Tailwind palette shades, and
sweep the remaining offenders (password-strength scale, injection-site
accents, redundant dark overrides). Add guard tests for width stability,
the single page-header vocabulary, and motion-reduce coverage on
indefinite animations.
2026-07-02 00:16:03 +02:00
Marc-André Bombeck fda0e504f0 fix(insights): compute the adherence storyline from the schedule-anchored engine
The Coach adherence storyline derived its rate from the raw coverage
rollup, whose denominator counts logged slots rather than scheduled
doses, so a user who logged only taken doses read ~100% and the dip gate
suppressed a real decline. Extract the dashboard tile's schedule-anchored,
cadence-aware compliance into one shared engine and route both surfaces
through it, so Coach and the dashboard report the same number.
2026-07-02 00:16:03 +02:00
Marc-André Bombeck 40859613aa fix(insights): bound the tiered rollup series to the requested window
The week/month/year rollup read anchored on now and always returned the
trailing window, so a historic from/to range (API and iOS callers) came
back with buckets outside the requested span. Thread the resolved from/to
bounds through so the read is bounded on both ends, matching the day-tier
and live-SQL fallback contract; tier selection is unchanged.
2026-07-02 00:16:02 +02:00
Marc-André Bombeck c9f8eb9423 fix(ui): replace native date pickers and tokenize caution colors
Swap the remaining native date inputs (allergy onset, environment
travel/backfill ranges, inbound-document dates) for the app DateField
so they no longer render in the browser's locale order. Route the
medication caution amber and the document info banner through the
warning/info tokens so they adapt to the theme.
2026-07-02 00:00:38 +02:00
Marc-André Bombeck 2afe32492b refactor(insights): route metric-page and chart colors through theme tokens
The per-metric reference bands, chart palettes, and status tints across
the insights pages and shared charts carried hardcoded hex that glared
on the light theme. Route them through the semantic and chart CSS
variables; dark rendering stays byte-identical, light gets the muted set.
2026-07-02 00:00:37 +02:00
Marc-André Bombeck 887c79ec86 refactor(ui): unify page headers and surface read failures
Add a PageHeader primitive (one H1 vocabulary, optional description,
back-link, and action slots) and route the hand-rolled headers through
it, so every module reads with the same title/subtitle rhythm and no
icon beside the page title. Mental Wellbeing gains a visible header.
Add a QueryErrorCard and wire the list surfaces so a failed read shows
a retry card instead of falling through to the empty state, and swap
their full-page spinners for skeletons that hold the layout.
2026-07-02 00:00:37 +02:00
Marc-André Bombeck 336ce5aef3 fix(api): narrow swallowed database errors and redact the incident URL
The incident forwarder stripped the query string but not path-segment
secrets from the reported URL; route it through the same redaction the
message and stack already use. Several delete-and-swallow paths caught
every error as the expected unique/absent-row case, masking real
failures on security-notification, logout, and integration-disconnect
paths and the Telegram reply-consume guard; narrow each to the specific
Prisma code and let real failures surface.
2026-07-01 23:45:08 +02:00
Marc-André Bombeck 9aa0a3597a fix(api): constrain client-writable sources and batch timestamps
Narrow the workout create schema's source to the client-writable set
(MANUAL, APPLE_HEALTH), matching the measurement write surfaces, so a
client cannot forge a server-owned source into the cross-source picker.
Apply the single-entry plausibility bound to the measurement batch
timestamps so a backdated or future-dated row can no longer slip past
the batch path. Regenerate the OpenAPI contract.
2026-07-01 23:45:08 +02:00
Marc-André Bombeck 86d80d2c04 refactor(admin): migrate section cards onto the shared card primitives
The admin tree hand-rolled its card surfaces and stepped padding at sm:
instead of md:, so it read denser than every other route on a tablet.
Move the sections onto SettingsCard/SettingsCardHeader and the mini-rows
onto ListRow; no behaviour, data, or copy changes.
2026-07-01 23:44:55 +02:00
Marc-André Bombeck c6dea5eb56 refactor(insights): route chart and classification colors through theme tokens
Reference-band fills, classification tones, and the trend-chart palette
carried hardcoded hex that rendered identically in both themes and
glared on the light surface. Route them through the semantic and chart
CSS variables, which already hold per-theme values, so the light theme
gets the muted set and the dark theme stays byte-identical.
2026-07-01 23:44:55 +02:00
Marc-André Bombeck f219072881 style(theme): soften the light-mode neutral spine
Lift the light theme off the pure-black-on-white extreme. Foreground
moves from a near-black L0.24 to a dark tinted neutral (foreground-on-
card drops from 16.5:1 to a comfortable 13.2:1), the card leaves pure
white so surfaces separate by lightness, borders lift to stay visible,
and every neutral carries a faint mauve tint toward the brand hue. Adds
an Alucard light value for the yellow accent so charts stop glaring.
Accents, chart hues, and the dark theme are unchanged. themeColor for
light matches the new background.
2026-07-01 23:44:55 +02:00
Marc-André Bombeck 7c4b54e3ea fix(medications): let the custom dose-window toggle stay on
The per-dose "custom window" switch could never be turned on. Switching it on
seeded the range from the default band and emitted immediately, but the
serialiser drops a range equal to the default band, so the persisted value
round-tripped straight back to empty, the controlled `checked` recomputed
false, and the switch snapped off. The explicit range fields only appear once
the switch is on, so there was no way to edit the range into a non-default one
first — a deadlock.

Track the opened-switch intent in local editor state, decoupled from
serialisation. A toggled-on row renders its default band immediately and the
switch stays on; an explicit range persists only once the user edits it away
from the default. Toggling off drops any stored range back to the default
derivation.
2026-07-01 18:56:43 +02:00
Marc-André Bombeck a0032ba7ed fix(medications): render the dose as a tag beside the category
The card header carried the dose on its own muted line under the name and the
category as an outline badge below that. Surface the dose as a tag sitting next
to the category badge instead — `[7,5 mg] [Sonstiges]` — so a blood-pressure
tablet and a GLP-1 injection present their strength the same way, in one tight
badge row directly under the name. Both cards share this header, so the generic
and GLP-1 variants pick it up together. State badges keep their own row below,
so the narrow-viewport two-line contract is unchanged.
2026-07-01 18:53:57 +02:00
Marc-André Bombeck bb72cbe092 fix(insights): stop the daily briefing vanishing on grounded vitals
The daily-briefing number-grounding gate strips the whole dailyBriefing block
to null when it finds a restated figure it cannot trace to a pre-computed
server value. Its allow-set covered signalsOfDay plus the glucose / labs /
preventive-care / workout / grip / waist / pain aggregates, but omitted the
core vitals blocks — weight, blood pressure, pulse, body fat, mood, sleep,
activity, rate-pressure product, seasonal variation, historical comparison —
even though the briefing prompt is fed all of them.

A verdict-first briefing that restated a blood-pressure average or a resting
pulse (figures every BP-tracking user has) therefore tripped the gate on every
run: one ungrounded number strips the entire briefing while the rest of the
insight refreshes, so the surface reads as "every other text updated, only the
briefing is gone".

Admit every figure the server pre-computes for those blocks into the allow-set.
The anti-fabrication guarantee is unchanged — only server-computed numbers are
ever added, so a genuinely invented value is still flagged and stripped.
2026-07-01 18:51:36 +02:00
Marc-André Bombeck 13028a2605 fix(settings): drop the back-link on Appearance subpages
The "back to Appearance" link above the heading on each /settings/layout/
<module> subpage pushed the whole section down and read as clutter. Remove
it — the subpage is a dead end reached from the hub or the module cog; the
left nav already returns to Appearance. The shared back-link primitive
stays (still used by the shell's layout-child fallback path).
2026-07-01 16:47:31 +02:00
Marc-André Bombeck d73656685a fix(settings): move pre-existing conditions into Anamnese
The chronic / pre-existing conditions the Coach watches lived under the
personal-context "About me" panel, but they belong with the rest of the
medical history. Move the conditions and the "what should the Coach
watch" focus into Settings -> Anamnese, next to allergies and family
history, and make them editable inline there.

The store is unchanged: both fields are still part of the
/api/coach/about-me payload, read and written through the same query key,
so the Coach and every other consumer keep reading the same data. The
About-me panel keeps the free-text note and the allergies line and omits
the two conditions fields on save, leaving them untouched.
2026-06-30 23:46:03 +02:00
Marc-André Bombeck 5bfba0ba60 feat(mental-health): open an instrument's trend from its card
Clicking a PHQ-9 / GAD-7 card body now opens that instrument's history
in a detail sheet, mirroring how a medication card opens its detail. The
card body is the navigation target; the bottom-pinned Start button stays
a separate action and keeps its canonical typography and spacing.

The detail reuses the existing history chart and dated list, pinned to
the chosen instrument: the PHQ-9 / GAD-7 toggle and the outer card chrome
drop away since the sheet owns the title. The combined history card on the
landing is unchanged.
2026-06-30 23:42:49 +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 5e691d3be2 fix(insights): give the comprehensive briefing a longer generation budget
The comprehensive briefing aborted its provider call at the 120s budget
on a full-history account — a heavier reasoning model spent longer than
that before the first token, so the chain timed out, fell through to no
fallback, and the briefing rendered its greeting with an empty body.
Raise the comprehensive timeout to 180s so a slow first token no longer
clips a generation that would otherwise complete; the per-user response
timeout still overrides it for operators who need more, and the worker
cap tracks this value.
2026-06-30 23:41:11 +02:00
Marc-André Bombeck 7e8e848eb2 fix(medications): drop the unused DoseUnitKey export
The shared dose-unit module exported a DoseUnitKey type alias that nothing
consumed, tripping the dead-code gate. Drop it; the const array is the
only export the wizard and formatDose need.
2026-06-30 21:45:56 +02:00
Marc-André Bombeck 220233e845 fix(medications): localise the per-schedule dose accent too
The medication and GLP-1 cards print a per-schedule dose override as a
trailing accent ("— 5 mg"), straight from the stored string, so the same
raw unit token the main dose carried still showed through there. Run it
through formatDose so the per-schedule accent localises like the rest.
2026-06-30 21:31:45 +02:00