Flyway V62 adds idx_documents_sender_id and idx_comments_author_id to speed up
FK-driven queries on the persons page and briefwechsel view. Closes#470.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Math.abs(Integer.MIN_VALUE) overflows back to Integer.MIN_VALUE (negative),
making the old pattern unsafe for any palette size that doesn't evenly divide
MIN_VALUE. Math.floorMod always returns a non-negative residue in [0, n-1],
eliminating the overflow edge case entirely.
Fixes SpotBugs RV_ABSOLUTE_VALUE_OF_HASHCODE (priority 1, CORRECTNESS).
Closes#471
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getBlockComments was missing documentId; replyToBlockComment was missing
blockId. Spring silently ignored undeclared path variables — the segments
were parsed but never bound. Now both parameters are explicitly declared so
Spring rejects non-UUID values with 400.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Null dto.permissions now produces an empty HashSet instead of propagating null
into the @ElementCollection — prevents a silent NPE after V64 adds NOT NULL.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
V63 deduplicates any phantom (group_id, permission) rows accumulated since
the initial schema. V64 sets NOT NULL on permission and adds pk_group_permissions.
V65 renames uq_tbmp_block_person to pk_tbmp for naming-convention consistency.
Integration tests confirm each constraint via pg_catalog.pg_constraint. Closes#469 (partial).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses @Nora review: ?sort=documentCount&size=999999 could trigger a
full-table query and large serialization. Cap enforced at controller boundary.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses @Elicit review concern: stories stat tile was permanently showing
"—" because StatsDTO had no published-story count. Now wired end-to-end.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Replaces @DirtiesContext(AFTER_EACH_TEST_METHOD), which restarted
the full Spring context per test (≈10–15s × 7), with @Transactional
rollback. Each test still sees a clean slate via the spring-test
default rollback, but the context is shared across the class.
Wall time for this class dropped from 35s to 17.87s in local runs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The empty-result case returns null for both bounds, which the TS
codegen surfaces as optional. Future contributors should not "fix"
the missing @Schema(REQUIRED) — it is deliberate.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
YearMonth.from(d).toString() emits the same canonical YYYY-MM string
as the previous String.format("%04d-%02d", …) call but reads as a
single intent-revealing expression. Existing assertions on
"1915-08", "1916-01", … pin the output format unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The index was added in anticipation of a SQL GROUP BY aggregation,
but DocumentService.getDensity aggregates in memory via
findAll(spec).stream(). The index is never touched by the current
query plan. Per Markus's round-2 review: drop the unused migration
to avoid mismatched rationale-vs-implementation debt. Revisit when
the archive crosses 50k rows (TODO already in getDensity Javadoc).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the in-memory aggregation trade-off in getDensity so the next
perf audit knows the row-count threshold at which to revisit. Addresses
Markus's review concern.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Density bars now recompute when other filters change so the chart always
matches the list it sits above. Selectable filters: q, senderId, receiverId,
tag (multi), tagQ, status, tagOp. Date bounds (from/to) are deliberately
omitted — the chart is the surface for picking those, so it must always
span the broader space the user is selecting within.
Architectural shift: drop the native SQL GROUP BY in favour of in-memory
grouping over the existing Specification-driven findAll. This composes for
free with all the search predicates (FTS-rank-then-filter, sender/receiver,
tag-with-descendants, tagQ partial match, status, tagOp) and keeps the
density implementation a thin layer on top of searchDocuments. At the
current archive size (~5k docs) this stays well under the p95 200ms target;
Cache-Control: max-age=300 absorbs repeated browse loads.
- Removes findDensityByMonth, findMinMaxDocumentDate, DocumentDateRangeProjection.
- Replaces DocumentService.getDensity(LocalDate, LocalDate) with the
filter-aware overload.
- Endpoint accepts the same query params as /api/documents/search minus
paging+sort+from+to.
- DocumentDensityIntegrationTest rewritten as @SpringBootTest covering
no-filter / sender / tag / status / sender+tag combos via real PostgreSQL.
- DocumentServiceTest unit tests updated to the new signature.
- DocumentControllerTest tests forwarding of senderId+tag+tagOp and q+status.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>