Commit Graph
63 Commits
Author SHA1 Message Date
Marc-André Bombeck 01ae3b542a feat(rollups): v1.4.35.1 — auto-converge rollups on worker boot
Removes the operator step that was implicit in the v1.4.35 foundation.
Self-hosters no longer need to run the backfill script after upgrade.

New `rollup-full-backfill` queue + handler folds one user per job at
serial concurrency. `enqueueBootTimeRollupBackfill` runs once at
`startReminderWorker` boot, discovers accounts with measurements but
zero rollup coverage, and enqueues one full-fold job per uncovered
user with a singleton key so fast restarts don't pile up duplicates.

Best-effort: discovery errors surface through the helper's return
value and are logged via workerLog; the worker boot never fails
because the backfill missed.

Tests: 4280 → 4285 unit + 228 → 230 integration. tsc + lint clean.
2026-05-17 11:28:21 +02:00
Marc-André Bombeck 67ccd55398 feat(rollups): v1.4.35 — partial read-swap onto measurement_rollups
Switches two reader surfaces onto the persistent rollup table for the
linearly-composable stats. The comprehensive insights aggregator and
the slim analytics summaries slice now source count / min / max / mean
from the DAY rollup buckets via aggregateBuckets, plus the comprehensive
dailyByType correlation feed straight from the bucket means. Slope /
R² / standard deviation / anomaly counts stay on live SQL — they do
not compose across DAY buckets.

A defensive parity check against the live aggregate's COUNT(*) falls
back to live SQL when divergence is detected: covers the cold-mount
edge case where the rollup populator is mid-flight, and degrades
cleanly on the slim slice's all-time window for accounts that never
ran the explicit backfill.

Three new integration cases pin the parity contract end-to-end
against a real Postgres testcontainer: the aggregator's
count / min / max / mean matches live SQL byte-for-byte, dailyByType
matches a parallel date_trunc('day', ...) GROUP BY query, and a
freshly-written measurement is reflected on the next read.

tests: 4249 → 4280 unit + 222 → 228 integration. tsc + lint clean.
2026-05-17 09:53:41 +02:00
Marc-André Bombeck e1bbb99f7d chore(release): v1.4.34.5
Follow-on to v1.4.34.4: 24 new critical-path integration tests
(auth-password-change + admin-reset-password + withings-oauth +
passkey-register + cache-invalidation-coverage) and the textarea
iOS-zoom-on-focus fix across 5 sites. Integration suite goes from
197/50 to 222/55.
2026-05-17 00:52:44 +02:00
Marc-André Bombeck 393eef82d8 chore(release): v1.4.34.4
Audit-driven hotfix bundle. CHANGELOG carries the full notes — this
commit stamps the version and the changelog header so the release
pipeline picks it up.

This is the sixth release deployed under v1.4.34.2's pull_policy:
always — the deploy mechanic should pick up the image without any
host-side retag fallback.
2026-05-17 00:39:20 +02:00
Marc-André Bombeck 8886c0ffdf feat(dashboard): remove the hero Coach CTA
Per maintainer directive — the v1.4.34 IW-B "Frag den Coach" sparkles
button sat next to the "Hinzufügen" dropdown on the dashboard hero and
read as visual noise rather than a useful surface. Remove it; the
Coach drawer remains reachable from the /insights tree (its original
home), and the auth-shell `<CoachLaunchProvider>` + drawer mount stay
intact so any future page that wants its own opt-in CTA can call
`askCoach()` without churn.

The hook usages this leaves orphaned in `src/app/page.tsx`:
  - `useCoachLaunch` (only consumer was the CTA)
  - `useFeatureFlags` (only gate was `featureFlags.coach`)
  - `Sparkles` icon import

All drop out cleanly.
2026-05-17 00:05:54 +02:00
Marc-André Bombeck 76128328ff chore(release): v1.4.34.2 — pull_policy: always on docker-compose
The v1.4.34.1 image published to GHCR successfully but never reached
production because Coolify's `docker compose up` kept reusing the
cached `:latest` digest on the apps-01 host. The default Docker pull
policy ("only pull if missing") skipped the registry round-trip and
restarted the container with the prior v1.4.34 image.

