feat: dedicated /documents search & browse page #281
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?
Summary
Split the current dual-mode homepage into two focused pages:
/— pure dashboard hub (no more switching to document list on search)/documents— dedicated document search & browse page (new route)The "Dokumente" nav tab will point to
/documentsinstead of/.Design spec
docs/specs/documents-page-spec.html(onmain, commit6494b13)Key design decisions:
border border-line bg-surface shadow-smcard per year, rows separated bydivide-y(no gaps between rows)flex-1); date, from/to, archive, progress ring and contributor avatars on the right (sm:w-48 lg:w-60)completionPercentage(0–100%)ContributorStack.svelte, driven by a newcontributors: ActivityActorDTO[]fieldsmthe row collapses to a single column with a compact 2×2 metadata grid; purely CSS via Tailwind responsive prefixes-mx-4 sm:-mx-6 lg:-mx-8) to span the fullmax-w-7xlcontainer without going full-widthBackend changes required
completionPercentage: intcontributors: ActivityActorDTO[]archiveBox,archiveFolderFiles to change
New:
frontend/src/routes/documents/+page.sveltefrontend/src/routes/documents/+page.server.tsChanged:
frontend/src/routes/AppNav.svelte— Documents tabhref:/→/documentsfrontend/src/routes/+page.svelte— remove dual-mode, always render dashboardfrontend/src/routes/+page.server.ts— remove search branchfrontend/src/routes/DocumentList.svelte— refactor to new two-column year-card layoutcompletionPercentage+contributorsprojection🏛️ Markus Keller — Application Architect
Observations
Good separation of concerns. Splitting
/(pure dashboard) from/documents(search/browse) is the right architectural move. The dual-modeisDashboardconditional in the homepage has been a liability since it was introduced — the homepage load function made up to 10 API calls depending on state. Removing that branch simplifies both the load function and the template significantly.The key architectural gap: how do
completionPercentageandcontributorsget onto the search response? The currentDocumentSearchResultrecord wrapsList<Document>— full JPA entities. These computed fields cannot go onto theDocumententity (they aren't entity data; they change with annotation block state). The spec calls out "new projection or join in the repository layer" but doesn't specify the shape. Two viable options:Option A — Parallel maps in
DocumentSearchResult(least invasive):Option B — New
DocumentSearchItemDTO (cleanest, more idiomatic):Option B is the right call — it collocates all per-document data in one object, the TypeScript codegen will produce a clean type, and it eliminates the parallel map lookups on the frontend. Option A is technically viable but produces a messier API shape. Recommend Option B.
Potential logic duplication with
AuditLogQueryRepository. The dashboard package already hasfindContributorsPerDocument()inAuditLogQueryRepository. A new contributors query in the document search layer will duplicate this logic. Before adding a new query, confirm whether the dashboard query can be reused or whether the two queries differ in ways that justify duplication (e.g., different grouping, different max count). If the logic is identical, extract it to a shared method or a dedicatedAnnotationBlockQueryRepository.Domain boundary: document search querying annotation blocks. The document search repository will need to count annotation blocks per document. If annotation blocks live in a separate feature package (
annotation), thedocumentpackage must not directly reach intoAnnotationBlockRepository— it should call anAnnotationService.getCompletionStats(documentIds)or similar. Check the current package layout before choosing an implementation approach.Recommendations
DocumentSearchItemDTO) — clean API shape, straightforward TypeScript codegen, no parallel map lookups.AuditLogQueryRepository.findContributorsPerDocument()can be reused for the 4-contributor case, or refactor it into a shared utility. Avoid duplicating the contributor query.Open Decisions
completionPercentage+contributorsin the API response — Option A (parallel maps in existing record) vs. Option B (newDocumentSearchItemDTO). Both work; Option B is cleaner but requires regenerating TypeScript types and updating the frontend data access pattern. Worth deciding before implementation starts.👨💻 Felix Brandt — Senior Fullstack Developer
Observations
Component decomposition opportunity. The refactored
DocumentList.sveltewill be handling year grouping, two-column rows, progress ring SVG, contributor stacks, mobile metadata grid, AND filter state. That's at minimum 4 visual boundaries in one file. The spec already identifies these as separate regions — they should be separate components:DocumentRow.svelte— the<li>with two-column split + mobile grid + ring + contributorsProgressRing.svelte— the SVG donut with percentage label (pure display, zero business logic)DocumentList.svelteshould become the year-card orchestrator onlyAppNav change is smaller than it looks. I checked
AppNav.svelte— the Documents tab active-state condition already readspage.url.pathname === '/' || page.url.pathname.startsWith('/documents'). So the active highlight will work correctly once the href changes from/to/documents. The change is one line.Progress ring SVG formula is spec-correct. Section 5 gives
circumference = 2π × 13 ≈ 81.7. The dasharray implementation should be:The
ProgressRingcomponent should accept a singlepercentage: numberprop — no business logic inside it.Both DOM layouts must exist simultaneously per spec. Section 2b is explicit: the mobile metadata grid (
sm:hidden) and the desktop right column (hidden sm:flex) are both in the DOM, toggled purely by Tailwind responsive classes. No JS toggling, no{#if}on viewport size. This is CSS-only and is the right approach.Keyed
{#each}is required in two places:Both need keys. Year group lists that reorder on sort change will corrupt state without them.
TypeScript types need regeneration. Adding
completionPercentageandcontributorsto the search response requires runningnpm run generate:apiafter the backend changes. The new fields won't be type-safe until that's done. Plan the backend PR first, then the frontend PR.page.server.spec.tsis already modified. Good — presumably the dashboard-only tests are already updated. The newdocuments/+page.server.tsneeds equivalent coverage from the start (write failing tests before implementing the load function).Recommendations
ProgressRing.svelteandDocumentRow.sveltefromDocumentList.sveltebefore the file gets large. TheDocumentList.svelterefactor is the right trigger for this split.ProgressRing.svelteas a pure display component:percentage: numberin, SVG out. No data fetching, no state.documents/+page.server.spec.tstest file before the load function implementation. The test structure will mirror the existingpage.server.spec.ts.🔐 Nora "NullX" Steiner — Security Engineer
Observations
No new attack surfaces introduced. The route change (dashboard at
/, search at/documents) is purely organizational — the underlying API endpoints and auth model are unchanged. The new+page.server.tsfor/documentswill use the samecreateApiClient(fetch)pattern as every other server-side loader, so auth cookie forwarding is handled correctly.Contributors field is an intentional information disclosure — confirm it's in scope. The new
contributors: ActivityActorDTO[]field on the search response exposes, for each document, the identities of users who have made annotation contributions (initials, color, and optionally full name). Any authenticated user withREAD_ALLwill see who has been working on what document. In a family archive context this is likely intentional, but it's worth noting explicitly: this is a deliberate expansion of what the search endpoint reveals. Thenamefield inActivityActorDTOis nullable — the backend query should confirm it only populates when the user has a displayable name, and that deleted users are handled gracefully (null out the name, keep the initials from a snapshot, or omit them).New backend queries must use parameterized JPQL/native SQL. The existing
AuditLogQueryRepository.findContributorsPerDocument()already uses parameterized native queries (IN (:documentIds)) — this is the pattern to follow for the new document search queries. If the implementation switches to string interpolation for theINclause, that's a SQL injection vector. Specifically watch for:archiveBox/archiveFolderexposure is safe. These fields already exist on theDocumententity and are within theREAD_ALLpermission boundary. Exposing them in the search response doesn't change what aREAD_ALLuser can already access via the document detail endpoint.Recommendations
namefield inActivityActorDTOis intentionally included in the search response (not just initials/color). If full names in search results is a concern, the contributors field could be limited toinitials + coloronly for the search context.AuditLogQueryRepositoryfor any new native SQL queries.🧪 Sara Holt — QA Engineer
Observations
The modified
page.server.spec.tsis a good signal — updating the homepage test before the feature is implemented suggests the dashboard-only behavior is being driven by tests. Good. The remaining gap is coverage for the newdocuments/+page.server.ts.Required test coverage — before implementation:
Backend (new queries):
Frontend
documents/+page.server.spec.ts:Frontend components:
Homepage regression:
Edge cases the spec doesn't make explicit:
documentDate = null— how does year grouping handle this? What year bucket does it fall into? Define behavior before implementing.DocumentList.svelte?)Recommendations
documents/+page.server.spec.tsbeforedocuments/+page.server.ts— the test file structure from the existing spec is the template.completionPercentageedge cases (0 blocks, partial, full) using Testcontainers against real PostgreSQL — the calculation usesmax(totalBlocks, 1)which is only correct at the SQL level, not an in-memory approximation.documentDategrouping behavior explicitly — it will come up.🎨 Leonie Voss — UX Designer & Accessibility Advocate
Observations
The overall design is well-constructed. The year-card grouping, full-row
<a>link for touch targets, two-column split, and CSS-only mobile layout are all strong decisions. The progress ring uses arc fill + percentage text as redundant cues — not colour alone — which satisfies WCAG SC 1.4.1.Critical: several text sizes fall below the 12px minimum.
text-[8px]text-[9px]text-[10px]text-[11px]The progress ring is the highest priority fix. The percentage label at 8px will be unreadable for the 60+ audience on any screen. Recommendation: use
text-[10px]minimum for the ring label (the ring itself is 36px, there's room), andtext-[11px]for the "No contributors" fallback. The contributor initials in the existingContributorStack.svelteshould also be increased from 9px totext-[10px].Sticky bar magic numbers will cause overlap if the header ever changes.
The spec specifies
top-[65px]for the search bar andtop-[113px]for the sort bar, derived from a 65px global topbar height. This is a fragile assumption. If a notification banner, a breadcrumb, or any header change adds even 1px, the sticky bars will slide under the topbar or leave a gap. Recommendation: define a CSS custom property--header-height: 65pxinlayout.cssand reference it here so there's one place to update when the header changes.Filter panel focus management is missing from the spec. When the "Filters" toggle opens the panel, keyboard focus should move to the first filter input (Date From). When the panel closes, focus should return to the Filters toggle button. Neither is mentioned in the spec, but both are required for WCAG SC 2.1.2 (no keyboard trap) and a good keyboard experience. Implement using Svelte's
afterUpdateor atick()+focus()call when the panel opens.The full-row
<a>touch target is correctly designed. Title + snippet + tags + metadata grid guarantees well over 44px on any content. No action needed — this is noted as done well.The "No contributors" empty state needs clarification. The existing
ContributorStack.svelteshows a dashed border circle with tooltip "Noch niemand angefangen". The spec says to rendertext-[9px]text "No contributors". These are different patterns — align with one before implementing.Recommendations
text-[8px]totext-[10px]minimum.text-[9px]totext-[11px].ContributorStack.svelteinitials font from 9px totext-[10px].65in three stickytopvalues.Open Decisions
⚙️ Tobias Wendt — DevOps & Platform Engineer
Observations
No infrastructure changes required. This is a pure application-layer feature — no new services, no Compose changes, no migration files (confirmed by the spec: "No schema migration needed"). Clean from an ops perspective.
Performance risk in the new search queries. The
completionPercentageandcontributorsfields require counting and joining annotation blocks for every document in every search result page. On a typical search with 20 results this means the backend computes annotation stats for 20 documents simultaneously. Check that the following indexes exist or are added:Without these, each search page load runs sequential table scans on
annotation_blocks. At a few thousand documents this will be noticeable. At tens of thousands it will be a problem.The new
/documentsroute should be added to the smoke test suite. The current smoke test (if one exists) probably only verifies/and/api/actuator/health. The/documentspage now has its own server-side load function and its own backend queries — it deserves an independent smoke check to catch deployment regressions where the dashboard works but the documents page fails silently.No Flyway migration needed — confirmed, purely computed from existing data. Good.
Recommendations
annotation_blocks(document_id, status)andannotation_blocks(document_id, created_at DESC)indexes before the feature ships. If they don't exist, add a Flyway migration as part of this issue.GET /documentsto the post-deploy smoke test check so a broken documents page is caught automatically on deploy.No open decisions from an infrastructure standpoint — this is a straightforward application change.
🗳️ Decision Queue — Action Required
2 decisions need your input before implementation starts.
Architecture
completionPercentage+contributors— The currentDocumentSearchResultwrapsList<Document>entities; computed fields can't go on entities. Two options: (A) Add parallel maps to the existing record (Map<UUID, Integer>+Map<UUID, List<ActivityActorDTO>>): minimal backend change, messier frontend access. (B) NewDocumentSearchItemper-document DTO that collocates document + matchData + completionPercentage + contributors: clean API shape, straightforward TypeScript codegen, but requires updating all existing frontend data access. Markus recommends B. (Raised by: Markus)UX / Component Design
text-[9px]). Both are accessible. Decision affects whetherContributorStack.svelteneeds a behavior change or whether the documents page uses a different empty state pattern than other places the component is used. (Raised by: Leonie)🏛️ Markus Keller — Architecture Discussion Summary
Decisions settled in discussion. Three open items, all resolved.
Resolved
API response shape → Option B. New
DocumentSearchItemrecord:document,matchData,completionPercentage,contributorsall collocated per item.DocumentSearchResultbecomesList<DocumentSearchItem>+total. The existingMap<UUID, SearchMatchData> matchDataonDocumentSearchResultis folded into the item — frontend drops the map lookups entirely. Regenerate TypeScript types before touching the frontend.Contributor query → extract to
AnnotationBlockRepository. NewAnnotationBlockRepositoryinrepository/ownsfindContributorsForDocuments(documentIds)and the completion stats query. Both the dashboard and the document search call it from there. The existingAuditLogQueryRepository.findContributorsPerDocument()in the dashboard package either delegates to the new repo or gets replaced. No cross-package coupling.Assembly point →
DocumentService.DocumentServicecallsDocumentSearchRepositoryfor documents + match data, callsAnnotationBlockRepositoryfor contributors + completion stats, assemblesDocumentSearchItemrecords before returning. Repositories stay single-purpose.Overall: the structural decisions are clean. The implementation order should be backend first (new repos + service assembly + DTO), then
npm run generate:api, then frontend.🎨 Leonie Voss — UX Discussion Summary
Decisions settled in discussion. Four open items, all resolved.
Resolved
"No contributors" empty state → dashed circle. Keep the existing
ContributorStack.sveltebehavior (dashed border circle, tooltip "Noch niemand angefangen"). The spec's plain-text label is overruled. No changes to the component's empty state — it stays consistent across all usages.Sub-12px text → bump to
text-[10px]. Progress ring percentage label:text-[8px]→text-[10px].ContributorStackinitials: 9px →text-[10px]. Both files need touching during implementation anyway.Sticky bar offsets → CSS custom property. Add
--header-height: 65pxtolayout.css. Search bar sticky offset:top-[var(--header-height)]. Sort bar:top-[calc(var(--header-height)+48px)](search bar height = 48px). Single source of truth — if the header grows, one value to update.Filter panel focus management → required. On panel open:
tick()+firstInput.focus()(Date From input). On panel close:filterToggle.focus(). Must be implemented explicitly — not a stretch goal. Required for WCAG SC 2.1.2.Overall: the spec is solid. These are targeted fixes, not redesigns. The implementation is clear.
Implementation complete — PR #282 opened.
All 15 tasks implemented (TDD throughout):
Backend:
TranscriptionBlockRepository— bulk completion stats query (0%/partial/100% integration-tested)AuditLogQueryRepository—findRecentContributorsForDocuments()(max 4, recency-ordered)AuditLogQueryService—findRecentContributorsPerDocument()mapDocumentSearchItemrecord with all fields@Schema(requiredMode=REQUIRED)DocumentSearchResultrefactored to{items, total}DocumentService.searchDocuments()assemblesDocumentSearchItemwith stats + contributorstranscription_blocks(document_id, reviewed)Frontend:
--header-height: 65pxCSS custom property; ContributorStack initialstext-[10px]; AppNav/documentslink/) rewritten to pure dashboard — always shows widgets, never searchProgressRing.svelte— SVG arc, spec-matched colours, 4 unit testsDocumentRow.svelte— two-column layout, highlight offsets, tags, progress ring, contributor stackDocumentList.svelte— year-card orchestrator withDocumentSearchItem[]propsdocuments/+page.server.ts— search load with full filter params, 7 unit testsdocuments/+page.svelte— URL-driven filter state,SearchFilterBar+DocumentList1006 frontend tests passing, backend tests green.