admin/groups/new: heading, both permission group renderings (4 standard
+ 4 administrative checkboxes), form-error banner branch, cancel link
href, submit button form-attribute wiring, name input requiredness.
Mocks $app/navigation so beforeNavigate doesn't crash the test runner.
enrich/+: heading, empty placeholder vs populated count + start CTA,
start CTA href derived from documents[0].id, per-row title rendering,
bulk-select checkbox gated on canWrite.
16 tests across two files.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
users/[id]: full-name derivation across all four branches
(both/firstName-only/lastName-only/email fallback), avatar initials
matrix, email/contact row visibility tied to data presence.
admin/ocr/global: heading + back link, runs prop pass-through,
defensive default for missing history fields.
geschichten/[id]: title rendering, author full-name vs email fallback
vs null, publishedAt suffix conditional, persons and documents sections
gated on array length, edit/delete actions gated on canBlogWrite. Mocks
the confirm service since it requires a ConfirmDialog mounted in layout.
26 tests across three files.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PersonEditForm: PERSON vs INSTITUTION/GROUP visibility matrix (firstName,
title, alias, birth/deathYear toggle), lastName label switch, prop
hydration of all populated fields, fallback to PERSON for unknown type,
empty-string handling for null fields. 10 tests, ~30 branches.
SegmentationTrainingCard: trainingInfo null vs populated, block count
display, button disabled-state matrix (training × tooFewBlocks ×
serviceDown), too-few-blocks and service-down hints, success message
after a mocked fetch, training history heading. 10 tests, ~25 branches.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each status (active / exhausted / revoked / expired) maps to a distinct
visual treatment via statusColor() — one focused test per branch
asserts the correct background class on a tbody element so the test
verifies user-observable behaviour rather than the internal switch.
Also covers: empty placeholder, loadError banner, filter chip
selection state, new-invite form toggle on button click, createError
message visibility inside the open form, created-invite success card
with shareable URL, revoke button gating to active invites only,
unlimited-uses display, no-expiry display.
16 tests, ~50 branches covered.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
empty state vs. populated, zoom controls visibility tied to node count,
URL ?focus= preselection (matching id selects, missing id does not),
zoom-out clamping safety. $app/state mocked at module boundary so the
test can drive page.url and page.data.canWrite without a SvelteKit
runtime.
Six tests focused on user-observable behaviour — one logical behaviour
per test (Sara's guidance).
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DocumentViewer: loading / error / no-scan / image rendering branches.
filePath conditionally drives the direct-download link in the error
state; fileUrl + non-PDF contentType drives the <img> render.
PersonalInfoForm: default render, prop hydration including the German
date conversion path, success/error banner branches, form action wiring.
profile/+page: notification-checkbox enabled/disabled depending on
hasEmail, no-email hint visibility, prefsSuccess/prefsError banners,
fallback when notificationPrefs is null.
20 tests across three files.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PersonDocumentList: empty/populated, year-range derivation across
no-date/single-year/multi-year inputs, sort toggle visibility (>1 doc),
sort-direction round trip, preview-limit + show-more expansion,
title→originalFilename fallback, no-date and no-location branches.
persons/new: PERSON vs INSTITUTION/GROUP visibility matrix
(firstName/alias/life-year fields toggle), lastName label switching
between Vorname/Nachname/Name, form-error banner, prior-form hydration,
cancel link href, fallback to PERSON for unknown personType.
24 tests across two files, hitting the 32+28 = 60 branches at the top
of the issue's leverage list.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CorrespondentSuggestionsDropdown: empty list still renders the static
heading and 'Alle Korrespondenten' row, populated rows when not loading,
loading hides correspondent rows, initials fallback (lastName-only when
firstName is null), click + keyboard selection, Escape closes.
PersonCard: full matrix of conditional UI — title visibility for PERSON
vs non-PERSON, avatar initials path (firstName+lastName vs lastName-only
fallback), PersonTypeBadge presence for non-PERSON types, alias, life
dates, notes, and the canWrite=true/false branches that gate the edit
link (Nora's authorization-rendering rule).
21 tests covering ~50 branches.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
+error.svelte: vi.mock('$app/state') drives the page state so each test
can assert one of the three rendering branches — populated error message,
distinct status code, and the 'Internal Error' fallback when page.error
is null.
forgot-password/+page.svelte: prop-driven tests for the four states —
default form, success banner, error message inside the form, and the
back-to-login link href.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
PasswordChangeForm: tests the null/success/error/mismatch banner branches
plus the form action wiring.
FileSectionNew: tests the no-file/file-selected toggle, onfileParsed
callback invocation with the parsed metadata, the early-return when no
file is in the change event, and the suggestedTitle fallback path.
Eleven tests across two files. Both follow the UploadZone template (props,
File API synthetic input, vi.fn() callback spies).
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes scaffolding pages from initial Paraglide setup that were never
navigated to in production. Shrinks the measured coverage surface and
removes dead code from the production bundle. CLAUDE.md route tables
updated to drop the demo/ entry.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract isPureTextRelevance() private static method to replace the
7-clause inline boolean in searchDocuments
- Guard long→int cast in relevanceSortedPageFromSql to prevent silent
overflow at page ≥43M (CWE-190)
- resolvePersonName now uses the typed API client (createApiClient)
instead of raw fetch, aligning with project conventions
- Update DocumentServiceTest stubs to match new FTS path (findFtsPageRaw
+ findAllById instead of findAllMatchingIdsByFts)
- Rewrite page.server.spec.ts person-name tests to mock via path-based
API dispatch, matching the new api.GET call site
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three root causes prevented filters from reflecting the URL after SvelteKit
client-side navigation:
1. +page.server.ts now resolves sender/receiver display names in parallel with
the document search (UUID validation + silent 404 drop), so initialSenderName
/ initialReceiverName land in server data ready for the UI to use.
2. +page.svelte passes initialSenderName, initialReceiverName, and navKey
(incremented via untrack on every navigation) down to SearchFilterBar.
The untrack() prevents the effect from re-running due to its own navKey write.
3. SearchFilterBar forwards navKey as resetKey to each PersonTypeahead, which
already had a void resetKey guard added in the previous commit.
Together these ensure that after navigating to /documents?senderId=<uuid> the
typeahead shows the person's display name, and clicking × reset clears it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The SvelteKit prerender crawler cannot reach this route because
hooks.server.ts redirects all non-public paths to /login before the
crawler follows links. Explicitly listing the route in kit.prerender.entries
tells SvelteKit to render it directly without crawling.
Also removes a misleading comment that claimed the auth hook guards
prerendered static files — it does not. Prerendered HTML is served as a
static file by the reverse proxy; hooks.server.ts only runs for SSR requests.
Closes#472
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Felix and Elicit both flagged that the isReader formula had no
in-code explanation at the point of definition; future maintainers
adding a new permission level need a fast pointer to the architectural
rationale.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a readerData fixture and five render-level assertions: the three
ReaderStatsStrip totals, the recent-docs heading, the absent
contributor mission caption, and the drafts module appearing only when
canBlogWrite is true.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- page.server.spec.ts: new test verifies topPersons=[] when that fetch
rejects, rest of reader data still loads — addresses @Sara concern
- ReaderPersonChips: replaces hardcoded "Dok." with
dashboard_reader_doc_count_suffix Paraglide key (de/en/es)
— addresses @Felix suggestion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Collapses 5x duplicated null-check pattern in the reader fetch branch into
a single typed helper — addresses @Felix review blocker.
Also adds isReader/incompleteDocs/incompleteTotal to page.svelte.spec.ts
baseData so it satisfies the discriminated PageData union introduced by this PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Manually adds totalStories to generated StatsDTO type and wires it from
readerStats into ReaderStatsStrip — resolves @Elicit: stories tile was
permanently showing "—".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds 5 new components for the permission-gated reader layout:
- ReaderStatsStrip: stat tiles (documents / persons / stories) linking to list pages
- ReaderPersonChips: top-N persons by doc count with avatar + name
- ReaderDraftsModule: blog draft list for BLOG_WRITE users
- ReaderRecentDocs: 5 most-recently-updated docs with Neu/Aktualisiert badge
- ReaderRecentStories: 3 latest published stories with 150-char HTML-stripped excerpt
Each component ships with a vitest-browser spec covering the key assertions.
Avatar color/initials logic is inlined to satisfy $lib/shared → $lib/person
boundary rule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Read-only users (no WRITE_ALL or ANNOTATE_ALL) now receive lean reader
data (stats, top-4 persons, 5 recent docs, 3 recent stories, and drafts
when BLOG_WRITE) instead of the contributor transcription queues.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Defining --timeline-bar-idle / --timeline-bar-outside on :root from
inside a scoped <style> block leaks the contract into the global
namespace via component-local CSS, even though the selector itself
makes it work. Move both variables to layout.css next to the other
--palette / --c-* design tokens; the component <style> now only
consumes them.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
triggerSearch(zoomOverride?) made the call site read "depends on
whether the source event happened to include zoomFrom/zoomTo". Splits
into triggerSearchKeepZoom() and triggerSearchWithZoom(from, to) so
the contract is explicit at every call site. Closes Felix's review
nit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tablet (640–1024px) is exactly the iPad audience for transcribers.
At 240 monthly bars on an 800px column the bars fall to ~3.3px wide,
well below the 44×44 touch-target floor. Bumps the visibility class
from hidden sm:block to hidden lg:block and matches the page.ts
matchMedia gate to (min-width: 1024px). Closes Leonie's tablet
touch-target finding.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the discrete zoom-in button with a Graylog-style drag-to-zoom
range selector and adds X/Y axis labels so the chart is readable.
Drag interaction
- Pointerdown on a bar attaches document-level pointermove/pointerup/
pointercancel listeners; pointermove maps clientX to a bar index via
the row's bounding rect, so the mint-bordered window expands smoothly
even when the cursor leaves the bar or the chart entirely.
- pointerup commits filter + zoom atomically. Same-bar release on a
year bar (year-aggregated mode) zooms into that year's months;
same-bar release on a month bar emits filter-only.
- setPointerCapture removed — it was suppressing pointerenter on
sibling bars and preventing the drag window from expanding.
- Bar buttons are now h-full so the entire 80 px column is the hit
target, not just the visible bar height.
Axis labels
- Y-axis: max-count and 0 labels left of the bar area.
- X-axis: tickIndicesFor() picks decadal years for long ranges, evenly
spaced months for short year-zoom views, January boundaries for
multi-year month ranges. formatTickLabel() drops the year when the
visible range is a single year so 12-month zooms read "Jan Feb Mär…".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a zoom action that narrows the visible timeline range to the current
selection so the user can drill from year-level back into month-level
density. Zoom state lives in the URL (zoomFrom / zoomTo) so it survives
reload and is shareable.
- New `clipBucketsToRange(buckets, from, to)` helper applied before the
>240-month year-aggregate decision, so a zoomed window flips back to
month bars automatically when the clip narrows the range enough.
- `TimelineDensityFilter` gains `zoomFrom`, `zoomTo`, and `onzoomchange`
props. Zoom button shown only when a selection exists and we aren't
already zoomed; reset-zoom shown only when zoomed. Both placed in a
shared right-edge action cluster alongside the × clear button.
- `+page.ts` reads zoomFrom/zoomTo from the URL and forwards them as
props. `+page.svelte` extends FilterSnapshot + buildSearchParams, and
triggerSearch accepts an optional zoom override so the onzoomchange
callback can write the new pair (or clear them) atomically.
- 7 new component tests + 2 new page-integration tests cover the
visibility rules and URL writes.
- 4 new unit tests for `clipBucketsToRange`.
- 3 new i18n keys (zoom in / zoom reset / drag aria-live) across de/en/es.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The +page.ts client-side load now forwards the active /documents URL
filters (q, senderId, receiverId, tag, tagQ, status, tagOp) to
/api/documents/density so the bars recompute when the user narrows the
search. Date bounds (from/to) are deliberately omitted — the chart is
the surface for picking those.
- New `DensityFilters` type and `buildDensityUrl(filters)` helper.
- `fetchDensity` accepts a filter snapshot (defaulting to {} for
back-compat in tests).
- 6 new unit tests cover URL building, multi-tag repetition, AND/OR
forwarding, the explicit-no-from/to invariant, and filter-aware fetch.
- Generated API types refreshed against the new backend signature.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SvelteKit's PageData type generation only picks up +page.ts return values
when both files exist, so the runtime-merged server data was invisible to
TypeScript and svelte-check flagged every q/from/to/etc access in
+page.svelte. Spreading data into the +page.ts return restores the merge
at the type level. No runtime behaviour change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mounts the timeline above the result count, hidden on mobile via
\`hidden sm:block\` (defense-in-depth — +page.ts already gates the fetch).
The component's onchange callback updates local from/to and triggers
the existing search reload, so timeline selection composes with the
SearchFilterBar's other filters via AND semantics for free.
3 new page-level integration tests cover: widget renders when density
present, hides when null, and bar click navigates with correct
from/to URL params.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The density data is fetched only on tablet/desktop (sm:+ breakpoint) and
when ?view=calendar is not set — mobile users and the future calendar view
(#386) skip the request entirely. Lives in +page.ts (client-side) so the
matchMedia gate can run in the browser; +page.server.ts continues to handle
the document search.
Non-ok responses and network failures degrade to an empty bucket list
rather than throwing, so the document list keeps rendering.
5 unit tests cover the gating + graceful degradation paths.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Playwright CDP click latency occasionally pushed past vi.waitFor's 1000ms
deadline, making the "opens a confirm dialog" test flaky. Switched to
btn.dispatchEvent(new MouseEvent(...)) — the same synchronous in-browser pattern
already used in GeschichteEditor.svelte.spec.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fixes all remaining failing tests in the browser project. Root cause in
every case: Playwright CDP-based clicks/keyboard events do not reliably
trigger Svelte 5 onclick/onkeydown handlers. Pattern applied throughout:
- Buttons / result items: native `.element().click()` or
`dispatchEvent(new MouseEvent('click', { bubbles: true }))`
- Keyboard events: `dispatchEvent(new KeyboardEvent('keydown', { key }))`
on the target DOM element
- TipTap selection: `element.focus()` + Selection API +
`document.dispatchEvent(new Event('selectionchange'))`
- ProseMirror focus for onFocus: `dispatchEvent(new FocusEvent('focus'))`
Also fixes pre-existing content/logic issues found during analysis:
- ChronikErrorCard, BulkDropZone, CorrespondenzHero: stale i18n strings
and wrong ARIA role (combobox not textbox)
- RichtlinienRuleCard: beide beispielInput + beispielOutput required for
arrow to render; querySelectorAll to get last code element
- admin/system/page: vi.unstubAllGlobals() in afterEach; strict-mode
heading selector; per-call mockResolvedValueOnce for dual-card page
- DocumentList: add total prop + result count paragraph (test relied on it)
- PersonTypeahead keyboard navigation: pressKey() helper with native
KeyboardEvent dispatch replaces userEvent.keyboard()
- PersonMultiSelect: native element clicks for result selection and
chip removal; keydown dispatch on result div for Enter key test
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TranscriptionEditView: fix 4 failing tests:
- textarea → [role="textbox"] selector (editor is contenteditable, not <textarea>)
- button clicks → dispatchEvent(MouseEvent) for reliable Svelte 5 onclick with TipTap
- mentionedPersons test: init block with @mention token so deserialize() creates a
mention node; use userEvent.type + vi.waitFor (real timers) instead of fill +
fake timers, which prevents TipTap onUpdate from firing the debounce timer
EntityNavSection: anchor link click → add capture-phase preventDefault before
clicking to stop iframe navigation while allowing Svelte onclick handler to run
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three distinct root causes:
1. hilfe/transkription: Wikipedia link test was checking .textContent but
the accessible text had moved to aria-label in a prior commit.
2. documents/[id]/edit: vi.spyOn on a Svelte 5 compiled .svelte.ts service
object does not reliably track calls in vitest-browser mode; replaced
with a plain closure-based mock.
3. GeschichteEditor: TipTap's onMount steals focus and its ProseMirror
view interferes with Playwright CDP event dispatch. Three workarounds:
- blur: dispatchEvent(new FocusEvent('blur')) bypasses focus-state check
- save buttons: dispatchEvent(new MouseEvent('click')) from in-browser JS
context reliably triggers Svelte 5 onclick vs. Playwright CDP click
- trailing-space fill: input.value + dispatchEvent('input') works where
userEvent.fill('value ') silently fails to update bind:value
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLEANUP-2 (#413): convert two actionable TODOs to issue-referenced stubs
- +layout.server.ts:29 → TODO(#453) for dedicated admin stats endpoint
- ChronikRow.svelte: TODO(#454) for commentPreview; keep SECURITY line
as standalone comment (XSS guard stays co-located with the risk)
CLEANUP-3 (#414): add one-line justification comments to both naming
violators — SecurityUtils and GlobalExceptionHandler are both justified
by framework convention; no rename needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MissionControlStrip is a document-processing pipeline visualiser — it
imports document-domain components (SegmentationColumn, TranscriptionColumn,
ReadyColumn) and belongs in the document domain. It was placed in
shared/dashboard, creating a shared → document coupling that the upcoming
boundaries rule would block.
Refs #410
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>