Dokumenttitel automatisch mit Datum/Ort synchronisieren (Save-time + einmaliger Backfill) #726
Reference in New Issue
Block a user
Delete Branch "%!s()"
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?
Problem
Document titles are stored strings built once at import (
{index} – {dateLabel} – {location}). When a date or location is later corrected in the edit UI, the title does not follow — it leaves a stale title behind (e.g. the title still shows2028after the date is fixed to1928). The title input is resubmitted unchanged on save, so the corrected date never reaches the title.Goal
C-0029 – Brief an Mutter) is never overwritten.Actor: Admin / archivist.
Non-goals
DocumentBulkEditDTOcarries nodocumentDate,metaDatePrecision, orlocation(it touchesdocumentLocation=meta_document_location, a different field). Bulk edit cannot make a title stale, so there is nothing to do there.indexis the document'soriginalFilename, andDocumentService.updateDocumentreassignsoriginalFilenameto the uploaded file's name on file-replace (doc.setOriginalFilename(newFile.getOriginalFilename())). After a replace it is no longer the catalog index (and no longer matches the importINDEX_PATTERN), so the title no longer matches the formula and is treated as manual — neither save-time nor backfill will rewrite it. This is accepted (decision: keep overloadingoriginalFilename, no dedicatedcatalogIndexcolumn). Fail-safe by design.How titles are built today (reference)
DocumentImporter.buildTitle()(private static), composing — in order," – "separators:documentDate,metaDatePrecision,metaDateEnd,metaDateRaw,locationall live on theDocumententity, so a title is rebuildable later without the import spreadsheet. The edit form already submitsmetaDatePrecision/metaDateEnd/metaDateRawas hidden inputs (WhoWhenSection.svelte), so the regenerated label is honest at the new precision.A. Single source of truth (FR-TITLE-001)
Extract the title composition (
{index} – {dateLabel} – {location}) out ofDocumentImporter.buildTitle()into one shared component in thedocumentpackage — e.g.DocumentTitleFactoryunderorg.raddatz.familienarchiv.document.DocumentImporter(packageimporting) then calls it. Direction matters:documentowns the formula,importingconsumes it — do not makeimportinginternals public to satisfydocument.B. Save-time regeneration — exact, no heuristic (FR-TITLE-002, primary mechanism)
In
DocumentService.updateDocumentonly (bulk edit is out of scope — see non-goals):autoTitleBefore = titleFactory.build(doc)from the document's currently-persisted index/date/precision/location. This must happen at the top of the method — the current code overwrites title/date/location at lines 379–382, andapplyDatePrecisionskips null fields, so the old state must be captured first.autoTitleBefore→ it was the machine value → set title =build(...)from the new state.This is an exact old-vs-new comparison — no false positives, no false negatives. It relies on the edit form round-tripping the stored title when untouched (
DescriptionSection.svelte,value={titleValue}— confirmed). Guard: a blank submitted title must not be regenerated to blank (title isNOT NULL).Reference shape:
C. One-time backlog cleanup — heuristic (FR-TITLE-003 / FR-TITLE-004)
The pre-edit state is gone for already-stale rows, so the cleanup uses a grammar heuristic.
FR-TITLE-003 —
POST /api/admin/backfill-titles, underAdminController's class-level@RequirePermission(Permission.ADMIN), synchronous (the sweep is microseconds/row; matchesbackfill-versions/backfill-file-hashes), returnsBackfillResult(count)wherecount= number updated. Iterates all documents; for each whose stored title passes the overwrite test, rebuilds it viatitleFactory.build. Idempotent. No frontend UI — invoked once viacurl/backend/api_tests/, called against the backend directly (port 8080) to bypass the SvelteKit proxy timeout.document_versionsrow per document. Save viadocumentRepository.savedirectly — follow theDocumentService.backfillFileHashesprecedent, which does not callrecordVersion. Never route the backfill throughupdateDocument(that path versions every write and would snapshot the whole corpus for a mechanical rename).scanned/updated/skippedlog line at completion (NFR-OBS-001), via SLF4J parameterized logging — never string-concatenate titles.FR-TITLE-004 — Overwrite test (heuristic, used only by the cleanup). Prefer literal structural parsing: split the stored title on
" – ", then test each segment. A title is overwritable iff it matches:{index}+ a date segment matching the known formatter grammar, optional trailing location segment, or{index}+ an optional location segment equal to the document's currentlocation, or{index}.Otherwise skipped. The
{index}comparison must be literal — if any regex is used,Pattern.quote(index)it and anchor^…$;originalFilenameis user-controlled (set from an uploaded filename on file-replace, and no longer constrained by the importINDEX_PATTERN), so an unquoted pattern is a ReDoS / regex-injection vector (CWE-1333 / CWE-625). Avoid(...)*/(...)+over title text. Fail closed: any malformed index/state → skip the document, never overwrite.{DATE_LABEL}= the formatter's emittable forms:YYYY,ca. YYYY,MMMM YYYY(German month),d. MMMM YYYY,{Frühling|Sommer|Herbst|Winter} YYYY,Datum unbekannt, and the range forms (ab d. MMM YYYY,d.–d. MMM YYYY,d. MMM – d. MMM YYYY,d. MMM YYYY – d. MMM YYYY).D. Edit-form feedback (FR-TITLE-005)
The title input shows the old string while the user changes the date, then changes on its own after save — a surprise, especially for the 60+ audience. Add a helper line under the title input (
DescriptionSection.svelte), associated viaaria-describedby(the helper'sidreferenced from the<input>), localized de/en/es via Paraglide, e.g. „Wird automatisch aus Datum und Ort gebildet — sobald du den Titel änderst, bleibt deine Version erhalten.“ Keep it ≥12px (prefer 16px), not color-only, and verify the chosen token (e.g.text-ink-3) meets WCAG AA contrast (≥4.5:1) onbg-surface. (Live preview was considered and declined.)Acceptance criteria
Non-functional requirements
documentpackage (FR-001); importer + save-time + backfill never diverge. Date-label Java/TS split (#666) stays as-is.scanned/updated/skipped(SLF4J parameterized);count(= updated) is the response.ADMIN; ordering/ReDoS guards per FR-004.Test strategy
@ExtendWith(MockitoExtension.class), mocked repo): all save-time scenarios + the heuristic matcher in isolation (eachDATE_LABELform matches; prose skipped; regex-metacharacter index matched literally and terminates). The logic is pure — keep it off Spring.postgres:16-alpine, never H2 —titleisNOT NULL):POST /api/admin/backfill-titlesfixes a stale row; idempotent (second runcount == 0); skips prose; asserts nodocument_versionsrow added per doc; returns 401/403 for non-admin.messages/{de,en,es}.json) — no missing-translation fallback.makeDocument(title, date, precision, location).Documentation
docs/adr/for "document title is a shareddocument-package service + save-time exact-match regeneration".docs/GLOSSARY.mdentry for auto-generated title.docs/architecture/c4/l3-backend-*.puml.backend/api_tests/entry forPOST /api/admin/backfill-titleshitting the backend directly on port 8080 (with the runbook note that it is a one-shot admin call).Implementation notes
DocumentTitleFormatterinto thedocumentpackage; do not duplicate the German formatting or collapse the TS mirror.autoTitleBeforefrom the persisted entity before mutating it with the DTO;projectedStatemirrors the date/location-overwrite vs precision-skip-null asymmetry.BackfillResult(count)shape and thebackfillFileHashessave pattern (documentRepository.save, norecordVersion); never route it throughupdateDocument.Implemented on
feat/issue-726-auto-title-syncAll acceptance criteria covered with red/green TDD; backend tests green (293 across the touched classes incl. a Testcontainers
postgres:16integration test),clean packagebuilds, frontend lint clean.What landed
A — Single source of truth (FR-TITLE-001)
DocumentTitleFactory(@Component,documentpackage) owns the{index} – {dateLabel} – {location}formula;DocumentImporternow consumes it.DocumentTitleFormattermoved intodocument(package-private); its #666 fixture-parity test moved with it.B — Save-time regeneration, exact match (FR-TITLE-002)
updateDocumentcapturesautoTitleBeforefrom the persisted state before any setter, thenresolveTitle+projectedStaterebuild only when the submitted title still equals it. Hand-written/freshly-typed titles kept; blank never persisted; file-replaced docs fall through as manual (by design).projectedStatemirrors the date/location-overwrite vs precision-skip-null asymmetry. All save-time gherkin scenarios are unit tests.C — One-time backlog cleanup (FR-TITLE-003/004)
POST /api/admin/backfill-titles(synchronous, ADMIN-only) →BackfillResult(count).DocumentTitleBackfillMatcherheuristic: literalstartsWithindex (no ReDoS/regex injection, CWE-1333), date-label forms derived from the sameLocale.GERMANformatters as the factory (no drift), prose left untouched, fail-closed. Each emittable date-label form unit-tested incl. the regex-metacharacter index case.backfillTitles()saves via the repository directly (norecordVersion— no version-spam), idempotent, logsscanned/updated/skipped.count==0) · prose skipped · nodocument_versionsrows added. Permission 401/403 covered by the@WebMvcTestslice.D — Edit-form feedback (FR-TITLE-005)
DescriptionSection.svelte(single-edit only, viashowTitleHelp),aria-describedby-wired,text-ink-3(AA onbg-surface). New Paraglide keyform_helper_title_autogeneratedin de/en/es. Component test + one Playwright E2E (create auto-titled doc → edit date → title follows on detail page).Docs — ADR-031,
GLOSSARY.mdauto-generated title,l3-backend-3b-document-management.puml, and abackend/api_tests/runbook entry hitting port 8080 directly.Commits
b1f77bcrefactor(document): extract title composition into shared DocumentTitleFactorye6ce000feat(document): regenerate auto-title on save when date/location change26b45f1feat(document): one-time backfill endpoint for stale auto-titles12db7b3test(document): integration-test title backfill against real Postgres83e0afbfeat(document): explain auto-generated title under the edit title fieldcf457cbdocs(document): ADR-031 + glossary/c4/api_testsOperator note
After deploy, run the cleanup once:
POST http://<backend>:8080/api/admin/backfill-titles(ADMIN, direct to 8080 to bypass the proxy timeout). Idempotent — a second run returns{"count": 0}.