Commit Graph
2687 Commits
Author SHA1 Message Date
Marc-André Bombeck 114db6bbca fix(blood-pressure): unify in-target window on 90 days across headline, score, and tile
The "BD im Zielbereich" share was narrated on three different windows on the
same screen: the dashboard headline read the trailing 30 days, the dashboard
Health-Score BP pillar read all-time, and the analytics route's pillar read
all-time while its headline read 30 days. A user could see one in-target rate
on the tile and a pillar graded off a different window directly above it.

Standardise the headline, the Health-Score BP pillar input, and the
week-over-week delta on a single trailing-90-day window everywhere — a span
wide enough to stay stable yet recent enough to track current control.
`computeBpInTargetWindows` and the rollup fast-path now both surface a
`last90Days` window; the analytics route and the dashboard snapshot route the
headline and the pillar through it, and both feed the pillar from the identical
value so the dashboard ring and the insights card can never disagree.

Label the window in the tile ("BD-Ziel · 90 T") so the scope is visible; the
30-day figure stays as the short-horizon caption and the all-time aggregate
stays carried for the detail view. Visible-number change is expected: the
headline now reflects the quarter rather than the month.
2026-06-14 13:02:17 +02:00
Marc-André Bombeck 3077adfe06 docs(whoop): correct the sync overlap comment after the workout widening
The header comment still described WHOOP_RECOVERY_SLEEP_OVERLAP_MS as 24 h
and claimed workout ingest used the narrow default. The constant has been
7 days, and workout ingest now shares it so a late phone-sync to the WHOOP
cloud no longer drops the workout outside the window. Comment only.
2026-06-14 11:03:24 +02:00
Marc-André Bombeck 6a4d3fd553 fix(whoop): widen workout ingest overlap to match recovery and sleep
The WHOOP workout sync used the default 1 h incremental overlap while
recovery and sleep use the wider re-score overlap. The activity collection
filters on the workout's own time range, not on when it reached the cloud,
so a workout whose phone sync lands more than an hour after its window fell
permanently before the cursor and was never ingested — operator-visible
data loss. The workout sync now reuses the recovery/sleep overlap; the
externalId upsert keeps the re-fetch idempotent.
2026-06-14 10:36:53 +02:00
Marc-André Bombeck 25a7834dec fix(medications): attribute historical intake edits against the era live then
The write/edit slot resolver loaded a medication's archived schedule
revisions but dropped them before running band attribution, so editing a
historical intake snapped it onto the LIVE schedule's slots instead of the
era that was valid at the dose's time. The read side (dose-history,
compliance) already era-resolves; the write side now matches.

`loadAttributeMedication` threads `scheduleRevisions` into the attributor
projection. Both `resolveSlotForWriteByBand` and `resolveForcedSlotForWrite`
now pick the correct era for any past instant.
2026-06-14 10:33:20 +02:00
Marc-André Bombeck f9876f9492 feat(consent): mint AI consent receipt from the web client
The server-side gate requires an active ConsentReceipt before any health
snapshot egresses via the operator's server-managed key, but the web
client never minted one — so web users on a shared-key deployment hit the
fail-closed no-key fallback on every AI surface.

Add ensureWebAiConsentReceipt() and POST /api/consent/ai/web: an
idempotent ai_full grant mirroring the iOS master grant (a user with an
active receipt is a no-op). The AI-settings card calls it on mount — the
web equivalent of the iOS shell-mount heal — so existing accounts gain a
receipt without a re-consent step.

Surface an honest signal: resolveReadOnlyStatusMiss now returns a
consent-missing outcome when a provider is configured but the gate would
block egress, distinct from no-provider, and skips the wasted enqueue.

