Commit Graph
3586 Commits
Author SHA1 Message Date
Marc-André Bombeck 57c7972755 Merge branch 'fix/coldstart-perf' into release/v1.27.32 2026-07-09 02:08:46 +02:00
Marc-André Bombeck 52a01b1bea Merge pull request #441 from MBombeck/release/v1.27.31
Release v1.27.31 — document AI provider governance
v1.27.31
2026-07-09 01:57:20 +02:00
Marc-André Bombeck de93e623f2 test(e2e): mock the new document-scoped capability endpoint
v1.27.31 routed the document AI section through a new document-scoped
capability probe (/api/documents/inbound/capability) instead of the labs
OCR probe. The AI-flow e2e helper only mocked the old probe, so the section
fell back to unavailable and the assist actions never rendered — the tests
timed out waiting for them. Mock the new endpoint with matching availability;
egress-notice behaviour is covered by the SSR component test.
2026-07-09 01:46:22 +02:00
Marc-André Bombeck e1a8da5dfb chore(release): v1.27.31 — document AI provider governance 2026-07-09 01:32:14 +02:00
Marc-André Bombeck 7b1f077888 Merge branch 'fix/docai-governance-rebased' into release/v1.27.31 2026-07-09 01:28:07 +02:00
Marc-André Bombeck aff845875e Merge pull request #440 from MBombeck/release/v1.27.30
Release v1.27.30 — correctness and resilience sweep
v1.27.30
2026-07-09 01:16:04 +02:00
Marc-André Bombeck 3d53aa50aa fix(i18n): merge duplicate documents.ai block so egress notice resolves
The merge of the pre-v1.27.29 governance branch left two documents.ai
blocks in each locale bundle: the consolidated Read-with-AI keys and a
separate egressNotice-only block. Duplicate JSON keys drop the first, so
documents.ai.egressNotice never resolved. Fold egressNotice into the
single documents.ai block across all six locales.
2026-07-09 01:10:45 +02:00
Marc-André Bombeck 6165e7f6b8 chore(release): v1.27.30 — correctness and resilience sweep 2026-07-09 01:05:47 +02:00
Marc-André Bombeck 4d6aab2816 Merge branch 'fix/docai-provider-governance' into fix/docai-governance-rebased
# Conflicts:
#	src/components/documents/__tests__/document-ai-section.test.tsx
#	src/components/documents/document-ai-section.tsx
2026-07-09 01:05:35 +02:00
Marc-André Bombeck 20275d19e0 Merge branch 'fix/resilience' into release/v1.27.30 2026-07-09 01:00:04 +02:00
Marc-André Bombeck 9ce88f01df Merge branch 'fix/tz-correctness' into release/v1.27.30 2026-07-09 01:00:04 +02:00
Marc-André Bombeck 023dc4ee4c Merge pull request #439 from MBombeck/release/v1.27.29
Release v1.27.29 — automatic document indexing and AI reading
v1.27.29
2026-07-09 00:49:17 +02:00
Marc-André Bombeck 40c46f6f5b test(e2e): scope the AI-read card-marker assertion to desktop
The vault timeline is virtualized; on the narrow mobile viewport the seeded
Radiology-note card renders outside the initial virtual window (not in the
DOM), so the card-level attribute read waited out the full test timeout on a
non-existent node. The provenance contract is asserted on every project via
the detail status pill; the card marker (viewport-independent render logic,
also covered by the SSR card test) is asserted on desktop where the card is
reliably in view.
2026-07-09 00:39:24 +02:00
Marc-André Bombeck 1a95739561 fix(crypto): log swallowed decrypt on undecryptable read-path fields
The cycle/illness/records note DTOs, the biomarker context, and the custom
mood/cycle labels each degraded an undecryptable value to a blank/generic
fallback with no signal — a key-rotation gap read as 'left blank' and the
catch was silent. Keep the fail-soft read posture (one bad row never 500s
the list) but emit a wide-event on each swallowed decrypt so a systemic
key gap surfaces. The visible per-field 'unreadable' marker in the UI is
the deferred remainder.
2026-07-09 00:21:16 +02:00
Marc-André Bombeck 00140e509e docs(documents): correct extract-route gauntlet comment for new gate 2026-07-09 00:10:20 +02:00
Marc-André Bombeck c76c3f373e chore(documents): drop unused import + prettier the openapi table 2026-07-09 00:04:34 +02:00
Marc-André Bombeck 801aa73de0 style: prettier formatting 2026-07-09 00:04:21 +02:00
Marc-André Bombeck 6831dc2a15 test(offline-banner): update copy assertions to the honest offline message 2026-07-09 00:03:25 +02:00
Marc-André Bombeck 7f968135b5 refactor(theme): back the provider with useSyncExternalStore
Reading localStorage/matchMedia in a mount effect tripped the
react-hooks/set-state-in-effect rule. Model the stored preference and the
OS preference as external stores read via useSyncExternalStore with an
SSR-stable 'dark' server snapshot, and apply the <html> class from a
DOM-only effect — no setState-in-effect, same hydration-seam fix.
2026-07-09 00:00:36 +02:00
Marc-André BombeckandMarc-André Bombeck c825d4b501 feat(documents): per-egress notice + document-AI capability probe
Add GET /api/documents/inbound/capability (document provider order + vendor-blind
egress class), repoint the vault capability hook to it, and show a per-egress
notice in the detail sheet before a document read leaves the machine. Cover the
document provider order, the egress consent gate, and the notice with tests.
i18n x6 + OpenAPI regenerated.

