users.insights_cached_text held the model's briefing, readings and
recommendations about the account in the clear. Migration 0411 adds
insights_cached_text_encrypted (AES-256-GCM, binary layout, purpose-bound
label) and a partial discovery index; the free-text encryption backfill
seals the existing rows one at a time, fail-closed, carrying updatedAt and
guarded on the value it read. Every reader, writer and clear goes through
cached-text-cipher.ts, and a guard keeps the column names there. The
column joins the encrypted-column registry and the key-rotation script,
stays out of backups as operational, and is reset by the data wipe.
The comprehensive-briefing cache (User.insightsCachedText) is one slot
per account with no record of the language it was written in. Every
reader served whatever was there, so a German-language account whose
nightly warm had written English kept seeing English on the dashboard
lead and the Today briefing. The request path only replaced it once the
row was a day old and the provider answered, which with a provider at
its usage limit was never.
Add User.insightsCachedLocale (migration 0335). Both writers store the
locale they generated in; the timestamp-only refresh on an unchanged
snapshot tags the row too, since the fingerprint covers the generation
locale. Every reset that nulls the text nulls the tag.
The snapshot briefing lift and GET /api/insights/generate treat a tag
that differs from the reader's locale as an empty cache: no prose, no
timestamp, the preparing / no-provider state, and the out-of-band warm
enqueued in the reader's locale through the existing path. The
generator's own short-circuit and the POST short-circuit refuse to call
a foreign-language row fresh. A NULL tag (rows written before this
change) is served as before until the next write tags it. The Today
digest derives from the snapshot and follows.
Covered by unit tests on the lift, the route and the generator, and an
integration test that writes the row through Prisma and reads the
snapshot for a reader in another language.
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.
Raising the middleware body cap to 512 MB for archive imports removed
the implicit ceiling that protected JSON routes, so any bulk endpoint
would buffer an arbitrarily large body before validation ran. Every
JSON body read under /api now carries an explicit byte cap sized to its
legitimate payload (64 KB for single-record routes up to 16 MB for the
measurement import), webhooks cap before signature work, and oversized
bodies return 413.
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>
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 direct OpenAI API calls with resolveProvider() abstraction.
Settings route now returns codexStatus/codexConnectedAt/hasAdminKey
instead of hasKey. Generate route validates AI response with
insightResultSchema and logs providerType/model. Also fixes
AppSettings lookup id from 1 to "singleton" in provider.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switch defaultLocale from "de" to "en" and make English the code-level
language throughout. All ~145 hardcoded German strings translated:
- API error messages (52 routes) now return English
- Zod validation messages in English
- Classification/analytics functions return English categories
- Notification type labels in English
- Component strings use i18n t() calls (heatmap, mood chart,
medication form, intake history, password strength, insights card)
- ~50 new i18n keys in both de.json and en.json
Route migration:
- /stimmung → /mood, /zielwerte → /targets (new pages)
- 301 redirects in proxy.ts for legacy URLs
- Sidebar + bottom nav updated
Bump version to 1.0.1.
HealthLog — personal health-tracking PWA with weight, blood pressure,
pulse, mood tracking, medication compliance, Withings integration,
doctor report PDF export, and Dracula-themed UI.