fix(chronik): surface REPLY events in Für-dich feed via youParticipated #297

Merged
marcel merged 9 commits from feat/issue-295-you-participated into main 2026-04-21 09:00:25 +02:00
Owner

Summary

  • Adds a youParticipated flag to ActivityFeedItemDTO, computed via a correlated EXISTS subquery against the notifications table (no schema migration needed)
  • Widens the fuer-dich filter in the Chronik to include youParticipated, so COMMENT_ADDED events now appear for thread participants — consistent with the notification bell's REPLY behaviour
  • Extracts filterFeed() to a pure function in chronik/feedFilters.ts for testability

Closes #295

Changes

Layer File Change
Interface audit/ActivityFeedRow.java +isYouParticipated()
SQL audit/AuditLogQueryRepository.java +you_participated EXISTS subquery; +payload via MIN(payload::text)::jsonb in aggregated CTE
DTO dashboard/ActivityFeedItemDTO.java +youParticipated field with @Schema(requiredMode = REQUIRED)
Service dashboard/DashboardService.java Wire row.isYouParticipated() into DTO constructor
Types frontend/src/lib/generated/api.ts +youParticipated: boolean on ActivityFeedItemDTO
Logic frontend/src/routes/chronik/feedFilters.ts New — extracted filterFeed() with widened fuer-dich predicate
Tests AuditLogQueryRepositoryRolledUpTest.java 4 new: youParticipated true/false + retroactive youMentioned coverage
Tests frontend/.../feedFilters.test.ts 13 unit tests covering all 5 filter values
View frontend/src/routes/chronik/+page.svelte Replace inline switch with filterFeed()

Test plan

  • Backend: ./mvnw test — 1196 tests, 0 failures
  • Frontend: npm run test (server project) — 318 tests, 0 failures
  • Manual: Add a comment to a document, reply as another user → the original commenter's Chronik "Für dich" filter shows the COMMENT_ADDED event
## Summary - Adds a `youParticipated` flag to `ActivityFeedItemDTO`, computed via a correlated `EXISTS` subquery against the `notifications` table (no schema migration needed) - Widens the `fuer-dich` filter in the Chronik to include `youParticipated`, so `COMMENT_ADDED` events now appear for thread participants — consistent with the notification bell's REPLY behaviour - Extracts `filterFeed()` to a pure function in `chronik/feedFilters.ts` for testability Closes #295 ## Changes | Layer | File | Change | |---|---|---| | Interface | `audit/ActivityFeedRow.java` | `+isYouParticipated()` | | SQL | `audit/AuditLogQueryRepository.java` | `+you_participated` EXISTS subquery; `+payload` via `MIN(payload::text)::jsonb` in aggregated CTE | | DTO | `dashboard/ActivityFeedItemDTO.java` | `+youParticipated` field with `@Schema(requiredMode = REQUIRED)` | | Service | `dashboard/DashboardService.java` | Wire `row.isYouParticipated()` into DTO constructor | | Types | `frontend/src/lib/generated/api.ts` | `+youParticipated: boolean` on `ActivityFeedItemDTO` | | Logic | `frontend/src/routes/chronik/feedFilters.ts` | New — extracted `filterFeed()` with widened fuer-dich predicate | | Tests | `AuditLogQueryRepositoryRolledUpTest.java` | 4 new: `youParticipated` true/false + retroactive `youMentioned` coverage | | Tests | `frontend/.../feedFilters.test.ts` | 13 unit tests covering all 5 filter values | | View | `frontend/src/routes/chronik/+page.svelte` | Replace inline switch with `filterFeed()` | ## Test plan - [ ] Backend: `./mvnw test` — 1196 tests, 0 failures - [ ] Frontend: `npm run test` (server project) — 318 tests, 0 failures - [ ] Manual: Add a comment to a document, reply as another user → the original commenter's Chronik "Für dich" filter shows the `COMMENT_ADDED` event
marcel added 9 commits 2026-04-20 21:55:13 +02:00
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>
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>
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>
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>
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>
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 youParticipated field to the DTO record and wire row.isYouParticipated()
through DashboardService.getActivity().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract filterFeed(items, filter) from +page.svelte inline switch to a
pure function, widening the fuer-dich branch to include youParticipated.
Regenerate ActivityFeedItemDTO type to include the new field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor(chronik): replace inline filter switch with filterFeed()
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m33s
CI / OCR Service Tests (push) Successful in 31s
CI / Backend Unit Tests (push) Failing after 2m49s
CI / Unit & Component Tests (pull_request) Failing after 2m37s
CI / OCR Service Tests (pull_request) Successful in 32s
CI / Backend Unit Tests (pull_request) Failing after 2m52s
7429db4f1e
Wire the extracted filterFeed function into the displayFeed derived,
removing 20 lines of inline switch logic from +page.svelte.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

🏗️ Markus Keller — Senior Application Architect

Verdict: ⚠️ Approved with concerns

What I checked

Domain boundary crossing, CTE payload aggregation correctness, migration necessity, layering.


Blockers

None. The deliberate cross-domain query decision was signed off in issue #295 and the SQL comment names the contract:

-- payload->>'commentId' matches notifications.reference_id per AuditKind.COMMENT_ADDED contract

That is exactly what I asked for.


Suggestions

1. MIN(payload::text)::jsonb needs a WHY commentAuditLogQueryRepository.java

The query carries payload through the aggregated CTE using MIN(payload::text)::jsonb. This is semantically correct because COMMENT_ADDED and MENTION_CREATED are never rolled up (each has is_new_session = 1 unconditionally), so each session contains exactly one event and MIN is the identity. However, that invariant is not stated anywhere near the SQL. A future developer who touches the session-marking logic could break this silently.

-- MIN is safe: COMMENT_ADDED and MENTION_CREATED are always their own session
-- (is_new_session = 1 unconditionally above), so each aggregated row has exactly
-- one source event. MIN collapses to that single value.
MIN(s.payload::text)::jsonb AS payload

Without this comment, the correctness of youParticipated depends on a non-obvious invariant in a different part of the same query.

2. The PR diff contains out-of-scope changes

