Commit Graph
1900 Commits
Author SHA1 Message Date
Marc-André Bombeck 2c87a93827 feat(glucose): add a diabetes opt-in preference
Introduce an explicit, user-declared "I have diabetes / clinician glucose
targets" preference (`User.hasDiabetes`, migration 0174, defaulted false) plus
its GET/PATCH endpoint at `/api/auth/me/diabetes`. The flag selects which
glucose target band a reading is judged against; it is never inferred from a
value and never asserts a diagnosis. Mirrors the existing per-user-scalar
preference pattern: 60/min rate limit, Zod validation, audit-logged,
field-by-field write. OpenAPI regenerated for the new route.
2026-06-18 20:17:13 +02:00
Marc-André Bombeck 9be30897fa feat(insights): add a cited reference-range backbone and richer metric explainers
Introduce src/lib/reference-ranges.ts: a structured, citation-backed
per-metric reference-range backbone (band + source body + year) with a
deterministic four-state placement helper, read by the UI surfaces. Each
band traces to the medical-citations registry, which gains the supporting
sources (ACC/AHA 2017 and ESC 2024 blood pressure, StatPearls pulse
pressure and mean arterial pressure, ESC/ESH 2018 pulse-wave velocity,
FDA 2024 and StatPearls pulse oximetry, J Gen Intern Med 2019 temperature,
RCP NEWS2 and American Lung Association respiratory rate, visceral-fat
imaging literature, AHA 2024 resting heart rate, ESC/NASPE 1996 HRV,
Wilcox 2000, ACOG 651, Phillips 2017 and CDC 2024 sleep).

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

# Conflicts:
#	src/components/onboarding/DoneScreen.tsx
2026-06-18 19:55:00 +02:00
Marc-André Bombeck 33fec28b29 feat(onboarding): per-module tour re-entry + consolidated replay
Add a 'Diese Tour zeigen' affordance to every module header that opens the
tour narrowed to that module's card in place, plus additive data-tour
anchors (measurements/medications/labs/illness/vorsorge/cycle/mood/
insights/coach/integrations/export/achievements) so the cross-page walk
has a real target on each page. Consolidate the full-tour replay onto a
single Settings -> Advanced card, relabel it, and drop the duplicate from
About. The replay reset clears the stored progress so it restarts cleanly.
2026-06-18 19:49:37 +02:00
Marc-André Bombeck 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 59e7b33285 feat(onboarding): add resumable module-tour progress field + API
Add nullable onboarding_tour_progress_json to users (migration 0173),
a Zod-validated progress shape, and extend POST /api/onboarding/tour to
accept a mid-tour resume checkpoint and clear it on replay. Surface the
resume point on GET /api/auth/me and register the contract in OpenAPI so
the native client mirrors it.
2026-06-18 19:27:28 +02:00
Marc-André Bombeck 63bca26177 refactor(dashboard): extract buildDashboardBands into a client-safe leaf
The band / target math lived inline in two places — the server snapshot
builder and the dashboard page's snapshot-disabled fallback — so a
threshold change had to be hand-mirrored or the two surfaces would grade
the same metric differently. Move the construction into the new
client-safe @/lib/dashboard/bands leaf (buildDashboardBands, with
buildTargetBands kept as an alias for the snapshot-era call sites) and
have both the snapshot builder and the page fallback call it. The page
always computes the client copy from the profile so a still-loading
snapshot frame keeps real bands, then prefers the server numbers once
they arrive. Behaviour-preserving.
2026-06-18 19:02:05 +02:00
Marc-André Bombeck 0af1fbb9d4 refactor(targets): extract the insights-targets builder into lib
The targets route held a ~1.4k-line domain function in the handler body.
Move buildTargetsResponse into src/lib/targets/build-response so it sits
beside the other targets helpers and can be unit-tested without the route
shell; the route is now auth -> cachedSwr(buildTargetsResponse) -> apiSuccess.

