mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
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>
1172 lines
62 KiB
Markdown
1172 lines
62 KiB
Markdown
# Changelog
|
||
|
||
## [1.4.13] — 2026-05-09
|
||
|
||
### Fixed — KI Insights via ChatGPT
|
||
|
||
- **Modell-Slug für ChatGPT-Auth korrigiert.** Der Codex-Backend
|
||
lehnt `gpt-5-codex` mit ChatGPT-Subscription ab
|
||
(`The 'gpt-5-codex' model is not supported when using Codex with a
|
||
ChatGPT account.`) — dieses Slug ist nur für API-Key-Auth gültig.
|
||
Wechsel auf `gpt-5`, das Standard-Modell der ChatGPT-Plus/Pro-Tarife.
|
||
Operator-Override via `CODEX_MODEL`-Env-Var möglich falls dein
|
||
Plan einen anderen Default kennt.
|
||
|
||
## [1.4.12] — 2026-05-09
|
||
|
||
### Fixed — KI Insights via ChatGPT (komplette Codex-Integration)
|
||
|
||
- **Codex-Backend-Aufruf jetzt vollständig nach Spec.** Die letzten
|
||
Versionen haben den Connect-Schritt richtig hinbekommen, aber der
|
||
eigentliche Insight-Call ist iterativ an immer neuen 400ern
|
||
gestorben. Statt weitere Trial-and-Error-Iterationen zu fahren
|
||
habe ich das vollständige Codex-Backend-Protokoll aus dem
|
||
offiziellen `openai/codex`-Quellcode dokumentiert
|
||
(`docs/codex-protocol-spec.md`) und einmal sauber dagegen
|
||
implementiert. Was alles fehlte: der Header `ChatGPT-Account-ID`
|
||
(ohne den 401, kommt aus dem `chatgpt_account_id`-Claim im JWT
|
||
id_token); die Header `originator`, `User-Agent`, `Accept`,
|
||
`session_id`, `thread_id`; die Body-Felder `reasoning: null` und
|
||
`include: []`; das richtige Modell-Slug `gpt-5-codex` (statt des
|
||
Test-Placeholders `gpt-5.3-codex`); JSON-Body beim Refresh
|
||
(vorher form-urlencoded); Persistenz des `accountId` neben dem
|
||
Access-Token. SSE-Streaming wird unverändert konsumiert
|
||
(output_text.delta + output_item.done).
|
||
- **Aufräumen:** der Browser-Authorization-Code-Pfad
|
||
(`/api/auth/codex/authorize` + `/callback`) ist gelöscht — er
|
||
funktioniert auf hosted Domains grundsätzlich nicht (Hydra-
|
||
Whitelist nur für localhost), war aber als Safety-Net noch da.
|
||
Device-Code ist jetzt der einzige Pfad.
|
||
- **Re-Connect nötig:** wer in v1.4.7-v1.4.11 schon connected hat,
|
||
muss einmal "Trennen" + "Mit ChatGPT verbinden" — die alte
|
||
Storage-Form trug die `accountId` nicht und kann nicht
|
||
nach-bereichert werden.
|
||
|
||
## [1.4.11] — 2026-05-09
|
||
|
||
### Fixed — KI Insights via ChatGPT
|
||
|
||
- **Codex-Backend bekommt jetzt SSE-Streaming.** Nach dem v1.4.10-
|
||
Body-Format-Fix kam der nächste 400er: `Stream must be set to true`
|
||
— der `chatgpt.com/backend-api/codex/responses`-Endpoint akzeptiert
|
||
ausschließlich Server-Sent-Events-Antworten, kein synchrones JSON.
|
||
v1.4.11 baut den CodexClient so um, dass er die Antwort als SSE
|
||
konsumiert: `output_item.done`-Events liefern den vollständigen
|
||
Assistant-Text, `output_text.delta`-Chunks dienen als Fallback,
|
||
`response.completed` trägt die Token-Usage. Damit läuft das gesamte
|
||
KI-Insights-Feature jetzt durch dein ChatGPT-Abo.
|
||
|
||
## [1.4.10] — 2026-05-09
|
||
|
||
### Fixed — KI Insights via ChatGPT
|
||
|
||
- **Codex-Backend bekommt jetzt das richtige Request-Format.** Nach
|
||
dem v1.4.9-Connect war der OAuth durch, aber jeder echte Insight-
|
||
Call ist mit `Codex request failed (400) — "Input must be a list"`
|
||
gestorben. Der `chatgpt.com/backend-api/codex/responses`-Endpoint
|
||
spricht das OpenAI-Responses-API-Schema (`input: ResponseItem[]`)
|
||
und nicht das Chat-Completions-Schema mit String-Input. v1.4.10
|
||
baut den Body korrekt — `input` ist eine Liste mit einem
|
||
Message-Item, das wiederum eine Liste von `input_text`-Content-
|
||
Blöcken trägt — exakt wie die `ResponsesApiRequest`-Struktur im
|
||
offiziellen `codex-rs/codex-api/src/common.rs`. Settings → KI
|
||
"Verbindung testen" und der Insight-Generator laufen damit jetzt
|
||
beide gegen dein ChatGPT-Abo durch.
|
||
|
||
## [1.4.9] — 2026-05-09
|
||
|
||
### Fixed — Settings · KI
|
||
|
||
- **Device-Code-Flow läuft jetzt komplett durch.** v1.4.8 hat den
|
||
Code richtig zugestellt und du konntest auf chatgpt.com
|
||
bestätigen, aber der Connect-Schritt am Ende ist mit "missing
|
||
organization_id" gestorben. Ursache: der id_token aus dem
|
||
Device-Flow trägt das `organization_id`-Claim nicht (das
|
||
bekommt nur die Browser-Authorize-Variante via
|
||
`id_token_add_organizations=true`-Param), und unsere
|
||
RFC-8693-API-Key-Exchange hat genau dieses Claim erwartet. v1.4.9
|
||
übernimmt das Verhalten des offiziellen Codex CLI im Device-Pfad:
|
||
den OAuth-Access-Token direkt verwenden und gegen den Codex-
|
||
Backend-Endpoint (`chatgpt.com/backend-api/codex/responses`)
|
||
schicken — kein API-Key-Tausch nötig. Refresh läuft analog: nur
|
||
Token-Rotation, keine Exchange.
|
||
|
||
## [1.4.8] — 2026-05-09
|
||
|
||
### Fixed — Settings · KI
|
||
|
||
- **"Mit ChatGPT verbinden" funktioniert jetzt wirklich.** v1.4.7 hat
|
||
zwar die OAuth-Endpoints korrigiert, ist aber an OpenAIs Hydra-
|
||
Allow-List gescheitert: die Public-Codex-Client-ID hat in der
|
||
Server-Allow-List nur `localhost:1455/1457` als Redirect-URI, jeder
|
||
hosted-Domain-Callback wird mit "An error occurred during
|
||
authentication (unknown_error)" abgelehnt. v1.4.8 schaltet auf den
|
||
**Device-Code-Flow** um — den selben Mechanismus den der offizielle
|
||
Codex CLI für Headless-/Hosted-Umgebungen nutzt: HealthLog zeigt
|
||
einen kurzen Code (`RGRP-N5F7U`-Stil), du gehst auf
|
||
https://auth.openai.com/codex/device, gibst den Code ein, bestätigst
|
||
bei dir im Browser, fertig. Kein Redirect zurück nötig, also keine
|
||
Allow-List-Konflikte. Die Settings-Seite pollt im Hintergrund und
|
||
schließt das Modal sobald deine Approval durch ist.
|
||
|
||
## [1.4.7] — 2026-05-09
|
||
|
||
### Fixed — Settings · KI
|
||
|
||
- **"Mit ChatGPT verbinden" funktioniert wieder.** Seit der Funktion
|
||
Anfang v1.4.x existiert war der OAuth-Pfad gegen die falsche Domain
|
||
verdrahtet — ChatGPT hat unsere Authorize-/Token-Requests still
|
||
geschluckt und das normale Web-Interface gerendert, kein Callback,
|
||
kein Fehler. Der Flow läuft jetzt über `auth.openai.com/oauth/...`,
|
||
identisch zum offiziellen `openai/codex` CLI: PKCE-Authorize, Token-
|
||
Exchange, dann ein zweiter RFC-8693-Token-Exchange, der den
|
||
`id_token` gegen einen OpenAI-API-Key tauscht. Dieser Key bucht
|
||
gegen das ChatGPT-Abo, kein separates API-Plan-Quota nötig. Der
|
||
Refresh-Token wird verschlüsselt mitgespeichert; vor jeder Anfrage
|
||
rotiert HealthLog den API-Key transparent wenn er abläuft.
|
||
|
||
## [1.4.6] — 2026-05-09
|
||
|
||
### Fixed — Dashboard
|
||
|
||
- **Tile strip fills its row.** Each tile in the dashboard's
|
||
measurement strip now stretches to its grid cell instead of
|
||
shrinking to content width. On a 375 px viewport tiles in the
|
||
same row are no longer 122-166 px wide — every tile takes the
|
||
same share of the row.
|
||
- **Secondary text is finally distinguishable from primary.**
|
||
`--muted-foreground` was identical to `--foreground` in both
|
||
light and dark mode, which flattened the entire visual
|
||
hierarchy. Tile labels, "Ø7d:" prefixes, units, and inactive
|
||
nav items now render in `#9aa3b3` (dark) / `#5b6273` (light) —
|
||
both ≥ 4.5:1 against the surface.
|
||
- **Trend cards land aligned with the chart strip below.**
|
||
Padding bumped from `p-3` to `p-4 md:p-6` so the tiles match
|
||
the surrounding chart cards. KPI typography (`text-3xl
|
||
tracking-tight tabular-nums` for the value, `text-xs uppercase
|
||
tracking-wide` for the label) gives the number proper weight
|
||
without making the tile feel tall.
|
||
- **Always-on Ø7d / Ø30d chips** — when an average is missing
|
||
the chip renders `—` instead of disappearing, so vertical
|
||
rhythm stays consistent across tiles.
|
||
- **Welcome subtitle is visible on mobile** (was hidden below
|
||
the `sm` breakpoint) and uses muted-foreground so it doesn't
|
||
fight the headline.
|
||
- **Medications card matches the other cards** — `rounded-xl`
|
||
and `p-4 md:p-6` instead of the smaller `rounded-lg p-4`.
|
||
|
||
### New — Charts
|
||
|
||
- **Long-range charts now bucket their data.** Hitting "All" on
|
||
an account with several years of measurements used to render
|
||
thousands of daily dots — unreadable, and slow on mobile. The
|
||
chart now picks an aggregation level from the visible range:
|
||
daily for ≤ 90 days, weekly mean for 91-730 days, monthly
|
||
mean for everything beyond. A small chip beside the chart
|
||
title surfaces the active bucket ("Wochendurchschnitt" /
|
||
"Monatsdurchschnitt" in DE, "Weekly avg" / "Monthly avg" in
|
||
EN) so the bucket is never silent. Empty buckets are dropped
|
||
rather than zero-padded.
|
||
|
||
### Fixed — KI
|
||
|
||
- **Hero recommendation no longer prints raw `metric:WEIGHT`
|
||
tokens.** When the model embedded a chart token in the
|
||
primary recommendation the renderer was printing the literal
|
||
string. The "Key Takeaway" card now strips the tokens for
|
||
prose and renders them as inline charts under the
|
||
recommendation, the same pattern the summary block has used
|
||
since v1.4.5.
|
||
- **"Verbindung testen" stays readable when the model returns
|
||
bad JSON.** `/api/insights/generate` returned 502 for parse
|
||
errors, which Cloudflare swapped for an HTML error page;
|
||
`await res.json()` in the browser then crashed. Same
|
||
Cloudflare-rewrite trap the v1.4.5 ai/test fix solved — parse
|
||
errors now map to 422 so the JSON body actually reaches the
|
||
client.
|
||
- **Saved API keys cannot leak to a stale local URL.** A user
|
||
who had configured the Local provider with `http://192.168.x.x`
|
||
and later switched to OpenAI / Anthropic kept that URL in the
|
||
shared `aiBaseUrl` column, and the OpenAI / Anthropic clients
|
||
were forwarding their cloud keys to that LAN host on every
|
||
request. The PATCH that switches providers now wipes the
|
||
column, and the OpenAI / Anthropic clients hardcode the
|
||
canonical base URL regardless of what the column carries.
|
||
|
||
### Improved — KI Insights
|
||
|
||
- **The model now sees three years of history per metric.** Each
|
||
per-card insight payload (general, blood pressure, pulse,
|
||
weight, BMI, mood, medication compliance) used to clip the
|
||
series to the last 30 daily means — way too narrow to detect
|
||
drift that plays out over a year. The new payload combines
|
||
360 daily means (`dayOffset 0..359`) with 24 monthly means for
|
||
the older window (`monthOffset 12..35`), with a 50 KB safety
|
||
guard that re-buckets at 180 daily days when a particularly
|
||
noisy account would blow past the prompt token budget.
|
||
|
||
### Fixed — Admin
|
||
|
||
- **Status-card CTAs go to the right place.** Every "Manage
|
||
users" / "View backups" link in the admin status grid was
|
||
bouncing back to `/admin` because the hrefs were pointing at
|
||
sub-routes that never shipped (`/admin/users`,
|
||
`/admin/audit-log`) or at anchors whose IDs do not exist
|
||
(`#integrations`, `#monitoring`, `#backups`). Each href now
|
||
points at a real `section-*` anchor on the admin page, the
|
||
CTA copy describes the destination honestly ("Open
|
||
integrations", "Open system status"), and the unit test
|
||
asserts each href maps to a known section ID — so the next
|
||
refactor cannot regress this silently.
|
||
- **Bug-report toggle finally hides the form.** The admin
|
||
"Bug reports" switch had no effect because the gate lived on
|
||
a legacy route the form does not call. The actual `/api/feedback`
|
||
endpoint and the `/bugreport` page now both honor the toggle —
|
||
the form disappears, and direct API calls return 503 with a
|
||
readable error.
|
||
- **Audit trail survives the data wipe.** The `Wipe all data`
|
||
admin action used to delete `AuditLog` in the same transaction
|
||
as the rest of the user data, which silently destroyed the
|
||
one record actually saying "an admin wiped data". The wipe
|
||
now leaves the audit log alone and adds an explicit
|
||
`admin.data.clear.start` entry before the transaction begins,
|
||
so even a crash mid-wipe leaves a trail. The success copy
|
||
also enumerates the full scope (API tokens, Withings
|
||
connections, auth challenges) instead of pretending only
|
||
measurements / medications / intakes are affected.
|
||
- **Cached insight reloads no longer burn a rate-limit token.**
|
||
`POST /api/insights/generate` checked the rate limit before
|
||
the cache return, so a noisy dashboard refresh would lock you
|
||
out for an hour even when every request returned the same
|
||
cached envelope. The check moved below the cache return.
|
||
- **One failing health probe no longer blanks the whole admin
|
||
status grid.** `Promise.all` in the status-overview API
|
||
short-circuited on the first rejection. `Promise.allSettled`
|
||
forces failed probes to the `alert` severity and lets the
|
||
rest render normally, with a Wide Event annotation noting
|
||
which probe(s) failed.
|
||
- **Settings load errors surface inline instead of spinning
|
||
forever.** When `useSystemStatus` or `useAdminSettings`
|
||
rejects, the admin page now shows a "Failed to load…"
|
||
banner instead of an indefinite skeleton.
|
||
|
||
### Improved — Settings · KI
|
||
|
||
- The KI section is fully internationalised — about 30 hard-coded
|
||
German strings now flow through `t("settings.ai.…")` keys, and
|
||
the model-preset list drops `gpt-5` (not a released model) and
|
||
`o3-mini` (would require an o-series parameter contract that
|
||
isn't wired up yet). The privacy "Raw data" warning uses
|
||
Dracula tokens so the colour matches the rest of the panel.
|
||
|
||
### Improved — Polish
|
||
|
||
- Numeric spans on the dashboard tiles use `tabular-nums` so
|
||
digits stop jiggling on every refresh.
|
||
- The bottom-nav buffer is tighter on mobile and removed on
|
||
desktop (`pb-[calc(4rem+env(safe-area-inset-bottom,0px))]
|
||
md:pb-0`), so desktop content sits flush instead of leaving
|
||
an unused 5 rem strip.
|
||
- Onboarding "X von Y" / "%" progress drops the monospace font
|
||
and keeps tabular-nums.
|
||
- Feedback inbox category badges use Dracula tokens
|
||
(`bg-dracula-red/15 text-dracula-red`) for theme cohesion.
|
||
- Codex provider mirrors the OpenAI client's structured-error
|
||
format (httpStatus / model / bodyExcerpt) so failures from
|
||
ChatGPT-OAuth show the same readable message as failures from
|
||
the BYO-key paths.
|
||
- Logging redacts third-party AI keys: `redactSecrets` now scrubs
|
||
`sk-` / `sk-ant-` patterns so a misconfigured client cannot
|
||
leak its key into Wide Events. The idempotency body-content
|
||
guard refuses to cache responses that contain those prefixes
|
||
(with a regex tight enough to avoid false positives on words
|
||
like "task-id" or "risk-management").
|
||
- Danger-zone result colour follows mutation state instead of
|
||
string-prefix-matching localized copy.
|
||
|
||
### Repo housekeeping
|
||
|
||
- `e2e` workflow is back to green: dropped the `pnpm/action-setup`
|
||
version override (now reads `packageManager` from
|
||
`package.json`), switched the mobile Playwright project from
|
||
iPhone 13 (webkit) to Pixel 5 so chromium-only CI installs are
|
||
enough, anchored the locale-cookie test at the configured base
|
||
URL, and made the login spec open the password form before
|
||
asserting on its inputs.
|
||
- Repo-wide prettier sweep — long-deferred reformatting drift
|
||
closed before the v1.4.6 tag.
|
||
|
||
## [1.4.5] — 2026-05-09
|
||
|
||
### Fixed — Settings · KI
|
||
|
||
- **"Verbindung testen" no longer crashes the page when the key is
|
||
bad.** The endpoint mapped every upstream error to HTTP 502, which
|
||
Cloudflare intercepts and replaces with its own HTML error page.
|
||
`await res.json()` in the browser then crashed with `Unexpected
|
||
token '<', "<!DOCTYPE "`. 401/403 from the provider now map to 422,
|
||
429 to 429, and only genuine 5xx upstream errors keep the 502 —
|
||
Cloudflare passes 4xx through untouched, so the React Query
|
||
mutation reads the JSON body and surfaces a readable message.
|
||
|
||
### Fixed — Dashboard
|
||
|
||
- **Dashboard tiles use a CSS Grid layout again** — the v1.3-era
|
||
pattern that gave each tile an identical width with symmetric gaps
|
||
and an edge-to-edge fit with the charts below. v1.4.4 distributed
|
||
width via `flex-1 basis-0`; CSS Grid is more honest about the
|
||
intent and survives viewport changes more gracefully.
|
||
`grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr))` keeps
|
||
every track equal-width with a 9 rem floor; the row wraps when
|
||
the floor no longer fits instead of triggering a horizontal
|
||
scroll.
|
||
|
||
## [1.4.4] — 2026-05-08
|
||
|
||
### Fixed — Dashboard
|
||
|
||
- **Blood pressure is now two distinct tiles again** (sys + dia next
|
||
to each other) — the v1.4.3 attempt at a combined tile read as a
|
||
single double-wide entry, which Marc preferred to keep as two
|
||
separate tiles for visual rhythm with the other metrics.
|
||
- **All tiles share an equal width.** The previous strip used
|
||
`shrink-0 grow basis-[10rem]`, which gave each tile a 160 px floor
|
||
and let wider content (like the BP tile) push past it. Switched
|
||
to `flex-1 basis-0 min-w-[9rem]` — every tile now starts from
|
||
zero and gets the same share of the available row width, with a
|
||
9 rem floor so they stay readable on narrow viewports. The strip
|
||
scrolls horizontally if the row no longer fits.
|
||
|
||
## [1.4.3] — 2026-05-08
|
||
|
||
### Fixed — Dashboard
|
||
|
||
- **Tile strip is now the same width as the charts below it.** The
|
||
small negative-margin bleed that helped the snap-scroll feel on
|
||
mobile was leaking onto desktop and made the tile row 16 px wider
|
||
than the chart strip. Fixed at `md` and up.
|
||
- **Tiles all have the same height.** Blood pressure used to render
|
||
as two stacked tiles inside one slot which made it taller than
|
||
every neighbour. Sys/dia now share **one** tile and display as
|
||
`117/79 mmHg` with one trend arrow. Tile count drops from 6 to 5.
|
||
- **Settings sub-section "Übersicht" renamed to "Dashboard"** —
|
||
matches the label in the main nav.
|
||
- **"Persönliche Zielwerte" is its own settings section now** at
|
||
`/settings/thresholds`, instead of being buried inside the
|
||
Dashboard settings panel under the layout customizer.
|
||
|
||
### Fixed — Settings
|
||
|
||
- **API & Tokens tables fit the content column on desktop.**
|
||
- **About → "Auf Updates prüfen" works again.** Production CSP
|
||
blocked the direct call to `api.github.com` — the check now goes
|
||
through a server-side proxy. The page also auto-checks once on
|
||
mount when the previous result is older than 24 h, and shows a
|
||
"Zuletzt geprüft am …" timestamp so you can tell when the answer
|
||
was confirmed.
|
||
- **About restructured into three titled cards** — HealthLog (version
|
||
- license inline, no boxed badge), Quellen & Dokumentation, and
|
||
Updates.
|
||
- **KI provider — OpenAI users can enter their own API key.** Schema
|
||
didn't have a column for it before; added it, plumbed it through
|
||
the resolver and test endpoint, surfaced the input in the Settings
|
||
→ KI panel.
|
||
- **Model field is now a dropdown** with provider-specific presets
|
||
plus an "Eigenes…" option for power users.
|
||
- **"Mit ChatGPT verbinden" no longer dead-ends on chatgpt.com.** The
|
||
OAuth URL was missing its `client_id`; with `CODEX_OAUTH_CLIENT_ID`
|
||
set the handshake completes. When the env var isn't configured the
|
||
button is hidden and a hint points the user at the API-key path.
|
||
|
||
### Improved — KI Insights
|
||
|
||
- **AI can illustrate findings with `metric:MOOD` charts inline.** A
|
||
mood drift or adherence-risk finding now renders the dedicated
|
||
Mood chart under the paragraph instead of just prose.
|
||
- **Every finding must cite a concrete number** ("138/85 mmHg",
|
||
"+0.4 mmol/L vs 30d-avg") rather than adjectives. Findings without
|
||
a snapshot anchor are now omitted; cardinality is variable 0–8
|
||
sorted by salience. Generic boilerplate ("drink enough water",
|
||
"consult your doctor", etc.) is explicitly forbidden outside the
|
||
disclaimer.
|
||
|
||
### Improved — Admin
|
||
|
||
- **Bug-report feature has an explicit on/off toggle.** Previously
|
||
the only way to hide the report button was to clear the encrypted
|
||
GitHub token, which forced a re-entry on resume.
|
||
|
||
### Improved — Polish
|
||
|
||
- **"Trennen" looks the same everywhere** (Withings, moodLog,
|
||
KI/Codex). Outlined button with red text — same affordance for
|
||
the same action.
|
||
|
||
## [1.4.2] — 2026-05-08
|
||
|
||
### Fixed — Production deploy hotfixes
|
||
|
||
- **Dashboard and Insights pages no longer crash for users without
|
||
weight data.** `data?.summaries.WEIGHT` only protected the outer
|
||
object — the optional chain stopped one level too early, so
|
||
brand-new users (where `summaries` is undefined) hit
|
||
`TypeError: undefined is not an object (evaluating 'E?.summaries.WEIGHT')`
|
||
on first load. Now `data?.summaries?.WEIGHT`.
|
||
- **Container healthcheck uses `127.0.0.1` instead of `localhost`.**
|
||
busybox-`wget` in Alpine resolves `localhost` to IPv6 `::1` first,
|
||
but Next.js standalone listens on IPv4 `0.0.0.0:3000` only — so the
|
||
healthcheck always returned ECONNREFUSED, Docker marked the
|
||
container unhealthy, and Traefik returned 503 from the public URL
|
||
even though the app was actually running. The Dockerfile-level
|
||
`HEALTHCHECK` already used 127.0.0.1; the `docker-compose.yml`
|
||
override was the one that drifted to `localhost`. Fixed.
|
||
|
||
### Notes
|
||
|
||
- The 1.4.1 GHCR image never published cleanly (the docker-publish
|
||
workflow reported success in GH Actions but the
|
||
`ghcr.io/mbombeck/healthlog:1.4.1` tag returned `manifest unknown`
|
||
when Coolify tried to pull). The 1.4.2 release supersedes 1.4.1
|
||
and includes everything 1.4.1 was supposed to ship — the v1.4.1
|
||
source on `main` was always healthy; only the Coolify deploy
|
||
surface was broken.
|
||
|
||
## [1.4.1] — 2026-05-08
|
||
|
||
### Security
|
||
|
||
- **moodLog integration no longer accepts internal-network URLs.** A
|
||
user could previously save `http://169.254.169.254/` (cloud-metadata)
|
||
or any RFC1918 address as their moodLog instance; the daily sync
|
||
worker would then fetch from that target with the user's API key in
|
||
the Authorization header. The credentials write path now refuses
|
||
non-public hosts, the sync worker re-checks the URL at the actual
|
||
fetch site (so legacy rows stored before the guard are also
|
||
refused), and the fetch is now `redirect: "manual"` so a public
|
||
host cannot 302 to an internal target with the bearer on the
|
||
redirect hop.
|
||
- **Error reports never echo bearer tokens, Telegram bot tokens, or
|
||
query-string secrets.** `WideEventBuilder.setError()` and the
|
||
Glitchtip incident path now run every error message and stack
|
||
trace through a central `redactSecrets()` filter that scrubs
|
||
`Bearer …`, Telegram `bot<digits>:<token>` URLs, and `?secret=`,
|
||
`?code=`, `?token=`, `?api_key=` query strings. The substitution
|
||
is generic `[REDACTED]` so partial entropy is never revealed.
|
||
|
||
### Fixed — Citation accuracy
|
||
|
||
- **Blood-pressure classification now cites ESH 2023.** The dashboard
|
||
tile, the doctor-report PDF, and the inline analytics comments
|
||
used to label the band as "ESC/ESH 2018". The numbers haven't
|
||
changed (the 2023 ESH update kept the 2018 thresholds), but the
|
||
joint authoring did — ESC withdrew from the 2023 document, so the
|
||
correct citation is "ESH 2023" alone.
|
||
- **Steps target source label is `Saint-Maurice JAMA 2020`** instead
|
||
of `WHO`. Every other surface in the app (AI prompts, inline
|
||
comments, drift tests) already enforced this attribution; the
|
||
insights/targets surface was the last "WHO" label in the tree.
|
||
WHO publishes physical-activity _time_, not a step quota.
|
||
- **Saint-Maurice "mortality plateau 8000–12000" attribution
|
||
softened.** The original JAMA 2020 paper reports continued
|
||
dose-response benefit (HR 0.49 at 8k, HR 0.35 at 12k) — not a
|
||
plateau. The plateau-shaped finding belongs to Paluch 2022
|
||
_Lancet Public Health_ (PMID 35247352), not Saint-Maurice. The
|
||
inline comments and AI prompts now say "continued dose-response
|
||
benefit through ~12,000 steps/day" instead.
|
||
|
||
### Added — CI safety nets
|
||
|
||
- **Postgres-backed integration test suite is now executable.** The
|
||
testcontainers infrastructure shipped in 1.4.0; this release wires
|
||
the per-test boilerplate through vitest's `globalSetup` so all
|
||
four files share one container. `pnpm test:integration` runs ten
|
||
tests (rate-limit race, idempotency replay-attack contract, GDPR
|
||
Article-17 cascade delete, session create / read / expire) against
|
||
a real Postgres in under four seconds. CI runs the suite on every
|
||
PR.
|
||
- **Playwright + axe-core E2E foundation.** A new `pnpm e2e` runs
|
||
five public-surface specs (version endpoint, proxy auth-redirect,
|
||
login form autofill hints, DE/EN locale switch, axe-core
|
||
accessibility gate) against the production build in CI. Authenticated
|
||
flow specs (quick-entry, doctor-report, settings round-trip,
|
||
test-buttons, onboarding) ride a follow-up release because they
|
||
need a seeded test user; the foundation makes adding them a
|
||
one-PR step.
|
||
|
||
### Changed — Admin internals
|
||
|
||
- **Admin page is now per-section components.** The status-card grid
|
||
shipped in 1.4.0 sat on top of a 2,700-line monolith; that monolith
|
||
is now 14 focused files in `src/components/admin/` with a 77-line
|
||
`src/app/admin/page.tsx` shell that mounts them. Every section
|
||
keeps the same DOM, ids, query keys, and i18n keys — no
|
||
user-visible change.
|
||
|
||
### Fixed
|
||
|
||
- **Final ESLint error is gone.** The medications page's "API
|
||
endpoint" dialog ran its initial-load fetch through a `useCallback`
|
||
paired with `useEffect` and triggered the strict
|
||
`react-hooks/set-state-in-effect` rule. Refactored to TanStack
|
||
Query — same network calls, no effect, lint count is now zero on
|
||
`main`.
|
||
|
||
### Documentation
|
||
|
||
- **Repo-internal docs synced for v1.4.** README adds the
|
||
Multi-tenant ready and Test connection buttons feature blocks, the
|
||
API reference table includes the eleven new v1.4 endpoints, and
|
||
the model count is corrected to 26 (RefreshToken). AGENTS.md and
|
||
CLAUDE.md reflect the per-route `/settings/[section]` layout and
|
||
the per-section admin layout. `docs/api/openapi.yaml` documents
|
||
the new endpoints (version, refresh, refresh/revoke,
|
||
status-overview, backup/test, the five test-connection probes).
|
||
`docs/migration/v1.3-to-v1.4.md` corrects the now-wrong "no
|
||
migrations" claim and adds full env-var sections for the
|
||
worker/web split, encryption-key versioning, and off-host backup
|
||
target.
|
||
|
||
### Notes
|
||
|
||
- No database migration in 1.4.1.
|
||
- No environment-variable change required to upgrade.
|
||
- No API contract change — every route added in 1.4.0 is still
|
||
there; no shapes or status codes flipped.
|
||
- The audit pass that drove this release identified five medium
|
||
security items and three P0 performance items that warrant
|
||
deeper architectural work; those are tracked in
|
||
`docs/ops/v141-followup-issues.md` and ride a future release.
|
||
|
||
## [1.4.0] — 2026-05-08
|
||
|
||
### Added — Foundation, safer ranges, and a faster dashboard
|
||
|
||
- **UI guidelines, design tokens, and shared primitives.** A new
|
||
`docs/ui-guidelines.md` is the single source of truth for spacing,
|
||
typography, button hierarchy, dialog-vs-sheet decisions, accessibility
|
||
baseline (WCAG 2.1 AA), and the autofill / honeypot pattern for
|
||
health-data forms. Two new shadcn primitives — `<Skeleton>` and
|
||
`<EmptyState>` — replace the previous mix of spinners and "No data"
|
||
placeholder strings. Future v1.4.x components reference the doc; the
|
||
primitives ship with screen-reader-aware semantics and respect
|
||
`prefers-reduced-motion`.
|
||
- **`/api/version` public endpoint** exposing the build's version,
|
||
optional Git SHA / build timestamp, license, and canonical links.
|
||
Wires the future Settings → About surface and a thin "Check for
|
||
updates" UX. Static-cached so the route adds zero DB load.
|
||
- **`src/lib/medical-citations.ts`** — single source of truth for
|
||
cited medical guidelines (id, name, year, URL, caveat). Future
|
||
medical surfaces import these constants instead of duplicating
|
||
strings in code, prompts, and `messages/*.json`. A new drift-test
|
||
asserts every entry has a non-empty URL + caveat and that the
|
||
recurring "WHO ≥ N steps" hallucination cannot reappear as a constant.
|
||
|
||
### Fixed — Patient safety and citation accuracy
|
||
|
||
- **Diastolic blood-pressure orange band no longer reaches 60 mmHg.**
|
||
With the default age-based targets (DBP 70–79), the lower orange
|
||
wing was computed as `diaLow − 10 = 60`. A reading of 60 mmHg landed
|
||
in "mildly low" yellow instead of red even though that level is the
|
||
general-adult hypotension threshold and the J-curve risk floor in
|
||
ESH 2023 for treated hypertensives. Orange floor is now clamped at
|
||
65 mmHg, so 60 mmHg lands in red. The user-override path stays
|
||
intact and remains audit-logged.
|
||
- **BP guideline citations consolidated on ESH 2023.** The codebase
|
||
had a mix of "ESC/ESH 2018" (analytics) and "ESC/ESH 2023" (AI
|
||
prompts). The 2023 hypertension document is ESH-only — ESC withdrew
|
||
from the joint authoring — so neither label was correct. Every site
|
||
now cites "ESH 2023" with the published source URL. Numbers
|
||
unchanged.
|
||
- **"WHO ≥ 8 000 steps/day" hallucination fully removed.** WHO
|
||
publishes activity _time_ (150–300 min/wk moderate), not a step
|
||
quota. The v1.3.3 fix only landed in `effective-range.ts`; four AI
|
||
prompt strings and the `getStepsRange()` helper carried the old
|
||
wording forward. Saint-Maurice et al., JAMA 2020 (mortality plateau
|
||
8 000–12 000) is now cited everywhere and the two surfaces agree on
|
||
the band. Sleep target moves from "ESC" (no adult sleep guideline)
|
||
to AASM 2015.
|
||
- **Body-fat ACE bands corrected and three-way drift resolved.** The
|
||
classifier used `essential = 6 (M) / 14 (F)` as the floor — but
|
||
that's actually ACE's _Athletes_ lower bound. Readings below were
|
||
mislabelled "Essential" instead of "Below essential" (a danger
|
||
band). Six-band classifier now mirrors the ACE table, and the three
|
||
sites that had three different green-band numbers
|
||
(`value-bands.ts`, `targets/route.ts`, `classifications.ts`) all
|
||
derive from `getBodyFatTargetRange` (ACE fitness + acceptable bands).
|
||
- **Bedtime-glucose citation softened.** ADA Standards 2024 §6
|
||
publishes pre-prandial 80–130 and post-prandial <180 — no published
|
||
adult bedtime target. The 90–150 mg/dL band stays (reasonable adult
|
||
overnight band) but the inline citation now states the absence
|
||
explicitly and references ISPAD 2022 (pediatric) as the closest
|
||
comparator.
|
||
|
||
### Fixed — Localisation reaches the notification path
|
||
|
||
- **Medication reminders now follow the user's locale.** Telegram,
|
||
ntfy, and Web Push reminders previously read "Erinnerung", "Bald
|
||
fällig", etc. regardless of the user's stored language. Templates
|
||
for every phase (`green`/`yellow`/`orange`/`red`) and every keyboard
|
||
button now resolve from `messages/{de,en}.json` per
|
||
`med.user.locale`. Telegram callback IDs stay stable English
|
||
identifiers so the dispatcher keeps matching across locale changes.
|
||
- **Dashboard greeting and streak label** are localised server-side.
|
||
Previously hard-coded `"Hi, ${name}"` and `"Tage in Folge"` — both
|
||
now i18n-key-resolved.
|
||
- **Mixed-locale Zod validation messages unified to English.** Two
|
||
measurement-form messages and four admin-validation messages
|
||
flipped between German and English depending on which schema fired.
|
||
All consolidated on English (the app is English-first; the German
|
||
UI maps field labels client-side).
|
||
|
||
### Fixed — Chart math edge cases
|
||
|
||
- **`summarize` and `trendSlope` use the same time anchor.** Averages
|
||
snapped to `Date.now()`; slopes snapped to the latest point in the
|
||
series. A stale series reported a trend even though the dashboard
|
||
tile correctly hid the average. Both now anchor on `Date.now()`, so
|
||
a stale series returns `null` consistently from every windowed stat.
|
||
- **`summarize([])` returns `null` for `min`/`max`/`mean`** instead
|
||
of zeros that leaked into chart axes and AI feature bundles as
|
||
fake readings.
|
||
- **`weeklyAverages` is Berlin-timezone aware.** A Sunday-evening
|
||
Berlin reading bucketed into the next week on the UTC production
|
||
container because `Date.getDay()` was system-local. ISO-Monday key
|
||
now resolves via `Intl.DateTimeFormat({ timeZone: "Europe/Berlin" })`.
|
||
- **`pairByTimestamp` JSDoc** documents the greedy nearest-match
|
||
heuristic and when a Hungarian-style match would matter (sparse
|
||
health data is well below that bar).
|
||
|
||
### Fixed — Hidden friction
|
||
|
||
- **AI provider connection-test honours the unsaved selection.**
|
||
Changing the AI provider in `/settings`, then clicking "Verbindung
|
||
testen" without saving first, used to silently run the test against
|
||
the stored provider — surfacing as a confusing OK / failure unrelated
|
||
to what the user had on screen. Plaintext keys never persist; the
|
||
existing SSRF guard, rate limit, and V3 error-leak shielding stay in
|
||
place.
|
||
- **Health-data inputs no longer autofill the user's account
|
||
password.** The base `<Input>` primitive defaults to
|
||
`autoComplete="off"` plus the LastPass / 1Password ignore attributes
|
||
whenever the caller doesn't pass a semantic value. Auth and profile
|
||
forms continue to autofill normally because they pass an explicit
|
||
`autoComplete` (`"username"`, `"email"`, `"current-password"`,
|
||
`"new-password"`).
|
||
- **Step-range target aligned across two callsites.**
|
||
`getStepsRange()` returned `{7000, 10000}` while
|
||
`effective-range.ts` returned `{8000, 15000}`; two surfaces showed
|
||
different "green" bands to the same user. Both now use
|
||
`{8000, 15000}`, anchored on Saint-Maurice 2020.
|
||
|
||
### Performance
|
||
|
||
- **Two more N+1 queries closed.** `extractFeatures` (used by every
|
||
AI-insight route) issued one `prisma.medicationIntakeEvent.findMany`
|
||
per active medication; replaced with a single batched query and an
|
||
in-memory group. `/api/insights/targets` issued one `findFirst` per
|
||
measurement type; replaced with a single `distinct: ["type"]` query.
|
||
Same shape as the v1.3.0 fix to `/api/insights/comprehensive`.
|
||
|
||
### Changed — Dashboard
|
||
|
||
- **Tile strip is always one row.** Replaces the wrapping
|
||
`grid-cols-2 sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5` layout
|
||
with a `flex snap-x snap-mandatory overflow-x-auto` strip. When the
|
||
user enables more tiles than fit the viewport, the strip
|
||
horizontal-scrolls instead of wrapping; the user trims the set in
|
||
Settings → Dashboard Layout.
|
||
|
||
### Added — Settings, integrations, and operations
|
||
|
||
- **Settings page now lives at `/settings/[section]`.** Eight focused
|
||
routes (`account`, `integrations`, `notifications`, `dashboard`,
|
||
`ai`, `api`, `advanced`, `about`) replace the single 3,000-line page.
|
||
Existing `/settings#anchor` links 308-redirect; the side-bar, in-app
|
||
deep links, and the AI / Withings / Codex callbacks all follow the
|
||
new structure.
|
||
- **About page** lists the running version, build SHA, license,
|
||
repository link, CHANGELOG link, docs link, and a "Check for
|
||
updates" button that pings the public GitHub releases API. Backed by
|
||
the `/api/version` endpoint shipped earlier in 1.4.0.
|
||
- **Admin console** is built around a status-first card grid (Users,
|
||
Integrations, Monitoring, Backups, Maintenance, Audit Log) with each
|
||
area in a focused panel beneath. Per-section extraction of the old
|
||
inline panels is tracked for v1.4.1 — the v1.4.0 admin page already
|
||
routes through the new aggregator endpoint and the status-card
|
||
grid.
|
||
- **Five new "Test connection" buttons in Settings.** Withings,
|
||
moodLog, Web Push, Glitchtip, and Umami now ship with one-click
|
||
connection probes — same pattern as the existing AI / Telegram /
|
||
ntfy tests, with per-button rate limit, sanitised error reporting,
|
||
redirect-follow SSRF guard, and an `errorCode` in the response
|
||
envelope so the UI can localise the message.
|
||
- **AI insights can reference any of your charts inline.** When a
|
||
finding centres on a single metric (e.g. systolic blood pressure),
|
||
the corresponding chart renders directly under the explanation.
|
||
Server-side allow-list — only the allowed metric tokens render; any
|
||
other model emission drops silently.
|
||
- **Off-host backup target.** Daily encrypted JSON dumps to any
|
||
S3-compatible bucket. Worker-side IAM grant is intentionally
|
||
PutObject + GetObject only — retention is the bucket's
|
||
lifecycle-rule job, so a compromised worker cannot wipe the backup
|
||
history. Restore script + step-by-step doc shipped under
|
||
`docs/ops/backup-restore.md`, and an admin "Backup target" test
|
||
button validates the configuration.
|
||
- **Encryption-key versioning.** Rotate the at-rest encryption key
|
||
without downtime via `pnpm tsx scripts/rotate-encryption-key.ts <id>`.
|
||
Existing data keeps decrypting under its original key while the new
|
||
one is rolled out. Walk-through + rollback notes in
|
||
`docs/ops/encryption-key-rotation.md`.
|
||
- **Worker / web split.** Optional
|
||
`HEALTHLOG_PROCESS_TYPE=web|worker|all` (default `all` for the
|
||
single-container setup) lets you scale background jobs and HTTP
|
||
traffic independently. The proxy refuses HTTP traffic with a 503 +
|
||
`X-HealthLog-Process-Type: worker` header in worker mode so a
|
||
misrouted request fails loudly instead of a silent half-served
|
||
response.
|
||
- **Native API clients now get short-lived 24-hour access tokens with
|
||
refresh-token rotation.** The browser keeps the existing 90-day
|
||
Bearer. Reuse-detection (presenting a refresh token a second time)
|
||
revokes every refresh token for the user — the small cost of a
|
||
forced re-login on the legitimate device buys defense-in-depth
|
||
against an undetected stolen-token replay.
|
||
- **Critical-path coverage on Telegram / Withings / moodLog /
|
||
Glitchtip webhook handlers + the four admin routes lifted to ≥80%
|
||
line coverage,** plus `src/lib/auth/audit.ts`. ~+100 new tests.
|
||
|
||
### Fixed — Operational hardening from the v1.4 review pass
|
||
|
||
- **Container time zone is correct.** Alpine images ship without
|
||
`tzdata`; the daily backup cron `30 2 * * *` Europe/Berlin was
|
||
silently falling back to UTC. The runner stage now installs
|
||
`tzdata` and exports `TZ=Europe/Berlin` so schedules fire at the
|
||
documented local time.
|
||
- **Compose healthcheck uses `wget --spider /api/version`** — `/api/version`
|
||
is now in the proxy's public-paths allowlist, so the healthcheck no
|
||
longer 302-redirects through the auth gate (which was accepting the
|
||
login page as a 200 success).
|
||
- **Idempotency replay-cache no longer caches refresh tokens.** The
|
||
guard already blocked the `hlk_` access-token prefix; the new
|
||
`hlr_` refresh tokens are blocked too.
|
||
- **Logout-on-device revokes the paired access token.** Calling
|
||
`/api/auth/refresh` with `revoke: true` now flips both the refresh
|
||
row and the matching `ApiToken` row to revoked, so a leaked access
|
||
token cannot outlive its refresh-token sibling.
|
||
- **`users.locale` migration drift backfilled.** The column had been
|
||
on `schema.prisma` since the v1.3 locale-aware reminder work but
|
||
never landed in the migration history (it must have been applied
|
||
via `prisma db push` to dev/prod). Any environment built strictly
|
||
from `prisma/migrations/` (CI testcontainers, brand-new self-host
|
||
installs) is now consistent. Migration is `ADD COLUMN IF NOT
|
||
EXISTS`, so it's a clean add on a fresh database and a safe no-op
|
||
against any environment that was already kept in sync.
|
||
|
||
### Notes
|
||
|
||
- Largely additive release. Existing API contracts (response
|
||
envelopes, OpenAPI 3.1 spec) are unchanged. New endpoints surface
|
||
optional fields; no breaking changes.
|
||
- New migration `0025_refresh_tokens` adds the rotating refresh-token
|
||
table; new migration `0025_user_locale_drift_fix` backfills the
|
||
schema-vs-migrations drift on `users.locale`. Both are
|
||
forward-compatible — `IF NOT EXISTS` guards make them idempotent on
|
||
any environment already pushed-to.
|
||
- Operators of the off-host backup feature must configure a bucket
|
||
lifecycle policy for retention. The worker has no DeleteObject
|
||
grant by design.
|
||
- Native API clients (iOS, n8n, Health Connect) need to update their
|
||
login flow: native logins now return both a 24-hour access token
|
||
and a refresh token. The browser flow is unchanged.
|
||
- **Tracked for v1.4.1:** per-section admin panel extraction (the
|
||
status-card grid + aggregator already ship in 1.4.0; the inner
|
||
per-section file split is structural cleanup), the Postgres-backed
|
||
integration test suite (testcontainers infrastructure ships in this
|
||
release; the four integration tests themselves need a follow-up
|
||
pass against the merged schema), and Playwright E2E + axe-core CI
|
||
gates.
|
||
|
||
## [1.3.3] — 2026-05-08
|
||
|
||
### Added
|
||
|
||
- **Pulse oximetry as a first-class measurement type (`OXYGEN_SATURATION`).**
|
||
Closes the SpO2 part of #109. Migration `0024_oxygen_saturation` extends
|
||
the `MeasurementType` enum. Plausibility range 50–100% (below 50% is
|
||
incompatible with sustained life and almost certainly a faulty sensor;
|
||
upper bound 100% is physical). Default severity bands follow BTS Guideline
|
||
2017 + ATS clinical practice: green 95–100%, orange 92–94%, red <92% —
|
||
lower-only concern (the upper orange wing collapses onto greenMax since
|
||
saturation cannot physically exceed 100%). COPD / chronic-respiratory
|
||
users with a doctor-set baseline of 88–92% can personalize via the
|
||
threshold-override UI. Wired through Withings (ScanWatch type 54),
|
||
measurement form, list, charts, doctor PDF, OpenAPI spec, and i18n (DE +
|
||
EN). iOS DTO already declared `OXYGEN_SATURATION` from a prior commit;
|
||
the server enum addition closes the long-standing drift.
|
||
- **Body composition surfaces (TOTAL_BODY_WATER, BONE_MASS, BLOOD_GLUCOSE)
|
||
in the measurements list filter, badge, mobile icon, edit dialog, and
|
||
server-rendered doctor-report PDF** — closes the UI side of #109. Root
|
||
cause was three local maps in `measurement-list.tsx` that drifted from
|
||
the v1.3 server enum; extracted to `measurement-list-meta.ts` with
|
||
fail-fast coverage tests so future enum additions are caught at build
|
||
time. Server-side PDF used a separately-drifted type map vs. the
|
||
browser-side renderer; both are now in sync.
|
||
- **Effective-range thresholds for `TOTAL_BODY_WATER` and `BONE_MASS`** —
|
||
severity logic was returning `nominal` for any value because no defaults
|
||
existed.
|
||
|
||
### Changed
|
||
|
||
- **OpenAPI `MeasurementType` enum extended + spec version bumped 1.3.0 →
|
||
1.3.3** to match the actual app. Spec was lagging by two minor releases.
|
||
- **Withings webhook secret now reads from `X-Withings-Webhook-Secret`
|
||
header** in preference to the legacy `?secret=…` URL query parameter.
|
||
Closes the URL-leak-via-access-logs vector flagged in audit C-3. Legacy
|
||
query-param path is retained for backwards compatibility and emits a
|
||
Wide Event warning so operators can spot still-using-the-old-flow
|
||
integrators. Plan: remove the query fallback in 1.4.x once warnings drain.
|
||
- **Idempotency `defaultUserIdResolver` now supports Bearer tokens.**
|
||
Cookie sessions tried first, then Bearer-token via `hashToken` lookup.
|
||
Without the Bearer fallback, every iOS / external-ingest retry was
|
||
hitting the handler again and creating duplicate measurements (audit
|
||
C-4 — the exact use case `withIdempotency` was built for).
|
||
- **GlitchTip URL stripping** — `reportToGlitchtip` now strips the URL
|
||
query string before forwarding so Withings legacy `?secret=…` and OAuth
|
||
`?code=…` callbacks cannot leak via the error tracker (audit H-B7).
|
||
|
||
### Fixed
|
||
|
||
- **Migration `0022_body_composition_metrics` unit comment lied** —
|
||
claimed `TOTAL_BODY_WATER: percent of body weight (%)` while every other
|
||
surface (validators, Withings client, doctor PDF) treated it as `kg`.
|
||
Comment corrected to match reality.
|
||
|
||
### Security
|
||
|
||
- **Bearer-scope wildcard handling (CRITICAL — V3-1).** `requireAuth()`
|
||
previously accepted any non-admin token regardless of declared
|
||
permission scope, so a token with `permissions:["medication:ingest"]`
|
||
could DELETE the user account. Spec now requires `permissions:["*"]`
|
||
or the explicit required permission.
|
||
- **Account-deletion completeness (CRITICAL — V3-2 / GDPR Art. 17).**
|
||
Cascades through `Feedback` + `AuditLog` rows so user-erasure is
|
||
actually total. Daily retention job sweeps orphaned audit rows after
|
||
90 days as a defence-in-depth.
|
||
- **Withings webhook secret header migration (audit C-3)**, idempotency
|
||
Bearer-resolver (audit C-4), GlitchTip URL strip (audit H-B7).
|
||
- **Truthfulness pass on medical citations** — SpO2 normal-range source
|
||
is now consumer-pulse-oximeter consensus + NICE NG115 + FDA labelling
|
||
(BTS-2017 was for clinical hypoxaemia thresholds, not consumer
|
||
monitoring); body-composition metrics are explicitly labelled
|
||
"bioimpedance-estimated, not DEXA-comparable" in the doctor PDF;
|
||
TBW citation now references the Watson formula / ICRP Reference Man
|
||
(was misattributed to ESPEN 2017); steps target now references
|
||
Saint-Maurice JAMA 2020 (WHO publishes minutes/week, not steps).
|
||
- **SpO2 user-override clamp** — overrides could emit physical
|
||
impossibilities (e.g. `orangeMax = 100.75`); clamped to METRIC_BOUNDS
|
||
for SpO2 + BODY_FAT.
|
||
- **moodLog webhook secret encrypted at rest with AES-256-GCM** (V3
|
||
STILL-V2-C-2). Read path tolerates legacy plaintext rows during the
|
||
transition window; one-shot startup migration in the worker rotates
|
||
any leftover plaintext rows.
|
||
- **CSP tightening** — `chatgpt.com` + `api.openai.com` `connect-src`
|
||
now gated to `/settings/ai/**` (was a global blanket on every page,
|
||
including `/auth/login` → DOM-XSS exfil channel).
|
||
- **Web-Push subscription endpoint SSRF guard** — `endpoint` now
|
||
requires HTTPS + passes `isPublicUrl()` (was `z.url()` only).
|
||
Side-fix: `isPublicUrl()` no longer falsely classifies DNS labels
|
||
starting with `fc`/`fd` (e.g. `fcm.googleapis.com`) as IPv6
|
||
unique-local; the IPv6 check is now gated on a colon being present.
|
||
- **IP-geolocation lookup is now HTTPS-only.** Default provider is
|
||
`ipwho.is` (free, HTTPS, no key). Existing `ip-api.com` plaintext
|
||
HTTP path leaked auth-event IP + timestamp on every login (GDPR Art.
|
||
32 + Art. 44). Operators can override via `IP_GEO_LOOKUP_URL` (HTTPS
|
||
only) or disable entirely with `IP_GEO_LOOKUP_DISABLED=1`.
|
||
- **`/api/ai/test` no longer returns provider error message + body
|
||
excerpt to the client.** Diagnostics land server-side via Wide Events
|
||
(annotate); client gets a categorised generic message. Closes provider
|
||
URL / partial key / internal header leak.
|
||
- **`/api/import` rate-limit added** — 5 imports/hour/user. Was
|
||
unlimited (bulk-injection vector).
|
||
- **Trusted-proxy XFF semantics** — `getClientIp()` now reads
|
||
`X-Forwarded-For` right-to-left with a configurable
|
||
`TRUST_PROXY_HOPS` (default 1, matches typical single-proxy
|
||
self-host). Closes XFF rotation bypass of per-IP rate-limits.
|
||
- **Audit-log retention job** — `audit_logs` rows older than
|
||
`AUDIT_LOG_RETENTION_DAYS` (default 365) are purged daily. Closes
|
||
GDPR Art. 5(1)(e) "storage limitation" gap.
|
||
- **Idempotency cachable-status filter** is now an exported, unit-tested
|
||
function — pins the do-not-cache contract for 401/403/408/429/5xx.
|
||
- **Bearer mock tightening** in `require-auth-bearer.test.ts` +
|
||
`idempotency.test.ts`: `apiToken.findUnique` calls are now asserted
|
||
to use `where: { tokenHash: <hashed> }`, so a regression to raw-token
|
||
comparison would break the suite immediately.
|
||
|
||
### Internal
|
||
|
||
- **Server-side enum drift cousins closed.** Five module-level
|
||
hardcoded type-arrays in `/api/insights/comprehensive`,
|
||
`/api/dashboard/summary`, `/api/analytics`, `/lib/insights/general-status`,
|
||
`/api/import` are now derived from `measurementTypeEnum.options`.
|
||
External-contract enums extended additively:
|
||
`/api/measurements/series` (`oxygen`, `totalBodyWater`, `boneMass`),
|
||
`/api/dashboard/widgets` (`oxygenSaturation`), `DashboardWidgetId` +
|
||
`DEFAULT_DASHBOARD_LAYOUT`. New coverage test asserts the canonical
|
||
enum stays the source of truth.
|
||
- **Doctor-PDF text-content tests** — replaced bytes-only "renders body
|
||
composition rows" theatre with `pdf-parse`-driven assertions on the
|
||
actual rendered DE + EN labels and values. Adds dev dep `pdf-parse`.
|
||
|
||
## [1.3.2] — 2026-04-28
|
||
|
||
### Fixed
|
||
|
||
- **Glucose tiles on the dashboard rendered the raw i18n key
|
||
`targets.glucoseFasting` instead of the translated label** (closes
|
||
#108). Both `messages/en.json` and `messages/de.json` had two
|
||
top-level `targets` blocks; `JSON.parse` silently keeps the last
|
||
occurrence, and that block was missing the four glucose labels. The
|
||
duplicate is now collapsed into a single block. A duplicate
|
||
`bugreport.bugTitlePlaceholder` shadowed inside `bugreport` was
|
||
cleaned up too. Two further keys (`dashboard.sleep`,
|
||
`dashboard.steps`) were missing from both locales and were falling
|
||
back to hard-coded English; both are now translated.
|
||
- **New i18n locale-integrity test**
|
||
(`src/lib/__tests__/i18n-locale-integrity.test.ts`) fails the build
|
||
on duplicate keys at any nesting depth and on key drift between
|
||
`en` and `de` — closes the structural gap that let the duplicate
|
||
`targets` block ship in the first place.
|
||
|
||
### Changed
|
||
|
||
- **Screenshot upload removed from the bug-report form** (also part
|
||
of #108). The form previously accepted an image attachment that
|
||
was stored in the local DB but never reached the published GitHub
|
||
issue — GitHub does not accept inline base64 data URIs in issue
|
||
bodies and offers no public API to attach images to an issue
|
||
programmatically. Rather than ship misleading
|
||
"a screenshot was attached" placeholder text in the resulting
|
||
issue, the upload UI is now gone and the placeholder note is no
|
||
longer added when promoting feedback. The `screenshotBase64`
|
||
column and the admin-side preview of previously-submitted
|
||
screenshots are unchanged — existing reports keep their
|
||
attachments locally. We plan to revisit a real screenshot pipeline
|
||
in a future release.
|
||
|
||
## [1.3.1] — 2026-04-27
|
||
|
||
### Fixed
|
||
|
||
- **Compose env-var validation no longer breaks Coolify-style deploys.**
|
||
`docker-compose.yml` previously used `${VAR:?required}` shell-parameter
|
||
syntax for the four secrets and `POSTGRES_PASSWORD`. Some hosting
|
||
platforms (Coolify in particular) parse compose files eagerly and
|
||
store the _fallback error string_ (`"POSTGRES_PASSWORD is required"`)
|
||
as the literal env-var value when `POSTGRES_PASSWORD` was unset,
|
||
which then collided with `DATABASE_URL` and broke the running app
|
||
with `P1000: Authentication failed`. Compose now uses plain `${VAR}`
|
||
interpolation; validation moved into `docker-entrypoint.sh`, which
|
||
fails fast with a clear stderr message listing the unset variables.
|
||
|
||
### Notes
|
||
|
||
If you upgraded an existing Compose stack from 1.2.x → 1.3.0 and hit
|
||
the `POSTGRES_PASSWORD is required` literal-as-value bug, set
|
||
`POSTGRES_PASSWORD` in your environment to whatever your existing
|
||
Postgres data volume was originally initialised with (likely
|
||
`healthlog` if you started from a pre-1.2.1 release), then redeploy.
|
||
Postgres only honours `POSTGRES_PASSWORD` on first volume init — the
|
||
existing user keeps the original password regardless of env changes.
|
||
|
||
## [1.3.0] — 2026-04-27
|
||
|
||
### Added — Body composition + targeted hardening
|
||
|
||
- **Total body water and bone mass as measurement types** (closes #89). New
|
||
enum values `TOTAL_BODY_WATER` and `BONE_MASS`, both stored canonically
|
||
in kilograms (matches Withings hydration/bone-mass measures and Health
|
||
Connect's `TotalBodyWaterRecord` / `BoneMassRecord`). Migration is
|
||
purely additive (`ALTER TYPE ... ADD VALUE`) — safe to apply against
|
||
any 1.2.x database without downtime.
|
||
- **Withings sync picks both up automatically.** The Withings client now
|
||
maps measure type `77` (hydration / water mass) and `88` (bone mass).
|
||
Anyone with a Withings Body+ scale and an active connection will see
|
||
the new metrics flowing in on the next sync without any extra config.
|
||
- **Doctor-report PDF includes both new types** in the vital-signs table
|
||
when data exists, with locale-aware labels in English and German.
|
||
- **Dashboard widgets registered for both** (default-invisible — opt in
|
||
via Settings → Dashboard layout).
|
||
|
||
### Security
|
||
|
||
- **SSRF guard hardened** (`isPublicUrl`). The previous implementation
|
||
used `parseInt` with permissive prefix checks like `h.startsWith("10.")`
|
||
which let `010.0.0.1` slip through — and worse, the WHATWG URL parser
|
||
silently normalises `010.0.0.1` to `8.0.0.1` (octal interpretation), a
|
||
real bypass on naive checks. The new guard adds a pre-URL leading-zero
|
||
check on the raw input, a strict IPv4 parser, and proper IPv6
|
||
bracket / loopback / link-local handling. Now blocks `127.0.0.0/8`,
|
||
`0.0.0.0/8`, `10.0.0.0/8`, `169.254.0.0/16`, `172.16.0.0/12`,
|
||
`192.168.0.0/16`, `100.64.0.0/10` (CGNAT), `::1`, `fe80::/10`,
|
||
`fc00::/7`. Comprehensive regression tests included.
|
||
- **GitHub PAT redacted from logged error bodies.** When a feedback
|
||
escalation to GitHub fails, the response body was being passed
|
||
verbatim into `getEvent()?.addWarning(...)` — flowing to Loki. The
|
||
body is now stripped of the configured token before logging.
|
||
- **Per-user threshold writes are rate-limited** (30 writes / 5 min) to
|
||
make audit-log enumeration unattractive. Audit-logging itself was
|
||
already there.
|
||
|
||
### Performance
|
||
|
||
- **N+1 in `/api/insights/comprehensive` fixed.** The medication-compliance
|
||
loop hit Postgres once per active medication. It now batches into a
|
||
single `medicationId IN (...)` query and groups in memory. Latency
|
||
improvement scales linearly with the user's active medication count.
|
||
|
||
### Reliability
|
||
|
||
- **pg-boss draining on SIGTERM/SIGINT.** `docker stop`, Coolify redeploys,
|
||
and Kubernetes pod terminations now trigger `boss.stop({ graceful: true,
|
||
timeout: 30s })` so in-flight handlers finish instead of being killed.
|
||
Previously, pending handlers could be lost or replayed on restart.
|
||
- **CI now blocks on TypeScript strict checks.** `continue-on-error: true`
|
||
removed for typecheck. Tests already were blocking. ESLint stays
|
||
non-blocking for now — the existing `settings/page.tsx` monolith has
|
||
long-standing `react-hooks/set-state-in-effect` violations whose proper
|
||
fix lives in the 1.4.0 settings-split refactor; a blocking lint here
|
||
would just gate every PR until that refactor lands. Required cleaning
|
||
up two `any` types in `api-handler.ts` (justified inline — Next.js
|
||
variadic handler signature constrained by `Promise<Response>` return).
|
||
|
||
### Polish
|
||
|
||
- **Bottom-nav touch targets** sized to WCAG 2.5.5 minimum (44×44 CSS px).
|
||
Visual icon stays at 20 px so the design doesn't shift.
|
||
- **Phase-config dialog** marks the decorative coloured dot `aria-hidden`
|
||
because the redundant text label already conveys the phase to screen
|
||
readers. No more meaningless `image` node announcements.
|
||
- **Admin-status labels** for "Web Push" and "Bug Report" now go through
|
||
`t()` (new `admin.integrationWebPush` / `admin.integrationBugReport`
|
||
keys in en + de). They were the last hard-coded English strings on
|
||
the admin page.
|
||
|
||
### What's _not_ in this release (tracked for later)
|
||
|
||
- **Onboarding redesign** (dashboard-first empty-state flow + persistent
|
||
Getting Started checklist) and the **typed `apiClient` wrapper** that
|
||
underpins it are tracked for a focused 1.4.0 cycle. The 1.2.1 patch
|
||
already closed the acute symptoms of #87 (silent-failure toast +
|
||
default schedule), so the redesign is now a proper UI investment
|
||
rather than a bug-fix.
|
||
- **Withings sync is mapping both new measures**, but **a dedicated
|
||
Bearer-auth ingest endpoint for external pipelines** (n8n + Health
|
||
Connect, requested in #89) ships in 1.4.0 alongside the API-token
|
||
flow.
|
||
|
||
## [1.2.1] — 2026-04-27
|
||
|
||
### Fixed
|
||
|
||
- **Onboarding**: Medications added during onboarding are now actually persisted (closes #87). The wizard previously sent an empty `schedules: []` array, the server-side validation rejected it with a 422, the client never checked `response.ok`, and the user was redirected to the dashboard as if everything had worked. Onboarding now wraps each step in `try/catch`, surfaces failures via toast, and attaches a default reminder window (`08:00–09:00 daily`) so the medication actually persists. A hint under the medication list explains the default.
|
||
- **Docker setup** (closes #88):
|
||
- `docker-compose.yml` now uses `ports: "3000:3000"` (was `expose: "3000"`, which made the app unreachable from the host).
|
||
- `POSTGRES_PASSWORD` is a single env var that both the Postgres service and `DATABASE_URL` interpolate, so they cannot drift apart.
|
||
- `.env.example` now points at the in-container hostname `db:5432` (was `localhost:5432`, which never resolves inside the app container).
|
||
- **Documentation**:
|
||
- `package.json` synced to 1.2.0 (was lagging on 1.1.0).
|
||
- `CLAUDE.md` and `AGENTS.md` corrected to 23 models (the `Feedback` model added in v1.2 was missing from the count).
|
||
- `README.md` Quick Start gives a realistic time estimate, generates the four secrets in one block straight into `.env`, and points reverse-proxy users at the docs.
|
||
|
||
### Added — Tooling & Supply Chain
|
||
|
||
- **Pre-built multi-arch images on GHCR**: `.github/workflows/docker-publish.yml` now builds `linux/amd64` + `linux/arm64` images on every push to `main` and on every `v*` tag, publishing to `ghcr.io/mbombeck/healthlog`. Self-hosters no longer need a build toolchain — `docker compose pull && docker compose up -d` is enough. The bundled `docker-compose.yml` references the published image with a `build:` block as fallback for contributors.
|
||
- **Supply-chain attestations**: each published image carries a SLSA build provenance statement and a Software Bill of Materials. `SECURITY.md` documents how to verify them and how to pin a specific version.
|
||
- **Documentation single source of truth**: `getting-started/installation.mdx` is now the canonical setup guide (mirrors the bundled `docker-compose.yml`); `self-hosting/docker.mdx` slimmed to image internals + ops notes only. The landing page's Quick Start terminal block now includes the secrets-generation step (was missing).
|
||
|
||
### Notes
|
||
|
||
This is a patch release that closes the install/onboarding friction reported in #87 and #88. The bigger user-facing changes (additional measurement types like total body water and bone mass per #89, full onboarding redesign, typed API client) are tracked for `1.3.0`.
|
||
|
||
## [1.2.0] — 2026-04-18
|
||
|
||
### Added — Personalization, Glucose & Multi-Provider AI
|
||
|
||
- **Per-user custom thresholds**: Override the computed default ranges (BP, BMI, glucose, pulse) with values from your clinician. Audit-logged with previous/new values and timestamps. Doctor Report PDF flags custom ranges and prints both your target and the standard guideline value.
|
||
- **Blood glucose tracking**: New metric with `fasting`, `postprandial`, `random`, and `bedtime` contexts. Display unit switch between mg/dL and mmol/L (lossless conversion). Context-aware classification per ADA 2024 / DGIM. Per-context charting on dashboard and Doctor Report PDF.
|
||
- **Dashboard customization**: Show/hide and drag-to-reorder every dashboard widget. Per-user preference, reset-to-defaults button. Layout persists across the same user on the same device.
|
||
- **Built-in feedback system**: New in-app Send Feedback flow (Bug / Feature / Question / Other) with anonymized system info attachment. Stored in HealthLog's own database — no GitHub config required. Optional `Escalate to GitHub` button for admins who configure a PAT.
|
||
- **Multi-provider AI insights**: Provider abstraction extended with **Anthropic Claude** and **local OpenAI-compatible endpoints** (Ollama, LM Studio, vLLM, LiteLLM) alongside OpenAI. Per-user provider selection. Local endpoints keep all health data on your network.
|
||
- **Locale-aware UI polish (English-first)**: Numbers, dates, glucose units, BP, weight, and BMI all formatted via `useFormatters()` from the active locale. Doctor Report PDF and AI insight prompts now respect locale end-to-end (no hand-rolled `Intl.*` with fixed locales).
|
||
|
||
### Changed
|
||
|
||
- Reference range computation extracted into a dedicated `src/lib/health/thresholds.ts` module with computed defaults and override resolution.
|
||
- AI provider routing reworked to dispatch by `provider` field on the user record; OpenAI remains the default for legacy users.
|
||
- Dashboard route renders widgets from `UserDashboardLayout` model when present, otherwise falls back to the default order.
|
||
- Doctor Report PDF: locale-aware headers, glucose section, custom-range badges.
|
||
|
||
### Security
|
||
|
||
- GitHub PAT for feedback escalation stored AES-256-GCM encrypted in the database (never as env var).
|
||
- Local AI endpoint URLs validated against SSRF (no localhost/RFC1918 unless explicitly allowed by admin).
|
||
- Custom threshold writes rate-limited and audit-logged with IP.
|
||
|
||
## [1.1.0] — 2026-04-06
|
||
|
||
### Added — AI Insights Overhaul
|
||
|
||
- **ChatGPT Proxy Integration**: Insights now run through a local openai-oauth proxy using your ChatGPT subscription — no separate API billing required
|
||
- **Admin AI Fallback**: Admins can configure a global API key (OpenAI/OpenRouter) as fallback for users without their own connection
|
||
- **Provider Abstraction**: New `src/lib/ai/` module with pluggable providers (Codex OAuth, Admin Key, None) and automatic failover
|
||
- **Medical Insight Prompts**: 7 specialized prompts based on ESC/ESH 2023, WHO, DGE, and DEGAM guidelines
|
||
- Blood Pressure: ESC/ESH classification, morning risk ladder (J-HOP), pulse pressure, seasonal variation
|
||
- Weight: 5%/10% milestone recognition, plateau detection, body composition divergence
|
||
- Pulse: Fitness interpretation ladder, 80-100 bpm elevated-risk band, rate-pressure product
|
||
- BMI: Age-adjusted DEGAM classification for 65+
|
||
- Medication Compliance: Chronotherapy hints, mood-adherence risk prediction, 90-day tracking
|
||
- General Status: Cross-domain synthesis with cardiovascular risk stratification
|
||
- Mood: Bidirectional correlations with vitals and adherence
|
||
- **Enriched Feature Extraction**:
|
||
- Sleep duration and activity steps (previously ignored)
|
||
- Rate-Pressure Product (pulse × systolic BP, myocardial demand indicator)
|
||
- Body composition divergence flag (weight stable + body fat rising)
|
||
- Mood-adherence risk predictor
|
||
- Seasonal BP variation (winter vs summer, requires >180 days data)
|
||
- BP standard deviation (sdSys30/sdDia30) as variability risk marker
|
||
- Pulse pressure (arterial stiffness marker)
|
||
- 5 cross-metric Pearson correlations (weight↔BP, pulse↔BP, mood↔pulse, mood↔BP, mood↔weight)
|
||
- 90-day averages and all-time statistics for all metrics
|
||
- Historical comparison (current 7d vs previous 30d baseline)
|
||
- **New UI Components**:
|
||
- `InsightStatusCard`: Compact per-metric status card with classification indicator and fade-in animation
|
||
- `InsightAdvisorCard`: Premium structured card with findings, correlations, recommendations (ready for integration)
|
||
- **OAuth Routes**: `/api/auth/codex/authorize`, `/callback`, `/disconnect` for ChatGPT connection
|
||
- **Admin AI Settings**: `/api/admin/ai-settings` for global API key management
|
||
|
||
### Changed
|
||
|
||
- Insight prompts now use personal advisor tone ("dein Blutdruck") with positive-first pattern
|
||
- Reasoning scaffold in system prompt (What changed? → Why? → What to do?)
|
||
- Conditional correlation instructions (only mention when |r| > 0.4)
|
||
- InsightResult schema enriched with `insightType`, `primaryRecommendation`, `classificationLabel`
|
||
- BP target calculation now uses paired readings (both sys AND dia must be in range simultaneously)
|
||
- Medication streak tracking extended from 7-day to 30-day window
|
||
- CSP updated to allow `chatgpt.com` for OAuth flow
|
||
|
||
### Security
|
||
|
||
- Rate limiting on all OAuth and admin endpoints
|
||
- PKCE (S256) + state parameter for OAuth CSRF protection
|
||
- Encrypted token storage at rest (AES-256-GCM)
|
||
- Error messages truncated to prevent upstream response body leaks
|
||
- Admin key preview shows last 4 chars of decrypted key (not ciphertext prefix)
|
||
- `prefers-reduced-motion` support for insight card animations
|
||
|
||
### Removed
|
||
|
||
- `openaiKeyEncrypted` field from User model (replaced by provider abstraction)
|
||
- Direct OpenAI API calls from insight generators (now routed through provider)
|
||
- Legacy API key input in settings UI (replaced by ChatGPT connect button)
|
||
|
||
## [1.0.1] — Previous release
|