The diff includes changes to DocumentController.java, IncompleteDocumentDTO.java, DocumentService.java, and their tests. These are from issue #296, already merged to main. Because this branch was cut from a point slightly before those commits, they appear in the PR diff.

This is a cosmetic issue for review purposes (the changes are already in main and will merge cleanly), but it makes the diff harder to reason about. Before merging: confirm git merge-base HEAD main and verify no actual regressions are introduced. A quick rebase onto current main tip would clean this up.


What's done well

  • No schema migration required — this is the right call for a pure query change.
  • CAST(:currentUserId AS uuid) instead of ::uuid suffix is the correct workaround for Spring Data JPA parameter parsing. Good catch.
  • DTO field is properly annotated with @Schema(requiredMode = REQUIRED) consistent with the rest of the record.
  • DashboardService.getActivity() stays a pure mapper — no business logic leak.
## 🏗️ Markus Keller — Senior Application Architect **Verdict: ⚠️ Approved with concerns** ### What I checked Domain boundary crossing, CTE payload aggregation correctness, migration necessity, layering. --- ### Blockers None. The deliberate cross-domain query decision was signed off in issue #295 and the SQL comment names the contract: ```sql -- payload->>'commentId' matches notifications.reference_id per AuditKind.COMMENT_ADDED contract ``` That is exactly what I asked for. ✅ --- ### Suggestions **1. `MIN(payload::text)::jsonb` needs a WHY comment** — `AuditLogQueryRepository.java` The query carries payload through the `aggregated` CTE using `MIN(payload::text)::jsonb`. This is semantically correct because `COMMENT_ADDED` and `MENTION_CREATED` are never rolled up (each has `is_new_session = 1` unconditionally), so each session contains exactly one event and `MIN` is the identity. However, that invariant is not stated anywhere near the SQL. A future developer who touches the session-marking logic could break this silently. ```sql -- MIN is safe: COMMENT_ADDED and MENTION_CREATED are always their own session -- (is_new_session = 1 unconditionally above), so each aggregated row has exactly -- one source event. MIN collapses to that single value. MIN(s.payload::text)::jsonb AS payload ``` Without this comment, the correctness of `youParticipated` depends on a non-obvious invariant in a different part of the same query. **2. The PR diff contains out-of-scope changes** The diff includes changes to `DocumentController.java`, `IncompleteDocumentDTO.java`, `DocumentService.java`, and their tests. These are from issue #296, already merged to main. Because this branch was cut from a point slightly before those commits, they appear in the PR diff. This is a cosmetic issue for review purposes (the changes are already in main and will merge cleanly), but it makes the diff harder to reason about. Before merging: confirm `git merge-base HEAD main` and verify no actual regressions are introduced. A quick rebase onto current main tip would clean this up. --- ### What's done well - No schema migration required — this is the right call for a pure query change. - `CAST(:currentUserId AS uuid)` instead of `::uuid` suffix is the correct workaround for Spring Data JPA parameter parsing. Good catch. - DTO field is properly annotated with `@Schema(requiredMode = REQUIRED)` consistent with the rest of the record. - `DashboardService.getActivity()` stays a pure mapper — no business logic leak.
Author
Owner

🔒 Nora Steiner — Application Security Engineer

Verdict: ⚠️ Approved with concerns

What I checked

IDOR risk on youParticipated, SQL injection in native query, scoping correctness, cross-user data leakage.


Blockers

None. The primary risk I was watching for — user A seeing youParticipated = true for a comment thread they are not part of — is correctly prevented: n.recipient_id = CAST(:currentUserId AS uuid) binds the EXISTS subquery to the querying user. The CAST() approach also avoids parameter injection via the ::uuid suffix workaround. Parameterized query throughout.


Concerns (not blockers, but should be addressed)

1. Missing cross-user scoping regression testAuditLogQueryRepositoryRolledUpTest.java

The current test suite covers:

  • youParticipated_is_true_when_user_has_reply_notification_for_comment — user has a REPLY notification
  • youParticipated_is_false_for_comment_with_no_reply_notification — no notification exists

What is not tested: a notification exists in the notifications table for the same comment but belonging to a different user. This is the exact IDOR case. The subquery should not let that notification bleed through.

@Test
void youParticipated_is_false_when_reply_notification_belongs_to_other_user() {
    insertUserAndDocs();
    UUID commentId = UUID.randomUUID();
    insertAuditEvent(OTHER_USER_ID, DOC_ID, "COMMENT_ADDED",
            Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString()));
    // Notification for OTHER_USER_ID, not USER_ID
    insertReplyNotification(OTHER_USER_ID, DOC_ID, commentId);

    List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);

    assertThat(rows).allSatisfy(r ->
            assertThat(r.isYouParticipated()).isFalse()
    );
}

This test is worth adding as a permanent regression. The current recipient_id = CAST(:currentUserId AS uuid) predicate correctly prevents the leak, but without this test there is no machine-readable proof it does.

2. Hand-rolled JSON in test helperAuditLogQueryRepositoryRolledUpTest.java

.map(e -> "\"" + e.getKey() + "\": \"" + e.getValue() + "\"")

This is test code, not production, so no exploitable surface. But if a future test passes a value containing a double quote (e.g. "comment's \"text\"") the resulting JSON is malformed and the test will fail with a confusing JSONB parse error rather than a clear assertion failure. Use Jackson:

import com.fasterxml.jackson.databind.ObjectMapper;
// ...
String payloadJson = payload.isEmpty() ? null : new ObjectMapper().writeValueAsString(payload);

This is already a dependency in the test classpath.


What's done well

  • No IDOR: recipient_id = CAST(:currentUserId AS uuid) correctly scopes to the requesting user.
  • reference_id = (ag.payload->>'commentId')::uuid casts the JSON text extraction to UUID — no text comparison of UUID values (which can differ in case).
  • CAST() syntax for the parameter avoids Spring Data JPA treating ::uuid as part of the named parameter, which would have caused silent failures.