Declare `pull_policy: always` on the `app` service so compose-up
re-checks the registry digest on every `up` regardless of the local
cache. Bundle-release because the v1.4.34.1 work ships through the
same `:latest` tag — this is the deploy mechanic that makes v1.4.34.1
actually land.
2026-05-16 23:12:21 +02:00
Marc-André Bombeck c86ee1a636 chore(release): v1.4.34.1
Hotfix on top of v1.4.34 bundling the chart-card dimension fix and the
Insights cold-mount perf rewrite. CHANGELOG carries the full notes; this
commit just stamps the version + changelog header so the release
pipeline picks it up.
2026-05-16 22:25:33 +02:00
Marc-André Bombeck 72b2506ac1 docs(changelog): clean v1.4.34 entry 2026-05-16 21:18:58 +02:00
Marc-André Bombeck bcdab48e96 test(cache): integration coverage + CHANGELOG entry for server cache
Three integration tests against the real Postgres container pin the
v1.4.34 IW-G contract:

  1. Ten concurrent /api/analytics reads against a cold cache fan into
     a single builder call (misses === 1; remaining 9 split between
     stampede joiners and warm-cache hits, with no second miss).
  2. Priming analytics then writing a measurement evicts the cache —
     the next read misses again and the response reflects the new row.
  3. Compliance buckets re-read after an intake POST flush the cache
     and surface the new taken-count.

Drops the unused _request parameter from buildAnalyticsResponse() so
the analytics route lints clean. CHANGELOG carries one Added line per
the marathon's user-facing voice rules.
2026-05-16 21:11:37 +02:00
Marc-André Bombeck a5dd8e1d1a feat(cache): server-side LRU primitive + per-user invalidation helpers
Extends the v1.4.33 Coach snapshot LRU shape into a reusable
ServerCache<T> with TTL expiry, capacity-bounded LRU eviction,
single-flight read coalescing, prefix delete, and per-instance
hit/miss/eviction/stampede counters. Adds five invalidation helpers
keyed by the per-user prefix scheme so write endpoints can flush every
cache that may reflect their changed rows in a single call.

The cached() wrapper attaches cache.<name>.outcome + cache.<name>.key_hash
to the active wide event so production logs carry the hit ratio without
leaking the userId portion of the cache key.