Co-Authored-By: Marc-André Bombeck
2026-07-08 23:59:56 +02:00
Marc-André Bombeck 3faa4ee76e fix(coach): log swallowed self-context decrypt failures
The Coach self-context loader failed soft per field (undecryptable
allergies/conditions → null) with no signal, so a key-rotation gap made
the Coach generate guidance without ever seeing the user's recorded
allergies, indistinguishable from an empty profile. Keep the per-field
fail-closed posture but emit a wide-event on each swallowed decrypt and on
the DB-read guard, so a systemic failure surfaces instead of masquerading
as 'the user never wrote anything'.
2026-07-08 23:59:37 +02:00
Marc-André Bombeck 9eadd0755f fix(withings): widen incremental overlap from 60s to 10 min
Withings backdates some readings and lastSyncedAt advances on every
success (including a healthy 200-with-0 cycle), so a reading landing more
than 60s before the next cycle's now() could be missed. Widen the poll
catch-up overlap to 10 minutes — the upserts are idempotent, so re-touching
a handful of rows is harmless.
2026-07-08 23:57:02 +02:00
Marc-André Bombeck 4c158f6abc feat(integrations): per-metric-type last-value freshness signal
Freshness was tracked only per-integration, so a provider returning a
healthy 200 with an empty body for ONE metric type recorded no failure and
the whole integration stamped green — a metric dead since launch was
indistinguishable from a healthy-idle one. Compute the last-value
timestamp per (source, type) straight from the Measurement table (no
migration) and surface it per integration on the status endpoint, so the
Settings card can flag a silently-dead metric the green pill hides. The
card UI badge + per-cadence staleness classification are the deferred
remainder.
2026-07-08 23:55:35 +02:00
Marc-André Bombeck 40be4d180d feat(boot): print a first-boot readiness summary before fail-closed loaders throw
The fail-closed crypto loader throws a stack trace on a missing/short
ENCRYPTION_KEY rather than a sentence an operator can act on, and
check-env is a dev/CI tool the image operator never runs. Add a boot-time
readiness summary (instrumentation register) that prints a clear line per
core secret — with the exact openssl fix on a bad one — before anything
throws, plus a one-glance status of the optional channels and the
Secure-cookie transport hint. Never logs secret values; never throws.
2026-07-08 23:55:33 +02:00
Marc-André Bombeck 24dafeafee feat(insights): make the AI no-provider empty state legible and privacy-first
The daily-briefing no-provider state now leads with the local-model path
(nothing leaves your network) before the BYO-key option and shows a
concrete worked-example briefing so a new user can see the payoff before
spending a key. Light discoverability improvement only — the larger
onboarding rework (a first-class AI step) is left for the maintainer.
2026-07-08 23:53:29 +02:00
Marc-André BombeckandMarc-André Bombeck c1e11e284a test(documents): update vault AI route mocks for document provider order
Point route + backfill test mocks at the document-scoped resolvers and the
document-egress consent gate.

