Commit Graph

1014 Commits

Author SHA1 Message Date
Marcel
007ec65dbd fix(viewer): move delete button inside annotation bounds to prevent edge clipping
Repositioning from top:-8px/right:-8px to top:4px/right:4px ensures the
44px touch target stays fully within the annotation shape. Annotations drawn
near the top or right edge of the PDF page no longer risk the button being
obscured or inaccessible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:37:17 +02:00
Marcel
e95a9312e8 test(viewer): verify delete button click does not bubble to onclick
Documents the stopPropagation guarantee: clicking the trash button must
not trigger the annotation's onclick (which opens the block detail panel)
while the delete confirm is in progress.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:36:51 +02:00
Marcel
f22596a29d fix(viewer): check res.ok on orphaned annotation DELETE to surface errors
Without the guard, a failed DELETE (4xx/5xx) was silently swallowed and
annotationReloadKey was incremented anyway, leaving the annotation visible
and the user with no feedback. Now matches the deleteBlock() pattern
immediately above.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:36:27 +02:00
Marcel
b13c10936b feat(viewer): show delete icon on annotation for direct block deletion (#339)
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m11s
CI / OCR Service Tests (push) Successful in 40s
CI / Backend Unit Tests (push) Failing after 3m4s
CI / Unit & Component Tests (pull_request) Failing after 3m7s
CI / OCR Service Tests (pull_request) Successful in 30s
CI / Backend Unit Tests (pull_request) Failing after 2m54s
Adds a trash icon button (44×44 px touch target) directly on each annotation shape in transcription mode so users can delete a block without navigating through the sidebar. Includes keyboard support (Delete key), confirm dialog via ConfirmService, prop-chain wiring through DocumentViewer → PdfViewer → AnnotationLayer → AnnotationShape, and orphaned-annotation fallback (calls DELETE /annotations/{id} when no block is linked). Backend security regression test added for deleteBlock 403 on READ_ALL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 21:00:50 +02:00
Marcel
5062513ae6 refactor(persons): extract inputCls/labelCls and PersonFormData type
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m20s
CI / OCR Service Tests (push) Successful in 38s
CI / Backend Unit Tests (push) Failing after 2m56s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
24d5381775 refactor(persons): rename page.server.test.ts to normalizePersonType.test.ts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
826283afcb test(persons): replace fragile CSS class tests with aria-checked behavior tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
1d5f99a2c8 a11y(persons): add aria-label to PersonTypeSelector radiogroup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
4c300da65e refactor(persons): remove what-comment from PersonCard title block
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
bccff232fe fix(persons): localize validation error messages via Paraglide i18n
validatePersonFields now returns a PersonValidationKey instead of a
hardcoded German string. resolveValidationMessage() translates the key
through Paraglide so English and Spanish locale users no longer see
German error text. Adds validation_last_name_required and
validation_first_name_required to all three message files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
327fd89cb9 refactor(persons): centralise PersonType, PERSON_TYPES and normalizePersonType in person-validation
Removes four independent PersonType type declarations and the duplicated
TYPES/PERSON_TYPES arrays. normalizePersonType moves from the edit route
module into the shared lib so page.server.test.ts no longer imports from a
route. Both server actions now use normalizePersonType for personType
extraction instead of an inline type cast.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
23861055d1 fix(persons): keyboard navigation now updates PersonTypeSelector reactive state
radioGroupNav now accepts an onChange callback; PersonTypeSelector passes
select() as the callback so ArrowLeft/Right navigation updates the hidden
input value. aria-live region starts empty and announces only on user
interaction (fixes initial page-load announcement).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
2ddeb485e3 test(persons): extract validatePersonFields and cover validation branches
- New src/lib/person-validation.ts exports validatePersonFields (pure function)
- 8 unit tests covering: valid PERSON, lastName missing/undefined,
  firstName missing/undefined for PERSON, non-PERSON types without firstName
- Both edit and new-person server actions now call the shared helper instead
  of inline if-chains, making the logic testable and non-duplicated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
1f19fa3462 refactor(persons): export normalizePersonType from edit server module
Tests now import from production code instead of a local copy, giving real
regression protection if the inline logic is changed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
45db75bdf2 fix(persons): use semantic color tokens in PersonTypeSelector for dark mode
Replaces hardcoded brand-navy/brand-sand/white classes with semantic
tokens (bg-primary/text-primary-fg, bg-surface/text-ink, border-line,
ring-focus-ring) so the segmented control adapts correctly in dark mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
8870cbe2fe feat(persons): show title in small-caps above display name in PersonCard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
b4cf7f1b21 feat(persons): add type selector + title + conditional fields to new-person form
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
d5587d1b95 feat(persons): extract personType + title in edit action; relax firstName for non-PERSON
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
7699a4e7e2 feat(persons): add type selector + title + conditional fields to edit form
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
110416d68b feat(persons): add PersonTypeSelector segmented control component
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
64fdc5b57e feat(i18n): add form_label_person_type, form_label_name, a11y_type_changed keys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
ac8d0d5796 feat(persons): normalize SKIP→UNKNOWN in edit-route load function
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
b8dcb2d3f4 feat(persons): add radioGroupNav action for keyboard navigation in type selector
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
d952fab4cd feat(persons): add INVALID_PERSON_TYPE error code with i18n translations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:37:34 +02:00
Marcel
18cad798fc fix(documents): preserve archiveBox + archiveFolder in markForReview; drop documentLocation
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m1s
CI / OCR Service Tests (push) Successful in 30s
CI / Backend Unit Tests (push) Failing after 2m53s
2026-04-25 20:25:08 +02:00
Marcel
0ddf43947b refactor(documents): drop documentLocation binding from edit layouts; wire archive fields 2026-04-25 20:23:37 +02:00
Marcel
45f7642f8d feat(documents): replace documentLocation with archiveBox/archiveFolder in edit form 2026-04-25 20:11:30 +02:00
Marcel
a91ee1f26d refactor(documents): unify count + action links into one row
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m7s
CI / OCR Service Tests (push) Successful in 32s
CI / Backend Unit Tests (push) Failing after 2m56s
Move result count, bulk-edit button, and new-document link into a shared
flex row so they appear on the same line. Adds an edit icon to the
bulk-edit button to visually match the existing plus icon on the add link.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 19:41:24 +02:00
Marcel
c59287fcfc fix(bulk-edit): cycle-3 polish — Felix C2/C3/C4/C5 + Sara coverage gaps
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m54s
CI / OCR Service Tests (pull_request) Successful in 39s
CI / Backend Unit Tests (pull_request) Failing after 2m56s
CI / Unit & Component Tests (push) Failing after 3m6s
CI / Backend Unit Tests (push) Failing after 2m56s
CI / OCR Service Tests (push) Successful in 34s
Felix C2 — `BatchMetadataRequest` controller now uses `@Valid` so future
@Size/etc. annotations on the record actually fire.

Felix C3 — Auto-clear `$effect` in `+layout.svelte` reads
`bulkSelectionStore.size` inside `untrack()` so the effect only re-fires on
route change, not on every checkbox toggle.

Felix C4 — `BulkDocumentEditLayout` edit-mode hydration loop now lives
inside `onMount` (not at top-level script) so the SvelteMap mutation is
unambiguously tied to instance lifecycle, matching the pattern used by
`WhoWhenSection`/`DescriptionSection` after the cycle-2 fix.

Felix C5 — Replaced fully-qualified `java.util.LinkedHashSet` in
`DocumentController` with a top-of-file import.

Sara coverage — six new spec files / blocks pin the cycle-1 and cycle-2
behaviours that were previously untested:
 - `WhoWhenSection.svelte.spec.ts` — onMount seeding from initialDateIso /
   initialLocation; doesn't stomp parent-bound dateIso; hideDate / editMode
   branch
 - `DescriptionSection.svelte.spec.ts` — onMount seeding from initialTitle /
   initialDocumentLocation; doesn't stomp parent-bound values; archive-box /
   archive-folder fields visible only in editMode
 - `BulkSelectionBar.svelte.spec.ts` — Esc-scope guard tests for `<dialog>`
   open and `aria-expanded` popover present
 - `BulkDocumentEditLayout.svelte.spec.ts` — topbar reads
   "Massenbearbeitung" + "werden bearbeitet" in edit mode (not the
   upload-flavoured "hochladen"/"werden erstellt" copy)
 - `DocumentControllerTest.patchBulk_returns400_whenArchiveBoxExceeds255Chars`
   — pins the @Size validator on archiveBox via the @Valid wiring

Refs #225, PR #331

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 19:18:56 +02:00
Marcel
8ce96294b0 fix(bulk-edit): cycle-2 blockers — restore initial-* props, missing import, scope Esc, edit-mode topbar
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m50s
CI / OCR Service Tests (pull_request) Successful in 27s
CI / Backend Unit Tests (pull_request) Failing after 2m54s
CI / Unit & Component Tests (push) Failing after 2m49s
CI / OCR Service Tests (push) Successful in 30s
CI / Backend Unit Tests (push) Failing after 2m55s
Felix B1 (data-loss regression on /documents/[id]/edit) — DocumentEditLayout
still passes initialDateIso, initialLocation, initialDocumentLocation, but
my cycle-1 cleanup removed those props. Result: existing values rendered
empty and a save would have overwritten them with "". Restored the props
on WhoWhenSection and DescriptionSection; initialisation now lives in
onMount so it runs exactly once and never stomps a parent-driven update on
a later prop change.

Felix B2 — `DescriptionSection.svelte:36` still had the top-level
`currentTitle = untrack(() => initialTitle)` mutation that I cleaned up in
WhoWhenSection but missed here. Same onMount-once treatment.

Leonie B5 — `enrich/+page.svelte:105` referenced `<BulkSelectionBar>` but
the import was lost in a prettier pass; svelte-check errored out and the
bar never rendered, leaving an 8 rem dead zone from the pb-32 reservation.
One-line fix: add the import.

Leonie B6 — Esc handler in `BulkSelectionBar` was unscoped and stole
Escape from NotificationBell, ConfirmDialog, HelpPopover, etc. (e.g.
selecting docs → opening notification bell → Esc would close the bell
AND silently wipe the selection). Now bails when an open dialog,
expanded menu, or popover is detected.

Elicit C1 — `BulkDocumentEditLayout` topbar now branches on `mode`:
shows "Massenbearbeitung" + "{count} werden bearbeitet" in edit mode
instead of the upload-flavoured "Mehrere Dokumente hochladen" + "werden
erstellt" copy. New i18n keys `bulk_edit_topbar_title` and
`bulk_edit_count_pill` in DE/EN/ES.

Tests added:
 - DocumentControllerTest.patchBulk_stripsCarriageReturnsAndNewlinesFromErrorMessages
   (Sara C2 follow-up — pin sanitizeForLog as a regression test)
 - BulkSelectionBar.spec — count=1 → "1 Dokument", count=2 → "2 Dokumente"
   (Sara C6 follow-up — pin the new bulk_edit_n_selected_one/_other branch)

Refs #225, PR #331

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 17:17:33 +02:00
Marcel
1803db86b5 test(bulk-edit): plug Sara's identified coverage gaps
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m1s
CI / OCR Service Tests (pull_request) Successful in 30s
CI / Backend Unit Tests (pull_request) Failing after 3m0s
CI / Unit & Component Tests (push) Failing after 2m59s
CI / OCR Service Tests (push) Successful in 37s
CI / Backend Unit Tests (push) Failing after 2m54s
- DocumentServiceTest.applyBulkEditToDocument_propagatesDomainException_whenSenderIdUnresolvable (Sara C1)
- DocumentServiceTest.findIdsForFilter_passesTagOperatorOR_throughBuildSearchSpec (Sara C3)
- bulkSelection.svelte.spec.ts: setAll([]) no-op + previous-IDs-absent + ids getter (Sara C4 + S4)
- /documents/bulk-edit/+page.server.ts now defensively handles a UserGroup
  with NULL `permissions` (treats it as not-WRITE_ALL instead of throwing
  on .includes()) + matching test (Sara C7)

233 backend tests + frontend bulk-edit specs all green.

Refs #225, PR #331

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:58:48 +02:00
Marcel
af8303dbf8 fix(bulk-edit): auto-clear selection store when leaving /documents and /enrich
Felix C4 — bulkSelectionStore is module-singleton; before this change it
silently followed the user from /documents to /persons / /admin / etc.,
then reappeared as a stale count when they wandered back. Root +layout.svelte
now watches page.url.pathname and clears the store the moment the user
leaves the two routes that surface BulkSelectionBar.

Refs #225, PR #331

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:49:07 +02:00
Marcel
7df00859c6 fix(bulk-edit): pluralization, edit-mode CTA, error UI, real loading state
Elicit C1+C3 — bulk-selection count uses ICU-style plural keys
(bulk_edit_n_selected_one / _other) so n=1 reads as "1 Dokument" instead
of "1 Dokumente". Save CTA in edit mode reads "Anwenden" via the existing
bulk_edit_save_button key; UploadSaveBar grew an editMode prop. Multi-
chunk progress text is now visible (not aria-only).

Felix C2 — bulk-edit page wires the backend error code through
parseBackendError + getErrorMessage instead of falling back to a generic
internal_error.

Felix C5 — editAllMatching no longer swallows fetch failures: the button
shows an inline error with the backend-mapped message (e.g. when the
filter cap is exceeded).

Leonie C8 — replace the literal "…" loading glyph on /documents/bulk-edit
with a spinner + role=status + aria-live=polite + visible "Loading
documents…" text.

Leonie C9 — partial-failure card and bulk-edit page error card now use
the design-system `text-danger` / `bg-danger/10` / `border-danger/40`
tokens (dark-mode safe) instead of raw red palette values.

Leonie C10 + C13 — German plural fixed; EN badges retensed
("+ added" → "+ will be added", "replaced" → "will replace") to match
the future-tense intent of DE/ES.

Refs #225, PR #331

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:46:58 +02:00
Marcel
156efe8b31 fix(bulk-edit): a11y + i18n hardening (Leonie blockers 1–4 + quick concerns)
B1 — i18n the archive-box / archive-folder labels and add helper text.
Karton/Mappe were hardcoded German and broke EN/ES locales (WCAG 3.1.2).

B2 — drop the hardcoded German aria-label on the onboarding callout.
role="note" + the visible localised text is self-describing; the redundant
label was overriding the translated content for AT users on EN/ES.

B3 — Escape clears the bulk selection while the bar is visible. Adds an
"Esc: Auswahl aufheben" hint visible at ≥ sm (WCAG 2.1.1).

B4 — /documents and /enrich reserve pb-32 when the bulk-selection bar is
visible so it doesn't occlude the last row or pagination (WCAG 1.4.10).

Folded in three Leonie quick-concerns:
 - C5: badge text-[10px] → text-[11px], raw text-gray-600 →
        design-token text-ink-2 (dark-mode safe)
 - C7: aria-live="polite" on bulk-selection-count
 - C11: "Alles aufheben" → "Auswahl aufheben" (DE/EN/ES) — disambiguates
        from "discard the operation entirely"

Refs #225, PR #331

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:35:40 +02:00
Marcel
499beca124 fix(bulk-edit): drop dead initial-* props and clear store on edit-mode discard
Felix B1 — `WhoWhenSection.svelte:37` and `DescriptionSection.svelte:42`
mutated $bindable props at top-level script scope, seeding them from
`initial*` companion props that no caller ever passes. The pattern stomps
parent-owned state in any future component re-evaluation.

Removed the dead initialDateIso / initialLocation / initialDocumentLocation
props and let the bindables carry their own initial value. dateDisplay and
currentTitle now seed from the bindable directly inside untrack — no
re-assignment required.

Elicit B2 — In edit mode the file map IS the user's bulk selection, so
discarding must clear bulkSelectionStore and bounce back to /documents,
otherwise the user is left on /documents/bulk-edit with an empty form
and a stale count in the bottom bar.

Refs #225, PR #331

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:29:44 +02:00
Marcel
2bb8fb8968 fix(bulk-edit): align BulkEditEntry shape with backend DocumentBatchSummary
Production bug — the backend serialises the document UUID as `id`, but
BulkEditEntry typed it as `documentId`. The runtime cast in /documents/
bulk-edit/+page.svelte was a TypeScript lie: every `entry.documentId`
became undefined, the SvelteMap collapsed all selections under the
undefined key, and the PATCH fired with `documentIds: []` (which the
controller correctly rejected with 400). Field semantics ACs could
therefore never fire end-to-end.

Renamed `BulkEditEntry.documentId` → `id`. The FileEntry built from each
summary still carries both `id` (local map key) and `documentId` (PATCH
payload) so the save handler is unchanged.

Reported by Elicit (B1) on PR #331.

Refs #225

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 16:14:53 +02:00
Marcel
f13f635161 test(bulk-edit): e2e coverage for selection bar and Massenbearbeitung flow
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m0s
CI / OCR Service Tests (push) Successful in 37s
CI / Backend Unit Tests (push) Failing after 2m53s
CI / Unit & Component Tests (pull_request) Failing after 3m0s
CI / OCR Service Tests (pull_request) Successful in 35s
CI / Backend Unit Tests (pull_request) Failing after 2m58s
Five Playwright scenarios on the bulk-edit feature:
 - sticky bar appears with count when checkboxes are toggled
 - Alles aufheben hides the bar
 - Massenbearbeitung navigates to /documents/bulk-edit and the edit-mode
   onboarding callout is rendered
 - direct navigation to /documents/bulk-edit with no selection redirects back
 - the same bar drives /enrich (skipped when the test DB has no incomplete docs)

Refs #225

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:30:18 +02:00
Marcel
6d3489d035 feat(bulk-edit): add /documents/bulk-edit route
Server load redirects READ_ALL-only users (or unauthenticated) to /documents.
Page load: onMount reads bulkSelectionStore — redirects to /documents when the
store is empty, otherwise POSTs the IDs to /api/documents/batch-metadata and
hands the resulting summaries to BulkDocumentEditLayout in mode="edit".

Refs #225

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:18:07 +02:00
Marcel
fa5dc43864 feat(bulk-edit): extend BulkDocumentEditLayout with mode="edit"
- New FieldLabelBadge component (additive / replace variants, WCAG AA contrast)
- WhoWhenSection: hideDate prop, editMode prop renders badges next to sender
  and receivers, hides the meta_location field
- DescriptionSection: editMode prop renders badges next to tags and archive
  fields; new bindable archiveBox / archiveFolder inputs only in editMode
- PersonTypeahead: optional badge prop forwards to FieldLabelBadge
- FileSwitcherStrip FileEntry: file is now optional, documentId added so
  edit-mode entries reference an existing document by UUID
- BulkDocumentEditLayout: mode prop branches drop zone / read-only title /
  callout / save handler. Edit save chunks 500 IDs per PATCH, stops on chunk
  failure with retry, marks per-document errors as chips, clears the bulk
  selection store on full success.

Refs #225

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:16:06 +02:00
Marcel
d4f32ed5d4 feat(bulk-edit): add BulkSelectionBar and Alle-X-editieren fast path
- BulkSelectionBar component: sticky bottom bar shown only when canWrite
  and selection is non-empty. Buttons meet WCAG 44px touch targets and
  iOS safe-area inset is honoured.
- Bar mounted on /documents and /enrich.
- Alle X editieren button on /documents replaces the selection with
  every UUID matching the active filter (via /api/documents/ids) and
  jumps to /documents/bulk-edit.

Refs #225

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:07:26 +02:00
Marcel
27e3d290e7 feat(bulk-edit): add canWrite-gated row checkboxes on /documents and /enrich
Each row in the document search list and the enrichment queue gets a
WCAG-compliant (44px touch target) checkbox bound to bulkSelectionStore.
Checkbox click does not trigger the row's stretched-link navigation —
it sits inside the z-10 content sibling, the link is in the z-0 sibling,
so click events do not bubble between them.

Refs #225

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 15:03:59 +02:00
Marcel
25446c9a5c feat(bulk-edit): add bulkSelection store backed by SvelteSet
Module-singleton live accumulator: selection persists across pagination
and route changes within /documents and /enrich. Cleared on successful
bulk save or via Alles aufheben.

Refs #225

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 14:54:59 +02:00
Marcel
660e34e016 feat(bulk-edit): add i18n keys, error mapping, and regenerate api types
- 14 new Paraglide keys in de/en/es for the bulk-edit UI strings (selection
  bar, callout, badges, save progress, retry, error)
- BULK_EDIT_TOO_MANY_IDS added to errors.ts type union and getErrorMessage()
- Regenerated api.ts now includes /api/documents/{bulk,batch-metadata,ids}
  and the DocumentBulkEditDTO / BulkEditResult / DocumentBatchSummary schemas

Refs #225

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 14:52:10 +02:00
Marcel
b690c74ddf fix(richtlinien): improve examples, copy, and Wikipedia link
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m57s
CI / OCR Service Tests (push) Successful in 34s
CI / Backend Unit Tests (push) Failing after 2m59s
- Rule cards now show before→after examples; strikethrough rule input
  renders with CSS line-through so the visual context is honest
- Illegible-words rule shows output only — can't represent unreadable
  text as readable characters
- Intro drops fictional family names in favour of "egal wer tippt"
- Wikipedia card copy is more direct; link uses icon instead of
  parenthetical "(öffnet in neuem Tab)" text

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 13:10:56 +02:00
Marcel
0797406f02 docs(bulk-upload): explain chunkSize=10 and 50-file cap constants
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m47s
CI / OCR Service Tests (push) Successful in 34s
CI / Backend Unit Tests (push) Failing after 2m56s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
c94d2cec03 feat(bulk-upload): guard discard-all with confirm dialog
Uses getConfirmService() (optional — null fallback when context is absent so
unit tests that don't exercise the discard path need no CONFIRM_KEY context)
and the new bulk_discard_confirm i18n key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
4da0bf71a0 fix(bulk-upload): add gradient overflow indicators to chip strip
Adds pointer-events-none left/right gradient fade overlays on the
FileSwitcherStrip track div so mouse-only users can see when more
chips are hidden beyond the visible area. The scrollbar is hidden
(scrollbar-width:none) so gradients are the only overflow signal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
da5d3c60b3 fix(bulk-upload): chip readability and focus management in FileSwitcherStrip
Chip label text increased from 11px to 12px (text-xs) and number badge
from 9px to 11px for the 60+ senior audience on laptops/tablets.

After removing a chip via the × button, focus moves to the previous chip
(falling back to the next chip when the first chip is removed) so keyboard
users are not stranded on <body>. Uses Svelte tick() to wait for DOM update.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
ed0d0bf331 fix(bulk-upload): handle network errors and partial upload success
save() now wraps each chunk fetch in try/catch — a thrown network error
marks all files in that chunk as errored. Also handles HTTP 200 responses
with a non-empty errors array (partial success): only the named filenames
are marked as errored rather than all files in the chunk. Navigation is
suppressed whenever any file fails.

Tests added:
- network error marks all chunk files as errored, no navigation
- HTTP 200 with errors array marks only affected files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00
Marcel
899508f9ca feat(bulk-upload): guard save() against concurrent invocations
Adds a saving $state flag that blocks re-entry while a chunk upload is
in flight. The UploadSaveBar save button is disabled via a new disabled
prop while saving is true. Tested: clicking Save twice fires fetch only
once.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 12:24:22 +02:00