Follow the as-needed compliance source pin to the new file location.
2026-06-18 18:34:27 +02:00
Marc-André Bombeck 04b0f38537 fix(measurements): isolate per-type failures in the series batch
The batched daily-series endpoint ran every type through one Promise.all
with no per-task catch, so a single type's transient reject 500'd the whole
batch and blanked every preloaded chart in the dashboard row at once.

Wrap each per-type read so a failure degrades to an empty slice for that
type only; the failing chart self-fetches through its existing fallback.
A degraded_types counter rides the wide event.
2026-06-18 18:29:46 +02:00
Marc-André Bombeck 1a73afa7e9 perf(illness): parallelise the insights recovery-gap fan-out
The cross-episode insights route looped up to 24 episodes, each running a
sequential 10-vital scan with two serial reads per vital — ~500 serialised
round-trips on a year of episodes, starving the shared Prisma pool.

Pair the baseline + episode-window reads, fan the per-vital scan out under a
p-limit(4) cap, and run the per-episode correlations concurrently under the
same discipline. Series order and the gated outputs are unchanged.
2026-06-18 18:28:37 +02:00
Marc-André Bombeck 1b354de68b fix(tz): anchor medication-intake day floor at local midnight
The intake aggregator built its day floor from en-US calendar parts pinned
to UTC midnight, so today's window opened off by the zone offset for any
non-UTC user — a 23:30-local reading could land in the wrong day's bucket.

Promote the DST-safe start-of-local-day primitive into src/lib/tz/local-day
as startOfLocalDayInTz and route the intake route, the cadence start-of-day,
and the recurrence day floor through it, removing three divergent copies.
2026-06-18 18:25:03 +02:00
Marc-André Bombeck 33dcf39adf fix(a11y): hold revealed state under the reduced-motion backstop
The global prefers-reduced-motion catch-all collapsed every animation to
near-zero duration but left animation-fill-mode at its default, so an
entrance animation whose element rests at opacity:0 and only reveals
through its keyframe snapped back to the hidden base style once the
near-instant animation completed. Under reduced motion that left charts
and list rows permanently invisible. Pin animation-fill-mode:forwards and
zero the stagger delay so the shortened reveal holds its final visible
frame and paints immediately. Per-component blocks that set animation:none
are unaffected (no animation to fill).
2026-06-18 17:48:41 +02:00
Marc-André Bombeck 82c6947633 perf(dashboard): batched rollup series endpoint + server-side target bands + gate-helper split 2026-06-18 15:33:53 +02:00
Marc-André Bombeck 437e501536 refactor(dashboard): thin page orchestrator + consume server bands and batched series
Extract the pure first-paint / skeleton-reservation gates
(resolveDashboardFirstPaintGate, resolveConfiguredTileCount,
resolveChartRowPlaceholderCount + their widget-id sets) into
components/dashboard/dashboard-gates; page.tsx re-exports them so the
existing unit-test imports stay valid. Wire the page to read the
server-computed targetBands when snapshot mode is on (legacy client
compute kept for the snapshot-off fallback) and to issue one
series-batch fetch for the visible non-sleep charts, threading each
chart its slice via preloadedSeries.
2026-06-18 15:29:29 +02:00
Marc-André Bombeck 143aefc709 feat(api): batched rollup-backed series endpoint to coalesce dashboard fetches
Add GET /api/measurements/series-batch returning every requested
MeasurementType's daily series in one response, so the dashboard chart
row stops issuing one self-fetch per chart. Extract the rollup-daily read
(per-source collapse, coverage probe + inline fold, live-SQL fallback)
into a shared readDailySeries reader so the single-type route and the
batch route read through the same path with no SQL duplication; the
single route now delegates to it. Cap the batch's per-type reads with
p-limit to protect the shared pool. Sleep stays on its per-night /series
adapter and is not batched.

