The settings / medications / admin / hooks tree still declared
bare-literal `queryKey: [ … ]` arrays at ~70 call sites. Centralise
them through `queryKeys.<entry>()` so mutation-invalidation prefixes
stay symmetrical and a future rename only touches one location.
New factory entries cover per-medication compliance / titration /
cadence / glp1-details / drug-level-chart / intake-list, the
Withings status read, the paginated admin audit-log filter, and the
workouts-recent list opts. Existing entries cover everything else
(passkeys, tokens, settings, telegram, integrations, research-mode,
admin status / users / backups / feedback / host-metrics / app-logs /
ai-quality / assistant-flags, coach-prefs, feature-flags, etc.).
Lockstep updates:
- `eslint-plugins/healthlog/queryKey-factory.js` extends
`GUARDED_DIRECTORIES` to cover `src/components/{settings,
medications,admin}` + `src/hooks`, plus the three guarded files
under `src/app/medications/**` and `src/app/targets/page.tsx`.
- `src/lib/__tests__/query-keys.test.ts` mirrors the guard's
`guardedRoots` and pins the shape of every new factory entry.
Tests stay green: 4 736 unit; the factory walker reports zero
bare-literal `queryKey:` in any newly-guarded surface.
Marc-Voice rule — the product never names itself "KI" or "AI" in
copy. Settings -> "KI-Auswertungen" becomes "Auswertungen" / "Insights",
the inner "KI-Insights" card heading collapses into the parent section
heading, "Aktiver KI-Provider" becomes "Aktiver Provider", and the
admin "AI Quality" view becomes "Insights Quality".
Sweep across de / en / es / fr / it / pl. Provider product names
(Anthropic, Claude, ChatGPT, OpenAI) are unchanged — those are
proper nouns, not the "AI" label.
MB7's CF-52 sweep retired three drift-prone NATIVE_SELECT_CLASS clones
behind the shared NativeSelect primitive. The AI provider section
carried five more raw <select> declarations with their own copy of the
border + bg + height contract — exactly the drift the primitive was
introduced to cure. Swap them over so the AI provider, OpenAI model,
Anthropic model, local model, and Add-provider pickers all render
through the same styled wrapper as the rest of Settings.
Move `<PasswordInput>` from `src/components/settings/password-input.tsx`
to `src/components/ui/password-input.tsx` so the auth surfaces and
every settings card that masks a secret consume one primitive. The
two surfaces previously diverged on toggle markup and aria-label
wiring; the new home in `ui/` keeps them in lock-step.
While the primitive moves, lift the icon-only show/hide toggle to a
44 px hit area per WCAG 2.5.5 — `inline-flex h-11 w-11` wrapper with
the eye icon centred inside. The input itself gets `pr-12` so user
input never collides with the toggle glyph.
Consumer imports updated in:
- `src/components/settings/account-section.tsx`
- `src/components/settings/ai-section.tsx`
- `src/components/settings/integrations-section.tsx`
- `src/components/settings/ntfy-card.tsx`
- `src/components/settings/telegram-card.tsx`
- `src/components/admin/_shared.tsx`
W10 design-review carry-over items. M7 lifts the medication-form
day-of-week buttons, the more-horizontal trigger, and the schedule
label-input gaps to the 44 px / space-y-2 standard used by every
other settings surface. L1 pairs every animate-spin/pulse/bounce
with motion-reduce:animate-none across the component tree so users
with prefers-reduced-motion get a still surface. L3 adds the
aria-controls + panel-id pairing to the inventory + coach-evidence
<details> blocks for parity with the Health Score provenance
accordion. L4 inserts a sr-only h4 per therapy-timeline entry so
screen-reader users can jump between medication blocks.
Sub-headings in the settings + admin section trees were mixing
font-medium and font-semibold; the password-change dialog used a
1.5-unit label-input gap while every other Settings form runs at 2.
Standardise on font-semibold for every h2 and h3, the card-internal
cadence to space-y-4 on the AI provider and thresholds-editor cards,
and lift the password-dialog field gaps to space-y-2.
Maintainer-voice sweep across `src/`. ~190 references to the
maintainer by first name in code comments and JSDoc neutralised to
"the maintainer", "the live tenant", "users", or "the user" depending
on context. Three sed passes covering ~50 distinct patterns
(`Marc reported`, `Marc wants`, `Marc kept`, `Marc rolled back`, etc.).
Test fixtures (`displayName: "Marc B."`, `username: "marc"`,
`<HeroStrip userName="Marc">`) kept as opaque test data per the brief.
CHANGELOG normalisation. The v1.4.14 + v1.4.15 entries paired a
German bullet with its English translation by historical design — the
in-app copy is English-default and the user docs are English, so the
mixed-language style was a holdover from the early-marathon notes.
A one-off node script extracted the `_..._` English paragraph from
each bilingual bullet and rewrote the entry as that text. ~44 bullets
reduced; one Docs bullet that lost a leading word in the regex
fixed by hand.
CLAUDE.md → CONTRIBUTING-AI.md rename so the filename is no longer
AI-vendor-specific. `CONTRIBUTING.md` reference updated. `AGENTS.md`
stays in place for multi-agent compatibility (the conventions doc is
shared with Codex / Cursor / etc.).
Wave-4 state wrap. STATE.md now records C1–C5 + D + D2 as complete;
v1421-backlog.md grew a "Closed in v1.4.22" header listing the 20
items the marathon landed; `.planning/phase-W4-report.md` documents
the per-commit decisions, test deltas, and the 23 items deferred to
v1.4.23 with reasoning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-fix the per-card vertical rhythm was either `space-y-3` (AI
provider config forms), `space-y-5` (dashboard layout card) or
`space-y-4` (Account → Profile form). Three competing strides for
forms that visually live in the same shell, so the eye picks up the
discontinuity even though the section-level `space-y-6` outer rhythm
was already consistent.
Standardize on `space-y-4` between fields inside any single Settings
card:
- Dashboard "Customize layout" card: `space-y-5` -> `space-y-4`
- AI Codex / OpenAI / Anthropic / Local provider forms: `space-y-3` ->
`space-y-4`
Account, Notifications and Export already used `space-y-4`. Section
roots and the AI parent card keep their `space-y-6` so distinct cards
still read as distinct surfaces.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Settings inputs and selects rendered at four different heights on
Pixel 5: 32 px (AI add-provider), 36 px (account form, export, ntfy /
telegram), 40 px (AI active-provider) and 44 px (dashboard "Compare to"
trigger). The mismatch was visible whenever two cards lived next to
each other, e.g. the 40-px provider select sat directly above a 32-px
"Add provider" select inside the same column.
Standardize on the shared 36-px contract (`h-9`, matching `<Input>` and
the default `<SelectTrigger>` size) for every text-entry field across
Settings:
- AI active-provider native select: `h-10` -> `h-9`
- AI fallback-chain "Add provider" native select: `h-8` -> `h-9`
- Dashboard "Compare to" `<SelectTrigger>`: drop `min-h-11` so the
default `h-9` applies
The 44-px tap-target floor (WCAG 2.5.5) still applies to mobile
nav-level controls; for inline form fields the 36-px height matches
the rest of the settings shell and the desktop sidebar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-existing format drift accumulated across v1.4.x; running
prettier brings the whole tree back to baseline so future diffs
read clean. No behaviour change.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The GET response used to surface the resolveProviderChain()-filtered
list, which DROPS disabled entries upstream. The Settings UI then
seeded `enabled: true` for every returned row, so disabling an entry
made it disappear on next refetch and lose its toggle position —
the toggle was functionally a "remove from chain" button.
Changed GET to read the *persisted* chain via parseProviderChain() so
disabled rows still round-trip with their flag. The active-provider
summary still uses resolveProviderChain so the displayed "Active: …"
line continues to reflect the actual runner ordering (enabled-only).
The Settings UI seeds entries with the wire-supplied `enabled` value
instead of always-true, so re-enabling becomes possible without a
page reload.
4 unit tests pin the GET shape (persisted-chain ordering, disabled
entries surfaced, cached-active overlay, default-chain fallback).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the v1.4.15-vintage two-card layout (Codex connect at the top,
personal-provider form at the bottom) with the dropdown-driven UX
called out in feedback_settings_no_split.md: one Pulldown picks the
active provider, the form below adapts to that choice, and the
fallback chain at the bottom reorders / toggles entries with arrow
controls.
Five provider config forms live in this file (Codex, OpenAI, Anthropic,
Local, Admin OpenAI) and share a single card frame so transitions feel
like updating one concept rather than swapping pages. The Codex form
keeps the device-code flow + OAuth callback handling unchanged. OpenAI
gets a model dropdown plus a collapsed Base URL override (advanced).
Anthropic and Local mirror that shape with the right credentials. The
fallback chain calls the new PUT /api/insights/provider-chain to
persist reorders.
The Select is URL-synced via ?provider=… so deep links work, the SSR
test can drive each branch deterministically, and a refresh keeps
the user on the form they were editing. Native <select> elements
keep the SSR-only render-to-static-markup test happy without bringing
a portal-based Radix tree into play. No new dependency — dnd-kit is
not in package.json so reorder uses up/down arrow buttons.
EN + DE i18n keys land alongside; the i18n parity guard test sees
both locales and the existing settings.ai.providerChain.types.* tree
keeps the per-provider labels stable across the chain summary and
the new chain-management UI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The AI section now renders a small read-only summary of the user's
fallback chain: "Active provider: ChatGPT (Codex)" plus "Configured
chain: ChatGPT (Codex), OpenAI (your key), Admin OpenAI". When the
in-process cache reroutes to a fallback (the primary 401'd in the
last hour), the line gains "(cached: OpenAI)" so the user can see the
runner has shifted without digging through the wide-event log.
The full management UX (drag-to-reorder, on/off toggles, add/remove
provider entries) is owned by phase B2 — this surface is deliberately
tight so the two phases don't collide. The card hides itself when no
provider is configured at all so accounts that haven't set up AI
don't see an empty header. EN + DE i18n keys added under
settings.ai.providerChain.*; provider-type labels are localised so
"Local model" reads "Lokales Modell" in German.
The new GET /api/insights/provider-chain endpoint is the data source.
Returns the resolved chain in priority order plus the cached active
provider (when set), wrapped in the standard apiSuccess envelope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops dead `id` props and `scroll-mt-28` classes from admin section
components (load-bearing in v1.4 monolithic admin, dead since the v1.5
phase-4b dynamic-route split). Also removes the unused
`LEGACY_ANCHOR_TO_SLUG` map, the `admin-shell` re-export shim, the
`BERLIN_TZ` and duplicate `MS_PER_DAY` constants, the verbose
`AdminSectionPage` docstring, the `useEffect` paraphrase comment in
`ai-section`, and rewrites the `_exhaustive: never` exhaustiveness
guard to the modern `slug satisfies never` form. `UserManagementSection`
now reads `currentUserId` via `useAuth()` instead of accepting it as a
prop.
Findings 1, 2, 3, 4, 6, 7, 8, 11, 15, 16, 17 from
.planning/phase-6-simplify-findings.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the v1.4.6 deferred polish item — feedback / mood / medication
status indicators now ask for *meaning* (success / warning / info)
instead of hard-coding `text-dracula-green` / `bg-dracula-orange/15`.
Adds three CSS aliases to `globals.css`:
--success → dracula-green (positive outcome)
--warning → dracula-orange (caution / attention)
--info → dracula-cyan (neutral informational)
Dark mode (Dracula): each alias resolves to the same hex as before, so
the visual is pixel-identical to v1.4.6 — what changes is the *intent*
of the class.
Light mode (Alucard): the raw Dracula greens and oranges fail AA
contrast on a white card. The new tokens get higher-contrast
counterparts (#14720a, #a34d14, #036a96) that match the existing
`--destructive` / `--chart-*` light-mode swap.
Migrated callers:
- `src/components/admin/feedback-inbox-section.tsx`:
- BUG category badge → `destructive` (semantic — bug *is* broken)
- "published to GitHub" badge → `success`
- `src/components/medications/medication-card.tsx`:
- in-window status → `success`
- very-overdue status → `warning`
- "late" intermediate stays on `dracula-yellow` (yellow doesn't
map to either semantic bucket — it's mid-warning)
- `src/components/settings/ai-section.tsx`:
- "ChatGPT connected" badge + success toast → `success`
- "Connection expired" badge + raw-data warning → `warning`
Charts (`src/components/charts/**`) untouched per
`feedback_charts_visual_identity.md`. Other dracula-token callsites in
settings/* (telegram, ntfy, web-push, account, about, test-connection)
are deferred to v1.5.1 — broader sweep, not in tonight's scope.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the v1.4.6 deferred polish item. When the user has saved a key
for the active provider (OpenAI, Anthropic, or Local), a "Remove saved
key" button now sits next to Save / Test connection. Clicking opens an
AlertDialog confirmation, then PATCHes `{ <provider>Key: null }` — the
existing `/api/user/ai-provider` route already treats null/empty as
"clear" (encrypted column set to NULL).
Provider, model, and base URL stay untouched so users keep their
selection while just rotating the secret. The button hides itself when
no key is stored, so the only state where it shows is exactly the one
where it's actionable.
Insights for the wiped provider fall back to the admin/Codex provider
on the next request — surfaced in the confirm-dialog body so the
behaviour is explicit.
i18n: 4 new keys under `settings.ai.*` (`removeKeyCta`, `keyRemoved`,
`removeKeyConfirmTitle`, `removeKeyConfirmBody`), EN canonical, DE
mirror. Existing `i18n-locale-integrity.test.ts` enforces parity.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.4.7 corrected the issuer (`auth.openai.com`) and added the
RFC-8693 api-key exchange, but the Connect button still failed in
production with `unknown_error`. Root cause: OpenAI's Hydra
allow-lists only `http://localhost:1455/auth/callback` and
`:1457/auth/callback` for the public Codex CLI client ID. Any hosted
redirect URI gets silently rejected — the v1.4.7 user landed on the
"An error occurred during authentication (unknown_error)" page.
The official Codex CLI ships a parallel **device-code flow** for
exactly this case (`codex-rs/login/src/device_code_auth.rs`):
1. POST `https://auth.openai.com/api/accounts/deviceauth/usercode`
with the client id → `{ device_auth_id, user_code, interval }`.
2. Show the user the code and a link to
`https://auth.openai.com/codex/device`. They open it on any
device, log in, and type the code.
3. POST `https://auth.openai.com/api/accounts/deviceauth/token`
repeatedly until Hydra returns `{ authorization_code,
code_challenge, code_verifier }` (otherwise 403 / 404).
4. Standard PKCE token exchange against `/oauth/token` — but with
`redirect_uri = https://auth.openai.com/deviceauth/callback`,
which IS allow-listed for the public client. Then the same
id_token → API-key exchange that v1.4.7 already added.
This sidesteps the redirect-URI allow-list entirely; HealthLog's
domain never appears in any OAuth round-trip.
Implementation:
- `src/lib/ai/codex-oauth.ts` adds `requestDeviceCode()` and
`pollDeviceCode()`.
- `POST /api/auth/codex/device-start` initialises the flow, persists
the `device_auth_id` + `user_code` in an encrypted httpOnly cookie
(15 min, scoped to the requesting user), and returns the user-
facing pieces.
- `POST /api/auth/codex/device-poll` does one poll attempt; on
`pending` it returns `{ status: "pending" }`, on success it
exchanges + persists the api key + refresh token and returns
`{ status: "connected" }`.
- The Settings → KI section replaces the "redirect to OAuth"
button with an inline card showing the user code, the
verification URL, and a copy button. The page polls every
`intervalSeconds` (5s default) until status flips to "connected"
or the user cancels. EN + DE strings under
`settings.ai.deviceCode*` cover the new copy.
- The legacy authorize / callback routes stay on disk for now —
they're harmless when the redirect-URI allow-list is updated for
a private client ID, so operators with an OpenAI partner client
can still use the original flow via `CODEX_OAUTH_CLIENT_ID`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Long-overdue prettier sweep; prettier had drifted vs. checked-in code
across ~130 files (mostly long-argument line-break wrapping in TS and
table-padding in markdown). Pure formatting — no semantic changes —
and required to clear the v1.4.6 release gate's `pnpm format:check`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small follow-ups surfaced by the v1.4.6 QA pass:
- Settings → KI: the "raw data" warning still used raw Tailwind orange
tokens instead of the Dracula `dracula-orange` ones, breaking theme
consistency and tipping below the WCAG-AA contrast floor on the
card surface.
- Admin status grid: the Backups card cta read "View backups" but
jumped to the system-status section (the dedicated backups view
does not exist yet — that's a v1.5 feature). Re-labelling the cta
to "Open system status" and the Integrations cta to "Open
integrations" makes the destination honest. The href targets
themselves are unchanged.
- `withIdempotency` body-content guard previously did `text.includes("sk-")`
which silently broke caching for benign payloads containing common
English compounds like "task-id", "risk-management", "disk-io".
Tightened to a regex requiring the canonical secret prefix plus a
realistic suffix length, so only actual API keys trip the guard.
Three regression tests cover the false-positive cases.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
P15: drop `gpt-5` (unreleased) and `o3-mini` from `MODEL_PRESETS["OPENAI"]`.
gpt-5 would surface as model_not_found from OpenAI; o3-mini uses the
o-series param contract (max_completion_tokens, no temperature override)
which the current OpenAIClient body shape doesn't speak — so picking
either preset just produced a confusing 400. Keep gpt-4o, gpt-4o-mini,
gpt-4-turbo. Power users can still type any model via the Custom… option.
P16: pipe ~30 hardcoded German strings in ai-section.tsx through
`t("settings.ai.…")` and add the matching keys to both messages/en.json
(canonical English) and messages/de.json. Behaviour unchanged — the
Connect/Disconnect/Save/Test/Provider/Model labels and the connect-help
copy now respect the user's locale toggle. The provider-test flow keeps
its own DE strings localized too.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bucket E+F of the v1.4.3 polish pass.
**Disconnect-button styling unified.** Three integrations had three
different "Trennen" buttons: Withings used `outline` + `text-
destructive`; moodLog used solid `destructive` (red bg, white text);
the AI/Codex card used `ghost` + `text-destructive`. Same action,
three readings — confusing. All three now use the Withings pattern
(`outline` + `text-destructive`). The actual confirmation button
inside the AlertDialog keeps its solid-red treatment so the final
"yes, do it" still reads as a strong commit.
**Bug-report explicit on/off toggle.** Until v1.4.3 the bug-report
feature was implicitly gated on "is the GitHub token + repo
configured". An admin who wanted to *temporarily* hide the report
button (e.g. during a freeze window) had to clear the encrypted
token, forcing re-entry on resume — destructive for a reversible
intent. Migration `0027_bug_report_enabled_toggle` adds
`app_settings.bug_report_enabled BOOLEAN NOT NULL DEFAULT TRUE`,
the admin settings schema accepts `bugReportEnabled`, and a Switch
in the admin → Bug Report card lets the operator flip it.
`POST /api/bugreport` now returns 503 ("Bug reports are disabled by
the administrator") when the flag is off — the client already
renders a "not configured" screen on that status, so the report
button hides gracefully without further client-side wiring.
i18n keys added: `admin.bugReportEnabledLabel` + `…Description` in
both DE and EN.
Tests + typecheck green (671/671).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bucket C of the v1.4.3 polish pass.
Three independent bugs in the KI-provider settings, fixed together
because they all live on the same panel:
1. **Selecting "OpenAI" left no way to provide a key.** The schema
stored Anthropic and Local API keys, but OPENAI was a valid
`aiProvider` value with no matching credential column — the
resolver silently fell through to the admin-level OpenAI key. So
a self-hosted user who picked OpenAI and typed nothing got the
admin's key (might not exist), and a user who *wanted* their own
OpenAI key had no UI surface to enter it.
Migration `0026_user_ai_openai_key` adds `users.ai_openai_key_
encrypted`. The resolver's `OPENAI` branch now picks up the
user-level key first and only falls back to admin if the user
slot is empty. The settings UI grew an OpenAI-key field gated on
`provider === "OPENAI"` mirroring the Anthropic flow. The
`/api/ai/test` override schema accepts `openaiKey` so the
"Verbindung testen" button can validate an unsaved key without
persisting plaintext.
2. **Model field was a freetext Input, easy to typo.** Replaced
with a `<select>` populated from `MODEL_PRESETS` per provider:
gpt-4o / gpt-4o-mini / gpt-4-turbo / gpt-5 / o3-mini for OPENAI;
claude-sonnet-4-6 / claude-opus-4-7 / claude-haiku-4-5 / claude-
3-5-sonnet-latest for ANTHROPIC; llama3.1:8b / llama3.1:70b /
mistral / qwen2.5 for LOCAL. An "Eigenes…" sentinel still
surfaces the freetext Input so power users can target preview
models or fine-tunes. The dropdown disables when no provider is
chosen — saves the user from filling a model that won't be used.
3. **"Mit ChatGPT verbinden" silently bounced to chatgpt.com login
with no OAuth grant.** The authorize URL was built without a
`client_id` parameter, so chatgpt.com couldn't recognise the
request as an OAuth handshake — it just rendered the regular
login form. The user came back, nothing connected, the button
"did nothing".
Added `CODEX_OAUTH_CLIENT_ID` env var. The URL builder now
throws `CodexOAuthNotConfiguredError` if it's unset; the
authorize route maps that to a 503 with an actionable message
instead of dispatching the dead URL. The settings card reads
`codexOauthConfigured` from `/api/insights/settings` and hides
the Connect button when the flag is false, replacing it with a
tiny italicised "ChatGPT-OAuth ist auf dieser Instanz nicht
konfiguriert — nutze stattdessen einen eigenen API-Key unten."
`CHATGPT_OAUTH` was also removed as a manually-selectable
provider option in the dropdown — the top connect button is the
path; having both confused the affordance.
Refresh + token-exchange both forward the client_id now too. Test
fixture seeds `process.env.CODEX_OAUTH_CLIENT_ID="test_client_id"`
in `beforeEach` so the existing URL-shape assertion still works,
plus a new test asserts the unset case throws.
Tests + typecheck green (670/670 — one new assertion).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the SectionPlaceholder fallbacks for Account, AI, Integrations,
Notifications, Dashboard, API, Advanced, and About with real per-section
components. Every /settings/<slug> URL now renders working content; the
placeholder only fires for unknown slugs.
A2-content extraction
- Account: profile + passkeys + password change (account-section.tsx)
- AI: insights + per-user provider override (ai-section.tsx)
- Integrations: Withings + moodLog (integrations-section.tsx)
- Notifications: Telegram + ntfy + Web Push (split into telegram-card,
ntfy-card, web-push-card to keep each file under ~600 LOC)
- Dashboard: layout + thresholds wrappers (dashboard-section.tsx)
- API: endpoints + tokens (api-section.tsx)
- Advanced: export + danger zone (advanced-section.tsx)
- About (greenfield): version, license, build SHA, repo/changelog/docs
links + "Check for updates" against GitHub releases
A2-about
- Reads /api/version via TanStack Query, surfaces app version, AGPL-3.0
badge, short build SHA, build timestamp, repo/changelog/docs links.
- "Check for updates" button compares against api.github.com latest
release tag with up-to-date / newer-available / failure feedback.
A8-UI test buttons
- Shared TestConnectionButton primitive that POSTs to a test endpoint,
surfaces ok-with-latency or translated meta.errorCode failures.
- Wired into Withings + moodLog (Integrations) and Web Push
(Notifications). Glitchtip + Umami remain admin-only.
i18n
- New settings.about.* and settings.testConnection.* key groups in en.json
and de.json.
Cleanup
- Delete src/app/settings/page.legacy.tsx (3230 LOC monolith).
- Convert sync-from-server patterns to the React-recommended
prev-payload-id render-time pattern so the strict
react-hooks/set-state-in-effect rule passes without a file-level disable.
Tests
- New src/components/settings/__tests__/sections.test.tsx with 9 SSR smoke
tests (one per section) — total suite 514 (was 505).