Shows recently reviewed documents as a dashboard widget with formatted
dates. Renders nothing when the list is empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows documents with missing metadata as a dashboard widget with links
to the enrich workflow. Renders nothing when the list is empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows unread mention notifications as a dashboard widget. Renders
nothing when the mentions list is empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Component reads familienarchiv.lastVisited from localStorage and
shows a 'Zuletzt geöffnet' link to the last-visited document
- Renders nothing when no localStorage entry exists
- Document detail page writes id+title to localStorage on mount
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds type, read (notifications) and status (documents/search),
size (documents/incomplete) to the generated TypeScript types.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds NotificationType filter params, IncompleteDocumentDTO, and status
param on document search.
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>
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>
- 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>
- 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>
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>
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>
In dark mode --c-primary switches from navy (#012851) to mint (#a1dcd8).
Buttons using bg-primary+text-white showed white text on mint at 1.4:1
contrast — invisible. bg-brand-navy buttons were also invisible (navy on
near-black canvas, 1.3:1).
Replaced in 28 components app-wide:
- bg-primary ... text-white → text-primary-fg
- hover:bg-primary hover:text-white → hover:text-primary-fg
- bg-brand-navy ... text-white + hover:bg-brand-navy/90 →
bg-primary ... text-primary-fg + hover:bg-primary/90
Light mode is unchanged: primary-fg = white in light mode.
Dark mode: primary-fg = navy (#012851) on mint bg = readable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restructure the "New Document" page so users can save quickly:
- FileSectionNew becomes the first element, redesigned as a prominent
upload zone with an icon and large click target
- Title field is rendered standalone below the upload zone; it
auto-populates from the filename (via parseFilename + stripExtension
fallback) unless the user has already typed something
- All remaining metadata (who/when, description, transcription) moves
into a collapsible "Weitere Details" section that auto-expands when
URL prefill data or a form error is present, or when filename parsing
detects a date/person
- title is no longer required — the form can be saved with only a file
- DescriptionSection gains a `hideTitle` prop for use in this layout
- `form_label_title` translation key no longer carries a hardcoded `*`;
the asterisk is rendered by the template only when `titleRequired` is
set (currently only the edit form)
- E2E tests added for all three scenarios from the issue
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In dark mode --c-primary flips to mint (#a1dcd8), making text-white
unreadable. text-primary-fg is already paired correctly in both modes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Show the discussion count badge on every state (including 0) instead of
a separate nudge button. Simpler, less intrusive, and works without
needing an extra element near the panel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add comment count badge on the Discussion tab (seeded from SSR, updated live)
- Add 'Diskussion starten' nudge above collapsed panel when no comments exist
- Add empty state hint with speech-bubble icon inside the discussion panel
- Fix CommentThread to fire onCountChange with SSR-seeded count on mount
- Add tests for all three behaviours in CommentThread and DocumentBottomPanel
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the four fixed regexes with a split-based algorithm:
- first segment = date → last segment = firstName, rest = lastName parts
- last segment = date → second-to-last = firstName, rest = lastName parts
18881025_de_Gruyter_Walter.pdf now correctly yields "Walter de Gruyter".
Simple two-segment names behave identically to before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a file is selected on the new document page, parseFilename runs
on the filename and suggests date, sender name and title via the new
suggestedDateIso / suggestedSenderName / suggestedTitle props. Each
suggestion is applied only while the respective field is still clean
(not dirty), so manual input is never overwritten.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Supports four patterns: date_lastname_firstname and lastname_firstname_date,
both with ISO (YYYY-MM-DD) and compact (YYYYMMDD) date formats.
Returns dateIso, personName and a formatted suggestedTitle.
Partial matches are rejected — unrecognised filenames return {}.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Home page shows "Needs metadata" card when incomplete documents exist.
/enrich list shows all incomplete documents; /enrich/[id] provides a
split PDF-preview + compact form view with Skip / Save / Save & reviewed
actions that auto-advance through the queue.
New document page gets Save vs Save & reviewed split. Edit page gets
"Mark for review" secondary button to push a document back into the queue.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Eliminates type duplication across 6 files by introducing a single
shared types module:
- Comment + CommentReply: were identically defined in CommentThread,
PanelDiscussion, and DocumentBottomPanel
- DocumentPanelTab: was identically defined in DocumentBottomPanel
and documents/[id]/+page.svelte
- Annotation: was defined in both AnnotationLayer and PdfViewer
(PdfViewer's variant with fileHash? is now the canonical definition)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The root-comment and reply rendering blocks were near-identical (view mode
with author/time/edit-delete, and edit mode with textarea/save/cancel).
Extracted a local {#snippet commentEntry(comment, threadId, showReplyButton)}
that handles both states, introducing Svelte 5 snippets to the codebase.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move isoToGerman and germanToIso from utils.ts into utils/date.ts alongside
formatDate, and add handleGermanDateInput for the shared date field handler.
Make utils.ts a re-export shim so existing imports continue to work.
Closes part of #75
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch errors from plain strings to { filename, code } objects so the
frontend can show translated messages instead of raw exception text
- Add UNSUPPORTED_FILE_TYPE error code end-to-end (Java enum → errors.ts
→ de/en/es messages)
- Fix IncorrectResultSizeDataAccessException when a filename exists more
than once in the DB: use findFirstByOriginalFilename instead of
findByOriginalFilename in storeDocument()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PdfViewer: add $effect that forces showAnnotations=true when annotateMode
becomes true, so hiding annotations before drawing no longer breaks drawing
- DocumentViewer: restore missing fileHash field on Doc type and pass
documentFileHash to PdfViewer (lost when rebase dropped the merge commit)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace text-gray-*, bg-gray-*, border-gray-*, divide-gray-*, placeholder-gray-*,
focus:border-blue-*, focus:ring-blue-*, hover:bg-blue-*, and ring-brand-mint with
their semantic-token equivalents (text-ink, bg-muted, border-line, etc.) across
all pages and shared components so dark mode renders correctly everywhere.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces bg-white, text-brand-navy, border-brand-sand, text-gray-*, bg-[#2A2A2A],
bg-brand-purple/15, hover:bg-brand-sand, etc. across all 35 .svelte files with
semantic token utilities (bg-surface, text-ink, border-line, bg-pdf-bg, bg-nav-active,
bg-muted, text-accent, bg-primary, ...).
Also adds CSS filter: invert(1) in layout.css for De Gruyter <img> icons in dark mode,
excluding icons that carry .invert already (to prevent double-inversion).
Closes#64
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Inline <script> in app.html applies saved localStorage theme before first
paint to prevent flash of wrong theme
- ThemeToggle.svelte: moon/sun button, localStorage persistence, sets
data-theme on <html>, defaults to system preference on first visit
- Placed in +layout.svelte between language selector and user menu
- E2E tests cover visibility, toggle, reverse toggle, persistence, and
no-flash behaviour — all 6 passing
Refs #64
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No logic changes — whitespace and indentation only. These were flagged
by the pre-commit hook when running lint after layout.css was modified.
Refs #64
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keep the new bottom-panel / AnnotationSidePanel architecture from this branch
while pulling in the documentFileHash / visibleAnnotations filter that was added
on main. Thread documentFileHash through DocumentViewer so outdated-annotation
filtering works end-to-end.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pointer-events-none and pointer-events-auto were both present as static
and conditional Tailwind classes simultaneously. CSS specificity meant
pointer-events-none always won, so clicks passed through to the
annotation toggle button behind the panel. Now pointer-events-none is
only applied when the panel is hidden (translated off-screen).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Annotation threads now open in a slide-in side panel (320 px, right
edge of the PDF viewer) instead of expanding the bottom drawer.
The PDF stays visible while the user reads and writes annotation
comments.
- Add AnnotationSidePanel component (absolute-positioned, CSS slide
transition, keyed CommentThread, close via X or Escape)
- Remove the $effect that opened the bottom drawer on annotation click
- Simplify PanelDiscussion back to document-level thread only (no
annotation sub-tabs)
- Remove annotation-related props from DocumentBottomPanel and
PanelDiscussion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The document viewer container was using fixed inset-0 z-50 which
covered the sticky global nav bar. Now measures nav height at mount
and offsets the container top accordingly, dropping z-index to z-40.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of rendering the diff at the bottom of the list (requiring the user
to scroll down), it now appears directly below whichever version item was
clicked. Compare-mode diff stays at the bottom of the compare form where it
makes sense, since it is not tied to a specific list item.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of an arbitrary 80 % cap, the panel now measures the actual
DocumentTopBar height at open time and fills the remaining viewport
exactly — so the PDF is fully covered and the drawer reaches right up
to the header. Drag-to-shrink still works as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Panel now opens to 80 % of the viewport height so the user can immediately
read comments and metadata without having to drag it up first.
The user can drag the top handle down to make it smaller; that size is
persisted to localStorage and restored on the next visit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>