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
pull from: feat/issue-753-journey-editor
merge into: marcel:feat/issue-750-lesereisen-data-model
marcel:main
marcel:feat/issue-750-lesereisen-data-model
marcel:worktree-feat+issue-738-nl-search-backend
marcel:feat/issue-286-notification-bell-form-actions
marcel:feat/issue-580-sentry-backend
marcel:fix/issue-593-management-port-zero
marcel:worktree-feat+issue-557-upload-artifact-v3-pin
marcel:worktree-chore+issue-556-drop-client-branches-coverage-gate
marcel:fix/issue-514-prerender-crawl-bakes-redirects
marcel:fix/issue-472-prerender-entries
marcel:feat/issue-395-readme
marcel:feat/issue-345-bulk-mark-reviewed
marcel:feat/issue-344-bell-tooltip
marcel:feat/issue-341-annotieren-contrast
marcel:feat/issue-225-bulk-metadata-edit
marcel:feat/issue-317-bulk-upload
marcel:feat/issue-271-dashboard-redesign
marcel:docs/issue-240-mission-control-spec
marcel:refactor/issues-193-200
marcel:feat/issue-162-korrespondenz-redesign
marcel:feature/68-new-document-file-first
marcel:feat/81-discussion-discoverability
marcel:feat/62-document-bottom-panel
marcel:feature/56-backfill-file-hashes
marcel:feat/38-document-edit-history
marcel:fix/svelte5-test-delegation-and-login-auth
No Reviewers
Labels
Clear labels
P0-critical
P1-high
P2-medium
P3-later
audit
bug
cleanup
collaboration
conversation
descoped
devops
documentation
epic
feature
file-upload
legibility
needs-discussion
notification
person
phase-1: security
phase-2: container-images
phase-3: prod-compose
phase-4: spring-prod-profile
phase-5: backups
phase-6: deployment-docs
phase-7: monitoring
refactor
security
test
ui
user
Blocks a core user journey, causes data loss, or is a security/accessibility barrier. Work on this before P1+.
Significant friction on a main user journey; workaround exists but is non-obvious. Next up after P0.
Noticeable improvement; doesn't block anything; schedule opportunistically.
Cosmetic or parking-lot work; fine to stay open indefinitely.
Read-only audit / assessment work; produces a report rather than changing code
Something isn't working
Removal of dead code, vague comments, naming violations, and scratch artifacts
We want to extend the family archive to have more collaboration tools
We will do that later
README, ARCHITECTURE, GLOSSARY, CONTRIBUTING, per-domain docs
Marker for epic-level issues that group multiple child issues
Codebase Legibility Refactor — preparing the codebase for human evaluation and bus-factor reduction
Has an open decision or design question that must be resolved before implementation can start.
Security hygiene — must be done first
Production-ready multi-stage Docker images
Production compose overlay + Caddy reverse proxy
Spring Boot production configuration profile
Database and object storage backup strategy
.env.example, DEPLOYMENT.md, runbook
Prometheus, Loki, Grafana, Alertmanager
Code restructuring without behaviour change
UI/UX and accessibility issues
No Label
Milestone
No items
No Milestone
Projects
Clear projects
No project
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: marcel/familienarchiv#792
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "feat/issue-753-journey-editor"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
createBlockDragDrop<T>— accepts any{ id: string }shape, used by JourneyEditor alongside the existing transcription editorGeschichteSidebar.svelte— Status badge + PersonMultiSelect with<details>mobile collapsibles (44px touch targets)DocumentPickerDropdown.svelte— single-select document search witharia-disabledfor already-added items and sr-only "bereits enthalten" hintDocumentMultiSelect.svelteontocreateTypeahead— removes rawsetTimeoutdebounceJourneyItemRow.svelte— drag handle, move-up/down buttons, note textarea (PATCH on blur), interlude visual treatment, inline remove confirmJourneyAddBar.svelte— "Brief hinzufügen" opensDocumentPickerDropdown; "Zwischentext hinzufügen" opens inline draft form witharia-disabledconfirm until text non-emptyJourneyEditor.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,GeschichteSidebargeschichten/[id]/edit/+page.svelte— branches ongeschichte.type: JOURNEY →JourneyEditor, STORY →GeschichteEditor(unchanged)journey_*keys in de/en/es; 4 newErrorCodevalues; interlude CSS semantic tokens (--color-interlude-bg/border/label) with light + dark valuesDesign notes
maxlength="2000"per spec — discrepancy tracked in issue #793 for resolution.bodyfield; backend skips sanitizer for JOURNEY (implemented in issue #751).markDirty()— reorder/add/remove are persisted optimistically (remove/reorder) or pessimistically (add); only title + intro edits trigger the unsaved-changes guard.data-drag-handle/data-block-wrappercontract reused from transcription editor unchanged.handleAddDocumentandhandleAddInterludeonly updateitemsafter a successful API response;handleRemoveandhandleReorderpre-update with snapshot rollback on failure.Test plan
cd frontend && npx tsc --noEmit— no errors in any new filenpm run test -- --project=server— all server-side tests greenDocumentPickerDropdown,JourneyItemRow,JourneyAddBar,JourneyEditor— run with--project=clientin CI/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/geschichtennow returnGeschichteViewinstead of the rawGeschichteentity (author →AuthorView {id, displayName}, persons →PersonView[], nodocuments). Motivation: withopen-in-view: falsethe lazyitemsproxy is dead at serialization time — every save/publish 500'd. See ADR-036. The same round also droppedemailfromGeschichteSummary.AuthorSummaryand the generatedGeschichteschema no longer exists.Corrections to the original body:
disabled(notaria-disabled) sinceb30c0d7f.Regen artifacts:
src/lib/generated/api.tsregeneration swept in unrelated drift from the base branch (/api/admin/backfill-titlesadded,getConversationremoved,hasTranscription,subtreeDocumentCount) — these are spec catch-up, not changes of this PR.i18n register:
comp_typeahead_error/comp_typeahead_no_resultsare Sie-form on purpose — allerror_*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).
- 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>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>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>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>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>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>The OWASP sanitizer entity-encodes ('&' → '&') while JourneyReader renders the intro via Svelte text interpolation — a curator typing 'Müller & Söhne' saw 'Müller & 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>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>View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.