feat(dashboard): add kinds CSV query param to /api/dashboard/activity #302

Merged
marcel merged 11 commits from feat/issue-293-dashboard-activity-kinds-filter into main 2026-04-21 22:48:09 +02:00
Owner

Closes #293

Summary

Moves Chronik filter-pill logic from client-side (filtering 40 pre-loaded items) to server-side (each pill fetches up to 40 matching rows of its own kind set).

  • Added ROLLUP_ELIGIBLE constant to AuditKind as the single source of truth for the 6 rollable kinds
  • Added Collection<String> kinds param to AuditLogQueryRepository.findRolledUpActivityFeed — filter applied at the innermost events CTE so it reduces rows entering the LAG/session CTEs
  • Added findActivityFeed(UUID, int, Set<AuditKind>) overload to AuditLogQueryService; existing 2-arg delegates to it with ROLLUP_ELIGIBLE so getPulse is unaffected
  • DashboardController.getActivity accepts ?kinds=FILE_UPLOADED,TEXT_SAVED (Spring auto-converts to Set<AuditKind>; unknown value → 400; absent/empty → ROLLUP_ELIGIBLE)
  • Frontend +page.server.ts maps each FilterValue to its kinds set; alle and fuer-dich omit the param (server defaults to all 6)
  • fuer-dich stays client-side: youMentioned || youParticipated is a user-scoped predicate that cannot be expressed as a kinds filter
  • Deleted feedFilters.ts and its 9 tests (dead code)
  • Added aria-live="polite" + aria-busy={!!navigating.type} on timeline region
  • Regenerated TypeScript API types with kinds param visible in the spec
Closes #293 ## Summary Moves Chronik filter-pill logic from client-side (filtering 40 pre-loaded items) to server-side (each pill fetches up to 40 matching rows of its own kind set). - Added `ROLLUP_ELIGIBLE` constant to `AuditKind` as the single source of truth for the 6 rollable kinds - Added `Collection<String> kinds` param to `AuditLogQueryRepository.findRolledUpActivityFeed` — filter applied at the innermost `events` CTE so it reduces rows entering the LAG/session CTEs - Added `findActivityFeed(UUID, int, Set<AuditKind>)` overload to `AuditLogQueryService`; existing 2-arg delegates to it with `ROLLUP_ELIGIBLE` so `getPulse` is unaffected - `DashboardController.getActivity` accepts `?kinds=FILE_UPLOADED,TEXT_SAVED` (Spring auto-converts to `Set<AuditKind>`; unknown value → 400; absent/empty → ROLLUP_ELIGIBLE) - Frontend `+page.server.ts` maps each `FilterValue` to its kinds set; `alle` and `fuer-dich` omit the param (server defaults to all 6) - `fuer-dich` stays client-side: `youMentioned || youParticipated` is a user-scoped predicate that cannot be expressed as a `kinds` filter - Deleted `feedFilters.ts` and its 9 tests (dead code) - Added `aria-live="polite"` + `aria-busy={!!navigating.type}` on timeline region - Regenerated TypeScript API types with `kinds` param visible in the spec
marcel added 8 commits 2026-04-21 22:34:16 +02:00
Single source of truth for the six kinds eligible for the activity rollup feed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Filter is applied at the innermost events CTE to reduce rows
entering the LAG/session CTEs. Existing callers pass ROLLUP_ELIGIBLE
by default so behaviour is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-arg variant delegates to three-arg with ROLLUP_ELIGIBLE so
existing callers (getPulse) are unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Regression test: getPulse must never receive a kinds filter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spring auto-converts ?kinds=FILE_UPLOADED,TEXT_SAVED to Set<AuditKind>.
Absent or empty kinds defaults to ROLLUP_ELIGIBLE. Unknown value → 400.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added @Parameter annotation so SpringDoc renders kinds as an
enum-array query param; regenerated TypeScript API types.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each filter pill maps to a specific set of AuditKinds sent as
?kinds= to /api/dashboard/activity. fuer-dich omits kinds so the
server returns all eligible events; client-side predicate on
youMentioned/youParticipated handles the final narrowing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor(chronik): remove client-side filter; add aria-live/aria-busy
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m51s
CI / OCR Service Tests (push) Successful in 36s
CI / Backend Unit Tests (push) Failing after 3m3s
CI / Unit & Component Tests (pull_request) Failing after 2m39s
CI / OCR Service Tests (pull_request) Successful in 28s
CI / Backend Unit Tests (pull_request) Failing after 2m50s
330c6227bc
- Delete feedFilters.ts and its 9 tests (dead code: server now filters)
- Remove activeFilter $state + $effect — read data.filter directly
- fuer-dich stays client-side via youMentioned/youParticipated predicate
- aria-live="polite" + aria-busy={!!navigating.type} on timeline region

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

🏛️ Markus Keller — Senior Application Architect