Hand-rolled to keep the dependency surface at zero — same recipe as the
snapshot cache the dashboard already relies on. Process-local; multi-
instance staleness is bounded by TTL, the Redis migration path stays a
future option.
2026-05-16 20:55:05 +02:00
Marc-André Bombeck cc3aae9794 docs(changelog): note the Apple Health export.zip import for v1.4.34
User-visible Added line for the synchronous-upload + asynchronous-
ingest path: POST /api/import/apple-health-export, polling at
GET /api/import/apple-health-export/{jobId}/status, admin variant
at POST /api/admin/import-apple-health-export.
2026-05-16 20:42:31 +02:00
Marc-André Bombeck b92f2b1ecc docs(changelog): note the AASA addition for v1.4.33
Records the Apple App Site Association handler + the proxy
`/.well-known/` allowlist entry under the v1.4.33 Added section so the
release notes capture the iOS-foundation work that landed alongside the
polish pass.
2026-05-16 18:57:57 +02:00
Marc-André Bombeck 0f3ae822cd docs(changelog): v1.4.33 polish and reliability
P0 analytics-500 hotfix, six per-metric status helper folds,
Insights mother-page next/dynamic deferral, slim analytics slice,
Coach snapshot LRU, Settings consolidation, navigation polish,
extensive accessibility sweep across icon-only buttons, Progress
bars, heading hierarchy, and Coach rail labels.
2026-05-16 18:11:57 +02:00
Marc-André BombeckandClaude Opus 4.7 15077482b1 chore(release): v1.4.32
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:57:49 +02:00
Marc-André Bombeck 00a6b67675 chore(release): v1.4.31 2026-05-16 14:11:35 +02:00
Marc-André Bombeck 993dfae2e1 chore(release): v1.4.30.1
Categories endpoint + conflict-resolution lock. A two-item follow-up
to v1.4.30: the categorisation overlay shipped yesterday as a
TypeScript map; the new `GET /api/measurement-categories` endpoint
projects it over HTTP so the iOS-side picker can fetch the canonical
shape rather than carry a hard-coded mirror — the unblocker the iOS
team flagged as the one missing piece. In parallel the SyncMode
conflict-resolution policy that the v0.6.0 standalone-first track
depends on lands as §13 of `08-locked-contracts.md`.
2026-05-16 13:14:00 +02:00
Marc-André Bombeck e1e628da50 chore(release): v1.4.30 2026-05-16 12:25:20 +02:00
Marc-André Bombeck c2aa8888e0 chore(release): v1.4.29.1
Daily-step aggregation hotfix. The dashboard 7-day step chart now
renders daily totals (sums in the thousands), not per-sample
averages, by routing cumulative HealthKit types through the
sum branch of the client-side daily aggregator.
2026-05-16 11:21:45 +02:00
Marc-André Bombeck 59968eb1da chore(release): v1.4.29
A targeted performance + polish patch. The headline is a faster
dashboard on data-rich accounts: the pulse chart drops up to
~5 000 raw rows per range tab in favour of one row per day, the
duplicate `/api/dashboard/widgets` fetch on every mount collapses
to one, and three long-running analytics reads are bounded to
the trailing window the tile path actually needs. Two production
regressions close — the `aggregate=daily|weekly|monthly` path on
`/api/measurements` 500'd for every grain, and the same path
averaged cumulative step counts instead of summing.
2026-05-16 10:49:14 +02:00
Marc-André Bombeck 4053c8f1fe chore(release): v1.4.28.1
"Speichern" on the Dashboard works again for every account whose layout was last persisted before v1.4.28. The v1.4.28 retire of the `glp1` widget id left an orphan entry in `dashboardWidgetsJson` for legacy users, which made the save validator reject the payload; the resolver now filters unknown widget ids on read.
2026-05-16 08:59:27 +02:00
Marc-André Bombeck ff4bfcfc3c chore(release): v1.4.28
Bug-fix and consistency follow-through after the v1.4.27 mobile sweep.
The headline is a tighter dashboard and insights surface: six widgets
retire from code entirely (GLP-1 tile, dashboard `<DrugLevelChart>`
mount, GLP-1 detail-page intake history + inventory, the
`<InsightAdvisorCard>` block and its regeneration affordance, the
weekly-report route), four broken edges close (workout-edit duplicate-
timestamp 409, BD-Zielbereich tile rebuilt on the shared
`<TrendCard>` primitive, `/insights/puls` chart timeout fallback,
sticky tab-strip scroll lock), and one consistency contract lands per
maintainer directive: every medication-list row, every medication-
detail section header, every trend tile, every Coach launch glyph,
every chart-height contract reads on one shape. The HealthScore card
grows to fill its column, gains an accessible `?` tooltip explaining
the delta, and drops the placeholder "Wochenbericht erstellen"
button. iOS contracts are intact; the only `/api/*` evolution is
additive (the `aggregate=monthly` grain on `/api/measurements` and an
internal-only `/api/internal/web-vitals` beacon route).
2026-05-16 02:58:25 +02:00
Marc-André Bombeck 5b04b6c5ba docs(release): document the resilient GeoLite2 fallback in v1.4.27 notes
Rewrites the `MAXMIND_LICENSE_KEY` entry in the v1.4.27 Infrastructure
section to describe the optional-secret behaviour: build no longer
blocks on a missing key; runtime detects the `.empty` marker, falls
back to `ipwho.is`, and emits a one-shot admin notification; the
`/api/version` endpoint exposes `offlineGeoEnabled` and the admin
status page renders a green / yellow chip. Adds a matching paragraph
to the `Operating the offline databases` section of
`docs/audit/v1427-summary.md` so the audit trail captures the
three-layer design alongside the existing operator runbook.
2026-05-15 22:21:58 +02:00
Marc-André Bombeck 004a8491c7 chore(release): v1.4.27 2026-05-15 21:30:09 +02:00
Marc-André Bombeck d98e85cd03 chore(release): v1.4.26 — privacy policy for App Store submission
Public privacy-policy page at /privacy enumerates every Apple
HealthKit identifier the iOS app reads, names every third-party
sub-processor, and restates the EU MDR + GDPR boundaries. Unblocks
Apple BLOCKER 2 of the iOS App Store Connect submission by giving
the registered Privacy URL a real page to point at.
2026-05-15 07:35:05 +02:00
Marc-André Bombeck 17b8d8d2c2 chore(release): v1.4.25
Largest feature delta in the v1.4.x line. Insights expands into seven dedicated metric sub-pages; GLP-1 medication tracking lands end-to-end (drug-level chart with Research Mode acknowledgment, EMA titration ladder, cadence + compliance, side-effect taxonomy, pen/vial inventory); cross-source priority becomes a two-axis resolver; per-user timezone threads through ten analytics surfaces; Withings activity + sleep v2 sync; onboarding rebuilt as a nested-route wizard; Personal Records ship end-to-end; Health Score provenance accordion; Coach runs native first-party prompts across six locales with a 1800-assertion refusal-probe matrix; OpenAPI drift gate hard-fails; multi-arch GHCR image. Migrations 0043 through 0060 are additive and forward-only. PROMPT_VERSION 4.24.0 → 4.25.0 with GROUND RULE 9 (refuses dose recommendations) and GROUND RULE 15 (refuses drug-level estimates with MDR + MDCG 2021-24 cites).