HealthChart gains an optional preloadedSeries prop: when the dashboard
hands it a covered slice for the daily window it folds those rows instead
of self-fetching, keyed so a range-tab change re-fetches cleanly. Non-
dashboard mounts are unchanged (empty discriminator keeps the cache tuple
byte-identical). Register the endpoint in the OpenAPI table and regen the
spec; add unit + parity coverage for the batch route.
2026-06-18 15:29:20 +02:00
Marc-André Bombeck 722af87382 fix(a11y): add a global prefers-reduced-motion backstop
Per-component motion-reduce gating stays in place with its tailored
static fallbacks; this adds a defensive catch-all so any animation or
transition that misses a per-component guard still collapses to near-
instant under reduced motion. Near-zero rather than zero keeps
animation/transition-end listeners firing so state machines don't
stall.
2026-06-18 15:15:14 +02:00
Marc-André Bombeck cc89695cbd feat(measurements): value-range filter on the management list
Add an optional numeric value-range filter to the measurement list:
readings whose `value` falls within `[valueMin, valueMax]`, either bound
omittable for an open-ended range. Server-authoritative — both bounds are
parsed on `listMeasurementsSchema` (coerced from the query string), an
inverted `valueMin > valueMax` returns 422, and the bounds thread into the
Prisma `where` as a `value: { gte, lte }` range on the raw per-sample list.

The filter rides a new `FilterBarNumberRange` pill that mirrors the existing
date-range control, hidden for the day-grouped and sleep-night types where
the painted value is a synthesised total rather than a stored reading.
2026-06-18 14:42:29 +02:00
Marc-André Bombeck 604aec631f feat(notifications): strengthen the Web Push path for PWA-only self-hosters
Self-hosters without an Apple Developer account rely on Web Push to the
installed PWA — there is no Live Activity or lock-screen widget to drive.
Make that path carry the dose lifecycle on its own:

- Stable per-slot tag. The medication dose-due reminder now sets a
  `med:<id>:<iso-slot>` Web Push tag (medicationDoseTag), so a re-fired
  reminder for the same slot coalesces instead of stacking, and a later
  clear can target it.
- Clear-on-taken. When a dose is marked taken or skipped, push a
  `type:"clear"` notification reusing that slot tag. The service worker
  closes the matching pending reminder and refreshes the badge — the PWA
  equivalent of ending a Live Activity. A snooze leaves the dose
  outstanding and clears nothing. Wired into the single + bulk intake
  routes alongside the existing APNs cross-device sync.
- App badge. The dispatcher carries a server-authoritative outstanding-
  dose count; the SW reflects it via the Badging API
  (setAppBadge/clearAppBadge), feature-detected and a no-op where
  unsupported.
- The SW push handler honours requireInteraction for urgent events and
  renotify so a re-fired reminder re-alerts.

Dose-due, preventive-care, and low-stock reminders already cascade to
Web Push when no APNs channel exists; tests pin that an APNs-less
instance still delivers.
2026-06-18 11:39:55 +02:00
Marc-André Bombeck 6c070abfdc feat(notifications): escalate urgent events to each channel's highest tier
Add an urgent dimension to dispatched notifications so a self-hosted
instance reaches every configured channel at its strongest level without
assuming APNs is present:

- APNs    -> interruption-level time-sensitive + priority 10 (critical
  only when APNS_CRITICAL_ENTITLEMENT=true; time-sensitive needs no
  Apple approval)
- ntfy    -> Priority 5 (max) + warning tag (suppressed in discreet mode)
- Web Push-> Urgency: high send option + requireInteraction
- Webhook -> priority "urgent" in the JSON body
- Telegram-> normal delivery (no urgency tier)