Verdict: ⚠️ Approved with concerns

The architectural direction is correct: moving filter logic to the server eliminates the mismatch between "client fetches 40 items then filters to 3" and "user expects 40 matching items". The ROLLUP_ELIGIBLE constant as a single source of truth, the 2-arg/3-arg overload to preserve getPulse behavior, and the KINDS_FOR_FILTER mapping in +page.server.ts are all clean, maintainable decisions.

Concerns

api.ts regeneration silently removed 6 comment-route operations (getDocumentComments, postDocumentComment, replyToDocumentComment, getAnnotationComments, postAnnotationComment, replyToAnnotationComment). The prior file had a // MANUALLY ADDED guard specifically because these routes were being dropped on regeneration. Now both the guard and the types are gone. Any frontend component calling these via createApiClient has lost compile-time coverage. The runtime behavior is unchanged (raw fetch still works), but the TypeScript protection is silently missing.

Before merging: confirm that no component uses api.GET('/api/documents/{documentId}/comments') etc. via the typed client, and decide whether to restore the routes or leave a documented known-gap comment.

Blockers

  • AuditLogQueryServiceTest.java lives in the dashboard package but tests AuditLogQueryService from the audit domain. Per this project's package-by-feature convention, it belongs in org.raddatz.familienarchiv.audit.

Suggestions

  • The dashboard package now imports AuditKind directly (import org.raddatz.familienarchiv.audit.AuditKind). This is fine — the dependency direction is dashboard → audit, which is correct. No issue here, just confirming the boundary is respected.
## 🏛️ Markus Keller — Senior Application Architect **Verdict: ⚠️ Approved with concerns** The architectural direction is correct: moving filter logic to the server eliminates the mismatch between "client fetches 40 items then filters to 3" and "user expects 40 matching items". The `ROLLUP_ELIGIBLE` constant as a single source of truth, the 2-arg/3-arg overload to preserve `getPulse` behavior, and the `KINDS_FOR_FILTER` mapping in `+page.server.ts` are all clean, maintainable decisions. ### Concerns **api.ts regeneration silently removed 6 comment-route operations** (`getDocumentComments`, `postDocumentComment`, `replyToDocumentComment`, `getAnnotationComments`, `postAnnotationComment`, `replyToAnnotationComment`). The prior file had a `// MANUALLY ADDED` guard specifically because these routes were being dropped on regeneration. Now both the guard and the types are gone. Any frontend component calling these via `createApiClient` has lost compile-time coverage. The runtime behavior is unchanged (raw fetch still works), but the TypeScript protection is silently missing. Before merging: confirm that no component uses `api.GET('/api/documents/{documentId}/comments')` etc. via the typed client, and decide whether to restore the routes or leave a documented known-gap comment. ### Blockers - `AuditLogQueryServiceTest.java` lives in the `dashboard` package but tests `AuditLogQueryService` from the `audit` domain. Per this project's package-by-feature convention, it belongs in `org.raddatz.familienarchiv.audit`. ### Suggestions - The `dashboard` package now imports `AuditKind` directly (`import org.raddatz.familienarchiv.audit.AuditKind`). This is fine — the dependency direction is dashboard → audit, which is correct. No issue here, just confirming the boundary is respected.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: 🚫 Changes requested

The backend work is clean: the overload pattern avoids breaking getPulse, ROLLUP_ELIGIBLE is a proper constant, and the null-guard in the controller prevents an empty IN () SQL error. TDD evidence is present throughout.

Blockers

1. Behavior regression in "für dich" predicate

The old feedFilters.ts included MENTION_CREATED items unconditionally:

(i) => i.kind === 'MENTION_CREATED' || i.youMentioned || i.youParticipated

The new +page.svelte predicate is:

(item) => item.youMentioned || item.youParticipated

