Commit Graph

121 Commits

Author SHA1 Message Date
Marcel
c10e8e8a3a fix(tests): replace flaky waitFor with synchronous dispatchEvent in edit-page delete spec
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m54s
CI / OCR Service Tests (pull_request) Successful in 32s
CI / Backend Unit Tests (pull_request) Failing after 3m18s
CI / Unit & Component Tests (push) Failing after 3m51s
CI / OCR Service Tests (push) Successful in 47s
CI / Backend Unit Tests (push) Failing after 3m19s
The Playwright CDP click latency occasionally pushed past vi.waitFor's 1000ms
deadline, making the "opens a confirm dialog" test flaky. Switched to
btn.dispatchEvent(new MouseEvent(...)) — the same synchronous in-browser pattern
already used in GeschichteEditor.svelte.spec.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 13:37:13 +02:00
Marcel
6ab7abb9df fix(tests): fix 3 pre-existing vitest-browser spec failures
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m41s
CI / OCR Service Tests (push) Successful in 43s
CI / Backend Unit Tests (push) Failing after 3m30s
CI / Unit & Component Tests (pull_request) Failing after 3m32s
CI / OCR Service Tests (pull_request) Successful in 40s
CI / Backend Unit Tests (pull_request) Failing after 3m17s
Three distinct root causes:

1. hilfe/transkription: Wikipedia link test was checking .textContent but
   the accessible text had moved to aria-label in a prior commit.

2. documents/[id]/edit: vi.spyOn on a Svelte 5 compiled .svelte.ts service
   object does not reliably track calls in vitest-browser mode; replaced
   with a plain closure-based mock.

3. GeschichteEditor: TipTap's onMount steals focus and its ProseMirror
   view interferes with Playwright CDP event dispatch. Three workarounds:
   - blur: dispatchEvent(new FocusEvent('blur')) bypasses focus-state check
   - save buttons: dispatchEvent(new MouseEvent('click')) from in-browser JS
     context reliably triggers Svelte 5 onclick vs. Playwright CDP click
   - trailing-space fill: input.value + dispatchEvent('input') works where
     userEvent.fill('value ') silently fails to update bind:value

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 11:27:24 +02:00
Marcel
567612761d refactor: move lib-root files to lib/shared/ and finalize domain structure
- Move api.server.ts, errors.ts, types.ts, utils.ts, relativeTime.ts to lib/shared/
- Move person relationship components to lib/person/relationship/
- Move Stammbaum components to lib/person/genealogy/
- Move HelpPopover to lib/shared/primitives/
- Update all import paths across routes, specs, and lib files
- Update vi.mock() paths in server-project test files
- Remove now-empty legacy directories (components/, hooks/, server/, etc.)
- Update vite.config.ts coverage include paths for new structure
- Update frontend/CLAUDE.md to reflect domain-based lib/ layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:53:31 +02:00
Marcel
efcc347c00 refactor: move shared components to lib/shared/ sub-packages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:40:14 +02:00
Marcel
d6db7a07bd refactor: move shared utilities to lib/shared/ sub-packages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:35:15 +02:00
Marcel
d5d36e661a refactor: move person domain components and utils to lib/person/
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:26:21 +02:00
Marcel
1e656d2db4 refactor: move document transcription, annotation, viewer sub-packages
- transcription/: TranscriptionBlock, Column, EditView, PanelHeader, ReadView,
  Section + transcriptionMarkers, blockConflictMerge, saveBlockWithConflictRetry
  + useBlockAutoSave, useBlockDragDrop hooks
- annotation/: AnnotationLayer, AnnotationShape, AnnotationEditOverlay
- viewer/: PdfViewer, PdfControls + useFileLoader, usePdfRenderer hooks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 14:01:39 +02:00
Marcel
e7f8aa5894 refactor: move document domain core to lib/document/
Moves ~25 components, utils (search, filename, groupDocuments,
documentStatusLabel, validateFile), bulkSelection store, and
TranscriptionSection sub-component. Fixes broken relative imports.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 13:56:36 +02:00
Marcel
ed270f68e1 feat(geschichten): wire discovery integrations on Person and Document pages
Person detail (/persons/[id]):
- Server load fetches GET /api/geschichten?status=PUBLISHED&personId={id}
  in parallel with the existing person/document queries.
