feat(journey-editor): JourneyEditor frontend — issue #753 #792

Open
marcel wants to merge 63 commits from feat/issue-753-journey-editor into feat/issue-750-lesereisen-data-model
Owner

Summary

  • Generalizes createBlockDragDrop<T> — accepts any { id: string } shape, used by JourneyEditor alongside the existing transcription editor
  • Extracts GeschichteSidebar.svelte — Status badge + PersonMultiSelect with <details> mobile collapsibles (44px touch targets)
  • Builds DocumentPickerDropdown.svelte — single-select document search with aria-disabled for already-added items and sr-only "bereits enthalten" hint
  • Refactors DocumentMultiSelect.svelte onto createTypeahead — removes raw setTimeout debounce
  • Builds JourneyItemRow.svelte — drag handle, move-up/down buttons, note textarea (PATCH on blur), interlude visual treatment, inline remove confirm
  • Builds JourneyAddBar.svelte — "Brief hinzufügen" opens DocumentPickerDropdown; "Zwischentext hinzufügen" opens inline draft form with aria-disabled confirm until text non-empty
  • Builds JourneyEditor.svelte — main orchestrator: title, intro textarea, drag-reorder list, optimistic remove/reorder (pre-mutation snapshot rollback on failure) and pessimistic add (item list updated only on API success), createUnsavedWarning, GeschichteSidebar
  • Wires geschichten/[id]/edit/+page.svelte — branches on geschichte.type: JOURNEY → JourneyEditor, STORY → GeschichteEditor (unchanged)
  • i18n: 30+ new journey_* keys in de/en/es; 4 new ErrorCode values; interlude CSS semantic tokens (--color-interlude-bg/border/label) with light + dark values

