Files
HealthLog/CHANGELOG.md
T
Marc-André BombeckandClaude Opus 4.7 4dcaa08b42 chore(release): v1.4.15
Bumps version to 1.4.15 and adds the corresponding CHANGELOG entry
covering backup completeness, achievements UI, onboarding tour,
doctor-report v2, auto-deploy on GHCR push, empty-states sweep,
notification + integration status UI, AI hallucination guardrails,
mobile UX fixes, and the BD-Zielbereich percentage fix.

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

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

1661 lines
89 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
## [1.4.15] — 2026-05-09
### Added
- **Backups: vollständig.** Die Sicherung am Sonntag früh ist jetzt
nur noch der Anfang. `/admin/backups` lässt jeden Snapshot als
`.json` herunterladen, neue Snapshots hochladen und — hinter einer
dreifachen Bestätigung („RESTORE“ tippen + Dialog + Confirm) —
einen Snapshot direkt in die Datenbank zurückspielen. Jede
Operation (Run, Download, Upload, Restore inkl. Start/Failure)
landet im Audit-Log mit Akteur und Ziel-Snapshot.
_Backups become a full lifecycle. The Sunday-morning snapshot is
no longer the end of the road. `/admin/backups` lets you download
any snapshot as `.json`, upload a new one, and — behind a
triple-confirm gate (type “RESTORE” + dialog + confirm) — restore
a snapshot straight into the database. Every operation (run,
download, upload, restore including start/failure) is recorded in
the audit log with actor and target snapshot._
- **Erfolge-Seite (`/achievements`) und Dashboard-Karte.** Dedizierte
Seite zeigt freigeschaltete und gesperrte Erfolge inklusive
Fortschrittsbalken (`{aktuell} / {ziel}`), gruppiert nach
Kategorie (Medikation, Vitalwerte, Sicherheit, Engagement). Auf
dem Dashboard sitzt eine zuschaltbare „Letzte Erfolge“-Karte; die
Sichtbarkeit lässt sich in den Layout-Einstellungen umschalten.
_New `/achievements` page lists locked + unlocked achievements
with progress bars (`{current} / {target}`), grouped by category
(medication, vitals, security, engagement). The dashboard gains a
toggleable “Recent achievements” card; visibility lives in the
Layout settings._
- **Onboarding-Tour für Erstnutzer.** Beim ersten Dashboard-Besuch
läuft ein Spotlight-Walk-Through über die Tile-Strip, das
Quick-Add-Menü, Insights, Integrationen und Erfolge. Skippable mit
Esc, Tastatur-navigierbar, `prefers-reduced-motion` respektiert.
Erneut anstoßen jederzeit unter Settings → Account → „Tour
wiederholen“.
_New onboarding tour for first-run users. A spotlight walk-through
on first dashboard load points out the tile strip, quick-add menu,
insights, integrations, and achievements. Skippable with Esc,
fully keyboard-navigable, honors `prefers-reduced-motion`. Replay
the tour any time from Settings → Account._
- **Doctor-Report v2.** Vor dem PDF-Generieren öffnet sich ein
Dialog mit konfigurierbarem Zeitraum (Voreinstellungen 90 Tage / 6
Monate / 12 Monate, manuell editierbar, max. 2 Jahre) und einem
optionalen Praxis-Namen, der auf der Titelseite des PDFs
erscheint. Der Praxis-Name wird als Nutzervorgabe gespeichert.
_Doctor-report v2. Before the PDF is generated a dialog asks for
the date range (presets 90 days / 6 months / 12 months, manually
editable, max 2 years) and an optional practice name that appears
on the PDF cover page. The practice name is persisted as a user
preference._
- **Auto-Deploy nach GHCR-Push.** Sobald die Docker-Publish-Action
ein neues `:latest`-Image hochgeladen hat, ruft sie den
Coolify-Deploy-Webhook auf. Coolify pingt anschließend einen
internen Endpoint mit dem Ergebnis; Erfolg/Fehler/Unbekannt
landen im Audit-Log, persistente Failures triggern eine
Telegram-Benachrichtigung an alle Admins. Kein manuelles
Force-Pull mehr nötig.
_Auto-deploy on GHCR push. Once the Docker publish action ships a
new `:latest`, it calls the Coolify deploy webhook. Coolify in
turn pings an internal endpoint with the result; success / failure
/ unknown all land in the audit log; persistent failures send a
Telegram alert to every admin. No more manual force-pull on the
host._
- **Empty-States überall.** Brandneue Accounts und leere Listen
bekommen jetzt überall einen sinnvollen Empty-State (Icon +
Beschreibung + Call-to-Action), wo vorher nur weiße Fläche stand.
Betroffen: Admin-Tabellen (Users, Backups, Login-Overview,
API-Tokens, Feedback, Audit-Preview), Messungs-, Stimmungs-,
Medikamenten- und Erfolge-Listen, Insights-Top-Level + BMI-ohne-
Größe, Dashboard für komplett leere Accounts.
_Empty states everywhere. Brand-new accounts and empty lists now
always show a sensible empty state (icon + description + CTA)
where there used to be just white space. Coverage: admin tables
(users, backups, login-overview, api-tokens, feedback,
audit-preview), measurement / mood / medication / achievement
lists, the insights top-level + BMI-without-height view, and the
dashboard for fully empty accounts._
- **Status-Karten für Notification-Channels.** Settings →
Benachrichtigungen zeigt für jeden Kanal (Telegram, ntfy, Web
Push) den aktuellen Status (verbunden / fehlerhaft / deaktiviert),
letzten Erfolg, letzten Fehler, Counter aufeinanderfolgender
Fehlversuche und ggf. Deaktivierungsgrund. Buttons: „Wieder
aktivieren“ (nur wenn auto-deaktiviert) + „Test-Nachricht
senden“.
_Notification channel status UI. Settings → Notifications now
shows per-channel (Telegram, ntfy, Web Push) the current state
(connected / error / disabled), last success, last failure,
consecutive-failure counter, and disable reason if any. Buttons
for “Re-enable” (only when auto-disabled) and “Send test”._
- **Status-Karten für Withings + moodLog.** Settings → Integrationen
zeigt für beide Provider Verbindungsstatus, letzten Sync, letzten
Fehler und einen Reauth-Hinweis, sobald ein Refresh-Token
abgelehnt wurde. Auf persistente Fehler (≥ 3 in Folge) bekommen
Admins eine Telegram-Benachrichtigung, sofern der Kanal aktiviert
ist.
_Withings + moodLog integration status UI. Settings → Integrations
shows per-provider the connection state, last sync, last error,
and a reauth hint as soon as a refresh-token was rejected. After
persistent failures (≥ 3 in a row) admins receive a Telegram alert
if the channel is enabled._
- **Top-Tiles unabhängig wählbar.** In den Layout-Einstellungen lässt
sich für jede Metrik die Sichtbarkeit der Trend-Kachel oben und
des Charts unten getrennt steuern. Bestehende Layouts behalten ihr
bisheriges Verhalten (eine Schaltfläche steuert beide), bis du
explizit den neuen Schalter umlegst.
_Top dashboard tiles selectable. The layout settings now expose a
separate toggle for each metric’s upper-row tile and lower-row
chart. Existing saved layouts keep their previous behaviour (one
switch controls both surfaces) until you explicitly flip the new
toggle._
- **7-Tage-Trend statt 7-Tage-Schnitt.** Auf jeder Tile-Kachel zeigt
ein farbiger Delta-Indikator `(±N.N)` neben dem Wert die
metrik-bewusste Veränderung der letzten 7 Tage gegenüber den
davor. Label umbenannt von „7-Tage-Schnitt“ auf „7-Tage-Trend“.
_7-day-trend instead of 7-day-average. Each tile gains a coloured
delta indicator `(±N.N)` next to the value, showing the
metric-aware change of the last 7 days vs. the prior 7. Label
renamed from “7d average” to “7d trend”._
### Changed
- **`/admin`-Übersicht neu gestaltet.** Statt einer Section-Grid
zeigt die Übersicht jetzt eine Audit-Log-Vorschau (letzte
Einträge, Akteur, Zielressource) und einen
System-Status-Snapshot. Die Section-Grid wandert in die Sidebar.
_The `/admin` overview replaces the section grid with an
audit-log preview (recent entries, actor, target resource) and a
system-status snapshot. The section grid moved into the sidebar._
- **Sidebar-Admin-Submenu kontextbewusst.** Die Admin-Untereinträge
klappen nur noch auf, wenn man tatsächlich auf einer
`/admin/*`-Route ist. Auf allen anderen Seiten bleibt die
Admin-Gruppe eingeklappt.
_The sidebar Admin sub-items only expand when you are actually on
an `/admin/*` route. Everywhere else the Admin group stays
collapsed._
- **Stimmung-Chart aggregiert wie die anderen Metriken.** Über 90
Tage wechselt das Stimmung-Diagramm automatisch auf wöchentliche
Aggregation, über 730 Tage auf monatliche — analog zu Gewicht und
Blutdruck. Im Header zeigt ein Chip die aktive Aggregationsstufe.
_The mood chart now auto-aggregates: weekly past 90 days, monthly
past 730 days — same thresholds as weight and BP. A chip in the
chart header shows the active aggregation level._
- **KI-Insights mit harten Anti-Halluzinations-Leitplanken.** Die
Provider-Antwort wird gegen ein striktes Zod-Schema geprüft
(Zusammenfassung, Empfehlungen mit Pflichtfeld `metricSource`,
Zitate, Warnungen). Empfehlungen, die Datenpunkte zitieren, die
nicht in der Snapshot-Liste stehen, werden abgewiesen. Der
Wrapper retried genau einmal mit korrigierender Systemnachricht;
scheitert auch der zweite Versuch, kommt 422 zurück statt
unsauberen Ausgaben. Slug-Drift im Codex-Backend (z. B. Wechsel
`gpt-5-codex` → `gpt-5.3-codex`) ist jetzt durch eine
konfigurierbare Fallback-Kette + 1-Stunden-Cache abgefedert; alle
Slugs durch → strukturierter 503 mit `attempted[]`-Liste.
_AI insights gain hard anti-hallucination guardrails. Provider
responses are validated against a strict Zod schema (summary,
recommendations with mandatory `metricSource`, citations,
warnings). Recommendations citing data points not present in the
snapshot are rejected. The wrapper retries exactly once with a
corrective system message; if the second attempt also fails, it
returns 422 instead of unsanitised output. Codex-backend slug
drift (e.g. `gpt-5-codex` → `gpt-5.3-codex`) is now cushioned by
a configurable fallback chain + 1-hour positive cache; if every
slug fails the route returns a structured 503 with the
`attempted[]` list._
- **`/admin/api-tokens`-Tabelle mobil.** Auf schmalen Viewports
blenden Spalten gestaffelt aus (zuletzt benutzt, erstellt am,
Owner) und der Owner-Username wandert inline neben den Token-
Namen, damit beim Ausblenden keine Information verloren geht.
_The `/admin/api-tokens` table is responsive on mobile. Narrow
viewports gate columns behind breakpoints (last-used, created-at,
owner) and the owner username falls back inline next to the token
name so hiding the column never drops data._
- **Stimmung-Kachel mobil.** Auf schmalen Viewports zeigt die
Mood-Karte oben jetzt nur noch die große Score-Zahl + Label statt
der doppelten Darstellung von vorher.
_Mood tile on mobile shows only the large score number + label,
no more doubled rendering._
- **Quick-Add-Submenu eindeutiger.** Die beiden Einträge im
Quick-Add-Menü heißen jetzt „Messung erfassen“ und „Stimmung
erfassen“ (DE) bzw. „Log measurement“ und „Log mood“ (EN) —
vorher beide schlicht „Hinzufügen“.
_Quick-add submenu disambiguated. The two entries are now called
“Messung erfassen” / “Stimmung erfassen” (DE) and “Log
measurement” / “Log mood” (EN) — previously both simply “Add”._
### Fixed
- **Blutdruck-Zielbereich-Prozent.** Die Zielbereich-Kachel zählte
Sys/Dia-Paare nicht korrekt, sobald durch Importe die Sys/Dia-
Zeitstempel weiter als 5 Minuten auseinander lagen — Ergebnis
konnte 0 % anzeigen, obwohl die meisten Werte im Zielbereich
waren. Fix: Tagesschlüssel-Fallback (gleicher Berliner Tag) +
Paaranzahl als Nenner.
_Blood-pressure target-range percentage. The target-range tile
miscounted sys/dia pairs once import drift pushed sys/dia
timestamps more than 5 minutes apart — the tile could show 0 %
even though most readings were inside the target range. Fix: a
same-Berlin-day key fallback + pair-count as denominator._
- **Onboarding-Flackern.** Auf bereits abgeschlossenen Accounts
rendert die Onboarding-Karte nicht mehr für ~500 ms, bevor sie
wieder verschwindet. Sie wird erst gemountet, sobald der
Analytics-Status geladen ist, und öffnet sich nicht mehr
automatisch, wenn alles erledigt ist.
_Onboarding flicker. On accounts where onboarding is already
complete, the onboarding card no longer renders for ~500 ms
before vanishing. It mounts only after the analytics status has
loaded, and refuses to auto-open when nothing is left to do._
- **Skip-Link blockiert Logo nicht.** Der „Zum Inhalt
springen“-Skip-Link liegt zwar weiterhin am Anfang des Tabbing-
Reihenfolge, blockiert aber nicht mehr Klicks aufs Logo.
_The “Skip to content” skip-link still leads the tab order but
no longer blocks clicks on the logo._
- **Bug-Report-Eintrag richtet sich nach dem Admin-Toggle.** Wenn
der Admin den Bug-Report deaktiviert hat, verschwindet der
Eintrag aus Sidebar, Bottom-Nav, Topbar und der Fehler-Detail-
„Bug melden“-Schaltfläche.
_The “Report a bug” entry follows the admin feature flag. With
bug reporting disabled, the entry vanishes from the sidebar,
bottom-nav, topbar, and the error-detail “Report bug” button._
- **Feedback-Link richtet sich nach dem Admin-Toggle.** Wenn die
Feedback-Sammlung deaktiviert ist, ist auch der UI-Einstieg
weg.
_The Feedback link follows its admin feature flag. With feedback
disabled, the UI entry point disappears too._
- **Notification-Channels deaktivieren sich auf harte Reject-
Fehler.** Antworten Telegram / ntfy / Web Push wiederholt mit
410 oder anderen permanenten Reject-Codes, deaktiviert sich der
Kanal automatisch und schreibt einen Audit-Log-Eintrag. Status
- Re-Enable-Button finden sich in Settings → Benachrichtigungen.
_Notification channels auto-disable on persistent hard rejects.
If Telegram / ntfy / Web Push respond repeatedly with 410 or
other permanent reject codes, the channel disables itself and
writes an audit-log entry. Status + re-enable button live in
Settings → Notifications._
- **Refresh-Token-Fehler markiert Integration als reauth-pflichtig.**
Bricht der Withings- oder moodLog-Refresh-Token-Tausch ab
(typischerweise nach 90 Tagen ohne Re-Auth), wechselt die
Integration in `error_reauth` und der Settings-Eintrag bittet um
Neu-Verbindung — statt jeden Sync-Tick weiter gegen den Provider
zu hämmern.
_Refresh-token failures flip the integration to “needs reauth”.
When the Withings or moodLog refresh-token exchange fails
(typically after 90 days without re-auth), the integration
switches to `error_reauth` and the Settings entry asks for
re-connect — instead of hammering the upstream every sync
tick._
- **Charts blockieren das Wischen nicht mehr.** Auf Touch-Geräten
fingen die Recharts-Wrapper vertikales Wischen ab; das fühlte
sich auf langsamen Geräten wie Hänger an. Fix: `touch-action:
pan-y` auf den Chart-Containern, vertikaler Scroll geht jetzt
durch.
_Mobile chart containers no longer eat vertical scroll. On touch
devices the Recharts wrappers swallowed vertical pans; on slower
devices that felt like a scroll lockup. Fix: `touch-action:
pan-y` on the chart wrappers, vertical scroll passes through
again._
- **`/admin/users` mobil ohne Overflow.** Auf schmalen Viewports
rendert die User-Tabelle als Card-List statt einer
horizontal-scrollenden Tabelle.
_`/admin/users` on mobile renders as a card list instead of a
horizontally-scrolling table._
- **A11y-Verbesserungen.** Chart-Range-Buttons, Medikamenten-
Primary-Buttons, Mood-List-Mobile-Icon-Buttons, Login-CTAs,
Settings → Account-Passkey-Tabelle, Onboarding-Tour-Focus-Trap +
DE-Overflow + Backdrop-Ring — alles auf 44 px Mindest-
Tap-Target bzw. mit `aria-label` / Keyboard-Bedienbarkeit
versehen.
_Accessibility round of fixes: chart range buttons, medication
primary buttons, mood-list mobile icon buttons, login CTAs,
Settings → Account passkey table, onboarding-tour focus trap +
DE-locale overflow + backdrop ring — all aligned to 44 px tap
targets and labelled / keyboard-navigable._
### Security
- **AI-Provider-Antworten gegen striktes Schema validiert.** Der
Insight-Wrapper akzeptiert nur Antworten, die das strikte
`aiInsightResponseSchema` erfüllen, und verweigert Empfehlungen,
die Datenpunkte zitieren, die nicht im übergebenen Snapshot
stehen. Schemafehler triggern einen einmaligen Retry mit
korrigierender Systemnachricht; ein zweiter Fehler liefert 422
statt halluzinierter Ausgaben.
_AI provider responses are validated against a strict Zod
schema. The insight wrapper only accepts responses that satisfy
the strict `aiInsightResponseSchema`, and rejects recommendations
citing data points absent from the supplied snapshot. Schema
failures trigger a single retry with a corrective system message;
a second failure returns 422 instead of hallucinated output._
- **Backup-Operationen vollständig auditiert.** Run, Download,
Upload und Restore — inklusive Start-Marker, Denial-Reasons und
Failures — schreiben jeweils einen Audit-Log-Eintrag mit Akteur
und Snapshot-ID. Restore wird zusätzlich durch fünf unabhängige
Gates abgesichert (Cookie-Auth-only-Admin, `confirm: "RESTORE"`-
Body, getypte UI-Bestätigung, Idempotency-Key, Pre-Tx-Enum-
Validation).
_Backup operations are fully audited. Run, download, upload, and
restore — including start markers, denial reasons, and failures
— each write an audit-log entry with actor and snapshot ID.
Restore is additionally protected by five independent gates
(cookie-only admin auth, `confirm: "RESTORE"` body, typed UI
confirmation, idempotency-key wrap, pre-transaction enum
validation)._
## [1.4.14] — 2026-05-09
### Added
- **Admin-Bereich nach Sektionen aufgeteilt.** Statt einer monolithischen
`/admin`-Seite mit Anker-Sprüngen hat jede Sektion jetzt ihre eigene
Route: `/admin/system-status`, `/admin/services`,
`/admin/integrations`, `/admin/feedback`, `/admin/reminders`,
`/admin/users`, `/admin/api-tokens`, `/admin/login-overview`,
`/admin/backups`, `/admin/danger-zone`. Sidebar bekommt eine
ausklappbare Admin-Gruppe; Status-Cards verlinken direkt auf die
passende Unterseite. Alte `/admin#section-…`-Pfade werden serverseitig
weitergeleitet.
_The admin area is split into per-section pages instead of one
monolithic dashboard with anchor jumps. Each section has its own URL
(`/admin/system-status`, `/admin/services`, `/admin/integrations`,
`/admin/feedback`, `/admin/reminders`, `/admin/users`,
`/admin/api-tokens`, `/admin/login-overview`, `/admin/backups`,
`/admin/danger-zone`); the sidebar grows an expandable Admin group;
status cards link to the relevant sub-page; legacy
`/admin#section-…` paths are redirected server-side._
- **Neue Backups-Sektion** unter `/admin/backups` mit Tabelle aller
bisherigen Sicherungen (Größe, Typ, Erstellungszeit) und einem
„Backup jetzt erstellen“-Button, der einen Ad-hoc-Job auf der
pg-boss-Queue auslöst.
_New `/admin/backups` view with a table of all stored backups (size,
type, timestamp) and a “Backup now” button that enqueues an ad-hoc
pg-boss job._
- **Neue User-Verwaltung** unter `/admin/users` mit Filter-Pills
(alle / nur Admins / nur Standardnutzer) und einer
„Force-Logout“-Aktion pro Nutzer hinter einem Bestätigungsdialog
(löscht alle aktiven Sessions des Ziels und schreibt einen
Audit-Log-Eintrag). Selbst-Logout ist gesperrt.
_New `/admin/users` view with role filter pills (all / admins only /
users only) and a per-row force-logout action behind a confirmation
dialog (deletes every active session of the target and writes an
audit-log entry). Self-target is disabled._
- **„Gespeicherten KI-Schlüssel entfernen“** in Settings → KI: ein
klar betitelter Knopf mit Bestätigungsdialog löscht den
gespeicherten OpenAI- bzw. lokalen Provider-Key, ohne Settings
ansonsten anzufassen.
_New “Remove saved AI key” button in Settings → AI: a clearly
labelled button behind a confirmation dialog clears the stored
OpenAI or local provider key without touching anything else._
- **`CODEX_MODEL`-Env-Var** als Operator-Override für das Codex-
Modell-Slug, damit alternative Modelle ohne Rebuild getestet werden
können (z. B. wenn dein ChatGPT-Plan ein anderes Default-Modell
bevorzugt).
_New `CODEX_MODEL` env var lets operators override the Codex model
slug without a rebuild — useful for testing alternate slugs against
different ChatGPT plan tiers._
### Changed
- **Trend-Pfeil-Farben sind jetzt metrik-bewusst.** Steigende Tendenz
bedeutet je nach Messwert etwas anderes: Blutdruck und Gewicht ↑ =
orange (Warnung), Stimmung ↑ = grün (positiv), Puls ↑ = neutral
(kontextabhängig). Die Pfeil-Richtung selbst bleibt unverändert.
_Trend-arrow colors are metric-aware: BP and weight rising = orange
(warning), mood rising = green (positive), pulse rising = neutral
(context-dependent). Arrow direction itself is unchanged._
- **Wipe-all-data deckt jetzt auch Notification-Channels und Web-Push-
Subscriptions ab.** Verschlüsselte Telegram-Bot-Tokens und
Web-Push-Endpoints überleben einen vollständigen Account-Wipe nicht
länger. Feedback und Audit-Log bleiben erhalten (wie bisher).
Bestätigungstext im UI nennt den neuen Umfang explizit.
_“Wipe all data” now also clears notification channels and web-push
subscriptions: encrypted Telegram bot tokens and web-push endpoints
no longer survive a full account wipe. Feedback and audit log stay
untouched (unchanged). The confirmation copy spells out the new
scope._
- **i18n-Schlüssel im Admin-Bereich** unter `admin.section.<slug>.*`
reorganisiert (EN + DE-Parität).
_Admin-area i18n keys reorganised under `admin.section.<slug>.*`
(EN + DE parity)._
### Fixed
- **Codex/ChatGPT-Modell-Slug korrigiert.** Der Codex-Backend-Endpoint
lehnt sowohl `gpt-5-codex` als auch `gpt-5` für ChatGPT-Account-
Auth ab; das tatsächliche, codex-optimierte Slug für die Plus/Pro-
Tarife heißt `gpt-5.3-codex`. v1.4.14 verdrahtet den richtigen
Default. „Verbindung testen“ in Settings → KI und der Insight-
Generator laufen damit beide gegen dein ChatGPT-Abo durch.
_Codex/ChatGPT model slug corrected: the Codex backend rejects both
`gpt-5-codex` and `gpt-5` when authenticated via a ChatGPT account;
the codex-optimised slug for the Plus/Pro tiers is `gpt-5.3-codex`.
v1.4.14 wires the correct default. Settings → AI “Test connection”
and the insight generator now both succeed against your ChatGPT
subscription._
- **Sommerzeit-Wechsel verschiebt keine Tageswerte mehr.** Die
Cross-Metric-Tagespaarung in den Insight-Buckets rechnete früher in
reinem UTC und verlor an den Berliner DST-Grenzen einen Tag. Die
neue Helper `dayOffsetToBerlinDayKey()` arbeitet DST-immun (Anker
über Berliner Y-M-D, dann Subtraktion in 86*400_000-ms-Schritten);
die Tagespaarung über Blutdruck, Gewicht und Stimmung ist jetzt
über DST-Wechsel hinweg konsistent.
\_DST transitions no longer shift daily values. The cross-metric
daily bucket pairing previously did naive UTC math and dropped a
day at the Berlin DST boundary. A new `dayOffsetToBerlinDayKey()`
helper anchors at Berlin Y-M-D and subtracts in 86_400_000-ms
steps; bucket pairing across blood pressure, weight, and mood is
now DST-safe.*
- **KI-Provider-Fehler werden korrekt klassifiziert.**
`/api/insights/generate` antwortet auf 401/403-Fehler des Providers
jetzt mit 422 („KI-Anbieter hat den Request abgelehnt — bitte
API-Key in Settings → KI prüfen“), auf 5xx mit 503 („KI-Anbieter
vorübergehend nicht erreichbar“) und auf 429 mit 429. Vorher kam
pauschal 500. Volle Fehlerdetails landen weiter im Wide-Event-Log.
_AI provider errors are now correctly classified by
`/api/insights/generate`: provider 401/403 → 422 (“AI provider
rejected the request — check your API key in Settings → AI”),
provider 5xx → 503 (“AI provider temporarily unavailable”), 429 → 429. Previously everything returned 500. Full error context still
lands in the structured logs._
- **Logging-Redactor schluckt keine harmlosen Wörter mehr.** Der
Secret-Redactor maskierte zuvor jedes Vorkommen von `sk-…` — auch
in `task-force`, `risk-management`, `disk-io`. Die neue Regex
verlangt eine Wortgrenze und mindestens 8 Folgezeichen, das Padding
bleibt für echte API-Keys (`sk-…`, `sk-ant-…`) intakt.
_The structured-logging secret redactor no longer mangles innocent
words. The previous regex matched any `sk-…` substring and masked
fragments inside `task-force`, `risk-management`, `disk-io`. The
new pattern requires a word boundary and at least 8 trailing
characters, while still scrubbing real API keys (`sk-…`,
`sk-ant-…`)._
### Performance
- **Insights-Seite startet schneller.** Die Recharts-Symbole für die
Korrelations-Scatter-Plots werden jetzt bedarfsweise per
`next/dynamic` geladen statt eager mitausgeliefert; das spart
ca. 108 KiB initiales JavaScript auf der Insights-Seite. Die
Chart-Optik bleibt unverändert.
_Bundle-size improvements on the insights page: Recharts symbols
for the correlation scatter plots are now loaded on demand via
`next/dynamic` instead of being shipped eagerly, saving roughly
108 KiB of initial JavaScript on `/insights`. Chart visuals are
unchanged._
- **Dashboard fragt die Onboarding-Checkliste nicht mehr ab, wenn
sie schon abgeschlossen ist.** Sobald `onboardingCompletedAt`
gesetzt ist, entfallen die `withings/status`- und
`notifications/preferences`-Calls beim Dashboard-Aufruf. Spart
rund 950 ms Netzwerkzeit bei wiederkehrenden Nutzern.
_The dashboard skips the onboarding-checklist API requests once
onboarding is complete. With `onboardingCompletedAt` set, the
`withings/status` and `notifications/preferences` queries no longer
fire on dashboard load — saves about 950 ms of network time for
established users._
### Tests & A11y
- **Erweiterte End-to-End-Suite.** Neue Playwright-Specs decken den
authentifizierten Dashboard-Render, den „Messung anlegen“-Flow,
den Doctor-Report-PDF-Download, einen gemockten Codex-Connect-
Flow, einen gemockten Insights-Generate-Flow und einen
Mobile-Viewport-Smoketest (Pixel 5) ab.
_Extended end-to-end suite. New Playwright specs cover the
authenticated dashboard render, the “add measurement” flow, the
doctor-report PDF download, a mocked Codex connect flow, a mocked
insights-generate flow, and a mobile-viewport smoke test (Pixel 5)._
- **Axe-core-Audit auf allen Schlüsselrouten grün.** Die ernsten
und kritischen Verstöße auf `/dashboard`, `/settings/integrations`
und `/admin` (inkl. `/admin/system-status` und `/admin/users`)
wurden behoben — fehlende `aria-label`s an Icon-Buttons, fehlende
Namen am Mobile-User-Menü-Trigger, leere `<dd>`-Platzhalter,
Login-Link nur durch Farbe erkennbar, Quick-Add-Menü-Items mit
doppeltem Namen.
_Axe-core accessibility audit now clean of serious/critical
violations on `/dashboard`, `/settings/integrations`, and `/admin`
(incl. `/admin/system-status`, `/admin/users`). Fixes: missing
aria-labels on icon-only buttons, unnamed mobile user-menu trigger,
empty `<dd>` placeholders, sign-up link distinguishable only by
color, quick-add menu with two indistinguishable items._
### Docs
- **Dokumentations-Site auf v1.4.14 aktualisiert** — Codex-Device-
Code-Pfad neu beschrieben, neue Admin-Sektionen (Backups, Users)
dokumentiert, `CODEX_MODEL`-Env-Var aufgenommen,
Dashboard-Performance-Hinweis ergänzt, Codex-Troubleshooting-Block
überarbeitet.
_Documentation site brought up to date with v1.4.14 — Codex
device-code flow rewritten, new Admin sections (Backups, Users)
documented, `CODEX_MODEL` env var added, dashboard performance note
added, Codex troubleshooting block revised._
## [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