feat(dashboard): Classic Split — remove notification widget, restructure into 2-col layout #172
Reference in New Issue
Block a user
Delete Branch "feat/issue-171-dashboard-classic-split"
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?
Closes #171
Summary
/api/notificationsfetch with/api/statsin the server load — dashboard no longer owns notification datalg:grid-cols-[1fr_300px]Classic Split: recent docs on the left, DropZone + metadata queue on the rightshowRightColumnguard so read-only users with a complete archive never see an empty 300 px ghost columnDashboardRecentDocumentswith failure isolationmin-h-[44px]to doc rows for WCAG 2.5.5 touch target complianceDashboardMentionsfrom the page (file kept for future notifications page)Test plan
npm run lintpassesnpm run checkpasses (no new type errors in source files)🤖 Generated with Claude Code
Six categories of breakage: 1. date.ts — add formatGermanDateInput(raw: string): string as a pure function covering both digit-stream auto-dot and manual-dot-with-padding modes. Refactor handleGermanDateInput to delegate to it. Fixes 16 failures in date.spec.ts where the function was imported but didn't exist. 2. Admin layout specs (groups/tags/users) — $effect fires on initial mount with manualCollapse=false, so the spy captured 'false' before the click's effect ran. Fix: move spy setup after render(), add await setTimeout(0) to flush Svelte effects before asserting. 3. DashboardMentions — component now renders a persistent "Benachrichtigungsverlauf ansehen" link, making getByRole('link') strict- mode violations. Fix: scope link queries to the actor name, and check absence of the actor link (not all links) in the no-documentId test. 4. Conversations page — empty-state copy changed from "Wählen Sie zwei Personen aus" to "Korrespondenz durchsuchen". Update the test. 5. Login page — AuthHeader adds a second aria-label="Familienarchiv" link. Use .first() to avoid strict-mode violation. 6. Persons page — alias is rendered with German quotation marks „…" not straight quotes "…". Update the test string. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ⚠️ Approved with concerns
Blockers
None.
Suggestions
1.
formatGermanDateInputbundled into a dashboard PRfrontend/src/lib/utils/date.tsgrows a substantial new function that handles date input formatting with dot-aware overflow logic. This is unrelated to the Classic Split. Atomic commit principle says one logical change per commit — the date utility extraction should have been its own commit (or PR). Not a blocker since the function is correct and tested, but worth noting for future PRs.2. The
formatGermanDateInputcomment explains what, not whyThe "two modes" explanation is documenting what the function does — which is fine when the logic is truly non-obvious. But the
dayOverflowed/monthOverflowedflag names already communicate the logic. I'd keep the comment brief: why are two modes needed (i.e. browsers fireinputevents differently when the user types vs. pastes), not a re-narration of the branches. Minor nit.3.
await new Promise((r) => setTimeout(r, 0))in three layout spec filesThis is a timing hack. It works today, but it's a smell — if the tick count changes, the test silently goes green for the wrong reason. Prefer
await vi.waitFor(() => expect(setSpy).toHaveBeenCalled())so the test polls until the assertion passes or times out with a meaningful message.4.
$derivedusage forshowRightColumn— ✓ correctExactly right — computed value via
$derived, not$state+$effect. Clean.5.
{#each recentDocs as doc (doc.id)}— ✓ keyedNoted and approved.
6. Stats footnote edge case for
totalDocuments === 0— ✓ testedThe
#if stats?.totalDocuments != nullguard correctly allows0through, and there is a test for it. Good.Overall the implementation is clean, the tests are well-structured, and the
$derivedusage is idiomatic Svelte 5. The bundling concern and the timing hack in the layout specs are the only things I'd ask to fix before the next PR in this area.🏛️ Markus Keller — Application Architect
Verdict: ✅ Approved
What I checked
Data flow — load function stays clean
+page.server.tscontinues to own all API calls and pass data down via props. No component is fetching its own data. The swap from/api/notificationsto/api/statsis a clean, surgical change in exactly the right place.Promise.allSettledfailure isolation — ✓Failure of any one widget API does not crash the page.
statsdefaults tonull,incompleteDocsdefaults to[]. This is the correct pattern for dashboard widgets.showRightColumnguard — ✓ right layerThis is display logic computed from server-supplied props — it belongs here, not in the load function. The load function stays declarative, the component decides how to render. Good boundary.
CSS Grid Classic Split —
lg:grid-cols-[1fr_300px]The use of a custom grid track value (
[1fr_300px]) instead of a named Tailwind column preset is fine. TheshowRightColumnconditional correctly avoids emitting the 2-column class entirely rather than hiding a 300px ghost column. Cleaner thanvisibility: hiddenorw-0tricks.flex-1 min-h-0on the NeedsMetadata wrapperThe comment explaining
min-h-0overriding the flex defaultmin-height: autois the rare case where a comment is justified — this is a known browser gotcha that trips up developers regularly.Observation (not a blocker)
The
+page.sveltenow handles two substantially different layouts (dashboard vs. search results) in a single file. As the Classic Split grows in complexity, it may be worth considering a route group split (/(dashboard)and/(search)) to isolate concerns. Not needed now, but worth flagging before the next layout change adds another branch.🧪 Sara Holt — QA Engineer & Test Strategist
Verdict: ⚠️ Approved with concerns
What's well covered
result.statsequals the mocked{ totalDocuments: 42, totalPersons: 7 }✓result.statsisnullwhen/api/statsrejects ✓0— three distinct cases tested ✓min-h-[44px]ondoc-row-{id}verified viadata-testid✓canWrite=false+empty,canWrite=true,incompleteDocs non-empty✓DashboardMentionsspec hardened — ambiguousgetByRole('link')replaced withgetByRole('link', { name: 'Anna Schmidt' })— this is a direct quality improvement to existing tests ✓Blockers
None.
Concerns
1.
await new Promise((r) => setTimeout(r, 0))in three layout specsfrontend/src/routes/admin/groups/layout.svelte.spec.ts:119frontend/src/routes/admin/tags/layout.svelte.spec.ts:97frontend/src/routes/admin/users/layout.svelte.spec.ts:140Raw
setTimeout(0)is a timing hack that passes today and may silently fail or flake when the reactive update cycle changes. The correct pattern is:vi.waitForretries the assertion up to the configured timeout and fails with a meaningful message. I'd make this change before merging to prevent a future flake.2. No E2E test for the Classic Split dashboard layout
The test plan lists unit tests only. Given that this PR restructures the primary dashboard layout (2-column grid, right column guard), a Playwright smoke test that visits
/as a read-only user (right column absent) and as a write user (right column present + DropZone visible) would be the appropriate E2E coverage. This is the kind of layout regression that unit tests won't catch.Suggestion — add to the existing Playwright suite:
Not a blocker for this PR, but track it as a follow-up ticket.
🔒 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
Attack surface review
New API call:
GET /api/statsThe load function now fetches
/api/statsvia the typed API client:createApiClient(fetch)in SvelteKit's server context forwards the session cookie automatically, so this call is authenticated. The response (totalDocuments,totalPersons) is aggregate count data — no PII, no document content exposed to the UI. ✓Stats data rendered in the DOM
totalDocumentsandtotalPersonsare numbers. Svelte auto-escapes template expressions. No XSS vector here. ✓showRightColumn— not a security boundaryThis is a display-layer guard only — it controls whether the DropZone and NeedsMetadata widgets render on screen. The actual write-permission enforcement lives on the server (Spring Security
@RequirePermission). A user manually enabling the hidden column via DevTools would find that any upload attempts are rejected at the API layer. This is the correct design. ✓data-testid="submit-password"on the password form buttonThis attribute is for Playwright test targeting only. It has no security implication. ✓
No findings.
This PR does not introduce any injection vectors, authentication changes, authorization bypasses, information disclosure beyond aggregate counts, or new third-party dependencies.
🎨 Leonie Voss — UI/UX Design Lead
Verdict: ⚠️ Approved with concerns
What works well
Touch targets — WCAG 2.5.5 ✓
min-h-[44px]is exactly the 44×44px minimum. This directly addresses WCAG 2.5.5 and is a real improvement for senior and mobile users. ✓showRightColumnguard — UX decision is correctAn empty 300px ghost column on a dashboard is a disorienting layout hole. Suppressing the column entirely when there's nothing to show is the right call. ✓
Mobile-first grid —
grid-cols-1base,lg:grid-cols-[1fr_300px]desktopSingle-column mobile, 2-column desktop. The pattern is mobile-first. ✓
Concerns
1. Stats footnote at
text-xs(12px) is at the absolute minimumtext-xsrenders at 12px. That is my hard floor — never below 12px — but "floor" means absolute minimum, not target. For a footnote that senior family members will read on a phone,text-sm(14px) with sufficient line-height would be more comfortable. Thetext-ink-3muted color compound the legibility issue at 12px. Suggest: bump totext-sm text-ink-3or keeptext-xsbut usetext-ink-2for higher contrast.2. Removal of the DashboardMentions widget is a UX regression without a replacement path
The PR correctly notes the file is "kept for future notifications page." But from the user's perspective, the mention/notification feed is gone from the dashboard with no replacement visible anywhere. Family members who relied on it to see who mentioned them in documents now have no discovery surface. The PR description mentions this is tracked but doesn't link a follow-up issue. Before this merges, there should be a Gitea issue open for the notifications replacement so it doesn't get lost.
3. Right column fixed at
300px— verify on narrow desktop viewportslg:breakpoint is 1024px. At 1024px with300pxright column andgap-4(16px), the left column gets1024px - 300px - 16px - (horizontal padding)≈ 660px. This should be fine, but worth a visual verification at exactly 1024px to confirm the DropZone doesn't feel squeezed.4. Dark mode — no evidence new elements are token-based
The
text-ink-3,border-line,bg-whiteclasses — if these map to CSS custom properties with dark-mode overrides, the new elements will inherit dark mode correctly. If any hardcoded colors crept in, they won't. I can't verify this from the diff alone. A quick visual smoke test in dark mode before merge would confirm.🚀 Tobias Wendt — DevOps & Platform Engineer
Verdict: ⚠️ Approved with concerns
What I checked
No changes to
docker-compose.yml, CI workflow files, or infrastructure configuration. This PR is frontend-only.Concerns
1.
frontend/e2e/.auth/user.jsonis a tracked file modified in this PRThis file stores Playwright's authenticated browser state (session cookies / storage). It is committed to the repository, which means real session tokens may be persisted in git history. Playwright's recommended approach is to either:
.auth/to.gitignoreand generate it at the start of every CI run viaplaywright/auth.setup.tsIf this file contains a real session token (even a test one), rotate it after this PR lands and add
.auth/to.gitignore. If it's already a regenerated test token, that's fine — but I'd still recommend gitignoring and regenerating it in the CI setup step to avoid accidental credential commits in the future.2.
proofshot-artifacts/binary PNGs committed to git6 PNG files per PR adds up. Git is not optimized for binary blobs — they are stored as full copies, not deltas. Over time this will inflate the repository size noticeably. Consider using Git LFS for screenshot artifacts, or uploading them as CI job artifacts (ephemeral, not stored in the repo) and referencing the artifact URL in the PR description instead.
No infrastructure blockers
The PR does not touch CI pipelines, compose configuration, or any deployed component beyond the SvelteKit frontend. Safe to merge from an infrastructure perspective once the
.auth/user.jsonsituation is clarified.