Files
HealthLog/CHANGELOG.md
T
Marc-André Bombeck 572f5176bd test(concurrency): cover the layout and about-me write guards
Add per-endpoint unit tests for the insights layout, medications layout, and
coach about-me write guards: an interleaved stale-base writer gets a 409 and
changes nothing, a tokenless request still writes (older-client compatibility),
the fresh token is echoed, and a malformed token 422s. Cover the client token
echo, the two 409 dispositions, and the insights read that must keep the token
through the resolver. Refresh the CHANGELOG and the generated OpenAPI spec.
2026-07-24 16:39:39 +02:00

894 KiB
Raw Permalink Blame History

Changelog

[Unreleased]

[1.32.21] — 2026-07-24

Insights and medications layout saves, and the coach about-me note, are now protected against a concurrent edit overwriting a newer one. This is the same protection the dashboard layout already had. If you save a layout in one place while something else saved a change to the same record a moment earlier, your save now stops and reloads the current state instead of quietly reverting that other change. Older clients that do not send the new marker keep working exactly as before.

  • Insights layout saves no longer clobber a concurrent edit. Saving the overview arrangement and saving the pill order are two separate surfaces that write the same record. If one lands while the other was based on an older read, the later save now reloads and asks you to redo it rather than reverting the first.
  • Medications layout saves get the same guard. The card/table view toggle and the manual order both write one record, so a stale toggle can no longer resurrect an order you just saved, and the reverse holds too.
  • The coach about-me note is guarded as well. A save based on an older read is stopped and reloaded instead of overwriting a newer version of the note.
  • Older clients are unaffected. A request that does not carry the new base marker keeps the previous save behavior unchanged, so nothing needs updating to keep working.
  • The dashboard layout contract is now in the published API spec. It was missing from the OpenAPI file even though it was the first surface to get this protection.

[1.32.20] — 2026-07-24

The medication-adherence insight now passes through the same safety screen as every other insight card. It was the one card of eight that showed the model's text without running that screen first. It already held back a reply that came back broken or cut off, but a dose-change instruction or an invented figure could still reach it. It now runs the same check the other seven cards run, and anything that trips the check falls back to the plain, data-grounded line the card shows when no model is set up.

  • The medication-adherence card is screened like the rest. A reply that reads as a dose change, or that cites a made-up risk figure, is held back and the card shows its plain fallback. A normal grounded summary still shows as before.
  • Two background notification lines can no longer print a raw model reply. The "just in" reaction line and the proactive coach nudge now catch a reply that arrived as structured data or got cut off partway, and use their fixed wording instead of printing it as is.
  • Smaller cleanups in the same area. A stray marker left behind by a cut-off coach reply is now stripped from the text, and an unreachable branch in the adherence figure was removed.

[1.32.19] — 2026-07-24

After you logged a dose, the Today view could keep showing it as still due until you reloaded the page. The Today hero and the dashboard read from a cache that refreshes on a timer, and a write made from another screen marked that cache stale without actually re-reading it. The change reached the server, but the screen you came back to still showed the old state for up to two minutes, or until a hard refresh.

  • A dose you take now clears from Today at once. This covers the "Take all due" button on the medications page, editing or deleting a logged dose, and importing intake history. The Today view and the dashboard now re-read the moment the write lands instead of waiting for the timer.
  • A preventive-care reminder marked done leaves the Today rail immediately. Marking one off, editing it, or deleting it updates the Today view straight away rather than after a refresh.
  • A lab reading you add updates its cards right away. Adding a reading refreshes that marker's assessment on the same page and the "what changed since your last panel" card, instead of leaving them on their earlier text.
  • A briefing you regenerate shows up on Today without a reload. The new briefing now reaches the Today hero and the dashboard as soon as it is ready.
  • Saving a dashboard layout keeps its safeguard against a conflicting edit. A visit to the home page could leave the layout editor without the token that guards a save, which quietly turned off the protection added in v1.32.16 against two edits overwriting each other. The editor now always loads its own copy, so the guard stays on.

Thanks to @lutzkind, whose run of reports surfaced the stale Today view.

Refs #581

[1.32.18] — 2026-07-24

When a Fitbit token was revoked or expired, the hourly sync noticed the failure but then stamped the same cycle as a success. The connection flipped back to connected, the sync position moved forward, and after you reconnected the catch-up window could no longer reach the days that were missed. So the data was skipped for good, and in the meantime the failing token was retried every hour.

  • A revoked Fitbit token no longer reports a successful sync. A failed token refresh now fails the whole cycle. The connection stays parked until you reconnect, the sync position holds instead of advancing past real data, and the once-per-failure alert stops repeating every hour.
  • Data after a reconnect is no longer skipped. Because the position is held back on a failed cycle, the next successful sync still covers the days that were missed rather than starting after them.
  • A write failure during a sync holds its place too. If rows were fetched but could not be saved, the cycle keeps its position so the next run fetches them again instead of letting them fall outside the catch-up window.
  • An interrupted history backfill no longer marks itself complete. A partial first-time import is retried until a full pass finishes, so the one-year history walk fills in rather than freezing halfway.

If your Fitbit history already has a gap from before this release: reconnect Fitbit if prompted, then open Settings, Integrations, Fitbit and run a full sync. It re-walks the last year and fills the gap in place. Gaps older than a year are outside the backfill window and cannot be recovered.

[1.32.17] — 2026-07-24

The mood correlations on the Insights page could line up a day off. Your mood is dated by the calendar day you logged it in your own timezone, but the metrics it was matched against were still grouped by the UTC day. If you live away from UTC, an evening reading slid onto the next UTC day, so a mood and a reading from the same evening were paired as if a day apart. A regression from v1.32.12, when the mood side moved to local days on its own.

  • Mood now pairs with metrics from the same local day. The blood pressure, weight, and resting pulse series that mood is correlated against are grouped by the day you logged them in your own timezone, so both sides of the pairing share one calendar. An evening entry no longer drifts onto the next day.
  • Non-UTC users get the correct same-day figures. For anyone west of UTC an evening reading used to pair with the wrong day's mood, turning a same-day relationship into a one-day-apart one. Those correlations and their scatter charts now reflect the day the data was actually recorded. UTC users see no change.

[1.32.16] — 2026-07-24

Changing which tiles and charts appear on your dashboard could get undone if you also toggled a hero score ring at about the same moment. Both actions save the whole layout, and the ring change was built from an older copy, so whichever reached the server last won. Thanks to @lutzkind for the clear report.

  • A saved layout now stays saved. Your dashboard save is treated as the authoritative one. If another change arrives after it but was based on an older version of the layout, the server declines that write rather than letting it overwrite your work. The ring toggle also carries only its own fields, so it can no longer pull stale tiles along with it.
  • The two saves take turns instead of racing. While a layout save is in flight the score ring controls are held, and while a ring change is saving the Save button waits, so the two writes can no longer overlap on the server.
  • A change underneath you reloads instead of getting lost. In the rare case where another tab saved first, the layout refetches from the server and a small note asks you to make your change again against the fresh copy.

Refs #581.

[1.32.15] — 2026-07-24

Your Polar training sessions now sync into HealthLog, alongside the recovery, sleep, and daily activity that Polar already brought in. Thanks to @StefanIndustries, whose report asked for this after the vitals sync landed.

  • Polar exercises arrive as workouts. Every run, ride, swim, or gym session you upload to Polar Flow shows up in your workout history with its sport, start time, duration, distance, calories, and heart rate. The raw Polar sport label and training load ride along in the background so nothing is lost. The sync reads your last 30 days of uploads once an hour, so a session synced late from your watch is still caught.
  • A Polar session does not double up with your other sources. If the same workout also reaches HealthLog through Apple Health or another connection, the two are matched and only one is shown. A Polar watch counts as a direct heart-rate recording, so it ranks above a Strava re-upload of the same run but below your primary on-wrist wearables. You can change that order under Settings, Sources.
  • The two Polar halves stay independent. A hiccup fetching your training sessions no longer holds back the recovery and sleep sync, and the reverse holds too, so a problem on one side never masks the other.

