# Changelog ## [Unreleased] ## [1.18.10] — 2026-06-20 — data-loss fix, measurement consolidation, grounding, and the full-page Coach A correctness-and-polish release. It closes a silent data-loss path and a stalled measurement-consolidation job, hardens AI output grounding, and acts on a broad round of UI feedback. Two additive migrations (a covering index and a labs setting); no breaking changes. ### Fixed - Editing one field of a cycle or condition-journal day entry no longer wipes an encrypted note that cannot be decrypted with the current key — the stored value is preserved untouched unless that field is explicitly changed. - The nightly consolidation of cumulative metrics (walking/running distance, flights climbed, active energy) no longer aborts on the first conflicting day, so raw per-sample history collapses to daily values as intended. A one-time compaction of the existing backlog runs after deploy. - The daily briefing refreshes when regenerated, and the home screen reflects a saved tile layout without a manual reload. - The Coach dictation button reports when speech input is unavailable instead of doing nothing. ### Added - The Coach has a full-width page (no panel-within-a-panel), reachable conversation history, and a clean new-chat start. - Scan a lab report without a vision model: optional on-device OCR (off by default) reads the image in the browser and sends only the extracted text to the configured AI provider, then the same mandatory per-row review applies. The labs "Add" action now offers "scan a document" or "add a value manually". - Withings sleep now records heart rate, respiratory rate, blood oxygen, sleep HRV, and the sleep score (previously discarded at sync). - Oura VO2max is recorded, and the VO2max tile is shown on the dashboard by default. ### Changed - Preventive-care cards show the cadence on the metric row rather than beside the heading, and the heading opens the measurement history filtered to that metric. - The danger zone and the daily-briefing card match the rest of the surface; the feedback tool is marked deprecated; the preventive-care and labs settings follow the standard section layout. - Lab values: delete moved to the value's detail view, the overview tags are quieter, and the reference-range confidence is surfaced when a new biomarker is created. - Location lookups default to the online resolver; the offline database is optional. - Mobile: the full-page Coach composer stays clear of the bottom navigation, long navigation labels stay readable, and touch targets meet 44 px. ### Performance and integrity - Blood-pressure in-target and grade read identically whether served warm or cold (one canonical source per day). - A per-session statement timeout stops a single stuck query from holding a connection; a soft-delete-aware measurements index; the all-time fallback scan is bounded. ### Security - Daily-briefing numbers are checked against the underlying signals; the Coach reply is screened for fabricated values or dose instructions; the period narrative is grounded before it is stored; the metric status cards state precomputed trends rather than re-deriving them. ### API - The labs OCR capability response carries a `mode` (vision or text) and a new local-OCR preference; additional measurement types are recorded server-side. All additive and back-compatible. ## [1.18.9] — 2026-06-20 — Coach surface, lab-report scanning, and fresher dashboards A broad UI and data release. It adds two migrations (both additive and nullable — qualitative lab results and Coach token columns) and three additive, back-compatible API changes (see the note below). ### Added - Coach opens on a calm new-chat surface: a centred greeting and a single composer, then each answer streams in word by word with a soft fade, a collapsible thinking disclosure, and a quiet per-message footer showing the tokens used and the model. The quick-access drawer is unchanged. - Scan a paper lab report. With an AI provider connected, upload a photo or PDF and the reader proposes the measurements and biomarkers it finds. Every row goes through a mandatory review — confirm, edit, or discard each one, with duplicate and reference-range hints — and only what you confirm is saved. The uploaded file is held in memory for the read alone and is never stored. - Record a qualitative lab result — positive/negative or another text value — where a number does not apply. - Sort lab values alphabetically, and delete a biomarker directly from the labs list and detail views. - VO2max shows on the dashboard by default, and Oura's VO2max reading is now ingested alongside the existing sources. ### Changed - The labs surface defaults to the list view, and its tiles share the medication-card layout. The labs and condition-journal settings now follow the same section pattern as the rest of Settings. - Preventive-care cards read the last-completed date as "today"/"yesterday" and the next due date as a discreet "today" (green) or "overdue" (orange), and drop the small trend strip. - Sleep-debt and chronotype sit side by side, and the daily-briefing card aligns with the surrounding type. ### Fixed - The dashboard reflects new readings without a hard refresh: an added or background-synced measurement now refreshes the home snapshot — including on Withings and Apple Health sync, and on returning to the tab. - The condition journal opens quickly again. The cross-episode correlation runs only when an entry's analysis is opened, and the typical-recovery-gap insight appears only when there is enough real data to support it. - The latest-report area shows its age and, when no AI provider is connected, points to where to connect one, instead of presenting an old report as current. - The Coach quick-access icon uses a softer tone. ### API - The Coach stream's `done` frame now carries an optional token `usage`, and a Coach message can carry `tokensUsed` and `model`; an optional `reasoning` frame is reserved. New `GET/POST /api/labs/ocr/{capability,extract,commit}` endpoints; rows committed from a scan carry `source: "OCR"`. All additive and back-compatible. ## [1.18.8] — 2026-06-20 — coded condition and lab terminology in the health-record export An additive export-only release: the FHIR R4 health record now carries standard terminology codes where it can assert them honestly. No migration, no API contract change. ### Changed - Condition resources for journal entries carry a SNOMED CT category coding per illness type (infection, allergy, injury, mental health, autoimmune, chronic, other) alongside the user's free-text label. These stay broad categories, not diagnoses — the patient-reported, unconfirmed guard rails are unchanged. - Lab Observations now emit a LOINC code and a canonical UCUM unit for the common biomarkers (HbA1c, lipid panel, ferritin, TSH, vitamin D, creatinine, eGFR, liver enzymes, CRP, fasting glucose, hemoglobin), resolved through an analyte alias table. An unrecognised analyte, or a unit that does not match the canonical UCUM symbol, keeps the honest text-only behaviour rather than asserting a code it cannot validate. ## [1.18.7] — 2026-06-19 — dashboard, Coach and insights polish A broad UI, insights, and AI-efficiency release. It ships database migrations (see the migration note below) and two client-facing contract changes: Coach budget responses now arrive as a stream error rather than a 429, and the measurement-reminder push carries the reminder id. ### Added - The daily briefing surfaces "signals of the day" — today's readings against your 7- and 30-day trend, emerging slopes and recent outliers — and leads with one concrete, present-focused nudge. - Preventive-care cards show a quiet seven-day strip of the metric, and their menu jumps straight to the measurement history filtered to that type. - The Coach reads your active illness state and Rest Mode, can point you to the relevant in-depth guide, and draws on a tiered view of your history (recent days in full, older periods progressively summarised with peaks preserved). - Dictate to the Coach by voice. - The share-link section is back as its own Settings entry, next to the health-record export: mint a time-boxed, revocable, read-only link to share your record with a clinician. - Share links now carry a passphrase second factor. A new link mints a one-time passphrase embedded in a QR code (in the URL fragment, never sent to the server); the public view stays locked until the passphrase is verified. Links created before this release keep working without one. - Labs, preventive care, and the condition journal each have their settings as a proper section of the Settings shell, in the same layout as every other section, instead of a separate page. ### Changed - The daily briefing regenerates every day and reads warmer and more motivating. - The Coach interface is rebuilt around a calm, centred reading column with a collapsible conversation list, a thinking indicator that hands off to streaming, and a quieter scrollbar. - The health-score rings are flattened to match the chart style — the glow, sheen and pulse are gone. - Sleep drops the redundant "last night" card; the chronotype reads as a prominent summary with your natural sleep midpoint and an expandable detail. - Every integration card follows one layout — a short description ending in an inline setup-guide link, with the credential hint above the save button. - A warm insight cycle now sends far fewer AI requests: the per-metric status assessments are batched into a single call, and the comprehensive path recovers from a malformed reply with one corrective retry instead of failing outright. - The daily briefing now varies its wording day to day while the underlying findings stay stable, so a quiet day no longer reads as a frozen cache. - Prompt construction is consolidated: grounding, tone, and the medication-safety rules come from one shared source across the briefing, status cards, narratives, and the Coach. - Settings cards share one card primitive and a consistent padding step, and touch controls across the app meet a 44-pixel target on phones while staying compact on wider screens. - The local-AI private-host switch accepts an explicit host allowlist instead of an all-or-nothing flag (the previous `true` still means "any private host"). - Coach conversations are pruned on a schedule, alongside the existing notification-attempt retention. ### Fixed - The daily briefing no longer goes stale: a fresh daily signal forces regeneration instead of re-stamping an unchanged narrative. - The sleep assessment no longer shows the "connect an AI provider" prompt when a provider is already connected. - A medication's supply now updates on the next read: adding a container or registering a new medication evicts the cached list instead of serving the pre-change stock for the cache window, and the card and table refresh together. - The seed for the public demo no longer produces an impossible blood-pressure reading, and reads as a current, healthy, fully-used account. ### Security - The Coach budget gate reserves spend atomically before each request, so concurrent requests can no longer slip past the cap together, and tokens burned on an empty or refused reply are still counted. - New share links require a passphrase to open the record; the passphrase is stored only as an HMAC hash, verified in constant time behind a rate limit, and unlocks a short-lived view cookie scoped to that one link. - The session and authentication-challenge tables are indexed on their expiry column for the reaper. - The development key-padding fallback fails closed unless the environment is explicitly development or test, so a missing `NODE_ENV` can no longer silently derive a weak key. ### Migration - This release adds three migrations: expiry indexes on the session and challenge tables, a daily-briefing reroll marker, and the nullable share-link passphrase hash. Run `prisma migrate deploy` (the entrypoint does this automatically). All three are additive — no backfill, no downtime. ## [1.18.6.1] — 2026-06-18 — settings and Coach UI follow-ups A small UI and settings patch on top of v1.18.6. No migration, no API contract change. ### Fixed - The settings and admin left menu now aligns with the top of the first card, instead of sitting slightly offset. - The Coach conversation scrolls within a bounded area with the composer pinned to the bottom, instead of overflowing the panel. - The health-record settings section (PDF and FHIR export) is reliably reachable again — its entry no longer disappears from Settings. ### Changed - The guided tour is first-run only. The per-page and Settings "show tour" triggers are removed. - Preventive-care cards now mirror the medication card: the cadence sits as a chip beside the name, due and last dates read as text, and "last done" always shows — with an em-dash when there is nothing yet. ## [1.18.6] — 2026-06-18 — offline reads, grounded guidance, and one consistent app A large release that makes the app genuinely useful offline, gives every metric cited reference context, grounds the AI Coach and Daily Briefing in that same context, adds responsible safety alerts for clinically urgent readings, and brings the newer modules (preventive care, illness, labs) and the whole settings area in line with the established patterns. A guided tour now explains what each module does and how they connect. No breaking changes; all schema changes are additive. ### Added - **A module-by-module guided tour.** After onboarding, a short tour walks each module — what it does, one capability you might miss, and how it feeds the others — so the whole app makes sense on day one. Replayable any time from Settings, or per module from its page. - **Cited reference ranges everywhere.** Metrics now carry plain-language explainers and the general reference range they sit in (blood pressure, resting heart rate, glucose, blood oxygen, temperature, and more), framed as general guidance and sourced from recognised guidelines — never a diagnosis. Blood pressure follows the ESH range. The blood-pressure detail now also shows pulse pressure and mean arterial pressure, derived from your readings. - **A Coach that knows the ranges.** The AI Coach and Daily Briefing are now grounded in those cited ranges, so a comment about a value reflects where it sits in the general range — still general guidance, never a diagnosis, with no third-party attribution. - **Proactive Coach check-ins you can actually see.** A proactive nudge now appears as a message in your Coach conversation (not only as a push), with a discreet unread dot on the Coach button that clears when you open it — so it works even without notifications set up. - **Responsible safety alerts.** A confirmed, re-measured reading in a clinically urgent range (very high or low blood pressure, low or high glucose) now raises an alert that says to re-check or contact your doctor — never a diagnosis, and only for the modules you use. - **A diabetes setting.** An explicit opt-in switches glucose targets to the tighter clinical goal range; it is never inferred from your readings. - **Per-module settings for preventive care, illness and labs.** Each gets its own settings page to reorder items and switch between card and list view; labs additionally lets you manage biomarkers and choose the sort order. - **Mark a reminder done.** A measurement reminder can be completed server-side (for the apps), instead of only being dismissed locally. - **Real offline reads.** Opened without a connection, the installed app shows your last synced dashboard and recent data with a "showing your last synced data" note, rather than blank placeholders. Changes you make offline still save when you reconnect. ### Changed - **Preventive-care cards match the medication cards.** A clear "measure now" action, the interval as a chip, and progress toward the next due date — with measurement-appropriate wording (no more "intake" for a blood-pressure check) and the same calm, neutral card. - **One consistent "add" and a settings wrench** across preventive care, illness and labs; the illness journal's resolved entries are collapsed by default. - **Every Settings and Admin page now has a clear heading and description**, in one shared frame, with the side menu lined up to the content — so the whole area reads as one piece. The AI section is now labelled "AI provider", and module availability has its own admin page. - **No more AI dead-end.** When no AI provider is connected, onboarding and the Coach/Insights/Briefing screens explain what the feature does and how to set it up, instead of a bare error. - **Clearer sleep, recovery and insights.** The estimated sleep-stage timeline is shown only when it reflects measured stages (with an honest note otherwise); the chronotype reads as labelled text; the sleep-quality assessment is more useful; the recovery page is de-cluttered; and insight cards link through to their detail pages, including steps. - **A faster dashboard.** All charts load in one batched request instead of one per chart, with target ranges computed on the server. - **Fewer disclaimers, one clear acknowledgment.** The repeated "not medical advice" banners are gone in favour of a single acknowledgment at onboarding (reachable any time in Settings). - **Mobile polish.** Small tap targets were brought up to a comfortable size, and a global reduced-motion safeguard quiets animation when your device asks for it. ### Fixed - **Reduced motion no longer hides content.** Charts and list rows that animate in now stay visible when the device requests reduced motion, instead of disappearing. - **Read-after-write.** A reading or medication you just added now reliably appears in its list. - **A timezone bug** that anchored the local day at UTC midnight for one intake path. - **Contrast.** The preventive-care action button now meets AA contrast in dark mode. ### Security - **The offline cache is hardened.** It stores only a safe, read-only allowlist (never anything tied to sign-in or sensitive AI conversations), is bound to the signed-in account, and is wiped when the session ends — not only on explicit sign-out. - The container scan ignores a known `picomatch` advisory (CVE-2026-33671) that exists only in the npm CLI bundled inside the base image, not in the app's own dependencies (pinned to the fixed version). ### Operator note - Additive migrations only (`0173`–`0177`: tour progress, diabetes opt-in, disclaimer acknowledgment, Coach last-seen, a Coach-message index). The optional `APNS_CRITICAL_ENTITLEMENT` env var (introduced in 1.18.4) gates native critical alerts; urgent alerts ship as time-sensitive without it. The service-worker cache version bumps with the release, so clients refresh their offline cache automatically on update. ## [1.18.5] — 2026-06-18 — filter by value, a tidier advanced panel, and a titration timeline for injectables A small polish release. You can now filter your readings by value, the advanced medication settings collapse into sections instead of one long page, and injectable medications show their dose-escalation plan as a timeline. No breaking changes; no migration. ### Added - **Filter readings by value.** Search now takes a numeric range — show only readings whose value is between two numbers (or just above / just below one), alongside the existing type, source and date filters. Useful for finding outliers or a specific episode. - **A titration timeline for injectables.** Injectable medications now show their planned dose-escalation (e.g. 2.5 mg → 5 mg → 7.5 mg over the weeks) as a timeline with a "you are here" marker, on the injection tab. Built from the dose-change history already recorded — nothing new to enter. ### Changed - **The advanced medication settings read more calmly.** The advanced and API panels group into collapsible sections (lifecycle expanded, data and danger areas collapsed by default) instead of one long wall — the settings themselves are unchanged. ## [1.18.4] — 2026-06-18 — urgent alerts that reach you on whatever you've set up, and a stronger free notification path This release makes notifications work well for self-hosters who don't run Apple push. Genuinely urgent health signals — a sustained low-oxygen or fever pattern flagged by the condition journal — now go out at the strongest level each notification channel you've configured supports, and reach you even with no Apple Developer account: max-priority ntfy, high-urgency Web Push, a webhook, or a time-sensitive iOS alert if you do run push. The installed web app gains real reminder handling: a dose reminder clears itself once you mark it taken, and an app badge counts what's still due. No breaking changes; no migration. ### Added - **Urgent alerts, on every channel you have.** A condition red-flag (sustained low SpO₂ or fever — "seek care", never reassurance) now sends an urgent alert that escalates per channel: time-sensitive on iOS (critical only if you've been granted and enabled Apple's entitlement), max priority on ntfy, high urgency on Web Push, flagged on the webhook. No Apple account required — an instance with only ntfy or Web Push still gets the urgent treatment. De-duplicated per condition. - **A real reminder experience in the installed web app.** Dose, preventive-care and low-stock reminders reach the home-screen web app over Web Push; marking a dose taken clears its reminder (no leftover notification), and an app badge shows how many doses are still due today — the closest the no-Apple-account path gets to the native lock-screen behaviour. ### Changed - **Notifications degrade gracefully without Apple push.** The channel cascade and every urgent path are now explicit that an instance without APNs still delivers through its other channels at their top tier. ### Docs - A self-hosting notifications guide: what each free channel (Web Push, Telegram, ntfy, webhook, email) can and can't do, the recommended PWA + Web Push setup, and a step-by-step path for running your own signed iOS build with your own Apple Developer account and APNs key for native push and the lock-screen widget. ### Operator note - UI/API-only; no migration. Optional new env var `APNS_CRITICAL_ENTITLEMENT` (default off) — set it only if your own iOS build carries Apple's Critical Alerts entitlement; otherwise urgent iOS alerts use the time-sensitive level, which needs no Apple approval. ## [1.18.3] — 2026-06-17 — the condition journal you can scroll back through, and a clearer view of what's tracked A small follow-up that finishes a few rough edges from the previous releases. The condition journal's day-by-day timeline now scrolls through the whole illness, not just today; deleting a condition can be undone; the condition journal is on by default like every other area; and medication inventory that was never set now reads as "unknown" instead of a misleading zero. No breaking changes; no migration. ### Added - **Restore a deleted condition.** Deleting a condition now offers an Undo — its day-by-day log and note come back exactly as they were, nothing is re-created from scratch. - **The full condition timeline.** A condition's detail page now lists every day you logged, newest first, instead of only today. ### Changed - **The condition journal is on by default.** It now behaves like every other optional area — present unless you switch it off under "What you track" — rather than starting hidden. Self-hosters can still make it unavailable instance-wide. - **Unknown medication stock reads as "unknown".** A medication whose inventory was never set now shows "unknown" rather than 0, so it can't be counted down into a confusing negative. A genuine zero still shows as zero, and low-stock reminders ignore the unknown case. ### Operator note - UI- and API-only; no schema change and no migration. The companion-app contract gains a date-less day-log list endpoint, a condition restore route, and nullable medication-inventory fields (`null` = unknown) — all additive. ## [1.18.2] — 2026-06-17 — preventive care, the same as your medications This release brings preventive care into line with how medications already work, and clears up the condition journal. Each preventive-care item is now its own card in the same style as a medication, and marking one done behaves the way you'd expect: a check-up you plan for yourself is a simple "done", while a reminder tied to a measurement opens the actual entry form — completing a blood-pressure reminder records the reading rather than just ticking a box. Items take a first-due date and a custom interval, the page gained a settings control like the labs and condition pages, and preventive care can now sit on the dashboard as its own tile. The condition journal was reorganised so it reads at a glance. No breaking changes; no migration. ### Changed - **Preventive care looks and works like your medications.** Every reminder is its own card with one clear action. A self-planned check-up is marked done in a tap; a reminder linked to a measurement opens the matching entry form, so finishing it captures the value and clears the reminder in one step. Reminders take a first-due date and a custom interval, can be edited from the card, and the page carries a settings control to manage them. Preventive care can be placed on the dashboard like any other area. - **The condition journal reads at a glance.** Conditions are grouped into active and resolved, flares sit under the condition they belong to, each row carries a single clear action with the rest behind a menu, and a condition's daily timeline now lives on its own page. The retrospective summary sits up top where you'll see it. ### Operator note - UI-only release — no schema change and no migration. Preventive care ships on the dashboard switched off by default; enable it from the dashboard layout settings. ## [1.18.1] — 2026-06-16 — labs that hold a catalog, a condition journal, and reminders that only nudge when you forget A large release that turns two thin features into proper ones and adds two more, all in the existing visual language. Lab results gain a biomarker catalog: define a marker once with its unit and reference range, then log a value by picking it — with a full dashboard-style chart, edit and undo. A new condition journal records an illness from onset to recovery and, once it has enough of your own history, reflects back how it announced itself and how long recovery took — retrospective only, never a diagnosis. Preventive-care reminders now fire only when a measurement is actually overdue and clear themselves the moment a reading arrives from any source, and the coach can suggest an evidence-based measurement cadence you accept with one tap — both feeding one shared reminder engine. The settings menu, the dashboard header, the recovery page and the coach view were tightened so every module reads as one app. Medications can now be switched off like any other module; the core vitals — weight, blood pressure, pulse — stay always on. No breaking changes. ### Added - **Lab biomarkers with a reference range you set once.** Define a biomarker — name, unit, lower and upper bound, an optional note — from a suggested common panel or your own, then record a value by picking the marker instead of retyping its range every time. Each biomarker gets a proper chart with its target band, a full reading history, and edit, correct and undo. Every reading links to a catalog marker, so none is a dead end. - **A condition journal.** Track any illness or condition — acute, chronic, recurring, or a flare hanging off an earlier one — with a daily symptom-and-severity log and an encrypted note. Once there is enough of your own data, a retrospective view shows how often you have been unwell and your typical recovery gap (when your body returned to baseline versus when you felt better), computed from your own baseline and withheld until it is sure. It never predicts or diagnoses, and a sustained red-flag pattern points you to care rather than reassuring you. The area ships switched off; turn it on under "What you track". - **Rest Mode.** While a condition is active, your scores, recovery and streaks are annotated rather than penalised and cadence nudges pause — an active illness never changes a measured number, only the narrative around it. A calm banner explains it on the dashboard and recovery view, and condition episodes carry into the doctor-report PDF and the FHIR export as patient-reported conditions. - **Coach measurement-cadence suggestions.** When a real change warrants it, the coach can suggest an evidence-based cadence — daily weight, morning-and-evening blood pressure for a week, structured glucose — that you accept with one tap to create a reminder. It stays non-naggy: capped, cooled-down, dismissible, and willing to say you already measure enough. ### Changed - **Preventive-care reminders only nudge when you forget.** A reminder now clears itself the moment a matching measurement arrives — entered by hand or synced from a device — and reschedules its next due date, so the self-disciplined are left alone and the reminder is a safety net for gaps. Coach suggestions and preventive-care reminders run on one shared engine; reminders can be edited, carry a quiet "Coach" badge and course-window end date, and the linkable measurement set now covers the full range of types. - **Every module reads as one app.** Labs, the condition journal and preventive care now use the same add-entry sheet, overflow menus and confirm-before-delete guards as medications. The settings sections drop their leading blurbs and align consistently, integrations split into Connections, Channels and Sources as their own entries, the coach setting reads as "activate the coach" (on by default), and the "About me" health context moved into the account profile. - **A calmer dashboard and a consistent recovery page.** The daily-overview header now sits flat alongside the other tiles instead of as a glowing hero. The recovery page follows the same shape as every other metric page — heading, a short explanation, history with high, median and mean, a chart with a 7- and 30-point view, a target band and an assessment — and the duplicate score block was removed. The coach view was rebalanced for symmetry and space. - **Medications is now an optional module.** It can be switched off like cycle, sleep or labs; weight, blood pressure and pulse remain the always-on core. - **A richer cycle log.** The day sheet gains a phase-context header, inline field explanations, and a fertility section that opens itself when your goal calls for it. - **Admin tidy-up.** The coach feedback, insight-quality and assistant-surface panels consolidate into one coach area, the backups panel leads with its counts, and the danger zone now requires typing a confirmation like the less-destructive restore already did. ### Fixed - The illness recovery-gap engine no longer reports a return before a vital deviated, escalates a red-flag for rock-steady oxygen or temperature, reads journalled fever, and keys days by your own time zone rather than UTC. - Lab readings logged without a catalog marker are linked on write instead of stranded; a failed note-load no longer wipes the note on edit; and a partial reference-range edit can no longer save an inverted range. - The morning-and-evening blood-pressure protocol now schedules both times instead of one. Reminder satisfaction is race-safe across the cron and event paths. ### Security - The encryption-key rotation script now covers every encrypted column in the schema — including the new lab, biomarker and condition notes and a tail of previously-missed integration and profile fields — driven from a single registry that a test asserts is complete, so a future encrypted column cannot silently miss rotation. The admin data-wipe is gated behind a typed confirmation and a per-user rate limit. - Pin `hono` to `≥ 4.12.25` through an override, clearing the CORS-middleware advisory (GHSA-88fw-hqm2-52qc). The package is pulled in only by Prisma's and the shadcn CLI's tooling; the app is Next.js and sets no CORS headers anywhere, so the issue was never reachable at runtime. - Pin `picomatch` to `≥ 4.0.4` through an override, clearing the ReDoS advisory (CVE-2026-33671). The package is a transitive build dependency. ### Operator note - Additive migrations only (`0169`–`0172`): reminder course-window fields, the biomarker catalog and lab link, the condition-journal tables, and a partial unique index for coach-minted reminders. No backfill and no deploy-time action required. The condition journal ships disabled by default. **If you rotate `ENCRYPTION_KEYS`, re-run `scripts/rotate-encryption-key.ts` after upgrading** — it now re-encrypts several columns it previously skipped; do not drop a legacy key until it reports zero remaining rows. ## [1.18.0] — 2026-06-16 — turn off what you don't track, and a settings menu that finally reads as one app This release makes the app yours to shape. Every optional area — cycle, mood, sleep, glucose, workouts, recovery, labs, achievements, the coach, AI insights and the doctor-report — can be turned off, and when it is, it disappears everywhere at once: out of the navigation, off the dashboard, gone from insights, the coach, your reminders, achievements and the exported report. Your data is kept; turn the area back on and it returns. The settings menu was redesigned alongside it, so notification channels, integrations, sources and per-area pages each sit where you'd look for them instead of spread across overlapping hubs. The core vitals — weight, blood pressure, pulse and medications — are always on. No breaking changes. ### Added - **Turn modules on and off.** A "What you track" settings hub lets you switch off any area you don't use. The switch is honoured by one server-side gate, so a disabled area is hidden from the navigation and the dashboard and is also refused at its API — the surface and the route vanish together, with no window where one leaks without the other. Re-enabling restores everything; nothing is deleted. - **Operator-level module availability.** A self-hoster can make an area unavailable for the whole instance from the admin settings; an operator "off" wins over any personal preference. Both layers project onto the same module map the web app and the companion app already read, so neither client needs special handling. ### Changed - **The settings menu reads as one app.** Notification channels moved under Integrations alongside the connection and source panels; the duplicated "Reminders" hub collapsed into Notifications; mood, medications and the health-record export each became a clear top-level entry; and every section now carries one consistent heading. Old deep links (`/settings/reminders`, `/settings/sources`, the standalone coach page) redirect to their new homes. - **One add-entry pattern across every feature page.** The cycle, labs, mood, medication and preventive-care pages now share the same header and primary "add" affordance, so a page built later doesn't read differently from one built earlier. ### Operator note - Migrations 0167 (per-user module preferences) and 0168 (operator-level module availability) are additive — a single nullable JSON column each, no backfill. Both default to "all on": an area is disabled only by an explicit choice. No deploy-time action is required. ## [1.17.1] — 2026-06-15 — preventive care, lab results, and more of the data you already have This release closes the loop from tracking to acting. Preventive-care reminders tell you when to measure or check what, structured lab results give your bloodwork a home, and a new recovery view surfaces signals that were already being collected but never shown. Sleep timelines now read in real clock times, marking a dose on one device clears its reminder on the others within seconds, and self-hosters gain a generic webhook channel, email, one-tap Web Push setup and a proper notifications guide. Every new number is computed once on the server and read the same way on the dashboard, the coach, the doctor-report and the companion app. No breaking changes. ### Added - **Preventive-care reminders.** Set a reminder to measure your blood pressure on a cadence or to schedule an annual blood panel, with a clear next-due date and where to do it. A matching measurement marks it done on its own; the rest you tick off. Reminders reach you over every notification channel, and the cards stay calm — status is a quiet badge, never an alarming colour. - **Structured lab results.** Record bloodwork and biomarkers with their reference range, see each one trend over time, and carry them into the doctor-report PDF and the FHIR export. An out-of-range value is shown plainly, not in red. - **A recovery view, and sleep quality in depth.** A new recovery page gathers strain, training load and autonomic-charge readings, and the sleep page gains an efficiency, performance and sleep-score block — metrics a connected device was already sending that nothing surfaced before. Each appears only once it has data and stays calm until it has enough to be sure. - **Import and backdated entry.** Import a CSV of past measurements with a previewed, per-row result and unit conversion, and log an entry with a past date and time — the cold-start escape hatches for bringing existing history in. - **Onboarding that does what it says.** A short, skippable health-baseline step, and the goals you pick now actually seed your dashboard. - **Polar and Oura credentials in the browser.** Both connect with your own developer-app credentials entered in settings, like the other integrations — no environment file needed. - **More ways to be notified.** A generic webhook channel reaches a self-hosted notifier or a chat service, an email channel sends over your own SMTP server, and an operator can see delivery health across every channel. Web Push keys can be generated in one click from the admin panel. ### Changed - **Sleep reads in real clock times.** Per-stage rows now carry their own start and end, so the "last night" timeline lays out across the night instead of stacking — measured where the device reports stage timing, and an honestly-labelled reconstruction where it only reports stage totals. Nights logged by more than one source resolve to a single total. - **A dose taken on one device clears the others in seconds.** Marking a dose sends a silent sync to your other devices, so a lock-screen reminder ends without waiting for the next app open. - **More of a connected device's data flows in.** Readiness contributors, body-temperature deviation, blood-oxygen, a sleep score and autonomic-charge and training-load now come through where the source provides them. Body weight is never taken from a wearable strap. - **One product across every screen.** Desktop and mobile navigation now tell the same story, the coach has a single home, the layout and reminder settings each gather under one hub, and every integration card links to its setup guide. ### Fixed - Sleep nights from some sources were stamped at the wrong instant and sat shifted earlier in the day; corrected, with a one-time backfill that re-syncs affected nights. - The doctor-report sleep figure now matches the dashboard and the companion app, reading the same reconstructed per-night total as every other surface. - Polish across the new surfaces: consistent loading and empty states, design-token colours, responsive grids, and a calm confirmation for regenerating Web Push keys. ### Security - The webhook channel pins a public host and refuses a private or loopback address unless explicitly allowed; webhook secrets, SMTP credentials and the Web Push private key are kept out of any recorded error; the key-generation and delivery-health endpoints are reachable only from an authenticated admin session, never a token. ### Self-hosting - A notifications guide spells out that no Apple account is needed — Web Push, Telegram, ntfy, the new webhook and email all work without one — alongside a backup-and-restore callout and a clearer note on which variables must be whitelisted to reach the container. ### Operator note - Migrations 0162–0166 are additive. A boot-time backfill re-syncs sleep nights from the affected sources to the corrected timeline once; it is idempotent and bounded. ## [1.17.0] — 2026-06-14 — clinical depth for glucose and sleep, and three new sources This release builds new depth on the coherent foundation laid over the v1.16 line. Blood glucose gains a clinical panel, sleep gains a debt and chronotype reading, and three new data sources — Nightscout, Polar and Oura — join Withings, WHOOP, Fitbit and Apple Health. Every new metric is computed once on the server and read the same way on the dashboard, the coach, the doctor-report and the companion app, and each holds back a confident reading behind a calm "still learning" state until it has enough data. No breaking changes. ### Added - **Blood-glucose clinical panel.** Beneath the glucose chart, a panel reads time-in-range on the consensus bands, the glucose management indicator and an estimated A1C, and the coefficient of variation with an instability flag — with the J-index and the low/high blood-glucose risk indices behind an advanced view. It is honest about its limits: a panel built from spot readings says so, and it waits until enough readings over enough days have accrued before asserting anything. - **Nightscout.** Connect a self-hosted Nightscout instance and its continuous glucose readings flow in — the density that makes the clinical panel meaningful. Public instances work by default; a per-connection toggle allows a private or home-network instance. - **Sleep debt and chronotype.** The sleep page now shows a rolling sleep debt against your age-based need and an MCTQ chronotype with your mid-sleep, social jetlag and a type band — the deeper detail behind an advanced view, and the type held back until enough free-day nights are recorded. - **Polar and Oura.** Connect either over OAuth to bring in sleep, heart rate, respiratory rate, activity and recovery. Recovery now reads from the strongest connected source — WHOOP, then Oura, then Polar, then the computed proxy. - **Reorder-aware medication supply.** The low-stock warning now accounts for how long a refill takes, so a weekly medication is flagged before its last dose rather than after, with a concrete "runs out on … — reorder by …" date. A reorder lead time is configurable globally and per medication. - **Invite deep-link.** Invites open through a link the companion app can intercept to start a prefilled registration, with a browser fallback. ### Changed - **The coach reasons from the new signals.** Glucose time-in-range, sleep debt and chronotype, and a connected device's native recovery and strain now reach the coach, which prefers the device's gold-standard number over a computed proxy and respects each metric's still-learning state. - **Mood and cycle entry is one sectioned sheet.** A quick row of your recent tags stays open, the rest folds into collapsible sections with a count of what's set inside — fast by default, deep on demand. - **Wearable data you already had now surfaces.** Heart-rate variability from a ring or strap appears on the HRV view, and Oura and Polar steps, energy and pulse are ranked as real sources. - **Onboarding tells the truth.** The setup flow now presents the sources that actually shipped, signposts importing existing history, and sets the expectation that some insights sharpen over the first week or two — instead of promising a configuration that did not happen. ### Fixed - The sleep "last night" timeline bar returns for nights whose stages share an end instant, and the dashboard clears a recorded dose without a reload (carried forward from v1.16.17). ### Security - Outbound Nightscout calls pin a public host by default with an explicit private opt-in, the stored token is kept out of any recorded error, and the new OAuth connections verify a signed, expiring state before exchanging a code. ### Operator note - On the first nightly low-stock check after deploy, a medication already flagged low re-notifies once as the warning threshold moves to its lead-time-aware value. Expected and bounded. ## [1.16.17] — 2026-06-14 — the sleep timeline returns, and the dashboard clears after a dose Two fixes to behaviour that surfaced in daily use. No breaking changes. ### Fixed - **The "Last night" sleep timeline bar returns.** The staged hypnogram — which lays the night out across deep, core, REM and awake by clock time — had collapsed to just the numbers for nights whose stages share an end instant, leaving the breakdown without its bar. The bar now appears whenever the night carries timed stages; only a session recorded on a single instant falls back to the breakdown alone. - **The dashboard clears a dose the moment it is recorded.** After recording an intake from the medication card or detail page, the dashboard kept prompting for that dose until a full page reload, because the home view was refreshed only while it was on screen. It now refreshes as soon as the dose is recorded, with no reload. ## [1.16.16] — 2026-06-14 — one engine for sleep, recovery and glucose This release continues the work of making one number mean one thing everywhere. Sleep, recovery and blood glucose are now read from a single source on every surface — the dashboard, the coach, the doctor-report, the CSV and FHIR exports, and the companion app — so the figure you act on is the same wherever you look. Several displayed numbers become correct-but-different; each is called out below. No breaking changes. ### Added - **Per-stage sleep on the night feed.** The sleep-night feed now rounds every duration to whole minutes and carries the per-stage breakdown (deep, core, REM, awake), so the companion app renders the same night the web does. ### Changed - **Sleep is reconstructed once, from one engine.** The sleep score now reads each night through the same reconstruction the rest of the app uses, with source de-duplication: a night recorded by more than one source (for example a watch and a ring) is counted once instead of summed. For multi-source nights the score — and the readiness that builds on it — drops to its true figure rather than an inflated one. - **WHOOP recovery is the recovery you see.** When a WHOOP-native recovery exists it is now the single canonical value on the tile, the chart, the doctor-report, and the coach; the computed proxy is the fallback when there is no native score. A connected account no longer sees the proxy and the native value as two competing numbers, and the night each belongs to is resolved consistently, so one night reads as one recovery. A WHOOP user's recovery figure can change to the native one. - **Blood glucose speaks your unit everywhere.** Glucose is converted once, at the point it is served, through a single helper — so the series the companion app charts, the CSV export, the dashboard tile, the detail page, and the coach all read the unit you chose. The CSV export now matches the FHIR document instead of emitting raw mg/dL against a converted clinical export. If you read in mmol/L, these surfaces now show the converted value (for example 100 mg/dL as 5.5 mmol/L); mg/dL readers see no change. The blood-glucose median no longer claims a fixed window its value does not hold. - **The coach reads the canonical numbers.** The coach now quotes recovery from the same resolved series as every other surface rather than a blend of sources, and states blood glucose in your unit instead of a bare mg/dL number. - **WHOOP sync keeps a cursor per data type.** Recovery, sleep, workouts and cycles each track their own sync position, so a single slow or rejecting collection no longer holds the others back; a change notification refreshes just the affected record, and a full sync can backfill the deep history an incremental tick skips. ### Fixed - **The coach appears when a shared model is configured.** A self-hoster who runs the coach from a server- or operator-managed model — with no personal key — now sees the coach instead of an "unconfigured" state, because the provider endpoint reports whether the coach can actually answer. - **A consent receipt cannot double-mint.** Two requests arriving together can no longer leave two active consent grants; the active receipt is enforced unique and minted atomically, and a concurrent first grant resolves to the winning receipt instead of an error. ### Security - **Consent endpoints are rate-limited.** The consent grant, read and revoke routes share a per-user limit, so the receipt path can no longer be driven in a tight loop. ## [1.16.15] — 2026-06-14 — an honest calendar that learns before it predicts A focused release on the cycle calendar: it leans on observed data, names its uncertainty plainly, and reads your charts the way the Sensiplan method does. No breaking changes. ### Added - **Mark a reading as disturbed.** A temperature taken after a fever, a late night, or any off day can be flagged "disturbed" and is dropped from the evaluation, so a single skewed point no longer drags the curve. - **A positive ovulation test anchors the prediction.** When you log a positive test, it refines the predicted ovulation day — it sharpens the estimate but never overrides a temperature shift you have already confirmed. - **Choose your secondary fertility sign.** Advanced cycle settings now let you track either cervical mucus (the default) or cervix observation alongside temperature, so the symptothermal evaluation follows the sign you actually record. - **Warmer phase descriptions.** Each cycle phase gained a second paragraph of plain-language context, so the calendar explains what is happening, not just which phase you are in. ### Changed - **The calendar learns before it predicts.** Below three logged cycles it shows a calm "still learning your cycle" state instead of a confident fertile-window or ovulation guess — and the same gate now applies on the API, so every client reads the same honest signal rather than painting a window the rest of the app holds back. - **Temperature evaluation no longer under-detects ovulation.** The symptothermal reading gained the two Sensiplan exception rules — the slow rise and the single fall-back day, each confirmed on a fourth reading — so a real shift is recognised in the patterns that the simple rule used to miss. - **The mucus peak settles before it is trusted.** A mucus peak is now confirmed only after three drier days follow it, so a stray late entry no longer shifts the peak backward. ### Fixed - **Longer cycles are no longer clipped.** Cycles up to roughly sixty days are evaluated in full instead of being cut short, so a naturally long cycle reads correctly. ## [1.16.14] — 2026-06-14 — one number everywhere: the score, the coach, and the doctor-report agree ### Changed - **Blood-pressure in-target speaks one window everywhere.** The in-target tile, the health-score blood-pressure pillar, and the coach now read a single 90-day window — labeled on the tile, so it is no longer hidden. The tile shows its real span ("· 23 T") until 90 days of history have accrued, and holds back a percentage ("sammelt noch Daten") until it has at least a handful of readings, so a single measurement can never imply a confident share. The all-time view stays on the detail page. The headline figure can differ from the previous unlabeled 30-day number — it is now the correct, consistent one. ### Fixed - **The dashboard ring and the insights card show the same health score.** Both assemble the score from one shared input builder over identical windows, so the same account can no longer see two different scores or a pillar present on one surface and absent on the other; the ring's day-delta now also reflects blood-pressure movement. - **The coach quotes the adherence the card shows.** Medication compliance routes through the same authority as every other surface instead of the coach's own per-day tally, so it can no longer reason from a number you never see — it matters most on irregular, rolling and as-needed schedules. - **The doctor-report matches the app.** The adherence in the PDF/FHIR export is computed from the same dose history — slot attribution, cadence and cross-source dedup honoured — as the detail page, instead of a raw intake-row count, so the figure a clinician reads on paper agrees with the screen. - **Measurement and mood entries reject implausible timestamps.** A future date beyond a small clock-skew allowance, or anything before 1900, is refused — matching the medication-intake guard — so a stray future-dated reading can no longer distort the recent-window metrics. ## [1.16.13] — 2026-06-14 — installable and offline, doses that find the right day, and headers that hold their line ### Added - **The app installs and works offline.** The service worker now registers for every visitor, not only after notifications are switched on, so the app installs to the home screen, its shell and assets are cached, and a lost connection shows an offline page instead of a dead tab. Authenticated pages are never cached — their data always loads live — the share-by-link page is kept out of the cache entirely, and signing out clears the page cache. The running app still heals itself across deploys, and the cached-version marker can no longer drift stale between releases. ### Changed - **The default mood tags are curated.** The redundant `happy`, `excited`, water and music defaults step aside, `overtime` returns to the work picker, a new `praise` ("Lob") tag joins it, and the manual sleep-quality rating retires — a measured night already covers it. Tags already recorded against any of these keep resolving their labels; nothing on existing entries changes. - **Page headers hold one line on a phone.** The medications header no longer drops its actions to a second row, the insights "Ask the coach" action sits to the right where it belongs, and a long label in the more-menu stays inside its tile. The mood, measurements and dashboard headers get the same treatment so none of them can wrap either. ### Fixed - **A historical dose edit credits the schedule that was live then.** Editing a past intake on a medication whose schedule has since changed now attributes it to the dosing era valid at the dose's own time rather than today's schedule — the same era the history and compliance already read it under. - **A CSV intake import moves the tracked stock.** It was the one path that recorded taken doses without decrementing the supply, so the runway read high; it now consumes exactly like every other path, once per dose, and a re-import never drains the shelf twice. - **A workout that reaches WHOOP late is no longer lost.** Workout ingest used a one-hour overlap window while recovery and sleep used a far wider one, so a phone that synced to the WHOOP cloud more than an hour after a session left the workout permanently outside the window. It now shares the wider window; the idempotent upsert keeps the widening from duplicating anything. - **Sleep night totals read as whole minutes** instead of a long fraction, matching the contract the clients decode. - **A tiles-only layout save no longer wipes section customization.** Reordering the navigation tiles without sending the section layout keeps the stored sections instead of resetting them to defaults, and the same holds the other way around. - **The web AI surfaces honour consent again.** The web client now records the consent receipt the server requires before any model call, so insights and the coach stop falling back to the empty no-provider state for people who have granted it, and the status reads "consent missing" honestly when it is genuinely absent. - **The coach button in the public demo no longer errors** — it steps aside there instead of offering a send the demo blocks. ## [1.16.12] — 2026-06-13 — split doses, steady cards, and a read that stays warm ### Added - **A dose can consume a fraction of a unit.** Split-pill medications now declare ½, ⅓ or ¼ of a tablet per dose — the wizard offers a curated set of fractions alongside the whole numbers, the most error-resistant input. The supply tracks the fractional remainder (30 tablets dosed at a half each reads 29.5 after one dose), the days-left projection follows, and undoing a dose refunds exactly what it consumed. Existing whole-number doses are untouched; the inventory unit counts widen to decimals across the API contract. (#316) ### Changed - **The medication cards hold one shape.** The low-supply notice moves onto the adherence row beside the streak — a slot every card already reserves — so a card that is low on stock no longer pushes its bars out of line with the card beside it. The gap between the drug class and the first line tightens at the same time. - **The dashboard and insights stay warm between visits.** The analytics and derived-insight reads keep a served-stale snapshot for an hour rather than ten minutes, so returning after a normal break paints instantly and refreshes in the background instead of paying the full cold rebuild — which a visit spaced more than ten minutes apart hit almost every time. ### Fixed - **The medications list refreshes when you return to it.** Navigating back to the page — or reopening the supply tab — now refetches the list, compliance and stock instead of serving a client cache that could be minutes stale, so a dose logged on another device shows up without a manual reload. ## [1.16.11] — 2026-06-12 — the supply speaks up, tags find their groups, and nothing asks twice ### Added - **The app says so before the pack runs out.** A daily pass projects every tracked supply against its schedule and notifies on the configured channels when the runway falls below a per-account threshold — once per crossing, re-armed by a refill, re-announced when the threshold changes. The threshold lives beside the reminder settings (1–60 days, default 7, or off), the push deep-links straight into the supply tab, the table's stock column carries the projection as "≈ X Tage", and the card states the remaining runway once it crosses the threshold — only then. - **Mood tags get groups, icons and a home of their own.** Custom groups with encrypted labels, a searchable 75-icon picker, and a manage page under settings; tags archive without touching the entries that carry them — editing an old entry can no longer drop its archived tags either — the picker offers inline creation right where a tag is missing, custom labels finally render in the picker AND across insights, and the arrangement persists per account. The whole surface ships in the public API contract. - **Every due dose in one confirmed action.** When two or more doses are due — including a dose escalated past its catch-up window — a button on the medications list collects them into one confirm dialog and records each through the same path as the card buttons: slot attribution, stock consumption and every dependent surface included; failures are counted, not hidden. - **A medication can declare itself as-needed.** No schedules, never due, never reminded, excluded from every compliance rate — but intakes log normally, consume inventory exactly like scheduled doses, and the history stays. The wizard offers it as a cadence choice; switching it onto a fixed schedule later never repaints the schedule-less stretch as missed doses, and the doctor report lists it without inventing a rate. (#316) - **Units-per-dose and the default pack size are editable from the supply tab** — a manufacturer switch with a different blister size no longer requires walking back through the wizard. ### Changed - **One wrench marks every customize entry point.** Dashboard, insights and medications share the glyph, the table corners clip to their container again, and the card/table view choice moves into its own settings section. - **The coach opens in place.** The button brings the side drawer over the page you were reading instead of navigating away, and grew to a comfortable tap size. - **The card's dose state lives on the next-intake row.** Take-now, late and the overdue escalation speak from the line that names the dose — with their time beside them, on mobile too — instead of a separately reserved status line that left a hole on every settled card. - A source ladder saved before a newer integration existed now lists and ranks the later-added sources everywhere — settings, charts, rollups and the sleep picker — instead of silently pinning them below every ranked one. - WHOOP's self-reported profile weight is no longer ingested as a measurement; the stale entry stops resurfacing as the newest weight, and existing accounts heal on the next sync. ### Fixed - **The hypnogram keeps its timeline when a summary writer joins the night.** A stage summary stamping all its rows on one instant no longer evicts a per-segment night; when only summaries exist, the card shows the stage breakdown instead of a fake skyline. - **A dose taken on the dashboard updates the dashboard.** The snapshot band refetches with every intake path instead of waiting out its polling interval. - The supply tab can delete a container, its controls carry the outline they always implied, each container states its condition inline next to its figures, and the table's stock warning follows the user's runway threshold instead of a leftover four-dose constant. - Every wide event carries the recent event-loop lag, and a stalled loop emits its own warning event — the instance-level signal per-request durations cannot carry. ## [1.16.10] — 2026-06-12 — the stock follows every dose, and the list takes your order ### Added - **A medication can declare how many units one dose consumes.** Two 2 mg tablets for a 4 mg dose register as one dose taken and decrement two units, the supply dialog accepts quantities in doses or units with live conversion, containers carry a type (pen, ampoule, blister pack, inhaler, bottle) and hold up to 1000 units — a 200-dose inhaler canister finally fits. (#316) - **The medication list has a table view and an order of your choosing.** A toggle beside the add button switches between cards and a compact, per-column sortable table; both views share a manual order, and the whole arrangement lives in its own settings section behind the same gear icon the dashboard and insights pages use. The choice persists per account. (#316) ### Changed - **The daily-overview band is opt-in now.** New accounts and accounts that never chose it start without the band and keep the plain greeting; it switches on under dashboard settings. - **Expired containers count as available nowhere.** Every surface — overview row, supply tab, list, table, GLP-1 card and the runway estimate — agrees on the same figure and shows expired stock separately instead of mixing it in. - The wellness rings sit exactly on the theme palette now — readiness green, recovery cyan, sleep purple, stress orange, strain pink — instead of neighbouring tones, and the light theme follows the same five hue families. - The inventory endpoints speak one wire dialect: request fields renamed to `unitsTotal` / `unitsRemaining` to match the responses they always returned. ### Fixed - **Every taken dose now moves the tracked stock.** Only one of the six intake paths decremented the supply before — web, the status toggle, the phone sync, the external API and the Telegram button all consume now, the next container opens automatically, and undoing, skipping or deleting a dose refunds exactly what it consumed. Each dose carries its consumption stamp, so a double tap racing a sync replay decrements once, and a re-posted history never drains the shelf. Doses recorded before this release stay untouched — the count starts moving with the next dose, or correct a container once under the supply tab. (#316) - **A dashboard full of readings no longer claims "not enough data".** While the score warms up the band says it is being computed, and a first reading of a new data type — a fresh wearable metric, a first glucose value — can no longer knock the score out, because the gate checks only the types the score actually reads. - **The score stops flickering on busy accounts.** The pre-aggregation rebuild deleted its window before inserting the replacement without a transaction — a concurrent sync write could abort the insert and leave whole types empty until the next pass. Delete and insert run atomically now, and the boot-time backfill shares the fold horizon, so an account with years of history stops re-backfilling on every deploy. - **The filter pills on the mood and measurements lists open their menu on screen again.** The menu opened off-viewport below the page since v1.16.1 — selecting a mood, type or source read as a dead control. - **The coach button stops blinking out on chart hovers.** It no longer hides whenever a chart tooltip is open anywhere on the page, and it sits evenly in the corner. - The pre-item GLP-1 supply ledger reads again: accounts that tracked pens through the older ledger see their count on the card and in the coach until they register containers, which then take over. ## [1.16.9] — 2026-06-12 — the day opens with a verdict, and every dose path tells the truth ### Added - **The dashboard opens with a daily band.** Greeting, the one item that matters right now — a fresh crisis-level blood-pressure reading, an overdue or upcoming dose, weight drifting from its range, a run of short nights, a quiet stretch, a score drop, or the briefing's key finding — with a single action, today's dose tally beside the next due time, and the health score as a ring. The verdict resolves from a fixed ladder with crisis floors that ignore personal target ranges, it never invites a dose the server has not called due, and the band can be turned off under dashboard settings, where the plain greeting then returns to the header. ### Fixed - **An intake recorded through the external API now credits the due slot.** The ingest endpoint was the one intake path that stored a bare row beside the pending slot — the card showed the intake as the last dose while still demanding it, and the ledger later counted a miss. It attributes to the dose band like every other path now, the Telegram confirm and skip buttons converge onto the reminder's own slot, and a nightly pass heals the pairs this has already produced — both days' history and compliance recompute, no manual repair needed. - **A cached page can no longer contradict an action you just took.** Invalidation now fences in-flight cache builds, so a read can neither join a build that started before your write nor have a background refresh re-store the pre-write state as fresh. - **Dose prompts err on the safe side everywhere.** A take shortly before the window credits the slot instead of floating loose, an off-schedule intake can no longer silently resolve a different slot and hide a genuinely due dose, a long-interval dose taken on an earlier day downgrades the slot-day prompt to "last dose n days ago" instead of inviting a double dose, and the overdue badge no longer disappears because the day's pending entries were counted as actions. - Intake times entered retroactively carry the same plausibility bounds as edits, moving a dose across a slot boundary keeps its recorded injection site and dose, both affected days recompute, and schedule corrections, data wipes and backup restores refresh the caches they feed. - The medication cards, the quick-entry preselection and the dose pills compute their clock in the profile timezone instead of a fixed one. - WHOOP credentials are trimmed on save — a stray space from the portal's copy button read as an unknown client — and an empty redirect override falls back to the derived callback URL. ## [1.16.8] — 2026-06-12 — texts that follow your data, charts that never strand ### Added - **The coach is one tap away on every page.** A round button sits bottom right, stays put while you scroll, shows a quiet dot when a nudge is waiting, and steps aside for the selection bar and the welcome tour. - **Tell the coach to remember something.** A remember action under your own chat messages stores the statement in the matching self-context field — allergies land under allergies, diagnoses under conditions — visible and editable in the settings, and fed into every future conversation. Statements phrased casually are recognised again, and remembered facts no longer drop out of the prompt on data-rich accounts. - **Mood notes in full.** Notes expand to their complete text right in the list — the toggle only appears when something is actually clipped — and the CSV export carries a note column. - **The hypnogram names its source** and paints only the stages the night actually contains. ### Changed - **Insight texts regenerate when your data changes, not on a timer.** Every generated text stores a fingerprint of the data it describes; nightly runs, page visits and sync bursts skip the model call when nothing moved. A notable new reading is narrated the same morning, a manual regenerate refreshes the cards along with the briefing, and texts can no longer claim freshness over readings that crossed a staleness boundary. An active day drops from hundreds of model calls to a handful. - **Medication cards arrive in one piece.** All cards share a single compliance request instead of one each, the heavy date math runs about five times faster, and the status line, adherence bars, streak and cycle line keep reserved slots — nothing shifts when data lands. A dose logged from the phone app is reflected immediately. - **A cold start paints the page you'll actually get.** The chart row reserves its space before any data arrives, the tile silhouettes match the real layout, and the health-score card holds its column while the numbers compute. The insights overview runs its reads in parallel and serves repeat visits from cache — the targets and derived endpoints answered in well over a second before. - **Sleep nights pick the richest recorder.** Several apps writing through the same health platform no longer blend into one night — the writer with actual stage data wins, time in bed unions every writer's window, and nights that reach the sleep service late are fetched up to seven days back, so a wrong night heals itself on the next sync. ### Fixed - Charts no longer strand on a skeleton after a deploy: the update check re-arms per version, a failed chart chunk retries and then degrades to a single card with a reload button, stalled requests time out and retry, and a failed data read shows an error with a retry instead of "no data in this period". - Screen readers hear chart failures and coach nudges, the floating button leaves the tab order when it hides, focus survives the remember confirmation, and the icon cluster on insight pages no longer has overlapping touch areas. - The duplicated customize entry and the per-chart target link are gone, show-all-values became an icon beside the target control, pill borders stop clipping in the tab strip, and the mood tag bars adopt the muted palette of the charts above them. - Changing a target range, profile detail or source priority is reflected in the analytics on the next read, AI consent is honoured before any stored text is reused, and CSV exports neutralise spreadsheet formula prefixes in free-text cells. ## [1.16.7] — 2026-06-11 — insights answer instantly, every evening ### Changed - **Insights always answer instantly.** A stale briefing is served immediately with a regeneration kicked off in the background — the response carries a revalidating flag and the open page polls briefly until the fresh content lands, instead of blocking the visit on up to ten seconds of inline generation. - **The nightly pre-warm no longer skips evening readers.** The staleness threshold drops from twenty hours to one — previously an evening visit stamped the cache fresh, the night run skipped it, and it expired exactly the next evening, so the people who read insights after work were the ones who always hit cold generation. The visit-triggered warm fills both locale families and collapses to one job per user. - **The session resolves faster.** The profile read inside `/api/auth/me` runs in parallel with the cookie refresh, the bug-report status answer comes from a ten-minute cache without a write per request, and the medications page is prefetched the moment its nav entry is touched. - **The insights route ships less JavaScript.** The schema validator loads lazily and the mood charts split out of the main chunk, trimming the cold download that dominated an uncached first visit. ### Fixed - A manually entered measurement is reflected in the analytics aggregates on the next read — interactive writes evict the cache instead of marking it stale, while background sync keeps the cheaper path. - The analytics script proxy degrades to a silent no-op instead of failing every page when the upstream is unreachable, and records the failure reason where the operator can find it. ## [1.16.6] — 2026-06-11 — corrected eras, an honest weekly status and a faster first paint ### Added - **Schedule eras can be corrected.** A recorded era can be edited through a superseding revision that keeps the original as an audit trail; manual eras edit in place, all era writes serialise per medication, and duplicate dose times collapse in validation. - **The server-wide assistant key has an admin home** — status, provider, model and base URL with the BYOK-first and consent rules stated where the operator works. ### Changed - **The first paint got faster again.** The dashboard snapshot fires the moment login succeeds instead of waiting for the page chunk — its request starts about half a second earlier under throttled conditions — handed over so the server and client render never disagree. - **Guided coach follow-ups speak to your data.** Questions are personalised from your own snapshot when a provider is available, and an answer receives the coach's streamed reaction before the next question. - **Settings and admin finally share one face.** Twenty-seven hand-rolled card headers move onto the shared pattern, sixty containers normalise to one chrome, descriptions get a consistent one-sentence voice, and the one duplicated coach preference collapses onto a single owner. ### Fixed - A rolling medication whose next dose is tomorrow no longer reads as heavily overdue — the card status derives strictly from the served next-due. - The adherence bars keep a fixed geometry in every state instead of growing with their status text. - The remaining hydration warnings on the dashboard, insights and AI settings are gone. ## [1.16.5] — 2026-06-11 — a guided coach dialog and a schedule that shows its past ### Added - **Open coach follow-ups run as a guided dialog.** A quiet card offers to start, the coach asks one question at a time with visible progress, answers can be adopted into the matching self-context field, skipping keeps a question pending, and a closing summary says what was taken over. - **The schedule shows its history.** Past eras appear as a collapsible timeline on the schedule tab, and an earlier era can be added manually for old history — validated against overlaps and bounded so it can never swallow tracked live history. - **Smarter proactive nudges.** Three new deterministic signals — weight drifting from its target band, a run of short nights, an active user falling silent — plus per-group toggles, a frequency preference and wording that addresses the focus you asked the coach to watch. - **Invites mint up to fifty uses** with a scrolling redemption list. ### Changed - **A documented path away from the moving latest tag.** The compose image reference accepts a pinned digest via env var, and a promote script resolves the published digest, verifies its cosign signature and hands the operator the exact pinning call. ### Fixed - The self-context adopt runs in one transaction, the admin area renders nothing before the role is confirmed, and the json-body helper keeps caller headers intact. ## [1.16.4] — 2026-06-11 — half the bundle, a typed client, and stock you can record ### Added - **The inventory tab records stock** — packs and pens with optional expiry, corrections and withdrawals. The detail page gains a dedicated API tab, an editor entry under lifecycle, and a danger zone without the red drama. - **Record the dose you actually took.** Intakes carry an optional actual dose, editable in the quick-add and log dialogs and shown as a quiet deviation in the history. - **Undo for deletes.** Removing a measurement or mood entry can be taken back from the toast. - **Pull-to-refresh** on the measurement and mood lists, and the customize entry on every insights sub-page. - **Answered coach follow-ups can flow into your self-context** with one tap. ### Changed - **The app got dramatically lighter and faster.** Only the active language ships to the browser — the dashboard route drops from 2.1 MB to under 1 MB parsed (288 KB gzip), with no language flash. The shell paints immediately while the session resolves, a redundant request stage is gone, and insight sub-pages prefetch on approach: a first tab switch lands in tens of milliseconds instead of nearly a second. - **Every client call goes through one typed API client** with the envelope, error and ok-handling in a single place, enforced by lint. The route registry, query keys, background worker and dashboard page split into feature-located modules behind thin indexes. - **Settings behave one way.** Provider and integration cards live in focused subcomponents, every setting persists server-side at exactly one home, save conventions are documented, and duplicated controls collapsed into single owners with links. - **Published images are signed**, dependabot merges only behind the required checks, a pre-commit secret scan guards every commit, the deploy webhook can demand a fresh timestamp, and a monthly drill proves the newest backup actually decrypts. ### Fixed - Server and first client render now agree everywhere query state used to leak into the chrome. - Delete confirmations admit the undo they offer, step counts drop their decimals, streaks know their singular, the medication header speaks your date format, and a failed coach turn keeps your message and says what went wrong. - A language switch updates pending status notes and only flips once the new language is actually loaded. ## [1.16.3] — 2026-06-10 — schedules remember their history ### Fixed - **Changing a schedule no longer rewrites the past.** Every material cadence change now archives the outgoing schedule as a dated revision, and history, compliance and write attribution evaluate each period against the schedule that was valid at the time — a dose era taken at its then-correct times stays on time forever. The repair script can infer historical eras from consistently dated old anchors behind an explicit flag. - **The welcome tour can no longer strand its buttons below the fold.** The target scrolls into view first, the popover flips above it when space runs out, and a centred sheet takes over on any viewport when neither fits. - The avatar menu drops its about entry; the section lives in settings. ## [1.16.1] — 2026-06-10 — a chat worth using, honest sleep stages, stock you can record ### Added - **The inventory tab records stock.** Packs and pens with optional expiry, plus corrections and withdrawals — no API detour. The medication detail page also gains a dedicated API tab, an editor entry under lifecycle replacing the misleading header button, and a danger zone on the shared card surface with only its buttons in red. - **A shared filter bar.** Measurements and mood lists filter through one pill-rail language — active filters read as removable chips, date ranges live in a popover, every page feels the same. - **Six care achievements.** Miss-free dose streaks (7/30/90 days), a four-week measurement routine, a complete self-context and a week of sleep logging. ### Changed - **The coach chat behaves like one.** Single-line composer that grows with the text, a typing indicator, no inline disclaimer, a context rail that hides until asked, conversations in the full view, and coach preferences in Settings. Clearly stated allergies and conditions are remembered immediately. - **Settings and admin share one alignment language.** Icon column left, content flush on its own column, neutral header icons, no duplicated titles, and every select on the account-style idiom with a visible chevron. - **First paint shows silhouettes, not bare cards**, and charts reveal within 1.2 seconds even when one widget lags. ### Fixed - **Sleep stages show again next to coarse sources.** A night carrying granular watch stages alongside coarse phone samples picked the coarse source and showed only awake/asleep; the granular source now wins its session. - **The next-dose line tells the truth after a schedule change.** It reads from the dose calendar instead of a stale schedule window; the repair script reconciles drifted windows and clears pending rows on abandoned anchors. - **Nightly insight texts are actually there in the morning.** The warm pass no longer skips the status cards when the briefing step is budget-blocked or fails, and one hanging provider cannot stall the whole run. - The welcome tour stays inside the viewport — no more sideways scrolling to reach its buttons, with a bottom-sheet fallback on narrow screens. - The dashboard customize icon matches insights, the target-settings link leaves the home charts, and the mood heatmap adopts the damped fills of its sibling charts. ## [1.16.0] — 2026-06-10 — a coach that knows you, invites, and a faster, calmer app ### Added - **Tell the assistant about yourself.** A new encrypted self-context in Settings → AI — free text plus structured fields for conditions, allergies and what the coach should watch — feeds the coach and the daily briefing as clearly fenced context. After saving, the coach may ask up to three targeted follow-up questions, surfaced as tappable chips above the chat composer. - **Proactive coach nudges.** A nightly pass watches four signals — low 7-day adherence, blood pressure above its target band, a falling recovery trend, and a stale self-context — and sends at most one gentle nudge per week through your configured notification channels, with its own opt-out. Off whenever the coach is disabled. - **Invite links with QR codes.** Operators on closed registrations can mint expiring invite links from a new admin section: status, uses, expiry and a redemption history of who joined when. Revoking keeps the history; the link and QR are shown exactly once at creation. - **Attribute an off-schedule dose to its slot.** History rows that landed off schedule now show when the dose had been due and offer to attribute the intake to that slot — visibly badged, always counted as taken late, reversible at any time. - **A time-format preference.** Automatic (follows the language), 24-hour, or 12-hour — set once in the profile, applied to every clock the app renders. - **Insight pills are manageable in one place.** The settings list that orders the navigation pills now also hides and shows them, with the same eye toggle the overview tiles use; the page-side manager folded into a link. ### Changed - **The medication detail page calms down.** Six tabs instead of seven (reminders live with the schedule they gate), every section on the card surface, an overview tab that actually summarises — next dose, reminder state, supply runway — and a history where settled rows carry one quiet menu instead of a button wall. Charts load on demand. - **The dashboard loads as one.** Structured skeletons replace bare tiles, and charts reveal together once their data is in instead of popping in one after another. - **The medication list stops hammering the server.** Compliance for the cards is computed in one calendar pass per request, cached for fifteen minutes and invalidated on every write; the dashboard drops two redundant queries; the BMI chart reuses the weight series; expensive analytics endpoints gain a shared per-user rate limit. - **Nightly assistant runs respect every gate.** Status generation skips users who disabled the coach and honours the operator kill switch, deleted measurements never feed a prompt, the mood card gets the nightly run the other cards always had, non-German locales consistently fall back to English, and worker failures finally reach the error tracker with real retries behind them. - **The README says what matters in one screen**, and the repository description and topics match it. ### Fixed - The wellness ring's glow is no longer clipped to the circle. - An overflowing insight-pill row is reachable by mouse — chevrons and wheel scrolling joined touch. - Safari favourites no longer render the icon on a white plate. - A rate-limited briefing regenerate stops claiming success, and a missing AI provider shows as exactly that instead of an eternal "preparing". - Editing schedule times migrates the open dose rows of the old anchors and reconciles a stale intake window instead of stranding both. - Auto-missed verdicts respect each schedule's real catch-up window, clear when the dose is taken after all, and skip deleted rows. - The intake repair script runs inside the production container. - Every workflow action is pinned, dependabot merges only patch and minor updates behind required checks, vulnerability gates block instead of warn, and the published image carries its build SHA so a deployment can prove what is running. ## [1.15.19] — 2026-06-10 — honest dose states, bounded bodies, new licence ### Changed - **The project licence is now PolyForm Noncommercial 1.0.0.** HealthLog stays free to use, modify and self-host for noncommercial purposes; commercial use needs a separate arrangement. Releases up to and including v1.15.18 were published under AGPL-3.0 and remain available under that licence. ### Fixed - **An open dose no longer reads as missed the moment its time passes.** A pending dose now shows as upcoming until its intake window actually closes — hours for daily schedules, days for weekly ones — so the history tab and the compliance rate stop punishing doses that are still takeable. Phantom "ad-hoc" lines for tonight's not-yet-due doses are gone too. - **Logging the same dose from two places no longer doubles it.** A dose acknowledged via a reminder and also logged from another client produced two live rows on the same slot, inflating the day's schedule count (4 of 4 on a two-dose day). Intake writes now converge onto the existing slot row regardless of source, the compliance rollup counts slots instead of rows — which also corrects historical days on the next recompute — and the duplicate sweep runs nightly instead of only at worker start. - **A weekly dose taken a few days late counts as late, not missed.** The write path only searched ±1 day around the intake time for a matching slot, so a weekly injection taken inside its multi-day catch-up window was stored as off-schedule while the slot stayed missed. The search now covers the schedule's full window reach. - **An intake edit can no longer park a dose on an impossible date.** Editing a dose's time now rejects future timestamps and dates before the medication existed, the picker caps at now, the dialog shows the scheduled slot next to the field and warns when the new time sits more than 48 hours away from it — a day/month typo no longer corrupts the today view silently. The edit dialog's note field is removed; it was never saved. - **One poisoned day no longer halts the nightly consolidation for everyone.** The daily-mean pass could collide with a row parked exactly on its canonical timestamp and abort the whole run on the first conflict, leaving every later user and metric unconsolidated night after night. Collisions now resolve deterministically and a failing day is logged, counted and skipped. - **Every JSON request body is now size-bounded.** Raising the upload limit for large Apple Health archives had removed the implicit ceiling on all other JSON routes; each one now enforces an explicit cap sized to its payload and answers 413 beyond it. ### Added - **An operator repair script for historical intake anomalies.** `scripts/repair-intake-anomalies.ts` lists duplicate rows sharing a dose slot and intakes whose taken time sits implausibly far from their slot; with `--fix` it merges the duplicates the same way the nightly sweep does and recomputes the affected compliance days. Runbook: `docs/ops/intake-repair.md`. ## [1.15.18] — 2026-06-08 — a traceable medication dose history ### Added - **A traceable dose history for every medication.** The Verlauf tab now shows every expected dose slot with a clear status — taken on time, taken late, skipped, or missed — and tags any off-schedule intake as ad-hoc. The history reads from the same ledger the compliance percentage is built on, so the rate you see can no longer disagree with the timeline beneath it. - **A configurable intake window per dose.** Each dose time can carry either a point time or an explicit range (for example 07:00–09:00); the default stays ±1 hour. The window defines the on-time band, so a take that lands inside it counts as on time rather than late. The window is editable on the schedule and in the medication wizard. - **An Insights settings page.** A cog at the top right of the overview opens a dedicated Insights section in Settings, where you arrange the overview sections and sort the navigation pills. The old inline "Anpassen" toggle is gone — the cog is the single entry point. - **A dose-history endpoint.** A read endpoint returns the resolved per-slot status over a date range, so the history view and any client render the same picture. ### Changed - **A redesigned medication detail page.** The detail page is now a tabbed layout — Übersicht, Zeitplan, Erinnerung, Bestand, Verlauf, Erweitert — and the old stacked advanced-settings modal is dissolved into tidy, grouped sections under Erweitert. Card actions open straight to the matching tab. - **The medication card keeps a constant surface.** The card no longer changes colour by dose status; the current status shows as a discreet label instead of tinting the whole card. - **Intake writes and edits attribute by the real intake time.** Marking a dose taken records it against the slot whose window it falls in; a take outside every window records as ad-hoc instead of snapping to a far-off slot. Editing a dose re-attributes it, a near-miss take offers "attribute to this dose anyway?", and marking taken or skipped recomputes the history and rate immediately. ### Fixed - **The daily briefing's regenerate no longer times out before the text is ready.** A slow but successful regeneration used to be discarded by a short timeout, leaving the old text in place while the toast still claimed success. The regenerate path now waits for the warm text and the toast is honest — it reports a refresh only when one happened, and tells you to try again when generation is still running. - **The sleep-phase chart no longer double-counts time in bed.** Time-in-bed was being stacked as if it were a separate phase on top of the individual phases, roughly doubling the bar and the per-night total. It is no longer counted as a phase, so the bar and tooltip total read correctly. ## [1.15.17] — 2026-06-07 — large imports, demo chart toggles ### Fixed - **Large Apple Health exports import correctly.** A request-body size limit in the middleware layer truncated uploads over ~10 MB before they reached the importer, so a multi-megabyte `export.zip` lost its ZIP structure and failed with an "end-of-central-directory" error. The limit is raised so real exports arrive intact. (GitHub #281) ### Changed - **Chart display toggles work in demo mode.** On a demo instance, the comparison-baseline selector and per-chart overlay toggles above charts are no longer blocked — these are harmless display preferences. ## [1.15.16] — 2026-06-07 — wellness ring glow no longer clipped ### Fixed - **The score ring's glow shows in full.** On a wellness score detail page the ring's soft glow was being clipped at the card edge. The card no longer clips its contents (its background gradient still follows the rounded corners), so the glow renders uncut on every side. ## [1.15.15] — 2026-06-07 — one warm, motivating assistant voice ### Changed - **The whole assistant speaks in one voice.** The daily briefing, key findings, the insight write-ups, and the Coach now share the same warm, motivating, advisory tone the metric and wellness assessments adopted last release — encouraging and grounded in your data, honest about what needs attention, never generic. (Already-generated briefing text refreshes within about a day; the Coach speaks the new way immediately.) ## [1.15.14] — 2026-06-07 — manage every detail page, calmer mood charts ### Added - **Manage every detail page from the overview.** "Anpassen" → "Detailseiten verwalten" now lists every insights sub-page, grouped by category, so you can reorder and hide any of them — not just the vitals tiles. The top navigation follows your choices: hide a page and its pill leaves the nav too. ### Changed - **Calmer, more compact mood charts.** The mood distribution and weekday-average charts on the mood page are smaller and use softer colour, so the page reads more easily. Their axis labels are now legible in the light theme too. ## [1.15.13] — 2026-06-07 — search, filter, and bulk delete ### Added - **Filter your measurements and mood entries.** Both lists now filter by source (manual, Apple Health, Withings, …) and by a date range, alongside the existing type / mood filter and sorting. - **Select and delete in bulk.** Tick several entries on a page and remove them in one step, with a confirmation step. Deletions stay reversible on your devices the same way a single delete does. ## [1.15.12] — 2026-06-07 — a fairer health score and a richer overview ### Fixed - **Blood pressure is scored fairly.** The Health Score's blood-pressure share used to be all-or-nothing per reading: a reading slightly over target counted exactly like one far over, so being consistently a little high collapsed the score even when you were well-controlled. It now grades by how close your readings sit to your target — recent readings weighted more — so "borderline" reads as borderline, not a crisis, while genuinely high pressure still scores low. - **Workout heart rate no longer counts against your resting pulse.** Apple Health files every heart-rate sample — including the hundreds recorded during a workout — under one type, and the resting-pulse view was scoring all of them against a resting range, so an active day looked "elevated." Resting-pulse surfaces now use your true resting heart rate (with a careful estimate when that isn't available), and raw heart rate is no longer judged against a resting target. - **The "in review" section is named "Rückblick"** and the spacing above it matches the rest of the overview. ### Added - **Tap a wellness score for the full picture.** Readiness, Recovery, Sleep, Strain, and Stress now open a detail view with an explanation of what the score means and why it matters, the breakdown of how it's built, and a personal assessment of where you stand. - **A warmer, more motivating assessment voice** across the insight assessments — encouraging and grounded in your data, never generic. - **The running version on the admin overview**, with a hint when a newer release is available. ### Changed - **Premium polish on the wellness detail view** — the detail card carries the score's colour, the ring has room to breathe, and the info control sits in the top corner. - **Self-hosted instances resolve a login's location online by default**, using a bundled offline database only when one is configured — so the admin login overview shows a location out of the box. ## [1.15.11] — 2026-06-07 — a customizable overview ### Added - **Arrange your overview.** Tap **Anpassen** on the Insights overview to rearrange it: drag sections into the order you want, hide the ones you don't, and — inside Vitals — reorder or hide individual metric tiles. Your layout is saved to your account and carries across devices. Tap **Fertig** to apply, **Zurücksetzen** to return to the default arrangement. ### Fixed - **Self-hosting docs now generate a URL-safe Postgres password.** The setup examples used `openssl rand -base64 24`, which can emit characters (`/`, `+`, `=`) that break the unescaped `DATABASE_URL` and stop a fresh instance from connecting to its database. Every example now uses `openssl rand -hex 32`. ## [1.15.10] — 2026-06-07 — Insights overview, medication follow-ups, health score ### Fixed - **Medication compliance now pairs doses to the right slot.** For a twice-daily medication, a dose logged off its usual time could be matched to the wrong slot — so a deliberately-skipped dose might read as taken, or a late evening dose as missed. Doses now snap to their own scheduled slot first, so the percentage reflects what actually happened. - **The card advances to your next real dose.** After you log a dose, the "next dose" line moves to the next upcoming, unresolved slot instead of lingering on one that's already taken or past. - **A single dose time reads cleanly.** A dose with one target time shows "07:00" rather than "07:00 to 07:00". - **Your weight trend counts toward the Health Score even without a target.** The weight pillar used to need a height/target to score; it now scores your weight trend on its own when no target is set, so it stops dropping out of the score. ### Changed - **A consistent Insights overview.** Every section now has its title above its card with a matching icon and even spacing throughout — the daily-briefing icon matches the rest, Trends has an icon, and the period-in-review and cycle sections carry proper headings. - **A clearer cycle ring for new trackers.** With only a little cycle data the ring now shows the four phases in sensible proportions instead of collapsing to a single arc. - **Mobile polish on Insights and Cycle** — the cycle ring fits narrow screens, the cycle tabs no longer crowd, and calendar days meet the tap-target size. - **Your photo in the top bar.** If you've uploaded a profile photo it now shows in the top-right avatar; otherwise the icon remains. - **The notifications area is just "Notifications"** (shorter), and admins get a link to the admin area from the mobile menu too. - **Dark theme by default** for a fresh visitor; an explicit theme choice is still remembered. ## [1.15.9] — 2026-06-07 — medication compliance and cards ### Fixed - **Compliance now counts a forgotten dose as missed.** A scheduled dose that was never taken or skipped used to quietly drop out of the calculation after a day, which made adherence read higher than it was. It now counts as missed, so your 7 / 30 / 90 / 365-day figures reflect what actually happened — and genuinely differ from one another instead of all looking the same. - **The dashboard medication chart is computed against your schedule.** It previously measured taken doses against the doses you had logged (not the doses you were due), which pushed every window toward 100%. It now measures against the schedule. - **Consistent skip handling.** A dose you deliberately skip is treated the same everywhere (a deliberate pause, not a miss); a dose simply forgotten counts as missed. ### Changed - **The two medication cards are now truly identical.** Both styles render from one shared card body, so structure, spacing, labels, and the streak row line up exactly — only the value content differs (a weekly injectable keeps its relative-day phrasing, a daily dose shows a time). The bottom-spacing mismatch between cards is gone. - **Cards highlight the dose that's due.** When a dose is in its take-now window the card is highlighted; once it is past due the card shows an overdue note, escalating to "Stark überfällig" near the cutoff. After the cutoff the dose counts as missed and the next dose becomes the focus. - **Dose windows are cadence-aware.** A daily dose has a short on-time window then an overdue grace before it counts as missed; a weekly injectable follows a wider, clinically-appropriate window. - **Tidier card.** The compliance bars show the percentage on its own, and the injection-site line was removed from the card (site tracking is unchanged). ## [1.15.8.1] — 2026-06-07 — large Apple Health imports ### Fixed - **Large Apple Health `export.zip` uploads no longer fail with "Could not locate ZIP End-Of-Central-Directory record".** The multipart upload reader could drop part of a file body when a network chunk ended exactly on the multipart boundary — which only showed up on larger, multi-chunk uploads (a small export imported fine). The reader now handles the boundary across chunks correctly, so the uploaded archive arrives byte-for-byte intact. (#281) ## [1.15.8] — 2026-06-07 — medication cards ### Changed - **Consistent medication cards.** Both medication card styles now use the same structure and the same labels for the same thing — "Next intake" and "Last intake" everywhere — so the two cards read the same way. The GLP-1 card keeps its relative-day hint ("Saturday 13 Jul (in 7 days)"). - **Compliance bars show the counts.** Each compliance bar now shows the doses behind the percentage (for example "100% · 12 / 12"), so two windows that genuinely land at the same percentage are legible at a glance instead of looking like a glitch. ## [1.15.7] — 2026-06-07 — data import in the browser ### Added - **Import your data from the browser.** Settings → Export is now **Export & Import** and carries a proper import area with two paths: - **Apple Health** — upload your `export.zip` directly. It streams server-side (handles multi-GB archives) and is idempotent, so re-uploading the same archive merges rather than duplicating; progress and the imported/skipped totals are shown as it runs. - **Generic JSON** — upload or paste a JSON document of measurements and mood entries (with a downloadable example and a documented schema) for manually-prepared or converted data. - The data-import schema, the full measurement-type/unit reference, and a "convert a CSV into this" how-to are documented at `docs/integrations/data-import.md`. - **Cycle export.** When cycle tracking is on, the Export area offers an explicit cycle export. ### Changed - **A more compact health-record export card.** The health-record export keeps its prominence but takes far less vertical space. ### Fixed - **The Apple Health import is reachable again.** The importer existed on the backend, but the web page the docs pointed to was never present; there is now a real import UI in Settings → Export & Import. (#281) ## [1.15.6] — 2026-06-06 — settings, insights, and medication-card polish ### Fixed - **The notifications settings panel no longer crashes.** Opening Settings → Notifications could throw a client-side error on some accounts; the panel now renders safely whatever shape the data arrives in. - **Even chevron in dropdowns.** The arrow on the right of selects (timezone, language, units, and everywhere else) now sits with the same inset from the right edge as the label has from the left, instead of pushing against the edge. ### Changed - **Clearer Insights overview.** The period-in-review card now carries a proper heading, and the "signals of the day" section only appears when there's an actual anomaly to flag — when every vital is within your personal range it stays hidden rather than showing an empty header. - **Tidier medication cards.** The next-dose line no longer appears twice on a card, and the injection-site suggestion text has been dropped from the card surface (site tracking and the post-dose picker are unchanged). - **The avatar menu lands in Account.** Clicking your avatar now opens the Account settings directly. - **Shorter integration save button.** The integrations credential form's button now simply reads "Save". - **Tidier API-tokens panel.** The token endpoint URL is shown inline with a copy button, and the "generate another token" button matches the rest of the page rather than standing out in a different colour. - **The export panel's create button is right-aligned**, consistent with other primary actions. ### Removed - **The moodLog integration** has been removed from the Integrations page; it is no longer used. - **The standalone sharing (create-link) feature** has been removed from the Export area. - **The doctor report is no longer offered twice** — it has been removed from the main Export page, where it duplicated the health-record export's own doctor report. ## [1.15.5] — 2026-06-06 — insights layout, navigation, password-reset CLI ### Added - **Reset a password from the command line.** Operators can reset a user's password from inside the container — useful when someone is locked out — via `docker compose exec app node scripts/reset-password.mjs `. See `docs/ops/password-reset.md`. ### Changed - **A tidier health-scores row.** With cycle tracking on, your cycle ring takes the place of the strain ring in the Insights health-scores row, so the row stays compact rather than growing an extra tile. - **Cycle sits with your other trackers.** The Cycle entry in the sidebar now appears between Medications and Insights, rather than at the very bottom. ### Fixed - **No more empty gap on the Insights overview.** The cycle summary card on the Insights page was held invisible by a style rule yet still took up space, leaving an odd gap above and below it; it now appears as intended and the spacing is even. ### Added - **Your cycle ring in the health-scores strip.** With cycle tracking on, the Insights health-scores row now includes your cycle ring — current phase and day — alongside the other scores. It shows only when cycle tracking is enabled. ### Changed - **A calmer cycle calendar and cards.** Predicted-period days are now a soft filled marker instead of a thin underline; the "what's happening now" card sits only under the ring (no duplicate above the calendar) and drops its own log button since logging is one tap away at the top; and the predictions and insights tabs lost their repeated "estimates / associations" footers (the caveat is stated once, where it matters). - **Tidier settings.** Section descriptions line up under their headings; the web-push enable action sits in the card header; and the standalone cycle-data export/delete actions are gone from the cycle settings (cycle data is already in the full export). - **Calendar tiles keep their size.** The mood and medication calendar tiles no longer balloon to several times the height of their neighbours when only a short window or a single medication is shown. ### Fixed - **Per-score explanations load faster.** The dashboard's batch read now carries each score's deterministic explanation, so the score sheets show their "why" without an extra request. ### Added - **Your cycle at a glance on Insights.** If you track your cycle, the Insights page now shows a compact card with your current phase and cycle day plus the one standout phase relation, with a tap-through to the full cycle insights. It appears only when cycle tracking is on, and the detailed phase analysis stays in the cycle section. ## [1.15.2] — 2026-06-06 — cycle insights crash fix ### Fixed - **Cycle insights no longer crash on mood or blood-glucose relations.** The insights tab could throw and fail to render when a mood or blood-glucose pattern cleared the significance check, because those two metrics were missing their on-screen labels and units. They now render correctly, and an unrecognised metric can never crash the tab again. ## [1.15.1] — 2026-06-06 — cycle premium parity, reminder polish, fixes ### Added - **Know your phase.** A "what's happening now" card next to the cycle wheel names your current phase, describes it in plain language, and surfaces the symptoms you yourself tend to log in that phase — drawn from your own history, never generic claims — with a one-tap shortcut to log today. It stays in a calm "still learning" state until there's enough of your data to be honest about. - **Your own symptoms.** Add custom symptoms beyond the built-in set, with a name and an icon, straight from the log sheet. Custom symptom names are encrypted at rest, included in export, and removed by the cycle purge. - **Fertile-window reminder.** An opt-in reminder a couple of days before your predicted fertile window — shown only when your goal is to conceive, off by default, and discreet-mode aware. It is an estimate, never a contraceptive or "safe day" claim. - **A richer cycle calendar and history.** Period days now shade by flow intensity, a confirmed ovulation is marked distinctly from a predicted one, and a new cycle-length history chart shows your recent cycles with average, variability, and a regular/irregular read. ### Changed - **Reminders read as plain text.** Medication, mood, and cycle reminder notifications no longer carry decorative emoji; emoji are kept only for system and failure alerts. ### Fixed - **Blood-pressure-in-target reads accurately.** The "in target" share on the home health score now pairs systolic and diastolic readings the same way the rest of the app does, so imported readings whose timestamps differ by more than a few minutes are no longer dropped from the calculation. - **A dose is never shown as taken before its time.** Logging a dose can no longer attribute it to a later slot still in the future — a late morning dose will not appear as the evening dose taken early. ### Security - **Closed a server-side request risk in Web Push.** Push subscription endpoints are now validated against internal-network addresses on save and again before delivery, and the cycle-insights endpoint is rate-limited. ## [1.15.0] — 2026-06-06 — cycle tracking ### Added - **Cycle tracking.** A new, private menstrual-cycle vertical. Log your period (flow and intermenstrual spotting), basal body temperature, cervical mucus, ovulation and pregnancy/progesterone tests, contraception, intercourse, and graded symptoms — all from one quick-entry sheet. A month calendar marks logged days, the predicted period window, the fertile window, and ovulation; the cycle wheel shows your current day and phase at a glance, and a logged day can be edited or deleted. - **Honest predictions.** Next-period and fertile-window forecasts are always shown as a range with a confidence level — never a single certain date — and carry a clear, non-medical caveat (with a stronger one when you track to avoid pregnancy). A "read your body" mode suppresses every interpretation if you prefer to see only your raw data. The forecast is computed from your own data so the iOS client produces the identical result offline. - **Basal-body-temperature chart** for the current cycle, with your fertility signs in the tooltip and the estimated — or confirmed — ovulation marked. - **Phase insights.** An insights tab relates your cycle phase to your vitals — resting heart rate, HRV, sleep, steps, weight, blood glucose, mood — showing where each differs between phases with statistical significance, and which of your logged symptoms cluster in which phase. Associations only, never cause. A per-cycle history lists your recent cycle and period lengths. - **Privacy first.** Cycle tracking is off by default except for accounts set to female, and anyone can turn it on or off from settings. The intent-revealing fields are encrypted at rest behind a default-on toggle; a discreet-notifications mode keeps any reminder generic on the lock screen; period reminders are opt-in per channel; and a one-tap purge hard-deletes every cycle row, the cycle audit trail, and reminder records, and clears your tracking intent. - **Apple Health import.** Reproductive-health categories from an Apple Health export fold into your cycle log, and cycle data is included in the health-record export (PDF and FHIR). ## [1.14.0] — 2026-06-06 — animated health scores, score explanations, mood-factor correlations, log past doses ### Added - **Each health score now explains itself.** The Readiness, Sleep, Recovery, Strain and Stress score sheets carry a short, plain-language read of why the score landed where it did, drawn from the inputs that moved it. It fills even without an AI provider configured, so the explanation is always there. - **See how your rated factors track your body.** When you rate factors like work, sleep quality or stress on a mood entry, HealthLog now relates those ratings to your vitals — for example how your sleep or resting heart rate differs on the days you rate work low versus high — as a new card that the assistant can also cite. Associations only: each is shown with a confidence level, computed over a stable window, and never presented as cause. - **Log a dose you took earlier.** The medications "Add" button now offers a choice — create a new medication, or log an intake — and the intake path accepts a backdated date and time against an existing medication. ### Changed - **The health scores got a signature redesign.** The score strip reveals with a calm, staggered animation — each ring springs to its value while the number counts up — with a distinct colour per score (readiness green, recovery teal, sleep blue, stress amber, strain violet), a soft glow, and a faint "vs your normal" reference arc on the scores that carry a history. The motion plays once per visit and fully respects reduced-motion; the colours hold their contrast in both light and dark themes. - **Sparse medications show where you stand at a glance.** A weekly injection or other "every N days" medication now shows its open-cycle status on the card — "next dose in N days", "due today", "overdue", or "no cycles yet" — instead of leaning on a percentage that says little between doses. - **Smaller refinements across Insights.** The three trend cards (mood, weight, blood pressure) are now exactly the same height; the daily-briefing heading sits above its card; and the assistant tone and length controls sit side by side to cut scrolling. ## [1.13.3] — 2026-06-05 — health score scores blood pressure again, steps in Insights ### Fixed - **The health score rates your blood pressure again.** The score was reading your blood-pressure-in-range rate from the trailing 30 days, but the score is meant to use your full history — so an account with blood-pressure history but no readings in the last month lost the whole blood-pressure pillar and showed "no rating". The score now reads the all-time rate (the at-a-glance tile headline still shows the last 30 days). If your blood pressure or weight still shows no rating, check that your date of birth (needed for the blood-pressure target) and height + target weight are set in your profile — those are required for the respective pillars. ### Added - **Steps now have their own Insights page.** "Activity / Steps" was tracked and shown on the dashboard but never appeared in the Insights metric navigation; it now has its own Insights sub-page like the other activity metrics, surfaced whenever you have step data. ## [1.13.2] — 2026-06-05 — honest weekly-med compliance, correct sleep average, calmer wellness scores ### Fixed - **Weekly injections (and other "every N days" medications) report honest adherence.** A rolling-cadence medication — the typical weekly GLP-1 injection — only ever counted its single next-due dose, so the rate flipped between a meaningless 100% and a hard 0% on a single dose and ignored a faithful history of shots. Compliance now reconstructs the full per-cycle history across the window: every logged dose counts, a genuinely skipped cycle reads as one miss, and the current cycle that simply hasn't come due yet no longer drags the number to zero. A new per-medication "current cycle" state (on track / due today / overdue / no cycles yet) lets the card show "next dose in N days" instead of a scary 0%. - **The sleep average is correct again.** When a device wrote both a combined "asleep" block and the detailed light/deep/REM breakdown for the same night, the average double-counted them and could show an impossible figure (e.g. ~20 h). The average now reads from the de-duplicated per-night totals. The night-detail reconstruction is also hardened so it never errors on an unusual night. ### Changed - **The wellness scores look calmer and clearer.** The "Your health scores" tiles (renamed from the old label) now use a gentle, card-toned surface instead of a heavy saturated slab, and each score draws a larger, thinner ring with a soft single-colour gradient tuned per score (readiness greener, sleep bluer, recovery and strain turquoise). The number and band stay fully legible in both light and dark themes. ## [1.13.1] — 2026-06-05 — wellness rings show the real score ### Fixed - **The wellness-score rings now fill to the actual score.** Each ring was sweeping a full circle regardless of its value, so every score looked like 100%. The ring now fills proportionally — a 74 fills roughly three-quarters of the circle — across the overview tiles and the score detail view. ## [1.13.0] — 2026-06-05 — your own mood tags ### Added - **Make the mood-tag list your own.** You can now create your own custom mood tags, give each a label and an icon, and hide any of the built-in tags you don't use — so the tag picker shows exactly the set that fits you. Custom tags work everywhere a built-in tag does: logging, history, and the analysis. Custom tag names are encrypted at rest like the rest of your personal content. Deleting a custom tag keeps it on your past entries by default; a separate purge removes it from history too. ## [1.12.12] — 2026-06-05 — the period retrospective is never empty ### Added - **The period retrospective fills in even without an AI provider.** When no AI provider is configured (or before the first AI summary has been written), the "period in review" card now shows a concise, factual summary built from your own data — the biggest changes versus the prior period, any vitals that moved outside your typical range, and an honest, strictly non-causal count of statistical associations. It reads in German or English and is replaced in place the moment an AI summary is generated. ## [1.12.11] — 2026-06-05 — settings notices read the same everywhere ### Changed - **Settings notices and status colours are unified.** Every callout, badge and status note in Settings now draws from the same set of meaning-based colours — success, caution, info — so a green "connected" or an amber "needs attention" looks identical from one card to the next and stays legible in both light and dark themes. Caution notes share one consistent boxed style with a leading icon. ## [1.12.10] — 2026-06-05 — a "taken" tap no longer back-fills the wrong dose ### Fixed - **Logging a dose "taken now" no longer marks a far-away scheduled dose.** For a medication taken more than once a day, a quick "taken" log without a specific time (including a synced record) was snapping onto the nearest scheduled slot up to six hours away — so a midday log could mark the morning dose taken. A timeless "taken" log now records as its own entry and never back-fills a distant slot; logging a specific dose by its time is unchanged. ## [1.12.9] — 2026-06-05 — a tighter mood catalogue, clearer cards, and a fixed retrospective ### Changed - **The mood factors are down to three sliders** — work, sleep quality, and a new sadness slider — and the day-tag list is trimmed to a tighter, higher-signal set. Tags you've already used stay on your past entries; they just no longer clutter the picker. - **Medication cards read cleaner.** The next-dose and last-dose lines are now their own rows: the label on the left, the time flush to the right. ### Fixed - **The period retrospective generates again.** A non-measurement metric was leaking into a measurement query and failing the background job; it's now filtered out. ### Documentation - **Every code directory has a README**, the main README embeds the architecture diagrams and a documentation index, and the contributor guide reflects the current trunk-based release model. ## [1.12.8] — 2026-06-05 — fuller WHOOP data, charts that follow your selection ### Added - **WHOOP now records more of what it measures.** Daily average heart rate, daily max heart rate, and per-night sleep disturbances are imported alongside the recovery, sleep, cycle and workout data already synced. ### Changed - **The stats above a chart follow the range you pick.** Choosing 7 / 30 / 90 days or a year now recomputes the min / max / median / average for exactly that span — the numbers always match what the chart shows. The separate draggable range slider is removed. - **Every chart leads with the same titled header** as the rest of the Insights tiles, for one consistent layout. - **The wellness dials are refreshed** — a white ring on a gradient tile, matching the greeting card. - **On the pulse page, the assessment sits above the cardio-fitness link**, and the assessment card's heading sits closer to its text. ### Fixed - **AI insights use the current default models consistently** across every path, so the shared-key experience matches the rest. ### Added - **The stats above a chart now follow your selection.** Brush a window on a metric chart and the min / max / median / average recompute for just that span; clear the selection to return to the full range. Works the same on every metric page, and blood pressure now shows systolic and diastolic in one card, side by side. ### Changed - **The mood page reads top to bottom the way you'd walk it.** Summary, then the mood calendar, then the chart, then the target, then the assessment — followed by the breakdowns. The two "what stands out" sections are merged into one card, the statistical footnote is gone (the detail lives in a small info tooltip), and the mood colours are aligned to the rest of the charts. - **VO₂max opens its full chart.** The compact row on the pulse page now links to the dedicated cardio-fitness page instead of showing a chart-less summary. - **AI assessments vary more across your metrics.** The anti-repetition context now reaches the main cards (blood pressure, weight, pulse, mood, BMI, medication, overall), a step is only suggested when the finding implies one, and the default models were refreshed so the shared-key fallback isn't a quality drop. ### Fixed - **Status colours stay legible on the light theme.** Connection states, recommendation badges, score provenance, and the settings success messages now use tokens that meet contrast in both themes. - **A late device sync no longer shrinks a day's step or energy total.** Trailing samples are folded into the existing daily total instead of replacing it. - **The medication streak is correct across time zones**, and the dashboard now serves its snapshot instantly after a sync while refreshing in the background. - **Metric pages and the Insights overview show a retry on error** instead of a blank or a false "no data yet". - **Account profile:** the timezone selector fills its row, and the "globally excluded injection sites" card matches the other cards. ## [1.12.7] — 2026-06-05 — Insights that react, read clean, and stay legible ### Added - **The stats above a chart now follow your selection.** Brush a window on a metric chart and the min / max / median / average recompute for just that span; clear the selection to return to the full range. Works the same on every metric page, and blood pressure now shows systolic and diastolic in one card, side by side. ### Changed - **The mood page reads top to bottom the way you'd walk it.** Summary, then the mood calendar, then the chart, then the target, then the assessment — followed by the breakdowns. The two "what stands out" sections are merged into one card, the statistical footnote is gone (the detail lives in a small info tooltip), and the mood colours are aligned to the rest of the charts. - **VO₂max opens its full chart.** The compact row on the pulse page now links to the dedicated cardio-fitness page instead of showing a chart-less summary. - **AI assessments vary more across your metrics.** The anti-repetition context now reaches the main cards (blood pressure, weight, pulse, mood, BMI, medication, overall), a step is only suggested when the finding implies one, and the default models were refreshed so the shared-key fallback isn't a quality drop. ### Fixed - **Status colours stay legible on the light theme.** Connection states, recommendation badges, score provenance, and the settings success messages now use tokens that meet contrast in both themes. - **A late device sync no longer shrinks a day's step or energy total.** Trailing samples are folded into the existing daily total instead of replacing it. - **The medication streak is correct across time zones**, and the dashboard now serves its snapshot instantly after a sync while refreshing in the background. - **Metric pages and the Insights overview show a retry on error** instead of a blank or a false "no data yet". - **Account profile:** the timezone selector fills its row, and the "globally excluded injection sites" card matches the other cards. ## [1.12.6] — 2026-06-05 — one consistent Insights surface ### Changed - **Every Insights card now reads the same way: an icon, a title, then the content.** Targets, the systolic and diastolic stats, the wellness and vital tiles, mood, and medications all lead with the same titled header in the same weight, so the surface is easier to scan and find your place in. The target tile is simply titled "Target". - **The wellness scores sit above the daily briefing, with legible numbers.** The score numbers now render in the foreground colour against the dark cards (the band still shows on the ring), and the strip leads the overview above the briefing. - **The min / max / median / average stats moved above each chart.** They now lead the metric page rather than trailing it, and blood pressure shows the full stat strip for both systolic and diastolic. - **The mood page leads with where you stand, then what fits your better days.** The classification (in-range share and stability) comes first, followed by the better-days read; the mood colours are aligned to the rest of the charts. - **The medications page is tighter.** The dose-by-dose therapy list is gone, the per-medication compliance tiles are denser, and the assessment sits directly under the target. ### Fixed - **The medical disclaimer appears once.** The "describes your own data, not a clinical assessment" line was repeated across many tiles; it now shows once as a single page footer (and no longer doubles up on the Coach page, which carries its own note). - **Medication status counts meet contrast on the light theme.** The taken / skipped / missed figures now use the semantic colour tokens, which stay legible in both themes. ### Documentation - **Provider setup is documented end to end.** The WHOOP guide now pins the exact OAuth scopes and the webhook URL format, and a new Google Health (Fitbit & Pixel) guide covers the Google Cloud OAuth client, scopes, and redirect URI. See `docs/integrations/`. ## [1.12.5] — 2026-06-05 — correct sleep summary and readiness contributors ### Fixed - **The sleep summary now reflects the whole night, not a single stage.** The Insights sleep figure and prose were reading one sleep stage (~16 minutes) instead of the reconstructed night total; they now use the night's time-asleep, the same number the dashboard and charts already show. - **Readiness contributors no longer read a misleading 0.** Resting heart rate, heart-rate variability, and respiratory rate are hidden (rather than shown as 0) when there isn't yet enough history to establish a baseline; a genuine low sub-score still shows. ## [1.12.4] — 2026-06-05 — a calmer, more consistent Insights ### Changed - **The Insights overview is reordered and easier to read.** Top to bottom: the morning greeting + Coach, then your wellness scores (now filling the row), vital values, trends, the period retrospective, and the signals of the day. The wellness numbers are legible on the dark theme, the steps figure in the daily briefing now opens its readings, the Coach suggests two focused questions, and the duplicate "prepare assessment" button at the foot of the page is gone. - **Every metric page now follows one layout.** Intro → chart → target → assessment → the min / max / median / average stats below it. The target tile matches the other cards, blood pressure gains the same stat strip the other metrics have, the redundant range / "vs. previous" row is removed, and the "last measurement" card no longer clutters the page. - **The mood page is tighter.** The little insight one-liners now sit side by side instead of each taking a full row, and the statistical explainers collapse into a small info icon you can hover or focus for the detail. ## [1.12.3] — 2026-06-05 — mark the dose you're looking at ### Fixed - **Marking a dose "taken" now records the dose on screen, not the one nearest the clock.** For a medication taken more than once a day (e.g. morning and evening), the "Taken" button now targets the specific dose the card is showing, instead of snapping to whichever slot is closest to the current time — so a morning tap can no longer land on the wrong dose. ## [1.12.2] — 2026-06-05 — WHOOP connect from the app, consistent assessments and medications ### Added - **WHOOP can be connected from the native app.** The OAuth handshake now works for an app that holds no web session: the client obtains a short-lived one-time connect token and opens the WHOOP authorization in an in-app browser that returns straight to the app. (No change for the web flow.) ### Changed - **One word for "assessment", everywhere.** The AI assessment was labelled four different ways across the app; it now reads consistently in every language, and the assessment is always the final block on a metric page. - **Every medication behaves the same when you log a dose.** Marking a GLP-1 dose taken now shows the same failure notice and one-tap undo that the other medication types already had, and medication status colours, the compliance percentage, and tap-target sizes are consistent across the cards. ### Operations - **An alarm when the TLS certificate's public key changes.** Self-hosters whose native clients pin the server certificate now get a loud, logged alert (and an admin notification) when the served leaf key rotates, so the pin can be refreshed before it lapses. Set the expected pin(s) via `TLS_LEAF_SPKI_PINS`; see `docs/ops/tls-cert-pin.md`. ## [1.12.1] — 2026-06-05 — security, data-integrity, and insight-quality hardening ### Changed - **Consent is now enforced on the server before any health data reaches a server-managed AI provider.** When the app uses an operator-provided AI key to generate insights, the assistant, or medication extraction, it first requires an active consent receipt — consent no longer depends on the client asking nicely. Bring-your-own-key and local providers are unaffected (the key is your own). - **Assessments read less repetitively and more relationally.** A per-metric card can now bring in a genuine correlation from your own data (shown as an association, never a cause), varies its wording instead of repeating a near-identical paragraph when a metric has been steady for weeks, and hedges honestly when there is little data rather than over-claiming. - **Settings feel like one surface.** The integration, account, sharing, and API cards share one header, the same button names and behaviour (so "Sync now" / "Sync all data" / "Test connection" mean the same thing everywhere), consistent tap-target sizes, and consistent spacing. - **Faster settings and insights.** The integrations screen now loads its status in a single request instead of one per provider, the mood insights stay warm instead of recomputing from cold on every entry, and below-the-fold charts load on demand. - **Sturdier Google Health / Fitbit sync.** A reading you delete stays deleted instead of reappearing on the next sync, large backfills write in batches, and the hourly poll no longer lets one slow account stall the rest. ### Fixed - **Re-taking a dose after deleting it works again.** The medication intake history no longer blocks a legitimate re-entry against a deleted row. - **Mood entries can round-trip without duplicating.** Mood entries now carry a stable identifier, so re-importing the same entry updates it in place instead of creating a second copy. - **Tag-to-metric comparisons no longer double-count.** When the same day's steps, energy, or sleep arrive from more than one source, the mood tag comparison counts them once. - Logging out now revokes a native session's token immediately, a long-lived refresh token is never handed to a browser, and a few authentication edge cases were tightened. ## [1.12.0] — 2026-06-05 — Google Health sync, rated mood factors, and a Coach and insights overhaul ### Added - **Google Health (Fitbit & Pixel) — experimental.** Connect a Google Health account through your own Google client and pull in activity, body, sleep, and workout data: steps, distance, active energy, floors, and VO₂max; weight, body fat, SpO₂, resting heart rate, heart-rate variability, respiratory rate, and wrist temperature; sleep stages; and workouts. The card works like the others — test the connection, sync now, sync everything, and reconnect after a pause. This integration is experimental: Google's health API is young and coverage for some data types is still being verified. - **Rated mood factors.** Alongside the yes/no mood tags you can now score factors on their own scale per entry — work, social, sleep quality, stress, conflict, and family — so a day can carry "stress 4, sleep quality 2" rather than a plain tag. - **Mood logging, rebuilt.** Capturing a mood now opens with a five-face picker, then a category-grouped tag picker and the factor ratings, replacing the old form. - **Hobbies and nutrition mood tags** — films, reading, gaming, music, outdoors, and fast food, no sweets, a big meal. - **How your tags line up with your health metrics.** The mood Insights now compares a health metric on the days you tagged an activity against the days you did not — active energy on workout days, sleep length on the nights you tagged sleep, next-day recovery after a tag like alcohol or food. Each row shows the difference, its confidence, and is framed as an association in your own data, not a cause; only relationships that clear the same statistical filter the rest of the relations surface uses are shown. - **A full-page Coach.** The Coach conversation can expand to its own full page from a maximize control. - **A quicker way to log.** Navigation gains a central capture action that opens a measurement, medication, or mood entry from anywhere, and a "More" hub gathers the less-frequent sections. - **A dashboard settings shortcut** — a wrench beside "Add" jumps straight to the dashboard layout settings. ### Changed - **The Coach reads like a chat.** Replies stream in as they are written, past conversations and the context behind an answer collapse out of the way, and the disclaimer appears once. - **Insights, de-duplicated.** The overview no longer repeats what the per-metric pages already show, and each metric page follows one consistent layout — a primary value, the last reading, the average/min/max/median strip, the chart with its range control below it, the metric's own correlation, and the assessment last — with clearer "what stands out" wording. - **A refined injection-site body map** with corrected proportions and a legend for the recommended and last-used markers. - **Export, reordered.** The health-record (FHIR) export is now the primary export, the doctor-report PDF moves below it, and the included-data list collapses by default. - **The unit system lives in your profile** as a dropdown beside timezone, treated like a preference such as language. - **Google Health and the other wearables share one source-priority ladder**, so when the same metric arrives from several sources the most appropriate one wins per metric. - **The standalone moodLog integration is deprecated.** Mood is now tracked fully inside HealthLog with entries, structured tags, and rated factors, so the external bridge no longer adds anything. It keeps working for existing setups but is slated for removal in a future major release. ### Fixed - **Weekly medications no longer read 0% compliance.** A weekly-cadence medication with recorded intakes now reports the correct rate instead of zero across the 7-, 30-, and 90-day frames. - **Twice-daily oral doses record independently.** Marking an evening dose no longer collapses onto the morning slot, so both land in the history. - The Google Health sync reads from a single point in time per cycle, so a gap after downtime isn't silently skipped, and a connection returned without a refresh token is rejected cleanly rather than saved half-formed. ## [1.11.5] — 2026-06-04 — mood relations, sleep depth, and a clear-the-decks polish pass ### Added - **What lifts your mood, and what your better days have in common.** Insights now learns from your own logs: it compares your average mood on days with a given activity or tag against days without it, surfaces a ranked "what's associated with your better days" board across both your tags and your tracked metrics, and lists the day-to-day relationships that survive a statistical filter. Every result shows its strength and is framed as an association in your own data — a pattern worth watching, not a cause. - **Last night, stage by stage.** The sleep section now draws a hypnogram of the most recent night — the progression through deep, core, REM, and awake — colour-coded by stage, alongside the per-stage breakdown. - **WHOOP: test the connection and reconnect, just like Withings.** The WHOOP card now has a "Test connection" button that checks the link live, and — if the connection was paused after repeated errors — a one-tap reconnect. ### Changed - **Adjust your targets from the header.** The target-range editor now opens from a gear next to the Coach button instead of a link buried in the tile. - **Account settings gather the personal preferences.** The unit system (metric or imperial) and your globally excluded injection sites now live under Account rather than Dashboard. - **Sleep reads as one value per night everywhere.** The measurements list, the per-metric sleep view, and the CSV export now collapse a night's individual stages into the night's total, with naps surfaced separately, matching what the tile and chart already showed. - Full Spanish, French, Italian, and Polish coverage for the remaining Insights, admin, and HealthKit-label copy that previously fell back to English. ### Fixed - **Sleep no longer double-counts an Apple Health night.** When Apple Health writes both an overall "asleep" figure and the deep/core/REM breakdown for the same night, the per-stage view and hypnogram now use one or the other rather than adding them together, and a nap on the same day no longer falls out of the night's total. - **The Insights assessment reads cleaner.** The "cached" tag is gone, "show more" appears only when there is more to show, and the last-updated time is right-aligned. - The "Signal of the day" and "Period at a glance" cards no longer leave a tall band of empty space below their content. - A revoked WHOOP authorization is now recognised as needing reconnection instead of being retried as a transient error, and a WHOOP plan that shares none of a sync cycle's data no longer looks like a successful sync. - The quick-entry sheet now asks before discarding mood ratings and dropdown changes, not only typed text. - Settings: copying an API token confirms the copy, the destructive-action dialog shows progress while it works, and a failed token revoke surfaces an error. ## [1.11.4] — 2026-06-04 — sleep totals, trend captions, and a round of fixes ### Fixed - **Sleep now shows the whole night.** The sleep tile and sleep chart previously showed a single sleep stage's minutes; they now show the night's total time asleep, grouped by sleep session so a night that crosses midnight counts as one night, with the unit shown explicitly. When two sources report the same night, the higher-priority one is used rather than adding them together. - **The daily briefing no longer makes the Insights overview wait.** The overview reads the existing briefing immediately and refreshes it in the background instead of blocking the page on generation. - **Recent achievements no longer flash in and vanish.** Cards that depend on your layout or your data now wait for that to load before appearing, so nothing shows up only to be retracted a moment later. - **Charts with only a day or two of data now show those points** instead of withholding them behind a "more days needed" card. A short series still notes that more days will fill out the trend. - **A reading logged by hand and mirrored to Apple Health no longer appears twice.** The same physical reading arriving from both sources is merged into one. - Long wellness-score labels (for example "Sleep score") no longer clip. - The mood-by-time-of-day chart no longer overflows its card. - The Trends row cards are now equal height and the mood card's axis labels stay inside the card. ### Changed - **The Trends row now describes the actual trend.** When there is no written summary yet, each trend card states the direction and size of the change over the period (for example "rising over 30 days") drawn from your own data, instead of always showing "awaiting more data." - **Settings → Advanced:** the erase-data and delete-account buttons now sit beside their descriptions, matching the rest of the app. ## [1.11.3] — 2026-06-04 — WHOOP body data, quality-of-life polish, fuller translations ### Added - **WHOOP body data.** WHOOP weight and maximum-heart-rate context now flow in alongside recovery, sleep, cycle, and workout data. Weight is ranked below a real scale by your source priority, so a connected scale or manual entry still wins; your profile height is filled in from WHOOP only if you haven't set one yet. - **Stop a Coach reply mid-stream.** A Stop control appears while the assistant is answering, and closing the Coach drawer now ends an in-flight response instead of leaving it running. - **Undo a logged dose.** The confirmation after taking or skipping a dose now offers an Undo, so a mis-tap is one tap to reverse instead of a trip into the history. ### Changed - **More of the app speaks your language.** Spanish, French, Italian, and Polish now cover the Settings, Medications, Insights rhythm-event, and Achievements copy that previously fell back to English. - **A WHOOP plan that doesn't expose every data type no longer breaks the connection.** A data class WHOOP declines to share is skipped on its own while the rest keeps syncing. - The first Coach reply on a cold cache arrives faster — its inputs are now gathered in parallel. ### Fixed - **A failed dose log is no longer silent.** If recording a dose doesn't go through, you now see an error instead of a confirmation for something that didn't happen. - The quick-entry sheets only ask before discarding when you have actually typed something. - A failed export now shows a clear message instead of a raw error code. - Saving feedback on an Insights recommendation surfaces an error if it can't be stored. - Several loading placeholders now reserve their final height, removing the small layout shifts on the Insights and dashboard surfaces as content lands. ### Accessibility - Reduced-motion now suppresses the Insights card entrance animation. - The API-token revoke button carries an accessible name and a larger target, and the medication scheduler's step controls have a larger touch target. ### Notes - WHOOP weight is the value stored in your WHOOP profile, not a scale reading; it is ranked last among weight sources and only surfaces when nothing higher-priority is present. ## [1.11.2] — 2026-06-04 — Coach-memory controls, privacy hardening, more pinnable metrics ### Added - **Review and clear what the Coach remembers.** A new "What the Coach remembers" settings section lists the durable facts the assistant has learned about you, grouped by category, and lets you forget a single one or clear all of them. Encrypted at rest; available wherever the Coach is enabled. - **More dashboard metrics you can pin.** Cardio recovery, six-minute walk distance, stair ascent/descent speed, breathing disturbances, wrist temperature, fall count, and walking steadiness can now be pinned to your Home layout. ### Security - **Outbound request hardening.** The AI provider clients (when pointed at a custom base URL) and the optional IP-geo lookup now reject any address that resolves to a private or internal host, closing a server-side request-forgery / DNS-rebinding gap. ### Fixed - A rare write race when two updates recompute the same day's rollup at once is now handled cleanly instead of surfacing a transient error. - Analytics fast paths resolve your source-priority preference once per request instead of repeatedly. ## [1.11.1] — 2026-06-04 — source-aware vitals + Coach long-term memory Closes the three follow-ups left open by v1.11.0: cross-source vital de-duplication, the Coach's conversation memory, and durable personal facts. ### Added - **Coach long-term memory.** The assistant now keeps a private, encrypted rolling summary of long conversations and remembers durable facts you have told it — standing preferences, goals, constraints, and conditions you have stated about yourself. Both are descriptive recall, never a diagnosis, and ride the same Coach setting; turning the Coach off stops all of it. - **A facts surface to review and clear what the Coach has learned.** List your active facts, forget a single one, or forget everything (`GET` / `DELETE /api/insights/coach/facts`, `DELETE /api/insights/coach/facts/{id}`). Facts are stored encrypted at rest. ### Changed - **Measurement rollups are now source-aware.** When two sources report the same standard vital on the same day (for example a resting heart rate from two devices), charts, summaries, and insights now show the higher-priority source's reading instead of a blend of the two. Cumulative metrics such as steps and energy continue to total per source. Your source-priority settings decide which reading wins. ### Notes - The "latest" value shown for a vital with two competing sources now follows your source priority for the most recent day, so it matches the chart line. For some metrics this may surface a different device's reading than before. - After clearing a Coach fact it can take up to a minute to disappear from the assistant's working context, in line with the existing snapshot refresh window. - On first start after upgrading, each account's rollup cache is rebuilt once in the background; charts fall back to a live read until it converges. ## [1.11.0] — 2026-06-04 — WHOOP, a longitudinal coach, and a clinician-grade record A multi-feature milestone across three fronts: a second connected provider, deeper Insights, and a shareable clinical record. ### Added - **WHOOP integration.** Connect a WHOOP account with your own developer keys to bring its recovery, day- and workout-strain, sleep-performance, HRV (RMSSD), and energy readings into HealthLog alongside Apple Health and Withings — synced on a schedule and via signed webhooks, each value kept distinct by its source so it never overwrites a reading from another device. - **A longitudinal Insights coach.** A weekly/monthly narrative summarises what changed over the period and the likely contributing factors (descriptive, never causal); the Coach now carries a rolling profile of your recent baselines and trends; and a short-horizon trajectory projection shows where a metric is heading with an honest, widening confidence band — shown only when there is enough history to mean something. - **A clinician-grade health record.** A read-only FHIR REST API (`GET /api/fhir/*`) serves your data as standard FHIR R4 resources, and you can mint a scoped, time-limited, revocable share link that opens a clean read-only clinician view — no account needed. Wellness figures stay fenced off from the clinical ones with a plain "not a clinical assessment" note. - **More resilient AI generation.** A durable provider-health record skips a known-bad credential instead of failing every run and surfaces an expired credential proactively, with a local model as a guaranteed fallback. ### Known limitations (planned follow-ups) - When two sources supply the same standard vital (e.g. WHOOP and an Apple Watch both report resting heart rate), both readings may currently appear for a day until a source-aware aggregation update resolves them to your preferred source. WHOOP's own scores are unaffected; a note explains this on the WHOOP card. - The coach reflects your rolling health profile and trajectory; full conversation-summary memory and saved personal facts are a later refinement. ## [1.10.4] — 2026-06-03 — Strain honesty + six-minute-walk caveat ### Changed - **The Strain reading now says which scale it used.** When the score is anchored to your own recent training load it reads "relative to your typical effort"; while there is too little history and it falls back to a general reference, it says so — so the framing always matches the number actually shown. - **The six-minute-walk band notes its reference range.** The reference equation was established for ages 40–80; outside that range the percent-of-predicted is now flagged as an extrapolation. ## [1.10.3] — 2026-06-03 — Personalised strain, a daily signal card, deeper derived metrics ### Added - **A "Today's signal" card.** The coincident-deviation read — which notices when several of your vitals sit off their personal baseline at once — now leads the Insights overview as a calm daily card instead of a flag buried in the grid. It shows an all-clear on a normal day, names the vitals to keep an eye on when a few drift, and always frames them as possible factors, never a cause or a diagnosis. - **Derived bands for more of what your watch measures.** Overnight wrist temperature and stair-climbing and stair-descent pace each get a personal typical-range band, and your device's estimated six-minute-walk distance is placed against a published reference for your age, height, weight and sex. Each is shown with its method and its cited standard, and only once there is enough history. They appear under a new "Mobility & body" group. - **Trailing trend sparklines** on the derived tiles, drawn from the readings the tile already uses. ### Changed - **The Strain score is now anchored to your own training load.** It reads how hard a day was relative to your own recent training-day effort rather than a fixed population figure, so a genuinely hard day reads high even while you are building back up; with too little history it falls back to a general reference and says so. - Age-banded reference norms now interpolate across bracket boundaries instead of stepping, and the sleep midpoint is computed in your own timezone. ## [1.10.2] — 2026-06-03 — Honest AI connection test, consistent insights, retention re-enabled ### Fixed - **The AI connection test now tells you what actually went wrong.** A failed test used to surface a cryptic parse error ("unexpected token") because the server answered with a gateway-level error page the settings screen could not read; worse, the test probed a different provider than the one your nightly insights are generated with, so it could report a problem with a fallback you do not even use. The test now always returns a readable, translated reason — rejected credentials, rate-limited, a provider server error, or unreachable — and it checks the same provider your insights run on, so the result is the truth about your setup. - **Every insight metric page carries the same time-range controls.** The core-metric pages (weight, blood pressure, pulse, BMI, sleep) were missing the period selector that the other metric pages already had; they now match. Drill-down back-links are consistent and a "show all values" link returns you to the metric you came from. - **The derived-metrics dashboard no longer collapses or jumps while loading.** It shows a placeholder row at the right height, keeps its heading hidden until there is something to show, and offers an inline retry if the data fails to load instead of silently vanishing. - **More status colours meet contrast on the light theme** across the medication and admin surfaces. ### Changed - **The intra-day retention drain is back on.** The storage optimisation behind the stress reading was switched off in 1.10.0.2 after a daily-summary collision kept the app cycling post-deploy. The fold now adopts the existing daily row deterministically and recovers cleanly if a concurrent write reaches the same slot first — verified against real data — and the start-up defer that protects the connection pool stays in place. - **The batch-ingest and health-record export endpoints cap request-body size**, rejecting an oversized payload before parsing it. ### Added - **A capability endpoint for API clients.** `GET /api/meta/capabilities` returns the server's live metric, tile, ingest and FHIR vocabularies alongside a contract version, so a client renders what it recognises and ignores what it does not — no more transcribing each release's new identifiers by hand. ## [1.10.1] — 2026-06-03 — Consistent medication cards ### Fixed - **Every medication card looks the same now, whatever the type.** Oral, injection, GLP-1, as-needed, cyclic and one-time medications previously drew their "next / last intake" line, dose accent, spacing and compliance bars slightly differently, so cards in the same row could sit at different heights with the action buttons misaligned. The cards now share one layout: the same intake line, a reserved-height compliance area, and the take/skip buttons pinned to the bottom — so a row of mixed medications lines up cleanly. The dose accent also meets contrast on the light theme. ## [1.10.0.2] — 2026-06-03 — Disable the intra-day retention drain ### Fixed - **Stops the post-deploy restart loop for good.** The intra-day retention drain (a storage optimisation behind the new stress reading) folded older samples into a daily summary, but on real data that summary collided with an existing daily row and the drain failed on every run, which kept the app cycling after a deploy. The drain is now switched off until it is reworked and tested against real data; the nightly pass is skipped, nothing is queued at start-up, and any work already queued completes harmlessly. No user-facing feature is lost — the stress reading simply falls back to its sparse-data behaviour. ## [1.10.0.1] — 2026-06-03 — Defer the intra-day retention drain past startup ### Fixed - **The app no longer turns unresponsive shortly after a deploy or restart.** The new intra-day retention drain ran its catch-up pass the moment the background worker started, at the same time as the database migration, the other start-up backfills, and the first health checks — all sharing one connection pool. On an instance with a lot of history this exhausted the pool, so the app stopped answering until it restarted, then repeated. The catch-up now waits until start-up has settled before it runs; it is unchanged otherwise and the nightly pass is unaffected. ## [1.10.0] — 2026-06-03 — Derived wellness metrics, device-event awareness, deeper ingestion ### Added - **Derived wellness metrics, each shown with how it is worked out.** From data you already sync, the Insights surface now derives a personal typical range for a vital, a cardio-fitness band, a vascular-age delta, an HRV (SDNN) balance read, body-mass index, a sleep score, a readiness index, and a coincident-deviation flag that notices when several vitals sit off their personal range at once. Every metric states its inputs, its method in plain language, and the published standard it is grounded in, with an honest read of how much data backs it — never a bare number. A metric appears only when there is enough history to mean something. - **Recovery, Stress, and Strain scores, computed each night.** Recovery blends your resting-heart-rate, HRV, sleep and respiratory trends against your own baseline; Strain reads the heart-rate load of your workouts; Stress is an HRV-derived proxy and is labelled as exactly that — not a sensor-measured stress reading. Each is stored as a first-class value you can chart, and each opens to a breakdown of its contributors and its cited basis. - **More of what your watch already measures.** Cardio recovery, overnight wrist temperature, fall count, six-minute-walk distance, stair ascent and descent speed, and the sleep breathing-disturbance index are now ingested and surfaced. - **An awareness timeline for device-flagged events.** Irregular-rhythm, high- and low-heart-rate, walking-steadiness, and breathing-disturbance notifications your device produced are shown as a timeline — strictly as awareness of your own device's certified result, never re-assessed and never a HealthLog diagnosis, with a permanent disclaimer to that effect. The section stays hidden until there is something to show. - **A per-workout heart-rate series**, so an indoor workout with no GPS still yields a training-load signal. - **A wellness summary in the doctor report.** The derived scores and metrics appear in their own section, kept out of the clinical vitals table, labelled descriptive rather than clinical, and exported to FHIR as `survey`-category observations with the same note — so a clinician's system never mistakes them for a vital sign or a diagnosis. ### Changed - **The health-record export carries richer medication codings.** A `MedicationAdministration` now adds a SNOMED route and injection-site coding alongside the existing plain text, a German-locale export can append the German BfArM ATC coding next to the unchanged WHO ATC entry, and the number of administrations an export can carry is raised and made configurable by the operator. - **The Insights overview reads its metrics through a single request** instead of one per tile, so opening the page no longer fans out into a burst of concurrent reads. - **Score-band colours meet contrast on the light theme**, and the device-notification copy — including its medical disclaimer — is translated across every supported language. ## [1.9.3] — 2026-06-02 — Medication card layout polish ### Fixed - **The medication cards look right again.** A recent alignment change pushed each card's action buttons to the very bottom of the cell, which left an empty gap between a shorter card's content and its buttons when it sat next to a taller card in the grid. The buttons now sit directly under the card's content again; the cards still share a height and the dose rows still line up, without the gap. ## [1.9.2] — 2026-06-02 — Document the medication compliance endpoint ### Changed - **The medication compliance endpoint is now in the public API contract.** `GET /api/medications/{id}/compliance` was missing from the OpenAPI document, so a client generating its types from the contract could mistake the separate cadence endpoint (a different shape) for it. The route is now documented with its exact response — the 7- and 30-day adherence summaries, the per-day compliance grid, and the two-row display block — with the field a client should read for the headline 30-day rate and how to draw the per-day history. No behaviour change; the handler was already correct. ## [1.9.1] — 2026-06-02 — No assessment warm on a page visit ### Fixed - **Opening the Insights overview no longer regenerates every assessment.** A returning visit fired a one-per-session background warm that asked the provider to regenerate the comprehensive briefing and every category and metric assessment at once — a burst that could run for minutes and make the whole app feel unresponsive while it ran, recovering on its own once it finished. Assessments are kept fresh by the nightly pass, and each metric's text refreshes gently on its own when you open it; the "prepare assessments" button still regenerates everything on demand. A page visit now only reads the cached text. ## [1.9.0] — 2026-06-02 — Insights time-ranges, deeper mood, medication coding, stability ### Added - **A time range you can choose on the Insights pages.** Each metric now carries range pills (week, month, quarter, year) and shows how the period compares with the one before it — the change in your average, stated plainly with its direction — so a trend reads at a glance instead of by eye. - **Deeper mood insights.** The mood page now surfaces your time-of-day pattern (when your mood tends to sit highest and lowest), a stability read, and correlation cards that line mood up against weight and blood pressure — each shown only when there are enough paired days to mean something. - **Standard drug codes on a medication.** A medication can carry an ATC and an RxNorm code (entered, never guessed). These flow into the health-record export: the exported `MedicationStatement` now codes the drug with the WHO ATC system and RxNorm alongside the plain name, and the export adds a `MedicationAdministration` for every dose you actually took or skipped, so a clinician's system can read the record without relying on free text. - **A deploy-verification script** (`scripts/assert-deploy.ts`) that checks a target reports the expected version after a release. ### Changed - **The medication advanced-settings page is rebuilt.** Import, intake-import and export sit in their own group, the external-API endpoints are listed one by one with collapsible request examples, and the layout is tidied so the technical settings read clearly instead of crowding together. - **Targets are edited inline where they belong.** The separate `/targets` page is retired — you set a metric's target range from the metric itself, with no detour to a standalone editor. - **The health-record export emits insurer coverage from a bare insurance number**, not only when a full insurer organisation is present, and carries a top-level narrative summary. - **Notification settings wording** is tightened for clarity. ### Fixed - **The app stays responsive under load.** The database connection pool was sized far too small by default and could starve while a background insight warm and a foreground request competed, which showed up as the occasional "server not responding" that recovered on its own; the pool is now sized for real concurrency and tunable for larger self-hosts. - **A background insight warm no longer blocks a page**, and a burst of Apple Health / Withings sync no longer triggers a storm of regenerations — the warm is decoupled and the invalidation is debounced. - **The medication card rows stay aligned.** "Last dose" and "next dose" could break onto different heights and look misaligned; the rows now hold an equal height. - **Glucose entered in mmol/L converts correctly** in the editor instead of being stored against the wrong unit. - **The admin and global layout no longer shifts** when a scrollbar appears, by reserving the gutter. - **The legacy status endpoints are documented** — six older `*-status` routes were missing from the API contract and are now included. ## [1.8.7.1] — 2026-06-02 — Assessments for every HealthKit metric, one-tap pre-generation ### Added - **A plain-language assessment on every HealthKit metric page.** The HealthKit metric pages (resting heart rate, heart-rate variability, blood oxygen, glucose, body composition, gait, audio exposure, sleep, and the rest) showed charts but no written assessment, because the core metrics' bespoke per-metric prompts don't scale to thirty more. Each page now carries an assessment, generated from the metric's own normal range and the direction a healthy value moves, anchored on your individual baseline — the same treatment the core metrics already had. A metric with no data keeps its existing insufficient-data note. - **One-tap pre-generation of every assessment.** A button on the Insights overview generates all assessments in the background so they are ready immediately the next time you open them. The overview also warms a cold cache on its own, and the nightly pass keeps everything fresh — so a page opens to its assessment instead of a wait. ### Changed - **"Auswertungen" is now "KI-Auswertungen" / "AI Insights"** in the settings heading and the menu, so it's clear the section is AI-generated. - **Targets and Sources are two separate settings pages** instead of one combined page, each with its own heading. - **The back link sits above the heading** on a metric's detail page. - **Dropdown fields have symmetric padding**, so the chevron no longer crowds the right edge. - **The Insights layout accepts the full metric set**, so a saved layout (order and visibility) can include every metric page, not just the core ones. ### Fixed - **Assessments generate again.** The assessment provider's accepted model identifiers had rotated, so generation failed and an assessment could sit on "preparing" indefinitely; the model ladder is updated and falls through to the current identifiers. - **The Active Energy assessment loads.** Its page requested an assessment under a mismatched identifier and silently fell back to "no analysis yet"; the identifier is corrected and now type-checked, so the same class of mismatch is caught at build time. - **The source-priority editor is fully translated** for Spanish, French, Italian and Polish — it was English-only after the settings split. ## [1.8.7] — 2026-06-02 — Instant insight assessments, GLP-1 blood-level chart, tile + caption polish ### Fixed - **Insight assessments are instant again.** A category's assessment was being re-prepared on almost every visit: the constant Apple Health / Withings sync kept evicting the cached text, and a miss only ever deleted-and-waited. Assessments now serve the last good text immediately (stale-while-revalidate) and refresh in the background — "preparing" shows only when an assessment has genuinely never been generated. The background refresh is also debounced per metric, so a sync burst no longer triggers a storm of regenerations, and it warms only the active language (halving the work). - **The dashboard tiles never truncate the value.** In a dense tile strip the number could clip or wrap mid-value (e.g. "130 mmHg" showing as "13"); the value now always renders in full and the unit yields the space instead. - **GLP-1 medications show the drug-level-in-blood estimate by default.** The estimated concentration curve was hidden behind the Research-Mode opt-in and below the dose-strength curve; for GLP-1 medications it now shows by default, above the dose-strength curve, with the "educational estimate, not a measurement" disclaimer kept clearly attached. - **The Trends captions read consistently.** Metrics without an advisor note previously rendered their caption in a different, plainer style that broke the row; every Trends caption now uses the same treatment. ### Changed - **The mood assessment sits directly under the first chart** on the mood insights page. ## [1.8.6] — 2026-06-01 — Compliance two-row return, insights polish, targets deprecation ### Added - **Mood narrative takeaways above the charts** — a ranked feed of plain-language findings (the weekday your mood dips, the overall trend, the tags that lift or lower it, your in-target share, your logging streak, the weekend effect), now drawing on the structured tags as well as free-text ones. - **A GLP-1 dose-strength curve** that plots the titration history (2.5 → 5 → 7.5 mg …) in place of the previous empty therapy-course block. - **A FHIR `Coverage` resource in the health-record export**, with an optional insurer institution number (IKNR) captured on the profile — making the insurer machine-resolvable for German systems. - **An optional `source` on the measurements batch endpoint**, so a client backfilling manually-entered history can tag it correctly instead of defaulting to Apple Health. ### Changed - **Medication compliance returns to two rows for every medication.** The cadence-aware timeline is gone; the two windows now scale to how often a medication is due — a daily medication keeps 7-day and 30-day, a rarely-taken one steps up to longer windows (up to twelve months) so both rows stay meaningful and the layout stays steady. - **The medication advanced settings are a single, calmer column** with a symmetric danger zone (matched delete-history / delete-medication actions), replacing the cramped two-column layout. - **The medication wizard uses a jumpable dot stepper** with a "next step" hint; in edit mode every step is reachable in one tap. - **Insights category headers are quieter.** The question-mark circle is gone (the metric definition stays inline beneath the heading), the measurement-diversity hint moved to a heading lightbulb with the explanation on hover, the assessment border is de-emphasised, and "Coach fragen" moved to the header as an icon. - **Target ranges are now edited inline from Insights.** The Targets page is deprecated — it carries a notice, is removed from the menu, and will be retired in a future release; editing happens where you read the trend. ### Fixed - **Every briefing-driven Trend card now shows its caption** — additive metrics no longer render a chart with empty space below it. - **A long mood note no longer stretches the entries table** — it truncates with the full text on hover. - **The mood in-target share is shown once**, not duplicated between the headline tile and the takeaway feed. - **Medication compliance no longer counts days before a medication existed**, so a newly-added medication's expected-dose figures reflect its real age. ## [1.8.5] — 2026-06-01 — Insights reference panels, mood depth, injection tracking ### Added - **A target reference panel on every Insights category page.** Each metric now shows the target band, where the latest reading sits inside it, a plain-language status, the guideline source behind the range, the 30-day average and a seven-day consistency strip — the same reference the Targets page carries, now on the page where you read the trend. Blood glucose surfaces one panel per context (fasting, post-meal, random, bedtime). - **A statistics strip per metric** — minimum, maximum, median and mean — plus a dedicated page that lists every underlying reading with edit and delete, reachable from each Insights category. - **Injection-site tracking and rotation.** Opt-in per medication (injections only): record the site after a dose, see the history on a body map, and get a rotation suggestion. Sites can be restricted per medication, and an account-level exclusion always wins. - **Structured mood tags and a per-entry note**, with a notes timeline so the context behind a mood is kept alongside the score. - **A richer mood page** — calendar heatmap, score distribution, weekday pattern, and correlations with sleep and activity. - **A measurement-diversity nudge** that suggests logging at other times or days when readings cluster, for a fuller picture. ### Changed - **The medication compliance display adapts to how often a dose is due** — a percentage for frequent schedules, a dose-by-dose adherence timeline for sparse ones, so an every-few-weeks medication reads sensibly. - **The Trends section now follows the daily briefing**, charting the metrics the briefing actually flags rather than a fixed set. - **Tightened the Insights category pages and rebalanced the overview** for a denser, calmer layout. - **Gait and mobility metrics from Apple Health are consolidated to a daily mean overnight**, while discrete clinical readings (weight, blood pressure, glucose, mood, pulse) stay raw. - **The health-record FHIR export codes HealthKit-only metrics under a dedicated code system** instead of the LOINC namespace, for FHIR R4 conformance and alignment with the native client. ### Fixed - **A rolling-interval medication now surfaces its first dose at the start date** — including when that date is already in the past — so the dashboard and reminders no longer skip the opening dose. - **The medication wizard no longer shows each time-of-day suggestion twice.** - **Deleting a medication no longer hangs on a spinner.** - **Mood entries with tags save atomically** — the entry and its tags commit together, and create and update return the saved tags immediately rather than after a refresh. - **Accessibility:** the injection-site picker has a visible focus state and screen-reader labels; mood tag chips and the status pill meet the contrast and minimum tap-target guidelines. ## [1.8.4] — 2026-06-01 — Medication card accuracy + Insights polish ### Fixed - **The medication card shows the correct next intake for every schedule.** Flexible "interval after the last dose" and one-time medications previously showed a next-intake derived from a simplified day-of-week rule that ignored the interval and the last logged dose — so a medication taken every few weeks proposed tomorrow and never moved after a dose was logged. Both the standard and the injection card now read the schedule engine's computed next-due, so the next intake is right on creation and advances the moment a dose is recorded. - **Insights status polling is bounded.** When a model is configured but generation stalls, the per-category assessment card no longer polls indefinitely on an open page — it stops after a fixed ceiling and shows its static state. - **Clean deploys no longer hit a missing settings column.** Four `app_settings` columns existed in the schema but had only ever been applied ad-hoc, so a migrations-only deploy could fail on first read; an idempotent migration now adds them (a no-op on instances that already have them). ### Changed - **Logging a dose confirms itself.** Marking a medication taken or skipped now shows a brief confirmation, and the card's next-intake updates immediately. - **Each Insights category explains itself inline.** The short metric definition now also appears as a caption under the category heading, alongside the existing question-mark tooltip. - **Medication history shows where each dose was logged** — web, the iOS app, a reminder, or an import — as a small origin label on each entry. ## [1.8.3] — 2026-06-01 — Insights no longer block the interface Opening an Insights category and switching between them could freeze the whole interface for several seconds while the per-category assessment was generated. The interface now stays responsive throughout. ### Fixed - **Switching Insights categories no longer freezes the interface.** Each category's short assessment is now served from cache as an immediate, read-only response; when it is not yet ready the page shows a brief preparing state and the assessment is generated in the background, so navigation and taps stay responsive. The client request is capped, so a slow or unreachable model can never block the interface — the worst case is a preparing state, never a frozen screen. - **The overnight pre-generation warms the assessment actually shown.** The nightly warm pass keyed its cache differently from what the page requests, so a returning visit often fell back to an on-demand generation; it now warms the languages the interface uses and runs even when the daily summary was already cached, so a returning visit lands on a ready assessment. - **A stalled model no longer re-blocks every visit.** A generation timeout is briefly remembered so the next visits return immediately instead of retrying the slow path each time. - **Streamed loading states.** Insights and the dashboard now render a skeleton while their first segment loads, rather than waiting on a blank transition. ## [1.8.2] — 2026-06-01 — One intake row per dose slot A multi-time medication could end up with two entries for the same dose — one still open, one marked taken — for a dose the user had not taken. This release guarantees a single entry per scheduled dose. ### Fixed - **A dose slot can no longer hold a duplicate entry.** Logging a dose now updates the slot's existing entry in place instead of inserting a second row, regardless of where the log came from, and the logged time is aligned to the scheduled dose rather than the exact moment of the tap. A twice-daily medication no longer shows the same time twice (once open, once taken), the daily count is no longer inflated, and the "due now" prompt is no longer suppressed for a dose that has not actually been taken. - **A recorded dose is never silently un-recorded.** A background re-sync that replays an open dose onto a slot already marked taken is now ignored rather than clearing the taken mark, so an offline sync can never flip a taken dose back to missed. - **Concurrent logs converge.** Two logs landing on the same dose at once resolve to one entry instead of failing or duplicating. - **Existing duplicates are cleaned up on startup.** A one-time pass collapses any duplicate dose entries already stored, keeping the recorded dose and correcting the affected compliance figures. ## [1.8.1] — 2026-06-01 — Multi-time medication compliance A follow-up to the multi-time compliance work in 1.7.3, closing the two surfaces it did not reach. ### Fixed - **Every scheduled time of a multi-time medication is now loggable** — a medication taken more than once a day on a single schedule with several times (e.g. 07:00 and 19:00) surfaced only its first dose in the today tile and the logging sheet, so the second dose could never be recorded and the day's compliance was counted against a single expected dose. The today-window projection now creates one pending dose per scheduled time, matching how reminders are issued. - **The compliance heatmap classifies each dose against its own time** — a twice-daily dose logged in the evening was measured against the morning time and read as very late, painting the day amber even when both doses were taken on time. Each logged dose is now matched to its nearest scheduled time, so a day with every dose taken on time reads as on time. Single-time schedules are unaffected. ## [1.8.0] — 2026-05-31 — Insights: reliable data-driven assessments, embedded targets, explainers The per-metric Insights pages become the coherent heart of the app. The data-driven assessment — a short, honest reading of your own trend — now reliably reaches the page: it previously timed out and cached a generic placeholder for the rest of the day, so most users only ever saw the platitude. The generation budget is aligned with the provider, the placeholder is never cached, and the AI payload is compressed into a graded shape (recent days raw, then weekly, monthly, and yearly aggregates drawn from the rollup tier) so a heavy logger no longer ships thousands of raw points. Each metric's prompt is rewritten to a consistent house style — name the finding, place it against your own baseline, offer one concrete step; honest on poor values, never alarmist, never diagnostic. Target ranges move onto each category page beside the chart, every category gains a plain-language "what is this?" tooltip, and the assessment is warmed overnight so the page is a cache read instead of a live call. Route slugs and tile ids move to English (German tile ids stay accepted as aliases, so existing clients keep working). ### Added - **Per-category target ranges** — each Insights category page shows its numeric target range and the share of recent days within target, beside the chart and the assessment, with a link to adjust the range. The dedicated targets page remains the editing surface. - **Per-category explainer tooltips** — a question-mark affordance next to every category heading opens a short, plain-language definition of the metric, available offline in all six languages and reachable by keyboard and touch. - **Nightly assessment pre-generation** — a budget-gated overnight job warms the per-metric assessment caches (and invalidates them when fresh measurements arrive), so opening a category page is a cache read rather than a synchronous model call. - **Graded measurement compression for assessments** — a shared compressor folds a metric's history into recent (daily), weekly, monthly, and yearly buckets, drawing the monthly/yearly tiers from the rollup store, so the model receives a compact picture instead of the full daily series. - **Naming decision record** — `docs/adr/0001-insights-naming-convention.md` documents the convention: localised UI, English internal keys and route slugs. ### Changed - **Data-driven assessments reach the page reliably** — the per-metric assessment generation budget is aligned with the provider (was capped well below the provider floor), runs through the provider chain with fallback, and a transient timeout or error is treated as a passing miss rather than cached as a placeholder for the rest of the day. - **Assessment wording** — every metric prompt follows one house style (name → place against own baseline → one actionable step; 2–4 sentences; honest, autonomy-supporting, non-diagnostic; says so plainly when the data is too thin). Blood pressure reads systolic and diastolic together; weight tracks the trend while BMI speaks to threshold bands; the mood assessment carries a calm support cue on a sustained low. - **Insights route slugs are English** — `/insights/` moves to `/insights/` with permanent redirects from the old paths. The dashboard-layout tile ids are English canonically; the previous German ids stay accepted as aliases and are normalised on read and write, so a client that stored the old ids keeps working. - **Per-metric assessment caches drop on measurement changes** — a fresh, edited, or deleted measurement invalidates the affected category's cached assessment so it regenerates rather than serving a stale reading. ## [1.7.3] — 2026-05-31 — Health-record PDF fixes, multi-time compliance Two fixes to live behaviour. ### Fixed - **Health-record export renders in the account language** — the PDF came out in English regardless of the in-app language because the export only consulted the browser's `Accept-Language`. The client now sends the active language and the server falls back to the language cookie before the browser header. - **Health-record export layout** — charts now label their time axis (start and end date of the plotted window), modules are kept together instead of breaking across a page, a consistent bottom margin is held on every page, and a glyph-encoding issue that stretched a few lines (trend arrows and the `m²` unit) is resolved by sanitising to the font's supported characters. - **Compliance counts every scheduled time on multi-time plans** — a medication taken twice a day on a weekday plan with two times but no recurrence rule reported 50% even when both doses were logged: the expected-dose count and the taken count expanded the schedule through two different code paths. Both now run through the one canonical recurrence engine, so the rate is correct for every schedule shape. ## [1.7.2] — 2026-05-31 — Coach source parity, first-paint snapshot, medication card cleanup A focused follow-up to v1.7.0. The unified dashboard snapshot that shipped in v1.7.0 is now the default, so a cold dashboard paints its above-the-fold tiles together instead of letting the mood tile arrive ahead of the rest. The Coach's in-chat data-source rail now reads and writes the same persisted data clusters as the settings sheet — one source of truth that survives a reload, so what the panel shows always matches what reaches the model. The medication overview cards collapse their action row into a single overflow menu, and the detail page is pared back to the intake history under a read-only plan summary. The advanced-settings sheet widens into a two-column layout and offers a medications export beside the existing import. ### Changed - **Unified dashboard snapshot on by default** — the above-the-fold tiles read from the single `/api/dashboard/snapshot` round-trip introduced in v1.7.0, so they share one completion moment instead of staggering in as independent queries resolve. The build-time toggle `NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false` falls back to the per-tile path if an operator needs it. - **Coach data-source rail matches the settings sheet** — the in-chat rail now toggles the same persisted data clusters as the gear-icon settings, stored per user rather than reset when the panel closes. The chat request derives its scope from those saved clusters, so the rail always reflects exactly what is sent to the model, and the analysis window persists alongside. - **Medication card actions in one menu** — the overview cards, standard and GLP-1 alike, move their edit / history / advanced actions into a single overflow menu; the card itself opens the detail page. - **Medication detail page is history-first** — the redundant header action row and the schedule block are gone; the page leads with the intake history under a read-only summary (name, dose, status, plain-language cadence), and editing happens from the card menu. The estimated active-ingredient curve stays an opt-in disclosure. - **Advanced-settings sheet widened** — a two-column layout (Data, Reminders, Lifecycle, with the danger zone full-width) so the sheet no longer scrolls on a desktop viewport, and a CSV export of that medication (its row plus intake history) sits beside the per-medication intake import. ### Fixed - **Medication cards render identically** — the day-streak flame used two different oranges between the standard and GLP-1 cards; both now share one token. The repeated card sections are a single set of shared components, so the two variants stay in lockstep instead of drifting on each edit. - **Intake import labelled honestly** — the import affordance described CSV/JSON but accepts a JSON intake array; the copy now says JSON. ## [1.7.1] — 2026-05-31 — Overview-card actions The v1.7.0 medication detail page gained edit / history / advanced-settings actions in its header, but the medication cards in the overview list kept only the edit pencil. This patch brings the **history** (intake history) and **advanced-settings** actions onto the overview cards — both the standard and the GLP-1 variant — so the card and the detail page offer the same three actions. The advanced-settings sheet is mounted once on the list and reused; the history button routes to the same intake-history view. ## [1.7.0] — 2026-05-31 — Health-record export, flexible schedules, first-paint snapshot, full HealthKit coverage A broad polishing release across the medication, dashboard, export, and HealthKit surfaces. Medications gain PRN (as-needed) and cyclic on/off-week schedules, a server-computed next-due instant, and cadence-canonical compliance so non-daily plans report adherence correctly everywhere — including the detail page, which previously still expanded through the legacy weekday walker. The dashboard now assembles its above-the-fold tiles in a single snapshot round-trip and pre-generates the daily briefing overnight, so a cold load no longer staggers tile-by-tile and never blocks on the model. The doctor handover grows into a selectable health-record export — an enriched PDF and a machine-readable HL7 FHIR R4 bundle, packaged together. Every previously-unplotted HealthKit metric gets a chart, walking speed reads in km/h, and the Coach can be fed any chosen cluster of data instead of a fixed five. The medication detail page restores its history view and gathers every setting into one redesigned advanced sheet. ### Added - **PRN and cyclic schedules** — a medication schedule can be marked as-needed (`asNeeded`), excluded from the expected-dose count and reminder projection while staying loggable, or cyclic (`cycleWeeksOn` / `cycleWeeksOff` / `cycleAnchor`) for on/off-week dosing. Both are projected by the canonical recurrence engine. Migration `0092` adds the schedule-type enum and cyclic columns. - **Server-computed next-due instant** — every medication carries a read-only `nextDueAt` (RFC-5545 + rolling math, `null` for PRN), so a client never has to duplicate the recurrence engine. - **Schedule-aware compliance payload** — the per-day compliance bucket gains `due` / `expectedCount`, so a history view can skip empty marks on non-due days for weekly, bi-weekly, rolling, and cyclic plans. - **Per-medication reminder flags** — `liveActivityEnabled` and `criticalAlarmEnabled` (both default off) ride the medication contract for clients that surface lock-screen or break-through reminders. A roaming user-level delivery default plus a per-device override let a device honour or locally override the default. Migration `0091`. - **Health-record export** — a new `POST /api/export/health-record` produces a selectable export: an enriched clinical PDF (patient identity, deterministic summary, native sparklines), an HL7 FHIR R4 document bundle (LOINC-coded `Observation`s, a BP panel, `MedicationStatement`s, a `DiagnosticReport`), or both packaged as one zip. The selection chooses date range and per-domain sections, and the AI summary is an explicit opt-in section clearly marked as not clinically validated. The bundle reuses the same aggregator the PDF consumes, so both describe identical numbers, and matches the iOS client's LOINC/UCUM conventions for interchangeable records. - **Patient identity on the profile** — optional full name, insurer, and insurance number on Account, for the report cover and the FHIR `Patient`. The insurance number is validated (KVNR mod-10) and encrypted at rest. - **Unified dashboard snapshot** — `GET /api/dashboard/snapshot` assembles the above-the-fold tile data in one round-trip from the rollup tier plus the cached briefing, so the tiles arrive together. A coverage miss returns the heavy slice as deferred rather than blocking the strip. The snapshot also carries a `metricStates` seed (latest value/timestamp/unit per metric) and the full widget-layout catalogue, so a native client can paint a cold launch from one cached request. - **Nightly insight pre-generation** — a budget-gated pg-boss job warms the comprehensive insight (and its daily briefing) overnight, so the `/insights` mount is a cache read instead of a synchronous model call. A cache miss now shows a preparing state rather than blocking the request. - **Charts for every stored HealthKit metric** — the previously-unplotted types (flights climbed, environmental and headphone audio exposure, walking speed, step length, asymmetry, double support, respiratory rate, body-composition family, mobility, daylight, and more) each gain a chart surface. Walking speed displays in km/h and distance in km via a render-time transform; stored and exported values stay canonical SI. - **Display-unit preference** — a metric/imperial toggle in Settings → Display (migration `0094`), read by the chart display transforms. - **Coach data clustering** — the Coach accepts a chosen set of data clusters (cardiovascular, body composition, activity, workouts, sleep, mood, glucose, medication, mobility, environment) instead of a fixed set, with a soft budget cap that degrades the lowest-signal clusters first so enabling everything stays within the prompt budget. The default reproduces the prior domains. - **Offline sync delta feed** — `GET /api/sync/changes` exposes a paginated, opaque-cursor delta feed with tombstones across three domains — measurements, mood entries, and medication intakes — over a single multi-domain cursor; `/api/sync/state` is now part of the documented API and reports the sync window. Measurement, mood, and intake delete routes soft-delete (tombstone) rather than hard-delete, pruned past a retention window keyed to the refresh-token lifetime. A stable `errorCode` on the refresh endpoint distinguishes a revoked family, a token reuse, and a transient failure. Migrations `0096` + `0097` add the keyset indexes and the mood/intake sync columns. - **Configurable mood-reminder time** — the evening mood-reminder hour is now per-user (it was a fixed 22:00), stored in the roaming notification preferences so a native client and the web share one source of truth. ### Changed - **Cadence-canonical compliance everywhere** — `calculateCompliance` and the medication detail page route expected-dose computation through the canonical recurrence engine, so `rrule`, `rollingIntervalDays`, weekday subsets, interval weeks, one-shot, and PRN all count correctly. The dashboard tile and the detail page no longer disagree for non-daily plans. - **Medication detail surface** — the header offers a direct edit, a history view (the clock-with-counterclockwise-arrow icon), and an advanced-settings button. The history view is intake-only, sorted newest-first, with the estimated active-ingredient curve as an opt-in disclosure. The advanced sheet is widened and regrouped into Data, Reminders, Lifecycle, and a danger zone, with CSV/JSON import surfaced as a real action. - **Dashboard widget layout round-trips the full catalogue** — saving a layout no longer rejects ids the web surface doesn't render; the full widget catalogue (web tiles + client-only tiles) is persisted and returned intact, so a native client keeps its own tiles without a local merge step. A genuinely unknown id is still ignored. - **Keyboard navigation for segmented controls** — the metric/imperial, export-format, and mood-rating radiogroups gain roving-tabindex arrow-key navigation (Home/End, wrap), via one shared hook. - **Dashboard charts refresh on an open page** — the snapshot and analytics queries poll on a shared interval so an open dashboard picks up new measurements without a manual reload, served from a warm per-user cache. - **Profile photo preserves its aspect ratio** — the avatar renders with `object-cover` instead of stretching. ### Fixed - **The mood-reminder card now delivers on its own** — enabling the reminder previously still required a buried, default-off per-event push preference, so a user who flipped the visible card received nothing; the card alone now drives delivery (an explicit per-event opt-out still suppresses). - **Multi-time-of-day reminders no longer re-fire** — the reminder ledger delete is scoped to the single dispatched slot rather than wiping every row for a medication, and logged doses are matched to slots by time rather than array order. - **Soft-deleted measurements stay deleted on direct access** — the single-resource GET and PUT now filter tombstoned rows. - **Mobile settings navigation no longer auto-scrolls the page** — the section strip positions its active chip horizontally without nudging the document, removing the dizzy scroll on sub-page navigation. ### Tests - 5628 → 5878 unit; 1 skipped. New coverage spans the recurrence-engine PRN/cyclic cases, cadence-canonical compliance parity per cadence type, the FHIR bundle shape and LOINC/UCUM mapping, the KVNR validator, the export route, the dashboard snapshot envelope and pre-generation cron, the measurement delta feed and soft-delete tombstones, the daily-mean consolidation drain, and the display-unit transforms. A six-reviewer audit (correctness, security, architecture, design/a11y, simplification, i18n) ran before release; every High and Medium finding was resolved. ## [1.6.0] — 2026-05-30 — Medication editor overhaul, route of administration, today-tile read-flip v1.5.6 settled the medication detail page into a pure history view, but the create/edit plan still funnelled every dose through the same flat shape, the injection-site picker only surfaced for GLP-1, and the today-tile on the dashboard and the Erfassen sheet still expanded daily schedules through a legacy walker that silently skipped bi-weekly, rolling, RRULE, and one-time cadences. This release reworks the medication editor and its detail surface, gives route of administration a first-class column so any injection can carry a site, adds a one-time-injection shape, and flips the today-tile onto the canonical recurrence engine the reminder worker already uses — so what the tile shows matches what the worker mints. It also restores the multi-language request snippets on the API-tokens row, adds a profile-photo upload to Account, and finishes an admin-surface polish pass. ### Added - **Route of administration** — a new `deliveryForm` column (`ORAL` | `INJECTION` | `OTHER`) decoupled from `treatmentClass`. The injection-site picker now surfaces for any `INJECTION` dose rather than only GLP-1, and the editor carries the route through create, update, and the detail snapshot. Migration `0088` adds the enum and backfills `ORAL` onto every existing row via a constant column default (a single non-blocking metadata operation). - **One-time injection** — a one-off dose modelled as `oneShot = true` + `deliveryForm = INJECTION`, with its own editor path that drops the recurring-schedule step. - **Profile-photo upload on Account** — Settings → Account gains an avatar card backed by the v1.5.5 upload endpoint (server-side magic-byte sniff, 2 MiB stream-level cap, 2048² dimension cap, per-user rate limit, owner-scoped read). - **Admin global mood-log toggle** — the admin Services section can suspend mood-log reminders site-wide alongside the existing Web-Push and API toggles. - **Host-metrics memory detail** — the admin host-metrics memory tooltip now reads `used / total GiB` alongside the percentage. ### Changed - **Medication editor + detail surface overhaul** — the editor uses the available desktop width while keeping the mobile sheet, restores the edit fields the modal-wizard rework had dropped, and aligns the detail surface with the new route-of-administration and one-time shapes. - **Measurement note cap raised 25 → 200 characters** — a 25-character note could not hold a meaningful clinical aside; the cap lifts to 200 through a single `MEASUREMENT_NOTES_MAX_LENGTH` constant the client char-counters import. The DB column was already unbounded. - **Dashboard range colors route through semantic tokens** — the dashboard date-range chips read the shared `success` / `warning` / `info` / `destructive` tokens (with their light-mode contrast overrides) instead of hard-coded hues, and the range inputs gain bound-clamping. - **Admin surface tidy** — every sidebar section gets a distinct icon, the shared `helpfulRateColour` helper and a `usePublicVersion` hook are consolidated into `_shared`, and the orphaned `status-overview` route is removed. - **API-tokens row restores multi-language request snippets** — the per-token row again offers copyable cURL / JavaScript / Python examples. ### Fixed - **Today-tile diverged from the reminder worker for non-daily cadences** — `/api/medications/intake?scope=today` and `/api/dashboard/summary` projected daily schedules through a legacy walker that read only `daysOfWeek` + `windowStart`, silently skipping `intervalWeeks > 1`, rolling, RRULE, and one-time cadences. Both routes now gate every "does this schedule emit today?" decision through the canonical recurrence engine (the same path the worker uses), anchoring the projected instant to `windowStart` so it stays byte-identical to the worker's row and dedupes against the existing unique index. The rolling-cadence baseline fetch mirrors the worker's per-medication `takenAt` query, so projector and worker resolve the same next-due instant. ### Tests - 5635 → 5628 unit; 1 skipped. The today-tile read-flip carries new coverage on the intake and dashboard-summary projections; the orphaned status-overview route and its test are gone. ## [1.5.6] — 2026-05-29 — Pure-history detail page, legacy step consolidation, egress finish v1.5.5 gave every retired medication feature a home on the detail page, but the page still carried the create/edit affordances that belong on the list. This release turns `/medications/[id]` into a pure history surface, collapses every setting into one advanced sheet, consolidates the pre-v1.5.0 granular step rows that bloated the per-sample read path, and finishes the outbound-fetch migration the `safeFetch` wrapper started. ### Added - **Legacy step consolidation** — a pg-boss job (`STEP_CONSOLIDATION_QUEUE`) folds pre-v1.5.0 granular `ACTIVITY_STEPS` rows into one canonical daily-total row per user per day, anchored to the user's timezone, and soft-deletes the originals. A day that already carries a post-v1.5.0 `stats:` total is left untouched (no double-count) while its legacy rows are still tombstoned. Boot-time discovery enqueues only users still holding live legacy rows, so the pass converges to zero work and is idempotent across reboots. Migration `0087` adds a partial index scoped to live step rows to keep the discovery scan cheap. - **`healthlog/safe-fetch-required` ESLint rule** — bans raw `fetch(` outside the wrapper, exempting only genuine same-origin relative paths. Positioned as the author-time egress floor. ### Changed - **`/medications/[id]` is now a pure history view** — the Today's-dose card stays on the list page; the detail page reads the medication's past intake, cadence summary, and trends only. Every setting (notifications, phase config, grace window, pause/resume, end, purge) moves into a new `` reached from the header. The edit trigger becomes a two-option picker (plan vs. advanced). The `/medications` list page is unchanged apart from matching the detail-nav card glyph to its destination. - **Outbound-fetch migration finished** — every remaining server-side fetch routes through `safeFetch`: Withings (token exchange, refresh, measurements, webhook subscribe/unsubscribe, activity + sleep sync), Codex OAuth + SSE client, the bug-report GitHub comment, and the operator-host calls (Umami script/send/test, GlitchTip envelope + store, Loki). Credential-carrying constant hosts inherit the `redirect: "manual"` default so secrets cannot replay onto a redirect hop; operator-supplied hosts additionally pin `requirePublicHost` for the connect-time IP guard. - **`/api/insights/layout`** is registered in the OpenAPI spec so the contract matches the shipped route. ### Fixed - **Intake-edit dialog opened empty** — editing a logged intake row now seeds the dialog from the row's real `takenAt` / `skipped` instead of an empty stub. - **Step consolidation could strand a day** — a minted daily total can collide on the second unique index when a manual step row already sits at the day's canonical-noon instant; the collision rolled back the in-transaction soft-delete and the day re-appeared on every boot. The pass now catches the constraint violation, logs and counts the skip, and steps to the next day. - **Avatar upload over-buffered on the abort path** — the upload body is now read once through a bounded reader that drops retained bytes past the 2 MiB cap, replacing the clone + parallel parse that left the native parse buffering an oversized body after the cap tripped. - **Egress lint exemption let absolute URLs through** — the same-origin exemption now rejects protocol-relative (`//host`) and backslash (`/\host`) forms, which both resolve off-origin. ### Removed - **Dead wizard landing-intent path** — the pure-history rewrite removed the last caller that passed a landing intent into the medication wizard, so the prop, its state-key segment, and the intent branch in `landingStepForEdit` are gone; the helper reduces to the schedule-count heuristic. ### Tests - 5615 → 5635 unit; 1 skipped. Detail-page history surface, advanced-settings sheet, intake-edit seed, and legacy step consolidation are covered; the pre-tag reconcile sweep keeps the suite green. ## [1.5.5] — 2026-05-29 — Medication detail page, iOS coordination, outbound-fetch hardening The v1.5.4 modal wizard landed the create + edit plan flow, but it took 16 features down with the retired flat form — Einnahmen bearbeiten / löschen, Medikament pausieren / beenden / löschen, per-Med API tokens, Phasen-Konfiguration, CSV-Import — and the trends-row on Insights had a 34 px overflow that pushed annotation copy on top of the charts. This release lands the medication detail page that gives every retired feature a coherent home, polishes the wizard against the live walk-through feedback, closes the iOS audit follow-ups in one go, and hardens every outbound fetch the server makes. ### Added - **`/medications/[id]/page.tsx`** — a single Server-Component detail page composed of eight sections (Header band → Today's-dose → Cadence summary → Phasen (GLP-1) → Intake history preview → Notifications → Settings → Verwaltung & Gefahrenzone). One-shot medications walk a five-section variant; paused medications keep the structure but the status pill flips to `Pausiert` and the dose-card surface mutes. Restores every one of the 16 features the v1.5.4 flat-form retirement displaced. - **Per-row + bulk intake history actions** — the preview now ships a per-row kebab (`Bearbeiten` / `Löschen`) and a multi-select toolbar that fires the new `POST /api/medications/{id}/intake/bulk-delete` endpoint. The maintainer's „Einnahmen löschen" + „Einnahmen bearbeiten" complaints from the v1.5.4 walk-through close cleanly. - **`safeFetch` wrapper** at `src/lib/safe-fetch.ts` — every outbound fetch the server makes for user-supplied hosts now inherits `redirect: "manual"` + `AbortSignal.timeout(15_000)` defaults. Closes #218. - **DNS-rebinding pinned `undici.Agent`** at `src/lib/safe-fetch-dispatcher.ts` — when `requirePublicHost: true`, the dispatcher resolves the hostname literally inside the connect hook, refuses any address `isPublicIp` would reject, and pins the connection to the first valid public address. The five user-host paths (MoodLog sync + push + test, local-AI client, ntfy) route through it. Closes #217. - **Self-hosted avatar storage** at `POST/GET/DELETE /api/user/avatar` — multipart-uploaded JPEG/PNG/WebP, 2 MiB / 2048×2048 max, hand-rolled magic-byte sniff + dimension probe so no native dependency lands. Stored on the User row as BYTEA; rides `pg_dump` alongside the rest of the row. The profile response carries an `avatarUrl` with a cache-busting timestamp. `src/lib/gravatar.ts` retires — Automattic no longer sees the email-hash on every authenticated page load. - **`POST /api/medications/{id}/intake/bulk-delete`** — owner-scoped, capped at 500 event IDs per call, rate-limited per user. Pre-work for the detail page's multi-select. - **`GET / PUT / DELETE /api/insights/layout`** — mirrors `/api/dashboard/widgets` so insights tile order + visibility persist server-side and sync across devices. Default order: `overview`, `blutdruck`, `puls`, `sauerstoff`, `koerpertemperatur`, `gewicht`, `bmi`, `aktive-energie`, `workouts`, `schlaf`, `ruhepuls`, `hrv`, `stimmung`, `medikamente`. Default-visible: overview, blutdruck, puls, gewicht, bmi, workouts, stimmung, medikamente. - **Eight new Apple Health quantity-type mappings** — `RESPIRATORY_RATE`, `BODY_MASS_INDEX`, `LEAN_BODY_MASS`, `WALKING_HEART_RATE_AVERAGE`, `WALKING_ASYMMETRY`, `WALKING_DOUBLE_SUPPORT`, `WALKING_STEP_LENGTH`, `WALKING_SPEED`. The convention block on `apple-health-mapping.ts` documents the project rule: raw HK values flow on the wire, the server scales ×100 server-side for percent metrics. Step length and walking speed flow raw in SI (m and m/s) — no scaling. - **Three new series-kind enum values** — `RESTING_HEART_RATE`, `HEART_RATE_VARIABILITY`, `VO2_MAX`. Detail + trend views on those metrics now respond 200 instead of 422. ### Changed - **Wizard polish** — dialog widens to `sm:max-w-2xl`; the X-close gets a 44 px target; spacing tokens converge on the existing shadcn cadence (`rounded-md` button, `rounded-lg` dialog, no new radii); the step-progress bar is a width-only `` + Tailwind `transition-all` with `motion-reduce` snap; step transitions are fade-only. `landingStepForEdit` accepts an `intent` argument so the cadence-summary edit pencil drops the user on Step 5 instead of bouncing them through Step 1. - **`/api/measurements/series` `days` cap** — raised from 365 to 3650, matching the recurrence engine's hard cap. The iOS app's „Alle"-range no longer paints a 422 banner on every metric. - **`/api/dashboard/summary` MetricCard shape** — `title` and `unit` ship as i18n keys (`dashboard.metric.title.*` + `dashboard.metric.unit.*`) instead of hardcoded German strings. Web resolves via the existing `messages/*.json` path; iOS resolves against its `Localizable.xcstrings`. Wire-shape change — clients that decoded the legacy `title` / `unit` strings keep working with the iOS team's tolerant fallback decoder during the transition. - **`assertMedicationOwnership` consistency sweep** — `purge`, `parent PUT`, `parent DELETE`, `intake/import`, `phase-config`, `bulk-delete`, `glp1 GET` all converge on the single ownership helper. The §10 invariant 24 from the design direction now holds across `src/app/api/medications/[id]/**`. - **Trends-row chart slot** — raised from 140 → 180 px so the mini-card shell's header + padding stays inside the slot. The TrendAnnotation under each chart no longer collides with the chart envelope. - **Wizard payload edit path** — `buildCreateBody` omits `notificationsEnabled` on edit so the toggle the user already set in the detail page Notifications section is not overwritten by the wizard's hydrated default. - **`` DOM ids** — section heading and row label carry distinct ids so the Switch's `aria-labelledby` resolves to the row title, not the section heading. - **`phase-config` route surface** — PUT returns the multi-issue 422 envelope on Zod failure (matching every other v1.5.5 route), and the upsert builds the Prisma payload field-by-field instead of spreading `parsed.data`. Mass-assignment surface closes structurally. ### Fixed - **Trends-row text overlapping the charts** — a specific Insights complaint from a user report. Chart slot was 140 px but the mini-card shell painted ~174 px; the 34 px overflow pushed annotation copy onto the chart envelope. The 180 px slot accommodates the full envelope. - **Grace-row save dead on arrival** — the detail page Settings section PUTs `reminderGraceMinutes` at the top level; the route now normalises the value onto the primary schedule before the Prisma update. The schema declares the top-level field with a description noting the normalisation. - **Purge route did not invalidate server caches** — Tier-3a `Verlauf löschen` dropped the rollup rows but left the analytics + iOS today-tally caches with the pre-purge counts for up to their TTL. The success path now calls `invalidateUserMedications(user.id)` alongside the rollup delete. - **Bi-weekly worker still emitted every Wednesday** — the v1.5.3 cadence engine fix was correct, but the cadence chart + medication card on the dashboard still read the legacy `daysOfWeek` column. The v1.5.x window keeps this in place; the read-flip arrives in v1.5.6. Operators see correct reminder fan-out today; the dashboard chip cosmetic catch-up follows. - **Compose-mode multi-schedule data loss** — the v1.5.4 wizard collapsed a multi-schedule medication to its first schedule on save and silently dropped the rest. Closed in v1.5.4 by the compose-mode commits; the regression test pinning that lives at `wizard-payload.test.ts`. ### Tests - 5594 → 5615 unit (+21 in the reconciliation sweep alone; the detail-page surface + audit fixes add ~110 across the cycle). - 262 integration unchanged + the avatar upload integration test (`tests/integration/user-avatar.test.ts`). - New Playwright spec pre-work — the detail-page surface is component-test pinned; e2e walk lands in a follow-up. ### Security - `safeFetch` + DNS-rebinding pinned dispatcher close issues #217 + #218 architecturally — the input-time `isPublicUrl` guard now pairs with a connect-time IP pin so DNS rebinding cannot flip the resolved host between accept and dispatch. - Avatar route enforces size before parse (Content-Length pre-flight + post-parse `file.size` check), magic-byte sniff over the declared content-type, dimension probe, owner-scoped on every method. No new XSS vector — the served content-type is whitelisted to the three image MIMEs. - `assertMedicationOwnership` sweep closes every detail-page route's ownership narrowing — the route layer is now the single ownership predicate across `src/app/api/medications/[id]/**`. - Pre-tag senior-dev + security architect audit produced two docs (internal working notes). The four senior-dev Criticals + five Highs landed in code; F-1 H-5 (`safeFetch` migration to constant-host call sites like Withings + Codex + the GitHub bug-reporter) + F-2 M-1..M-3 (operator-host `requirePublicHost`, avatar chunked-body pre-flight, raw-fetch lint rule) deferred to v1.5.5.1. ### iOS coord - The iOS team's v0.8.0 audit closed cleanly. Detailed acknowledgements live in the internal iOS coordination notes. - The iOS team flips `walkingAsymmetryPercentage` + `walkingDoubleSupportPercentage` from pre-multiplied to raw in their next release; until then the server fails-closed (`skipped:"value_out_of_range"`) on > 100% values, so no DB pollution. ### Notes - **No new npm dependencies.** Avatar image-header parsing is hand-rolled; no `sharp` / `jimp`. The animation surface is shadcn `` + Tailwind `transition-all`; no Framer Motion. Only `undici` was promoted from transitive to explicit so the safeFetch-dispatcher import sits on a documented contract. - **Test totals.** 5615 unit + 1 skipped, 262 integration + 3 skipped. `pnpm typecheck`, `pnpm lint` (one pre-existing withings/resume warning), `pnpm openapi:check`, locale-integrity + call-site coverage all green. ## [1.5.4] — 2026-05-28 — Medication wizard: modal dialog, compose-mode, ten-bucket taxonomy The v1.5.3 creation wizard shipped as a single-page card with seven inline steps. Patient feedback during the first day of live use was unanimous: the page felt dense, the step-by-step intent was lost, the "every N days from my last injection" cadence was unintelligible without a worked example, and the edit form was visibly wider than its container. The bi-weekly worker bug was closed in v1.5.3 but the surface a patient actually touches was not yet where it needed to be. This release replaces the wizard with a real modal-dialog flow, lands compose-mode so a single medication can carry multiple parallel schedules (the insulin short-acting + long-acting case), widens the treatment-class taxonomy to ten buckets, and retires the flat edit form. ### Added - **Modal-dialog wizard**. `src/components/medications/wizard/MedicationWizardDialog.tsx` is the new entry surface — a shadcn `` on desktop, a `` on mobile, switched through the existing `` primitive at 768 px. The wizard renders its own header inside the body so the iconified step anchor sits where the patient reads, and the footer carries Back / Save with a fixed sticky position on mobile. Eight steps total, one focused question each: name → treatment class → dose → course window → cadence → sub-cadence detail → times of day → review with reminders toggle and per-schedule summary cards. Cadence + sub-cadence + times collapse out of the path when they do not apply (one-shot walks five, daily walks seven, recurring walks eight); the visible counter mirrors the path the patient actually walks. - **Compose-mode**. A medication can now hold multiple parallel schedules, each with its own cadence and times. Steps 5–7 are per-schedule; Step 8 renders the schedule list with an "Bearbeiten" and "Entfernen" action on each card plus a "+ Weiteren Zeitplan hinzufügen" card at the bottom. The last remaining schedule is non-removable so the medication always carries at least one schedule. A medication opened in edit mode with more than one configured schedule lands the patient directly on the list view; single-schedule edits keep the Step 1 entry. The header on Steps 5–7 carries a small "Zeitplan {n} von {total}" caption when more than one schedule is configured. - **Ten-bucket treatment-class taxonomy on Step 2** — Blutdruck, Diabetes, Hormone, GLP-1-Injektion, Schmerz, Allergie, Vitamine, Nahrungsergänzung, Antibiotikum, Sonstiges. Schmerz lands the chronic-pain cohort with its own rolling-rescue-medication pattern; Antibiotikum lands the one-shot course pattern cleanly so neither falls through to `OTHER` for analytics purposes. Each row carries a monochrome Lucide glyph (Stethoscope / Droplet / Activity / Syringe / Flame / Wind / Apple / Leaf / ShieldCheck / Tag). - **`DIABETES` + `ANTIBIOTIC`** as first-class values of the `MedicationCategory` Zod enum so insights routes that filter on the clinical category surface those buckets without falling back to `OTHER`. Additive at the schema layer; existing rows do not need touching. - **Rolling-cadence mental-model copy.** Step 5's "Flexibel ab letzter Einnahme" row carries the example sentence directly under the label (not behind a tooltip) — "Du lässt den nächsten Termin offen, drückst 'genommen' wenn du sie genommen hast — ab dem Zeitpunkt zählt der Counter wieder." The patient-pain research across chronic-illness adherence studies identified this exact gap as the #1 reason patients abandon medication apps in week one; the in-line example sentence closes it. ### Changed - **Edit path routes through the same dialog.** "Bearbeiten" on a medication card opens ``; the header swaps to "{name} bearbeiten", the CTA to "Änderungen speichern", and the entire payload hydrates from the existing medication shape including the schedule `id` so a PUT preserves the per-schedule identity. The flat `` (1314 LOC, surfaced as a `` that rendered wider than its container on the medications list page) retires in the same change. - **`/medications/new` retires** to a redirect that opens the dialog from the list page (`/medications?new=1`). Existing bookmarks survive without losing the entry point. - **i18n.** A clean `medications.wizard.*` namespace replaces `medications.create.wizard.*` so the locale-integrity guard surfaces every dangling key during the cut. German and English carry native copy; Spanish, French, Italian, Polish ship the English string verbatim as a machine fallback per the project convention. Native polish for the four fallback locales follows. - **``** (v1.5.3's inline-stepped Card) and **``** (an unused titration-phase helper that `knip` flagged once the list page swapped over) retire alongside the form. ### Fixed - **Multi-schedule data-loss risk** — the v1.5.3 wizard collapsed a multi-schedule medication to its first schedule on save and silently dropped the rest. Compose-mode is the proper fix; the encoder now emits every schedule, the hydrator reads every schedule, the per-schedule `id` round-trips so the PUT preserves identity. - **The edit form being wider than its container** — a specific user complaint from the v1.5.3 hand-walk. The container is now a constrained Dialog at `sm:max-w-md` on desktop and a sticky-footer Sheet at `max-h-[90dvh]` on mobile; the form-as-page surface is gone. ### Tests - `src/components/medications/wizard/__tests__/wizard-payload.test.ts` — 54 pure-helper cases pinning `validateStep`, `buildCreateBody`, `buildUpdateBody`, `summariseCadence`, the treatment-class row → request body mapping, the multi-schedule encoder, schedule-`id` preservation on edit, the hydration that lands on the list view when more than one schedule is present, the remove-guard against the last remaining schedule, and the add-then-active-bump behaviour. - `e2e/medications-wizard-{daily,weekdays,biweekly,monthly,rolling,oneshot}.spec.ts` — six Playwright specs updated to walk the new dialog surface via the German label set; `e2e/medications-wizard-compose.spec.ts` walks a two-schedule create flow (daily Ramipril + weekly-Wednesday addendum) and asserts both summary cards land on Step 8 before save. - Route tests assert POST `/api/medications` accepts a body with `category: "DIABETES"` and `category: "ANTIBIOTIC"`. ### Notes - **No API or wire-format change.** The wizard writes the same `createMedicationSchema` payload the v1.5.3 wizard wrote; iOS clients reading and writing the existing schedule shape are unaffected. The native `treatmentClass` enum already shipped `GLP1`; the wizard's Step 2 maps the maintainer-confirmed labels onto the existing enum + the two new `MedicationCategory` values. - **Cadence-engine read-flip is still v1.5.x deferred** — the today-projector, the cadence chart, and the medication card continue to read the legacy `daysOfWeek` / `intervalWeeks` columns. A wizard-minted rolling or one-shot medication will render a daily-looking next-due chip on the dashboard until the read-flip ships. The reminder worker and the engine work with the new fields correctly. - **Test totals.** 5500 unit + 1 skipped, 261 integration + 3 skipped, 14 Playwright wizard instances. Typecheck, lint, `openapi:check`, i18n call-site coverage, locale-integrity all green at HEAD. ## [1.5.3] — 2026-05-28 — Medication scheduling: RRULE cadences, rolling intervals, one-shot lifecycle, creation wizard The medication surface in v1.5.2 covered daily and weekday-subset schedules cleanly, but everything else — bi-weekly, monthly, quarterly, yearly, "every N days from my last injection", single-dose appointments — either drifted or was unreachable from the UI. The reminder worker also carried a quiet pre-existing bug where `intervalWeeks` was ignored: a schedule meant to fire every other Wednesday fired every Wednesday. This release lands the full cadence surface, closes the worker bug behind a regression test, and adds a step-driven creation flow patients can walk without consulting a manual. ### Added - **Four new cadence shapes**, modelled at the schema level with explicit columns instead of an overloaded legacy string: - **Calendar-anchored RRULE patterns** (`rrule TEXT`) — `FREQ=WEEKLY;INTERVAL=2;BYDAY=WE`, `FREQ=MONTHLY;BYMONTHDAY=1`, `FREQ=MONTHLY;INTERVAL=3;BYMONTHDAY=10`, `FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1` all expand correctly. Powered by the `rrule` npm package; the engine adds an `UNTIL` suffix derived from the medication's `endsOn` automatically (and skips that suffix when the user's RRULE already carries `COUNT` or `UNTIL`, so the two never collide). - **Flexible-rolling cadence** (`rollingIntervalDays INT`) — "every N days from the last logged intake". The next-due date re-anchors when an intake is logged; skipped doses pause the schedule until the next real intake. The driving use case is the GLP-1 weekly injection where the calendar Wednesday does not match the user's actual cycle. - **One-shot single-administration** (`oneShot BOOLEAN`) — vaccines, post-op final doses, anything with one scheduled occurrence and an auto-deactivate after the dose is logged. - **Course window** (`startsOn DATE`, `endsOn DATE`) at the medication level. Required for one-shot; optional everywhere else. The reminder worker stops minting slots past `endsOn`. - **`POST /api/medications/extract`** — natural-language extraction for the wizard's first step. A user types "Mounjaro 5mg weekly Wednesday morning starting next Monday" and the route returns a structured payload that pre-fills the wizard. Rate-limited per user, budget-gated, and carries a citation-coverage guard so the extraction cannot return a name or dose the user did not write. - **`/medications/new`** — the seven-step creation wizard (compressed to five on the one-shot path) replacing cold-start entry into the legacy flat form. The summary step interpolates the actual picked weekdays / day-of-month / yearly date so the patient confirms the specifics rather than a category label. - **Reusable picker primitives** under `src/components/medications/scheduling/`: `CadencePicker` (eight cadence kinds, mode-aware via `allowedKinds`), `TimesOfDayChips` (one or more `HH:mm` entries with morning / noon / evening / night presets), `CourseWindowRow` (start + optional end with `lockEndsToStart` for one-shot). Composed by both the wizard and the refactored edit form so any future cadence tweak reaches both surfaces at once. - **Canonical recurrence engine** at `src/lib/medications/scheduling/recurrence.ts` exposing `occurrencesBetween`, `nextOccurrenceAfter`, and `matchesInstant`. The reminder worker routes through the engine via a narrow `worker-helpers.ts` adapter; the today-projector, the cadence chart, and the medication card continue to read the legacy fields through v1.5.x and switch over in the v1.5.4 read-flip. - **Migration `0081_v15_medication_scheduling`** — adds the new columns, backfills `rrule` and `timesOfDay` for every existing schedule (closed-enum regex on the legacy `daysOfWeek` shapes, ELSE NULL fallback), and stages a CHECK constraint forbidding both `rrule` and `rolling_interval_days` populated on the same row. - **OpenAPI coverage** for every medication route — `GET / POST / PUT / DELETE /api/medications`, `GET /api/medications/{id}`, `POST /api/medications/{id}/intake`, `GET /api/medications/{id}/cadence`, and the new `POST /api/medications/extract` — registered in `src/lib/openapi/routes.ts` and regenerated into `docs/api/openapi.yaml`. The `rrule XOR rollingIntervalDays` invariant is documented at both the schema description and the per-field descriptions so iOS code-gen surfaces the mutual exclusion. ### Fixed - **Bi-weekly worker regression** — `src/lib/jobs/reminder-worker.ts` now consumes the canonical engine, which honours `intervalWeeks > 1` via the legacy fallback's week-phase math. A bi-weekly schedule that previously fired every Wednesday now correctly fires every other Wednesday. Pinned by an explicit regression test (`recurrence.test.ts` — `daysOfWeek = "i2;3"` emits 2 of 4 candidate Wednesdays in a 4-week window). - **One-shot lifecycle reconciliation** — `src/lib/medications/lifecycle.ts` defines `reconcileOneShotState(prisma, medicationId, userId)` and the helper runs after every intake mutation (POST + PUT + DELETE). A user who logs the single dose then immediately undoes the log gets the medication back as `active: true`; a user who flips an existing intake from real to skipped also gets the medication reactivated. The helper is a no-op on non-one-shot medications. - **Legacy fallback `startsOn` floor** — `expandLegacy` now respects `medication.startsOn` the way every other dispatch tier does. A legacy-shape schedule with a future `startsOn` no longer emits historical slots between today and the start date. - **Course-window invariants** — the create and update schemas refuse `oneShot: true` without `startsOn` (the design contract requires the anchor date) and refuse any course where `endsOn < startsOn` (which previously produced a silently dead medication). Rolling-cadence schedules cap `timesOfDay` at one entry to match the engine's single-time emission. - **Wizard `notificationsEnabled` toggle** — the Step 7 reminders switch now actually reaches the POST body. Earlier the toggle was visually live but its value was discarded by the body builder and every wizard-created medication ended up with the default `notificationsEnabled = true`. - **Edit form: one-shot single source of truth** — the medication-level `oneShot` switch now drives the picker's `allowedKinds`, so the per-schedule picker can no longer encode `kind: "oneShot"` while the medication-level switch is off (or vice versa). Toggling the switch on with multiple schedules surfaces a confirmation toast before the collapse-to-single-schedule, instead of silently dropping the extras. - **Wizard accessibility and tap-target hygiene** — focus advances into the new step's first input on Next; the cadence-picker rows are now full-row click targets (44 px); the nav buttons (Back / Next / Create) and the natural-language trigger rise to 44 px; the wizard card carries `aria-busy` while the submit is in flight. ### Changed - **Recurrence-engine defence-in-depth** — `nextOccurrenceAfter` carries a `MAX_CHUNKS = 80` cap alongside the pre-existing 10-year `hardCap`, so a pathological RRULE that walks zero forward (e.g. leap-day-only) can no longer compound through many 90-day chunks. RRULE parse failures now surface via `annotate({ action: "medications.rrule.parse_error" })` instead of silently returning `[]`. - **Edit form refactor** — `src/components/medications/medication-form.tsx` now composes the picker primitives for the edit path. Pre-v1.5 medications round-trip through `inferCadenceFromLegacy` on load and dual-write both shapes on save, so the existing legacy schedules keep working unchanged. - **i18n** — 111 new keys × six locales populating the wizard, the picker primitives, the natural-language overlay, the edit-form sections, and the plain-language cadence summary. German and English carry native copy; Spanish, French, Italian, Polish ship the English string verbatim as a machine fallback for this cut. Native polish for the four fallback locales follows. ### Tests - `src/lib/medications/scheduling/__tests__/recurrence.test.ts` — 28 unit cases covering every cadence kind plus the edge-case matrix (DST spring-forward, timezone shift mid-course, skipped + late + retroactive doses, paused medication, `endsOn` cap, missing `startsOn`, multi-schedule fan-out). - Eight `tests/integration/v15-cadence-shapes.integration.test.ts` cases exercising every cadence shape against a testcontainer Postgres, plus the one-shot lifecycle (take → reconcile-deactivate → delete-intake → reconcile-reactivate → put-skip → reconcile-reactivate). - `src/components/medications/scheduling/__tests__/{CadencePicker,TimesOfDayChips,CourseWindowRow,CreationWizard}.test.tsx` — 90 component tests against the picker primitives and the wizard helpers (`validateStep`, `buildCreateBody`, `summariseCadence`, `progressIndices`, `allowedKinds`). - `src/lib/ai/coach/__tests__/medication-extract-prompt.test.ts` — five snapshot cases pinning the extraction prompt across the cadence shapes plus a citation-coverage guard test. - `src/app/api/medications/extract/__tests__/route.test.ts` — six route cases covering auth, rate limit, budget, missing provider, parse failure, and the happy path. - `e2e/medications-wizard-{daily,weekdays,biweekly,monthly,rolling,oneshot}.spec.ts` — six Playwright specs walking the wizard end-to-end via the German label surface, one per cadence shape. CI runs the full suite; locally the specs typecheck without executing the Next.js prod build. ### Notes - **Dashboard read-flip caveat (v1.5.x window).** Until the v1.5.4 read-flip lands, the dashboard card, the cadence chart, and the medication-card "next dose" line read the legacy `daysOfWeek` / `intervalWeeks` columns directly. A wizard-minted medication with `rollingIntervalDays = 7` or an RRULE encoding renders a daily-looking next-due chip on the dashboard. The reminder worker, the integration tests, and the canonical engine all consume the new fields correctly — the schedule fires on the right date, only the visual chip on the card lags. The read-flip with legacy fallback is the next release. - **OpenAPI structural enforcement.** The `rrule XOR rollingIntervalDays` invariant is enforced at four runtime layers (Zod refine, route invariant, engine dispatch, DB CHECK). The OpenAPI surface documents the constraint in prose; a structural `oneOf` discriminator lands before the iOS v0.7.x cut. - **Worker `lastIntakeAt` aggregation.** The reminder worker currently fires one `findFirst` per rolling-medication per 15-minute tick. Functionally correct; one `groupBy` collapses it to a single round-trip per tick. Deferred to v1.5.4 with the read-flip. - **Test totals.** 5494 unit + 1 skipped, 261 integration + 3 skipped, 12 Playwright wizard instances. Typecheck, lint, and `openapi:check` all green at HEAD. ## [1.5.2] — 2026-05-26 — Plumb SESSION_COOKIE_SECURE through docker-compose v1.5.1 added a `SESSION_COOKIE_SECURE` env var so plain-HTTP self-hosts can drop the cookie's `Secure` flag. The Node helper read it correctly and the unit tests passed, but a self-hoster following the documented `.env` workaround reported that `docker compose exec app env | grep SESSION_COOKIE` came back empty — the value was set in `.env`, set in the helper, but never reached the running container. Root cause is the way the bundled `docker-compose.yml` passes env vars to the `app` service: it lists each one explicitly under `environment:` rather than mounting the `.env` file wholesale. Variables not on that whitelist are read by `docker compose` for `${VAR}` substitution but never propagated to the container's process env. `SESSION_COOKIE_SECURE` wasn't on the list, so setting it in `.env` was a silent no-op. ### Fixed - `docker-compose.yml` now lists `SESSION_COOKIE_SECURE: "${SESSION_COOKIE_SECURE:-}"` under the `app` service's `environment:` block. Defaults to empty (so the helper falls back to `NODE_ENV === "production"`, the pre-v1.5.1 behaviour); setting it to `false` in `.env` now actually reaches the Node process. ### Self-hoster recipe (full, now end-to-end working) ```bash git pull # picks up the new compose file docker compose pull # picks up the new image (no-op if already on :latest) echo 'SESSION_COOKIE_SECURE=false' >> .env docker compose up -d --force-recreate ``` Verify with: ```bash docker compose exec app env | grep SESSION_COOKIE # SESSION_COOKIE_SECURE=false curl http://10.x.x.x:3000/api/version # {"data":{"version":"1.5.2",...}} ``` Then log in over plain HTTP from a non-localhost browser — the session cookie no longer carries `Secure`, the browser keeps it, and the round-trip completes. ## [1.5.1] — 2026-05-26 — Self-hosting opt-out for the session-cookie `Secure` flag A self-hoster running HealthLog on a LAN address (`http://10.x.x.x:3000`) over plain HTTP reported a silent login failure: the `/api/auth/login` POST returned 200 with a `Set-Cookie` header on the response, but the very next `/api/auth/me` request came back 401 and the page reloaded to the login screen. Root cause is the modern browser behaviour around `Secure`-flagged cookies — every cookie the app issues in `NODE_ENV=production` carries `Secure`, and on a plain-HTTP origin that is not `localhost` / `127.0.0.1` / `::1`, the browser silently drops the cookie before sending the next request. The default-Docker image runs `NODE_ENV=production`, so any operator browsing from a different host than the one running `docker compose up` (NAS / homelab / VPS / Tailscale + Magic-DNS) used to hit the dead-end. ### Added - `SESSION_COOKIE_SECURE` environment variable. Controls the `Secure` flag on the session cookie, the onboarding-hint cookie, the Withings OAuth state cookie, and the Codex device-OAuth state cookie. Three values: - **unset** (default) — flag is set when `NODE_ENV === "production"`, the long-standing behaviour. - **`false`** — flag is never set. Use this for LAN / VPN / Tailscale-only deployments where the operator deliberately serves plain HTTP and accepts the trade-off (the session cookie crosses the wire unencrypted; do NOT use on an open-internet HTTP origin). - **`true`** — flag is always set, useful when a developer fronts a `pnpm dev` server with HTTPS to test the production cookie path. - New shared helper `src/lib/auth/secure-cookie.ts` (`shouldEmitSecureCookie()`) that every `Secure`-bearing cookie call now reads. Replaces four scattered `secure: process.env.NODE_ENV === "production"` literals in `src/lib/auth/session.ts`, `src/app/api/auth/codex/device-start/route.ts`, and `src/app/api/withings/connect/route.ts`. - `.env.example` block documenting the new variable with a clear warning about the open-internet-HTTP case. ### Tests - `src/lib/auth/__tests__/secure-cookie.test.ts` — six cases covering the default path, both explicit overrides, whitespace / case-insensitive parsing, and the fall-through for unrecognised values. ### Notes - The default behaviour is unchanged for every deployment that runs behind an HTTPS-terminating reverse proxy (the documented self-hosting path) — those continue to set `Secure` exactly as before. The opt-out is intentional and explicit. - Operators currently working around the issue with `NODE_ENV=development` can move back to `NODE_ENV=production` plus `SESSION_COOKIE_SECURE=false` on this release; that keeps build optimisation + production error masking on while letting the cookie reach the browser over HTTP. ## [1.5.0] — 2026-05-24 — Native iOS client public-beta + per-day cumulative stats overwrite + cadence-aware medication compliance The minor-version cut that marks the native iOS client publicly available. The SwiftUI iOS app (separate repository) is now joinable via TestFlight: https://testflight.apple.com/join/bucuTBpa. The backend contract the iOS app speaks against has been live since v1.4.23 and has been continuously validated across every v1.4.2x–v1.4.50 release. The 1.5.0 cut also lands the highest-leverage iOS-client unblocker per the v0.6.1 code audit: `/api/measurements/batch` now overwrites per-day cumulative `stats:*` rows on a re-post instead of dropping the new value as a duplicate. ### Added - `POST /api/measurements/batch` recognises `externalId` values starting with `stats:` (`stats:HKQuantityTypeIdentifierStepCount:YYYY-MM-DD` and every other per-day cumulative HK metric — Active Energy, Sleep Duration, Walking/Running Distance, Flights Climbed) and treats a duplicate on those as an **overwrite**, not a discard. Each re-post of the same day's external id replaces the row's `value`, `unit`, `measuredAt`, `externalSourceVersion`, `deviceType`, and `sleepStage`. Sample-class externalIds (every other prefix — `uuid-*`, opaque HK identifiers) keep the strict immutable `duplicate` contract because each sample is a canonical reading. - New per-entry status `"updated"` on the batch response envelope so the iOS sync cursor can distinguish a fresh insert from a value-bump re-post. The aggregate envelope now carries an `updated` count alongside `inserted` / `duplicates` / `skipped`. - New wide-event annotation `measurement.batch.stats-overwrite` (fires only when at least one row was overwritten) so operators can grep how often per-day cumulative re-posts happen as a healthy ingest signal. - `measurement.batch.ingest` audit-log details now include the `updated` count alongside `inserted` / `duplicates` / `skipped`. ### Why Before this change, the iOS HealthKit observer would POST today's running step total once in the morning, the server would persist row #1, and every subsequent same-day re-post (as the user walked) would come back `status: "duplicate"` with the new value silently dropped. Today's Schritte tile froze at the first-sync value until next midnight. The same shape would hit every cumulative metric on a deterministic per-day external id. Closes [#213](https://github.com/MBombeck/HealthLog/issues/213); cross-device parity (web ↔ iOS) for `stats:*` metrics now works for today and every historical day on a re-sync. ### Fixed - Medication compliance now honours `daysOfWeek` and `intervalWeeks` across every call site that surfaces a rate. The legacy aggregator computed `totalExpected = schedules.length * days`, which silently ignored cadence. A weekly Ozempic schedule with all four Mondays taken in the last 30 days reported ~13% adherence (4 / 30) instead of 100%; a weekday-only 3×/day metformin schedule with every weekday dose taken reported ~73% (66 / 90) instead of 100%. `calculateCompliance` is now a cadence-aware adapter on top of `buildCadenceTimeline` — the same pair-matching pipeline that drives the per-medication cadence chart — so the rate on the medication card, the AI Coach prompt context (7d/30d/90d windows in `src/lib/insights/features.ts`), the BP-status compliance gate, the medication-compliance status insight, `/api/insights/targets`, `/api/insights/comprehensive`, and the medication-compliance pillar of the dashboard Health Score all agree on a single, cadence-correct denominator. The wire shape (`{ totalExpected, taken, skipped, missed, rate, streak }`) is unchanged so every UI tile and persisted-insight consumer keeps reading the same fields. Closes [#214](https://github.com/MBombeck/HealthLog/issues/214). Expected user-visible shift: users on weekly meds (GLP-1 agonists, biologics) will see their Health Score rise as the medication pillar moves from ~13 to ~100; users on weekday-only multi-dose schedules will see their score rise as the pillar moves from ~73 to ~100; users on daily-only schedules see no change because the legacy denominator was already correct for that path. Migrations across the eight production call sites are mechanical — the function signature is unchanged. ### Changed - README rewrite for the v1.5 cut: TestFlight badge in the badge row plus an iOS TestFlight link in the Website / Demo / Docs row and the footer. Buy Me A Coffee badge added. Status block updated to reflect that v1.5 is now the current line, with a new "Heavily developed" advisory directly below it that tells self-hosters to pin a tag, take a backup before every upgrade, and read the CHANGELOG before pulling `latest`. Tech-Stack table flags the iOS app as TestFlight-available. Roadmap table promotes v1.5 from "in active development" to "current". - README simplification: the `How it works` diagram cluster (four SVGs covering data flow, Coach pipeline, source priority, and security model) is no longer inlined in the README. The diagrams continue to live in [`docs/diagrams/`](docs/diagrams/) and are surfaced through [docs.healthlog.dev](https://docs.healthlog.dev) where they render reliably across themes and viewport widths. The `03-self-hosting-topology.svg` stays inline under Deployment because it carries deployment-time information a self-hoster wants on the first scroll. ### Tests - `tests/integration/measurements-batch.test.ts` — three new cases pinning the `stats:*` overwrite contract: solo re-post overwrites the value and returns `status: "updated"`; sample-class duplicate keeps the strict first-write-wins contract; a mixed batch with one insert + one overwrite + one duplicate returns all three statuses correctly. - `src/lib/analytics/__tests__/compliance.test.ts` — parameterised cadence matrix: 1×/day daily (7 / 0 / 18 of 21), weekly Mondays-only (all taken / one missed), bi-weekly (`intervalWeeks=2`), weekday-only 3×/day metformin, skipped-dose denominator exclusion, `medicationCreatedAt` truncation, DST spring-forward boundary in Europe/Berlin, and over-logged-day rate cap. The matrix pins the contract for every cadence the production app exercises so future schedule-shape work can't silently regress. - `src/lib/analytics/__tests__/health-score-fast-path.test.ts` — two cadence-aware regression cases: a weekly Mondays-only med with every Monday taken now lifts the medication-compliance pillar to ≥ 50 (previously ~13 under the bug); a daily-only med with every dose taken stays ≥ 90 (no regression on the path that worked). - Eight stale integration assertions retired across `withings-oauth.test.ts`, `withings-oauth-flow.test.ts`, `analytics-bp-aggregate-paged.test.ts`, `analytics-sleep-stages.test.ts`, and `apns-dispatch.test.ts` — drift from the v1.4.47.x OAuth fine-grained reason tags, the v1.4.47.2 ES256 PEM verify guard, and the v1.4.49.1 analytics slim-slice annotation rename. Three `source-priority-two-axis` cases skipped with an inline TODO referencing the v1.4.49.1 commit and the relocation candidate (`pick-canonical-workout-rows`); these tests exercised picker semantics that no longer fire on the default analytics summaries path. ### Notes - iOS coordination items closed alongside this cut: the v1.4.49 server-side `clientManaged` MEDICATION_REMINDER suppression rule is now active for iOS v0.6.0.8+ clients that opt in via `PATCH /api/auth/me/notification-prefs`; tracked in healthlog-iOS#9. Issue [#206](https://github.com/MBombeck/HealthLog/issues/206) is closed. - HealthLog suite: 5285 unit (5279 carryover + 3 new stats-overwrite cases + new compliance matrix and Health-Score regression cases that net out the legacy assertions retired during the cadence-aware migration) + 253 integration tests pass on the local Vitest run, lint clean, typecheck clean. ## [1.4.50] — 2026-05-24 — MoodLog reverse-sync (HealthLog → MoodLog push) A user reported that mood entries logged inside HealthLog — specifically via the iOS app — never reached MoodLog. The historic integration ran one-way: `syncMoodLogEntries` polled MoodLog every 15 minutes and pulled new rows, but nothing flowed the other direction. A user tracking mood in HealthLog ended up with one log per app and no overlap. ### Added - `src/lib/moodlog/push.ts` — `pushMoodEntriesToMoodLog(userId, entries)`. Best-effort reverse-sync push to MoodLog's new `POST /api/integrations/health-log/mood` endpoint. Same per-user `moodLogUrlEncrypted` + `moodLogApiKeyEncrypted` credentials the pull side uses, same `isPublicUrl` SSRF guard, same manual-redirect policy. Wraps every failure mode in a wide-event warning so a network blip or a stale MoodLog deploy can never bubble back to the user's mood-create request. - `POST /api/mood-entries` (single) and `POST /api/mood-entries/bulk` now fire the push as a fire-and-forget side-effect after a successful create. Entries with `source === "MOODLOG"` skip inside the helper to avoid an echo loop. Bulk push only includes rows that landed as `inserted` — duplicates and skips don't retry a known failure. ### Changed - `src/lib/moodlog/sync.ts` (pull side) filters out entries where MoodLog reports `loggedVia: "HEALTHLOG"`. Those are echoes of rows we just pushed; re-importing them would flip the `source` column from the original attribution (MANUAL / WEB / TELEGRAM / iOS) to MOODLOG and double-count in any source-segregated dashboard. An `echoSkipped` wide-event annotation lets the operator confirm the round-trip is closing at one hop. ### Tests - `src/lib/moodlog/__tests__/push.test.ts` — 9 cases covering the MOODLOG-source filter, no-credentials skip, SSRF refusal, headers + body shape on the successful path, 5xx + network-reject + 3xx-redirect failure modes, and the `tags` JSON-string → array key normalisation. Pins the contract the call sites no longer await so future helper refactors can't silently break the fire-and-forget integration. - HealthLog suite: 5279 unit + integration pass, lint clean, typecheck clean. ### Cross-repo coordination The matching MoodLog v0.2.0 release adds the `POST /api/integrations/health-log/mood` handler this push targets. Both apps must deploy together for the round-trip to close — MoodLog first so HealthLog never hits a 405 / 404 on the new endpoint. ## [1.4.49.4] — 2026-05-24 — BD-tile number truncation + medication-compliance test tz drift A user reported the dashboard BD (Sys) tile rendering `"13… mmHg"` — the systolic / diastolic pair (`131/85`) was wider than the narrowest grid column and the value-row's `truncate` ellipsis chopped the most important part of the tile (the number itself). ### Fixed - `src/components/charts/trend-card.tsx` — paired-metric tiles (BP systolic/diastolic) now render the value at `text-2xl` (one Tailwind step down from `text-3xl`). Single-value tiles (weight, pulse, glucose, …) keep `text-3xl` because their value never threatens the column-width budget on its own. The `truncate` class stays as a defence-in-depth safety net so an outlier value (e.g. `200/120 mmHg` plus a future trend caption) still clips gracefully rather than overflowing the card boundary. - `cn()` argument ordering tightened so `tailwind-merge` keeps the explicit `leading-none` — Tailwind v4's `text-3xl` / `text-2xl` carry their own default `line-height`, and the last-wins dedup rule would otherwise drop the explicit `leading-none` and break the across-tile baseline alignment. The `` baseline-alignment tests pin the contract. - `src/app/api/medications/intake/__tests__/route.test.ts` (one pre-existing test) computed its `todayKey` from UTC date components but the route's `readMedicationCompliance` uses the user's timezone (Europe/Berlin in the test fixture). At every nightly 22:00 — 24:00 UTC window — once the Berlin clock crossed midnight — the two computations diverged by one calendar day and the rollup-tier read returned zero for "today". The test now derives `todayKey` from the same `Europe/Berlin` zone the route uses; the assertion stays the same. ## [1.4.49.3] — 2026-05-23 — Full i18n call-site audit + 28 stale missing keys filled A user reported additional raw key strings (`notifications.eventMoodReminder`, `notifications.eventMoodReminderDesc`) reaching the UI after v1.4.49.2 had landed the relative-time fix. The narrow `{count}`-call audit that drove v1.4.49.2 had missed every dynamic key construction; the full exhaustive sweep below picked up 28 keys called from real code that had never existed in any locale bundle. ### Fixed - Filled 28 missing translation keys across all 6 locales (de / en / es / fr / it / pl = 168 entries), grouped by feature surface that introduced them and never wired the strings: - **Notifications event matrix** (14 keys — added with v1.4.41 / MOOD_REMINDER surfaced the gap in v1.4.49): `notifications.event{MedicationReminder,MeasurementAnomaly,ComplianceLow,WithingsSyncFailed,SystemAlert,PersonalRecord,MoodReminder}` and the `Desc` variant of each. The `/notifications` settings page is the consumer; before this release the per-event rows on an affected account rendered the bare keys instead of the localised name + description. - **Welcome carousel** (13 keys — added with the 3-slide intro in v1.4.45): `onboarding.welcome.{title,carouselLabel,slideOf,prevSlide,nextSlide,gotoSlide,cta}` for the chrome plus `slide{1,2,3}.title/body` for the content. The carousel mounted with raw keys for every screen-reader label, dot-pager aria-label, and slide body. - **Measurement list error state** (1 key — added with v1.4.44): `measurements.loadError`. The error branch in `measurement-list.tsx` rendered the raw key when a fetch failed. ### Added - `src/__tests__/i18n-call-site-coverage.test.ts` — call-site audit that walks every `.ts` / `.tsx` file under `src/` (excluding tests + generated code), extracts every literal `t("ns.key")` call with comment-aware parsing (skips line + block + JSDoc comments so example pseudocode in docstrings is not flagged), and asserts each key resolves to a string leaf in `messages/en.json`. The existing `i18n-locale-integrity.test.ts` then propagates the EN guarantee to every other locale via key-set parity. Pre-fix this guard would have surfaced 17 stale call sites at every test run; future regressions print a structured punch list so the offender can land every gap in one commit. ### Why this happened The guard above did not exist before. v1.4.27 B6 introduced `i18n-drift-guard.test.ts` with hand-curated key groups; surfaces introduced after that point (notifications matrix in v1.4.41, measurement list error in v1.4.44, welcome carousel in v1.4.45) shipped without anyone editing the drift-guard groups, and the locale-integrity test only catches DRIFT across locales — it does not flag keys that are missing from EVERY locale uniformly. The call-site test closes that hole: any `t()` call without a backing key now fails at `pnpm test` time, before reaching production. ## [1.4.49.2] — 2026-05-23 — Raw i18n key leak on relative-time helper A user reported the raw string `insights.relativeHoursAgo` rendering verbatim on medication cards, recent-achievements, admin sections, the iOS notification preview, and every other consumer of `formatDateOrRelative`. Audit across the full `t(…, { count })` surface (~20 call sites) confirmed exactly two leaks: `insights.relativeMinutesAgo` and `insights.relativeHoursAgo`. Both pointed to keys that have NEVER existed in the translation bundle — only the pluralised `*One` / `*Other` variants ship. ### Fixed - `formatDateOrRelative` in `src/lib/format.ts` now dispatches to `insights.relativeMinutesAgoOne` / `relativeMinutesAgoOther` / `relativeHoursAgoOne` / `relativeHoursAgoOther` based on `count === 1`, mirroring the `src/lib/i18n/relative-time.ts:24-48` pattern the v1.4.43 i18n fix-up added to its twin helper. The format-twin was missed at the time — `t()` performs no auto-pluralisation, so the bare key passed straight through `t()`'s identity fallback into the UI. ### Test hygiene - `src/lib/__tests__/format-date-or-relative.test.ts` expectations updated to the new `*One` / `*Other` dispatch, plus a new regression guard that drives every relative bucket and asserts each `t()` key has a matching entry in `messages/en.json`. Pre-fix this guard would have failed on `relativeMinutesAgo` and `relativeHoursAgo`; future twin-helper divergence will trip the same check before it reaches prod. ### Why the audit found no other leaks Cross-checked every `t(…, { count })` call across the codebase against `messages/en.json`. The other 17 sites (`insights.dayStreak`, `medications.importDuplicatesSkipped`, `achievements.metricPercent`, `passwordStrength.minLength`, `settings.integrationPill.*`, `targets.relativeDay.daysAgo`, `targets.card.streak`, `admin.section.backups.uploadSuccess`, `trendHints.remainingMany`, `dashboard.staleHint`, …) all use plain (non-pluralised) keys that exist verbatim in the bundle. The bug was isolated to the format-helper twin. ## [1.4.49.1] — 2026-05-23 — Default `/api/analytics` cold-path fix (rollup-tier delegation) v1.4.49 shipped the slim-slice (`?slice=summaries`) cold-fallback fix, but the production HAR captured against the new image still showed `GET /api/analytics` at 8 s cold on the 467 745-row tenant — and crucially, the supposedly-fixed `?slice=summaries` request observed the same 8 s when fired concurrently. Investigation found the slim slice was queuing behind the default slice's 15-way per-type `fetchMeasurementSeriesChunked` live walk: the fan-out held the `p-limit(4)` lanes saturated and the 20-slot Prisma pool packed for ~8 s, starving every other Prisma client (including the concurrent slim slice) until it drained. ### Fixed - `GET /api/analytics` (default slice) no longer fans out 15 per-type `findMany` reads against `measurements`. The route now delegates the per-type `summaries` work to `computeSummariesSlice`, which reads the same data from `measurement_rollups` DAY buckets + a 90-day narrow `$queryRaw` for the windowed avg / slope / r² columns. On a large production account this drops the cold critical path from ~8 s to sub-second; warm cache is unchanged. - Pool starvation that ALSO affected the supposedly-fast `?slice=summaries` request when fired concurrently — the slim slice now resolves in its own SQL budget regardless of whether the default-slice request is also in flight. ### Changed - `computeSummariesSlice` narrow `$queryRaw` extended with a `FILTER (WHERE measured_at >= NOW() - INTERVAL '60 days' AND measured_at < NOW() - INTERVAL '30 days')` `avg30_last_month` column. The 60-day lower bound stays inside the existing 90-day outer cap, so the additional column adds zero extra row scan work — the planner already touches every block this clause reads. Dashboard `tileCompareDelta` (`compareBaseline === "lastMonth"`) now sees a real value via the slim path; previously only the deleted live walk produced it. - Deleted dead code: `fetchMeasurementSeriesChunked`, `ChunkedRow`, `MEASUREMENT_CHUNK_SIZE`, the `ANALYTICS_TYPE_FETCH_CONCURRENCY = 4` constant. None had callers outside the removed fan-out. - `meta.analytics.bp_aggregate` wide-event field retired — it carried the per-type walk's row-count + `live_since` cutoff, both meaningless without the walk. The slim slice's `meta.analytics.slim_summaries` (`row_count`, `type_count`, `path`, `year_over_year_types`) carries the equivalent signal. - `summary.anomalyCount` on the default-slice response now consistently returns `0`. No consumer in the codebase reads it from `/api/analytics`; the insights pipeline (`/api/insights/comprehensive`, `/api/insights/cards`) sources `anomalyCount` from its own `comprehensive-aggregator` narrow query. ### Test hygiene - `route.test.ts` updated: dropped the 130k-row PULSE stress test and the `caps per-type Prisma fan-out` concurrency test (both pinned the removed fan-out). Added a negative assertion that `prisma.measurement.findMany` is never called with the chunked-walk shape `(select: { id, source, deviceType }, take: 5000)` on the default critical path, plus a positive assertion that `avg30LastMonth` exists on the response. - `since-cap.test.ts` updated: dropped the `where.measuredAt.gte` and `bp_aggregate.live_since` annotation assertions (both pinned the deleted code path). The slim-slice negative invariant (no per-type loop on `?slice=summaries`) remains. - Final: **5 268 unit + integration tests pass** (one pre-existing skip), zero failures, lint clean. ### Verification Post-deploy production logs from `/api/analytics` (default + slim, cold cache) confirm the fix landed: both routes report `path: "rollup"` for every component and `duration_ms` returns to the sub-second envelope. See the v1.4.49.1 deploy verify section in the project memory. ## [1.4.49] — 2026-05-23 — Server-side reminder suppression + diagnostic endpoint backed + backlog closure v1.4.47 closed the high-priority punch list. v1.4.49 bundles the remaining v1.4.47 follow-ups together with the items deferred out of v1.4.48: server-side suppression of `MEDICATION_REMINDER` APNs for iOS clients that manage their own local reminders, a `push_attempts` table backing the diagnostic endpoint, the `/api/admin/notifications/diagnostic` OpenAPI entry, and a sweep of v1.4.48 forward-findings (Withings reason-tagging, observability PII hardening, MoodReminderCard auto-clear parity, simplifier dead-code, sub-locale copy gaps). The workout-batch integration suite that had been red on `main` for three releases turns green; the cold-mount analytics fallback path picks up the same 90-day outer cap v1.4.47.1 shipped for the rollup-fresh path; iOS validation-failure audit rows carry the rejected payload shape so iOS serialiser drift can be chased from a single log line. ### Added - `/api/admin/notifications/diagnostic` — admin-only endpoint that surfaces device APNs token presence (masked to an 8-char prefix + 8-char suffix), notification channel config presence, and recent push attempts for the calling account. Closes the DB-shell bottleneck that slowed the v1.4.47 APNs investigation. The endpoint is registered in `docs/api/openapi.yaml` so future iOS clients can codegen against it. - `push_attempts` table backs the diagnostic endpoint's `recentPushAttempts` field. Every APNS / Web-Push / Telegram / NTFY sender writes a fire-and-forget row per attempt (channel + eventType + result + reason + createdAt). The diagnostic endpoint now returns the last 20 attempts ordered by recency. Daily cleanup cron at 03:35 Europe/Berlin prunes rows older than 90 days. - `GET` + `PATCH /api/auth/me/notification-prefs` — per-user notification preferences with deep-merge semantics. Initial category `medication.clientManaged: boolean` (default `false`); future categories slot in without overwriting siblings. The cron `medicationReminderJob` skips dose-due APNs when the flag is `true`, emitting a `medication_reminder.suppressed_client_managed` wide-event annotation per skip. Other notification kinds (mood reminder, personal record, system alert) are unaffected. Closes the server side of GitHub issue #206. - `redactSensitiveFields(body)` helper at `src/lib/observability/redact-payload.ts` with a denylist (`password` / `token` / `secret` / `apiKey` / `authorization` / `csrfState` / `nonce`, case-insensitive, recursive). Used by the wide-event `received_shape_excerpt` surface so future free-text routes adopting the pattern cannot leak credentials. - Native sub-locale translations for `settings.about.tourReplay` and `tourReplayHint` (es / fr / it / pl), and for `settings.about.linksHeading` + `newerAvailable` across the same four sub-locales. Tightened `dashboard.dragHandleHint` copy in de + pl (was ~30 words on a `sr-only` paragraph; screen readers spent ~6 s reading it on focus). - OpenAPI documentation for `GET` + `PATCH /api/auth/me/disable-coach`. - Operations runbook notes: `attemptNumber` audit-row vs alert-signal semantics at `docs/ops/attempt-number-semantics.md`, and explicit migration-before-container ordering at `docs/ops/deploy.md` for manual / partial-deploy scenarios. ### Changed - `/api/analytics` cold-fallback path splits the heavy single-statement aggregate into two parallel queries. The linearly composable columns (`count` / `min` / `max` / `mean`) keep the full-partition scan; the windowed 7 / 30 / 90-day `avg` / `slope` / `r²` columns take a 90-day outer cap so the planner does an index range scan on `(user_id, type, measured_at)` instead of a full-partition sequential scan. Output is bit-identical; cold slim slice drops from multi-second to sub-second on tenants with large measurement partitions. - Dashboard reorder + onboarding tour helpers consolidated. Arrow-button reorder delegates to the same swap path as drag-and-drop. `restartOnboardingTour` extracted so About + Account sections share the surface. `useSortable` honours `prefers-reduced-motion`. Drag-handle `aria-describedby` hint gates on visible widget count so the empty-state cannot orphan the hint paragraph. - Coach disable surface tightened. The status banner auto-clears after 3 s. `PATCH /api/auth/me/disable-coach` body migrated to Zod + `returnAllZodIssues` for the uniform 422 envelope. The `eslint-disable react-hooks/rules-of-hooks` line in `useDisableCoach` is replaced with a shared `useQueryClientMounted` helper that both `useFeatureFlags` and `useDisableCoach` consume. - Withings OAuth callback consumes the nonce atomically. `delete` is issued first; the `P2025` catch handles the replay branch. The previous read-then-delete pattern allowed a theoretical race between two concurrent callbacks with the same nonce. Silent `.catch(() => {})` on delete failures now surface via a Wide-Event warning so a real infra failure reaches the audit trail. - Withings callback `?reason=state` redirect is now differentiated into four distinct reason tags so operators can tell `csrf1` (URL/cookie mismatch), `replay` (nonce already consumed), `expired` (TTL elapsed), and `cross_user` (session/row userId mismatch) apart without DB-shell access. The delete-failure warning template interpolates `err.name` rather than `${err}` so Prisma error messages cannot echo offending values into the audit log. - Withings `connect` endpoint rate-limited to 10 calls per 60 s per user (mirroring the disable-coach pattern); row-create failures redirect to the consistent error surface (`/settings/integrations?withings=error&reason=connect`) instead of bubbling a 500. - `buildPayloadDiagnostic(body)` helper extracted to `src/lib/api-response.ts`. The dashboard widgets PUT route and the measurements series GET route now route their bodies through `redactSensitiveFields` first then call the shared helper — single source of truth for the iOS payload-diff wide-event shape, redaction layered in front composably. - `sanitiseZodIssues(issues, { stripValuesFromMessage: true })` overload added; 14 audit-log sites that wrote `JSON.stringify({ issues })` to `details` now strip the `message` string to a `{ path, code }` pair. Most impactful catch: the `/api/devices` route's `invalid_format` issues were echoing `apnsToken` values verbatim into the audit log. - `disableCoachBody` + `disableCoachData` Zod schemas collapsed into a single `disableCoachFlag` schema in the OpenAPI route table; `zod-openapi` emits the request + response pair from the one source. - `yaml@2` emitter no longer sorts map entries during `pnpm openapi:generate`. Alphabetical sort had placed alias references before their anchors when `.meta()`-tagged sub-schemas appeared inside `z.array(...)`. Output stability is preserved by Zod's declaration-order guarantee. - Worker boot emits a Wide-Event warning if any `integration_statuses` row still has a NULL per-kind counter after the v1.4.47 legacy-column drop. Closes the silent alert-ladder gap where such rows would alert two strikes later than they should. - `MoodReminderCard` status banner auto-clears after 3 s — parity with `DisableCoachCard` (the existing docstring claimed parity that the implementation lacked). - `mergeReorderIntoLayout` emits a dev-only `console.warn` when a reorder id has no matching widget in the layout. Statically unreachable today; defence-in-depth for the upcoming per-tile Suspense refactor that introduces dynamic widgets. ### Fixed - Workout-batch integration tests (`workout-batch-create`, `workout-batch-race`) updated to match the v1.4.42 cross-source dedup semantics. The integration suite had been red on `main` since v1.4.45 (three release commits shipped through it). Suite is green again. - Twelve stale "migration 0076" references in code comments corrected. `consecutiveFailures` sites read 0077; `disable-coach` sites read 0078. Comment-only sweep, no behavioural change. - `docs/ops/attempt-number-semantics.md` referenced migration 0076 for the dropped `consecutiveFailures` column; the actual migration is 0077. - Per-channel APNs "Send test" button (Settings → notification channels) now fires with `eventType: "MEDICATION_REMINDER"` so the dispatcher's `time-sensitive` interruption-level + `apns-priority: 10` branch runs. The previous `SYSTEM_ALERT` event-type used the dispatcher's default `active` level, which iOS may summarise into the Notification Center rather than presenting a lock-screen banner — leaving the user with no signal whether a real dose reminder will surface visibly. A successful test now exercises the exact path production reminders take. Title and body strings still read "Test notification" so the push cannot be mistaken for an actual scheduled dose. ### Performance - Cold-fallback `/api/analytics` slim slice — `aggregates` query split so the windowed columns get a 90-day outer `WHERE` cap, matching the inner `FILTER` windows. Multi-second TTFB on cold mounts drops to sub-second on tenants with large measurement partitions. The `narrows` query received the same cap in v1.4.47.1. ### Security - Withings `connect` rate-limited (per-user 10 / 60 s) to bound ledger-row growth from a misbehaving client. - Coach `disable-coach` validation envelope unified through `returnAllZodIssues` for a uniform 422 shape. ### Observability - `dashboard.widgets.validation-failed` and `measurements.series.validation-failed` audit rows now carry `received_keys`, a truncated `received_shape_excerpt` (256-char hard cap), and the sanitised Zod issues. The excerpt is generated through `redactSensitiveFields` so credential-shaped keys (password / token / secret / apiKey / authorization / csrfState / nonce) land as the literal `"[redacted]"` instead of their raw values. One log line per validation failure carries both the iOS-sent shape and the server's rejection reason, so iOS serialiser drift can be chased without DB-shell access. - Push attempts persisted in `push_attempts` for every channel (APNS / Web-Push / Telegram / NTFY). The diagnostic endpoint reads the last 20 per user; the daily cron at 03:35 Europe/Berlin prunes attempts older than 90 days. Operators can now grep the wide-event `notifications.send` annotation OR query the table directly for an APNs incident triage. - Withings callback redirect reasons emit matching `meta.reason` annotations (`csrf1` / `replay` / `expired` / `cross_user`) so the wide-event stream carries the same differentiation the URL query param does. ### Documentation - `NEXT_PUBLIC_APP_VERSION`, `NEXT_PUBLIC_APP_BUILD_SHA`, `NEXT_PUBLIC_APP_BUILT_AT` documented as build-time-only env-vars in `docs/self-hosting/reverse-proxy.md`. The Coolify runtime entry was misleading; Next.js bakes the values into the bundle at build time via the `next.config.ts` `env:` block. ### Test hygiene - New grep-discovery test mirrors the `flags.coach` discovery test for `user.disableCoach` / `useDisableCoach`. New sites must be added to the allowlist explicitly so CI fails on an unreviewed addition. - `target-card.tsx` per-card "Ask the coach" CTA pinned in the user-disable fixture (the cascade fixture already covered the page-level gate). - `textarea` `forwardRef` `Symbol.for("react.forward_ref")` sentinel assertion deleted (React-internal API). Ref forwarding is covered indirectly by the call sites that pass `ref` to `