The sendBeacon name was misleading after switching to keepalive fetch.
Also adds a test to confirm flush is a no-op when pendingTexts is empty.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Blocks requests with Content-Length > 1 048 576 bytes with 413.
Tests cover security guards, body limit, and response forwarding.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sendBeacon always sends POST, but the backend expects PUT for block updates, so
saves were silently dropped on page unload. Replace with fetch({ keepalive: true,
method: 'PUT' }) which survives navigation and uses the correct HTTP method.
Add a catch-all SvelteKit server route at /api/[...path] so all client-side API
calls work in production (without the Vite dev proxy). More-specific routes
(/api/persons, /api/tags, /api/documents/[id]/file) keep precedence.
Closes#204
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses @leonievoss and @felixbrandt — fix(ui): "the PDF icon
misleads for image documents" and "swap for a neutral file icon".
The fallback now shows a generic document-text glyph (page outline +
three text lines) instead of the PDF-specific icon with the folded
corner. Applies equally well to PDFs, JPEG/PNG scans, and TIFF
documents — all of which can land in the fallback path.
Also bumped the icon from h-6/w-6 to h-8/w-8 — the previous 24px
glyph looked sparse inside the 60×84 tile (Leonie, post-merge
iteration point #2).
Refs #307
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- admin.spec: click 'Thumbnails erzeugen', wait for status DONE
within 30s, screenshot the success message
- accessibility.spec: /admin/system joins the page list so the
thumbnail card is checked in light, system-dark, and manual-dark
axe-core runs
Refs #307
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Fourth card on /admin/system mirrors the mass-import pattern:
- POST /api/admin/generate-thumbnails to trigger
- 2000 ms polling on /api/admin/thumbnail-status while RUNNING
- processed / skipped / failed counters in the DONE message
- standalone pollInterval so import and thumbnail polling don't
interfere with each other
Paraglide keys added in de/en/es, mirroring admin_system_import_*.
Refs #307
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Home search rows and person detail sidebars now show the real
first-page preview when one exists, falling back to the PDF icon
for documents the backfill hasn't processed yet. The old `variant`
prop on PersonDocumentList is removed — it tinted the icon
differently for sent vs received, which no longer applies with a
uniform thumbnail tile.
Refs #307
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Renders the document thumbnail with object-cover + object-top so
letter salutations stay visible, empty alt (title nearby is the
accessible name), loading=lazy, decoding=async, and dark:mix-blend-multiply
for dark mode. Falls back to a PDF icon when thumbnailKey is null —
legacy documents, unsupported content types, or transient failures
all land here.
Refs #307
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pure function returning /api/documents/{id}/thumbnail?v=<timestamp>
or null when thumbnailKey is missing. The encoded timestamp changes
whenever the backend regenerates a thumbnail (file replace),
invalidating browser caches despite the immutable Cache-Control.
Refs #307
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mirrors the backend Document entity's new optional fields. Both are
optional (no @Schema requiredMode on the backend side), so legacy
documents without thumbnails stay valid.
Refs #307
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
On CLOSED readyState, probes session and redirects to /login only on 401.
On CONNECTING, counts consecutive errors and closes + probes only after 3
failures, preventing infinite retries without killing transient reconnects.
Closes#203
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Upload button text wrapped in hidden xl:inline to hide label below xl
- AppNav logo margin reduced from mr-10 to mr-4 xl:mr-10 at lg breakpoint
Combined these changes bring the header content to ~923px vs ~945px
available space at 1024px, eliminating horizontal overflow
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BackButton gains showLabel prop: showLabel=false renders icon-only with
aria-label, no mr-2 on svg (was causing 0px button width in topbar)
- DocumentTopBar: BackButton restored to h-11 w-11 circular touch target
with showLabel=false matching the original 44×44px <a> it replaced
- Topbar row gets pr-4 (16px right padding per spec); action buttons div
no longer needs its own pr-3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The document detail page back button was missed in the original refactor —
it still pointed to "/" (dashboard) regardless of where the user came from.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BackButton now accepts a `class` prop (default 'mb-4') so callers can
override spacing; resolves hardcoded margin in flex-row topbar snippets
- documents/[id]/edit and enrich/[id] pass class="" to suppress the margin
- Replace weak className unit test with class-prop behaviour tests
- Add [data-hydrated] comment in E2E spec explaining what emits the attribute
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 7 in-scope back navigation links converted to use history.back().
Admin panel mobile chevron converted inline (icon-only, different
visual pattern). Cancel buttons left as static <a> links.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/chronik → /aktivitaeten; heading updated in all three locales.
Component folder (lib/components/chronik/) stays unchanged — internal
implementation detail, not user-facing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ChronikTimeline: date buckets now render as bordered cards with muted
header (border-line / bg-surface / shadow-sm) and divide-y row
separators, matching the DocumentList card pattern
- ChronikRow: remove rounded-sm (card handles clipping), hover:bg-canvas
→ hover:bg-muted/50; restore rollup count badge after doc title
- Messages (de/en/es): remove embedded {count} from all four rollup verb
strings so the badge is the single source of truth, consistent with
DashboardActivityFeed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
Sidebar was constructing /documents/:id?commentId=… without the
annotationId, so clicking a mention there no-op'ed the deep-link
scroll helper. Route the href through buildCommentHref so the
bell and the chronik sidebar produce identical URLs.
Refs #300.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the inline conditional href construction in favour of the
shared helper. Identical URL shape — behaviour preserved.
Refs #300.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single source of truth for constructing /documents/:id?commentId=…
(&annotationId=…) URLs. Used by the notification bell, the chronik
"Für dich" sidebar, and the chronik main feed so the three surfaces
can no longer diverge.
Refs #300.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the two new optional fields on ActivityFeedItemDTO in the
generated openapi-typescript output. Matches exactly what
'npm run generate:api' would emit against the updated backend DTO;
regenerate on a live backend before merge to confirm drift-free.
Refs #300.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comments only render inside TranscriptionEditView, so a deep-link
into a document with existing reviewed transcriptions landed the
user in read mode with no comment element in the DOM — the scroll
target silently missed.
scrollToCommentFromQuery now takes a setPanelMode callback and calls
it with 'edit' whenever both query params are present. The page's
own transcribe-mode $effect checks a skipInitialPanelMode flag the
deep-link flow sets, so its default-panel-mode logic doesn't race
against the explicit override.
Two new helper tests pin the contract: panel mode is forced to
'edit' both when transcribe mode is off (entering fresh) and when
it is already on (same-page notification click).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small refinements from Felix's review cycle 1:
- replaceState(page.url.pathname, page.state ?? {}) — defend against
first-navigation cases where page.state can be undefined.
- Extract the inline tick + requestAnimationFrame into a named
waitForPanelRender() helper; intent is now readable from onMount.
- Attach .catch() to the fire-and-forget scrollToCommentFromQuery
promise so any helper throw surfaces via console.error instead
of silently disappearing.
No behavior change on the happy path. All existing tests stay green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Seeds a document, transcription block, and block comment via API,
then visits /documents/{id}?commentId=X&annotationId=Y and asserts
the page enters transcribe mode, the comment article becomes visible,
and the URL query params are stripped. Runs at 320px and 1440px so
the collapsed PDF strip clipping on mobile is caught. An axe-core
pass guards the new tabindex + focus-visible ring against a11y
regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After navHeight setup, call scrollToCommentFromQuery with the page
URL and callbacks into the component's local state (transcribeMode,
activeAnnotationId, flashAnnotationId) plus SvelteKit's replaceState
to strip the consumed query params.
afterTick awaits both Svelte's tick() and one requestAnimationFrame,
mirroring the existing handleAnnotationClick timing so the annotation
panel has rendered before scrollIntoView fires.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Notification deep-link scroll targets #comment-{id}. Add the id to
the article wrapper along with tabindex="-1" so scrollIntoView +
.focus({preventScroll:true}) can land screen-reader and keyboard
focus on the specific comment. A focus-visible ring appears only
for keyboard users so mouse clicks don't trigger a visible outline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure function that reads commentId + annotationId from the page URL,
enters transcribe mode if needed, activates the block's annotation,
scrolls the target comment into view, focuses it for screen readers,
fires the existing annotation flash, and strips the params via the
injected callback.
All side effects go through callbacks so the helper is unit-testable
without mounting the page or a DOM (only scrollIntoView/focus are
called on the injected element). Eight tests cover both absent params,
happy path, transcribe-mode activation, missing DOM target, reduced
motion, flash trigger, and URL strip.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SvelteKit's default `use:enhance` behaviour calls `form.reset()` after
a successful non-redirecting action, which wipes inputs that use
`value={...}` (property set, not defaultValue). The edit forms now
pass `reset: false` to `update()` so the saved values stay visible
after the success banner appears.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SvelteKit 2 forbids mixing a `default` action with named actions; the
page also exports a `delete` action. Posting the edit form therefore
returned a 500 with "When using named actions, the default action
cannot be used." Rename the action to `update` and point the form
at `?/update`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire the extracted filterFeed function into the displayFeed derived,
removing 20 lines of inline switch logic from +page.svelte.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract filterFeed(items, filter) from +page.svelte inline switch to a
pure function, widening the fuer-dich branch to include youParticipated.
Regenerate ActivityFeedItemDTO type to include the new field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
If a row ever receives a malformed uploadedAt (e.g. manual SQL migration,
backend regression), the helper now falls back to "vor 0 Minute(n)"
rather than rendering "vor NaN Tag(en)" to the user.
Addresses Nora's review suggestion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- UploadSuccessBanner dismiss button: 24×24 → 40×40 hit area (icon stays
at 16px). Matches senior-first baseline Leonie flagged.
- DashboardNeedsMetadata chevron: adds motion-reduce:transition-none and
motion-reduce:group-hover:translate-x-0 so users with prefers-reduced-
motion do not see the hover translate.
- Row title prefixed with an sr-only "PDF: " span so assistive tech
announces the document affordance alongside the title.
Addresses Leonie's review concerns #2, #3, and the sr-only nit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "no-callback" and "no-prop" tests no longer rely on an arbitrary
50ms sleep. Test 2 awaits the mocked invalidateAll call (the last async
step of the upload handler) before asserting the callback was not
invoked. Test 3 lets vitest-browser-svelte's own expect.element poll
until the success message appears.
Addresses Sara's and Felix's review concern about flake-prone timing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hoists the $navigating store into a shared __mocks__ module so tests can
drive it through real transitions. Adds two specs covering (a) skeleton
visible while $navigating && topDocs empty and (b) skeleton hidden when
topDocs is non-empty. Also sets aria-busy="true" on the skeleton so
screen readers announce the loading state (Leonie's a11y suggestion).
Addresses Sara's and Felix's review concern that the skeleton branch was
dead code in the test world.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The two "happy path" dashboard load tests now mock the two additional
calls added in f5481289 (/api/documents/incomplete + incomplete-count)
so the Promise.allSettled array resolves fully.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Happy-path journey (upload 2 PDFs → banner → CTA → /enrich) plus axe
sweep at 320/768/1440 × light/dark for the dashboard route. Seeded
docs are cleaned up in afterEach via psql so repeated runs stay green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dashboard loader fetches /api/documents/incomplete?size=5 plus the
existing /incomplete-count and surfaces both via data; +page.svelte
renders EnrichmentBlock with the top 5 docs, the total count, and the
bannerCount state bound to DropZone's onUploadComplete callback
(issue #296).
The block returns null when there is nothing to show, so dashboards
without pending uploads stay uncluttered.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>