## 🔒 Nora Steiner — Application Security Engineer **Verdict: ⚠️ Approved with concerns** ### What I checked IDOR risk on `youParticipated`, SQL injection in native query, scoping correctness, cross-user data leakage. --- ### Blockers None. The primary risk I was watching for — user A seeing `youParticipated = true` for a comment thread they are not part of — is correctly prevented: `n.recipient_id = CAST(:currentUserId AS uuid)` binds the EXISTS subquery to the querying user. The CAST() approach also avoids parameter injection via the `::uuid` suffix workaround. Parameterized query throughout. ✅ --- ### Concerns (not blockers, but should be addressed) **1. Missing cross-user scoping regression test** — `AuditLogQueryRepositoryRolledUpTest.java` The current test suite covers: - `youParticipated_is_true_when_user_has_reply_notification_for_comment` — user has a REPLY notification ✅ - `youParticipated_is_false_for_comment_with_no_reply_notification` — no notification exists ✅ What is **not** tested: a notification exists in the `notifications` table for the *same comment* but belonging to a *different user*. This is the exact IDOR case. The subquery should not let that notification bleed through. ```java @Test void youParticipated_is_false_when_reply_notification_belongs_to_other_user() { insertUserAndDocs(); UUID commentId = UUID.randomUUID(); insertAuditEvent(OTHER_USER_ID, DOC_ID, "COMMENT_ADDED", Instant.parse("2026-04-20T10:00:00Z"), Map.of("commentId", commentId.toString())); // Notification for OTHER_USER_ID, not USER_ID insertReplyNotification(OTHER_USER_ID, DOC_ID, commentId); List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); assertThat(rows).allSatisfy(r -> assertThat(r.isYouParticipated()).isFalse() ); } ``` This test is worth adding as a permanent regression. The current `recipient_id = CAST(:currentUserId AS uuid)` predicate correctly prevents the leak, but without this test there is no machine-readable proof it does. **2. Hand-rolled JSON in test helper** — `AuditLogQueryRepositoryRolledUpTest.java` ```java .map(e -> "\"" + e.getKey() + "\": \"" + e.getValue() + "\"") ``` This is test code, not production, so no exploitable surface. But if a future test passes a value containing a double quote (e.g. `"comment's \"text\""`) the resulting JSON is malformed and the test will fail with a confusing JSONB parse error rather than a clear assertion failure. Use Jackson: ```java import com.fasterxml.jackson.databind.ObjectMapper; // ... String payloadJson = payload.isEmpty() ? null : new ObjectMapper().writeValueAsString(payload); ``` This is already a dependency in the test classpath. --- ### What's done well - No IDOR: `recipient_id = CAST(:currentUserId AS uuid)` correctly scopes to the requesting user. - `reference_id = (ag.payload->>'commentId')::uuid` casts the JSON text extraction to UUID — no text comparison of UUID values (which can differ in case). - `CAST()` syntax for the parameter avoids Spring Data JPA treating `::uuid` as part of the named parameter, which would have caused silent failures.
Author
Owner

🧪 Sara Holt — QA Engineer & Test Strategist

Verdict: ⚠️ Approved with concerns

What I checked

TDD evidence, test coverage at all layers, test helper quality, edge cases.


Blockers

None.


Concerns

1. Missing cross-user scoping testAuditLogQueryRepositoryRolledUpTest.java

See also Nora's comment. From a test coverage standpoint: youParticipated_is_false_for_comment_with_no_reply_notification proves the negative when no notification exists. But the more important negative is: a REPLY notification exists in the table for OTHER_USER_ID on the same comment — USER_ID's feed should still return youParticipated = false. Without this test, the scoping constraint is not machine-verified.

2. insertAuditEvent payload JSON is hand-rolledAuditLogQueryRepositoryRolledUpTest.java

String payloadJson = payload.isEmpty() ? null
        : payload.entrySet().stream()
            .map(e -> "\"" + e.getKey() + "\": \"" + e.getValue() + "\"")
            .collect(java.util.stream.Collectors.joining(", ", "{", "}"));

This will silently produce malformed JSON if any value contains a ", \, or control character. Tests using this helper would fail with a JSONB parse error from PostgreSQL rather than an assertion failure — confusing diagnostic. Use ObjectMapper.writeValueAsString(payload) instead, which is already available via the Jackson dependency.

3. No test for youParticipated when payload has no commentId key — suggestion

What happens when COMMENT_ADDED is inserted without a commentId in the payload? ag.payload->>'commentId' returns NULL, (NULL)::uuid is NULL, and n.reference_id = NULL evaluates to UNKNOWN (never true). This is the correct behavior and it means youParticipated stays false — but it is not tested. A one-line test would document this graceful-degradation behavior:

@Test
void youParticipated_is_false_when_comment_added_has_no_commentId_in_payload() {
    insertUserAndDocs();
    insertAuditEvent(OTHER_USER_ID, DOC_ID, "COMMENT_ADDED",
            Instant.parse("2026-04-20T10:00:00Z"), Map.of()); // no commentId key
    // no notification inserted

    List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40);

    assertThat(rows).allSatisfy(r ->
            assertThat(r.isYouParticipated()).isFalse()
    );
}

What's done well

  • TDD evidence is solid: repository tests were written before the SQL was added (evidenced by the two-commit structure).
  • 4 repository-layer tests covering youParticipated true/false + retroactive youMentioned true/false coverage closes a pre-existing gap.
  • 13 Vitest unit tests in feedFilters.test.ts cover all 5 filter values, positive and negative cases. The makeItem() factory follows the project's pattern exactly.
  • DashboardServiceTest mock was correctly updated to implement the new interface method.
  • @Transactional on the test class ensures no cross-test state leakage — correct use of the test infrastructure.
