mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
Maintainer-voice sweep across `src/`. ~190 references to the maintainer by first name in code comments and JSDoc neutralised to "the maintainer", "the live tenant", "users", or "the user" depending on context. Three sed passes covering ~50 distinct patterns (`Marc reported`, `Marc wants`, `Marc kept`, `Marc rolled back`, etc.). Test fixtures (`displayName: "Marc B."`, `username: "marc"`, `<HeroStrip userName="Marc">`) kept as opaque test data per the brief. CHANGELOG normalisation. The v1.4.14 + v1.4.15 entries paired a German bullet with its English translation by historical design — the in-app copy is English-default and the user docs are English, so the mixed-language style was a holdover from the early-marathon notes. A one-off node script extracted the `_..._` English paragraph from each bilingual bullet and rewrote the entry as that text. ~44 bullets reduced; one Docs bullet that lost a leading word in the regex fixed by hand. CLAUDE.md → CONTRIBUTING-AI.md rename so the filename is no longer AI-vendor-specific. `CONTRIBUTING.md` reference updated. `AGENTS.md` stays in place for multi-agent compatibility (the conventions doc is shared with Codex / Cursor / etc.). Wave-4 state wrap. STATE.md now records C1–C5 + D + D2 as complete; v1421-backlog.md grew a "Closed in v1.4.22" header listing the 20 items the marathon landed; `.planning/phase-W4-report.md` documents the per-commit decisions, test deltas, and the 23 items deferred to v1.4.23 with reasoning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1961 lines
104 KiB
Markdown
1961 lines
104 KiB
Markdown
# Changelog
|
||
|
||
## [1.4.21] — 2026-05-10
|
||
|
||
### Fixed
|
||
|
||
- **Daily Briefing regenerate produced an empty card.** The
|
||
`/api/insights/generate` route was still calling the legacy
|
||
`getInsightsSystemPrompt`, which returns the v1.4.5 schema and
|
||
never asks the model for a `dailyBriefing` block. Re-runs from the
|
||
hero strip therefore stored a payload with no briefing and the
|
||
card painted its empty state forever. The route now uses
|
||
`getStrictInsightsSystemPrompt(locale)` so the v1.4.20 ground
|
||
rules apply to manual regeneration too. Cached legacy blobs still
|
||
parse — every new schema field is optional with `passthrough()`.
|
||
- **Duplicate streaming bubble after the assistant reply landed.**
|
||
After SSE `done` the streaming hook keeps `streaming.content`
|
||
populated to support the in-flight render path, then fires a
|
||
TanStack invalidate that pulls the persisted assistant message
|
||
into `conversation.messages`. The thread therefore rendered the
|
||
reply twice until the next `send` reset cleared it. The render
|
||
path now suppresses the in-flight bubble as soon as the persisted
|
||
twin lands, keyed on `streaming.messageId`.
|
||
- **Settings cog overlapped by the drawer's close-X.** Radix Sheet
|
||
paints its default close-X at `top-4 right-4`, which visually
|
||
swallowed the cog in the right-edge button cluster. The cog moves
|
||
to the left header zone (next to the gradient avatar) and a
|
||
`pr-12 / sm:pr-14` padding rule keeps the New-chat button out of
|
||
the close-X area on narrower viewports.
|
||
- **Suggested-prompt chips below the 36px touch target.** Chips
|
||
rendered at 28px which fell short of the WCAG-AA target-size
|
||
guideline. Padding bumped to land on a 36px hit area.
|
||
|
||
### Changed
|
||
|
||
- **Coach context now carries day-level readings instead of
|
||
aggregates only.** `buildCoachSnapshot` folds in a
|
||
`timeline.recent` block (one row per UTC day for the last 14 days,
|
||
each tagged with weekday) and a `timeline.weekly` block (ISO-week
|
||
buckets covering the rest of the analysis window). Systolic and
|
||
diastolic pair into a single BP row per day; half-measured days
|
||
drop so the model never fabricates a complement. Per-day
|
||
medication-adherence rows draw from `MedicationIntakeEvent`. The
|
||
EN and DE Coach system prompts gain a DAY-LEVEL READINGS section
|
||
instructing the model to answer day-specific or weekday-specific
|
||
questions out of `timeline.recent` with date + weekday citations,
|
||
acknowledge missing days plainly, and fall back to
|
||
`timeline.weekly` for older windows. Token cost per Coach turn
|
||
rises from ≈190 tokens (aggregates only) to ≈3000 tokens for a
|
||
full-scope snapshot. The new scope picker is the relief valve.
|
||
|
||
### Added
|
||
|
||
- **Per-source + per-window scope picker on the Coach sources rail.**
|
||
The sources rail grew real per-source checkboxes (BP / Weight /
|
||
Pulse / Mood / Compliance — 36px touch target, 60% opacity when
|
||
excluded) and a window selector (last 7 days / 30 days / 90 days /
|
||
all-time). The drawer owns the scope state and forwards picked
|
||
scope through `useSendCoachMessage` to the chat request body.
|
||
Toggles reset to the all-source last-30-days default each drawer
|
||
open. The scope payload only ships when the user has narrowed
|
||
away from the default — server can tell "no opinion" from
|
||
"intentionally narrow". A single-source last-7-days narrowed turn
|
||
lands around ≈600–700 tokens.
|
||
|
||
## [1.4.20] — 2026-05-10
|
||
|
||
### Added
|
||
|
||
- **Insights redesign — hero strip, Daily Briefing, Suggested Prompts.**
|
||
`/insights` opens with a new hero strip that pairs a time-of-day
|
||
greeting and primary action row with a strip of suggested-prompt
|
||
chips. Below the hero, a Daily Briefing card surfaces an AI-generated
|
||
paragraph plus three keyFindings drawn from the last 24-hour window.
|
||
Suggested-prompt chips drive the Coach drawer with prefill text.
|
||
- **AI Coach drawer with streaming chat and encrypted persistence.**
|
||
A right-side drawer over `/insights` hosts a streaming Coach
|
||
conversation. `POST /api/insights/chat` is an SSE endpoint that
|
||
walks the existing AI provider chain and emits `token` →
|
||
`provenance` → `done` frames. Conversation history persists across
|
||
sessions; `GET /api/insights/chat`, `GET /api/insights/chat/[id]`,
|
||
and `DELETE /api/insights/chat/[id]` round-trip the history rail.
|
||
Source-chip provenance attaches to every assistant turn (metric,
|
||
window, n-count — labels only, never raw values). The drawer mounts
|
||
via the hero strip's "Ask the coach" button or any suggested-prompt
|
||
chip. Three-column layout on `lg+` (history rail · message thread ·
|
||
sources rail), full-screen single-column on mobile with chevron-
|
||
button trays for the rails.
|
||
- **Correlation discovery — 3 hypothesis cards.** A new
|
||
`<CorrelationRow>` between Daily Briefing and the Advisor card
|
||
surfaces three pre-defined hypotheses (BP × medication compliance,
|
||
mood × pulse, weight × weekday) with Pearson r + Fisher-z
|
||
confidence interval. Surfacing gate: n ≥ 14 paired observations
|
||
and p < 0.05; below the gate the cards stay collapsed.
|
||
- **Trends row with AI annotations.** A new `<TrendsRow>` mounts
|
||
alongside the correlation cards. Each annotation is a short
|
||
AI-generated callout tied to a specific date in the analyzed
|
||
window; the BP timeline gains storyboard markers at the same
|
||
positions via an additive `annotations[]` prop on `<HealthChart>`.
|
||
- **Weekly Report at `/insights/report/[week]`.** A newsletter-style
|
||
printable surface with Summary / Going-well / Worth-watching /
|
||
Tips / Data-quality sections. `window.print()` export via
|
||
Tailwind `print:` variants; the hero strip surfaces fresh weekly
|
||
reports with Read · Share · Export PDF actions (Web Share API
|
||
with clipboard fallback).
|
||
- **Personal Health Score (composite 0–100).** A deterministic
|
||
`<HealthScoreCard>` panel renders alongside the hero strip on
|
||
`lg+`. Score weights: 30% BP-target rate + 20% weight-trend
|
||
alignment + 20% mood stability + 30% medication compliance.
|
||
Three bands (green ≥75, yellow 50–74, red <50). "Ask the
|
||
Coach" CTA opens the drawer with a score-aware prefill.
|
||
|
||
### Changed
|
||
|
||
- **`PROMPT_VERSION` 4.19.0 → 4.20.2.** Three controlled bumps
|
||
across B1 → B3 → B4 carry GROUND RULES 8–11 covering the new
|
||
schema fields. `aiInsightResponseSchema` extended with optional
|
||
`dailyBriefing` / `trendAnnotations` / `weeklyReport` /
|
||
`storyboardAnnotations` / `healthScore` blocks; legacy v1.4.19
|
||
cached payloads still parse because every new field is nullable
|
||
+ optional.
|
||
- **Branch + release model.** `CONTRIBUTING.md` documents a
|
||
long-lived `develop` branch as the daily target and `main` as
|
||
release-only. End users follow `main` / latest tag; contributors
|
||
branch from and PR into `develop`. The mirror page at
|
||
`/contributing/branch-model/` on the docs site explains the
|
||
same flow for would-be contributors.
|
||
- **Repository hygiene.** New `.github/CODE_OF_CONDUCT.md`
|
||
(Contributor Covenant 2.1), three issue templates, a pull-request
|
||
template, expanded Dependabot scopes (npm + github-actions +
|
||
docker), and complete `package.json` metadata (description,
|
||
license, homepage, repository, bugs, keywords).
|
||
|
||
### Fixed
|
||
|
||
- **Coach SSE idempotency replay no longer returns null.** The
|
||
idempotency wrapper double-read the body on replay; the streaming
|
||
route now caches the original SSE response correctly. Also
|
||
repairs the dead error-frame branch and stabilises
|
||
`useSendCoachMessage` opts.
|
||
- **Streaming assistant text announces to screen readers.** The
|
||
message thread now has `aria-live="polite"` on the in-flight
|
||
bubble so token-by-token streams reach assistive tech.
|
||
- **Coach drawer width capped on `lg+`.** Drawer no longer hits
|
||
1080 px on common laptops; sources rail moves to a tray below
|
||
`xl`.
|
||
- **Hero glow z-isolation under sticky nav.** The hero radial
|
||
glow used to bleed through the sticky section nav on scroll.
|
||
- **Suggested-prompt chip touch target ≥ 36 px.** Was 28 px,
|
||
below the project's interactive-control floor.
|
||
- **"Generate weekly report" hero button enabled.** Previously
|
||
rendered disabled; now wired to the report route.
|
||
- **`buildCoachSnapshot` bounded to 90 days.** The Coach context
|
||
helper used to walk every measurement on every turn; capped at
|
||
90 days to keep token cost predictable.
|
||
- **`formatRelativeTime` consolidated.** Three near-identical
|
||
copies in the insights surfaces collapsed into one shared
|
||
helper.
|
||
|
||
### Security
|
||
|
||
- **Coach conversation persistence is encrypted at rest.** New
|
||
Prisma models `CoachConversation`, `CoachMessage`, and
|
||
`CoachUsage` (migration `0035_coach_conversations_v1420`) store
|
||
message bodies under AES-256-GCM via the existing `crypto.ts`.
|
||
Provenance lives in `metricSourceJson` as labels only (metric,
|
||
window, n-count) — never raw values. GDPR cascade-on-user-delete
|
||
wired through the FK chain.
|
||
- **Prompt-injection refusal pattern.** The Coach SSE route runs
|
||
every incoming message through an EN+DE refusal scanner that
|
||
short-circuits prompt-injection attempts and obvious off-topic
|
||
asks before the provider call.
|
||
- **Per-user daily token budget.** `CoachUsage` ledgers
|
||
(user, UTC-day) → tokens; the 25 000-token cap (≈13 turns for
|
||
a heavy user) prevents runaway spend on shared provider keys.
|
||
|
||
### Deferred to v1.4.21
|
||
|
||
- 22 MED + 16 LOW + 4 simplify-apply-maybe items from the multi-
|
||
reviewer Phase-D pass. Highlights at `.planning/v1421-backlog.md`
|
||
include: senior-dev call to consolidate the duplicated
|
||
Pearson / linear-regression maths layer; refactor the
|
||
`<CoachDrawer key={prefill}>` state-reset shortcut into a
|
||
controlled prefill prop; transactional `recordSpend()`; refusal
|
||
accounting + lexicon expansion.
|
||
|
||
### Deferred to v1.5
|
||
|
||
- iOS native app, Apple Health integration, and per-metric APNs
|
||
alerts. Strategic plan at
|
||
`.planning/phase-D-v1420-product-lead-review.md`.
|
||
|
||
## [1.4.19] — 2026-05-10
|
||
|
||
### Fixed
|
||
|
||
- **BD-Zielbereich headline now shows independent values for 7T / 30T /
|
||
total.** The big number used to be identical to the 30T sub-value
|
||
because `/api/analytics` aliased `bpInTargetPct` to `last30Days?.pct`.
|
||
`computeBpInTargetWindows()` now returns a third `allTime` window and
|
||
the route routes the headline through it, so the three numbers can
|
||
legitimately differ across the three windows.
|
||
- **Charts mobile header no longer breaks the layout on Pixel 5.** The
|
||
card header switches to a mobile-first stack below `sm` (title +
|
||
chips on row 1, range tabs + cog right-aligned on row 2 with
|
||
`flex-nowrap`) so the tabs always own a single row down to 280 px
|
||
(Galaxy Fold compact). Bucket-aggregation chips and comparison
|
||
captions hide on mobile to free horizontal budget.
|
||
- **Charts x-axis tick density unified across every chart wrapper.**
|
||
New `src/lib/charts/x-axis-density.ts` helper + `useViewportWidth`
|
||
hook caps visible ticks at 4 (Fold) / 6 (Pixel 5 / iPhone 12) / 8
|
||
(small tablet) / 10 (desktop). Wired into HealthChart, MoodChart,
|
||
MedicationComplianceChart, and ComplianceLineChart, so the
|
||
medication chart no longer overloads with one tick per day.
|
||
- **`/admin/api-tokens` table no longer triggers a horizontal
|
||
scrollbar at any viewport (4th attempt).** Truncate-with-tooltip
|
||
pattern on token-name, username, and permission badge, plus
|
||
`table-fixed` + colgroup widths on the desktop table. Mobile falls
|
||
back to the existing card list, now walked end-to-end by an e2e
|
||
regression.
|
||
- **Spurious mini-scrollbar on `/admin/feedback` tab strip.** The
|
||
shared `tabsListVariants` primitive picked up `overflow-y-hidden`
|
||
so the strip no longer paints a 1 px slither below the tabs.
|
||
- **`/insights` raw `metric: blood_pressure_sweet` template leak.**
|
||
`STRIP_TOKEN_REGEX` widened to `[A-Za-z0-9_]+` so lowercase template
|
||
remnants are scrubbed from AI prose; the uppercase render allowlist
|
||
(`PARSE_TOKEN_REGEX`) is unchanged.
|
||
- **AdminShell hides the collapse button on single-section pages.**
|
||
No more dead "Einklappen" affordance when the route only exposes
|
||
one section.
|
||
- **Mobile Sys/Dia badge enum mismatch on blood-pressure rows.**
|
||
CRITICAL from QA — the badge enum on mobile measurement rows
|
||
decoded the wrong key. Fixed before tag with a TDD guard.
|
||
- **6 CRITICAL + 21 HIGH copy / consistency / a11y findings from the
|
||
quality-of-life audit.** Time-window range strings now respect the
|
||
active locale, login overview filters out non-auth events, the
|
||
date / datetime input pair forwards `lang` so the native picker
|
||
uses the user's locale, raw enum badges are humanised, audit-action
|
||
labels are localised and link back to the row, achievement titles
|
||
no longer insult the user, and a long tail of admin / settings copy
|
||
consistency.
|
||
|
||
### Changed
|
||
|
||
- **Settings → Integrations status displays consolidated.** Withings
|
||
and Mood Log cards now share a single canonical
|
||
`<IntegrationStatusPill>` chip top-right ("Connected · 12 min ago"
|
||
with locale-aware relative-time bucketing). The redundant v1.4.15
|
||
banner trio (`connected / last successful / last attempt`) and Mood
|
||
Log's bottom-of-card "letzter Sync" line are gone; both cards now
|
||
carry a divider between header and body for visual symmetry.
|
||
Actionable error text stays as a compact inline alert above the
|
||
action row.
|
||
- **Comparison overlay control removed from the dashboard.** The
|
||
toggle now lives only on `/insights`, folded into the hero meta
|
||
band where there is room for it. The dashboard ditches the
|
||
always-visible knob.
|
||
- **`/insights`: single page-level refresh button.** The hero owns
|
||
the only refresh affordance; redundant per-section refresh links
|
||
removed (the per-recommendation Regenerate button from v1.4.16
|
||
stays).
|
||
- **`/insights`: small BP / Weight tile strip removed.** Duplicated
|
||
the dashboard tiles. `-157` lines on `src/app/insights/page.tsx`
|
||
plus dead helpers, plus the orphan "Persönlicher AI Berater"
|
||
subtitle.
|
||
- **AI insight prompt no longer opens with a default-positivity
|
||
sentence about data quality.** GROUND RULE 7 (EN + DE) forbids
|
||
"Your data foundation is strong" / "Datengrundlage ist sehr stark"
|
||
openers; data-quality caveats only allowed when n < 7 in the
|
||
analyzed window, recencyDays > 14, or a coverage gap biases the
|
||
comparison. `PROMPT_VERSION` bumped 4.16.1 → 4.19.0 so feedback
|
||
aggregation can attribute responses to the new rule.
|
||
- **Settings input heights, vertical spacing, and right-side action
|
||
buttons consistent across all sub-routes (mobile + desktop).**
|
||
Every form input is now 36 px (`h-9`); Account → Password,
|
||
Account → Restart onboarding tour, and Dashboard → Reset to
|
||
defaults stack the action below the title on `<sm` (full-width)
|
||
and right-align on `≥sm`, fixing the Pixel-5 right-edge overflow
|
||
on the tour button. Language select gets its own row at the bottom
|
||
of the Profile card; card-internal `space-y` standardised to
|
||
`space-y-4`.
|
||
- **Target-range status labels translated to German** (Low / On Target
|
||
/ Stable / Moderate). 11 `targets.label.<TYPE>` + 41
|
||
`targets.status.<key>` entries in EN + DE; page uses
|
||
`STATUS_CATEGORY_KEY` to normalise server strings.
|
||
|
||
### Deferred to v1.4.20
|
||
|
||
- 3 HIGH from QA — `/insights` `data?.` narrowing refactor,
|
||
`/admin/api-tokens` touch-tooltip (needs Popover swap), and
|
||
`/insights` hero density (folded into the v1.4.20 redesign).
|
||
- 31 MED + 16 LOW from the quality-of-life audit. Short-list at
|
||
`.planning/v1420-backlog.md`.
|
||
- `/insights` redesign with AI Coach — separate roadmap, design
|
||
handoff at `~/Downloads/design_handoff_insights_redesign`.
|
||
|
||
## [1.4.18] — 2026-05-10
|
||
|
||
### Added
|
||
|
||
- **Per-chart overlay toggles.** Every chart on the dashboard now has a
|
||
cog menu (44 × 44 tap target) with three independent switches: trend
|
||
indicator, trend arrow, and target-range overlay. Defaults are off so
|
||
the default look is a clean line. State is persisted per user per
|
||
chart in `User.dashboardWidgetsJson.chartOverlayPrefs` (no migration)
|
||
and round-tripped through a new `PUT /api/dashboard/chart-overlay-prefs`
|
||
in a Serializable transaction.
|
||
- **Expanded achievements roster.** 21 new achievements bring the total
|
||
to 59. Adds streak, milestone, consistency, improvement, and discovery
|
||
categories on top of the existing security / engagement / vitals /
|
||
medication groups. Locked public badges only render once the user has
|
||
data for the underlying metric so the page isn't a wall of grey on
|
||
day one.
|
||
- **Hidden Easter-egg achievements.** Six hidden badges that paint as
|
||
opaque "Hidden achievement" placeholders until unlocked. The real
|
||
strings, descriptions, and icons never reach the DOM (or the API
|
||
response) for locked-and-hidden entries, so peeking the bundle or the
|
||
network tab doesn't spoil them. Unlock toast adds a longer Sparkles
|
||
celebration with a "you unlocked a hidden achievement!" headline.
|
||
- **BD-Zielbereich tile 7T / 30T sub-values.** The two sub-values on
|
||
the blood-pressure-in-target tile now show real measurement data.
|
||
Backed by a new `computeBpInTargetWindows()` helper that re-uses the
|
||
v1.4.16 ceiling predicate but filters input by `measuredAt`.
|
||
`/api/analytics` surfaces `bpInTargetPct7d` / `bpInTargetPct30d`.
|
||
|
||
### Changed
|
||
|
||
- **Charts use clean lines without gradient fills.** The v1.4.16
|
||
Apple-Health-style gradient backgrounds are gone. Lines stay smooth-
|
||
interpolated, tooltips stay rich, animation-on-render still respects
|
||
`prefers-reduced-motion` — only the gradient fill area is removed.
|
||
- **Mood chart shows simple dots at data points.** The emoji glyphs
|
||
introduced in v1.4.16 are replaced with plain coloured dots; the
|
||
emoji still appears in the tooltip.
|
||
- **Personal-baseline / mean overlays are opt-in.** The 90-day-median
|
||
reference line on health and mood charts is no longer always-on; it
|
||
only renders when the per-chart Trend toggle is enabled.
|
||
|
||
### Fixed
|
||
|
||
- **`/admin/api-tokens` mobile horizontal scrollbar.** Third attempt,
|
||
this time pinned to the actual offender via Playwright probe of the
|
||
prod page at Pixel-5 viewport. The scrollbar was painted by the
|
||
AdminShell mobile section strip (13 entries, scrollWidth ≈ 1700 px),
|
||
not the api-tokens table. Added a `.no-scrollbar` utility and applied
|
||
it to both AdminShell and SettingsShell mobile strips. Swipe and
|
||
keyboard-arrow scrolling preserved.
|
||
- **`/insights` legacy-payload crash.** Already shipped in the v1.4.17
|
||
hotfix; documented here for completeness. Cached pre-strict insights
|
||
in the v1.4.14 `{changed, stable, drivers, …}` shape now render a
|
||
"Regenerate insights" card instead of throwing
|
||
`Cannot read properties of undefined (reading 'replace')`.
|
||
- **BD-Zielbereich 7T / 30T sub-values.** Were always rendering "—"
|
||
because `/api/analytics` only computed a single 30-day window and
|
||
the tile passed `avg7={null}, avg30={null}`. Wired correctly — see
|
||
Added.
|
||
|
||
### Deferred to v1.4.19 / v1.5
|
||
|
||
- **i18n bundle leak (security HIGH).** The hidden-achievement
|
||
redaction landed at the API layer, but `messages/en.json` and
|
||
`messages/de.json` are still statically imported into the client
|
||
bundle, so a determined user can `Cmd-F` for the hidden strings in
|
||
`_next/static/chunks/*.js`. Needs a build-time strip or reversible
|
||
obfuscation. Tracked in `.planning/v1419-backlog.md`.
|
||
- See `.planning/v1419-backlog.md` for the short-list and
|
||
`.planning/v15-backlog.md` for the strategic v1.5 items.
|
||
|
||
## [1.4.17] — 2026-05-10
|
||
|
||
### Fixed
|
||
|
||
- `/insights` no longer crashes for users with cached insights from before
|
||
v1.4.16. A "Regenerate insights" card is shown for legacy cached
|
||
payloads (the pre-strict `{changed, stable, drivers, …}` shape) instead
|
||
of throwing a `Cannot read properties of undefined (reading 'replace')`
|
||
error. One click on the card refreshes the insight in the new format.
|
||
|
||
## [1.4.16] — 2026-05-09
|
||
|
||
### Added
|
||
|
||
- **Per-recommendation explainability.** Every insight recommendation now
|
||
carries a rationale: which time window was analysed, what was compared,
|
||
and the deviation that triggered the recommendation. The card expands
|
||
inline to reveal the rationale plus a mini-chart of the data window.
|
||
- **Confidence score per recommendation.** Each recommendation gets a
|
||
deterministic 0–100 confidence score (server-computed from sample size,
|
||
recency, and signal strength) shown as a colour-banded ring + bar meter.
|
||
Below-threshold recommendations are tagged "low confidence — based on
|
||
limited data" rather than hidden.
|
||
- **"Was this helpful?" feedback.** Thumbs-up / thumbs-down on every
|
||
recommendation, persisted per user with provider attribution. Daily
|
||
aggregator writes per-(severity × provider) helpful-rate to admin
|
||
settings; the aggregate is visible under `/admin/ai-quality`.
|
||
- **Medical-reference grounding.** Recommendations cite curated AHA / ESH
|
||
/ ESC / WHO / DGE guidelines via a validated `referenceId`; the
|
||
citation links open the source guideline in a new tab.
|
||
- **Multi-provider AI fallback chain.** The insight wrapper tries each
|
||
configured provider in turn on hard failures (401 / 403 / 429 / 5xx /
|
||
transport). Schema 422 still bubbles. Last-working provider is cached
|
||
per user for an hour. Order, enable / disable, and provider list are
|
||
configurable under Settings → AI.
|
||
- **Apple-Health-style chart polish.** Blood pressure, weight, pulse,
|
||
body fat, sleep, steps, mood, and medication-compliance charts gain
|
||
gradient fills, smooth interpolation, a 90-day-median personal-baseline
|
||
reference line, in-target zone shading, rich tooltips with delta
|
||
vs. baseline, and 600 ms ease-out animation that respects
|
||
`prefers-reduced-motion`. Sparse data (<3 points) renders an explicit
|
||
empty state instead of a degenerate line.
|
||
- **Comparison overlay (vs. last month / vs. last year).** A new toggle
|
||
at the top of every chart, tile, and the insights surface overlays the
|
||
prior period as a dimmed line beneath the current one and adds a delta
|
||
callout (Δ ±N). The AI summary narrates the comparison ("your average
|
||
BP improved by 4 mmHg vs. last month") when the toggle is active.
|
||
- **Settings → Export.** Consolidated `/settings/export` page with one
|
||
card per export type: doctor-report PDF (configurable date range +
|
||
practice name), measurements CSV, medications CSV (optional intake
|
||
history), mood CSV, full JSON backup. Each download writes a
|
||
`user.export.<kind>` audit-log entry and shares a 10/h rate-limit
|
||
bucket. Doctor-report entry-point relocated under this route.
|
||
- **Achievements page.** New `/achievements` page with locked + unlocked
|
||
breakdown grouped by category (medication / vitals / security /
|
||
engagement). Recent unlocks card on the dashboard, toggleable from
|
||
layout settings.
|
||
- **Onboarding tour for new users.** First-run spotlight walk-through
|
||
highlighting tile-strip, quick-add menu, insights, integrations, and
|
||
achievements. Skippable, keyboard-navigable, replayable from
|
||
Settings → Account.
|
||
- **Admin host-load chart.** New chart over the system-status section
|
||
shows host CPU, memory, and disk-IO over the last 2 hours, sampled
|
||
every minute, retained for 7 days.
|
||
- **Admin app-log preview.** Tail of the last 1 hour of structured
|
||
wide-events from the per-process ring buffer, filterable by trace_id /
|
||
level / action / time-window with a JSON inspector modal.
|
||
- **Admin AI quality preview.** New `/admin/ai-quality` route surfaces
|
||
helpful-rate per (severity × provider), tinted by band so degrading
|
||
providers stand out at a glance.
|
||
|
||
### Changed
|
||
|
||
- **Insights surface uses the new RecommendationCard everywhere.** The
|
||
`/insights` page and the dashboard insights tile both render the
|
||
polished card — rationale expand, confidence meter, citation footnote,
|
||
feedback thumbs — backed by a shared TanStack Query cache.
|
||
- **Trend label normalised to "7-day trend".** Every chart, tile, and
|
||
subtitle uses the long form. A signed numeric delta indicator (±N.N)
|
||
with metric-aware colouring appears next to the value on every chart,
|
||
including mood and medication-compliance.
|
||
- **`/admin` overview redesigned.** The section grid is gone; the
|
||
overview now shows a system-status snapshot with host-load chart and
|
||
an audit-log preview. Sidebar carries the section list.
|
||
- **Sidebar admin sub-items only expand on `/admin/*`.** Clicking the
|
||
Admin link or opening the Gravatar dropdown no longer auto-expands
|
||
the sub-list when you are not on an admin route.
|
||
- **Settings → AI is a single dropdown.** The provider selector at the
|
||
top drives the configuration form below; no more top/bottom split. All
|
||
five providers (Codex / OpenAI / Anthropic / Local / Admin OpenAI) are
|
||
reachable from the same UI.
|
||
- **Top dashboard tiles selectable per metric.** The widget-id enum bug
|
||
from v1.4.15 that silently 422'd every layout PUT is fixed; the
|
||
per-metric tile toggle in layout settings now actually persists.
|
||
- **AI rate-limit raised to 10/hour.** Default bumped from 2/h to 10/h;
|
||
configurable via `INSIGHTS_RATE_LIMIT_PER_HOUR`. Generating a new
|
||
insight evicts every previously cached per-status insight for that
|
||
user, so the dashboard never shows a stale cached payload.
|
||
|
||
### Fixed
|
||
|
||
- **BD-Zielbereich percentage now counts in-range readings correctly.**
|
||
The v1.4.15 fix corrected the denominator but kept the ESH narrow-band
|
||
predicate (`sysLow ≤ sys ≤ sysHigh AND diaLow ≤ dia ≤ diaHigh`), so
|
||
normotensive readings (e.g. 117/79) below `sysLow=120` counted as out
|
||
of range. Predicate switched to one-sided ceiling semantics with a
|
||
hypotension floor (`90 ≤ sys ≤ sysHigh AND 50 ≤ dia ≤ diaHigh`),
|
||
centralised in `isBpReadingInTarget()` shared by six call sites.
|
||
- **Trend on "all" filter shows a meaningful split-half delta.** Long
|
||
windows where the per-week rate fell below the 1-decimal display
|
||
precision now also surface a split-half mean delta (second-half mean
|
||
minus first-half mean) for windows ≥ 90 days.
|
||
- **Login overview no longer strips umlauts.** The geo helper decodes
|
||
via `arrayBuffer()` + `TextDecoder('utf-8')` instead of
|
||
`Response.json()`, so an upstream proxy stripping the
|
||
`Content-Type: charset` parameter cannot poison the umlaut path.
|
||
Nürnberg, München, Düsseldorf, Köln, Würzburg, Bückeburg, Weißenfels
|
||
all roundtrip correctly.
|
||
- **`/admin/api-tokens` table no horizontal overflow on mobile.** The
|
||
desktop `<table>` falls back to a card list at `<md` viewports, with
|
||
long names + permission badges wrapping cleanly inside the card.
|
||
- **Skip-link no longer blocks logo click.** The "Skip to content"
|
||
shortcut still leads the tab order but no longer blocks pointer events
|
||
on the logo.
|
||
- **Bug-Report nav entry follows the admin feature toggle.** When the
|
||
admin disables bug reporting the entry vanishes from sidebar,
|
||
bottom-nav, topbar, and the error-detail "Report bug" button.
|
||
- **Feedback link follows the admin feature toggle.** When feedback
|
||
collection is disabled, the UI entry point disappears too.
|
||
- **Cached AI insights replaced when the user regenerates.** Generating
|
||
a new insight invalidates the per-status `audit_logs` entries plus the
|
||
TanStack Query cache, so the dashboard always shows the freshest
|
||
payload.
|
||
|
||
### Performance
|
||
|
||
- **Comparison overlays computed via reusable bucket-series helper.** No
|
||
extra round-trip per chart — the dashboard fetcher and AI snapshot
|
||
both read from the same `avg30LastMonth` / `avg30LastYear` fields.
|
||
- **`/api/insights/feedback` persists with optimistic UI updates.**
|
||
Thumbs-up / thumbs-down apply instantly; the localStorage refresh-
|
||
defence keeps the verdict on rerender even before the mutation
|
||
resolves.
|
||
|
||
### Security
|
||
|
||
- **AI provider apiKeys encrypted at rest.** Provider chain entries that
|
||
carry a key store it AES-256-GCM via the existing `src/lib/crypto.ts`
|
||
helper.
|
||
- **`/api/insights/feedback` gated by `requireAuth` + idempotency-key.**
|
||
The dedicated rate-limit bucket prevents thumbs-spam.
|
||
- **Per-provider attribution server-filled.** Clients cannot tamper with
|
||
which provider gets credit for a recommendation; the attribution is
|
||
resolved server-side from the latest `insights.generate` audit row.
|
||
- **Admin egress redacts secrets.** Audit-log `details` and app-log
|
||
`meta` fields run through `redactSecrets()` before leaving the admin
|
||
API surface.
|
||
|
||
### Internal
|
||
|
||
- **`docker-publish` workflow no longer hangs on main-branch builds.**
|
||
Root cause was a qemu-arm64 SIGILL during multi-arch emulation; arm64
|
||
dropped from the platforms list. Native arm64 runner matrix scheduled
|
||
for v1.5.
|
||
- **CI integration tests + e2e workflows green again.** Both had been
|
||
pre-existing red since `d8c549e` (encryption-key YAML scalar parsed
|
||
as integer 0 + spotlight tour overlay intercepting clicks). Both fixed
|
||
this milestone.
|
||
|
||
### Deferred to v1.5
|
||
|
||
- Coolify image-digest auto-deploy trigger (currently fires on every
|
||
git-push; the manual Coolify UI toggle is the realistic fix).
|
||
- Native arm64 runner matrix for full multi-arch docker publish.
|
||
- Cross-user feedback aggregation prompt-tuning ratchet (depends on
|
||
v1.4.16 feedback collection accumulating data).
|
||
- Dedicated `/insights/compare` page (i18n keys
|
||
`comparison.insightsCallout.{lastMonth,lastYear}` reserved).
|
||
|
||
## [1.4.15] — 2026-05-09
|
||
|
||
### Added
|
||
|
||
- 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.
|
||
- 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.
|
||
- 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. 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 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 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.
|
||
- 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”.
|
||
- 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 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-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
|
||
|
||
- 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.
|
||
- The sidebar Admin sub-items only expand when you are actually on an
|
||
`/admin/*` route. Everywhere else the Admin group stays collapsed.
|
||
- 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.
|
||
- 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.
|
||
- 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.
|
||
- Mood tile on mobile shows only the large score number + label, no more
|
||
doubled rendering.
|
||
- 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
|
||
|
||
- 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 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.
|
||
- The “Skip to content” skip-link still leads the tab order but no longer
|
||
blocks clicks on the logo.
|
||
- 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.
|
||
- The Feedback link follows its admin feature flag. With feedback
|
||
disabled, the UI entry point disappears too.
|
||
- 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.
|
||
- 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.
|
||
- 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` on mobile renders as a card list instead of a
|
||
horizontally-scrolling table.
|
||
- 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 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 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
|
||
|
||
- 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.
|
||
- 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.
|
||
- 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.
|
||
- 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.
|
||
- 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-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” 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.
|
||
- Admin-area i18n keys reorganised under `admin.section.<slug>.*` (EN + DE
|
||
parity).
|
||
|
||
### Fixed
|
||
|
||
- 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.*
|
||
- 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.
|
||
- 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
|
||
|
||
- 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.
|
||
- 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
|
||
|
||
- 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 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
|
||
|
||
- 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; reverting to two separate tiles preserves
|
||
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
|