Commit Graph
760 Commits
Author SHA1 Message Date
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 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
Marc-André Bombeck 9b191a208d feat(settings): round-2 information-architecture rework
Apply the v1.18.0 walkthrough notes across the settings surface:

- Drop the explanatory top-text block from every settings section so
  content starts at one consistent vertical height (the sr-only heading
  stays for the accessibility tree).
- Move "Restart onboarding tour" out of Account into Advanced.
- Module hub: drop the explanatory line above the first card.
- Split the Integrations page into three left-side entries — the
  Connections content is now the Integrationen page itself, with
  Channels and Sources (source weighting) as their own entries. Drop the
  /settings/sources redirect (it is a real route again); Channels is a
  new entry.
- Notifications: drop the top text, the channels / inbox cross-links,
  and the embedded Vorsorge editor (it has its own page). Keep the mood,
  low-stock, and proactive-coach reminder cards.
- Rename the Layout hub entry to "Layout".
- Flip the Coach toggle to "Activate Coach" (default on, opt-out) over
  the same single disableCoach column — one source of truth.
- Move the AI "About me" context into Account → Profil, before
  cycle tracking.
- Gesundheitsakte: replace the gradient hero with a normal card.

i18n across all six locales for the renamed / added labels; settings
tests updated for the new structure.
2026-06-16 21:47:00 +02:00
Marc-André Bombeck cdf7fa2d41 merge: admin consolidation — unified coach section, backups layout, danger-zone parity 2026-06-16 21:42:29 +02:00
Marc-André Bombeck bc2ce702f1 fix(insights): rebuild the recovery page onto the canonical metric structure
The recovery page rendered a grid of compact device-score tiles plus a
redundant cross-link to the recovery-score anatomy at the top. It now
follows the same structure as every other metric page (blood pressure,
pulse, the HealthKit scaffold): each present device-native signal renders
a block carrying a short explainer, the max / median / mean stat strip,
the shared chart with the identical 7 / 30 / 90 / All toggle, and a
per-chart assessment. Multiple charts now carry multiple matching texts.

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

Regenerate the OpenAPI contract for the widened metric-status enum.
2026-06-16 21:35:09 +02:00
Marc-André Bombeck 3a29387393 refactor(admin): consolidate assistant + feedback-quality into one Coach section
The ai-quality, assistant, and coach-feedback admin sections were three
separate navigation entries for one coherent coach-quality concern. Fold
them into a single "Coach" section that stacks the assistant surface
toggles, the server-key + module-availability operator config, and both
feedback-quality tables (Coach + Insights). Drop the three old slugs and
their section copy; add admin.section.coach.* across all six locales.
2026-06-16 21:32:06 +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 ba8804cce0 merge: coach-cadence suggestions — SUGGEST-REMINDER sentinel, suggestion SSE frame, one-tap reminder 2026-06-16 21:19:36 +02:00
Marc-André Bombeck 12f4bc24e8 feat(coach): evidence-based cadence suggestions with one-tap in-chat reminder
The Coach can append a ---SUGGEST-REMINDER--- sentinel to propose a
measurement cadence (weight daily, the ESH/AHA 7-2-2 home-BP protocol,
structured glucose). The server resolves the cadence from a closed catalog
and emits an additive 'suggestion' SSE frame the chat renders as a one-tap
action card; accepting it creates a MeasurementReminder with origin COACH
through the same engine the Vorsorge surface uses (server-authoritative
schedule, finite course window self-expires via endsOn).

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

The reminder DTO now echoes origin and endsOn for the iOS client.
2026-06-16 21:17:51 +02:00
Marc-André Bombeck 6ee0060c8a merge: illness/condition journal P2 MVP — born-gated module, episode CRUD, symptom timeline 2026-06-16 21:17:09 +02:00
Marc-André Bombeck cc32e2adf7 merge: Labs overhaul — biomarker catalog, structured entry, dashboard-style chart, edit/undo, backfill
# Conflicts:
#	src/app/api/labs/route.ts
2026-06-16 21:17:09 +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 90e0f2191c feat(labs): OpenAPI contract, i18n, and module-gate inventory
Add the Biomarker + restore routes to the OpenAPI registry and carry
biomarkerId on the LabResult schema (additive; iOS resolves the unit +
range from the linked marker, never recomputes). Regenerate the YAML.
Add the labs.* / labs.catalog.* keys across en/de/es/fr/it/pl. List the
biomarker catalog tree + restore route on the module-gate inventory's
data-layer exemptions (the module gates the surfaces, not the row store).
2026-06-16 21:06:16 +02:00
Marc-André Bombeck 04c95e5ba1 feat(labs): structured entry, per-biomarker chart, and edit/delete UI
Rework the Labs surface to match the rest of the app:

- Add a biomarker manager on the Labs page (define/edit/delete markers,
  seed from a common panel) — the catalog lives with Labs, not Settings.
- Replace the free-text add form with a structured one: pick a biomarker,
  enter the value against its known unit/range; '+ define new' inline.
- New /labs/[biomarkerId] detail page with a proper dashboard-style chart
  (Recharts line + shaded reference band + dashed bounds, range tabs,
  tooltip) replacing the 72x20 sparkline, plus the reading history with
  full edit/amend/delete and a delete-Undo toast.
- List cards group by linked marker and deep-link to the detail chart.

The reference band uses a muted token fill, never an alarm colour; the
neutral ReferenceRangeBadge is reused for the in/out-of-range verdict.
2026-06-16 21:06:08 +02:00
Marc-André Bombeck e9037a0d92 feat(labs): biomarker catalog with server-resolved reference ranges
Add a user-scoped Biomarker catalog so a marker is defined once — name,
unit, lower/upper bound, optional encrypted context, panel — and every
later reading just links it. New /api/biomarkers CRUD plus a structured
/api/labs create path: a biomarkerId resolves the unit + reference range
server-side, so the list and detail DTOs carry the resolved values and
the client never recomputes the range. Lab reads include the linked
marker and fall back to the legacy free-text fields for unlinked rows.

Add a lab-result restore endpoint for the delete-Undo affordance, a
shared serialiser, a static common-panel seed list, and tests for
catalog validation, structured-entry resolution, and the context codec.
2026-06-16 21:05:51 +02:00
Marc-André Bombeck 7b57c1d3d8 feat(vorsorge): resolve reminders on ingest, not only on the cron
Add a reminder-satisfy pg-boss queue (mirrors pr-detection) that every
measurement and lab ingest path enqueues — manual create, batch, and the
Withings / Polar / Oura / Nightscout / Fitbit / WHOOP syncs. A reminder
now auto-resolves the moment a matching reading lands instead of waiting
up to 15 minutes. The 15-minute cron stays as the idempotent safety net;
the forward-only guard lets the eventful hook and the cron converge
without double-stamping.

The worker honours the module toggle (a reminder for a disabled module
produces no engine activity) and runs the same satisfaction primitives
the cron uses.
2026-06-16 20:58:56 +02:00
Marc-André Bombeck f92247760c feat(vorsorge): one satisfaction primitive + lab auto-resolve
Extract a single satisfyReminder() that every path uses to mark a
reminder's cadence fulfilled: stamp lastSatisfiedAt (forward-only) and
recompute the server-authoritative nextDueAt. The cron auto-resolve and
the manual satisfy route now route through it instead of duplicating the
reschedule.

