Commit Graph
5608 Commits
Author SHA1 Message Date
Marc-André Bombeck dceca62f8e Pin the async-truth contract over both document state machines
Two contract tests enumerate every branch of the summary job and the
index tree and assert each ends in a terminal state write: the summary
job's opt-out branch heals a phantom PENDING back to NONE, every
failure branch lands UNAVAILABLE or WITHHELD, success lands READY; the
index tree records an attempt outcome on every terminal branch,
success included. A third block pins the enqueue side: no PENDING is
claimed when the job's only branch would be the opt-out no-op, and
none before a job id exists. Honest scope stated in the test header:
these pin the two known machines, not a generic detector.

Refs #776, #788
2026-08-13 12:42:33 +02:00
Marc-André Bombeck 893db887e9 Record why a document's index attempt failed and say so
A content-index attempt that failed (raster failure, a scan with no
text layer, a provider error) recorded its reason only in the server's
observability stream. In the vault, hasContentIndex false looked
identical for a document nobody ever tried and one that failed three
times, and the detail sheet had nothing to say about either.

Apply the summaryState pattern to the index. Two additive columns on
InboundDocument (migration 0333) record when the last attempt finished
and why it produced no index, as a closed reason set: local-empty,
local-unsupported, decrypt-error, raster-failed, provider-error,
pdf-needs-anthropic, empty-transcription. The writer sits at the choke
point the auto job and the corpus backfill share, and in the manual
index route; a success clears the reason. The provider path's specific
failure now survives the local fallback, since "no text" is less
actionable than "the PDF would not render". rasterizePdf names its
failure class so a build that cannot rasterize at all is distinguished
from a PDF that would not render. New guard in both index paths: an
empty provider transcription never becomes a successful empty index.

The detail sheet renders the reason in plain words (muted, six
locales). The DTO and OpenAPI carry the two fields; the disaster
recovery backup and restore carry them verbatim like their summary
siblings.

Refs #776
2026-08-13 12:40:50 +02:00
Marc-André Bombeck 45dc93932a Settle a timed-out assessment refresh instead of calling it failed
The force regenerate aborts its request at 45 s while the server's
inline generation keeps running and usually writes the fresh briefing
moments later. The old handling fired an error toast at the abort and
re-read the old cache once; with a one hour staleTime and focus
refetch off, nothing ever converged, so the page claimed failure while
holding a finished assessment at arm's length.

A timeout now enters a settling state: the hook remembers the cachedAt
baseline it was showing and polls the read path on the existing
bounded cadence (25 s, up to 10 attempts) until it can prove an
outcome. Cache advanced past the baseline: the fresh payload lands and
the success toast fires. A failure marker newer than the baseline, or
the attempt cap: an honest error. While settling, the strip's button
stays busy and says the assessment is still being prepared; no toast
fires until the outcome is proven. Both poll reasons live in the
nextAdvisorPollInterval seam, the verdict in resolveAdvisorSettle, the
toast mapping in regenerateToastKind; all three are pure and pinned by
unit tests. The contract is documented at both ends (hook and generate
route). New copy in all six locales.

Refs #786
2026-08-13 12:24:46 +02:00
Marc-André Bombeck 86a3d74187 Report how many documents the reindex actually queues
The index-all trigger returned Boolean(jobId) while the client typed
the field as a number, so the toast read "Indexing true document(s)".
The route now answers with the count of live documents still lacking a
content index, computed from the same live-scoped counts the usage
gauge reads, and an honest 0 when no job was created. OpenAPI schema
updated to the integer shape; the hook's type carries a contract note
so the wire and the toast cannot drift apart again. Route test pins
the number, a component test pins that the toast renders the count and
never the word "true".

Refs #776
2026-08-13 12:13:55 +02:00
Marc-André Bombeck 4543fa7192 Let the app CSP frame and render its own blob previews
Since the record fence, vault previews load through object URLs: the
detail sheet frames a decrypted PDF from a blob: URL and thumbnails
render from blob: images. The app CSP carried no frame-src, so the PDF
iframe fell through to default-src 'self' and was blocked outright,
while img-src 'self' data: silently degraded every thumbnail to its
fallback icon.

Add frame-src 'self' blob: and extend img-src with blob: in both the
dev and prod policies. This admits exactly the app's own documents: a
blob: URL is mintable only by same-origin script, script execution
stays nonce-bound in production, and object-src 'none', frame-ancestors
'none' and default-src 'self' are untouched. The vault serve route
keeps its stricter document CSP. A unit test pins the directives so a
future CSP edit cannot silently regress the previews.

Refs #787
2026-08-13 12:08:59 +02:00
Marc-André Bombeck 2a85da67f7 Stop claiming a summary is pending when auto-read is off
The upload path enqueued a summary job and stamped PENDING without
consulting the owner's auto-read opt-in; the job's opt-out branch then
deliberately left the state alone, so a document uploaded with the
opt-in off said "summary is being generated" forever. Two writers,
contradictory assumptions.

Both ends of the contract now agree. enqueueDocumentSummary checks the
opt-in first and refuses to mint a job (or a PENDING) it knows the job
cannot resolve; the job keeps its own check for a pref flipped between
enqueue and run and heals a stranded PENDING back to NONE, scoped so
READY / WITHHELD / UNAVAILABLE stay untouched. Migration 0333 drains
the rows already orphaned. The view needs nothing: NONE already
renders the manual summary action.

Refs #788
2026-08-13 12:07:39 +02:00
Marc-André Bombeck c8b3f49915 Merge pull request #785 from MBombeck/fix/detail-fixture-horizon
Keep the workout-detail fixture inside the retention horizon
2026-08-13 11:51:32 +02:00
Marc-André Bombeck 698844d0e2 test(workouts): keep the detail fixture inside the retention horizon
Sister file of the HR-curve integration fixture fixed on the release
branch: the same fixed session day aged past the 90-day dense-intraday
horizon today. This suite stayed green because its pulse read is mocked
empty either way, which is worse — the fallback branch its comments
describe was silently no longer reachable, and the timezone assertion
pinned a literal next-day key. The session day is relative now and the
day key is computed from it.
2026-08-13 11:33:26 +02:00
Marc-André Bombeck 5ac096ede2 Merge pull request #784 from MBombeck/release/v1.37.14
Release v1.37.14
v1.37.14
2026-08-13 10:43:19 +02:00
Marc-André Bombeck 3e346e299c test(integration): keep the HR-curve fixture inside the retention horizon
The workout session sat on a fixed calendar day, and the pulse-window
fallback only reconstructs a curve inside the 90-day dense-intraday
retention horizon. On day 91 the fixture aged out and all three curve
assertions went red on every branch, ours and main alike. The session
day is relative to the test run now, ten days back, which stays inside
the horizon forever.
2026-08-13 10:25:39 +02:00
Marc-André Bombeck 499ca2c4b4 chore(release): v1.37.14 — the rail tells today's truth, immediately 2026-08-13 10:05:23 +02:00
Marc-André Bombeck e991ef7c77 Refetch the inactive daily reads after a quick-add intake
The medication quick-add mounts from the bottom-nav capture picker on
every page, not only on the dashboard, so its bare dependent-key bundle
invalidation (default active refetch) left the unmounted Today digest
and dashboard snapshot stale until the next poll — back on the home
screen the dose still read as due for up to two minutes. Route the
submit through invalidateMedicationReads, the one blessed entry point
that pairs the bundle with the forced inactive refetch, and drop the
surface's allowlist entry in the refetch guard: its dashboard-only
rationale stopped being true when the capture picker started mounting
this form globally. The other capture-picker forms (measurement, mood)
already pair the refetch.
2026-08-13 09:14:55 +02:00
Marc-André Bombeck 767a2b039b Bound the Today rail's non-overdue doses to the user's local day
A weekly or rolling medication with the default dose window opens its
availability ~25 h ahead of the anchor (one early day plus the grace
minutes), so the rail announced tomorrow's slot as today's pending
business — on Friday the Saturday dose appeared as due. Thread the
profile-timezone end of today from the digest IO seam into the builder
and keep a not-yet-overdue candidate off the rail unless it falls due
before that instant. Overdue candidates still always surface, and the
early-take affordance on the medication card is untouched.
2026-08-13 09:12:20 +02:00
Marc-André Bombeck 7e4a4d2544 Merge pull request #783 from MBombeck/release/v1.37.13
Release v1.37.13
v1.37.13
2026-08-13 08:12:36 +02:00
Marc-André Bombeck 17fb291aaf fix(import): draw the extraction temp name from crypto randomness
The extracted export.xml lands in the shared system tmpdir under a
name built from the clock and Math.random, which is predictable enough
for a local pre-creation or symlink attack to aim at. The name comes
from crypto.randomBytes now. Flagged by code scanning on the import
path.
2026-08-13 07:55:07 +02:00
Marc-André Bombeck 91d4908204 e2e: pin the auto-read contract per document read state
The auto-read spec pinned the old collapse rule: with auto-read ON the
sheet always dropped the AI block. Since the read action stays visible
for a document that has no content index yet, the collapse assertion
now runs against the indexed content fixture, and a second test pins
the recovery path: an unread document keeps its AI section and manual
read under auto-read. Refs #777.
2026-08-13 07:35:15 +02:00
Marc-André Bombeck ff88dd1f8f coach: keep cancelled-turn markers out of the plan fold too
The facts fold already skips the empty assistant row a cancelled turn
leaves behind; the plan fold read it as a blank turn and spent one of
its recent-turn slots on nothing. Same skip, same reason. Also state
the accepted one-slot slack a late rate-limit refund can leave in a
freshly opened window. Refs #781.
2026-08-13 07:11:32 +02:00
Marc-André Bombeck 47b0d340b7 chore(release): v1.37.13 — the record reads honestly under load 2026-08-13 06:19:13 +02:00
Marc-André Bombeck ce6364f265 coach: keep cancelled-turn markers out of summary and fact folds
The rolling-summary and fact-extraction workers read message bodies
directly, so an empty cancelled marker would ride their prompts as a
blank assistant line. Skip empty bodies in both folds. Refs #781
2026-08-13 06:16:45 +02:00
Marc-André Bombeck a1b1617d0f settings: move the About-me note and free-text allergies into the Anamnese
The free-text self-context note and the free-text allergy line are
personal medical context, so they join conditions, allergies and family
history in Settings -> Anamnese. The note is its own card (same AI-surface
gate as the inclusion card); the allergy line sits directly under the
structured AllergyManager in the same card, labelled as a free-text
supplement so the two inputs never read as duplicates. The account
section drops the old panel. Store, API and query keys are unchanged;
the scoped Clear (note only, allergies untouched) carries over with its
tests. Orphaned strings removed, new labels in all six languages.
Refs #159
2026-08-13 06:16:45 +02:00
Marc-André Bombeck 13b7d0379b coach: reopen an interrupted conversation with a visible retry
An aborted stream now leaves a trace on both ends. The unmount cleanup
records the interrupted conversation (sessionStorage, one-shot) before
the abort fires, and invalidates the cached thread + rail so the return
visit refetches the advanced server state. The /coach page consumes the
record on its next mount: a known id reopens that thread, a first-turn
abort resolves via most-recent; every explicit entry intent (?c=, ?doc=,
?workout=, ?scope=, ?ask=) wins over the resume. The persisted cancelled
marker renders as a muted interrupted note with an Ask-again action that
resubmits the question through the normal send path and its gates.
Localised in all six languages. Refs #781
2026-08-13 06:16:45 +02:00
Marc-André Bombeck 9a631c8f8b coach: close an aborted turn with a persisted cancelled marker
Navigating away mid-reply aborts the SSE fetch; the provider call tears
down, but the conversation was left with the user message dangling and
no trace of the interruption. Persist an empty assistant row tagged
providerType "cancelled" (the refusal/nudge sentinel channel) when the
request signal is aborted, refund stays intact, and the abort is
annotated as insights.coach.cancelled instead of a provider failure or
a GlitchTip defect. Cancelled markers are filtered out of the provider
transcript on later turns. Refs #781
2026-08-13 06:16:45 +02:00
Marc-André Bombeck 12e5817c16 test(import): prove bounded heap on a large synthetic export archive
Generate a 700k-record export.xml (~200 MB decompressed, 500k
cumulative step records each carrying a fresh HKDevice runtime address,
200k interleaved spot rows) streamed into a temp stored-method ZIP —
never checked in, never held in memory whole — and push it through the
real extractExportXml + streamParseExportXml pipeline against a
discarding Prisma stand-in.

Live heap is measured after forced collections at every progress tick;
the asserted growth bound (48 MiB) is a quarter of the decompressed
volume. Both historical failure modes blow past it: reverting the
device-address normalization retains ~61 MiB of fragmented source
buckets, and unbounding the batch buffers retains ~76 MiB and starves
the progress ticks. The fixed pipeline measures under 1 MiB of live
growth. The suite also pins semantics alongside memory: every record
read, every spot row flushed, and exactly one cumulative estimate row
per day rather than one per device address. Refs #775.
2026-08-13 06:16:44 +02:00
Marc-André Bombeck d26bbca52a fix(import): translate machine-readable import failure reasons
A user whose import died to a worker restart saw the literal string
interrupted_by_restart in the import card — the reconcile writes it as
a machine-readable code, and the card echoed it raw. Classify the two
coded reasons (interrupted_by_restart, the insufficient_memory prefix
from the memory preflight) onto translated, actionable copy across all
six locales; the copy tells the user their data is safe and to upload
the export again, which the re-enabled card already supports. Honest
free-text reasons keep passing through verbatim. Refs #775.
2026-08-13 06:16:44 +02:00
Marc-André Bombeck 50ff90a409 feat(import): refuse an Apple Health import the heap cannot carry
Before the multi-minute unpack starts, compare the archive's declared
uncompressed export.xml size against the V8 heap limit. The streaming
parse keeps resident state span-bounded, so the model is generous —
a fixed 192 MiB base plus 1/64 of the XML size; the 8 GiB extraction
cap needs ~320 MiB, which any default heap clears. The check only
refuses the truly impossible (an operator-pinned tiny heap against a
huge export) and does so with an actionable failureReason naming
NODE_OPTIONS=--max-old-space-size, instead of an OOM-killed worker
and a job stuck in a running state (#775).

Document the memory model, the insufficient_memory refusal, and the
interrupted-import reconcile in the self-hosting scaling guide.
2026-08-13 06:16:44 +02:00
Marc-André Bombeck dd02f983cb fix(import): bound Apple Health import memory independent of archive size
The export.zip extractor buffered the whole archive through
readFileSync — hundreds of MB resident for the entire unpack phase on
a real multi-year export. The central directory is now located through
a bounded tail read on a file handle, read as one size-capped slice,
and the export.xml member streams through a byte-range read stream
into the inflate pipeline, so peak extraction memory no longer scales
with archive size.

The cumulative fold hashed the raw device attribute into its per-day
source buckets. Apple serialises that attribute from the live HKDevice
object including its runtime address (<<HKDevice: 0x283a08640>, ...),
which is not stable across the export enumeration — the map fragmented
toward one bucket per record and an 11-million-record export grew it
past the default 1 GB Node heap, killing the worker mid-parse (#775).
Stripping the volatile address before hashing restores the documented
invariant: parser state bounded by actual source cardinality.

Also carry split multi-byte UTF-8 sequences across read-chunk
boundaries with a StringDecoder — a bare per-chunk toString decayed a
split umlaut in a source name to U+FFFD.
2026-08-13 06:16:44 +02:00
Marc-André Bombeck a075a2fc50 Bump the service-worker version fallback to v1.37.12
The prebuild generator rewrites it on every build; keep the committed
literal in step so a stale checkout cannot resurrect old caches.
2026-08-13 02:27:05 +02:00
Marc-André Bombeck 0716f7a2fb Pin the declared diabetes flag's route contract with tests
GET/PATCH /api/auth/me/diabetes carried no test; the web toggle on the
Targets page and the iOS client both ride it. Cover the auth gate, the
resolved-state echo both ways, the field-by-field write, the audit row,
the 422 shape rejections, and the 429 path.
2026-08-13 02:27:05 +02:00
Marc-André Bombeck e83b58c9da Let the AI-consent card grant again after a withdrawal
POST /api/consent/ai/web now accepts an optional { intent } body. Without
one (the AI-settings mount heal) a standing revocation still blocks the
mint; intent "affirmative" is the user's own consent act and may
supersede an earlier revocation, exactly as a first grant would. The
consent card offers the explicit grant control whenever consent does not
stand, and the audit row records which intent minted the receipt.

The document auto-read opt-in already declared "affirmative" on enable;
the settings-mount heal posts no body. Both stay as they are.
2026-08-13 02:27:05 +02:00
Marc-André Bombeck fd104dd0ac feat(documents): review and extract lab values on the document itself
The server chain existed end to end (extract stages PENDING facts, per-fact
edit clears the low-confidence gate, confirm is the only write into the
structured stores) but the document detail offered no control over any of
it: it counted observation facts and linked to Labs. A user whose automatic
staging run was skipped had to re-upload the same PDF through Labs > Scan a
report.

- The detail sheet gains a staged-facts block with honest states: pending
  facts open an inline review (approve/reject per fact; a low-confidence
  fact is checked and saved first via the existing per-fact edit); a
  lab-filed, already-read document with nothing staged offers "Extract lab
  values" over its stored text; otherwise nothing renders.
- The extract route gains { mode: "stored" }: the same text-structuring
  pass the automatic worker runs, over the document's own content index.
  No re-upload, no second read of the original; full gauntlet including
  the shared document-AI bucket with charge-at-dispatch honesty; 422
  notIndexed when no stored text exists.
- A manual "Read with AI" / "Read again" now continues into the same
  lab auto-staging the background worker performs; the response reports
  labFactsStaged and the toast names the values waiting for review.
- Unchecked values are rejected and the copy says so; an unverified
  low-confidence fact receives no decision and stays pending rather than
  being silently discarded. Confirmation remains the only write into
  Labs, conditions, and medications; approved facts carry the
  committedRecordId link back to the source document (existing model, no
  schema change).
- The "lab values from this document" link now counts APPROVED facts
  only — it claims values that actually exist in Labs.
- Copy in all six locales; OpenAPI regenerated for the new extract mode
  and the index response field.
2026-08-13 00:33:14 +02:00
Marc-André Bombeck 8e16df6894 feat(documents): make the document-AI rate limit an operator setting with honest accounting
The document-AI actions (read/index, extract, suggest, summarise) shared a
hard-coded 6/hour bucket that lied twice: the 429 said "try again in a few
minutes" while the window is an hour, and several failure paths burned a
slot before any provider was dispatched.

- DOCUMENT_AI_LIMIT_PER_HOUR configures the shared per-user ceiling
  (default 6, clamped 1-1000), wired end to end: compose environment
  whitelist, .env.production.example, env manifest, operator docs.
- INSIGHTS_RATE_LIMIT_PER_HOUR joins the compose whitelist and manifest;
  it was documented but never listed, so an operator value could not reach
  the container.
- The 429 mirrors the bucket's reset instant into meta.retryAt (plus the
  existing X-RateLimit-Reset header); the client derives "try again in
  about N minutes" from it in all six locales, with a plural-aware key.
- A slot is consumed only when a request actually reaches the provider (or
  the local index/echo work): malformed bodies, unreadable originals,
  refused consent, and exhausted daily budgets refund the charged slot via
  a window-scoped refundRateLimit primitive. Consistent across all four
  routes sharing the bucket.
2026-08-13 00:33:14 +02:00
Marc-André Bombeck 592d1e9aa8 fix(documents): let index-all rasterise PDFs for image-input providers
The content-index backfill built its candidate MIME list from the
provider's native PDF support, so a provider that reads images but not
PDFs never saw a single PDF in "index all documents" — while the
per-document path already rasterises a PDF to page images for exactly
that provider class. The prefilter was stricter than the authority it
fronts.

PDFs are always candidates now; prepareVisionInput stays the per
document authority (native pass-through or rasterised pages). The run
summary also stops folding outcomes into one number: skipped (nothing
billable happened, retried later) and failed (a provider call missed)
are counted separately, carried through the wide event and the worker
log, and reruns stay idempotent because indexed documents drop out of
the candidate set.

Refs #776
2026-08-12 22:52:03 +02:00
Marc-André Bombeck 3254e44281 fix(documents): keep the read action visible for an unread document under auto-read
The AI section of the document detail sheet collapsed whenever auto-read
was on and nothing was pending review, without checking whether the
document actually has a content index. A document the automatic pass
never reached (uploaded before the preference, or the pass did not
complete) then had NO visible way to be read; turning auto-read off was
the only workaround.

Collapse only once the document is read (or when indexing is unavailable
on the transport, where no action could be offered anyway). An unread
document keeps the Read with AI action, introduced by a short honest
note in all six locales; the suggest and summarise extras stay hidden
because the auto-read promise still covers them. The three states stay
distinct: unread shows the note plus the action, an in-flight read shows
the reading label, a read document collapses the block.

Refs #777
2026-08-12 22:52:03 +02:00
Marc-André Bombeck 19c54082bf fix(score): read biomarker bounds by their real column names
The score reader's labs query selected referenceLow/referenceHigh on the
joined Biomarker, but the catalog's range columns are lowerBound and
upperBound. Prisma rejects a select against names a model does not have
at query-validation time, before any row is read, so every labs read
threw and both the Glycaemia and Lipids pillars reported a failed read
permanently, Retry included, even for accounts with valid lab results.

Select the real columns, map the catalog bounds onto the reader's range
fallback, and pin two contracts: a real-Postgres round trip that scores
both pillars through the biomarker join (including a lipid row whose
range lives only on the catalog), and the honest empty state, where a
labs account with no rows reads as not tracked rather than read_failed,
which stays reserved for a read that genuinely failed.

Refs #780
2026-08-12 22:52:03 +02:00
Marc-André Bombeck d21fe48e3c Merge pull request #773 from MBombeck/release/v1.37.12
v1.37.12 — the public privacy policy matches the app
v1.37.12
2026-08-11 18:59:00 +02:00
Marc-André Bombeck 5191f213f4 chore(release): v1.37.12 — the public privacy policy matches the app 2026-08-11 18:20:18 +02:00
Marc-André Bombeck 40e2c71f42 Merge pull request #772 from MBombeck/fix/public-privacy-support
Align the public privacy data-flow disclosure
2026-08-11 17:56:18 +02:00
Marc-André Bombeck 92898b5271 docs(privacy): align the public data-flow disclosure 2026-08-11 17:36:15 +02:00
Marc-André Bombeck ca88d8c1a9 Merge pull request #771 from MBombeck/release/v1.37.11
v1.37.11 — one save for the lifestyle context
v1.37.11
2026-08-10 10:43:54 +02:00
Marc-André Bombeck 1e89b8de9c chore(release): v1.37.11 — one save for the lifestyle context 2026-08-10 10:23:59 +02:00
Marc-André Bombeck 6e85cbb2f1 test(e2e): scan the mood insights page and fix its stale a11y mock
The `/api/mood/insights` mock still returned the pre-heatmap shape, so
`summary.totalEntries` was undefined, the empty-state guard never
short-circuited, and the page threw reading `heatmap.cells`. Correct the
mock to the current MoodInsightsResponse and add `/insights/mood` to the
a11y route matrix so the page is scanned henceforth.
2026-08-10 10:23:24 +02:00
Marc-André Bombeck cfda1d9a78 refactor(charts): route the mood chart header through TileHeader
The mood chart hand-rolled a CardTitle where every other insights tile
header goes through TileHeader. Swap it for the shared primitive, which
renders the same CardTitle at text-base, and drop the now-unused CardTitle
import. Like-for-like; the surrounding control rows are untouched.
2026-08-10 10:23:24 +02:00
Marc-André Bombeck 6130f6ee6f feat(records): save the whole lifestyle-context tile with one action
The lifestyle-context tile carried a Save button per fact kind. Collapse
them into one footer Save that writes only the dirty kinds, firing the
existing bitemporal call per kind (POST a first value, PATCH a revision)
and settling them together so a single conflict cannot take the healthy
writes down with it. A kind that 409s keeps its draft on screen and names
itself, instead of vanishing behind one green toast. The per-kind Remove
stays.
2026-08-10 10:23:24 +02:00
Marc-André Bombeck b81e3928d8 Merge pull request #770 from MBombeck/release/v1.37.10
v1.37.10 — one drug, a different dose at each time
v1.37.10
2026-08-10 08:45:18 +02:00
Marc-André Bombeck 61e5c36d0a style(medications): prettier-format the per-slot consumption test 2026-08-10 08:22:42 +02:00
Marc-André Bombeck 196b37fa78 chore(release): v1.37.10 — one drug, a different dose at each time 2026-08-10 07:59:29 +02:00
Marc-André Bombeck dbb3f216d8 test(medications): type the consumption stamp reduce accumulator 2026-08-10 07:58:48 +02:00
Marc-André Bombeck 88763aaf7f test(medications): update consume mock for per-slot units reads
The consume hook now loads the user's zone, the schedules, and the
intake's scheduled anchor to resolve per-slot units, so the fake
client grows the schedule read and the batch scheduledFor lookup. The
Italian per-dose card label reworded so it no longer echoes English.
2026-08-10 07:58:48 +02:00
Marc-André Bombeck 83d31dc9c1 style(medications): prettier formatting for the per-slot override step 2026-08-10 07:58:48 +02:00
Marc-André Bombeck 33769de372 test(medications): cover per-slot units consumption and wizard round-trip
Integration: a medication with a morning slot at 1 unit and a noon slot
at 0.5 draws the right count for each taken dose against a real
Postgres. Unit: a per-slot dose + units set in the wizard survives
create and round-trips on edit-hydrate.
2026-08-10 07:58:48 +02:00