Commit Graph

3412 Commits

Author SHA1 Message Date
Marcel
5b2ee31292 feat(i18n): add journey_item_document_deleted placeholder key
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>
2026-06-08 20:06:09 +02:00
Marcel
3d80bc656c refactor(journeyitem): use saveAll in reorder for efficiency
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>
2026-06-08 20:05:28 +02:00
Marcel
4a0fed617a refactor(geschichte): route all reads through GeschichteQueryService
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>
2026-06-08 20:04:21 +02:00
Marcel
7ba6342a84 chore(config): remove empty JacksonConfig placeholder
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:02:51 +02:00
Marcel
598ad622e7 fix(journeyitem): use specific error codes in append() — JOURNEY_AT_CAPACITY and GESCHICHTE_TYPE_MISMATCH
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m21s
CI / OCR Service Tests (pull_request) Successful in 22s
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 1m5s
- 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>
2026-06-08 19:31:30 +02:00
Marcel
c5611250ec test(journey): rename updateItemNote test to clarify Optional deserialization semantics
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:17:25 +02:00
Marcel
e400b1d77e feat(error): add GESCHICHTE_TYPE_MISMATCH error code with i18n (de/en/es)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:15:45 +02:00
Marcel
1fb0c41216 feat(error): add JOURNEY_AT_CAPACITY error code with i18n (de/en/es)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:13:47 +02:00
Marcel
147aa56386 feat(audit): add JOURNEY_ITEM_NOTE_UPDATED audit kind and wire into updateNote()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:04:43 +02:00
Marcel
7c06609816 refactor(journey): make toView() and toSummary() package-private
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:03:24 +02:00
Marcel
2ae1bb3a30 fix(journey): reorder() throws 404 when Geschichte does not exist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:02:03 +02:00
Marcel
69db198319 refactor(geschichte): introduce GeschichteQueryService with existsById()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 19:00:32 +02:00
Marcel
e157d90b53 docs(backend): add JourneyItemService and GeschichteQueryService to CLAUDE.md package table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 18:58:20 +02:00
Marcel
97f22e1ce8 fix(review): friendlier i18n message for journey position conflict error
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m17s
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 45s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 18:13:39 +02:00
Marcel
5539158e8f fix(review): add JourneyItemService to C4 L3 supporting-domains diagram
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 18:12:50 +02:00
Marcel
7ed0032661 fix(review): replace Set<Person> with Set<PersonView> in GeschichteView — prevents leaking admin fields
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 18:12:13 +02:00
Marcel
2f471155b8 fix(review): replace email fallback with [Unbekannt] in AuthorView — prevents CWE-359 leak
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 18:09:40 +02:00
Marcel
4eb6abd920 fix(review): address PR #788 review blockers
Some checks failed
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 3m53s
CI / fail2ban Regex (pull_request) Failing after 46s
CI / Semgrep Security Scan (pull_request) Successful in 25s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
- GlobalExceptionHandler maps uq_journey_items_geschichte_position constraint
  violation to HTTP 409 JOURNEY_ITEM_POSITION_CONFLICT
- JourneyItemService.reorder() rejects duplicate IDs before set-equality check
  to prevent silent position overwrite
- JourneyItemRepository removes orphaned findAllByGeschichteId method
- GeschichteView removes stale com.fasterxml.jackson import
- Tests: add appendItem_returns409_on_position_conflict (controller),
  reorder_returns400_when_itemIds_contain_duplicates (service)
- Fix JourneyItemIntegrationTest compilation after repository cleanup
- db-orm.puml annotates journey_items position with CHECK + UNIQUE DEFERRABLE

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 17:16:32 +02:00
Marcel
6fc5ce6ddd docs: update GLOSSARY for JourneyItem view types; add ADR-035
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m20s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 3m48s
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 1m7s
Fixes GLOSSARY position-step value (1000→10), adds DEFERRABLE constraint note,
and documents GeschichteView, JourneyItemView, and DocumentSummary read-model types.

