fix(reader-dashboard): 500 crash for READ_ALL users — recentDocs always undefined #661
Reference in New Issue
Block a user
Delete Branch "worktree-fix+reader-dashboard-doc-undefined"
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
READ_ALL-only users hit a 500 on the homepage. The crash was:
Root cause
+page.server.tsmapped the search result as:But
/api/documents/searchreturnsDocumentSearchResultwhoseitemsare flatDocumentListItem[]. Soi.documentwas alwaysundefined, producing[undefined, undefined, ...]instead of an empty array.Fix
+page.server.ts(itemsmapped directly, not via.document)createdAt/updatedAttoDocumentListItem(backend record +toListItem()factory) — needed byReaderRecentDocsfor relative-time display and the "new" badgeReaderRecentDocs.svelteto acceptDocumentListItem[]instead ofDocument[]api.ts(generated types) to include the two new timestamp fieldsTest
Added a regression test to
page.server.spec.tsthat was red before the fix and green after:Backend tests (
DocumentControllerTest,DocumentSearchResultTest) updated for the new record arity — all 105 pass.The server mapped DocumentSearchResult items as { document: Document }[] but the API returns flat DocumentListItem[] — so i.document was always undefined, crashing the reader homepage with a 500. Fix the type + mapping in +page.server.ts, add createdAt/updatedAt to DocumentListItem (needed by ReaderRecentDocs for relative-time display), and update the component to accept DocumentListItem instead of Document. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ✅ Approved
Good fix. The root cause was a classic shape mismatch — assuming
{ document: Document }[]when the wire format was a flatDocumentListItem[]. The regression test proves it was red first, and the change is surgical.Blockers
None.
Suggestions
1. Missing key expression on
{#each}inReaderRecentDocs.svelteThe diff doesn't touch the
{#each}loop, but given we're now receivingDocumentListItem[](which always has anid), I'd verify the existing loop is keyed:If it isn't, reordering silently corrupts local component state. Worth a quick check — not a blocker since we're not changing the loop behaviour here, but flag it if it's missing.
2.
isNew()logic is fragile — equal timestamps means "new"?createdAt === updatedAtis a weak signal: it breaks if the backend ever touchesupdatedAton save (e.g. a migration backfill, or any eager@UpdateTimestamptrigger on record creation). The intent is "never edited" — consider a dedicatedisNew: booleanfield onDocumentListItemwhen the requirement is formalised. For now it works, and it's better than a crash.3. Test mock count is order-dependent
The regression test mocks 5
.GET()calls in a specific order. If the load function ever reorders its parallel fetches, the mock sequence breaks silently (no error — just wrong data). The existing suite has this pattern throughout, so this is consistent with the codebase convention, but it's worth noting.4. Test data factory is missing for
searchItemThe new test builds
searchIteminline with 8 fields. The existing suite uses amakeReaderStats()/makeDocument()pattern (from other tests in the file). Consider extracting amakeSearchItem(overrides?)factory to keep things consistent — minor, but it's the codebase's own standard.Overall: clean fix, good TDD discipline, no regressions introduced. The type narrowing from
Document→DocumentListItemthroughoutpage.server.ts, the component, andapi.tsis exactly right.🏛️ Markus Keller — Senior Application Architect
Verdict: ✅ Approved
This is a pure correctness fix with a well-scoped contract change: adding
createdAt/updatedAttoDocumentListItemso the frontend can render relative timestamps without reaching for the heavierDocumententity. The layering is respected throughout —DocumentService.toListItem()owns the mapping, nothing crosses domain boundaries, and the OpenAPI type generation is kept in sync. Boring, correct, good.Blockers
None.
Suggestions
1. No Flyway migration — verify this is intentional
DocumentListItemis a Javarecordprojected fromDocumententity fields. The newcreatedAt/updatedAtfields map todoc.getCreatedAt()/doc.getUpdatedAt(), which are existing@CreationTimestamp/@UpdateTimestampcolumns on thedocumentstable. No schema change. This is correct — no migration needed. Just worth confirming the reviewer is aware this is a projection, not a new column.2. Documentation compliance check
Per the team's doc-update table:
document/domaindocs/architecture/c4/l3-backend-*.puml— but this is a record field addition, not a new service/controller. The domain's C4 diagram does not change. ✅ No update neededDocumentListItemrecord shape changeAll clean. The checklist is satisfied.
3.
isNew()heuristic — document the assumption somewhereThe
createdAt === updatedAtheuristic for "never edited" is an implicit assumption about how@UpdateTimestampbehaves on first save. Spring/Hibernate sets both to the same value at insert time only if the entity has never been dirty-checked after creation. This holds for the current implementation but is fragile if batch import or migration scripts touchupdatedAtindependently. If this logic stays, a brief inline comment naming the assumption is worthwhile — not a comment explaining what the code does, but why this heuristic is valid (and when it would break).4. No architectural decision required
Adding
createdAt/updatedAtto a search result DTO is evolutionary, not architectural. No ADR needed. The decision to surface timestamps at the list level rather than forcing a per-document detail fetch is pragmatically correct — it avoids N+1 round trips.🔒 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
No security concerns here. This PR fixes a data-mapping bug and extends a projection DTO. I've checked the relevant OWASP categories and found no issues.
Blockers
None.
Findings (informational — no action required)
1. No authorization surface change
The changed endpoint is
GET /api/documents/search, which is already authenticated (requiresREAD_ALL). AddingcreatedAt/updatedAtto the response payload does not expand the authorization surface — these are audit timestamps, not sensitive fields. AREAD_ALLuser was already able to retrieve fullDocumententities with these timestamps; surfacing them in the list projection is not a privilege escalation.2. No new input paths introduced
The fix is purely output-side: new fields on a response record. No new query parameters, path variables, or request body fields are introduced. No injection surface is expanded.
3.
nulltimestamps in test fixtures are acceptable, not a concernDocumentControllerTestandDocumentSearchResultTestnow passnull, nullforcreatedAt/updatedAt. SinceDocumentListItemis a Javarecordwith no@Schema(requiredMode = REQUIRED)enforcement on the record level itself, this is valid for unit tests. However, in production,doc.getCreatedAt()anddoc.getUpdatedAt()are populated by@CreationTimestamp/@UpdateTimestampand will never benullfor real persisted entities. The testnulls are not a security or reliability issue.4. Frontend null-safety for timestamps
In
ReaderRecentDocs.svelte, theisNew()function callsnew Date(doc.createdAt)without a null guard. Since the generatedapi.tsmarkscreatedAtandupdatedAtas required (createdAt: string), TypeScript will not compile ifnullis passed. This is correctly enforced by the@Schema(requiredMode = REQUIRED)annotation on the record fields. No runtime null-deref risk under normal conditions.5. Information disclosure — timestamps in list response
Document creation/modification timestamps are low-sensitivity metadata. They do not reveal file contents, sender identity beyond what is already in the list, or user credentials. No concern here.
Clean from a security perspective. Good to merge.
🧪 Sara Holt — Senior QA Engineer
Verdict: ⚠️ Approved with concerns
The regression test is present and clearly named. The fix is covered. But there are gaps in the test pyramid worth flagging before they bite us later.
Blockers
None — the critical path is covered.
Concerns
1. Backend test fixtures pass
nullforcreatedAt/updatedAt— production values are never nullIn
DocumentControllerTestandDocumentSearchResultTest, the updated record constructors now passnull, nullfor the two new timestamp fields:This is syntactically fine but creates a divergence: in production,
DocumentService.toListItem()passes realLocalDateTimevalues. The tests verify the controller's serialization ofDocumentListItem, but with timestamps that will never appear in real data. If the JSON serialization ofLocalDateTimeever becomes relevant (e.g. format, timezone), this test won't catch it.Suggestion: Use a fixed
LocalDateTimein the test fixture (e.g.LocalDateTime.of(2024, 1, 15, 12, 0)) rather thannull. This is a suggestion, not a blocker — the controller test is primarily checking HTTP status and response shape, not timestamp formatting.2. No test for the
isNew()badge logicThe new
isNew(doc)function inReaderRecentDocs.sveltedetermines whether a "new" badge is shown. There is no unit test for this logic — specifically:createdAt === updatedAt→ badge showncreatedAt !== updatedAt→ badge not showncreatedAtorupdatedAtis an unexpected format?Given this is a user-visible feature and a recent addition, it should have a component test or at minimum a Vitest unit test for the
isNewfunction. This is a suggestion (the badge is non-critical UX), but it's a gap.3. Regression test mock order is fragile
The new test in
page.server.spec.tschains 5mockResolvedValueOncecalls in a specific order to match the load function's parallelPromise.allSettledcalls. If the fetch order in the load function ever changes (and parallel fetches are refactored), this test will produce wrong results silently — it won't throw, it'll just give the wrong mock to the wrong call.The existing test suite already uses this pattern, so this is a systemic issue not introduced here. But now is a good time to note it: a named mock or URL-matching approach would be more resilient.
4. No
createdAt/updatedAtpresent in thesearchItemfixture of the regression testThe new
api.tsmarkscreatedAtandupdatedAtas required onDocumentListItem. The test object doesn't include them. TypeScript in the test file may not catch this because the mock bypasses the type system at theReturnValuelevel. This meansresult.recentDocs[0].createdAtwould beundefinedat runtime in the test, which is the exact bug this PR is fixing in production. The test assertsresult.recentDocs[0].id === 'd1'(correct) but doesn't assert oncreatedAt, so the gap isn't caught.Suggestion: Add
createdAtandupdatedAttosearchItemin the test fixture to match the real API shape, and add assertions for them.The critical regression (500 crash on homepage for
READ_ALLusers) is correctly tested and would be caught by the new test. The gaps above are future-risk items, not immediate blockers.🎨 Leonie Voss — UI/UX Design Lead & Accessibility Strategist
Verdict: ⚠️ Approved with concerns
The crash fix is the right thing to do and the
DocumentListItemmigration is correct. I'm not blocking this — READ_ALL users getting a 500 is worse than any of the concerns below. But I want to call out two things that affect real users.Blockers
None — but concern #1 is close to a blocker for the senior audience.
Concerns
1.
isNew()uses timestamp equality as a proxy for "never edited" — this is invisible to usersThe badge is a user-visible signal. The
createdAt === updatedAtheuristic is fragile in ways the user will experience as random badge disappearance — for example:updatedAtset to a slightly different millisecond thancreatedAt, causing the badge to never appear even for freshly imported items@UpdateTimestampseparately from@CreationTimestampwould break this silentlyThis function is now surfaced to users for the first time (previously
Document[]didn't even reach the component correctly). Before this becomes load-bearing UI, the "new" criterion should be explicitly defined in a Gitea issue — what does "new" mean to a reader? "Uploaded in the last 7 days"? "Never opened by this user"? Right now it means "backend timestamps coincidentally equal", which is an implementation detail dressed as a feature.Priority: High — affects the user-facing "new" badge logic, and the audience includes seniors who rely on clear, reliable cues.
2. The diff touches
ReaderRecentDocs.sveltebut the template is not shown — I cannot verify the renderingThe diff only shows the
<script>block changes (type swap,isNewsignature). The template portion ({#each}, the card markup, the "new" badge rendering, touch targets, text size) is not changed and therefore not visible in this diff. I'm trusting that the existing template was reviewed previously and meets:font-seriffor document titlestext-xssection label withuppercase tracking-widestaria-labelor visible text on any badge elements (the "new" badge must not be icon-only)If the "new" badge is rendered as a colored dot without text or
aria-label, that fails WCAG 1.4.1 (use of color) and is invisible to screen readers. Please confirm the badge has both a visible label and anaria-label— or link to where this was previously reviewed.3. No visible change to the reader dashboard layout — good
This PR does not introduce new visual elements beyond what existed. The transition from
Document[]toDocumentListItem[]is transparent to the rendered output as long as the fields used in the template (id,title,originalFilename, etc.) are present onDocumentListItem. They are. No layout, spacing, or typography concerns introduced by this diff.Fix the crash, merge this. But open a follow-up issue to define what "new" means to a reader before the badge logic calcifies further.
🖥️ Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
This PR has zero infrastructure footprint. No Compose changes, no CI changes, no new services, no new environment variables. From a platform perspective this is a straightforward application fix.
Blockers
None.
Observations
1. No CI impact
The changed files are:
backend/src/main/java/...— compiled by Maven, tested by existing JUnit suitebackend/src/test/java/...— added test cases, no new test infrastructure requiredfrontend/src/...— built by Vite/SvelteKit, tested by existing Vitest suitefrontend/src/lib/generated/api.ts— generated file, committed as part of the workflowAll of these are already handled by the CI pipeline. No pipeline changes needed.
2.
api.tsis a committed generated file — regeneration workflow is respectedThe
frontend/src/lib/generated/api.tsdiff shows the two new timestamp fields added correctly. Per the project's workflow,npm run generate:apimust be run against a running backend before committing. The diff matches whatopenapi-typescriptwould produce for aLocalDateTimefield with@Schema(requiredMode = REQUIRED):createdAt: stringwith/** Format: date-time */JSDoc. This was done correctly.3. No new dependencies or Docker image changes
java.time.LocalDateTimeis part of the JDK standard library. Nopom.xmlorpackage.jsonchanges. Renovate has nothing new to track here.4. Backward compatibility on the wire
Adding fields to a JSON response is backward-compatible for any consumer that ignores unknown fields (which is the standard for JSON clients). No versioning concern. The generated
api.tsis the only consumer, and it's been updated in the same commit.Nothing to see here from an infrastructure and deployment perspective. Clean.
📋 Elicit — Requirements Engineer & Business Analyst
Verdict: ⚠️ Approved with concerns
The fix is correct and necessary. The crash for READ_ALL-only users is a Critical defect by any severity scale — it blocks the primary user journey for an entire user class. Merging is the right call. My concerns are about requirements debt that this fix surfaces, not about the fix itself.
Blockers
None — the defect is fixed.
Requirements Concerns
1. The "new" badge has no specified acceptance criteria
The
isNew()function implements a user-facing feature — a badge indicating a document is new — with no traceable requirement behind it. From a requirements perspective this is a hidden assumption masquerading as a feature:The current implementation is (b), encoded as a timestamp equality check. This has never been accepted by a stakeholder as the definition of "new." If the badge is shown to real users, they will form an expectation about what it means — and that expectation will be wrong the first time the heuristic breaks (see Felix and Leonie's comments on the fragility).
Recommendation: Open a Gitea issue to define FR-READER-DASHBOARD-NEW-BADGE with a proper user story and Given-When-Then acceptance criteria before the feature ships to real readers. Until that issue exists and is accepted, consider whether the badge should be rendered at all.
2. The PR description names READ_ALL users as the affected class — but the reader dashboard requirement is implicit
The PR body is excellent at describing the technical bug. What it doesn't capture is the user requirement that was silently broken: "As a READ_ALL user, I want to see my recently uploaded documents on the homepage, so that I can quickly continue reviewing them." This requirement was never written down — it's inferred from the fact that a
ReaderRecentDocscomponent exists.This is requirements debt, not a blocker. But it means there is no acceptance criterion to verify the fix against beyond "it doesn't crash anymore." The regression test correctly covers the mapping fix. What's not tested is whether the list shows the right documents in the right order for a real user — because that acceptance criterion was never written.
3. The
DocumentListItemtype expansion is a contract change — is it documented?Adding
createdAtandupdatedAttoDocumentListItemchanges the public API contract ofGET /api/documents/search. From a requirements standpoint, this should be noted in the API changelog or release notes so any future integrators (e.g. a potential mobile client) are aware. For the current single-consumer setup (SvelteKit frontend) this is low risk, but the habit of documenting API contract changes is worth establishing.4. No non-functional requirement captured for dashboard load performance
The reader dashboard now fetches 5 parallel API calls on page load (
Promise.allSettled). There is no NFR specifying acceptable load time for the reader dashboard for READ_ALL users. This is pre-existing, not introduced by this PR — but this fix is the first time READ_ALL users actually see a populated dashboard, so it's the right moment to ask: what is the acceptable page load time for readers on a typical connection?Suggested NFR (draft, for refinement):
NFR-PERF-READER-001: The reader dashboard shall render initial document list content within 2 seconds on a 20 Mbps connection for 95% of page loads.Approve the fix. Open a follow-up issue for the "new" badge requirement. The technical implementation is sound; the requirements foundation for the badge feature is missing.
👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ⚠️ Approved with concerns
The core bug fix is clean and correct — the type alias swap in
+page.server.tsis exactly the minimal change needed. TheisNew()heuristic update to a 7-day recency window is a clear improvement over the brittle equality check. Backend record expansion is tidy.Blockers
1.
api.tsis a generated file edited by handfrontend/src/lib/generated/api.tsis produced bynpm run generate:api. This PR manually addscreatedAt/updatedAttoDocumentListIteminstead of regenerating. That works today but creates drift the next time someone runsgenerate:api— the manual edit will be overwritten.The fix is to run
npm run generate:api(requires the backend running with--spring.profiles.active=dev) and commit the regenerated file. If that's not feasible in the worktree context, a comment in the PR description explaining why it was done manually would at least document the risk.Suggestions
2.
ReaderRecentDocs.svelte.spec.tsstill importsDocument, notDocumentListItemThe component now declares
documents: DocumentListItem[], but the spec file builds test fixtures typed asDocument. At runtime this works due to structural compatibility, but it's the wrong type and could silently mask regressions. Should be:3.
isNew()callsDate.now()directly — boundary cases are untestableThis is fine for production use. The concern is that the boundary (exactly 7 days) cannot be tested without actual time travel. Not a blocker, but worth knowing when writing edge-case tests. A future refactor could accept a
nowparameter with a default ofDate.now()if exact boundary tests become necessary.🧪 Sara Holt — Senior QA Engineer
Verdict: ⚠️ Approved with concerns
Good regression coverage for the core bug. The server-side test in
page.server.spec.tsclearly documents the failure mode and proves the fix. Timestamp propagation is now explicitly asserted. The backend test fixture cleanup (replacingnull, nullwith a fixedLocalDateTime) is correct and consistent.Blockers
1.
ReaderRecentDocs.svelte.spec.tsuses the wrong type for test fixturesThe component's prop type is
DocumentListItem[], but the spec file builds fixtures astype Document. This creates a type contract gap: ifDocumentListItemgains a required field in future, the spec's fixtures won't fail at compile time.Fix: migrate
baseDocandupdatedDoctoDocumentListItemshape (removestatus,metadataComplete,scriptType; addcompletionPercentage,receivers,tags,contributors,matchData).Suggestions
2. No boundary test for
isNew()at exactly 7 daysThe test suite covers "2 days ago → shows badge" and "40 days ago → hides badge". The boundary — a document created exactly 7 days ago — isn't tested. The off-by-one direction matters: is the threshold
>= 7 daysor> 7 days?Current implementation:
createdAt.getTime() > Date.now() - 7 * 24 * 60 * 60 * 1000— so exactly 7 days old is NOT new (strict>). A test pinning this behavior would prevent a silent regression if the threshold or operator changes.3.
Date.now()in browser test fixtures — timezone noteTests in
.test.tsand.spec.tsnow compute relative dates withnew Date(Date.now() - N * 86400000).toISOString(). This is deterministic and correct. Just confirming: in CI, these dates will always be within the 7-day window by construction (2 days, 6 days), so no timezone-related flakiness expected. ✅4. Missing error-path coverage for reader search failure
page.server.spec.tscovers the happy path (search returns items) and thetopPersonsfailure path, but not the case where the search API itself fails for a reader.recentDocsshould default to[]in that case. Thesettled()helper handles this, but it would be good to have explicit test coverage given that this is the exact path that caused the original 500.🏛️ Markus Keller — Senior Application Architect
Verdict: ⚠️ Approved with concerns
The fix is architecturally correct.
DocumentListItemis the right DTO for list endpoints — it's distinct from theDocumententity, it's flat, and it now carries all fields the frontend needs. The layering is clean:DocumentService.toListItem()is the only place that constructs this DTO, and the controller test verifies the JSON contract.Blockers
1.
api.tswas hand-edited instead of regeneratedfrontend/src/lib/generated/api.tsis produced byopenapi-typescriptfrom the live backend spec. Manually adding fields to it creates a gap between the actual OpenAPI spec the backend emits and the TypeScript types the frontend sees. The next time someone runsnpm run generate:api, this change is silently overwritten.The proper fix: run
npm run generate:apiafter the backend changes are committed, and include the regenerated file in this PR. If the tooling can't run in the current environment, document the gap explicitly in the PR body. This is a blocker because it means the TypeScript contract and the actual API contract are currently diverged.Suggestions
2.
LocalDateTimeis timezone-naive — worth a noteDocumentListItemnow carriesLocalDateTime createdAtandupdatedAt.LocalDateTimehas no timezone offset. When Jackson serializes it to JSON it produces2026-01-15T10:00:00(no Z), which the frontend'snew Date(doc.createdAt)will parse as local time, not UTC.The existing
Documententity already usesLocalDateTimevia@CreationTimestamp/@UpdateTimestamp, so this is consistent with the existing pattern. As long as the server's JVM timezone is stable (UTC is the Docker default), there is no practical bug here. But it's worth knowing: if the server timezone ever diverges from UTC, theisNew()window calculation in the frontend will be off.If consistency with the rest of the codebase is the priority,
LocalDateTimeis fine. If precision is the priority,InstantorOffsetDateTimewith a Jackson@JsonSerializeto ISO-8601 with offset would be more robust.No documentation updates needed — no new packages, no schema changes, no new routes. ✅
🔒 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
Reviewed the full diff with an adversarial eye. No security concerns in this PR.
What I checked
Data exposure:
DocumentListItemnow includescreatedAtandupdatedAt. These are metadata timestamps, not sensitive data. No passwords, sessions, file paths, or internal IDs that shouldn't be public are being added. ✅Input trust boundaries:
isNew()usesdoc.createdAtwhich comes from the server-side API response (not user input). The 7-day comparison usesDate.now()on the client — this is UI logic with no server-side consequence. ✅Authorization: No new endpoints, no permission changes, no
@RequirePermissionmodifications. The fix is purely in the data mapping layer. ✅XSS surface:
ReaderRecentDocs.svelterendersdoc.titleanddoc.sender.displayNamein template expressions ({doc.title},{doc.sender.displayName ?? doc.sender.lastName}). Svelte's template compiler HTML-escapes these by default — no raw{@html}usage. ✅Type safety: The manual
api.tsedit (noted by Markus) is a drift concern, not a security concern. The fields added are typed asstring, matching what the backend serializes. ✅One observation (informational): The frontend uses
new Date(doc.createdAt)to parse timestamps from the API. If the backend ever sends a malformed timestamp,new Date(malformed)returnsInvalid DatewithgetTime()returningNaN.NaN > thresholdisfalse, soisNew()would silently returnfalse. This is acceptable graceful degradation — the badge just doesn't show. Not a vulnerability.🎨 Leonie Voss — Senior UX Designer & Accessibility Strategist
Verdict: ⚠️ Approved with concerns
The fix resolves a crash that prevented READ_ALL users from using the homepage at all — that's critical. The "Neu" badge has visible text, correct semantic classes (
bg-accent-bg,rounded-full,text-ink), and the row links maintainmin-h-[44px]touch targets. Accessibility-wise, the component is in good shape.Concerns
1. "Neu" badge semantics: 7 days regardless of modifications may mislead
The old logic showed "Neu" when
createdAt === updatedAt(never edited = truly new). The new logic shows "Neu" whencreatedAtis within 7 days, regardless ofupdatedAt.This means a document created 2 days ago but edited 15 times still says "Neu". For the reader persona (younger users on phones checking what's new in the archive), this may not matter — they care about "was this added recently?" not "has it ever been touched?". But for a family archivist who adds a document and immediately starts annotating it, the badge would show on a document they've already interacted with heavily.
Consider whether the intent is:
createdAtis correct ✅If the 7-day window is the intended semantic, the badge label "Neu" accurately reflects "recently added." I'd accept this. Just confirming the intent is deliberate.
2. Badge shown in the document row but relative date shown via
relativeTimeDe(updatedAt)The row shows the "Neu" badge based on
createdAtbut the right-side timestamp usesupdatedAt. A document created 6 days ago that was updated yesterday would show both "Neu" and "vor 1 Tag". These two signals are coherent (recently added, also recently touched) — but a document created 6 days ago with no updates would show "Neu" and "vor 6 Tagen". Slightly mixed signals, but not a blocker.Looks good ✅
min-h-[44px]on both the "Alle Dokumente" link and each document row link ✅focus-visible:ring-2 focus-visible:ring-brand-navy focus-visible:outline-none✅doc.sender.displayName ?? doc.sender.lastNamethen—em-dash ✅{#each documents as doc (doc.id)}for stable DOM reconciliation ✅🛠️ Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
No infrastructure, CI, or Docker Compose changes in this PR. The fix is entirely application-layer. LGTM from a platform perspective.
What I checked
CI pipeline impact: The PR adds tests to existing test files and modifies one source file per stack. Existing CI jobs (backend Maven build, frontend Vitest server + client, lint) will pick these up automatically. No new test targets, no new build steps needed. ✅
No new dependencies:
package-lock.jsonshows minor changes (10 ++++-----), likely lockfile normalization, not new packages. ✅No new environment variables or secrets: The feature uses timestamps already stored in the database. No new configuration needed. ✅
One note (shared with Markus)
The manually edited
api.tsis a generated file. Our CI doesn't currently validate that the checked-inapi.tsmatches whatnpm run generate:apiwould produce. If we wanted to enforce this, we could add a CI step:This would catch future drift. Not a blocker for this PR — the manual edit is intentional and documented — but worth tracking as a CI hygiene item.
📋 Elicit — Requirements Engineer & Business Analyst
Verdict: ✅ Approved
The root cause is clearly documented in the PR description. The fix is tightly scoped. No new user-visible features are introduced — the homepage now works as originally intended for READ_ALL users.
Observations
1. No linked Gitea issue
This PR doesn't reference a Gitea issue in its title or body (
Fixes #NNN). For a production bug that caused a 500 for a whole user segment, an issue would:Closes #NNNauto-close on mergeMinor in the context of a solo project, but worth tracking.
2. The "Neu" badge redefinition is a requirements change, not just a bug fix
The original implementation defined "new" as
createdAt === updatedAt(a document that has never been modified). The PR changes this to "created within the last 7 days."These are materially different requirements:
The new definition is more useful for the reader persona ("what's been added recently?") and less brittle technically. But this change in definition isn't documented in the PR body. A one-sentence addition like "Changed 'Neu' badge semantics: now means 'added within the last 7 days' instead of 'never modified'" would make the intent explicit for future readers of the git log.
3. The 7-day threshold is an assumption, not a requirement
Why 7 days? It's a reasonable convention (one week), but it's not derived from a user story or explicit requirement. If the family archive is updated infrequently (e.g., new documents added monthly), 7 days may be too short — a reader who visits weekly might miss "new" items. A threshold configurable via a future feature, or derived from visit frequency, might serve the readers better. For now, 7 days is a sensible default; just noting this is an assumption worth revisiting if users report missing the badge.
The test raced a real 150 ms setTimeout: fill('Walter') started the debounce, then focus + keyboard(Escape) had to complete before 150 ms elapsed. Under CI load the Playwright CDP round-trips exceeded 150 ms, letting the debounce fire first. Fix: install vi.useFakeTimers() after the stable-state setup (so vi.waitFor()'s real-timer polling still works), freeze the Walter debounce, let Escape trigger onExit/cancel, then advance fake time with vi.advanceTimersByTimeAsync() — no real-wall-clock race. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>