Commit Graph
4169 Commits
Author SHA1 Message Date
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 11a7124d12 refactor(import): move the JSON import writers out of their route 2026-10-23 14:51:51 +02:00
Marc-André Bombeck 24af254249 refactor(medications): move the bulk dose ingest out of its route
POST /api/medications/intake/bulk keeps authentication, its own bulk bucket, the body with its size and schema refusals, the payload schema and the HTTP mapping. The ingest, one 846-line function, moves to lib/medications/intake-bulk/ in the order it always ran: the ownership read, the Apple mirror gate and the batch pre-reads (ingest.ts), each entry's refusals, site, write bookkeeping and P2002 answer (entry.ts), its slot attribution (entry-attribution.ts) and its slot, replay, probe or standalone write (entry-write.ts), then the audit row, the dose days, the snooze, the tails, the arrival record and the checkpoint. The two identical canonical-slot write blocks of the loop are one function now. The route falls from 1044 to 231 lines; no function in the new modules is over 70.

The parity walk recorded before the move (medication-intake-bulk.parity) is unchanged. The per-entry external-id markers name the module that classifies each entry, the batch-surface and key-scope legs read the moved code, and the intake-tracking allowlist and the dense-read guard follow it.
2026-10-23 14:47:57 +02:00
Marc-André Bombeck 7a21796b63 docs: keep block openers out of line comments in server sources
A '/*' inside a line comment opens a block for every guard that strips
block comments first, and the code after it disappears from that guard's
view. Four server comments and one new one did this; reword them.
2026-10-23 14:47:00 +02:00
Marc-André Bombeck ad27e5b89d refactor(sync): move the delta feed's domain pages out of its route 2026-10-23 14:45:26 +02:00
Marc-André Bombeck 2fcc2bdab2 refactor(medications): move the today list, the compliance buckets and the status update out of their route
GET and POST /api/medications/intake keep authentication, the query and body parsing with their refusals, the 404 and the HTTP mapping. The today list moves to lib/medications/intake-today.ts and the cached compliance buckets to intake-compliance.ts. The status update, one 432-line callback, moves to lib/medications/update-intake-status/ in the order it always ran: the event lookup, the record-only rule, the delegated refusals and the injection site (admission.ts), the take in place or onto another slot (take.ts), then the skip or snooze, the audit row, the rollups and the tails (update-intake-status.ts). The route falls from 634 to 149 lines; no function in the new modules is over 70.

The parity walk recorded before the move (medication-intake-status.parity) is unchanged. The sharing and activity-verb guards read the status-update modules beside the route, and the dense-read guard keeps the moved code in scope.
2026-10-23 14:37:47 +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 315ee0ff98 refactor(mood): move the bulk ingest out of its route 2026-10-23 14:34:36 +02:00
Marc-André Bombeck c4ee519012 refactor(medications): move the per-medication dose write and history out of their route
POST and GET /api/medications/{id}/intake keep authentication, the intake and ownership guards, the body and query parsing with their refusals, the delegated outcome-change refusal and the HTTP mapping. The dose write, one 607-line function, moves to lib/medications/record-intake/ in the order it always ran: the injection-site check and the slot attribution (attribution.ts), the replay and dedup answers and the slot, probe or standalone write (write.ts), then the inventory, the audit row, the rollup, the lifecycle and the tails (record-intake.ts). The history list moves to list-intake-events.ts. The route falls from 767 to 200 lines; no function in the new modules is over 75.

The parity walk recorded before the move (medication-intake.parity) is unchanged. The activity-verb guard now reads the awaited audit actions of a delegable write's handler modules beside the route, for this route and for the medication create that moved before it, and holds each module to its route's imports; the sharing guard, the intake-tracking allowlist and the dense-read guard follow the moved code.
2026-10-23 14:29:58 +02:00
Marc-André Bombeck da937718bc refactor(measurements): move the series readers out of their route
GET /api/measurements/series keeps authentication, the query and its
refusal; readSeries (lib/measurements/series/read-series.ts) picks the
reader in the old order (sleep nights, paired blood pressure, the bucketed
read of a dense kind, the raw rows) and adds the direction, change,
reference lines and stat strip. The kinds, units and point shape live in
kinds.ts. The route falls from 752 to 95 lines.

