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.
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.
The dashboard snapshot fires the moment login succeeds instead of
waiting for the page chunk (start ~670 to ~200 ms throttled). Guided
follow-ups are personalised from the user's snapshot and an answer gets
the coach's streamed reaction before the next question. Archived
schedule eras can be corrected through a superseding revision that
keeps the original as an audit trail, era writes serialise per
medication, and duplicate dose times collapse in validation. A rolling
medication whose next dose is tomorrow can no longer read as heavily
overdue — the card status derives strictly from the served next-due —
and the adherence bars keep a fixed geometry in every state. The
server-wide assistant key gains its admin card, the one duplicated
coach preference collapses onto a single owner, twenty-seven admin and
settings card headers move onto the shared pattern with sixty
containers normalised, and the remaining hydration warnings are gone.
The client bundle carries only the active locale — resolved on the
server and handed to the provider with no hydration flash, halving the
dashboard route to under a megabyte parsed (288 KiB gzip). The auth
shell renders the app chrome immediately instead of a spinner while the
session resolves in parallel, the widgets request folds into the
snapshot it duplicated, and insight pills prefetch their sub-pages —
a first visit to a sub-page lands in about ninety milliseconds instead
of nearly a second.
A night with coarse apple-health samples next to granular watch data
showed only awake and asleep — the per-session source pick now prefers
the source that knows its stages. Tour popovers clamp into the
viewport, wrap their footer and fall back to a bottom sheet on narrow
screens. Settings and admin sections share one alignment rule — icon
column left, everything else flush on the content column — with neutral
header icons, no duplicated section titles under the shell heading, and
every select on the account-style idiom with a visible chevron. The
dashboard's customize icon matches insights, the target-settings link
leaves the home charts, and the mood heatmap adopts the damped fills
the other mood charts already use.
Icons inside buttons drop their manual margins in favour of the
button's built-in gap (two hundred call sites), form field stacks settle
on one spacing step, card titles on exactly two sizes, inline field
errors on one text size, and a dozen hand-rolled loading pulses move
onto the shared skeleton primitive while keeping their silhouettes. The
intake dialog wording joins the rest of the app on 'übersprungen', and
the UI guidelines now document the two deliberate footer and
touch-target systems.
Users can now tell the assistant about themselves in an encrypted
free-text profile that feeds the coach and briefing prompts as clearly
fenced context, with the coach allowed one targeted follow-up question
when the picture is incomplete. Operators on closed registrations can
mint expiring invite links with a QR code from the admin area; a valid
invite opens registration exactly as many times as it allows and records
who redeemed it. A nightly pass watches three deterministic signals —
low adherence, blood pressure above its target band, a falling recovery
trend — and sends at most one coach nudge per week through the existing
notification channels, gated behind the same opt-outs as every other
assistant surface.
Route every Settings callout, badge and status note through the semantic
colour tokens (success / warning / info / primary) instead of raw palette
classes, so the same meaning reads identically across cards and stays
legible in light mode. Add a single SettingsInfoTile primitive — icon +
tone + text on one fixed surface — and adopt it for the threshold-override
caution note, replacing a bespoke box.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a 'What the Coach remembers' settings section: lists the user's
durable facts grouped by category with a learned-when stamp, forgets a
single fact or clears all (behind a confirm), and notes the rolling
conversation summary. Consumes the v1.11.1 CoachFact routes via the
query-key factory; gated on the Coach feature; localised in all six
locales. Closes the API-only gap from v1.11.1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Return a stable, secret-free reasonCode from /api/ai/test alongside the
English reason, map it to a localised string in the AI settings panel,
and fall back to the plain reason for any unmapped code. A German
operator no longer sees an English failure sentence in an otherwise
localised panel.
The test and regenerate handlers called res.json() unconditionally. A
proxy-rewritten HTML error page made that throw "Unexpected token '<'",
which leaked into the toast. Both handlers now check the content-type and
wrap the parse, showing a clean message on a non-JSON body. The test
handler also renders the new { ok:false, reason } payload verbatim.
Bundles the deferred v1.4.47 Mediums + Lows with the v1.4.48-deferred items
into a single release. Headline: server-side suppression of MEDICATION_REMINDER
APNs for iOS clients that own their local reminders (closes GH #206), a
push_attempts table backing /api/admin/notifications/diagnostic, and a sweep
of v1.4.48 QA forward-findings.
Performance (the user-visible win):
- /api/analytics cold-fallback path splits the heavy single-statement
aggregate into two parallel queries. The linearly composable columns
(count / min / max / mean) keep the full-partition scan; the windowed
7 / 30 / 90-day avg / slope / r² columns take a 90-day outer cap so
the planner does an index range scan on (user_id, type, measured_at)
instead of a full-partition sequential scan. Output is bit-identical;
cold slim slice drops from multi-second to sub-second on tenants with
large measurement partitions.
Server-side notifications:
- GET + PATCH /api/auth/me/notification-prefs with deep-merge semantics.
Initial category medication.clientManaged: boolean (default false). Cron
medicationReminderJob skips dose-due APNs when the flag is true,
emitting a medication_reminder.suppressed_client_managed wide-event
annotation per skip.
- push_attempts table backs the diagnostic endpoint. Every APNS /
Web-Push / Telegram / NTFY sender writes a fire-and-forget row.
Daily 03:35 Europe/Berlin cleanup prunes rows older than 90 days.
- /api/admin/notifications/diagnostic — admin-only endpoint surfacing
device APNs token presence (masked), channel config presence, and
recent push attempts. Registered in OpenAPI.
- Per-channel APNs "Send test" button now fires with
eventType: "MEDICATION_REMINDER" so the dispatcher's time-sensitive +
priority-10 branch runs. A successful self-test now exercises the
exact path production reminders take; an unsuccessful one points the
user at iOS Settings → Time Sensitive Notifications rather than
appearing to be a server-side bug.
Withings reliability:
- OAuth callback consumes the nonce atomically (delete-first with
P2025 catch for the replay branch). The previous read-then-delete
pattern allowed a race between two concurrent callbacks.
- ?reason=state redirect differentiated into csrf1 / replay / expired /
cross_user so operators can tell mismatch types apart without DB
shell access.
- connect endpoint rate-limited to 10/60s/user; row-create failures
redirect to a consistent error surface.
Observability hardening:
- redactSensitiveFields(body) denylist helper covers password / token /
secret / apiKey / authorization / csrfState / nonce, recursive,
case-insensitive. Caught: /api/devices invalid_format issues were
echoing apnsToken values verbatim into the audit log.
- sanitiseZodIssues(issues, { stripValuesFromMessage: true }) overload
applied to 14 audit-log sites.
Migrations: 0079 (notification_prefs), 0080 (push_attempts).
Tests: 4815 → 5272 unit + integration. Workout-batch integration suite
that had been red on main since v1.4.45 (three release commits shipped
through it) now green again — updated to match v1.4.42 cross-source
dedup semantics.
Resolves PR #207.
Six touch-disjoint waves landed on develop before this release:
- W1-KNIP — exports/types tiers flipped to enforcing-mode on main;
35 unused exports + 52 unused types triaged to 0/0; shadcn surface
+ zod-infer contract types scoped via knip.json ignore-block.
- W2-ZOD-MULTI-ISSUE — `returnAllZodIssues` shared helper +
`/api/dashboard/widgets` 422 now returns every Zod issue + worker
audit-ledger breadcrumb. 41 sibling routes documented for v1.4.43.
- W3-QUERYKEY-LONGTAIL — 40 settings/medications/admin/hooks files
migrated to the queryKeys factory; 9 new factory entries; ESLint
rule + test-guard scope extended in lockstep.
- W4-ARCH-HYGIENE — BERLIN_DAY_FORMATTER consolidated across nine
sites (seven status helpers + bucket-series + bp-in-target);
Suspense double-comment consolidated; doctor-report sanitiser regex
rewritten to escape-form so diffs are reviewable; pr-detection-
worker soft-delete filter; offhost-backup DR-intent comment.
- W5-IOS-WORKOUTS — `dedupeWorkoutBatch` write-time cross-source
dedup helper for the v1.5 iOS HealthKit observer ingest; dashboard
tile-strip placeholder parity with the live TrendCard chrome.
- W6-WITHINGS-OFF + ENV-CHECK — Withings off-response classifier
(transient/reauth/persistent) + WithingsApiError typed throw + the
`pnpm check-env` CLI. APNs `allOrNone: true` closes the AP-2
silent-disable gap the wave was conceived to close.
No migration, no env-var change, no API contract break for iOS v0.5.4.
The new `/api/dashboard/widgets` 422 envelope is additively extended.
pnpm test --run green at 4815 passing / 1 skipped.
pnpm typecheck, pnpm lint, pnpm knip (no --include flag) green.
Quality-leap release between two HealthKit milestones. The headline is a P0 hotfix for the `/api/analytics` 500 that broke the Insights mother page for any account with more than a few thousand measurements: the six per-metric status helpers were spreading the entire numeric history into `Math.min` / `Math.max`, which trips a stack overflow once a single argument list exceeds the V8 spread cap. Both the surfaced symptom and the latent risk in five sibling helpers were folded onto a reduce-based min/max path.
Around that hotfix landed a deep polish pass: the Insights mother page now defers three below-the-fold blocks behind `next/dynamic` to trim the cold mount cost, the analytics endpoint gained a `?slice=summaries` slim slice that the per-metric sub-pages now ride, the Coach snapshot builder picked up a 60 s LRU keyed on `(userId, scope)`, the Settings shell consolidated three duplicate copies of the about / notifications / about-dropdown surfaces, the navigation strip dropped a redundant "Home" group label and a colliding "Notifications" sibling, every icon-only button on the medications and admin surfaces gained an accessible name, every Progress bar gained one too, three pages had their heading hierarchy repaired to a sequential h1 → h2 → h3, the mobile Coach FAB now hides while a chart tooltip is open so it cannot occlude the read-out, and the auth shell normalised every authenticated route to a single `max-w-screen-xl` container.
Also lands the Apple App Site Association handler at `/.well-known/apple-app-site-association` on every host that fronts the app, advertising the iOS bundle's App ID prefix (S8WDX4W5KX.dev.healthlog.app) under `webcredentials.apps` so the passkey ceremony shares cleanly between the web origin and the iOS app.
The release closes nineteen issues from a five-surface audit + a runtime bug-hunt pass, plus six follow-up tracks of polish across the affected surfaces.
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).