ActivityFeedItemDTO gains nullable commentId and annotationId fields.
DashboardService.getActivity forwards commentId from the projection
and batch-resolves annotationId via the new
CommentService.findAnnotationIdsByIds lookup. Both remain null for
non-comment kinds, so the bulk lookup is skipped entirely when the
feed has no comment rows.
Refs #300.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Exposes a CommentService method that maps a collection of
commentIds to their annotationIds via commentRepository.findAllById.
Unknown comments and comments with null annotationId are omitted.
Used by the dashboard activity feed enrichment to supply the
deep-link annotationId without growing the audit SQL query.
Refs #300.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds getCommentId() to ActivityFeedRow and selects
(ag.payload->>'commentId')::uuid from findRolledUpActivityFeed so
chronik consumers can build deep-link URLs for COMMENT_ADDED and
MENTION_CREATED events. Null for other kinds.
Refs #300.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Only block comments are surfaced by the frontend now. The document-level
and annotation-level comment endpoints and service methods existed but
had no consumer. Remove them along with their repository queries and
test coverage so the surface area matches the actual feature set.
Shared edit, delete, and block reply endpoints stay. postBlockComment
now carries the authorName/mention/audit behaviors previously tested
through the dropped postComment method, so those behaviors remain
covered by the block-scoped test suite.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously issued block-comment notifications were stored with
annotation_id=NULL because CommentService.postBlockComment did not
populate DocumentComment.annotationId. Now that the code fix is in
place, existing rows need to be filled in so legacy notifications
can also carry the query param that the frontend deep-link flow
expects.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
postBlockComment now looks up the block via TranscriptionService and
sets comment.annotationId from block.getAnnotationId(). This closes
the upstream root cause of issue #276, where notifications for block
comments were stored with annotationId=null, breaking the notification
deep-link flow on the document detail page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comments created before audit logging was added in 428c63a2 have no
corresponding audit_log rows, so the Chronik activity feed (which
reads exclusively from audit_log) cannot surface them in "Alle" or
"Für dich", even though the fix from #295 is wired up correctly.
V50 inserts the missing events idempotently from document_comments
and comment_mentions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add youParticipated field to the DTO record and wire row.isYouParticipated()
through DashboardService.getActivity().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add you_participated correlated subquery to findRolledUpActivityFeed.
Carries payload through the aggregated CTE via MIN(payload::text)::jsonb
so the commentId can be matched against notifications.reference_id.
Uses CAST(:currentUserId AS uuid) to avoid Spring Data JPA misparsin ::
cast syntax as a parameter name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add isYouParticipated() to ActivityFeedRow interface and cover four
behaviours in AuditLogQueryRepositoryRolledUpTest: youParticipated
true/false and retroactive youMentioned true/false coverage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes the CWE-285 gap Nora flagged on issue #296: both endpoints expose
enrichment-queue information that only writers should see. Brings them in
line with the new /incomplete list endpoint and every other write-path
under DocumentController.
Frontend callers (/enrich/[id]/+page.server.ts) already gate on WRITE_ALL
at the route level, so no client-side change is needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Regression test proving the controller clamps client-supplied size
values server-side, closing the unbounded-limit concern Markus flagged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Only users who can enrich documents should see the queue.
Mirrors the frontend guard in enrich/+page.server.ts and closes the
CWE-285 gap Nora flagged on issue #296.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restores the list endpoint removed in ddd811c6 and caps size at 200.
The dashboard enrichment block (issue #296) and /enrich page both
consume it; /enrich was silently 404ing since the deletion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mapper populates uploadedAt from Document.createdAt so the dashboard
enrichment block can show a relative-time meta line ("vor 2 Min.")
per issue #296.
LocalDateTime matches the convention used by NotificationDTO,
DocumentVersionSummary and InviteListItemDTO.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cheap insurance for the day someone widens the WHERE clause in
findRolledUpActivityFeed. Suggested by Sara in PR #288 review cycle 1.
Inserts two non-eligible events alongside one TEXT_SAVED and asserts the feed
returns the TEXT_SAVED row only.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- V49__add_audit_log_rollup_index.sql: partial covering index on
(actor_id, document_id, kind, happened_at DESC) filtered by the 6 rollup
kinds. Matches the WHERE clause of findRolledUpActivityFeed exactly so the
session-grouping window scan is index-backed.
- DashboardController: clamp limit to 40 (was 20). Chronik requests up to 40
activity items per page; dashboard side-rail still passes 7.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The method no longer deduplicates by hour-trunc — it performs session-style
rollup via LAG()+120-min gap. Rename aligns the public name with the
behavior.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rewrites the activity feed query to group consecutive events on the same
(actor, document, kind) into sessions separated by >120 min gaps. A session
becomes one row with count = events-in-session and happenedAtUntil = last
event timestamp. Singletons keep count=1 / happenedAtUntil=null.
Algorithm: LAG() to get the previous event's timestamp in the same partition,
mark a new session when gap > 7200s, then SUM() over an unbounded preceding
window yields a running session_id. Aggregation groups by session_id.
COMMENT_ADDED and MENTION_CREATED always start a new session — these kinds
never roll up so each event stays its own row.
Also adds BLOCK_REVIEWED to the eligible-kinds WHERE clause (Chronik spec §02)
so reviewed blocks appear in the activity feed.
Five new integration tests cover combine-within-2h, split-at-boundary,
no-hard-cap-on-long-session, never-rolls-up-comments/mentions, and the
count/happenedAtUntil contract on both singletons and rollups.
Part of #285.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prepares the activity feed data shape for session-style rollup (#285). Adds two
new fields that carry null-operation defaults for the existing hour-truncated
dedupe query:
- count: int (required) — always 1 for singleton rows
- happenedAtUntil: OffsetDateTime (nullable) — end-of-session timestamp for
future rollup rows; null for singletons
No behavioral change yet — the rollup SQL rewrite lands in a follow-up commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DocumentService.searchDocuments now fetches completion percentages and recent
contributors per document and zips them into DocumentSearchItem records.
Update affected tests to use the new items-based result shape.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a window-function query that returns at most 4 contributors per document
ordered by most-recent activity. Used by DocumentService to populate the
contributors field in DocumentSearchItem (issue #281).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds findCompletionStatsForDocuments() returning reviewed-block percentage
per document in a single native SQL GROUP BY query. Needed for the new
DocumentSearchItem DTO in issue #281.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TranscriptionQueueService was importing ActivityActorDTO and AuditLogQueryService
from the dashboard package, creating an inverted dependency (service → dashboard).
Moving these to the audit package where AuditLog lives gives both DashboardService
and TranscriptionQueueService the correct dependency direction (→ audit).
Moved to audit:
- ActivityActorDTO, ActivityFeedRow, ContributorRow, PulseStatsRow (projections)
- AuditLogQueryRepository, AuditLogQueryService
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
findMostRecentDocumentIdByActor only matched TEXT_SAVED events, so documents
where the user drew annotation bounding boxes (but typed no transcription text)
were invisible to the hero resume card. Extending the IN clause to include
ANNOTATION_CREATED lets annotation-only work surface in the card (0% progress,
no excerpt — the correct state before transcription begins).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AuditService.logAfterCommit() called writeLog() inline inside the afterCommit()
callback. At that point Spring's transaction synchronizations are still active on
the thread, so SimpleJpaRepository.save() throws IllegalStateException which the
catch block silently swallowed — leaving audit_log permanently empty.
Fix: submit writeLog() to auditExecutor so it runs on a fresh thread with no active
synchronization context. Also switch auditExecutor from CallerRunsPolicy to AbortPolicy
to prevent the bug from silently recurring when the queue fills under load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GET /api/documents/incomplete and GET /api/documents/recent-activity are
superseded by the new dashboard endpoints (GET /api/dashboard/activity etc.)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The AppUser entity is mapped to the 'users' table (not 'app_users').
V46 had a broken REFERENCES clause and hardcoded role in REVOKE; V47 and the
native query in AuditLogQueryRepository had the same wrong table name.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both DocumentController and TranscriptionBlockController contained
identical private requireUserId helpers. Extracted to a shared static
utility in the security package ahead of DashboardController which
also needs actor resolution.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Required for dashboard Pulse stat 2 (COUNT DISTINCT blockId).
Without it, two saves on different blocks on the same page
were indistinguishable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds color field assigned from an 8-colour palette keyed on the user's UUID
hash (Math.abs(id.hashCode()) % 8). Fires via @PrePersist/@PreUpdate/@PostLoad
so both new and existing users get the correct colour at runtime.
V47 migration adds the column and fixes the V46 REVOKE bug that hardcoded
role name 'app_user' instead of CURRENT_USER.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instruments CommentService.postComment(), postBlockComment(), and
replyToComment() to fire COMMENT_ADDED after each successful save and
MENTION_CREATED once per mentioned user. The shared logCommentPosted()
helper avoids duplicating the two-call pattern across all three post
methods.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract logAfterCommit() from AnnotationService and TranscriptionService
into AuditService, eliminating duplicate boilerplate (Markus)
- Remove UserService from DocumentService; add actorId param to
storeDocument(), attachFile(), updateDocument() instead — resolves
SecurityContextHolder coupling concern (Markus)
- Update DocumentController to inject UserService and resolve actorId
from Authentication, passing it through to service methods
- Add logAfterCommit() tests to AuditServiceTest with MockedStatic
- Update all test verify() calls to use logAfterCommit() (not log())
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>