ADR-035 records the decision to use Optional<String> for three-way PATCH semantics
instead of jackson-databind-nullable (which targets Jackson 2.x and is incompatible
with Spring Boot 4.0 / Jackson 3.x).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 17:06:25 +02:00
Marcel
4603e335fd feat(frontend): add JOURNEY_ITEM error codes, i18n keys, regen API types
Adds JOURNEY_ITEM_NOT_FOUND and JOURNEY_ITEM_POSITION_CONFLICT to the frontend
ErrorCode union and getErrorMessage() switch. Adds de/en/es translations.
Regenerates api.ts from the current OpenAPI spec (needs a second run once the
backend is restarted with the new endpoints compiled in).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 17:04:51 +02:00
Marcel
a0fa8f4d02 feat(journeyitem): add CRUD endpoints for JourneyItems on GeschichteController
Adds POST/PATCH/DELETE/PUT-reorder endpoints for journey items, backed by
JourneyItemService. Replaces jackson-databind-nullable (Jackson 2.x, incompatible
with Spring Boot 4 / Jackson 3.x) with Optional<String> three-way PATCH semantics:
null = absent/no-op, empty = clear, present = set.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 17:02:12 +02:00
Marcel
d29f217328 feat(geschichte): getById returns GeschichteView; author email never exposed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 16:52:29 +02:00
Marcel
fdc9273c86 feat(geschichte): implement JourneyItemService — append, updateNote, delete, reorder (35 unit tests)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 16:47:42 +02:00
Marcel
2ad5c36e3c feat(geschichte): extend JourneyItemRepository and add item DTOs
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m19s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m59s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m7s
Repository: findByIdAndGeschichteId (IDOR-safe lookup),
findByGeschichteIdOrderByPosition, findIdsByGeschichteId (Set<UUID> for
set-equality reorder check), findMaxPositionByGeschichteId, countByGeschichteId.
DTOs: JourneyItemCreateDTO (documentId+note), JourneyItemUpdateDTO
(JsonNullable<String> note for 3-way PATCH), JourneyReorderDTO (List<UUID>).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 16:32:50 +02:00
Marcel
160ca1c3e9 feat(geschichte): add DocumentSummary, JourneyItemView, GeschichteView read models
DocumentSummary: lean document projection for journey item embedding —
skips tag-color resolution (getSummaryById), includes receiverCount
(0 when no receivers, non-null). JourneyItemView: response record for
item CRUD and GET. GeschichteView: detail response with summarised
author {id, displayName} to prevent AppUser email/group leak.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 16:31:40 +02:00
Marcel
7b06c3adec feat(migration): V73 adds UNIQUE DEFERRABLE and CHECK position > 0 on journey_items
DEFERRABLE INITIALLY DEFERRED allows mid-transaction position swaps
during reorder (checked at COMMIT, not per-row). CHECK (position > 0)
guards against off-by-one in the append path. Both verified by
JourneyItemConstraintsTest via raw pg_constraint query + jdbcTemplate
inserts against a real postgres:16-alpine container.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 16:30:07 +02:00
Marcel
408ae3345c feat(audit,error): add JourneyItem AuditKind values and ErrorCodes
Adds JOURNEY_ITEM_ADDED, JOURNEY_ITEM_REMOVED, JOURNEY_ITEMS_REORDERED
(last is ROLLUP_ELIGIBLE — drag-heavy editing produces many events).
Adds JOURNEY_ITEM_NOT_FOUND (404) and JOURNEY_ITEM_POSITION_CONFLICT
(409) to ErrorCode for IDOR protection and concurrent-edit feedback.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 16:27:15 +02:00
Marcel
0b17724785 feat(config): add jackson-databind-nullable for JsonNullable PATCH DTO support
Registers JsonNullableModule globally so JsonNullable<String> in
JourneyItemUpdateDTO can distinguish absent (unchanged) from explicit
null (clear field) on PATCH operations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 16:26:26 +02:00
Marcel
df5d880e09 fix(review): GeschichtenCard uses GeschichteSummary type; focus-visible on journey links; fix stale tests
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m17s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m42s
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 1m4s
- GeschichtenCard.svelte: use GeschichteSummary instead of Geschichte
  (list endpoint returns summaries; no items/createdAt/updatedAt needed)
- GeschichtenCard.svelte.test.ts: factory returns GeschichteSummary with
  lean author shape; drop Geschichte-only fields (createdAt, groups, etc.)
- geschichten/[id]/+page.svelte: add focus:outline-none focus-visible:ring-2
  focus-visible:ring-focus-ring to journey item document links (WCAG 2.4.7)
- page.svelte.test.ts ([id]): replace stale documents[] factory field with
  items[]; test now checks placeholder text + note caption
- page.svelte.test.ts (new): remove removed initialDocuments from baseData;
  rename test to reflect that only initialPersons is passed through

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 16:01:50 +02:00
Marcel
45500cc5e2 fix(review): separate note from link label in journey item stub
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m54s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 3m50s
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
item.note is editorial prose — it must not be used as the anchor label.
Always show the i18n placeholder as the link text; render note as a
caption below the link when present.

