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>
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>
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>
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>
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 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>
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>
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>
#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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>