refactor: bulk-edit follow-ups deferred from PR #331 #332
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?
Context
Deferred work from the cycle-1 multi-persona review on PR #331. Items are ordered Must → Should → Could. Tackle Musts before Shoulds. Each item is a standalone, atomic change — implement in a separate commit.
MUST
M1 — Structured error code in
BulkEditErrorFile:
backend/src/main/java/org/raddatz/familienarchiv/dto/BulkEditError.javaCurrent
message: Stringfield is a free-form string that round-trips user-controlled data through the response body. The frontend also cannot localize it.Changes:
message→code, change type fromStringtoErrorCode:new BulkEditError(id, message)call sites to pass anErrorCodevalue.ErrorCodeentry if needed (e.g.BULK_EDIT_DOCUMENT_FAILED).frontend/src/lib/errors.tsand add a translation key inmessages/de.json,en.json,es.json.npm run generate:api.Done when:
grep -r "BulkEditError" backend/shows noString messagefield; the frontend renders a localized string when a per-document failure occurs.M2 — WCAG focus ring on
DocumentRowbulk-select checkboxFile:
frontend/src/lib/components/DocumentRow.svelteThe
<label data-testid="bulk-select-checkbox">is the 44×44 hit target, butfocus-within:ring-2is not on it — only the inner 20×20<input>gets the ring. Violates WCAG 2.1 AA SC 2.4.7.Change: Add
focus-within:ring-2 focus-within:ring-brand-mint focus-within:ring-offset-1to the<label>wrapping the checkbox input.Done when: Keyboard tab to the checkbox shows the ring on the full label area;
DocumentRow.svelte.spec.tshas a test assertingfocus-withinis present on the label wrapper.M3 — Guard
handleDiscardagainst mid-save raceFile:
frontend/src/lib/components/document/BulkDocumentEditLayout.svelte(line ~130handleDiscard,savingstate at line 60)handleDiscardhas no guard against running whilesavingistrue.handleSave(line 284) hasif (saving) return;buthandleDiscarddoes not.Decision required: Is mid-save discard intentional?
if (saving) return;at top ofhandleDiscard.Done when: A test in
BulkDocumentEditLayout.svelte.spec.tscovers the mid-save discard path and asserts the intended behaviour. The code either has a guard or a comment making intent explicit.M4 —
@SpringBootTestintegration test forapplyBulkEditToDocumenttransactional boundaryFile to create:
backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceBulkEditIntegrationTest.javaMethod under test:
DocumentService.applyBulkEditToDocument(line 443 inDocumentService.java)The claim "a per-document failure cannot partially mutate other documents in the batch" is unverifiable by the existing Mockito unit suite because Mockito cannot exercise the Spring
@Transactionalproxy boundary.Test scenario (use Testcontainers PostgreSQL — same setup as any existing
@SpringBootTest):DocumentBulkEditDTOwithsenderId= a UUID that does not exist in the DB (forces aDomainExceptionon doc2).patchBulkwith IDs[doc1.id, doc2.id, doc3.id].BulkEditErrorwithid = doc2.id.Done when: Test passes against a real PostgreSQL container and runs as part of
./mvnw test.M5 — Round-trip ID preservation in chunking test
File:
frontend/src/lib/components/document/BulkDocumentEditLayout.svelte.spec.tsTest to update: Find the test for chunked saving with 1100 IDs (search for
1100orchunkin the file).Current test asserts call counts only. A regression dropping or duplicating IDs would pass silently.
Change: After asserting call counts, also assert the union of all chunk IDs equals the original input exactly:
Done when: The test fails if any chunk sends a different set of IDs than the input.
M6 —
+layout.svelteauto-clear$effecttest matrixFile:
frontend/src/routes/layout.svelte.spec.tsEffect under test: Lines 28–40 in
frontend/src/routes/+layout.svelteThe auto-clear effect has zero test coverage. A Svelte reactivity regression silently breaks it.
Add these 5 test cases:
/documents→/persons: store is cleared./enrich→/persons: store is cleared./documents→/documents/123: store is NOT cleared./documents→/documents/bulk-edit: store is NOT cleared.bulkSelectionStore(add/remove an ID) while on/documents: the clear does NOT fire (theuntrackguarantee from line 36).Done when: All 5 cases pass; a comment in the test references lines 28–40 in
+layout.svelte.SHOULD
S1 — Pre-resolve tags/sender/receivers once per bulk-edit request (N+1 fix)
File:
backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.javatagService.findOrCreate()is called insideapplyBulkEditToDocument, which is called per document. 500 docs × 10 tags = ~5000 tag-lookup queries per request.Change: In the method that loops over document IDs (search for the loop calling
applyBulkEditToDocument), resolve tags, sender, and receivers once before the loop. Pass resolved objects intoapplyBulkEditToDocument(update signature or add overload).Done when: A unit test asserts
tagService.findOrCreateis called exactlydto.getTagNames().size()times regardless of document count.S2 — Add
RateLimitInterceptortoPATCH /api/documents/bulkFiles:
backend/src/main/java/org/raddatz/familienarchiv/config/WebConfig.java(lines 12–13)backend/src/test/java/org/raddatz/familienarchiv/config/RateLimitInterceptorTest.javaConfirmed: interceptor currently only covers
/api/auth/invite/**and/api/auth/register.PATCH /api/documents/bulkcan process 500 documents per call with no rate cap.Change:
Also add a test in
RateLimitInterceptorTest.javaasserting HTTP 429 after 10 calls on/api/documents/bulk.Done when: The test passes;
WebConfig.javaincludes the bulk path.S3 — Move
/documents/bulk-editdata fetch fromonMountto server loadFiles:
frontend/src/routes/documents/bulk-edit/+page.svelte(removeonMountat lines 2 and 15–16)frontend/src/routes/documents/bulk-edit/+page.server.tsThe
onMountreadsbulkSelectionStore.idsand calls the batch-metadata endpoint client-side, causing a loading flash and breaking the project rule that data flows from server load via props.Challenge: The store is client-side state. One approach: persist selected IDs into a server-side session (cookie) before navigating, so the server load can read them. Confirm the approach before implementing.
Done when: Page renders with document data on first paint;
onMountdata-fetch is removed from+page.svelte; empty-store redirect is in the server load.S4 — Responsive stacking for
BulkDocumentEditLayoutbelow 768pxFile:
frontend/src/lib/components/document/BulkDocumentEditLayout.svelteThe 55/45 panel split renders unusably narrow on phones. The
MassenbearbeitungCTA inBulkSelectionBaris reachable from mobile.Two options (choose one):
flex-colbelowmd:. PDF preview on top, form below.Massenbearbeitungbutton inBulkSelectionBar.sveltebelowmd:breakpoint. Simpler but removes mobile access entirely.Done when: At 375px viewport width, the chosen option is implemented and the page is fully usable (no horizontal overflow, no panel narrower than the viewport).
S5 — Extend
accessibility.spec.tsto cover/documents/bulk-editFile:
frontend/e2e/accessibility.spec.tsNew page with
role="note"callouts, sticky bars, and focus-managed checkboxes — high a11y-regression risk.Change: Add a test that logs in, selects 2+ documents, navigates to
/documents/bulk-edit, and runscheckA11y(). Assert zero critical/serious axe violations.Done when: Test passes; any violations found during implementation are fixed or explicitly suppressed with a comment.
S6 — Cover
tagOp=ORend-to-end ingetDocumentIdscontroller testFile: Find the test containing
getDocumentIdstests — likelybackend/src/test/java/org/raddatz/familienarchiv/controller/DocumentControllerTest.javaCurrent test only forwards
senderId;tagOpis untested.Change: Add a test case passing
tagOp=ORand verifyingTagOperator.ORis forwarded toDocumentService.getDocumentIds. Add a case for the default (tagOpomitted →TagOperator.AND).S7 — Snapshot save-flow metadata across chunks
File:
frontend/src/lib/components/document/BulkDocumentEditLayout.svelte.spec.tsChunked-save test counts calls but doesn't verify
FormDatacontent. A regression resettingsenderIdortagNamesbetween chunks would pass silently.Change: Capture the
FormDatabody of each chunk call and assertsenderIdandtagNamesare identical across all chunks.S8 — ADR for bulk-edit additive-vs-replace semantics
File to create:
docs/adr/— increment from the last ADR number in that directory.Lift the semantics documented in the
DocumentBulkEditDTOJavadoc (lines 11–19) into a standalone ADR:After writing the ADR, add a
@seereference to it in theDocumentBulkEditDTOJavadoc.S9 — Parametrize
@Sizevalidator rejection tests onDocumentBulkEditDTOFile: Find the test containing
patchBulk_returns400_whenArchiveBoxExceeds255Chars— search inbackend/src/test/.Replace individual test methods with a
@ParameterizedTest @CsvSourcecovering all 6@Sizeconstraints:tagNameslist lengthtagNamesitem lengthreceiverIdslist lengthdocumentLocationarchiveBoxarchiveFolderEach row must assert HTTP 400.
COULD
C1 — Rename
BatchMetadataRequest→BulkEditIdsRequest,DocumentBatchSummary→BulkEditDocumentSummaryFiles:
backend/src/main/java/org/raddatz/familienarchiv/dto/BatchMetadataRequest.javabackend/src/main/java/org/raddatz/familienarchiv/dto/DocumentBatchSummary.javaDone when:
grep -r "BatchMetadataRequest\|DocumentBatchSummary" backend/returns zero results.C2 — Refactor
DocumentBulkEditDTOfrom@DatatorecordFile:
backend/src/main/java/org/raddatz/familienarchiv/dto/DocumentBulkEditDTO.javaBlocker: Tests that mutate via setters must first be rewritten to use constructor args (see Javadoc lines 23–26). Do S9 first — it touches the same tests.
Done when: Class is a
record; all@Sizeannotations preserved on record components;@Data,@NoArgsConstructor,@AllArgsConstructorremoved.C3 — Split
BulkDocumentEditLayout.svelteinto upload and edit componentsFile:
frontend/src/lib/components/document/BulkDocumentEditLayout.svelte(~520 lines)Extract:
BulkUploadLayout.svelte— upload mode (mode='upload'branch)BulkEditLayout.svelte— edit mode (mode='edit'branch)chunkAndSave()helper — shared chunking concernBulkPdfPreview/FileSwitcherStrip— shared sub-componentsDone when: The
modeflag is no longer needed inside any single component; existingBulkDocumentEditLayout.svelte.spec.tstests pass against the new structure.C4 — Add tooltip to
FieldLabelBadgeFile:
frontend/src/lib/components/document/FieldLabelBadge.svelteAdd a
titleattribute explaining the behaviour on hover:additive:title="Wird zu bestehenden Werten hinzugefügt"replace:title="Überschreibt bestehende Werte, wenn ausgefüllt"Done when: Both variants have
title;FieldLabelBadge.svelte.spec.tsasserts the value for each.C5 — Pin checkbox-click workaround in
DocumentRow.svelte.spec.tsinto one helperFile:
frontend/src/lib/components/DocumentRow.svelte.spec.tsThe Tailwind-in-browser checkbox-click workaround is duplicated across ~5 test cases. Extract into a single
clickCheckbox(element)helper.Done when: The workaround logic appears exactly once in the file.
C6 — Cover remaining Esc-scope guard selectors in
BulkSelectionBar.svelte.spec.tsFile:
frontend/src/lib/components/document/BulkSelectionBar.svelte.spec.tsonEscapechecks 4 selectors (lines 24–31 inBulkSelectionBar.svelte). Current tests cover selectors 1 and 2 only:dialog[open]✅ line 90[aria-expanded="true"]✅ line 108[role="menu"]:not([hidden])❌ missing[role="dialog"]:not([hidden])❌ missingAlso missing:
e.defaultPreventedshort-circuit (line 26 inBulkSelectionBar.svelte).Add 3 test cases: Escape does not clear when selector 3 matches; does not clear when selector 4 matches; does not clear when
e.defaultPreventedis true.WON'T (this cycle)
👨💻 Felix Brandt — Senior Fullstack Developer
Observations
M1 — BulkEditError.message is confirmed free-form.
BulkEditError.javais a one-liner record withString message. The controller catchesDomainExceptionand passese.getMessage()directly, and catches genericExceptionand passes the hardcoded string"Internal error". Both paths need to change: theDomainExceptionbranch should usee.getErrorCode()(the ErrorCode enum value), and the fallback branch should use a newErrorCode.BULK_EDIT_DOCUMENT_FAILEDconstant.M3 — handleDiscard guard is a bug, not a design choice. I looked at
handleDiscard(line 130) vshandleSave(line 284).handleSavehasif (saving) return;as its first line.handleDiscardhas nothing. There's no comment, no test, no documented intent for this asymmetry. Given thathandleDiscardineditmode callsbulkSelectionStore.clear()andgoto('/documents')— both state mutations — running it mid-save is almost certainly destructive. Treat this as a bug: addif (saving) return;at the top ofhandleDiscard.M5 — Chunking test counts calls but doesn't verify ID completeness. This is a classic off-by-one trap. The
flatMap+ Set size assertion pattern in the issue is correct. TheextractIdsFromCallhelper will need to deserialize the FormData body — make sure to account for however the chunk payload encodes IDs (likely a JSON array field inside FormData, or URL-encoded array params).M6 — Auto-clear $effect has zero test coverage. The effect at lines 28–40 of
+layout.sveltereadspage.url.pathnamereactively andbulkSelectionStore.sizeinsideuntrack. The five test cases in the issue are exactly right. Case (e) —untrackprevents the effect from re-firing on store mutations — is the trickiest to write: you'll need to trigger the effect once by navigating into a non-bulk route, verify it fires, then mutate the store while still on that route and assert the effect does NOT fire a second time.C3 — BulkDocumentEditLayout.svelte at 535 lines is well past the split threshold. The issue's decomposition is sensible. The
mode='upload'andmode='edit'branches share very little beyond the file switcher and save logic. ThechunkAndSavehelper is already a pure function and extracts cleanly.Recommendations
DocumentController.patchBulk, changenew BulkEditError(id, sanitizeForLog(e.getMessage()))tonew BulkEditError(id, e.getErrorCode())for theDomainExceptionbranch. AddErrorCode.BULK_EDIT_DOCUMENT_FAILEDfor the genericExceptionbranch. Update theBulkEditErrorrecord last — after all call sites are migrated — to catch compilation errors.if (saving) return;as the first line ofhandleDiscard. Then write the test — a test that callshandleDiscardwhilesaving === trueand asserts neitherbulkSelectionStore.clear()norgoto()was called.vi.spyOnonbulkSelectionStore.clearto count invocations across the two triggers (route change vs. store mutation). The spy count should be 1 after a route change, and still 1 after a subsequent store mutation on the same non-bulk route.🏗️ Markus Keller — Application Architect
Observations
S3 — onMount data fetch is a confirmed SSR violation. I verified it:
+page.svelteimportsonMountfrom svelte and calls the batch-metadata endpoint client-side (lines 2 and 15–16). This exposes the API route to the browser, bypasses server-side auth cookie forwarding, and causes a load flash. The challenge the issue names — "store is client-side state, so the server load can't read it" — is real. The proposed session-cookie approach is the right call for SvelteKit. Before implementing: persist the selected IDs into a short-lived cookie (e.g.bulk_ids, HTTP-only is not needed here since it's just IDs, not credentials) immediately before navigating to/documents/bulk-edit. The+page.server.tsload function reads the cookie, fetches batch metadata, then clears the cookie. This matches how SvelteKit handles client-state-to-server-load handoff in the standard checkout flow pattern.S8 — The ADR is overdue. The additive-vs-replace semantics are buried in a Javadoc on
DocumentBulkEditDTOlines 11–19. This is an API contract decision — if someone changesapplyBulkEditToDocumentsix months from now and removes the additive behavior for tags, nothing in the codebase will warn them that this was intentional. The next ADR number is006. The Javadoc@seeback-reference is a good touch; make sure the ADR also captures the "why additive for tags" rationale (partially-reviewed documents must not lose tags accumulated over time).C1 — Name mismatch in the issue. The issue says rename
BatchMetadataRequest→BulkEditIdsRequest. Codebase hasBatchMetadataRequest.java(confirmed) andDocumentBatchSummary.java(confirmed). But there is also a separateDocumentBatchMetadataDTO.javawhich is used for the upload-batch flow — do NOT rename that one. The rename target is specificallyBatchMetadataRequest(used for thePOST /api/documents/batch-metadataIDs payload). Confirm the rename scope before starting to avoid breaking the upload path.S1 — N+1 is confirmed.
resolveTagsinDocumentServicecallstagService.findOrCreate(cleanName)per tag name inside a private method that is called per document insideapplyBulkEditToDocument. The controller loops 500 documents. With 10 tags, that is ~5000 tag-lookup queries. The fix — resolve tags once before the loop and pass the resolvedSet<Tag>intoapplyBulkEditToDocument— is correct. Consider also pre-resolving sender and receivers in the same way; all three suffer from the same pattern.Recommendations
document.cookieon the client beforegoto('/documents/bulk-edit'), parse it in+page.server.ts. Keep the cookie lifetime to one navigation (max-age: 60 seconds is sufficient). If the cookie is absent in the server load (user navigated directly), redirect to/documents— same behavior as the current empty-store redirect.applyBulkEditToDocument. The ADR anchors the "why" so future refactors don't accidentally break the contract.BatchMetadataRequestandDocumentBatchSummaryonly. Addgrep -r "BatchMetadataRequest\|DocumentBatchSummary" backend/srcto the done-when check to verify no references remain.Open Decisions
/documents/bulk-edit?ids=uuid1,uuid2,...). With 500 UUIDs × 36 chars = ~18kb URL — too long for browsers (4KB limit). The cookie approach is the right one. No decision needed here; just confirming the issue's instinct is correct.🔒 Nora "NullX" Steiner — Application Security Engineer
Observations
M1 — CWE-209: Information Exposure via BulkEditError.message. The current code in
DocumentController.patchBulkdoes:sanitizeForLogstrips\r\nonly — it is a log-injection defense (CWE-117), not an information-exposure defense. The fullDomainException.getMessage()— which can contain internal document IDs, person names, or DB-level entity details — reaches the API response body and therefore the browser. Switching toErrorCodeenum eliminates this: the enum value carries no internal state, is safe to serialize, and lets the frontend render a localized string viagetErrorMessage().Additionally, the generic
Exceptioncatch branch passes the hardcoded literal"Internal error"— this is fine as-is for exception safety, but once the type changes toErrorCode, this branch must use a real enum value (BULK_EDIT_DOCUMENT_FAILED) rather than a string literal.S2 — No rate limit on PATCH /api/documents/bulk. Confirmed:
WebConfig.addInterceptorsonly registers theRateLimitInterceptorfor/api/auth/invite/**and/api/auth/register.PATCH /api/documents/bulkis completely uncapped. An authenticated user withWRITE_ALLcan loop calls at maximum throughput: each call processes up to 500 documents, each of which performs multiple DB writes (tags, sender, receivers, audit log). Without a cap, this is a potential self-DoS or resource amplification vector. The fix in the issue (addPathPatternsto include/api/documents/bulk) is correct.M3 — Mid-save discard: security consequence is data integrity. This is less a confidentiality concern and more an integrity concern. If
handleDiscardruns mid-save, it clearsbulkSelectionStoreand navigates away, while the in-flight PATCH request continues applying edits. The user sees an empty selection and thinks nothing happened — but the edits were applied to some subset of documents. Theif (saving) return;guard is the right defense.M1 — Test coverage for the security fix. Per the persona's methodology: the fix must start with a failing test. Before changing
BulkEditError, add a@WebMvcTesttest asserting that whenpatchBulkcatches aDomainException, the response body contains a fieldcode(enum value) and NOT a fieldmessage(string). The test proves the old behavior and will go red; the fix makes it green.Recommendations
ErrorCode.BULK_EDIT_DOCUMENT_FAILEDconstant, (2) Change call sites inDocumentController, (3) Change theBulkEditErrorrecord signature last. This keeps compilation green throughout. Do not try to change the record and call sites simultaneously.RateLimitInterceptorTestcovers the 429 case on the new path. The existing test pattern for/api/auth/registershould be copy-extensible in under 10 lines.PATCH /api/documents/bulkendpoint has no per-user-session concurrency guard either — two simultaneous calls from the same session would both apply. This is lower risk (it requires coordination) and I'd park it as a future hardening item rather than blocking this cycle.🧪 Sara Holt — Senior QA Engineer
Observations
M4 — The integration test is the right call; the Mockito suite is structurally blind here.
applyBulkEditToDocumentis annotated@Transactional. The controller calls it in a loop with individual try/catch per document. Mockito cannot exercise the@Transactionalproxy, so the claim "a per-document failure is isolated" is untestable at unit level. The test scenario in the issue is precise and correct. Two implementation notes:@Transactionalon the test method itself for automatic rollback — BUT for this specific test, you need to actually verify DB state after the call. If you use@Transactionalon the test, the transaction spans the test and the save, and rollback happens together — making it impossible to read back the committed state. Use@Sql(executionPhase = AFTER_TEST_METHOD, scripts = "cleanup.sql")or a manual@AfterEachdelete instead of@Transactionalon the test method.PersonServiceIntegrationTestandDocumentSearchPagedIntegrationTestshow the Testcontainers setup pattern (postgres:16-alpine,@DynamicPropertySource). Mirror that exactly — don't introduce a new Postgres config class if one already exists.M5 — ID round-trip assertion is a legitimate regression guard. The
flatMap + Set.sizepattern catches both duplicates and drops. One addition: also assert thatnew Set(allSentIds)contains no IDs that weren't in the original 1100 — i.e., check for IDs appearing in chunks that weren't in the input (though this is less likely with a straightforward slice implementation).M6 — Five test cases are the right coverage. Case (e) —
untrackguarantee — is the highest-value case because it's the one most likely to regress silently. If someone changes the$effectto readbulkSelectionStore.sizeoutsideuntrack, 500checkbox toggleevents will fire 500 clears. The test should trigger 10 rapid store mutations and assertclearwas called 0 times (not 10, not 1).S6 — tagOp=OR is untested. Confirmed: looking at the
DocumentControllerTest, thepatchBulk_*tests cover auth, validation, deduplication, and the happy path, butgetDocumentIdstests only passsenderId.tagOp=ORis a code path branch that is completely unexercised at the controller layer.S9 — Parametrizing the @Size tests is correct. I found
patchBulk_returns400_whenArchiveBoxExceeds255CharsinDocumentControllerTest. The current approach has separate test methods per field. A@ParameterizedTest @MethodSource(rather than@CsvSource, since you need to construct lists of UUIDs forreceiverIds) is cleaner for the list-type fields. Use@CsvSourcefor the string fields, and a separate@ParameterizedTest @MethodSourcefor the list fields.S7 — Snapshot FormData across chunks. Without this, you can't distinguish "bug: sender resets between chunks" from "feature: sender is consistently applied." The test is cheap to write and catches a whole class of regression.
Recommendations
backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceBulkEditIntegrationTest.java. Use@SpringBootTest+ Testcontainers (followPersonServiceIntegrationTestpattern). Do NOT annotate the test method@Transactional— you need to read committed DB state afterpatchBulk. Clean up with@AfterEachrepository deletes.@CsvSourcefor string fields, one@MethodSourcefor list fields (since@CsvSourcecan't construct aList<UUID>of 201 entries inline).vi.spyOn(bulkSelectionStore, 'clear')before the test; assertclearSpy.callCount === 0after 10 store mutations while on a non-bulk route.🎨 Leonie Voss — UX Designer & Accessibility Strategist
Observations
M2 — Focus ring on DocumentRow checkbox is a confirmed WCAG 2.1 AA SC 2.4.7 failure. The
<label data-testid="bulk-select-checkbox">is the 44×44 hit target. From the grep output, thefocus-withinring is not on the label — only the inner<input>receives it. The fix is exactly as specified:focus-within:ring-2 focus-within:ring-brand-mint focus-within:ring-offset-1on the<label>element. Addoutline-noneto the<input>so the browser's native ring doesn't render inside the custom ring area. Do not usefocus-visible:ring-2here —focus-withinon the label is the right selector because the<input>inside receives focus, not the label itself.S4 — Responsive stacking for BulkDocumentEditLayout. The 55/45 panel split at mobile viewport is unusable. I recommend Option A (stack panels) — hide the PDF preview by default on mobile, show only the form, and allow the user to expand the preview with a toggle. This preserves mobile access to the bulk-edit feature (Option B, hiding the CTA entirely, removes it). The stacking order should be: form on top, PDF preview below, with a "Vorschau einblenden" disclosure button. This follows the project's established pattern of "content before chrome" at small viewports. Avoid the horizontal overflow issue by using
overflow-hiddenon the outer container.S5 — /documents/bulk-edit is high a11y-regression risk. It has:
role="note"callouts (the FieldLabelBadge additive/replace indicators)All four are common axe violation sources. The Playwright test should: (1) log in, (2) select 2 documents via checkbox, (3) navigate to
/documents/bulk-edit, (4) runcheckA11y()targetingwcag2aandwcag2aa. Expected zero critical/serious violations.C4 — FieldLabelBadge tooltips are a polish improvement but meaningful for seniors. Our 60+ audience reading "Additiv" or "Ersetzt" badge labels without context will not understand the semantics. A
titleattribute is the minimum viable improvement. A more robust solution would use a<details>/<summary>pattern or an accessible tooltip (withrole="tooltip",aria-describedby), buttitleis acceptable for this cycle given the badge is supplementary UI chrome.Recommendations
focus-within:ring-2 focus-within:ring-brand-mint focus-within:ring-offset-1 rounded-smto the<label>wrapper. Addfocus:outline-noneto the<input>inside. Test by tabbing through a document list — the ring should appear on the full 44×44 label area, not the inner 20×20 checkbox.flex-col md:flex-rowon the outer panel container. On mobile, add a<button class="md:hidden ...">Vorschau {showPreview ? 'ausblenden' : 'einblenden'}</button>toggle. The PDF panel hasclass="hidden md:block"unlessshowPreviewis true. Keep the form always visible.DocumentRow.svelte.spec.tsshould assertlabel[data-testid="bulk-select-checkbox"]has classfocus-within:ring-2(or query computed styles after programmatic focus on the input). The existing checkbox-click workaround helper (C5) should be extracted first to keep the new test clean.Open Decisions
🚀 Tobias Wendt — DevOps & Platform Engineer
Observations
M4 — @SpringBootTest with Testcontainers: standard and correct. The existing integration tests (
PersonServiceIntegrationTest,DocumentSearchPagedIntegrationTest,UserManagementAuditIntegrationTest) all use the same Testcontainers setup withpostgres:16-alpine. Confirm there's a sharedPostgresContainerConfigor static container pattern before writing a new one — avoid spinning up a second Postgres container per test class. If a shared@Container static PostgreSQLContainer<?>already exists in a base class or config, use it.S2 — Rate limiter on /api/documents/bulk: infra impact is negligible. Adding
/api/documents/bulktoWebConfig.addInterceptorsis a one-liner config change. TheRateLimitInterceptoris an in-memory per-IP counter — no external dependency, no new infrastructure. The CI pipeline picks this up automatically on the next backend build. No deployment changes needed.S3 — onMount data fetch: no infra impact. The switch from
onMountclient-fetch to+page.server.tsload is a pure frontend refactor. The session-cookie approach Markus recommended uses the existing SvelteKit cookie API — no server-side session store, no Redis, no new infrastructure. This is correct for a solo-hosted family project where adding infrastructure is the last resort.S8 — ADR in docs/adr/: confirm numbering before writing. Last ADR is
005-thumbnail-aspect-and-page-count.md. Next is006. Write todocs/adr/006-bulk-edit-additive-replace-semantics.md. The WON'T entry at the bottom of the issue mentions cross-referencing issue #225 — verify that issue is still open before adding the reference (a reference to a closed issue in an ADR is confusing but not blocking).CI timing impact of M4: A new
@SpringBootTestintegration test with Testcontainers adds roughly 10–20 seconds to the backend test run (cold Postgres container pull is cached after first run). This keeps total CI well within the 2-minute integration test budget. No concern here.Recommendations
PostgresContainerConfig.javaor a@Container staticfield in a base test class) before writing new setup code. Reuse it.BatchMetadataRequest→BulkEditIdsRequest, regenerate TS types and runnpm run checkinfrontend/before the commit. Type errors from the rename show up there first.No concerns from my angle on observability either — the existing
log.info("bulkEdit actor=... errors=...")inDocumentControlleralready captures per-request error counts. If S2 adds the rate limiter, confirmRateLimitInterceptorlogs the 429 response (or at minimum doesn't swallow it silently) so we can spot abuse in Loki.📋 Elicit — Requirements Engineer
Observations
Issue structure is exemplary. Must → Should → Could ordering with explicit "Done when" conditions on every item is exactly the Definition of Ready pattern that prevents implementation ambiguity. Each item is atomic and independently committable. No concerns on the overall structure.
M3 — "Decision required" framing is correct but the answer is evident from the code. The issue asks "Is mid-save discard intentional?" Looking at the code:
handleSavehas an explicit guard (if (saving) return;),handleDiscarddoes not. That asymmetry in code written in the same feature is the strongest signal that the missing guard is a bug, not a deliberate design choice. The requirement can be tightened: remove the "Decision required" framing entirely and specify "Addif (saving) return;guard — the asymmetry with handleSave is a defect." No ambiguity remains.S3 — "Confirm the approach before implementing" is an open requirement. This is the only item in the issue where the implementation path is genuinely uncertain. The acceptance criterion ("Page renders with document data on first paint;
onMountdata-fetch is removed") is clear, but the mechanism (session cookie vs. store serialization vs. URL param) is unresolved. Markus's analysis above eliminates the URL param option (URL length limit with 500 UUIDs). The cookie approach is recommended. This decision should be locked before S3 is started — it affects+page.server.tsstructure and the store clear timing.S8 ADR references issue #225. The WON'T entry says "Add to issue #225's Out of Scope register — verify #225 is still open before acting." This is a cross-issue dependency that should be tracked. If #225 is closed, the ADR absorbs the rollback/versioning note directly. Recommend: check issue #225 state before writing ADR-006, and record the outcome explicitly in the ADR's "Alternatives considered" section.
Missing acceptance criterion on M1 (frontend rendering). The "Done when" for M1 says "the frontend renders a localized string when a per-document failure occurs." This implies a change to
getErrorMessage()inerrors.tsand Paraglide translation keys — but the issue does not specify what the localized string should say. ForBULK_EDIT_DOCUMENT_FAILED, a reasonable default is"Dieses Dokument konnte nicht bearbeitet werden."/"This document could not be edited."/"Este documento no pudo editarse."Pin these strings in the issue or in the translation files directly; don't leave it to implementation judgment.Recommendations
if (saving) return;at top ofhandleDiscard(parity withhandleSaveguard at line 284)."bulk_idsis written before navigation and cleared by the server load after reading." This makes the acceptance criterion testable."Dieses Dokument konnte nicht bearbeitet werden."forde.json.Open Decisions
bulk_idscookie beHttpOnly? It contains only UUIDs (no credentials), so JavaScript read access is harmless. Recommend: notHttpOnly, since the store also holds the IDs client-side anyway. But document this choice explicitly in the cookie write so a future security auditor doesn't flag it.🗳️ Decision Queue — Action Required
2 decisions need your input before implementation starts.
Frontend / UX
Security / Architecture
bulk_ids: The session cookie holding selected UUIDs before navigating to/documents/bulk-editcontains no credentials, only document IDs. Should it beHttpOnly: false(allows the store to read it back client-side, simpler) orHttpOnly: true(slightly more defensive, but client JS cannot read it — the store already holds the IDs anyway so this is redundant). Recommendation:HttpOnly: false, but add a comment in the code so a future auditor doesn't flag it. Confirm before implementing S3. (Raised by: Elicit)S4: A
S3: Is the cookie needed anyway? If it's needed, we make HttpOnly false, if its not needed, remove it. Please verify beforehand.