Commit Graph
21 Commits
Author SHA1 Message Date
Marc-André Bombeck e95612e3d3 fix(sharing): show a guardian the controls the routes accept (#939)
useRecordCapabilities answers per section now. canWriteDomain and
canManageDomain read the two lists the account payload publishes; in
one's own record they are always true, and under a refused, pending or
unproven context always false. canManage becomes the coarse switch,
true when any section is manageable, and is what the banner and the
navigation read.

Every add, edit and delete control names its own section: labs, mind,
profile, illness, cycle, medications, measurements, documents. Creates
that a WRITE route answers (visits, practitioners) ask canWriteDomain.
The generic delete button, the selection bar, the capture picker, the
dashboard quick entry and the bottom nav take the section from their
callers. Controls whose routes resolve the caller rather than the
record (settings shortcuts, chart overlay preferences, custom metrics,
the Today rail, OCR, efficacy targets, credential tabs) ask for the
caller's own record instead, and the customise-shortcut guard now
requires that shape.

The vault has no delegated write route, so its controls stay withheld
under every grant, and the documents page says so in one sentence
where the upload control would be, in all seven locales.
2026-09-09 19:42:27 +02:00
Marc-André Bombeck 689cacc039 Keep the measurements and mood filters in the URL
The documents view already round-trips its filters through the query
string; the measurements and mood lists kept theirs in memory, so any
navigation dropped them. The documents pattern is extracted into a shared
hook (use-url-filter-sync) and both lists adopt it: type/source/day-bounds
and the measurement value range survive reload, back and sharing, defaults
stay out of the URL, invalid facets drop leniently. The insights readings
subpage keeps its locked type in memory — that URL belongs to the insights
route. /measurements no longer strips ?type=, so the deep links the
Vorsorge cards mint now persist as filter state.
2026-08-14 09:24:08 +02:00
Marc-André Bombeck 7f6eae8425 style(ui): pull off-scale spacing and micro-labels onto the scale
Sweep the spacing rests that sat off the 1/1.5/2/3/4/6/8/10 scale: the
health-score panels and their skeleton, the recommendation ask-Coach
gutter, the onboarding tour footer, the vaccination row, the danger-zone
alert margins, the recovery-codes action wrapper, and the chart-tooltip
tile. Move the last hand-written micro-sizes (coach counters, the
personal-record badge, the score-ring label, the dose-history and
cycle-calendar headers, the mood tag chips) onto the text-2xs token, and
give the documents upload buttons the mobile tap floor their peers set.
2026-08-09 11:27:05 +02:00
Marc-André Bombeck be9c79974f feat(documents): a document can say which visit it belongs to
Every arm here is the same shape as the one that already exists for
conditions, because the vault's filing model is settled and a second shape
would be the thing to explain rather than the thing to use: encounterIds on
upload and on PATCH with replace-set semantics, linkEncounter / unlinkEncounter
beside the episode pair in the bulk action, and an encounterId list filter.

Both directions ride ONE table. The visit's sheet writes encounter_document_links
from its end and the document reads it from the other, declared as a second
direction in the link service rather than a fourth table. A visit's kind
crosses the wire as the enum constant rather than a rendered name: unlike a
condition label, which is the person's own words, a kind is one of eight closed
values whose human name the reader's own bundle owns.

The suggestion is offered where the document is being looked at, resolved by
the shared rule rather than by this surface: one candidate pre-selects with an
undo, two offer a picker with nothing pre-selected, none offers nothing at all.
It resolves with no AI provider configured, and it renders nothing once the
document already names a visit.

The bulk verb has a caller — the vault's selection bar files a whole selection
against a visit, mirroring the condition verb and hiding itself on an account
with no visits. Nothing here is one-ended.

OpenAPI regenerated in the same commit.
2026-08-08 13:41:42 +02:00
Marc-André Bombeck f49defbc34 fix(documents): do not check a close that deliberately kept its selection
Maximizing the Coach drawer hands the conversation to /coach and carries
?doc= with it, so browser-Back can rebuild the sheet. That is the one
close-shaped transition whose whole point is to LEAVE the selection where
it is, and the close path has always returned early for it.

The dropped-traversal check added beside that close did not know. It saw
a closed sheet and a selection still in the address bar, which is exactly
the state it exists to repair, and it repaired away the return URL a
quarter of a second after the close had deliberately preserved it.
Whether it won that race was a question about how fast the machine
navigated, so it surfaced as one flaky run on the narrow viewport rather
than as a broken feature.

Both sides read one named predicate now, and the check is skipped when
the close kept the selection on purpose. Proved by driving the grace
window to zero, which is the worst case the timer can have: before, the
maximize never even reached /coach, three attempts out of three; after,
it passes there and the whole documents family passes with it. Twenty
consecutive runs of that family on the narrow viewport are clean.
2026-08-07 13:15:23 +02:00
Marc-André Bombeck a5b6508961 fix(documents): check that closing a document consumed its history entry
Closing a card-opened document asks the browser to go back, so the entry
the selection pushed is consumed and the reader's next Back reaches the
page before it. A traversal is a request, not a guarantee: one asked for
while another is still settling can be dropped, and nothing raises when
it is. The sheet closes, React stays consistent, and the address bar goes
on naming a document nobody has open, so a reload or a shared link
re-opens the sheet that was just closed.

Pressing Escape straight after the browser's Forward button is exactly
that shape, and it reproduces: ten repeats across both viewports failed
two of twenty with the URL still carrying the id, and none of twenty once
the close verifies itself. The check is guarded by the document id, so a
traversal that landed on a different selection is left alone, and it is
cancelled the moment the sheet re-opens.

The browser test that found it could not say so. It asserted the sheet
was closed through a locator matched on the document TITLE, and the sheet
falls back to an untitled label while its detail read is pending, so the
assertion also passed on an open sheet showing a skeleton, resolved
immediately instead of waiting, and pushed the failure into a URL poll
ten seconds later. It now asserts the dialog role, which is what the
neighbouring deep-link test already does.
2026-08-07 10:26:25 +02:00
Marc-André Bombeck 7104578d27 fix(02-12): honor shared scope in module pages
- keep scoped record pages out of actor module redirects\n- suppress unrelated illness reads from document-only records
2026-08-06 05:12:23 +02:00
Marc-André Bombeck 7817632385 Hide the controls a delegate cannot use, surface by surface
Every page a switch can reach now renders the controls the grant covers and
none of the others. Readings, lab results and illness entries keep their add
path at WRITE and lose edit, delete and selection at both levels. Mood,
screeners, cycle days, checkup reminders and the document vault lose every
mutation control at both levels, because no grant admits those verbs and the
routes behind them refuse a switched session outright.

Absent, not disabled, throughout. The banner already says whose record this
is, so no chip repeats it beside each missing button; the one exception is
the measurements empty state, where somebody who may add will look for the
edit control beside the row they just entered, and one muted line says where
the limit is.

Two dead ends went with them: the custom-metric section on the measurements
page linked every row into a route the shell turns a delegate away from, and
the wrench glyphs point at settings pages sharing does not cover.
2026-08-03 12:11:42 +02:00
Marc-André Bombeck 62eb431d66 release: v1.32.0 — hardening and scalability (#580)
* release: prepare v1.32.0

* test: stabilize water capture re-entry
2026-07-22 04:14:15 +02:00
Marc-André Bombeck b00450aec8 fix(pwa): fix pull-to-refresh's at-top check and extend coverage
The authenticated shell's actual scroll container is <main
id="main-content"> (AuthShell sets it overflow-y-auto inside a
height-locked layout); document.scrollingElement never moves there, so
the gesture's "only arm at the top of the page" check was permanently
true regardless of how far a user had scrolled a long list. Point it
at the real scroll container instead, with the old document-root check
kept as a fallback for the few body-scrolled surfaces outside the
shell.

Extend pull-to-refresh from 4 pages (labs/checkups/mood/measurements)
to the dashboard, /insights, /medications, and the documents vault —
the sharpest PWA-resume-staleness surfaces, per the audit: a
background-refreshed dose or a cross-device write otherwise sits
stale until a full remount or the staleTime window lapses.
2026-07-18 03:52:32 +02:00
Marc-André Bombeck 1417137162 fix(daily-loop): stop bad ?doc= links from trapping the coach, add upload processing feedback
An invalid ?doc=<id> deep-link (a deleted document, a mistyped id) 404s
its detail fetch forever. The fenced send-gate never cleared, so every
message stayed blocked behind a phantom "untitled" attachment banner
that could never resolve — a bad link was a dead end instead of
degrading to a normal chat. Dropping a staged id the moment its detail
fetch settles into an error now clears the doc-scope, drops the banner,
and lets the coach send normally; a toast explains the link is gone.

Also gives the document vault processing feedback: the auto-index (and
optional AI summary) job that starts right after upload is
fire-and-forget server-side, so a freshly uploaded card sat with no
signal while it indexed. The vault list now polls while any recently
uploaded document is still unindexed (bounded to a few minutes so an
old, permanently-unindexed document never keeps it polling), and the
card shows a "Processing…"/"Ready" chip that clears on its own once the
index lands.
2026-07-18 00:37:24 +02:00
Marc-André Bombeck 4852421f05 chore(release): v1.28.38 — documents polish, waist reminders, bulk sharing
Document vault: card list drops a stray glyph and gives the real filename
its own line (was truncating on the meta row); the detail sheet's close
button aligns correctly — ResponsiveSheet reserved the right gutter for a
close button it wasn't rendering, now conditional on showCloseButton. The
open-in-coach action reads 'Coach fragen'.

Bulk share: a multi-selection in the overview shares several documents in
ONE link (documents-only privacy preserved, owner-scoped, up to 50 per
link; >50 refuses with a toast rather than silently truncating).

Reminders can link to WAIST_CIRCUMFERENCE (additive MeasurementReminderType
enum value, OpenAPI regenerated). The WHO-5 and SCI well-being reminder
labels were absent in all six locales and rendered their raw dotted keys
(the shared 'measurementReminders.types' prefix is what showed as the stray
'Measurement Reminders Types'); added in every language. A new
type-labels-i18n test asserts every dynamic reminder-type key resolves
non-empty in all six bundles — the static coverage guard cannot see the
dynamic key that let this through.
2026-07-16 00:33:20 +02:00
Marc-André Bombeck ee76672798 feat(documents): preview thumbnails in the vault overview
Render a small JPEG preview for each stored document and show it at the
card's leading edge, falling back to the kind icon when none exists.

- Generate previews in the background on upload (new document-thumbnail
  pg-boss queue) plus a boot-time corpus backfill for existing documents;
  upload never blocks on or fails because of it. Images are downscaled via
  @napi-rs/canvas, a PDF's first page is rasterised then downscaled, every
  other type is skipped. Generation never throws.
- Store the preview encrypted at rest in a 1:1 DocumentThumbnail side table
  (migration 0236) so the list/detail queries that omit the original blob
  never drag the preview either. Same AES-256-GCM string codec + fail-closed
  decrypt as the content index; wired into the key-rotation walk.
- Serve it through an owner-scoped GET /api/documents/inbound/[id]/thumbnail
  (auth + module gate + userId-scoped query + fail-closed decrypt), always
  image/jpeg + nosniff + private, no-store; 404 on miss. The card renders an
  authed lazy <img>, onError falls back to the icon.
- Add hasThumbnail to the document DTO so the card only fetches a preview
  that exists.

Also drop the "search looks inside your documents" caption from the overview
(the index-all prompt stays) and the per-card searchable marker; both were
noise. Remove the now-unused i18n keys across all six locales.
2026-07-10 01:15:32 +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 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 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 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 8f442c3d6c feat(documents): keyboard-first vault timeline
Roving tabindex over the card grid: arrows move (grid-aware by measured
column count), Home/End jump, Enter opens, Space selects, Delete removes
with the undo toast; focus rides the virtualizer window (scroll-to-row,
bounded retry focus). The container carries list semantics with a
translated label, and the transient upload queue stays a polite live
region. Selection state is conveyed by the per-card checkbox, which the
focus ring reveals for keyboard users.

Refs: document-vault plan W3-T3
2026-07-07 13:24:07 +02:00
Marc-André Bombeck aef2003d4c feat(documents): multi-select bulk actions on the vault timeline
Selection grows file-manager gestures — shift-click ranges (additive,
anchored on the last plain toggle) and press-and-hold on touch — and a
floating toolbar appears with the selected count and the bulk verbs:
change type, link condition, delete, clear. All verbs drive the bulk
endpoint in <=100-id chunks with merged per-id results; partial failures
surface as one counted toast, and a bulk delete gets one aggregate undo
that bulk-restores the succeeded rows (both sides no-op-safe by
contract). Escape clears the selection unless a dialog owns the key.

Refs: document-vault plan W3-T2
2026-07-07 13:21:16 +02:00
Marc-André Bombeck a8a733fc4b feat(documents): page-wide drop and clipboard paste feed the upload queue
Dragging files anywhere over /documents raises a dimmed full-page drop
overlay (Escape dismisses); dropping or pasting files/screenshots feeds
the same upload queue as the picker, pre-linked to the active condition
filter. The overlay is decorative (aria-hidden) — intake announces
through an sr-only live region. Copy translated in all six locales.

Refs: document-vault plan W3-T1
2026-07-07 13:15:54 +02:00
Marc-André Bombeck b8009d2e73 feat(documents): the vault surface
The page becomes one wide, calm filing-cabinet view: PageHeader with an
Upload action, a drop-target upload zone whose accept list and per-file
cap come from the usage endpoint (quota bar only above 80 %), a sticky
filter rail (200 ms debounced search focused by '/', nine type chips,
condition chips from linked episodes, a year segmenter, one-tap clear),
and the virtualized timeline below. Filter state lives in the URL so
every view is shareable and back-button-safe; uploads filtered to an
episode arrive pre-linked. Hovering a card prefetches detail metadata —
never the blob. Designed states: a skeleton grid that matches the loaded
layout, a teaching empty state, a distinct no-matches state, and a
retryable error card. /documents joins the shell's wide-container list;
the shell stays the single owner of the page frame. Locale bundles gain
the vault vocabulary in all six languages and drop the retired
extraction-review strings.

Plan-Task: W2-T1, W2-T4 (zone), W2-T5 (prefetch), W2-T6
2026-07-07 12:26:06 +02:00