Wire the illness red-flag escalation (sustained low SpO2 / sustained
fever) to emit an urgent SYSTEM_ALERT through the dispatcher, owner-scoped
and module-gated, de-duplicated per episode via the push-attempt ledger.
Normal events are unchanged.
2026-06-18 11:28:18 +02:00
Marc-André Bombeck 4cce49374f feat(illness): soft-delete restore route + undo toast 2026-06-17 14:15:44 +02:00
Marc-André Bombeck 55e7835ef4 feat(illness): restore a soft-deleted episode via delete-Undo
Add POST /api/illness/episodes/{id}/restore: clears deletedAt so the
episode and its preserved day-logs and encrypted note re-surface in
every read. Soft-delete keeps the row and its children intact, so
restore is a pure flag flip rather than a re-create. apiHandler +
idempotency + module gate + owner-scoped 404; restoring a live episode
is a no-op.

Wire the episode-menu delete to a restore Undo toast, mirroring the
labs delete-Undo affordance. Register the route in the OpenAPI source
and regenerate the contract.
2026-06-17 14:15:12 +02:00
Marc-André Bombeck 6ba2d435c9 feat(modules): illness defaults on (remove born-gated mechanism) 2026-06-17 14:11:30 +02:00
Marc-André Bombeck bc94a5893f feat(modules): default the illness journal on
The illness module shipped opt-in/default-off via a bornGated flag, the
inverse of the standard disabled-allowlist. Make it a normal optional
module that defaults on (opt-out), matching labs and its siblings.

No module used bornGated other than illness, so drop the flag, its
isBornGatedModule helper, and the gate resolver's born-gated branch
entirely. The module stays a normal toggle under What you track and the
operator-level availability override is unchanged; an account that wants
it off sets an explicit false. Refresh the gate, doctor-report, labs and
biomarkers comments plus the OpenAPI route descriptions accordingly.
2026-06-17 14:10:46 +02:00
Marc-André Bombeck cdf43c3669 feat(illness): add date-less day-log list and full-history timeline
The episode day-logs endpoint only accepted a single `date`, so the
detail timeline could anchor on today alone. Add a date-less LIST variant
to GET /api/illness/episodes/{id}/day-logs: when no `date` is given,
return the episode's day-logs newest-first with limit/offset/sortDir
paging and meta.total (mirrors the Labs list envelope). The single-date
read stays intact, selected by the param's presence.

Wire the detail timeline onto the list for a full historical scroll
instead of today only, keeping the neutral palette and retrospective
copy. Register the list response in the OpenAPI contract for the iOS
client.
2026-06-17 14:07:16 +02:00
Marc-André Bombeck cba3c0c94f feat(vorsorge): rebuild preventive care to mirror the medication module
Each reminder is now its own med-styled card reusing the medication card
primitives (MedicationCardHeader / MedicationNextLastSlot) on a neutral
surface with a single bottom-pinned action and a kebab carrying edit +
delete. The mark-done action branches on the linked measurement type:

- a free-text / self-planned exam keeps the silent satisfy ("Done");
- a measurement-linked reminder opens the real measurement-entry form
  ("Log value") pre-filled with its type, and on a successful write
  satisfies the reminder in the same action (forward-only guard keeps
  the cron a no-op).

The create/edit sheet gains a first-due date (anchorDate), a free
custom interval, and an RFC-5545 RRULE option, all already accepted by
the create/update schema. A wrench/customize affordance beside the page
Add button re-homes the per-reminder enable toggles moved off the card.

Vorsorge also joins the dashboard grid layout like medications: a new
chart-row summary card listing the next few due reminders with inline
done/capture buttons, default-off so existing dashboards are unchanged.