## 🧪 Sara Holt — QA Engineer & Test Strategist **Verdict: ⚠️ Approved with concerns** ### What I checked TDD evidence, test coverage at all layers, test helper quality, edge cases. --- ### Blockers None. --- ### Concerns **1. Missing cross-user scoping test** — `AuditLogQueryRepositoryRolledUpTest.java` See also Nora's comment. From a test coverage standpoint: `youParticipated_is_false_for_comment_with_no_reply_notification` proves the negative when no notification exists. But the more important negative is: a REPLY notification exists in the table for `OTHER_USER_ID` on the same comment — `USER_ID`'s feed should still return `youParticipated = false`. Without this test, the scoping constraint is not machine-verified. **2. `insertAuditEvent` payload JSON is hand-rolled** — `AuditLogQueryRepositoryRolledUpTest.java` ```java String payloadJson = payload.isEmpty() ? null : payload.entrySet().stream() .map(e -> "\"" + e.getKey() + "\": \"" + e.getValue() + "\"") .collect(java.util.stream.Collectors.joining(", ", "{", "}")); ``` This will silently produce malformed JSON if any value contains a `"`, `\`, or control character. Tests using this helper would fail with a JSONB parse error from PostgreSQL rather than an assertion failure — confusing diagnostic. Use `ObjectMapper.writeValueAsString(payload)` instead, which is already available via the Jackson dependency. **3. No test for `youParticipated` when payload has no `commentId` key** — suggestion What happens when `COMMENT_ADDED` is inserted without a `commentId` in the payload? `ag.payload->>'commentId'` returns NULL, `(NULL)::uuid` is NULL, and `n.reference_id = NULL` evaluates to UNKNOWN (never true). This is the correct behavior and it means `youParticipated` stays `false` — but it is not tested. A one-line test would document this graceful-degradation behavior: ```java @Test void youParticipated_is_false_when_comment_added_has_no_commentId_in_payload() { insertUserAndDocs(); insertAuditEvent(OTHER_USER_ID, DOC_ID, "COMMENT_ADDED", Instant.parse("2026-04-20T10:00:00Z"), Map.of()); // no commentId key // no notification inserted List<ActivityFeedRow> rows = auditLogQueryRepository.findRolledUpActivityFeed(USER_ID.toString(), 40); assertThat(rows).allSatisfy(r -> assertThat(r.isYouParticipated()).isFalse() ); } ``` --- ### What's done well - TDD evidence is solid: repository tests were written before the SQL was added (evidenced by the two-commit structure). - 4 repository-layer tests covering `youParticipated` true/false + retroactive `youMentioned` true/false coverage closes a pre-existing gap. - 13 Vitest unit tests in `feedFilters.test.ts` cover all 5 filter values, positive and negative cases. The `makeItem()` factory follows the project's pattern exactly. - `DashboardServiceTest` mock was correctly updated to implement the new interface method. - `@Transactional` on the test class ensures no cross-test state leakage — correct use of the test infrastructure.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: ⚠️ Approved with concerns

What I checked

TDD discipline, naming, function size, SQL invariant comments, frontend patterns.


Blockers

None.


Concerns

1. MIN(payload::text)::jsonb has no invariant commentAuditLogQueryRepository.java

The correctness of youParticipated depends on MIN(payload::text)::jsonb collapsing to the single payload of a COMMENT_ADDED event. This works because COMMENT_ADDED events are always their own session (unconditional is_new_session = 1 in sessions_marked). That invariant is invisible from the aggregation line alone.

Without a comment, a developer who later changes the session-marking logic for comment events would not know they are about to break youParticipated. One line:

-- COMMENT_ADDED/MENTION_CREATED always have is_new_session=1, so MIN=the single event's payload
MIN(s.payload::text)::jsonb AS payload

This is the "why" comment that the code style guide explicitly calls for — not explaining what MIN does, but explaining the invariant that makes it correct.

2. Hand-rolled JSON serialization in test helperAuditLogQueryRepositoryRolledUpTest.java

.map(e -> "\"" + e.getKey() + "\": \"" + e.getValue() + "\"")

Test code, but fragile. A UUID value like uuid-with-no-special-chars is fine; but if someone passes a map entry whose value contains a " or \, the produced JSON is invalid and the failure from PostgreSQL will be a parse error, not an assertion failure. Jackson is already on the classpath:

private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER =
        new com.fasterxml.jackson.databind.ObjectMapper();

// in the method:
String payloadJson = payload.isEmpty() ? null : MAPPER.writeValueAsString(payload);

What's done well

  • filterFeed() extraction from +page.svelte is exactly right: pure function, clear name, zero DOM dependencies, trivially testable. The $derived(filterFeed(...)) call in the page is one line. Clean.
  • feedFilters.ts has no business logic in template markup.
  • ActivityFeedRow interface change is the minimal prerequisite for everything else — correct place to start the TDD chain.
  • isYouParticipated() and isYouMentioned() are consistent boolean naming with the Spring projection interface convention.
  • TDD order is demonstrably correct: the test commit predates the SQL commit in the branch log. The failing AopInvocation Null return value error (before the SQL was added) confirms the test was genuinely red before going green.
## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: ⚠️ Approved with concerns** ### What I checked TDD discipline, naming, function size, SQL invariant comments, frontend patterns. --- ### Blockers None. --- ### Concerns **1. `MIN(payload::text)::jsonb` has no invariant comment** — `AuditLogQueryRepository.java` The correctness of `youParticipated` depends on `MIN(payload::text)::jsonb` collapsing to the single payload of a `COMMENT_ADDED` event. This works because `COMMENT_ADDED` events are always their own session (unconditional `is_new_session = 1` in `sessions_marked`). That invariant is invisible from the aggregation line alone. Without a comment, a developer who later changes the session-marking logic for comment events would not know they are about to break `youParticipated`. One line: ```sql -- COMMENT_ADDED/MENTION_CREATED always have is_new_session=1, so MIN=the single event's payload MIN(s.payload::text)::jsonb AS payload ``` This is the "why" comment that the code style guide explicitly calls for — not explaining what `MIN` does, but explaining the invariant that makes it correct. **2. Hand-rolled JSON serialization in test helper** — `AuditLogQueryRepositoryRolledUpTest.java` ```java .map(e -> "\"" + e.getKey() + "\": \"" + e.getValue() + "\"") ``` Test code, but fragile. A UUID value like `uuid-with-no-special-chars` is fine; but if someone passes a map entry whose value contains a `"` or `\`, the produced JSON is invalid and the failure from PostgreSQL will be a parse error, not an assertion failure. Jackson is already on the classpath: ```java private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = new com.fasterxml.jackson.databind.ObjectMapper(); // in the method: String payloadJson = payload.isEmpty() ? null : MAPPER.writeValueAsString(payload); ``` --- ### What's done well - `filterFeed()` extraction from `+page.svelte` is exactly right: pure function, clear name, zero DOM dependencies, trivially testable. The `$derived(filterFeed(...))` call in the page is one line. Clean. - `feedFilters.ts` has no business logic in template markup. ✅ - `ActivityFeedRow` interface change is the minimal prerequisite for everything else — correct place to start the TDD chain. - `isYouParticipated()` and `isYouMentioned()` are consistent boolean naming with the Spring projection interface convention. - TDD order is demonstrably correct: the test commit predates the SQL commit in the branch log. The failing `AopInvocation Null return value` error (before the SQL was added) confirms the test was genuinely red before going green.
Author
Owner

