Commit Graph

1240 Commits

Author SHA1 Message Date
Marcel
3b6b117c75 fix(documents): cleanup timeline drag listeners on unmount (#385)
Pointerdown attaches three document-level listeners. Without an
explicit teardown, an unmount mid-drag (route change, view toggle,
viewport drops below lg) left them attached and they kept writing
to torn-down state cells.

Wrap the cleanup in $effect's return, which Svelte 5 invokes on
unmount. The listener-removal regression test pins this so the bug
cannot come back silently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 10:56:58 +02:00
Marcel
2e9ce8e1da fix(documents): surface timeline density fetch failures via console.warn (#385)
Previously a 5xx, network blip, or JSON parse error all collapsed
into the same silent "no buckets" rendering. The widget still
degrades gracefully — failure should not block the document list —
but operators and Sentry now see the failure in browser devtools
instead of having to reverse-engineer a missing chart.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 10:55:04 +02:00
Marcel
e5739d7f8e refactor(documents): extract TimelineControls (#385)
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 4m10s
CI / OCR Service Tests (pull_request) Successful in 36s
CI / Backend Unit Tests (pull_request) Failing after 3m16s
CI / Unit & Component Tests (push) Failing after 3m54s
CI / OCR Service Tests (push) Successful in 39s
CI / Backend Unit Tests (push) Failing after 3m34s
Splits the reset-zoom and clear buttons out of the orchestrator into
their own component. Closes part 3 (final) of Felix's component-split
concern. Orchestrator now composes four single-purpose children
(TimelineBars, TimelineYAxis, TimelineXAxis, TimelineControls) and
keeps only the pointer choreography that links them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 10:07:45 +02:00
Marcel
219d9a816e refactor(documents): extract Y-axis and X-axis components (#385)
Felix's review named "TimelineAxes" as one of four split targets.
The Y-axis and X-axis don't sit adjacent in the DOM — Y is a flex
sibling of the bars+X column — so two single-purpose components
beats a discriminator-prop component. tickIndicesFor and the
omitTickYear derivation move to TimelineXAxis where they belong.
Closes part 2 of Felix's component-split concern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 10:06:46 +02:00
Marcel
00682bac4f refactor(documents): extract TimelineBars from density filter (#385)
Splits the bar row + drag-window overlay + bar styling out of the
377-line orchestrator into a single-purpose component. The pointer
choreography (handle{PointerDown,DocumentMove,DocumentUp},
indexFromClientX, cleanupDragListeners) stays in the orchestrator
per Felix's note. Closes part 1 of Felix's component-split concern.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 10:04:38 +02:00
Marcel
77d282bbeb refactor(documents): split triggerSearch by zoom semantics (#385)
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>
2026-05-08 10:02:02 +02:00
Marcel
52827ccc87 feat(documents): hide timeline density widget below lg (#385)
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>
2026-05-08 10:00:47 +02:00
Marcel
61d1c1793b fix(documents): bump dark-mode timeline bar contrast to 3.33:1 (#385)
Previous #0d3358 measured 1.44:1 against the dark surface (#011526),
failing WCAG 1.4.11 (Non-text Contrast) for large UI elements.
#3a6e8c clears 3:1 with 3.33:1 while staying in the navy palette.
Closes Leonie's dark-mode contrast finding.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:59:29 +02:00
Marcel
c06987da95 style(documents): respect prefers-reduced-motion on timeline bars (#385)
Disables the .bar-fill background-color transition for users who set
prefers-reduced-motion: reduce. Closes Leonie's vestibular-comfort
finding for users running the timeline alongside the live drag
cursor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:57:14 +02:00
Marcel
5028082da4 feat(documents): aria-live drag preview for screen readers (#385)
Adds a visually-hidden polite live region whose text reflects the
current drag range using the existing timeline_dragging_aria_live
i18n key. Closes Leonie's WCAG follow-the-drag-preview gap and turns
the previously orphaned i18n key into used markup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:56:20 +02:00
Marcel
ea106e9414 style(documents): timeline axis text to 12px / h-4 (#385)
text-[10px] failed Leonie's 12px font floor. Bumps Y-axis labels and
the X-axis tick row to text-xs (12px); the X-axis row grows to h-4 to
accommodate the line height. Regression-pinned via two new specs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:54:43 +02:00
Marcel
dfdcacdb85 feat(documents): localise timeline bar aria-label (#385)
Replaces the raw "1915-08 · 5" aria-label, which a screen reader
announces as "1915 dash 08 middle dot 5", with the i18n template
timeline_bar_aria("{when}, {count} ...") and a getLocale-formatted
month/year string. Closes Leonie's WCAG 1.3.1 / 4.1.2 finding and
Felix's localisation flag.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:53:18 +02:00
Marcel
c9fb677499 chore(i18n): remove dead timeline keys (#385)
The timeline_count_label, timeline_loading, timeline_filtered_count,
and timeline_zoom_in keys were never referenced from src/. Felix's
review flagged them as 15 dead strings to translate. Removed across
de/en/es; the timeline_dragging_aria_live key is kept and will be
wired up in the next commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-08 09:48:10 +02:00
Marcel
5d92f5a32b refactor(documents): rework timeline UX after live testing (#385)
Some checks failed
CI / Unit & Component Tests (push) Failing after 4m29s
CI / OCR Service Tests (push) Successful in 48s
CI / Backend Unit Tests (push) Failing after 3m46s
CI / Unit & Component Tests (pull_request) Failing after 4m31s
CI / OCR Service Tests (pull_request) Successful in 38s
CI / Backend Unit Tests (pull_request) Failing after 3m31s
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>
2026-05-08 08:54:48 +02:00
Marcel
a6123e1867 feat(documents): zoom-in tool for the timeline (#385)
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>
2026-05-07 23:23:38 +02:00
Marcel
bd81ff81f9 feat(documents): drag-to-select-range on the timeline (#385)
The original AC required drag-to-select; the MVP shipped with click-only.
This adds pointer-driven range selection while preserving keyboard access:

- Pointer events (pointerdown / pointerenter / pointerup) drive the drag.
  Pointer capture on pointerdown so the cursor leaving the bar still
  produces drag-end events. Live preview class `in-drag-preview` highlights
  the spanning bars while dragging; the URL/list refetch only fires on
  pointerup (Felix R3).
- Click handler kept for keyboard activation (Enter/Space on focused bar).
  A `suppressClick` flag prevents the synthesized click after a mouse
  pointerup from double-emitting.
- Drag from later → earlier still emits ascending boundaries (drag direction
  doesn't matter).
- Existing single-click keyboard selection unchanged.

4 new component tests cover the drag paths plus the live-preview class.
Existing 13 tests (single click, year mode, clear, visibility) still green.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 23:16:48 +02:00
Marcel
76023a99ed feat(documents): timeline density refetches when other filters change (#385)
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>
2026-05-07 23:10:12 +02:00
Marcel
59a2faa145 fix(documents): collapse timeline to year bars when range > 240 months (#385)
Surfaced during proofshot: the production archive spans 1873 → 2023
(≈1809 month bars). With flex-1 + gap-px on a 1280 px container, every
pixel was consumed by gaps and bars rendered at 0 px width — visible as
"empty box, no bars".

Fix:
- Add aggregateToYears(buckets) that sums month counts per year and
  returns YYYY-keyed entries.
- Add selectionBoundaryFrom/To that handle both YYYY and YYYY-MM labels
  (Jan 1 → Dec 31 for years, first → last day for months).
- Component switches to year granularity when the gap-filled month
  sequence exceeds 240 entries (~20 years), keeping each bar clickable.
- Drop the gap-px between bars and add min-w-px so sub-pixel rounding
  still leaves something visible.

5 new tests cover aggregation, boundary helpers, and the component-level
year-mode + click behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 22:54:02 +02:00
Marcel
8e29f428d7 fix(documents): merge +page.server data into +page.ts return (#385)
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>
2026-05-07 22:29:24 +02:00
Marcel
6786c0112d feat(documents): wire TimelineDensityFilter into /documents/+page (#385)
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>
2026-05-07 22:16:05 +02:00
Marcel
d43d73f231 feat(documents): add TimelineDensityFilter component (#385)
Density timeline widget: one bar per month within minDate/maxDate,
proportional heights, click-to-select-month with onchange callback,
and a clear button when a selection is active.

Notable details:
- Hidden entirely when density is null (mobile / calendar view; +page.ts
  controls the gating).
- Zero-count months render at 2 px so the time axis stays readable
  (Leonie's design intent overrides AC's literal "no bar" wording).
- Component-scoped --timeline-bar-idle CSS var for the dim idle color
  (light: mint-tinted rgba; dark: structural navy #0d3358 — meets
  WCAG 1.4.11 3:1 against surface, unlike the spec's #0E2535).
- Clear button is a real <button> with aria-label per Nora's a11y note.
- Bars are <button>s with aria-pressed selection state.
- Drag-range, tooltip, and year-tick labels are deferred for follow-ups —
  the AC-required behaviours (click filter, clear, AND-with-other-filters)
  are all in.

11 vitest-browser tests cover visibility gating, bar rendering with
gap-fill, zero-height floor, and selection/clear callback paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 22:13:58 +02:00
Marcel
ad82f2e1e2 feat(documents): add fetchDensity helper and /documents/+page.ts (#385)
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>
2026-05-07 22:06:57 +02:00
Marcel
5fdcc95c3d feat(documents): add timeline helpers (boundary + gap-fill) (#385)
Pure utilities backing the TimelineDensityFilter component:
- monthBoundaryFrom/To convert YYYY-MM into LocalDate strings the existing
  /api/documents/search accepts (first/last day of the month).
- buildMonthSequence enumerates months between minDate and maxDate, crossing
  year boundaries.
- fillDensityGaps merges sparse backend buckets with the full month sequence,
  producing zero-count entries for months that the API omitted.

14 unit tests cover leap years, year boundaries, null inputs, and out-of-order
buckets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 22:04:21 +02:00
Marcel
142459b916 feat(i18n): add timeline density widget keys (#385)
Five new keys across de/en/es for the upcoming TimelineDensityFilter:
aria label, clear selection, abbreviated count label, loading state, and
parametrised filtered-count message.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 22:02:15 +02:00
Marcel
b31979c4f0 chore(frontend): regenerate API types after density endpoint (#385)
Adds DocumentDensityResult, MonthBucket and the /api/documents/density path
to the openapi-typescript output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 22:00:58 +02:00
Marcel
baa0a9811c chore: merge main into branch; resolve ChronikRow conflict
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 4m21s
CI / OCR Service Tests (pull_request) Successful in 42s
CI / Backend Unit Tests (pull_request) Failing after 3m36s
CI / Unit & Component Tests (push) Failing after 3m46s
CI / OCR Service Tests (push) Successful in 31s
CI / Backend Unit Tests (push) Failing after 3m17s
TODO/SECURITY placeholders from main are superseded by the #454 implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 20:05:12 +02:00
Marcel
9ef3c82398 fix(review): address review blockers from PR #475
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 3m51s
CI / OCR Service Tests (pull_request) Successful in 47s
CI / Backend Unit Tests (pull_request) Failing after 3m31s
- CommentData.java: add @Nullable on annotationId to match codebase convention
- DashboardService: isEmpty() → isBlank() for commentPreview null-guard
- ChronikRow.svelte: always set aria-label on comment rows (not only when preview present)
- ChronikRow.svelte.spec.ts: add test for aria-label on comment row without preview

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 19:54:56 +02:00
Marcel
abe8ab8668 refactor(comment): remove dead findAnnotationIdsByIds; fix aria-label i18n; rename misleading test
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m36s
CI / OCR Service Tests (pull_request) Successful in 35s
CI / Backend Unit Tests (pull_request) Failing after 3m24s
CI / Unit & Component Tests (push) Failing after 3m30s
CI / OCR Service Tests (push) Successful in 38s
CI / Backend Unit Tests (push) Failing after 3m22s
- Remove `findAnnotationIdsByIds` from CommentService — no production caller exists now
  that DashboardService uses `findDataByIds` directly; along with its test coverage
- Fix aria-label construction in ChronikRow: pass actorName to i18n message function
  instead of manually prepending the actor, so all locales render correctly
- Rename `findDataByIds_does_not_truncate_at_exactly_120_chars` →
  `findDataByIds_preserves_content_at_exactly_120_chars` for accurate description

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 19:05:46 +02:00
Marcel
e3a3f209f9 feat(chronik): render commentPreview in ChronikRow; add aria-label for screen readers
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m49s
CI / OCR Service Tests (push) Successful in 45s
CI / Backend Unit Tests (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Failing after 3m37s
CI / OCR Service Tests (pull_request) Successful in 48s
CI / Backend Unit Tests (pull_request) Failing after 3m21s
Replace the „…" placeholder with {item.commentPreview ?? '„…"'}. Plain-text
binding — no {@html} — as specified in the security note from issue #285.
Adds aria-label to the <a> wrapper for COMMENT_ADDED rows that carry a preview,
giving screen reader users the full context in one announcement.

Generated api.ts updated manually to include commentPreview?:string; will be
regenerated by npm run generate:api once the backend is running.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 18:53:26 +02:00
Marcel
c10e8e8a3a fix(tests): replace flaky waitFor with synchronous dispatchEvent in edit-page delete spec
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m54s
CI / OCR Service Tests (pull_request) Successful in 32s
CI / Backend Unit Tests (pull_request) Failing after 3m18s
CI / Unit & Component Tests (push) Failing after 3m51s
CI / OCR Service Tests (push) Successful in 47s
CI / Backend Unit Tests (push) Failing after 3m19s
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>
2026-05-07 13:37:13 +02:00
Marcel
0c765d8112 fix(tests): fix 13 pre-existing vitest-browser spec failures
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m54s
CI / OCR Service Tests (pull_request) Successful in 33s
CI / Backend Unit Tests (pull_request) Failing after 3m13s
CI / Unit & Component Tests (push) Failing after 3m48s
CI / OCR Service Tests (push) Successful in 39s
CI / Backend Unit Tests (push) Failing after 3m24s
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>
2026-05-07 13:15:54 +02:00
Marcel
cdb54c7545 fix(tests): fix 2 more pre-existing vitest-browser spec failures
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>
2026-05-07 12:22:06 +02:00
Marcel
6ab7abb9df fix(tests): fix 3 pre-existing vitest-browser spec failures
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m41s
CI / OCR Service Tests (push) Successful in 43s
CI / Backend Unit Tests (push) Failing after 3m30s
CI / Unit & Component Tests (pull_request) Failing after 3m32s
CI / OCR Service Tests (pull_request) Successful in 40s
CI / Backend Unit Tests (pull_request) Failing after 3m17s
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>
2026-05-07 11:27:24 +02:00
Marcel
d28c455991 cleanup(legibility): repo hygiene — untrack artifacts, update gitignore
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m44s
CI / OCR Service Tests (push) Successful in 44s
CI / Backend Unit Tests (push) Failing after 3m43s
CI / Unit & Component Tests (pull_request) Failing after 3m42s
CI / OCR Service Tests (pull_request) Successful in 43s
CI / Backend Unit Tests (pull_request) Failing after 3m23s
CLEANUP-4 (#415):

Untracked from git (files stay on disk where appropriate):
- frontend/e2e/.auth/user.json — dev credential, already gitignored in
  frontend/.gitignore; git rm --cached so the rule takes effect
- proofshot-artifacts/ (44 files, ~7.6MB) — browser verification
  screenshots committed by mistake; added root .gitignore entry
- frontend/.svelte-kit.old/ — stale type stub from stammbaum route
  rename; deleted from disk
- frontend/test-results.locked/ — Playwright E2E artifacts; deleted
  from disk
- node_modules/.vite/vitest/.../results.json — Vite test cache committed
  by mistake

Deleted from repo:
- package.json / package-lock.json at root (3 testing-library devDeps
  with no justification for living outside frontend/)

.gitignore additions:
- root: proofshot-artifacts/, node_modules/
- frontend: **/test-results.locked/, **/.svelte-kit.old/

After this commit, git status on a fresh clone shows zero unexpected
items (only docs/superpowers/ and familienarchiv-408/ remain untracked,
both pre-existing).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07 09:27:09 +02:00
Marcel
0fa90d58cb cleanup(legibility): convert TODOs to issue refs; justify naming violators
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>
2026-05-07 09:25:55 +02:00
Marcel
d01b9a7508 docs(claude-md): replace hex values with CSS var refs, expand route trees
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m31s
CI / OCR Service Tests (pull_request) Successful in 35s
CI / Backend Unit Tests (pull_request) Failing after 3m16s
CI / Unit & Component Tests (push) Failing after 3m23s
CI / OCR Service Tests (push) Successful in 30s
CI / Backend Unit Tests (push) Failing after 3m19s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 09:01:40 +02:00
Marcel
5c72364899 docs: fix stale CLAUDE.md content after design-system refactoring
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m45s
CI / OCR Service Tests (push) Successful in 44s
CI / Backend Unit Tests (push) Failing after 3m25s
CI / Unit & Component Tests (pull_request) Failing after 3m29s
CI / OCR Service Tests (pull_request) Successful in 34s
CI / Backend Unit Tests (pull_request) Failing after 3m13s
Brand colors, font name, dev port, route tree, and card pattern were
all outdated relative to layout.css and the current route structure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 08:49:47 +02:00
Marcel
86c13a230c docs(legibility): migrate CLAUDE.md rules into human docs — DOC-7
Processes all 7 CLAUDE.md files according to the 3-bucket classification.
Migration targets (CONTRIBUTING.md, docs/ARCHITECTURE.md, docs/DEPLOYMENT.md,
domain READMEs) are introduced by DOC-2/4/5/6 — this PR must merge last.

### scripts/CLAUDE.md → scripts/README.md
New `scripts/README.md` with full script documentation (preserving the
⚠️ destructive-operation warning on reset-db.sh). `scripts/CLAUDE.md`
reduced to a pointer + "document new scripts in README.md" reminder.

### .devcontainer/CLAUDE.md → .devcontainer/README.md
New `.devcontainer/README.md` with all configuration, usage, and limitations.
`devcontainer/CLAUDE.md` reduced to a single pointer line.

### docs/CLAUDE.md → docs/README.md
New `docs/README.md` covering the folder structure, ADR guide, infrastructure
docs, and specs folder. `docs/CLAUDE.md` reduced to pointer + ADR reminder.

### ocr-service/CLAUDE.md
Reduced to pointer to `ocr-service/README.md` (content migrated in DOC-6).
Kept LLM reminders: single-node constraint, ALLOWED_PDF_HOSTS SSRF risk.

### backend/CLAUDE.md
- Layering Rules → pointer to docs/ARCHITECTURE.md
- Error Handling → pointer to CONTRIBUTING.md + reminder
- Security/Permissions → pointer to docs/ARCHITECTURE.md + reminder
- Package Structure → tagged TODO post-REFACTOR-1
- Fixed errors.ts path to frontend/src/lib/shared/errors.ts
- Added ANNOTATE_ALL + BLOG_WRITE to permission list
- Key Entities, Entity Code Style, Services → kept (Bucket-2)

### root CLAUDE.md
- Stack, Infrastructure, Dev Container → pointers
- Layering Rules, Error Handling, Security, OpenAPI, API Client,
  Date Handling, UI Components, Frontend Error Handling → pointers + reminders
- Package Structure → tagged TODO post-REFACTOR-1
- Domain Model, Entity Code Style, Form Actions, Styling → kept (Bucket-2)

### frontend/CLAUDE.md
- API Client Pattern, Date Handling → pointers + reminders
- Key UI Components → pointer to domain READMEs
- Styling, Form Actions, How to Run, Vite Proxy, i18n → kept (Bucket-2)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 07:41:02 +02:00
Marcel
a1b89670c0 docs(legibility): add 18 per-domain README.md files (DOC-6)
Backend (9): document, person, tag, user, geschichte, notification,
ocr, audit, dashboard.
Frontend (8): document, person, tag, user, geschichte, notification,
ocr, shared.
OCR service (1): ocr-service/README.md.

Each README covers: what the domain owns, explicit non-ownership,
public surface (verified by grep against the codebase), internal
layout, and cross-domain dependencies.

Closes #400
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 07:36:38 +02:00
Marcel
39e7ee2c71 fix(e2e): use dedicated reset user instead of admin in password-reset test
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m34s
CI / OCR Service Tests (push) Successful in 37s
CI / Backend Unit Tests (push) Failing after 3m13s
Introduces a separate reset@familyarchive.local / reset123 seed account
(e2e profile only) so the password-reset flow test never touches the
shared admin credentials.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 21:17:00 +02:00
Marcel
f14c8b9eea test(e2e): fix deep-link Fertig selector — strict mode violation at desktop viewport
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m28s
CI / OCR Service Tests (pull_request) Successful in 44s
CI / Backend Unit Tests (pull_request) Failing after 3m24s
CI / Unit & Component Tests (push) Failing after 3m47s
CI / OCR Service Tests (push) Successful in 42s
CI / Backend Unit Tests (push) Failing after 3m23s
getByRole('button', { name: 'Fertig' }) matched two buttons at 1440px width:
the transcribe-mode Fertig button and 'Alle als fertig markieren'. Add exact: true.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 20:08:01 +02:00
Marcel
2632434263 test(e2e): fix J5 relationship selector — scope to Beziehungen section, drop baseURL
Some checks failed
CI / OCR Service Tests (pull_request) Successful in 38s
CI / Unit & Component Tests (push) Failing after 3m13s
CI / OCR Service Tests (push) Successful in 34s
CI / Backend Unit Tests (push) Failing after 3m14s
CI / Unit & Component Tests (pull_request) Failing after 3m25s
CI / Backend Unit Tests (pull_request) Failing after 3m22s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:12:12 +02:00
Marcel
5518122b69 test(e2e): fix notification-deep-link — relative paths, afterAll cleanup, accurate J10 comment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:12:12 +02:00
Marcel
64110033bd test(e2e): replace E2E_BASE_URL absolute URL construction with relative paths
All page.goto() calls in documents.spec.ts now use relative paths (/documents/{id})
so Playwright's configured baseURL is the single source of truth. Removes the
fragility of keeping process.env.E2E_BASE_URL in sync with playwright.config.ts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:12:12 +02:00
Marcel
29bf45d15a test(e2e): fix J6 — use correct tag URL param, update report from sender to tag filter
The test was using tagId=nonexistent-tag-id which is not a recognised search parameter;
the correct param is tag= (tag name). Updated the test and the coverage report to
accurately describe what is verified: text + tag filter AND combination. The sender
filter test remains an acknowledged gap noted in the report.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:12:12 +02:00
Marcel
3f25f1fd73 test(e2e): fix J4 — add page reload assertion, unique title, afterAll cleanup, precise selector
Four concerns addressed:
- Persistence: reloads the detail page after save and re-asserts the tag link,
  making the report's "after page reload" claim accurate
- Unique title: adds stamp to document title to prevent accumulation across runs
- Cleanup: afterAll deletes the test document
- Selector: replaces getByText(newTagName) with a[href*="?tag="] scoped to the tag link

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:12:12 +02:00
Marcel
fcd91c2e81 test(e2e): fix J3 — seed unique tag via API, scope chip selector, add afterAll cleanup
Three concerns addressed:
- Race condition: "Familie" tag is renamed by admin tests; now seeds a unique
  timestamped tag via a throwaway document PUT so J3 never depends on seeded data
- Chip selector: replaces getByText(/Familie/) with a[href*="?tag="] scoped to the
  actual tag link in the metadata section
- Cleanup: afterAll deletes both the test document and the seeder document

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:12:12 +02:00
Marcel
c7bf35f011 test(e2e): tighten J12 import status regex to match only import-specific messages
The previous regex /Importiert|Dokument|Import|Läuft|DONE|laufend/i was too broad —
it would match almost any German text on the page including unrelated copy. Replaced
with /Import läuft|Import abgeschlossen|Fehler:/ which matches only the three status
messages the mass import feature actually emits.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 19:12:12 +02:00
Marcel
20cceefbe1 test(e2e): add coverage for all 12 critical journeys (TEST-3 #405)
Some checks failed
CI / Backend Unit Tests (pull_request) Failing after 3m23s
CI / Unit & Component Tests (pull_request) Failing after 3m23s
CI / OCR Service Tests (pull_request) Successful in 37s
CI / Unit & Component Tests (push) Failing after 3m36s
CI / OCR Service Tests (push) Successful in 35s
CI / Backend Unit Tests (push) Failing after 3m27s
Adds docs/audits/e2e-coverage-report.md mapping all 12 critical journeys
to their test files. Fills the 6 coverage gaps with new e2e tests:

- J1: Register via invite code (auth.spec.ts)
- J3: Edit document tags via TagInput (documents.spec.ts)
- J4: Create brand-new tag via TagInput (documents.spec.ts)
- J5: Add SPOUSE_OF relationship on person edit page (persons.spec.ts)
- J6: Multi-filter search (text + date, text + tagId) (documents.spec.ts)
- J10: Notification bell opens dropdown (notification-deep-link.spec.ts)
- J11: Non-admin blocked from /admin/* (permissions.spec.ts)
- J12: Mass import trigger shows status (admin.spec.ts)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 18:10:17 +02:00
Marcel
d9a4faf4da refactor(document): remove statusLabel() alias, use formatDocumentStatus directly
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m17s
CI / OCR Service Tests (push) Successful in 33s
CI / Backend Unit Tests (push) Failing after 3m22s
statusLabel() was a one-line alias for formatDocumentStatus() with no
additional behaviour. Remove it and update DocumentStatusChip.svelte to
call formatDocumentStatus() directly. Remove the corresponding alias
test suite from the spec file.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 18:09:01 +02:00