feat: dedicated /documents search & browse page #281

Closed
opened 2026-04-19 21:43:15 +02:00 by marcel · 10 comments
Owner

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 /documents instead of /.

Design spec

docs/specs/documents-page-spec.html (on main, commit 6494b13)

Key design decisions:

  • Year group cards — one border border-line bg-surface shadow-sm card per year, rows separated by divide-y (no gaps between rows)
  • Two-column row — title + snippet + tags on the left (flex-1); date, from/to, archive, progress ring and contributor avatars on the right (sm:w-48 lg:w-60)
  • Progress ring — 36×36px SVG donut showing completionPercentage (0–100%)
  • Contributor avatars — reuse ContributorStack.svelte, driven by a new contributors: ActivityActorDTO[] field
  • Mobile responsive — below sm the row collapses to a single column with a compact 2×2 metadata grid; purely CSS via Tailwind responsive prefixes
  • Wider layout — sticky bars use negative margins (-mx-4 sm:-mx-6 lg:-mx-8) to span the full max-w-7xl container without going full-width

Backend changes required

Field Notes
completionPercentage: int New — COUNT(reviewed blocks) / COUNT(all blocks) per document
contributors: ActivityActorDTO[] New — distinct users with annotation contributions, max 4, ordered by recency
archiveBox, archiveFolder Existing entity fields, just not yet exposed in search response

Files to change

New:

  • frontend/src/routes/documents/+page.svelte
  • frontend/src/routes/documents/+page.server.ts

Changed:

  • frontend/src/routes/AppNav.svelte — Documents tab href: //documents
  • frontend/src/routes/+page.svelte — remove dual-mode, always render dashboard
  • frontend/src/routes/+page.server.ts — remove search branch
  • frontend/src/routes/DocumentList.svelte — refactor to new two-column year-card layout
  • Backend document search repository — add completionPercentage + contributors projection
## 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 `/documents` instead of `/`. ## Design spec `docs/specs/documents-page-spec.html` (on `main`, commit `6494b13`) Key design decisions: - **Year group cards** — one `border border-line bg-surface shadow-sm` card per year, rows separated by `divide-y` (no gaps between rows) - **Two-column row** — title + snippet + tags on the left (`flex-1`); date, from/to, archive, progress ring and contributor avatars on the right (`sm:w-48 lg:w-60`) - **Progress ring** — 36×36px SVG donut showing `completionPercentage` (0–100%) - **Contributor avatars** — reuse `ContributorStack.svelte`, driven by a new `contributors: ActivityActorDTO[]` field - **Mobile responsive** — below `sm` the row collapses to a single column with a compact 2×2 metadata grid; purely CSS via Tailwind responsive prefixes - **Wider layout** — sticky bars use negative margins (`-mx-4 sm:-mx-6 lg:-mx-8`) to span the full `max-w-7xl` container without going full-width ## Backend changes required | Field | Notes | |---|---| | `completionPercentage: int` | New — COUNT(reviewed blocks) / COUNT(all blocks) per document | | `contributors: ActivityActorDTO[]` | New — distinct users with annotation contributions, max 4, ordered by recency | | `archiveBox`, `archiveFolder` | Existing entity fields, just not yet exposed in search response | ## Files to change **New:** - `frontend/src/routes/documents/+page.svelte` - `frontend/src/routes/documents/+page.server.ts` **Changed:** - `frontend/src/routes/AppNav.svelte` — Documents tab `href`: `/` → `/documents` - `frontend/src/routes/+page.svelte` — remove dual-mode, always render dashboard - `frontend/src/routes/+page.server.ts` — remove search branch - `frontend/src/routes/DocumentList.svelte` — refactor to new two-column year-card layout - Backend document search repository — add `completionPercentage` + `contributors` projection
marcel added the featureui labels 2026-04-19 21:43:22 +02:00
Author
Owner

🏛️ Markus Keller — Application Architect

