Commit Graph

3410 Commits

Author SHA1 Message Date
Marcel
74fdc0cef7 feat(devops): migrate deprecated renovate.json keys + enable vuln surfacing
matchPackagePatterns → matchPackageNames (regex-glob form /^@tiptap/)
matchPaths → matchFileNames for the digest-bump rule.

Adds osvVulnerabilityAlerts, dependencyDashboard, vulnerabilityAlerts
(labels: security + P1-high), a weekly routine schedule, and
lockFileMaintenance (no automerge) so newly-published advisories are
surfaced proactively rather than discovered on contributor PRs.

Closes part of #818.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 11:16:11 +02:00
Marcel
bde1237358 docs(adr): record title-length guard obligation for #775
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 4m53s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 5m42s
CI / fail2ban Regex (pull_request) Successful in 54s
CI / Semgrep Security Scan (pull_request) Successful in 27s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m13s
CI / Unit & Component Tests (push) Successful in 4m59s
CI / OCR Service Tests (push) Successful in 29s
CI / Backend Unit Tests (push) Successful in 6m20s
CI / fail2ban Regex (push) Successful in 48s
CI / Semgrep Security Scan (push) Successful in 25s
CI / Compose Bucket Idempotency (push) Successful in 1m3s
title is VARCHAR(255) with no planned service-level check; ADR-040's
consequences listed the optimistic-lock and forgery obligations for #775 but
not this one, so an over-long title would have surfaced as a raw
DataIntegrityViolationException -> HTTP 500. Mirrors GESCHICHTE_TITLE_TOO_LONG.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 00:47:53 +02:00
Marcel
788a804810 docs(timeline): pin relative issue ordinals to Gitea issue numbers
The issue body's milestone-relative ordinals ("issue 3", "issue 5") become
unreadable once the milestone closes. Resolved against the Zeitstrahl milestone:
issue 3 = #775 (CRUD API: service/controller/DTO), issue 5 = #777 (assembly
endpoint with the per-person filter). Mapping anchored by issue 6 = #778
(date-label helper) and issue 9 = #781 (curator forms) in #774's forward notes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 00:47:22 +02:00
Marcel
62b96f718f fix(timeline): mark always-populated audit fields REQUIRED in OpenAPI schema
createdBy/updatedBy are NOT NULL and createdAt/updatedAt/version are Hibernate-
populated on every persisted row, so per the CLAUDE.md rule they must carry
@Schema(requiredMode = REQUIRED) like id/title/type/eventDate/precision already
do. Keeps the generated TypeScript types honest if the entity ever reaches the
OpenAPI spec (responses in #775 are planned as views, per ADR-040).

Extends the #774 task list (which named only the five domain fields) per PR #816 review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 00:46:45 +02:00
Marcel
6ed5151e50 fix(timeline): drop join-table indexes redundant with composite PKs
idx_timeline_event_persons_event_id and idx_timeline_event_documents_event_id
duplicated the leading column of their composite primary keys — Postgres already
serves timeline_event_id lookups from the PK index, so the extra indexes only
added write overhead. The inverse-side indexes (person_id, document_id) stay;
they cover the FK cascade path.

Deviates from the #774 task list ("all four FK columns") per PR #816 review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 00:46:06 +02:00
Marcel
3a7c86fc87 test(timeline): allow timeline package in entity-location ArchRule
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 5m9s
CI / OCR Service Tests (pull_request) Successful in 25s
CI / Backend Unit Tests (pull_request) Successful in 5m42s
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
The entities_reside_in_domain_packages ArchUnit rule has a hardcoded
allow-list of domain packages; add ..timeline.. so TimelineEvent passes.
CI caught this — the new domain package was not yet whitelisted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 00:34:18 +02:00
Marcel
1226bd0a07 docs(timeline): register timeline domain in package tables and diagrams
Add timeline/ to the root and backend package tables, TimelineEvent to the
domain-model entity tables, TimelineEvent/EventType/Zeitstrahl to the
glossary, a new l3-backend-timeline C4 component diagram, and the
timeline_events table + two join tables (with their CHECKs and cascade FKs)
to the db-orm and db-relationships ER diagrams. Bumps the db-orm snapshot to
V77.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 00:34:18 +02:00
Marcel
00a00b2c87 docs(adr): add ADR-040 timeline domain data model
Records the architectural commitment for the timeline domain: views-not-
entities for issue 3 (ADR-036 rationale), DatePrecision import coupling
(ADR-025), the UNKNOWN-forbidden / SEASON-APPROX-legal precision contract,
the strict biconditional RANGE CHECK as a deliberate divergence from
Document, the @Version + NOT NULL audit-trail decisions, the optimistic-
lock-to-conflict translation contract (CWE-209), the server-populated-only
createdBy/updatedBy forgery guard (CWE-639), and the EventType stable
frontend styling contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 00:34:18 +02:00
Marcel
cc841a7a4c test(timeline): cover persistence, constraints, and FK cascade
@DataJpaTest against real Postgres (never H2): required-field round-trip,
YEAR default, linked persons/documents, eventDateEnd null/range round-trip,
TEXT description with no length cap, both RANGE-invariant rejections, the
UNKNOWN-precision rejection (NOT_SUPPORTED so the constraint violation does
not poison the test transaction), version null-before-persist/0-after-save,
and a parameterized accept-side proving DAY/MONTH/SEASON/YEAR/APPROX all
persist. makeEvent() defaults createdBy/updatedBy to random UUIDs so every
red is red for the intended reason.

@SpringBootTest cascade guard: deleting a linked Person/Document via the
domain service drops the join row (verified by direct COUNT) and leaves the
event intact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 00:34:18 +02:00
Marcel
513cdb7a4d feat(timeline): add V77 migration for timeline_events table
Creates timeline_events plus the timeline_event_persons and
timeline_event_documents join tables, all FK columns ON DELETE CASCADE
(a person/document delete drops the join row, the event survives —
V71-class hardening). Two CHECK constraints push integrity to Postgres:
chk_timeline_event_range enforces event_date_end non-null IFF RANGE (a
strict biconditional, intentionally tighter than Document's open-ended
ranges), and chk_timeline_event_precision forbids exactly UNKNOWN while
keeping SEASON/APPROX legal. FK and query-column indexes added up-front
to avoid the V62 retrofit debt. Forward-only, additive DDL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 00:34:18 +02:00
Marcel
595007213c feat(timeline): add TimelineEvent entity and repository
Curated timeline event mirroring Document's date block (eventDate /
precision / eventDateEnd) so events and letters share one rendering path.
Audit footprint deliberately diverges from Document: @Version optimistic
lock plus NOT NULL createdBy/updatedBy for the multi-curator edit flow.
precision reuses document.DatePrecision (imported, not duplicated) and
defaults to YEAR. ManyToMany persons/documents with explicit @JoinTable +
@BatchSize, matching Document's join conventions.

