Dashboard "Recently Added" widget calls ?status=REVIEWED&size=5.
Null status is a no-op — existing callers without the param are unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dashboard widget calls ?size=3 to cap the list. Response now returns
{id, title} DTO instead of full Document entity.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Dashboard widget uses ?type=MENTION&read=false to fetch unread mentions.
Also adds MethodArgumentTypeMismatchException → 400 handler so invalid
enum values in any @RequestParam return 400 instead of 500.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes dark mode rendering: list stayed white and text stayed dark because
bg-white, text-brand-navy, border-brand-sand were not theme-aware.
Replace with bg-surface, text-ink/ink-2/ink-3, border-line, bg-muted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add unit tests for all service classes. Cover happy paths, error paths, and edge cases including structurally unreachable null guards via reflection to reach 90.2% branch coverage (431/478) in the service package.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Native queries bypass the JPA first-level cache; flush+clear is required before
reloading entities to see the updated state in the same transaction.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PersonControllerTest: expand from 2 to 26 tests — covers all endpoints
(GET persons/id/correspondents/documents, POST create/merge, PUT update)
and all validation branches (missing/blank firstName, lastName,
targetPersonId → 400). Reveals and fixes a real bug: ResponseStatusException
thrown by controllers was caught by the catch-all ExceptionHandler(Exception)
in GlobalExceptionHandler, returning 500 instead of the intended status.
Fix: add explicit ExceptionHandler(ResponseStatusException) handler.
- DocumentSpecificationsTest: 18 @DataJpaTest tests covering every branch in
DocumentSpecifications (hasText null/blank/match/case, hasSender null/match,
hasReceiver null/match, isBetween both-null/both-set/start-only/end-only,
hasTags null/empty/match/AND-logic/case/whitespace-skip). This is the
primary driver of the 0% repository branch coverage reported in #148.
- PersonRepositoryTest: 10 new tests for previously untested native queries —
findCorrespondents (order by doc count), findCorrespondentsWithFilter
(case-insensitive), reassignSender, insertMissingReceiverReference
(no-duplicate guard), deleteReceiverReferences.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Kept our version of accessibility.spec.ts (color-contrast rule enabled,
exclusion comment removed) over main's disabled version — the contrast
fixes in this branch make the exclusion unnecessary.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
text-ink/60 produces an opacity-blended colour whose contrast is
background-dependent: it passes on white (4.8:1) but fails on the sandy
canvas #f0efe9 (3.97:1, below WCAG AA 4.5:1). Replace every occurrence
with text-ink-2 (#4b5563, 6.6:1 on canvas — WCAG AA ✓).
Also adds a warning comment above --c-accent in layout.css to prevent
the text-accent misuse from recurring.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--c-accent (#a1dcd8 light / #00c7b1 dark) is a decorative mint token —
1.52:1 on white, nowhere near WCAG AA. Every place it appeared as the
colour of a text label or interactive button is switched to text-primary
(#012851, 16.8:1 on white) with hover:text-ink-2 for consistency.
Affected: UsersTab, GroupsTab, CommentThread (Reply), DocumentList
(Clear search), PdfViewer (Direkt öffnen link).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces the wcag2a/wcag2aa E2E suite from the test-suite branch with
the color-contrast rule active — no disableRules exclusion. Also adds
/coverage/ to .prettierignore so generated lcov reports don't fail the
lint hook.
This commit intentionally fails the axe suite until the contrast fixes
land in the next commits.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
upload_label was referenced but never added to messages — caused a
500 on every page render. Reuses the existing doc_file_upload_label
key ("Datei hochladen" / "Upload file") which has the same meaning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces hardcoded German strings with Paraglide message keys
(page_title_home/persons/admin/login/error) across de/en/es.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add <svelte:head><title> to home, persons, admin, login, and error pages
- Add aria-label to hidden file input in DropZone (sr-only but must be labelled)
- Add aria-label to search input in SearchFilterBar
- Create +error.svelte so error pages always have a document title
- axe-core spec: add buildAxe() helper, disable color-contrast (brand palette, tracked separately)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Installs @axe-core/playwright and adds e2e/accessibility.spec.ts covering:
- home, persons, admin (authenticated via stored admin session)
- login (unauthenticated context)
Uses wcag2a + wcag2aa tags. Violations are logged with impact level and
node count before the assertion fails, so the first run against the live
stack will produce a clear inventory of any issues to fix or exclude.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Installs @vitest/coverage-v8 and configures coverage measurement over
src/lib/utils/** and src/lib/server/** — the utility and server-side
logic that is meaningful to measure in the Node test project.
Svelte component files and generated code (api/**, paraglide/**) are
excluded; those run in the browser project.
Baseline: 87.87% branch coverage — already above the 80% threshold.
Adds test:coverage script for local runs; produces lcov report for CI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds server-project spec files for the four priority routes:
- routes/+page.server (home/search) — happy path, 401 redirect, network error fallback
- routes/documents/[id]/+page.server — happy path, comments fetch failure, 401/403/404
- routes/persons/[id]/+page.server — happy path, partial API failure, 403/404
- routes/admin/+page.server — ADMIN permission gate (none/read-only/undefined/no groups)
All tests run in Node environment with vi.mock() for createApiClient and
$env/dynamic/private. No real network calls; total suite runs in < 1 second.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds JaCoCo 0.8.12 with prepare-agent, report, and check executions.
Baseline measured at 46.8% branch coverage. Gate set at 42% (baseline
minus 5%) to prevent regression while giving room to close the gap.
Excluded from measurement: DTOs, config classes, model entities,
ErrorCode enum — these contain no testable branch logic.
Target is 80%; gap documented in issue #120.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds spring-boot-testcontainers and testcontainers-postgresql deps.
PostgresContainerConfig declares a shared @ServiceConnection container
used by DocumentRepositoryTest, PersonRepositoryTest, and an
ApplicationContextTest smoke test.
Flyway migrations are imported via FlywayConfig and run on every test
execution, verifying the migration chain against a real PostgreSQL 16
container. No H2 is used.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deletes the npm create svelte scaffold file that tested arithmetic
instead of application code. Inflated the test count and added noise
to coverage reports.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Notifications are already fetched lazily inside toggleDropdown() when
the user opens the dropdown. Only fetchUnreadCount() is needed on mount
to show the badge.
Closes#725
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All five comment write endpoints (post doc comment, reply to doc comment,
post annotation comment, reply to annotation comment, edit comment) only
listed ANNOTATE_ALL in @RequirePermission. Users with WRITE_ALL received
403 on every comment action. Same pattern as the annotation fix.
Tests: CommentControllerTest (+5 RED→GREEN for WRITE_ALL on each method).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@RequirePermission on POST and DELETE annotation endpoints previously
only listed ANNOTATE_ALL. Users with WRITE_ALL (but not ANNOTATE_ALL)
received 403. A user who can write documents should also be able to
annotate them — both permissions now accepted on both methods.
Also updates canAnnotate in +layout.server.ts to match, so the UI
correctly reflects annotation capability for WRITE_ALL users.
Tests: AnnotationControllerTest (+2 RED→GREEN).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced one-way checked={...} with bind:group={selected} driven by a
writable $derived. In Svelte 5, the $derived pattern guarantees the DOM
checked state is always in sync at FormData capture time, so groupIds
is never accidentally sent as [] when the admin edits their own profile.
Sending groupIds:[] causes adminUpdateUser to clear all groups, which
revokes the admin's own permissions on the next request.
Tests: UserServiceTest (+4 for adminUpdateUser group behaviour),
page.svelte.spec.ts (+1 FormData assertion at submit time).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AnnotationSidePanel: cover visibility (null vs set annotationId),
close button callback, and targetCommentId forwarding
- layout.svelte.spec: mock $env/static/public to satisfy
PUBLIC_NOTIFICATION_POLL_MS import from NotificationBell
- mention.spec: update assertion to match span-based mention rendering
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mention spans injected via {@html} need global CSS since scoped styles
don't reach dynamically inserted content. Uses ink text on accent-bg
background for visible but subtle chip appearance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Profile page now greys out the notification checkboxes and save button when
the user has no email set, with a hint to add one first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NotificationBell now includes annotationId in the deep-link URL when available
- +page.svelte reads ?annotationId= param and sets activeAnnotationId on mount,
opening the side panel instead of the bottom discussion drawer
- AnnotationSidePanel accepts and forwards targetCommentId to CommentThread
so the specific comment is highlighted when navigating via a notification
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove @RequirePermission(READ_ALL) from NotificationController class level so
authenticated users with any permission (or none) can access their own notifications
- Add V19 migration, annotationId field to Notification entity and NotificationDTO
- NotificationService now stores annotationId from comment on both REPLY and MENTION
- Update controller tests: permission tests now expect 200, DTO constructor includes annotationId
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@RequirePermission now accepts Permission[] so a single annotation can
express "any of these" rather than a single required permission.
PermissionAspect updated accordingly — all existing single-value usages
compile unchanged (Java auto-wraps scalars in arrays for annotation attrs).
NotificationController: preference endpoints (GET/PUT /api/users/me/
notification-preferences) override the class-level READ_ALL gate with
{READ_ALL, WRITE_ALL, ANNOTATE_ALL} so users without READ_ALL can still
manage their own settings. Notification list endpoints retain READ_ALL.
UserSearchController: same broadened set so ANNOTATE_ALL users can search
for users to @mention when writing comments.
Tests: added WRITE_ALL and ANNOTATE_ALL passing cases for preferences and
user search; added 403 case for preferences with no permission; confirmed
WRITE_ALL cannot reach notification list endpoints.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BLOCKERs:
- Remove direct AppUserRepository/CommentRepository access from CommentService and
NotificationService — replaced with UserService.findAllById() and UserService
(fixes layering contract from CLAUDE.md)
- Switch Optional<JavaMailSender> constructor injection — removes @Autowired(required=false)
field and ReflectionTestUtils hack in tests
- Add @RequirePermission(READ_ALL) to UserSearchController — prevents user enumeration
without read access
Data bug:
- Promote actorName from @Transient to persisted VARCHAR column (V18 migration)
- Set actorName in notifyReply and notifyMentions from comment.getAuthorName()
Architecture:
- Add @RequirePermission(READ_ALL) to NotificationController
- Introduce NotificationDTO — controller returns DTO instead of Notification entity,
eliminating lazy-load N+1 and AppUser field leakage
- Change mentions FetchType to EAGER — fixes LazyInitializationException outside transaction
- Add @Transactional(propagation=REQUIRES_NEW) to notifyReply/notifyMentions so a
notification failure cannot roll back the parent comment
- N+1 fix: replace per-ID findById loops with single findAllById bulk fetch
- Move collectParticipantIds to CommentService; notifyReply accepts Set<UUID> directly
Security:
- Escape displayName before injecting into renderBody HTML span
- Replace <a href="#"> with <span class="mention"> — no profile page to link to, and
the anchor's scroll-to-top behaviour is harmful
Tests added/fixed:
- markRead_throwsNotFound, markAllRead_delegatesToRepository, countUnread_delegatesToRepository
- markOneRead_returns401, @RequirePermission 403 coverage for both controllers
- postComment/replyToComment_triggersNotifyMentions_whenMentionedUserIdsProvided
- search_returnsAtMostTenResults now asserts $.length() <= 10
- XSS regression test for escaped displayName in mention.spec.ts
Frontend minors:
- relativeTime() uses Intl.RelativeTimeFormat (locale-aware, not German-hardcoded)
- aria-label uses m.notification_unread() Paraglide key (de/en/es added)
- <div role="button"> replaced with <button> (native Enter+Space handling)
- onDestroy clears debounceTimer in MentionEditor
- setTimeout(100) replaced with await tick() + requestAnimationFrame in CommentThread
- Notification prefs form uses checkbox name attributes + formData.has() pattern
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- +page.svelte: read ?commentId= from URL; on mount, if present open bottom panel to discussion tab
- CommentThread: add targetCommentId prop — scrolls to comment on mount (scrollIntoView), applies ring highlight, removes highlight on first user interaction (click/keydown/scroll)
- CommentThread: add data-comment-id attributes to thread root and reply divs
- PanelDiscussion / DocumentBottomPanel: thread targetCommentId prop through the chain
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- NotificationBell.svelte: bell icon in header with unread badge, dropdown showing last 10 notifications, mark-all-read, click-outside close, keyboard Escape support, polls every PUBLIC_NOTIFICATION_POLL_MS ms
- Wire NotificationBell into +layout.svelte between ThemeToggle and UserMenu (authenticated users only)
- Profile page: add notification preferences card with notifyOnReply / notifyOnMention toggles, loaded via GET and saved via PUT /api/users/me/notification-preferences
- i18n: de/en/es message keys for bell, notifications list, and preference labels
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add touch-action:none to container when in annotate mode so the
browser doesn't intercept touch gestures for scroll/pan
- Replace onmouseenter/onmouseleave with onpointerenter/onpointerleave
so the highlight effect also fires on touch/stylus
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes duplicated locale logic from +layout.svelte and AppNav.svelte.
Context-specific sizing (text-xs/min-h-[44px]) stays in the wrapper
via [&_button]: selectors so the component itself is layout-agnostic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- AppNav: hide entire logo div (incl. mr-10 margin) below md: breakpoint
to eliminate the phantom whitespace left of the hamburger button
- admin: 2×2 grid on mobile → flex row at sm:, so "Schlagworte" fits
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On mobile the header is now cleaner — language buttons move to the
bottom of the hamburger panel. Desktop header is unchanged (sm:flex).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matches the FileSectionNew design: upload arrow icon, hidden <input>,
styled label as the click target, shows selected filename on pick.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced position:fixed on the bottom panel with shrink-0 flex child,
so the viewer (flex-1) naturally stops at the panel top instead of
extending behind it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
At 320px, showing "Annotieren" + "Bearbeiten" + download pushed the
toolbar past its bounds. Icon-only at mobile, labels revealed at sm:.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrap tabs in overflow-x-auto container with hidden scrollbar so all 4
German labels ("Transkription" etc.) are reachable at 320px. Close
button stays pinned outside the scroll area, always visible.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DropZone: raise border opacity from /20 to /30 for dashed drop zone
- layout.css: bump dark mode --c-line from #2e2e2e to #3d3d3d (was
~1.3:1 contrast on #1a1a1a surface, effectively invisible)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>