Co-Authored-By: Marc-André Bombeck
2026-07-08 23:51:50 +02:00
Marc-André Bombeck 081bb2289d fix(pwa): fail offline mutations fast instead of pausing + silent write loss
Mutations ran in TanStack's default 'online' network mode, which PAUSES a
write fired offline: isPending stays true forever, onError never fires,
and the paused mutation is dropped on reload — a health entry vanishes
with no 'not saved' signal. Switch mutations to 'always' network mode so
an offline write rejects immediately and the form surfaces an honest
failure, and add a global OfflineMutationToaster backstop for call sites
without their own onError. Correct the offline banner copy, which promised
changes would auto-save.
2026-07-08 23:51:08 +02:00
Marc-André Bombeck 451946b4fa feat(admin): quiet reminder while public registration is open
Registration stays open after the first admin so a household can be
invited, but on an internet-exposed instance that means anyone who can
reach the URL can sign up — and nothing prompted the admin to close it.
Surface a muted hint under the registration toggle whenever it is open
(no behaviour change), and note the reality in getting-started's
first-user section.
2026-07-08 23:51:00 +02:00
Marc-André Bombeck 88bff471cc test(tz): pin DST-safe today bounds, user-tz compliance bucketing, greeting zone
- getUserTodayBounds: a 25h fall-back day keeps a 23:30 dose inside today,
  a 23h spring-forward day does not bleed tomorrow's 00:30 dose, ordinary
  days stay 24h, and a negative-offset zone anchors on the local day.
- dailyComplianceRatesFromLedger: a Los-Angeles user's two same-local-day
  doses land in ONE bucket where the old fixed-Berlin key split them.
- classifyIntakeTiming: an 08:00-local dose reads on_time with the user's
  zone and very_late without it.
- hourInTz: reads the wall-clock hour in the given zone.
- hero-strip greeting: resolves the hour in the stored zone, not the
  browser clock; existing greeting fixtures re-pinned to explicit UTC
  instants mapped to Berlin wall-clock.
2026-07-08 23:50:08 +02:00
Marc-André BombeckandMarc-André Bombeck 8e5f5f04ae feat(documents): route vault AI surfaces through document provider order
Repoint suggest/summary/extract/index/reindex routes and the content-index
backfill job to the document-scoped resolvers (local-first, codex last) and the
pick-based document-egress consent gate. Labs OCR and Coach/insights untouched.