See CHANGELOG.md for the full per-section delta.
2026-05-14 23:49:32 +02:00
Marc-André Bombeck ca03c6099f chore(release): v1.4.24
Security + accessibility hardening release. Closes the highest-priority
findings from a focused security audit of the v1.4.23 release:

- Bearer auth fail-closed when a route declares no scope (api-handler)
- Public /api/send Umami proxy now rate-limits 120 req/min/IP
- Proxy nonce generation switched to btoa(...) for Edge portability
- Session-expiry delete race uses deleteMany + catch (no more 500s)
- POST /api/import returns the standard apiError envelope on 422
- Runtime Docker image pins Prisma 7.8 + worker deps to minors
- Notifications page renders a real load-error alert
- Accessibility: switch aria-labels, htmlFor pairing, mobile admin
  Overview link, deep-link anchors on notification settings
- Chart overlays: per-chart comparisonBaseline pref, BMI/bodyFat/
  sleep/steps overlay keys, TrendCard layout tolerates long labels

Verification: pnpm typecheck && pnpm lint && pnpm test (2244/2244)
&& pnpm format:check all green.
2026-05-12 22:33:35 +02:00
Marc-André BombeckandClaude Opus 4.7 d2331d9756 chore(release): v1.4.23
Pre-iOS prep + hygiene. Apple Health backend foundation (schema +
batch ingest + APNs scaffolding), OpenAPI generator + warn-only CI
gate, per-device refresh-token revocation + device-management
endpoints, Coach Apple Health metric slot at PROMPT_VERSION 4.23.0,
per-user Coach prefs surface, per-message thumbs feedback loop.
Seven W5 hygiene items closed alongside.

