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).
The Module hub rendered live switches for the delegated keys cycle and coach,
but the gate resolves those from their own sources (cycle from CycleProfile,
coach from the operator flag + disableCoach), so a toggle wrote an inert entry
to modulePreferencesJson, snapped back, and still fired a success toast. Render
delegated modules as read-only rows that deep-link to their real control, strip
delegated keys from the modules PATCH schema so no inert value is stored, drop
the duplicate cycle control, and surface an operator-disabled state with the
previously orphaned hint string.
Second layer of the module enable/disable model: an operator can turn a
module off for the whole server, and an operator-disabled module is off
for every account regardless of personal preference — mirroring the
coach master flag above the per-user opt-out.
Storage is a DISABLED allowlist on AppSettings.moduleAvailabilityJson
(absent/empty/true = available, explicit false = disabled), resolved by
getOperatorModuleAvailability(). The gate now resolves every toggleable
module as operatorAvailable(key) AND userEnabled(key); cycle and coach
keep their existing delegation but are still AND-ed with operator
availability. Core domains are never disableable.
GET/PATCH /api/admin/settings/module-availability is cookie-only
(requireAdmin); a strict per-module-key schema rejects core domains and
unknown keys at the boundary.
registry.ts is the single source of truth for the toggleable secondary
domains (cycle, mood, sleep, glucose, workouts, recovery, labs,
achievements, coach, insights, doctorReport) plus the always-on core
domain denylist. Each entry carries a stable key, label/description i18n
keys, a category, and an optional delegatesTo marker.
gate.ts is the one enforcement point: isModuleEnabled / requireModuleEnabled
(403 module.disabled envelope) plus resolveModuleMap for the /api/auth/me
projection. No double source of truth — cycle delegates to isCycleEnabled
and coach delegates to disableCoach AND the operator assistant master
flag; every other module resolves from the disabled allowlist. Default-on,
fail-open per key, per-request memoised reads.
insights gates only the AI-analysis surfaces (briefing, status cards,
correlation narration, score explainer) — never the raw measurement
charts, which are core and render regardless.