loadUserByUsername now calls findByEmail and returns email as the
Spring Security principal name. Tests updated to assert email identity.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DocumentService.attachFile() now catches IOException internally and
re-throws as DomainException.internal — the IOException no longer leaks
through the service boundary
- DocumentController.attachFile() is now a plain delegate (no try/catch)
- ALLOWED_CONTENT_TYPES whitelist (PDF/JPEG/PNG/TIFF) is now enforced on
the attachFile endpoint, matching the existing quick-upload validation
- Added 5 DocumentService unit tests for attachFile (notFound, status
transition PLACEHOLDER→UPLOADED, no-change when already UPLOADED,
field assignment from upload result, IOException→DomainException)
- Added controller tests: 400 on disallowed content type, 404 on missing doc
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consistent with triggerManualSenderTraining — both defensive paths now use
DomainException.internal(OCR_TRAINING_CONFLICT) when the expected RUNNING row
is not found after creation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ensures the unexpected-state path produces a structured JSON error response
instead of an unmapped 500 RuntimeException. Adds OCR_TRAINING_CONFLICT
ErrorCode and mirrors it in the frontend errors.ts. Adds coverage tests for
getAllSenderModels() and runSenderTraining().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Controller was deciding when to fire runSenderTraining based on the returned run
status — a business rule that belongs in the service. Introduces @Lazy self-reference
to preserve @Async proxy dispatch without self-invocation bypassing Spring AOP.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add findByPersonIdIsNullOrderByCreatedAtDesc + findByPersonIdOrderByCreatedAtDesc to
OcrTrainingRunRepository. Add dto/TrainingHistoryResponse. Expose
GET /api/ocr/training-info/global and GET /api/ocr/training-info/{personId} on
OcrController, both requiring ADMIN; getSenderTrainingHistory guards person existence
via PersonService and returns 404 for unknown personId.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move TrainingInfoResponse from private nested record to dto/TrainingInfoResponse.java,
add senderModels field, inject SenderModelService into OcrTrainingService so personNames
covers all known senders rather than only recent-run participants.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add V42 partial unique index on ocr_training_runs(person_id) WHERE status='QUEUED'
to enforce the per-person queued coalescing guarantee at the DB level. Also adds
@ExtendWith(MockitoExtension.class) to SenderModelServiceTest for consistency with
the rest of the service test suite, with lenient() on the shared txTemplate stub.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the per-run getById loop with a single getAllById call on distinct
person IDs, eliminating the N+1 query when training history contains multiple
sender model runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the intermediate Map<String,Object> and return the typed record directly
so OpenAPI codegen produces a concrete TypeScript type. Fixes lastRun serializing
as {} (empty object) instead of null when no training run exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
OcrAsyncRunner now passes the per-sender model path to streamBlocks for
HANDWRITING_KURRENT documents. processDocument replaced extractBlocks
with streamBlocks + AtomicReference, removing the unchecked raw-array
pattern.
Also stages all previously uncommitted foundational files for this
feature: SenderModel entity, SenderModelRepository, Flyway migrations
V40/V41, updated OcrClient/RestClientOcrClient streaming API,
TrainingDataExportService.exportForSender, TranscriptionService Kurrent
hook, application.yaml OCR config, and frontend i18n/test additions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eliminates cross-domain repository access: OcrTrainingService no longer
holds SenderModelRepository. SenderModelService now owns the full sender
training lifecycle (runOrQueueSenderTraining, triggerSenderTraining,
promoteNextQueuedRun), removing the circular dependency risk.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The controller now builds the map inline (with personNames support).
This method had zero callers.
Fixes reviewer concerns from @felixbrandt and @mkeller.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces catch(Exception ignored){} with log.debug() in getTrainingInfo().
Adds controller test documenting the graceful degradation behavior
(response stays 200 when personService.getById() throws).
Fixes reviewer concerns from @felixbrandt and @nullx.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- OcrAsyncRunnerTest: switch from extractBlocks/4-arg streamBlocks stubs
to 5-arg streamBlocks (senderModelPath param) via doAnswer
- TranscriptionServiceTest: stub documentService.getDocumentById in
updateBlock tests so the new Kurrent training hook does not NPE
- OcrControllerTest: add @MockitoBean PersonService (now injected into
OcrController for personNames assembly in getTrainingInfo)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace stringly-typed "AND"/"OR" tagOperator with TagOperator enum (DocumentService, DocumentController)
- Replace Object[] with TagCount projection interface in TagRepository.findDocumentCountsPerTag()
- Use @NotNull + @Valid on MergeTagDTO.targetId; remove manual null check from TagController
- Correct ALLOWED_TAG_COLORS to match actual frontend CSS tokens (sage/sienna/amber/slate/violet/rose/cobalt/moss/sand/coral)
- Add TOCTOU comment to validateNoAncestorCycle() with mitigation explanation
- Add test: deleteWithDescendants_skipsDocTagDeletion_whenDescendantIdsIsEmpty
- Update TagServiceTest to use mock TagRepository.TagCount projection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Colors are stored only on root-level tags. DocumentService now calls
TagService.resolveEffectiveColors() before returning search results and
single-document responses, so child tags carry their parent's color when
serialised to JSON. Parent tags are batch-loaded in a single query.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- Replace hasTags(List<String>) spec with hasTags(List<Set<UUID>>, useOr)
- AND mode: one EXISTS subquery per expanded tag ID set; empty set = disjunction
- OR mode: union of all expanded sets into a single EXISTS subquery
- DocumentService calls tagService.expandTagNamesToDescendantIdSets() before building spec
- DocumentController exposes ?tagOp=AND|OR query param (default AND)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds parent_id FK (ON DELETE SET NULL), self-reference check constraint,
parent_id index, and nullable color column to the tag table.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Lesefertig pulse was removed from the UI; drop the backend support
for it too — removes the subquery from findWeeklyStats(), the projection
getter, the DTO field, and updates all affected tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The original needsExpert V37 migration was applied to the dev DB before
the feature was removed. Renaming our new indexes migration to V38 avoids
the Flyway checksum conflict. Regenerated api.ts now reflects the
@Schema(requiredMode=REQUIRED) annotations — DTO fields are non-optional.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>