# Reader Dashboard — Design Spec **Date:** 2026-05-06 **Status:** Approved for implementation planning --- ## Problem The archive has two distinct user groups: - **Contributors** — transcribe, annotate, upload. Comfortable with the current dashboard (MissionControlStrip, EnrichmentBlock, enrichment queue, activity feed). - **Readers** — browse and consume finished content. Older, less technical. Currently overwhelmed by contribution-focused UI they cannot use and do not need. --- ## Solution Introduce a **permission-gated reader dashboard** that replaces the current dashboard for users without `WRITE_ALL` or `ANNOTATE_ALL` (including pure readers and story writers). The contributor dashboard remains unchanged. --- ## Detection Logic ``` isReader = !canWrite && !canAnnotate showDrafts = canBlogWrite // overlays on reader dashboard only ``` `BLOG_WRITE` users land on the **reader dashboard** (not the contributor dashboard), because story writers are conceptually closer to readers than to transcribers. The drafts module appears on top of the reader layout when `canBlogWrite` is true. `canWrite`, `canAnnotate`, `canBlogWrite` are already derived in `+layout.server.ts` and available in `$page.data` on all routes. No new backend work required for detection. --- ## Reader Dashboard Layout Five zones, rendered top-to-bottom: ### 1. Greeting Unchanged from current dashboard — personalized, time-based greeting. Warm entry point for less technical users. ### 2. Stats Strip Three linked stat tiles in a single row: | Tile | Value | Link | |---|---|---| | Dokumente | Total document count | `/documents` | | Personen | Total person count | `/persons` | | Geschichten | Total published story count | `/geschichten` | Each tile is a full-area anchor (``). Values come from the existing stats endpoint already called by the current dashboard. ### 3. Drafts Module *(conditional — BLOG_WRITE only)* Shown only when `canBlogWrite` is true, between the stats strip and the person chips. - Section heading: "Meine Entwürfe" - Lists all draft stories authored by the current user, sorted by `updated_at DESC` - Each entry shows: story title + "Entwurf · zuletzt bearbeitet vor X" relative timestamp - Each entry links to `/geschichten/[id]/edit` - Empty state: "Keine Entwürfe" (no empty-state CTA needed — they can create from `/geschichten`) This module appears on the reader dashboard because a user can hold `READ_ALL + BLOG_WRITE` without `WRITE_ALL`. ### 4. Person Chips Top **4** persons by total document count, each linking to `/persons/[id]`. Followed by an overflow link "Alle N Personen →" pointing to `/persons`. - Chip content: display name + document count (e.g., "Käthe Raddatz · 23 Dok.") - Layout: `flex flex-wrap gap-2` — chips reflow gracefully at narrower viewports - Data source: persons list endpoint, sorted by document count DESC, limit 4 Rationale: readers most often browse by person rather than searching for a specific document. Surfacing the most-documented family members at the top answers "where do I start?" ### 5. Two-Column Content Row Side-by-side at desktop; stacks to single column on mobile (below `md` breakpoint). **Left column — "Zuletzt aktualisiert" (flex: 3)** 5 most recently updated documents, sorted by `updated_at DESC`. No status filter — uploads and transcription updates are both relevant. Each row shows: - Document thumbnail placeholder (or actual thumbnail if available) - Document title (linked to `/documents/[id]`) - Sender name (linked to `/persons/[id]`) if present; omitted if document has no sender + relative timestamp **Right column — "Geschichten" (flex: 2)** 3 most recently published stories. Each entry shows: - Story title (italic serif, linked to `/geschichten/[id]`) - First ~150 characters of body text as excerpt - Relative publication timestamp --- ## What Is Hidden from Readers | Component | Reason | |---|---| | `MissionControlStrip` | Transcription queue — contributor-only | | `EnrichmentBlock` | Incomplete-document workflow — contributor-only | | `DashboardResumeStrip` | Contribution resume metric — meaningless to readers | | `DashboardFamilyPulse` | Contribution-focused activity metrics | | `DashboardActivityFeed` | Replaced by the simpler "Zuletzt aktualisiert" feed | | `DropZone` | Already gated on `canWrite` — unchanged | --- ## Backend Changes The reader dashboard reuses data from endpoints already called by the existing dashboard where possible. New or adapted calls: | Data | Endpoint | Notes | |---|---|---| | Stats (docs, persons, stories) | Existing stats endpoint | Already fetched | | Top 4 persons by doc count | `GET /api/persons?sort=documentCount,desc&size=4` | Verify sort param exists; add if not | | Recent 5 documents | `GET /api/documents?sort=updatedAt,desc&size=5` | Verify sort param exists; add if not | | Recent 3 stories | `GET /api/geschichten?published=true&sort=updatedAt,desc&size=3` | Verify sort param and published filter | | Draft stories (BLOG_WRITE only) | `GET /api/geschichten?published=false&authorId=currentUser&size=10` | Verify author filter exists; add if not | The `+page.server.ts` load function should branch on `isReader`: fetch the reader data set instead of the contributor data set. This avoids loading transcription queues, enrichment data, and weekly stats for users who will never see them. --- ## Frontend Changes - `+page.server.ts`: add `isReader` flag derived from layout data; branch fetch logic - `+page.svelte`: conditional render — reader layout vs. current contributor layout - New components (all in `src/lib/shared/dashboard/`): - `ReaderStatsStrip.svelte` — the three linked stat tiles - `ReaderPersonChips.svelte` — top-N person chips + overflow link - `ReaderRecentDocs.svelte` — recent documents feed - `ReaderRecentStories.svelte` — recent stories feed - `ReaderDraftsModule.svelte` — draft stories (rendered conditionally on `canBlogWrite`) --- ## Non-Functional Requirements - **NFR-PERF-001**: Reader dashboard must load in ≤ 2 s on broadband (time-to-interactive). Achieved by fetching only the 4 lean endpoints above instead of the current 10. - **NFR-A11Y-001**: All stat tiles and person chips must be keyboard-navigable (`` elements, not `
`). - **NFR-RESP-001**: Two-column row stacks to single column at `< md` (768 px). Person chips wrap via `flex-wrap`. - **NFR-I18N-001**: All new section headings and labels must have keys in `messages/{de,en,es}.json`. --- ## Out of Scope - Mobile-specific reader dashboard (responsive reflow is sufficient for now) - Admin dashboard variant - Any change to the contributor dashboard - Personalization / "favourite persons" feature (possible future enhancement) - Notification or messaging features for readers --- ## Open Questions | ID | Question | Blocks | |---|---|---| | OQ-01 | Does `GET /api/persons` support `sort=documentCount,desc`? | ReaderPersonChips data | | OQ-02 | Does `GET /api/documents` support `sort=updatedAt,desc`? | ReaderRecentDocs data | | OQ-03 | Does the stories endpoint support `published=false` + author filter for drafts? | ReaderDraftsModule data | | OQ-04 | Should the "Zuletzt aktualisiert" label distinguish uploads from transcription updates (e.g., badge)? | ReaderRecentDocs UX |