The snapshot's fast phase gains a medsToday block — active count,
today's scheduled/taken/skipped tally after the idempotent projection,
and the earliest server-computed next due across active medications —
and the warm phase derives a compact health score from the coverage
map and graded blood-pressure windows the extras build already holds.
A pure verdict resolver ranks the day's single highlighted item over
nine rungs from an overdue dose down to a quiet-day fallback, with
fixed crisis floors for the blood-pressure rung and timezone-correct
day checks. The layout grows a heroVisible flag riding the stored
widgets JSON, the iOS summary route adopts the shared dose-state
builder with an unchanged wire shape, and the new sub-blocks report
their build time alongside the existing snapshot timings.
Per-status texts check AI consent before the unchanged-data refresh can
re-stamp them, the comprehensive fingerprint folds in the comparison
baseline and generation language so toggling either regenerates once,
and profile, threshold and source-priority writes evict the analytics,
targets and derived caches they feed. CSV export neutralises spreadsheet
formula prefixes on free-text cells while numeric columns stay typed,
the derived batch validates its type token against the measurement enum
and carries a per-user rate limit, and a dose synced from the phone app
counts as the interactive write it is. Background cache refresh
failures log a structured event instead of vanishing, the warm queue
runs visit-triggered jobs alongside the nightly walk, the formatter
memo evicts one entry instead of flushing wholesale with its option
objects frozen, the targets cold build bounds its fan-out, and time in
bed unions every writer's envelope while stages stay with the richest
recorder.
Staleness can re-enter a generated text: measurement age is hashed as a
bucketed tier, so the fingerprint stays stable day to day but flips
exactly when a reading crosses the boundary the prompt caveat cares
about. The ingest debounce becomes a one-hour minimum gap with the
hash gate carrying the budget, so a notable morning reading is
narrated the same morning instead of after the next nightly run. A
manual regenerate now refills the status cards through the same
hash-gated pipeline — changed cards regenerate, unchanged cards
refresh free, no eviction touches the hash baselines. The user's
self-context joins the comprehensive fingerprint so a newly remembered
fact reaches the briefing, the sleep writer collapse covers every
caller that reads nights, the batched compliance build fans out
bounded instead of sequentially, and a failed compliance read renders
a quiet retry state in the exact skeleton footprint instead of
swallowing the error.
Sleep nights collapse per writer instead of per source: several
HealthKit apps land behind the same source value, so a phone's coarse
awake and in-bed blocks could blend into or fully constitute a night
whose stages another writer had recorded. The winning writer per
session is the one with the most distinct granular stages, then the
user's source ladder, and its rows are kept whole. The device type is
threaded through every sleep read including the stage stacked bar, and
the sync overlap for late-arriving nights widens from one day to seven
so a night scored after the cursor passed is fetched on the next tick —
assembly is read-time, so affected nights heal without manual repair.
Mood notes expand to their full text in the list on desktop and touch,
replacing the hover-only tooltip, and the mood CSV export carries a
note column.
A remember action under every persisted chat message adopts its content
into the matching self-context field — allergy statements land in
allergies, diagnosis wording in conditions, everything else in the
coach focus — reusing the adopt endpoint's dedupe and encryption, with
the result visible and editable where the self-context lives. The
deterministic fact patterns accept closed filler-word and possessive
variants that previously defeated them, and stored facts survive the
snapshot budget squeeze instead of being the first block shed on
data-rich accounts.
The dashboard reserves chart-row skeleton cells from the saved layout
before the snapshot resolves, so a cold load paints close to its final
footprint instead of growing by a screen when data lands; the tile
silhouettes count only tile-capable widgets with blood pressure as two,
the greeting line keeps its box through name personalisation, and the
getting-started checklist moves below the charts where its arrival
shifts nothing. On the insights overview the targets build runs its
eight reads in parallel with a one-year floor on the glucose scan
instead of strictly in sequence over an unbounded history, the derived
batch gains a server cache where it previously recomputed sixteen
metrics on every read, both serve stale-while-revalidate, and the
health-score column reserves its exact footprint while the analytics
payload is pending.
Intl.DateTimeFormat instances are memoised per locale, timezone and
options signature — construction dominated the 365-day ledger build and
the hot path drops about fivefold. A batched GET /api/medications/compliance
serves every card from one request through the same per-medication cache
cells the detail page reads, replacing the per-card fan-out; the list
response stays untouched so the due line never waits on a ledger build.
The medications and compliance buckets serve stale-while-revalidate,
interactive writes evict while background bulk sync marks stale, the
per-id route defers ownership and schedule reads until a cache miss,
and the card body reserves constant slots for the status and cycle
lines so late data stops shifting the layout.
A sha256 fingerprint of the compacted feature snapshot is stored beside
every generated text; nightly, forced and ingest-triggered regeneration
all skip the model call when the fingerprint matches and refresh the
timestamp instead. Warms cover only the user's resolved locale and the
blanket per-status eviction after a comprehensive generation is gone —
a lazily-read second locale regenerates on its own miss path. The
ingest debounce widens from thirty minutes to six hours and no longer
deletes cache rows, the visit-triggered warm re-checks freshness at job
start, backs off for an hour after a failure and is capped at two
forced runs per user per day, and its dedup window now outlives the
client poll horizon. An active day drops from hundreds of model calls
to a handful tied to actual data change.
Stale-served insights responses now carry a revalidating flag; the
advisor and comprehensive queries poll on a bounded 25 s cadence until
the background warm lands, so an open session converges instead of
pinning yesterday's briefing for an hour. Interactive measurement
writes (single create, edit, delete, bulk-delete, restore) hard-evict
the analytics cache so a manually entered value is reflected on the
next read; background batch sync keeps the mark-stale path. The
visit-triggered force warm collapses to one job per user instead of
one per locale, and the umami script proxy failure now annotates a
named action with the failure reason instead of a bare flag.
The insights read path served a cold cache by generating inline — up to
ten seconds on first visit — and the nightly warm pass structurally
excluded evening visitors: their visit stamped the cache, the half-four
run skipped it as fresh, and it expired exactly at the next evening's
visit. Stale content now serves immediately with its timestamp while
one background job revalidates, the warm threshold drops to an hour,
forced warming fills both message locales, the analytics caches serve
stale-while-revalidate, the profile endpoint parallelises its reads,
the bug-report status drops a write per call, the umami proxy degrades
to a no-op script instead of an error, the insights route sheds its
heaviest static imports, and the medication page prefetches on intent.
The dashboard snapshot fires the moment login succeeds instead of
waiting for the page chunk (start ~670 to ~200 ms throttled). Guided
follow-ups are personalised from the user's snapshot and an answer gets
the coach's streamed reaction before the next question. Archived
schedule eras can be corrected through a superseding revision that
keeps the original as an audit trail, era writes serialise per
medication, and duplicate dose times collapse in validation. A rolling
medication whose next dose is tomorrow can no longer read as heavily
overdue — the card status derives strictly from the served next-due —
and the adherence bars keep a fixed geometry in every state. The
server-wide assistant key gains its admin card, the one duplicated
coach preference collapses onto a single owner, twenty-seven admin and
settings card headers move onto the shared pattern with sixty
containers normalised, and the remaining hydration warnings are gone.
The manual-era boundary anchors on the medication's creation instead of
the current moment, so a backfilled era can only describe time before
the app was tracking.
The schedule tab shows its eras as a collapsible timeline — current
plan since its date, predecessors with their times and span — and a
past era can be added manually for old history, with validation against
overlaps and the live plan. Invite creation chooses one to fifty uses
and the redemption list scrolls.
Open follow-up questions now run as a guided in-chat sequence — 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. Proactive nudges learn three new deterministic
signals — weight drifting from its target band, a run of short nights,
an active user falling silent — address the focus the user asked to
watch, and gain per-group toggles and a frequency preference.
The self-context adopt runs read-modify-write inside one transaction,
the admin shell renders nothing until the role is confirmed, and the
json-body helper merges Headers instances correctly. The compose image
reference accepts a pinned digest through an env var, and a promote
script resolves the published digest, verifies its signature and hands
the operator the exact pinning call — the documented path away from a
moving latest tag.
The measurements page consumed its add parameter during render, which
crashed server rendering now that every route renders dynamically with
real search params. Compliance day buckets anchored to the current
wall-clock minus twelve hours, so before local noon the current day was
missing from every bucket series — they anchor on local noon now. The
wizard spec accepts the localized start date its summary deliberately
shows, and the generated contract picks up the version bump.
The client bundle carries only the active locale — resolved on the
server and handed to the provider with no hydration flash, halving the
dashboard route to under a megabyte parsed (288 KiB gzip). The auth
shell renders the app chrome immediately instead of a spinner while the
session resolves in parallel, the widgets request folds into the
snapshot it duplicated, and insight pills prefetch their sub-pages —
a first visit to a sub-page lands in about ninety milliseconds instead
of nearly a second.
An intake can carry the dose that was actually taken — editable in the
quick-add and log dialogs, shown as a quiet deviation line in the
history. The measurements and mood lists refresh by pull on touch, the
insight sub-pages gain the same customize entry as the overview, and an
answered coach follow-up offers to adopt the answer into the matching
self-context field.
Published manifests are now cosign-signed keyless with a documented
digest-pinning path, a lefthook pre-commit runs gitleaks over staged
changes, responses opt into a credentialless embedder policy, the
deploy webhook can require a fresh timestamp, and a monthly drill
decrypts and parses the newest offhost backup so a broken key or format
surfaces before an emergency does.
A dose past its slot time but still inside its catch-up window now stays
on the medication card as overdue and takeable instead of yielding to
the next occurrence — the next-due field can therefore carry a past open
slot, flagged with nextDueOverdue. Y-axis padding no longer dips below
zero for series that cannot be negative, so a steps chart stops
suggesting minus nine hundred steps.
Editing a schedule's times no longer rewrites the past. Each material
change archives the outgoing cadence as a dated revision, and every
band consumer — history, compliance, write attribution, the auto-miss
cutoff — mints past periods from the schedule that was valid at the
time, so a dose era taken at its then-correct times stays on time
forever. The repair script can infer historical eras from consistent
old anchors behind an explicit flag. The welcome tour scrolls its
target into view, flips above it when space below runs out and falls
back to a centred sheet on any viewport, so no step can strand its
buttons below the fold. The avatar menu drops the about entry — that
lives in settings.
The composer starts as a single line with send beside it and grows only
when the text does; the shift-enter hint and the inline disclaimer are
gone. A typing indicator bubbles before the first token. The context
rail hides by default behind a toggle and stops scrolling sideways,
conversations open in the full view instead of a broken side drawer,
and the coach preferences move into Settings where the rest of the
configuration lives — context toggles and the exclusion list now derive
from one source so they can no longer contradict each other. A floating
bubble appears only while a coach-initiated nudge is unread. Clearly
stated allergies and conditions are remembered immediately through a
deterministic extractor instead of waiting for the twenty-turn
summary pass.
The route-level loading state now renders the same structured tile and
chart silhouettes the page uses, so the first paint no longer flashes a
grid of bare cards before the skeletons arrive. The chart reveal cap
drops to 1.2 seconds, the QR library loads only when an invite is
minted, and list filtering moves onto a shared pill-rail filter bar —
active filters read as removable chips, date ranges live in a popover,
and the measurements and mood lists speak it first.
The detail page gains a dedicated API tab, the full-edit button becomes
a lifecycle row that opens the editor, the danger zone drops its red
wash in favour of the shared card chrome, and the card menu's edit
entry leads to the detail page it describes. The inventory tab finally
records stock — packs and pens with optional expiry, plus corrections —
instead of pointing at an API. The card's next-dose line reads from the
canonical band model instead of a stale schedule window, which also
showed old dose times after a schedule change; the repair script learns
to reconcile drifted windows and tombstone pending rows parked on
abandoned anchors.
Every standalone intake anchors its slot on the take time since
v1.15.19, so guarding the nightly dedup on that shape excluded ordinary
drift duplicates from collapsing — the original point of the sweep.
Releasing a pinned dose now persists the user's choice on the row, the
dedup and the repair script key their exclusion on that flag, the read
model reports a released row as deliberately off-schedule instead of
re-snapping it within the anchor epsilon, and the attributed badge only
shows on rows actually bound to a slot. A few fixture names and stray
comments also drop personal references.
A deliberately detached ad-hoc intake could be folded back into its slot
cluster by the nightly dedup, and a pinned row could lose its slot to a
sibling on the sync-version tiebreak — pins now outrank every other row
and detached rows never join a cluster, mirrored into the operator
repair script. Pinning onto an already-served slot no longer overwrites
the existing record silently; the API refuses with a clear error while
an identical re-post stays idempotent. The idempotency cache also
refuses to store invite-token-shaped responses.
Invites move into their own admin section with a real ledger: status
badges, expiry, uses, a redemption history of who joined when (new
redemption table, revocation is now soft so history survives), and an
honest one-time link/QR moment. The assistant self-context grows
structured fields for conditions, allergies and focus — encrypted like
the free text — merges age and gender from the profile into one fenced
prompt block, and asks up to three targeted follow-up questions that
surface as tappable chips above the coach composer. A fourth nudge
trigger gently suggests a check-up when the self-context is stale and
the coach is actually in use.
Users can now tell the assistant about themselves in an encrypted
free-text profile that feeds the coach and briefing prompts as clearly
fenced context, with the coach allowed one targeted follow-up question
when the picture is incomplete. Operators on closed registrations can
mint expiring invite links with a QR code from the admin area; a valid
invite opens registration exactly as many times as it allows and records
who redeemed it. A nightly pass watches three deterministic signals —
low adherence, blood pressure above its target band, a falling recovery
trend — and sends at most one coach nudge per week through the existing
notification channels, gated behind the same opt-outs as every other
assistant surface.
An off-schedule row now tells the user when the dose had been due, and
its row menu offers to attribute it to that slot when the slot is still
unserved. A pinned intake is a deliberate decision with a visible badge,
it always counts as taken late — never on time — and the pin can be
released again. The read model binds pinned rows by their slot anchor so
a pin outside the catch-up window survives reloads, and the suggestion
respects the schedule's own windows instead of a hard-coded tolerance.
The clock format followed the UI language with no way out — an English
interface forced AM/PM everywhere. A new profile preference (automatic,
24-hour, 12-hour) feeds every formatter through the shared locale
context, persists on the user row and rides the profile endpoints. The
insight pill list in settings gains the same eye toggle the overview
tiles have, so ordering and visibility live in one place; the page-side
'manage detail pages' block folds into a link there. Includes the
locale bundles for both features and the regenerated API contract.
The auto-miss cron stamped every pending row missed a flat day after
its slot even though weekly schedules stay takeable for days, touched
tombstones because it never filtered deleted rows, and a verdict stuck
even after the dose was taken. The cron now derives its cutoff from the
schedule's window reach, skips deleted rows and bumps the sync counter,
and a take clears the auto-miss flag. Editing a schedule tombstones the
open pending rows of the old anchors and reconciles a stale window onto
the new times. The bulk write path attributes takes through the same
band engine as the single-intake routes and accepts forceSlotInstant
per entry. Configured dose windows now reach every compliance call site
through one shared schedule select, and the card window pill takes a
timezone, treats a degenerate window as a point and counts dose times
instead of schedule rows.
The medication list fired one compliance request per card every 30
seconds, and each request scanned the medication's full intake history
and expanded the recurrence calendar five to six times. The endpoint now
mints bands once per request, serves the sub-windows from that single
ledger, bounds the event read to a year of selected columns and sits
behind a write-invalidated 15-minute server cache; the cards poll at
five minutes. The dashboard drops two redundant checklist queries by
reading counts from the snapshot, the BMI chart derives from the weight
series instead of refetching it, the detail page no longer serialises
behind the auth probe and seeds its query from the list row. Expensive
analytics reads gain a shared per-user rate limit, and the service
worker stops serving cross-version shells, enables navigation preload
and trims caches in one pass.
Raising the middleware body cap to 512 MB for archive imports removed
the implicit ceiling that protected JSON routes, so any bulk endpoint
would buffer an arbitrarily large body before validation ran. Every
JSON body read under /api now carries an explicit byte cap sized to its
legitimate payload (64 KB for single-record routes up to 16 MB for the
measurement import), webhooks cap before signature work, and oversized
bodies return 413.
The project licence changes from AGPL-3.0 to the PolyForm Noncommercial
License 1.0.0 for this and future versions: 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. LICENSE carries the
verbatim PolyForm text, and the SPDX identifier is updated in
package.json, the version endpoint, the OpenAPI info block, the image
labels and the README.
A take whose slot the legacy resolver rejected fell back to a standalone
insert that kept the client's scheduledFor, parking a second live row on
an anchor the reminder projector had already minted — the partial unique
index permits that because it spans source. Both intake write routes now
probe the slot source-agnostically and converge onto the existing row
before inserting, and a standalone insert anchors on the take time per
the ad-hoc contract. The compliance rollup counts distinct slots instead
of rows, which also corrects historical duplicates on the next recompute,
and the slot dedup pass runs nightly instead of only at worker boot.
An intake edit accepted any takenAt value, so a date typo could park a
dose a month away from its slot and poison the today feed, the rollup
and the rolling next-due anchor at once. The update schema now rejects
future timestamps (with clock-skew tolerance) and values older than five
years, the route rejects edits before the medication's start date, and
the edit dialog caps the picker at now, shows the scheduled slot and
warns when the new time sits more than 48 hours from it. The dialog's
note field is gone — it was never persisted and silently discarded
input.
Blood-pressure fixtures in the in-target integration suite were copied
from live readings; they are now synthetic with the same in/out split so
every assertion holds. Hard-coded operator hostnames move behind env vars
(DEPLOY_LOGS_URL, HEALTHLOG_PROD_URL), the codex client identifies via
the public repository URL, the OpenAPI servers entry points at a neutral
example host, and remaining workflow, docs and comment references to the
operator's instance or working notes are generalised. The compose
whitelist also gains DEPLOY_WEBHOOK_SECRET, which was missing and made
the deploy webhook unusable on compose installs.
These tests passed in Europe/Berlin but failed under UTC (or against the
shipped engine) because they leant on the host clock:
- intake route test: the mocked user had no timezone, so slot resolution
fell back to the host TZ and the 07:00 schedule slot only matched in
Berlin. Pin the user to Europe/Berlin.
- health-score: 30 daily takes seeded at an unaligned now-i*DAY shifted
their Berlin hour with the runner TZ, dropping them out of the
08:00-10:00 on-time band. Anchor each take to a fixed 07:30 UTC.
- sleep-stages: the night sat on the trailing-30-day window edge for a
fixed calendar instant. Anchor it to now-5d.
- weekly-compliance: not a timezone issue. The dose-window engine treats
a weekly dose inside the 4-day overdue tail as still-takeable, not
missed, so skipping the most-recent Monday no longer lowers the rate.
Skip an older, settled-miss Monday instead; the gap assertion stands.
Accept an optional doseWindows array on the schedule create/update routes:
each entry pins an explicit on-time range [start,end] to one of the
schedule's times-of-day, defaulting to the symmetric grace band when
absent. Reject malformed ranges (bad HH:mm, a timeOfDay outside the
schedule, an inverted span) with a 422 so the persisted window can never
feed the band minter a degenerate range.
Regenerates the OpenAPI contract for the additive field (iOS-safe).
Thread the persisted `doseWindows` through the shared band minter so the
on-time band for a dose time uses its explicit `[start, end]` range
(minted DST-correctly on the slot's local day) instead of the symmetric
±1h default; the late tail stays cadence-derived. A `normaliseDoseWindows`
helper coerces the stored JSON into a clean entry list, dropping anything
malformed so the read/write paths never defend against an arbitrary shape.
Every band consumer reads the same field: the dose-history endpoint, the
compliance %, the cadence timeline (card last/next dose), and the
write/edit slot attribution all resolve through one minter, so the
history view and the rate can never disagree. The write/edit and
health-score schedule selects now pull the column.
Convert the bulk-delete hard deleteMany to a soft-delete updateMany that sets
deletedAt and bumps syncVersion, matching the single-event DELETE handler so a
multi-select removal surfaces each row as a tombstone on the sync delta feed
to clients offline at delete time. The where clause now scopes deletedAt:null
so a re-post of already-removed ids counts zero.
GET /api/medications/[id]/dose-history?from=&to= returns the full per-slot
ledger over the window: every expected slot with a status (taken on-time / late
/ skipped / missed / upcoming) plus every off-schedule intake tagged ad-hoc,
built from the same band minter the compliance % consumes so the history view
and the rate can never disagree. Ownership-scoped, rate-limited, reads only
non-tombstoned rows, instants serialised as ISO strings. Additive — iOS-safe.
This is what the medication Verlauf tab renders.
Editing a takenAt or a status toggle now re-runs window-band attribution and
sets scheduledFor to the matched slot (the take's own time on a miss), instead
of leaving the slot stale. A slot move tombstones the source row and routes the
corrected dose through the shared canonical-slot upsert so the write is
P2002-safe rather than bare-updating into an occupied slot. Both edit surfaces
(the per-event PUT and the status-toggle POST) accept forceSlotInstant to pin
an off-window edit onto a named real slot, rejecting a non-slot with 422.
The per-medication intake POST resolves a taken dose's slot through the band
attributor instead of the ±6h snap: a take inside a slot's configurable dose
window snaps to that slot (canonical upsert), a take outside every window
records as a standalone ad-hoc row, and a PRN medication always inserts
standalone. A skip still binds to its slot by the canonical scheduledFor snap.
Add an optional forceSlotInstant to the intake body so the UI's late-take
"diesem Slot zuordnen?" nudge can pin an off-window take onto a chosen slot;
the server validates the instant is a real slot of the medication on its day
and rejects a non-slot with 422. The dose-safety future-slot guard stays.
The medication compliance percentage and the history view drew their
per-dose attribution from four different matchers — the ±12h pairDoses
proximity used by calculateCompliance, the deriveDoseStatus bands (open
dose only), the 3h+120min classifyIntakeTiming heatmap heuristic, and a
raw takenAt-IS-NOT-NULL per-day rollup. They could and did disagree: a
dose could read "taken late" in the rate while the history called it an
off-schedule ad-hoc take.
Route the rate through the shared cadence-aware band minter +
reconstructDoseHistory so the percentage is a tally over the SAME ledger
the history view renders. The same intake can no longer be taken in the %
and ad-hoc in the ledger.
- calculateCompliance: when a medication context is supplied, the
count fields (taken / skipped / missed / rate) come from
tallyComplianceFromLedger. Numerator = taken_on_time + taken_late (a
late dose still counts as taken — the taking-vs-timing split is kept
available); denominator = taken + missed; skipped, ad-hoc, upcoming and
whole PRN groups are excluded; rate capped at 100. The streak still
walks the cadence timeline. Context-less pure-math callers keep the
legacy timeline tally byte-stable.
- complianceChips + missedDoses: adherence rate + missed count come from
the same ledger when an engine context is threaded.
- /api/medications/[id]/compliance heatmap: the per-day timing split and
missed marks are bucketed from the one ledger instead of the per-day
classifyIntakeTiming + expectedSlotCountForDay walk, so the cells agree
with the headline rate.
- medication_compliance_rollups stays a raw cross-medication daily
coverage count, documented as distinct from the band-attributed % (the
band minter is per-cadence and cannot run inside a single SQL tally).
Downstream readers (health score, insight compliance status) keep calling
calculateCompliance; their numbers shift to the honest band-attributed
values. Some currently-counted off-time takes (e.g. an 11:29 take of a
07:00 dose) now read ad-hoc with the slot missed rather than taken-late —
the configurable per-dose window is the lever to widen the on-time band.
Tests: the irregular twice-daily, single-time-late heatmap, and legacy
parity fixtures are updated with rationale for the intended number
changes; new tally tests pin the taking/timing split, the skip / ad-hoc /
upcoming / PRN exclusions, auto-missed=missed, the 100% cap, rolling
retrospective families, and the cross-consistency invariant.
Add `POST /api/measurements/bulk-delete` and `POST /api/mood-entries/bulk-delete`,
mirroring the bulk-insert conventions (`withIdempotency`, per-user rate
limit, response envelope). Body is `{ ids: string[] }`, capped at 200.
Soft-delete only, ownership-scoped: a single `updateMany` pins `userId` +
`deletedAt: null` and sets `deletedAt` + bumps `syncVersion`, matching the
single-DELETE iOS tombstone contract. A forged or foreign id is a silent
no-op — the returned `deleted` count reflects owned rows only, never a 404
existence leak.
Side effects fan out efficiently: measurements collapse the deleted rows to
the unique `(type, day)` set before recomputing rollups (one recompute per
type-day, not per row) and invalidate the affected status-insight caches;
mood collapses to the unique day set for its rollup recompute. Both bust the
per-user read caches.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a source filter to both management lists. Mood gains an optional
`source` param (validated against the mood source set); measurements gain
`sourceEq` (validated against MeasurementSource) — the `source` key on the
measurements list is the rollup-tier opt-in, so the filter rides a distinct
param. Both thread into the list `where` alongside the pinned `userId` +
`deletedAt: null`.
Migration 0136 adds two additive indexes — `(userId, source, measuredAt)`
on measurements and `(userId, source, moodLoggedAt)` on mood — so a source
filter + recency sort is an index range scan rather than a userId scan plus
in-memory sort at 100-200k rows per user. Pure `CREATE INDEX IF NOT EXISTS`,
no backfill.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every surface that applied a resting-pulse target judged the raw PULSE
series against a resting band. Apple Health maps all heart-rate samples
— including workout HR — onto PULSE, and the resting figure onto a
separate RESTING_HEART_RATE type. A single workout emits hundreds of
expected-high samples, so the resting-target evaluation read a large
share "outside target" and tanked the in-target % / consistency even
when the clean resting series averaged a healthy figure.
Judge the resting band against a resting series. A shared helper prefers
RESTING_HEART_RATE; when the user has none it derives a proxy from the
low percentile of each day's PULSE samples, so a dense workout burst
sitting in the upper tail cannot drag the resting estimate up. Applied
to the resting-pulse tile + its consistency strip, the pulse in-target %
in the assessment snapshot, and the dashboard / sub-page classification.
Raw heart rate, where it is merely charted, no longer carries the
resting-band overlay. The doctor report is unchanged — it reports raw
heart-rate stats factually, with no resting-target judgment to mislead.
The BP pillar value equalled the binary all-time in-target rate: the
share of paired readings where both systolic and diastolic sat at or
below the age-band ceiling. A borderline stage-1 profile (diastolic a
few mmHg over the ceiling on most readings) collapsed the pillar to the
teens even though the reading is "a bit high", not catastrophic — and
improvement never surfaced because the rate is all-time and binary.
Replace the binary rate AS THE SCORE with a smooth clinical-proximity
grade. A recency-weighted representative reading (45-day half-life) is
mapped per axis onto a 0-100 curve anchored on the same age-band ceiling
the rest of the app uses — optimal ≈ 100, at the ceiling ≈ 85, then
piecewise-linear down through the over-target bands; hypotension is
penalised symmetrically below the clinical floors. The pillar takes the
worse of the two axes, so an elevated diastolic counts without
binary-zeroing the result. A 134/87 profile against a 129/79 ceiling now
reads in the mid-50s.
The in-target rate stays available as a secondary "X% of readings in
range" stat. The healthScore.components.bp shape is unchanged — only the
value moves.
- Layout PUT accepts version 1 as well as 2 (live iOS client still PUTs
version 1); the serializer normalises to the canonical v2 blob, so a v1
body never 422s. Mirrored in the OpenAPI schema + description; yaml regen.
- Add the Weight tile to the inline tile editor so it can be hidden /
reordered like every other Vitals grid tile.
- Replace the non-transitive Vitals tile-drag comparator with a pure,
total-order rebuild helper (reorder the Vitals subset, substitute it back
into the full list in original-order slots, densify).
- Gated-off section rows keep a disabled eye toggle in the same position
with the gate hint as a caption below the title, so the row never jumps.
- Cap the disclosed Vitals tile list height (max-h-[60vh] scroll,
overscroll-contain) so a tile drag does not fight the page scroll.
- Gate edit mode until the layout GET settles so a save can never flush
defaults over a real saved layout.
- Drop the duplicate top-right "Done" affordance: hide the customize toggle
while editing; the in-card Done / Reset own save and reset.
- Restore focus on enter (edit-card heading) and on close (customize toggle).
Tests: route regression for a version-1 PUT persisting a resolved v2 layout;
unit coverage for the new Vitals rebuild helper (order, non-Vitals stability,
per-tile visibility, no mutation).