5650 Commits
Author SHA1 Message Date
Marc-André Bombeck 1cde353965 Merge pull request #792 from MBombeck/release/v1.37.18
Release v1.37.18
v1.37.18
2026-08-14 04:19:03 +02:00
Marc-André Bombeck dd885043c4 chore(release): v1.37.18 — the app stops promising what it does not do 2026-08-14 04:01:21 +02:00
Marc-André Bombeck 9e555b7fd2 Expect the fenced label on the real-DB preventive-care read
The integration twin of the tool test still asserted the raw reminder
label; the tool now fences it like the appointment reason beside it.
2026-08-14 03:56:38 +02:00
Marc-André Bombeck 8ee123d06b Settle the suite-wide fallout of the wave
The evaluator stubs hoist properly for vitest's factory rewrite, the
WHOOP webhook route test's prisma double learns the pre-image read the
delete branch now performs, and the connector-token card description
shrinks to the one-sentence, 120-character shape the card guard
enforces — still saying what the mint truthfully does.
2026-08-14 02:40:53 +02:00
Marc-André Bombeck 2b3a67d43c Keep the leaf-guard regexes inside the compiler's target 2026-08-14 02:34:40 +02:00
Marc-André Bombeck 95baf58be7 Harden the guards that could go green for the wrong reason
The bearer MINT matcher demanded a literal apiToken.create( in the very
file that documents how a whitespace-wrapped call walked past its T1
sibling; it is whitespace-tolerant now, asserts a non-zero match count,
and the always-true filter beside it is gone. The T1 resolver matcher no
longer demands tokenHash first in the where object. The English-leak
guard pins a non-zero key floor per bundle so a broken walker cannot
pass as no-leaks-found. The two audit-row integration suites poll for
the fire-and-forget rows instead of sleeping a fixed span, with one
deliberately widened grace kept for the absence assertion. And the
fixture fuses with 2027 horizons (suggested-actions next-due, the
printed-expiry trio) are now-anchored so they can never age into
inverted assertions.
2026-08-14 02:33:07 +02:00
Marc-André Bombeck b49013e6c6 Bound the sleep-timeline delete to the window the re-sync can restore
The Withings arm of the backfill deleted the source's entire
SLEEP_DURATION history while its re-sync fetches only the trailing 30
days regardless of fullSync — every older night was raw data loss with
orphaned rollup buckets on top. The delete is now bounded to the
re-sync window and the touched rollup span is recomputed afterwards;
WHOOP keeps the whole-history clear because its full sync genuinely
re-imports every night.
2026-08-14 02:29:01 +02:00
Marc-André Bombeck 2c0b4d60dd Re-anchor the service-worker fallback cache version
The fallback stayed at v1.37.15 through two releases while package.json
moved on, and the anchor test that exists to catch exactly this reads
red on the tip. Point it at the current release.
2026-08-14 02:26:05 +02:00
Marc-André Bombeck b7dfd236ee Move the API error classes into a leaf module
api-handler imports the record-session fence and the fence threw
RecordSessionChangedError / SharingAccessDeniedError imported back from
api-handler — the one real import cycle in the tree, working only by
evaluation-order luck. HttpError, the SharingAuthError family and
StepUpRequiredError now live in api-errors.ts (importing nothing but
the fence contract), api-handler re-exports the identical class objects
so no importer moves, and the fence imports the leaf. A guard pins both
the identity and the leaf structure.
2026-08-14 02:26:05 +02:00
Marc-André Bombeck d75034a9f4 Mint a default Idempotency-Key on every client POST and PATCH
Every apiHandler route accepts the header and the wrapped write routes
dedupe on it, yet almost no client call site sent one — a replayed
create could land a duplicate dose or vaccination and shift the series
index and booster-due date behind it. apiPost and apiPatch now mint a
per-call crypto.randomUUID() key unless the caller set its own; reads
stay bare.
2026-08-14 02:19:59 +02:00
Marc-André Bombeck 3973af69d4 Fence the reminder free text the preventive-care tool shipped raw
get_preventive_care passed a reminder's label and location — the user's
own free text — unfenced beside a fenced sibling, and get_lab_history
shipped the document-derived canonical analyte raw while sanitizing the
valueText next to it. Both are the marker-forging class the fence
doctrine exists for: fence label and location like the appointment
reason, scrub and sanitize the analyte like the default labs path.
2026-08-14 02:16:46 +02:00
Marc-André Bombeck 30aad51fac Sweep the measurement caches after Google Health and Withings syncs
Both hourly sync tails left the per-user analytics, correlations,
targets, derived and achievements caches serving pre-sync bodies until
their TTL lapsed — the correlations route documents the exact invariant
these tails were breaking. Mirror the Fitbit tail: mark the cells stale
whenever rows landed, on partial failures too.
2026-08-14 02:12:49 +02:00
Marc-André Bombeck 4e12aef5fd Type the evaluator stubs as variadic mocks
tsc rejects spreading unknown[] into a zero-parameter vi.fn; give the
context-evaluator stubs a variadic signature and cast the recorded call
tuples where the tests read them back.
2026-08-14 02:07:42 +02:00
Marc-André Bombeck db6465c17d Account for every dropped Apple Health sample the result used to hide
Three silent gaps in the import accounting, one commit:

The reproductive samples of an export land in recordsRead but were
discarded without a counter when cycle tracking is off — the result now
names them (samplesSkippedModuleDisabled). A cycle flush that failed
partway zeroed its stats AFTER day-by-day writes had landed; the flush
is now fault-isolated per day and reports daysFailed plus the first
failure reason instead of claiming nothing happened. And the import
card hardcoded skipped: 0 while the worker has computed, persisted and
served per-class skip counters since v1.15 — it now renders them,
splitting deliberate exclusions (unsupported and deferred types, a
disabled cycle module) from genuine refusals (unparseable, out of
range, failed writes), with a per-type breakdown. Only refusals tip
the outcome tone.
2026-08-14 02:06:19 +02:00
Marc-André Bombeck a45dde2039 Retire the guardian toggle over a pref nothing reads
The evidence disclosure stopped honouring showEvidenceByDefault in
v1.4.27 and the owner surface retired the control with it — but the
managed-record coach form still offered the switch, silently governing
nothing. Remove the control, the managed-schema pick, the response
field, and the label in all six locales; the strict schema now refuses
the key outright.
2026-08-14 01:56:25 +02:00
Marc-André Bombeck 15fb5dbbdb Point the import-guide links at the docs site that actually has them
Both import cards linked /docs/integrations/data-import — a path the
app never serves, so every click 404'd. Link the external docs site
through the shared INTEGRATION_DOCS_BASE constant, opening in a new tab
like every other integrations runbook link.
2026-08-14 01:54:10 +02:00
Marc-André Bombeck c23e677b6e Gate the bottom-nav Meds slot on the medications module
The mobile bar pinned /medications unconditionally while the page
renders nothing when the medications module is off — a visible tab over
a blank screen. Run both fixed flanks through the one slot filter the
Insights slot already uses (sharing coverage + module map, fail-closed
until mounted), so a disabled module is hidden everywhere instead of
relocated.
2026-08-14 01:52:37 +02:00
Marc-André Bombeck 11aa3dc060 Stop describing a Research-Mode gate that no longer exists
The titration-curve docblock contrasted itself against an estimated-PK
chart 'behind the Research-Mode gate' — that switch was retired as inert
and the columns dropped with migration 0321. Describe the real contract:
both charts render ungated, the estimated one carrying the persistent
modelled-not-measured disclaimer.
2026-08-14 01:48:31 +02:00
Marc-André Bombeck cd127010fe Evaluate the context cues the reminder grammar has promised since v1.22
A Coach reminder captured with NEXT_BP_LOGGED, NEXT_WEIGHT_LOGGED,
NEXT_SLEEP_LOGGED or NEXT_APP_OPEN confirmed to the user and then never
fired: the daily sweep skipped context triggers and no other evaluator
existed.

Build the evaluator. The three logging cues resolve on evidence — a
live matching-type measurement logged after capture — and ride the same
eventful reminder-satisfy hook every ingest path already fires, with the
daily sweep re-running them as the idempotent backstop. NEXT_APP_OPEN
resolves on the nudge-status read (the poll that mounts with the app
chrome), owner-only so a delegate's visit never satisfies it, with a
capture grace so the session that created the reminder does not count as
the next open. Every trigger delivers through the surfacing transaction
extracted from the sweep: conversation + assistant message + status
flip, all-or-nothing.
2026-08-14 01:45:47 +02:00
Marc-André Bombeck 0cec8b07bd Publish the vaccination booster mint and upload suggestion
The iOS tracker was told both endpoints were in the spec; neither was
registered. Publish POST /api/vaccinations/{id}/booster (201 mint /
200 re-anchor with the resolved MeasurementReminder DTO) and
GET /api/vaccinations/suggest (the server-decided one/many/none
verdict), and regenerate the YAML. A structural guard now derives the
expected path set from the route files so the next vaccination route
cannot ship unpublished.
2026-08-14 01:30:28 +02:00
Marc-André Bombeck cb86b24c0d Tell the truth about what a connector token can do
The mint card called every connector token read-only while the
health:write toggle sat directly beneath it — a token minted with the
toggle on writes measurements and mood entries. Rewrite the card
description and scope note in all six locales so the copy matches the
mint: read is the floor, write is a per-token choice made at mint time.
The copy guard now pins the pair so the read-only claim cannot return.
2026-08-14 01:28:03 +02:00
Marc-André Bombeck c7400abb48 Scrub document-derived biomarker names before they reach the prompt
The biomarker catalog mints name and unit from labResult.analyte —
model-transcribed free text out of an uploaded document. Both rode into
the status generator raw, the name in system-prompt position, so a
hostile PDF could smuggle instructions past the fence the coach
labs-snapshot already applies to the same fields. Sanitize both at the
snapshot and the system-prompt site.
2026-08-14 01:25:15 +02:00
Marc-André Bombeck 9fb5d7d7d0 Route measurement deletions into the rollup recompute path
A soft-deleted measurement never reached the rollup tier: the WHOOP
delete webhook tombstoned rows without recomputing the dirtied buckets,
and the freshness watermark probe filtered tombstones out, so the
deletion's updatedAt bump was invisible and the ghost aggregate read as
fresh forever.

Extract the post-write tail every measurement surface repeats into
afterMeasurementMutation (rollup recompute + status-insight
invalidation, both best-effort), call it from the WHOOP delete webhook
with the (type, measuredAt) pre-image collected before the updateMany,
and drop the deletedAt filter from the watermark probe so a tombstone
counts as staleness.
2026-08-14 01:22:41 +02:00
Marc-André Bombeck a3f0698525 Merge pull request #791 from MBombeck/release/v1.37.17
Release v1.37.17
v1.37.17
2026-08-13 23:16:12 +02:00
Marc-André Bombeck 82a535c9b5 chore(release): v1.37.17 — the discovery reads days, not readings 2026-08-13 22:58:38 +02:00
Marc-André Bombeck 35d798ad68 Round-trip the rollup swap through the real writer
The unit battery derives rollup fixtures in JS; this pins the same
parity through the pipe itself. Seed raw measurements, read the tiered
fetch before the fold (coverage miss, raw path baseline), fold with the
real recomputeUserRollups DAY pass, read again, and assert the two
daily series are equivalent — including a dual-source weight day whose
per-source rows must compose to the all-source mean. A drifted writer
projection, coverage-probe SQL, or bucket compose surfaces here.
2026-08-13 22:07:57 +02:00
Marc-André Bombeck f9ac7a3327 Pin the rollup swap's parity, fallback, and eligibility contracts
Both paths read identical fixtures — per-(day, source) DAY rollup rows
derived from the same raw rows the fallback reads — and must produce
equivalent daily series: the plain daily-mean case, the multi-source
day (the all-source weighted mean, never a ladder-collapsed single
source), and the pre-sum_value mean-times-count compose. A far-from-UTC
profile timezone must force the raw path so day keys stay profile-tz
keyed; the near-UTC midnight band's single-day attribution shift is
pinned explicitly so it stays a documented decision rather than drift.
Coverage misses and empty in-window bucket reads fall back per channel;
sleep and the cumulative types never enter the rollup query.

Every assertion family was broken on purpose and observed failing with
the site named before restoring green; the record sits in the test's
doc comment. The route test mocks the new coverage probe and bucket
read as empty so every channel rides the raw path unchanged there.
2026-08-13 22:06:10 +02:00
Marc-André Bombeck 8390b4d24e Serve correlation-discovery spot channels from the DAY rollup tier
The discovery scan read every raw measurement in the 180-day window for
all nine measurement channels and bucketed them into daily means in JS,
paying tens of thousands of rows on every cache miss (a CGM glucose
channel alone can exceed the read cap). The DAY rollup tier already
stores those per-day aggregates, so eligible spot channels now read one
pre-aggregated row per day per source and compose the all-source
count-weighted mean — the exact reduction toDailyMeans applies.

Read-swap replaces the raw read per channel, with fallback-on-miss:
structural ineligibility (SLEEP_DURATION's per-night reconstruction,
the cumulative types' device-type collapse axis the rollup rows do not
carry), a far-from-UTC profile timezone (the v1.4.38 W-A near-UTC
guard), a coverage-probe miss, or an empty in-window bucket read all
route that channel to the existing raw path. The SWR cache cell stays
in front; the swap only lowers the cost of a cache miss.
2026-08-13 22:00:27 +02:00
Marc-André Bombeck f44d2276fa Merge pull request #790 from MBombeck/release/v1.37.16
Release v1.37.16
v1.37.16
2026-08-13 21:15:19 +02:00
Marc-André Bombeck 5374bed5b7 Raise the nanoid override floor past the zero-size loop advisory 2026-08-13 20:57:58 +02:00
Marc-André Bombeck 5d508575c9 chore(release): v1.37.16 — every write reaches every reader 2026-08-13 20:37:28 +02:00
Marc-André Bombeck 84176af515 Enrol the two new prefetching pages in their structural guards
The prefetch-caching guard's frozen route set and the session-surface
guard's direct-derivation list both name every RSC prefetch wrapper;
/checkups and /mood join them with the same reasoning as their five
predecessors — getUnswitchedSession only, skipped under a switch.
2026-08-13 19:11:57 +02:00
Marc-André Bombeck 61114fab90 Accelerate the chart range switch, prefetch two list pages, calm two polls
placeholderData keeps the previous range painted while a health-chart
range switch fetches, paired with its truth condition: the container
dims and goes inert while isPlaceholderData is true, so the old range
cannot masquerade as the new one. The mood chart needs no pairing — its
range is a client-side slice over one static-key query and never drops
to a skeleton.

/checkups and /mood gain the dashboard-template RSC prefetch: the
primary list read moves into a shared lib read (route + wrapper consume
one query, so filter/order/wire shape cannot drift), the wrapper honours
the DASHBOARD_SSR_PREFETCH kill-switch, resolves identity through
getUnswitchedSession, JSON-round-trips the dehydrated state, and fails
soft to the bare client on every error path. Page-prefetch unit tests
pin the seeded keys and the fail-soft arms; the SSR hydration spec
gains an assertion per page.

The achievement-unlock poll drops to five minutes (in-app writes already
invalidate the cell; the poll only covers out-of-band ingest), and the
plan-proposal poll gains the advisor-style bounded attempt ceiling so an
open thread stops polling after its budget.
2026-08-13 19:06:56 +02:00
Marc-André Bombeck ac96e5e975 Cache the correlations discovery read and keep writes off the hit path
GET /api/insights/correlations was the most expensive uncached read in
the app: a 180-day window, ~10 parallel series reads, Pearson + FDR
in-process, plus two syncAcceptedPatterns DB writes — all paid on every
mount. The built body now reads through the analytics bucket's SWR cell
(60 s fresh / 1 h stale, keyed userId|correlations|locale), and the
pattern-sync writes live inside the builder, so a cache hit performs no
write on the read path. Eviction truth comes free: measurement, mood,
medication, illness and custom-metric writes already sweep the user's
prefix in that bucket, and a pattern dismiss sweeps it through
invalidateUserCorrelationPatterns.

The route test pins the contract: a second GET inside the fresh window
re-runs neither the measurement window read nor the pattern sync, and
serves a byte-equal body.
2026-08-13 18:52:07 +02:00
Marc-André Bombeck f2f40ae8b2 Pair every daily-read invalidation with its inactive refetch
Ten client mutation sites forgot a consumer: a coach-accepted checkup
stayed invisible for up to five minutes, a threshold or target edit left
the dashboard bands stale, a score-config save marked the daily reads
stale but never refetched them, a booked visit missed the Today rail, a
source-priority reorder kept every chart on the old canonical picks, an
adopted self-context note never reached the Settings read. Each site now
invalidates what its write changed and forces the unmounted snapshot +
digest reads to refetch through refetchInactiveDailyReads.

New illnessDependentKeys bundle carries the daily reads (Rest Mode
frames both), dailyDigest joins encounterDependentKeys, and the
chart-data prefix gains a factory accessor for whole-cache flushes.

The daily-reads guard learns the cycle / encounter / vaccination /
illness bundle families and a second matcher for direct snapshot/digest
invalidateQueries without a forced refetch, each with a frozen reasoned
allowlist. Both matchers were proven live by reverting a fix and
watching the guard go red (see the test doc comment).
2026-08-13 18:48:10 +02:00
Marc-André Bombeck 002d120b69 Reach every cached reader from the integration and context write paths
Integration syncs (Fitbit, Oura, Polar, WHOOP, Strava, Nightscout) now
mark the per-user analytics cells stale and sweep the workouts cache at
their sync tails — imported rows used to stay invisible to the cached
dashboard, analytics and workout readers until the TTL lapsed. The WHOOP
per-resource webhook/cron driver and the Polar workouts leg carry the
same call, matching the posture Google Health and the workout batch
route already had.

Illness, cycle and encounter write routes hard-evict the analytics
bucket through the new invalidateUserHealthContext helper: Rest Mode,
the cycle context and the Today-rail appointment feed all read from
cached cells under that bucket, and an interactive write must be true on
the very next read.
2026-08-13 18:33:46 +02:00
Marc-André Bombeck e3169fe3b3 Merge pull request #789 from MBombeck/release/v1.37.15
Release v1.37.15
v1.37.15
2026-08-13 14:19:17 +02:00
Marc-André Bombeck feb3ad93fd chore(bundle): raise the aggregate cap for the async-truth copy
The settling state's copy, the index-outcome plain-words line across
six locale catalogs and the reindex-count wiring take the whole-build
client sum to 3429 KB gzip, one kilobyte over the cap set at v1.37.4.
Every per-route ceiling stays met, so nothing a person downloads on a
given route grew beyond its ceiling; the aggregate cap moves to 3452
with the same margin as previous re-measurements.
2026-08-13 14:01:37 +02:00
Marc-André Bombeck 297496b517 chore(release): v1.37.15 — async work reports what actually happened 2026-08-13 13:38:44 +02:00
Marc-André Bombeck 589b35589b Prove the raster-failed chain and name the fax-codec cause
The residual scan from the report is a CCITT Group 4 single-image PDF,
the codec class pdfjs rasterization is known to fail on. Chain checks:
a pdfjs open failure or throw already lands as render-failed with the
error text annotated; the zero-pages shape now annotates too instead
of returning silently. An end-to-end test drives the REAL rasterizer
through prepareVisionInput and pins the rasterFailed mapping; the test
header documents why a tiny in-repo CCITT fixture is deliberately not
checked in (probed against this pdfjs version, it renders the page as
blank instead of throwing, so the fixture cannot deterministically
fail and would be flaky across upgrades). The detail view's
raster-failed line now names scanned fax compression formats as a
known cause, in all six locales.

Refs #776
2026-08-13 13:04:41 +02:00
Marc-André Bombeck 408c1f599a Resolve the settle verdict in render, not in an effect
Same guarded transition, expressed as the render-phase state
adjustment the hooks lint rule asks for: a resolved settle clears the
state in the same committed render that carries the outcome, so the
falling-edge toast still sees both flips at once and the block runs at
most once per settle.
2026-08-13 12:46:55 +02:00
Marc-André Bombeck b2908343c4 Use the consent surface the error type expects in the contract test 2026-08-13 12:43:27 +02:00
Marc-André Bombeck dceca62f8e Pin the async-truth contract over both document state machines
Two contract tests enumerate every branch of the summary job and the
index tree and assert each ends in a terminal state write: the summary
job's opt-out branch heals a phantom PENDING back to NONE, every
failure branch lands UNAVAILABLE or WITHHELD, success lands READY; the
index tree records an attempt outcome on every terminal branch,
success included. A third block pins the enqueue side: no PENDING is
claimed when the job's only branch would be the opt-out no-op, and
none before a job id exists. Honest scope stated in the test header:
these pin the two known machines, not a generic detector.

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

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

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

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

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

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

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

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

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

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

Refs #788
2026-08-13 12:07:39 +02:00
Marc-André Bombeck c8b3f49915 Merge pull request #785 from MBombeck/fix/detail-fixture-horizon
Keep the workout-detail fixture inside the retention horizon
2026-08-13 11:51:32 +02:00
Marc-André Bombeck 698844d0e2 test(workouts): keep the detail fixture inside the retention horizon
Sister file of the HR-curve integration fixture fixed on the release
branch: the same fixed session day aged past the 90-day dense-intraday
horizon today. This suite stayed green because its pulse read is mocked
empty either way, which is worse — the fallback branch its comments
describe was silently no longer reachable, and the timezone assertion
pinned a literal next-day key. The session day is relative now and the
day key is computed from it.
2026-08-13 11:33:26 +02:00