Commit Graph
1387 Commits
Author SHA1 Message Date
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 a887a03c01 Merge branch 'feat/document-share-entrypoint' into release/v1.27.28 2026-07-08 20:34:45 +02:00
Marc-André Bombeck 1305e8d4bc docs(documents): correct the detail-sheet header comment for the Share action 2026-07-08 09:56:15 +02:00
Marc-André Bombeck 46c85e8671 test(documents): share entry point seeds the create flow with the document
Unit: the shared ShareLinkCreateForm mounts bare (Settings default) and
seeded (document flow) — a pre-attached document rides in as a chip with
the label pre-filled. e2e: the document detail Share action opens the
create flow with that document pre-attached and reaches the one-time QR
reveal; marked slow (two stacked sheets + a real create).
2026-07-08 09:50:26 +02:00
Marc-André Bombeck d4009a3587 feat(documents): share action on the document detail sheet
Extract the clinician share-link create flow (create form, document
picker mount, one-time QR + passphrase reveal) from the Settings sharing
section into a reusable ShareLinkCreateForm. Settings mounts it unchanged
alongside the active/revoked lists; the document detail sheet adds a
Share action beside Download that opens the same flow in a bottom sheet
with the current document pre-attached and its title pre-filled as the
label. The one-time QR reveal lands in that sheet, scannable on mobile.

No backend change: the shared POST /api/share-links handles the attached
document set as before.
2026-07-08 09:41:47 +02:00
Marc-André Bombeck abb2db37ee test(dashboard-hero): cover hero-ring-order default + reorder; i18n hint
Add resolveHeroRingOrder unit coverage (default health-score-first,
reconcile against selected set, drop/append, dedupe) plus resolver/serializer
round-trip; hero SSR tests for the default lead + a persisted custom order +
dropped-when-unrendered; Settings picker tests for the pinned health-score
anchor row, its locked-on switch, drag/arrow reorder, and a persisted
non-leading order. Extend the score-rings picker description across six
locales.
2026-07-08 01:23:58 +02:00
Marc-André Bombeck abe688caba feat(dashboard-hero): health-score-first default + reorderable hero rings
Add a persisted hero ring ORDER over the health-score anchor ring plus the
selected score rings. Defaults to the health-score ring on the leading edge;
the user can reorder the full set (health score included) from Settings →
Darstellung with the existing drag-handle + arrow-button pair.

