- diagram 3c: GroupController delegates to UserService (not groupRepo directly) - diagram 3c: add TagService; TagController delegates to TagService (not tagRepo) - diagram 3e: add DashboardController serving /api/dashboard/resume|pulse|activity - diagram 3e: add StatsService; StatsController delegates to StatsService Addresses blocker feedback from Markus, Felix, and Elicit in PR #448 review. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.2 KiB
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 (<a>). 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: addisReaderflag 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 tilesReaderPersonChips.svelte— top-N person chips + overflow linkReaderRecentDocs.svelte— recent documents feedReaderRecentStories.svelte— recent stories feedReaderDraftsModule.svelte— draft stories (rendered conditionally oncanBlogWrite)
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 (
<a>elements, not<div onclick>). - NFR-RESP-001: Two-column row stacks to single column at
< md(768 px). Person chips wrap viaflex-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 |