Design notes

  • Note max length: spec says 2000 chars, backend constant is 5000. Implemented maxlength="2000" per spec — discrepancy tracked in issue #793 for resolution.
  • Intro field: stored raw (no TipTap sanitizer for JOURNEY type) — sent as plain string to body field; backend skips sanitizer for JOURNEY (implemented in issue #751).
  • Item mutations do NOT call markDirty() — reorder/add/remove are persisted optimistically (remove/reorder) or pessimistically (add); only title + intro edits trigger the unsaved-changes guard.
  • Drag on desktop, move buttons on all screensdata-drag-handle/data-block-wrapper contract reused from transcription editor unchanged.
  • Add is pessimistic, remove/reorder are optimistichandleAddDocument and handleAddInterlude only update items after a successful API response; handleRemove and handleReorder pre-update with snapshot rollback on failure.

Test plan

  • Type-check: cd frontend && npx tsc --noEmit — no errors in any new file
  • Unit tests: npm run test -- --project=server — all server-side tests green
  • Component test files written for: DocumentPickerDropdown, JourneyItemRow, JourneyAddBar, JourneyEditor — run with --project=client in CI
  • E2E journey path (requires running backend) — deferred to follow-up issue
  • Manual: navigate to a JOURNEY-type record /geschichten/[id]/edit, verify JourneyEditor renders; add document, add interlude, reorder, check STORY type still opens GeschichteEditor

🤖 Generated with Claude Code


Review round 2 addendum (2026-06-10)

Breaking API change (intentional): POST/PATCH /api/geschichten now return GeschichteView instead of the raw Geschichte entity (author → AuthorView {id, displayName}, persons → PersonView[], no documents). Motivation: with open-in-view: false the lazy items proxy is dead at serialization time — every save/publish 500'd. See ADR-036. The same round also dropped email from GeschichteSummary.AuthorSummary and the generated Geschichte schema no longer exists.

Corrections to the original body:

  • The interlude confirm uses native disabled (not aria-disabled) since b30c0d7f.
  • The note max length is settled at 2000 end-to-end (backend constant, V74 CHECK constraint, frontend maxlength, i18n) — resolves #793.
  • "Item mutations do NOT call markDirty()" still holds, but remove is no longer instant-optimistic: it shows the spec'd pending state (dimmed + aria-live) until the DELETE resolves.
  • "Backend skips sanitizer for JOURNEY": this is now actually true (it wasn't at first push) — JOURNEY intros are stored verbatim and rendered via text interpolation only.

Regen artifacts: src/lib/generated/api.ts regeneration swept in unrelated drift from the base branch (/api/admin/backfill-titles added, getConversation removed, hasTranscription, subtreeDocumentCount) — these are spec catch-up, not changes of this PR.

i18n register: comp_typeahead_error/comp_typeahead_no_results are Sie-form on purpose — all error_* strings are Sie project-wide (4c620619); the du-form in journey UI hints is a different message class.

Deferred work tracked in #796 (E2E journey path, axe dual-theme/320px scan, mobile sticky save bar, intro auto-resize, publish-disabled focus hand-off, keyed-each + noteSaving race tests).

## Summary - **Generalizes `createBlockDragDrop<T>`** — accepts any `{ id: string }` shape, used by JourneyEditor alongside the existing transcription editor - **Extracts `GeschichteSidebar.svelte`** — Status badge + PersonMultiSelect with `<details>` mobile collapsibles (44px touch targets) - **Builds `DocumentPickerDropdown.svelte`** — single-select document search with `aria-disabled` for already-added items and sr-only "bereits enthalten" hint - **Refactors `DocumentMultiSelect.svelte`** onto `createTypeahead` — removes raw `setTimeout` debounce - **Builds `JourneyItemRow.svelte`** — drag handle, move-up/down buttons, note textarea (PATCH on blur), interlude visual treatment, inline remove confirm - **Builds `JourneyAddBar.svelte`** — "Brief hinzufügen" opens `DocumentPickerDropdown`; "Zwischentext hinzufügen" opens inline draft form with `aria-disabled` confirm until text non-empty - **Builds `JourneyEditor.svelte`** — main orchestrator: title, intro textarea, drag-reorder list, **optimistic remove/reorder** (pre-mutation snapshot rollback on failure) and **pessimistic add** (item list updated only on API success), `createUnsavedWarning`, `GeschichteSidebar` - **Wires `geschichten/[id]/edit/+page.svelte`** — branches on `geschichte.type`: JOURNEY → `JourneyEditor`, STORY → `GeschichteEditor` (unchanged) - **i18n**: 30+ new `journey_*` keys in de/en/es; 4 new `ErrorCode` values; interlude CSS semantic tokens (`--color-interlude-bg/border/label`) with light + dark values ## Design notes - **Note max length**: spec says 2000 chars, backend constant is 5000. Implemented `maxlength="2000"` per spec — discrepancy tracked in issue #793 for resolution. - **Intro field**: stored raw (no TipTap sanitizer for JOURNEY type) — sent as plain string to `body` field; backend skips sanitizer for JOURNEY (implemented in issue #751). - **Item mutations do NOT call `markDirty()`** — reorder/add/remove are persisted optimistically (remove/reorder) or pessimistically (add); only title + intro edits trigger the unsaved-changes guard. - **Drag on desktop, move buttons on all screens** — `data-drag-handle`/`data-block-wrapper` contract reused from transcription editor unchanged. - **Add is pessimistic, remove/reorder are optimistic** — `handleAddDocument` and `handleAddInterlude` only update `items` after a successful API response; `handleRemove` and `handleReorder` pre-update with snapshot rollback on failure. ## Test plan - [x] Type-check: `cd frontend && npx tsc --noEmit` — no errors in any new file - [x] Unit tests: `npm run test -- --project=server` — all server-side tests green - [x] Component test files written for: `DocumentPickerDropdown`, `JourneyItemRow`, `JourneyAddBar`, `JourneyEditor` — run with `--project=client` in CI - [ ] E2E journey path (requires running backend) — deferred to follow-up issue - [ ] Manual: navigate to a JOURNEY-type record `/geschichten/[id]/edit`, verify JourneyEditor renders; add document, add interlude, reorder, check STORY type still opens GeschichteEditor 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- ## Review round 2 addendum (2026-06-10) **Breaking API change (intentional):** `POST`/`PATCH /api/geschichten` now return `GeschichteView` instead of the raw `Geschichte` entity (author → `AuthorView {id, displayName}`, persons → `PersonView[]`, no `documents`). Motivation: with `open-in-view: false` the lazy `items` proxy is dead at serialization time — every save/publish 500'd. See ADR-036. The same round also dropped `email` from `GeschichteSummary.AuthorSummary` and the generated `Geschichte` schema no longer exists. **Corrections to the original body:** - The interlude confirm uses native `disabled` (not `aria-disabled`) since b30c0d7f. - The note max length is settled at **2000** end-to-end (backend constant, V74 CHECK constraint, frontend maxlength, i18n) — resolves #793. - "Item mutations do NOT call markDirty()" still holds, but remove is no longer instant-optimistic: it shows the spec'd pending state (dimmed + aria-live) until the DELETE resolves. - "Backend skips sanitizer for JOURNEY": this is now actually true (it wasn't at first push) — JOURNEY intros are stored verbatim and rendered via text interpolation only. **Regen artifacts:** `src/lib/generated/api.ts` regeneration swept in unrelated drift from the base branch (`/api/admin/backfill-titles` added, `getConversation` removed, `hasTranscription`, `subtreeDocumentCount`) — these are spec catch-up, not changes of this PR. **i18n register:** `comp_typeahead_error`/`comp_typeahead_no_results` are Sie-form on purpose — all `error_*` strings are Sie project-wide (4c620619); the du-form in journey UI hints is a different message class. **Deferred work tracked in #796** (E2E journey path, axe dual-theme/320px scan, mobile sticky save bar, intro auto-resize, publish-disabled focus hand-off, keyed-each + noteSaving race tests).
marcel added 9 commits 2026-06-09 13:01:24 +02:00
Removes the hard-typed TranscriptionBlockData constraint so JourneyEditor
can reuse the pointer-drag module without importing transcription types.
Selector contract (data-block-wrapper / data-drag-handle) unchanged.
Adds type-regression guard test verified via tsc --noEmit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves Status + Persons sections into a shared component so both
GeschichteEditor (STORY) and the upcoming JourneyEditor (JOURNEY) can
use the same sidebar without duplicating markup. Adds <details> mobile
collapsibles with 44px summary hit areas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 30+ journey_* message keys added to de/en/es.json. Four new ErrorCode
values for journey item operations wired into errors.ts + getErrorMessage().
Interlude CSS primitives (--c-interlude-bg/border/label) defined for light
and dark themes so JourneyItemRow can reference them via semantic aliases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
New DocumentPickerDropdown: single-select document search with aria-disabled
for already-added items and sr-only "bereits enthalten" hint. DocumentMultiSelect
refactored to use createTypeahead, removing raw setTimeout/debounceTimer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Item row with drag handle, move-up/down buttons, inline note textarea (PATCH
on blur), interlude visual treatment, and inline confirm for removes that
would discard a note. Interlude note cannot be cleared (blocked on empty).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two add buttons: document picker (DocumentPickerDropdown) and interlude inline
draft form. Interlude confirm is aria-disabled until text is non-empty. Closing
one panel opens the other. Tests cover all three plan test cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Main editing surface for JOURNEY-type Geschichten. Manages sorted item list
with optimistic add/remove/reorder (rollback on failure), drag-and-drop reorder
via createBlockDragDrop, intro textarea, and sidebar via GeschichteSidebar.
Publish requires at least one item + non-empty title.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Static imports for both editors; type-aware <h1> title; JOURNEY type routes
to JourneyEditor, STORY type continues to GeschichteEditor unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docs(journey-editor): update README and strike stale spec references
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m38s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m42s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m7s
1f9107b620
Add JourneyEditor, JourneyItemRow, JourneyAddBar, GeschichteSidebar to the
geschichte README props table. Strike @dnd-kit/svelte-dnd-action library refs
and raw orange-*/blue-600 color classes in the editor spec HTML.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-06-09 13:21:25 +02:00
fix(tests): resolve 9 CI test failures in journey-editor specs
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m29s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 3m52s
CI / fail2ban Regex (pull_request) Successful in 48s
CI / Semgrep Security Scan (pull_request) Successful in 24s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m7s
ddcf61cc5e
- useBlockDragDrop: add runtime expect() alongside expectTypeOf so
  browser-mode runner counts at least one assertion
- JourneyAddBar: use exact:true on 'Hinzufügen' button — partial match
  was hitting '+ Brief hinzufügen' and '+ Zwischentext hinzufügen' too
- JourneyEditor: fix 4 issues — drop wrong not.toBeInTheDocument()
  (placeholder creates accessible name); pass title:'' in publish-disabled
  test (default was non-empty); use getByPlaceholder for interlude
  textarea to avoid 4-element strict-mode violation; exact:true for
  'Hinzufügen' button
- DocumentPickerDropdown: use .click({force:true}) on aria-disabled
  option — userEvent refuses non-enabled elements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 8 commits 2026-06-09 14:47:18 +02:00
The screen-reader live announcement was calling m.journey_item_moved()
without the required {position, total, newPosition} parameters, which
the i18n template uses to build the full announcement string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the item list area was blank when no items had been added.
The empty-state paragraph uses the existing journey_empty_state i18n key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both move-up and move-down buttons had inline style="min-height: 22px"
which is below the WCAG 2.2 success criterion 2.5.8 (44×44 CSS pixels
minimum). Replaced with Tailwind min-h-[44px] min-w-[44px] classes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The remove button was using the confirmation-question text as its
aria-label. Added a new dedicated journey_remove_item_aria key
in all three locales so the button has a clear accessible name
before the confirmation dialog opens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The document search input had no accessible label — role="combobox"
without a label is an accessibility violation. Bound aria-label to
the existing placeholder prop so screen readers announce the field purpose.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add move-up and move-down tests that verify PUT /items/reorder is called
  with the swapped ID order; 50ms delay accounts for two await levels before
  csrfFetch is called (click → handleMoveUp → handleReorder → csrfFetch)
- Replace vacuous 'isDirty stays false' test (was asserting a dialog that
  never renders) with a meaningful publish-button-enabled assertion after
  adding an item
- Update remove button query from 'Wirklich entfernen?' to 'Eintrag entfernen'
  to match the new journey_remove_item_aria aria-label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removed the inline vi.unstubAllGlobals() call from the end of the
'reveals picker' test body and added it to the shared afterEach hook
so every test in the file gets the same global cleanup regardless of
which test runs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docs: document 4 new journey/geschichte ErrorCodes in CLAUDE.md and ARCHITECTURE.md
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m37s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m49s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m7s
1b2776aa89
Both error-handling sections now list JOURNEY_ITEM_NOT_IN_JOURNEY,
JOURNEY_NOTE_TOO_LONG, JOURNEY_DOCUMENT_ALREADY_ADDED, and
GESCHICHTE_TYPE_IMMUTABLE alongside the existing security codes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-06-09 15:08:08 +02:00
fix(journey-item-row): update JourneyItemRow spec to match new aria-label
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m22s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m48s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m7s
db4a5c0028
The 'remove confirm' tests were still finding the remove button by the
old label ('Wirklich entfernen?'). After the aria-label change the
button is named 'Eintrag entfernen'; the visible confirmation text
'Wirklich entfernen?' in the DOM is unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 6 commits 2026-06-09 16:48:13 +02:00
Interlude is a first-class editorial concept introduced by the JourneyEditor
(#753): a note-only JourneyItem with no backing document, stored in the note
column and distinguished visually by --color-interlude-* CSS tokens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the '#753 deferred' placeholder with the actual implementation:
JOURNEY edit route now opens JourneyEditor (item list, drag/move-up/down,
add bar, sidebar) while STORY route continues to use GeschichteEditor.
Add item-mutation endpoints to the relation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous check used document.body.textContent which includes ARIA labels
and hidden elements — a match in those could misfire the indexOf comparison.
compareDocumentPosition(DOCUMENT_POSITION_FOLLOWING) checks DOM tree position
directly and is not affected by non-visible text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The noteError alert path (role=alert paragraph) was untested.
The catch block in handleNoteBlur was already implemented; this
test verifies it renders the alert when onNotePatch rejects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A persistent non-empty aria-live region can cause stale re-announcements
on adjacent DOM mutations. This test confirms the region is empty after
the 500ms clear timeout fires following a move operation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(journey-editor): clear liveAnnounce 500ms after move; document svelte-ignore suppressions
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m26s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m49s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
4572572c94
NVDA+Chrome and VoiceOver+Safari can re-announce a persistent non-empty
aria-live region when adjacent DOM mutations occur. Clearing with a
500ms delay gives the announcement time to fire once before going quiet.

The two svelte-ignore a11y_no_static_element_interactions suppressions are
given preceding comments explaining the keyboard accessibility contract so
they are not mistaken for unaddressed tech debt.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 8 commits 2026-06-09 17:53:52 +02:00
Adds JOURNEY_DOCUMENT_ALREADY_ADDED to ErrorCode, an
existsByGeschichteIdAndDocumentId() repo method, and a 409 guard in
JourneyItemService.append() — the error code was registered on the
frontend but never thrown on the backend, allowing concurrent tabs to
add the same document twice.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The PR removed the documentId filter from list() along with the old
Geschichte.documents ManyToMany, but the document-detail page and its
frontend server still query GET /api/geschichten?documentId=<id> to show
related stories. Without the filter the endpoint silently returned every
published story. Restores the filter through a JPQL EXISTS check on
journey_items so only journeys that include the given document are returned.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
null ?? undefined evaluated to undefined, causing JSON.stringify to omit
the key entirely — the backend treated an absent note field as a no-op,
so clearing a note never persisted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
handleNoteRemove mutated UI state optimistically without try/catch.
A failed PATCH left the note visually deleted while it survived on the
server. Now uses snapshot/rollback identical to handleNoteBlur.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(journey-add-bar): replace aria-disabled with native disabled on confirm button
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m42s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m48s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m4s
b30c0d7f96
aria-disabled alone leaves the button keyboard-activatable, violating
WCAG 4.1.2. Native disabled removes it from the tab order and prevents
activation via Enter/Space.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-06-09 18:09:57 +02:00
fix(search-filter-bar): wait for slide transition before clicking undated toggle
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m23s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m43s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
67861239f8
track_reactivity_loss in Svelte 5 async.js fires when a $bindable write
happens while the slide transition is still being tracked asynchronously.
Waiting for the toggle to be visible ensures the transition has settled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 8 commits 2026-06-09 19:10:18 +02:00
Verifies the EXISTS subquery in findSummaries actually filters by document
membership — a journey containing the document appears, one without does not.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DocumentPickerDropdown and DocumentMultiSelect had identical createTypeahead
configs, fetch logic, and formatDocLabel helpers. Extracted to
documentTypeahead.ts; all four consumers import from the shared module.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
role=listbox + role=option without arrow-key navigation is misleading — the
WAI-ARIA combobox pattern requires aria-activedescendant handling that isn't
implemented. Downgraded to plain <ul>/<li>; input keeps role=combobox +
aria-controls pointing to the list id.

listboxId was a module-level constant so two simultaneous instances would share
the same DOM id. Fixed with a <script module> counter.

Updated spec queries from getByRole('option') to getByText() — tests behaviour,
not the ARIA implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
#6b7280 (gray-500) on #f5f4f0 yielded ~4.0:1 — below the 4.5:1 AA minimum
for normal text. Changed to #4b5563 (gray-600) which provides ~6.9:1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
px-2 py-1 gave ~28px height — half the WCAG 2.2 / project 44px minimum.
Changed to min-h-[44px] inline-flex items-center for both buttons.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
chore(i18n): delete unused journey_item_pending_add/remove keys
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m41s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m49s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m7s
ba394e1165
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-06-09 19:22:23 +02:00
fix(test): update JourneyEditor spec to use getByText after listbox role downgrade
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m41s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 3m56s
CI / fail2ban Regex (pull_request) Successful in 46s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m8s
abe5d62102
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-06-09 19:32:46 +02:00
feat(journey): replace new-journey placeholder with JourneyCreate form
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m40s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / fail2ban Regex (pull_request) Has been cancelled
CI / Semgrep Security Scan (pull_request) Has been cancelled
CI / Compose Bucket Idempotency (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
3e54b6e90a
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-06-09 19:38:42 +02:00
fix(test): narrow getByText selector to dropdown-only item in aria-disabled test
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m26s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m49s
CI / fail2ban Regex (pull_request) Successful in 47s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
bd5e6e6fbe
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 1 commit 2026-06-09 22:17:50 +02:00
fix(geschichte): persist GeschichteType on create — JOURNEY was silently dropped
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m25s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m56s
CI / fail2ban Regex (pull_request) Successful in 47s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
44f15dd4a2
GeschichteUpdateDTO lacked a `type` field, so the `type: 'JOURNEY'` sent by
JourneyCreate was discarded by Jackson and every new Geschichte was saved as
STORY. The edit page branched on type, so journeys always showed the STORY
editor with no document-adding capability.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
marcel added 6 commits 2026-06-10 00:07:10 +02:00
The pure-text RELEVANCE fast path loaded documents via plain findAllById,
which carries no entity graph. With Document.tags LAZY (ADR-022) and no
surrounding transaction, resolveDocumentTagColors hit the dead proxy and
every q-only search (document picker typeaheads) failed with 500
LazyInitializationException. Dedicated findByIdIn declares the same fetch
shape as the other search loaders.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PATCH /api/geschichten/{id} (save draft, publish) returned the raw entity;
with open-in-view false, Jackson serialized the lazy items collection after
the transaction closed and every save failed with LazyInitializationException.
Write methods now assemble GeschichteView in-transaction, completing the
read-model boundary already used by GET — entities no longer cross the
controller.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The document picker parsed the response without checking r.ok, so a 401/500
rendered identically to 'no matches' and the dropdown silently vanished —
which is how the broken relevance path shipped invisibly. The fetch now
throws on non-OK, the useTypeahead hook exposes an error flag, and the
picker renders a visible failure message (de/en/es).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The reader dashboard renders 'bearbeitet vor X' from updatedAt, but the
summary projection dropped the field when the list endpoint moved off the
entity — drafts have no publishedAt to fall back on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
With create/update returning GeschichteView, no endpoint serves the raw
Geschichte entity and springdoc drops its schema. Dashboard modules and the
home loader now use GeschichteSummary; GeschichteEditor takes GeschichteView
and maps persons into the displayName shape PersonMultiSelect renders —
fixing blank person chips on story edit. PersonMultiSelect/Sidebar narrow to
Pick<Person, 'id' | 'displayName'>, mirroring the DocumentOption precedent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(journeyitem): explicit JPQL for the document-dedup guard — derived query unmappable
Some checks failed
CI / Unit & Component Tests (pull_request) Successful in 3m30s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Failing after 3m46s
CI / fail2ban Regex (pull_request) Successful in 48s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m11s
ae0b143685
POST /api/geschichten/{id}/items with a documentId failed 500: Spring Data
resolved the derived existsByGeschichteIdAndDocumentId path as a direct
documentId attribute (shadowed by the transient getDocumentId() getter)
instead of document.id, producing JPQL Hibernate cannot map. Existing tests
only appended note items, so the document branch was never exercised.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
marcel added 10 commits 2026-06-10 07:58:37 +02:00
GESCHICHTE_TYPE_IMMUTABLE and JOURNEY_NOTE_TOO_LONG were declared in
errors.ts, translated, and documented — but never existed in the backend.
update() now rejects a type change with 409 (omitted/same type still pass);
note length is enforced at 2000 with its own code, matching the frontend
maxlength and the i18n message (resolves the #793 discrepancy in favour of
the spec). JOURNEY_ITEM_NOT_IN_JOURNEY is deleted everywhere instead — the
deliberate 404 posture for cross-journey item ids must not leak existence
via a distinct code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The append dedup guard was check-then-insert: two concurrent appends of the
same document both pass exists() and both insert. The partial unique index
on (geschichte_id, document_id) WHERE document_id IS NOT NULL closes the
race; append saveAndFlush-es and maps the DataIntegrityViolationException to
the same 409 JOURNEY_DOCUMENT_ALREADY_ADDED as the friendly pre-check. The
CHECK on note length pins the 2000-char contract in the schema, mirroring
chk_text_length on transcription_blocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GET /api/geschichten shipped every author's AppUser email to all readers via
GeschichteSummary.AuthorSummary — contradicting the documented rule that
author projections never expose email or group memberships. The frontend
only used it as a display-name fallback; it now falls back to [Unbekannt],
matching the server-side rule in GeschichteService.toView.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The OWASP sanitizer entity-encodes ('&' → '&amp;') while JourneyReader
renders the intro via Svelte text interpolation — a curator typing
'Müller & Söhne' saw 'Müller &amp; Söhne', re-encoded cumulatively on every
editor round-trip. JOURNEY bodies now bypass the sanitizer (safe: the reader
never uses {@html}); STORY bodies keep the full allow-list sanitization.
This makes the code match the PR's documented design note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Captures the read-model invariant that was only living in a code comment:
views are assembled in-transaction, entities never cross the controller
boundary in the geschichte domain. CLAUDE.md §DTOs now names the exception
so the convention text stops contradicting the code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JourneyEditor fed GeschichteView.PersonView (no displayName) straight into
the displayName-rendering PersonMultiSelect, so every chip on a journey was
empty. The name mapping both editors need now lives once in
$lib/person/personOption.ts (with the [Unbekannt] fallback matching
GeschichteService.toView), and PersonMultiSelect/GeschichteSidebar import
the narrow PersonOption contract from there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DocumentMetadataDrawer links to /geschichten?documentId={id}, but the list
loader silently dropped the param — the user got the unfiltered list. The
loader now validates the UUID and forwards it to GET /api/geschichten,
returning it as documentIdFilter in page data.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The input declared role=combobox + aria-autocomplete=list while arrow keys
did nothing (WCAG 4.1.2). Wired the useTypeahead activeIndex the same way
PersonTypeahead does: ArrowUp/Down cycle, Enter/Space select, Escape closes,
aria-activedescendant tracks the active option; the list is a real listbox
with option roles again (the interim role downgrade is reverted). Zero hits
now render a 'Keine Treffer' row instead of silently vanishing, aria-expanded
matches the visible state, and the hook sets loading at setQuery so the
debounce window can't read as 'no results'. DocumentMultiSelect renders the
shared error state too. _uid counter replaced with $props.id().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Addresses the remaining #792 review blockers and concerns in the journey
editor cluster:

- Interlude rows show 'Zwischentext' (dedicated key), not the add-button text
- All four mutation handlers route the backend ErrorCode through
  getErrorMessage (a 409 duplicate no longer says 'bitte Seite neu laden')
  and console.error their failures so client-side errors leave a trace
- Remove implements the spec'd pending state: row stays dimmed with an
  aria-live 'wird entfernt…' until the DELETE resolves; failure keeps the row
- Move announcements fire after the reorder resolves (no false 'verschoben')
- Touch targets ≥44px (remove ×, note links, create submit); focus moves to
  the new row after add, to a sensible neighbor after remove, back to × on
  confirm-cancel; drag handle is pointer-only; title/intro get aria-labels;
  publish-disabled reason is a visible hint, not a title tooltip
- Amber warning styles use new --color-warning-* tokens with dark remaps
- Blocked interlude-clear restores the draft instead of showing phantom text
- useBlockDragDrop moves to $lib/shared/hooks — geschichte no longer imports
  another domain's internals
- Test hardening: reorder-failure rollback (non-ok + reject), publish/
  unpublish/empty-warning surface, destructive confirm path, maxlength
  assertions, JourneyCreate failure path, edit-page STORY/JOURNEY branch,
  fixture factory, m.* assertions, all fixed sleeps replaced with polling

67 component tests green across 6 spec files; transcription consumer of the
moved hook re-verified (30 green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
chore(types): regen api.ts — AuthorSummary email removed from spec
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 3m6s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Failing after 4m2s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
4374f75d3c
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
marcel added 1 commit 2026-06-10 08:37:49 +02:00
test: align unit stubs and fixtures with the review-round changes
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 4m16s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 4m17s
CI / fail2ban Regex (pull_request) Successful in 43s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
a65a55448e
CI caught three spots the targeted local runs missed: the relevance-path
unit tests still stubbed findAllById (the path now calls findByIdIn — the
batchMetadata stubs legitimately keep findAllById), the second
GeschichtenCard test file still expected the removed email fallback, and
the AND/OR-toggle describe lacked the wait-for-slide-transition guard its
sibling describe documents — the flake that failed run 2208.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 4m16s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 4m17s
CI / fail2ban Regex (pull_request) Successful in 43s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/issue-753-journey-editor:feat/issue-753-journey-editor
git checkout feat/issue-753-journey-editor
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#792