The wheel tile, phase-education card, and dashboard cycle tile each set
both data-revealed and .wellness-tile-rise on the same node. The keyframe
selector is [data-revealed="true"] .wellness-tile-rise (descendant
combinator), so it never matched a same-node pair — the tiles stayed at
the rise rule's opacity: 0, invisible but still boxed. Lift data-revealed
onto a contents wrapper (the documented pattern from the cycle insight
summary card) so the entrance plays and the signature ring paints.
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.
The settings dashboard-layout client component imported WIDGET_MODULE_BY_ID
from @/lib/dashboard/snapshot, which transitively pulls the server snapshot
builder -> lib/db -> @prisma/adapter-pg -> pg -> node:dns. Turbopack tried to
bundle pg for the browser and failed (Module not found: Can't resolve 'dns'),
breaking next build and therefore e2e and both Docker arch builds.
Move the two pure data maps (WIDGET_MODULE_BY_ID, SUMMARY_TYPE_MODULE) into
@/lib/dashboard/widget-modules, which imports only the ModuleKey and
MeasurementType types. snapshot.ts re-exports both for the server call sites
and the existing tests; the client component imports from the new module.
The v1.18.0 IA restructure reworded the mood + medications section descriptions;
update the SSR copy assertions to the new strings (the i18n parity guards stayed
green, but these per-section copy assertions pinned the old text).
A widget toggle whose owning module is disabled is a dead control: the
dashboard snapshot gates that tile and chart out server-side regardless
of the switch state, so the row in Settings just confuses. Read the
resolved per-user module map from the session and drop each widget row
whose owning module is off, mapping widget id to module key through the
shared WIDGET_MODULE_BY_ID. Core widgets (no module entry) and any row
whose module key is unset stay visible; the gate fails open when the map
is absent.
Mark the top-level `/insights` nav destination `requiresModule: "insights"`
so it drops from both bars when the account has the insights module off,
mirroring mood / labs / coach / achievements. The bottom-nav pinned
`/insights` as a fixed primary slot regardless of the module map; it now
filters that slot through the same per-user map the More hub honours
(fail-open on a missing key / unloaded /me payload). A disabled insights
module is hidden everywhere — the primary-slot filter keeps it out of the
More hub too, so it is hidden, not relocated.
Extend the nav-model + bottom-nav tests with insights on/off coverage.
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.
The dashboard tour launcher now derives includeAchievements from the
per-user module map (modules?.achievements !== false), so an account
that has the achievements module turned off no longer walks the user to
a page it has hidden.
Mention the module on/off concept in the integrations tour stop, where
the tour introduces Settings and customization, so a first-time user
learns whole areas can be turned on or off.
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.
The compliance number + card already render neutral (no grade tint); only a
stale comment still described a green take-window highlight the implementation
does not do. Reword it to state status drives a discreet line/glyph, never a
card background/border tint.
mood, medications, Vorsorge, the health-record export panel and the export
section hand-rolled their card headers; route them through the shared settings
card-header primitive so every settings card header is structurally identical.
Reference-range classification + low/high range rendering was implemented three
times (validations, PDF core, lab list). Extract a single classifier and a
structure-only range formatter that takes a per-surface number formatter, so
each surface keeps its own number formatting while sharing the canonical
classification. Behaviour is unchanged per surface and now covered by one test.
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.
The single-row delete on lab results and on preventive-care reminders
fired immediately on click, unlike the measurement and mood lists which
gate the same action behind a confirm dialog. Route both through the
shared DeleteButton so a stray tap can no longer drop a reading or a
reminder, and add the per-surface confirm copy across all locales.
The Personal Health Score weighted the 0.20 mood-stability pillar
unconditionally — the fast-path read mood entries regardless of the
account's module state, so a user who turned the mood module off was
still scored (and silently penalised or rewarded) by a pillar hidden
everywhere else in the product. The dashboard already blanks the mood
tile and the coach snapshot unions mood out; the score ring and the
insights card pillar list did not.
Thread a default-on `moodEnabled` flag through the fast-path. When the
caller resolves the mood module as disabled the helper skips the mood
read entirely and leaves the series empty, so `moodStability` returns
null and the existing proportional null-redistribution re-scales the
surviving pillars to sum to 100. The analytics route resolves the
module server-side and threads the flag; the insights card hides the
Mood row via a new SSR-safe `useModuleEnabled` accessor so it never
advertises a pillar the account turned off.
Golden fixtures pin mood-on vs mood-off to different, correctly
re-weighted scores and assert the disabled read is never issued.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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).
Consolidate every reminder TYPE onto the Notifications screen and drop the
redundant stacked breadcrumb for one clean heading. Each reminder-type card
is shown only when its module is enabled, read from the resolved
useAuth().user.modules map (mood card on mood, Coach nudge on coach); the
gate fails open so a stale /me payload never blanks a card. Low-stock maps
to the medications CORE domain and Vorsorge (preventive care) is not a
toggleable module, so both stay shown — the Vorsorge editor embeds in place.
The delivery channels keep a single concise pointer to Integrations rather
than a duplicated section. Reword the heading copy and drop the now-unused
settings.sections.reminders.* keys across all six locales.
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`.
Channels (Telegram, ntfy, Web Push, Webhook, Email) and the live
per-channel status surface are delivery providers, not notification
preferences, so they now sit under Integrations next to the connected
services. Integrations gains a Connections / Channels sub-tab split;
Notifications keeps the reminder-type content (mood, low-stock, coach
nudge) and cross-links to the relocated channels.
Add an operator-side toggle matrix to the admin assistant section that
flips each toggleable module on or off server-wide via PATCH
/api/admin/settings/module-availability. Mirrors the assistant-flags
matrix: one neutral toggle per module, optimistic flip with a revert on
failure, and an authMe invalidation so the resolved module map re-gates
the session immediately. New admin.modules.* labels across all six
locales; core domains never appear.
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.
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.
When an account turns the achievements module off the whole gamification
surface disappears. The route enforces the gate after auth and returns the
403 module.disabled envelope, so no badge evaluation, unlock persistence,
or payload is produced. The achievements page, the dashboard recent-unlocks
tile, and the unlock notifier read the resolved module map from
GET /api/auth/me and render nothing / disable their query when the module
is off. Default-on: an absent key reads as enabled, a stale /me payload
coerces to an empty map so no surface vanishes on a partial-deploy
rollback.
Generalise the per-destination cycle gate into a requiresModule marker
on each nav destination and read it from the account's resolved module
map (the modules field on GET /api/auth/me). Fold the bespoke cycle
boolean into requiresModule: "cycle" so there is one gating mechanism;
cycle and coach are delegated keys already reflected in the map, so
reading them here is not a re-derivation.
Tag mood, cycle, labs, coach and achievements with their module keys;
core destinations (measurements, medications, vorsorge, insights, the
dashboard) carry none and always render. The sidebar, the mobile More
hub, and the Insights tab strip all filter on the same map.
Insights pills: a metric pill belonging to a toggleable module (mood,
sleep, glucose, workouts) hides when its module is disabled, on top of
the existing data + saved-layout gates; the Recovery pill gates on the
recovery module. Overview and core-metric pills always show. Every gate
fails open — a missing key or an absent map keeps the entry, matching
the gate's default-on contract, so a stale /me payload never blanks the
nav.
Workouts already surfaces as an Insights tab-strip pill (the workouts
slug, gated on a logged workout row), so a parallel left-nav home was
redundant. Removing it from NAV_DESTINATIONS lets the Insights nav home
read active on /insights/workouts like any other /insights/* sub-route.
Recovery is a composite wearable surface (WHOOP / Polar / Oura recovery,
strain, ANS charge, cardio load), not a top-level tracking destination.
It moves off the left nav and onto the Insights tab strip as a pill at
/insights/recovery. The pill is always present (like Overview) since the
page self-gates every block and falls back to a calm empty note rather
than carrying its own availability signal. Adds the insights.navRecovery
label across all six locales.
The Coach lived under /insights/coach, rendering inside the Insights
layout's tab strip. It is the product's headline surface, not a metric
sub-page, so it now lives at the top-level /coach in the standard page
chrome without the Insights tab strip. The nav model points at /coach,
every in-app link and the proactive-nudge deep link follow, and a 301
redirect keeps /insights/coach (bookmarks, cached PWA navigation, older
push payloads) resolving.
The mobile "More" overflow sheet renders a two-column grid of icon-plus-label
tiles. The label span carried min-w-0 but no break handling, so a long
single-word label such as the German "Benachrichtigungen" — which has no
natural break opportunity — pushed past its grid track and overflowed the
tile box.
Add break-words to the label so an unbroken long word breaks inside the tile;
combined with the existing min-w-0 shrink and the auto-height tile (min-h-14),
the second line is absorbed cleanly. The fix is layout-level, so every label
in all six locales fits without special-casing the longest one.
The WHOOP and Fitbit OAuth callbacks redirect back with
`?whoop=connected|error` / `?fitbit=connected|error`, but the settings page
only read that param for Withings, Polar and Oura — a user returning from a
WHOOP or Fitbit round-trip landed on a silently unchanged page with no toast.
Extend the generic return handler to all four OAuth providers, add the
matching connected/failed/error i18n keys across every locale, and invalidate
both the per-card key and the consolidated envelope on a successful return.
Polar and Oura already write the shared IntegrationKey ledger but each fired
its own `/api/<provider>/status` round-trip from the card, so the page issued
one consolidated request plus three per-card ones. Fold Polar and Oura into
`/api/integrations/status` and let the cards read a passed view-model instead
of fetching, retiring the extra round-trips. The per-provider status routes
stay for the iOS and test callers.
The VAPID regenerate overwrite guard used a native window.confirm for a
subscription-invalidating destructive action, where the rest of the
release uses the project's AlertDialog. Drive the overwrite confirm
through an AlertDialog (calm in-app dialog, destructive action variant)
and retry with force from its action.
The /labs header subtitle used truncate, clipping mid-word in longer
locales on a 375px viewport. Drop truncate so it wraps; the add button
stays shrink-0 and the row aligns to the top.
The four Layout personalization editors (dashboard, insights, medications,
mood) are reached through the Layout & Personalization hub or a page-header
cog, and the left rail highlights the hub while the body shows the child.
The only return path was tapping the highlighted rail entry — no explicit
up-affordance, sharper on mobile where the rail collapses to a chip strip.
Add a shared back-to-hub link at the top of each child so the hub → child
→ hub loop reads clearly.
Also fix the stale doc-comment on the settings section route: the slug set
is 16 (12 nav-visible plus the four routable-but-hidden Layout children),
not eight.
Vorsorge (preventive-care) was a top-level tracking surface reachable only
three taps deep through Settings → Reminders, while peer surfaces (Labs,
Recovery) already had a top-level nav home. Add it to the one shared
destination model in the clinical spine, so it renders on the desktop
sidebar and the mobile More hub like every other feature, with a
nav.vorsorge key across all six locales.
Bug Report, Settings and Notifications were still a second hand-curated
list on each bar — the exact drift the one-model contract set out to kill,
just pushed down a level. Promote them to a shared NAV_UTILITY_DESTINATIONS
list both bars consume: the sidebar footer + avatar menu and the bottom-nav
More-hub tail now read from one source and can no longer disagree. The
More hub itself is now computed by mobileMoreHubDestinations() so the
headline invariant (hub == feature list minus primary slots, plus the
utility tail) is a tested model function rather than inline bar logic.
Add a sidebar utility-parity guard and a mobile More-hub invariant test.
The reminder list read isLoading but rendered nothing, so the page
showed a header over blank space while fetching.
- Loading now paints a card-shaped Skeleton stack via the shared
primitive.
- The no-data case routes through the shared EmptyState with an icon
and an add action, matching the lab empty.
- Reminder cards title themselves with CardHeader/CardTitle + CardAction
and the form card drops its pt-6 override, so the card token owns the
vertical rhythm and the double-pad goes away. The card stays neutral —
due state still reads only through the discreet badge.