Co-Authored-By: Marc-André Bombeck
2026-07-08 23:49:30 +02:00
Marc-André Bombeck 80bbf2ba3b perf(ui): harden theme-provider hydration seam + cache per-row Intl formatters
Theme provider now initializes to the SSR-stable default and reconciles
the stored preference in a mount effect instead of reading localStorage
in the useState initializer — closes the same React #418 hydration seam
the sidebar already fixed (the inline script still handles the pre-paint
class, so no FOUC). Add a process-wide Intl formatter cache and route the
per-row workout/medication list helpers through it so a formatter is
constructed once per (locale, options) rather than once per row per
render.
2026-07-08 23:48:24 +02:00
Marc-André Bombeck a53dd2b59c fix(doctor-report): mark an undecryptable allergy reaction instead of blanking it
The clinician-facing export failed SOFT on an undecryptable allergy
reaction (→ null, rendered as a bare '—' indistinguishable from 'no
reaction recorded') while a sibling dose-change note fails CLOSED. Extract
a pure decryptAllergyReaction helper that keeps 'unreadable' distinct from
'unset', log the swallowed decrypt, and render an honest localized marker
in the PDF so a clinician is never shown a silent blank where an
unreadable reaction exists. Read-path only — the fail-closed write posture
is untouched.
2026-07-08 23:46:54 +02:00
Marc-André BombeckandMarc-André Bombeck f7ad2339ae feat(documents): document-scoped provider order and egress consent
Add a document-class provider resolution that demotes external/train-by-default
providers below local (local -> BYOK no-train API -> operator key -> codex last)
and a pick-based document-egress consent gate that requires an active receipt
for any external provider. Coach/insights keep the app-wide cost-first order.

Co-Authored-By: Marc-André Bombeck
2026-07-08 23:46:44 +02:00
Marc-André Bombeck 3fcae204f7 perf(backup): keyset-page the intake/mood/cycle reads in the off-host DR dump
Measurements were already paged, but the medication-intake, mood, and
cycle tables in the same per-user payload were still unbounded full-table
findMany reads — peak heap tracked the largest tenant's history. Give
them the same id-keyset page walk (full row projection, so payload bytes
are unchanged) so at most one page is resident per round-trip.
2026-07-08 23:45:04 +02:00
Marc-André Bombeck 329f78d295 perf(import): batch CSV measurement upserts instead of one round-trip per row
The CSV importer ran a serial prisma.upsert/create per accepted row, so
an N-row file cost N round-trips and a few-thousand-row import could
approach the request timeout. Adopt the /measurements/batch shape:
pre-fetch existing rows under the IMPORT source in one query per
dedup-key shape (externalId and natural-key paths), partition into bulk
createMany + grouped updateMany, and reconstruct the per-line
inserted/updated/duplicate envelope from the partition. In-file repeats
and re-import idempotency preserved; response contract unchanged.
2026-07-08 23:43:11 +02:00
Marc-André Bombeck 982fe53b51 fix(measurements): fall back to live SQL when the rollup read throws
The daily-series reader only fell back to live SQL on a coverage MISS
(zero rollup rows); a transient rollup-table error (statement_timeout,
deadlock, connection reset) was unwrapped and 500'd the tile on the
single-type route even though live SQL could serve it — and diverged from
the batched series route, which already caught. Wrap the primary daily
read and the long-range tiered read: treat a throw as a coverage miss and
fall through to the live date_trunc aggregate.
2026-07-08 23:41:52 +02:00
Marc-André Bombeck 62c7278275 fix(fitbit): port the non-rethrowing hard-fail ledger from Google Health
Fitbit's handleCollectionFetchError soft-skipped a 403 but RETHREW any
other hard failure, aborting every endpoint ordered after the bad one in
the same resource and stalling the watermark so the whole integration
stayed dead. Record hard failures on a per-cycle AsyncLocalStorage ledger
and return 0 instead of rethrowing — the sibling endpoints keep fetching,
and the ledger still fails the cycle so success is not stamped and the
next tick refetches. Also wrap the per-metric map fan-out in the same
per-collection catch so one malformed point can't skip its siblings.
2026-07-08 23:40:36 +02:00
Marc-André Bombeck fb6813bcdb fix(medications): bucket the local day in the user's zone, DST-safe
Five timezone-correctness fixes across the medication / compliance surfaces:

- getUserTodayBounds now derives both bounds through the DST-safe
  startOfLocalDayInTz, so the today window is 23h on spring-forward and
  25h on fall-back instead of a hardcoded 24h sampled at now. A 23:30
  local dose no longer drops off the med cards, dashboard, PWA badge and
  reminder idempotency near midnight on a transition day.
- dailyComplianceRatesFromLedger takes the user's zone and buckets each
  per-day rate with userDayKey, so a non-Berlin user's evening dose is
  filed on the local day it was due rather than the next Berlin day.
- classifyIntakeTiming interprets the window HH:mm in an optional zone;
  the achievements perfect-day check passes the user's zone and reads the
  scheduled minute-of-day in-zone, so a dose far from UTC is graded
  against local wall time.
- The /insights greeting resolves the hour in the user's stored zone via
  the new hourInTz helper, matching the dashboard hero.
- The doctor-report and schedule-anchored compliance queries load
  schedules through SCHEDULE_COMPLIANCE_SELECT instead of a broad
  include, so a future engine column reaches both surfaces.
2026-07-08 23:38:48 +02:00
Marc-André Bombeck 50031911c2 feat(auth): warn on a Secure-cookie / plain-HTTP transport mismatch at login
The most common self-host login loop is a Secure session cookie issued
over plain HTTP: the browser drops it and bounces the user back to the
sign-in page with no error. Detect it at cookie-mint time
(detectInsecureCookieTransport) and emit a clear warn-log plus a
session_cookie_secure_transport_mismatch wide-event, pointing at
SESSION_COOKIE_SECURE=false / X-Forwarded-Proto. Non-fatal; login still
completes.
2026-07-08 23:36:48 +02:00
Marc-André Bombeck 4b84a12ede fix(oura): isolate the 8 daily collections per-collection (allSettled)
Oura fetched all 8 collections under one Promise.all: a single flaky
endpoint or one throwing mapper rejected the whole batch and imported
nothing (readiness, sleep, activity, spo2 all went dark together). Settle
each collection independently, import what succeeds, and carry the failed
ones as a per-collection ledger — mirroring Google Health / Fitbit. The
reactive 401-refresh is preserved: a shared expired token 401s every
collection, one refresh is done, and only the failed collections retry.
A partial failure records the failure and does not stamp success, so the
freshness surface stays honest and the next tick refetches.
2026-07-08 23:36:24 +02:00
Marc-André Bombeck ced4e45272 docs(self-hosting): reconcile DATABASE_URL/POSTGRES_PASSWORD guidance with the compose contract
The bundled compose constructs DATABASE_URL inside the app service from
POSTGRES_PASSWORD, so the getting-started 'edit CHANGE-ME' step was a
no-op and .env.example's literal POSTGRES_PASSWORD collided with the
quick-start's appended one. Comment both out in .env.example so the
append pattern yields a single key, and rewrite step 2 to state compose
derives the URL. Add a first-user note to close public registration on
an internet-exposed host and to save the password (no-SMTP CLI reset).
Refresh the stale onboarding step-2 source comment.
2026-07-08 23:34:50 +02:00
Marc-André Bombeck 77c73517c0 fix(jobs): apply pool cap + statement timeout to worker Prisma clients
The 60s statement_timeout + pool ceiling applied to the web client in
db.ts were never applied to the worker clients. A pathological nightly
drain on a heavy tenant could run unbounded, pin a connection, and wedge
the shared worker pool. Route both worker clients (reminder handlers and
the Apple Health import worker) through the same getPoolMax() +
buildSessionOptions() helpers so every worker session is timeout-bounded
from its first query.
2026-07-08 23:32:48 +02:00
Marc-André Bombeck 50e630bf2d test(e2e): lift expect timeout to 10s and CI retries to 2 for loaded-runner headroom
A rotating single e2e test failed each CI run (wizard step transition,
list-refetch card provenance, disclosure toggle) while 211 passed — DOM-
settle-gated assertions sampling a mid-transition frame on a starved shared
runner. Raise the global expect timeout 5s→10s (suite-wide settle headroom)
and CI retries 1→2 (absorb the transient without masking a real break, which
fails all attempts). Also scroll the provenance card into view before reading
its attribute, since the mobile timeline can render it off-viewport.
2026-07-08 23:04:22 +02:00
Marc-André Bombeck e48f9859c8 test(e2e): widen step-transition and provenance assertion timeouts
The wizard step-advance and the AI-read card marker are gated on async
settles (per-step validation; list invalidate→refetch); on a loaded CI
runner those exceed the default 5s expect timeout, so the assertion samples
a mid-transition frame. Widen just those two assertions to 20s — orthogonal
to test.slow() (which extends the test, not the expect, timeout).
2026-07-08 22:50:00 +02:00
Marc-André Bombeck 1dd9b92b7d test(e2e): mark the medication-wizard variants and AI-flow group slow
The wizard-variant specs (rolling/weekdays/compose/monthly/biweekly/oneshot)
and the document-AI flow group are legitimately long multi-step flows; under
a loaded CI runner they race the default 30s and flake green paths — a
different one fails each run. Give them the tripled timeout, matching the
already-slow daily wizard.
2026-07-08 22:38:30 +02:00
Marc-André Bombeck 76277ca31d chore(release): v1.27.29 — automatic document indexing and AI reading 2026-07-08 22:16:12 +02:00
Marc-André Bombeck e261985a48 test(documents): pin contentIndexSource provenance through list/detail/PATCH
Cover the new DTO provenance end-to-end against a real Postgres: the index
source threads through the list and detail reads, and — the regression the
UI wave flagged — survives a metadata PATCH, which now reads the index row
rather than returning a stale null source.
2026-07-08 22:05:05 +02:00
Marc-André Bombeck ecebe796ef test(documents): cover the Read with AI action and AI-read provenance in e2e
Add two acceptance tests to the vault AI suite: the prominent Read with AI
action maps to the vision index endpoint (empty body, no text-mode misroute)
and confirms with a toast; a vision-sourced document surfaces the AI-read
provenance on both the detail status pill and the timeline card marker.
2026-07-08 22:03:13 +02:00
Marc-André Bombeck eed705be7b fix(documents): make auto-index enqueue never fail a stored upload
The upload route awaited an unguarded enqueueDocumentIndex after the
document was already committed. A transient boss.send rejection (a DB
hiccup) would then surface as a 500 for an upload that in fact stored the
file. Guard the send inside the enqueue (a failure annotates and no-ops,
matching the missing-boss path) and drop the await at the call site so the
response never blocks on or fails because of background indexing.
2026-07-08 21:56:17 +02:00
Marc-André Bombeck 0b973f4f76 test(documents): mock content-index lookup and pair new animations with motion-reduce
The PATCH detail route now reads the content-index provenance, so its route
test mocks documentContentIndex.findUnique; the searchable status spinner and
the Read-with-AI pulse pair with motion-reduce:animate-none per the coverage
guard.
2026-07-08 21:41:49 +02:00