mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
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>
1661 lines
89 KiB
Markdown
1661 lines
89 KiB
Markdown
# 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
|