The third and final layer of the stale-tile bug: saving invalidated the
dashboard snapshot with the default refetchType 'active', but the dashboard
page is unmounted while the user sits in settings — the cell only went stale,
and the snapshot query's deliberate refetchOnMount: false then suppressed the
re-read on same-tab navigation back. The save, reset and ring mutations now
invalidate with refetchType 'all', refreshing the cell mounted or not
(proven live in one SPA session against the real query client).
The sortable settings editors (dashboard layout, medication order, modules,
mood tags x2) painted a second page scrollbar: the sr-only drag hint below
each list is position: absolute, no ancestor was positioned, so it resolved
against the initial containing block, escaped the shell's overflow clipping
and lengthened the DOCUMENT by its static offset. The list wrappers are now
relative; the e2e overscroll guard asserts the one-scroll-floor on these
routes and was negative-tested against the revert.
The Local provider speaks the standard JSON wire: response_format by default,
one self-healing fallback per endpoint when a server rejects it (dialect
cached per base URL) — LiteLLM, OpenRouter, vLLM, LM Studio and plain Ollama
all work from the same form, and the form + provider docs now say so. A
gateway that shims a Claude-family model behind a synthesized tool call is
parsed from the tool-call arguments instead of yielding silently empty
insights.
The insight token budget is configurable (INSIGHTS_MAX_TOKENS, default 2500,
clamped 500-8000) and unified across all six call sites; a reply cut off
mid-JSON returns a distinct truncation error instead of a generic parse
failure. A briefing withheld by the number-grounding check now says so on the
card instead of pretending nothing was generated. The provider test button
answers 4xx with a bad_request category instead of claiming the endpoint was
unreachable.
Sleep repair follow-up: measurements carries a second full unique index — the
natural key (type, measured_at, source, sleep_stage) — so a key-format
migration could tombstone the old rows while their re-keyed replacements
collided with that index and were silently dropped (live incident: a
full-history re-sync erased a user's sleep). Planned creates now run a
natural-key rescue probe: a match, live or tombstoned, is updated in place —
fresh value, the new externalId, deletedAt cleared. A rescue-probe failure
holds the watermark so nothing is lost past the overlap.
Sharing a single document from the vault minted a full-record share: the
create form always defaulted `resourceTypes` to ["Patient","Observation"]
and never signalled a documents-only intent, so the served /c/<token> view
aggregated the whole doctor report — every vital, lab, medication and
wellness figure — alongside the attached file. An empty section blob was
indistinguishable from "defaults", so the report always resolved to the
full record.
Introduce an explicit documents-only share:
- add a `documentOnly` flag to the create payload; when set the server
forces an empty report scope (all-off sections, no FHIR resource types,
FHIR API off) and requires at least one document, so a document link can
never widen into a record — whatever the body sends.
- gate the clinician-view aggregation on an enabled section: a share with
no section on never calls the report aggregator, so no health metric
leaves the database. The recipient sees only the attached document(s).
- the document detail Share action opens the create form in documents-only
mode: hide the FHIR toggle, the FHIR resource-type checkboxes and the
history-range selector, keep the picker/label/expiry/passphrase, and
always carry the launched document into the created link.
- Settings sharing keeps the full record-share form unchanged.
Extract the create-payload builder as a pure function and cover the scope
contract, the empty-scope data load, and the served-view metric-free
guarantee with tests; refresh the share e2e and regenerate the OpenAPI
contract.
Introduce an operator-level ChatGPT-subscription (Codex) credential that the
operator connects once and shares server-wide, plus a per-user switch to route
AI egress through it.
- Schema (migration 0235): three encrypted columns on AppSettings for the
operator's central Codex access/refresh tokens and the ChatGPT-Account-ID
claim, plus expiry/connected-at/status; a per-user useCentralCodex flag
(default false). Register the new encrypted columns in the rotation registry
and script.
- Resolution: add the admin-codex chain type; append it to a user's resolved
chain only when they opted in AND the operator connected it. Tokens refresh
server-side against the singleton row (CAS-style), same primitives as the
per-user Codex path.
- Budget: bill an admin-codex primary against the operator cost cap, not the
user plan — it drains the operator's account.
- Consent: admin-codex is server-managed external egress, gated by the existing
consent machinery (self-snapshot and document surfaces alike). No existing
consent path is weakened.
- Document order: rank admin-codex with the subscription tier (last), so the
document class stays local-first.
- UI: admin device-code connect/disconnect card (cookie-only admin routes) with
honest copy on account pooling, shared rate limits, and training-by-default;
a per-user opt-in switch shown only once the operator has connected it, off by
default, behind a one-time honesty confirm.
- i18n across all six locales; unit tests for the chain type, budget cap,
consent gate, vision capability, and document order; a mocked e2e for both
the admin connect surface and the per-user switch.
The central credential's tokens are encrypted at rest and never returned to a
non-admin API; the opt-in only takes effect once the operator has connected the
shared account.
Lab-report OCR now rasterizes a PDF to page images for any non-Anthropic
vision provider, exactly as the document vault already does — Anthropic keeps
its native PDF block, every other vision provider (codex/OAuth, OpenAI, local)
reads the rendered pages. The capability probe reports `pdfSupported` whenever
the picked provider reads PDFs natively or the server-side rasterizer is
available, so the "scan a PDF" affordance shows for a codex provider too.
Drop the dedicated document-scan (Lab-OCR) provider: labs OCR always uses the
main configured provider chain. Removes the settings card, the `aiOcr*`
resolution branch, the GET/PATCH handler fields, the OpenAPI schema fields, the
i18n keys across all locales, and the five backing `User` columns (migration
0234). The on-device tesseract fallback is untouched.
Dashboard hero: stack the overdue-dose action directly beneath the verdict
text (tight gap, left-aligned) on every viewport instead of pushing it to the
far container edge on desktop.
Add Strava as a per-user BYO-OAuth workout source, mirroring the shipped
WHOOP/Oura/Polar pattern. Strava caps every new app at single-player mode, so
each self-hoster registers their own app and pastes the client id/secret into
Settings (falling back to a shared env app). Strava is a workout source only —
it exposes no daily metrics and no training-depth analytics are derived.
- Schema (migration 0233): per-user encrypted client id/secret + access/refresh
tokens, plaintext athlete id, incremental cursor, and backfill marker on
`User`; `STRAVA` added to `MeasurementSource`.
- Token model follows Oura's reactive-refresh-on-401 with a compare-and-set on
the stored refresh ciphertext, because Strava rotates its refresh token on
every refresh.
- Client over safeFetch: authorization-code + refresh, activity list + budget-
aware per-activity detail (for calories/HR), honouring the 200/15min cap; each
activity maps to one `Workout` row. Activity name/description stay in metadata
as data, never surfaced or interpolated.
- Dedup reuses the existing source-agnostic two-layer picker: `STRAVA` slots
into the workout ladder below the device-native wearables and above
MANUAL/IMPORT, so an Apple-Watch + Strava twin collapses to one canonical row.
- Poll-cohort hourly sync (:17) + self-converging boot backfill, wired into the
integration-sync registrar. Webhook deferred.
- OAuth routes (connect/callback/credentials/disconnect/status/test) with signed
state, cross-account re-auth rejection, and a surgical `www.strava.com`
connect-src gated to the Strava surfaces.
- Settings card + consolidated status envelope entry; env + compose whitelist;
i18n across all six locales.
- Garmin: no direct connector is possible (business-partner-only). Add
docs/integrations/garmin.md documenting the Apple Health / Google Health
Connect path and Garmin's withheld proprietary scores, cross-linked from both
platform docs, plus a non-connector info note in the Connections panel.
Regenerate docs/api/openapi.yaml for the new `STRAVA` MeasurementSource value.
Bring the structural card-anatomy offenders onto the spacing scale. The
cycle wellness-tile family (ring tile, phase-education, insight summary,
cycle view) and the insights wellness-scores tile drop banned p-5/px-5
py-5 for p-4 md:p-6 and keep their premium hue surface; their header and
info glyphs move off text-primary to foreground, spinners to muted. The
onboarding hand-rolled shells (anamnesis card, baseline form,
getting-started checklist, tour tooltip) drop p-5/space-y-5/rounded-2xl
for p-4 md:p-6/space-y-4/rounded-xl. settings/anamnesis-section moves its
three raw Card+CardTitle shells onto SettingsCard + SettingsCardHeader.
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.
Add a single switch in AI settings — off by default — that turns on ambient AI
reading of uploaded vault documents. Turning it on reveals a once-shown,
vendor-blind honesty confirm (subscription providers may use the content to
improve their models; a self-hosted local model never egresses) that must be
acknowledged before the setting is written. Copy is translated across all six
locales.
Introduce a shared PageAuthGate (header + tile skeleton) and adopt it on the
top-level pages that flashed a lone centered spinner while auth resolved:
mood, measurements, achievements, illness, mental-wellbeing, checkups, cycle,
and the labs / medications / custom-metrics / illness detail routes. The
auth-resolving frame now reserves the page geometry instead of snapping in
after a spinner.
- Onboarding 'more sources' chips anchor to their provider card
(/settings/integrations#oura etc.); connection cards gain scroll anchors.
Apple Health (no web card) stays on the page root.
- Cycle module gains a header settings wrench for parity with labs /
medications / illness; the tab strip is now controlled so the wrench jumps
to the settings tab and ?tab= deep-links keep working.
- Admin SettingsToggle: name the Radix switch via aria-label so it no longer
announces as a bare 'switch, on/off' (one shared component, six call sites).
- Replace the phantom text-destructive-foreground / text-warning-foreground
utilities with the vetted destructive pattern (bg-destructive text-white
dark:bg-destructive/60) and a readable foreground on the warning wash — no
more dead classes riding on cascade order.
- Drop the sub-AA opacity tiers: lab sparkline + titration marker off
muted/70, state icons (query-error, chart empty/error, med sort) off
muted/60 to the full token so they clear the light-theme floor.
- Chart range toggles expose aria-pressed (lab, custom-metric, health, mood,
compliance) so the active window is conveyed to assistive tech.
- The 'no channels' CTA now points at Settings → Integrations#channels
(where channels are actually configured since v1.25.7) instead of the
reminder-types screen, which had become a dead-end.
- A failed load renders the shared QueryErrorCard with Retry; the loading
frame reserves a matrix skeleton instead of a lone spinner.
- The globally-disabled hint reads as a full sentence rather than the bare
'(Disabled)' inline tag.
- The three notification surfaces (per-event matrix, reminder types,
delivery channels) now cross-link each other.
- noChannels copy names its destination.
Unit: the shared ShareLinkCreateForm mounts bare (Settings default) and
seeded (document flow) — a pre-attached document rides in as a chip with
the label pre-filled. e2e: the document detail Share action opens the
create flow with that document pre-attached and reaches the one-time QR
reveal; marked slow (two stacked sheets + a real create).
Extract the clinician share-link create flow (create form, document
picker mount, one-time QR + passphrase reveal) from the Settings sharing
section into a reusable ShareLinkCreateForm. Settings mounts it unchanged
alongside the active/revoked lists; the document detail sheet adds a
Share action beside Download that opens the same flow in a bottom sheet
with the current document pre-attached and its title pre-filled as the
label. The one-time QR reveal lands in that sheet, scannable on mobile.
No backend change: the shared POST /api/share-links handles the attached
document set as before.
Add resolveHeroRingOrder unit coverage (default health-score-first,
reconcile against selected set, drop/append, dedupe) plus resolver/serializer
round-trip; hero SSR tests for the default lead + a persisted custom order +
dropped-when-unrendered; Settings picker tests for the pinned health-score
anchor row, its locked-on switch, drag/arrow reorder, and a persisted
non-leading order. Extend the score-rings picker description across six
locales.
Add a persisted hero ring ORDER over the health-score anchor ring plus the
selected score rings. Defaults to the health-score ring on the leading edge;
the user can reorder the full set (health score included) from Settings →
Darstellung with the existing drag-handle + arrow-button pair.
The order rides the dashboard-layout blob (dashboardWidgetsJson) — no
migration. resolveHeroRingOrder reconciles a stored order against the current
ring set on every read/serialize, so a deselected ring drops and a
newly-selected ring appends; the hero orders only the rings it actually
renders.
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.
/labs gains a quiet link to the LAB_RESULT-filtered vault view (the
original report PDFs behind the values); the health-record export panel
links to the vault filtered to the current year next to its date-range
picker (navigation only — attaching documents to the export is out of
scope here). Both render only when the documents module is enabled for
the account, in all six locales.
Refs: document-vault plan W3-T5
A measured pass over every insights sub-page, the dashboard, and the
settings sections (computed text colour, left text edge relative to the
card edge, header→body distance, per card) surfaced the drift the eye
kept catching:
- The blood-pressure pulse-pressure/MAP explainer was a bare muted
paragraph at the container edge, 16–24 px left of every neighbouring
card's text. It now rides a compact Card in the regular foreground.
- The 'Usual range' read strip was a hand-rolled bg-card/60 shell with
its own padding; rebuilt on Card + TileHeader + CardContent at the
compact density so background, radius, icon spacing and text edge
match the neighbouring tiles.
- The spine tiles carried three different header→body distances (6, 12
and 24 px) via off-scale overrides (gap-1.5, md:gap-3, md:py-5,
gap-3). Assessment, target summary, medication tiles, the mood
stand-out card and the sleep hypnogram now share the sanctioned
compact tier (gap-2 py-3 md:py-4).
- The hero score card and its skeleton kept their translucent identity
but move from a static px-4 py-4 to the p-4 md:p-6 card norm, putting
their text on the same edge as every other card.
- Two dashboard shells (recent workouts, recent unlocks) froze at p-4
on every breakpoint; now p-4 md:p-6.
- Settings: the modules hint moves into the header's description slot
and the injection-site grid joins the section-wide mt-4 rhythm — the
two gaps that deviated from the settings 16 px header→content norm.
Migrate every remaining *-dracula-* utility site (127 lint warnings,
193 class occurrences) onto the semantic vocabulary. The swaps are
pixel-identical in both themes — each target token aliases the same
primitive the utility read, including the light-theme AA overrides:
- green/orange/cyan/red → success/warning/info/destructive
- purple → primary (same value in both themes)
- pink → the new --brand-pink token (Coach gradient, Apple-Health
badge, recording mic): dark #ff79c6 — 6.86:1 card / 5.97:1 bg;
light #a3134d — 7.32:1 card / 6.83:1 bg (computed)
- yellow sites resolve to their status meaning: warn-level log icon,
update-available pill and offline-geo hint → warning; the admin
helpful-rate ramp → success/warning/destructive; the confidence
meter mirrors the coverage-meter precedent (medium + low share the
caution tint, the lit count carries the distinction)
- the confidence-badge low tier dropped dead *-dracula-comment/*
classes (no generated utility) for border-border + bg-muted/50
Close two rule blind spots the sweep surfaced — directional borders
(border-l-dracula-red) and the parenthesised CSS-var shorthand
(bg-(--dracula-green)) — and flip healthlog/no-dracula-utility from
warn to error so the class cannot regrow. var(--dracula-*) references
in chart code stay: they are token references the light theme
overrides, and the chart palette is pinned visually.
The ring toggles rode the section's draft/Save machine; combined with
the server snapshot cache and client staleness this read as 'flipping
does nothing, rings appear later'. Ring choices now bypass the draft: a
dedicated mutation PUTs the server layout copy with only
selectedScoreRings changed (an open draft's other edits stay unsent,
its ring slice is kept in sync), optimistically updates the widgets
query, rolls back on error, and invalidates the dashboard snapshot on
success. The other layout controls keep the draft/Save flow.
Selected rings are now reorderable with the same drag-handle + arrow
interaction as the widget rows below — the array order is the hero
render order, which the server already preserves. Unselected rings
stay plain switch rows and disable at the three-ring cap.
The MED_COMPLIANCE hero ring carried the pooled 7-day adherence
percentage — a mystery number next to today-focused scores. It now
answers the question the hero poses: how many of today's doses are
done. The resolver reads takenToday/scheduledToday off the snapshot's
own medsToday block (one shared builder call, no second tally); score
stays the rounded 0..100 progress for wire compatibility and a new
additive doses field carries the taken/scheduled pair. No dose
scheduled today means no ring, and the band is progress semantics —
green once everything is taken, yellow while doses remain, never red
(a pending morning dose is not an alert state).
The web ring renders the tally (1/3) with a doses label over the ring
system's constant green arc — the same green family the compliance
surfaces in Insights paint — instead of a band gradient; the derived
rings keep their exact insights hues. ScoreRing gains documented
valueText/ariaLabel overrides for tally-shaped centres. OpenAPI
regenerated; labels land in all six locales.
The structured Allergy / FamilyHistoryEntry stores reached FHIR export
only: the doctor-report PDF shipped without the two sections every
clinical intake asks for first, and the Coach knew allergies only from
the free-text answer — a penicillin allergy recorded in the structured
records UI never reached either surface.
Doctor report: two new section toggles (allergies, familyHistory; both
default ON, same stance as labs) across the prefs schema, the export
selection, the aggregator, the PDF renderer, and the export panel. The
tables render stored fields only — substance/category/kind/severity/
reaction/status and relationship/condition/age-at-onset; the reaction
decrypts fail-soft per row and free-text notes are never read, matching
the illness-journal stance. OpenAPI regenerated for the extended
sections shape.
Coach: the about-me context block now appends the structured rows as
clearly labelled entries (authoritative over the free-text answer on
conflict); the projection is a pure composer with unit tests. New
section labels land in all six locales.
The hero's dose text row gives way to a ring row: the selected score
rings render at the health-score ring's sm flat geometry, readiness /
recovery / sleep in their wellness-strip hues and the 7-day adherence
ring in band semantics — the ring that carries the dose row's
information role. Rings self-gate on data, the verdict line above is
untouched, and the skeleton mirrors the new geometry (plain card shell,
no dose pill, the always-present health circle reserved). Settings ->
Dashboard gains a picker under the overview toggle: up to three rings,
module-gated rows, switches disable at the cap, persisted through the
existing widgets PUT. Locale bundles drop the retired dose-row strings
and pick up the picker copy in all six languages.
The muted-foreground token is AA-tuned in both themes; the /70 and /80
opacity modifiers silently undid that (2.9-4.2:1 measured). Every
informational text node drops the modifier for the full token: sleep
hypnogram captions, learning-gate caveat, integration-card setup hints,
module lock notes, glucose panel and token-footer meta, invite-token
hint, wizard hint, notification channel labels and disabled notes (the
disabled state reads through the note and the disabled switches, not
through dimming), plus the resting state of the timestamp, suggestion
chip and sidebar-collapse controls. User-authored mood notes move to
foreground text-sm — user data is never muted. Decorative non-text
strokes (sparkline grid, calendar dot, titration connector) keep their
opacity; icons keep the /60 graphics floor.
Reconciles the v1.26.1 Fitbit setup-guide fix with the Google Health
release: google-health.md stays the full runbook (with a pointer to the
classic fitbit.md guide), the doc-parity guard now asserts the Google
Health page against the google-health client's endpoints and scopes,
and the changelog carries both sections.
Wire the settings card to the live connection so it reflects reality
after a connect.
- Emit a google-health entry on the consolidated integration-status
envelope (configured, connected, last sync, backfill, and the
needs-reconnect flag). Without it the card read an undefined status and
showed a permanently disconnected connection even after a successful
OAuth grant, hiding the sync, disconnect, and reconnect controls.
- Add the resume and test-connection routes the card calls, mirroring the
classic Fitbit ones.
- Use the shared refresh icon on the reconnect action.
- Drop the experimental-scope opt-in from the runbook and describe ECG
and irregular-rhythm as a planned addition rather than a current toggle.
Surface the Google Health integration to the operator and document the
setup honestly.
- A settings card matching the other integration cards: the BYO Google
client id and secret form, connect and disconnect, a manual sync, and
a distinct reconnect prompt when the connection needs fresh consent.
- Register the card in the connections panel and extend the integration
key, the docs-link provider, and the query-key factory.
- Translations for the card copy across all six locales. The copy states
plainly that this reads Fitbit, Pixel Watch, and Fitbit Air through the
user's Google account, that it succeeds the Fitbit connection Google
retires in September 2026, that the operator registers their own Google
client, that stress and readiness are not available, and that a
periodic reconnect may be needed.
- Rewrite the Google Health runbook into the real setup: a Google Cloud
project, the Health API, an OAuth consent screen kept in Testing, a
Web Server client, the callback redirect, the four Restricted scopes,
the hundred-test-user ceiling, and the reconnect caveat.
- Regenerate the OpenAPI contract for the new measurement source.
The Fitbit integration connects through the classic Fitbit Web API, so a
self-hoster registers a Fitbit developer app at dev.fitbit.com — but the
setup guide walked users through creating a Google Cloud OAuth client,
whose client id the Fitbit sign-in rejects (unauthorized_client). Rewrite
the guide for the dev.fitbit.com flow (app type, HTTPS callback, scopes,
PKCE), leave the old page as a pointer, and add a test that keeps the
documented scopes and endpoints in sync with the client so the guide
cannot drift from the code again. Flags the September 2026 sunset of the
classic Fitbit Web API.
Swap the remaining native date inputs (allergy onset, environment
travel/backfill ranges, inbound-document dates) for the app DateField
so they no longer render in the browser's locale order. Route the
medication caution amber and the document info banner through the
warning/info tokens so they adapt to the theme.
The chronic / pre-existing conditions the Coach watches lived under the
personal-context "About me" panel, but they belong with the rest of the
medical history. Move the conditions and the "what should the Coach
watch" focus into Settings -> Anamnese, next to allergies and family
history, and make them editable inline there.
The store is unchanged: both fields are still part of the
/api/coach/about-me payload, read and written through the same query key,
so the Coach and every other consumer keep reading the same data. The
About-me panel keeps the free-text note and the allergies line and omits
the two conditions fields on save, leaving them untouched.
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.
The Input and Textarea primitives already opted config and secret fields
out of 1Password and LastPass autofill (data-1p-ignore / data-lpignore)
whenever autocomplete was off, but Bitwarden had no opt-out hint anywhere,
so it kept offering to fill and save API keys, tokens and integration
URLs. Emit data-bwignore alongside the existing hints, which covers every
field flowing through the primitives. Pin explicit autocomplete on the
change-password dialog so genuine credentials still autofill, and add the
ignore hints to the export passphrase and the few new-password admin
config fields that opt in directly.
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.
Hand-rolled section headers had drifted into a dozen variants — some
without an icon, some with a text-sm description instead of the shared
text-xs, the data-reset title once even tinted red. Route the remaining
ad-hoc headers through the SettingsCardHeader primitive so icon sizing,
title weight and description size live in one place.
Give the integrations connections group (the data sources) its own
labelled header for parity with the delivery-channels group below it.
A coverage test now globs every settings *-section.tsx / *-card.tsx and
fails on any hand-rolled <h2 text-lg> outside the primitive, with a short
allowlist for the collapsible-toggle security cards and the layout
section pending its hub rebuild.
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.
The H24 / H12 preference only reached makeFormatters().time. Surfaces that
build their own Intl.DateTimeFormat — the sleep hypnogram axis, the workout
list and detail headers — omitted the hour cycle, so an en-locale (or AUTO)
user saw AM/PM even with H24 selected. The security activity feed and session
list rendered raw toLocaleString(), bypassing both the locale and the
preference entirely.
Export hourCycleOptions from format-locale as the single source for the
H12 / H24 / AUTO Intl options and spread it into the self-built formatters,
preserving each surface's locale and timezone. Route the security cards
through useFormatters().dateTime. Drop the date-time field's local copy of
the helper.
Guards: the helper contract, the en-US hour/minute call-site pattern (no
AM/PM under H24, AM/PM under H12), and formatUpdatedLabel fed an H24
formatter for the briefing freshness label.
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.