Add findSatisfyingEvent(): a typed reminder resolves from a matching
Measurement, a free-text reminder from any LabResult. This closes the
Lab<->Vorsorge loop — an annual-blood-panel reminder is now satisfied
when a lab result lands, not only on a manual check-off.
2026-06-16 20:58:46 +02:00
Marc-André Bombeck f0614a8080 fix(a11y): raise inactive tab-trigger contrast on dark theme
The inactive tab text used --muted-foreground (#9aa3b3) on the muted
(#44475a) tab strip, reading 3.6:1 — below the WCAG AA 4.5:1 floor that
axe flags on /settings/integrations. Add a --tab-inactive token (#b3bccd
dark, 4.79:1 on the strip; light mirrors the existing 4.58:1 muted tone)
and point the dark-mode inactive trigger color at it. The active trigger
keeps --foreground.
2026-06-16 16:51:56 +02:00
Marc-André Bombeck 3f3b8cbf0c fix(ui): unify the add-entry pattern across feature pages and add the Vorsorge heading 2026-06-16 02:55:27 +02:00
Marc-André Bombeck 3be34ceb11 merge(reconcile): b2-insightsai audit findings 2026-06-16 02:46:34 +02:00
Marc-André Bombeck 261f3cea1d test(insights): walk the insights AI tree in the module-route gate inventory
Add `src/app/api/insights` to the inventory's walked trees so every
`route.ts` beneath it must classify: the 14 AI-narrative routes as
insights-module-gated, the Coach sub-tree as coach-delegated, and the
AI-settings / provider-chain / tile-layout / feedback / targets /
glp1-timeline routes as explicit infra-and-config EXEMPT entries with
documented reasons. A future ungated insights AI route now fails the
inventory BY NAME rather than leaking the surface over a Bearer token when
the account turned insights off.

Add representative insights-off → 403 module.disabled coverage to the
blood-pressure-status and correlations route tests (alongside the existing
insights-on → 200 path).
2026-06-16 02:45:23 +02:00
Marc-André Bombeck 4cf923647d feat(insights): gate every AI-narrative insights route on the insights module
Require the `insights` module (in addition to the existing assistant-surface
gate and any per-metric data-module gate) on all 14 narrative insights
routes: blood-pressure / bmi / pulse / weight / medication-compliance /
mood / metric / derived / derived-batch status, cards, correlations,
narrative, pregenerate, and rhythm-events. A 403 module.disabled envelope
now precedes any provider or cache work when the account has insights off.

The per-metric sleep/glucose/recovery/mood gates and the
requireAssistantSurface("insightStatus"|"correlations") checks are kept;
this adds the module layer on top of them.

Route tests that now resolve the gate mock @/lib/modules/gate
default-enabled so their existing data-shape and assistant-flag assertions
ride through unchanged.
2026-06-16 02:40:38 +02:00
Marc-André Bombeck f73bc32a72 merge(reconcile): b5-achievements audit findings 2026-06-16 02:20:45 +02:00
Marc-André Bombeck 95f4b5a263 merge(reconcile): b3-doctorreport audit findings 2026-06-16 02:20:45 +02:00
Marc-André Bombeck 63901edea7 refactor(doctor-report): remove the orphaned legacy doctor-report surface
The DoctorReportDialog + its /api/doctor-report, /pdf and /availability routes
were referenced only by their own tests — the health-record (Gesundheitsakte)
export is the live surface. Remove the dead dialog, routes, their tests, and
the now-stale docs page; the shared doctor-report-data aggregator stays (the
health-record export uses it). The route-gate inventory exempt list is updated
accordingly.
2026-06-16 02:16:38 +02:00
Marc-André Bombeck 629e5396c6 test(modules): move gated workout read routes off the exempt inventory
The workout list + detail reads now carry requireModuleEnabled, so the
route-gate inventory's exempt allowlist must stop claiming them as
ungated data-layer surfaces. Only the iOS batch ingest stays exempt.
2026-06-16 02:07:34 +02:00
Marc-André Bombeck f966ae884a fix(dashboard): gate the workouts + recovery surfaces by module
The dashboard's recent-workouts and steps tiles painted regardless of
the workouts module state; AND the resolved per-account flag into both
gates (fail-open — only an explicit false hides the tile).

Extend the snapshot module maps to cover the recovery domain: the
cardioRecovery / sixMinuteWalk / stairAscentSpeed / stairDescentSpeed
widgets and their HealthKit measurement types now resolve to the
recovery module, and the per-night breathing-disturbance widget + type
resolve to sleep. A new guard test asserts every toggleable-domain
widget id carries a mapping and every mapped value is a real module
key, so a future device widget cannot leak onto a disabled account.
2026-06-16 02:05:37 +02:00
Marc-André Bombeck a524cad4c4 fix(insights): guard the module sub-pages against direct-URL hits
The sleep, blood-glucose, workouts, and recovery insights pages
fetched their series and half-rendered when the backing module was
disabled — a disabled account that navigated straight to the URL saw
the surface flash before any guard. They now resolve a shared
useModulePageGuard hook (mirroring the cycle page): an unauthenticated
visitor bounces to login, a disabled-module account bounces to
/insights, and the page renders a calm loader until the gate clears.
The backing /api routes enforce the gate server-side, so this is the
UX layer, not the security boundary.
2026-06-16 02:01:47 +02:00
Marc-André Bombeck f72d0b0ef3 fix(achievements): skip disabled-module badges and decouple unlock writes from the cached read
Gate badge evaluation on the per-user module map: mood badges no longer
evaluate, render, or persist while the mood module is off, and the same
holds for any metric that maps to a toggleable module (sleep). Core and
account-wide badges have no owning module and always pass through.

Move unlock persistence out of the cached builder. The builder now
returns the not-yet-persisted unlocks and the handler runs the
idempotent createMany after the cache read, so a cache hit can no longer
skip the write and a concurrent miss can no longer duplicate it.

Add the parity fields the iOS DTO dropped (category, points, target,
current, isHidden) so the native client groups and renders badges in
lock-step with the web surface.
2026-06-16 02:01:47 +02:00
Marc-André Bombeck b6db529dee feat(doctor-report): wire the doctorReport module toggle to the health-record surface
The doctorReport module toggle was inert: turning it off hid nothing and the
server-side export route accepted requests regardless. Gate the live surface
on the module — the Health-record settings entry now hides client-side when
the module is off, and POST /api/export/health-record refuses with a 403
module.disabled envelope, so the doctor-report surface cannot be reached over a
Bearer token either.
2026-06-16 02:00:48 +02:00
Marc-André Bombeck 6eef3ab21c fix(workouts): gate the workout read APIs behind the workouts module
GET /api/workouts and GET /api/workouts/{id} returned a disabled-
module account's workout list and detail to any valid Bearer or
cookie session. Both now require the workouts module and return the
403 module.disabled envelope when it is off.

The batch INGEST route (POST /api/workouts/batch) stays ungated by
design: synced workouts must keep landing in storage even while the
surface is hidden, so re-enabling the module reveals a complete
history rather than a gap.
2026-06-16 01:57:54 +02:00
Marc-André Bombeck 8cc879f6dc fix(analytics): gate glucose clinical panel behind the glucose module
The default /api/analytics slice served the per-context glucose
summaries and the TIR / GMI / eA1C clinical panel unconditionally,
leaking a disabled-glucose account's readings to a surface the
account has turned off. The route also serves core weight / BP /
pulse metrics, so a 403 on the whole payload is wrong.

Resolve the glucose module once server-side and null out
glucoseByContext + glucoseClinical when it is off; the core metrics
keep flowing. Mirrors the mood-pillar gate already in this route.
2026-06-16 01:55:03 +02:00
Marc-André Bombeck e86ba40e5b merge(reconcile): g2-dashboard audit findings 2026-06-16 01:49:20 +02:00
Marc-André Bombeck 372554bc11 merge(reconcile): g1d-coachroutes audit findings 2026-06-16 01:49:20 +02:00
Marc-André Bombeck 6a96accc51 merge(reconcile): g1a-cycleop audit findings 2026-06-16 01:49:20 +02:00
Marc-André Bombeck ca4952b2c8 fix(insights): gate the batch derived route on the module map
The batch derived endpoint (the one the dashboard wellness strip reads)
computed Recovery/Strain/Stress unconditionally while the single derived route
already gated them, so disabling the recovery module did not hide the rings.
Reuse the single route's DERIVED_MODULE map (now exported, with READINESS added
since it equals the computed recovery score) and drop disabled-module metrics
from the batch payload per request, without mutating the cached map.
2026-06-16 01:39:18 +02:00
Marc-André Bombeck 85b0b635f2 test(coach): cover the module gate on the chat and about-me routes
Per-route tests assert the two-layer Coach module gate: a disabled
module (operator availability OR per-user disableCoach) yields 403
module.disabled before any Coach work runs, and an enabled module falls
through to the normal path. Covers the chat POST + list GET, about-me
GET/PUT, the questions GET/DELETE, and the adopt POST short-circuit
ahead of the locked read-modify-write.
2026-06-16 01:28:28 +02:00
Marc-André Bombeck 064ed92071 fix(coach): enforce the module gate on the about-me routes
The structured self-context endpoints — about-me GET/PUT, the adopt
POST, and the clarifying-questions GET/DELETE — read and wrote Coach
self-context with no module check. Each handler now runs the two-layer
requireModuleEnabled(userId, "coach") gate right after auth, returning
403 module.disabled when the operator availability layer or the
per-user disableCoach opt-out has Coach turned off. The adopt route
test mocks the gate to stay focused on the locked read-modify-write.
2026-06-16 01:25:51 +02:00
Marc-André Bombeck 2b5974cf4a fix(coach): enforce the module gate on coach chat and list routes
The streaming POST and the conversation-list GET enforced only the
legacy operator assistant flag (requireAssistantSurface("coach")),
missing the per-user disableCoach opt-out and the operator
module-availability layer. Both handlers now run the two-layer
requireModuleEnabled(userId, "coach") gate right after auth, returning
403 module.disabled when the account has Coach turned off — even with a
valid Bearer token. The legacy assistant-flag check stays in place.
2026-06-16 01:24:20 +02:00
Marc-André Bombeck e974cdb5ff fix(cycle): gate the cycle ring tile and page redirect on the operator-aware module flag
The insights cycle ring tile and the /cycle page redirect gated on
user.cycleTrackingEnabled, which /api/auth/me resolves without the
operator availability layer — so with the Cycle module disabled
server-wide the ring still rendered (and fired its calendar read) and a
direct /cycle URL still loaded.

Read user.modules.cycle instead, which the /me map already AND-s with the
operator switch, so an operator-off instance suppresses both surfaces.
2026-06-16 01:19:54 +02:00
Marc-André Bombeck b5506bfc73 test(analytics): stub the module gate now that the health-score path resolves it
The health-score fast path resolves the per-user module map to drop the mood
pillar when the mood module is off; the analytics route exercises that path, so
its tests must stub the gate (default all-enabled) instead of hitting the
DB-backed resolver.
2026-06-16 01:06:54 +02:00
Marc-André Bombeck 2425832f47 merge(reconcile): r4-logic audit findings 2026-06-16 00:59:31 +02:00