Mirrors the getBoundingClientRect pattern from JourneyItemCard.svelte.spec.ts.
Tests actual rendered height rather than presence of a CSS class string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Screen readers now announce the hint paragraph text on focus when no type
is selected, so users hear why the button is disabled without having to
click it first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The action was writing aria-checked directly and then firing onChange,
which also triggered Svelte's own aria-checked={selected === type} binding.
Double-ownership: action now only calls focus() + onChange(value);
Svelte owns the attribute update.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Function names already communicate intent. Comments that restate the
function name add noise without explaining why.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JourneyReader filters items to only those where document != null before
passing them here — the ! assertion is valid by caller invariant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds deleteError $state to [id]/+page.svelte, parses backend error via
parseBackendError/getErrorMessage on !res.ok, and displays a role=alert
paragraph. Adds two browser-tier tests: success path (goto called) and
error path (alert visible, goto not called).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sentry's wrapLoadWithSentry reads event.request.method — the test's makeEvent
now provides a real Request object. createApiClient mock was a plain function;
wrapping with vi.fn() enables vi.mocked(...).mockReturnValue in individual tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Verifies radioGroupNav action moves selection forward and wraps backward
so keyboard users can navigate the STORY/JOURNEY cards without a mouse.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CSS class string assertion was fragile — class names can change without
breaking the actual layout. DOM measurement via getBoundingClientRect is the
correct way to verify computed height meets WCAG 2.2 minimum.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
13 tests covering null/undefined inputs, partial names, email fallback,
and TZ-safe date slicing for formatPublishedAt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves the confirm-then-delete flow out of StoryReader and JourneyReader into
the single [id]/+page.svelte owner. Both reader components gain an optional
ondelete prop — the delete button calls ondelete?.() so the handler is opt-in
and never duplicated. Tests verify the prop is called on click.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the 3-line inline join with the shared formatAuthorName helper from
utils.ts. Test switches from CSS class string assertion to getComputedStyle
for the badge font-size check.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces aria-label="Kuratorennotiz" with m.journey_interlude_aria_label()
so screen readers get the correct label in all three supported locales.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
--c-journey-bg/text/border wired in light :root, dark @media, dark [data-theme]
blocks. Exposed via @theme inline as color-journey-tint/journey/journey-border.
Light: #B46820 on #FEF0E6 ≈ 4.6:1 AA at 12px bold. Dark: #E8862A on #3A2A1A ≈ 4.7:1.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Self-check: GeschichteView.items present; type emitted as 'STORY'|'JOURNEY' union literal.
List endpoint returns GeschichteSummary[]; detail endpoint returns GeschichteView.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The method intentionally skips permission checks and tag-colour resolution.
Renaming it to findSummaryByIdInternal makes the internal-only contract
visible at every call site, closing the latent CWE-284 risk flagged in
the PR review.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Re-inject JourneyItemService into GeschichteService (no cycle:
JourneyItemService → GeschichteQueryService, not GeschichteService).
Add getView(UUID) that loads the Geschichte and its items in a single
@Transactional(readOnly=true) session. Controller now delegates to
getView() instead of making two separate service calls. Tests updated
to stub getView() and cover the new method.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Create PersonNameFormatter with a single static join(firstName, lastName) method.
Replace the inline string concatenation in GeschichteService.toView() and the
private join() method in JourneyItemService with calls to PersonNameFormatter.join().
The new helper handles null-safety and trimming consistently in one place.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add @Operation annotation to reorderItems() clarifying that itemIds must
contain ALL item IDs for the journey in the desired order — a partial list
returns 400 Bad Request. This surfaces the contract in the generated
OpenAPI spec and Swagger UI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Clarify in the Javadoc that getSummaryById intentionally skips scope checks
and tag-colour resolution. This is safe under the current single-tenant model
and is explicitly used by JourneyItemService.append() to validate that a linked
document exists before persisting a JourneyItem.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
doesNotExist() asserts the key is absent from the JSON object, but Jackson
serializes a null Optional<String> as {"note": null} — the key is present with
a null value. nullValue() correctly matches that case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add two service-level integration tests to JourneyItemIntegrationTest:
- append_persists_item_at_position_10: verifies that the first append to an
empty journey creates an item at position 10 in the DB.
- reorder_swaps_positions_atomically: appends two items then reorders them,
asserting the DB reflects the new position assignment.
Both tests use the SecurityContextHolder authentication pattern from
GeschichteServiceIntegrationTest and mock S3Client to avoid MinIO connections.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add GeschichteQueryService component to the L3 supporting-domains diagram.
Remove the now-deleted Rel(geschSvc, journeyItemSvc, "Delegates getItems()")
arrow and add the correct Rel(journeyItemSvc, geschQuerySvc, ...) arrow that
reflects the actual dependency direction after the refactor in the prior commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add findByGeschichteIdWithDocument() to JourneyItemRepository with a
LEFT JOIN FETCH on document. getItems() now uses this query so that all
documents for a journey's items are loaded in a single SQL round-trip.
toView() now reads item.getDocument() directly from the already-fetched
association instead of issuing a separate documentService.getSummaryById()
call per item.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GeschichteService.getById() now returns the Geschichte entity (with the
DRAFT visibility guard intact). The controller calls journeyItemService.getItems()
and geschichteService.toView() to assemble the GeschichteView, removing the
need for GeschichteService to hold a reference to JourneyItemService.
Tests updated accordingly: GeschichteServiceTest tests toView() directly;
GeschichteControllerTest stubs both service calls; integration test uses the
two-step pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JacksonConfig was deleted (empty placeholder) — remove the now-stale
import and @Import reference from the controller slice test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds de/en/es translations for the case where a JourneyItem's linked
document has been deleted (document field is null), so the UI PR can
display a meaningful fallback string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the per-item save() loop in reorder() with a single
saveAll() call, reducing database round-trips for large journeys.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JourneyItemService no longer injects GeschichteRepository directly.
GeschichteQueryService gains findById() so JourneyItemService.append()
can load the Geschichte entity via the service layer, satisfying the
cross-domain layering rule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- JourneyItemService.append(): replace VALIDATION_ERROR with GESCHICHTE_TYPE_MISMATCH (409 conflict)
for non-JOURNEY type guard and JOURNEY_AT_CAPACITY (409 conflict) for 100-item cap
- JourneyItemServiceTest: update assertions to expect the new specific error codes
- CLAUDE.md: expand geschichte/ package table entry with GeschichteQueryService and journeyitem/ sub-domain
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>