No migration and no API schema change; OpenAPI stays in sync.
2026-06-17 07:52:48 +02:00
Marc-André Bombeck 370b2f9c14 chore(cleanup): prune dead i18n keys, fix knip glob, scope Rest Mode to window
Remove 18 unreferenced i18n subtrees (75 leaves per locale, 450 strings
across de/en/es/fr/it/pl) left behind by the settings D0-D8 reshuffle and
the medication advanced/erweitert naming drift: settings.ai.disableCoach
(superseded by settings.coach.activate), doctorReport.dialog/sections, the
dead settings.sections export.hero / export.cards.doctorReport /
layout.medications / layout.mood / integrations.tabs subtrees and the
orphaned integrations connections/channels blurbs, the medications
detail.advanced.title/group + detail.edit duplicates, insights.recovery
.scoreLink, insights.range, targets.relativeDay (duplicate of
targets.consistency.relativeDay), and insights.subPage.primary /
lastMeasurement. Each subtree confirmed unused; the i18n coverage,
locale-integrity, and sections-parity suites stay green. The per-slug
settings.sections.<slug>.description keys are kept — they back the
SectionPlaceholder fallback and the parity test asserts them.

Point the stale knip entry at src/proxy.ts (renamed from middleware.ts)
and drop the no-match src/lib/health glob.

Resolve the analytics Rest Mode annotation as of the scored window: thread
a single scoredAt instant into both the health-score computation and
resolveRestMode, instead of letting the annotation read a later now().
2026-06-16 23:51:51 +02:00
Marc-André Bombeck 53dc7f76b5 refactor(jobs): decompose reminder worker into domain registrars; extract achievements result builder 2026-06-16 23:29:46 +02:00
Marc-André Bombeck 03034339d6 refactor(gamification): extract achievements builder out of the route
Move the day-series, completion-date, and streak helpers plus
buildAchievementsResult and the hidden-card spoiler shields into
src/lib/gamification/achievements-result.ts (verbatim). The route drops
from 1247 to 136 LOC and is now auth -> module gate -> cached ->
idempotent unlock-persistence -> apiSuccess (web + iOS format).

No behaviour change: the aggregation moved as-is, the cached factory
contract and the off-cache pendingUnlocks persistence are unchanged.
2026-06-16 23:28:55 +02:00
Marc-André Bombeck cdc76620fb fix(insights,dashboard): rest-mode web banner, sleep per-chart assessments, steps explainer, recovery heading level 2026-06-16 23:20:41 +02:00
Marc-André Bombeck 201686f498 fix(insights): surface Rest Mode on the web and align sleep assessments
Add a calm, value-free Rest Mode indicator to the dashboard score card and
the recovery insight so the web matches what iOS already shows: when an
illness/condition episode is active the score and recovery are framed, never
penalised, and the surface now says so. The new banner is neutral and
untinted, self-gates on an active episode, and reveals only the onset date —
never a decrypted note or episode label. The dashboard reads the annotation
from the snapshot (resolved server-side alongside the score); the recovery
section reads it from the analytics payload.

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

