PersonController GET /api/persons?sort=documentCount&size=N returns the top N
persons by combined sender+receiver document count for the reader dashboard.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GeschichteService.list() now applies hasAuthor(currentUser()) whenever
status == DRAFT, so BLOG_WRITE users cannot read other users' unpublished stories.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLEANUP-2 (#413): convert two actionable TODOs to issue-referenced stubs
- +layout.server.ts:29 → TODO(#453) for dedicated admin stats endpoint
- ChronikRow.svelte: TODO(#454) for commentPreview; keep SECURITY line
as standalone comment (XSS guard stays co-located with the risk)
CLEANUP-3 (#414): add one-line justification comments to both naming
violators — SecurityUtils and GlobalExceptionHandler are both justified
by framework convention; no rename needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docs/README.md: remove duplicate infrastructure/ entry at end of folder tree
- ocr-service/CLAUDE.md: add **LLM reminder:** prefix to ALLOWED_PDF_HOSTS
SSRF warning (consistent with all other machine-readable instructions)
- backend/CLAUDE.md: restore ResponseStatusException note for simple controller
validation — avoids LLMs reaching for DomainException for trivial checks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- person/README.md: findAll(String q) and findByName(String firstName, String lastName)
- notification/README.md: replace 'None inbound' with actual outbound dep on DocumentService.findTitlesByIds
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- notification: remove phantom NotificationPreferenceRepository entity; fix
notifyReply signature (DocumentComment + Set<UUID>, not parentComment/reply)
- tag: correct delete(UUID) description — TagService.delete() is called BY
DocumentService.deleteTagCascading(), not the other way around
- person: fix findOrCreateByAlias to single-String signature; type classification
is internal to PersonTypeClassifier
- dashboard: replace fabricated cross-domain calls with verified ones
(removed NotificationService + GeschichteService; added TranscriptionService,
UserService, CommentService per actual DashboardService imports)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- notification/README.md: notifyMentions second param is DocumentComment, not String contextUrl
- document/README.md: transcription queue methods take int limit param
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces a separate reset@familyarchive.local / reset123 seed account
(e2e profile only) so the password-reset flow test never touches the
shared admin credentials.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MassImportService delegates to other domain services (no direct repo
access), and AuditService only touches its own AuditLogRepository —
both pass the boundary rule cleanly. Closes the known hole flagged
by Sara and Markus in PR #428.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace substring contains() with a regex exact-segment match so a
domain whose name is a substring of another (e.g. "tag" in "tagging")
cannot silently escape the predicate and produce a false negative.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rules enforced:
- Rule 1: no @RestController may inject a JpaRepository directly (preserves @RequirePermission AOP enforcement)
- Rule 2: @Service classes access only their own domain's repositories, never a foreign domain's
- Rule 3: no @Configuration class (except @SpringBootApplication) in domain packages
- Rule 4: all @Entity classes reside in a domain package
Rule 5 (URL prefix per controller domain) deferred — tracked in #427.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AnnotationService was changed to call transcriptionBlockRepository
directly, but the test still mocked TranscriptionService — causing a
NPE and leaving the cascade path uncovered.
Replace the @Mock TranscriptionService with @Mock
TranscriptionBlockRepository, update the two existing delete-test
verifications, and add a dedicated
deleteAnnotation_cascadesToTranscriptionBlocks test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No production code calls this method since ThumbnailService was changed
to write thumbnail metadata via documentRepository.save() directly.
Removing the unreachable wrapper eliminates false coverage and noise
during future security audits.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ThumbnailService now calls documentRepository.save() directly.
DocumentService.updateThumbnailMetadata() has no production callers,
so its test describes behaviour that no longer exists in the
production path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ThumbnailAsyncRunner was changed to inject DocumentRepository directly
(breaking the DocumentService cycle), but the test still passed
DocumentService to the constructor — a type mismatch that prevented
the test suite from compiling.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spring Framework 7 prohibits constructor injection cycles even with @Lazy.
Replace DocumentService dependencies in ThumbnailAsyncRunner and ThumbnailService
with direct DocumentRepository calls — both are intra-domain reads/saves.
Update ThumbnailServiceTest to mock DocumentRepository accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spring Framework 7 prohibits constructor injection cycles even with @Lazy.
Replace the TranscriptionService dependency in AnnotationService with a
direct TranscriptionBlockRepository call for the cascade-delete, which is
an intra-domain operation within the document package.
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>
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>
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>
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.
Without this, the Geschichten feature ships dark on prod day-one — no group
holds BLOG_WRITE, so the editor controls never render even for admins. The
mapping "anyone who can write documents can also author family stories" is
the safest default and admins can revoke afterwards via the new checkbox UI.
Closes Tobias's review S5 on PR #382.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The end-to-end test creates a DRAFT, verifies it is hidden from a READ_ALL
reader (list and getById), publishes it, verifies the reader sees it, then
deletes it and confirms the join rows go with it but the linked Person
remains. Also corrects the V58 author FK to reference the actual users
table (not app_users).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>