The order rides the dashboard-layout blob (dashboardWidgetsJson) — no
migration. resolveHeroRingOrder reconciles a stored order against the current
ring set on every read/serialize, so a deselected ring drops and a
newly-selected ring appends; the hero orders only the rings it actually
renders.
2026-07-08 01:18:00 +02:00
Marc-André Bombeck 50b628abb8 Merge branch 'feat/hero-and-wellbeing-polish' into release/v1.27.26 2026-07-08 00:42:33 +02:00
Marc-André Bombeck 96860c69ab Merge branch 'feat/documents-filter-dropdown' into release/v1.27.25 2026-07-08 00:19:29 +02:00
Marc-André Bombeck 4b50ac8b6a feat(documents): make filter dropdowns responsive and update the e2e flows
Give each facet trigger a leading icon and show the text label from sm up
(and on phones only while the facet is active, truncated), so three facets
plus the search never overflow a 360-430px row — the bar stays one line
without a horizontal scroller. Drop the now-dead filter.groupLabel key and
point the vault e2e flows at the dropdown menu items instead of the removed
inline chips.
2026-07-08 00:03:04 +02:00
Marc-André Bombeck 7cd317f1a5 feat(dashboard,mental-health): tighten hero gap, link hero rings, move wellbeing attribution to a header info popover
- Dashboard hero: pull the briefing closer to the ring row (md:gap-3 -> md:gap-2); mobile spacing unchanged.
- Each hero ring is now a focusable link to its natural detail: readiness/recovery/sleep -> their score-anatomy page, doses -> the medications surface, health score -> the Insights overview. Real anchors keep tap-vs-drag native on the mobile carousel.
- Mental-wellbeing: shorten the page title to the short noun form (matches the nav/module label) across all six locales.
- Instrument cards: move the required attribution/licence line off the space under the Start button to a discreet info control at the top-right of the header; a popover carries the verbatim citation. Start now stands alone.
2026-07-07 23:34:58 +02:00
Marc-André Bombeck 71698f3248 feat(documents): replace inline type-chip row with compact dropdown filters
The vault filter bar rendered the type/condition/year facets as a
horizontally-scrolling chip set beside the search field, which wrapped to a
second line on phones. Replace it with three compact dropdown facets — type
(multi-select), condition and year (single-select) — that share one
non-wrapping row with the flexing search and the clear control at every
width. The content-search hint keeps its own helper line below.
2026-07-07 23:29:24 +02:00
Marc-André Bombeck 77f7caf4bf Merge branch 'feat/vault-p3-qa' into release/v1.27.24 2026-07-07 23:04:20 +02:00
Marc-André Bombeck 9298e7ca9f Merge branch 'feat/hero-ring-carousel' into release/v1.27.23 2026-07-07 21:56:00 +02:00
Marc-André Bombeck 829345a57b fix(sharing): keep the document cap literal client-side (no DB import leak) 2026-07-07 21:25:17 +02:00
Marc-André Bombeck e734c161f4 test(documents): clinician doc list + share picker/QR + i18n x6 2026-07-07 21:19:58 +02:00
Marc-André Bombeck fdc0f83a47 feat(documents): share document picker, clinician document list, mobile QR 2026-07-07 21:15:19 +02:00
Marc-André Bombeck ee98a0c738 feat(dashboard): swipeable one-ring carousel on mobile hero, tighter desktop drop
Below md the hero wellness rings no longer wrap into a multi-row grid;
a dependency-free CSS scroll-snap track shows one ring centred at a time
with dot indicators, native touch/keyboard swipe, and reduced-motion
respected. At md and up the same nodes revert to the inline
right-aligned row unchanged. Desktop tightens the greeting/ring-row drop
to the briefing one step (md:gap-3).
2026-07-07 20:53:40 +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 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 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 154d1cfebe Merge branch 'fix/hero-polish' into release/v1.27.21 2026-07-07 17:32:27 +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 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 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 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 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 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 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 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 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 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
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 016b83da1d style: prettier pass over the chart-loader touchpoints
Formatting-only: the chart-runtime import repointing left five files
with non-canonical line breaks.
2026-07-07 13:21:36 +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 8a4521156b fix(dashboard): give the hero rings and sections room to breathe
On desktop the four rings sat 12px apart and the briefing section
followed the greeting row at the intra-block distance — the band read
top-heavy against its own symmetric padding. Rings now space at 24px
on md+ and the section gap steps up to match the card inset, so the
head zone and the briefing zone separate at the same rhythm the
padding sets.
2026-07-07 13:19:18 +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 9f2a3e25fe perf(i18n): generate the catalog boot scripts as static assets
The dynamic /i18n/[locale] route handler answered with Next's default
Cache-Control (private, max-age=0, must-revalidate) and no validator —
its immutable-cache header was overridden, so a non-PWA browser would
re-download the full catalog on every hard load. Static public/ files
keep an ETag (repeat loads revalidate to a 304) and the service
worker's cache-first rule covers the offline relaunch; the ?v= build
key the layout appends busts both per deploy.

scripts/generate-i18n-boot.mjs writes public/i18n/<locale>.js for all
six locales as a prebuild/predev step (the sw-version.js pattern);
output is gitignored. The route handler is removed.

Measured on a production build, authenticated DE session: dashboard
document 522 KB raw / 139 KB gz -> 170 KB raw / 24.6 KB gz; the DE
catalog moves into a one-time 119 KB gz cached asset.

