Settings sections and Layout groups no longer declare their own module
keys; they ask the surface map. The Insights overview arrange list marks
the breathing, labs changes and cycle blocks unavailable when their
modules are off, as the overview itself would not show them. The rows
stay in the list so a save keeps each block's place.
Move the rule that decides which Settings sections a shared record
lists out of the settings shell and into the record-settings
classification module, next to the inventory it reads. The shell
filters through it unchanged; the navigation reads the same answer
in the next change, so the two cannot disagree about whether a record
has Settings pages and where they start.
Refs #939
The two consoles used the same primitives and still read as two systems,
because everything without a primitive had drifted: the header-to-body
step existed in three competing techniques at once (`mt-4` on the first
body child, `space-y-4` on the card, `mb-4` on the header), the body
started either at the card edge or 28 px in, and both shells hand-rolled
the page title instead of calling `PageHeader`.
Move the decisions into the primitives so a call site cannot re-open them:
- `SettingsCard` is gap-based like `Card` (`flex flex-col gap-4`). The
header-to-body step is the container's, not a margin somebody remembers.
A dense card still overrides the gap on the card itself.
- The body starts at the card edge. `pl-7` is gone — off the spacing scale,
and it mis-aligned action rows against their own forms wherever two
neighbouring cards disagreed about it.
- The card header's description slot loses its `space-y-1`. That was the
licence call sites used to stack explainer paragraphs into a muted
`text-xs` slot; the slot holds one sentence.
- New `SettingsCardActions`: one action row, trailing edge, last thing in
the card. `align="start"` spells the one exception, a card whose only
actions are links out.
- New `StatTile` replaces four hand-rolled label/value boxes in Admin.
- `SectionHeading` moves from `insights/` to `ui/`. It is the heading above
a group of cards, which is what Settings needs; Settings had been using a
card header with no card, which reads as a card that failed to paint.
Both shells now render their heading through `PageHeader`, which gained
`headingAs="div"` for the second copy each shell paints (mobile above the
nav strip, desktop inside the grid) so only one is a real `<h1>`. The
page explainer is muted everywhere: the primitive said foreground, the
standards and every hand-rolled copy said muted, so pages disagreed with
each other. Medications, Vorsorge, the labs / custom-metric / illness
detail pages and the clinician view drop their hand-rolled title blocks
too, including the mid-word truncation on the medications subtitle.
Tests pin the parts that can be checked exactly: the card's gap, the
description slot's missing rhythm, the action row's two alignments, the
`<h1>`-free shells, and a sweep asserting `pl-7` stays gone.
The privacy dashboard re-implemented the audit-retention resolver with its own
default and its own floor. Two implementations of one disclosure, quoted to the
same person on two pages and by the purge job; it uses the shared resolver now.
The record-content branch in the settings gate matched a classification rather
than a page, so a second manage-writable destination would have rendered the
allergy and family-history managers under its own heading without anybody
choosing that. It resolves through a slug map, and the contract test fails if
the map and the classification disagree in either direction — a destination
cannot join the kind without somebody deciding what it renders.
The managed-profile nav filter assumed a guardian grant is always manage. It is
today, which is why the check was easy to leave out; an entry below that level
would have listed destinations the gate refuses.
Interpolation used a replacement string, and String.replace reads $&, $`, $',
$1 and $$ out of one. A display name containing $' rewrote the sentence around
it — and display names land in exactly the sentences that say who did what in
somebody's health record. A replacer function has no $ grammar at all, which
removes the class rather than escaping the members of it somebody thought of.
POST /api/allergies and POST /api/family-history have resolved
requireRecordAuth("manage", "profile") since the level shipped, while the only
form in the product that posts to either sat behind a Settings destination
classified unavailable. The write was admitted, audited and owner-scoped, and
nobody could reach it — the one-ended change this repository keeps
rediscovering, with the permission shipped and the surface left as a follow-up.
The destination now carries a classification that means what the routes mean:
reachable by everybody they already admit, which is a guardian inside the
profile they administer and an adult delegate holding manage. The four places
that decided visibility on the guardian-only literal were each updated
deliberately, and the shell that refused every settings path in an adult share
now admits exactly the destinations classified as record content. Record
configuration — modules, thresholds, notification routing — stays with the
owner at every level.
The page inside a record is its own component holding the two managers whose
routes answer for a delegate. Mounting the personal section would have put
three more forms on screen that refuse on submit. The family-history card says
whose statement the row is, which the route comment could not.
Both routes carried a long paragraph arguing the write was deliberately
unreachable. It is gone rather than left to send the next reader back to close
what this opens.
The integration coverage moves from the undeclared-mode refusal to a manage
contract: a manage delegate and a guardian each write and the row lands under
the owner with the delegate named as actor, while read and write delegates get
the insufficient-access refusal. Nothing exercised either route at manage
before.
Settings gains one section for one concept: who else can open this health
record, whose this account may open, and when they were opened. Split across
two pages it would be two half-answers.
An invitation confers nothing until the other person accepts it, and the card
says so above the field rather than after the submit — that is the part
somebody needs to know before they type a name. Refusals are named from the
stable errorCode, because "no account with that name" and "that person already
has access" are different situations with different next steps.
Ended grants stay in the list. The row is the consent record, and "who had
access, from when to when, and who ended it" is the question the panel exists
to answer; a list that dropped them could not. Revoking asks once and then does
it: reducing access must never be harder than granting it was.
`GET /api/account/activity` answers the owner's other question. One row per
person per day with a count, not one per request — a hundred identical lines
is a way of answering nothing. The window is published with the rows rather
than assumed: audit history is purged on a schedule the operator sets, so the
view prints the number the server resolved from the same setting the purge job
reads. An instance running ninety days says ninety.
Six locales ship with it.
A dedicated settings page lists every scorable pillar grouped by the area
of health it speaks to, so the fact that blood pressure, glycaemia and
lipids are three pillars of one area is visible rather than buried. Each
row shows the three things that were never separable before: whether the
pillar is being recorded, whether it is shown, and whether it counts
toward the score. Only the last is writable here, and the page says so.
A pillar whose tracking is switched off is not listed at all, because it
is not waiting for anything. A pillar this build cannot score yet is
listed, disabled, and says why. Wellbeing under safety signposting is
named as safety guidance and never as a mistake in the selection. The
refusal for a selection too narrow to produce a score comes from the
server and is shown in the reader's own language.
The score's foot disclosure gains one link into the page.
The insights tab strip's "Cardiovascular" popover held only three rare
Withings markers while pulse, resting heart rate, HRV, and cardio
recovery sat scattered across a flat pill and the unrelated Vitals
group. Cardiovascular is now Heart and gathers all six; bmi and
workouts — flat only by historical accident — collapse into their
correct Body / Activity groups, and breathing disturbances moves out
of Vitals into the Sleep manager bucket instead of vanishing whenever
the sleep module is off. Blood pressure, weight, sleep, mood, and
medications stay pinned as flat top-level pills — now a documented
`PINNED_SUB_PAGE_SLUGS` set instead of an unexplained "predates
v1.4.32" carve-out. Active energy moves from Vitals to Activity to
match its own tab-strip section comment. All changes are grouping /
presentation data only — no slug, route, or redirect changed.
The Settings sidebar's 19 entries rendered as one flat list with no
visual structure. They now cluster under seven group headers (Account,
Your data, Display, Health profile, AI, Connectivity, System),
reordered to match; the desktop sidebar gets a non-interactive text
header per group, the mobile chip strip a quiet divider. Presentation
only — no route changed.
Reference: 2026-07-17 UX/IA audit, H2 + M1 + M2 + M3 + M6.
SettingsShell's content column carried pb-24 md:pb-0 that double-counted
the bottom-nav clearance AuthShell's <main> already reserves, stacking
~96 px of dead scroll below the last card on mobile. AdminShell still
carried the pre-#154 min-h-[calc(100dvh-12rem)] column reserve on <main>,
over-reserving on short sections at every breakpoint.
Delete the redundant SettingsShell gutter, move AdminShell's floor off
<main> onto its grid, and factor the budget-derived grid floor into one
shared SUB_SHELL_GRID_FLOOR constant so the two shells can never drift.
Extend the guard to /admin/* and measure against each element's
content-box bottom so a redundant sub-shell pb-* now fails instead of
inflating the allowed budget.
A short settings section over-scrolled into a dark empty band below the
last card: the content column's min-height reserve was tuned without the
AuthShell wrapper's pt-6/pb-20 in the budget, so it pushed the column past
the viewport. Move the floor onto the grid (top bar + wrapper padding =
10.5rem) and make the content column the grid's 1fr row, so it fills the
viewport exactly on a short section — no over-scroll — and grows normally
on a long one. The sticky sidebar and the stable scrollbar gutter are
unchanged, so switching sections stays width-stable.
Appearance ("Darstellung") stacked every module's view and ordering
settings on one long page. Split it into a hub that lists the modules as
navigable rows, each opening its own /settings/layout/<module> subpage
with a back link — the per-module settings move verbatim, no restyling.
The module routes now redirect to those subpages instead of an in-page
anchor, and the in-app cogs link straight to them.
NotificationStatusCard read useAuth().isAuthenticated, which is false
during SSR and true on the client, so the card rendered null on the
server and a loading card on first client paint — a tree-shape change
React reported as a hydration mismatch on every authenticated load of
the integrations page, regenerating the subtree and dropping the
channel controls' handlers.
Gate the card behind the shared useMounted() hydration probe so the
server and the first client paint agree (both render null); the real
status loads as an ordinary post-hydration update. Fold the settings
shell's own inline copy of the probe onto the same hook.
The settings section navigation filtered its entries by the resolved
`user.modules` map directly in render. That map resolves at different
points on the server-rendered pass and the hydrating client pass, so the
two renders emitted a different number of nav entries — a hydration
mismatch that made React discard and regenerate the tree, leaving every
control on the page non-interactive (a disclosure that wouldn't open, an
export button that did nothing).
Emit the full fail-open list on both the server pass and the first client
paint (gated behind a `useSyncExternalStore` hydration probe), then apply
the module filter once, after hydration. Server and first client render
now always match, so the nav — and the rest of the settings page — stays
interactive.
The authenticated shell's <main> scroll container already reserves the
scrollbar gutter; the settings content column is not a scroll container, so a
gutter rule on it has no effect. Remove it (and its misleading comment) rather
than imply a fix that does nothing.
Collapse the duplicated settings navigation so each surface lives in
one place:
- Appearance ("Darstellung") stops being a link hub and renders the
dashboard, insights, and every tracking module's settings inline as
stacked, anchored sections. Each module section keeps its module
gate, so a disabled module's section does not render. The standalone
Medikamente / Stimmung / Labor / Krankheit / Vorsorge nav entries are
gone; their routes 301-redirect to /settings/layout#<anchor>.
- Move the delivery channels out of Notifications into Integrations,
where the other connected services live. Notifications keeps only the
reminder types. /settings/channels now redirects to
/settings/integrations#channels; the per-channel anchors still resolve.
- Fold the clinician share-link surface into the health-record section
as a labelled Sharing group. /settings/sharing redirects to
/settings/gesundheitsakte#sharing.
- Drop the duplicated active-sessions and security-activity cards from
Account; they remain under Data & Privacy.
- Reserve the scrollbar gutter on the settings content column so a
section with a vertical scrollbar and one without render at the same
width.
Move-only: every card renders through its existing component unchanged.
Two surgical, additive, reversible changes to the flat settings navigation.
Notifications becomes one surface with two sequenced, labelled groups:
Reminders ("what you receive") first, then Delivery channels ("where it
lands") = the existing channels panel, unchanged. The standalone Channels
sidebar entry and slug are dropped; /settings/channels 301-redirects to
/settings/notifications#channels (mirroring the reminders redirect). The
per-channel anchors keep resolving.
The mis-titled "Dashboard" area (slug kept as layout, so no route churn) is
renamed Appearance / Darstellung and widened from two links to six: dashboard
arrangement, insights overview and pills, plus deep-links to the view/sort/
order cards of medications, labs, the illness journal, and checkups. Each
module keeps its view cards on its own page; the hub only indexes them. The
checkups view card gains a vorsorge-view anchor so the deep-link lands.
i18n updated across all six locales; settings and notifications tests track
the new structure.
Revert the nine-group consolidation and the card relocations back to the
flat sidebar: sessions, trusted devices, and login activity return to
Privacy, and account deletion returns inline under Advanced. Drop the
/settings/layout redirect and the now-unused group and sign-in cross-link
i18n keys across all six locales.
Keep the collapse-by-default sessions and activity cards, the medical-history
cross-link from "About me", and the chronic-conditions consolidation.
Follow-ups on the v1.25.1 settings group consolidation.
- Sign-in security is no longer split across two top-level groups. Active
sessions, trusted devices, and the login-activity feed move into Account
> Security alongside the second-factor and passkey controls, so every
"who can sign in as me" control sits in one place. The Data & Privacy
pane keeps the data / retention / encryption story and cross-links to
the security home. Sessions and activity are no longer duplicated on the
Profile tab.
- First-child sub-tabs no longer repeat their group name: the section
titles become "Profile & preferences", "Connections", and
"Privacy & retention" across all six locales.
- Account deletion moves out of Advanced into the Account group as a
standalone card; research mode and the data reset stay in Advanced.
- Disambiguate the Spanish labels for the health record vs medical
history (they both read "Historial médico"); medical history becomes
"Antecedentes médicos".
- The section heading carries its id on a single breakpoint instance to
clear a duplicate-id-aria violation; the focus hook keeps targeting
both instances and picks the visible one.
- The in-page sub-tab strip scrolls its active tab into view on a narrow
viewport, matching the group strip.
- Redirect /settings/layout to the Display landing now that nothing links
to the old personalisation hub.
- Note in code that delivery channels deliberately stay under
Integrations (a channel is a delivery provider).
Login only wiped the in-memory query cache; the IndexedDB query snapshot
and the service-worker data/page caches from the previous account stayed
on disk. Logout and the 401/expiry redirect already drop all three. Run
the same offline-cache wipe on a successful login too, before the new
user's dashboard prefetch warms, so a fresh sign-in on a shared browser
inherits nothing from the prior session. The per-account restore guard
still stands; this removes the stale data at the source.
Settings shell: move focus to the section heading when the active section
changes within the mounted shell, so keyboard and screen-reader users are
placed in the new content instead of being left on the nav link they
activated. The first mount is skipped to avoid an unexpected focus move on
a deep link. Raise the inactive sub-tab text from muted-foreground to
foreground/70 so it clears the WCAG AA contrast floor over the tinted
sub-tab strip.
The settings left rail rendered one entry per section — 26 visible — which
read as a long flat list on desktop and a 26-chip scroll strip on mobile.
Group the sections into nine top-level entries and surface each group's
children as in-page sub-tabs, keeping the rail one level deep and the second
level inside the page.
Every section route stays live at its own URL. The rail is now driven by a
SETTINGS_GROUPS model; a reverse index maps each child slug onto its group so
deep links and the per-page settings links keep resolving and highlight the
right group, with the active child marked on the sub-tab strip. This
generalises the existing hidden-child highlight mechanism rather than moving
routes, so bookmarks and the contract surface are untouched.
Groups: Account (profile, sign-in), Tracking (areas, the per-domain pages,
targets), Display (dashboard, insights), Integrations (connections, channels,
source priority), Notifications, AI & Coach, API & Access, Data & Privacy
(export, health record, sharing, privacy, advanced), About. Per-domain
settings keep their view and data together inside one sub-tab — only the nav
groups, never a single concept.
Rename the Display group title away from "Dashboard" so it no longer collides
with its own Dashboard child. Add the nine group labels and the sub-section
nav label across all six locales. Drop the now-unused hub back-link.
Add the opt-in Environment module (default off), its Settings surface (home
location search, travel overrides, backfill, with Open-Meteo attribution), the
gated API routes (overview, home, geocode, travel CRUD, backfill), the nightly
pg-boss fetch job with a lookback window plus on-demand backfill, the query
keys, six-locale strings, and the regenerated OpenAPI. The module performs an
outbound weather fetch tied to a coarse location, so it ships dark and the user
turns it on deliberately; off means no job, no row, no egress.
Add a new "Medical history" (Anamnese) Settings section that hosts the two
structured-record managers — allergies and family history — each a
list + add/edit sheet + delete-with-confirm built on the shared query-key
factory and the ResponsiveSheet pattern. It sits between the illness journal
and the preventive-care reminders and is always shown (not module-gated, like
Vorsorge), since these are foundational health-profile records.
The section carries a plain non-diagnostic note ("your own records, not a
medical diagnosis"). Full i18n across all six locales (de/en/es/fr/it/pl).
Combine the remote MCP server (stdio + remote /mcp + OAuth 2.1 + read
tools + deep-value reads + a confirmed, audience-bound write surface)
with the v1.23 account-security line (MFA, known/trusted devices,
free-text note encryption).
- schema: keep every v1.23 User relation (MFA, known/trusted devices)
alongside the McpOAuthConnection relation; the MeasurementSource enum
carries the MCP value.
- api-handler: the MCP audience guard (a token scoped to
health:read/health:write is bound to /mcp and refused a REST write)
and the step-up MFA helpers coexist.
- maintenance queues: register the MCP token-cleanup sweep next to the
note-encryption backfill and key-rotation queues; shift it to 04:00
so the two daily discovery ticks do not collide.
- migrations: renumber the MCP migrations to 0206/0207 so they follow
the v1.23 chain (0199-0205).
- MCP mood writes route the note through the v1.23 at-rest cipher.
Bring the scattered data-sovereignty controls into one /settings/privacy pane.
It assembles already-shipped pieces rather than rebuilding them: the encryption-
at-rest summary and configured retention windows (read from the server so the
copy stays truthful), links to the export surfaces (including the passphrase-
encrypted option) and the account-deletion / data-reset surface, the active-
session list, trusted devices, and the security-activity log. The backup↔
deletion lag is disclosed honestly — deletion is immediate, but encrypted
off-host backups still hold the data until they roll off retention.
Introduce a dedicated /settings/security section that gathers every
account-protection control in one place: TOTP enrolment (QR plus manual
secret, confirm, recovery codes shown once with copy and download),
WebAuthn security keys as a second factor (register, list with last-used,
rename, step-up-gated removal), recovery-code count and regeneration, and
primary passkey management.
The second-factor WebAuthn path keeps its own credential store, separate
from primary passkeys, and uses non-resident keys so an account can hold
many. Relying-party resolution is shared with the passkey path. A status
endpoint backs the hub; the step-up gate now recognises a registered
security key as an active second factor so a WebAuthn-only account can
still reach the gated actions.
Standardise the labs marker subtitle to text-xs across both the compact list
and the card grid, and build the localized name->slug lookup map once per
locale instead of re-scanning the catalog seeds per group on every render.
Add relative to the mobile section strips in the settings and admin shells so
the active chip's offsetLeft is measured from the strip, giving an exact
horizontal scroll target.
Drop the orphaned insights.sleep.headlineCaption and headlineCaptionSuffix
keys from all six locales; nothing references them after the sleep headline
removal.
A dedicated Settings card co-locates the off-by-default MCP module toggle
with token management for connecting an external assistant:
- Mint / list / revoke a dedicated `health:read`-scoped token (shown once,
copy to clipboard) via new `/api/mcp/tokens` routes — never the
`medication:ingest` scope, never the wildcard.
- Surface the connector URL and the enable switch in one place, wired to
the existing module-preference endpoint.
- New section wired into the Settings shell next to API & Tokens, with
EN + DE strings (and the other locales) under `settings.mcp` and
`settings.sections.mcp`.
The desktop section nav in both shells used a single sticky wrapper at
top-20. Because the nav can run taller than the scroll viewport and the
top offset assumed a topbar that sits outside the scroll region, the
panel drifted with the content and re-pinned abruptly on scroll-up.
Cap the wrapper height to the viewport and let it scroll independently
(top-6, max-h-[calc(100dvh-5.5rem)], overflow-y-auto, no-scrollbar) so
the nav stays pinned. Covers every /settings/* and /admin/* page through
the shared shells.
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
Bring back the Settings → Sharing ("Freigabe") section: the owner mints
a time-boxed, revocable, read-only share link to their health record at
the public /c/[token] view. The backing model, the /api/share-links
lifecycle routes, and the public clinician view were left in place; this
restores the owner Settings surface and re-wires it.
Place the slug directly after gesundheitsakte and before export — sharing
the health record belongs next to the health-record export. The entry is
always available (no module gate), like account and export.
Render the card through the shared SettingsCard primitive so it matches
every sibling section; drop the section's own page title (the shell
supplies the heading and subtitle). Route the list, create, and revoke
calls through the api-fetch helper instead of raw fetch, and align the
touch targets with the mobile sizing contract.
Promote the three per-module customise surfaces from standalone
ModuleSettingsFrame pages to first-class Settings sections. They now
render inside the Settings shell — same left nav, heading, and chrome as
account / ai / integrations / notifications — instead of their own
back-button-and-title frame outside it.
Add labs, illness, and vorsorge to the slug registry (after mood) and to
SETTINGS_SECTIONS with FlaskConical / Thermometer / CalendarCheck icons.
labs and illness gate on their toggleable modules; vorsorge has no
module, so its entry is always shown. Three thin section components wrap
the existing LabsSettings / IllnessSettings / VorsorgeSettings content,
mirroring MedicationsSection / CoachSection.
Delete the three standalone /settings/{labs,vorsorge,illness} routes so
the wrench deep links resolve through the dynamic [section] route, and
drop ModuleSettingsFrame now that nothing renders it. Add the section
title / subtitle / description keys across all six locales and remove the
dead per-module *.settings.* blocks the old pages consumed.
The health-record section (PDF + FHIR R4 export) was the only entry-point
gated on the doctorReport module's nav toggle, so flipping that opt-out in
the Modules hub made the whole section vanish from Settings with no
in-context way back. Drop the nav gate: the entry-point now always shows.
The doctorReport module still governs the data layer — the server-side
/api/export/health-record route stays the hard enforcement (403
module.disabled over Bearer when opted out). Assert the entry shows for a
default, empty, and opted-out modules map.
The settings and admin shells reserved the page-heading height in the left
nav with a hidden replica block. The replica wrapped differently in the 220px
nav column than the real heading in the wide content column, so the first nav
item sat too low and drifted across locales.
Hoist the heading into the shell and place it in its own grid row spanning only
the content column: row 1 holds the heading, row 2 holds the nav and the cards.
The nav's first item and the first card now start the same grid row, so they
align by construction regardless of how the title or subtitle wraps. Drop the
spacer component.
Drop the overstated claim that the due-reminder green button mirrors the
medication card (the med take button is always the default tone), fix the
module-tour anchor attribute named in a doc comment, and feed the active
section title into the settings nav spacer so its heading line wraps the same
way a long localized title does.
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.
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.
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.
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.
Move the Coach preference cards (disable toggle, preferences, durable-fact
memory) out of the AI / Assistant section into their own Coach entry,
module-gated on the coach module. The AI entry keeps its name and owns
provider / model / BYOK configuration plus the "about me" context (which
the daily briefing reads too, so it is not Coach-only).
The proactive Coach-nudge card stays in Notifications: it is a reminder
TYPE, already coach-module-gated there alongside the other reminder types,
and belongs with the reminder family rather than the Coach preferences.
Promote the Medikamente section from a hidden Layout-hub child to its own
standalone nav entry (always shown — medications is a CORE domain) and
gather the injection-site exclusions card here from the account profile,
so every medication-specific preference lives on one screen. The Layout
hub keeps only the dashboard + insights arrangement editors.
The low-stock card stays whole in Notifications: its runway config and the
low-stock reminder toggle are one tightly-coupled control writing a single
prefs row, so splitting only the config out would break the card.
Promote the mood-tag management surface from a hidden Layout-hub child to
its own standalone settings nav entry, shown only when the mood module is
enabled. The shell now module-gates per-submodule entries off the resolved
`useAuth().user.modules` map (fail-open: a missing key reads as enabled).
The Layout hub keeps only genuine layout/personalisation editors.
Lift the full health-record export (PDF + FHIR R4 + zip package) out of
Export & Import into a dedicated top-level Gesundheitsakte section. Export
& Import keeps the generic CSV/JSON data-out and import paths only. The
new section is always available (not module-gated).
The standalone /settings/reminders page was a link-only hub that merely
deep-linked into the canonical editors — duplicating the Notifications
entry. Drop the section, its slug, and its placeholder/route wiring, and
301-redirect /settings/reminders to /settings/notifications. Reminder
types consolidate onto the Notifications home in a follow-up.
Source priority decides which connection wins when two report the same
metric, so it belongs with the integrations rather than as a standalone
settings page. Sources renders as the third Integrations sub-tab
(Connections / Channels / Sources); the standalone `sources` slug is
removed from the shell nav, slug registry, and route dispatch, and
`/settings/sources` 301-redirects to `/settings/integrations`.
Surface a dedicated settings section for enabling and disabling the
secondary tracking domains (mood, sleep, glucose, workouts, recovery,
labs, achievements, coach, insights, doctor report, cycle). Each row
carries an icon, label, and one-line description with a switch bound to
the resolved module map on /auth/me; toggling PATCHes /api/auth/me/modules
and invalidates the auth/me query so the navigation, Insights pills, and
dashboard tiles re-gate live. Core domains (weight, blood pressure, pulse,
medications) render as a read-only always-on group. The entry sits right
below Account in the settings navigation.