Repository is empty for now with a TODO marker for the issue-5 per-person
filter query.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 00:34:18 +02:00
Marcel
45001f042a feat(timeline): add EventType enum
PERSONAL/HISTORICAL classify a curated timeline event. The string value
names are a stable frontend styling contract (family vs. muted world
accent) — no mapping layer; renaming requires a coordinated frontend
change. First piece of the new timeline domain (Zeitstrahl, issue #774).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 00:34:18 +02:00
Marcel
d11378c254 fix(deps): pin esbuild 0.28.1 and cookie >=0.7.0 to clear npm audit gate
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 4m46s
CI / OCR Service Tests (pull_request) Successful in 24s
CI / Backend Unit Tests (pull_request) Successful in 5m42s
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 1m8s
CI / Unit & Component Tests (push) Successful in 5m23s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 6m4s
CI / fail2ban Regex (push) Successful in 48s
CI / Semgrep Security Scan (push) Successful in 25s
CI / Compose Bucket Idempotency (push) Successful in 1m12s
nightly / deploy-staging (push) Successful in 5m20s
The CI step "Security audit (no dev deps)" (npm audit --audit-level=high
--omit=dev) failed repo-wide on every PR: newly-published advisories landed
against the already-pinned esbuild 0.27.7 (GHSA-gv7w-rqvm-qjhr,
GHSA-g7r4-m6w7-qqqr — both high), pulled in transitively via
vite -> @sveltejs/vite-plugin-svelte. The fix for both only exists at
esbuild@0.28.1. A scoped vite@7 minor bump cannot help — all vite 7.x pin
esbuild ^0.27.0.

Add an overrides block:
  - esbuild 0.28.1 (exact, no caret — a future 0.29.x must not silently
    float in and re-break vite; let Renovate propose bumps explicitly)
  - cookie >=0.7.0 (clears the low GHSA-pxg6-pf52-xh8x reaching the prod
    tree via @sentry/sveltekit; drop-in, done in the same pass)

npm audit --audit-level=high --omit=dev now exits 0 with 0 vulnerabilities.
npm run build, lint, and a dev-server boot all succeed with the forced
esbuild 0.28.1 (validated, not assumed — it sits outside vite@7.3.3's
declared ^0.27.0 range).

Closes #817

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 00:23:31 +02:00
Marcel
f64acbc697 test(geschichten): add drafts to the overview page test mock data
Some checks failed
CI / Compose Bucket Idempotency (pull_request) Waiting to run
CI / OCR Service Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / fail2ban Regex (pull_request) Has been cancelled
CI / Semgrep Security Scan (pull_request) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Successful in 4m37s
CI / OCR Service Tests (push) Successful in 26s
CI / Backend Unit Tests (push) Successful in 5m25s
CI / fail2ban Regex (push) Successful in 47s
CI / Semgrep Security Scan (push) Successful in 27s
CI / Compose Bucket Idempotency (push) Successful in 1m7s
PR #813 made +page.svelte read data.drafts, which the load function
always returns, but the pre-existing page.svelte.test.ts mock predates
the field — all 15 tests crashed with TypeError on main after merge.

Closes #814
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 22:05:19 +02:00
Marcel
75e48f2922 docs(person): note YEAR seeding of legacy precisions in ADR-039
Some checks failed
CI / Unit & Component Tests (push) Failing after 4m25s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 5m30s
CI / fail2ban Regex (push) Successful in 45s
CI / Semgrep Security Scan (push) Successful in 27s
CI / Compose Bucket Idempotency (push) Successful in 1m9s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
ad344db2bf fix(i18n): add trailing period to error_invalid_date_precision
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
3626cd1a6d refactor(person): share yearOf between relationship services
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
fe4e2d97d0 fix(import): degrade gracefully when canonical life dates conflict
The canonical upsert path skips validateLifeDates, so a spreadsheet row
with birth_year > death_year - or a preserved hand-entered birth date
conflicting with a canonical death year - violated the V76 CHECK
constraint at flush time and aborted the whole import batch with a raw
500. Resolve the pairs first and, on conflict, keep the person's stored
life dates (empty for a new person), drop the canonical refresh, and log
a WARN with the sourceRef (REQ-IMP-001: never abort the batch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
e712477d2b fix(person): block submit while a life-date input is partial
A partial date (e.g. "14.03.") left the hidden ISO input empty, so
saving the edit form silently cleared a stored date. PersonLifeDateField
now delegates to the shared DateInput primitive (inline format error,
calendar validation) and sets a custom validity while the error is
present, so the browser blocks native submission for both person forms.
A full clear stays submittable - that is the intentional clear path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
4419c434a1 fix(person): type mention items as PersonSummaryDTO, regenerate api
The dropdown and editor typed /api/persons list items as the full Person
entity. The actual wire shape is PersonSummaryDTO, which until the
previous commit had no date fields - so the life-date subtitle rendered
blank in production while fixtures (built from the entity type) kept the
tests green. Retype items as the summary projection and guard the two
personId consumers against the schema-optional id.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
687353a819 fix(person): expose life dates on PersonSummaryDTO projection
The mention dropdown renders precise life dates but receives
PersonSummaryDTO items from /api/persons, which only carried the derived
years - the date fields were silently undefined at runtime. Add
birth/death date + precision to the projection and all four native
queries (searchWithDocumentCount's GROUP BY already listed the columns).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
e4e277219e test(person): add now-required precision fields to Person test fixtures
birthDatePrecision/deathDatePrecision are @Schema REQUIRED, so the
generated Person type makes them non-optional — fixtures that were
type-clean before the regen get UNKNOWN defaults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
a75c46351f docs(person): ADR-039, DB diagrams, and V76 deploy runbook note
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
65a34d48b4 feat(person): date + precision controls on person new/edit forms
New PersonLifeDateField (German date input + hidden ISO + DAY/MONTH/YEAR
precision select, min-h-44px, sm: side-by-side) used for birth and death
in both forms. Legacy APPROX precision seeds the select as YEAR so an
untouched save never claims DAY. Server actions send date+precision
pairs or omit both; obsolete year i18n keys removed, 9 form keys added.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
0e7095fee6 feat(person): render precise life dates on cards, hover card, and mention dropdown
Cards compose aria-hidden * / † glyphs in markup so screen readers only
announce the dates; PersonSummaryDTO list card stays year-shaped by
design (ADR-039). MentionDropdown subtitle wraps instead of truncating
so DAY-precision ranges fit at 320px.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
adac1b1f99 feat(person): formatLifeDateRange takes date + precision, delegates to formatDocumentDate
New formatLifeDate single-date helper carries no glyph so cards can wrap
* / † in aria-hidden spans. Missing precision falls back to YEAR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
29ada9b681 chore(api): regenerate TypeScript types for Person date fields
Person gains birthDate/deathDate + required precision enums;
PersonSummaryDTO, PersonNodeDTO, and RelationshipDTO keep derived
integer years. familyForest/buildLayout tests still pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
92a2feba1e feat(person): store birth/death as LocalDate + DatePrecision
Entity swap mirroring Document.metaDatePrecision; PersonUpdateDTO takes
date + precision; validateLifeDates (badRequest BIRTH_AFTER_DEATH /
INVALID_DATE_PRECISION) replaces validateYears; preferHumanDate keeps
DAY/MONTH/SEASON hand-entered dates on re-import and refreshes
YEAR/UNKNOWN from the canonical year (ADR-025 extension);
PersonUpsertCommand stays year-shaped. Native queries project
EXTRACT(YEAR ...) so PersonSummaryDTO and PersonNodeDTO stay
year-shaped, null-safe for undated persons.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
ba7e8ca6f5 feat(person): V76 migration — birth/death year to date + precision columns
Pre-check aborts on corrupt year data, backfills YYYY-01-01/YEAR,
adds five named CHECK constraints, drops birth_year/death_year.
Staged-Flyway Testcontainers test covers pre-check aborts, backfill
shapes, and post-migration schema.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
Marcel
f408f60631 feat(person): add BIRTH_AFTER_DEATH and INVALID_DATE_PRECISION error codes
Backend enum, frontend ErrorCode mirror, getErrorMessage cases, and
error message i18n keys (de/en/es) incl. the mixed-precision workaround
hint in error_birth_after_death.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 21:49:16 +02:00
38a6d6b0fc feat(geschichten): show blog writers' own drafts on the Geschichten overview (#807) (#813)
Some checks failed
CI / Unit & Component Tests (push) Failing after 3m48s
CI / OCR Service Tests (push) Successful in 22s
CI / Backend Unit Tests (push) Successful in 5m24s
CI / fail2ban Regex (push) Successful in 53s
CI / Semgrep Security Scan (push) Successful in 23s
CI / Compose Bucket Idempotency (push) Successful in 1m9s
2026-06-12 19:46:03 +02:00
b33d0eb850 feat(lesereisen): implement lesereisen
All checks were successful
CI / Unit & Component Tests (push) Successful in 4m34s
CI / OCR Service Tests (push) Successful in 27s
CI / Backend Unit Tests (push) Successful in 5m1s
CI / fail2ban Regex (push) Successful in 47s
CI / Semgrep Security Scan (push) Successful in 23s
CI / Compose Bucket Idempotency (push) Successful in 1m11s
2026-06-12 14:04:02 +02:00
Marcel
4bcf568ed4 Merge branch 'main' of ssh://git.raddatz.cloud:222/marcel/familienarchiv
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m22s
CI / OCR Service Tests (push) Successful in 23s
CI / Backend Unit Tests (push) Successful in 3m41s
CI / fail2ban Regex (push) Successful in 46s
CI / Semgrep Security Scan (push) Successful in 22s
CI / Compose Bucket Idempotency (push) Successful in 1m7s
nightly / deploy-staging (push) Successful in 2m50s
2026-06-08 16:27:41 +02:00
Marcel
ddb1ec4df8 docs(timeline): add Zeitstrahl visual specs (global Concept A, event editor)
Visual design specs for Milestone #14:
- zeitstrahl-global-concepts.html — A/B/C exploration of the global timeline
- zeitstrahl-final-spec.html — canonical Concept A (global + per-person Lebensweg)
- zeitstrahl-event-editor-spec.html — curator event editor + document quick-action

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 16:27:15 +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
e63eaadc33 docs(timeline): add Person date+precision migration as foundational issue
Replace Person birthYear/deathYear integers with birthDate/deathDate +
DatePrecision so known exact birthdays render precisely. Migration,
re-import preservation rule, and bounded blast radius captured; becomes
issue 1 the timeline's derived events depend on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 19:26:13 +02:00
Marcel
d4a25e34d8 docs(timeline): add family timeline (Zeitstrahl) design spec
Hand-curated, year-banded vertical timeline weaving derived person
life-events, curated personal/historical events, and date-placed
letters. Includes proposed sub-issue breakdown for a milestone.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 19:18:55 +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
Marcel
ca52145556 feat(person): add resolveByName for direct/partial name matching (#763)
Token-set containment over all of a person's name components (firstName,
lastName, alias, each PersonNameAlias first+last, title) decides direct vs
partial. Orchestrates tokenize → cap(8) → fetch pool → classify → cap(10)
after classification, with an empty-token guard and a PII-free debug log of
the outcome bucket. MAX_TOKENS is a DoS control; the after-classify cap keeps a
direct match that sorts past position 10 among partials. Read-only transaction
keeps lazy nameAliases reachable during classification (ADR-022).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 08:47:47 +02:00
Marcel
9a26bf75b0 feat(person): match alias first names in searchByName (#763)
The direct-match classifier accepts alias firstName tokens, so the fetch must
surface candidates matchable only via an alias first name. Add a.firstName to
the searchByName LIKE clause (reuses the bound :query — injection-proof). The
person_name_aliases.first_name column already exists; no migration.

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