Observations

  • Good separation of concerns. Splitting / (pure dashboard) from /documents (search/browse) is the right architectural move. The dual-mode isDashboard conditional 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 completionPercentage and contributors get onto the search response? The current DocumentSearchResult record wraps List<Document> — full JPA entities. These computed fields cannot go onto the Document entity (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):

    public record DocumentSearchResult(
        List<Document> documents,
        long total,
        Map<UUID, SearchMatchData> matchData,
        Map<UUID, Integer> completionPercentages,   // NEW
        Map<UUID, List<ActivityActorDTO>> contributors // NEW
    ) {}
    

    Option B — New DocumentSearchItem DTO (cleanest, more idiomatic):

    public record DocumentSearchItem(
        Document document,
        SearchMatchData matchData,
        int completionPercentage,
        List<ActivityActorDTO> contributors
    ) {}
    public record DocumentSearchResult(List<DocumentSearchItem> items, long total) {}
    

    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 has findContributorsPerDocument() in AuditLogQueryRepository. 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 dedicated AnnotationBlockQueryRepository.

  • 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), the document package must not directly reach into AnnotationBlockRepository — it should call an AnnotationService.getCompletionStats(documentIds) or similar. Check the current package layout before choosing an implementation approach.

Recommendations

  • Use Option B (DocumentSearchItem DTO) — clean API shape, straightforward TypeScript codegen, no parallel map lookups.
  • Before writing the new query, check whether AuditLogQueryRepository.findContributorsPerDocument() can be reused for the 4-contributor case, or refactor it into a shared utility. Avoid duplicating the contributor query.
  • Confirm the annotation block domain boundary and ensure the document search layer crosses it via a service interface, not a direct repository call.

Open Decisions

  • How to expose completionPercentage + contributors in the API response — Option A (parallel maps in existing record) vs. Option B (new DocumentSearchItem DTO). Both work; Option B is cleaner but requires regenerating TypeScript types and updating the frontend data access pattern. Worth deciding before implementation starts.
