Commit Graph
3619 Commits
Author SHA1 Message Date
Marc-André Bombeck 813dc2547b feat(documents): add per-user automatic AI reading opt-in
Add the documentsAutoAiRead per-user Boolean (migration 0231, default
false) that authorises ambient AI reading of uploaded vault documents.
OFF by default keeps the vault local-first for every self-hoster.

- schema column + migration 0231_documents_auto_ai_read
- documentAutoReadEnabled(userId) read helper
- GET/PATCH /api/auth/me/documents-auto-ai-read toggle route; the ON
  write mints an ai_full consent receipt for the audit trail
- the document consent gate short-circuits an external pick when the
  toggle is ON; a local pick stays ungated and OFF still requires a
  receipt for any external egress
2026-07-09 10:32:59 +02:00
Marc-André Bombeck 168a8ea5c5 chore(security): suppress npm-bundled sigstore CVE-2026-48815 in trivy
The finding is npm's OWN bundled sigstore (v3.1.0) inside the Node base
image, used for publish provenance at build time only. It is not in our
application dependency tree and is unreachable at runtime — same class as the
already-suppressed picomatch entry. Re-check when the base image ships an npm
with sigstore >= 4.1.1.
2026-07-09 10:06:20 +02:00
Marc-André Bombeck 3483e7c67e Merge pull request #445 from MBombeck/hotfix/v1.28.1-document-load
hotfix/v1.28.1 — document vault load fix
v1.28.1
2026-07-09 09:41:15 +02:00
Marc-André Bombeck 951db4947e fix(documents): load the local PDF parser lazily so the vault routes stop crashing
The local content-extraction reader imported pdf-parse at module top level.
pdf-parse pulls in pdfjs, whose module evaluation references the browser-only
DOMMatrix global; on the server that reference throws during chunk
instantiation and takes down every route sharing the chunk — the document
list, upload, and search among them, so old and new documents alike fail to
load. Move the parser to a lazy import() inside the extraction function
(surface type kept as a type-only import). Verified under plain Node that the
lazy path still extracts a real PDF text layer, so local indexing is
unaffected.
2026-07-09 09:29:35 +02:00
Marc-André Bombeck 403cdf8cab chore: drop stray screenshot-harness files, ignore the pattern
Three local Playwright screenshot harnesses at the repo root were picked up
by a wide stage during the v1.28.0 assembly. They are never imported by the
app build; remove them and ignore the .audit-*.mjs pattern so a root-level
add can't recatch them.
2026-07-09 08:59:46 +02:00
Marc-André Bombeck 3ef067fb53 Merge pull request #444 from MBombeck/release/v1.28.0
release/v1.28.0 → main
v1.28.0
2026-07-09 08:58:32 +02:00
Marc-André Bombeck b143576627 chore(release): v1.28.0 — document intelligence
Marks the document line as one coherent milestone: the vault becomes a
place to understand files, not just store them — read, indexed, searched,
shared, and answered — with a local-first / consented AI posture and a
vendor-blind egress notice held at every step. Ships alongside a full
correctness, resilience, and quality sweep down to the low-priority findings.
2026-07-09 08:42:23 +02:00
Marc-André Bombeck 1c7ab080f0 Merge pull request #443 from MBombeck/release/v1.27.33
Release v1.27.33 — chat about a document
v1.27.33
2026-07-09 04:18:33 +02:00
Marc-André Bombeck 6577bd287e chore(release): v1.27.33 — chat about a document 2026-07-09 04:08:38 +02:00
Marc-André Bombeck c1bbb01afd test(documents): serialise vault-fixture seeding across parallel e2e workers
fullyParallel spreads a spec's tests across workers and each re-runs the
file's beforeAll; two workers seeding the fixed-id vault corpus at once
raced on the document_condition_links primary key (ON CONFLICT only
suppresses its named arbiter index). Guard both seeders with a Postgres
advisory lock so concurrent workers seed one-at-a-time and every insert
re-runs as a clean idempotent upsert.
2026-07-09 04:01:13 +02:00
Marc-André Bombeck a38b836089 test(documents): e2e injection + XSS-inert render for the document chat 2026-07-09 03:43:26 +02:00
Marc-André Bombeck 8cead5ccdb test(documents): e2e for the document chat panel; clear tail on settle
Add an e2e suite mocking the capability probe, the chat history GET, and the
streaming chat POST so the panel renders (v1.27.31 lesson): the entry opens an
empty safety-noted surface, a turn streams a reply that settles to one assistant
bubble, a non-indexed document shows the calm hint, and no provider leaves no
chat surface. Clear the streamed tail + optimistic bubble once the persisted
twin lands so the reply never double-renders.
2026-07-09 03:24:03 +02:00
Marc-André Bombeck 2ede0886c3 test(documents): split a pure chat body + pin its render states
Extract DocumentChatConversation (pure) from the chat panel so the open
conversation surface is statically renderable, and pin: composer + always-on
safety note, plain-text/XSS-safe assistant render (raw HTML escaped), the
streaming tail vs thinking placeholder, mapped error keys, and the container's
indexed vs not-indexed gating. Assert the AI section renders the chat slot only
in the provider-available branch.
2026-07-09 03:19:42 +02:00
Marc-André Bombeck 635c45011b feat(documents): chat-about-this-document panel in the detail sheet AI area
Add a scoped 'Chat about this document' panel beneath the Read-with-AI
actions. Opens a conversation grounded only in the document's stored text:
a message list + input streaming the assistant reply token-by-token over
the SSE route, with prior turns loaded via GET.

