Files
familienarchiv/docs/superpowers/specs/2026-05-06-reader-dashboard-design.md
Marcel 42c6e58d36 docs(c4): fix diagram 3c service layer and add missing 3e components
- 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>
2026-05-06 10:28:16 +02:00

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: 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 (<a> elements, not <div onclick>).
  • 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