Commit Graph
13 Commits
Author SHA1 Message Date
Marc-André Bombeck d0677adac9 perf(bundle): keep zod out of the documents and Coach client routes
Both routes reached the schema library through a constant: the list of
document kinds, the list and bulk limits, the attachment cap, the Coach
data clusters and preference defaults, the conversation title length,
the scope source list and the report leaf order. Each sat in a schema
module, so importing the number shipped the library. The constants now
live in zod-free modules (lib/documents/inbound-constants,
lib/ai/coach/prefs-constants, lib/ai/coach/scope-sources,
lib/report-selection/catalogue) and the schema modules re-export them
for server code; the Coach page checks its scope parameter against the
closed list instead of parsing it. A guard walks each route's static
client graph with the TypeScript parser and fails on a zod import.

Two guards follow earlier commits in this branch: the fetch-limit
allowlist names the shared document picker sheet, and the card density
test states a floor on the cards it finds.
2026-10-20 14:04:06 +02:00
Marc-André Bombeck 56daa57350 refactor(format): one byte formatter for every surface
The document vault and the admin area each carried a formatBytes; the
same backup read 1.25 MB in one and 1.2 MB in the other, and only the
vault knew gigabytes. The vault rule (one decimal below 100, up to GB,
digits in the reader's locale) now lives in src/lib/format/bytes.ts and
takes the locale, so the server-rendered share page uses it too. A test
holds the definition to that one module.
2026-10-20 12:55:06 +02:00
Marc-André Bombeck 415da581f8 fix(client): bound the staged-attachment poll and list every client poll
The Coach polled a staged document every 2.5 s until its content index
existed, which for a format that is never indexed meant for as long as the
Coach stayed open. It now stops with the same upload processing window the
documents list uses. Every client poll, its interval and its stop
condition is listed beside the dashboard cadence constant.
2026-10-20 06:49:23 +02:00
Marc-André Bombeck 94542e7f2d fix(i18n): drop dot and dash separators from visible strings
Every bundle value that chained facts with " · " now reads as a phrase,
a comma list or a parenthesis, in all seven locales, and the em-dash
separators in the day, documents, correlation, sharing, environment,
vaccination and Apple Health import strings become sentences or clauses.

The day view's running list names the record first, with the dose in
the name and a life event's category as a chip, and puts a quiet day
count below: "Day 4, since 31 December" instead of a dot chain. Document
meta, location periods, vaccination rows and mood crosstab rows drop
their composed dots too.

A guard walks every bundle and fails on any " · " outside a reasoned
allowlist, and on an allowlist entry that no longer needs its place.
2026-10-09 13:37:14 +02:00
Marc-André Bombeck 4273af231e feat(documents): choose how the vault is laid out, and confirm every delete
The wrench beside Upload opens Settings, Appearance, Documents, the way
the illness journal offers its display options: preview cards or a
compact list, and months stacked as blocks or flowing across the full
width with the month name inline above the document that opens it. The
choice is stored per account in `users.documents_layout_json` behind
GET/PUT /api/documents/inbound/layout (preserve-when-absent, module
gated, the read delegable like the rest of the vault), carried in the
account-settings backup and cleared by the data wipe.

Deleting from the vault now always asks first. The bulk bar's Delete and
a focused card's Delete key both go through one confirmation on the page,
and the detail sheet's Delete opens its own; before, all three deleted on
the spot with only an undo toast behind them.

The selection bar is two tight lines: the count with an X at its right
edge, then one action row with Delete alone at the right in the
destructive style. Below 1024 px the verbs are icon-only at the 44 px tap
floor, so the row stays one line at 360 px. The bar carries the
selection-bar slot the Coach launcher already steps aside for.
2026-10-08 16:45:06 +02:00
Marc-André Bombeck 0219a85e92 feat(documents): filter the archive by procedure or surgery
The vault's filter row gains a procedure facet beside the condition
facet. It lists the record's procedures and surgeries that hold at least
one live document, headed the way the visits page heads them (the
reason, else the practice, else the kind) with the body site and date
beneath, and narrows the list through the existing `encounterId` filter.
Same control, placement and clear behaviour as the condition facet; the
choice rides the URL as `?encounter=`, and an upload dropped into the
filtered view is filed against the procedure.

The usage read publishes the choices as `linkedProcedures` (additive),
decrypting only the label columns, and returns none when a sharing
grant does not cover the visits section.

On a phone an active facet now gives up label width before the search
field does, and the search keeps room for its icon and padding.
2026-10-04 21:59:22 +02:00
Marc-André Bombeck cfddf097c7 fix(documents): file an undated document under the reader's upload day
The archive grouped a document without a stated date by the UTC day
of its upload, so an upload late on the last evening of a month west
of UTC opened next month's section (and early on the first morning
east of UTC, last month's). The upload day is now read in the
profile time zone for the month sections and the year filter.
2026-10-03 18:47: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 e4a4c996b7 fix(documents): stop Backspace deleting a document, and show the key that does
A focused card deleted on Backspace as well as Delete. The timeline's
roving tabindex gives a card focus the moment someone tabs into the
grid, Backspace is the reflex "go back" key, and a card carries no
delete control at all — so the reflex press removed a medical document
with nothing but a transient undo toast behind it.

Delete alone now deletes. The key resolution moves into a pure helper
in vault-utils so the contract is testable without a render harness,
and the card stops keeping that contract in a code comment: the overlay
button carries aria-keyshortcuts, and a kbd chip appears while the card
holds focus. The label is per-locale (Entf / Suppr / Supr / Canc).
2026-07-27 14:47:56 +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 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 37e9d933e2 feat(documents): vault list contract on the client
URL-backed facets (q / kind / episode / year) parse and serialise through
one pure helper pair so deep links round-trip byte-stable; the API search
builder pins the timeline sort. Month bucketing + row chunking feed the
virtualized grid as a flat item list. The upload-response parser owns the
duplicate contract: HTTP 200 with envelope-level meta.duplicate returns
the existing row, every 413/415 reason maps to a translated-copy class,
and proxy-stripped bodies fall back to the HTTP status. Query keys move
to the vault filter shape and gain the usage read.

Plan-Task: W2-T1 (URL state), W2-T5 (keys)
2026-07-07 12:25:45 +02:00