Full-panel drop target that supports multi-file selection via drag-and-drop
or file picker. Fires onFilesAdded callback with the full File array.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Converts a raw filename into a human-readable title candidate by
stripping the extension and replacing underscore/hyphen runs with spaces.
Reuses the existing stripExtension() helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add DocumentBatchMetadataDTO (titles, senderId, receiverIds, documentDate, location, tags, metadataComplete)
- Add BATCH_TOO_LARGE to ErrorCode
- Extend quickUpload to accept optional @RequestPart("metadata"); dispatches to storeDocumentWithBatchMetadata when present
- Cap batch at 50 files/request; reject 400 when titles.size > files.size
- Add DocumentService.storeDocumentWithBatchMetadata applying shared fields + index-based titles to both created and updated docs
- Raise max-request-size to 500MB (10-file chunk at max per-file size)
- Add structured SLF4J logging for every quickUpload call
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
hover: on the <span> only fired on the 20×20px visual circle, not the
full 44×44px touch target. Add `group` + `focus-visible:ring-*` to the
outer button; switch to `group-hover:` on the inner span so the visual
response covers the entire interactive area.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- createEmptyDocument now uploads a minimal PDF so the Transkribieren
button is rendered (requires isPdf = true in DocumentTopBar)
- add 'Transcribe coach — with blocks' describe: seeds a block via API,
waits for blocks to settle in read mode, switches to edit, confirms
'Für Training vormerken' is visible
- fix dark-theme axe test: ThemeToggle uses aria-label 'dark mode',
not the previous /Farbmodus|theme/ regex
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The key was orphaned when TranscriptionEditView's empty state was replaced
by TranscribeCoachEmptyState. Removed from de/en/es to avoid accumulating
unreferenced strings. (Felix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
handleAuth in hooks.server.ts is in the sequence() chain and redirects
unauthenticated users at runtime regardless of prerender. Adding a comment
so the next reader doesn't mistake this for a security hole. (Markus/Nora)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap page content in <main> so AT users can jump to main content (Nora)
- Closing card "Fehlt eine Regel?" was <h2> after two existing <h2> siblings
but styled like a card title, not a section label; downgrade to <h3> to
fix the heading hierarchy (Sara/Leonie)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace style="grid-template-columns: 34px 1fr; align-items: start;"
with Tailwind grid-cols-[34px_1fr] items-start (Felix: inline styles)
- Add aria-label="Schritt N von 3" on each <li> so screen readers announce
step position when the numeric badge is aria-hidden (Nora/Sara)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Raw hex bypassed the token system and wouldn't remap in dark mode.
Now uses --color-parchment which has a proper dark-mode counterpart.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace one-shot $derived(.matches) snapshot with $state + addEventListener
so the static/animated branch reacts when the user toggles OS reduced-motion
at runtime (Felix: non-reactive media query)
- Replace bg-[#FAF8F1] raw hex with bg-parchment design token so the SVG
background remaps correctly in dark mode (Felix/Markus)
Also update TranscriptionPanelHeader.svelte.test.ts to expect role="region"
after the HelpPopover ARIA fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- role="tooltip" → role="region" + aria-label={label}: tooltip semantics
are wrong for a click-triggered panel (Nora/Sara)
- expand button to 44×44px with inner visual <span>: WCAG 2.5.8 touch
target for 60+ transcriber audience (Sara/Leonie)
- replace Math.random() with module-level counter: SSR/hydration mismatch
when server and client generate different IDs (Felix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds --c-parchment (#faf8f1 light / #041828 dark) to :root and both
dark-mode blocks, exposed as --color-parchment via @theme inline.
Prerequisite for replacing bg-[#FAF8F1] raw-hex in RichtlinienRuleCard
and TranscribeDragDemo.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New coach card replaces the icon+sentence empty state in the Transcribe
panel (edit mode). Three-step guide with 5-s SMIL drawing animation in
step 1 only. Animation freezes at the final frame when
prefers-reduced-motion is active. Footer links to Wikipedia Kurrent and
the Richtlinien page open in new tabs with visible '(öffnet in neuem Tab)'
annotations. 34 new i18n keys in de/en/es.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Final UI/UX spec for the /hilfe/transkription page referenced from
the Transcribe panel coach card. Card-grid layout with per-rule
Beispiel boxes, Wikipedia info-card, "Noch in Klärung" strip, and
closing invitation. Includes impl-ref tables, Paraglide keys for
de/en/es, print styles, and Gherkin acceptance criteria.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ConversationThumbnail still imported the `$lib/thumbnails` helper that
a02f6cdc deleted, so every SSR render of /briefwechsel crashed with
"Cannot find module '$lib/thumbnails'". Finish that refactor by reading
`doc.thumbnailUrl` straight off the Document DTO (same shape
DocumentThumbnail already uses), and update the spec fixtures to match.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Seeds 120 UPLOADED docs with a deterministic date spread and runs
DocumentService.searchDocuments against a Testcontainers Postgres, not
a Mockito mock. Five cases:
1. First page returns exactly page_size items + correct totalElements
2. Last partial page returns the tail slice (offset 100 → 20 items)
3. Page beyond last returns empty content, totalElements still 120
4. SENDER sort path slices in-memory + reports correct total
5. Different pages return disjoint document id sets
Closes the integration-coverage gap between the Mockito unit tests and
the full Spec→Pageable→Page→DTO path that unit tests can't exercise.
Runs in ~87 s against the shared Testcontainers instance. (#316)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
<a aria-disabled="true"> is the documented pattern but screen readers
still announce "Previous, link, disabled" on pagination bounds — noise
users don't need because the disabled state is purely visual. Switching
to <span aria-hidden="true"> removes the bound control from the AT tree
entirely (Leonie's recommendation). Visual parity preserved via a
disabledBase Tailwind class (same layout + cursor-not-allowed + opacity-40).
Tests updated: "disabled prev/next" assertions now check for aria-hidden
and no href — the active-state href/aria-current assertions are
unchanged. (#316)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
triggerSearch (local state, filter change) and buildPageHref (server data,
page nav) were each iterating over the same ~10 filter params. Any new
filter would have had to land in two places. buildSearchParams is now the
single source of truth for which params the /documents URL understands;
both callers just pass their snapshot and an optional targetPage. (#316)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PageRequest.of(0, 10_000) was inlined at ~12 sites across DocumentServiceTest
and DocumentServiceSortTest as an "effectively unpaged" sentinel for tests
that don't care about paging. Extracted to a named constant on each class
so the intent is visible at each callsite and we don't risk copy-paste
drift of the magic number. No behaviour change. (#316)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Frontend side of the /documents pagination work. The page.server.ts load
reads ?page= from the URL, forwards page+size=50 to the backend, and
exposes the new totalElements/pageNumber/pageSize/totalPages fields on
`data`. +page.svelte renders a <Pagination> component below the result
list; buildPageHref preserves every filter param and only updates page.
The existing triggerSearch debounce flow intentionally drops `page`
when any filter changes, so filter edits reset to page 0 automatically.
<Pagination> uses plain <a href> links (not goto) so SvelteKit's default
scroll restoration scrolls new pages to the top — the expected senior-UX
behaviour. Decorative chevrons wrapped in aria-hidden spans, 44px touch
targets, focus-visible ring, stacks vertically under 640px. The control
hides itself when totalPages ≤ 1.
Test coverage: 9 cases on Pagination (label, aria-current, prev/next
enable/disable, makeHref invocation, decorative chevron, touch target),
plus a filter-reset assertion on +page.svelte (page 5 → edit q →
goto URL must drop page=). Adds i18n keys in de/en/es. Manual edit to
api.ts pending a post-merge npm run generate:api against a rebuilt
dev backend. (#315)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds 5 dedicated controller cases — paging fields exposed on the JSON,
rejections for size>100 / size<1 / page<0 / page>100000, and a
captor assertion that the built PageRequest is forwarded to the service.
The size>100 case is the load-bearing guard on @Validated at
DocumentController — removing the annotation silently reopens the DoS
window this PR is meant to close.
Adds 5 service cases — fast path uses findAll(Spec, Pageable) (not Sort),
propagates page+size to the DB, carries totalElements/totalPages/
pageNumber/pageSize back on the result, and for SENDER sort slices in
memory and reports the pre-slice total. Page-beyond-last returns empty
content with a correct totalElements (JPA edge case). (#315)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fast path (DATE/TITLE/UPLOAD_DATE) pushes sort + paging into the DB via
findAll(Specification, PageRequest) and enriches only the returned slice
— 30× cheaper than enriching all 1500 matches when the user is only
going to see 50. In-memory sort paths (SENDER/RECEIVER/RELEVANCE) keep
their LEFT JOIN-friendly sort but now slice in-memory too, so enrichment
still runs against the page slice only.
Controller passes PageRequest.of(page, size) built from @RequestParam
values. Plan-level "add @Validated" prerequisite comes in the next commit.
All existing tests updated mechanically to pass a pageable argument
(PageRequest.of(0, 10_000) as an "effectively unpaged" sentinel). Stubs
that previously matched findAll(Specification, Sort) for the fast path
now match findAll(Specification, Pageable) with PageImpl<>.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rename `total` → `totalElements` for Spring-Page parity and add three new
required paging fields: pageNumber, pageSize, totalPages. Adds a `paged(
slice, pageable, totalElements)` factory alongside the existing single-page
`of(list)` shortcut. Enables offset pagination of /documents search (#315).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds two specs for extending issue #294 with bulk uploads:
- bulk-upload-concepts.html — three concepts (stack, split-panel
with file switcher, progressive accordion) with a decision
matrix and the Concept B recommendation.
- bulk-upload-split-panel-spec.html — refined final spec for
Concept B. Covers all three states (N=0 empty · N=1 single ·
N≥2 multi) across 320 / 375 / 768 / 1280 viewports in both
light and dark mode, using the real tokens from layout.css.
Includes impl-ref tables for every new surface, Paraglide keys
in de/en/es, component tree, and backend contract.
The polymorphic-state model means /documents/new is a single
route: N=1 is byte-identical to #294, N=0 shows a whole-panel
drop zone with bulk-first copy, N≥2 grows a file-switcher strip
under the PDF preview plus a two-card form split.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
h-16 w-16 looked undersized in the 180×252 strip container (~25% of
the height). h-24 w-24 gives ~38% visual weight, matching the ratio
DocumentThumbnail uses for its lg (120×168) fallback (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Prior coverage only exercised getThumbnailUrl() as a Java method call.
The new case serialises via ObjectMapper and asserts the resulting JSON
contains "thumbnailUrl":"..." so we catch silent breakages in the wire
contract (getter rename, @JsonIgnore, visibility drop) — not just
regressions in the method's return value (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Uses the same heroicon as DocumentThumbnail so the "no thumbnail yet"
signal reads identically across the app: one shape, one meaning. The
parchment SVG still lives on in the fully-empty state (no resume doc
at all), where it represents a different thing — we removed it only
from the "document exists, thumbnail not generated yet" branch (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the generic parchment SVG placeholder with an <img> pointing at
the backend's thumbnail endpoint when the document has one. The 180×252
container matches DocumentThumbnail's 5:7 A4 convention so the
dashboard tile sits visually next to the list/person-sublist tiles
instead of looking squatter than they do. dark:mix-blend-multiply keeps
paper scans from glaring on a dark page background (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The helper had a single consumer (DocumentThumbnail) and its only job
was to compose what the backend's Document.getThumbnailUrl() now
produces. Deleting it locks the single-source-of-truth invariant —
there is no longer a way to build a thumbnail URL on the client (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The backend now exposes thumbnailUrl as a serialised computed property
on Document, so the component drops its dependency on the frontend
URL-builder. PersonDocumentList's inline Doc prop type follows the
same shift (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Reflects the new @JsonProperty getter on Document. Kept as a minimal
manual edit rather than a full regen because the running dev backend
belongs to the main workspace and swapping JARs there would be a
side effect on a parallel worktree's state. `npm run generate:api`
will converge on the same shape.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DashboardService now reads the URL from the Document's computed getter
instead of passing null, so the resume strip can display the real
thumbnail of whatever the user was last working on (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@JsonProperty makes the computed getter part of every Document response
Jackson produces, so any DTO returning a Document automatically carries
the thumbnail URL without per-controller plumbing. The accompanying
comment warns future readers that the cache-buster is load-bearing
for the endpoint's `immutable` cache header (CWE-525) (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Matches the shape the frontend previously built via
encodeURIComponent(thumbnailGeneratedAt), so the backend is now the
single source of truth for the thumbnail URL convention (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The no-cache-buster branch covers documents whose thumbnail key is set
but whose thumbnailGeneratedAt is still null — which only happens in
the narrow window between the key being persisted and the async worker
stamping the timestamp (#309).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
First TDD step for centralising the thumbnail URL convention on the
Document entity (#309). Adds a stub getter returning null and a test
that locks the "no key → no URL" branch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR #311 dropped the right/left arrow icons that signalled whether a
letter was sent or received. Readers who don't decode the colored
left border (new users, color-blind users, users at a glance) had
no visual cue for direction. Restore a 20×20 arrow inline with the
title — right-arrow for outgoing, left-arrow for incoming — kept
decorative (aria-hidden) since the aria-label already announces
"Gesendet:" / "Empfangen:".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 168px-tall thumbnail tile was dominating rows where the text
column only rendered at text-xs / text-sm — visually the right
column sat half-empty. Three changes:
- Title: text-sm → text-lg
- Summary: text-sm → text-base
- Meta + tag chips: text-xs → text-sm
And remove the "vor N Jahren" chip entirely. The documentDate
in the meta row already carries the temporal context and the
chip was adding visual noise without new information.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Makes `max` an optional prop with default 3 — the common row-layout
case doesn't need to name the cap explicitly. ThumbnailRow's callsite
drops to `<TagChipList tags={doc.tags ?? []} />`, consistent with how
other shared components in $lib/components expose sensible defaults.
Refs #305
Fixes @leonievoss round-2 follow-up from PR review
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>