The parity walk recorded before the move is unchanged. The guards on the
verdict resolver, the spread, the pulse day mean and the unit literals read
the new files, the last two with their scope widened to the series folder.
2026-10-23 14:13:52 +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 967fa2c56b refactor(medications): move the medication read, edit and delete out of their route
GET, PUT and DELETE /api/medications/{id} keep authentication, the ownership guard, the body and the HTTP mapping. The read moves to lib/medications/read-medication.ts; the edit, one 886-line callback, moves to lib/medications/update-medication/, split along its stages in the order they always ran: the preflight checks and refusals, the schedule normalisation, the transaction (era archive, tombstones, the row update, the pause era, the course write) and the commit tail with its audit row. The delete moves to delete-medication.ts. The route falls from 1270 to 100 lines; no function in the new modules is over 115.

The parity walk recorded before the move (medication-detail.parity) is unchanged. The sharing guard reads the edit and delete modules for the handler body, the intake-tracking allowlists name the modules that now read the schedule and the open slots, the published-DTO leg reads the read module, and the dense-read guard keeps the moved code in scope.
2026-10-23 14:10:17 +02:00
Marc-André Bombeck 817a922df5 refactor(workouts): move the workout detail read and the manual delete out of their route
GET keeps authentication, the module gate and the AI capability, and hands
the rest to readWorkoutDetail (lib/workouts/detail.ts): the stored row, the
canonical winner of its cluster, the enrichment reads and the body, as
named stages. DELETE keeps authentication and the HTTP answers; the
ownership check, the synced-source refusal and the transactional removal
with its records, audit, cache and detection tail move to
lib/workouts/delete-manual-workout.ts.

The parity walk recorded before the move is unchanged; the guards that name
the canonical-identity reads and the track readers point at the service.
2026-10-23 14:00:28 +02:00
Marc-André Bombeck 38baafcdd7 fix(openapi): admit the bare-instant route samples older tracks carry
A track stored before the object form holds one ISO instant per point, and the detail answers it as stored. The route-sample declaration accepts both forms.
2026-10-23 14:00:05 +02:00
Marc-André Bombeck ab15096a6a fix(openapi): declare the route samples a workout detail answers
WorkoutRouteGeometry.sampleTimestamps was declared as an array of strings. Both track ingests (the workout batch and the Apple Health export) store one object per point, { t, speedMs?, hr? }, and the detail answers them as stored. The response is unchanged; the contract now says what it holds.
2026-10-23 13:57:17 +02:00
Marc-André Bombeck 51835fe1d0 refactor(analytics): move the default body builder out of its route
The route keeps authentication, the slice refusal, the summaries slice, the
cache read and the headers. buildAnalyticsResponse (446 lines) moves to
lib/analytics/response/default-body.ts and calls one module per block, in
its old order: the blood-pressure in-target windows, the glucose summaries
and clinical panel, the correlation hypotheses with their stored patterns,
the Health Score with its Rest Mode framing, and the sleep stages. The
route falls from 761 to 155 lines.

The parity walk recorded before the move is unchanged; the guards on the
glucose window and the bounded reads follow the code.
2026-10-23 13:52:57 +02:00
Marc-André Bombeck c2a63307ed test: point guards at the split provider modules
The page-walk exception for the Google Health list walk follows
forEachDataPointPage into data-points.ts, and the workout provenance guard
reads the raw Polar and Strava sport labels where the mappers now live.
The write-step helper's tests assert the result it returns, including a
failed fold and a caller that re-warms the status cards itself.
2026-10-23 13:44:21 +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 8b3c095df5 refactor(fitbit): keep the Fitbit jobs inside the Fitbit folder
The backfill, the sleep repair, the OAuth-state cleanup and the sync
handlers move from lib/jobs into lib/fitbit/jobs, beside the queue facts
and definitions the registry already spreads. Their tests move with them.
Removing the integration is now the folder, its routes, the registry
spread and the drain's two cases.

