Commit Graph
1061 Commits
Author SHA1 Message Date
Marc-André Bombeck 074cccd920 docs(api): regenerate the contract for the workout delete error code
Refs #1085
2026-10-02 21:36:24 +02:00
Marc-André Bombeck 5e6fd327a5 feat(workouts): delete a workout entered by hand
DELETE /api/workouts/{id} removes a MANUAL workout outright, with its
child rows and the personal records keyed on it, then queues a silent
detection pass so the previous best comes back. Any other source answers
409: the next sync would write it back. Own record only, 404 for another
user's row.

Refs #1085
2026-10-02 21:22:56 +02:00
Marc-André Bombeck fe9a94c853 chore(release): v1.39.8 — new ECGs from the iPhone get through, resting heart rate by hand, and ranges, decimals and dates as you read them 2026-10-02 00:06:09 +02:00
Marc-André Bombeck 9175aea316 chore(release): v1.39.7 — your units, your day and your phone, everywhere, and backups that keep your settings 2026-10-01 15:34:08 +02:00
Marc-André Bombeck 8b84e08145 Merge remote-tracking branch 'origin/main' into fix/v1397-account-backup 2026-10-01 13:21:55 +02:00
Marc-André Bombeck f9babade02 chore(openapi): regenerate for the account settings backup section 2026-10-01 13:12:28 +02:00
Marc-André Bombeck 729401e032 chore(openapi): describe the rounded ECG heart rate and frequency 2026-10-01 12:53:25 +02:00
Marc-André Bombeck 6725110b25 chore(openapi): regenerate for the backup fields that now round-trip 2026-10-01 11:34:19 +02:00
Marc-André Bombeck ada490c7e9 Merge remote-tracking branch 'origin/main' into fix/v1397-tz 2026-10-01 10:59:51 +02:00
Marc-André Bombeck 27ac4c1d63 Merge origin/main into fix/v1397-tz 2026-10-01 10:39:26 +02:00
Marc-André Bombeck da8cc7a6ee Merge remote-tracking branch 'origin/main' into fix/v1397-derive 2026-10-01 10:34:17 +02:00
Marc-André Bombeck 533b75fdcf Merge remote-tracking branch 'origin/main' into fix/v1397-derive 2026-10-01 10:30:10 +02:00
Marc-André Bombeck 4a012be37e Merge branch 'main' into fix/v1397-units 2026-10-01 10:12:47 +02:00
Marc-André Bombeck 5f548d331f fix(labs): say when a scanned PDF was read from its first pages only
A PDF longer than the pages a scan renders was read from its first ten
pages, and the cut was recorded only in the wide event. The person checking
the extracted rows had no way to know that values on later pages were never
read.

The rasterizer now returns the PDF's page count, the extract response
carries pageCoverage { read, total } when the cap applied, and the review
screen says how many of the pages were read.
2026-10-01 10:05:35 +02:00
Marc-André Bombeck a66976874e fix(mood): report the tag and factor keys a mood write did not store
An unknown or archived tag key, or a factor the catalog no longer carries,
was dropped on the way in while the entry still reported "inserted" (bulk)
or 201 (create, edit). A client had no way to tell that part of what it sent
never landed.

Each write now names those keys: the bulk result per entry, the create and
edit responses on the entry, as droppedTagKeys and droppedFactorKeys. Both
are present only when non-empty, so a response where everything landed is
unchanged. An archived tag an edit preserved is on the entry and is not
listed. The web shows a short notice after saving when a picked tag was left
off.
2026-10-01 10:01:06 +02:00
Marc-André Bombeck 2dc73be233 fix(dashboard): put the mood card's instant on the user's local day
The mood series carries local day keys, and the summary wrote the newest
one out as UTC midnight. West of UTC that instant is the previous evening,
so a client formatting it in its own zone showed today's entry as
yesterday's. Emit the first instant of the day in the account's zone
instead; the field keeps its shape and type.
2026-10-01 09:27:56 +02:00
Marc-André Bombeck 5486014360 fix(score): write the glycaemia labels in the reader's glucose unit
The fasting-glucose pillar served its observed and reference labels as
"Fasting median 99 mg/dL" and "70-99 mg/dL fasting" whatever the
account's unit, and clients print those labels as they come. They are
now written in the record's glucose unit. The scored value, its unit
and the band edges stay canonical, so the score does not move and the
web, which converts from the value, cannot convert twice. The schema
now says which fields are canonical and which are display text.

Refs #1067
2026-10-01 09:00:53 +02:00
Marc-André Bombeck 5f1eb05445 fix(measurements): bound the per-night sleep list to a year ending at to
The sleep list capped its read to the last year only when no `from`
was sent. An explicit early `from` walked every per-stage row back to
that date, on a multi-source account several rows per night for years.

The read window is now at most a year ending at `to` (or now). `from`
narrows it but never widens it, older nights are reached by moving
`to` back, and `meta.windowFrom` names the instant a cut window
started, so a client can tell a cut from an empty history.
2026-10-01 08:58:42 +02:00
Marc-André Bombeck b2c484e25e fix(series): bucket heart-rate variability and blood oxygen like pulse
The dense rule in /api/measurements/series covered pulse and glucose
only. Heart-rate variability and blood oxygen read every raw row for
any window up to 3650 days, and a watch or ring that writes them every
minute makes that the same unbounded read pulse had.

Both kinds now fold per local day beyond 90 days and per local hour
inside 90 days once the window holds more than 10 000 readings. A
bucket keeps the point shape of its kind: only pulse carries
valueMin / valueMax. Statistics stay computed over every reading, and
a sparse series inside 90 days is unchanged. The OpenAPI description
names the four kinds.
2026-10-01 08:52:10 +02:00
Marc-André Bombeck 3e4f431373 chore(release): v1.39.6 — a critical framework fix, a workout token, a lab scan that keeps every reading, and an instant restore preview 2026-10-01 00:48:46 +02:00
Marc-André Bombeck 85d11bc364 Merge branch 'main' into fix/v1396-small-fixes 2026-09-30 23:27:25 +02:00
Marc-André Bombeck 731e073284 fix(measurements): hour-bucket a CGM glucose series inside the 90-day window
A sensor reading every five minutes puts about 26 000 glucose readings in
a 90-day window, and the per-kind series sent every one of them raw. The
hourly bucketing that already applies to a dense heart-rate stream now
applies to glucose too, with the same 10 000-reading cap: the hour's mean
converted once to the user's unit, statistics over every raw reading.
A fingerstick meter stays far under the cap and keeps its raw points.

The doctor report keeps glucose raw inside 90 days. Its clinical panel is
computed from the raw readings of every source, while the day path works
from canonical-source day buckets, so the two do not give the same figures
when a meter and a sensor overlap.
2026-09-30 23:01:57 +02:00
Marc-André Bombeck e7166cb3c6 feat(tokens): mint a workouts:write token for headless workout ingest
A bridge that pushes workouts from a watch vendor had no credential that
reached POST /api/workouts/batch: the route called requireAuth() with no
scope, so under the fail-closed default only a cookie session or a
wildcard token got through, and a measurements:write token was refused
403. The route's header still described the pre-fail-closed behaviour.

Add a separate workouts:write scope rather than widening
measurements:write, so a token already handed to a scale does not start
accepting workouts.

- POST /api/tokens/workouts mints it, mirroring the measurement and
  document mints: cookie session plus a fresh proof, a literal permission
  array, 10 mints a minute, at most 10 live tokens (409
  tokens.workouts.ceiling_reached), a one-year default lifetime, audited.
- POST /api/workouts/batch names the scope. Sessions and wildcard tokens
  behave as before. A scoped caller's workouts are stored as EXTERNAL,
  resolved from the credential, and a batch in which any entry names a
  source is refused 422 (workout.batch.source_not_permitted), the same
  rule the measurement batch applies. EXTERNAL is on no source ladder, so
  when the phone recorded the same session the phone's copy is shown.
- Settings > API offers a workout token card next to the measurement one,
  and the endpoint list shows the batch route.
- Guards: the mint site, the declared scope vocabulary, the recent-proof
  routes, and a new check that only the batch declares the scope.
- OpenAPI documents the mint, the batch's narrow-token behaviour and both
  error codes; docs/api/README.md lists the workout and document mints.

Refs #1054
2026-09-30 22:21:46 +02:00
Marc-André Bombeck 3f3bb0579a chore(release): v1.39.5 — a dense heart-rate history within memory, and restores that hold less 2026-09-28 00:55:23 +02:00
Marc-André Bombeck 0e991c3640 fix(measurements): fold a dense pulse stream inside the 90-day raw window
Past 90 days every pulse reader already folded in SQL. Inside it, three
still read every raw heart-rate row, which on a watch that records a
reading a minute is 130 000 rows per read (#1023):

- The insights page's resting-pulse proxy, used when an account has no
  resting heart rate, walked the whole window to take one percentile per
  day and held the event loop for over a second per view. It now folds in
  Postgres (`readRestingPulseProxy`) with the same percentile, rounding,
  minimum-sample rule and day anchor.
- The per-kind series sent every reading: 14.7 MB for 90 days. A pulse
  window with more than 10 000 readings is now bucketed per local hour,
  mean plus low/high band, with stats still over the readings.
- The doctor report drew every reading into one sparkline. A pulse window
  over the same cap now takes the per-day path longer reports already use.

Sparse pulse (a cuff, hourly imports, a watch sampling every few minutes)
stays under the cap and keeps its raw shape.
2026-09-27 21:07:29 +02:00
Marc-André Bombeck 5ab157a908 chore(release): v1.39.4 — a Coach you can see working, encrypted routes and contacts, and restores that finish on large accounts 2026-09-27 15:12:37 +02:00
Marc-André Bombeck f98f8eaa24 Merge branch 'fix/chart-all-range-meaning' into release/v1.39.4 2026-09-27 14:22:57 +02:00
Marc-André Bombeck 206da07e8c Merge branch 'fix/meds-ended-and-mental-health' into release/v1.39.4 2026-09-27 14:18:09 +02:00
Marc-André Bombeck 6f37cbdb23 fix(backup): carry each medication's category through backup and restore
The clinical category a person picks for a medication lives in a side table
created at runtime, outside the schema. The backup builder and the restore
only walk schema models, so no backup ever carried it: after a restore every
medication read as "Other".

Both backup formats now carry the category inside its medication, and the
restore writes it back in the same transaction as the medication. A file
from before this change restores as before; a category this server does not
know reads as Other instead of refusing the file.

The backup plan gains a list for tables created with raw SQL, and a check
that every such table in the source either maps to a schema model or has a
verdict there, so the next one cannot drop out of backups unnoticed.
2026-09-27 14:17:42 +02:00
Marc-André Bombeck b45a668cf4 fix(charts): serve the All range's coarse buckets as their average day
The All range is read from the WEEK or MONTH rollup tier once a history is
long enough, from DAY rows for a short one, and from the live table on a
coverage miss. The chart folds every path into the average day of each
week or month, but a tier row carried a cumulative metric's whole total
and a level's reading-weighted mean. A month of steps therefore read about
thirty times higher for a long history than for a short one, under a
caption that said average.

A tier row now carries the average day of its bucket on every tier: a
cumulative metric's total over the days that hold a reading, a level's
mean of its daily means. The rollup fold counts the bucket's days and adds
up their means for it. Only the web chart's series reads (the chart
endpoint and the dashboard batch) use these rows.
2026-09-27 14:08:49 +02:00
Marc-André Bombeck c7e53176c1 Merge branch 'fix/v1394-review-correctness' into release/v1.39.4 2026-09-27 14:00:04 +02:00
Marc-André Bombeck a224875ed9 feat(medications): add mental health as a medication category
Antidepressants, anxiolytics, mood stabilisers and ADHD medication had no
category of their own and ended up under "Other". MENTAL_HEALTH joins the
clinical category list: the add and edit wizard offers it as its own row
with a brain icon, the card badge names it, and it is translated in all
seven languages.

The category is a text value, so no migration is needed. Clients that
read the category as a string show the new value; an app that does not
know it yet falls back to its own label for it.
2026-09-27 13:51:39 +02:00
Marc-André Bombeck 8295dfe9b6 fix(medications): stop offering a dose once the course has ended
A medication whose end date had passed still showed taken and skip in the
table and on its card, because the action row only looked at whether the
medication was active. The schedule engine already stops minting doses
after the end date, so reminders, the doses card and the today list were
quiet, but the medication itself kept inviting a dose that is no longer
part of the course.

The server now resolves one rule and publishes it on the list and detail
reads: `courseStatus` (UPCOMING, CURRENT or ENDED, in calendar days on the
user's clock, both ends inclusive) and `intakeActionable` (active, intake
tracked, course includes today). The table and both card variants render
their actions from it, the take-all flow skips such a medication, the card
and table name an ended course with an "Ended" badge, and no overdue
escalation is raised for it. A course that ends today keeps its actions
for the whole local day.

The detail header's "Ended" status now follows the same field. It compared
the end date's UTC midnight with the clock and so called the whole last
course day ended. Logging a past dose from the history is unchanged.
2026-09-27 13:49:21 +02:00
Marc-André Bombeck eda87fd4a7 fix(sync): expire delta cursors issued before a restore or a data wipe
A restore writes rows back with their original updatedAt, behind every
paired client's watermarks, and removes rows without leaving tombstones.
A client that kept its cursor would miss both on its next incremental
pull. The same holds after the person deletes all their data.

`users.sync_reset_at` (migration 0363) records when the record was last
replaced: the restore stamps it as its last write and again right after
the commit, the per-account and the admin wipe stamp it inside their
transactions. Cursors now carry the time they were issued, and
/api/sync/changes answers its existing `cursorExpired` to a cursor issued
before the reset, or one without an issue time on an account that has
one. The comparison is on the issue time, because after a restore a
freshly drained cursor still carries old watermarks.
2026-09-27 13:46:38 +02:00
Marc-André Bombeck 21a2e2fcef fix(coach): keep the chart on "as a table" so the chart is offered back
"As a table" used to drop the table's chart, so the reply offered no way
back to it. The copied table now keeps its chart and carries view: table,
which the panel opens on the table (the chart one tap away on the toggle)
and the chips read to offer "as a chart". show_result with view table
does the same. The field is optional on the result metadata; a client
that ignores it shows the chart first as before.
2026-09-27 13:36:48 +02:00
Marc-André Bombeck 6a458ac5b0 fix(coach): hold restored dialog ids to the shapes the server mints
Table refs, step ids, chip ids, a chip's anchor ref, clarification choice
ids and a reused table's source are now validated against their exact
server-minted patterns when a stored reply is read back. Some of them are
written into the model's context on a later turn (the earlier-tables list,
the "keep looking" fetched list), so a stored row edited by hand can no
longer carry text into a prompt through them.
2026-09-27 13:22:09 +02:00
Marc-André Bombeck bc6c782745 fix(coach): quote formula starts behind whitespace, in fullwidth and pipe when copying a table 2026-09-27 13:03:27 +02:00
Marc-André Bombeck d63d2e6d83 Merge branch 'coach/wp7-continue' into release/v1.39.4
# Conflicts:
#	src/lib/ai/coach/turn/pipeline.ts
#	src/lib/ai/coach/turn/provenance.ts
2026-09-27 12:43:23 +02:00
Marc-André Bombeck 968b8b1efd Merge branch 'coach/wp4-followups' into release/v1.39.4 2026-09-27 12:41:44 +02:00
Marc-André Bombeck b5023ae935 feat(coach): keep looking after the round cap
When the tool loop forces an answer at its round cap, the reply leads
its chips with "Keep looking". Tapping it runs a normal turn, under the
budget gate as ever, with two server-written context lines: the question
that was left unfinished (at most 1000 characters) and what the forced
turn already read, each table by the name show_result takes, so the
continued turn shows those again instead of fetching them twice.

An answer is continued at most once. The continued reply records the
message it continues in metricSource.continuationOf and offers no
further "Keep looking", even when it is forced again; a chip on a reply
that is itself a continuation is refused.
2026-09-27 12:41:20 +02:00
Marc-André Bombeck 66718a1c7d feat(coach): follow-up chips under the latest answer
Up to three chips, derived on the server from what the turn read: the
other view of a table with a chart, the period before, a year earlier,
a wider window, and a metric a discovered pattern links to. A chip is
offered only when its read was done this turn and the record holds the
metric; the year-ago and wider-window chips also need the record's
first reading to reach back that far. The model may name chips in a
FOLLOWUPS block, but only to reorder ones the rules already allow; every
label is rendered from the catalog.

A tapped chip resolves against the chips stored on the conversation's
latest reply. "As a chart" and "as a table" are answered from the
stored table without a model call or a budget reservation: the rows are
copied unchanged and the message is tagged providerType "reuse". Other
chips run a normal turn with a server-written context line. A chip that
is no longer current falls back to a plain message.

A setting turns the chips off (absent means on).
2026-09-27 12:36:01 +02:00
Marc-André Bombeck 02801bdfda Merge branch 'fix/restore-large-account-clearing' into release/v1.39.4 2026-09-27 12:14:04 +02:00
Marc-André Bombeck a967ccac44 feat(coach): metric tables and show_result for the chat tools
get_metric_table reads one metric as days, weeks or months through the
chart reader, cut in the user's zone and collapsed to one source per day,
with null for a period without a reading. The model reads a bounded
summary; the full table rides the turn, encrypted at rest. show_result
shows a table an earlier reply of the same conversation fetched, resolved
only against that conversation's own messages. Workouts, labs, sleep and
adherence results project into tables, earlier tables are named in the
context by metadata alone, and result:rN marks are stripped from the reply.
2026-09-27 11:54:30 +02:00
Marc-André Bombeck 93b6c086b4 fix(restore): let a large restore outlast the request statement timeout
A restore of an account with 1.9 million readings, most of them deleted
by the nightly consolidation and still awaiting the purge, was cancelled
in its clearing step: `DELETE FROM measurements` ran past the 60 s
statement timeout every pooled connection carries, and the whole restore
rolled back with a message that named no cause (#1031).

Most of that statement was not the delete. Two columns point into
`measurements` with ON DELETE SET NULL and had no index, so Postgres
scanned `personal_records` and `ecg_recordings` once per deleted reading:
38 of 40 s on a seeded account with 300 personal records. Migration 0362
indexes both, which also speeds up the tombstone purge and account
deletion.

The restore transaction now sets its own statement and idle-in-transaction
limits to the transaction budget with SET LOCAL, and the budget counts
the rows the account holds as well as the rows the file carries. A
rolled-back restore names what the database refused it over (timeout,
lock, constraint, storage, connection, other) on the job row, the
console, the audit row and the wide event.
2026-09-27 11:24:41 +02:00
Marc-André Bombeck 78fc22d0f8 Merge branch 'coach/wpF-contracts' into release/v1.39.4 2026-09-27 11:10:37 +02:00
Marc-André Bombeck ad72ae3e4c feat(coach): serve a message's stored tables and publish the stream contract
GET /api/insights/chat/{id}/messages/{messageId}/results returns the
tables one assistant message read, narrowed to the conversation's owner
like the conversation read, with a table withheld as module_disabled when
its domain's module is off and as unavailable when it cannot be read. A
data read: no AI capability.

The published document now describes every stream frame as the
CoachStreamEvent union, including the suggestedAction frame it never
named, the new step, result, followUps and clarification frames, their
components, the provenance fields, the followUp and clarification request
fields, and the reuse provider type.
2026-09-27 10:45:18 +02:00
Marc-André Bombeck f2e32afb24 fix(privacy): store environment coordinates at 1 decimal instead of 2
The home location of the environment module was rounded to 2 decimals,
about 1 km, which places a home in a neighbourhood rather than the town the
schema comment promised. The weather archive it feeds is gridded at 9 to
25 km, so the extra precision bought nothing.

Every stored coordinate of the module now goes through the same 1-decimal
rounding (about 11 km north-south, 7 km east-west at 50° N): the home, the
travel periods, the geocoder's results, the per-day weather rows, and a
restore of a file written with finer values. Migration 0361 rounds the
existing users, travel periods and per-day rows; running it twice changes
nothing more. The answers keep their shape, plain numbers. The API
descriptions and the schema comment say 1 decimal now.
2026-09-27 10:37:37 +02:00
Marc-André Bombeck b8e78a78b3 chore(openapi): regenerate for the practitioner backup ciphertext fields
The practitioner entry of the backup payload gains the optional
locationEncrypted and phoneEncrypted fields. The rotation script names each
new column once, at its call, so a dropped call is noticed rather than
covered by a comment.
2026-09-27 10:21:48 +02:00
Marc-André Bombeck 45bae0013c chore(release): v1.39.3 — documents from Paperless and Papra, confirmations that hold, the right day everywhere, and backups that delete what they should 2026-09-27 02:12:27 +02:00
Marc-André Bombeck 3cbfe7f7f9 Merge branch 'fix/v1393-review-security' into release/v1.39.3 2026-09-27 00:59:23 +02:00