Final-pass hygiene for the W4d GLP-1 integration:
- `buildGlp1SnapshotBlock()` and `detectGlp1Plateau()` short-circuit
to `null` when the test environment mocks prisma partially (only
`measurement` / `moodEntry` mocked, `medication` undefined). Matches
the production behaviour for accounts without an active GLP-1 row.
- DE locale parity: adds the matching German string for
`insights.subPage.therapyTimelineDescription` that landed without
its de.json counterpart.
- Drops unused `locale` destructure on the GLP-1 medication card and
the dead `DailyValueRow` interface in the snapshot helper.
- Regenerated `docs/api/openapi.yaml` so the new GLP-1 endpoints
(/api/medications/[id]/glp1, /api/insights/glp1-timeline) appear
in the spec.
The W7 timezone agent committed an openapi/routes.ts change without
running pnpm openapi:generate. This regeneration brings the committed
yaml back in sync (cosmetic whitespace normalisation on multi-line
descriptions — no schema changes from this run).
HealthKit's API does not surface a stream of deleted UUIDs, so the iOS
HealthLog client performs a periodic 30-day reconciliation: it diffs
HealthKit's current UUID set against the server's externalIds and
posts the surplus here. This endpoint deletes the user's matching
rows and never touches another user's data (Prisma's deleteMany with
the userId predicate is the cross-user 404 guard).
Contract:
- Body: { externalIds: string[] } with min 0, max 500 entries.
- Auth: requireAuth() — session cookie or bearer.
- Empty array: returns { deletedCount: 0 } with 200 (no-op, not a
failure — matches the ingest path's symmetric tolerance).
- Over-cap: 422 with errorCode "measurement.delete.too_large" so
the iOS client can surface a specific diagnostic.
Integration coverage: happy path, cross-user 404 guard, empty array,
and batch-cap rejection. The cap (500) mirrors POST /measurements/batch
so the two surfaces share one operational ceiling.
OpenAPI: registered as DELETE on /api/measurements/by-external-ids
with the Measurements tag; req/res schemas reuse the same z.array cap
the runtime validator enforces. openapi.yaml regenerated.
v1.4.25 W7 phase 3.4 — proposal §3 symptom 10.
The schema patterns on every 'format: date-time' field already
accept both 'Z' and '±HH:MM' suffixes (the Zod sources use
{ offset: true }), so the wire contract is technically correct. The
discoverability problem is that consumers reading the spec assume
'date-time' implies UTC.
Adds an explicit 'Date-time contract' paragraph to the API
description naming the convention: server responses emit the
requesting user's offset when a user context is present (exports,
doctor-report PDF), background-job-authored payloads emit 'Z',
clients must accept both.
Regenerated docs/api/openapi.yaml via pnpm openapi:generate; check
passes.
v1.4.24 (security + accessibility hardening) shipped directly on main as
a hotfix-style release. Back-merging the two main-only commits
(ca03c60 chore(release): v1.4.24, 604009d per-user timezone proposal)
into develop so the v1.4.25 work continues on the standard branch model.
# Conflicts:
# .planning/ROADMAP.md
STATE ticks all Wave 7 sub-items: pre-release verify, format + planning
+ openapi sweep commits, version bump + CHANGELOG, release-merge, tag,
GHCR build, Coolify deploy (via host-side retag fallback after the
auto-deploy path skipped on missing secrets and the :latest digest
cache pinned to v1.4.22), /api/version=1.4.23, smoke, GH release,
docs + landing sync, release brief, v1.5 P1 plan refresh cross-link.
ROADMAP moves the v1.4.23 milestone into the Previous-milestone slot
(Waves 1-7 all done) and scaffolds v1.4.24 at the top with the
deferred items from v1423-backlog: Pearson incomplete-beta, OpenAPI
drift gate flip, settings-cog UX consolidation, NextRequest URL mock
fix, security MED + LOW cluster, S-05 simplify.
Release brief at docs/audit/v1423-summary.md mirrors the v1421-summary
structure: lead, live state, smoke, wave-by-wave overview, iOS
readiness statement with the 13 DTO open questions cross-link, branch
model, carry-overs, strategic next.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zod-openapi emits long descriptions inline; the committed copy carried
a stale newline+indent fold. Generator output is what `pnpm openapi:check`
compares against, so re-snapshot before tagging.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
- Coolify runbook + audit briefs scrub the live deploy URL to a
placeholder (Sec-MED-2). Real values live in repo secrets.
- /api/auth/me/devices isCurrent now compares against the session's
device id, not the unauthenticated X-Device-Id header (Sr-MED-5).
- Migration 0036 sleep-duration unit shift documented + verification
command added to the coolify-auto-deploy howto.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The v1.4.22 prose rewrite shipped without a way for users to signal
whether the new tone is landing. v1.4.23 H7 closes that loop with a
light-touch thumbs row + an admin first-week dashboard.
Schema: extend `RecommendationFeedback` with a polymorphic
`target_type` column (default "recommendation" so legacy rows stay
representative) plus a nullable `reason` field. New index on
(target_type, created_at) so the aggregator can scan per-surface
buckets without a full sweep. Migration `0040_recommendation_feedback_target_type`.
Route: POST /api/insights/chat/messages/:id/feedback. Body
{ rating: "helpful" | "unhelpful", reason?: string }. Verifies
ownership (404 on cross-account messages — no existence leak),
decrypts the message body for the dedup key, and writes the row with
target_type="coach" + a metric-source string encoding the user's
active prefs (`coach:tone=warm:verbosity=default`). Per-user 60/h rate
limit. 409 on a P2002 (already_rated) so a double-click is a no-op.
Aggregator: new `buildCoachFeedbackBuckets()` slices Coach rows by
(promptVersion, tone, verbosity). The summary blob the worker writes
to AppSettings.adminAiInsightsFeedbackSummary now carries both
`buckets` (pre-existing) and `coachBuckets` (H7 addition). Targets
the same singleton row so no second worker handler is needed.
Admin view: `/admin/coach-feedback` (new section slug) renders the
Coach buckets as a small DataTable (prompt version × tone × verbosity
× helpful × not-helpful × n × helpful-rate). Helpful-rate column
tinted via Dracula tokens (green ≥80%, yellow ≥50%, orange below).
First useful question this answers: "Is the v1.4.22 prose rewrite
landing well, or did the warm tone overshoot?" If the helpful-rate
for PROMPT_VERSION 4.23.x drops below 50% within the first 100
ratings, v1.4.24 walks the persona back.
UI: `<ThumbsUp>` / `<ThumbsDown>` buttons (lucide icons, no emoji)
under each persisted assistant bubble. Streaming bubbles + refusals
+ error bubbles skip the row so users can't rate before the message
lands on disk. Localised "Thanks for the signal." after submit.
OpenAPI: spec carries the new POST route + the request schema.
Coverage now includes the H4 + H7 routes — three additions over
v1.4.22 baseline.
Tests +9 unit (aggregator Coach-bucket slicing, admin section
render-paths) plus an integration round-trip for the route.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The v1.4.22 B5 audit removed the placeholder cog from the Coach
drawer header because it had no real wiring. v1.4.23 H4 returns
the cog with an actual settings surface backed by a per-user
preferences row.
Schema: nullable JSONB column `users.coach_prefs_json` (migration
0038_coach_prefs). Null = legacy v1.4.22 defaults; non-null carries
`{ tone, verbosity, excludeMetrics, showEvidenceByDefault }` validated
by the Zod schema in `src/lib/validations/coach-prefs.ts`.
Routes: GET + PUT `/api/auth/me/coach-prefs`. PUT validates the body,
persists the canonical defaulted form, and emits an
`auth.me.coach-prefs.put` annotation with the four-knob meta. OpenAPI
registry covers both verbs; the spec regenerates without drift.
Coach prompt builder: `getCoachSystemPrompt(locale, prefs)` prepends a
per-user OVERRIDE paragraph for non-default tone (neutral / concise)
and verbosity (brief / detailed). Concise tone forces brief verbosity
regardless of the explicit selection — concise == short. Default
prefs leave the prompt unchanged so the v1.4.22 train stays in place.
Snapshot builder: reads the prefs row BEFORE any measurement query
and intersects `excludeMetrics` with the resolved scope. The model
never sees data the user opted out of — that's the load-bearing
order: filter first, query second.
UI: settings cog in the drawer header opens a right-edge `<Sheet>`
with three Select pickers (tone, verbosity) and one Switch column
(per-metric exclusions + showEvidenceByDefault). Save calls the PUT
route and invalidates the `["coach-prefs"]` query so the next reply
honours the new defaults without a reload. EN + DE i18n keys for
every label and hint.
Message-thread reads the same `["coach-prefs"]` cache and toggles the
`open` attribute on the evidence `<details>` element when
`showEvidenceByDefault` is true.
Tests +24 unit (coach-prefs schema, system-prompt prefix, snapshot
mock for new prefs row). Integration round-trip seeds a session,
PUTs a non-default prefs shape, asserts the GET reflects the saved
values + the DB row carries the canonical defaulted form.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the v1.4.23 baseline route table — auth/login, passkey verify,
refresh, measurements GET + POST + batch, devices POST, insights
comprehensive — into the new generator. Schemas reuse the existing
validation files via Zod v4's `.meta()`, so the iOS DTO contract stays
single-source-of-truth.
The freshly emitted spec covers ~880 lines (down from the 5468-line
hand-maintained legacy preserved in `openapi-v1422-legacy.yaml`).
Future routes get added incrementally via `src/lib/openapi/routes.ts`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires the OpenAPI 3.1 generator scaffolding ahead of the iOS-touched
route registrations. `zod-openapi` (samchungy variant, picked in W1
stream-3) reads Zod v4's native `.meta()` so schemas can stay in their
original validation files; `yaml@2` with `sortMapEntries: true` keeps
the emitted spec stable across re-runs.
`pnpm openapi:generate` overwrites `docs/api/openapi.yaml` from the
registry; `pnpm openapi:check` exits non-zero on drift. The legacy
hand-maintained spec is preserved at `docs/api/openapi-v1422-legacy.yaml`
so the iOS contract reference doesn't disappear during the
incremental migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures the same-day patch on top of v1.4.20: Daily Briefing
regenerate fix, Coach day-level snapshot, duplicate streaming bubble,
drawer header, scope picker UI. STATE + ROADMAP rolled forward; v1.4.22
slot scaffolded for the e2e fix wave already in flight on develop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Marc-Brief plus what-landed / deferred / CI / production-deploy /
v1.4.20-preview sections. Mirrors the v1.4.18 report shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marc-Brief + per-area landed/deferred breakdown for the v1.4.18
release. Charts walked back from gradients/emoji/auto-mean to clean
lines + opt-in per-chart overlay toggles, achievements roster grew
38 → 59 with 6 hidden Easter-eggs, BD-Zielbereich tile sub-values
wired, /admin/api-tokens scrollbar finally pinned to AdminShell.
STATE.md flipped to `finished`; final status block + Phase E3 block
appended; Phase E checklist all-ticked. Phase E1 and E3 reports
included with this commit (drift left from earlier marathon waves).
v1.4.18 LIVE — image digest sha256:c636fca7db66…, /api/version=1.4.18.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Whitespace-only changes from the same pretter pass that cleared
the rest of the tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closing brief for the v1.4.16 release: live image digest, what shipped
across waves A-E, what was deferred to v1.5, CI/test status, production
state, docs/landing sync. STATE.md status flipped to finished.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The git-push trigger that causes Coolify container churn during marathon
work is the GitHub-App "Auto Deploy" toggle on the application — it
fires on every push to `main`, including doc-only commits. Disabling
it leaves the Actions-driven webhook (already shipped in v1.4.15) as
the sole trigger.
The Coolify v4-beta MCP API does not expose this toggle programmatically.
Marc-side UI flip is the realistic fix; doing it now without API access
would require either a Watchtower install (larger blast radius — vetoed
in the v1.4.15 audit and re-vetoed here) or hand-issuing a Coolify token.
Audit doc lays out the manual step + the v1.5 plan to automate it once
the API exposes `auto_deploy_enabled`. Marc can flip the toggle whenever
convenient; does not block v1.4.16 release.
Single-pass audit of every /settings/<slug> route + section component
+ i18n keys. Documents inventory, naming drift, top/bottom-split
anti-patterns, duplicate functionality, settings-vs-admin scope rule,
and the three follow-up refactor commits. AI section flagged for B2,
Export section already handled by B7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Phase C1 (v1.4.15) — closes the last acceptance criterion.
Marc, verbatim 2026-05-09: "können wir auch gerne in vielen
verschiedenen Iterationen weiter das ausarbeiten" ("happy to keep
iterating in many further passes"). This document is the iteration
roadmap that explicit invitation called for.
Captures everything intentionally deferred from v1.4.15:
v1.4.16
- Medical-reference grounding: hand-curated bundle of ~40 short
excerpts (ESH/ESC 2024, AHA, WHO, AASM, DGE/DEGAM); pre-flight
`selectRelevantReferences(features)` injects 3-5 into the
system prompt; schema extension `referenceId` rejects non-bundle
citations.
- Multi-provider redundancy: `MultiProviderCascade` wrapping the
`AIProvider` interface (already in place from this phase's
refactor commit). Per-tenant order in admin settings.
- Per-recommendation explainability: schema gains
`rationale.{dataWindow, comparedTo, deviation}`; UI renders a
"Why?" tooltip wired through to the linked reference excerpt.
- Route-side wiring: `/api/insights/generate` adopts
`generateInsight()` + the strict prompt; UI components migrated
to the new shape.
v1.4.17
- User-feedback loop: per-recommendation helpful / not-helpful;
`InsightFeedback` table; admin dashboard `/admin/ai-quality`
with helpful-rate by recommendation type, locale, provider,
`PROMPT_VERSION`. Ratchets the prompt revision workflow.
- Confidence score per recommendation: calibrated from the
feedback loop's ground-truth.
v1.5
- Optional "deep mode" with Codex reasoning summaries —
per-user toggle, separate `reasoning` schema field, never
folded into `summary` (5x token cost).
Also documents the operational rule: re-fetch
`models-manager/models.json` before each iteration to keep
`DEFAULT_SLUG_FALLBACK_CHAIN` aligned with the upstream allow-list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase C1 (v1.4.15) — slug-drift defence per Marc's hard mandate:
"Die Integration des Slug Drift Risiko darf halt immer überhaupt nicht
sein." ("Slug-drift risk must NEVER happen.")
Background: the ChatGPT-Codex backend rotates its allow-list silently.
v1.4.7..v1.4.13 lost a week to chasing the rotation manually
(`gpt-5-codex` → `gpt-5.3-codex`). A single hard-coded `CODEX_MODEL`
flips invalid the moment upstream rotates it.
Solution — the client now walks an ordered fallback chain on every
fresh request series and caches the first slug that returns 200 +
`response.completed` for 1 hour. Subsequent requests skip the chain
walk entirely on a cache hit.
Spec: extends `docs/codex-protocol-spec.md` with new §7b documenting
the chain semantics, cache TTL, walk triggers (400 + "not supported
when using Codex with a ChatGPT account" / `model_not_found` /
"does not exist" / 404), non-walk responses (5xx, 429, 401-with-
refresh, `invalid_prompt`), and the all-failed structured-503 error
shape. Per Marc's v1.4.6 hard rule ("read source-of-truth before
iterating; no more 6-iteration line-fixes"), the spec was extended
FIRST and the implementation cross-referenced against it.
Implementation:
- New `src/lib/ai/codex-slug-cache.ts` — process-local positive
cache. Single-slot Map (per-account scoping is a one-line change
when v1.4.16 multi-tenant rolls out). 1 h TTL. `getCachedCodexSlug`
auto-evicts expired entries on read.
- Rewritten `CodexClient.generateCompletion()` — walks the chain
`[cached?, ...slugChain]` in order. On 401 → refresh + retry SAME
slug (auth ≠ slug-state). On 400/404 with rejection body → drop
cache, walk to next. On 5xx/429 → propagate immediately (walking
won't help). On all-rejected → throw structured 503 with
`attempted[]` list, `httpStatus: 503`, message "AI provider
unreachable — all configured Codex slugs were rejected".
- `CodexAttemptDiagnostics` exposes `{ attempted, cacheState,
workingSlug }` for Wide-Event annotations the route can read via
`client.getLastDiagnostics()`.
- `CODEX_MODEL_FALLBACK_CHAIN` env override (comma-separated). When
`CODEX_MODEL` is also set, it folds into chain position 0; the
default chain (`gpt-5.3-codex` → `gpt-5-codex` → `gpt-5` →
`gpt-4o`) survives behind it. Stable de-dup.
Tests: +20 across 2 new files.
`codex-slug-cache.test.ts` (7 tests) — TTL-edge boundaries,
invalidation, replacement semantics.
`codex-slug-fallback.test.ts` (13 tests) — cold-cache happy path,
walk on canonical 400 body, walk on `model_not_found`, walk on 404,
no-walk on 500/429/401, all-rejected → 503-with-attempted-list,
cache-hit shortcut, cache invalidation on rejection, isSlugRejection
body classifier, loadFallbackChain env-pin merge.
The 5 existing `codex-client.test.ts` cases continue to pass without
edit — the new `slugChain` constructor parameter is opt-in and the
single-slug behaviour reduces to chain length 1.
Tests: 1028 → 1048 unit pass (+20). Typecheck clean for AI files.
Lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Walks every list/table/tile a brand-new user hits and records the empty
path. 13 surfaces flagged for upgrade across admin, feature pages,
insights and dashboard. Adds matching EN+DE i18n keys
(emptyTitle/emptyDescription/CTA copy) under measurements, mood,
medications, admin.section.users/backups, admin login-overview/api-tokens,
admin.feedback, insights, dashboard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase C4 audit found EN/DE locale files in perfect parity (1817 keys
each, 0 drift, 0 empty values, 0 TODO/FIXME placeholders) — the five
B-agents and the C2/C3 phases each shipped their own translations
inline.
This commit hardens the parity test to lock that state in:
- empty-string assertion (per locale, fails on `"key": ""`)
- key-equals-value assertion (fails when EN==DE==key.lastSegment, with
an explicit allow-list for 5 legitimate cases: `settings.ntfy`,
`classifications.{bp.Optimal,bp.Normal,pulse.Normal,bodyFat.Fitness}`)
- TODO/FIXME/XXX/TBD assertion (per locale)
EN-only key=value cases (e.g. `"of": "of"` where DE = `"von"`) are
deliberately NOT flagged — those are natural, since key names derive
from the EN word. The dual-locale check catches the real placeholder
pattern (`"bugReport": "bugReport"` in both locales) without false
positives.
Audit report: docs/audit/v1415-i18n-coverage.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-v1.4.15 the page silently dropped locked achievements, so the user
only saw what they had already earned. Now every badge renders — locked
ones are grayed out with a "Locked" badge, criterion hint
({current} / {target}), and a progress bar. Unlocked entries keep the
existing primary-gradient card with the "Completed on …" footer.
Achievements are grouped by a new `category` axis (medication, vitals,
security, engagement) — derived in `getAchievementCategory()` so a
metric is always in exactly one bucket. Inside a category, unlocked
items sort first; among locked items the closest-to-unlock one ranks
first so the user always sees their immediate next goal at the top.
The sidebar link to /achievements was already in place under the
user-area group; this commit only adds the new locked/grouped render
plus an e2e smoke spec that walks the desktop sidebar → click →
heading visible path.
i18n: 9 new keys under `achievements.*` (locked, criterionHint,
progressPercent, categories.{medication,vitals,security,engagement},
dashboardCard.{title,viewAll,empty}) — EN + DE. C4's parity audit will
sweep later.
Tests: +9 unit (groupByCategory ordering + page render in EN/DE) +
1 e2e (sidebar → /achievements). 909 / 909 unit pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Marc-Brief (DE) at the top with what shipped, what's fixed, perf
deltas; below it the commit-by-commit landing report, deferred items,
CI/prod state, Codex-OAuth resolution, and the v1.4.15 backlog seed.
STATE.md flipped to finished.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The v1.5.0 release was redirected to v1.4.14 because it is bugfix-heavy
with only additive features, so semver-patch is the correct increment.
Rename the v15-* audit notes and the v15-1 backlog accordingly, and log
the docs/landing rebrand (commits 06bc616 docs, 92c6588 landing) under
Phase 9 in STATE.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Top-10 friction points from the canonical user journey
(login → dashboard → add measurement → view insights → admin overview
→ admin users → admin backups → settings/integrations → settings AI →
logout). Each finding has severity, file location, status (fixed
tonight / deferred), and a v1.5.1 backlog of the LOW items.
Notable: the audit was driven by static code review because Node 25
on Marc's local machine surfaces the documented `Reflect.get
private-member` regression in `api-handler.ts`, blocking both `next
build` and `next dev`. CI runs on Node 22 (canonical) and will
validate the reactivated `/admin` axe-core spec on the next push.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Captures Playwright+PerformanceObserver measurements against
production (https://healthlog.bombeck.io, /api/version=1.4.13) at
2026-05-09T14:14Z. Four pages × two viewports (1920x1080 desktop,
Pixel 5 mobile): /, /settings/integrations, /admin, /insights.
Key findings:
- /insights eagerly imported Recharts (108.5 KiB) — fixed in
bb2b1de via per-symbol next/dynamic.
- Dashboard checklist fired 2 redundant API calls per load for
established users (~950 ms wasted) — fixed in 519e36e by gating
on user.onboardingCompletedAt.
- Recharts replacement deferred to v1.5.1 (no new deps in v1.5).
Local re-measurement is blocked by the Node-25 turbopack regression
already noted in CLAUDE.md; verification will land via the v1.5.0
prod measurement once Coolify force-pulls the new bundle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up the prettier line-wraps the v1.5 phase-1 planning docs and
codex-protocol-spec.md missed, and brings the redact.ts + insights
generate route test into the project's standard form. No behaviour
change — pnpm format:check now passes clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
v1.4.13 swung from gpt-5-codex to plain gpt-5, but the chatgpt.com
backend rejects both with the same error:
{"detail":"The 'gpt-5' model is not supported when using Codex with
a ChatGPT account."}
The accepted slugs come from the bundled `models.json` in
`codex-rs/models-manager/` — neither `gpt-5` nor `gpt-5-codex` is in
the current bundle. `gpt-5.3-codex` IS in the bundle and is available
on Plus/Pro plans (per `available_in_plans`); verified live against
Marc's account today.
Operators can still override via `CODEX_MODEL` env var if they're on
a different plan tier.
Spec updated with §7a documenting the chatgpt-account-auth allow-list
constraint — the lesson is that the model_migration.rs slug references
are migration *prompts*, not wire slugs. Always cross-check
`models.json`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
Marc-facing morning brief at the top, full per-finding list,
deferred-to-v1.5 backlog, production state, docs/landing sync log,
and the multi-agent QA reconciliation.
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>
Add .planning state files and check in the v1.4.6 findings spec so
the workstream and the source-of-truth audit live in git from here on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Revert "revert: roll source tree back to v1.4.0 to restore production"
This reverts commit e444a90b10.
* fix(v1.4.2): summaries.WEIGHT + medications array guards + healthcheck IPv4
Three production bugs surfaced after the v1.4.0 deploy stabilized:
1. `TypeError: undefined is not an object (evaluating 'E?.summaries.WEIGHT')`
on /dashboard and /insights for users without weight readings —
the optional chain stopped at `data` when it needed to also cover
`data.summaries`. Fixed: `data?.summaries?.WEIGHT` (one level deeper).
2. `TypeError: y?.filter is not a function` on /medications — the
medications query value was non-array (likely a stale
service-worker response from an older API shape). Replaced with
`Array.isArray(medications) ? medications : []` so the page is
immune to any future shape drift.
3. Healthcheck-induced 503 outage (already in 1.4.1 — included here
so 1.4.2 is the single coherent release). The docker-compose
healthcheck used `localhost` which busybox-wget resolves to IPv6
::1; Next.js standalone listens on IPv4 0.0.0.0 only. Pinned
to 127.0.0.1.
This release also includes everything from v1.4.1 (which never
published a clean GHCR tag): the per-section admin extraction, the
testcontainers integration suite, the Playwright + axe-core E2E
foundation, the moodLog SSRF fix, the central error-redaction in
WideEventBuilder + Glitchtip, the medical-citation drift fixes
(ESH 2023, Saint-Maurice attribution, steps source), and the docs
sync.
Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
Production at healthlog.bombeck.io has been 503-ing since the v1.4.1
deploys started landing on apps-01 (Coolify). The container boots —
Next.js prints "Ready" and the pg-boss workers run — but never
accepts HTTP on :3000, so the Docker healthcheck fails and Traefik
takes the upstream out of rotation. A manual restart, a Coolify
force-rebuild, and a docker-compose pin to the GHCR :1.4.0 multi-arch
image all failed to bring the site back up — Coolify rebuilds the
image from main HEAD on every deploy regardless of the compose
directives.
This commit resets the working tree to commit 21bd46d (v1.4.0
release). Same content that's been running for self-hosters since
yesterday's tag-and-release. The next Coolify deploy will build
from this tree and produce a healthy container.
The v1.4.1 work is NOT lost:
- PRs #144, #145, #137, #146, #147, #148, #149, #150 remain in
git history.
- Their commits are still tagged (`v1.4.1`), still on the GHCR
multi-arch image (`ghcr.io/mbombeck/healthlog:1.4.1`), still in
the GitHub Release notes.
- Self-hosters who have already pulled the v1.4.1 image keep it.
- Local development continues from main HEAD with the v1.4.1
code — the regression only surfaced under the Coolify build
flow.
Re-applying v1.4.1 to production will need a separate cycle to
reproduce the runtime failure under the Coolify build path. That
work is tracked in docs/ops/v141-followup-issues.md (added back
when the tree is reapplied) and the deploy gating in
.github/workflows/e2e.yml will catch this class of bug going
forward.
No DB migration. No env-var change. No API contract change.
Co-Authored-By: Marc-André Bombeck <mbombeck@gmail.com>
CHANGELOG entry covers the three buckets of change between 1.4.0 and
1.4.1: security hardening from the audit pass (moodLog SSRF closed
+ centralised error redaction), citation-accuracy fixes (ESH 2023
label, Saint-Maurice attribution, steps source), and the CI safety
nets (executable Postgres testcontainers + Playwright/axe-core
foundation), plus the structural per-section admin extraction and
the in-tree docs sync. The deferred items from the audit pass land
in docs/ops/v141-followup-issues.md so they're discoverable for the
v1.4.2 cycle without re-running the audit.
package.json bumped 1.4.0 → 1.4.1.
No DB migration. No env-var change required. No API contract
change.
The v1.4 marathon and the v1.4.1 cycle that followed (per-section
admin extraction, Postgres testcontainers integration suite,
Playwright + axe-core E2E foundation) landed several user- and
operator-facing changes that hadn't been backfilled into the docs
shipped inside the repo. This commit closes that drift.
README.md
- Adds Multi-tenant ready and Test connection buttons feature
bullets.
- Tech-stack model count 25 → 26 (RefreshToken added in v1.4.0).
- API reference: new "Public + v1.4 additions" section listing
/api/version, the five Test connection endpoints, the two refresh
endpoints, and the two new admin endpoints (status-overview,
backup/test).
AGENTS.md
- Status block rewritten to reflect what shipped in 1.4.0 vs
what's in flight for 1.4.1.
- File layout: settings monolith replaced by /settings/[section]
routing, admin/page.tsx now the 77-LOC shell, src/components/
settings/ and src/components/admin/ added.
- Removed the stale "Settings Page" gotcha (about non-blocking
set-state-in-effect lint); the rule is strict now.
- Database Models 25 → 26.
CLAUDE.md
- Same model-count and component-folder updates as AGENTS.md.
- Adds Multi-tenant prep and Native API client patterns under
Important Patterns (HEALTHLOG_PROCESS_TYPE, ENCRYPTION_KEYS,
BACKUP_S3_*, refresh-token rotation).
- Lists tests/integration/ and e2e/ in File Layout.
docs/api/openapi.yaml
- Adds 11 new endpoints: GET /api/version, the five test endpoints
(Withings, moodLog, Web Push, Glitchtip, Umami), the two refresh
endpoints, GET /api/admin/status-overview, POST /api/admin/backup/test.
- The bulk of the diff is prettier reformatting that the existing
file had drifted from (single → double quotes throughout YAML);
no semantic change.
docs/migration/v1.3-to-v1.4.md
- Corrects the now-wrong claim that v1.4 ships no migrations
(it ships 0025_refresh_tokens + 0025_user_locale_drift_fix).
- Adds full env-var sections for the Worker/Web split, Encryption
key versioning, and Off-host backup target.
- Documents URL changes (/settings#anchor → /settings/[section]).
- Replaces the "deferred to a future release" list with a
landed-in-v1.4 vs landed-in-v1.4.1 status table.