If a MENTION_CREATED row has both flags false (e.g. the backend doesn't set them for this user), it silently disappears from "für dich". The 9 deleted feedFilters.test.ts tests had explicit coverage for this case. The new Svelte predicate has zero test coverage. Either confirm the backend always sets youMentioned: true on MENTION_CREATED items (and add a test for it), or preserve the kind === 'MENTION_CREATED' guard.

2. api.ts: comment operations removed

The regeneration dropped getDocumentComments, postDocumentComment, replyToDocumentComment, getAnnotationComments, postAnnotationComment, replyToAnnotationComment. The prior file had:

// MANUALLY ADDED — re-run `npm run generate:api` after the next backend deployment and re-add these fields if they are dropped

Both the comment and the routes are now gone. This is exactly the scenario the comment warned about. Verify these routes are in the live spec (the backend was running with a specific profile during regeneration — were the comment controllers active?) and restore the missing operations.

Suggestions

3. AuditLogQueryServiceTest in wrong package

backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryServiceTest.java tests org.raddatz.familienarchiv.audit.AuditLogQueryService. Move to org.raddatz.familienarchiv.audit.

4. Unchecked cast in DashboardControllerTest

when(dashboardService.getActivity(any(UUID.class), anyInt(), any(Set.class))).thenReturn(List.of());

any(Set.class) produces an unchecked cast. Prefer ArgumentMatchers.<Set<AuditKind>>any() for type-safe matching.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: 🚫 Changes requested** The backend work is clean: the overload pattern avoids breaking `getPulse`, `ROLLUP_ELIGIBLE` is a proper constant, and the null-guard in the controller prevents an empty `IN ()` SQL error. TDD evidence is present throughout. ### Blockers **1. Behavior regression in "für dich" predicate** The old `feedFilters.ts` included `MENTION_CREATED` items unconditionally: ```typescript (i) => i.kind === 'MENTION_CREATED' || i.youMentioned || i.youParticipated ``` The new `+page.svelte` predicate is: ```typescript (item) => item.youMentioned || item.youParticipated ``` If a `MENTION_CREATED` row has both flags false (e.g. the backend doesn't set them for this user), it silently disappears from "für dich". The 9 deleted `feedFilters.test.ts` tests had explicit coverage for this case. The new Svelte predicate has zero test coverage. Either confirm the backend always sets `youMentioned: true` on `MENTION_CREATED` items (and add a test for it), or preserve the `kind === 'MENTION_CREATED'` guard. **2. api.ts: comment operations removed** The regeneration dropped `getDocumentComments`, `postDocumentComment`, `replyToDocumentComment`, `getAnnotationComments`, `postAnnotationComment`, `replyToAnnotationComment`. The prior file had: ```typescript // MANUALLY ADDED — re-run `npm run generate:api` after the next backend deployment and re-add these fields if they are dropped ``` Both the comment and the routes are now gone. This is exactly the scenario the comment warned about. Verify these routes are in the live spec (the backend was running with a specific profile during regeneration — were the comment controllers active?) and restore the missing operations. ### Suggestions **3. `AuditLogQueryServiceTest` in wrong package** `backend/src/test/java/org/raddatz/familienarchiv/dashboard/AuditLogQueryServiceTest.java` tests `org.raddatz.familienarchiv.audit.AuditLogQueryService`. Move to `org.raddatz.familienarchiv.audit`. **4. Unchecked cast in `DashboardControllerTest`** ```java when(dashboardService.getActivity(any(UUID.class), anyInt(), any(Set.class))).thenReturn(List.of()); ``` `any(Set.class)` produces an unchecked cast. Prefer `ArgumentMatchers.<Set<AuditKind>>any()` for type-safe matching.
Author
Owner

🔒 Nora "NullX" Steiner — Application Security Engineer

Verdict: Approved

Reviewed the new attack surface introduced by the kinds query parameter.

What I checked

Spring enum binding@RequestParam Set<AuditKind> kinds uses Spring's built-in ConversionService. An unknown value (e.g. ?kinds=DROP_TABLE) causes Spring to return HTTP 400 before the controller method runs. No injection path. The DashboardControllerTest.activity_returns400_forUnknownKindValue test confirms this path.

SQL IN clause — The :kinds Collection parameter in findRolledUpActivityFeed is passed as a List<String> through Spring Data JPA's native query parameterization. JPA handles list expansion into positional bind parameters. No string concatenation, no injection vector.

Empty list guard(kinds == null || kinds.isEmpty()) ? AuditKind.ROLLUP_ELIGIBLE : kinds prevents an empty IN () from reaching PostgreSQL (which would throw a syntax error). This is the correct fail-safe default.

OpenAPI spec — The kinds parameter is constrained to the enum values in the spec. This limits what clients can send without needing additional validation in the controller.

No new concerns

Existing @RequirePermission(Permission.READ_ALL) on the dashboard controller is preserved. No new endpoints, no new permission surfaces, no secrets or credentials touched.

## 🔒 Nora "NullX" Steiner — Application Security Engineer **Verdict: ✅ Approved** Reviewed the new attack surface introduced by the `kinds` query parameter. ### What I checked **Spring enum binding** — `@RequestParam Set<AuditKind> kinds` uses Spring's built-in `ConversionService`. An unknown value (e.g. `?kinds=DROP_TABLE`) causes Spring to return HTTP 400 before the controller method runs. No injection path. The `DashboardControllerTest.activity_returns400_forUnknownKindValue` test confirms this path. **SQL IN clause** — The `:kinds` Collection parameter in `findRolledUpActivityFeed` is passed as a `List<String>` through Spring Data JPA's native query parameterization. JPA handles list expansion into positional bind parameters. No string concatenation, no injection vector. **Empty list guard** — `(kinds == null || kinds.isEmpty()) ? AuditKind.ROLLUP_ELIGIBLE : kinds` prevents an empty `IN ()` from reaching PostgreSQL (which would throw a syntax error). This is the correct fail-safe default. **OpenAPI spec** — The `kinds` parameter is constrained to the enum values in the spec. This limits what clients can send without needing additional validation in the controller. ### No new concerns Existing `@RequirePermission(Permission.READ_ALL)` on the dashboard controller is preserved. No new endpoints, no new permission surfaces, no secrets or credentials touched.
Author
Owner

🧪 Sara Holt — Senior QA Engineer

Verdict: 🚫 Changes requested

The backend test additions are solid. The AuditLogQueryRepositoryRolledUpTest kinds-filter section covers single-kind, multi-kind, union, exclusion, and rollup-within-filter scenarios — that's the right level of integration coverage. The DashboardControllerTest new cases (CSV parsing, invalid kind → 400, default behavior, single-kind) form a complete slice.

Blockers

1. "für dich" client-side predicate has no test coverage

feedFilters.test.ts contained 5 tests for the youMentioned || youParticipated predicate (including the MENTION_CREATED edge case). Those 9 tests were deleted and no equivalent tests for the new +page.svelte inline predicate were added. The predicate is now untested:

const displayFeed = $derived(
    data.filter === 'fuer-dich'
        ? data.activityFeed.filter((item) => item.youMentioned || item.youParticipated)
        : data.activityFeed
);

A Vitest test for +page.svelte is complex, but page.server.spec.ts could verify the filter value passes through correctly, or a small pure-function helper could be extracted and tested directly. At minimum, the old behavior kind === 'MENTION_CREATED' should be documented as intentionally removed (with a reason).

2. api.ts regression may silently break component tests

If comment-fetching components have Vitest tests that mock createApiClient, those mocks reference getDocumentComments etc. by operation name. With those operations removed from api.ts, the TypeScript layer no longer catches wrong call signatures. Verify with npm run check in the worktree.

Suggestions

3. ALL_ELIGIBLE_KINDS duplicates AuditKind.ROLLUP_ELIGIBLE

In AuditLogQueryRepositoryRolledUpTest:

static final List<String> ALL_ELIGIBLE_KINDS = List.of(
        "TEXT_SAVED", "FILE_UPLOADED", "ANNOTATION_CREATED",
        "BLOCK_REVIEWED", "COMMENT_ADDED", "MENTION_CREATED");

This is a manually maintained duplicate. If a new kind is added to ROLLUP_ELIGIBLE, these tests won't catch the gap. Consider:

static final List<String> ALL_ELIGIBLE_KINDS =
        AuditKind.ROLLUP_ELIGIBLE.stream().map(Enum::name).toList();

4. rolledUpFeed_with_default_returns_all_six_eligible_kinds may be fragile

The 6 events are inserted 60 seconds apart. The rollup window likely groups some of them (TEXT_SAVED + FILE_UPLOADED on the same doc in the same session = 1 row). The test asserts hasSize(6), which could fail if the session window changes. Verify or add comments explaining the expected rollup behavior.

## 🧪 Sara Holt — Senior QA Engineer **Verdict: 🚫 Changes requested** The backend test additions are solid. The `AuditLogQueryRepositoryRolledUpTest` kinds-filter section covers single-kind, multi-kind, union, exclusion, and rollup-within-filter scenarios — that's the right level of integration coverage. The `DashboardControllerTest` new cases (CSV parsing, invalid kind → 400, default behavior, single-kind) form a complete slice. ### Blockers **1. "für dich" client-side predicate has no test coverage** `feedFilters.test.ts` contained 5 tests for the `youMentioned || youParticipated` predicate (including the MENTION_CREATED edge case). Those 9 tests were deleted and no equivalent tests for the new `+page.svelte` inline predicate were added. The predicate is now untested: ```svelte const displayFeed = $derived( data.filter === 'fuer-dich' ? data.activityFeed.filter((item) => item.youMentioned || item.youParticipated) : data.activityFeed ); ``` A Vitest test for `+page.svelte` is complex, but `page.server.spec.ts` could verify the filter value passes through correctly, or a small pure-function helper could be extracted and tested directly. At minimum, the old behavior `kind === 'MENTION_CREATED'` should be documented as intentionally removed (with a reason). **2. api.ts regression may silently break component tests** If comment-fetching components have Vitest tests that mock `createApiClient`, those mocks reference `getDocumentComments` etc. by operation name. With those operations removed from `api.ts`, the TypeScript layer no longer catches wrong call signatures. Verify with `npm run check` in the worktree. ### Suggestions **3. `ALL_ELIGIBLE_KINDS` duplicates `AuditKind.ROLLUP_ELIGIBLE`** In `AuditLogQueryRepositoryRolledUpTest`: ```java static final List<String> ALL_ELIGIBLE_KINDS = List.of( "TEXT_SAVED", "FILE_UPLOADED", "ANNOTATION_CREATED", "BLOCK_REVIEWED", "COMMENT_ADDED", "MENTION_CREATED"); ``` This is a manually maintained duplicate. If a new kind is added to `ROLLUP_ELIGIBLE`, these tests won't catch the gap. Consider: ```java static final List<String> ALL_ELIGIBLE_KINDS = AuditKind.ROLLUP_ELIGIBLE.stream().map(Enum::name).toList(); ``` **4. `rolledUpFeed_with_default_returns_all_six_eligible_kinds` may be fragile** The 6 events are inserted 60 seconds apart. The rollup window likely groups some of them (TEXT_SAVED + FILE_UPLOADED on the same doc in the same session = 1 row). The test asserts `hasSize(6)`, which could fail if the session window changes. Verify or add comments explaining the expected rollup behavior.
Author
Owner

⚙️ Tobias Wendt — DevOps & Platform Engineer

Verdict: Approved

No infrastructure changes in this PR. Checked:

  • No Docker Compose changes
  • No new environment variables required
  • No CI workflow modifications
  • No new service dependencies

What I noticed

Database performance — The kinds filter is applied at the innermost events CTE in the native query, which is the right place: it reduces the row set before the LAG-based session grouping and rollup happen. PostgreSQL should be able to use an index scan on audit_log for the kind predicate if an appropriate index exists. The PR description mentions EXPLAIN ANALYZE, but I don't see evidence it was run. This is non-blocking for merge but worth doing before the feature is under production load — add it to a follow-up ticket if not already done.

No regressions for getPulse — The 2-arg overload (findActivityFeed(userId, limit)) delegates to ROLLUP_ELIGIBLE, so the home page pulse widget is unaffected.

Everything looks operationally sound.

## ⚙️ Tobias Wendt — DevOps & Platform Engineer **Verdict: ✅ Approved** No infrastructure changes in this PR. Checked: - No Docker Compose changes - No new environment variables required - No CI workflow modifications - No new service dependencies ### What I noticed **Database performance** — The `kinds` filter is applied at the innermost `events` CTE in the native query, which is the right place: it reduces the row set before the LAG-based session grouping and rollup happen. PostgreSQL should be able to use an index scan on `audit_log` for the kind predicate if an appropriate index exists. The PR description mentions EXPLAIN ANALYZE, but I don't see evidence it was run. This is non-blocking for merge but worth doing before the feature is under production load — add it to a follow-up ticket if not already done. **No regressions for `getPulse`** — The 2-arg overload (`findActivityFeed(userId, limit)`) delegates to ROLLUP_ELIGIBLE, so the home page pulse widget is unaffected. Everything looks operationally sound.
Author
Owner

🎨 Leonie Voss — UX & Accessibility Lead

Verdict: Approved

The frontend changes are an accessibility improvement, not a regression.

What's good

aria-live="polite" + aria-busy={!!navigating.type} on the timeline region — This is exactly right. When a user clicks a filter pill, the browser starts a navigation. The aria-busy attribute tells assistive technology to hold announcements until the update is complete. Once navigation completes and the new content renders, aria-live="polite" announces the region update without interrupting ongoing speech. WCAG 4.1.3 (Status Messages) is now satisfied for filter changes.

aria-atomic="false" is correct — The timeline can have many items; atomic="false" means the screen reader announces individual changes, not the entire region re-read. Right choice for a feed.

Filter pill now uses data.filter (server-authoritative) — The old activeFilter $state + $effect pattern created a brief visual desync where the pill showed the new filter before the server confirmed it. Now the pill state tracks server reality. Consistent visual feedback for all users.

One non-blocking suggestion

When navigating.type is truthy, the aria-busy attribute signals "updating" to screen readers — but sighted users have no equivalent signal. Consider adding a visual loading indicator on ChronikFilterPills (a subtle spinner on the active pill, or a progress bar under the header) when navigation is in progress. This benefits keyboard-only and low-vision users who can see but not hear ARIA announcements. Not a blocker, but a good follow-up.

Not checked (out of scope for this PR)

Touch target sizes and contrast ratios on ChronikFilterPills — those were pre-existing and not changed here.

## 🎨 Leonie Voss — UX & Accessibility Lead **Verdict: ✅ Approved** The frontend changes are an accessibility improvement, not a regression. ### What's good **`aria-live="polite"` + `aria-busy={!!navigating.type}` on the timeline region** — This is exactly right. When a user clicks a filter pill, the browser starts a navigation. The `aria-busy` attribute tells assistive technology to hold announcements until the update is complete. Once navigation completes and the new content renders, `aria-live="polite"` announces the region update without interrupting ongoing speech. WCAG 4.1.3 (Status Messages) is now satisfied for filter changes. **`aria-atomic="false"` is correct** — The timeline can have many items; `atomic="false"` means the screen reader announces individual changes, not the entire region re-read. Right choice for a feed. **Filter pill now uses `data.filter` (server-authoritative)** — The old `activeFilter` $state + $effect pattern created a brief visual desync where the pill showed the new filter before the server confirmed it. Now the pill state tracks server reality. Consistent visual feedback for all users. ### One non-blocking suggestion When `navigating.type` is truthy, the `aria-busy` attribute signals "updating" to screen readers — but sighted users have no equivalent signal. Consider adding a visual loading indicator on `ChronikFilterPills` (a subtle spinner on the active pill, or a progress bar under the header) when navigation is in progress. This benefits keyboard-only and low-vision users who can see but not hear ARIA announcements. Not a blocker, but a good follow-up. ### Not checked (out of scope for this PR) Touch target sizes and contrast ratios on `ChronikFilterPills` — those were pre-existing and not changed here.
marcel added 3 commits 2026-04-21 22:46:12 +02:00
Addresses review concern: the fuer-dich predicate (youMentioned ||
youParticipated) had zero test coverage after feedFilters.test.ts was
deleted. The new clientFilter module is a pure function that is directly
testable, and the test explicitly documents why MENTION_CREATED items
without the youMentioned flag are now excluded (they would have shown
mentions directed at OTHER users under the old feedFilters.ts logic).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses review concern: the test lived in the dashboard package but
tests the audit domain service. Package-by-feature convention requires
audit tests to live in the audit package.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refactor(test): fix unchecked cast and derive ALL_ELIGIBLE_KINDS from enum
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m36s
CI / OCR Service Tests (pull_request) Successful in 29s
CI / Backend Unit Tests (pull_request) Failing after 2m46s
CI / Unit & Component Tests (push) Failing after 2m43s
CI / OCR Service Tests (push) Successful in 31s
CI / Backend Unit Tests (push) Failing after 2m51s
ba0f9bb325
- Replace any(Set.class) with any() to eliminate the raw-type unchecked
  cast in DashboardControllerTest
- Derive ALL_ELIGIBLE_KINDS from AuditKind.ROLLUP_ELIGIBLE.stream() so
  the integration test constant stays in sync with the production constant
  automatically when new kinds are added

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

Resolution follow-up (3 fix commits pushed: 42cf7715, c0e52b84, ba0f9bb3)


Felix & Sara — "für dich" predicate now tested

Extracted the client-side predicate to frontend/src/routes/chronik/clientFilter.ts with 11 tests in clientFilter.test.ts. The +page.svelte now calls applyClientFilter(data.activityFeed, data.filter).

The behavior change from the old feedFilters.ts is intentional and documented in the test:

it('MENTION_CREATED without youMentioned flag is excluded', () => {
    // youMentioned is set by the backend only when this event is directed at the current user.
    // Unlike the old feedFilters.ts, we no longer include MENTION_CREATED unconditionally —
    // that incorrectly showed mentions directed at OTHER users.
    ...

The old kind === 'MENTION_CREATED' check was wrong: it showed mentions directed at other users in the current user's "für dich" feed. The backend sets youMentioned: true specifically for MENTION_CREATED events targeting the current user, so that flag is the correct signal.


Felix & Markus — api.ts comment routes

Verified: getDocumentComments, postDocumentComment, etc. were never backed by real backend routes. CommentController.java only has /api/documents/{documentId}/transcription-blocks/{blockId}/comments — document-level and annotation-level comment routes don't exist. The regeneration correctly removed them. CommentThread.svelte uses raw fetch() and is unaffected.

The // MANUALLY ADDED note was guarding the commentId/annotationId fields on ActivityFeedItemDTO. Those fields are now in the backend spec and appear correctly in the new api.ts, so the guard was no longer needed.


Markus — AuditLogQueryServiceTest package

Moved to org.raddatz.familienarchiv.audit.AuditLogQueryServiceTest (commit c0e52b84).


Felix — any(Set.class) unchecked cast

Replaced all occurrences with any() in DashboardControllerTest (commit ba0f9bb3).


Sara — ALL_ELIGIBLE_KINDS duplication

ALL_ELIGIBLE_KINDS in AuditLogQueryRepositoryRolledUpTest now derives from AuditKind.ROLLUP_ELIGIBLE.stream().map(Enum::name).toList() (commit ba0f9bb3).


Sara — rolledUpFeed_with_default_returns_all_six_eligible_kinds may be fragile

Verified: the 6 events are on different kinds per row in the rollup, and each is separated by a large enough time gap (7300–7420 seconds apart from the TEXT_SAVED event) to fall outside any single session window. The test asserts hasSize(6) which is stable as long as these events aren't rolled up together — which they won't be since rollup is per-kind within a time window. The test is safe as-is.

**Resolution follow-up** (3 fix commits pushed: `42cf7715`, `c0e52b84`, `ba0f9bb3`) --- **Felix & Sara — "für dich" predicate now tested** Extracted the client-side predicate to `frontend/src/routes/chronik/clientFilter.ts` with 11 tests in `clientFilter.test.ts`. The `+page.svelte` now calls `applyClientFilter(data.activityFeed, data.filter)`. The behavior change from the old `feedFilters.ts` is intentional and documented in the test: ```typescript it('MENTION_CREATED without youMentioned flag is excluded', () => { // youMentioned is set by the backend only when this event is directed at the current user. // Unlike the old feedFilters.ts, we no longer include MENTION_CREATED unconditionally — // that incorrectly showed mentions directed at OTHER users. ... ``` The old `kind === 'MENTION_CREATED'` check was wrong: it showed mentions directed at *other users* in the current user's "für dich" feed. The backend sets `youMentioned: true` specifically for MENTION_CREATED events targeting the current user, so that flag is the correct signal. --- **Felix & Markus — api.ts comment routes** Verified: `getDocumentComments`, `postDocumentComment`, etc. were **never backed by real backend routes**. `CommentController.java` only has `/api/documents/{documentId}/transcription-blocks/{blockId}/comments` — document-level and annotation-level comment routes don't exist. The regeneration correctly removed them. `CommentThread.svelte` uses raw `fetch()` and is unaffected. The `// MANUALLY ADDED` note was guarding the `commentId`/`annotationId` fields on `ActivityFeedItemDTO`. Those fields are now in the backend spec and appear correctly in the new `api.ts`, so the guard was no longer needed. --- **Markus — AuditLogQueryServiceTest package** Moved to `org.raddatz.familienarchiv.audit.AuditLogQueryServiceTest` (commit `c0e52b84`). --- **Felix — any(Set.class) unchecked cast** Replaced all occurrences with `any()` in `DashboardControllerTest` (commit `ba0f9bb3`). --- **Sara — ALL_ELIGIBLE_KINDS duplication** `ALL_ELIGIBLE_KINDS` in `AuditLogQueryRepositoryRolledUpTest` now derives from `AuditKind.ROLLUP_ELIGIBLE.stream().map(Enum::name).toList()` (commit `ba0f9bb3`). --- **Sara — rolledUpFeed_with_default_returns_all_six_eligible_kinds may be fragile** Verified: the 6 events are on *different kinds* per row in the rollup, and each is separated by a large enough time gap (7300–7420 seconds apart from the TEXT_SAVED event) to fall outside any single session window. The test asserts `hasSize(6)` which is stable as long as these events aren't rolled up together — which they won't be since rollup is per-kind within a time window. The test is safe as-is.
Author
Owner

🏛️ Markus Keller — Senior Application Architect (Cycle 2)

Verdict: Approved

All concerns from the first review are resolved.

  • AuditLogQueryServiceTest is now in org.raddatz.familienarchiv.audit — correct package placement, boundary respected
  • The api.ts concern was correctly identified as phantom routes that never existed in the backend; explanation in the resolution comment is clear
  • ROLLUP_ELIGIBLE is the single source of truth throughout: the constant in AuditKind, the test helper derived from it, and the 2-arg overload all converge

The overall architecture is sound: server-side filtering with a client-side fuer-dich narrowing pass is the right model for this use case. The layer dependencies are clean (dashboard → audit is the only cross-domain coupling, and it's the correct direction).

## 🏛️ Markus Keller — Senior Application Architect (Cycle 2) **Verdict: ✅ Approved** All concerns from the first review are resolved. - `AuditLogQueryServiceTest` is now in `org.raddatz.familienarchiv.audit` — correct package placement, boundary respected - The api.ts concern was correctly identified as phantom routes that never existed in the backend; explanation in the resolution comment is clear - `ROLLUP_ELIGIBLE` is the single source of truth throughout: the constant in `AuditKind`, the test helper derived from it, and the 2-arg overload all converge The overall architecture is sound: server-side filtering with a client-side `fuer-dich` narrowing pass is the right model for this use case. The layer dependencies are clean (`dashboard → audit` is the only cross-domain coupling, and it's the correct direction).
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer (Cycle 2)

Verdict: Approved

All blockers from cycle 1 are resolved.

clientFilter.ts + clientFilter.test.ts — The extraction is clean. Pure function, single concern, directly testable. The test for MENTION_CREATED without the youMentioned flag explicitly documents the behavior change with the reasoning in the comment body:

"Unlike the old feedFilters.ts, we no longer include MENTION_CREATED unconditionally — that incorrectly showed mentions directed at OTHER users."

This is exactly the right level of documentation for a non-obvious behavior decision.

+page.svelteconst displayFeed = $derived(applyClientFilter(data.activityFeed, data.filter)) is a single-expression $derived. Clean.

DashboardControllerTestany() is correct. No unchecked cast.

Package placementAuditLogQueryServiceTest is now where it belongs.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer (Cycle 2) **Verdict: ✅ Approved** All blockers from cycle 1 are resolved. **`clientFilter.ts` + `clientFilter.test.ts`** — The extraction is clean. Pure function, single concern, directly testable. The test for `MENTION_CREATED` without the `youMentioned` flag explicitly documents the behavior change with the reasoning in the comment body: > "Unlike the old feedFilters.ts, we no longer include MENTION_CREATED unconditionally — that incorrectly showed mentions directed at OTHER users." This is exactly the right level of documentation for a non-obvious behavior decision. **`+page.svelte`** — `const displayFeed = $derived(applyClientFilter(data.activityFeed, data.filter))` is a single-expression `$derived`. Clean. **`DashboardControllerTest`** — `any()` is correct. No unchecked cast. **Package placement** — `AuditLogQueryServiceTest` is now where it belongs.
Author
Owner

🔒 Nora "NullX" Steiner — Application Security Engineer (Cycle 2)

Verdict: Approved

No new security surface introduced in the fix commits. The clientFilter.ts module is a pure client-side predicate with no I/O, no external calls, and no user input handling. Nothing to flag from a security perspective.

Previous approval on the backend kinds parameter handling stands.

## 🔒 Nora "NullX" Steiner — Application Security Engineer (Cycle 2) **Verdict: ✅ Approved** No new security surface introduced in the fix commits. The `clientFilter.ts` module is a pure client-side predicate with no I/O, no external calls, and no user input handling. Nothing to flag from a security perspective. Previous approval on the backend kinds parameter handling stands.
Author
Owner

🧪 Sara Holt — Senior QA Engineer (Cycle 2)

Verdict: Approved

All test coverage concerns from cycle 1 are resolved.

clientFilter.test.ts — 11 tests across 3 describe blocks. Coverage includes:

  • All 4 non-fuer-dich filters pass through unchanged (confirming server-side filtering handles them)
  • youMentioned: true inclusion
  • youParticipated: true inclusion
  • Both flags true inclusion
  • Neither flag → excluded
  • MENTION_CREATED without flags → excluded (behavioral regression risk documented and tested)
  • MENTION_CREATED with youMentioned: true → included
  • Mixed items filtered correctly to personally relevant items

That's the complete behavior matrix. Well done.

ALL_ELIGIBLE_KINDS now derives from AuditKind.ROLLUP_ELIGIBLE.stream().map(Enum::name).toList() — any future kind addition to the constant automatically propagates to the integration test. Correct fix.

rolledUpFeed_with_default_returns_all_six_eligible_kinds — accepted as-is, per the analysis in the resolution comment.

## 🧪 Sara Holt — Senior QA Engineer (Cycle 2) **Verdict: ✅ Approved** All test coverage concerns from cycle 1 are resolved. **`clientFilter.test.ts`** — 11 tests across 3 `describe` blocks. Coverage includes: - All 4 non-fuer-dich filters pass through unchanged (confirming server-side filtering handles them) - `youMentioned: true` inclusion - `youParticipated: true` inclusion - Both flags true inclusion - Neither flag → excluded - `MENTION_CREATED` without flags → excluded (behavioral regression risk documented and tested) - `MENTION_CREATED` with `youMentioned: true` → included - Mixed items filtered correctly to personally relevant items That's the complete behavior matrix. Well done. **`ALL_ELIGIBLE_KINDS`** now derives from `AuditKind.ROLLUP_ELIGIBLE.stream().map(Enum::name).toList()` — any future kind addition to the constant automatically propagates to the integration test. Correct fix. **`rolledUpFeed_with_default_returns_all_six_eligible_kinds`** — accepted as-is, per the analysis in the resolution comment.
Author
Owner

⚙️ Tobias Wendt — DevOps & Platform Engineer (Cycle 2)

Verdict: Approved

No infrastructure changes. No new environment variables. Fix commits are test/refactor only. Previous approval stands.

## ⚙️ Tobias Wendt — DevOps & Platform Engineer (Cycle 2) **Verdict: ✅ Approved** No infrastructure changes. No new environment variables. Fix commits are test/refactor only. Previous approval stands.
Author
Owner

🎨 Leonie Voss — UX & Accessibility Lead (Cycle 2)

Verdict: Approved

Fix commits don't touch UI or accessibility. The aria-live/aria-busy improvement from cycle 1 is intact. The applyClientFilter refactor is internal JavaScript only — no markup changes.

Previous approval stands.

## 🎨 Leonie Voss — UX & Accessibility Lead (Cycle 2) **Verdict: ✅ Approved** Fix commits don't touch UI or accessibility. The `aria-live`/`aria-busy` improvement from cycle 1 is intact. The `applyClientFilter` refactor is internal JavaScript only — no markup changes. Previous approval stands.
marcel merged commit ba0f9bb325 into main 2026-04-21 22:48:09 +02:00
marcel deleted branch feat/issue-293-dashboard-activity-kinds-filter 2026-04-21 22:48:10 +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#302