Document the receipt precondition + the consent surface in the OpenAPI
contract (it shipped without a bump): the /api/consent/ai family, a
Consent tag, and a 403 consent.ai.required response on the AI-egress
route.
2026-06-14 10:25:18 +02:00
Marc-André Bombeck 7fa10b43a0 fix(insights): preserve stored layout dimension on a partial PUT
A PUT that omits sections (an iOS tiles-only reorder) reset the user's
section customization to defaults, and symmetrically a sections-only PUT
reset the stored tiles. serializeInsightsLayout now takes the resolved
stored layout and falls back to it — not to defaults — for whichever
dimension the body omits; the PUT handler loads the stored layout first.
A first-ever single-dimension PUT (no stored layout) still fills
defaults, so the v2 blob stays complete. Pins the merge behaviour with
three unit cases.
2026-06-14 10:15:47 +02:00
Marc-André Bombeck 6db4bd9793 perf(cache): widen the analytics and insights stale window to an hour
The analytics and derived-insight reads served stale for ten minutes, but a
single reader visits the dashboard and insights every twenty to thirty
minutes — so nearly every return landed past the window and paid the full
cold rebuild instead of an instant stale serve. At an hour the return
serves the prior snapshot immediately and refreshes in the background, and
each stale-serve re-warms the entry; the dashboard's time-derived fields
stay bounded and self-correct through the snapshot client's poll.
2026-06-13 16:08:08 +02:00
Marc-André Bombeck ff92ba46c2 fix(medications): the cards hold one shape and the list refetches on return
The low-supply notice moves onto the adherence meta row beside the streak —
a slot every card already reserves in every state — so a card that is low
on stock no longer inserts a row that pushes its bars out of line with the
card beside it. The gap between the drug class and the first line tightens
at the same time.

