Leonie (UX): the toggle pill (text-[7.5px]) and loading subtitle
(text-[9px]) were below the 12px floor for the 60+ audience. Bump both
to text-xs and the toggle icon to h-3.5/w-3.5. Overrides the visual
spec's tokens, which conflicted with the issue's own legibility mandate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mock POST /api/search/nl (delayed fixture: 2-name directional + applied
keyword), assert loading announcement → chips render → axe-clean in light
and dark → removing the keyword chip re-runs a keyword GET with the
remaining sender+receiver params. Adds a data-testid wrapper on the NL
results region for axe scoping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the smart-search sub-component directory to the frontend Project
Structure tree (merge blocker per #739).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SearchFilterBar drives chip-clearing via onModeToggle (mode switch) and
onSmartSearch (new query); pin that callback contract.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Lift smartMode to documents/+page.svelte and drive the full smart-search
lifecycle: POST /api/search/nl via csrfFetch, loading/error panels, chip
row, single-select disambiguation, and a transparent empty state. Chip
removal and disambiguation selection map the interpretation to keyword
params and re-run via GET (Option A in-page fallback). Mode toggle and
new queries reset prior interpretation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add smartMode $bindable plus onSmartSearch/onModeToggle callbacks. The
toggle pill sits in the input's right slot (decorative icon moved to the
left); smart mode disables the live oninput keyword search, adds
maxlength=500, and submits the NL query on Enter. 4 integration specs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Accessible disclosure: aria-expanded/aria-controls trigger, focus moves
into the option list on open, Escape and click-outside close and return
focus to the trigger, selecting a candidate emits onSelect. Single-select
(GET re-run) per the resolved#738 open decision — backend has no
multi-sender OR param. 5 vitest-browser-svelte specs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Renders type-prefixed chips (Absender/Zeitraum/Stichwort), a single
directional chip for 2-name queries, gates keyword chips on
keywordsApplied, and emits onRemoveChip(type, value?). Truncating name
spans keep the 44px × button visible; chip wrappers show a focus ring.
9 vitest-browser-svelte specs (red/green).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Toggle pill with aria-pressed, active/resting styles matching the
AND/OR operator button pattern, and mobile-expanded KI/Text labels.
4 vitest-browser-svelte specs (red/green).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Toggle labels, loading panel, error panels (503/429), empty-state
retry, chip type-prefixes + remove label, and disambiguation strings
for the smart search UI (#739). Formal Sie form per project standard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both the first-time model pull runbook (from this branch) and the model
upgrade procedure (from main) belong in DEPLOYMENT.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers the SmartModeToggle pill (inside the search input, Google AI Mode
style), InterpretationChipRow anatomy, DisambiguationPicker, and all
status/error/empty states as full-result-area panels.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- error_smart_search_unavailable/rate_limited now use "Sie" (formal) to
match the tone of all existing German error messages
- Replace inline FQNs in DocumentService.buildPersonSpec with proper
JoinType + Predicate imports
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch to wiremock-jetty12 artifact and force ee10 Jetty deps to 12.1.8
to resolve compatibility with Spring Boot 4's Jetty 12.1.8 core.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses Elicit's and Sara's review concerns on PR #749:
- Expand §6 ollama_models section into a full model upgrade runbook (step-by-step
docker volume rm + recreate, including production volume name prefix)
- Add re-deploy idempotency note to §3.4 (init container exits quickly when model
already present on the volume)
- Add NL search smoke test to §3.4 (curl command distinguishing 200 from 503
NL_SEARCH_UNAVAILABLE)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Updated OLLAMA_API_KEY env vars table from 0.6.5 to 0.6.5 or 0.30.6 to
match both tested versions. Added an explicit warning in §3.4 that
docker compose up -d --wait blocks for 60–90 min on first deploy when the
model pull has not yet completed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both versions were tested and neither enforces the key. Comment updated to
say "0.6.5 or 0.30.6" and surface archiv-net as the sole effective control.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hardcoded literal overrides any .env setting — setting APP_OLLAMA_BASE_URL=
in .env had no effect on the backend container. Now uses the same pattern
as APP_OCR_TRAINING_TOKEN with a safe default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
§12 stated OLLAMA_API_KEY guards against lateral movement — contradicts
§7's empirical finding that it is not enforced. Replaced with an accurate
note referencing §7. Stale pre-merge placeholder in Consequences ("Three
TBD items must be resolved") removed; all three are resolved. §7 section
title updated from "0.6.5" to "0.6.5 and 0.30.6" to match the body text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Docker Compose interpolates $VAR in command strings — use $$ to pass a
literal $ to the shell so SERVE_PID=$! and kill $SERVE_PID work correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- OLLAMA_API_KEY: non-enforcement confirmed on both 0.6.5 and 0.30.6
- read_only: true: confirmed working on both 0.6.5 and 0.30.6
- Peak RSS during pull: ~108 MiB (well under 2g limit)
- All TBD placeholders resolved
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ollama-model-init: one-shot init container that pulls qwen2.5:7b-instruct-q4_K_M
into the ollama_models volume on first start
- ollama: main inference service on archiv-net (expose: only, no public port)
- ollama_models named volume for persistent model storage
- APP_OLLAMA_BASE_URL + APP_OLLAMA_API_KEY added to backend env
- Both services: cap_drop ALL, no-new-privileges, read_only+tmpfs (ADR-019 + ADR-028)
- start_period: 60s — model pre-pulled by init container
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- prometheus.yml: ocr:8000 → ocr-service:8000 (Docker service name is
ocr-service, not ocr — current scrape target has never resolved)
- Add Ollama scrape job on ollama:11434 /metrics
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace all references to the CX32 VPS (8 GB RAM, Hetzner Cloud) with the
actual production server: a Hetzner Serverbörse dedicated server with an
Intel Core i7-6700 (4C/8T, 3.4 GHz) and 64 GB RAM.
Affected files:
- .claude/personas/devops.md — monthly cost line + upgrade example
- docs/infrastructure/production-compose.md — sizing section + cost table
- docs/DEPLOYMENT.md — OCR memory table + OCR_MEM_LIMIT env var description
- docs/adr/004-pdfbox-thumbnails.md — thumbnailExecutor memory ceiling note
- docs/adr/021-tmpdir-persistent-volume-staging.md — OOMKill rationale in alternatives
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both #730 (tag case-collision) and #684 (person-delete DB integrity) landed
an ADR-032 on main. Renumber the tag/case-collision one to 033 — it is
referenced only from this PR's person-domain comments and its own file, so the
move is self-contained and touches no Flyway migration. The person-delete
ADR-032 and the V71 migration comment that cites it are deliberately left
untouched (editing an applied migration would drift its Flyway checksum).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review noted the "never throws" claim was overstated: the exact-case Optional
lookups still surface a NonUniqueResultException on two byte-identical
same-case rows. That is a true data anomaly out of #731's scope (ambiguous =
case-insensitive) and resolves to the opaque INTERNAL_ERROR, never a wrong
row. Record that boundary at both resolution points and in ADR-032 so the gap
is not silently assumed covered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
findByName resolved via Optional<Person>
findByFirstNameIgnoreCaseAndLastNameIgnoreCase, which threw
NonUniqueResultException once two people shared a first+last name case-
insensitively (hans müller / Hans Müller) — a 500 on the routine upload path
(DocumentService.storeDocument sender resolution).
findByName now resolves exact-case → single case-insensitive match → else
empty. The sender path deliberately diverges from the alias path: an
ambiguous name leaves the sender UNSET rather than guessing the lowest id,
because correct provenance beats a confidently-wrong pre-fill a reviewer
won't re-check. The two new name queries use explicit HQL equality so a null
first name binds as `= NULL` (no match) instead of the derived-query fold to
`first_name IS NULL`, which would widen a last-name-only row in as a sender.
Pins the opaque error path (IncorrectResultSizeDataAccessException stays
INTERNAL_ERROR with no Hibernate/SQL/row-count leak) and extends ADR-032 with
the Person section.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
findOrCreateByAlias resolved via Optional<Person> findByAliasIgnoreCase,
which throws NonUniqueResultException once two aliases collide only by case
(müller / Müller) — a generic 500 on the importer path. Mirror the #730 tag
fix: resolve exact-case first, then the lowest-id case-insensitive sibling,
then create-when-absent (institution/group and maiden-name alias preserved).
The throwing Optional<…>IgnoreCase variant is deleted so it can't be reused.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
V71 gives transcription_block_mentioned_persons.person_id a real FK, so two
TranscriptionBlockMentionsRepositoryTest cases that inserted mention rows with
random (non-existent) person ids now violate fk_tbmp_person. Persist real
Person rows and use their ids. Caught by CI's full suite.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>