User-visible delta is small on purpose — the weight is in the
contract surface the iOS app gets to compile against on day zero.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:30:23 +02:00
Marc-André BombeckandClaude Opus 4.7 f1e6630958 chore(format): prettier sweep across develop
Mostly table column normalisation under docs/audit/ + planning, object-
literal expansion in src/lib/ai/coach/snapshot.ts after the W4 F6 Apple
Health metric block landed, and small wrap/unwrap line adjustments in
the device + APNs routes / tests touched across W3/W4/W5 sessions.
No behavioural change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 08:27:51 +02:00
Marc-André BombeckandClaude Opus 4.7 d71e87983e chore(release): v1.4.22
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:32:02 +02:00
Marc-André BombeckandClaude Opus 4.7 363e73cbf2 chore: voice + filename hygiene + wave-4 state wrap
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>
2026-05-10 21:46:32 +02:00
Marc-André BombeckandClaude Opus 4.7 995528801c chore(release): v1.4.21
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 19:36:05 +02:00
Marc-André BombeckandClaude Opus 4.7 aac8b5ebdb chore(release): v1.4.20
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:24:37 +02:00
Marc-André BombeckandClaude Opus 4.7 a1cf9bcb37 docs: scrub PII + internal jargon + German leaks from user-facing artifacts
Retroactive cleanup of CHANGELOG.md and docs/audit/v1.4.X-summary.md
files. Removes maintainer's real name, specific health figures tied to
the live tenant, internal release-process jargon, and stray German
strings in English-only release notes. Pre-release PII-grep gate added
to the v1.4.20 release pipeline. No git history rewrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:14:28 +02:00
Marc-André BombeckandClaude Opus 4.7 89f00cf832 chore(release): v1.4.19
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:34:01 +02:00
Marc-André BombeckandClaude Opus 4.7 0243e20853 chore(release): v1.4.18
Per-chart overlay toggles, expanded achievements roster (incl.
hidden Easter-eggs), BD-Zielbereich 7T/30T sub-values wired,
chart visual revert to clean lines, and the AdminShell mobile
section-strip scrollbar fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 11:14:30 +02:00
Marc-André BombeckandClaude Opus 4.7 adab80ac81 chore(release): v1.4.17
Hotfix release for the legacy-payload /insights crash users hit on
2026-05-10 the morning after v1.4.16 shipped. See CHANGELOG.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 09:49:39 +02:00
Marc-André BombeckandClaude Opus 4.7 d443c22d4e chore(release): v1.4.16
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 03:39:53 +02:00
Marc-André BombeckandClaude Opus 4.7 4dcaa08b42 chore(release): v1.4.15
Bumps version to 1.4.15 and adds the corresponding CHANGELOG entry
covering backup completeness, achievements UI, onboarding tour,
doctor-report v2, auto-deploy on GHCR push, empty-states sweep,
notification + integration status UI, AI hallucination guardrails,
mobile UX fixes, and the BD-Zielbereich percentage fix.

Includes the formatting drift left over by the v1.4.15 development
phases (Prettier sweep on touched files) and a typecheck fix in
`dashboard-layout.test.ts` so `pnpm typecheck`, `pnpm lint`,
`pnpm format:check`, `pnpm test`, and `pnpm test:integration` all
pass green on the release tag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 22:27:13 +02:00
Marc-André BombeckandClaude Opus 4.7 e5fae9bce6 chore(release): v1.4.14
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:12:24 +02:00
Marc-André BombeckandClaude Opus 4.7 cfa8ea60a0 fix(ai): use plain gpt-5 model slug for ChatGPT-account auth
The Codex backend rejects `gpt-5-codex` when authenticated via the
ChatGPT-account OAuth flow with:

  "The 'gpt-5-codex' model is not supported when using Codex with a
   ChatGPT account."

The `-codex` suffix is API-key-auth-only. ChatGPT subscriptions route
through the plain `gpt-5` slug instead. Switch the client default and
expose a `CODEX_MODEL` env override for operators on plan tiers that
prefer a different slug (e.g. `gpt-5-codex-mini` or a future
preview model).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 15:10:54 +02:00
Marc-André BombeckandClaude Opus 4.7 e4a07bc2e4 fix(ai): complete Codex backend protocol implementation per spec
Stop iterating on individual 400 errors and implement the full Codex
Responses-API protocol once, from the source. Wrote a 713-line
reference at docs/codex-protocol-spec.md from the official
openai/codex Rust client; this commit is the TS implementation
against that spec.

Required pieces that were missing in v1.4.11:

- `ChatGPT-Account-ID` header — extracted from the `chatgpt_account_id`
  claim of the OAuth id_token (also accepts the URI-namespaced
  variant under `https://api.openai.com/auth`). Without it: 401.
- `originator`, `User-Agent`, `Accept: text/event-stream`,
  `session_id`/`session-id` (both spellings), `thread_id`/`thread-id`.
- Body fields `reasoning: null` and `include: []` are required even
  when no reasoning is requested.
- Model slug is `gpt-5-codex`, not `gpt-5.3-codex` (the latter is a
  test placeholder upstream).
- Refresh endpoint takes a JSON body, not form-urlencoded.

Storage codec rewrite: `codexAccessTokenEncrypted` now holds an
encrypted JSON blob `{ accessToken, accountId, expiresAt }` instead
of just the raw access token, so the account id stays available
across requests. v1.4.7-v1.4.11 records that lack the field cannot
be revived — `decryptCodexCreds` returns null for those rows and
`resolveCodexProvider` flips the connection to `expired`, prompting
the user to re-link once.

Removed the dead browser-flow routes (`/api/auth/codex/authorize`
and `/callback`). They never worked on hosted domains because
Hydra's redirect-URI allow-list for the public Codex client ID
covers only localhost; device-code is the only viable path.

CodexClient SSE parsing extended to handle `response.failed` and
`response.incomplete` terminal events, and to read the actual
routed model from the `OpenAI-Model` response header.

Tests rebuilt: codex-oauth covers the full device-code flow with a
synthesised id_token JWT, the JSON-body refresh, and the
storage-codec round-trip. CodexClient tests carry an `accountId`
through the constructor. All 720 tests pass; typecheck clean.

Refs: docs/codex-protocol-spec.md §1-§6 for every claim.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:36:12 +02:00
Marc-André BombeckandClaude Opus 4.7 e0de659fdd fix(ai): CodexClient consumes SSE stream from chatgpt.com backend
After v1.4.10 fixed the request body, the Codex backend started
returning 400:

  { "detail": "Stream must be set to true" }

The endpoint is streaming-only — `stream: false` is rejected
outright. The official Codex CLI consumes a Server-Sent-Events
stream (`codex-rs/codex-api/src/sse/responses.rs`); we have to do
the same.

CodexClient now requests `stream: true` and reads the response body
incrementally via the standard `ReadableStream` reader. Each SSE
message ends in a blank line; the `data:` payload is JSON. We listen
for three event types from the OpenAI Responses API protocol:

- `response.output_item.done` with an assistant `message` item — the
  final text is read directly from the message's `output_text`
  content. Preferred path because the backend has already stitched
  any deltas together.
- `response.output_text.delta` — incremental text chunks. Fallback
  when no `output_item.done` arrived (e.g. older models or
  reasoning-only responses), all deltas are concatenated.
- `response.completed` — picks up `usage.total_tokens`.
- `response.error` / `error` — bubbled as a thrown Error with the
  upstream message.

Tests rebuilt to drive the client with synthetic SSE bodies, plus
new coverage for the delta-fallback path. All 720 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:16:45 +02:00
Marc-André BombeckandClaude Opus 4.7 68e5797e48 fix(ai): Codex backend request body matches OpenAI Responses schema
After v1.4.9 the OAuth connect went green, but every real insight
call still failed with:

  Codex request failed (400): { "detail": "Input must be a list" }

The `chatgpt.com/backend-api/codex/responses` endpoint speaks the
OpenAI Responses API shape (`ResponsesApiRequest` in
`codex-rs/codex-api/src/common.rs`), where `input` is a list of typed
message items rather than a single string. CodexClient was sending
the older Chat-Completions-style body (`input: <string>`) which the
upstream rejects with the 400 above.

The body now mirrors the canonical shape:

  {
    "model": "gpt-5.3-codex",
    "instructions": "<system prompt>",
    "input": [
      { "type": "message", "role": "user",
        "content": [{ "type": "input_text", "text": "<user prompt>" }] }
    ],
    "tools": [], "tool_choice": "auto",
    "parallel_tool_calls": false, "store": false, "stream": false
  }