The medications list, compliance summary and supply reads now refetch on
every mount. App Router unmounts the page on navigation away and remounts
it on return; inside the client staleTime that return served a cache that
could be minutes old, hiding a dose logged on another device or surface. A
Playwright spec guards the return-navigation refetch.
2026-06-13 16:08:01 +02:00
Marc-André Bombeck f4dec8c28f feat(medications): fractional dosing via decimal units per dose (#316)
A dose can now consume a fraction of a unit — a half, third or quarter
tablet for a split pill. The wizard offers a curated fraction set beside
the whole numbers (the most error-resistant input); units-per-dose and the
per-container counts widen from Int to Decimal, so a 30-tablet pack dosed
at a half reads 29.5 after one dose and the days-left projection follows.
The consumption ledger stamps and refunds the exact fraction, and existing
whole-number doses are untouched. OpenAPI regenerated — the three unit
fields move from integer to number — and the units-per-dose hint is
updated across the six locales.

Also anchors a first-use date relative to now in an inventory route test
that had aged past its 30-day in-use window.
2026-06-13 16:07:50 +02:00
Marc-André Bombeck a1511b33b3 chore(mood): drop the manage module's unused exports
The layout hook, the cap mirrors and the layout type shipped unused —
the dead-code gate flags them; the section reads everything through the
manage query and the caps surface as server 422 toasts.
2026-06-12 23:58:31 +02:00
Marc-André Bombeck 4ced89553d fix(medications): the schedule flip cannot rewrite history and the doctor report stops inventing a rate
Switching an as-needed medication onto a fixed schedule archived no
revision (there were no prior rows), so the era segmentation attributed
the entire schedule-less stretch to the new schedules — every as-needed
day would have read as missed across the era-aware consumers. The flip
now archives an empty-payload era covering the stretch, which expects
nothing. The doctor report's compliance table excludes as-needed
medications: with no schedule every logged intake is taken by
definition, and a fabricated 100% has no place on a clinical document;
the medication list itself still carries them.
2026-06-12 23:47:43 +02:00
Marc-André Bombeck 6fef2081d0 fix(medications): the audit findings on the new supply and dose surfaces
- The escalated card row keeps its time anchor: the red badge carries
  the matched window or the overdue-slot value beside it, and the
  inline row's range stays visible on mobile, where it was the only
  clock on the card.
- Take-all-due collects what the cards offer: a dose escalated past its
  catch-up band joins the set with exactly the slot the card's own
  button would record; as-needed medications never join.
- The table's stock warning follows the user's runway threshold instead
  of a leftover four-dose constant; threshold off means no amber, empty
  stays red.
- The card itself shows the remaining runway in the warning tone once
  it crosses the threshold — only then, the card stays calm above.
- The low-stock push deep-links straight into the supply tab and its
  APNs payload keeps url, runwayDays and thresholdDays; supply tab and
  threshold setting link to each other; the reminder-thresholds and
  notification-prefs endpoints enter the OpenAPI contract.
2026-06-12 23:17:20 +02:00
Marc-André Bombeck 3943dcfb72 feat(medications): as-needed medications log without a schedule
A medication can declare itself as-needed: no schedules, never due,
never reminded, excluded from every compliance rate — but intakes log
ad-hoc, consume inventory exactly like scheduled doses, and the
history stays. The wizard offers it as a cadence choice and round-
trips it on edit; card, table and detail show a calm marker where the
next dose normally sits, the take-all dialog never collects one, and
the supply runway stays unknowable by construction. (#316)
2026-06-12 22:56:03 +02:00
Marc-André Bombeck 550f314a54 test(measurements,whoop): the workout fall-through test reaches its path again
Ladder reconciliation ranks every enum source, so the unranked
fall-through needs out-of-enum fixtures now; a stored single-source
ladder resolving one canonical row is pinned as the new contract. The
body-sync test sheds the import its retired weight path used.
2026-06-12 22:56:03 +02:00
Marc-André Bombeck 97822e9e6d fix(mood): entry edits spare archived-tag history and insights speak the custom labels
Editing any field of a mood entry re-sent its tag set, and archived
tag keys can no longer resolve — the diff deleted the entry's links to
them invisibly, against the archive contract the tag suite just
shipped. Submitted tag keys now govern active links only; links to
archived tags survive every edit, and the explicit purge stays the one
removal path. The insights surfaces — tag breakdown, influence, better
days, crosstab, the model snapshot and the narratives — render the
decrypted custom-tag label instead of the raw key, decrypted once per
tag server-side.
2026-06-12 22:55:50 +02:00
Marc-André Bombeck 1f45cf4da4 fix(observability,settings): the lag event names its real fields and ladders shed duplicates
The wide-event type declared loop_p99_ms while the monitor publishes
loop_last_ms — a dashboard built on the declared field would find
nothing; the type now states what is emitted. Ladder reconciliation
collapses duplicate entries first-occurrence-wins, so a duplicate-laden
ladder written through the API can no longer overflow the schema's
eight-entry cap when the settings page echoes the resolved shape back.
2026-06-12 22:38:03 +02:00
Marc-André Bombeck e122b50477 fix(whoop): the profile weight stops posing as a measurement
WHOOP's body endpoint carries a self-reported profile weight, not a
reading — the overwrite-in-place row took a fresh measuredAt on every
sync, so a stale manual entry kept resurfacing as the newest weight no
matter what the scale said. The sync no longer ingests it and clears
the legacy row off existing accounts on its next pass.
2026-06-12 22:34:13 +02:00
Marc-André Bombeck a86c59b77f fix(settings): saved source ladders learn about sources added later
A stored per-metric ladder replaced the default wholesale, so a source
integrated after the user last saved — WHOOP on a pre-v1.11 sleep
ladder, Fitbit on pre-v1.12 ones — never surfaced again: the settings
page renders only the stored array and offers reorder, not add, and
the picker ranked the invisible source behind everything. The resolver
now appends default-ladder sources missing from the stored ladder
after the user's explicit order — visible, reorderable, and ranked
where the old behaviour already ranked them.
2026-06-12 22:16:47 +02:00
Marc-André Bombeck 058d1d7a69 feat(mood): tags get groups, icons and a home of their own
/settings/mood manages the whole tag vocabulary: own groups (encrypted
labels, twelve per account), custom tags with a searchable icon picker
over a 75-icon catalog, hide and unhide of catalogue tags, archive
semantics that never touch historical entries (hard delete only behind
an explicit second confirm), and drag-or-arrow ordering of groups and
tags shared by the picker. One GET serves the fully ordered tree —
groups, placements, hidden and archived state — so the entry form, the
settings section and a native client all read the same resolved layout.
The picker finally renders custom tag labels (they were ignored since
v1.13.0) and offers inline tag creation per group. The mood-tag API
enters the OpenAPI contract for the first time, and the key-rotation
script now covers both encrypted tag and group labels — custom tag
labels had been missing from it since v1.13.0.
2026-06-12 22:03:01 +02:00
Marc-André Bombeck 19a24a5160 fix(dashboard): a dose taken on the dashboard updates the dashboard
The hero band, dose tally, verdict and checklist all read one snapshot
query that neither refetches on mount nor on focus — and no mutation
ever invalidated its key, so a dose taken right there stayed visibly
due for up to the next two-minute poll. The snapshot key joins the
medication-dependent invalidation bundle; the intake routes already
hard-evict the server-side bucket, so the triggered refetch returns
post-write data immediately. (#316)
2026-06-12 21:47:47 +02:00
Marc-André Bombeck cac9a32756 feat(medications): low-stock notifications with a runway threshold
A daily job projects each tracked medication's remaining supply against
its schedules and notifies through the normal channel cascade when the
runway falls below the user's threshold (default seven days, 1-60 or
off, set beside the reminder thresholds). One notification per
crossing: a stamp on the medication row re-arms on refill or threshold
change, and a failed dispatch leaves it clear for the next day's retry.
The GLP-1 card's fixed four-dose flag stays as the locked iOS read
contract; the engine covers it like any other medication.
2026-06-12 21:04:09 +02:00
Marc-André Bombeck 954701b5f3 feat(observability): every wide event carries the recent event-loop lag
Production showed multi-second windows where every response — routes,
RSC payloads, static assets — waited together while handler durations
stayed under half a second: the process stalled, and nothing
route-shaped could name it. A self-rescheduling tick now records how
late it fires; a tick past the stall threshold emits its own
event_loop_stall background event, and every wide event carries the
recent worst lag so the first request finishing after a stall is
annotated with it. Measured via a timeout chain rather than
perf_hooks.monitorEventLoopDelay — the histogram's sampling timer races
a sibling read-and-reset interval in the same timers phase and the
stall sample can vanish into the reset window.
2026-06-12 20:58:30 +02:00
Marc-André Bombeck 5859072561 fix(sleep): the hypnogram keeps its timeline when a summary writer joins the night
A writer whose stage rows all share one timestamp carries night totals,
not segments — reconstructing them as segments paints every stage
flush against the night's right edge. The session picker now prefers a
writer with genuinely timed rows among the granular candidates, so a
stage summary backfilled for a night an actually-timed writer also
recorded no longer evicts the real timeline; summary-only nights still
resolve. The hypnogram itself degrades a summary-shaped session to the
stage breakdown instead of drawing the false skyline.
2026-06-12 20:30:59 +02:00
Marc-André Bombeck 9dc0ccd03a fix(medications): release-QA findings on the inventory overhaul
- The legacy GLP-1 ledger reads again: GET and the Coach snapshot fall
  back to ledger-derived pen/dose figures when a medication has no
  inventory items; items always win once they exist. The delta branch
  on the POST is marked deprecated in favour of the inventory endpoints.
- Expired containers count as available nowhere: one shared supply
  summary (ACTIVE/IN_USE with units) feeds the overview row, the
  Bestand summary, the list payload, the GLP-1 card, and the runway
  estimate; expired units surface as a muted suffix instead.
- The inventory endpoints speak one wire dialect: request fields renamed
  to unitsTotal/unitsRemaining to match the responses, OpenAPI regenerated.
- The reorder dialog mirrors the page's grouping: active and inactive
  sections, each sortable within itself, so a saved order can never
  interleave them.
- Polish strings avoid the 2-4 plural trap, sub-dose quantities render
  as below one dose instead of approximately zero, and the table's name
  sort collates by the active locale instead of hardcoded German.
2026-06-12 19:11:53 +02:00
Marc-André Bombeck 8a5a26b49a feat(dashboard): the daily-overview band becomes opt-in
The hero renders only when the layout carries the literal
heroVisible: true. Legacy blobs without the field, malformed values,
and fresh accounts all keep it off; the Settings toggle persists an
explicit opt-in through the existing layout PUT.
2026-06-12 19:03:32 +02:00
Marc-André Bombeck 7eeab2ae98 fix(medications): stock claims are atomic and only a dose becoming taken consumes
Two hardening passes on the consumption stamp:

- consumeForIntake claims the stamp with a single conditional UPDATE
  (NULL-stamp predicate + row lock) instead of read-then-write, so a
  double tap racing a sync replay decrements exactly once; the loser
  sees count 0. restoreForIntake clears the stamp the same way, so
  concurrent restores cannot refund twice.
- Every seam consumes only on the pending-to-taken transition. A sync
  replay that re-posts an already-taken history row, or an edit that
  corrects the timestamp of an old dose, never touches today's stock.
2026-06-12 18:48:30 +02:00
Marc-André Bombeck e21dfd040e feat(medications): a sortable table view with an order you choose
The medications page gains a compact table beside the card view — one
row per medication with the same served-due status pill, next-intake
line, cadence-scaled adherence bars, dose-derived stock and the take
and skip actions the cards use, all from the payloads the cards
already read; the list response aggregates stock in the same round
trip. Columns sort tri-state with nulls last, the default order is
yours to arrange — the same drag dialog the dashboard and insights
use, applied to both views — and the chosen view and order persist
per user on their own settings column with field-scoped writes, so
toggling the view never touches the order. Inactive medications pin
to the end, the first column stays put under horizontal scroll, and
the loading skeleton follows the view you left.
2026-06-12 18:10:21 +02:00
Marc-André Bombeck d142a6ed31 fix(dashboard,rollups): the score survives new data types and folds stop eating buckets
The snapshot's thick phase gated on every type the account had ever
logged having rollup coverage — one fresh reading from a new
integration nulled the health score and the blood-pressure extras
while the insights page showed a score for the same account. The gate
now checks only the three types the phase reads, per the established
per-type pattern. Underneath sat the destabiliser: the large rollup
fold deleted the window's buckets and re-inserted without a
transaction or duplicate tolerance, so a racing sync write aborted the
insert and left the delete committed — whole types lost their buckets
until the next clean fold, and accounts with data older than the
five-year fold window re-ran the full backfill on every boot because
discovery scanned unbounded history. Delete and insert now commit
atomically with duplicate tolerance, discovery shares the fold's
bound, the provisional ring says the score is being computed when the
inputs exist and reserves the no-data wording for accounts that have
none, and the greeting leads the band typographically.
2026-06-12 17:42:44 +02:00
Marc-André Bombeck 7d3557a3fd fix(medications): every taken dose moves the stock, in units that match the product
Stock only decremented on the web card's take button — the status
toggle, the phone sync, the Telegram buttons and the token API all
recorded the dose and left the inventory untouched, which is why a
fresh pack kept reading full. Consumption now stamps the intake row
itself: every path that turns a row taken consumes through one module
— open container first, then earliest expiry with auto-open, spilling
into the next pack and flooring at zero — and every path that takes it
back (undo, delete, a take turned skip, a converged duplicate)
refunds exactly what the stamp recorded. Replays and pending sync
echoes can never double-charge, and the nightly convergence pass
refunds the rows it tombstones. A medication can declare units per
dose, so two 2 mg tablets book as one 4 mg dose and stock reads in
doses with the unit count alongside; containers carry a named type
including tablet packs, hold up to a thousand units for inhalers, and
the GLP-1 card reads the same stock as the inventory tab instead of a
separate ledger. The inventory endpoints join the documented API.
2026-06-12 17:42:31 +02:00
Marc-André Bombeck b4d2d51d46 fix(dashboard,medications): review findings before the cut
A fresh crisis-level blood-pressure reading outranks an overdue dose in
the daily verdict, and once it ages past a day the dose leads again.
The greeting returns to the page header whenever the daily band is
hidden, and the settings toggle says it rides along. The nightly
convergence pass honours the early grace, so a take recorded shortly
before a window that starts at the dose time heals like the write path
credits it. The early-take context line states a fact — last dose
yesterday, last dose n days ago — instead of claiming an early intake
after a late one, the quick-add default pick runs in the profile
timezone, a dose line without a medication name reads cleanly, the
early-grace constant has one home, and the Telegram confirm writes
intake and snooze atomically again.
2026-06-12 11:22:21 +02:00
Marc-André Bombeck aaecd141ee fix(medications): every intake path converges on the slot, and caches cannot resurrect a pre-write body
The external ingest endpoint was the one intake writer that never
attributed to the minted band: it stored a bare row beside the pending
slot, so an early take through the API showed as the last intake while
the slot kept demanding a dose, the ledger recorded an extra plus a
miss, and the day tally counted both. It now resolves the band and
converges onto the pending row like every other path, the Telegram
confirm and skip buttons do the same preferring the reminder's own
slot, and the nightly convergence pass heals the pairs this has
already produced — one band match, one live anchor row, both days'
rollups recomputed. The cache layer fences in-flight builders on
invalidation so a read can neither join a pre-write build nor have a
background refresh re-cache a pre-write body as fresh. Ad-hoc rows no
longer resolve a different slot through the six-hour radius, a bounded
early grace credits a take up to an hour before the window without
crossing the previous band, day-scale cadences taken early downgrade
the slot-day prompt instead of inviting a double dose, the overdue
pill ignores the projector's pending mints, created intakes carry the
same plausibility bounds as edits, slot moves keep the recorded
injection site and dose, both affected days recompute on a moved
toggle, deliberate slot-less skips converge instead of orphaning, the
coach reads adherence from the ledger, the cards and quick-add run in
the user's timezone with the served next-due as the gate, and
schedule-revision, wipe and restore writes invalidate the caches they
feed.
2026-06-12 10:43:32 +02:00
Marc-André Bombeck ea29e9cf1b feat(dashboard): open the day with a verdict
The dashboard now leads with a daily band: the greeting, the one item
that matters right now — an overdue or upcoming dose, a very high
blood-pressure reading, weight drifting from its range, a run of short
nights, a quiet stretch, a score drop, or the briefing's key finding —
with a single action, today's dose tally beside the next due time, and
the health score as a ring on the right. The verdict resolves from a
fixed nine-rung ladder on the client with the thresholds shared from
the nudge engine through a dependency-free module, the blood-pressure
rung uses fixed crisis floors rather than personal target ranges, and
a cached next-due in the past never renders as overdue. The band
reserves its exact footprint from the first paint, swaps in with the
tiles in one pass, respects the hour-format preference, and can be
turned off in the dashboard settings; the greeting moves out of the
page header into the band.
2026-06-12 09:26:55 +02:00
Marc-André Bombeck ad33fa46ed feat(dashboard): snapshot carries the day's dose state, score and verdict inputs
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.
2026-06-12 09:02:58 +02:00
Marc-André Bombeck 2718930e8d fix(whoop): trim pasted credentials and honour the derived redirect fallback
A trailing space from the portal's copy button reached WHOOP verbatim
and answered as an unknown client; the credential schema trims both
fields. The redirect override fell through nullish coalescing, so the
empty string the compose whitelist materialises for an unset var was
sent as the literal redirect target — empty now derives the callback
from the app URL.
2026-06-12 08:34:10 +02:00
Marc-André Bombeck 6bfba2f187 chore: drop the orphaned locale-prefix helper
The cross-locale status-cache eviction is gone, taking the prefix
builder's last caller with it.
2026-06-11 23:29:09 +02:00
Marc-André Bombeck f790e14edd fix(api,lib): consent before reuse, tighter inputs and caches that tell on themselves
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.
2026-06-11 23:20:38 +02:00
Marc-André Bombeck a2a995e429 fix(insights,medications): close the staleness escape hatches and harden the review findings
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.
2026-06-11 22:49:42 +02:00
Marc-André Bombeck 657f2323a3 fix(sleep,mood): pick the richest writer per night and show the whole note
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.
2026-06-11 21:52:52 +02:00
Marc-André Bombeck a664395021 fix(coach): make remembering reliable and survivable
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.
2026-06-11 21:52:51 +02:00
Marc-André Bombeck 242d2514ee perf(dashboard,insights): reserve the cold-paint footprint and cache the slow overview reads
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.
2026-06-11 21:52:36 +02:00
Marc-André Bombeck b5e799c2d6 fix(charts): degrade chart failures to one card and heal stale shells per version
The update poller and the chunk-error reload guard key on the target
version instead of disarming for the whole session, so a second deploy
re-arms the self-heal and a stale shell can no longer strand with
404ing chunks. Lazy chart imports retry once and mount inside a
per-card error boundary with a reload affordance — a failed chunk
degrades to one card instead of replacing the page. The JSON API
client attaches a fifteen-second timeout unless the caller brings its
own signal (the raw streaming path is exempt), chart queries surface
errors with a retry instead of rendering as an empty period, per-type
fetch failures reject instead of caching an empty series as fresh, and
the session and snapshot queries retry once on transient network or
server errors.
2026-06-11 21:17:27 +02:00
Marc-André Bombeck b346e78cc7 perf(medications): one compliance round trip and a memoised formatter hot path
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.
2026-06-11 21:17:17 +02:00
Marc-André Bombeck 23f7c74c04 perf(insights): regenerate texts only when the underlying data changed
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.
2026-06-11 21:17:05 +02:00
Marc-André Bombeck 03453aaed2 fix(insights,cache): surface revalidation to clients and evict on interactive writes
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.
2026-06-11 19:45:32 +02:00
Marc-André Bombeck 78fe82e348 perf(insights,api): always answer instantly and regenerate in the background
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.
2026-06-11 19:05:49 +02:00
Marc-André Bombeck 6cca9b83aa fix(dashboard): hand the preloaded snapshot over as a promise, not a cache write
Prefetching the snapshot into the query cache before hydration made the
server-rendered skeleton disagree with the first client render. The
early kickoff stays, but the response now commits through the mounted
query itself, so the speedup keeps none of the mismatch. The dead
nudge-resolver leftover goes with it.
2026-06-11 16:15:24 +02:00
Marc-André Bombeck 40f7196c20 feat(medications,coach,admin,perf): era corrections, a truthful weekly status and one card language everywhere
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.
2026-06-11 13:57:15 +02:00
Marc-André Bombeck 5639da4333 feat(medications,admin): the schedule's history is visible and invites count their uses
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.
2026-06-11 11:45:58 +02:00
Marc-André Bombeck a62e00cfcd feat(coach): a guided dialog for follow-ups and nudges that know their owner
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.
2026-06-11 11:45:58 +02:00
Marc-André Bombeck f13aa684f9 fix(api,ops): transactional adopt, admin gate without a flash, and a digest path to deploys
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.
2026-06-11 11:45:58 +02:00