Commit Graph
9450 Commits
Author SHA1 Message Date
Marc-André Bombeck 7c1ed25915 refactor(documents): split the document detail sheet
The sheet becomes a composer over detail/: the original-file preview and
download controls, the detail read with its writes, the AI layer with its
gate and review-first apply, and the field, meta and footer sections.
Guards that read the sheet's source now read the detail folder.
2026-10-23 13:11:20 +02:00
Marc-André Bombeck b161ed3bd2 test(workouts): drive the manual workout form in a real browser
Pick a sport, type a duration and distance, press Save and read the
batch body: miles asked of an imperial reader arrive in metres, a blank
start counts back from now, a retry after a failed save reuses the
form's external id, and an edit re-posts the stored instants and the
fields the form does not show. Field errors, the pending state and the
phone layout are measured in the page.

The markup and edit-render cases are deleted from the old suite, which
keeps the save and delete helper checks as manual-workout-save.test.ts.
2026-10-23 13:09:01 +02:00
Marc-André Bombeck 3ab85f85f9 test(measurements): drive the measurement form in a real browser
Type into the form, press Save and read the request body off the fetch
boundary: blood pressure as one batch, a weight typed in lb stored in
kg, a decimal comma, glucose typed in mmol/L stored in mg/dL with its
meal context, a sleep typed as 7:30 stored as 450 minutes. Native
required validation, the pending state, a refused write's localised
message, the module-off type list, the last-used type and the menu's
Reset are covered too.

The unit-label and glucose-unit markup tests are deleted; their source
pins are replaced by the wire assertions. The module-types file keeps
its pure ownership checks and drops its render.

The browser helper gains a Date-only clock pin, clears localStorage
between tests, carries an envelope's meta, and answers the providers'
locale mirror without counting it as the component's request.
2026-10-23 13:07:29 +02:00
Marc-André Bombeck 34a93b883f test(e2e): a settings change persists on the server
The time-format choice is changed through the account settings and read
back after a reload, in a second browser context that shares only the
session cookie, and on /api/auth/me, on the settings journey's own account.
2026-10-23 13:04:04 +02:00
Marc-André Bombeck ac77e52e77 refactor(measurements): hand both row layouts one row context
The table and the card list each took nine props per row and passed
them on to the shared row actions. One MeasurementRowContext carries
them, and rowState derives a row's grouping, selection and drill-down
state in one place for both layouts.
2026-10-23 13:03:39 +02:00
Marc-André Bombeck 0e9daa51f9 test(e2e): read a WHO-5 check-in word for word in German and Brazilian Portuguese
The check-in journeys anchor on data attributes so they read the same in
every language, which leaves the words themselves unchecked. Two journeys on
the wellbeing account now read the stem, the first and last item and the
answer labels in German (the validated WHO translation, reproduced exactly)
and in Brazilian Portuguese (the translation written for the app, with its
note), submit, and check the score on screen against the assessment the
server stored, with its language and kind of wording.
2026-10-23 13:02:32 +02:00
Marc-André Bombeck cc36f46f10 test(filters): drive the URL filter rail in a real browser
A FilterBar over useUrlFilterSync now proves the contract end to end:
a facet picked from the open menu pushes a history entry, typed input
replaces the current one, a pill's clear and Reset leave a bare URL,
two writes in a row both land, Back and an edited address re-seed the
pills, and the open menu rests inside the viewport under its pill.

The markup tests for FilterBar and useUrlFilterSync are deleted; the
popper-placement source pin is replaced by the measured menu position.
The browser project re-bundles its dependencies on every run so a
cache warmed before a new file existed cannot load a second React.
2026-10-23 13:00:19 +02:00
Marc-André Bombeck 0bf6a54be0 test(e2e): import lab results from a CSV on an account of its own
The import journey ran on the shared account in both projects, so one
project's duplicate verdict could rest on the other's import. It now runs on
the labs journey's account, emptied first, checks that the dry run writes
nothing and the import writes exactly the accepted row, and finds the result
on the labs page before the second run.
2026-10-23 13:00:00 +02:00
Marc-André Bombeck d7f0d88d59 refactor(checkups): split the checkups section into form, card and postpone sheet
The checkups file held two components of about 620 lines each. Their
parts move to components/measurement-reminders/checkups/:

- reminder-form: the type groups, cadence presets, the form state and
  its mapping to and from a reminder (pure)
- reminder-form-sheet: the create / edit sheet and its cadence fields
- checkup-card: the card in both views, its menu, details, skip and
  snooze lines, next / last lines and action row
- postpone-sheet: the postpone chips and date, the honest skip and its
  confirmation, and the calendar-day offsets