- Renders <GeschichtenCard> below the received-documents list when the
  person has at least one published story.

Document detail (/documents/[id]):
- Server load adds the same parallel call with documentId={id}.
- DocumentTopBar gains geschichten + canBlogWrite props that flow through
  to DocumentMetadataDrawer.
- DocumentMetadataDrawer's grid expands to lg:grid-cols-4 when the
  Geschichten column should appear (stories exist OR user can author),
  and shows "+ Geschichte anhängen" / "Alle anzeigen" links following the
  >= 3-story threshold from issue comment #5758.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-02 18:01:19 +02:00
Marcel
db66d0cc61 fix(document-page): add .catch() to task deep-link tick promise
Some checks failed
CI / OCR Service Tests (push) Successful in 34s
CI / Backend Unit Tests (push) Failing after 3m7s
CI / Unit & Component Tests (push) Failing after 3m22s
Addresses @felix — tick().then() had no error handler; console.error
is now logged on failure, matching the existing deep-link scroll pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:38:05 +02:00
Marcel
7dc5dc6f71 feat(document-page): auto-open transcription panel when ?task=transcribe is present
On mount, reads the task query param before the comment deep-link handler.
When task=transcribe, opens the transcription panel, scrolls the close button
into view, moves focus to it, then strips the param from the URL via replaceState.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 21:38:05 +02:00
Marcel
ba73387d50 refactor(transcription): extract saveBlockWithConflictRetry into a util
Tester #5506 §2 + Markus #5504 §2: the 409 orchestration was inline in
+page.svelte and untested. Extract into a pure module that takes the
fetch function as a dependency, so the full happy path / 409 path / 500
path / refetch-fails path / UUID-guard path can be unit-tested with
mock Responses. The route file now reads as 12 lines: call the helper,
on conflict apply the merged snapshot to local state, re-throw.

BlockConflictResolvedError now carries the merged block on its
`merged` property so callers don't have to redo the refetch.

6 new unit tests cover every branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 01:20:49 +02:00
Marcel
43aacd9f60 fix(transcription): UUID-guard saveBlock path interpolation
Sina #5505 concern 1: doc.id and blockId are server-trusted today, but
the path-interpolation pattern is repeated three times across the route
and the autosave hook. Validate both ids against the standard UUID
regex before any fetch fires so a future feature taking user-supplied
ids cannot silently introduce a path-injection vector.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 01:09:52 +02:00
Marcel
fd3a44d10c refactor(transcription): typed BlockConflictResolvedError instead of prose throw
Felix #3: the 409 path was throwing a human-prose Error which read like
an i18n string that escaped translation. Replace with a named class
carrying code='CONFLICT_RESOLVED' so callers can branch on intent and
future error reporters can map the structured code instead of grepping
strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 01:05:47 +02:00
Marcel
64a61f705c feat(transcription): handle 409 rename-mid-edit conflict on block save (B12b)
When PersonService renames a person while a transcriber is editing a
block that mentions them, the block-save endpoint returns 409 (carrying
the new ErrorCode.PERSON_RENAME_CONFLICT from PR-A). saveBlock now:

1. Refetches the latest server snapshot of the block.
2. Calls mergeBlockOnConflict to combine: server's mentionedPersons
   (post-rename displayNames win) + transcriber's unsaved text + any
   local-only mentions added since the last save.
3. Updates the local block state with the merged result.
4. Re-throws so the autosave indicator surfaces the conflict and the
   pending payload is preserved for retry (B12).

The merge logic is a pure function so it can be unit-tested in
isolation and reused for any future conflict-resolution scenarios.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:35:27 +02:00
Marcel
02d3e2ab61 feat(transcription): swap plain textarea for PersonMentionEditor and thread mentionedPersons through autosave
- TranscriptionBlockData now carries mentionedPersons (matches backend
  schema added in PR-A).
