Prevents LLM planning docs and Claude Code runtime files from being
accidentally committed to future branches.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
.claude/worktrees/agent-* and .claude/scheduled_tasks.lock are
Claude Code runtime files with no relationship to domain packaging.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These are LLM-generated planning documents for a different issue
(import pipeline work), unrelated to the domain packaging refactor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ExcelService was deleted in fa60c5be. Both the root and backend
CLAUDE.md still listed it under importing/ and in the services table.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Issue numbers in code comments rot as the codebase evolves. The why
(keeping real-database fidelity without pulling full service trees in)
is what matters, not the fix number.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TranscriptionService injected AnnotationRepository; AnnotationService injected
TranscriptionBlockRepository. Each side now talks through the other domain's
service:
- TranscriptionService.deleteByAnnotationId — new write delegation; called
from AnnotationService.deleteAnnotation in place of the foreign repo.
- AnnotationService.deleteById / deleteAllById — new write delegations; called
from TranscriptionService for cascading annotation cleanup.
- AnnotationService.findById (added in #417 commit 6) replaces the read.
- @Lazy on AnnotationService's TranscriptionService field breaks the
resulting two-bean cycle at construction time, mirroring the existing
@Lazy self-reference pattern in SenderModelService.
Refs #417 (C6.2 violations #10 and #11).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both services injected TranscriptionBlockRepository directly to read block
counts. They now go through TranscriptionBlockQueryService (count() and
countManualKurrentBlocksByPerson() added as 1-line delegations) — chosen over
TranscriptionService to avoid the existing
SenderModelService → TrainingDataExportService → TranscriptionBlockQueryService
chain reaching back into TranscriptionService and creating a cycle.
Refs #417 (C6.2 violations #8 and #9).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
SegmentationTrainingExportService and TrainingDataExportService each injected
TranscriptionBlockRepository, AnnotationRepository and DocumentRepository
directly. They now go through:
- TranscriptionBlockQueryService (extended) for the three eligible-block
queries — used over TranscriptionService to keep
SenderModelService → TrainingDataExportService → TranscriptionService cycle-free.
- AnnotationService.findById (new) — read API on the annotation domain.
- DocumentService.findById (already added in #417 commit 3).
The TrainingDataExportServiceTest @DataJpaTest delegates the new service reads
to the real JPA repositories via Mockito stubs in the new makeService helper,
so the integration coverage stays unchanged.
Refs #417 (C6.2 violations #6 and #7).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MassImportService injected DocumentRepository for the find-or-create pattern
during ODS/Excel import. Move the two repository touchpoints (findByOriginalFilename,
save) onto DocumentService as 1-line delegations and update the consumer.
Refs #417 (C6.2 violation #1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
TranscriptionQueueService injected DocumentRepository to fetch the four queue
projections. Move the four read methods (findSegmentationQueue,
findTranscriptionQueue, findReadyToReadQueue, findWeeklyStats) onto
DocumentService as 1-line delegations and update the consumer.
Refs #417 (C6.2 violation #5).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The Thumbnail trio (ThumbnailService, ThumbnailBackfillService,
ThumbnailAsyncRunner) all injected DocumentRepository directly. They now go
through three new DocumentService delegations:
- findById(UUID): Optional<Document> — no-throw variant for the runner's
log-and-skip behaviour on missing documents.
- findForThumbnailBackfill() — wraps the existing
findByFilePathIsNotNullAndThumbnailKeyIsNull query.
- updateThumbnailMetadata(Document) — wraps save() for the post-thumbnail
entity update.
DocumentService also gains @Lazy on its existing ThumbnailAsyncRunner field
to break the new DocumentService ↔ ThumbnailAsyncRunner cycle. lombok.config
adds @Lazy to copyableAnnotations so the field annotation reaches the
generated constructor parameter.
Refs #417 (C6.2 violations #2, #3, #4).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- PasswordResetService injects UserService instead of AppUserRepository.
- New UserService.findByEmailOptional preserves the silent-fail behaviour of
the old findByEmail-returning-Optional path; the existing throwing
findByEmail is unchanged.
- New PasswordResetService.findLatestActiveTokenForEmail exposes the latest
active reset token without leaking the repository upward.
- New @Profile("e2e") PasswordResetTestHelper wraps that read so the
AuthE2EController no longer touches PasswordResetTokenRepository directly.
Profile guard moves from the controller-only annotation to also cover the
helper bean, so the production graph never instantiates either.
Refs #417 (C6.1 violation #2 + C6.2 violation #12).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
StatsController previously injected PersonRepository and DocumentRepository
directly, violating the controller→service→repository layering rule. Move the
two count() calls into a thin StatsService that delegates to PersonService.count
and DocumentService.count. While here, add the missing @RequirePermission(READ_ALL)
flagged by AUDIT-2 §7 — anonymous callers were able to read aggregate document/
person counts.
Refs #417 (C6.1 violation #1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Aligns the auth-account table name with the AppUser entity. The historical
mismatch (table 'users' alongside table 'persons') misled schema-first readers
into assuming the two were related; renaming to 'app_users' makes the
deliberate split between auth accounts and historical persons explicit at the
schema layer.
Scope: the table itself, the users_groups join table, and the three FK columns
whose name was literally 'user_id'. Semantic FK columns (audit_log.actor_id,
notifications.recipient_id, document_versions.editor_id, etc.) keep their
names — the role they describe is the documentation, not the type.
Closes#418. Unblocks #407 (REFACTOR-1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move `transcription_block_placeholder contains @ mention trigger` out of
`describe('PersonMentionEditor — placeholder behavior')` into a new
`describe('PersonMentionEditor — i18n message content')` block so each
describe group has a single, clear responsibility.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract repeated `new java.util.HashSet<>(Set.of(TrainingLabel.KURRENT_RECOGNITION))`
into a `kurrentLabels()` helper in TrainingBlockQueryTest and add `import java.util.HashSet`.
Add clarifying comments on the two person-scoped queries in TranscriptionBlockRepository
explaining that they use `MEMBER OF d.trainingLabels` — aligned with the pre-existing
`findEligibleKurrentBlocks()` pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the generic "Type text here..." placeholder in TranscriptionBlock
with copy that teaches the @Name trigger inline (Leonie Voss design review,
issue #370). No new DOM, no new i18n keys — just the three existing
`transcription_block_placeholder` strings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
scriptType is only set after OCR runs, which can't happen before we have
a trained model. Both sender-based queries now filter on the training label
instead, consistent with findEligibleKurrentBlocks.
Also adds missing test coverage for findManualKurrentBlocksByPerson and
countManualKurrentBlocksByPerson (4 cases + count parity check).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The multi-person filter e2e previously typed 'a' then 'b' into the
typeahead and trusted the dev seed to contain matching names.
If the seed ever changes, the test would silently degrade — both
calls might resolve to the same row, or the listbox might never
populate.
Refactor to use a single broadly-occurring probe vowel ('e') and
extract person ids straight from the listbox option DOM (the option
id encodes the person id as `${listboxId}-option-${personId}`).
For the second pick, iterate options and select the first whose
id differs from the first selection. The test now only depends on
the seed having ≥2 distinct persons whose name contains 'e' — a
much weaker, more durable assumption — and asserts on the URL
params with full equality instead of toHaveLength + first-element
spot checks.
Addresses Sara's iteration-3 concern #4 on PR #382.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Senior-author persona requires 44px minimum touch targets on every
interactive control. The /geschichten filter row had three pills
(All / chip / + Person wählen) at h-9 (36px), missing the rule that
the toolbar already follows. Bumped all three to h-11.
Test added in page.svelte.spec.ts asserts the className contains
h-11 on every pill variant.
Addresses Leonie's iteration-3 concern #6 on PR #382.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The story rows on the person detail page now match the
PersonDocumentList pattern: the entire row is a single anchor with a
hover background, and the title gets group-hover:underline. Author,
date, and body excerpt are all part of the same clickable area, so
the touch target matches the visual rhythm of the document panels
above.
Adds a Playwright flow that picks two persons through the typeahead,
asserts both ?personId= params end up in the URL with two chips on
screen, then removes the first chip and verifies only the second
person id remains.
Also extends .prettierignore so a stale root-owned test-results
directory left over from running tests inside Docker doesn't break
the pre-commit lint hook.
The /geschichten list page now renders one removable chip per active
person filter and lets users add more via the existing typeahead. The
URL uses repeated ?personId= params (matching the documents tag
filter), which the regenerated API client passes straight through to
the backend's new array-bound endpoint. New translation keys cover the
chip remove aria-label, the AND hint shown while picking, and the
multi-person empty state.
GET /api/geschichten now accepts repeated personId query params and
returns only stories that mention every person supplied. Refactors the
list path to a JPA Specification chain (one EXISTS subquery per id,
mirroring DocumentSpecifications.hasTags) and embeds the
COALESCE(publishedAt, updatedAt) DESC ordering inside the spec so a
single repository.findAll covers all filter combinations.
Three e2e tests against the real stack:
- admin can navigate to /geschichten, create a draft, publish, and see the
story appear on the index
- a reader (or admin) can click a story card and reach the detail page
with an <article> landmark visible
- AxeBuilder scan of /geschichten reports no serious or critical WCAG
violations
Partial fix for Sara's review B1 on PR #382. The deeper 5-spec a11y suite
and visual-regression coverage are deferred to a follow-up issue.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
10 browser-based component tests:
- title-empty disables both DRAFT save buttons
- inline title-required error appears after blur
- DRAFT mode renders "Entwurf speichern" + "Veröffentlichen"
- PUBLISHED mode renders "Speichern" + "Zurück zu Entwurf"
- initialPersons / initialDocuments props render as chips on first paint
- title input is populated from a geschichte prop
- "Entwurf speichern" passes trimmed title + status=DRAFT to onSubmit
- "Veröffentlichen" passes status=PUBLISHED
- personIds / documentIds from initial props flow through onSubmit
Closes Felix's review B1 on PR #382.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Browser-based component spec asserting:
- empty geschichten → no <section> rendered
- >= 1 story → heading + story link visible
- canWrite=false → no "+ Geschichte schreiben" link
- canWrite=true → link with /geschichten/new?personId pre-fill
- 0–2 stories → no footer link
- 3+ stories → "Alle Geschichten zu {name}" footer link to /geschichten?personId
- excerpt is plain text (no <strong>, no <script>)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>