The response shape (Output `ResponseItem[]` with `output_text`
content) is unchanged, so `parseResponse` still works without
modification. Both Settings → KI "Verbindung testen" and the insight
generator now complete end-to-end through the ChatGPT subscription.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:34:12 +02:00
Marc-André BombeckandClaude Opus 4.7 3dbc8af627 fix(ai): device-code flow uses OAuth access token directly (v1.4.8 follow-up)
v1.4.8's device-code flow got the user as far as the chatgpt.com
approval, but the connect step at the end still failed with:

  Codex api-key exchange failed (401): "Invalid ID token: missing
  organization_id"

Root cause: the api-key exchange (RFC 8693, swap id_token for an
OpenAI API key) requires the id_token to carry an `organization_id`
claim. That claim only gets added when the authorize URL is hit with
`id_token_add_organizations=true` — which the browser/auth-code
flow does, but the device-code flow does NOT (the user types the
code on chatgpt.com's own page; we never control the authorize URL).

The official Codex CLI handles this by NOT calling the api-key
exchange in the device path: it persists the OAuth access_token /
refresh_token directly and uses them against
`https://chatgpt.com/backend-api/codex/responses` (the Codex backend
that bills against the ChatGPT subscription). v1.4.9 does the same.

`pollDeviceCode` no longer calls `obtainApiKey`; it returns the raw
OAuth access_token instead. New `refreshDeviceTokens()` mirrors that
on the refresh path. The provider rewires Codex back through the
existing `CodexClient` (bg backend-api), with proactive refresh
+ on-401 refresh both going through `refreshDeviceTokens`.

The browser/auth-code flow path is left in place — operators who
register a private OAuth app with OpenAI (e.g. for an enterprise
deployment) can still use the api-key exchange via the
`exchangeCodeForTokens` helper. For the public Codex client ID on
hosted instances, device-code is the only working path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:25:50 +02:00
Marc-André BombeckandClaude Opus 4.7 19eb1d95d6 fix(ai): switch ChatGPT-OAuth to device-code flow (v1.4.7-followup)
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>
2026-05-09 10:31:30 +02:00
Marc-André BombeckandClaude Opus 4.7 2f7f487228 fix(ai): Codex OAuth flow — correct issuer + api-key exchange
The "Connect with ChatGPT" button never actually completed an OAuth
flow. The implementation hit `chatgpt.com/authorize` and
`chatgpt.com/oauth/token`, which are not OAuth endpoints — ChatGPT
silently rendered its normal web app, dropped our params on the
floor, and the user landed on a regular logged-in chat session with
nothing else happening. There was no error and no callback, which is
why the regression survived multiple releases.

The canonical issuer for ChatGPT-OAuth is `auth.openai.com`. The
official `openai/codex` CLI confirms this in
`codex-rs/login/src/auth/manager.rs` and `codex-rs/login/src/server.rs`.
The flow is also slightly more involved than a vanilla PKCE
authorisation:

  1. PKCE authorize against `auth.openai.com/oauth/authorize` with the
     `openid profile email offline_access api.connectors.read
     api.connectors.invoke` scope set, plus `id_token_add_organizations`
     and `codex_cli_simplified_flow` flags so the resulting `id_token`
     carries the ChatGPT-subscription claims.
  2. Token exchange against `auth.openai.com/oauth/token` (form-
     urlencoded body, NOT JSON) yields `{ id_token, access_token,
     refresh_token }`.
  3. **Second token exchange** with grant
     `urn:ietf:params:oauth:grant-type:token-exchange`, swapping the
     `id_token` for an OpenAI API key (`requested_token=openai-api-key`).
     This key bills against the ChatGPT subscription rather than a
     separate API plan — exactly what the v1.2/v1.3 design intended.
  4. HealthLog stores the API key in `codexAccessTokenEncrypted`
     (re-purposing the existing column to avoid a migration) and the
     OAuth refresh token in `codexRefreshTokenEncrypted`. When the
     key is about to age out we refresh + re-exchange transparently
     before the next request.

The downstream provider is now a regular `OpenAIClient` tagged with
`providerType: "codex"` so logs and analytics still distinguish
ChatGPT-subscription billing from plain BYOK. The bespoke
`codex-client.ts` (which hit the chatgpt.com/backend-api/codex/responses
endpoint with the OAuth access token) is still on disk for now so the
prisma columns and the provider tag stay stable, but is unreferenced
by the active path.

The public Codex CLI client ID `app_EMoamEEZ73f0CkXaXp7hrann` is
shipped as a default; operators who registered a private OAuth app
with OpenAI can override via `CODEX_OAUTH_CLIENT_ID`. The
`isCodexOAuthConfigured()` gate is a no-op now because the default
always has a value, so the UI surface no longer silently hides the
"Connect with ChatGPT" button.

Tests cover the new shape end-to-end: PKCE generation, the
authorize-URL parameters, the form-encoded token exchange, the
api-key exchange, the refresh path, and the encrypt/decrypt
round-trip. The provider integration test is updated to expect a
`providerType: "codex"` OpenAIClient instead of the legacy
`CodexClient`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 09:31:13 +02:00
Marc-André BombeckandClaude Opus 4.7 a852612bfe chore(release): v1.4.6
Release notes in CHANGELOG.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 02:29:08 +02:00
Marc-André BombeckandClaude Opus 4.7 02b99553b9 chore(style): repo-wide prettier sweep before v1.4.6 tag
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>
2026-05-09 02:27:17 +02:00
Marc-André BombeckandClaude Opus 4.7 e8f4820018 fix(dashboard): tile strip back to CSS Grid for true symmetric width
Marc's v1.4.4 feedback: the tiles look better when they're all
exactly the same size and the strip begins and ends at the same
x-coordinate as the charts — the v1.4.4 `flex-1 basis-0` distribution
was close but not quite symmetric, and at 6 tiles the right edge
of the strip didn't line up with the charts below.

Switched to the v1.3-era pattern but with the modern `auto-fit +
minmax(9rem, 1fr)` form so it doesn't need explicit breakpoints:

  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 0.75rem;

CSS Grid puts every tile in an identical 1fr track. Width is
literally identical, gaps are symmetric, and the strip is bounded
by the same parent box as the charts so left/right edges align by
construction. When the row no longer fits a 9 rem floor the grid
wraps to a new row — Marc explicitly tested both and prefers the
wrap over the v1.4.x horizontal-scroll for the symmetry it
preserves.

Tests + typecheck green (671/671).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 00:05:35 +02:00
Marc-André BombeckandClaude Opus 4.7 cbded7c39e fix(dashboard): symmetric tile widths, BP back to two tiles
Marc's v1.4.3 feedback:
  - Tiles end up the same height now but uneven widths — they should
    distribute symmetrically across the strip.
  - BP shouldn't be one combined tile; sys + dia should sit side by
    side as two distinct tiles like every other metric.

Two fixes:

1. **Strip wrapper switches from `shrink-0 grow basis-[10rem]` to
   `flex-1 basis-0 min-w-[9rem]`.** With `basis-[10rem]` the floor
   was 160 px, and tiles whose content rendered wider (the combined
   BP tile rendering "117.0/79.0 mmHg" plus a paired avg row) pushed
   past their share, breaking symmetry. `flex-1 basis-0` starts every
   tile from zero, so the row-width gets divided evenly regardless of
   how much text each card contains. `min-w-[9rem]` is the readable
   floor — if the row no longer fits at that width, the parent's
   existing `overflow-x-auto` lets the user swipe instead of wrap.

2. **BP renders as two tiles again.** Reverted the v1.4.3 combined
   `secondary` rendering: the dashboard now pushes a `bp-sys` and
   a `bp-dia` entry, ordered with a 0.001 sub-step so dia stays
   immediately after sys without colliding with the next widget's
   integer order. The `secondary` prop on TrendCard stays in the
   API for callers that *do* want a paired-value tile.

Tests + typecheck green (671/671).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 22:14:24 +02:00