The admin panel offered a toggle for the Health-Score explainer and
described it as the ? next to the delta line. That ? had already been
taken off the insights surface a release earlier, and the caption that
replaced it went away entirely when the health score was replaced by
the reference composite. Since then an operator could turn the switch
off, get a saved confirmation, and change absolutely nothing.
The toggle, its wording in all six languages, its place in the flag
matrix the app and the iOS client both read, and its column on
app_settings are all gone. Nothing is left to promise a control that
does not exist.
A guard goes in with it: every remaining assistant sub-flag has to be
read by some surface, and the panel and the plumbing that carry all of
them by construction do not count as proof.
Five layers, because the defect had five places to hide.
The catalogue guard checks what the compiler cannot state: every leaf resolves
a label a human can read, every group resolves one, the fenced tier stays last,
and the shipped template intersects the fence at nothing.
The selection guard pins the three minting functions against each other. A
stored blob this build cannot read resolves to the empty selection, never to a
default set — that is what makes the revoked legacy links revocable rather than
silently re-scoped. A request naming an id the server does not know is refused
by name rather than dropped, because dropping it would narrow a scope the
caller did express.
The egress guard freezes the set of aggregator callers with the source of each
one's selection written down. The required brand-typed parameter already stops
a new path compiling without an answer; this stops one being added without a
human writing the answer down, which is the moment a reviewer asks the
question.
The behavioural sweep aggregates every leaf in and out against a fixture that
carries a row for each, and asserts the payload differs. It is parametrised
over the catalogue, so a leaf added later is covered the day it is added. Its
positive control asserts the all-on payload is genuinely populated first, so a
"no difference" result means the gate is inert and not that the account is
empty. Beside it: an unmapped measurement type is excluded rather than passed
through, no table behind an unchosen leaf is queried at all, and an empty
selection produces an empty record.
The artefact layer runs the real route and reads what comes out — the FHIR
bundle, the PDF text and the zip's contents — with a positive control beside
every exclusion.
Mutation-checked: making the gate admit everything reddens the sweep for every
leaf; passing an unmapped type through reddens the fail-closed case; dropping
the mood query's condition reddens the zero-read case; returning the BMI
unconditionally reddens the sweep naming BODY_MASS_INDEX; adding a fenced leaf
to the template reddens the purity guard; removing the panel's seeding block
reddens the first-run and repeat-run cases; dropping the insurance refusal
reddens the share-link case; adding a caller outside the frozen list reddens
the egress guard naming the file.
The panel suite grew past the practice name it was written for and is renamed
for what it now covers: the three states, the fenced tier having no group
control, and the scope line naming its sensitive inclusions.
Integration coverage moves to the leaf model, including the case this release
exists for: an account with data in every domain and a caller that named
nothing gets an empty record.
The OpenAPI contract catches up with the routes — the required selection, the
refused keys, the share summary's new fields, the moved `Patient/$everything`,
the Coverage search and the five typed reads.
The doctor-report contract has always described the practice / clinic name as
a stored preference, and `/api/auth/me` has returned
`User.lastReportPracticeName` since it shipped, but nothing ever wrote the
column. Every report asked for the clinic name again.
The export route now persists the sanitised name after the requested artefact
has actually been produced, so a generation that throws cannot overwrite the
remembered value with a half-typed one. Clearing the field persists null, which
keeps the cover line cleared next time. The export panel seeds its input from
the auth payload during render, gated per user id, so a late resolve never
clobbers a field the user is already typing into, and a finished export
invalidates the cached payload so a remount reads the new value.
Add interleaved-writer, tokenless-compat, and malformed-token coverage for the
notification-prefs, modules, coach-prefs, and mood-tag-layout write guards,
including the notification-prefs case where a stale web write must not revert
the app-managed medication flag. Pin the inventory PATCH and DELETE routing
through the advisory lock, and cover the insight-generation privacy re-check on
both the main commit and the reroll arm.
The Modules hub listed Coach and Cycle as read-only "manage in X" deep-links
because their enabled-state is owned outside `modulePreferencesJson` (Coach in
Coach settings, Cycle in the Account card). Give both a real switch like every
other module, driving the SAME canonical state instead of a second copy:
- Coach → `PATCH /api/auth/me/disable-coach` (`User.disableCoach`, inverted);
the switch reads `!disableCoach`.
- Cycle → `PATCH /api/auth/me/cycle-prefs` (`cycleTrackingEnabled`); the switch
reads the resolved per-user gate.
Precedence stays honest. A module the operator turned off server-wide can never
be re-enabled by a per-user toggle, so the switch renders disabled with a
"disabled server-wide" hint. For Coach the operator layer is BOTH the
module-availability blob AND the assistant master flag (`flags.coach`); for
Cycle it is the module-availability blob. Each delegated row keeps a small
"manage" deep-link beside its switch, since the canonical surface carries more
than on/off (Coach cadence/memory; cycle goal, predictions, lengths).
Flipping either switch invalidates `authMe()` so every gated surface re-gates
live; the cycle flip also evicts its own `cyclePrefs`/`cycle` reads, matching
the canonical cycle card. Reuses the existing endpoints — no new route, no
OpenAPI change.
The inbound-documents surface is withdrawn from this release and returns
later with more depth. Park its module key (`inboundDocuments`) in a new
`CODE_DISABLED_MODULE_KEYS` list so it is hard-off for every account ahead
of both the operator-availability and the per-user opt-in layer.
The gate short-circuits a code-disabled key to disabled, so the resolved
module map reports it off everywhere: the nav entry stays born-gated and
hidden, the `/documents` page redirects, and every `/api/documents/inbound/*`
route returns the 403 module-disabled envelope. The Modules hub and the
operator-availability panel drop the row, and the per-user PATCH surface
refuses to write the key (a clean 422 instead of an inert preference).
The whole implementation stays intact — routes, the inbound-documents view,
the registry entry with its opt-in marker, the validation schemas, and the
migration are all preserved. Re-enabling the module is a one-line removal
from the list once the rebuild lands.
Regenerate the OpenAPI contract: the module-preferences PATCH body no
longer advertises the parked key.
Accept dateFormat on the profile PATCH field-by-field, echo it from
the profile + /me reads, mirror it into the client auth user, and add
the field to the OpenAPI request/response schemas.
Introduce a shared SettingsSectionFrame (the settings twin of the admin
renderer's SectionFrame) that emits the canonical visible heading +
subheading, and route all 19 settings sections through it. Every settings
page now shows a real h1 + subtitle in the reference Übersicht style
instead of an invisible sr-only title; the section bodies drop their own
headings and off-style intro paragraphs (the explanatory copy folds into
the frame subtitle).
- Route the admin About page through SectionFrame so it carries the same
visible heading as every other admin section.
- Rename the AI settings section from KI-Auswertung to KI-Anbieter across
six locales: the page configures the provider and key, not an analysis.
- Add a heading-block spacer to the settings and admin sidebars so the
first menu item lines up with the top of the first card.
- Remove the always-on core-domains card from Module: a tracking area that
can't be turned off doesn't need listing.
- Move the proactive-coach control out of Benachrichtigungen into Coach.
- Move admin module availability out of the Coach section into its own
page: it gates every module, not just the coach.
- Give the Layout Insights editor a proper Insights heading.
Adds settings.sections.<slug>.subtitle and the new admin keys across all
six locales; removes the now-unused module core-domain strings.
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.
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.
Render every toggleable and core switch, confirm a disabled module reads
unchecked while an absent key stays default-on, assert the core domains
are locked, and verify a toggle PATCHes only the flipped key as a
disabled-allowlist entry and invalidates the auth/me query.