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:
Marcel
2026-05-06 10:28:16 +02:00
parent e1f66e2e65
commit 42c6e58d36
2 changed files with 175 additions and 2 deletions

View File

@@ -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(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(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, authCtrl, "Registration and password reset", "HTTP / JSON")
Rel(userCtrl, userSvc, "Delegates to", "")
Rel(groupCtrl, groupRepo, "Reads / writes groups", "")
Rel(tagCtrl, tagRepo, "Reads / writes tags", "")
Rel(groupCtrl, userSvc, "Delegates to", "")
Rel(tagCtrl, tagSvc, "Delegates to", "")
Rel(tagSvc, tagRepo, "Reads / writes tags", "")
Rel(inviteCtrl, userSvc, "Creates and validates invites", "")
Rel(authCtrl, userSvc, "Registers users, resets passwords", "")
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(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(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(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.")
}
Rel(frontend, dashCtrl, "Dashboard requests", "HTTP / JSON")
Rel(frontend, statsCtrl, "GET /api/stats", "HTTP / JSON")
Rel(frontend, notifCtrl, "Notification stream and history", "HTTP / JSON / SSE")
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(notifCtrl, notifSvc, "Delegates to", "")
Rel(notifCtrl, sseRegistry, "Registers client SSE connection", "")

View 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 |