1209 Commits
Author SHA1 Message Date
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 44f344bf52 fix(medications): a manual era cannot swallow tracked live history
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.
2026-06-11 12:08:41 +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
Marc-André Bombeck 5778dd0be0 fix(release): ssr-safe add-param, noon-anchored compliance days, localized wizard assertion
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.
2026-06-11 11:01:22 +02:00
Marc-André Bombeck a8149e0bdf fix(ux): honest deletes, stable hydration and tidy numbers after release review
Server and first client render now agree everywhere query state used to
leak into the chrome, checkboxes no longer nest inside buttons, delete
confirmations admit the undo they offer, raw step counts drop their
decimals, the truncated unit label wraps instead, streaks know their
singular, the medication header speaks the user's date format, a failed
coach turn keeps the user's message and says what went wrong, settings
status notes follow a language switch, the locale only flips once its
bundle actually loaded, and the last raw api fetches in hooks move onto
the shared client.
2026-06-11 03:30:18 +02:00
Marc-André Bombeck 25f41f88fe feat(ux,settings): undo where it was missing and settings that behave one way
Deleting a measurement or mood entry can be undone from the toast via
new restore endpoints, dialogs submit on enter with busy states, silent
mutations gain error toasts, and the settings area finishes its
consolidation: provider and integration cards live in focused
subcomponents, every setting persists server-side at exactly one home,
and the save conventions are written down. The remaining raw fetches in
settings move onto the shared client.
2026-06-11 02:19:28 +02:00
Marc-André Bombeck 3b480b9885 perf(i18n,shell): ship one locale and stop blocking the first paint
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.
2026-06-11 02:19:27 +02:00
Marc-André Bombeck 9445d89b76 refactor(api,tz): one typed client for every call and one home for wall-clock math
A typed apiFetch wrapper owns the envelope unwrap, the ok-check and the
error read for every client call — two hundred call sites across the
app migrate onto it and a lint rule keeps raw api fetches out for good.
The legacy timezone helpers fold into the tz module and test fixtures
drop personal names.
2026-06-11 02:19:27 +02:00
Marc-André Bombeck 7e7e17c68c feat(medications,coach,insights): record the dose you actually took
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.
2026-06-11 00:10:01 +02:00
Marc-André Bombeck ce3b4c70c8 chore(ops): signed images, a secret gate before commit, and a monthly restore drill
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.
2026-06-11 00:10:01 +02:00
Marc-André Bombeck 410c62c292 refactor(jobs,dashboard): a thin worker registry and a composed dashboard
The reminder worker becomes a registry over twelve queue modules cut
along the existing test structure, and the dashboard page sheds its
quick-entry sheets, header and range logic into components — same
behaviour, pinned by the existing suites.
2026-06-11 00:10:01 +02:00
Marc-André Bombeck 35e8f076a2 refactor(openapi,queries): feature-located registries behind thin indexes
The five-thousand-line route registry splits into thirteen domain
modules and the query-key factory into twelve feature files — each
behind an index that preserves exact registration order, proven by a
byte-identical generated contract. The factory lint rule learns the new
home directory.
2026-06-11 00:10:00 +02:00
Marc-André Bombeck bcbb05c770 fix(charts,medications): hold the overdue dose on the card and floor non-negative axes
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.
2026-06-10 22:41:28 +02:00
Marc-André Bombeck bd78cd7f7a feat(medications): schedules remember their history
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.
2026-06-10 22:21:44 +02:00
Marc-André Bombeck 5e1035e47a fix(medications): drop the due-context line when no slot is plausibly near
History rows recorded before a schedule's first minted slot paired with
an anchor days or weeks away and read 'due 09:00 (-1904 h)'. The
nearest-anchor fallback now caps at the band's capture reach plus one
inter-slot gap; beyond that the row simply reads off-schedule.
2026-06-10 21:41:12 +02:00
Marc-André Bombeck 024da9fe96 fix(ui): granular sleep sources, viewport-safe tour, one aligned settings language
A night with coarse apple-health samples next to granular watch data
showed only awake and asleep — the per-session source pick now prefers
the source that knows its stages. Tour popovers clamp into the
viewport, wrap their footer and fall back to a bottom sheet on narrow
screens. Settings and admin sections share one alignment rule — icon
column left, everything else flush on the content column — with neutral
header icons, no duplicated section titles under the shell heading, and
every select on the account-style idiom with a visible chevron. The
dashboard's customize icon matches insights, the target-settings link
leaves the home charts, and the mood heatmap adopts the damped fills
the other mood charts already use.
2026-06-10 21:19:05 +02:00
Marc-André Bombeck 9f2c403f84 feat(coach): a chat that behaves like one
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.
2026-06-10 21:19:05 +02:00
Marc-André Bombeck b55fab5e94 fix(insights,coach): warm every card nightly and round out the smaller findings
The pregenerate pass now warms the status cards even when the
comprehensive step is budget-blocked, failed or timed out — the gap
that left cards generating on first visit despite the nightly runs —
and the loop gains a per-user timeout so one hanging provider cannot
stall the tick. Schedule edits recompute their rollup days immediately,
the compliance cache keys on the timezone, open doses no longer count
against the nudge trigger, an invite sent against an open registration
still lands in the ledger, the self-report prompt fence uses explicit
data markers, the maintenance crons invalidate the caches they touch,
and a flaky audit assertion polls instead of racing. Six care-focused
achievements join the engine: miss-free streaks, measurement routine,
a complete self-context and a sleep-log run.
2026-06-10 20:48:46 +02:00
Marc-André Bombeck 1e34671d92 feat(ux): silhouettes from the first paint and one filter language
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.
2026-06-10 20:48:46 +02:00
Marc-André Bombeck 258e2fd365 feat(medications): an api tab, a stockable inventory and a next-due that tells the truth
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.
2026-06-10 20:48:46 +02:00
Marc-André Bombeck cc00d68d8c fix(a11y): underline the new cross-links and give the chart link a touch target
The integrations/source-priority hint links relied on colour alone and
the chart's adjust-targets link measured sixteen pixels tall on mobile —
both caught by the e2e gate. The links carry a permanent underline now
and the chart link a full-height touch area.
2026-06-10 17:54:56 +02:00
Marc-André Bombeck 922fe88c11 fix(ci): repin the metadata action and drop two dead exports
The freshly released metadata-action v6.1.0 exits without output on the
pull-request build path; both call sites pin back to the proven v6.0.0
commit. The dead-code gate also caught an unused read helper and an
unused analytics type left behind by the wave — both removed.
2026-06-10 17:46:43 +02:00
Marc-André Bombeck a5650d0617 fix(medications): key the dedup pin guard on persisted attribution, not a timestamp shape
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.
2026-06-10 17:33:56 +02:00
Marc-André Bombeck 04765ea8ac style: one spacing, title and skeleton language across the app
Icons inside buttons drop their manual margins in favour of the
button's built-in gap (two hundred call sites), form field stacks settle
on one spacing step, card titles on exactly two sizes, inline field
errors on one text size, and a dozen hand-rolled loading pulses move
onto the shared skeleton primitive while keeping their silhouettes. The
intake dialog wording joins the rest of the app on 'übersprungen', and
the UI guidelines now document the two deliberate footer and
touch-target systems.
2026-06-10 16:54:15 +02:00
Marc-André Bombeck 4ca3de5ff3 fix(medications): make the nightly dedup and the pin path respect each other
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.
2026-06-10 16:54:15 +02:00
Marc-André Bombeck 5b75184649 feat(coach,admin): finish the self-context and invite surfaces end to end
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.
2026-06-10 16:18:48 +02:00
Marc-André Bombeck 9fe5673251 fix(dashboard): one skeleton language and a single chart reveal on first load
First load showed bare dark tiles, then the mood chart finished alone
and the remaining charts popped in one after another. Every widget now
renders a structured silhouette of its final shape, charts mount in
parallel but stay behind height-stable skeletons until all visible
charts report ready — with a two-second fallback so one slow widget
cannot hold the page — and the reveal fades in once, without layout
shift. The intake repair script becomes self-contained on plain pg so
it actually runs inside the production container, with the runbook
updated to the working invocation.
2026-06-10 16:18:26 +02:00
Marc-André Bombeck 51dd614199 feat(coach,auth): self-context for the assistant, invite links, proactive nudges
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.
2026-06-10 15:40:48 +02:00
Marc-André Bombeck b108ae7318 feat(ux): close the feedback gaps a new user runs into first
The Withings OAuth callback finally reports its outcome — connected or
a readable reason — instead of dropping the user on the settings page
without a word. The adherence number carries one name everywhere and an
explanation right where it is shown. Demo mode announces itself with a
banner instead of failing silently in English. Bug reports become
reachable from the mobile menu, the about section returns to the
settings navigation, charts link to the target settings they draw,
integrations and source priority point at each other, and the
medication wizard warns when reminders are switched on without a
configured channel. The longest explainer texts shrink to two
sentences.
2026-06-10 15:40:37 +02:00
Marc-André Bombeck 0cf9bc9348 feat(medications): attribute an off-schedule intake to its slot, visibly and reversibly
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.
2026-06-10 15:39:55 +02:00
Marc-André Bombeck d7bf0df10b chore(ci): pin the supply chain and surface build provenance
Every workflow action is pinned to a commit SHA, dependabot auto-merge
is limited to patch and minor updates behind the now-required status
checks on main, and the audit, Trivy and ESLint gates block instead of
warning. The published image finally carries its build SHA and build
timestamp so the version endpoint can prove which commit is running.
The API-token HMAC key fails closed under 32 characters, compose pins
the database minor and denies privilege escalation, and the changelog
drops working-note references in favour of neutral wording.
2026-06-10 15:05:05 +02:00
Marc-André Bombeck aabc7915cb feat(settings): a time-format preference and one home for insight pills
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.
2026-06-10 13:34:46 +02:00
Marc-André Bombeck 3cf48fa0f1 fix(insights): honest assistant pipeline — gates, tombstones, retries, truthful states
The two-o'clock status crons generated for every account, ignoring both
the per-user coach opt-out and the operator kill switch; they now share
one gated discovery that also skips users the pregenerate pass will
rewarm anyway, and the mood card finally gets the nightly run the other
six always had. Deleted measurements no longer feed any prompt. Locales
without an assistant language fall back to English consistently instead
of German texts with English fallbacks. A rate-limited regenerate stops
claiming success, the dashboard briefing state distinguishes a missing
provider from a pending generation and serves a stale briefing with its
timestamp instead of hiding it, narrative warming actually processes
oldest first, and worker failures reach the error tracker through one
shared reporter while the insight queues gain real retries.
2026-06-10 13:34:37 +02:00
Marc-André Bombeck 0a4bf34a2b fix(medications): align the slot lifecycle with the band model end to end
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.
2026-06-10 13:34:26 +02:00
Marc-André Bombeck fb6e494f64 feat(medications): calmer detail page — one card language, six tabs, a status overview
Every detail section now sits on the card surface with the standard
padding instead of a bare border on the page background, the schedule
tab loses its duplicated summary header, and the reminder tab folds
into the schedule tab next to the times it gates (old deep links
redirect). The overview tab becomes a real landing surface — next due
dose, reminder state and supply runway with jump links — instead of a
lone compliance bar. History rows calm down: settled rows carry a
single row menu, actionable rows exactly one visible take button plus
the menu, and the quick-mark toast gains undo. The card menu drops the
advanced-settings entry and its edit action opens the editor it
promises. Schedule-tab controls share one height system, and the
injection charts load on demand so the chart library leaves the
initial bundle.
2026-06-10 13:34:13 +02:00
Marc-André Bombeck 6f45d72a84 fix(ui): unclip the score-ring glow, reachable insight pills, consistent destructive actions
The score ring's bloom was clipped by the SVG's own viewport — the
drop-shadow sits on a child of the viewBox-filling arc — so the glow
never extended past the circle; the SVG now overflows visibly on every
host. The insight pill strip gains chevron buttons and wheel scrolling
so an overflowing row is reachable with a mouse. Safari favourites
rendered the transparent icon on a white plate; a flattened
apple-touch-icon and a maskable manifest icon fix both. The
time-of-day mood chart adopts the damped fill the weekday chart already
uses, eighteen delete confirmations move onto the destructive button
variant (one of them had the wrong text colour), five confirmation
dialogs gain pending states, the sources dialog stacks its primary
action on top on mobile, and the token form aligns input and button
heights.
2026-06-10 12:56:06 +02:00
Marc-André Bombeck 298e4c34b4 perf: collapse the medication compliance hot path and the dashboard fan-out
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.
2026-06-10 12:55:56 +02:00
Marc-André Bombeck 65677c2112 fix(api): bound every JSON body read
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.
2026-06-10 11:36:51 +02:00
Marc-André Bombeck eb1bbef295 chore(license): move to PolyForm Noncommercial 1.0.0
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.
2026-06-10 11:18:12 +02:00
Marc-André Bombeck 9e6d0d5acb fix(medications): converge intake writes onto existing slot rows across sources
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.
2026-06-10 11:18:00 +02:00
Marc-André Bombeck 09dbb1131d fix(medications): guard intake edits against implausible timestamps
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.
2026-06-10 11:17:48 +02:00
Marc-André Bombeck 0761ce2346 fix(medications): widen write-path band minting to cover day-scale tails
The write path minted attribution bands over a fixed ±1 day around the
take, so a weekly or rolling dose taken two to four days late — inside
its own overdue tail — never found its anchor and was stored ad hoc
while the slot stayed missed. Day-scale schedules now mint over the full
on-time-plus-overdue reach derived from the configured dose windows;
minute-scale schedules keep the ±1 day probe.
2026-06-10 11:17:38 +02:00
Marc-André Bombeck 76012bf8e2 chore: scrub instance details and personal data from committed artefacts
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.
2026-06-10 11:00:26 +02:00
Marc-André Bombeck b8c2b2e8c5 fix(measurements): keep the mean-consolidation pass alive past unique collisions
The nightly mean-consolidation upsert targets the externalId arbiter, but
the measurement table carries a second full unique on (user, type,
measured_at, source, sleep_stage) that also spans tombstones. Any row
parked exactly on the canonical local-noon instant made the insert throw
P2002, and because the global pass had no per-bucket fence the first
poisoned day aborted consolidation for every remaining user and type.