⚙️ Tobias Wendt — DevOps & Platform Engineer

Verdict: Approved

What I checked

Migration necessity, infrastructure changes, CI impact, index implications.


LGTM notes

  • No Flyway migration required — this is a pure query change. The notifications table (created in V16) and its recipient_id / reference_id columns are already present. The idx_notifications_recipient index on (recipient_id, read, created_at DESC) covers the recipient_id filter in the correlated subquery at family archive scale.
  • No infrastructure changes — Docker Compose, CI pipeline, and environment variables are untouched.
  • OpenAPI regen handledapi.ts updated manually (consistent with what generate:api would produce).

One note for future consideration

The subquery filters notifications by recipient_id AND reference_id. The current index is on (recipient_id, read, created_at DESC)reference_id is not indexed. At current family archive scale (tens of notifications per user), the recipient_id prefix reduces the scan to one user's rows and the sequential scan over those rows is fast.

If notification volume grows past ~500 per user, the partial index proposed in issue #295 would become worthwhile:

-- V50__add_notifications_reference_id_index.sql (when volume justifies it)
CREATE INDEX idx_notifications_reference_id ON notifications(recipient_id, reference_id)
    WHERE type = 'REPLY';

Not a blocker — flagging as a known future improvement when/if the scale requires it.

## ⚙️ Tobias Wendt — DevOps & Platform Engineer **Verdict: ✅ Approved** ### What I checked Migration necessity, infrastructure changes, CI impact, index implications. --- ### LGTM notes - **No Flyway migration required** — this is a pure query change. The `notifications` table (created in V16) and its `recipient_id` / `reference_id` columns are already present. The `idx_notifications_recipient` index on `(recipient_id, read, created_at DESC)` covers the `recipient_id` filter in the correlated subquery at family archive scale. ✅ - **No infrastructure changes** — Docker Compose, CI pipeline, and environment variables are untouched. ✅ - **OpenAPI regen handled** — `api.ts` updated manually (consistent with what `generate:api` would produce). ✅ --- ### One note for future consideration The subquery filters `notifications` by `recipient_id` AND `reference_id`. The current index is on `(recipient_id, read, created_at DESC)` — `reference_id` is not indexed. At current family archive scale (tens of notifications per user), the `recipient_id` prefix reduces the scan to one user's rows and the sequential scan over those rows is fast. If notification volume grows past ~500 per user, the partial index proposed in issue #295 would become worthwhile: ```sql -- V50__add_notifications_reference_id_index.sql (when volume justifies it) CREATE INDEX idx_notifications_reference_id ON notifications(recipient_id, reference_id) WHERE type = 'REPLY'; ``` Not a blocker — flagging as a known future improvement when/if the scale requires it.
Author
Owner

🎨 Leonie Voss — UX Designer & Accessibility Strategist

Verdict: Approved

What I checked

UI impact of the data fix, rendering of COMMENT_ADDED events in the new "Für dich" context, brand compliance, accessibility.


LGTM notes

This PR is a data correctness fix — no new components, no layout changes, no color or typography changes. The COMMENT_ADDED event type already renders in the Chronik (alle and kommentare filters), so the only UX impact is that these events now also appear under fuer-dich. The existing ChronikTimeline row component handles them correctly: actor avatar, document title link, timestamp. No new visual treatment is needed.

The filterFeed() function extraction in feedFilters.ts improves the code structure without any user-visible change.


One minor point to verify manually

Before merge, do a quick manual check that a COMMENT_ADDED row in the fuer-dich context renders acceptably at 320px. The document title could be longer than typical in this filter (since reply events can appear for any document), and the ChronikTimeline component's title truncation should handle it. This was flagged in the original issue review as a "quick check, not a new concern" and the existing styling should handle it — just worth a visual confirmation.

## 🎨 Leonie Voss — UX Designer & Accessibility Strategist **Verdict: ✅ Approved** ### What I checked UI impact of the data fix, rendering of COMMENT_ADDED events in the new "Für dich" context, brand compliance, accessibility. --- ### LGTM notes This PR is a data correctness fix — no new components, no layout changes, no color or typography changes. The `COMMENT_ADDED` event type already renders in the Chronik (`alle` and `kommentare` filters), so the only UX impact is that these events now also appear under `fuer-dich`. The existing `ChronikTimeline` row component handles them correctly: actor avatar, document title link, timestamp. No new visual treatment is needed. ✅ The `filterFeed()` function extraction in `feedFilters.ts` improves the code structure without any user-visible change. ✅ --- ### One minor point to verify manually Before merge, do a quick manual check that a `COMMENT_ADDED` row in the `fuer-dich` context renders acceptably at **320px**. The document title could be longer than typical in this filter (since reply events can appear for any document), and the `ChronikTimeline` component's title truncation should handle it. This was flagged in the original issue review as a "quick check, not a new concern" and the existing styling should handle it — just worth a visual confirmation.
marcel added 4 commits 2026-04-20 22:17:26 +02:00
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor(audit): document MIN invariant in aggregated CTE comment
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m37s
CI / OCR Service Tests (push) Successful in 32s
CI / Backend Unit Tests (push) Failing after 2m53s
CI / Unit & Component Tests (pull_request) Failing after 2m36s
CI / OCR Service Tests (pull_request) Successful in 31s
CI / Backend Unit Tests (pull_request) Failing after 2m48s
47f20c17c5
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

Review concerns addressed

All four concerns from the previous review round have been resolved in commits 2104658c47f20c17:

Concern Reviewer(s) Commit Resolution
Hand-rolled JSON in insertAuditEvent @Nora, @Sara, @Felix 2104658c Replaced string concat with ObjectMapper.writeValueAsString(payload) — already on classpath
Missing cross-user IDOR scoping test @Nora, @Sara 8726b964 Added youParticipated_is_false_when_reply_notification_belongs_to_other_user — inserts REPLY notification for OTHER_USER_ID, asserts USER_ID's feed returns false
Missing no-commentId graceful degradation test @Sara ba54cd6d Added youParticipated_is_false_when_comment_added_has_no_commentId_in_payload — confirms NULL payload->>'commentId' yields false via SQL NULL semantics
MIN(payload::text)::jsonb has no invariant comment @Markus, @Felix 47f20c17 Added inline SQL comment explaining the is_new_session=1 invariant that makes MIN safe

Note on the SQL comment: an initial version used event's (apostrophe) which caused Spring Data JPA's text-block parser to reject the query at startup (IllegalArgumentException: The string <WITH events AS ...>). Fixed by removing the apostrophe from the comment text.

Test counts after these changes:

  • Backend: 1198 tests, 0 failures (./mvnw test)
  • Frontend: 318 tests, 0 failures (vitest run --project=server)
## Review concerns addressed All four concerns from the previous review round have been resolved in commits `2104658c`–`47f20c17`: | Concern | Reviewer(s) | Commit | Resolution | |---|---|---|---| | Hand-rolled JSON in `insertAuditEvent` | @Nora, @Sara, @Felix | `2104658c` | Replaced string concat with `ObjectMapper.writeValueAsString(payload)` — already on classpath | | Missing cross-user IDOR scoping test | @Nora, @Sara | `8726b964` | Added `youParticipated_is_false_when_reply_notification_belongs_to_other_user` — inserts REPLY notification for `OTHER_USER_ID`, asserts `USER_ID`'s feed returns `false` | | Missing no-commentId graceful degradation test | @Sara | `ba54cd6d` | Added `youParticipated_is_false_when_comment_added_has_no_commentId_in_payload` — confirms NULL `payload->>'commentId'` yields `false` via SQL NULL semantics | | `MIN(payload::text)::jsonb` has no invariant comment | @Markus, @Felix | `47f20c17` | Added inline SQL comment explaining the `is_new_session=1` invariant that makes `MIN` safe | **Note on the SQL comment:** an initial version used `event's` (apostrophe) which caused Spring Data JPA's text-block parser to reject the query at startup (`IllegalArgumentException: The string <WITH events AS ...>`). Fixed by removing the apostrophe from the comment text. **Test counts after these changes:** - Backend: 1198 tests, 0 failures (`./mvnw test`) - Frontend: 318 tests, 0 failures (`vitest run --project=server`)
Author
Owner

🏗️ Markus Keller — Senior Application Architect

Verdict: Approved

What I checked

Domain boundary correctness, CTE payload aggregation invariant, diff scope, layering.


Previous concerns — resolved

1. MIN(payload::text)::jsonb invariant comment

The comment is now in place:

-- COMMENT_ADDED/MENTION_CREATED always have is_new_session=1, so each group has one row and MIN collapses to that row payload
MIN(s.payload::text)::jsonb AS payload

A future developer changing session-marking logic for comment events will see this immediately. The event's apostrophe failure was caught and fixed — good catch on the Spring Data JPA text-block parser limitation.