Add the steps explainer (insights.subPage.explainer.steps*) and pass
explainerMetric="steps", and demote the recovery per-block title to an h3 so
the heading outline nests cleanly under the page heading and group heading.
2026-06-16 23:20:04 +02:00
Marc-André Bombeck d92af094ed fix(illness): wire P3 engine to UI + OpenAPI, correctness (return-anchor, red-flag, fever, tz), FLARE/edit, guards 2026-06-16 23:18:40 +02:00
Marc-André Bombeck c243f6a963 feat(illness): episode detail, retrospective surface, edit + delete guard
Add an episode-detail page at /illness/[id] mirroring the lab-biomarker detail:
it renders the per-episode correlation card (recovery gap, red flags, pre-onset
and nadir findings, or 'still learning' when the signal is thin) and surfaces
the stored note. List rows link into it; the cross-episode retrospective summary
sits below the history. Fold the row's inline buttons into a shared overflow
menu and gate delete behind a confirmation dialog. Let the new-episode sheet
double as an edit form, add the FLARE course and a parent-condition picker, and
let the day-log sheet backdate within the episode window. Swap the loading and
empty states onto the shared primitives.
2026-06-16 23:18:06 +02:00
Marc-André Bombeck d7a698950c fix(illness): harden the episode + day-log write paths
Wrap episode-create in withIdempotency so an iOS retry can't mint duplicates.
Enforce resolvedAt >= onsetAt on create, edit, and the resolve verb (a partial
edit is checked against the stored onset). Run the day-log upsert, its symptom-
link replacement, and the read-back in one transaction and return the full DTO,
dropping the route's second round-trip. Restrict symptom severity to 1-3 (a
link's presence already means present, so 0 is not a distinct state). Return
the standard { deleted: true } envelope from DELETE instead of a bodyless 204.
2026-06-16 23:18:06 +02:00
Marc-André Bombeck e8e5ece6a1 fix(labs): auto-link biomarker on write, note-preserve, inverted-range guard, footer-pin, fail-soft decrypt, self-edit 2026-06-16 23:14:01 +02:00
Marc-André Bombeck 9c8c7051aa fix(labs): pin add-entry action bars to the sheet footer; tighten header
The lab and biomarker forms portal their Cancel/Save row into the
ResponsiveSheet sticky footer (footerSlot pattern) so Save stays reachable
on mobile instead of scrolling away. The Labs header folds Manage
biomarkers into an overflow kebab beside the primary Add, mirroring the
medication header. The biomarker detail gains a self-edit Pencil opening
the pre-filled form. Add buttons on the labs detail and measurements page
meet the min-h-11 sm:min-h-9 tap floor.
2026-06-16 23:12:04 +02:00
Marc-André Bombeck 5514271668 fix(reminders,coach): BYHOUR multi-time, vorsorge edit + sheet, COACH label i18n, TOCTOU, origin/endsOn UI 2026-06-16 23:11:55 +02:00
Marc-André Bombeck 6df0c64d85 fix(labs): fail-soft the biomarker list context decrypt
The catalog GET eagerly threw on the first un-decryptable context row,
500ing the whole list. The list serializer now decrypts fail-soft (null
on a bad-key/malformed row) via decryptContextSoft, bounding the
fail-closed blast radius to one marker; the single-resource GET stays the
authoritative throwing read for the plaintext context.
2026-06-16 23:11:54 +02:00
Marc-André Bombeck 9141b8f74f fix(labs): guard partial bound edits against an inverted range
A PUT that moves a single reference bound past the row's stored other
bound slipped through the schema refine (which only fires when both bounds
arrive together). Both the lab-result and biomarker routes now merge the
effective bounds and 422 on low > high. Editing catalog-resolved fields
(analyte/unit/panel/range) on a linked reading is also rejected rather than
silently no-oped. Shared isInvertedRange/effectiveBound helpers with tests.
2026-06-16 23:11:48 +02:00
Marc-André Bombeck 796f6bacd6 fix(labs): auto-link lab readings to a biomarker on write
A free-text lab write resolves or mints a user-scoped biomarker by
(userId, lower(analyte)) so no LabResult row ever persists unlinked. An
existing marker is reused as-is (its unit/range stay authoritative); a
miss mints one from the reading's own spelling. Every reading is now
editable and detail-navigable, and the boot-time backfill becomes a pure
pre-upgrade migration. Closes the unlinked-row dead-end and the contract
hole in one path.
2026-06-16 23:11:28 +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 88c4a8428f fix(admin): harden the Danger-Zone wipe to match the Backups Restore gate
The global data wipe auto-supplied confirm:'DELETE ALL' from a single-click
dialog, leaving the server's typed-token defence dead and making the most
destructive action a weaker gate than the less-destructive restore. Mirror
RestoreRowDialog: require typing 'DELETE ALL' into an Input that arms the
destructive button, reset on close.

Also attach an Idempotency-Key header to the wipe so a double-submit cannot
re-run the transaction, add the documented post-requireAdmin()
'admin-data-delete:<userId>' rate-limit bucket alongside the anonymous IP
throttle, and switch BackupsSection off the non-reactive formatDateTime
onto the locale-reactive useFormatters() hook its siblings use.
2026-06-16 23:05:11 +02:00
Marc-André Bombeck b28fa4d4a5 merge: suppress-during-illness Rest Mode + FHIR Condition + doctor-report 2026-06-16 22:27:13 +02:00
Marc-André Bombeck c59cf0b652 feat(illness): Rest Mode — annotate score/recovery/streaks during an active episode + FHIR/doctor-report
An active illness/condition episode annotates, it never penalises: the
measured number is untouched, only the narrative changes.

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

