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>
This commit is contained in:
@@ -186,6 +186,7 @@ C4Component
|
|||||||
Component(authCtrl, "AuthController", "Spring MVC — /api/auth", "Handles user registration (POST /register) and password reset token endpoints (/forgot-password, /reset-password).")
|
Component(authCtrl, "AuthController", "Spring MVC — /api/auth", "Handles user registration (POST /register) and password reset token endpoints (/forgot-password, /reset-password).")
|
||||||
|
|
||||||
Component(userSvc, "UserService", "Spring Service", "User CRUD with BCrypt password encoding, group assignment, and audit logging. Orchestrates invite-based registration and password reset tokens.")
|
Component(userSvc, "UserService", "Spring Service", "User CRUD with BCrypt password encoding, group assignment, and audit logging. Orchestrates invite-based registration and password reset tokens.")
|
||||||
|
Component(tagSvc, "TagService", "Spring Service", "Tag CRUD with name search, hierarchical tree structure, merge/reparent operations, and recursive subtree deletion.")
|
||||||
Component(dataInit, "DataInitializer", "CommandLineRunner", "On startup: creates default admin user and groups if none exist. Seeds test data if DB is empty.")
|
Component(dataInit, "DataInitializer", "CommandLineRunner", "On startup: creates default admin user and groups if none exist. Seeds test data if DB is empty.")
|
||||||
|
|
||||||
Component(userRepo, "AppUserRepository", "Spring Data JPA", "Finds users by email. Supports search by email or display name.")
|
Component(userRepo, "AppUserRepository", "Spring Data JPA", "Finds users by email. Supports search by email or display name.")
|
||||||
@@ -199,8 +200,9 @@ C4Component
|
|||||||
Rel(frontend, inviteCtrl, "Invite validation", "HTTP / JSON")
|
Rel(frontend, inviteCtrl, "Invite validation", "HTTP / JSON")
|
||||||
Rel(frontend, authCtrl, "Registration and password reset", "HTTP / JSON")
|
Rel(frontend, authCtrl, "Registration and password reset", "HTTP / JSON")
|
||||||
Rel(userCtrl, userSvc, "Delegates to", "")
|
Rel(userCtrl, userSvc, "Delegates to", "")
|
||||||
Rel(groupCtrl, groupRepo, "Reads / writes groups", "")
|
Rel(groupCtrl, userSvc, "Delegates to", "")
|
||||||
Rel(tagCtrl, tagRepo, "Reads / writes tags", "")
|
Rel(tagCtrl, tagSvc, "Delegates to", "")
|
||||||
|
Rel(tagSvc, tagRepo, "Reads / writes tags", "")
|
||||||
Rel(inviteCtrl, userSvc, "Creates and validates invites", "")
|
Rel(inviteCtrl, userSvc, "Creates and validates invites", "")
|
||||||
Rel(authCtrl, userSvc, "Registers users, resets passwords", "")
|
Rel(authCtrl, userSvc, "Registers users, resets passwords", "")
|
||||||
Rel(userSvc, userRepo, "Reads / writes users", "")
|
Rel(userSvc, userRepo, "Reads / writes users", "")
|
||||||
@@ -296,7 +298,9 @@ C4Component
|
|||||||
Component(auditSvc, "AuditService", "Spring Service — @Async", "Writes audit log entries asynchronously via a dedicated TaskExecutor, with transaction-aware logging to prevent deadlocks on concurrent saves.")
|
Component(auditSvc, "AuditService", "Spring Service — @Async", "Writes audit log entries asynchronously via a dedicated TaskExecutor, with transaction-aware logging to prevent deadlocks on concurrent saves.")
|
||||||
Component(auditQuery, "AuditLogQueryService", "Spring Service", "Queries audit logs for activity feeds, pulse stats, recent contributors, and per-document history. Facade over AuditLogRepository.")
|
Component(auditQuery, "AuditLogQueryService", "Spring Service", "Queries audit logs for activity feeds, pulse stats, recent contributors, and per-document history. Facade over AuditLogRepository.")
|
||||||
|
|
||||||
|
Component(dashCtrl, "DashboardController", "Spring MVC — /api/dashboard", "REST endpoints for the user dashboard: recent document resume (/resume), weekly transcription pulse stats (/pulse), and activity feed (/activity) with kind filtering and pagination.")
|
||||||
Component(statsCtrl, "StatsController", "Spring MVC — /api/stats", "Returns aggregate counts (total persons, total documents) for the UI stats bar.")
|
Component(statsCtrl, "StatsController", "Spring MVC — /api/stats", "Returns aggregate counts (total persons, total documents) for the UI stats bar.")
|
||||||
|
Component(statsSvc, "StatsService", "Spring Service", "Queries aggregate counts: total persons and total documents.")
|
||||||
Component(dashSvc, "DashboardService", "Spring Service", "Assembles the user dashboard: recent document resume, weekly transcription pulse stats, and activity feed with contributor avatars.")
|
Component(dashSvc, "DashboardService", "Spring Service", "Assembles the user dashboard: recent document resume, weekly transcription pulse stats, and activity feed with contributor avatars.")
|
||||||
|
|
||||||
Component(notifCtrl, "NotificationController", "Spring MVC — /api/notifications", "REST and SSE endpoints for notification stream, history with filtering, read/unread state, and per-user preference management.")
|
Component(notifCtrl, "NotificationController", "Spring MVC — /api/notifications", "REST and SSE endpoints for notification stream, history with filtering, read/unread state, and per-user preference management.")
|
||||||
@@ -309,9 +313,13 @@ C4Component
|
|||||||
Component(exHandler, "GlobalExceptionHandler", "Spring @RestControllerAdvice", "Converts DomainException, validation errors, and generic exceptions to ErrorResponse JSON with machine-readable ErrorCode and HTTP status.")
|
Component(exHandler, "GlobalExceptionHandler", "Spring @RestControllerAdvice", "Converts DomainException, validation errors, and generic exceptions to ErrorResponse JSON with machine-readable ErrorCode and HTTP status.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rel(frontend, dashCtrl, "Dashboard requests", "HTTP / JSON")
|
||||||
Rel(frontend, statsCtrl, "GET /api/stats", "HTTP / JSON")
|
Rel(frontend, statsCtrl, "GET /api/stats", "HTTP / JSON")
|
||||||
Rel(frontend, notifCtrl, "Notification stream and history", "HTTP / JSON / SSE")
|
Rel(frontend, notifCtrl, "Notification stream and history", "HTTP / JSON / SSE")
|
||||||
Rel(frontend, geschCtrl, "Story requests", "HTTP / JSON")
|
Rel(frontend, geschCtrl, "Story requests", "HTTP / JSON")
|
||||||
|
Rel(dashCtrl, dashSvc, "Delegates to", "")
|
||||||
|
Rel(statsCtrl, statsSvc, "Delegates to", "")
|
||||||
|
Rel(statsSvc, db, "Reads aggregate counts", "JDBC")
|
||||||
Rel(dashSvc, auditQuery, "Fetches activity feed and pulse stats", "")
|
Rel(dashSvc, auditQuery, "Fetches activity feed and pulse stats", "")
|
||||||
Rel(notifCtrl, notifSvc, "Delegates to", "")
|
Rel(notifCtrl, notifSvc, "Delegates to", "")
|
||||||
Rel(notifCtrl, sseRegistry, "Registers client SSE connection", "")
|
Rel(notifCtrl, sseRegistry, "Registers client SSE connection", "")
|
||||||
|
|||||||
165
docs/superpowers/specs/2026-05-06-reader-dashboard-design.md
Normal file
165
docs/superpowers/specs/2026-05-06-reader-dashboard-design.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# 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 |
|
||||||
Reference in New Issue
Block a user