The updatedAt backfill guard also walks provider jobs folders, so the
moved backfill stays covered.
2026-10-23 13:33: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 3a4e97733d refactor(medications): move the medication create out of its route
POST /api/medications keeps authentication, the body, the validation
answer and the HTTP mapping; the create (the mirrored-medication dedupe and
cap, the one-shot shape refusal, the initial course, the nested schedules,
the insert and its tail) moves verbatim to lib/medications/create-medication.ts
and answers created, existing or refused. The route falls from 541 to 94
lines. The parity walk recorded before the move is unchanged, and the
sharing guard reads the service beside the route for the handler body.
2026-10-23 12:41:46 +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 9725ba394d refactor(measurements): move the list modes and the hand-entry writes out of their route
GET /api/measurements keeps authentication and the query refusal and hands
the validated query to measurementListResponse (lib/measurements/list),
which tries the list modes in their old order. POST keeps authentication,
the body, the bridge-source refusal and the three validation answers; the
single and combined writes with their shared tail move to
lib/measurements/create/create-readings.ts, the named sleep-stage write to
create-sleep-stage.ts. The route falls from 938 to 274 lines.

The parity walk recorded before the move is unchanged; the guards that name
the measurement writers, the write tail and the bounded reads follow the
code.
2026-10-23 12:38:27 +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 f7051ee15f fix(openapi): declare what the measurement list and create answers carry
The sleep list rows of GET /api/measurements carry the main night's stage
minutes, time in bed, time awake and awakenings, and the aggregate buckets
carry the bucket's lowest and highest reading; none of these were in the
contract. The create answer's union tried the plain row before the named
sleep-stage result, so a contract check read a named-stage answer as a plain
row and dropped its status. The responses are unchanged; the contract now
says what they hold.
2026-10-23 11:47:08 +02:00
Marc-André Bombeck a76258f985 refactor(insights): keep the card temperature private to the pipeline 2026-10-23 11:44:48 +02:00
Marc-André Bombeck c1052c4f17 refactor(dashboard): move the widgets layout logic out of its route
The route keeps authentication, the body, the base token, the layout schema
(the inline-schema inventory reads it there), the conflict mapping and the
responses. The rest moves verbatim into lib/dashboard/widgets/, one module per
stage: the layout read and the availability probe for GET, the unknown-id
drop, the refusal report and the stored-field merge for PUT, and the locked
reset for DELETE. The route is 317 lines, down from 513; its longest
function is 85 lines, down from 199.

The parity walk recorded before the move (dashboard-widgets.parity) is
unchanged. The tombstone-read guard now also scopes lib/dashboard/widgets/,
so the moved HRV probes stay policed.
2026-10-23 11:43:10 +02:00
Marc-André Bombeck 62b050f711 refactor(medications): move the dose correction and undo out of their route
The PUT handler was one 390-line callback. The route keeps the record
resolver, the 404, the body and its schema refusal; the correction moves
verbatim into lib/medications/update-intake-event.ts, split along its
stages in the order they always ran: the start-day floor, the slot
attribution, the in-place edit or the move onto another slot, then the
audit row, the event and the tails. The undo moves into
delete-intake-event.ts. The route is 124 lines; no function in either
module is over 140.

The parity walk recorded before the move (medication-intake-event.parity)
is unchanged. The sharing guard's audit, tombstone and overwrite legs read
the route together with the modules it hands its bodies to, listed in one
map that each route must still import, and the moved-dose provenance leg
now also asserts the original source is carried.
2026-10-23 11:40:06 +02:00
Marc-André Bombeck 899b86f4d4 refactor(workouts): move the batch ingest out of its route
postBatch was a 700-line function. The route keeps authentication, the
per-account preference read, the body caps, the rate limit, the parse and
the bridge-source refusal; the ingest moves verbatim into
lib/workouts/batch-ingest/: the first pass (entries), the duplicate probe
and write plan with the manual-correction rules (plan), the transaction
that writes corrections, workouts, tracks and series (write), and the
orchestrator with the audit, record detection, wide event, cache and
arrivals (ingest). The route is 275 lines.