H2
2026-07-07 13:15:37 +02:00
Marc-André Bombeck f9ca7fef79 fix(proxy): allow same-origin framing of the document serve route
The blanket X-Frame-Options: DENY + frame-ancestors 'none' refused the
detail preview's same-origin <iframe>, and the middleware page CSP
clobbered whatever the serve route set — the inline PDF preview rendered
as a permanent skeleton. Carve out exactly the /original path:
SAMEORIGIN framing, frame-ancestors 'self', and a document CSP of
default-src 'none'. Deliberately NO sandbox directive and no sandbox
attribute on the preview iframe — Chromium force-downloads PDFs in
sandboxed documents instead of rendering them (verified against a real
Chromium channel; headless test builds ship no PDF viewer at all). The
load-bearing boundaries for user-uploaded bytes are unchanged:
magic-byte classification at upload, true Content-Type + nosniff at
serve, HTML/SVG denied entirely, attachment-only posture for every
non-passive format. Tests pin the carve-out and that every neighbouring
path keeps the DENY posture.

Plan-Task: W2-T3 (preview integration)
2026-07-07 12:57:12 +02:00
Marc-André Bombeck e24db8a889 perf(i18n): stop shipping two full catalogs on every page load
Two independent every-load costs, one per layer:

1. The static EN import in load-locale.ts put the full en.json catalog
   (334 KB raw / ~106 KB gz) into the client JS of EVERY route, for
   every user in every language. No catalog is statically imported by
   client code anymore: the EN fallback floor of the t() chain loads
   lazily on the first actual key miss (the locale-integrity guard
   keeps every locale key-complete, so the miss path is drift-only),
   and the labs biomarker canonical-name lookup fetches EN on demand.

2. The root layout serialized the ENTIRE active-locale catalog into the
   RSC flight payload of every document (392 KB of a 505 KB dashboard
   HTML, re-downloaded on every hard load, uncacheable). The handoff is
   now a versioned, immutable-cacheable boot script
   (/i18n/<locale>?v=<build>, new route handler) that assigns
   self.__HL_I18N before the hydration scripts run, plus a server-side
   cache seed in load-locale.ts behind a typeof-window branch that
   Turbopack compiles out of the browser bundle (verified: zero catalog
   bytes in any client chunk, zero route manifests referencing one).
   SSR text stays correct and the first client render holds the same
   bundle the server rendered with — no EN flash, no hydration drift.

The boot script is public (proxy allowlist: the login page needs it
pre-auth; the body is the same catalog JSON that ships in the public
repo) and cache-first in the service worker, so an offline relaunch
finds it next to the cached shell.

Measured (client-reference manifests, gzip): route JS baseline
/ 391->287 KB gz, /insights 377->273, /measurements 379->275,
/insights/mood 394->290 — the ~104 KB gz EN catalog left every route.

H2
2026-07-07 12:34:26 +02:00
Marc-André Bombeck e260b2d5c4 perf(charts): one shared async boundary for the whole recharts surface
Every next/dynamic chart import used to point at its own module, so
Turbopack emitted a separate chunk group per boundary — the production
build carried EIGHT ~312 KB copies of the recharts module graph, and
/insights (the worst-LCP route) downloaded two of them (~92 KB gz of
pure duplication, one fully unused at load).

All chart loaders now resolve the single chart-runtime barrel
(src/components/charts/chart-runtime.ts), which owns the only path to
recharts: one shared chunk group, recharts exists exactly once in the
build (verified: 8 -> 1 recharts-fingerprinted chunks), cached across
routes. Charts stay recharts and stay pixel-identical; every call site
keeps its own matching skeleton and ssr:false flag.

Riders of the same rule:
- the mood-trio barrel (mood-charts.ts, the v1.16.7 precedent this
  generalises) folds into the runtime and is removed
- the vitals sparkline body extracts to delta-sparkline.tsx and loads
  through the runtime; the tile keeps the fixed 40 px band, so the
  async gap is an identically-sized empty box
- /cycle's BBT chart (the last static recharts import on a live route)
  defers through the runtime with a card-matching skeleton

H3
2026-07-07 12:32:43 +02:00
Marc-André Bombeck 2b7a6544d6 feat(documents): retire the v1.25 extraction-review surface
The parked inbound-documents view (upload form + staged-fact review UI)
and its helpers leave the tree; /documents renders only the vault. The
extraction routes stay dark server-side with no UI entry points, per the
release plan. Orphaned i18n keys left with the locale rewrite in the
previous commit; the reverse-coverage guard pins the result.

Plan-Task: W2-T7
2026-07-07 12:26:07 +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