- useBlockAutoSave.saveFn signature widens to (blockId, text, mentions);
  pendingMentions is tracked alongside pendingTexts and is preserved on
  failure so a retry resends the in-flight payload (B12).
- TranscriptionBlock.svelte renders <PersonMentionEditor>, exposing the
  textarea node back through a captureTextarea callback so the existing
  quote-selection feature still works.
- saveBlock in routes/documents/[id]/+page.svelte forwards mentions on
  PUT.
- flushOnUnload sends mentions in the keepalive payload too.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 00:32:09 +02:00
Marcel
b658a13247 feat(stammbaum): show inferred relationship in the document drawer
- New presentational RelationshipBadge component (labelFromA → arrow →
  labelFromB) wired into DocumentMetadataDrawer's Personen column,
  rendered after the receivers block when both endpoints are family
  members.
- DocumentTopBar gains an optional inferredRelationship prop and
  passes it through.
- documents/[id]/+page.server.ts loads the badge: only when sender is
  a family member, exactly one receiver, and that receiver is also a
  family member; 404 (no path) → null.
- relationshipLabels.ts maps the backend label keys (parent/child/...)
  to localised strings, so the server load returns badge-ready strings.

Refs #358.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 19:32:17 +02:00
f6cf2e0e42 feat(transcription): add "Alle als fertig markieren" bulk action (#345) (#352)
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m15s
CI / OCR Service Tests (push) Successful in 51s
CI / Backend Unit Tests (push) Failing after 3m13s
## Summary

Implements the bulk "Alle als fertig markieren" action for the transcription panel requested in #345.

### Backend

- Added `PUT /api/documents/{documentId}/transcription-blocks/review-all` endpoint to `TranscriptionBlockController`, guarded with `@RequirePermission(Permission.WRITE_ALL)`
- Added `markAllBlocksReviewed(UUID documentId, UUID userId)` to `TranscriptionService` — `@Transactional`, single DB round-trip via `blockRepository.saveAll()`, emits one `BLOCK_REVIEWED` audit event per previously-unreviewed block
- Returns full updated block list (same shape as `listBlocks`) for a clean frontend update pass
- 5 new `TranscriptionServiceTest` unit tests (idempotency, audit events, empty document)
- 5 new `TranscriptionBlockControllerTest` `@WebMvcTest` tests (401, 403, 200 happy path, 200 empty, 401 user not found)
- All 68 backend tests pass

### Frontend

- Added `onMarkAllReviewed?: () => Promise<void>` prop to `TranscriptionEditView` (optional, consistent with `onTriggerOcr` pattern)
- Button placed in sticky progress header, right-aligned next to `reviewedCount / totalCount geprüft`
- Button is **disabled** (not hidden) when all blocks are already reviewed — `title="Alle Blöcke sind bereits als fertig markiert"` (Decision 1)
- Loading spinner replaces checkmark icon during operation — always shown (Decision 4, no threshold)
- Handler `markAllReviewed()` added to `documents/[id]/+page.svelte`, wired as `onMarkAllReviewed`
- 5 new `TranscriptionEditView.svelte.spec.ts` Vitest Browser component tests; all 25 tests pass

### Decisions applied

| # | Question | Choice |
|---|---|---|
| 1 | Button when all reviewed | **Disabled** with `title` tooltip |
| 2 | Audit log | **N individual BLOCK_REVIEWED events** (one per unreviewed block) |
| 3 | Atomicity | **All-or-nothing** via `@Transactional` |
| 4 | Loading indicator | **Always show** during operation |

Closes #345

Co-authored-by: Marcel <marcel@familienarchiv>
Reviewed-on: http://heim-nas:3005/marcel/familienarchiv/pulls/352
2026-04-28 08:34:26 +02:00
Marcel
163e99016a 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:56:37 +02:00
Marcel
d6f3ca5c43 feat(viewer): show delete icon on annotation for direct block deletion (#339)
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:56:37 +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
367dcc66f2 refactor(nav): add class prop to BackButton, remove mb-4 from topbar contexts
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m41s
CI / OCR Service Tests (push) Successful in 37s
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 2m44s
CI / OCR Service Tests (pull_request) Successful in 32s
CI / Backend Unit Tests (pull_request) Failing after 2m51s
- BackButton now accepts a `class` prop (default 'mb-4') so callers can
  override spacing; resolves hardcoded margin in flex-row topbar snippets
- documents/[id]/edit and enrich/[id] pass class="" to suppress the margin
- Replace weak className unit test with class-prop behaviour tests
- Add [data-hydrated] comment in E2E spec explaining what emits the attribute

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 11:16:49 +02:00
Marcel
cc9c47254b refactor(nav): replace static back-link hrefs with BackButton
All 7 in-scope back navigation links converted to use history.back().
Admin panel mobile chevron converted inline (icon-only, different
visual pattern). Cancel buttons left as static <a> links.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 10:49:23 +02:00
Marcel
b07f9efa9c fix(document-detail): force edit panel on notification deep-link
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m39s
CI / OCR Service Tests (push) Successful in 30s
CI / Backend Unit Tests (push) Failing after 2m46s
Comments only render inside TranscriptionEditView, so a deep-link
into a document with existing reviewed transcriptions landed the
user in read mode with no comment element in the DOM — the scroll
target silently missed.

scrollToCommentFromQuery now takes a setPanelMode callback and calls
it with 'edit' whenever both query params are present. The page's
own transcribe-mode $effect checks a skipInitialPanelMode flag the
deep-link flow sets, so its default-panel-mode logic doesn't race
against the explicit override.

Two new helper tests pin the contract: panel mode is forced to
'edit' both when transcribe mode is off (entering fresh) and when
it is already on (same-page notification click).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:22:38 +02:00
Marcel
3bf0b38c42 polish(document-detail): address review concerns on onMount deep-link wiring
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m40s
CI / OCR Service Tests (pull_request) Successful in 30s
CI / Backend Unit Tests (pull_request) Failing after 2m44s
CI / Unit & Component Tests (push) Failing after 2m41s
CI / OCR Service Tests (push) Successful in 37s
CI / Backend Unit Tests (push) Failing after 2m55s
Three small refinements from Felix's review cycle 1:

- replaceState(page.url.pathname, page.state ?? {}) — defend against
  first-navigation cases where page.state can be undefined.
- Extract the inline tick + requestAnimationFrame into a named
  waitForPanelRender() helper; intent is now readable from onMount.
- Attach .catch() to the fire-and-forget scrollToCommentFromQuery
  promise so any helper throw surfaces via console.error instead
  of silently disappearing.

No behavior change on the happy path. All existing tests stay green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 14:10:46 +02:00
Marcel
e22265f5bc feat(document-detail): wire notification deep-link scroll in onMount
After navHeight setup, call scrollToCommentFromQuery with the page
URL and callbacks into the component's local state (transcribeMode,
activeAnnotationId, flashAnnotationId) plus SvelteKit's replaceState
to strip the consumed query params.

afterTick awaits both Svelte's tick() and one requestAnimationFrame,
mirroring the existing handleAnnotationClick timing so the annotation
panel has rendered before scrollIntoView fires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:42:55 +02:00
Marcel
208c1adc3e test(edit): add tests for handleDelete on the edit page
Covers: button present, confirm dialog opens, form submitted on confirm,
form not submitted on cancel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 23:36:31 +02:00
Marcel
fcc4c4665c feat(edit): unify edit page with enrich split-panel layout
Extract DocumentEditLayout shared component for the PDF+form split-panel
UI, replacing the old scrolling layout on /documents/[id]/edit with the
same fixed-panel structure used by /enrich/[id]. Removes TranscriptionSection
and FileSectionEdit from the edit page; file upload/replace is now handled
by the shared layout. Delete SaveBar and FileSectionEdit as dead code.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-18 23:36:31 +02:00
Marcel
e8e54cc282 feat(#221): change TagInput binding to Tag[], add color dots and hierarchy grouping
Backend:
- TagRepository: add findDescendantIdsByName() recursive CTE query
- TagService: add expandTagNamesToDescendantIdSets() for document search

Frontend:
- TagInput: accept Tag[] (id, name, color, parentId) instead of string[]
- Chips show color dot via var(--c-tag-{color}) when tag has color
- Suggestions grouped hierarchically: children indented under their parents
- Update DescriptionSection, edit/new pages, SearchFilterBar, +page.svelte

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 16:11:38 +02:00
Marcel
34e7436fdc refactor(fileloader): extract createFileLoader hook from document/enrich pages
Move blob URL lifecycle management into a reusable createFileLoader()
hook that owns revoke-before-create and revoke-on-destroy. Replace
identical inline logic in documents/[id] and enrich/[id] with the hook.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 13:20:32 +02:00
Marcel
dbf7f0bc16 fix(fileloader): revoke blob URLs before re-assignment and on destroy
Calling loadFile a second time previously leaked the previous object URL.
Add URL.revokeObjectURL(fileUrl) before creating a new one and in
onDestroy so all URLs are freed. Revoke behavior will be covered by the
useFileLoader hook tests in the next commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 13:13:21 +02:00
Marcel
ee58b63517 feat(ocr): add guided OCR mode using existing annotation regions
When a document has manually drawn annotation boxes, the user can now
enable "Nur annotierte Bereiche" in the OCR trigger panel. The engine
skips layout detection entirely and runs recognition only within the
pre-drawn bounding boxes, preserving manual transcription blocks.

- Python: adds OcrRegion model, extend OcrRequest/OcrBlock; guided
  branch in /ocr/stream groups by page and crops each region
- Engines: add extract_region_text() to both Kraken and Surya
- Java: adds OcrBlockResult.annotationId, OcrClient.OcrRegion,
  TriggerOcrDTO.useExistingAnnotations; OcrAsyncRunner dispatches to
  upsertGuidedBlock when annotationId is present; OcrService threads
  the flag through to runSingleDocument
- TranscriptionService: adds upsertGuidedBlock (creates, updates OCR,
  or preserves MANUAL blocks)
- Frontend: guided OCR toggle in OcrTrigger shown when blocks exist;
  skips destructive-replace confirmation in guided mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 15:57:54 +02:00
Marcel
fdf1eb92ad feat(training): add document-level training enrollment
- V29 migration: document_training_labels join table
- TrainingLabel enum: KURRENT_RECOGNITION, KURRENT_SEGMENTATION
- Document.trainingLabels @ElementCollection
- DocumentService.addTrainingLabel / removeTrainingLabel
- PATCH /api/documents/{id}/training-labels (WRITE_ALL)
- Auto-enroll on Kurrent OCR trigger (OcrService.startOcr)
- TranscriptionEditView: enrollment chips in panel footer
- JPQL queries updated to use MEMBER OF trainingLabels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 14:30:51 +02:00
Marcel
6b94882409 fix(ocr): remove redundant page counter from progress display
The progress message already says "Seite 3 von 7 wird analysiert…"
so the separate "3 / 7" counter was redundant. Remove the
OcrProgressBar from the page and inline only the skipped-pages
warning.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:50:05 +02:00
Marcel
bac67706b9 feat(ocr): integrate progress bar and streaming progress into document page
Replace inline translateOcrProgress with the extracted module. Add
OcrProgressBar below the spinner during OCR. Parse page numbers from
ANALYZING_PAGE progress codes and feed them to the bar. On Done, fill
bar to 100% briefly before clearing the overlay.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 10:15:55 +02:00
Marcel
d8dcba1a71 fix(ocr): unblock event loop during OCR and show errors in UI
OCR engines are CPU-bound and were blocking Uvicorn's single async
event loop, making /health unresponsive during processing. This caused
new OCR requests to fail silently (health check failure → no DB record
→ UI shows NONE). Wrap engine calls in asyncio.to_thread() to keep the
event loop free. Also surface OCR trigger errors in the frontend
instead of silently resetting the spinner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:50:39 +02:00
Marcel
ef11e4af09 fix(ocr): disable manual annotation drawing while OCR is running
Some checks failed
CI / Unit & Component Tests (push) Failing after 1s
CI / Backend Unit Tests (push) Failing after 1s
CI / Unit & Component Tests (pull_request) Failing after 1s
CI / Backend Unit Tests (pull_request) Failing after 1s
Prevents users from drawing annotations that would be cleared when
the OCR job finishes. transcribeMode is set to false for the PDF
viewer while ocrRunning is true.

Refs #226

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 23:32:55 +02:00
Marcel
971527a50e feat(ocr): show translated progress messages during OCR processing
Some checks failed
CI / Unit & Component Tests (push) Failing after 2s
CI / Backend Unit Tests (push) Failing after 1s
CI / Unit & Component Tests (pull_request) Failing after 2s
CI / Backend Unit Tests (pull_request) Failing after 1s
Backend sends progress codes (PREPARING, LOADING, ANALYZING,
CREATING_BLOCKS:N, DONE:N, ERROR) via OcrJob.progressMessage.
Frontend translates them via Paraglide (de/en/es) and displays
below the spinner.

- V27 migration: adds progress_message column to ocr_jobs
- OcrAsyncRunner updates progress at each phase
- Poll interval reduced to 2s for snappier updates

Refs #226

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 23:31:23 +02:00
Marcel
c1befd3fa3 fix(ocr): resume polling on page reload + track single-doc job status
Some checks failed
CI / Unit & Component Tests (push) Failing after 1s
CI / Backend Unit Tests (push) Failing after 1s
CI / Unit & Component Tests (pull_request) Failing after 0s
CI / Backend Unit Tests (pull_request) Failing after 1s
Single-document OCR now creates an OcrJobDocument row so
GET /api/documents/{id}/ocr-status can find running jobs.
OcrAsyncRunner updates the job document status (RUNNING → DONE/FAILED).

Frontend checks OCR status when entering transcription mode —
if a job is running, resumes polling and shows the spinner.

Refs #226

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 23:16:59 +02:00
Marcel
dd175d09e2 refactor(ocr): make single-document OCR async, fix circular dependency
Some checks failed
CI / Unit & Component Tests (push) Failing after 1s
CI / Backend Unit Tests (push) Failing after 1s
CI / Unit & Component Tests (pull_request) Failing after 1s
CI / Backend Unit Tests (pull_request) Failing after 1s
OcrService → OcrAsyncRunner was circular. Fixed by moving all OCR
processing logic (processDocument, clearExistingBlocks, createBlocks)
into OcrAsyncRunner. OcrService is now a thin entry point that
validates, creates the job, and dispatches to OcrAsyncRunner.

Architecture:
- OcrService: validates document, checks health, creates OcrJob, delegates
- OcrAsyncRunner: @Async processDocument + runSingleDocument + runBatch
- OcrBatchService: creates job + job documents, delegates to OcrAsyncRunner
- No circular dependencies

Single-document OCR is now async (returns jobId immediately).
Frontend polls GET /api/ocr/jobs/{jobId} every 3s until DONE/FAILED.

816 backend tests pass, 687 frontend tests pass.

Refs #226

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 22:55:52 +02:00
Marcel
f6667e0e15 feat(frontend): show OcrProgress during OCR job + check status on load
Some checks failed
CI / Unit & Component Tests (push) Failing after 1s
CI / Backend Unit Tests (push) Failing after 2s
CI / Unit & Component Tests (pull_request) Failing after 1s
CI / Backend Unit Tests (pull_request) Failing after 1s
- triggerOcr captures jobId from POST response and shows OcrProgress
- OcrProgress rendered in the transcription panel when ocrJobId is set
- handleOcrDone reloads blocks and annotations when OCR completes
- checkOcrStatus called when entering transcription mode — resumes
  progress display if a job is already running for this document

Refs #226

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 22:09:24 +02:00
Marcel
8dc9243add feat(frontend): wire OCR trigger + review toggle into transcription panel
Some checks failed
CI / Unit & Component Tests (push) Failing after 1s
CI / Backend Unit Tests (push) Failing after 1s
CI / Unit & Component Tests (pull_request) Failing after 1s
CI / Backend Unit Tests (pull_request) Failing after 1s
- OcrTrigger component rendered in the transcription empty state when
  the document has a file and user has write permission
- Review checkmark toggle on each TranscriptionBlock (turquoise when
  reviewed, muted outline when not). Calls PUT .../review to toggle.
- TranscriptionBlockData type: added source + reviewed fields
- +page.svelte: triggerOcr() and reviewToggle() functions wired up
- Paraglide translations (de/en/es) for review toggle + reviewed count

All 687 frontend tests pass.

Refs #226, #230

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 22:02:56 +02:00
Marcel
498679234a refactor(docs): replace inline confirmDelete toggle with ConfirmService in SaveBar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-12 14:12:01 +02:00
Marcel
f11d8a38ed feat(frontend): replace all name concatenation with displayName
- Add displayName default method to PersonSummaryDTO
- Update native SQL queries to include title, person_type columns
- Add getInitials() utility to personFormat.ts
- Update abbreviateName/abbreviateCompact for nullable firstName
- Replace firstName+lastName concatenation with displayName in all
  person-displaying components and server load files
- Regenerate API types with displayName on Person and PersonSummaryDTO

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 12:22:30 +02:00
Marcel
b5ec4ebc0c refactor(ui): rename shadowed m parameter to newMode
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 1s
CI / Backend Unit Tests (pull_request) Failing after 3s
CI / Unit & Component Tests (push) Failing after 3s
CI / Backend Unit Tests (push) Failing after 2s
Avoids shadowing the Paraglide m import in the onModeChange callback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:58:54 +02:00
Marcel
4d5b8b4ead feat(ui): add collapsible PDF strip and abbreviated labels on mobile
Some checks failed
CI / Unit & Component Tests (push) Failing after 2s
CI / Backend Unit Tests (push) Failing after 2s
CI / Unit & Component Tests (pull_request) Failing after 3s
CI / Backend Unit Tests (pull_request) Failing after 1s
PDF viewer collapses to 70px on mobile in read mode, expandable to
50vh. Toggle button with chevron. Paragraph tap auto-expands strip.
Mode toggle abbreviates to "Bearb." on small screens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:30:36 +02:00
Marcel
10cecb01f5 feat(a11y): respect prefers-reduced-motion for scroll-sync
Uses scrollIntoView behavior 'instant' instead of 'smooth', skips
CSS animations (static highlight instead), and extends timeout to
2s for reduced-motion users.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:27:01 +02:00
Marcel
81b14e5026 feat(ui): add bidirectional scroll-sync with flash animations
Paragraph click flashes the PDF annotation outline (1.5s fade).
Annotation click highlights the paragraph with a background flash.
Both directions scroll the target into view.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:25:23 +02:00
Marcel
e089192d7a feat(ui): wire panelMode state with read/edit view switching
Adds TranscriptionPanelHeader and TranscriptionReadView to the
document detail page. Default mode is 'read' when blocks exist,
'edit' otherwise. Annotations dimmed in read mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 11:21:15 +02:00
Marcel
f359c19e4c fix: bump comment text to text-base + reload annotations on block delete
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
Comment text:
- Body and quote bumped from text-sm (14px) to text-base (16px)
  to visually match the font-sans author name at text-sm

Annotation reload on delete:
- Add annotationReloadKey prop through DocumentViewer → PdfViewer
- Increment key after block delete in +page.svelte
- PdfViewer reloads annotations when key changes
- Annotation rectangle disappears immediately, not just after refresh

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-05 23:40:23 +02:00