Adds @NotBlank @Size(max=255) on lastName, @NotNull on type,
@Valid on controller parameter. Blank/null input now returns
400 instead of reaching the DB constraint. 2 new controller tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds sender alias LEFT JOIN and receiver alias EXISTS subquery to
DocumentSpecifications.hasText(). Uses entity-graph navigation via
Person.nameAliases (@OneToMany) to avoid a separate DB roundtrip
while respecting domain boundaries. 2 new integration tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds LEFT JOIN to person_name_aliases in both searchByName (JPQL)
and searchWithDocumentCount (native SQL). Uses DISTINCT/GROUP BY
to prevent duplicate results. 4 new integration tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET returns aliases (no permission required), POST requires
WRITE_ALL, DELETE requires WRITE_ALL. 5 new controller tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getAliases (sorted by sort_order), addAlias (auto-incrementing
sort_order), removeAlias (with IDOR protection verifying alias
belongs to the given person). All TDD with 7 new unit tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces the alias domain model: entity with @ManyToOne to Person,
@OneToMany on Person for JPA graph navigation, repository with
sort_order queries, input DTO, and ALIAS_NOT_FOUND error code.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates the alias table for historical name changes (marriage,
widowhood, etc.) and adds GIN trigram indexes on both the new
alias table and the existing persons table for substring search.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add TODO comment explaining why SENDER/RECEIVER sort is in-memory
(JPA INNER JOIN drops null-sender docs) and note that pagination
will require a DB COUNT query in DocumentSearchResult.of().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A sender with lastName=null produced sort key "null Bob" which sorted
before names starting with lowercase letters (n < s, t, u, v...).
Now returns "" for null lastName, which the comparator places at end.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously any value other than ASC/DESC silently defaulted to
DESC with no feedback. Now returns 400 Bad Request.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SENDER and RECEIVER are handled by in-memory sort before resolveSort
is called, making those switch cases unreachable. Removed and added
a comment making the invariant explicit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DocumentSort is a query parameter enum, not a JPA entity.
Placing it in model/ violated the layer boundary — model/ should
contain only domain entities.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DocumentSort enum validated by Spring MVC (400 for unknown values)
- SENDER sort uses Spring Data Sort on sender.lastName/firstName
- RECEIVER sort uses in-memory sort by first receiver alphabetically
- UPLOAD_DATE sort uses createdAt; default sort is DATE DESC
- tagQ param wired to hasTagPartial specification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- hasText now JOINs sender (LEFT JOIN) and uses EXISTS subqueries for
receivers and tags to avoid duplicate rows
- hasTagPartial added for live debounced tag text filter (ILIKE partial match)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RED/GREEN for CommentService:
- getCommentsForBlock(blockId): returns root comments filtered by blockId
- postBlockComment(documentId, blockId, content, mentions, author): creates
comment with block_id set
RED/GREEN for CommentController:
- GET /api/documents/{docId}/transcription-blocks/{blockId}/comments
- POST /api/documents/{docId}/transcription-blocks/{blockId}/comments
- POST .../comments/{commentId}/replies (reuses existing replyToComment)
4 new tests: 2 service unit tests + 2 controller integration tests
All 25 CommentServiceTest + 24 CommentControllerTest green
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TranscriptionService.reorderBlocks() now returns void (command).
Controller calls listBlocks() separately after reorder (query).
Updated test to match new void signature.
Fixes @Felix: "reorderBlocks violates command-query separation"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
V18: text column now has CHECK (length(text) <= 10000) to enforce
the 10,000 character limit at the database level, complementing
the application-level enforcement in TranscriptionService.sanitizeText().
Fixes @Nora: "DB constraint catches anything the application misses"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes from PR #178 review:
Migration fixes:
- V18/V19: fix FK references from app_users to users (correct table name)
- V18: change annotation_id FK from ON DELETE CASCADE to ON DELETE RESTRICT
(block is aggregate root, cascade flows from block, not annotation)
Backend fixes:
- TranscriptionService.deleteBlock(): remove userId param, delete block first
then annotation directly via repository (no ownership check — block owns annotation)
- TranscriptionService.sanitizeText(): remove flawed regex HTML stripping,
textarea content is plain text by design — just enforce max length
- TranscriptionBlockController.requireUserId(): throw DomainException.unauthorized()
instead of silently returning null on auth failure
- CreateTranscriptionBlockDTO: add @Min/@Positive validation on coordinates
- Add @Slf4j logging to TranscriptionService for create/delete operations
Frontend fixes:
- Delete DocumentBottomPanel.svelte entirely (issue #175 requirement)
- Remove redundant mode exclusivity $effect (handled at toggle call sites)
- Remove dead handleCommentClick + onCommentClick prop (comments are future work)
- Remove quote hint UI (depends on comment feature)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TranscriptionBlock entity with @Version optimistic locking
TranscriptionBlockVersion for edit history
TranscriptionService facade: CRUD, reorder, version history
TranscriptionBlockController: REST endpoints under /api/documents/{docId}/transcription-blocks
DTOs: Create, Update, Reorder
ErrorCode: TRANSCRIPTION_BLOCK_NOT_FOUND, TRANSCRIPTION_BLOCK_CONFLICT
DocumentComment: add block_id field for block-level comment threads
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
V18: transcription_blocks table with optimistic locking version column
V19: transcription_block_versions for edit history capture
V20: add block_id FK to document_comments for block-level threads
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Blockers (14):
- B1: fix senderName/receiverName to use $derived instead of $state + sync $effect
- B2: migrate all korrespondenz components from messages-extra shim to paraglide m.*
- B3: i18n CorrespondenzEmptyState (heading, subtext, search placeholder)
- B4: add response.ok checks to admin layout server load
- B5: add response.ok checks to korrespondenz page server load
- B6: add page.server.spec.ts with 5 test suites for korrespondenz load function
- B7: add axe-core accessibility checks to all e2e korrespondenz tests
- B8: add Testcontainers JPQL tests for findSinglePersonCorrespondence (DISTINCT + sender)
- B9: hide auth reset-token endpoint from OpenAPI spec; remove from generated api.ts
- B11: replace amber hardcoded hex colors in SinglePersonHintBar with brand tokens
- B12: replace clipboard emoji with Heroicons SVG in SinglePersonHintBar
- B13: create DateInput component (German dd.mm.yyyy); use it in CorrespondenzFilterControls
- B14: add Paraglide compile step to CI workflow before lint/test
Suggestions (11):
- S1: make CorrespondentSuggestionsDropdown a pure display component; lift fetch to PersonBar
- S2: fix leftover messages-extra import in ConversationTimeline; use brand tokens for status dots
- S3: add intent comment to EntityNav openFlyout behavior
- S4: rename canManageGroups → canManagePermissions throughout admin
- S6: remove domFlush helper from DateInput spec; use expect.poll instead
- S7: replace test.skip with throw new Error in bilateral e2e tests
- S8: add inverse aria-disabled test for filter strip
- S9: remove sm:min-h-0 from sort button to preserve 44px touch target
- S10: add title attributes to tablet trigger buttons in EntityNav
- S11: delete messages-extra.ts shim entirely
Also: fix admin pages revealing blank strip at bottom (-mb-6 on admin layout)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A query of only spaces previously fell through to findAllWithDocumentCount,
exposing the full person list. Whitespace-only queries now return empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When receiverId is omitted, returns all documents where the person is
sender or receiver (single-person mode). Bilateral mode is unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes IDOR: the endpoint was publicly accessible to any authenticated user.
Now requires ADMIN_USER permission, matching all other user management endpoints.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolved conflicts in messages/de.json, en.json, es.json by keeping
both the persons-redesign keys (feature branch) and the notification
keys (main) in all three locale files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Native queries compute sender + receiver document count in one SQL call,
eliminating N+1. GET /api/persons now returns PersonSummaryDTO list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
createPerson now takes PersonUpdateDTO, persisting birthYear, deathYear,
notes in addition to firstName, lastName, alias.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added PERSON_NOT_FOUND to ErrorCode; getById, updatePerson, mergePersons
now throw DomainException.notFound for missing persons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
birthYear and deathYear must be positive integers; extracted shared
validateYears() method for reuse in createPerson.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
firstName/lastName max 100, alias max 200, notes max 5000 chars.
PUT /api/persons/{id} returns 400 for oversized fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST /api/persons, PUT /api/persons/{id}, POST /api/persons/{id}/merge
now return 403 for READ_ALL-only users.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NullX Finding 2: unbounded size param allowed full table scan. Added
spring-boot-starter-validation, @Validated on the controller, @Min(1) @Max(100)
on the size param, and ConstraintViolationException → 400 in GlobalExceptionHandler.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Notification rows in the history page need the document title. Added
findTitlesByIds(Collection<UUID>) to DocumentService (one query via a new
JPQL projection on DocumentRepository). NotificationService.getNotifications()
now fetches all titles for the page in a single extra query and maps them into
the DTO. documentTitle is null when the document has been deleted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NullX Finding 1: GET /api/notifications?read=false with no type param fell through
to the all-notifications branch, silently ignoring the read filter. Added
findByRecipientIdAndReadFalseOrderByCreatedAtDesc to NotificationRepository and
the missing Boolean.FALSE.equals(read) branch in NotificationService.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DocumentService.getRecentActivity: replace findAll(Sort)+stream().limit()
with findAll(PageRequest) so LIMIT is pushed to the database
- +page.svelte: collapse two-column grid to single column when mentions is empty
- DashboardNeedsMetadata: raise "show all" link from text-xs (12px) to text-sm
(14px) and add hover:underline for WCAG 1.4.1
- DashboardRecentDocuments: add comment explaining why T12:00:00 noon-anchor
is absent (updatedAt is a full ISO datetime, not a date-only string)
- DocumentServiceTest: update getRecentActivity tests to assert PageRequest
usage instead of findAll(Sort)
- DocumentRepositoryTest: add @DataJpaTest verifying findAll(PageRequest)
returns only size rows, not the full table
- DocumentControllerTest: add test for default size=5 when param is omitted
- NotificationServiceTest: add test documenting that type+read=true falls
through to the type-only query (intentional)
- page.server.spec.ts: replace stale tests with full dashboard-mode coverage
- DashboardMentions.svelte.spec.ts: add tests for REPLY type and absent documentId
- DashboardResumeStrip.svelte.spec.ts: add corrupt localStorage test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
spring.jpa.open-in-view=true (the default) holds a DB connection open for
the entire HTTP request lifecycle. Under concurrent dashboard API calls
(Promise.allSettled fires 3 at once), the pool of 10 is exhausted and the
backend crashes with connection timeout errors.
Setting open-in-view=false releases connections as soon as each
@Transactional method completes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add GET /api/documents/recent-activity?size=N endpoint that returns
the N most recently updated documents sorted by updatedAt DESC.
Includes TDD: failing tests written first, then production code.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add type-only filter to notification repo/service (previously only
worked with type+read=false together)
- Dashboard widget now fetches all recent notifications (mentions +
replies, both read and unread) instead of unread mentions only
- Update component heading and show type label per row
Root cause: Berit's mentions were read=true, so the unread-only filter
returned 0 results. The recent docs widget had no REVIEWED documents
because 'marking ready' sets metadata_complete, not status=REVIEWED.
Recent docs now shows all uploads without a status filter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dashboard "Recently Added" widget calls ?status=REVIEWED&size=5.
Null status is a no-op — existing callers without the param are unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dashboard widget calls ?size=3 to cap the list. Response now returns
{id, title} DTO instead of full Document entity.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dashboard widget uses ?type=MENTION&read=false to fetch unread mentions.
Also adds MethodArgumentTypeMismatchException → 400 handler so invalid
enum values in any @RequestParam return 400 instead of 500.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add unit tests for all service classes. Cover happy paths, error paths, and edge cases including structurally unreachable null guards via reflection to reach 90.2% branch coverage (431/478) in the service package.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Native queries bypass the JPA first-level cache; flush+clear is required before
reloading entities to see the updated state in the same transaction.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>