vorsorge-section.tsx keeps the list, the value capture and the visit
filing. Markup and behaviour are unchanged; the guards name the modules
that now hold what they check.
2026-10-23 12:59:49 +02:00
Marc-André Bombeck f185855d76 test(e2e): log a mood at a chosen time against the real server
The mood timestamp spec answered the POST and the list read itself. It now
runs on the mood journey's account: the instant typed into the form is the
instant stored, and the entry is in the list, also after a reload.
2026-10-23 12:58:39 +02:00
Marc-André Bombeck 7445b69842 test(e2e): add a weight through the dashboard against the real server
The add-measurement flow answered its own POST and list read, so it proved
that the list renders a row the test typed in. It now runs on the
measurement journey's account, emptied first, and checks that the reading
the server stored is the one entered and that the dashboard tile, the weight
chart's data table and the day view all show it.
2026-10-23 12:58:36 +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 518a381ff9 test(e2e): sign in with a password and a passkey, and sign out, against the real server
The login spec stubbed the login route to prove the form shows an error. It
now spends a real 401, and three journeys run on the sign-in account of
their project: a password sign-in that lands on the dashboard as that
account, a sign-out whose old cookie is refused afterwards (with a positive
control before it), and a passkey registered in settings through Chromium's
virtual authenticator that signs the account back in.
2026-10-23 12:55:19 +02:00
Marc-André Bombeck ca1cceb3a9 test(segmented): drive the control in a real browser
Roving focus, Home/End, wrap, toggle mode, disabled and the chosen
segment's fill are now asserted through key presses and computed styles
instead of markup greps. The static render test is deleted; its two
source sweeps move to segmented-single-implementation.test.ts.

The browser setup sets the default theme class so colour tokens
resolve, and pre-bundles the browser files' dependencies so a cold
cache does not reload the page mid-run.
2026-10-23 12:54:24 +02:00
Marc-André Bombeck 25b5833b11 refactor(settings): split the dashboard layout section into draft, lists and hero settings
DashboardLayoutSection was a 620-line component. Its parts move to
components/settings/dashboard-layout/:

- layout-model: the reorder and hero-kind helpers and the label maps
- use-dashboard-layout-draft: the tokened read, the draft, save and
  reset (with the snapshot and digest refresh) and the draft edits
- hero-settings-fieldset: the top card's switch, lead and highlights
- widget-lists: the sortable web list, the native-only group and rows

The section composes them. Markup and behaviour are unchanged.
2026-10-23 12:52:36 +02:00
Marc-André Bombeck b826642853 test(e2e): tag the unmocked release journeys @critical and hold them to the real API
Backup and restore, the adherence journey, delegated writes and the offline
page already run against the real server. They carry the @critical tag now,
and a structural guard refuses any /api interception in a file that carries
it, directly or through an imported helper, with a floor on the tagged count.

Global setup seeds one account per critical journey and per project, so the
journeys still to come can run at both widths without writing into each
other or into the shared fixture account. pnpm e2e:critical runs the set.
2026-10-23 12:52:12 +02:00
Marc-André Bombeck 3d1692204b test: add a browser Vitest project for interactive components
Run *.browser.test.tsx in headless Chromium through the Playwright
provider. The shared helper in tests/browser/ mounts the app's own
providers, stubs only the fetch boundary, and shims next/navigation
onto the page's real location and history.
2026-10-23 12:50:42 +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 58215f3338 refactor(medications): split the dose-history ledger into its hook, row and dialogs
The ledger file held two components of 404 and 314 lines. It now reads:

- dose-history/use-dose-history-ledger: the window read, summary, day
  groups and the optimistic mark, plus the owner's edits (toggle, pin,
  release, delete) and one reconcile step after every write
- dose-history/ledger-row-item: the row and, apart from it, the owner's
  row menu
- dose-history/ledger-summary-chart and ledger-confirms

dose-history-ledger.tsx keeps the ledger layout. Markup and behaviour
are unchanged; the guards name the modules that now hold what they
check.
2026-10-23 12:41:46 +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 3eb5aa9364 refactor(documents): split the vault page into filters, library, selection and detail
DocumentsView carried 970 lines of state before its first element. The
state moves to components/documents/vault/:

- selection-history: the `?doc=` history entry helpers (pure, tested)
- use-vault-filters: URL filters, the debounced search and the `/` key
- use-vault-library: usage, the paged list and its processing poll,
  layout, uploads and their intake paths, the corpus-index affordance
- use-vault-filter-choices: conditions, procedures, visits, years
- use-vault-selection and use-vault-bulk: the selection with ranges and
  Escape, the confirmed delete, re-filing, linking and the bulk share
- use-vault-detail: the detail sheet driven by `?doc=`, its close and
  the check that the close took
- vault-page-actions, vault-drop-overlay

The page composes them in about 300 lines; markup and behaviour are
unchanged, and the guards follow the code they watch.
2026-10-23 12:35:46 +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 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 8bb58d8760 test(medications): pin the medication list and create before the create moves 2026-10-23 11:44:26 +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 3c0063edb2 refactor(measurements): split the measurement list into view state, data, rows and sheet
MeasurementList was a 1,586-line component with fourteen pieces of
state. Its parts move to components/measurements/list/:

- list-model: the row type, page size, labels and formatting helpers
- use-measurement-list-filters: URL-owned filters, the value-range
  draft, page, sort and the page-scoped selection
- use-measurement-list-data: the list read, the soft deletes with undo
  and the selection operations
- use-measurement-edit: the edit draft, its unit conversion both ways,
  the save and the discard guard
- use-measurement-row-display: how a row shows its reading, as a pure
  rule (rowDisplayFor) bound to the account
- measurement-filter-rail, measurement-table (one row component),
  measurement-card-list (one card component), row-actions shared by
  both layouts, measurement-edit-sheet, day-drill-down

measurement-list.tsx lays these out in about 330 lines. Markup and
behaviour are unchanged. The glucose edit checks call the conversion
functions instead of matching source, and the guards that named the
old file name the module that now holds what they check.
2026-10-23 11:42:43 +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 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 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 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 7ea247e193 Merge branch 'feat/v146-server-parse' into feat/v146-server 2026-10-23 11:30:47 +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 ca04ef01b3 refactor(api): read the remaining settings and admin route bodies through parseJsonBody
Admin, coach, consent, chart-overlay, device registration, insights
feedback and provider chain, ECG ingest, onboarding, search and the AI
provider settings move to parseJsonBody with the same message, error
code, status and wide-event action they answered with. A refusal whose
annotate meta is computed from the issues, or puts its keys in another
order than the helper would, keeps that annotate verbatim in onInvalid.

The routes that still call safeJson do something between the read and the
schema: an optimistic-concurrency token split off the body, a count cap
answered first, a refunded quota slot, a conditional read, a non-Zod
check, or the body handed to a service.
2026-10-23 11:29:23 +02:00
Marc-André Bombeck af14d0aa7e refactor(api): read the health-record route bodies through parseJsonBody
Labs, biomarkers, custom metrics, cycle, anamnesis, illness, measurement
reminders, the measurement edit, medication sub-routes, mood and mental
health assessments still spelled out safeJson, safeParse and the 422 by
hand. Each becomes one parseJsonBody call with the size cap, action,
issue count, error code and status it had. The audit rows and the
unstable-externalId breadcrumbs move into onInvalid, in the order they
ran; where a breadcrumb reads the body, the wide-event annotate moves with
it. A `body ?? {}` or a payload unwrapped from the body becomes `input`.

The medication ingest route reads through the helper too; its Bearer
resolution happens before the read and is unchanged.
2026-10-23 11:29:22 +02:00
Marc-André Bombeck f06df5fd71 test(api): count parseJsonBody refusals in the issue-list guard
A route that reads its body through parseJsonBody has no
`if (!parsed.success)` block, so the guard lost sight of every migrated
site: its floor of 200 failed at 139 and the token inventory reported the
coach message feedback entry as stale while that route still answers
`feedback.body.invalid` in `error`, now through the `message` option.

Each parseJsonBody call is now a site of its own, carrying the issue list
by construction unless it names a status other than 422, and its `message`
option is read as the refused string by the token sweep.
2026-10-23 11:25:36 +02:00
Marc-André Bombeck 5075536f7f refactor(charts): split HealthChart into its read, view, layers and header
HealthChart was a 1,929-line component. Its parts move to
components/charts/health-chart/:

- series-math: series names, day keys, moving average, personal
  baseline, annotation and marker placement (pure)
- use-health-chart-series: the fetch window, the batched-slice decision,
  the daily read and the data-ready latch
- use-health-chart-view: the visible slice, bucketing, the average and
  trend lines, the prior period, the visible-range stats and the y domain
- use-health-chart-overlays: value bands, target zones, baselines, the
  trend read-out, the spoken summary and the table columns
- chart-header, band-overlay, chart-tooltip-content
- chart-layers: the reference layers and series as plain functions that
  return the Recharts elements, so the plot's child list is unchanged

health-chart.tsx keeps the component and its path (chart-runtime still
imports it from there) and shrinks to 488 lines. Output is unchanged.
The guards pinned to the file follow the code into the folder, and the
baseline-gating check now calls the layer instead of matching source.
2026-10-23 11:25:16 +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 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 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 a812a902dd refactor(dashboard): build tiles and charts from shared card builders
Most strip tiles differ only in their series, label, unit and icon, and
most chart cards share the reveal report, the batched slice, the time
zone and the day doors. One metric-card and one chart builder carry the
shared props, so each registry entry states only what is its own. The
dashboard route's client code shrinks by 0.2 KB gz; rendering is
unchanged. The tile-destination guard now builds every strip tile from
the registry instead of reading ids out of the source.
2026-10-23 11:14:47 +02:00