i18n x6 for the report + reframe copy. Tests: score-annotation-not-
penalisation, streak-freeze, coach-nudge pause, FHIR Condition/Encounter
mapping, doctor-report inclusion, Rest Mode resolver. typecheck/lint/test
green, openapi in sync, knip clean.
2026-06-16 22:25:50 +02:00
Marc-André Bombeck 6a74025b0a merge: illness correlation + recovery-gap engine with reliability battery 2026-06-16 22:17:29 +02:00
Marc-André Bombeck 745fa28c8b feat(illness): correlation + recovery-gap engine
Add the retrospective correlation engine for illness episodes: a pre-onset
anomaly scan (deviation in robust SDs from the user's own median±MAD
baseline), the nadir / what-dropped across the active span, and the
recovery-gap (physiological return-to-baseline vs the felt-better marker).
CHRONIC_ONGOING is excluded from the gap. Every derived number is gated by
the shared coverage/confidence model — a thin signal asserts nothing rather
than showing a wrong figure.

The baseline window ends strictly before the pre-onset lookback, so an
episode's own days can never contaminate the baseline it is measured against.
Reads are DAY-native (rollup tier with a live-SQL fallback), reusing the
baseline engine's read path so the rollup-vs-live parity carries over.

Surfaces:
- GET /api/illness/episodes/{id}/correlation — per-episode findings (gated
  Derived wire shape; red-flag escalation for sustained low SpO2 / fever).
- GET /api/illness/insights — cross-episode retrospective ("sick N times ·
  typical recovery gap X days"), with the typical-gap withheld below the
  min-sample floor. Retrospective only — never a predictor or diagnoser.

Reliability battery: golden-fixture recovery-gap math, rollup-vs-live parity,
baseline-contamination guard, thin-data / coverage-gate, red-flag escalation.
Born-gated + owner-scoped; server-authoritative DTO (iOS renders). i18n ×6.
2026-06-16 22:16:19 +02:00
Marc-André Bombeck 042510d1d4 merge: flatten the dashboard hero to match the chart-card design 2026-06-16 21:55:34 +02:00
Marc-André Bombeck 4a10505500 merge: coach card redesign — wider rail, symmetric dividers, FAB inset 2026-06-16 21:54:58 +02:00
Marc-André Bombeck 651aa65c28 fix(dashboard): flatten the hero band to match the tile + chart surfaces
The daily-verdict band wore a gradient surface and a health-score ring
with a drop-shadow bloom, a green-band breathing pulse, a conic sheen,
and a sweep entrance. It read louder than the chart cards and tiles it
sits among.

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

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

- Widen the inline history rail (260px -> 300px on lg, 340px on xl) and
  bump rail typography + padding off the cramped 10/11px scale so the
  conversation list and source toggles read at a comfortable size.
- Align the dividers across columns: the header, the history-rail
  heading, and the centre rail-tray strip now share one fixed h-14 band
  with identical borders, so the lines land on the same row instead of
  the restless two-height split.
- Redesign the header leading cluster: the surface-toggle sits in its
  own group behind a hairline divider rather than crowding the avatar,
  which steps up to size-9.
- Give the full-page coach card more vertical room and a soft shadow so
  it fills the standalone route instead of leaving a wasted band below.
- Even the coach FAB corner inset so the right gap matches the bottom
  gap at each breakpoint.
2026-06-16 21:54:08 +02:00
Marc-André Bombeck 8447ad90cc merge: settings IA round 2 — top-text removal, integration split, medications toggleable, coach activate-flip, gesundheitsakte de-hero 2026-06-16 21:50:27 +02:00