2. Out-of-scope diff (issue #296 changes) — still cosmetic

The DocumentController, IncompleteDocumentDTO, DocumentService, and related test changes are still in the diff. This is a base-branch calculation artifact (branch cut from b8ffd81b, those commits were already at 758c7087 on main). No regressions are introduced — all those changes are already merged. The diff just doesn't match the code review scope cleanly.

This is the same cosmetic note from the previous round. It's not a blocker and cannot be fixed without a rebase. Consider rebasing onto current main before merge to produce a clean diff that matches the PR description exactly.


What's done well

  • MIN invariant is now documented. Future developers won't break youParticipated unknowingly.
  • CAST(:currentUserId AS uuid) remains the correct Spring Data JPA workaround — the comment in the SQL correctly documents the contract it relies on.
  • The full change is a pure query addition — no schema migration, no new tables, no new indexes required at current scale.
  • DashboardService.getActivity() remains a pure mapper with no business logic leak.
## 🏗️ Markus Keller — Senior Application Architect **Verdict: ✅ Approved** ### What I checked Domain boundary correctness, CTE payload aggregation invariant, diff scope, layering. --- ### Previous concerns — resolved **1. `MIN(payload::text)::jsonb` invariant comment** ✅ The comment is now in place: ```sql -- COMMENT_ADDED/MENTION_CREATED always have is_new_session=1, so each group has one row and MIN collapses to that row payload MIN(s.payload::text)::jsonb AS payload ``` A future developer changing session-marking logic for comment events will see this immediately. The `event's` apostrophe failure was caught and fixed — good catch on the Spring Data JPA text-block parser limitation. **2. Out-of-scope diff (issue #296 changes)** — still cosmetic The `DocumentController`, `IncompleteDocumentDTO`, `DocumentService`, and related test changes are still in the diff. This is a base-branch calculation artifact (branch cut from `b8ffd81b`, those commits were already at `758c7087` on main). No regressions are introduced — all those changes are already merged. The diff just doesn't match the code review scope cleanly. This is the same cosmetic note from the previous round. It's not a blocker and cannot be fixed without a rebase. Consider rebasing onto current main before merge to produce a clean diff that matches the PR description exactly. --- ### What's done well - MIN invariant is now documented. Future developers won't break `youParticipated` unknowingly. - `CAST(:currentUserId AS uuid)` remains the correct Spring Data JPA workaround — the comment in the SQL correctly documents the contract it relies on. - The full change is a pure query addition — no schema migration, no new tables, no new indexes required at current scale. - `DashboardService.getActivity()` remains a pure mapper with no business logic leak.
Author
Owner

🔒 Nora Steiner — Application Security Engineer

Verdict: Approved

What I checked

IDOR scoping, SQL injection, cross-user notification leakage, JSON serialization safety in tests.


Previous concerns — resolved

1. Missing cross-user IDOR scoping test

The regression test is now in place:

void youParticipated_is_false_when_reply_notification_belongs_to_other_user()

This test inserts a REPLY notification for OTHER_USER_ID against the same commentId, then queries as USER_ID and asserts isYouParticipated() returns false. The recipient_id = CAST(:currentUserId AS uuid) predicate is now machine-verified against the specific IDOR pattern I was watching for.

2. Hand-rolled JSON serialization

The insertAuditEvent helper now uses ObjectMapper.writeValueAsString(payload):

private static final ObjectMapper MAPPER = new ObjectMapper();
// ...
payloadJson = payload.isEmpty() ? null : MAPPER.writeValueAsString(payload);

A RuntimeException wraps the checked JsonProcessingException — this is the correct pattern for a test helper where JsonProcessingException is truly unexpected.


Security posture summary

  • No IDOR: recipient_id = CAST(:currentUserId AS uuid) correctly scopes youParticipated to the querying user. Machine-verified by three negative tests.
  • No SQL injection: all parameters use Spring Data JPA named parameters (CAST(:currentUserId AS uuid), :limit).
  • No data leakage: youParticipated is a boolean; no notification content is exposed to the feed consumer.
  • The reference_id = (ag.payload->>'commentId')::uuid cast ensures UUID comparison is type-safe, not string comparison.
## 🔒 Nora Steiner — Application Security Engineer **Verdict: ✅ Approved** ### What I checked IDOR scoping, SQL injection, cross-user notification leakage, JSON serialization safety in tests. --- ### Previous concerns — resolved **1. Missing cross-user IDOR scoping test** ✅ The regression test is now in place: ```java void youParticipated_is_false_when_reply_notification_belongs_to_other_user() ``` This test inserts a REPLY notification for `OTHER_USER_ID` against the same `commentId`, then queries as `USER_ID` and asserts `isYouParticipated()` returns `false`. The `recipient_id = CAST(:currentUserId AS uuid)` predicate is now machine-verified against the specific IDOR pattern I was watching for. **2. Hand-rolled JSON serialization** ✅ The `insertAuditEvent` helper now uses `ObjectMapper.writeValueAsString(payload)`: ```java private static final ObjectMapper MAPPER = new ObjectMapper(); // ... payloadJson = payload.isEmpty() ? null : MAPPER.writeValueAsString(payload); ``` A `RuntimeException` wraps the checked `JsonProcessingException` — this is the correct pattern for a test helper where `JsonProcessingException` is truly unexpected. --- ### Security posture summary - No IDOR: `recipient_id = CAST(:currentUserId AS uuid)` correctly scopes `youParticipated` to the querying user. Machine-verified by three negative tests. - No SQL injection: all parameters use Spring Data JPA named parameters (`CAST(:currentUserId AS uuid)`, `:limit`). - No data leakage: `youParticipated` is a boolean; no notification content is exposed to the feed consumer. - The `reference_id = (ag.payload->>'commentId')::uuid` cast ensures UUID comparison is type-safe, not string comparison.
Author
Owner

🧪 Sara Holt — QA Engineer & Test Strategist

Verdict: Approved

What I checked

TDD coverage, test helper quality, all three previously raised concerns, test naming, edge case coverage.


Previous concerns — resolved

1. Cross-user scoping test
youParticipated_is_false_when_reply_notification_belongs_to_other_user — the key negative case is now in the suite. Arrangement is clear: insert notification for OTHER_USER_ID, assert USER_ID's feed returns false.

2. Hand-rolled JSON in test helper
ObjectMapper.writeValueAsString(payload) replaces string concatenation. The try/catch wrapping JsonProcessingException into RuntimeException is the right choice for a test helper — test failures will be assertion failures, not confusing JSONB parse errors from PostgreSQL.

3. No-commentId graceful degradation test
youParticipated_is_false_when_comment_added_has_no_commentId_in_payload — the empty payload case is now documented. SQL NULL semantics (ag.payload->>'commentId' → NULL → reference_id = NULL → UNKNOWN → EXISTS returns false) are verified by a real database test.


Final test count

  • AuditLogQueryRepositoryRolledUpTest: 12 tests, all named as sentences, all passing.
  • feedFilters.test.ts: 13 tests covering all 5 filter values with positive and negative cases.
  • DashboardServiceTest: mock correctly implements the new isYouParticipated() interface method.

The test suite now covers: true positive, false negative (no notification), false negative (notification for wrong user), and false negative (missing payload key). That is complete behavioral coverage for this feature.

No concerns remaining.

## 🧪 Sara Holt — QA Engineer & Test Strategist **Verdict: ✅ Approved** ### What I checked TDD coverage, test helper quality, all three previously raised concerns, test naming, edge case coverage. --- ### Previous concerns — resolved **1. Cross-user scoping test** ✅ `youParticipated_is_false_when_reply_notification_belongs_to_other_user` — the key negative case is now in the suite. Arrangement is clear: insert notification for `OTHER_USER_ID`, assert `USER_ID`'s feed returns false. **2. Hand-rolled JSON in test helper** ✅ `ObjectMapper.writeValueAsString(payload)` replaces string concatenation. The `try/catch` wrapping `JsonProcessingException` into `RuntimeException` is the right choice for a test helper — test failures will be assertion failures, not confusing JSONB parse errors from PostgreSQL. **3. No-commentId graceful degradation test** ✅ `youParticipated_is_false_when_comment_added_has_no_commentId_in_payload` — the empty payload case is now documented. SQL NULL semantics (`ag.payload->>'commentId'` → NULL → `reference_id = NULL` → UNKNOWN → EXISTS returns false) are verified by a real database test. --- ### Final test count - `AuditLogQueryRepositoryRolledUpTest`: **12 tests**, all named as sentences, all passing. - `feedFilters.test.ts`: **13 tests** covering all 5 filter values with positive and negative cases. - `DashboardServiceTest`: mock correctly implements the new `isYouParticipated()` interface method. The test suite now covers: true positive, false negative (no notification), false negative (notification for wrong user), and false negative (missing payload key). That is complete behavioral coverage for this feature. No concerns remaining.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: Approved

What I checked

TDD discipline, SQL invariant comment, JSON helper, naming, function size, frontend patterns.


Previous concerns — resolved

1. MIN invariant comment

The comment reads:

-- COMMENT_ADDED/MENTION_CREATED always have is_new_session=1, so each group has one row and MIN collapses to that row payload

This explains the WHY (the is_new_session invariant), not the WHAT (MIN is a min function). Exactly what the code style guide calls for. The Spring Data JPA apostrophe issue was also caught and fixed before committing — good discipline.

2. Jackson ObjectMapper in test helper

Static final MAPPER instance, writeValueAsString called per-invocation, JsonProcessingException wrapped in RuntimeException. Clean, correct, no state shared across tests.


Clean code notes

  • filterFeed() extraction remains the highlight of this PR. One line in the component ($derived(filterFeed(...))), a pure function in a dedicated file, 13 unit tests. This is exactly the "extract business logic from template markup" principle in action.
  • isYouParticipated() and isYouMentioned() are consistent boolean naming conventions for the projection interface.
  • The three new test names all read as sentences describing precise behaviors — youParticipated_is_false_when_comment_added_has_no_commentId_in_payload leaves no ambiguity about what is being proven.

LGTM. Ready to merge.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: ✅ Approved** ### What I checked TDD discipline, SQL invariant comment, JSON helper, naming, function size, frontend patterns. --- ### Previous concerns — resolved **1. `MIN` invariant comment** ✅ The comment reads: ```sql -- COMMENT_ADDED/MENTION_CREATED always have is_new_session=1, so each group has one row and MIN collapses to that row payload ``` This explains the WHY (the is_new_session invariant), not the WHAT (MIN is a min function). Exactly what the code style guide calls for. The Spring Data JPA apostrophe issue was also caught and fixed before committing — good discipline. **2. Jackson ObjectMapper in test helper** ✅ Static final `MAPPER` instance, `writeValueAsString` called per-invocation, `JsonProcessingException` wrapped in `RuntimeException`. Clean, correct, no state shared across tests. --- ### Clean code notes - `filterFeed()` extraction remains the highlight of this PR. One line in the component (`$derived(filterFeed(...))`), a pure function in a dedicated file, 13 unit tests. This is exactly the "extract business logic from template markup" principle in action. - `isYouParticipated()` and `isYouMentioned()` are consistent boolean naming conventions for the projection interface. - The three new test names all read as sentences describing precise behaviors — `youParticipated_is_false_when_comment_added_has_no_commentId_in_payload` leaves no ambiguity about what is being proven. LGTM. Ready to merge.
Author
Owner

⚙️ Tobias Wendt — DevOps & Platform Engineer

Verdict: Approved

What I checked

Migration necessity, infrastructure impact, CI, index implications, test counts.


LGTM notes

  • No new migrations required — this is still a pure query change. All referenced tables (notifications, audit_log) and their columns are already present.
  • No infrastructure changes — Docker Compose, CI pipeline, environment variables untouched.
  • Test counts confirmed: backend 1198 tests, frontend 318 tests, all passing per the completion comment.
  • OpenAPI types updated manuallyyouParticipated: boolean in api.ts matches what generate:api would produce.

The idx_notifications_recipient index on (recipient_id, read, created_at DESC) continues to cover the recipient_id filter in the correlated subquery. At family archive scale, no additional indexing is needed. The future (recipient_id, reference_id) WHERE type = 'REPLY' index remains a valid deferred improvement if notification volume grows. No action needed now.

## ⚙️ Tobias Wendt — DevOps & Platform Engineer **Verdict: ✅ Approved** ### What I checked Migration necessity, infrastructure impact, CI, index implications, test counts. --- ### LGTM notes - **No new migrations required** — this is still a pure query change. All referenced tables (`notifications`, `audit_log`) and their columns are already present. ✅ - **No infrastructure changes** — Docker Compose, CI pipeline, environment variables untouched. ✅ - **Test counts confirmed**: backend 1198 tests, frontend 318 tests, all passing per the completion comment. ✅ - **OpenAPI types updated manually** — `youParticipated: boolean` in `api.ts` matches what `generate:api` would produce. ✅ The `idx_notifications_recipient` index on `(recipient_id, read, created_at DESC)` continues to cover the `recipient_id` filter in the correlated subquery. At family archive scale, no additional indexing is needed. The future `(recipient_id, reference_id) WHERE type = 'REPLY'` index remains a valid deferred improvement if notification volume grows. No action needed now.
Author
Owner

🎨 Leonie Voss — UX Designer & Accessibility Strategist

Verdict: Approved

What I checked

UI impact, brand compliance, accessibility, rendering of newly surfaced COMMENT_ADDED events in fuer-dich context.


LGTM notes

No UI changes in this round of commits. The new commits (2104658c47f20c17) are all backend test and SQL changes — no frontend components, no markup, no styles.

The earlier approval from the first review round stands:

  • COMMENT_ADDED events already render correctly in ChronikTimeline (actor avatar, document title link, timestamp).
  • filterFeed() extraction produced no visual change.
  • The fuer-dich predicate widening is a data correctness fix, not a UI change.

Manual check suggestion from the first round (320px rendering of COMMENT_ADDED in fuer-dich) is still worth a quick pass before merge, but no code change is required — the existing ChronikTimeline title truncation handles it.

## 🎨 Leonie Voss — UX Designer & Accessibility Strategist **Verdict: ✅ Approved** ### What I checked UI impact, brand compliance, accessibility, rendering of newly surfaced `COMMENT_ADDED` events in `fuer-dich` context. --- ### LGTM notes No UI changes in this round of commits. The new commits (`2104658c`–`47f20c17`) are all backend test and SQL changes — no frontend components, no markup, no styles. The earlier approval from the first review round stands: - `COMMENT_ADDED` events already render correctly in `ChronikTimeline` (actor avatar, document title link, timestamp). - `filterFeed()` extraction produced no visual change. - The `fuer-dich` predicate widening is a data correctness fix, not a UI change. Manual check suggestion from the first round (320px rendering of `COMMENT_ADDED` in `fuer-dich`) is still worth a quick pass before merge, but no code change is required — the existing `ChronikTimeline` title truncation handles it.
marcel merged commit 1fd2fab9a1 into main 2026-04-21 09:00:25 +02:00
marcel deleted branch feat/issue-295-you-participated 2026-04-21 09:00:27 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#297