writeDay now probes the canonical slot inside the transaction and
resolves the collision deterministically: rows the pass owns (tombstones,
per-sample rows about to fold) shift to the next free second, foreign
live rows keep their instant and the mean row yields instead. The runner
gains an optional per-bucket error fence so one bad day is logged,
counted as daysFailed, and skipped.
2026-06-10 11:00:11 +02:00
Marc-André Bombeck eb2c7ced73 fix(medications): read pending doses as upcoming until their window closes
A server-minted pending row (taken_at null, not skipped, not auto-missed)
was scored missed the moment its slot time passed, so the history tab and
the compliance rate punished doses that were still takeable under the
band model. Pending rows now read upcoming until the band's overdue end;
auto-missed keeps its terminal verdict. Pending rows outside the minted
band window are dropped instead of surfacing as phantom ad-hoc lines.
2026-06-10 11:00:02 +02:00
Marc-André Bombeck 050ddbecb2 test(medications): make compliance and analytics tests timezone-stable
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.
2026-06-08 20:09:17 +02:00
Marc-André Bombeck dfb6a7f298 fix(medications): keep dose-window defaults off the client server graph
The dose-window editor pulled DOSE_WINDOW_DEFAULTS from
@/lib/analytics/compliance, dragging the server-only scheduling graph
(which imports node:async_hooks) into the client bundle. Turbopack
rejected the client chunk; tsc does not model the boundary so the type
check stayed green.

Move the constant into a leaf module with no server imports and
re-export it from compliance so the server path is unchanged. The
client-reachable editor now imports the leaf directly.
2026-06-08 20:09:09 +02:00