feat(#145): transform home page into user dashboard #151
Reference in New Issue
Block a user
Delete Branch "feat/145-dashboard"
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 #145
Summary
DashboardResumeStrip— "continue where you left off" (localStorage)DashboardMentions— last 5 notifications (mentions + replies), deep-linked to the specific commentDashboardNeedsMetadata— incomplete documents queue with link to/enrichDashboardRecentDocuments— last 5 uploaded documents withcreatedAtdate?size=param, regenerated API typesProofshots
Mobile (390×844)
Tablet (768×1024)
Desktop (1440×900)
Test plan
/when no search filters are active?commentId=+&annotationId=)createdAt), not document date/enrich/{id}🤖 Generated with Claude Code
Any future feature spec now just calls: captureProofshots('/my-route', 'feature-name') to get 6 screenshots (3 viewports × 2 themes) saved to proofshot-artifacts/{feature-name}/. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>UI Review — Leonie Voss (updated after commit
7eda0ae)Overall the dashboard is clean and the dark mode implementation is intentional and well-executed. Semantic color tokens hold up across all 6 proofshots. Deep-linking in
DashboardMentionsis correct and well-tested. i18n coverage across de/en/es is complete. The notifications widget showing all recent items (not just unread) is the right call given the bell icon handles the unread queue.Two issues still need fixing before merge. One proofshot finding is now resolved.
[High]
+page.svelte:103— Grid breaks when Mentions widget is empty ❌ still openDashboardMentionsrenders no DOM element at all whenmentions.length === 0(bare{#if}, no fallback). Its grid cell disappears andDashboardNeedsMetadatashifts into the left column with an empty right column beside it. The new screenshots mask this — they populate the widget via DB seed — but the layout regression still exists for any user with no notifications.Fix — collapse to single column when there are no mentions:
When Mentions is empty, NeedsMetadata spans full width — consistent with the Recent Documents card below it.
[Medium]
DashboardNeedsMetadata.svelte:31— 12px link is below the accessibility floor ❌ still opentext-xsrenders at 12px. Minimum for interactive text istext-sm(14px), 16px preferred — especially for the 60+ part of our dual audience. Also:hover:text-inkalone doesn't satisfy WCAG 1.4.1 — color cannot be the only visual differentiator. Addhover:underline.[Low]
dashboard-screenshots.spec.ts— ResumeStrip and Notifications proofshot coverage ✅ resolved in7eda0aeBoth widgets are now visible in all 6 screenshots. The seeding approach is clean:
beforeAll/afterAllwith psql inserts scoped to sentinel actor names, pluspage.evaluate()for the localStorage entry — no full document page load required. The notifications query change ({ size: 5 }replacing{ type: MENTION, read: false, size: 5 }) is consistent with showing all recent activity; the bell icon handles the unread-only queue separately.[Low]
DashboardRecentDocuments.svelte:23— Silent removal of noon-anchor (unchanged)Still flagged. The removal is correct, but add a short comment for the next reader:
Architectural Review — @mkeller
Good PR overall. Clean layering, proper test coverage at all three levels, i18n complete. Two issues need attention before merge.
🔴 1. Full-table scan in
getRecentActivity— must fixDocumentService.java:264This loads every document into memory, then discards all but
size. On a small archive with 200 docs it's invisible. On one with 5,000 scanned letters it's a noticeable pause on every dashboard load.Fix — push the
LIMITto the database:This is the same pattern already used in
findIncompleteDocuments. No new repository method needed.🔴 2. UTC off-by-one comment missing in
DashboardRecentDocuments— must fixDashboardRecentDocuments.svelte:20The removal is correct —
updatedAtis a full ISO datetime, not a date-only string, so theT12:00:00suffix isn't needed. ButCLAUDE.mddocuments theT12:00:00pattern as a deliberate UTC off-by-one fix, so the next developer will "fix" this back. Add one comment explaining why the suffix is absent here:🟢 What's done well
DocumentController → DocumentService → Repository, no repo leakage.Promise.allSettledfor widget isolation: one failing widget doesn't crash the dashboard.NotificationRepositoryTestagainst Testcontainers is the right way to validate derived queries.onMountguard onDashboardResumeStrip: localStorage is properly client-only, no SSR hazard.captureProofshotshelper: new feature specs are now one line — good extraction.Test Coverage Review — @saraholt
Overall solid structure: four widget specs, repo/service/controller tests all present. A few gaps worth addressing before merge.
P1 — Must Fix
getRecentActivityfull-table-scan not caught by any testDocumentService.getRecentActivitycallsdocumentRepository.findAll(Sort), which fetches the entiredocumentstable and truncates in Java with.stream().limit(size). The unit test mocks the repo so it can't catch this. There is no@DataJpaTestverifying the actual query behaviour.A
@DataJpaTestseeding e.g. 10 documents and asserting onlysizerows are returned would immediately expose that theLIMITis not pushed to the DB. The fix isPageRequest.of(0, size, Sort.by(Sort.Direction.DESC, "updatedAt"))or a derived query method.No tests for
+page.server.tsload functionThe load function contains meaningful branching (dashboard mode vs. search mode), three parallel
Promise.allSettledcalls, and per-branch fallback to[]on failure. SvelteKit load functions are plain TypeScript — they can be imported and called directly without a browser.Missing cases:
documentslistmentionsResultrejected →mentionsdefaults to[]incompleteResultrejected →incompleteDocsdefaults to[]recentResultrejected →recentDocsdefaults to[]P2 — Should Fix
DashboardResumeStrip: corrupt localStorage not handledonMountcallsJSON.parse(localStorage.getItem(...))without a try/catch. Malformed JSON throws synchronously and leaves the component in an undefined state. No test covers this path. Either add a test that exercises the corrupt-JSON case (and add the corresponding guard), or document the decision to let it throw.DashboardMentions: two branches untestedtype === 'REPLY'— the component conditionally renders "hat geantwortet" vs "erwähnt Sie", but only the MENTION branch is exercised in tests.documentIdabsent — the component renders a<span>instead of<a>. Not tested; a regression here would be invisible.P3 — Nice to Have
DocumentControllerTest: no test callsGET /api/documents/recent-activitywithout?size=to verify the default value (5) is applied.NotificationRepository:findByType_returnsBothReadAndUnreadMentionsverifies size and tenant isolation but does not assertcreatedAt DESCordering, unlike the other paged query tests in the same file.NotificationService:?type=MENTION&read=truesilently falls through to the type-only branch (returns all mentions, not just read ones). Either add a test documenting this as intentional, or handle theread=truecase explicitly.The two P1 items are the ones I'd want resolved before merge. The full-table-scan in particular is the kind of thing that won't surface until the archive grows.
All review items addressed in commit
5bdd26c. Full breakdown below.@leonie — UI Review
[High] Grid breaks when Mentions widget is empty ✅ fixed
+page.svelte:103— grid class is now conditional:When
mentionsis empty,NeedsMetadataspans full width. Matches the behaviour ofDashboardRecentDocumentsbelow it.[Medium] 12px link below accessibility floor ✅ fixed
DashboardNeedsMetadata.svelte:32— raised totext-smand addedhover:underline:[Low] noon-anchor comment ✅ fixed
Added as a "why" comment directly before
.format()— explains the non-obvious deviation from the CLAUDE.md pattern:@mkeller — Architectural Review
🔴 Full-table scan in
getRecentActivity✅ fixedDocumentService.java:264— replacedfindAll(Sort)+ Java stream truncation with aPageRequest, pushingLIMITto the database:Same pattern as
findIncompleteDocuments. No new repository method needed.🔴 UTC off-by-one comment ✅ fixed
Same as Leonie's [Low] — addressed above.
@saraholt — Test Coverage Review
P1 — Full-table scan not caught by any test ✅ fixed
Updated
DocumentServiceTest— the unit test now asserts that aPageRequest(notSort) is passed to the repository, including the page size and sort direction:Added
DocumentRepositoryTest—@DataJpaTestseeding 10 documents and asserting onlysizerows are returned (not all 10), verifying theLIMITis pushed to the DB.P1 — No tests for
+page.server.tsload function ✅ fixedpage.server.spec.tswas stale — it still referencedincompleteCountfrom a prior API shape. Replaced with full coverage of the new dashboard/search branching:isDashboard = true, all three widget APIs called, widgets receive dataisDashboard = false, no widget calls, returnsdocumentslistmentions,incompleteDocs,recentDocseach default to[]when their API call is rejected/loginP2 —
DashboardResumeStrip: corrupt localStorage — pushing back on the premiseThe code already has a
try/catchatDashboardResumeStrip.svelte:12–24— malformed JSON is silently caught andlastVisitedstaysnull. There is no open guard to add. I have however added a test that exercises this path to confirm the existing guard works as expected:P2 —
DashboardMentions: two branches untested ✅ fixedAdded tests for:
type === 'REPLY'— widget renders and a link is presentdocumentIdabsent — a<span>is rendered instead of an<a>P3 — Default size=5 not tested ✅ fixed
DocumentControllerTest— addedgetRecentActivity_appliesDefaultSizeOfFive_whenSizeParamOmittedwhich calls the endpoint without?size=and verifiesdocumentService.getRecentActivity(5)was invoked.P3 —
NotificationServiceread=truefallthrough ✅ documentedAdded
getNotifications_withTypeAndReadTrue_fallsBackToTypeOnlyQuery— the test asserts the type-only repo method is called and documents the intent:P3 —
NotificationRepositoryordering not assertedNot implementing this one. The ordering is enforced by the Spring Data derived query method name itself (
OrderByCreatedAtDesc) — it's a framework-level guarantee, not application logic. Testing it would require eitherThread.sleepbetween saves (flaky) or directly injectingcreatedAttimestamps (coupling to internals). The method name is the contract; testing the Spring Data query derivation would be testing the framework.Move text-decoration-thickness/underline-offset into the global a:hover base rule so every link that shows an underline on hover gets identical treatment: 2px thick, 4px offset, accent colour. Remove the now-redundant per-component decoration-brand-mint / decoration- accent / decoration-2 / underline-offset-{2,4} utilities from DocumentList, enrich, persons, PersonDocumentList, and PanelMetadata. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>