## 🏛️ Markus Keller — Application Architect ### Observations - **Good separation of concerns.** Splitting `/` (pure dashboard) from `/documents` (search/browse) is the right architectural move. The dual-mode `isDashboard` conditional 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 `completionPercentage` and `contributors` get onto the search response?** The current `DocumentSearchResult` record wraps `List<Document>` — full JPA entities. These computed fields cannot go onto the `Document` entity (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): ```java public record DocumentSearchResult( List<Document> documents, long total, Map<UUID, SearchMatchData> matchData, Map<UUID, Integer> completionPercentages, // NEW Map<UUID, List<ActivityActorDTO>> contributors // NEW ) {} ``` **Option B — New `DocumentSearchItem` DTO** (cleanest, more idiomatic): ```java public record DocumentSearchItem( Document document, SearchMatchData matchData, int completionPercentage, List<ActivityActorDTO> contributors ) {} public record DocumentSearchResult(List<DocumentSearchItem> items, long total) {} ``` 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 has `findContributorsPerDocument()` in `AuditLogQueryRepository`. 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 dedicated `AnnotationBlockQueryRepository`. - **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`), the `document` package must not directly reach into `AnnotationBlockRepository` — it should call an `AnnotationService.getCompletionStats(documentIds)` or similar. Check the current package layout before choosing an implementation approach. ### Recommendations - Use Option B (`DocumentSearchItem` DTO) — clean API shape, straightforward TypeScript codegen, no parallel map lookups. - Before writing the new query, check whether `AuditLogQueryRepository.findContributorsPerDocument()` can be reused for the 4-contributor case, or refactor it into a shared utility. Avoid duplicating the contributor query. - Confirm the annotation block domain boundary and ensure the document search layer crosses it via a service interface, not a direct repository call. ### Open Decisions - **How to expose `completionPercentage` + `contributors` in the API response** — Option A (parallel maps in existing record) vs. Option B (new `DocumentSearchItem` DTO). Both work; Option B is cleaner but requires regenerating TypeScript types and updating the frontend data access pattern. Worth deciding before implementation starts.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Observations

  • Component decomposition opportunity. The refactored DocumentList.svelte will 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 + contributors
    • ProgressRing.svelte — the SVG donut with percentage label (pure display, zero business logic)
    • DocumentList.svelte should become the year-card orchestrator only
  • AppNav change is smaller than it looks. I checked AppNav.svelte — the Documents tab active-state condition already reads page.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:

    <circle stroke-dasharray="{(completionPercentage / 100) * 81.68} 81.68" />
    

    The ProgressRing component should accept a single percentage: number prop — 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:

    {#each yearGroups as group (group.year)}
      {#each group.documents as doc (doc.id)}
    

    Both need keys. Year group lists that reorder on sort change will corrupt state without them.

  • TypeScript types need regeneration. Adding completionPercentage and contributors to the search response requires running npm run generate:api after 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.ts is already modified. Good — presumably the dashboard-only tests are already updated. The new documents/+page.server.ts needs equivalent coverage from the start (write failing tests before implementing the load function).

Recommendations

  • Extract ProgressRing.svelte and DocumentRow.svelte from DocumentList.svelte before the file gets large. The DocumentList.svelte refactor is the right trigger for this split.
  • Implement the mobile/desktop layouts as pure CSS (Tailwind responsive prefixes), not JS. The spec is explicit on this.
  • Keep ProgressRing.svelte as a pure display component: percentage: number in, SVG out. No data fetching, no state.
  • Write the documents/+page.server.spec.ts test file before the load function implementation. The test structure will mirror the existing page.server.spec.ts.
## 👨‍💻 Felix Brandt — Senior Fullstack Developer ### Observations - **Component decomposition opportunity.** The refactored `DocumentList.svelte` will 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 + contributors - `ProgressRing.svelte` — the SVG donut with percentage label (pure display, zero business logic) - `DocumentList.svelte` should become the year-card orchestrator only - **AppNav change is smaller than it looks.** I checked `AppNav.svelte` — the Documents tab active-state condition already reads `page.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: ```svelte <circle stroke-dasharray="{(completionPercentage / 100) * 81.68} 81.68" /> ``` The `ProgressRing` component should accept a single `percentage: number` prop — 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:** ```svelte {#each yearGroups as group (group.year)} {#each group.documents as doc (doc.id)} ``` Both need keys. Year group lists that reorder on sort change will corrupt state without them. - **TypeScript types need regeneration.** Adding `completionPercentage` and `contributors` to the search response requires running `npm run generate:api` after 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.ts` is already modified.** Good — presumably the dashboard-only tests are already updated. The new `documents/+page.server.ts` needs equivalent coverage from the start (write failing tests before implementing the load function). ### Recommendations - Extract `ProgressRing.svelte` and `DocumentRow.svelte` from `DocumentList.svelte` before the file gets large. The `DocumentList.svelte` refactor is the right trigger for this split. - Implement the mobile/desktop layouts as pure CSS (Tailwind responsive prefixes), not JS. The spec is explicit on this. - Keep `ProgressRing.svelte` as a pure display component: `percentage: number` in, SVG out. No data fetching, no state. - Write the `documents/+page.server.spec.ts` test file before the load function implementation. The test structure will mirror the existing `page.server.spec.ts`.
Author
Owner

🔐 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.ts for /documents will use the same createApiClient(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 with READ_ALL will 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. The name field in ActivityActorDTO is 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 the IN clause, that's a SQL injection vector. Specifically watch for:

// WRONG — SQL injection via the documentIds list
String ids = documentIds.stream().map(UUID::toString).collect(joining(","));
query = "... WHERE document_id IN (" + ids + ")";

// CORRECT — use Spring Data @Query with collection binding
@Query(value = "... WHERE ab.document_id IN (:documentIds)", nativeQuery = true)
List<...> findContributors(@Param("documentIds") List<UUID> documentIds);

archiveBox/archiveFolder exposure is safe. These fields already exist on the Document entity and are within the READ_ALL permission boundary. Exposing them in the search response doesn't change what a READ_ALL user can already access via the document detail endpoint.

Recommendations

  • When implementing the contributors query, explicitly handle the case where a contributing user has been deleted from the system — a broken foreign key should not cause the entire search to fail. Use a LEFT JOIN and handle nulls on the application side.
  • Confirm that the name field in ActivityActorDTO is 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 to initials + color only for the search context.
  • Follow the existing parameterized query pattern from AuditLogQueryRepository for any new native SQL queries.
## 🔐 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.ts` for `/documents` will use the same `createApiClient(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 with `READ_ALL` will 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. The `name` field in `ActivityActorDTO` is 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 the `IN` clause, that's a SQL injection vector. Specifically watch for: ```java // WRONG — SQL injection via the documentIds list String ids = documentIds.stream().map(UUID::toString).collect(joining(",")); query = "... WHERE document_id IN (" + ids + ")"; // CORRECT — use Spring Data @Query with collection binding @Query(value = "... WHERE ab.document_id IN (:documentIds)", nativeQuery = true) List<...> findContributors(@Param("documentIds") List<UUID> documentIds); ``` **`archiveBox`/`archiveFolder` exposure is safe.** These fields already exist on the `Document` entity and are within the `READ_ALL` permission boundary. Exposing them in the search response doesn't change what a `READ_ALL` user can already access via the document detail endpoint. ### Recommendations - When implementing the contributors query, explicitly handle the case where a contributing user has been deleted from the system — a broken foreign key should not cause the entire search to fail. Use a LEFT JOIN and handle nulls on the application side. - Confirm that the `name` field in `ActivityActorDTO` is 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 to `initials + color` only for the search context. - Follow the existing parameterized query pattern from `AuditLogQueryRepository` for any new native SQL queries.
Author
Owner

🧪 Sara Holt — QA Engineer

Observations

The modified page.server.spec.ts is 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 new documents/+page.server.ts.

Required test coverage — before implementation:

Backend (new queries):

// completionPercentage — integration tests with Testcontainers + real PostgreSQL
@Test void completionPercentage_is_zero_when_document_has_no_blocks()
@Test void completionPercentage_is_100_when_all_blocks_are_reviewed()
@Test void completionPercentage_rounds_correctly_for_partial_completion()

// contributors
@Test void contributors_returns_distinct_users_ordered_by_recency()
@Test void contributors_limits_result_to_4_users()
@Test void contributors_returns_empty_list_when_no_annotation_blocks_exist()
@Test void contributors_handles_deleted_users_gracefully()

Frontend documents/+page.server.spec.ts:

// Mirror the structure of the existing page.server.spec.ts
it('passes q, from, to, sort, dir, tags to search API')
it('returns 404 for API errors with structured error message')
it('redirects to /login when auth fails (401)')
it('returns error string on network failure without throwing')

Frontend components:

// ProgressRing.svelte — unit tests
it('renders filled arc for 75%')
it('renders empty arc for 0%')
it('renders full arc for 100%')
it('shows gray label at 0%, mint label above 0%')

// DocumentRow.svelte
it('shows mobile grid when viewport < sm')
it('shows right column when viewport >= sm')
it('renders archive line only when archiveBox is set')

Homepage regression:

// page.server.spec.ts — should already cover this from the modification
it('always fetches dashboard data regardless of URL params')
it('never calls /api/documents/search')

Edge cases the spec doesn't make explicit:

  • Document with documentDate = null — how does year grouping handle this? What year bucket does it fall into? Define behavior before implementing.
  • All documents from the same year — single card, many rows (no year boundary bugs)
  • Zero search results — empty state rendering (spec is silent on this; reuse existing empty state from DocumentList.svelte?)
  • Search results spanning 50+ years — performance and scroll behavior

Recommendations

  • Write documents/+page.server.spec.ts before documents/+page.server.ts — the test file structure from the existing spec is the template.
  • Add backend integration tests for completionPercentage edge cases (0 blocks, partial, full) using Testcontainers against real PostgreSQL — the calculation uses max(totalBlocks, 1) which is only correct at the SQL level, not an in-memory approximation.
  • Define and test the null documentDate grouping behavior explicitly — it will come up.
## 🧪 Sara Holt — QA Engineer ### Observations **The modified `page.server.spec.ts` is 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 new `documents/+page.server.ts`. **Required test coverage — before implementation:** **Backend (new queries):** ```java // completionPercentage — integration tests with Testcontainers + real PostgreSQL @Test void completionPercentage_is_zero_when_document_has_no_blocks() @Test void completionPercentage_is_100_when_all_blocks_are_reviewed() @Test void completionPercentage_rounds_correctly_for_partial_completion() // contributors @Test void contributors_returns_distinct_users_ordered_by_recency() @Test void contributors_limits_result_to_4_users() @Test void contributors_returns_empty_list_when_no_annotation_blocks_exist() @Test void contributors_handles_deleted_users_gracefully() ``` **Frontend `documents/+page.server.spec.ts`:** ```typescript // Mirror the structure of the existing page.server.spec.ts it('passes q, from, to, sort, dir, tags to search API') it('returns 404 for API errors with structured error message') it('redirects to /login when auth fails (401)') it('returns error string on network failure without throwing') ``` **Frontend components:** ```typescript // ProgressRing.svelte — unit tests it('renders filled arc for 75%') it('renders empty arc for 0%') it('renders full arc for 100%') it('shows gray label at 0%, mint label above 0%') // DocumentRow.svelte it('shows mobile grid when viewport < sm') it('shows right column when viewport >= sm') it('renders archive line only when archiveBox is set') ``` **Homepage regression:** ```typescript // page.server.spec.ts — should already cover this from the modification it('always fetches dashboard data regardless of URL params') it('never calls /api/documents/search') ``` **Edge cases the spec doesn't make explicit:** - Document with `documentDate = null` — how does year grouping handle this? What year bucket does it fall into? Define behavior before implementing. - All documents from the same year — single card, many rows (no year boundary bugs) - Zero search results — empty state rendering (spec is silent on this; reuse existing empty state from `DocumentList.svelte`?) - Search results spanning 50+ years — performance and scroll behavior ### Recommendations - Write `documents/+page.server.spec.ts` before `documents/+page.server.ts` — the test file structure from the existing spec is the template. - Add backend integration tests for `completionPercentage` edge cases (0 blocks, partial, full) using Testcontainers against real PostgreSQL — the calculation uses `max(totalBlocks, 1)` which is only correct at the SQL level, not an in-memory approximation. - Define and test the null `documentDate` grouping behavior explicitly — it will come up.
Author
Owner

🎨 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.

Element Spec class Rendered size Status
Progress ring percentage label text-[8px] 8px Below 12px minimum
Contributor initials (ContributorStack.svelte) 9px bold (existing component) 9px Below 12px minimum
"No contributors" label text-[9px] 9px Below 12px minimum
Meta labels (DATE, FROM, TO) text-[10px] 10px ⚠️ At absolute floor
Meta values text-[11px] 11px ⚠️ Below preferred 12px

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), and text-[11px] for the "No contributors" fallback. The contributor initials in the existing ContributorStack.svelte should also be increased from 9px to text-[10px].

Sticky bar magic numbers will cause overlap if the header ever changes.
The spec specifies top-[65px] for the search bar and top-[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: 65px in layout.css and 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 afterUpdate or a tick() + 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.svelte shows a dashed border circle with tooltip "Noch niemand angefangen". The spec says to render text-[9px] text "No contributors". These are different patterns — align with one before implementing.

Recommendations

  • Increase the progress ring percentage label from text-[8px] to text-[10px] minimum.
  • Increase the "No contributors" fallback text from text-[9px] to text-[11px].
  • Update ContributorStack.svelte initials font from 9px to text-[10px].
  • Extract the topbar height assumption into a single CSS custom property — don't hardcode 65 in three sticky top values.
  • Add focus management to the filter panel toggle: focus first input on open, return to toggle on close.

Open Decisions

  • "No contributors" empty state — dashed circle with tooltip (existing ContributorStack behavior) or plain text label (spec calls for). Both are accessible. The text label is more discoverable for new users.
## 🎨 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.** | Element | Spec class | Rendered size | Status | |---|---|---|---| | Progress ring percentage label | `text-[8px]` | 8px | ❌ Below 12px minimum | | Contributor initials (ContributorStack.svelte) | 9px bold (existing component) | 9px | ❌ Below 12px minimum | | "No contributors" label | `text-[9px]` | 9px | ❌ Below 12px minimum | | Meta labels (DATE, FROM, TO) | `text-[10px]` | 10px | ⚠️ At absolute floor | | Meta values | `text-[11px]` | 11px | ⚠️ Below preferred 12px | 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), and `text-[11px]` for the "No contributors" fallback. The contributor initials in the existing `ContributorStack.svelte` should also be increased from 9px to `text-[10px]`. **Sticky bar magic numbers will cause overlap if the header ever changes.** The spec specifies `top-[65px]` for the search bar and `top-[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: 65px` in `layout.css` and 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 `afterUpdate` or a `tick()` + `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.svelte` shows a dashed border circle with tooltip "Noch niemand angefangen". The spec says to render `text-[9px]` text "No contributors". These are different patterns — align with one before implementing. ### Recommendations - Increase the progress ring percentage label from `text-[8px]` to `text-[10px]` minimum. - Increase the "No contributors" fallback text from `text-[9px]` to `text-[11px]`. - Update `ContributorStack.svelte` initials font from 9px to `text-[10px]`. - Extract the topbar height assumption into a single CSS custom property — don't hardcode `65` in three sticky `top` values. - Add focus management to the filter panel toggle: focus first input on open, return to toggle on close. ### Open Decisions - **"No contributors" empty state** — dashed circle with tooltip (existing ContributorStack behavior) or plain text label (spec calls for). Both are accessible. The text label is more discoverable for new users.
Author
Owner

⚙️ 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 completionPercentage and contributors fields 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:

-- completionPercentage — needs to count blocks per document, filtered by status
CREATE INDEX IF NOT EXISTS idx_annotation_blocks_document_id ON annotation_blocks(document_id);
CREATE INDEX IF NOT EXISTS idx_annotation_blocks_document_status ON annotation_blocks(document_id, status);

-- contributors — needs to find distinct users ordered by recency
CREATE INDEX IF NOT EXISTS idx_annotation_blocks_document_created ON annotation_blocks(document_id, created_at DESC);

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 /documents route should be added to the smoke test suite. The current smoke test (if one exists) probably only verifies / and /api/actuator/health. The /documents page 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

  • Verify or create the annotation_blocks(document_id, status) and annotation_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.
  • Add GET /documents to 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.

## ⚙️ 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 `completionPercentage` and `contributors` fields 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: ```sql -- completionPercentage — needs to count blocks per document, filtered by status CREATE INDEX IF NOT EXISTS idx_annotation_blocks_document_id ON annotation_blocks(document_id); CREATE INDEX IF NOT EXISTS idx_annotation_blocks_document_status ON annotation_blocks(document_id, status); -- contributors — needs to find distinct users ordered by recency CREATE INDEX IF NOT EXISTS idx_annotation_blocks_document_created ON annotation_blocks(document_id, created_at DESC); ``` 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 `/documents` route should be added to the smoke test suite.** The current smoke test (if one exists) probably only verifies `/` and `/api/actuator/health`. The `/documents` page 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 - Verify or create the `annotation_blocks(document_id, status)` and `annotation_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. - Add `GET /documents` to 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.
Author
Owner

🗳️ Decision Queue — Action Required

2 decisions need your input before implementation starts.

Architecture

  • API response shape for completionPercentage + contributors — The current DocumentSearchResult wraps List<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) New DocumentSearchItem per-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

  • "No contributors" empty state in ContributorStack — The existing component shows a dashed circle with tooltip "Noch niemand angefangen". The spec calls for a plain-text label ("No contributors", text-[9px]). Both are accessible. Decision affects whether ContributorStack.svelte needs a behavior change or whether the documents page uses a different empty state pattern than other places the component is used. (Raised by: Leonie)
## 🗳️ Decision Queue — Action Required _2 decisions need your input before implementation starts._ ### Architecture - **API response shape for `completionPercentage` + `contributors`** — The current `DocumentSearchResult` wraps `List<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)** New `DocumentSearchItem` per-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 - **"No contributors" empty state in ContributorStack** — The existing component shows a dashed circle with tooltip "Noch niemand angefangen". The spec calls for a plain-text label ("No contributors", `text-[9px]`). Both are accessible. Decision affects whether `ContributorStack.svelte` needs a behavior change or whether the documents page uses a different empty state pattern than other places the component is used. _(Raised by: Leonie)_
Author
Owner

🏛️ Markus Keller — Architecture Discussion Summary

Decisions settled in discussion. Three open items, all resolved.

Resolved

  • API response shape → Option B. New DocumentSearchItem record: document, matchData, completionPercentage, contributors all collocated per item. DocumentSearchResult becomes List<DocumentSearchItem> + total. The existing Map<UUID, SearchMatchData> matchData on DocumentSearchResult is folded into the item — frontend drops the map lookups entirely. Regenerate TypeScript types before touching the frontend.

  • Contributor query → extract to AnnotationBlockRepository. New AnnotationBlockRepository in repository/ owns findContributorsForDocuments(documentIds) and the completion stats query. Both the dashboard and the document search call it from there. The existing AuditLogQueryRepository.findContributorsPerDocument() in the dashboard package either delegates to the new repo or gets replaced. No cross-package coupling.

  • Assembly point → DocumentService. DocumentService calls DocumentSearchRepository for documents + match data, calls AnnotationBlockRepository for contributors + completion stats, assembles DocumentSearchItem records 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.

## 🏛️ Markus Keller — Architecture Discussion Summary Decisions settled in discussion. Three open items, all resolved. ### Resolved - **API response shape → Option B.** New `DocumentSearchItem` record: `document`, `matchData`, `completionPercentage`, `contributors` all collocated per item. `DocumentSearchResult` becomes `List<DocumentSearchItem>` + `total`. The existing `Map<UUID, SearchMatchData> matchData` on `DocumentSearchResult` is folded into the item — frontend drops the map lookups entirely. Regenerate TypeScript types before touching the frontend. - **Contributor query → extract to `AnnotationBlockRepository`.** New `AnnotationBlockRepository` in `repository/` owns `findContributorsForDocuments(documentIds)` and the completion stats query. Both the dashboard and the document search call it from there. The existing `AuditLogQueryRepository.findContributorsPerDocument()` in the dashboard package either delegates to the new repo or gets replaced. No cross-package coupling. - **Assembly point → `DocumentService`.** `DocumentService` calls `DocumentSearchRepository` for documents + match data, calls `AnnotationBlockRepository` for contributors + completion stats, assembles `DocumentSearchItem` records 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.
Author
Owner

🎨 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.svelte behavior (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]. ContributorStack initials: 9px → text-[10px]. Both files need touching during implementation anyway.

  • Sticky bar offsets → CSS custom property. Add --header-height: 65px to layout.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.

## 🎨 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.svelte` behavior (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]`. `ContributorStack` initials: 9px → `text-[10px]`. Both files need touching during implementation anyway. - **Sticky bar offsets → CSS custom property.** Add `--header-height: 65px` to `layout.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.
Author
Owner

Implementation complete — PR #282 opened.

All 15 tasks implemented (TDD throughout):

Backend:

  • TranscriptionBlockRepository — bulk completion stats query (0%/partial/100% integration-tested)
  • AuditLogQueryRepositoryfindRecentContributorsForDocuments() (max 4, recency-ordered)
  • AuditLogQueryServicefindRecentContributorsPerDocument() map
  • New DocumentSearchItem record with all fields @Schema(requiredMode=REQUIRED)
  • DocumentSearchResult refactored to {items, total}
  • DocumentService.searchDocuments() assembles DocumentSearchItem with stats + contributors
  • Flyway V48: composite index on transcription_blocks(document_id, reviewed)

Frontend:

  • TypeScript API types regenerated
  • --header-height: 65px CSS custom property; ContributorStack initials text-[10px]; AppNav /documents link
  • Homepage (/) rewritten to pure dashboard — always shows widgets, never search
  • ProgressRing.svelte — SVG arc, spec-matched colours, 4 unit tests
  • DocumentRow.svelte — two-column layout, highlight offsets, tags, progress ring, contributor stack
  • DocumentList.svelte — year-card orchestrator with DocumentSearchItem[] props
  • documents/+page.server.ts — search load with full filter params, 7 unit tests
  • documents/+page.svelte — URL-driven filter state, SearchFilterBar + DocumentList

1006 frontend tests passing, backend tests green.

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()` map - New `DocumentSearchItem` record with all fields `@Schema(requiredMode=REQUIRED)` - `DocumentSearchResult` refactored to `{items, total}` - `DocumentService.searchDocuments()` assembles `DocumentSearchItem` with stats + contributors - Flyway V48: composite index on `transcription_blocks(document_id, reviewed)` Frontend: - TypeScript API types regenerated - `--header-height: 65px` CSS custom property; ContributorStack initials `text-[10px]`; AppNav `/documents` link - Homepage (`/`) rewritten to pure dashboard — always shows widgets, never search - `ProgressRing.svelte` — SVG arc, spec-matched colours, 4 unit tests - `DocumentRow.svelte` — two-column layout, highlight offsets, tags, progress ring, contributor stack - `DocumentList.svelte` — year-card orchestrator with `DocumentSearchItem[]` props - `documents/+page.server.ts` — search load with full filter params, 7 unit tests - `documents/+page.svelte` — URL-driven filter state, `SearchFilterBar` + `DocumentList` 1006 frontend tests passing, backend tests green.
Sign in to join this conversation.
No Label feature ui
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#281