Adds TODO(#786) comment so the stub degradation is tracked.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 15:38:44 +02:00
Marcel
b1819867be fix(review): address second-pass PR #787 review blockers
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m54s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m47s
CI / fail2ban Regex (pull_request) Successful in 43s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
- CLAUDE.md: add Geschichte and JourneyItem rows to the Domain Model table
- GeschichteSummary: add @Schema(requiredMode=REQUIRED) to getId, getTitle,
  getStatus, getType, and AuthorSummary.getEmail so the TypeScript generator
  emits non-optional fields when api.ts is next regenerated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 13:13:38 +02:00
Marcel
2c5f7ac12d fix(review): address PR #787 review blockers — db-orm diagram, C4 diagram, UUID link text
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m50s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 3m45s
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 1m4s
- db-orm.puml: replace geschichten_documents with journey_items, add type column to geschichten, bump schema version to V72
- l3-backend-3g-supporting.puml: update GeschichteController and GeschichteService descriptions to mention STORY/JOURNEY subtypes and JourneyItem
- geschichten/[id]/+page.svelte: replace raw UUID fallback with m.geschichten_document_link_placeholder() i18n key
- messages/{de,en,es}.json: add geschichten_document_link_placeholder translation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:54:12 +02:00
Marcel
13fa4123c1 docs: update ARCHITECTURE, GLOSSARY, db diagram for JourneyItem and Lesereise
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m50s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 3m49s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / fail2ban Regex (pull_request) Successful in 51s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m4s
- ARCHITECTURE.md: expand geschichte domain description — two subtypes
  (STORY/JOURNEY), JourneyItem ownership, ON DELETE SET NULL FK note
- GLOSSARY.md: add JourneyItem and Lesereise terms; update Geschichte
  entry to mention type discriminator
- db-relationships.puml: replace geschichten_documents with journey_items
  (ON DELETE CASCADE to geschichten, ON DELETE SET NULL to documents)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:41:41 +02:00
Marcel
ccdf358b40 test(frontend): fix Geschichte component specs for GeschichteType and JourneyItem model
- GeschichteEditor.svelte.spec.ts: remove docFactory + initialDocuments test;
  rename documentIds test to personIds-only; add familyMember+provisional to
  personFactory (were pre-existing omissions)
- GeschichtenCard.svelte.spec.ts: add type:'STORY', replace documents:[] with
  items:[], change body null→undefined to match Geschichte schema
- GeschichtenCard.svelte.test.ts: add status/type/createdAt/updatedAt to factory;
  cast result as Geschichte to avoid spread-widening type inference

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:40:17 +02:00
Marcel
e6c890c61e feat(frontend): update generated API types and Geschichte routes for JourneyItem model
- api.ts: add GeschichteType, JourneyItem, GeschichteSummary schemas;
  remove documentId param from list endpoint; change list response to
  GeschichteSummary[]; add type + items to Geschichte; remove documents field
- GeschichteEditor: remove DocumentMultiSelect + documentIds from payload
  (journey items are managed via the future Lesereisen editor, not here)
- GET /geschichten page: remove documentId filter from server load + URL logic
- geschichten/new: remove documentId pre-population from server load
- geschichten/[id]: replace g.documents with g.items (document-backed JourneyItems)
- geschichten/new + [id]/edit: remove documentIds from submit payload type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:39:53 +02:00
Marcel
93ef26690f test(geschichte): add JourneyItemIntegrationTest, GeschichteListProjectionTest, GeschichteHttpTest; update unit tests
New tests:
- JourneyItemIntegrationTest: @OrderBy, cascade delete, orphan removal,
  type round-trip, note-only item, document-backed item, ON DELETE SET NULL,
  CHECK constraint violation (9 tests)
- GeschichteListProjectionTest (@DataJpaTest): findSummaries status filter,
  AuthorSummary, type field, authorId filter, AND-semantics person filter (8 tests)
- GeschichteHttpTest (@SpringBootTest RANDOM_PORT): list 200, getById 200 with
  JOURNEY items, getById 404, DRAFT hidden from non-BLOG_WRITE user (5 tests)

Updated tests:
- GeschichteServiceTest: mock signature (3 args), anonymous GeschichteSummary
  stubs (Jackson can't serialize Mockito interface mocks), remove documentId cases
- GeschichteControllerTest: summaryStub() concrete anonymous impl,
  updated mock matcher to 3-arg list()
- GeschichteServiceIntegrationTest: List<GeschichteSummary> return type,
  3-arg list() calls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:25:18 +02:00
Marcel
439385dd35 refactor(geschichte): update service, controller, repository for JourneyItem model
- GeschichteService.list() now returns List<GeschichteSummary> via JPQL
  projection query; accepts (status, personIds, limit); DRAFT clamp for
  non-BLOG_WRITE users; AND-semantics person filter with sentinel UUID guard
- GeschichteService.getById() is @Transactional(readOnly=true) and calls
  Hibernate.initialize(g.getItems()) to force-init the LAZY bag under
  open-in-view=false
- GeschichteRepository: add findSummaries() JPQL query with person subquery
- GeschichteController.list(): remove documentId param, change return type
  to List<GeschichteSummary>
- GeschichteSpecifications: remove hasDocument() and documentSubquery() —
  TODO left for lesereisen-editor follow-on

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:24:50 +02:00
Marcel
b3ce9b930f feat(geschichte): add GeschichteType, JourneyItem entity, GeschichteSummary, and V72 migration
- GeschichteType enum {STORY, JOURNEY} — default STORY
- JourneyItem entity replaces geschichten_documents junction table;
  position-ordered, document_id nullable (note-only items allowed),
  CHECK(document_id IS NOT NULL OR note IS NOT NULL)
- GeschichteSummary interface projection for list() queries (avoids lazy-init)
- Geschichte entity gains `type` + `items` (LAZY, orphanRemoval, CascadeType.ALL)
  replacing the old `documents` ManyToMany bag
- GeschichteUpdateDTO: remove documentIds (replaced by JourneyItem API)
- V72 migration: adds `type` column, creates `journey_items` table with
  FK ON DELETE CASCADE (geschichte) / ON DELETE SET NULL (document),
  migrates geschichten_documents ordered by meta_date, drops junction table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 12:24:24 +02:00
d650b6c066 refactor(search): remove NLP/smart-search feature entirely (#772)
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m23s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 3m46s
CI / fail2ban Regex (push) Successful in 46s
CI / Semgrep Security Scan (push) Successful in 25s
CI / Compose Bucket Idempotency (push) Successful in 1m8s
## Summary

- Removes the NLP/smart-search feature completely — the feature was too unreliable and slow; users get better results with the regular search filters
- Deletes the entire backend `search/` package (NlSearchController, NlQueryParserService, NlpClient, NlSearchRateLimiter — 14 classes + 6 test classes)
- Deletes the `nlp-service/` Python microservice (FastAPI, rapidfuzz, DB-backed person matching)
- Removes all frontend NL search components: SmartModeToggle, SmartSearchStatus, InterpretationChipRow, DisambiguationPicker, chip-types, theme-chip-removal
- Strips smart-mode logic from SearchFilterBar and documents/+page.svelte
- Removes `SMART_SEARCH_UNAVAILABLE` / `SMART_SEARCH_RATE_LIMITED` error codes from backend, frontend types, and all three i18n files (de/en/es)
- Removes `nlp-service` container and `APP_NLP_BASE_URL` from both docker-compose files
- Removes Ollama/NLP Prometheus scrape job and Grafana dashboard
- Deletes ADRs 028 (×2), 034, 035

## Test plan

- [ ] Backend compiles: `cd backend && ./mvnw compile -q` → BUILD SUCCESS
- [ ] Frontend server tests pass: `cd frontend && npm run test -- --project=server`
- [ ] No NLP/smart-search references remain in source: `grep -r "SmartSearch\|NlSearch\|nlp-service\|SMART_SEARCH" backend/src frontend/src`
- [ ] `docker compose config` validates both compose files
- [ ] Search page loads, filter bar works, no smart-mode toggle visible

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Marcel <marcel@familienarchiv>
Reviewed-on: #772
2026-06-08 10:57:00 +02:00
Marcel
8e63867ad8 docs(specs): UI specs for Lesereisen reader and Journey editor
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m23s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 4m2s
CI / fail2ban Regex (push) Successful in 46s
CI / Semgrep Security Scan (push) Successful in 21s
CI / Compose Bucket Idempotency (push) Successful in 1m8s
nightly / deploy-staging (push) Successful in 2m44s
lesereisen-reader-spec.html — Issue #752
  LR-0 type selector on /geschichten/new
  LR-1 REISE badge on the list
  LR-2 Journey reader (ordered cards, interlude asides, no position numbers)

lesereisen-editor-spec.html — Issue #753
  LE-1 empty JourneyEditor layout
  LE-2 editor with mixed items (documents + interludes, drag handles)
  LE-3 inline note-editing state
  LE-4 mobile layout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 19:07:34 +02:00
Marcel
6b0a06e8b1 feat(nlp-service): scaffold — models, requirements, CLAUDE.md
Task 1: Create standalone FastAPI service scaffold with models, test framework,
and documentation. Includes ParseRequest, ParseResponse Pydantic models matching
OllamaExtraction contract, plus three passing tests validating model validation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 10:11:34 +02:00
Marcel
7c1eef710c docs(nlp): add spaCy NLP service implementation plan
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 09:52:07 +02:00
Marcel
03e22a2f26 docs(nlp): add spaCy NLP service prototype design spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 09:40:00 +02:00
Marcel
6878419156 merge: resolve conflicts with origin/main (#763 person name-match integration)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m31s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 3m48s
CI / fail2ban Regex (pull_request) Successful in 45s
CI / Semgrep Security Scan (pull_request) Successful in 22s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
CI / Unit & Component Tests (push) Successful in 3m20s
CI / OCR Service Tests (push) Successful in 23s
CI / Backend Unit Tests (push) Successful in 3m48s
CI / fail2ban Regex (push) Successful in 46s
CI / Semgrep Security Scan (push) Successful in 23s
CI / Compose Bucket Idempotency (push) Successful in 1m8s
- Drop unused MAX_CANDIDATES constant (not referenced in service)
- Keep detached-entity safety comment in resolveTags()
- Add 3 new partial-name match tests (23a/b/c) from #763
- Use resolveByName() API in test 28 (replaces findByDisplayNameContaining)
- Add NameMatches glossary entry from #763

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-07 08:50:48 +02:00
Marcel
09b77e9b36 test(person): pin fetchPool dedup when one person matches two tokens (#763 review)
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m20s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 3m53s
CI / fail2ban Regex (push) Successful in 44s
CI / Semgrep Security Scan (push) Successful in 21s
CI / Compose Bucket Idempotency (push) Successful in 1m5s
Assert that when the same person id is returned by two different token
fetches, the person appears exactly once in the result -- pinning
fetchPool's putIfAbsent dedup so a future refactor can't silently
double-classify a candidate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:47:47 +02:00
Marcel
9d202b042b test(person): close fetch-to-classify seam for alias matches on real Postgres (#763 review)
AC#4 (maiden alias -> direct) and AC#5 (alias first name -> fetchable +
classifiable) were each split across PersonRepositoryTest (the fetch) and
PersonServiceTest (the classifier with stubs) -- nothing walked
searchByName -> resolveByName end-to-end on real Postgres. Add two tests
in the existing @DataJpaTest slice that build a real PersonService over
the autowired repositories, persist a person with a MAIDEN_NAME alias and
one with an alias firstName, and assert both classify as direct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:47:47 +02:00
Marcel
8429b1e9f8 fix(search): derive disambiguation trigger aria-label from match count (#763 review)
The trigger hardcoded the multiple-people label for every count, so a
single did-you-mean picker announced "Mehrere Personen gefunden" to
screen readers while sighted users saw one name and a "Meintest du …?"
heading. Derive the trigger's accessible name from persons.length: a
single suggestion reuses the heading prop, two or more keep the
multiple-people label. Visible truncated name span unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:47:47 +02:00
Marcel
6959651b36 docs(search): document NameMatches and resolveByName (#763)
GLOSSARY entry for NameMatches (direct vs partial name-match strength and how
the search layer maps it); person/README adds resolveByName to the public
surface. No ADR — the matching rule is localized and justified inline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:47:47 +02:00
Marcel
0ef4f4f07c feat(search): case-appropriate disambiguation picker copy (#763)
A 1-item picker now reads "Meintest du …?" (a single direct match auto-selects
and never reaches the picker), while ≥2 keeps the "Person auswählen" framing.
The prompt lives in a visible, non-truncated panel heading (the trigger span
clips at 320px), and the "(auswählen…)" cue is dropped for the 1-item case.
DisambiguationPicker takes heading + showCue props; the page derives both from
ambiguousPersons.length. New search_disambiguation_did_you_mean key in de/en/es.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:47:47 +02:00
Marcel
f1bb9d3a69 feat(search): map direct/partial NameMatches into resolve buckets (#763)
resolveNames now delegates to PersonService.resolveByName and maps by match
strength: 1 direct → resolved (auto-select), ≥2 direct → ambiguous, 0 direct
with partials → ambiguous suggestions, 0 candidates → folded into full-text.
A single direct match no longer forces the picker when looser substring hits
coexist. The MAX_CANDIDATES cap moved into PersonService (after classification);
the MAX_NAME_LENGTH guard, resolved-cap overflow, and sender/receiver mapping
are preserved.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:47:47 +02:00