The parity walk recorded before the move is unchanged; the guards that
name the track writer, the Workout writer set and the per-entry
external-id check point at the service files.
2026-10-23 11:39:00 +02:00
Marc-André Bombeck 25c3c050ee refactor(measurements): move the batch ingest out of its route
postBatch was one 1255-line function. The route keeps authentication, the
rate limit, the body, the entry cap, the schema and the bridge-source
refusal; the ingest moves verbatim into lib/measurements/batch-ingest/, one
module per stage in the order they always ran: the first pass (prepare),
the measurement writes with their prefilters and set-based insert, the
hourly sums, the sync checkpoint, the audit and wide-event report, and the
cache and arrival tail. The route is 274 lines; no function in the folder
is over 200.

The parity walk recorded before the move (measurements-batch.parity) is
unchanged, as is the set-insert golden. The guards that read the route for
the per-entry external-id check, the plausibility validator, the write
tail, the dense-read bound and the unit audit now read the service files.
2026-10-23 11:30:46 +02:00
Marc-André Bombeck 17e0f47411 refactor(insights): move the feature window arithmetic to a leaf module so signals-of-day no longer imports the extractor 2026-10-23 11:23:32 +02:00
Marc-André Bombeck daaa44fd82 refactor(insights): read the mood bands from a leaf module so mood-patterns no longer imports its hub 2026-10-23 11:21:29 +02:00
Marc-André Bombeck 6a58a7df78 refactor(insights): compose the biomarker card from the pipeline stages 2026-10-23 11:18:46 +02:00
Marc-André Bombeck 1a99b8f4f0 refactor(insights): compose the generic metric card from the pipeline stages 2026-10-23 11:17:06 +02:00
Marc-André Bombeck d448e6c9c6 refactor(insights): move the medication card onto the pipeline with its own note codec 2026-10-23 11:14:00 +02:00
Marc-André Bombeck 08403a93b4 Merge branch 'feat/v146-server-tools' into feat/v146-server 2026-10-23 11:13:05 +02:00
Marc-André Bombeck 07e1e83ee8 feat(api): read and validate a JSON request body in one call
parseJsonBody composes safeJson, the Zod safeParse, the validation-failed wide-event action and the multi-issue 422 envelope, with the variations routes use (size cap, input derivation, issue count, error code, message, status, an onInvalid hook) as options. The refusals are the existing helpers' bytes, which the unit test compares directly.
2026-10-23 11:12:38 +02:00
Marc-André Bombeck 6f3ad9defa refactor(insights): reduce the mood card to its snapshot and its relation board 2026-10-23 11:11:13 +02:00
Marc-André Bombeck 87a35b670d refactor(insights): reduce the blood-pressure card to its snapshot 2026-10-23 11:08:35 +02:00
Marc-André Bombeck 6d3cfbba05 refactor(insights): reduce the overview card to its snapshot 2026-10-23 11:06:42 +02:00
Marc-André Bombeck 49b1888009 refactor(insights): reduce the BMI card to its height check, input gate and snapshot 2026-10-23 11:06:41 +02:00
Marc-André Bombeck b1400a8130 refactor(cycle,medications): break the gate and compliance import cycles
The module gate and the cycle gate imported each other for one pure
predicate. isCycleEnabled moves to lib/cycle/enabled.ts and both gates
import it from there.

The band minter read DOSE_WINDOW_DEFAULTS through the analytics/compliance
re-export barrel, which loads the compliance display and with it the
scheduling modules that import the minter. It now imports the constant from
the module that defines it. Runtime import cycles fall from 6 to 4.
2026-10-23 11:05:08 +02:00
Marc-André Bombeck de866107ad refactor(insights): reduce the weight card to its input gate and snapshot 2026-10-23 11:04:15 +02:00
Marc-André Bombeck ea01610a56 refactor(insights): run the status cards through one staged pipeline, starting with pulse 2026-10-23 11:04:14 +02:00