[1.32.14] — 2026-07-24

When the Coach removes a number it could not check against your data, the reply now reads clearly and tells you what happened.

  • A withheld figure reads as a quiet omission. The Coach checks every number in its answer against the figures it was actually shown, and holds back any it cannot verify so a drifted value never reaches you as if it were real. That held-back number used to show up as an internal [unverified] marker in the middle of the sentence. It now appears as a short editorial mark, and a small muted line under the message tells you that some figures could not be checked and were left out. The line is worded for each of the six languages and stays out of the way until there is something to say.
  • Coach action cards and the tool trace survive a reload. A suggested reminder, a confirm-to-create action card, and the "what I looked at" tool trace were saved with the message but dropped when the conversation was reopened, so they quietly vanished on reload. They are now restored with the rest of the message, so reopening a thread shows the same cards and trace it showed the first time.

[1.32.13] — 2026-07-24

An Insights assessment card no longer shows the raw model output when a response comes back incomplete. If the underlying text was cut off, the card now falls back to its computed summary instead of printing the unfinished data.

  • A cut-off assessment stays hidden. The per-metric assessment cards, like the one for resting pulse, ask the model for a small structured reply and then read the summary out of it. When that reply arrived truncated, the reader could see the whole envelope on the card, including the property name, the braces, the quotes, and the escaped line breaks. The card now recognises an incomplete or malformed reply and serves the deterministic summary it already falls back to when no model text is available, so the raw output never reaches you.
  • Plain-text assessments are untouched. A model that answers in an ordinary sentence rather than the structured shape is still shown as written, so this change only affects broken replies and leaves normal ones exactly as before.
  • The same guard covers the other cards. Medication compliance and the derived-score assessments read their summary the same way, so they now hold back a broken reply too rather than persisting it as the day's text.

Thanks to @lutzkind for reporting this one.

[1.32.12] — 2026-07-24

A mood you log late in the evening now lands on the day you logged it, in your own timezone, both on the dashboard and in the Coach's mood view.

  • Your late-night mood stays on the right day. The pre-computed mood history used to file each entry by its UTC date, so a mood logged after dark in a timezone that runs ahead of or behind UTC could jump to the neighbouring day once the nightly rebuild ran. Every mood now files under the calendar day it was logged in your own timezone, matching the raw entry list.
  • The dashboard and the Coach agree. The daily mood series behind the dashboard tile and the Coach's mood view now read the same day for an entry as the entry list does, so a value no longer shifts by a day when the faster pre-computed path takes over from the live one.
  • The correction applies itself. On upgrade the pre-computed mood history is cleared and rebuilt under the corrected keying. While it rebuilds, mood reads fall back to the live calculation, which was already correct, so there is no window where the wrong day is shown.

[1.32.11] — 2026-07-24

Signing in to the iOS app on a self-hosted domain now runs through your own web login, so saved passwords and passkeys behave the way they should.

  • Sign in through your own domain. On a self-hosted instance the app now opens your instance's real web login page inside a secure in-app browser. Because the login happens on your own origin, your password manager files the credentials under the right site and passkeys can run at all, which the older native form could not do on a custom domain.
  • A single-use handoff, not a token in a link. Once you have logged in, the server hands the app a one-time code that lives for about ninety seconds and can be redeemed only once. The access and refresh tokens never ride a URL. The whole exchange mirrors the existing native single sign-on handoff and reuses its protections, including the per-request PKCE binding.
  • The login has to happen inside the flow. The app receives a code only when you actually authenticate during that browser session. A login left open in the background from before cannot complete the handoff by itself, so a stray link can never quietly sign the app in as you.
  • Your second factor still applies. An account with an authenticator app or a security key completes that step on the web page before any code is issued, exactly as it does everywhere else.

[1.32.9] — 2026-07-23

The Coach output guard now remembers the numbers you were actually shown across a whole conversation, so figures you legitimately carry forward stop getting marked unverified while an invented one still gets caught.

  • A figure from earlier in the chat is no longer flagged when you bring it back up. The guard now keeps a running record of every number the Coach was given this turn and in earlier turns, plus your goal figures, your medication doses, and the reference bands it was shown. A value it can trace to one of those is left alone even a few turns later. A value the Coach only wrote in an earlier reply is never trusted on that basis, so a number that once slipped through cannot quietly become authoritative later.
  • An educational sentence keeps its numbers. A line that states a general guideline, like the note that adults usually need seven to nine hours of sleep, is recognised as a reference and left intact instead of having its numbers stripped. This works in every supported language.
  • A dose you are actually on reads as a restatement, a different one does not. When the Coach says to keep taking a dose, the reminder is allowed only when the amount matches a dose on your schedule. A wrong maintenance amount, or any wording that nudges the dose up or down, is still held back.
  • Fabricated risk numbers are caught in more languages. The screen that blocks an invented clinical risk figure or a named risk-engine result now reads French, Spanish, Italian, and Polish to the same depth it already read English and German, including a percentage written out in words.

[1.32.8] — 2026-07-23

Health records written through the app now carry where they came from, and a HealthKit sync can say what set it off.

  • A dose logged from the app is recorded as an app entry. Every intake used to be stored as a web entry no matter how it arrived. The server now reads the transport it authenticated the call with and tags the dose to match, so a dose logged in the native app reads as an app entry while one logged in the browser stays a web entry. A reminder confirmation, an import, and an Apple Health mirror keep their own labels. The value is derived on the server and can never be set from the request body.
  • The dose history shows how each dose was recorded. The per-dose history returns a source label on every intake, so a client can show at a glance whether a dose came from the web, the app, a reminder, an import, or an Apple Health mirror. Rows written before the label existed report as unknown rather than guessing.
  • A HealthKit sync can report what woke it. The batch upload accepts an optional trigger tag reading foreground, background, or push. It is recorded for diagnostics only and never changes which readings are stored or how they are deduplicated, so sending it or leaving it off produces the same result.

[1.32.7] — 2026-07-23

The Coach output guard now reads numbers the way you actually write them, so a correct figure stops getting marked unverified while an invented one still gets caught.

  • A number you reformatted is no longer flagged. The guard recognises a rounded restatement, a delta narrated as a drop, an adherence rate written as a percent, minutes read back as hours, a reformatted date, and a thousands-separated step count, each checked against the figure the server actually gave the Coach. A drifted number, like an average of 128 quoted back as 138, is still caught.
  • A blood-pressure claim can no longer borrow a weight number. Every figure is matched inside its own unit family, and a raw reading never earns the looser rounding reserved for a headline average, so a fabricated average cannot slip through on a nearby sample.
  • A correct clinical-risk refusal streams through untouched. Naming ASCVD or a ten-year risk to explain that a clinician computes it now passes. What still blocks is an asserted figure or a categorical verdict, whether it carries digits ("your risk is about 14%"), spelled-out words ("roughly twelve percent"), or no number at all ("SCORE2 would put you in the high-risk band").
  • A dose you already take reads as support. "Keep taking your 7.5 mg as prescribed" passes, while anything that moves the dose stays blocked.
  • A figure removed as unverifiable leaves the sentence intact. The strip touches only the exact token, so a year like 2023 is never clipped by a flagged 23.

Refs #587, #591.

[1.32.6] — 2026-07-23

Hardening and data-integrity fixes that were reviewed earlier and are now folded into the trunk.

  • Outbound Web Push dials through the guarded network path. A push endpoint now goes through the same safeFetch wrapper the rest of the app uses, so an operator-configured endpoint cannot be pointed at an internal address.
  • A bulk mood edit saves all at once. The mood rows and their tag links are written in one transaction, so a failure part way through no longer leaves a partial save.
  • Nutrient entries are bounded on write and included in the backup. Values are range-checked when saved, and the per-day nutrient records now travel with the full backup and restore.
  • Every response path carries the baseline security headers, including the early returns that used to skip them.
  • Provider re-sync tolerates an empty token-refresh body. Oura, Polar, and Fitbit treat an empty body as transient rather than a hard failure, so a reconnect is not lost to a blank response.
  • The Coach reply stream aborts cleanly when you navigate away, and the daily metric series carries its own min and max.
  • Also folded in: the WebAuthn relying-party origin gating (localhost only in development), the medication-compliance point-window bound, a request-time future-date bound on the medication-intake import, and pinned regression tests for the snapshot-cache tenant key, the GlitchTip path-secret redaction, and the Withings credentials delete.

