Maps the repository's Object[] rows into a DocumentDensityResult and pairs
them with the archive-wide min/max meta_date range. Read-only, no
@Transactional needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Response shape for the upcoming GET /api/documents/density endpoint.
minDate and maxDate are nullable (null on empty archive); buckets is always
present.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Issue #385 introduces GET /api/documents/density which aggregates documents
by month via date_trunc. Adding the index now keeps the query cheap as the
archive grows and removes a future-investigation tax.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CommentData.java: add @Nullable on annotationId to match codebase convention
- DashboardService: isEmpty() → isBlank() for commentPreview null-guard
- ChronikRow.svelte: always set aria-label on comment rows (not only when preview present)
- ChronikRow.svelte.spec.ts: add test for aria-label on comment row without preview
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves the nested `CommentData` record out of `CommentService` into its own
`document/comment/CommentData.java` file, removing the cross-domain coupling
where `DashboardService` depended on an inner type of `CommentService`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove `findAnnotationIdsByIds` from CommentService — no production caller exists now
that DashboardService uses `findDataByIds` directly; along with its test coverage
- Fix aria-label construction in ChronikRow: pass actorName to i18n message function
instead of manually prepending the actor, so all locales render correctly
- Rename `findDataByIds_does_not_truncate_at_exactly_120_chars` →
`findDataByIds_preserves_content_at_exactly_120_chars` for accurate description
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ActivityFeedItemDTO gains a nullable commentPreview field (plain-text, 120 chars max).
DashboardService.getActivity() now calls findDataByIds() once instead of
findAnnotationIdsByIds(), halving DB round-trips for the Chronik page load.
Empty-string previews are normalised to null so the frontend can use ?? cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the single-purpose findAnnotationIdsByIds() (kept as delegation shim).
Introduces CommentData record (annotationId + preview) and stripAndTruncate()
using Jsoup.parse().text() for DOM-safe HTML stripping. Truncates to 120 chars.
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>