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>
Optional callback lets the parent route pop a post-upload banner without
lifting state into a store. Dashboard uses it to drive
UploadSuccessBanner (issue #296). Only fires when the server actually
created new documents — duplicates and errors do not trigger it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Composes UploadSuccessBanner + DashboardNeedsMetadata and reserves a
360px skeleton while \$navigating re-runs the loader with a fresh
incomplete list. Prevents the layout-shift jump after a batch upload
(Leonie's resolved decision #3 on issue #296).
Renders nothing when there is nothing to show — keeps the clean empty
dashboard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Transient post-upload banner for issue #296: singular/plural German copy,
aria-live=polite for screen readers, manual X dismiss, 8s auto-dismiss.
"Jetzt ergänzen →" CTA links directly to /enrich so seniors can continue
straight into the enrichment flow after a batch upload.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switches to two props — topDocs (max 5, capped by caller) and totalCount —
so the footer link can surface "Alle 12 anzeigen →" even when only 5
items are shown. Each row gets a generic document icon, title, relative
upload time and a chevron, wrapped in a single <a> per the issue spec.
Still returns null when topDocs is empty, keeping the empty dashboard
clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Singular/plural banner copy, a count-aware "show all" footer link, and
the dismiss aria-label for the new dashboard enrichment-list-block
(issue #296). Covers de / en / es.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pure function with injectable now — lets the dashboard enrichment block
render "vor 2 Min." / "vor 3 Std." / "vor 2 Tagen" without clock-based
test flakiness. Reuses the existing comment_time_minutes / _hours /
_days Paraglide keys, no new translations needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up the restored list endpoint and the new uploadedAt field on
IncompleteDocumentDTO (issue #296).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two PR #288 blockers from Felix and Leonie:
Felix: verbText.indexOf(docTitle) broke when the title was empty (indexOf
returned 0, the before/after slices both emptied) or when the title
substring-matched any word in the compiled Paraglide message (e.g. "Brief"
appearing inside a translated verb). Swap to a sentinel approach: interpolate
{doc} with U+0001, then split the compiled text on that sentinel — robust
regardless of title content or translator sentence order. Two new red tests
lock the invariant: empty title still renders the row link; short titles
that could substring-match render exactly once as a single chronik-doc-title
span.
Leonie: the comment variant rendered „{documentTitle}" as a placeholder,
which made the row show the same title twice — once as the underlined link,
once as the italic "preview quote" — implying the comment was quoting
itself. Replace with an italic ellipsis „…". A new red test asserts the
preview no longer contains the document title text verbatim.
While here, add a SECURITY comment next to the TODO so the next person who
wires item.commentPreview knows the backend must truncate/strip server-side
and the frontend must use {text}, never {@html} (Nora, issue #285#3552).
Part of #285, address PR #288 review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two items flagged as blockers in PR #288 review:
- Markus + Sara: "Mehr laden" calls GET /api/dashboard/activity?offset=N but
the backend's DashboardController only accepts `limit` — `offset` was
silently ignored, and every click re-fetched the same top-40 rows. Rather
than add backend offset/cursor support in this PR (scope creep), remove
the Load-more UI and defer pagination to a follow-up issue. 40 items
covers the default case; the feature can come back with proper backend
support and its own tests.
- Markus + Sara: ?/dismiss and ?/mark-all form actions were dead code —
the UI calls `onMarkRead` / `onMarkAllRead` callbacks (→ singleton →
raw PATCH) and never submits either form. Delete both actions and their
tests. Using the form-action path would require deprecating the
NotificationBell's raw-PATCH as well — that's tracked separately as
#286.
The Dismiss markup split from the previous commit stands on its own.
Part of #285, address PR #288 review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HTML5 forbids interactive content (<button>, <a>, <input>...) as descendants
of <a>. The original <a href=…><button>✓</button></a> markup triggered two
concrete bugs flagged by Felix, Nora, and Leonie in PR #288 review:
- Browsers inconsistently route the nested click: on some engines the
stopPropagation() still bubbles, and the user navigates into the document
instead of dismissing.
- The senior audience (60+) tap-selects with a slight drag, and the OS
treats the interaction as anchor vs. button inconsistently — a
reproducible usability failure Leonie has seen in testing before.
Refactor to the Option-C layout from issue #285 comment #3573: outer <li>
flex container, <a> wrapping avatar + body + time, <button> as a sibling.
Independent focus stops, invalid-HTML gone, no behavioural regression.
A new spec locks the invariant: `dismiss.closest('a')` must be null.
Part of #285, address PR #288 review.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>