No migrations. No breaking changes.

[1.32.5] — 2026-07-23

  • Browser SSO sign-in no longer fails on a duplicated callback. With the service worker's navigation preload enabled, a single-use OIDC callback could be requested twice. The first request redeemed the authorization code and set the session. The second hit the identity provider with the now-consumed code and failed, so every successful sign-in landed on an error page. The service worker now settles one-shot auth navigations itself, so the callback reaches the server exactly once. The callback is also idempotent: a duplicate that already holds a database-valid session is sent into the app instead of erroring, while a forged or session-less state still fails closed and never redeems a code.

No migrations. No breaking changes.

Thanks to @doenke (#600), whose report pinned the cause (service worker navigation preload plus the single-use OIDC code) down to the exact mechanism, so the fix went in as reported.

[1.32.4] — 2026-07-23

  • The Coach's numeric verifier stops flagging figures it was actually given. A restated difference, average, or percent change computed from two of the numbers the model was shown, a sample count restated from the data inventory that rides every turn, a calendar-date ordinal ("July 21st"), and a numbered-list marker no longer trip the inline [unverified] rewrite that was appearing in place of correct values. A genuinely fabricated figure, one with no grounding at all, is still caught and stripped.

No migrations. No breaking changes.

[1.32.3] — 2026-07-23

Security release. It ships on its own so the change to how a second factor is proven can be watched in isolation.

  • A native client can manage its own second factor. The iOS app can now turn two-factor authentication on or off, add or remove a security key, and regenerate recovery codes over its API session. Those actions were reachable only from a browser before. It does so through a short-lived, single-use elevation the app earns by re-proving a factor, never by presenting its token alone.
  • The re-proved factor decides what the elevation reaches. Re-proving with a password reaches exactly what a plain browser session reaches. The actions a browser gates behind a completed second factor now require the same over the API: a re-proved TOTP code or security key, never the password alone. Administrative endpoints remain reachable only from a browser session, unchanged.

One migration (0267), additive. Addresses the native 2FA-management gap (healthlog-iOS #57).

[1.32.2] — 2026-07-23

Security release. Ships on its own and ahead of everything queued, because it closes a set of freshly disclosed framework vulnerabilities that affect the running instance today.

  • Next.js updated to 16.2.11, closing four high-severity advisories in the App Router: a middleware/proxy bypass, two server-side request-forgery paths (rewrites and Server Actions), and a denial of service. The middleware/proxy advisory is the most relevant here, since the request edge enforces the public-path allowlist and the authentication redirect. No application code changed; this is the dependency patch alone.

Carries the fixes already merged for 1.32.1 (honest health-score absence, Coach score questions, Apple Health import and Pulse fidelity, three silent data-loss races), since those reached the trunk first. No migrations beyond what 1.32.1 declared (none). No breaking changes.

[1.32.1] — 2026-07-23

  • The Personal Health Score reads "not enough data yet" instead of zero. An account with records that do not yet meet any score pillar's minimum (for example a single mood entry, no paired blood pressure, one weight reading) showed a red 0 rather than an honest absence. The score is now withheld until at least one pillar is computable; a genuinely computed 0 is still shown.
  • The Coach stops refusing ordinary questions about your own scores. A question about a Sleep Score or Health Score no longer trips the fabricated-clinical-risk refusal; the guard now matches the specific risk calculator it was meant to catch, not the plain word "score", and still blocks a genuinely invented risk figure.
  • The Apple Health import no longer stalls at "unpacking". A large export is now unpacked as a stream rather than decompressed whole into memory on the shared event loop, and a job left mid-unpack by a restart is picked up by a periodic reconcile instead of sitting stuck with no error. A byte ceiling still guards against a decompression bomb.
  • The Pulse insights page shows pulse, not resting heart rate. It no longer silently swaps its main chart to resting heart rate when any resting data exists; resting heart rate appears as a clearly labelled second series when present, and the resting-calibrated target band no longer paints behind raw pulse readings.
  • Dashboard layout changes can no longer be reverted by a concurrent score-ring save. The instant score-ring toggle now sends only the ring fields instead of resending a cached snapshot of the whole layout, closing a race where an in-flight ring update could land after a tile or chart Save and silently restore the older layout.
  • Measurement-reminder cadence edits recompute against the cadence that was actually saved. Clearing an interval or a recurrence rule to switch a reminder's schedule no longer leaves the next-due date computed from the just-replaced value for one cycle.
  • ntfy notification settings keep a saved auth token across unrelated edits. Toggling the channel, or editing the server URL or topic, no longer silently clears a previously configured token.

No migrations. No breaking changes.

Thanks to @lutzkind (health-score zero-state, the Coach refusing score questions, the dashboard layout race, the Pulse page) and @Nazza01 (the Apple Health import stall). These fixes came straight from their reports.

[1.32.0] — 2026-07-22

  • Provider ingestion and daily reactions now preserve the correct identity. Arrival reactions are bound to the exact marker revision, workout selection matches field donors to the correct surviving session, long-range buckets honor the user's timezone, MCP pages stay stable, and one Withings subscription repair can no longer block fallback polling for other users.

  • Large reads and writes are bounded end to end. Measurement exports stream in pages without splitting sleep sessions, oversized projections cannot evict the shared cache, dense reports and metric series use database aggregation, upload reads have byte, time, and concurrency limits, full-history backfills share admission control, and medication imports use durable chunks with resumable, bounded final rollups.

  • Capture and history flows are complete on mobile and desktop. Workout history loads beyond the first page, metric pages open preselected capture forms and return to their origin, failed water submissions retain their draft, and confirmed discards clear it.

  • Interactive forms share one safe dismissal contract. Dirty forms require confirmation across tracked Back and Forward traversals, Apple Health setup exposes successful native syncs, document selection is consumed on Coach dismissal but preserved across full-page Coach navigation, conversation titles can be renamed with rollback, and hidden tabs refresh meaningful server state.

  • Accessibility coverage now exercises representative authenticated routes, themes, dialogs, and sheets. Small text keeps compliant contrast, OCR uses a visible labeled control, workout sections use real headings, and compact actions retain 44-pixel touch targets.

  • Client and API boundaries are explicit. Localized stable error codes cover measurement, mood, lab, and onboarding failures; client fetches and query keys are lint-enforced; AI overrides are documented; and large Coach, report, target, and provider modules now use one-way focused builders.

  • Production image processing uses the patched Sharp 0.35 runtime. This closes the current high-severity libvips advisory chain inherited through Next.js while retaining verified production-build compatibility.

Migrations 0264_whoop_owner_identity, 0265_withings_subscription_state, and 0266_medication_intake_import_jobs. No breaking changes.

[1.31.8] — 2026-07-21

  • MCP search cursors keep lab results stable across pages. Distinct analyte names are ordered before offset pagination, preventing repeated or skipped lab entries when a client follows nextCursor.
  • Opening the medication wizard no longer risks clearing entered fields. The one-shot URL flag is removed without starting a second page navigation, so slower browsers keep the active wizard instance and its form state.

No migrations. No breaking changes.

[1.31.7] — 2026-07-20

  • Current OpenAI reasoning models use their supported request contract. Official GPT-5, o1, o3, and o4 models receive max_completion_tokens without unsupported temperature or seed fields; compatible custom gateways retain the legacy parameters they advertise.
  • Apple Health XML cumulative totals no longer add overlapping sources. Imports aggregate each source/day independently and select the highest source subtotal, while explicit provenance prevents XML estimates or late legacy samples from overwriting authoritative native HealthKit statistics. Existing legacy aggregates can be repaired by re-uploading their archive.
  • Every currently actionable medication reaches Today. Equal-time doses no longer collapse to one row, overdue doses win deterministic ties, and weekly or custom intake windows use the scheduling engine's real availability boundary. The shared digest fixes both the web start page and iOS Home.

Migration 0263_apple_health_aggregate_authority. No breaking changes.

[1.31.6] — 2026-07-21

  • The hardened runtime image can run database migrations again. The isolated Prisma CLI now exposes its pinned dotenv and prisma/config dependencies to the copied production configuration while package managers remain absent from the final image.

No migrations. No breaking changes.

[1.31.5] — 2026-07-21

  • The production image no longer ships build-time package managers. pnpm 11 verifies the lockfile under explicit build-script policy, npm and Corepack are removed after migrations are prepared, and linked worktrees stay out of local Docker build contexts.

No migrations. No breaking changes.

[1.31.4] — 2026-07-21

  • Web and background processes now start and recover predictably. Invalid encryption-key configuration fails before the service accepts traffic, worker-only processes report their real health, connection limits are divided safely across replicas, and provider backfills keep their bounded queues during reconnects.
  • Published images are reproducible and verified before promotion. The release pipeline builds one multi-architecture image, validates its digest, startup, migrations, health, and reported version, and only then promotes that exact artifact to production.
  • CI now uses isolated, deterministic test environments. Integration tests receive their own database and cryptographic settings before application code loads, worktrees install hooks consistently, and browser tests use the intended application process on every platform.
  • Concurrent iOS registrations no longer fail intermittently. Device and APNs notification-channel reconciliation now share one database lock and transaction, so simultaneous first registration remains idempotent.

No migrations. No breaking changes.

[1.31.3] — 2026-07-20

  • Document summaries now stay in the language you chose. The same account language is used whether a summary is created in the background or requested from the document sheet. A saved summary remains visible and can be replaced with Generate again after you switch languages.
  • The Coach composer has more room on phones. The writing field now gets its own line, with document and conversation actions below it and Send on the right. The unreliable browser dictation button and its dead-end permission states are gone.

No migrations. No breaking changes.

[1.31.2] — 2026-07-20

  • Provider imports now commit identity and progress atomically. Withings, WHOOP, Oura, Polar, Strava, and Apple Health reconcile external and natural measurement identities under database locks, preserve tombstones, emit arrivals only for genuine inserts, and advance cursors only after durable writes. OAuth refresh-token rotation is serialized per user and provider.
  • Webhook and import success now means the source data is durable. moodLog writes before acknowledging delivery, while Withings ECG callbacks enter a durable queue with bounded retry and replay-safe processing. WHOOP backfills commit bounded chunks without advancing failed resource cursors, and legacy mood imports use deterministic, collision-free retry identities. Operational JSON import failures are reported as failures instead of misleading duplicate skips.
  • Recovery is complete, exact, and owner-safe. Canonical backups round-trip every supported record class, including encrypted document content, measurement reconciliation identities, and tombstones. Restore rejects account mismatches before writing, validates backup enums at the boundary, processes stable measurement IDs in bounded batches, and preserves recovery integrity across both uploaded and off-host payloads.
  • Device and mood edits no longer lose related state. Same-user APNs registrations merge transactionally without crossing account boundaries, refresh-token bindings follow the surviving device, and mood edits preserve rated factors unless the caller explicitly replaces them.
  • Polar sync follows the current AccessLink collection API. Collection requests no longer include the obsolete user ID path segment, and current activity, Nightly Recharge, cardio-load, sleep, and SpO2 response shapes are normalized without losing stable measurement identity.
  • The July dependency bundle is included. PDF.js 6 now releases its loading task correctly; Canvas 1.0, Radix UI 1.6.2, Tailwind CSS 4.3.3, the Tailwind Prettier plugin 0.8.1, and setup-node 7 are integrated.

No migrations. No breaking changes.

[1.31.1] — 2026-07-19

  • Arrival events now follow committed inserts across every writer. Provider re-sync updates stay silent, partial Apple Health imports retain arrivals from earlier committed batches, all newly inserted workouts emit, and sleep refreshes distinguish new stages from updates. Web-only processes now retry their bounded send-only queue connection instead of permanently losing arrival jobs after one transient startup failure.
  • Reaction and workout generation are single-spend operations. Durable database claims close worker races and provider-retry gaps, daily token reservations are atomic and reconciled on every terminal path, and both generation and display honor the user's Insights and Workouts module choices plus server-managed consent.
  • AI context is narrower and more accurate. Arrival reactions use the record that actually landed, completed sleep uses the reconstructed night rather than one stage segment, user-authored lab fields are fenced as data, workout comparisons use canonical sessions and dates, and every metric and derived-score coach entry carries its exact supported scope.
  • Dashboard state survives old and partial clients. Saved comparison range points and newly supported metric widgets are preserved, while Today freshness remains aligned across local-day boundaries, DST, tab focus, and split web/worker deployments.

Three migrations (0260, 0261, 0262). No breaking changes.

[1.31.0] — "The record that reacts" — 2026-07-19

A milestone release: the record now visibly responds when something lands, instead of waiting for a daily rollup to notice.

  • Today shows what just came in. A completed night's sleep, a workout, a first weigh-in of the day — the home screen now carries a calm "just in" note for a few hours after each one, and at most one written line a day per kind, grounded in your own numbers. Nothing changes if you run without an AI provider: the sentence is the garnish, not the feature — the arrival itself still appears, including on an otherwise-empty record, and an open dashboard refreshes it as soon as the tab becomes visible again. The underlying change that makes this possible without new polling or a flood of background work: every write path that can produce a genuinely new reading now emits a single typed event, day-scoped and de-duplicated, that a mass import or a provider re-sync produces none of — a ten-year import or a month of catch-up sync stays silent, by construction, not by luck.
  • A finished workout gets its own paragraph. Opening a recent session now shows a short, grounded read of that one activity — duration, effort, where it sits against your own recent sessions for the sport — with no training advice and no target zones. It is generated once, the first time you open a fresh session, never on re-sync and never for older workouts already in your record.
  • "Ask why" reaches every read-only card. Every per-metric status card, every derived-score page, and a finished workout now carries a way straight into the coach, pre-loaded with just that card's own data so the conversation starts already grounded — not with a blank prompt.
  • The lab-marker card and the offline metric texts speak with the same voice as everything else — meaning first, in your own language across all six locales, checked by an automated tone pass that now runs on every change to these surfaces so this kind of drift gets caught before it ships again.
  • The dashboard layout stops discarding a comparison choice on save, and four clinical widgets stop disappearing from a saved layout — both from a save made by a client that does not know those particular fields yet.
  • Mood and BMI reach the app's home screen, and a medication pen can now carry the maker and printed strength that show up in the native pen list.
  • Every new AI call this release can cause is metered and bounded: a daily cap per kind, a duplicate-content check that makes a re-post free, and a token reservation that is always reconciled, including on every failure path — the same discipline the rest of the AI surface already holds to.

Two migrations this release (0257, 0259); an additional dashboard-widget migration carried over from the prior line. No breaking changes.

[1.30.35] — 2026-07-19

  • The metric texts you see without an AI provider now speak your language. They shipped in German and English only; French, Spanish, Italian and Polish fell through to English. These are what nine metric cards render whenever no provider is reachable — no key configured, an outage, a spent budget — so for those readers this text was the app's voice, in the wrong language. Written per language rather than translated: where a sentence could not be formed naturally, that language got its own sentence.
  • The lab-marker card leads with what a value means, like every other card already did. It was the last one still instructed to state the number first.
  • The response length for a metric assessment is set in one place again, instead of one card carrying its own inline pair.
  • The fallback texts open on something honest rather than on an instruction, and the lab-marker card no longer shows the multi-metric overview tip on a single value.

No migrations. No breaking changes.

[1.30.34] — 2026-07-19

  • Rearranging your dashboard no longer wipes the comparison baseline. A layout save from a client that does not know about that setting — the app never sends it — silently reset it to "none". Every field of the layout now has to declare whether an absent value means "clear it" or "leave it alone", so the next field added cannot repeat this.
  • Four clinical widgets stopped disappearing from a saved layout. Pain, grip strength, waist and waist-to-height were dropped by the save as unknown, so a placed widget was lost every time.
  • Mood and BMI reach the app's home screen. Both existed in the record but were absent from the summary the app reads, so those two tiles could not be shown while every other one worked.
  • A pen carries its manufacturer and printed strength. Without them, containers entered on the web could not be shown in the app's pen list.
  • Destructive controls ask first. Signing every other device out, revoking a session, and revoking a trusted device fired on a single tap. A sweep found two more with neither a confirmation nor an undo: a Coach reminder and a travel entry. Deleting a document still does not ask — it can be undone, which covers the mis-tap.
  • Delete confirmations stop overclaiming. Four of them promised the deletion could not be undone while the same screen offered an undo. Warnings that are wrong make the ones that are right easier to ignore.
  • The sleep-debt figure is described correctly again. The calculation changed to a running balance that credits surplus in v1.19.0; the label still said "cumulative shortfall". The wrong description had spread to the Coach, which was reasoning about the number on the old definition, and to the published API description.
  • The glucose reference band can be chosen on the web. The setting already worked and already fed the Coach — nothing on the web set it. It selects a reference range; it is not a diagnosis and is never inferred from a reading.

One migration (0256), additive. No breaking changes.

[1.30.33] — 2026-07-19

Security release. It ships on its own because it changes how a browser session is recognised.

  • The session cookie is a purpose-generated secret. It used to be the session record's own database key — an identifier built to avoid collisions, not to be unguessable, and one that appears wherever that record is referenced. The cookie now carries 32 bytes from the system's random source, and only its hash is stored, so the value in your browser exists nowhere on the server.
  • You are not signed out by this. Sessions from before the change keep working and are simply not renewed, so they run out their normal 30 days and disappear. Every sign-in from this release onward is on the new scheme.
  • The device list no longer hands out working credentials. It identified each device by that same record key — which, for a session predating this release, is that device's cookie. It now shows an opaque handle that is useless as a login, and the same handle is what signs a device out.
  • A refresh token cannot be rotated from a different device, and an expired one is refused. The test that was supposed to prove the expiry passed a token that had never been issued, so it proved nothing: with the expiry check deleted, the suite still went green. It is a real test now.
  • A Nightscout address is checked when you save it, not only when it happens to be probed first. The check itself already refused private, loopback and cloud-metadata addresses.

Operators: rotating API_TOKEN_HMAC_KEY now also signs out every browser session, not just API tokens. One migration (0255), additive.

[1.30.32] — 2026-07-19

  • A withdrawn AI consent stays withdrawn. Opening the AI settings granted consent again if you had taken it back, and recorded it as though you had done it yourself. A withdrawal is now a standing decision that only an explicit act can lift.
  • The web can withdraw AI consent at all. Until now only the app could; on the web, consent could be given and never taken back. The AI settings show what currently stands and offer the control that changes it. Withdrawing takes effect immediately and deletes nothing you have recorded.
  • Documents get read after you switch automatic reading on. Reading was only ever started at upload, so documents already in your vault stayed unread no matter what the setting said. Turning it on now works through what is already there, in batches, asking the same permission as any other read.
  • A quoted passage in a document review is the document's own text. The quote shown for a finding was the wording that came back from the read, not the line it was taken from — so it could be a paraphrase of your own record presented as a verbatim quote. The passage is now looked up in the document itself, and a finding whose source cannot be located says so instead of showing a quote you cannot check.
  • Pages that arrive with your data pre-loaded now say so on the wire. They were never cacheable in practice, but nothing stated it. Anything behind the sign-in wall is now marked private and uncacheable at the edge, so no proxy or future change can hold one person's record and hand it to someone else.
  • Deleting your last reading of a kind no longer leaves stale weekly, monthly and yearly figures. The daily figure went; the longer spans kept the old aggregate indefinitely.
  • The documented API matches what the endpoints return. Twenty corrections, including a total that the measurements list reports in a different place than the specification claimed, four medication figures named wrongly, a missing intake source, and eleven error codes that were never written down.
  • A retry after an unusable AI answer asks for the right shape. The correction prompt demanded fields that do not exist and named none of the ones actually required, so the one corrective attempt steered away from the contract instead of towards it.

No migrations. No breaking changes.

[1.30.31] — 2026-07-19

  • Today shows a dose before it is late. The card only surfaced a medication once it was already overdue, so it could report nothing needing attention while a dose was due. A due dose now reads as a calm note, an overdue one as a warning, and a day with everything taken still shows the all-clear. Reminders are unchanged — they still only fire once a dose is late.
  • The daily briefing stops disappearing. The check that verifies every figure against your own data was looking in fewer places than the briefing draws from, so a briefing quoting your medication adherence or a change since last month was thrown away as unverifiable. It now checks against everything the briefing is built from. And when a figure genuinely cannot be verified, yesterday's briefing stays up instead of the section going blank.
  • The "read the full briefing" link is gone from Today. Tapping the health-score ring already opens the same page.
  • The close button on a document sits flush with the edge again.
  • A document no longer says its summary is being generated forever. Summaries are only started when a document is uploaded while automatic reading is on — there is no catch-up for documents you already had, so the view promised a summary that was never coming, and hid the button that would have made one. It now says what actually happened — not attempted, unavailable, or held back — and offers to generate one in every case. A summary you generate that way is kept.
  • Removed the "read by AI" note at the bottom of a document.

One migration (0254). No breaking changes.

[1.30.30] — 2026-07-19

Four languages get their own words; a failed read stops claiming you have no data.

  • Reminder notifications, the assistant's reply language, chart and calendar labels, and durations were English or German for French, Spanish, Italian and Polish. The translations existed; the code that picked between them only knew two languages. It now uses the account's own.
  • Polish counts correctly. Polish has a third plural form that a two-form scheme cannot express, so counted phrases read wrong. The other five languages resolve exactly as before.
  • A screening history that failed to load said "not taken yet". On a mental-health surface that is a false statement about your own record — you could reasonably conclude you never completed something you did complete. That surface, and a nutrient card with the same flaw, now say the reading failed and offer to try again. Nothing retries silently: a failure you cannot see is worse than one you can.
  • Three sleep-rhythm cards that share one reading each printed the same failure notice; the page now shows it once.

[1.30.29] — 2026-07-19

  • A connector token that may only read is now refused at the write itself, not only by never being offered the write. The protection was real but rested entirely on how the connection is assembled, so a future change to that assembly could have opened writing with no test noticing. Any write added later inherits the check.
  • Names and text read out of your records now carry an explicit boundary when handed to a connected assistant, matching what the app already does for its own assistant.
  • Reading a medication list from a photo measured its cost against the server-wide limit even when you use your own key. It now uses your own limit. The older way of counting that caused this — the third surface to have it — is removed entirely rather than fixed in place, so nothing can reach for it again.
  • Removed a module that read as a check on generated recommendations but had not run for some time. It could not simply be switched on: it required fields the current format does not have, so enabling it would have rejected nearly every generation. The measurement it fed is unaffected and still runs.

No breaking changes.

[1.30.28] — 2026-07-19

  • Daily briefings count against the AI budget. They were the one remaining kind of generated text that did not, so a morning's briefings could run past the limit you set. On your own key they count against your limit, not the server's.

[1.30.27] — 2026-07-19

  • The resting heart-rate chart no longer stops partway. Older readings get condensed after a while, and the chart treated the condensed days as the whole story — so recent days went missing. It now shows both, and says when a day is an estimate.
  • Summaries recompute for the whole day or month. Recomputing from a point in the middle replaced the full period with a partial figure. Affected summaries correct themselves the next time that day is written to, or on the next nightly pass.
  • A repeated daily total in one upload keeps the newer value. It kept the older one.

No breaking changes.

[1.30.26] — 2026-07-19

  • Text read out of your documents is treated as data. Lab field names were passed to the assistant unchecked, so a document whose labels were written to look like instructions could influence later answers. Those names are cleaned now, and document text is marked as yours rather than as something addressed to the assistant.
  • A reminder the assistant picks up from a chat is proposed, not active. It waits for you to confirm it, like its other suggestions. Reminders from before this release keep working unchanged.

No breaking changes.

[1.30.25] — 2026-07-19

  • Generated cards count against the AI budget. Metric cards, biomarker cards, scores and the period narrative were not counted at all. On your own key they count against your limit, not the server's.
  • Switching language no longer regenerates everything twice. The same card in six languages counted as six separate jobs.
  • Long assistant conversations get cheaper again. Past a certain length the full context was re-sent on every turn instead of once.

No breaking changes.

[1.30.24] — 2026-07-19

  • The safety check on generated text covers every card, in every language. It ran on the assistant's replies but not on the metric cards, biomarker cards or document summaries — so a sentence the assistant would refuse to send could still appear on a card. It also only knew English and German wording, leaving four languages unprotected. Both are fixed.
  • A document transcription is deliberately never filtered: it reproduces your own document, and a letter from your prescriber is a record, not advice from us.

No breaking changes.

[1.30.23] — 2026-07-19

  • Switching a module off now also applies to connected assistants. With a module off, a connected assistant could still read that data — including the full doctor summary — and so could a shared clinician link. Both now honour the switch.

No breaking changes.

[1.30.22] — 2026-07-19

  • Background jobs stop piling up on themselves. Duplicate jobs were meant to be collapsed but were not, so a large sync could queue thousands of identical recomputations and a restart could re-queue a running history import. Existing installations are updated on the next start.

No breaking changes.

[1.30.21] — 2026-07-19

  • Dose reminders land on the right hour across a clock change. On the day the clocks change, a reminder could resolve an hour off, and the same dose could end up recorded twice. Nothing stored changed.
  • The next-dose day label was wrong when your phone's timezone differed from the one in your profile.

No breaking changes.

[1.30.20] — 2026-07-19

  • A restore refuses when the backup file names a different account than the backup you selected.

No breaking changes.

[1.30.19] — 2026-07-19

  • Switching a module off now holds everywhere. The sync feed still sent cycle data, the FHIR export still returned the full record, and the dashboard summary still built glucose and sleep cards regardless of the switch. Nothing is deleted — turning a module back on finds the history complete.
  • Two assistant features sent data out without checking your consent first. Both now check, and fall back to their plain text when consent is absent.

No breaking changes. The native app may see a 403 from the FHIR endpoints where a module is off.

[1.30.18] — 2026-07-19

  • Health data no longer reaches the logs or the backups. Cached responses are encrypted at rest — they echo what was just saved, so cycle notes and mood text sat unencrypted in every backup for a day. Diagnostic log entries are now scrubbed where they are written, and a failed AI request no longer records what came back with it.

Log lines and backups written before this release may still contain those values. No breaking changes.

[1.30.17] — 2026-07-19

  • API tokens enforce their scope. A token issued for medication intake reached every endpoint, including the full backup export. Routes that accept a narrow token now have to say so.
  • Revoke and re-issue any token you gave to a third party. Treat it as having had that reach for its whole lifetime.
  • The generic "create API token" button is gone. The token it made never worked for the medication intake it advertised, while reaching everything else. Tokens are issued per medication from that medication's own toggle. Existing tokens stay listed and revocable.

No breaking changes to the web app, the native client or the MCP connector.

[1.30.16] — 2026-07-19

  • Resting heart rate stops being lost. Older heart-rate readings are condensed on a schedule, and a resting value is derived before the raw readings go. A faulty check stopped that derivation after the first run while the raw readings were still cleared, so each run lost another day's resting figure for good. Days already affected cannot be recovered; from here nothing further is lost.

No breaking changes.

[1.30.15] — 2026-07-18

  • Assessments come in the language you set. French, Spanish, Italian and Polish accounts got German or English text instead of their own. Insight cards, the per-metric assessments, scores, the biomarker card and the period narrative now follow your language. German and English are unchanged. Stored texts in the four languages refresh on their next nightly run.

No breaking changes.

[1.30.14] — 2026-07-18

The workouts page joins the pages that paint on first load.

  • The workouts list opens with the sessions already there. It was the last high-traffic surface still fetching its rows after the page mounted, so it showed a loading skeleton first and filled in a moment later. The list is now read on the server and handed to the page populated, the same way the dashboard, insights, medications, and coach pages already work. The list logic moved into a shared read so the page and the API endpoint go through one cached, deduplicated pass instead of two.
  • API docs: the heart-rate bucket example matches what the server accepts. The aggregated heart-rate externalId was documented with an hourly example from its first version; the contract has been 10-minute buckets since v1.30.7. The description, the example timestamp, and the min/max field notes now describe the shipped contract.

No breaking changes.

[1.30.13] — 2026-07-18

More pages that paint on first load.

  • The insights, medications, and coach pages open with their content already there, the same way the dashboard now does — their above-the-fold data is read on the server and handed to the page populated, instead of a shell that fills in after a second round of requests. Nothing that would trigger AI work is pre-run: the daily briefing and the coach's own reads stay on-demand as before.

No breaking changes.

[1.30.12] — 2026-07-18

Faster medication reads.

  • The dashboard's medication section stays fast as your log grows. Its reads now use dedicated indexes instead of scanning the full intake history on every load, so a long history no longer slows down the daily view.

No breaking changes.

[1.30.11] — 2026-07-18

Native single sign-on.

  • The iOS app can sign in through your identity provider (OIDC SSO). Until now, single sign-on only worked in the browser — the native app couldn't complete it, which locked native users out entirely on an SSO-only instance. The server now supports a native sign-in handoff (an in-app authorization flow that returns the app's normal token, never a cookie), so the app can offer SSO once it adopts the flow. Existing password and passkey sign-in are unchanged. Multi-factor sign-in still applies, and an SSO session never satisfies a step-up prompt — those still require your own second factor.

No breaking changes.

[1.30.10] — 2026-07-18

Localized insight text, and a richer workout view.

  • Correlation insights now read in your language. The "this tends to go with that — a pattern worth watching, not a cause" lines under a metric were always written in English; they now render in your interface language (German, Spanish, French, Italian, Polish), with the association-not-causation framing preserved in each.
  • A workout you open shows more of what was recorded. The detail view now draws the session's heart-rate curve over time, its route as a clean line, the time spent in each effort zone, per-kilometre splits, and how it compares to your own average for that sport — all from data already synced, with no new connection. Anything a given workout didn't record simply isn't shown.

No migration. No breaking changes.

[1.30.9] — 2026-07-18

Faster dashboard first load.

  • The top card and the charts paint with their data on first load instead of flashing empty and then filling in. The daily summary and the charts' 31-day series are now read on the server and handed to the page already populated, so there's no wait for a second round of requests after the page appears. The welcome/summary card, likely the slowest thing to show before, now lands with the first paint.

No migration. No breaking changes.

[1.30.8] — 2026-07-18

Hardening for the new heart-rate upload validation.

  • A malformed bucket timestamp is rejected, not silently misfiled. A bucket whose time rolled over (a 24:00, an impossible day) is now refused rather than stored under a non-standard key, where it would have failed to merge with a re-post and could land on the wrong day.
  • The per-window low/high is sanity-checked before it counts. A window's low and high are kept only when they are in a plausible range and sit either side of the average; a glitched reading no longer skews the day's high/low band. The average is always kept.

No migration. No breaking changes.

[1.30.7] — 2026-07-18

Groundwork for a tidier heart-rate history.

  • The server accepts a 10-minute aggregated heart-rate upload. A watch records heart rate every few seconds, and uploading one row per raw sample fills the measurements list with the same value repeated many times a minute. The server now takes one row per 10-minute window — its average, with the low and high of the window — and the within-day pulse curve reads it directly, so the detailed 10-minute chart and the elevated-at-rest note are preserved. The de-duplication reaches the list once the mobile app sends the aggregated shape; existing per-sample history is unaffected and reconciles itself over time.
  • The daily high/low reflects the true spread. A day built from 10-minute windows now reports the real intra-window extremes, not the range of the averages.

No migration. No breaking changes.

[1.30.6] — 2026-07-18

Accessibility pass.

  • Heatmaps read to a screen reader. The mood and medication-compliance grids now expose each recorded day — its date and value — through a hidden day list, so the per-day figures are no longer reachable by pointer only. The chart looks exactly the same.
  • Small labels meet the minimum size. Muted meta text across settings, admin, the coach panel, and the insights cards is lifted off the sub-text-xs sizes; tabular figures and chart chrome are unchanged.
  • A couple of section headings. Two metric-page sections became proper headings, so heading navigation lands where it should.

No migration. No breaking changes.

[1.30.5] — 2026-07-18

Making a shipped feature findable, and a sync-status fix.

  • ECG has a home you can reach. Recordings now have their own page in Insights, a pill in the Heart group that appears once you have a recording, and links to it from the resting-pulse and heart-rate-variability pages. Before this they were reachable from a single spot, and a deep link into the overview could land before the section had drawn. The page is unchanged in substance — the same non-diagnostic, device-attributed view.
  • The "last synced" time stops lagging. A sync driven by an incoming update now records the connection's last-success time straight away, instead of leaving it frozen until the next full poll — so the Settings status and the assistant's integration read reflect a sync that already happened.

No migration. No breaking changes.

[1.30.4] — 2026-07-18

The read-only assistant connector now reaches the full record.

  • More of your data is readable. The connector gained direct reads for nutrition and vitamins, the intraday pulse curve, and stored ECG recordings, plus discovery for a set of device metrics that were collected but not advertised — wrist temperature, cardio recovery and load, day and workout strain, sleep score, breathing disturbances, fall count, the six-minute-walk distance, stair speeds, and energy expenditure. They can now be compared and baselined by name.
  • Correctness. Heart-rate variability resolves whether the source records the SDNN or the RMSSD flavour, so a ring- or strap-only account is no longer reported as having none. A workout logged by two devices is counted once, matching the workouts list — this also corrects the figure the coach sees.
  • Exposure stays deliberate. The mental-health screeners and the environmental signals remain off the connector by construction; a metric is only readable once it has been named for it.

No migration. No breaking changes.

[1.30.3] — 2026-07-18

Follow-up fixes from a full quality re-check of the app.

  • Correctness reaches every surface. The timezone, window, and grain fixes now also cover the coach's correlations, the per-metric cards, and the period narrative (they had kept an older copy). Daily insight cards roll over at your own midnight, not a fixed one. Personal records that a second device had inflated are re-derived on the next start, so a genuine record can be set again.
  • The metrics catalog shows a loading state and a retry on error, instead of briefly reading as "not tracked".
  • Small. One shared info popover, larger tap targets on the view toggles, a notch-safe height fix on the coach history page, and a few wording and translation gaps closed (a body-composition caveat, study citations, the backup scope note).

No breaking changes.

[1.30.2] — 2026-07-18

More audit-backlog fixes.

  • Coach history is fully reachable. The conversation list pages through your whole history and search runs on the server (by title), instead of only the first ~20 loaded chats.
  • Navigation and flows. A document links to the lab values it produced; the two "Recovery" pages are named distinctly and cross-linked; mood, mental wellbeing, and mood insights link to each other; the nutrients card moved out of "Source priority"; the onboarding checklist and tour no longer point at dead ends; asking the coach from a metric or a plan carries that context in.
  • Correctness. Correlations, the intraday chart, achievements, and the nutrients overview now bucket days by your own timezone and lead with the most recent data; a multi-source day is no longer counted twice.
  • Performance. The workouts list caches its de-duplication per filter, so paging through a long history is quick.

No migration. No breaking changes.

[1.30.1] — 2026-07-18

Polish and small fixes.

  • Mobile. Coach and document text wraps instead of overflowing; the intake history is a card list on small screens instead of a sideways-scrolling table; tap targets meet the minimum size; two dialogs became bottom sheets; the workout detail header stacks cleanly.
  • Feedback. Loading skeletons match the content that replaces them (no more layout jump), and the Today priority-card actions show a pending state and can’t fire twice on a double-tap.
  • Remembered choices. A chart’s time range and the measurement type you last logged are remembered; a coach chat deletes with an Undo; a backdated dose can be undone; lab panels get “Save & add another”; pull-to-refresh works on more pages (and a scroll-position bug that made it fire anywhere is fixed).
  • Consistency, accessibility, wording. Spacing and contrast tidied, more empty states use the shared component, headings and focus rings improved, a few German phrasings corrected, the daily line reads more calmly, and several explanations gained their guideline citation.

No migration. No breaking changes.

[1.30.0] — 2026-07-18 — Quality pass

A broad correctness, performance, and polish release, plus two new features. Nothing new is required of you; existing data is untouched.

Added

  • Document summaries. An uploaded document can get a short plain-language summary, generated once at upload and stored encrypted. It runs only when the external-AI document reading you already opted into is on, and never touches older documents.
  • Efficacy chart, readable. The medication efficacy chart scales its axis to the data (a real trend no longer looks flat), gains a “% since start” view, and the history tab reaches your full intake history, not just the last 90 days.
  • A metric catalog. A quiet “All metrics” view lists everything HealthLog can track and which device supplies each — so a metric you don’t have data for stays out of your daily views but is still discoverable in one place.

Fixed — correctness

  • The number shown matches reality. The workouts and measurements lists now lead with the most recent entries (a long history no longer hid recent days), and a metric recorded by two devices on the same day is counted once, matching the dashboard — instead of being summed twice.
  • Strava workouts carry their real sport (cycling, running, …) — like WHOOP already did — with a history backfill, and back-to-back rides no longer collapse into one.
  • Personal records and correlations no longer double-count multi-source days or mix per-reading and daily-total values.

Fixed — medical safety

  • The hypoglycemia alert now uses your own glucose unit; the doctor-report PDF states your own timezone; the crisis card offers Austrian and Swiss lines, not only German ones; and resting-pulse labels drop clinical-verdict wording for calmer, neutral framing.

Fixed — the daily view

  • The Today view is faster (its heaviest reads are now cached instead of recomputed on every refresh) and updates immediately after you log a measurement, mood, or water — not only medication.
  • The intraday heart-rate chart no longer draws a continuous shape across gaps in sparse data, and shows how many readings it’s based on. A failed load shows a retry instead of an empty state.

Fixed — onboarding, dates, consistency, accessibility

  • Connecting a device no longer dead-ends on a fresh account, and the setup steps (including the callback URL) are shown up front.
  • A date from a previous year now shows the year, everywhere it’s displayed.
  • Contrast, chart colours in the light theme, and a handful of duplicated components were tidied; keyboard access and headings improved on several surfaces; the insights and settings menus are grouped more sensibly.

Changed

  • A fuller backup. The full backup now also includes lab results, illness episodes, allergies, family history, workouts, and a documents list. What isn’t included (raw document files, workout GPS/heart-rate detail) is stated plainly.

Performance

  • The Today digest and the insights cards read through the pre-aggregated tier instead of scanning raw rows; batch imports write in one pass.

Two migrations (0250, 0251). No breaking changes.

[1.29.5] — 2026-07-17

Security

  • Hardened the same-origin redirect guard. A crafted relative post-login next path (e.g. /..//evil.com) passed the same-origin check but left a protocol-relative pathname that could re-resolve to a foreign origin when re-used in a redirect or client navigation. The guard now re-verifies the reconstructed path resolves on-origin and rejects it otherwise. Covers the OIDC callback and the password/passkey login redirect.

Fixed

  • OIDC login redirects use the operator app URL, not the request URL. Behind a reverse proxy that does not forward the original host, the login and callback routes built their browser redirects from the address the app was reached at inside the container, sending users to an unreachable internal URL after login. Every OIDC redirect is now built from the operator-set NEXT_PUBLIC_APP_URL, matching the convention the other integrations already use.

[1.29.4] — 2026-07-17

  • The workouts list shows the most recent first. The list was ordered oldest-first, so on a longer history the newest sessions never made the first page. It now leads with the latest workout.
  • Past-year dates show the year. A workout from a previous year now carries its year in the list, so a December date is never mistaken for this year's.

[1.29.3] — 2026-07-17

  • Hydration and micronutrients now have a home. Your fluid intake and the vitamins and minerals your device records get a surface. A water tile on the dashboard shows the day's total with quick-add amounts; a new Nutrients page under Insights shows hydration over the last month, caffeine when you record it, and the vitamins and minerals you actually have data for — each against its reference daily intake where that applies, shown as context, never as a verdict. Manual water entries and device-synced totals now coexist instead of overwriting each other. Sparse logging is never read as a deficiency.

    The nutrients tracking stays opt-in — enable it in Settings, or with the one-tap prompt on the page. Device-synced micronutrients flow once your client sends them.

One migration (0249). No breaking changes.

[1.29.2] — 2026-07-17

  • WHOOP workouts carry their real sport. WHOOP was the only source that didn't translate its activity into the app's own sport names, so WHOOP-tracked rides and runs landed unlabelled instead of as cycling or running. New and existing WHOOP workouts are now mapped, so your bike rides show up as cycling — history included. When the same session comes in from two sources, the canonical row now keeps the richest reading (heart rate, energy, distance) instead of dropping it.
  • Scroll back through the day's heart rate. The intraday heart-rate view gains a day navigator to step through previous days. The fine-grained history now stays for 90 days (was 14); older days show the coarser hourly shape.
  • Clear a card from Today. A milestone, a new ECG, or a tension note can be dismissed from the Today rail once you've seen it; the section tightens when the last one is gone. The cards that need an action stay until you act.
  • Vitals context moves into an info tap. The vitals tiles carry an (i) with the context — your reading against your personal range, and against your age where that applies — instead of a permanent caption. The VO₂ max heading no longer gets cut off.
  • Oura cycle phases feed the cycle tracker. Where your Oura connection provides cycle-phase data, it now fills the cycle tracker as a background source; your own entries always win.

Two migrations (0247, 0248). No breaking changes.

[1.29.1] — 2026-07-17

A round of fixes and small refinements from living with the daily view.

  • Today refreshes right after a dose. Logging or undoing a medication now updates the day's read immediately — the medication line no longer lingers on a stale count until the next reload.
  • A calmer Today hero. The extra score rings are gone; the hero keeps the one health-score ring and a tighter layout.
  • The Coach names the document you attached. Asking about a document now shows its title in the chat instead of a plain "1 document" count, and the attach menu stays within its popover on long labels.
  • A fresh install starts with tracking on. New installs now enable the tracking modules by default — you can still switch any of them off. The external-AI options and the daily push stay off until you turn them on.
  • Insights vitals lose the explainer line. The "your personal normal range is the median…" caption is removed from the baseline tiles.
  • Illness journal: set and edit the end. You can mark an episode's end date, clear it back to ongoing, and backdate the start. When nothing strayed from your personal range, the course section now reads a short settled line instead of appearing empty.
  • The morning refresh covers every sleep source. The refresh that folds last night's sleep into the day now triggers from Google Health, Fitbit, Oura, and Polar as well, not only Withings, WHOOP, and Apple Health. Each skipped refresh is now recorded so a stale morning read can be traced to its cause.

No new data is collected and there is no migration.

[1.29.0] — 2026-07-17 — A daily health companion

This release draws a line under a run of work that turns HealthLog from a place you record your health into one that meets you each day — and it completes that picture with the pieces that make it feel whole.

Over the last releases the dashboard gained a Today view that leads with the day's read, a health score that refreshes the moment last night's sleep syncs, a coach check-in when a plan is due for a look-back, and an optional morning note. Documents came closer to hand — the Coach opens beside them, you can bring several into a conversation, and lab values move between a document and your labs. This release adds the layer that ties it together:

  • Your heart's rhythm, in context. ECG recordings are no longer a page on their own — they're referenced where your resting heart rate and rhythm are discussed, always as the reading your device produced, never a reading of the app's own. A new recording surfaces gently in Today.
  • The shape of your day. Heart rate is now drawn across the day, not just as a daily number — so you can see it settle and climb. When it stays elevated while you're at rest and not moving, Today notes it plainly as possible tension, carefully and never as a verdict.
  • Quiet milestones. Reaching a steady stretch in your target range, or a new personal best, is marked with a calm moment — states you've reached, not streaks to keep, so there's nothing to break and nothing to nag.
  • One piece throughout. The Today view brings back the health rings you choose, each surface carries its metric's own calm colour, and the whole thing reads as one design rather than a stack of features.

Nothing new generates when you open the app — every daily surface reads the insight prepared overnight. No new data is collected; this makes more of what's already yours.

Four changes bring documents and your record closer together:

  • Ask the Coach opens beside your document. Asking about a document now opens the Coach in a side panel rather than jumping to a full page — expand it to full screen whenever you like, and it keeps its place.
  • Bring documents into a Coach chat. From the composer's "+", attach one or more documents — pick from your vault or upload a new one, at the start or mid-conversation — and the Coach answers grounded in them. A conversation with a document runs in its fenced mode: grounded in those documents only, with no access to your health data or tools, so nothing in an uploaded file can reach anything it shouldn't.
  • Lab values from a document, proposed for your labs. When a document you've had read looks like lab work, its values are detected and proposed — one tap to add them to your labs; they're never added without your say-so.
  • A lab scan is also kept. Scanning a lab report into your labs now also files the document in your vault (encrypted, like every upload) and links the two, so the reading and its source stay together.

Migration 0245 (attaching multiple documents to a conversation) accompanies this release; existing document chats carry over unchanged.

Three pieces turn the Today view into a daily loop:

  • A coach check-in. When a coach plan is due for a look-back, it surfaces as a calm card in Today with keep, adjust, or let-go — one tap. Letting go retires the plan quietly; silence never changes it.
  • A morning that reflects last night. The score and read are prepared overnight, but last night's sleep usually hasn't synced by then. Now, the moment it arrives, the sleep-dependent parts refresh on their own — so by the time you look, the day is up to date. Until sleep is in, Today says so plainly instead of showing a stale score.
  • An optional daily nudge. A once-a-day morning notification with the day's read, off by default and opt-in per channel under Notifications. It carries the refreshed digest when sleep is in, never repeats within a day, and is a calm note — never an alert.

All three read the digest prepared overnight; nothing is generated when you open the app.

With the Today view now leading the dashboard, the older opt-in hero card it replaced is retired, so the two never stack. The greeting stays, the tiles and charts are unchanged, and a layout stored with the old hero switched on simply ignores that setting — nothing to redo.

[1.28.53] — 2026-07-16 — A "Today" overview at the top of your dashboard

The dashboard opens with a Today view: your health score, the day's lead read, and a short "worth a look" list — a dose window that's open, an integration to reconnect, a check-up coming due — each a single tap to the right place. The familiar tiles and charts sit just below, unchanged.

It reads the insight that's already prepared overnight, so it loads instantly and never generates anything on open. When last night's sleep hasn't synced yet, it says so plainly and fills in the sleep-dependent parts once the data arrives, rather than showing a stale or empty score. This is the first piece of a single daily view that later surfaces — a morning refresh, an optional daily note — will all build on.

The dashboard gains tiles for readings that were synced and charted but never had a place on the home strip: heart-rate variability, blood oxygen, breathing rate, wrist temperature, and — for anyone with a body-composition scale — muscle mass, body water, and bone mass.

Each tile appears only once you have that reading, so nothing new shows up on an account that doesn't track it, and every tile can be toggled or reordered under Settings → Dashboard like the rest. Tapping one opens its detail view. Nothing changed in how the readings are stored or synced — this only surfaces what was already there.

[1.28.51] — 2026-07-16 — Chatting about a document lives in the Coach now

Asking the Coach about a document used to open a separate, half-finished chat panel that never showed up in your Coach history. It now opens the real Coach: the conversation appears in your Coach list like any other, marked with a small document badge and a "Chatting about: