Commit Graph
1309 Commits
Author SHA1 Message Date
Marc-André Bombeck b02caed221 fix(ai): treat a stored provider key that cannot be opened as no key
A key id retired from ENCRYPTION_KEYS, or a corrupt row, made decrypt throw
inside the provider builders, and a briefing generation answered 500. An
unreadable key now drops its entry from the chain like a missing one, with a
warning on the event, so the generation answers the no-provider 422
(ai.provider.none). The connection test reads it as no stored key. The
parity walk holds that case to the contract again.
2026-10-23 16:44:50 +02:00
Marc-André Bombeck 976b2ba0bc fix(workouts): read the stored route sample shape for splits and the GPX export
Both ingests store each route point's sample as { t, speedMs?, hr? }, while
computeSplits parsed every sample as a bare instant and the GPX export
skipped anything that was not a string. Splits were therefore null for
every ingested track, and an exported track carried no times. One reader,
routeSampleInstant, takes either shape. The workout detail walk is
re-pinned: its ingested run now answers its first kilometre split.
2026-10-23 16:37:58 +02:00
Marc-André Bombeck cfa5fe537b Merge remote-tracking branch 'origin/release/v1.46.0' into feat/v146-server 2026-10-23 16:15:37 +02:00
Marc-André Bombeck 18ff14ba1a fix(openapi): declare the body-cap 413 and the outbound-screen 502 of a briefing generation
Both have always been answered; neither was declared. The parity walk now
checks them against the contract instead of skipping it.
2026-10-23 15:53:59 +02:00
Marc-André Bombeck af15be4708 Merge branch 'feat/v146-server-insights' into feat/v146-server 2026-10-23 15:44:47 +02:00
Marc-André Bombeck db623ef8bc test(insights): pin the briefing read and generation before moving them 2026-10-23 15:31:36 +02:00
Marc-André Bombeck 1b32217844 test(dashboard): pin the summary before moving its builder 2026-10-23 15:20:27 +02:00
Marc-André Bombeck d76497ec13 test(auth): pin the SSO callback before moving its sign-in resolution 2026-10-23 15:13:25 +02:00
Marc-André Bombeck 2e13b7754e test(insights): pin the comprehensive overview before moving its builder 2026-10-23 15:05:10 +02:00
Marc-André Bombeck 69527d926d test(documents): pin the vault upload and list before moving them 2026-10-23 15:04:39 +02:00
Marc-André Bombeck 94ad53756e test(import): pin the CSV import before moving its writer 2026-10-23 14:58:14 +02:00
Marc-André Bombeck 5d9a9107fa test(import): record the status re-warm tail in the JSON import walk
Run for real it is detached from the request but inside its context, so
its annotate lands on the request's wide event on some runs and misses it
on others.
2026-10-23 14:58:14 +02:00
Marc-André Bombeck 7f06e28c37 test(kit): let callRoute send a raw text body 2026-10-23 14:58:13 +02:00
Marc-André Bombeck 40eb1255b7 Merge branch 'feat/v146-server-meds2' into feat/v146-server
# Conflicts:
#	src/__tests__/external-id-stability-guard.test.ts
2026-10-23 14:52:03 +02:00
Marc-André Bombeck ed12625ab6 test(import): pin the JSON import before moving its writers 2026-10-23 14:48:51 +02:00
Marc-André Bombeck 2a7833d7dc test(medications): pin the bulk dose ingest before it moves
One walk through POST /api/medications/intake/bulk: a mixed batch with one entry per per-entry outcome (unknown and record-only medications, unstable keys, band, pinned and refused pins, named-slot skips, pending echoes, a standalone dose with site, override and key and its replay, a disallowed site, Apple doses with an in-batch repeat and an extra dose on a held slot, a skip converging onto the standalone dose), the same batch again, a background and a backfill sync with the arrival rows and checkpoint they write, the batch refusals, and the callers the route admits and refuses.
2026-10-23 14:42:32 +02:00
Marc-André Bombeck 29620ffa49 test(sync): pin the delta feed before moving its domain pages 2026-10-23 14:40:37 +02:00
Marc-André Bombeck c40d728b47 Merge branch 'release/v1.46.0' into feat/v146-server
# Conflicts:
#	src/__tests__/baselines/source-function-lines.json
#	src/__tests__/baselines/version-stamp-comments.json
2026-10-23 14:35:19 +02:00
Marc-André Bombeck 704b51ca02 test: size the dense pulse seed's transactions for a loaded host
Each month of the million-row seed runs in an interactive transaction with
an explicit timeout. The batch form ran under Prisma's 5 s default and a
month took 9.9 s at a high load average, expiring the transaction before
the heap assertion ran.
2026-10-23 14:35:01 +02:00
Marc-André Bombeck baa1ba018d test(medications): pin the today list, the compliance buckets and the status update before they move
One walk through GET and POST /api/medications/intake: today's list with its projection and every status a row reads as, the compliance buckets cold and cached, a take in place, a take that re-binds to another slot, a pin and a pin that is no slot, a skip, both snoozes, the not-found answers, the record-only rule both ways, the injection-site refusal and an allowed site, the schema refusals, and a delegate marking an open dose and refused a snooze and a correction.
2026-10-23 14:32:52 +02:00
Marc-André Bombeck 74e08e6906 test(mood): pin the bulk mood ingest before moving it into a service 2026-10-23 14:30:19 +02:00
Marc-André Bombeck af53d3d6f0 test(medications): pin the per-medication dose write and history before they move
One walk through POST and GET /api/medications/{id}/intake: band, standalone, named-slot, pinned and backfilled doses, skips onto slots and onto a standalone dose, the dedup window, both idempotency forms and their replays, the delegated refusal on both write paths, the injection-site and pin refusals, a one-shot dose, the schema refusals, and the history list in its filters. The parity step can now rewrite an id carried inside a longer string, and a refusal's unawaited audit row is waited for on the real timer so the pinned clock does not move.
2026-10-23 14:21:24 +02:00
Marc-André Bombeck 870c8e053a Merge branch 'release/v1.46.0' into feat/v146-integrations 2026-10-23 14:13:32 +02:00
Marc-André Bombeck b2f6c37c37 test(kit): pin parity walks to today where a route reads the database clock
Some read routes cut their windows in SQL against the database's now(),
which a pinned Date cannot reach; a walk pinned to a fixed date then reads
less of its record every day the calendar moves on. relativeToAnchor writes
every instant and day key of a snapshot relative to the start of the
current UTC day, so a walk can lay its record out from today and still read
the same snapshot on any day.

The series walk uses it with the clock unpinned; the analytics walk pins
the clock to noon of today, keeps its account in UTC and is re-recorded in
that form (its earlier snapshots proved the builder move before this
change).
2026-10-23 14:09:41 +02:00
Marc-André Bombeck 8feeaed473 test(measurements): pin the measurement series before moving its readers
A parity walk over GET /api/measurements/series through every reader it chooses between: the plain and ladder-picked rows, pulse with its spread, the paired blood pressure (two sources and a corrupt systolic), the sleep nights, glucose raw and day-bucketed, a dense stream bucketed per hour, a refused query and a delegate in another language. The readers cut their windows against the database clock, so the record is laid out from the start of the current UTC day and the snapshot holds instants and day keys relative to it.
2026-10-23 14:05:55 +02:00
Marc-André Bombeck e8510ae167 test(medications): pin the medication read, edit and delete before they move
One walk through GET, PUT and DELETE /api/medications/{id}: the read with stock, composition and courses, every edit branch the handler tells apart (schedule replace and echo, the grace bridge, pause and resume, the category rules, the record-only rule and both tracking transitions, the as-needed and one-shot refusals, the course refusals, a composition change), the refusals, and delegated access at each level. The parity step can now wait for a write the route does not await and read state outside the medication tables.
2026-10-23 13:59:41 +02:00
Marc-André Bombeck 32dec72564 test(workouts): pin the workout detail and delete before moving them into services
A parity walk over GET and DELETE /api/workouts/{id} on workouts landed through the batch ingest: the canonical row, the compact body, a hidden twin, a ride, another account's row, a missing one and the module switched off; a synced, a missing, another account's and a hand-entered workout with its record for the delete.
2026-10-23 13:57:18 +02:00
Marc-André Bombeck d24c544e98 test(analytics): pin the analytics response before moving its builder
A parity walk over GET /api/analytics on an account with a full record
(record-fixture.ts, reusable by the other read routes): an unknown slice,
the summaries slice and the default body cold and warm, the modules off, a
second language with a personal blood-pressure target, an empty account and
an anonymous call. Background work a request leaves running is settled
before the next one. The blood-pressure fast path's rollup row counts are
left out of the pinned wide event: they follow the rollup tier's coverage,
not the code, while the answer they fed stays pinned in the body.
2026-10-23 13:49:24 +02:00
Marc-André Bombeck 905d4b167c test(insights): write the status goldens with sorted keys
The goldens held their fields in the order the code assigned them. They are now written with every object's keys sorted, so the text depends only on content and diffs and compresses well; the parsed content of every golden is unchanged. They carry no clock reading or generated id: the clock is pinned and the ids are fixed.
2026-10-23 13:42:58 +02:00
Marc-André Bombeck 3f8c2a98dd fix(measurements): answer the day drill-down without the sealed note
The dayKey drill-down of GET /api/measurements answered the stored rows as
they were, notesEncrypted ciphertext included and the note itself unread.
It now selects the contract's columns by name and shapes each row like the
plain page: the note decrypted onto notes, the ciphertext never on the wire.

A route test walks the measurement reads over rows with a sealed note
(the page, the drill-down, the day-grouped list, an aggregate, the detail)
and fails on any sealed field; it failed on the drill-down before this
change. The parity walk drops its contract exemption for the drill-down.
2026-10-23 13:41:11 +02:00
Marc-André Bombeck 7b9737c7b1 test(kit): refuse a sealed field in any response the route kit drives
callRoute now fails a call whose JSON body carries a key ending in
Encrypted, or bytes serialised as an index-keyed object or a Buffer, on
every route it drives and regardless of the contract exemption. A sealed
column is decrypted or dropped before a response is built; one that leaks
is a defect, whatever the contract says. The kit's own test proves the
check finds both shapes and leaves an hour map alone.
2026-10-23 13:41:11 +02:00
Marc-André Bombeck 7e54b6be81 refactor(integrations): split the provider clients into wire types, endpoints and mappers
Oura, Polar, WHOOP and Strava each keep the transport and endpoints in
`client.ts` and gain `wire-types.ts` (the record shapes) and `map.ts` (the
pure record-to-row mappers, no I/O). Google Health's 1,600-line client
becomes `budget.ts` (slice budget and outcome tally), `request.ts` (error
detail, retry, rate limits), `data-points.ts` (the paginated walker) and a
small `client.ts` for the profile and roll-up reads; its `export * from
"./mappers"` facade is gone and its importers name the module that defines
what they use.

The code moved verbatim; the mapper goldens written before the move and
the provider integration tests pass unchanged.
2026-10-23 13:30:10 +02:00
Marc-André Bombeck e626f2123f refactor(integrations): run one write step after every provider sync
`integrations/sync-kit/after-write.ts` is the tail every provider write
ends in: the arrivals go out, the touched days are refolded, the read
caches are marked stale back to the earliest touched reading, and the
touched status cards are re-warmed (through `afterMeasurementMutation`,
shared with the interactive routes); Withings also evicts the dashboard
snapshot. A full-history backfill refolds its whole touched range once
through `afterProviderBackfill`.

Withings, WHOOP, Polar, Nightscout, Google Health, Fitbit and Oura drop
their hand-written tails, and no provider module imports the comprehensive
insight generator any more; a guard holds the provider folders to that.
Differences from the copies: the status cards are re-warmed even when the
fold failed, and the caches are marked stale back to the earliest touched
reading instead of wholesale, so older closed blocks survive a sync.

Several provider unit tests mocked an export the old tails never reached
(their failures were caught and only logged); the mocks now name what the
code calls.
2026-10-23 13:12:23 +02:00
Marc-André Bombeck b6f45c0d88 refactor(integrations): move every provider OAuth client onto one factory
`oauth-kit/client.ts` builds a provider's consent URL, code exchange,
refresh and deauthorization from a declaration of what differs: endpoints
and scope, client authentication (form or HTTP Basic, with or without the
client id beside it), the fields a grant adds, how an answer is classified
and the refusal named, the error type, and where the token sits in the
body. The seven hand-written copies of the redirect URI, authorization URL,
token post, Basic header and PKCE pair are gone; each provider keeps an
`oauth.ts` with its declaration.

Shared behaviour is now uniform: a 2xx without a token object (an empty or
non-JSON body) is a transient `empty_token_body` failure for every
provider. WHOOP, Strava and Google Health used to return null, and
Withings threw an unclassified SyntaxError; the OAuth wire golden records
exactly these lines changing and nothing else. Oura's live/legacy endpoint
fallback, Google's invalid_grant lift, Strava's Fault summary and Fitbit's
errorType are declared per provider.
2026-10-23 12:57:47 +02:00
Marc-André Bombeck 0e0e661cf0 Merge branch 'feat/v146-server-insights' into feat/v146-server 2026-10-23 12:48:08 +02:00
Marc-André Bombeck f942389a4b Merge branch 'feat/v146-server-meds' into feat/v146-server 2026-10-23 12:41:47 +02:00
Marc-André Bombeck 069e07b510 Merge branch 'release/v1.46.0' into feat/v146-server
The size baselines take the lower value of the two sides per entry, and every ratchet baseline is written back to what the tree measures now (file and function sizes, version stamps, source reads, import cycles, route body parsing).
2026-10-23 12:39:42 +02:00
Marc-André Bombeck 1a84cf27a8 Merge branch 'release/v1.46.0' into feat/v146-integrations
# Conflicts:
#	src/__tests__/baselines/source-file-lines.json
#	src/__tests__/baselines/source-function-lines.json
2026-10-23 12:39:13 +02:00
Marc-André Bombeck 4807a70c1d refactor(jobs): declare every queue in a typed registry and load handlers on first job
The five hand-written registrars (about 5,500 lines, one 997-line function)
are replaced by `jobs/registry/`: one `defineJob` entry per queue with its
cron, schedule send options, policy, work options, lock, lane and a handler
loader. `registerJobGroup` creates, schedules, retires and binds each group
in the order the registrars used; the worker boot walks the groups and runs
their boot discovery.

Handlers load on their queue's first job. The facts a declaration needs at
boot (names, crons, concurrency, send options) moved out of the heavy job
modules into light `*-queue.ts` siblings, and their importers point there,
so loading the registry evaluates 288 modules instead of the 1,164 the
registrars pulled in. The formerly inline handlers moved verbatim into
handler modules under `jobs/reminder/`.

Fitbit's queues live in `fitbit/jobs/definitions.ts` and enter the registry
through one spread, so removing the integration later is that folder and
that line.

`tests/integration/queue-registration-parity.test.ts` proves pg-boss holds
exactly what the registrars left (queues, schedules, bindings, byte for
byte) and loads every handler once. The queue tests and the runtime guard
assert on the registry objects instead of reading registrar source.
2026-10-23 12:36:09 +02:00
Marc-André Bombeck 05b9697848 Merge branch 'feat/v146-server-status' into feat/v146-server 2026-10-23 11:47:13 +02:00
Marc-André Bombeck 8d48733c6a test(measurements): pin the measurement list and create before moving them into services
A parity walk over GET and POST /api/measurements: every create arm (single, combined, named sleep stage) with its duplicate and refusal, a bridge token, a delegated write, and every list mode. Sealed bytes are recorded by length, since each write carries a fresh nonce.
2026-10-23 11:47:09 +02:00
Marc-André Bombeck 8bb58d8760 test(medications): pin the medication list and create before the create moves 2026-10-23 11:44:26 +02:00
Marc-André Bombeck 2bfc5aee4b test(dashboard): pin the widgets layout route before moving its logic
Record the parity walk for GET, PUT and DELETE /api/dashboard/widgets on the
current code: the first and the cached read, a full save with ids outside the
catalogue, a partial save that keeps stored fields and the per-chart range,
the schema, empty-body and base-token refusals, a save on the current token,
the recovery-off availability probe with and without an HRV series, a
delegate's read and refused writes, the reset, and an anonymous call.
2026-10-23 11:39:48 +02:00
Marc-André Bombeck dd52a8d92a test(workouts): pin the batch ingest before moving it into a service
A parity walk over POST /api/workouts/batch: a mixed first batch with every per-entry refusal and the cross-source twin, the same batch with an enrichment, a corrected hand entry, a kept track, both whole-batch refusals and a narrow bridge token. Responses, wide events, queue tails and stored workouts with their series and tracks are pinned.
2026-10-23 11:34:11 +02:00
Marc-André Bombeck 4c9dc2be5b test(medications): pin the dose correction and undo before they move 2026-10-23 11:33:51 +02:00
Marc-André Bombeck 3df80389ab test(kit): record route parity snapshots, starting with the batch ingest
tests/kit/parity.ts pins what a route answers, stores and reports for a
fixed walk of cases, so the code behind it can move under a byte-for-byte
comparison: the response, the rows read back, the request's wide event and
the queue and arrival tails it asked for, with generated ids, server
timestamps and request ids replaced by stable placeholders.

The first walk covers POST /api/measurements/batch: a mixed first sync with
every per-entry refusal, the same sync again, a background day total, the
manual and HealthKit mirror merge, a backfill, both whole-batch refusals and
a narrow bridge token. The clock is pinned, so the snapshot holds on any day.
2026-10-23 11:19:46 +02:00
Marc-André Bombeck 78ac2c5aeb refactor(sharing): read and judge grants from a leaf module
lib/sharing/grants.ts held both the grant transitions and the predicates
every reader asks (state, level, scope, the live grant for a pair). The
transitions reach into the managed-profile lifecycle and the OAuth connect
target, and both of those read a live grant, so grants, lifecycle and
connect-target formed an import cycle.

The predicates, findActiveGrant and touchGrantUsage move verbatim to
lib/sharing/grant-access.ts, which imports only the database client and
the scope vocabulary. Importers and the mocks that replaced those
functions point at the new module. Runtime import cycles fall from 7 to 6.
2026-10-23 11:02:10 +02:00
Marc-André Bombeck 4308682c96 test(insights): pin every status card's prompts, results and stored notes in golden files 2026-10-23 10:52:49 +02:00
Marc-André Bombeck 0182f91092 Revert "feat(topics): compute topic overviews and topic pages on the server"
This reverts commit cc04e0944d.
2026-10-23 10:46:55 +02:00
Marc-André Bombeck 5c6a1cbb36 test(jobs): pin every queue, schedule and handler binding the worker registers 2026-10-23 10:44:53 +02:00