- Availability: the panel renders in the provider-available branch of the
  AI section (a pure chatSlot keeps the section statically renderable), and
  gates internally on whether the document is content-indexed. Not indexed
  shows a calm read-it-first hint, never an error.
- Plain-text only: assistant prose renders via ProseBlocks / StreamedProse
  as React children (no markdown library, no dangerouslySetInnerHTML).
- Streaming reuses the Coach SSE frame parser; the wire frames are a subset.
- A one-line safety note states answers describe the document and are not
  medical advice, consistent with the summary panel's not-a-diagnosis.
- Query keys via the central factory; history invalidated on the done frame.
- i18n x6 for every new string.
2026-07-09 03:16:06 +02:00
Marc-André Bombeck 09bc94a1f0 chore(crypto): register + rotate verbatim index column; format 2026-07-09 03:00:24 +02:00
Marc-André Bombeck 718388ee04 test(documents): document-chat prompt, verbatim capture, and route security-model tests 2026-07-09 02:50:43 +02:00
Marc-André Bombeck 6e84048217 feat(documents): document-chat SSE route + history GET + OpenAPI (no-tools, no-snapshot, fenced, consent/budget/rate) 2026-07-09 02:44:55 +02:00
Marc-André Bombeck f5e4dc2b94 feat(documents): verbatim capture, chat prompt builder, budget, persistence documentId threading 2026-07-09 02:37:38 +02:00
Marc-André Bombeck d4d04c75f4 feat(documents): add document_id chat discriminator + verbatim index column (mig 0230) 2026-07-09 02:34:06 +02:00
Marc-André Bombeck 2b9d5a41c8 Merge pull request #442 from MBombeck/release/v1.27.32
Release v1.27.32 — quality-of-life, accessibility, cold-start and performance polish
v1.27.32
2026-07-09 02:26:16 +02:00
Marc-André Bombeck 30b745c62f chore(release): v1.27.32 — quality-of-life, accessibility, cold-start and performance polish 2026-07-09 02:14:31 +02:00
Marc-André Bombeck e4eb3b04ab Merge branch 'fix/qol-a11y-sweep' into release/v1.27.32
# Conflicts:
#	src/components/dashboard/recent-workouts-tile.tsx
2026-07-09 02:09:57 +02:00
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 46e6842b55 chore(i18n): drop the orphaned side-effect deleteConfirm key
Replaced by deleteConfirmTitle/deleteConfirmBody when the native window.confirm
became a themed AlertDialog.
2026-07-09 00:26:13 +02:00
Marc-André Bombeck 23ded20306 fix(pages): replace bare auth-gate spinners with a layout-reserving skeleton
Introduce a shared PageAuthGate (header + tile skeleton) and adopt it on the
top-level pages that flashed a lone centered spinner while auth resolved:
mood, measurements, achievements, illness, mental-wellbeing, checkups, cycle,
and the labs / medications / custom-metrics / illness detail routes. The
auth-resolving frame now reserves the page geometry instead of snapping in
after a spinner.
2026-07-09 00:23:13 +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 96c99f4dfa fix(medications): render intake dialogs as bottom sheets on mobile
Port the six medication dialogs (log-intake, intake-edit, dose-history-add,
take-all-due, log-injection-site, intake-import) from the centred Dialog to
ResponsiveSheet with the sticky-footer slot, matching every other module's
add/edit form. Form-backed dialogs associate their footer submit button via a
useId form id so Enter-to-submit and the footer button share one form; the
nested slot-nudge confirm rides the same sheet. Primary actions now sit within
thumb reach on phones.
2026-07-09 00:13:08 +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 1c57da7d44 fix(labs): mark un-linked lab groups and bridge biomarker detail to source reports
- Lab groups not backfilled to a catalog biomarker now carry a muted
  'not linked to a biomarker yet' affordance in both list and card views, so
  an inert row no longer reads as a broken link next to clickable neighbours.
- The biomarker detail page links to the document vault filtered to
  lab-result reports (gated on the documents module), giving a path from a
  value to its source scans. A per-value document relation remains a
  data-model follow-up.
2026-07-09 00:02:56 +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 e5ca125dfb fix(onboarding,integrations,cycle): deep-link source chips + cycle settings wrench
- Onboarding 'more sources' chips anchor to their provider card
  (/settings/integrations#oura etc.); connection cards gain scroll anchors.
  Apple Health (no web card) stays on the page root.
- Cycle module gains a header settings wrench for parity with labs /
  medications / illness; the tab strip is now controlled so the wrench jumps
  to the settings tab and ?tab= deep-links keep working.
2026-07-08 23:59:10 +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