feat(#153): notification history page (/notifications) #155

Merged
marcel merged 17 commits from feature/153-notification-history into main 2026-03-29 19:12:16 +02:00
Owner

Closes #153

Summary

  • Backend bug fix (NullX Finding 1): GET /api/notifications?read=false with no type param silently returned all notifications. Added findByRecipientIdAndReadFalseOrderByCreatedAtDesc to NotificationRepository and the missing branch in NotificationService.
  • Backend security fix (NullX Finding 2): Added @Min(1) @Max(100) on the size param and @Validated on the controller. Added ConstraintViolationException → 400 to GlobalExceptionHandler. Added spring-boot-starter-validation dependency.
  • documentTitle in DTO: New DocumentService.findTitlesByIds(Collection<UUID>) fetches all titles in one query. NotificationService.getNotifications() populates documentTitle per page — null when document is deleted.
  • Shared frontend utility: $lib/utils/notifications.tsNotificationItem type, relativeTime(ts, now?) (deterministic, testable), parseNotificationEvent() (SSE payload shape validation, NullX Finding 3). NotificationBell.svelte updated to import from there.
  • "Alle anzeigen →" link added to bell dropdown footer (always rendered).
  • /notifications page: Filter pills (role="radiogroup" / role="radio" / aria-checked bound to URL params via $derived), notification rows with border+dot unread indicators (WCAG 1.4.1), "Alle als gelesen markieren" form action (only when unreadCount > 0), "Ältere laden" client-side append, empty state.
  • Profile cross-link: "Benachrichtigungsverlauf ansehen →" added below notification preference toggles.
  • i18n: 15 new keys added to de.json, en.json, es.json.

Screenshots

Empty state

320px 768px 1440px
Light empty-320-light empty-768-light empty-1440-light
Dark empty-320-dark empty-768-dark empty-1440-dark

List view (~20 items, first page)

320px 768px 1440px
Light 3msgs-320-light 3msgs-768-light 3msgs-1440-light
Dark 3msgs-320-dark 3msgs-768-dark 3msgs-1440-dark

Load-more view (~40 items after clicking "Ältere laden")

320px 768px 1440px
Light 50msgs-320-light 50msgs-768-light 50msgs-1440-light
Dark 50msgs-320-dark 50msgs-768-dark 50msgs-1440-dark

Test plan

  • ./mvnw test -Dtest="NotificationServiceTest,NotificationControllerTest" — 52 tests pass (3 new service tests, 3 new controller tests)
  • npx vitest run --project=server — 114 tests pass (7 new page server tests, 15 new utility tests)
  • GET /api/notifications?read=false returns only unread notifications (previously returned all)
  • GET /api/notifications?size=200 returns 400
  • Notification response includes documentTitle field
  • /notifications renders with filter pills, rows show document titles, "Alle anzeigen →" in bell dropdown links here
  • /profile shows "Benachrichtigungsverlauf ansehen →" link below notification prefs

Security checklist (NullX findings)

  • Finding 1 — filter logic gap: fixed and covered by NotificationServiceTest
  • Finding 2 — unbounded size: clamped to 100, covered by NotificationControllerTest
  • Finding 3 — SSE payload validation: parseNotificationEvent() guards shape, covered by notifications.spec.ts
  • Finding 4 — no {@html} on any notification field
  • Finding 5 — Vite proxy confirmed: /api/* proxied with auth token injection

Out of scope

  • E2E / visual regression tests → #124

🤖 Generated with Claude Code

Closes #153 ## Summary - **Backend bug fix (NullX Finding 1):** `GET /api/notifications?read=false` with no `type` param silently returned all notifications. Added `findByRecipientIdAndReadFalseOrderByCreatedAtDesc` to `NotificationRepository` and the missing branch in `NotificationService`. - **Backend security fix (NullX Finding 2):** Added `@Min(1) @Max(100)` on the `size` param and `@Validated` on the controller. Added `ConstraintViolationException → 400` to `GlobalExceptionHandler`. Added `spring-boot-starter-validation` dependency. - **`documentTitle` in DTO:** New `DocumentService.findTitlesByIds(Collection<UUID>)` fetches all titles in one query. `NotificationService.getNotifications()` populates `documentTitle` per page — null when document is deleted. - **Shared frontend utility:** `$lib/utils/notifications.ts` — `NotificationItem` type, `relativeTime(ts, now?)` (deterministic, testable), `parseNotificationEvent()` (SSE payload shape validation, NullX Finding 3). `NotificationBell.svelte` updated to import from there. - **"Alle anzeigen →" link** added to bell dropdown footer (always rendered). - **`/notifications` page:** Filter pills (`role="radiogroup"` / `role="radio"` / `aria-checked` bound to URL params via `$derived`), notification rows with border+dot unread indicators (WCAG 1.4.1), "Alle als gelesen markieren" form action (only when `unreadCount > 0`), "Ältere laden" client-side append, empty state. - **Profile cross-link:** "Benachrichtigungsverlauf ansehen →" added below notification preference toggles. - **i18n:** 15 new keys added to `de.json`, `en.json`, `es.json`. ## Screenshots ### Empty state | | 320px | 768px | 1440px | |---|---|---|---| | **Light** | ![empty-320-light](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/empty-320-light.png) | ![empty-768-light](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/empty-768-light.png) | ![empty-1440-light](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/empty-1440-light.png) | | **Dark** | ![empty-320-dark](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/empty-320-dark.png) | ![empty-768-dark](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/empty-768-dark.png) | ![empty-1440-dark](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/empty-1440-dark.png) | ### List view (~20 items, first page) | | 320px | 768px | 1440px | |---|---|---|---| | **Light** | ![3msgs-320-light](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/3msgs-320-light.png) | ![3msgs-768-light](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/3msgs-768-light.png) | ![3msgs-1440-light](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/3msgs-1440-light.png) | | **Dark** | ![3msgs-320-dark](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/3msgs-320-dark.png) | ![3msgs-768-dark](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/3msgs-768-dark.png) | ![3msgs-1440-dark](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/3msgs-1440-dark.png) | ### Load-more view (~40 items after clicking "Ältere laden") | | 320px | 768px | 1440px | |---|---|---|---| | **Light** | ![50msgs-320-light](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/50msgs-320-light.png) | ![50msgs-768-light](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/50msgs-768-light.png) | ![50msgs-1440-light](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/50msgs-1440-light.png) | | **Dark** | ![50msgs-320-dark](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/50msgs-320-dark.png) | ![50msgs-768-dark](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/50msgs-768-dark.png) | ![50msgs-1440-dark](http://192.168.178.71:3005/marcel/familienarchiv/raw/branch/feature/153-notification-history/proofshot-artifacts/2026-03-29_notifications-page-153/50msgs-1440-dark.png) | ## Test plan - [ ] `./mvnw test -Dtest="NotificationServiceTest,NotificationControllerTest"` — 52 tests pass (3 new service tests, 3 new controller tests) - [ ] `npx vitest run --project=server` — 114 tests pass (7 new page server tests, 15 new utility tests) - [ ] `GET /api/notifications?read=false` returns only unread notifications (previously returned all) - [ ] `GET /api/notifications?size=200` returns 400 - [ ] Notification response includes `documentTitle` field - [ ] `/notifications` renders with filter pills, rows show document titles, "Alle anzeigen →" in bell dropdown links here - [ ] `/profile` shows "Benachrichtigungsverlauf ansehen →" link below notification prefs ## Security checklist (NullX findings) - [x] Finding 1 — filter logic gap: fixed and covered by `NotificationServiceTest` - [x] Finding 2 — unbounded `size`: clamped to 100, covered by `NotificationControllerTest` - [x] Finding 3 — SSE payload validation: `parseNotificationEvent()` guards shape, covered by `notifications.spec.ts` - [x] Finding 4 — no `{@html}` on any notification field - [x] Finding 5 — Vite proxy confirmed: `/api/*` proxied with auth token injection ## Out of scope - E2E / visual regression tests → #124 🤖 Generated with [Claude Code](https://claude.com/claude-code)
marcel added 7 commits 2026-03-29 14:13:22 +02:00
NullX Finding 1: GET /api/notifications?read=false with no type param fell through
to the all-notifications branch, silently ignoring the read filter. Added
findByRecipientIdAndReadFalseOrderByCreatedAtDesc to NotificationRepository and
the missing Boolean.FALSE.equals(read) branch in NotificationService.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Notification rows in the history page need the document title. Added
findTitlesByIds(Collection<UUID>) to DocumentService (one query via a new
JPQL projection on DocumentRepository). NotificationService.getNotifications()
now fetches all titles for the page in a single extra query and maps them into
the DTO. documentTitle is null when the document has been deleted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NullX Finding 2: unbounded size param allowed full table scan. Added
spring-boot-starter-validation, @Validated on the controller, @Min(1) @Max(100)
on the size param, and ConstraintViolationException → 400 in GlobalExceptionHandler.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extracted from NotificationBell.svelte into $lib/utils/notifications.ts so the
history page can reuse them. relativeTime() now accepts an optional `now` param
for deterministic unit testing. Added parseNotificationEvent() for SSE payload
shape validation (NullX Finding 3).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New route with server load function (reads URL params, derives unreadCount from
the page, single API call per Sara's architecture requirement), mark-all form
action, and the full page UI: filter pills with ARIA radiogroup, notification
rows with border+dot unread indicators (WCAG 1.4.1), "Ältere laden" client-side
append, and empty state. Includes all de/en/es translation keys.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feat(profile): add Benachrichtigungsverlauf cross-link below notification preferences
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 2m31s
CI / Backend Unit Tests (pull_request) Failing after 2m28s
CI / E2E Tests (pull_request) Failing after 15m58s
8f2a7a3528
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-03-29 14:33:08 +02:00
fix(notifications): rename spec file to remove + prefix
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 2m25s
CI / Backend Unit Tests (pull_request) Failing after 2m32s
CI / E2E Tests (pull_request) Failing after 1h21m19s
edeff35393
SvelteKit reserves all + prefixed files as route files. The spec was named
+page.server.spec.ts which caused a 500 on /notifications in the dev server.
Renamed to page.server.spec.ts following the convention in the rest of src/routes/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-03-29 14:38:16 +02:00
chore(screenshots): add /notifications page proofshots for #153
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 2m39s
CI / Backend Unit Tests (pull_request) Failing after 2m26s
CI / E2E Tests (pull_request) Failing after 1h25m32s
8d5f1f5458
18 screenshots: empty state, list view (~20 items), load-more view (~40 items)
across 320/768/1440 px viewports in light and dark themes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-03-29 14:53:48 +02:00
chore(screenshots): fix empty-state proofshots for #153
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 2m27s
CI / Backend Unit Tests (pull_request) Failing after 2m23s
CI / E2E Tests (pull_request) Failing after 1h19m50s
f42ec6f115
Empty state now correctly shows zero notifications (bell icon + body text).
Previous screenshots were taken with DB data present, causing the filtered
URL to still return results.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-03-29 15:08:19 +02:00
fix(notifications): use bg-canvas on list so items match page background
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 2m31s
CI / Backend Unit Tests (pull_request) Failing after 2m26s
CI / E2E Tests (pull_request) Failing after 1h25m46s
dc02ee165d
The <ul> had bg-surface (white), causing unread rows to inherit white
instead of blending with the canvas background. Read rows already set
bg-canvas explicitly, so they looked fine. Unread rows were white.

Fix: set bg-canvas on the <ul> so all rows inherit the page background.
The redundant explicit bg-canvas on read rows is removed.
Unread items remain visually distinct via the left accent border + dot only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-03-29 15:11:16 +02:00
chore(screenshots): retake proofshots after bg-canvas fix
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
fa6879d0e0
Notification items now correctly show the canvas background instead
of white (bg-surface). Screenshots updated across all 18 combinations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-03-29 15:27:52 +02:00
fix(notifications): set bg-canvas directly on <li> to prevent white bleed
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
badf2767f0
The <a> inside each row has transparent background by default — CSS
background-color does not inherit. Putting bg-canvas only on the <ul>
was not enough; browsers still painted items white. Setting bg-canvas
on the <li> itself ensures the canvas color is explicitly applied to
each row in both light and dark mode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-03-29 15:32:34 +02:00
fix(notifications): use bg-surface on <li> rows
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
5e5791139d
bg-canvas matched the page background making rows invisible against it.
bg-surface gives each row the correct card/surface color (white in light,
dark panel in dark mode), matching what was always intended.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-03-29 15:38:40 +02:00
feat(dashboard): add /notifications link to DashboardMentions widget
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
fb82dcdb95
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-03-29 16:36:11 +02:00
fix(dashboard): wrap mention items so last:border-0 works correctly
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
3c09833591
Adding the link div after the {#each} broke last:border-0 — the last
mention item was no longer the last child, so it kept its border-b,
creating a double line with the link's border-t. Wrapping the each in
its own div restores correct last:border-0 targeting.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-03-29 16:40:53 +02:00
fix: remove always-on underline from notification cross-links
Some checks failed
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / E2E Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
902e593e04
underline decoration-accent/60 was forcing a permanent underline.
The global a:hover rule already handles underline + accent color on hover.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel merged commit 5374bdabd4 into main 2026-03-29 19:12:16 +02:00
marcel deleted branch feature/153-notification-history 2026-03-29 19:12:18 +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#155