3492 Commits
Author SHA1 Message Date
Marc-André Bombeck 945e8b8df4 Merge pull request #432 from MBombeck/release/v1.27.22
Release v1.27.22 — document assist and content search
v1.27.22
2026-07-07 20:48:59 +02:00
Marc-André Bombeck 1545f3ec00 test(e2e): wait for the settled sidebar width on the iPad-portrait guard
The tablet icon-rail collapse is mounted-gated (SSR and the hydration
frame paint the expanded 256px shell to avoid a React #418 mismatch),
so the resolved layout lands on the first client render after hydration
settles. On a loaded runner that can arrive just after networkidle,
letting the guard measure a mid-hydration frame (main=512 instead of
~704). Wait for the content column to reach its settled width before
measuring; a real regression still fails via the wait timing out.
2026-07-07 20:38:32 +02:00
Marc-André Bombeck c0676564e6 chore(release): v1.27.22 — document assist and content search 2026-07-07 20:16:08 +02:00
Marc-André Bombeck 5dd8f23890 test(documents): e2e for AI assist, session-only summary, and content search
Six specs over the P2 surface: the review-first draft that only writes on
an explicit save (never auto-committed), the transient summary panel with
its not-saved / not-a-diagnosis note, whole-word content search reaching a
body-only word through the real blind-token index union, the corpus
index-all backfill CTA, the provider-unavailable settings pointer, and the
text-mode image-only refusal that fires before any OCR. The vault fixture
gains a seeded content-index row (tokens hashed under the same HKDF subkey
the server derives) plus a bland assist probe document.
2026-07-07 19:45:08 +02:00
Marc-André Bombeck 1c0c194820 fix(documents): scope the content-index gauge to live documents
The usage endpoint counted every DocumentContentIndex row for the user,
including those whose parent document has been soft-deleted (the index
row survives until the hard purge). Against totalCount, which filters
live documents, the gauge could read past 100 %. Scope the indexed count
to live documents so coverage never exceeds the corpus.

Also drop a redundant identical-branch ternary in the per-document
content-index status row.
2026-07-07 19:31:41 +02:00
Marc-André Bombeck 2319988a96 feat(documents): surface content search in the vault list
The list search already unions matches from the encrypted content index
server-side; surface it so it is legible and reachable:

- a subtle "search now looks inside your documents too (whole words)" hint
  under the search box once any document is indexed;
- a per-row "searchable" marker on indexed documents;
- an "index all for search" affordance that fires the corpus backfill when
  some documents are not yet indexed, driven off the usage coverage gauge.

All of it is gated on the same provider/consent precondition as the assist
layer — with no provider, search degrades to title and filename exactly as
before.
2026-07-07 19:14:59 +02:00
Marc-André Bombeck afeb693562 feat(documents): review-first AI assist and session-only summary in the vault detail sheet
Add an optional AI layer to a stored document, gated on the existing AI
provider and consent — no new toggle. All of it rides the shared OCR
capability probe so the client only posts a transport the endpoint accepts
(vision empty-body POST, or local-OCR text posted from the browser).

- "Suggest details" prefills the title / type / date as reviewable DRAFTS.
  Nothing is written on its own: the title lands in the editable field and
  is saved through the existing commit path; type and date each apply on an
  explicit tap. The card carries a visible "review before saving" affordance.
- A summary / extracted-text button opens a transient panel that renders the
  result once and always shows a "not saved / not a diagnosis" note; closing
  it discards the text. Nothing persists beyond the view.
- When no provider is configured the AI area is a calm pointer to the AI
  settings, never an error; the manual metadata form stays fully usable.

The detail-sheet AI area is lifted into a presentational section component so
the availability gate and the review-first wiring render without the sheet's
dialog portal and are pinned by static-render tests. Strings land in all six
locales.
2026-07-07 19:14:34 +02:00
Marc-André Bombeck 2653647f97 style(documents): prettier-format the list route test
P2-1. Formatting-only follow-up on the content-index mock additions.
2026-07-07 18:45:48 +02:00
Marc-André Bombeck ce2f4808e2 test(documents): content-index integration coverage on real Postgres
P2-1. End-to-end: TEXT-mode indexing persists only ciphertext text + opaque
HMAC tokens (A4), a body-only word finds the document through the GIN token
overlap while the list still omits the blob, a word that appears nowhere does
not match, re-indexing upserts in place, and hasContentIndex + the usage gauge
reflect the index. Migration 0228 applies clean on the fresh testcontainer.
2026-07-07 18:43:39 +02:00
Marc-André Bombeck 1f7807aead test(documents): wire content-index mocks into the list route test
P2-1. The list GET now issues a documentContentIndex lookup and unions a
content-token OR branch when a search term is present, so the existing route
test gains the mock + the third OR branch in its where assertion. Fix the
backfill test's findFirst mock signature under strict tsc.
2026-07-07 18:41:09 +02:00
Marc-André Bombeck b210c743e9 test(documents): cover content index, assist/summary/index routes, backfill
P2-1. Unit tests for the token scheme (whole-word match, stopword drop,
de-accent, no plaintext at rest, subkey follows the active key), HKDF subkey
derivation, the suggest route (drafts only, writes nothing, provider-gated),
the summary route (session-only, budget+audit only), the index route (vision
transcribe→upsert, text posts OCR text, provider/local-OCR gated), and the
backfill (no-provider/no-consent no-op, indexes, budget-reached resume).
2026-07-07 18:29:02 +02:00
Marc-André Bombeck 5a0fd47eb5 feat(documents): content-index backfill job, rotation coverage, OpenAPI
P2-1-T7/T8/T9.

- document-content-index-backfill: on-demand per-user pg-boss job that indexes
  a user's not-yet-indexed image/PDF documents via one provider transcription
  each. Consent + budget gated, id-cursor forward walk, capped per run, stops
  on budget exhaustion, resumable. Queue + worker registered in the maintenance
  registrar (no cron — deliberate action only). POST /reindex triggers it.
- rotation: register DocumentContentIndex.textEncrypted and add a dedicated
  block that re-encrypts the text AND re-tokenises the blind index under the new
  active HKDF subkey in one update (P2-D7).
- OpenAPI: suggest / summary / index / reindex paths; hasContentIndex on the
  document schema; assistAvailable + contentIndex on usage. Regenerated
  docs/api/openapi.yaml.
2026-07-07 18:21:51 +02:00
Marc-André Bombeck 85a343a68c feat(documents): assist, summary, content-index endpoints + search union
P2-1-T2/T3/T4/T5/T6.

- assist.ts + [id]/suggest: one provider call proposes a { title, kind,
  documentDate } filing draft over the stored original or browser-OCR text.
  Runs the full extract gauntlet (module → provider → consent → rate-limit →
  budget) but writes nothing — never touches the fact rail or status.
- describe.ts + [id]/summary?mode=summary|text: session-only summary or raw
  transcribed text. Descriptive only, no diagnosis; persists only the budget
  ledger + audit.
- content-index.ts + [id]/index: normalise → tokenise (de-accent, stopword
  drop, whole-word) → HMAC-SHA256 under an HKDF-derived subkey → store the
  token array plus AES-256-GCM ciphertext of the text. Vision path transcribes
  via the provider (consent + budget); text path indexes posted local-OCR text
  (no egress). Gated on the existing AI consent — no new per-user toggle.
- crypto.deriveSubkey: HKDF-SHA256 purpose-bound subkey off the active key.
- list search now unions title/filename ILIKE with a GIN-accelerated
  whole-word content-token match; DTO gains hasContentIndex.
- usage gains assistAvailable + contentIndex { enabled, indexedCount,
  totalCount }; detail gains hasContentIndex.
2026-07-07 18:17:13 +02:00
Marc-André Bombeck 7c5d1a264e feat(documents): add DocumentContentIndex table for blind content search
P2-1-T1. Migration 0228 adds the 1:1 document_content_index sibling of
inbound_documents: AES-256-GCM ciphertext of the extracted text plus a
deduped HMAC-SHA256 token array (search_tokens) with a GIN index for the
array-overlap match the list search will union with title/filename ILIKE.
Both foreign keys cascade. Nothing readable sits at rest; the index subkey
is HKDF-derived at runtime, so tokens are opaque without the server key.
2026-07-07 18:08:12 +02:00
Marc-André Bombeck 1474c87011 Merge pull request #431 from MBombeck/release/v1.27.21
Release v1.27.21 — hero spacing and one green
v1.27.21
2026-07-07 17:59:03 +02:00
Marc-André Bombeck 4aa3342ba2 chore(release): v1.27.21 — hero spacing and one green 2026-07-07 17:35:50 +02:00
Marc-André Bombeck 154d1cfebe Merge branch 'fix/hero-polish' into release/v1.27.21 2026-07-07 17:32:27 +02:00
Marc-André Bombeck 2fc7160ead Merge pull request #430 from MBombeck/release/v1.27.20
Release v1.27.20 — coach and cycle toggle from the modules hub
v1.27.20
2026-07-07 17:21:33 +02:00
Marc-André Bombeck d560bc4575 chore(release): v1.27.20 — coach and cycle toggle from the modules hub 2026-07-07 17:11:51 +02:00
Marc-André Bombeck 4752b48a21 Merge branch 'feat/module-hub-delegated-toggles' into release/v1.27.20 2026-07-07 17:07:53 +02:00
Marc-André Bombeck d70ae37c11 Merge pull request #429 from MBombeck/release/v1.27.19
Release v1.27.19 — vault page polish
v1.27.19
2026-07-07 16:57:21 +02:00
Marc-André Bombeck 8bc2f0f2ef feat(modules): direct on/off switches for the delegated Coach + Cycle rows
The Modules hub listed Coach and Cycle as read-only "manage in X" deep-links
because their enabled-state is owned outside `modulePreferencesJson` (Coach in
Coach settings, Cycle in the Account card). Give both a real switch like every
other module, driving the SAME canonical state instead of a second copy:

- Coach → `PATCH /api/auth/me/disable-coach` (`User.disableCoach`, inverted);
  the switch reads `!disableCoach`.
- Cycle → `PATCH /api/auth/me/cycle-prefs` (`cycleTrackingEnabled`); the switch
  reads the resolved per-user gate.

Precedence stays honest. A module the operator turned off server-wide can never
be re-enabled by a per-user toggle, so the switch renders disabled with a
"disabled server-wide" hint. For Coach the operator layer is BOTH the
module-availability blob AND the assistant master flag (`flags.coach`); for
Cycle it is the module-availability blob. Each delegated row keeps a small
"manage" deep-link beside its switch, since the canonical surface carries more
than on/off (Coach cadence/memory; cycle goal, predictions, lengths).

Flipping either switch invalidates `authMe()` so every gated surface re-gates
live; the cycle flip also evicts its own `cyclePrefs`/`cycle` reads, matching
the canonical cycle card. Reuses the existing endpoints — no new route, no
OpenAPI change.
2026-07-07 16:50:09 +02:00
Marc-André Bombeck a2c6eeeb77 chore(release): v1.27.19 — vault page polish 2026-07-07 16:47:08 +02:00
Marc-André Bombeck 1fe753c4d0 Merge branch 'fix/documents-width' into release/v1.27.19 2026-07-07 16:31:20 +02:00
Marc-André Bombeck 6f1833eb10 fix(dashboard): anchor the hero greeting and unify the ring green
The greeting drifted to the vertical centre of the taller ring column
and the briefing sat a full step below it — the top row now aligns to
its start and the section gap tightens, so the greeting anchors to the
top edge and the briefing follows closely.

The band-fallback ring green landed on its own hue (#3fb868) while every
positive signal and trend delta beside it used --success (#50fa7b); a
green ring now lands on --success too, so 'green' reads as one green
across rings, signals, and deltas. Computed contrast 11.9:1 dark /
6.1:1 light — well past the graphics floor.
2026-07-07 16:19:14 +02:00
Marc-André Bombeck 421c407811 Merge pull request #428 from MBombeck/release/v1.27.18
Release v1.27.18 — late doses no longer jump the evening slot
v1.27.18
2026-07-07 16:11:08 +02:00
Marc-André Bombeck 20e052b207 chore(release): v1.27.18 — late doses no longer jump the evening slot 2026-07-07 16:01:24 +02:00
Marc-André Bombeck 4760f6695d Merge branch 'fix/med-late-intake-slot' into release/v1.27.18 2026-07-07 15:58:10 +02:00
Marc-André Bombeck cd450a8186 fix(documents): conform the vault page to the app frame and trim its chrome
- width: drop the /documents wide-content exception; the page uses the
  standard content frame like every other route (the multi-column
  timeline still fits, and no page should be wider than the rest).
- upload: retire the standing dashed drop card — it took a vertical band
  on every visit for what the header Upload button and the page-wide
  drag-and-drop overlay already cover. The hidden input and the
  above-80% quota bar stay.
- filter: search shares one row with the tag chips on desktop instead of
  sitting on its own line above them.
- detail: move Delete to the bottom-left as a quiet text button; the
  bottom-right slot reads as the primary/confirm action, so a
  destructive control there is a footgun. Download keeps the trailing edge.
2026-07-07 15:56:30 +02:00
Marc-André Bombeck ad503c45c0 fix(medications): keep a late take off a future slot the card supplied
A twice-daily card advances its display-due to the evening slot once the
morning slot's catch-up window has lapsed, so a late-morning "Genommen"
tap posts `scheduledFor = <evening slot>` with `takenAt = now`. Band
attribution correctly refuses the take (it lands in no window), but the
intake routes' source-agnostic convergence probe then bound it to the
evening slot's pending REMINDER row — recording the morning dose as the
evening one, silently consuming a slot the user had not reached, and
jumping the dashboard "next intake" to the following day.

Gate the convergence probe with `mayConvergeOntoSuppliedSlot`: a taken
write may not converge onto a slot whose anchor is in the future relative
to the take (the same forward-grace invariant `resolveCanonicalSlotInstant`
already enforces). Off-window late takes now record standalone (ad-hoc),
leaving both real slots intact; skips and pending echoes still name their
slot deliberately. Applied to both the per-medication and bulk intake
routes.
2026-07-07 15:55:50 +02:00
Marc-André Bombeck b6e6fa3bdc Merge pull request #427 from MBombeck/release/v1.27.17
Release v1.27.17 — the document vault
v1.27.17
2026-07-07 15:31:13 +02:00
Marc-André Bombeck 1ae5bd8151 chore(release): v1.27.17 — the document vault 2026-07-07 15:19:58 +02:00
Marc-André Bombeck 03058b0210 Merge branch 'feat/vault-w4-qa' into release/v1.27.17 2026-07-07 15:10:45 +02:00
Marc-André Bombeck 8b4b9f848c fix(documents): arm the preview focus guard at mount, not at load
Chromium's PDF viewer steals focus while it initialises — before the
frame's load event fires (and in environments without a PDF plugin the
load event may not fire at all), so a load-armed guard never engaged and
Escape stayed dead after opening a preview. The guard now opens when the
preview mounts, extends on load, bounces any focus landing on the frame
back to the dialog for its duration, and ends early on a deliberate
pointer interaction with the preview.
2026-07-07 15:01:28 +02:00
Marc-André Bombeck beaf51fadd docs: describe the document vault in the README highlights
The roadmap parenthetical becomes a shipped feature bullet.
2026-07-07 14:33:20 +02:00
Marc-André Bombeck c899f5054e test(e2e): document-vault acceptance suite
Pins the doctor-flow script (type chip, condition chip, text search —
three independent routes to one document), the URL deep-link contract
(?kind / ?episode / ?year / ?doc), delete with undo, the bulk flows
(50-item kind change, per-id partial failure, client-side chunking above
100 ids), keyboard-only navigation over the roving grid, page-wide drop
and clipboard-paste intake, the duplicate-upload contract, the admin
per-file cap (commit-on-Enter, rejection copy naming the limit), the
touch long-press selection on the mobile project, and axe scans over the
timeline, the open detail sheet, and bulk mode.

The fixture seeds the 60-document corpus straight through Postgres —
the upload endpoint's 60/hour bucket cannot carry API seeding across
two Playwright projects — with blobs encrypted in the same binary2
layout the server writes, so previews exercise the real decrypt path.
Idempotent throughout; mutating tests use disposable namespaces.
2026-07-07 14:32:15 +02:00
Marc-André Bombeck 0ed3d2fcc1 fix(documents): reclaim keyboard focus after the PDF preview loads
Chromium's PDF viewer grabs focus asynchronously once the inline preview
initialises, stranding keyboard events inside the embedded document —
Escape stopped closing the detail sheet the moment a PDF rendered. Watch
for the steal over a short bounded window after load and hand focus back
to the dialog container; a user who clicks into the PDF afterwards is
left alone.
2026-07-07 14:32:15 +02:00
Marc-André Bombeck d937991af4 fix(documents): source the condition chips from the usage endpoint
The filter bar derived its condition chips from the loaded corpus, so an
episode whose only linked documents sat pages deep in the timeline never
surfaced a chip — the condition route into an older document silently
disappeared at four-digit corpus sizes. The usage endpoint (already read
on page mount) now returns `linkedEpisodes` — every episode carrying at
least one live document link, one indexed grouped query, no blobs — and
the view unions it with the active-filter fallback. Additive DTO change;
OpenAPI regenerated.
2026-07-07 14:15:40 +02:00
Marc-André Bombeck af45899f89 fix(documents): solid destructive delete on the bulk bar
The outline variant's destructive text on the card surface fails the
WCAG AA contrast gate (axe serious). Matches the detail sheet's Delete.
2026-07-07 14:15:40 +02:00
Marc-André Bombeck b5f4153a42 Merge pull request #426 from MBombeck/release/v1.27.16
Release v1.27.16 — offline that survives, and a faster first paint
v1.27.16
2026-07-07 14:09:30 +02:00
Marc-André Bombeck 6bab2ce774 fix(documents): serialise the upload quota gate per user
Concurrent uploads all read the same pre-insert usage SUM, so a burst
could overshoot the per-user quota by rate-limit x cap in one window. A
transaction-scoped advisory lock keyed on the user id serialises the
sum-and-insert; uploads by different users never queue on each other.
Integration probes pin both races: four parallel uploads against a
two-file quota land exactly two, and two racing identical uploads
collapse onto one row via the partial unique index.

Also aligns the round-trip integration test with the serve-route CSP
posture: the proxy carve-out owns the response CSP, the route sets none.
2026-07-07 14:00:38 +02:00
Marc-André Bombeck 9cde1715e6 fix(dashboard): spread the hero rings across phone width
On phones the four score rings wrapped into a 2-up grid clustered in
the centre with dead margin on either side. They now spread evenly
across the full width — equal space before, between, and after — so the
block reads symmetric on all four sides instead of centred; desktop's
right-aligned row is unchanged.
2026-07-07 14:00:17 +02:00
Marc-André Bombeck 32033f504b ci(e2e): build through the package script so prebuild assets exist
The e2e job called next build directly, skipping the prebuild hook
that generates the service-worker version stamp and the static i18n
catalog assets — fourteen specs timed out waiting on a UI that never
got its catalog.
2026-07-07 13:47:39 +02:00
Marc-André Bombeck 23e371965b fix(documents): consume the ?doc= deep link during render, not in an effect
The one-shot deep-link consumption moves to the render-phase derived-
state pattern the view already uses for its search draft (the effect
variant trips the set-state-in-effect rule), gated on resolved auth so
a deep link never burns before the module flag is known.

Refs: document-vault plan W3-T4
2026-07-07 13:46:02 +02:00
Marc-André Bombeck 8bff4d0c86 chore(i18n): vault copy polish across the six locales
Rewrite the inboundDocuments module description in every locale to
describe what the module now is — one encrypted, searchable, condition-
linked store — instead of the retired extraction flow (the display name
stays Dokumente/Documents; the module key is wire contract). Harmonise
the Italian and Polish vault terms with each locale's existing
vocabulary (patologia, powiązać). Reverse-coverage guard confirms no
orphaned keys remain from the pre-vault surface.

Refs: document-vault plan W3-T6
2026-07-07 13:36:30 +02:00
Marc-André Bombeck 0d37061d43 feat(admin): document-vault limit fields on the admin surfaces
The two vault knobs become editable where operators already work: the
app-settings card gains the per-file upload cap (MB, 1-100, mirrors the
hard server ceiling) and the default per-account storage quota (GB,
commit-on-blur with clamped conversion to bytes); the user edit form
gains the per-user quota override (GB, empty = instance default). The
admin users list now carries the override so the form can show it.

Refs: document-vault plan W3-T7
2026-07-07 13:34:55 +02:00
Marc-André Bombeck 4dbeafe725 chore(release): v1.27.16 — offline that survives, and a faster first paint 2026-07-07 13:31:12 +02:00
Marc-André Bombeck 0ade02e163 feat(documents): labs and health-record entry points into the vault
/labs gains a quiet link to the LAB_RESULT-filtered vault view (the
original report PDFs behind the values); the health-record export panel
links to the vault filtered to the current year next to its date-range
picker (navigation only — attaching documents to the export is out of
scope here). Both render only when the documents module is enabled for
the account, in all six locales.

Refs: document-vault plan W3-T5
2026-07-07 13:30:46 +02:00
Marc-André Bombeck ed2ac8b61d feat(documents): condition page lists and links its documents
The illness-episode detail gains a Dokumente card: the episode's most
recent linked documents as compact rows (deep-linking into the vault,
where ?doc= now opens the detail sheet directly), a link-existing picker
(searchable, toggle rows driving the idempotent bulk link/unlink), an
upload entry that lands pre-linked via the episode filter, and a
filtered show-all view. Module-gated — accounts without the documents
module see nothing. An episode without documents keeps a quiet one-line
affordance instead of an empty card. The preview and picker reads get
their own query keys; reusing the vault list key would collide with the
infinite-query shape.

Refs: document-vault plan W3-T4
2026-07-07 13:28:54 +02:00
Marc-André Bombeck cb9e7b7b7e Merge branch 'fix/hero-spacing' into release/v1.27.16 2026-07-07 13:27:08 +02:00