From aff15fd208ce0a2249a38e51f47f897e938454b0 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 09:51:46 +0200 Subject: [PATCH 01/34] docs(c4): split L3 monolith diagrams into five focused sub-diagrams MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend L3 split into 3a (Security & Auth), 3b (Document/File/Import), 3c (People/Users/Groups). Frontend L3 split into 3a (Middleware/Auth/Layout) and 3b (Pages & Shared Components). Each sub-diagram stays within dagre's clean-layout range (5–10 components, 6–12 relationships). Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 173 ++++++++++++++++++++----------- 1 file changed, 110 insertions(+), 63 deletions(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index a5b2b22b..8cf1dfe9 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -59,114 +59,171 @@ C4Container ## Level 3 — Components: API Backend -The internal structure of the Spring Boot backend. +The internal structure of the Spring Boot backend, split into three focused views. + +### 3a — Security & Authentication + +How requests are authenticated and write operations are authorised. ```mermaid C4Component - title Component Diagram: API Backend + title Component Diagram: API Backend — Security & Authentication + + Container(frontend, "Web Frontend", "SvelteKit") + ContainerDb(db, "PostgreSQL") + + System_Boundary(backend, "API Backend (Spring Boot)") { + Component(secFilter, "Security Filter Chain", "Spring Security", "Enforces authentication on all requests. Parses Basic Auth header and validates credentials via BCrypt.") + Component(permAspect, "PermissionAspect", "Spring AOP", "Intercepts methods annotated with @RequirePermission. Checks user's granted authorities against the required permission. Throws 401/403 if denied.") + Component(secConf, "SecurityConfig", "Spring @Configuration", "Configures filter chain: all routes require authentication, CSRF disabled, BCrypt password encoder, DaoAuthenticationProvider with CustomUserDetailsService.") + Component(userDetails, "CustomUserDetailsService", "Spring Security UserDetailsService", "Loads AppUser by username from DB. Converts group permissions to Spring GrantedAuthority objects.") + } + + Rel(frontend, secFilter, "All requests", "HTTP / Basic Auth header") + Rel(secFilter, permAspect, "Authenticated requests proceed to guarded methods", "AOP @Around") + Rel(secConf, userDetails, "Wires as UserDetailsService", "") + Rel(userDetails, db, "Loads user by username", "JDBC") +``` + +### 3b — Document, File & Import Domain + +Document management, file storage, and bulk Excel import. + +```mermaid +C4Component + title Component Diagram: API Backend — Document, File & Import Domain Container(frontend, "Web Frontend", "SvelteKit") ContainerDb(db, "PostgreSQL") ContainerDb(minio, "MinIO") System_Boundary(backend, "API Backend (Spring Boot)") { - - Component(secFilter, "Security Filter Chain", "Spring Security", "Enforces authentication on all requests. Parses Basic Auth header and validates credentials via BCrypt.") - Component(permAspect, "PermissionAspect", "Spring AOP", "Intercepts methods annotated with @RequirePermission. Checks user's granted authorities against the required permission. Throws 401/403 if denied.") - Component(docCtrl, "DocumentController", "Spring MVC — /api/documents", "CRUD for documents. Endpoints: search, get by ID, update metadata, upload file, download file, get conversation thread.") - Component(personCtrl, "PersonController", "Spring MVC — /api/persons", "Lists and searches family members. Also returns all documents sent by a person.") - Component(userCtrl, "UserController", "Spring MVC — /api/users", "Returns current user (/me). Creates and deletes users (requires ADMIN_USER permission).") Component(adminCtrl, "AdminController", "Spring MVC — /api/admin", "Triggers asynchronous Excel mass import (requires ADMIN permission).") - Component(groupCtrl, "GroupController", "Spring MVC — /api/groups", "Lists and manages permission groups.") - Component(tagCtrl, "TagController", "Spring MVC — /api/tags", "Lists tags for typeahead.") Component(docSvc, "DocumentService", "Spring Service", "Core business logic: store, update, search documents. Resolves persons and tags. Delegates file I/O to FileService. Builds JPA Specifications for dynamic search queries.") Component(fileSvc, "FileService", "Spring Service", "Wraps AWS SDK v2 S3Client. Uploads files with UUID-keyed paths. Downloads with content-type detection (PDF, JPEG, PNG, octet-stream).") - Component(excelSvc, "ExcelService", "Spring Service", "Parses Excel workbooks (Apache POI). Column indices are configurable via application.properties. Creates/updates document records per row.") Component(massImport, "MassImportService", "Spring Service — @Async", "Reads Excel files from /import mount. Delegates to ExcelService. Runs asynchronously so the HTTP response returns immediately.") - Component(userSvc, "UserService", "Spring Service", "User CRUD. Encodes passwords with BCrypt. Assigns users to permission groups.") - Component(dataInit, "DataInitializer", "CommandLineRunner", "On startup: creates default admin user and groups if none exist. Seeds test data (persons, documents) if DB is empty.") + Component(excelSvc, "ExcelService", "Spring Service", "Parses Excel workbooks (Apache POI). Column indices are configurable via application.properties. Creates/updates document records per row.") + Component(minioConf, "MinioConfig", "Spring @Configuration", "Creates the S3Client bean with path-style access for MinIO. Validates MinIO connectivity on startup.") Component(docRepo, "DocumentRepository", "Spring Data JPA", "Queries documents. Supports Specification-based dynamic search, conversation thread queries (bidirectional sender/receiver), and filename lookups.") Component(docSpec, "DocumentSpecifications", "JPA Criteria API", "Factory for composable query predicates: hasText (full-text across title/filename/transcription/location), hasSender, hasReceiver (join), isBetween (date range), hasTags (subquery AND logic).") - Component(personRepo, "PersonRepository", "Spring Data JPA", "Lists all persons sorted by last name. Supports name search for typeahead.") - Component(userRepo, "AppUserRepository", "Spring Data JPA", "Finds users by username. Used by Spring Security and UserService.") - Component(tagRepo, "TagRepository", "Spring Data JPA", "Finds or creates tags by name (case-insensitive).") - Component(groupRepo, "UserGroupRepository", "Spring Data JPA", "Manages permission groups.") - - Component(minioConf, "MinioConfig", "Spring @Configuration", "Creates the S3Client bean with path-style access for MinIO. Validates MinIO connectivity on startup.") - Component(secConf, "SecurityConfig", "Spring @Configuration", "Configures filter chain: all routes require authentication, CSRF disabled, BCrypt password encoder, DaoAuthenticationProvider with CustomUserDetailsService.") - Component(userDetails, "CustomUserDetailsService", "Spring Security UserDetailsService", "Loads AppUser by username from DB. Converts group permissions to Spring GrantedAuthority objects.") } - Rel(frontend, secFilter, "All requests", "HTTP / Basic Auth header") - Rel(secFilter, permAspect, "Authenticated requests proceed", "") - - Rel(secFilter, docCtrl, "Routes to", "") - Rel(secFilter, personCtrl, "Routes to", "") - Rel(secFilter, userCtrl, "Routes to", "") - Rel(secFilter, adminCtrl, "Routes to", "") - - Rel(permAspect, docCtrl, "Guards", "AOP @Around") - Rel(permAspect, userCtrl, "Guards", "AOP @Around") - Rel(permAspect, adminCtrl, "Guards", "AOP @Around") + Component(personRepo, "PersonRepository", "Spring Data JPA", "See diagram 3c. Used here by DocumentService to resolve sender / receiver persons.") + Component(tagRepo, "TagRepository", "Spring Data JPA", "See diagram 3c. Used here by DocumentService to find or create tags.") + Rel(frontend, docCtrl, "Document requests", "HTTP / JSON") + Rel(frontend, adminCtrl, "Trigger import", "HTTP / JSON") Rel(docCtrl, docSvc, "Delegates to", "") Rel(adminCtrl, massImport, "Triggers", "") - Rel(userCtrl, userSvc, "Delegates to", "") - Rel(docSvc, fileSvc, "Upload / download files", "") Rel(docSvc, docRepo, "Reads / writes documents", "") Rel(docSvc, docSpec, "Builds search predicates", "") Rel(docSvc, personRepo, "Resolves sender / receivers", "") Rel(docSvc, tagRepo, "Finds or creates tags", "") - Rel(massImport, excelSvc, "Parses Excel file", "") Rel(excelSvc, docSvc, "Creates / updates documents", "") - - Rel(userSvc, userRepo, "Reads / writes users", "") - Rel(userSvc, groupRepo, "Assigns groups", "") - Rel(userDetails, userRepo, "Loads user by username", "") - + Rel(minioConf, fileSvc, "Provides S3Client bean", "") Rel(fileSvc, minio, "PUT / GET objects", "S3 API / HTTP") Rel(docRepo, db, "SQL queries", "JDBC") Rel(personRepo, db, "SQL queries", "JDBC") - Rel(userRepo, db, "SQL queries", "JDBC") Rel(tagRepo, db, "SQL queries", "JDBC") - Rel(groupRepo, db, "SQL queries", "JDBC") +``` + +### 3c — People, Users & Group Administration + +Person, user, and group management, including startup seed data. + +```mermaid +C4Component + title Component Diagram: API Backend — People, Users & Group Administration + + Container(frontend, "Web Frontend", "SvelteKit") + ContainerDb(db, "PostgreSQL") + + System_Boundary(backend, "API Backend (Spring Boot)") { + Component(personCtrl, "PersonController", "Spring MVC — /api/persons", "Lists and searches family members. Also returns all documents sent by a person.") + Component(userCtrl, "UserController", "Spring MVC — /api/users", "Returns current user (/me). Creates and deletes users (requires ADMIN_USER permission).") + Component(groupCtrl, "GroupController", "Spring MVC — /api/groups", "Lists and manages permission groups.") + Component(tagCtrl, "TagController", "Spring MVC — /api/tags", "Lists tags for typeahead.") + + Component(userSvc, "UserService", "Spring Service", "User CRUD. Encodes passwords with BCrypt. Assigns users to permission groups.") + Component(dataInit, "DataInitializer", "CommandLineRunner", "On startup: creates default admin user and groups if none exist. Seeds test data (persons, documents) if DB is empty.") + + Component(personRepo, "PersonRepository", "Spring Data JPA", "Lists all persons sorted by last name. Supports name search for typeahead.") + Component(userRepo, "AppUserRepository", "Spring Data JPA", "Finds users by username. Used by Spring Security and UserService.") + Component(groupRepo, "UserGroupRepository", "Spring Data JPA", "Manages permission groups.") + Component(tagRepo, "TagRepository", "Spring Data JPA", "Finds or creates tags by name (case-insensitive).") + } + + Rel(frontend, personCtrl, "Person requests", "HTTP / JSON") + Rel(frontend, userCtrl, "User requests", "HTTP / JSON") + Rel(frontend, groupCtrl, "Group requests", "HTTP / JSON") + Rel(frontend, tagCtrl, "Tag requests", "HTTP / JSON") + Rel(personCtrl, personRepo, "Reads persons", "") + Rel(userCtrl, userSvc, "Delegates to", "") + Rel(groupCtrl, groupRepo, "Reads / writes groups", "") + Rel(tagCtrl, tagRepo, "Lists tags", "") + Rel(userSvc, userRepo, "Reads / writes users", "") + Rel(userSvc, groupRepo, "Assigns groups", "") Rel(dataInit, db, "Seeds initial data", "JDBC") - Rel(secConf, userDetails, "Wires", "") - Rel(minioConf, fileSvc, "Provides S3Client bean", "") + Rel(personRepo, db, "SQL queries", "JDBC") + Rel(userRepo, db, "SQL queries", "JDBC") + Rel(groupRepo, db, "SQL queries", "JDBC") + Rel(tagRepo, db, "SQL queries", "JDBC") ``` --- ## Level 3 — Components: Web Frontend -The internal structure of the SvelteKit frontend. +The internal structure of the SvelteKit frontend, split into two focused views. + +### 3a — Middleware, Auth & Layout + +Per-request middleware: session validation, i18n, and auth cookie handling. ```mermaid C4Component - title Component Diagram: Web Frontend + title Component Diagram: Web Frontend — Middleware, Auth & Layout Person(user, "User") Container(backend, "API Backend", "Spring Boot") System_Boundary(frontend, "Web Frontend (SvelteKit / SSR)") { - Component(hooks, "hooks.server.ts", "SvelteKit Server Hook", "Two responsibilities: (1) userGroup handle — reads auth_token cookie, fetches /api/users/me, stores user in event.locals. (2) handleFetch — intercepts all outgoing fetch() calls, injects Authorization header from cookie. Redirects to /login if token absent.") Component(i18n, "hooks.ts (Paraglide)", "SvelteKit Client Hook", "Client-side i18n middleware. Detects language from URL and sets the active locale for Paraglide.js translation functions.") - Component(layout, "+layout.server.ts", "SvelteKit Layout Loader", "Passes event.locals.user down to all child pages so every route has access to the authenticated user.") + Component(loginPage, "/login", "SvelteKit Route", "Form action: encodes username:password as Base64 Basic Auth token, POSTs to /api/users/me to validate, sets auth_token httpOnly cookie (SameSite=strict, maxAge=86400), redirects to /.") + Component(logoutPage, "/logout", "SvelteKit Route (server-only)", "Clears the auth_token cookie and redirects to /login.") + } + Rel(user, hooks, "Every browser request", "HTTPS") + Rel(hooks, backend, "GET /api/users/me (session check)", "HTTP / Basic Auth") + Rel(hooks, loginPage, "Redirect if no token", "") + Rel(hooks, layout, "Stores authenticated user in event.locals", "") + Rel(loginPage, backend, "POST /api/users/me (auth check)", "HTTP / Basic Auth") +``` + +### 3b — Pages & Shared Components + +Application pages and reusable UI components. + +```mermaid +C4Component + title Component Diagram: Web Frontend — Pages & Shared Components + + Person(user, "User") + Container(backend, "API Backend", "Spring Boot") + + System_Boundary(frontend, "Web Frontend (SvelteKit / SSR)") { Component(homePage, "/ (Home / Search)", "SvelteKit Route", "Loader: parses URL search params (q, from, to, senderId, receiverId, tags), fetches /api/documents/search and /api/persons, returns results. Page: renders search form with full-text, date range, sender/receiver typeahead, tag filters. Displays paginated document list.") Component(docDetail, "/documents/[id]", "SvelteKit Route", "Loader: fetches /api/documents/{id}. Handles 401 redirect to login, 404 error. Page: shows document metadata, file viewer (PDF/image inline), transcription, tags.") Component(docEdit, "/documents/[id]/edit", "SvelteKit Route", "Form with PersonTypeahead for sender/receiver, TagInput for tags, date/location fields. Submits PUT to /api/documents/{id}.") Component(persons, "/persons and /persons/[id]", "SvelteKit Routes", "Lists all persons. Detail page shows person metadata and all documents they sent.") Component(conversations, "/conversations", "SvelteKit Route", "Selects two persons via PersonTypeahead, fetches /api/documents/conversation, displays chronological exchange.") - Component(loginPage, "/login", "SvelteKit Route", "Form action: encodes username:password as Base64 Basic Auth token, POSTs to /api/users/me to validate, sets auth_token httpOnly cookie (SameSite=strict, maxAge=86400), redirects to /.") - Component(logoutPage, "/logout", "SvelteKit Route (server-only)", "Clears the auth_token cookie and redirects to /login.") Component(adminPage, "/admin", "SvelteKit Route", "User management UI (create/delete users). Excel import trigger button (calls /api/admin/trigger-import).") Component(apiPersons, "/api/persons (SvelteKit API)", "SvelteKit Server Route", "Proxies GET /api/persons?q=... to backend. Used by PersonTypeahead for typeahead suggestions.") @@ -176,32 +233,22 @@ C4Component Component(tagInput, "TagInput.svelte", "Svelte Component", "Multi-tag input. Supports free-text entry and selecting existing tags from /api/tags.") } - Rel(user, hooks, "Every browser request", "HTTPS") - Rel(hooks, backend, "GET /api/users/me (session check)", "HTTP / Basic Auth") - Rel(hooks, loginPage, "Redirect if no token", "") - - Rel(layout, homePage, "Provides user context", "") - Rel(layout, docDetail, "Provides user context", "") - Rel(layout, adminPage, "Provides user context", "") - + Rel(user, homePage, "Searches and browses", "HTTPS / Browser") Rel(homePage, backend, "GET /api/documents/search", "HTTP / JSON") Rel(homePage, backend, "GET /api/persons", "HTTP / JSON") Rel(docDetail, backend, "GET /api/documents/{id}", "HTTP / JSON") Rel(docDetail, backend, "GET /api/documents/{id}/file", "HTTP / Binary stream") Rel(docEdit, backend, "PUT /api/documents/{id}", "HTTP / Multipart") Rel(conversations, backend, "GET /api/documents/conversation", "HTTP / JSON") - Rel(loginPage, backend, "POST /api/users/me (auth check)", "HTTP / Basic Auth") Rel(adminPage, backend, "GET/POST/DELETE /api/users", "HTTP / JSON") Rel(adminPage, backend, "POST /api/admin/trigger-import", "HTTP / JSON") - - Rel(apiPersons, backend, "GET /api/persons", "HTTP / JSON") - Rel(apiTags, backend, "GET /api/tags", "HTTP / JSON") - Rel(homePage, typeahead, "Uses for sender/receiver filter", "") Rel(docEdit, typeahead, "Uses for sender/receiver selection", "") Rel(docEdit, tagInput, "Uses for tag management", "") Rel(typeahead, apiPersons, "Fetches suggestions", "HTTP") Rel(tagInput, apiTags, "Fetches existing tags", "HTTP") + Rel(apiPersons, backend, "GET /api/persons", "HTTP / JSON") + Rel(apiTags, backend, "GET /api/tags", "HTTP / JSON") ``` --- -- 2.49.1 From d4eb0edfe14ea58a2305c1f2409bb7077f92f206 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 09:53:38 +0200 Subject: [PATCH 02/34] docs(c4): update L1 personas and L2 frontend container description --- docs/architecture/c4-diagrams.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 8cf1dfe9..e8df323a 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -10,13 +10,13 @@ Who uses the system and what external systems does it interact with. C4Context title System Context: Familienarchiv - Person(admin, "Administrator", "Manages users, triggers bulk imports, reviews documents") - Person(member, "Family Member", "Searches, browses, and reads archived documents") + Person(admin, "Administrator", "Manages users, triggers bulk imports, reviews and transcribes documents") + Person(member, "Family Member", "Searches, browses, reads, and transcribes archived documents") System(familienarchiv, "Familienarchiv", "Web application for digitising, organising, and searching family documents") Rel(admin, familienarchiv, "Manages via browser", "HTTPS") - Rel(member, familienarchiv, "Searches and views via browser", "HTTPS") + Rel(member, familienarchiv, "Searches, reads, and transcribes via browser", "HTTPS") ``` --- @@ -32,7 +32,7 @@ C4Container Person(user, "User", "Admin or family member") System_Boundary(archiv, "Familienarchiv (Docker Compose)") { - Container(frontend, "Web Frontend", "SvelteKit / Node.js", "Server-side rendered UI. Handles session cookies, search UI, document viewer, and admin panel.") + Container(frontend, "Web Frontend", "SvelteKit / Node.js", "Server-side rendered UI. Handles auth session cookies, document search and viewer, transcription editor, annotation layer, family tree (Stammbaum), stories (Geschichten), activity feed (Chronik), enrichment workflow, and admin panel.") Container(backend, "API Backend", "Spring Boot 4 / Java 21 / Jetty", "REST API. Implements document management, search, user auth, file upload/download, transcription, OCR orchestration, and SSE notifications.") -- 2.49.1 From d51c5e9b8071db9ac30ebe92af28dbf5c065b440 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 09:54:31 +0200 Subject: [PATCH 03/34] =?UTF-8?q?docs(c4):=20fix=203a=20security=20?= =?UTF-8?q?=E2=80=94=20email=20field,=20permitted=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture/c4-diagrams.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index e8df323a..ef158937 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -73,16 +73,16 @@ C4Component ContainerDb(db, "PostgreSQL") System_Boundary(backend, "API Backend (Spring Boot)") { - Component(secFilter, "Security Filter Chain", "Spring Security", "Enforces authentication on all requests. Parses Basic Auth header and validates credentials via BCrypt.") + Component(secFilter, "Security Filter Chain", "Spring Security", "Enforces authentication on all requests. Parses Basic Auth header and validates credentials via BCrypt. Permits password-reset, invite, and register endpoints without authentication.") Component(permAspect, "PermissionAspect", "Spring AOP", "Intercepts methods annotated with @RequirePermission. Checks user's granted authorities against the required permission. Throws 401/403 if denied.") Component(secConf, "SecurityConfig", "Spring @Configuration", "Configures filter chain: all routes require authentication, CSRF disabled, BCrypt password encoder, DaoAuthenticationProvider with CustomUserDetailsService.") - Component(userDetails, "CustomUserDetailsService", "Spring Security UserDetailsService", "Loads AppUser by username from DB. Converts group permissions to Spring GrantedAuthority objects.") + Component(userDetails, "CustomUserDetailsService", "Spring Security UserDetailsService", "Loads AppUser by email from DB. Converts group permissions to Spring GrantedAuthority objects. Logs unknown permissions.") } Rel(frontend, secFilter, "All requests", "HTTP / Basic Auth header") Rel(secFilter, permAspect, "Authenticated requests proceed to guarded methods", "AOP @Around") Rel(secConf, userDetails, "Wires as UserDetailsService", "") - Rel(userDetails, db, "Loads user by username", "JDBC") + Rel(userDetails, db, "Loads user by email", "JDBC") ``` ### 3b — Document, File & Import Domain -- 2.49.1 From 1e3423934c1d41c3fec01940822b913967c7fbd1 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 09:55:28 +0200 Subject: [PATCH 04/34] =?UTF-8?q?docs(c4):=20update=203b=20document=20doma?= =?UTF-8?q?in=20=E2=80=94=20descriptions,=20batch=20ops,=20FTS,=20presigne?= =?UTF-8?q?d=20URLs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture/c4-diagrams.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index ef158937..4e32ffaf 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -85,33 +85,33 @@ C4Component Rel(userDetails, db, "Loads user by email", "JDBC") ``` -### 3b — Document, File & Import Domain +### 3b — Document Management & Import -Document management, file storage, and bulk Excel import. +Document management, file storage, and bulk Excel/ODS import. ```mermaid C4Component - title Component Diagram: API Backend — Document, File & Import Domain + title Component Diagram: API Backend — Document Management & Import Container(frontend, "Web Frontend", "SvelteKit") ContainerDb(db, "PostgreSQL") ContainerDb(minio, "MinIO") System_Boundary(backend, "API Backend (Spring Boot)") { - Component(docCtrl, "DocumentController", "Spring MVC — /api/documents", "CRUD for documents. Endpoints: search, get by ID, update metadata, upload file, download file, get conversation thread.") - Component(adminCtrl, "AdminController", "Spring MVC — /api/admin", "Triggers asynchronous Excel mass import (requires ADMIN permission).") + Component(docCtrl, "DocumentController", "Spring MVC — /api/documents", "CRUD for documents: search, get by ID, update metadata, upload/download file, conversation thread, and batch metadata updates.") + Component(adminCtrl, "AdminController", "Spring MVC — /api/admin", "Triggers asynchronous Excel/ODS mass import (requires ADMIN permission). Reports import state (IDLE/RUNNING/DONE/FAILED).") - Component(docSvc, "DocumentService", "Spring Service", "Core business logic: store, update, search documents. Resolves persons and tags. Delegates file I/O to FileService. Builds JPA Specifications for dynamic search queries.") - Component(fileSvc, "FileService", "Spring Service", "Wraps AWS SDK v2 S3Client. Uploads files with UUID-keyed paths. Downloads with content-type detection (PDF, JPEG, PNG, octet-stream).") - Component(massImport, "MassImportService", "Spring Service — @Async", "Reads Excel files from /import mount. Delegates to ExcelService. Runs asynchronously so the HTTP response returns immediately.") - Component(excelSvc, "ExcelService", "Spring Service", "Parses Excel workbooks (Apache POI). Column indices are configurable via application.properties. Creates/updates document records per row.") - Component(minioConf, "MinioConfig", "Spring @Configuration", "Creates the S3Client bean with path-style access for MinIO. Validates MinIO connectivity on startup.") + Component(docSvc, "DocumentService", "Spring Service", "Core document business logic: store, update, search. Resolves persons and tags, delegates file I/O to FileService, builds dynamic JPA Specifications, and integrates with audit logging.") + Component(fileSvc, "FileService", "Spring Service", "Wraps AWS SDK v2 S3Client. Uploads files with UUID-keyed paths, computes SHA-256 hash, downloads with content-type detection, and generates presigned URLs for OCR access.") + Component(massImport, "MassImportService", "Spring Service — @Async", "Reads Excel/ODS files from /import mount. Tracks import state (IDLE/RUNNING/DONE/FAILED) and delegates to ExcelService. Returns immediately; processing runs asynchronously.") + Component(excelSvc, "ExcelService", "Spring Service", "Parses Excel/ODS workbooks (Apache POI). Column indices configurable via application.properties. Creates/updates document records per row.") + Component(minioConf, "MinioConfig", "Spring @Configuration", "Creates the S3Client and S3Presigner beans with path-style access for MinIO. Validates MinIO connectivity on startup.") - Component(docRepo, "DocumentRepository", "Spring Data JPA", "Queries documents. Supports Specification-based dynamic search, conversation thread queries (bidirectional sender/receiver), and filename lookups.") - Component(docSpec, "DocumentSpecifications", "JPA Criteria API", "Factory for composable query predicates: hasText (full-text across title/filename/transcription/location), hasSender, hasReceiver (join), isBetween (date range), hasTags (subquery AND logic).") + Component(docRepo, "DocumentRepository", "Spring Data JPA", "Queries documents with Specification-based dynamic search, bidirectional conversation thread queries, full-text search with ranking and match highlighting, and transcription pipeline queue projections.") + Component(docSpec, "DocumentSpecifications", "JPA Criteria API", "Factory for composable predicates: hasText (full-text), hasSender, hasReceiver, isBetween (date range), hasTags (subquery AND/OR logic).") } - Component(personRepo, "PersonRepository", "Spring Data JPA", "See diagram 3c. Used here by DocumentService to resolve sender / receiver persons.") + Component(personRepo, "PersonRepository", "Spring Data JPA", "See diagram 3c.2. Used here by DocumentService to resolve sender / receiver persons.") Component(tagRepo, "TagRepository", "Spring Data JPA", "See diagram 3c. Used here by DocumentService to find or create tags.") Rel(frontend, docCtrl, "Document requests", "HTTP / JSON") @@ -123,10 +123,10 @@ C4Component Rel(docSvc, docSpec, "Builds search predicates", "") Rel(docSvc, personRepo, "Resolves sender / receivers", "") Rel(docSvc, tagRepo, "Finds or creates tags", "") - Rel(massImport, excelSvc, "Parses Excel file", "") + Rel(massImport, excelSvc, "Parses Excel/ODS file", "") Rel(excelSvc, docSvc, "Creates / updates documents", "") - Rel(minioConf, fileSvc, "Provides S3Client bean", "") - Rel(fileSvc, minio, "PUT / GET objects", "S3 API / HTTP") + Rel(minioConf, fileSvc, "Provides S3Client and S3Presigner beans", "") + Rel(fileSvc, minio, "PUT / GET / presigned URL objects", "S3 API / HTTP") Rel(docRepo, db, "SQL queries", "JDBC") Rel(personRepo, db, "SQL queries", "JDBC") Rel(tagRepo, db, "SQL queries", "JDBC") -- 2.49.1 From 04b2b33d7fa78df8b10b5ee90b4843677ad59d73 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 09:56:19 +0200 Subject: [PATCH 05/34] =?UTF-8?q?docs(c4):=20add=203b.2=20transcription=20?= =?UTF-8?q?pipeline=20=E2=80=94=20annotations,=20blocks,=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture/c4-diagrams.md | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 4e32ffaf..cfe88f68 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -132,6 +132,41 @@ C4Component Rel(tagRepo, db, "SQL queries", "JDBC") ``` +### 3b.2 — Document Transcription Pipeline + +Annotation-driven transcription: page markup, text blocks, versioning, and comment threads. + +```mermaid +C4Component + title Component Diagram: API Backend — Document Transcription Pipeline + + Container(frontend, "Web Frontend", "SvelteKit") + ContainerDb(db, "PostgreSQL") + + System_Boundary(backend, "API Backend (Spring Boot)") { + Component(transcriptionCtrl, "TranscriptionBlockController", "Spring MVC — /api/transcription", "CRUD for transcription text blocks per document page. Manages sort order, review status, and block version history.") + Component(annotationCtrl, "AnnotationController", "Spring MVC — /api/documents/{id}/annotations", "CRUD for free-form page annotations with polygon coordinates, colour coding, and file-hash tracking.") + Component(commentCtrl, "CommentController", "Spring MVC — /api/documents/{id}/comments", "Threaded comment CRUD on transcription blocks with @mention support and notification triggers.") + + Component(transcriptionSvc, "TranscriptionService", "Spring Service", "Creates and updates transcription blocks from annotation regions. Tracks block versions, sanitizes text with an HTML allow-list, and triggers mentions.") + Component(transcriptionQueueSvc, "TranscriptionQueueService", "Spring Service", "Exposes segmentation, transcription, and review queue projections for the mission-control enrichment workflow.") + Component(annotationSvc, "AnnotationService", "Spring Service", "Manages document page annotations with polygon coordinates. Called by OcrAsyncRunner to persist OCR-generated block boundaries.") + Component(commentSvc, "CommentService", "Spring Service", "Creates and manages threaded comments with @mention parsing. Triggers NotificationService for REPLY and MENTION events.") + } + + Rel(frontend, transcriptionCtrl, "Transcription block requests", "HTTP / JSON") + Rel(frontend, annotationCtrl, "Annotation requests", "HTTP / JSON") + Rel(frontend, commentCtrl, "Comment requests", "HTTP / JSON") + Rel(transcriptionCtrl, transcriptionSvc, "Delegates to", "") + Rel(transcriptionCtrl, transcriptionQueueSvc, "Queries pipeline queues", "") + Rel(annotationCtrl, annotationSvc, "Delegates to", "") + Rel(commentCtrl, commentSvc, "Delegates to", "") + Rel(transcriptionSvc, db, "Reads / writes blocks and versions", "JDBC") + Rel(annotationSvc, db, "Reads / writes annotations", "JDBC") + Rel(commentSvc, db, "Reads / writes comments", "JDBC") + Rel(transcriptionQueueSvc, db, "Queue projection queries", "JDBC") +``` + ### 3c — People, Users & Group Administration Person, user, and group management, including startup seed data. -- 2.49.1 From dbbb989aa65efde584a027d6f6f0a6ce77cb17fe Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 09:57:39 +0200 Subject: [PATCH 06/34] docs(c4): restructure 3c users/groups, add 3c.2 persons and family graph --- docs/architecture/c4-diagrams.md | 64 ++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index cfe88f68..6161aa01 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -167,49 +167,83 @@ C4Component Rel(transcriptionQueueSvc, db, "Queue projection queries", "JDBC") ``` -### 3c — People, Users & Group Administration +### 3c — Users, Groups & Administration -Person, user, and group management, including startup seed data. +User lifecycle, permission groups, tag management, and authentication endpoints. ```mermaid C4Component - title Component Diagram: API Backend — People, Users & Group Administration + title Component Diagram: API Backend — Users, Groups & Administration Container(frontend, "Web Frontend", "SvelteKit") ContainerDb(db, "PostgreSQL") System_Boundary(backend, "API Backend (Spring Boot)") { - Component(personCtrl, "PersonController", "Spring MVC — /api/persons", "Lists and searches family members. Also returns all documents sent by a person.") - Component(userCtrl, "UserController", "Spring MVC — /api/users", "Returns current user (/me). Creates and deletes users (requires ADMIN_USER permission).") + Component(userCtrl, "UserController", "Spring MVC — /api/users", "Returns current user (/me), creates and deletes users (requires ADMIN_USER), supports user search and profile updates.") Component(groupCtrl, "GroupController", "Spring MVC — /api/groups", "Lists and manages permission groups.") - Component(tagCtrl, "TagController", "Spring MVC — /api/tags", "Lists tags for typeahead.") + Component(tagCtrl, "TagController", "Spring MVC — /api/tags", "Lists tags for typeahead, supports tag merge, tree structure, and subtree deletion.") + Component(inviteCtrl, "InviteController", "Spring MVC — /api/auth/invite", "Creates invite codes and validates them at registration time. Rate-limited via WebConfig interceptor.") + 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. Encodes passwords with BCrypt. Assigns users to permission groups.") - Component(dataInit, "DataInitializer", "CommandLineRunner", "On startup: creates default admin user and groups if none exist. Seeds test data (persons, documents) if DB is empty.") + 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(dataInit, "DataInitializer", "CommandLineRunner", "On startup: creates default admin user and groups if none exist. Seeds test data if DB is empty.") - Component(personRepo, "PersonRepository", "Spring Data JPA", "Lists all persons sorted by last name. Supports name search for typeahead.") - Component(userRepo, "AppUserRepository", "Spring Data JPA", "Finds users by username. Used by Spring Security and UserService.") + Component(userRepo, "AppUserRepository", "Spring Data JPA", "Finds users by email. Supports search by email or display name.") Component(groupRepo, "UserGroupRepository", "Spring Data JPA", "Manages permission groups.") - Component(tagRepo, "TagRepository", "Spring Data JPA", "Finds or creates tags by name (case-insensitive).") + Component(tagRepo, "TagRepository", "Spring Data JPA", "Finds or creates tags by name (case-insensitive). Supports recursive ancestor/descendant CTE queries and merge/reparent helpers.") } - Rel(frontend, personCtrl, "Person requests", "HTTP / JSON") Rel(frontend, userCtrl, "User requests", "HTTP / JSON") Rel(frontend, groupCtrl, "Group requests", "HTTP / JSON") Rel(frontend, tagCtrl, "Tag requests", "HTTP / JSON") - Rel(personCtrl, personRepo, "Reads persons", "") + 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, "Lists tags", "") + Rel(tagCtrl, tagRepo, "Reads / writes tags", "") + Rel(inviteCtrl, userSvc, "Creates and validates invites", "") + Rel(authCtrl, userSvc, "Registers users, resets passwords", "") Rel(userSvc, userRepo, "Reads / writes users", "") Rel(userSvc, groupRepo, "Assigns groups", "") Rel(dataInit, db, "Seeds initial data", "JDBC") - Rel(personRepo, db, "SQL queries", "JDBC") Rel(userRepo, db, "SQL queries", "JDBC") Rel(groupRepo, db, "SQL queries", "JDBC") Rel(tagRepo, db, "SQL queries", "JDBC") ``` +### 3c.2 — Persons & Family Graph + +Person management including family relationship modelling and transitive inference. + +```mermaid +C4Component + title Component Diagram: API Backend — Persons & Family Graph + + Container(frontend, "Web Frontend", "SvelteKit") + ContainerDb(db, "PostgreSQL") + + System_Boundary(backend, "API Backend (Spring Boot)") { + Component(personCtrl, "PersonController", "Spring MVC — /api/persons", "Lists and searches family members. Returns documents sent by or received by a person, correspondent suggestions, and person summary with document counts.") + Component(relCtrl, "RelationshipController", "Spring MVC — /api/network, /api/persons/{id}/relationships", "CRUD for explicit person relationships and the full family network graph (nodes + edges) used by the Stammbaum view.") + + Component(personSvc, "PersonService", "Spring Service", "Person CRUD, alias management, and merge operations (reassigns all document sender/receiver references before deleting duplicate persons).") + Component(relSvc, "RelationshipService", "Spring Service", "Manages explicit directional family relationships (PARENT_OF, SPOUSE_OF, SIBLING_OF, etc.) with optional date ranges and notes.") + Component(relInference, "RelationshipInferenceService", "Spring Service", "Computes transitive family relationships from explicit edges to infer grandparent/grandchild, aunt/uncle, and other extended-family links for the network graph.") + + Component(personRepo, "PersonRepository", "Spring Data JPA", "Queries persons with name search (including aliases), correspondent discovery, person summaries with document counts, and merge/reassignment helpers.") + } + + Rel(frontend, personCtrl, "Person requests", "HTTP / JSON") + Rel(frontend, relCtrl, "Relationship and graph requests", "HTTP / JSON") + Rel(personCtrl, personSvc, "Delegates to", "") + Rel(relCtrl, relSvc, "Delegates to", "") + Rel(relCtrl, relInference, "Queries inferred graph", "") + Rel(personSvc, personRepo, "Reads / writes persons", "") + Rel(relSvc, db, "Reads / writes relationships", "JDBC") + Rel(relInference, db, "Reads relationships for inference", "JDBC") + Rel(personRepo, db, "SQL queries", "JDBC") +``` + --- ## Level 3 — Components: Web Frontend -- 2.49.1 From f744e8c59daeb72d868416b709c098a9bc34f83c Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 09:58:55 +0200 Subject: [PATCH 07/34] docs(c4): add 3d OCR orchestration and 3e supporting domains --- docs/architecture/c4-diagrams.md | 79 ++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 6161aa01..ea7483db 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -244,6 +244,85 @@ C4Component Rel(personRepo, db, "SQL queries", "JDBC") ``` +### 3d — OCR Orchestration + +How the Spring Boot backend manages OCR jobs, streams results, and trains recognition models. + +```mermaid +C4Component + title Component Diagram: API Backend — OCR Orchestration + + Container(frontend, "Web Frontend", "SvelteKit") + ContainerDb(db, "PostgreSQL") + ContainerDb(minio, "MinIO") + Container(ocrPy, "OCR Service", "Python FastAPI") + + System_Boundary(backend, "API Backend (Spring Boot)") { + Component(ocrCtrl, "OcrController", "Spring MVC — /api/ocr", "REST entry point: trigger single or batch OCR jobs, stream progress via SSE, query job status, and manage training runs and per-sender models.") + Component(ocrSvc, "OcrService", "Spring Service", "Creates OcrJob and OcrJobDocument records, checks Python service health, and delegates async execution to OcrAsyncRunner.") + Component(ocrBatch, "OcrBatchService", "Spring Service", "Orchestrates multi-document OCR jobs, iterating documents and delegating each to OcrAsyncRunner.") + Component(ocrAsync, "OcrAsyncRunner", "Spring Component — @Async", "Async worker that streams OCR results from Python page by page, persists transcription blocks and annotations via domain services, and emits progress via SSE.") + Component(ocrClient, "RestClientOcrClient", "Spring Component", "HTTP client wrapping the Python service: POST /ocr/stream (NDJSON), /train, /segtrain, and /train-sender. Falls back from streaming to batch on 404.") + Component(ocrTraining, "OcrTrainingService", "Spring Service", "Orchestrates model training: exports training data as ZIP, calls Python /train or /segtrain, persists training metrics in OcrTrainingRunRepository.") + } + + Rel(frontend, ocrCtrl, "OCR trigger, status, and progress requests", "HTTP / JSON / SSE") + Rel(ocrCtrl, ocrSvc, "Single-document jobs", "") + Rel(ocrCtrl, ocrBatch, "Batch jobs", "") + Rel(ocrCtrl, ocrTraining, "Training runs", "") + Rel(ocrSvc, ocrAsync, "Delegates async execution", "") + Rel(ocrBatch, ocrAsync, "Delegates async execution", "") + Rel(ocrAsync, ocrClient, "Streams OCR results page by page", "HTTP / NDJSON") + Rel(ocrTraining, ocrClient, "Sends training data ZIP", "HTTP / multipart") + Rel(ocrClient, ocrPy, "POST /ocr/stream, /train, /segtrain, /train-sender", "HTTP / REST") + Rel(ocrAsync, db, "Reads / writes job state, transcription blocks, annotations", "JDBC") + Rel(ocrAsync, minio, "Generates presigned URLs for PDF fetch", "S3 API") + Rel(ocrPy, minio, "Fetches PDF via presigned URL", "HTTP / S3 presigned") + Rel(ocrTraining, db, "Persists training run metrics", "JDBC") +``` + +### 3e — Supporting Domains + +Audit logging, dashboard stats, SSE notifications, stories (Geschichten), and cross-cutting exception handling. + +```mermaid +C4Component + title Component Diagram: API Backend — Supporting Domains + + Container(frontend, "Web Frontend", "SvelteKit") + ContainerDb(db, "PostgreSQL") + + System_Boundary(backend, "API Backend (Spring Boot)") { + 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(statsCtrl, "StatsController", "Spring MVC — /api/stats", "Returns aggregate counts (total persons, total documents) for the UI stats bar.") + 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(notifSvc, "NotificationService", "Spring Service", "Creates REPLY and MENTION notifications, optionally sends email, marks as read, and pushes events to connected clients via SseEmitterRegistry.") + Component(sseRegistry, "SseEmitterRegistry", "Spring Component", "In-memory ConcurrentHashMap of Spring SseEmitter instances per user. Handles registration, deregistration, and JSON event broadcasts.") + + Component(geschCtrl, "GeschichteController", "Spring MVC — /api/geschichten", "CRUD for publishable stories that link persons and documents. Requires BLOG_WRITE permission for write operations.") + Component(geschSvc, "GeschichteService", "Spring Service", "Manages story lifecycle (DRAFT → PUBLISHED with timestamp). Sanitizes HTML body with an allowlist policy.") + + Component(exHandler, "GlobalExceptionHandler", "Spring @RestControllerAdvice", "Converts DomainException, validation errors, and generic exceptions to ErrorResponse JSON with machine-readable ErrorCode and HTTP status.") + } + + 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(dashSvc, auditQuery, "Fetches activity feed and pulse stats", "") + Rel(notifCtrl, notifSvc, "Delegates to", "") + Rel(notifCtrl, sseRegistry, "Registers client SSE connection", "") + Rel(notifSvc, sseRegistry, "Broadcasts events to connected clients", "") + Rel(geschCtrl, geschSvc, "Delegates to", "") + Rel(auditSvc, db, "Writes audit_log", "JDBC") + Rel(auditQuery, db, "Reads audit_log", "JDBC") + Rel(notifSvc, db, "Reads / writes notifications", "JDBC") + Rel(geschSvc, db, "Reads / writes geschichten", "JDBC") +``` + --- ## Level 3 — Components: Web Frontend -- 2.49.1 From be40cc0f381e3a1f572f570f6e0d07a5c2aecc65 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 09:59:46 +0200 Subject: [PATCH 08/34] =?UTF-8?q?docs(c4):=20update=20frontend=203a=20?= =?UTF-8?q?=E2=80=94=20hooks=20layers,=20add=20register/forgot/reset=20rou?= =?UTF-8?q?tes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/architecture/c4-diagrams.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index ea7483db..9b82347b 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -327,11 +327,11 @@ C4Component ## Level 3 — Components: Web Frontend -The internal structure of the SvelteKit frontend, split into two focused views. +The internal structure of the SvelteKit frontend, split into four focused views. ### 3a — Middleware, Auth & Layout -Per-request middleware: session validation, i18n, and auth cookie handling. +Per-request middleware: session validation, i18n, auth cookie handling, and auth pages. ```mermaid C4Component @@ -341,11 +341,14 @@ C4Component Container(backend, "API Backend", "Spring Boot") System_Boundary(frontend, "Web Frontend (SvelteKit / SSR)") { - Component(hooks, "hooks.server.ts", "SvelteKit Server Hook", "Two responsibilities: (1) userGroup handle — reads auth_token cookie, fetches /api/users/me, stores user in event.locals. (2) handleFetch — intercepts all outgoing fetch() calls, injects Authorization header from cookie. Redirects to /login if token absent.") + Component(hooks, "hooks.server.ts", "SvelteKit Server Hook", "Four handle layers: (1) handleAuth — redirects unauthenticated users to /login; (2) userGroup — reads auth_token cookie, fetches /api/users/me, stores user in event.locals; (3) handleFetch — injects Authorization header on all outgoing /api/ calls; (4) handleLocaleDetection — sets language cookie from Accept-Language header.") Component(i18n, "hooks.ts (Paraglide)", "SvelteKit Client Hook", "Client-side i18n middleware. Detects language from URL and sets the active locale for Paraglide.js translation functions.") Component(layout, "+layout.server.ts", "SvelteKit Layout Loader", "Passes event.locals.user down to all child pages so every route has access to the authenticated user.") Component(loginPage, "/login", "SvelteKit Route", "Form action: encodes username:password as Base64 Basic Auth token, POSTs to /api/users/me to validate, sets auth_token httpOnly cookie (SameSite=strict, maxAge=86400), redirects to /.") Component(logoutPage, "/logout", "SvelteKit Route (server-only)", "Clears the auth_token cookie and redirects to /login.") + Component(registerPage, "/register", "SvelteKit Route", "Loader validates invite code via GET /api/auth/invite/{code}. Form action: POST /api/auth/register to create the user account.") + Component(forgotPw, "/forgot-password", "SvelteKit Route", "Form action: POST /api/auth/forgot-password. Always responds with success to prevent email enumeration.") + Component(resetPw, "/reset-password", "SvelteKit Route", "Form action: POST /api/auth/reset-password with the token from the query string.") } Rel(user, hooks, "Every browser request", "HTTPS") @@ -353,6 +356,9 @@ C4Component Rel(hooks, loginPage, "Redirect if no token", "") Rel(hooks, layout, "Stores authenticated user in event.locals", "") Rel(loginPage, backend, "POST /api/users/me (auth check)", "HTTP / Basic Auth") + Rel(registerPage, backend, "GET /api/auth/invite/{code}, POST /api/auth/register", "HTTP / JSON") + Rel(forgotPw, backend, "POST /api/auth/forgot-password", "HTTP / JSON") + Rel(resetPw, backend, "POST /api/auth/reset-password", "HTTP / JSON") ``` ### 3b — Pages & Shared Components -- 2.49.1 From e1f66e2e65fc2ba658efe8efdc9ced6f1272990d Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 10:02:38 +0200 Subject: [PATCH 09/34] docs(c4): rewrite frontend 3b, add 3c people/stories/discovery, add 3d admin/help Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 99 ++++++++++++++++++++++++++------ 1 file changed, 80 insertions(+), 19 deletions(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 9b82347b..f7e73608 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -361,43 +361,41 @@ C4Component Rel(resetPw, backend, "POST /api/auth/reset-password", "HTTP / JSON") ``` -### 3b — Pages & Shared Components +### 3b — Document Workflows -Application pages and reusable UI components. +Document search, viewing, editing, enrichment, and the shared components that support them. ```mermaid C4Component - title Component Diagram: Web Frontend — Pages & Shared Components + title Component Diagram: Web Frontend — Document Workflows Person(user, "User") Container(backend, "API Backend", "Spring Boot") System_Boundary(frontend, "Web Frontend (SvelteKit / SSR)") { - Component(homePage, "/ (Home / Search)", "SvelteKit Route", "Loader: parses URL search params (q, from, to, senderId, receiverId, tags), fetches /api/documents/search and /api/persons, returns results. Page: renders search form with full-text, date range, sender/receiver typeahead, tag filters. Displays paginated document list.") - Component(docDetail, "/documents/[id]", "SvelteKit Route", "Loader: fetches /api/documents/{id}. Handles 401 redirect to login, 404 error. Page: shows document metadata, file viewer (PDF/image inline), transcription, tags.") - Component(docEdit, "/documents/[id]/edit", "SvelteKit Route", "Form with PersonTypeahead for sender/receiver, TagInput for tags, date/location fields. Submits PUT to /api/documents/{id}.") - Component(persons, "/persons and /persons/[id]", "SvelteKit Routes", "Lists all persons. Detail page shows person metadata and all documents they sent.") - Component(conversations, "/conversations", "SvelteKit Route", "Selects two persons via PersonTypeahead, fetches /api/documents/conversation, displays chronological exchange.") - Component(adminPage, "/admin", "SvelteKit Route", "User management UI (create/delete users). Excel import trigger button (calls /api/admin/trigger-import).") - - Component(apiPersons, "/api/persons (SvelteKit API)", "SvelteKit Server Route", "Proxies GET /api/persons?q=... to backend. Used by PersonTypeahead for typeahead suggestions.") - Component(apiTags, "/api/tags (SvelteKit API)", "SvelteKit Server Route", "Proxies GET /api/tags to backend. Used by TagInput for autocomplete.") + Component(homePage, "/ (Home / Search)", "SvelteKit Route", "Loader: parses URL params (q, from, to, senderId, receiverId, tags), fetches /api/documents/search and /api/persons. Renders search form with full-text, date range, sender/receiver typeahead, and tag filters.") + Component(docDetail, "/documents/[id]", "SvelteKit Route", "Loader: GET /api/documents/{id}. Page: metadata panel, inline file viewer, transcription editor, annotation layer, and comment thread.") + Component(docEdit, "/documents/[id]/edit", "SvelteKit Route", "Edit form with PersonTypeahead, TagInput, date/location fields. Form action: PUT /api/documents/{id}.") + Component(docNew, "/documents/new", "SvelteKit Route", "Upload form for a new document. Loader: GET /api/persons. Form action: POST /api/documents with multipart file.") + Component(docBulkEdit, "/documents/bulk-edit", "SvelteKit Route", "Multi-document metadata editor. Loader: GET /api/documents/incomplete. Requires WRITE_ALL (redirects otherwise). Action: PATCH /api/documents/batch.") + Component(enrichPage, "/enrich/[id]", "SvelteKit Route", "Guided enrichment workflow. Loader: GET /api/documents/{id}. Progressively saves annotations and transcription blocks.") + Component(apiPersons, "/api/persons (SvelteKit API)", "SvelteKit Server Route", "Proxies GET /api/persons?q=... to backend for PersonTypeahead suggestions.") + Component(apiTags, "/api/tags (SvelteKit API)", "SvelteKit Server Route", "Proxies GET /api/tags to backend for TagInput autocomplete.") Component(typeahead, "PersonTypeahead.svelte", "Svelte Component", "Async autocomplete for selecting a person. Debounces input, calls /api/persons?q=.") Component(tagInput, "TagInput.svelte", "Svelte Component", "Multi-tag input. Supports free-text entry and selecting existing tags from /api/tags.") } Rel(user, homePage, "Searches and browses", "HTTPS / Browser") - Rel(homePage, backend, "GET /api/documents/search", "HTTP / JSON") - Rel(homePage, backend, "GET /api/persons", "HTTP / JSON") - Rel(docDetail, backend, "GET /api/documents/{id}", "HTTP / JSON") - Rel(docDetail, backend, "GET /api/documents/{id}/file", "HTTP / Binary stream") + Rel(homePage, backend, "GET /api/documents/search, GET /api/persons", "HTTP / JSON") + Rel(docDetail, backend, "GET /api/documents/{id}, GET /api/documents/{id}/file", "HTTP / JSON + Binary") Rel(docEdit, backend, "PUT /api/documents/{id}", "HTTP / Multipart") - Rel(conversations, backend, "GET /api/documents/conversation", "HTTP / JSON") - Rel(adminPage, backend, "GET/POST/DELETE /api/users", "HTTP / JSON") - Rel(adminPage, backend, "POST /api/admin/trigger-import", "HTTP / JSON") + Rel(docNew, backend, "GET /api/persons, POST /api/documents", "HTTP / JSON + Multipart") + Rel(docBulkEdit, backend, "GET /api/documents/incomplete, PATCH /api/documents/batch", "HTTP / JSON") + Rel(enrichPage, backend, "GET/POST /api/transcription, POST /api/documents/{id}/annotations", "HTTP / JSON") Rel(homePage, typeahead, "Uses for sender/receiver filter", "") Rel(docEdit, typeahead, "Uses for sender/receiver selection", "") + Rel(docNew, typeahead, "Uses for sender selection", "") Rel(docEdit, tagInput, "Uses for tag management", "") Rel(typeahead, apiPersons, "Fetches suggestions", "HTTP") Rel(tagInput, apiTags, "Fetches existing tags", "HTTP") @@ -405,6 +403,69 @@ C4Component Rel(apiTags, backend, "GET /api/tags", "HTTP / JSON") ``` +### 3c — People, Stories & Discovery + +Person directory, bilateral conversations, activity feed, stories, family tree, and user profiles. + +```mermaid +C4Component + title Component Diagram: Web Frontend — People, Stories & Discovery + + Person(user, "User") + Container(backend, "API Backend", "Spring Boot") + + System_Boundary(frontend, "Web Frontend (SvelteKit / SSR)") { + Component(personsPage, "/persons and /persons/[id]", "SvelteKit Routes", "Person directory and detail. Detail: metadata, document list sent/received, correspondents, explicit and inferred family relationships.") + Component(personEdit, "/persons/[id]/edit and /persons/new", "SvelteKit Routes", "Create and edit person forms. Edit: metadata, aliases, explicit relationships. Actions: PUT/POST /api/persons.") + Component(briefwechsel, "/briefwechsel", "SvelteKit Route", "Bilateral conversation timeline. Selects two persons via PersonTypeahead, fetches GET /api/documents/conversation, displays chronological exchange.") + Component(aktivitaeten, "/aktivitaeten", "SvelteKit Route", "Unified activity feed (Chronik). Loader: GET /api/dashboard/activity and GET /api/notifications?read=false.") + Component(geschichten, "/geschichten and /geschichten/[id]", "SvelteKit Routes", "Story list and detail pages. Loader: GET /api/geschichten?status=PUBLISHED.") + Component(geschichtenEdit, "/geschichten/[id]/edit and /geschichten/new", "SvelteKit Routes", "Story editor with rich text, person and document linking. Actions: PUT/POST /api/geschichten. Requires BLOG_WRITE permission.") + Component(stammbaum, "/stammbaum", "SvelteKit Route", "Family tree visualisation. Loader: GET /api/network (nodes + edges). Renders interactive D3-based Stammbaum.") + Component(profilePage, "/profile", "SvelteKit Route", "Current user profile settings. Loader: GET /api/users/me/notification-preferences. Actions: update name/password and notification preferences.") + Component(userProfile, "/users/[id]", "SvelteKit Route", "Public user profile view. Loader: GET /api/users/{id}.") + } + + Rel(user, personsPage, "Browses family members", "HTTPS / Browser") + Rel(personsPage, backend, "GET /api/persons, GET /api/persons/{id}", "HTTP / JSON") + Rel(personEdit, backend, "GET /api/persons/{id}, PUT /api/persons/{id}, POST /api/persons", "HTTP / JSON") + Rel(briefwechsel, backend, "GET /api/documents/conversation", "HTTP / JSON") + Rel(aktivitaeten, backend, "GET /api/dashboard/activity, GET /api/notifications", "HTTP / JSON") + Rel(geschichten, backend, "GET /api/geschichten", "HTTP / JSON") + Rel(geschichtenEdit, backend, "GET/PUT/POST /api/geschichten", "HTTP / JSON") + Rel(stammbaum, backend, "GET /api/network", "HTTP / JSON") + Rel(profilePage, backend, "GET/PUT /api/users/me, notification-preferences", "HTTP / JSON") + Rel(userProfile, backend, "GET /api/users/{id}", "HTTP / JSON") +``` + +### 3d — Administration & Help + +Admin panel sub-routes and the transcription help guide. + +```mermaid +C4Component + title Component Diagram: Web Frontend — Administration & Help + + Person(admin, "Administrator") + Container(backend, "API Backend", "Spring Boot") + + System_Boundary(frontend, "Web Frontend (SvelteKit / SSR)") { + Component(adminUsers, "/admin/users, /admin/users/[id], /admin/users/new, /admin/invites", "SvelteKit Routes", "User directory, create/update/delete users, and manage invite codes. Requires ADMIN_USER permission.") + Component(adminGroups, "/admin/groups, /admin/groups/[id], /admin/groups/new", "SvelteKit Routes", "Permission group management: create/edit groups and their permission sets.") + Component(adminTags, "/admin/tags and /admin/tags/[id]", "SvelteKit Routes", "Tag administration: edit tag hierarchy, merge tags, delete subtrees.") + Component(adminOcr, "/admin/ocr and /admin/ocr/[personId]", "SvelteKit Routes", "Global and per-person OCR configuration. Manages script types and triggers sender model training.") + Component(adminSystem, "/admin/system", "SvelteKit Route", "System status panel. Triggers Excel/ODS mass import (POST /api/admin/trigger-import). Displays import state.") + Component(hilfe, "/hilfe/transkription", "SvelteKit Route", "Static transcription style guide for Kurrent and Sütterlin character recognition. No backend calls.") + } + + Rel(admin, adminUsers, "Manages users and invites", "HTTPS / Browser") + Rel(adminUsers, backend, "GET/POST/DELETE /api/users, POST /api/auth/invite", "HTTP / JSON") + Rel(adminGroups, backend, "GET/POST/PUT/DELETE /api/groups", "HTTP / JSON") + Rel(adminTags, backend, "GET/PUT/DELETE /api/tags", "HTTP / JSON") + Rel(adminOcr, backend, "GET/POST /api/ocr (global config and sender training)", "HTTP / JSON") + Rel(adminSystem, backend, "POST /api/admin/trigger-import, GET /api/admin/import-status", "HTTP / JSON") +``` + --- ## Authentication Flow -- 2.49.1 From 42c6e58d366016ffd1ef50a0477c72fdeff025da Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 10:28:16 +0200 Subject: [PATCH 10/34] 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 --- docs/architecture/c4-diagrams.md | 12 +- .../2026-05-06-reader-dashboard-design.md | 165 ++++++++++++++++++ 2 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 docs/superpowers/specs/2026-05-06-reader-dashboard-design.md diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index f7e73608..995994c8 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -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", "") diff --git a/docs/superpowers/specs/2026-05-06-reader-dashboard-design.md b/docs/superpowers/specs/2026-05-06-reader-dashboard-design.md new file mode 100644 index 00000000..8456d3e4 --- /dev/null +++ b/docs/superpowers/specs/2026-05-06-reader-dashboard-design.md @@ -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 (``). 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 | -- 2.49.1 From 18a7ee1fa518bbd9c560fc41941bd372abd04904 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 10:29:30 +0200 Subject: [PATCH 11/34] docs: remove accidentally committed spec file Spec file was pre-staged from a prior session and bundled into the previous commit. Specs belong in Gitea issues, not committed to the repo. Co-Authored-By: Claude Sonnet 4.6 --- .../2026-05-06-reader-dashboard-design.md | 165 ------------------ 1 file changed, 165 deletions(-) delete mode 100644 docs/superpowers/specs/2026-05-06-reader-dashboard-design.md diff --git a/docs/superpowers/specs/2026-05-06-reader-dashboard-design.md b/docs/superpowers/specs/2026-05-06-reader-dashboard-design.md deleted file mode 100644 index 8456d3e4..00000000 --- a/docs/superpowers/specs/2026-05-06-reader-dashboard-design.md +++ /dev/null @@ -1,165 +0,0 @@ -# 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 | -- 2.49.1 From 83c3d85b0086dbd418bac618a63af6fe29aec954 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 11:03:23 +0200 Subject: [PATCH 12/34] docs(c4): fix service layer relationships in diagrams 3b and 3b.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Diagram 3b: DocumentService calls PersonService and TagService, not their repositories directly. Replace personRepo/tagRepo cross-ref stubs with personSvc/tagSvc to accurately reflect the layering rule. Diagram 3b.2: TranscriptionService, AnnotationService, and CommentService each use a JPA repository, not JDBC directly. Add TranscriptionBlockRepository, AnnotationRepository, and CommentRepository components and route the service→repo→db chain. TranscriptionQueueService delegates to DocumentService and AuditLogQueryService (no repo of its own); replace the incorrect →db arrow with cross-diagram stubs. Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 995994c8..1980b6e6 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -111,8 +111,8 @@ C4Component Component(docSpec, "DocumentSpecifications", "JPA Criteria API", "Factory for composable predicates: hasText (full-text), hasSender, hasReceiver, isBetween (date range), hasTags (subquery AND/OR logic).") } - Component(personRepo, "PersonRepository", "Spring Data JPA", "See diagram 3c.2. Used here by DocumentService to resolve sender / receiver persons.") - Component(tagRepo, "TagRepository", "Spring Data JPA", "See diagram 3c. Used here by DocumentService to find or create tags.") + Component(personSvc, "PersonService", "Spring Service", "See diagram 3c.2. Called by DocumentService to resolve sender / receiver persons by ID.") + Component(tagSvc, "TagService", "Spring Service", "See diagram 3c. Called by DocumentService to find or create tags by name.") Rel(frontend, docCtrl, "Document requests", "HTTP / JSON") Rel(frontend, adminCtrl, "Trigger import", "HTTP / JSON") @@ -121,15 +121,13 @@ C4Component Rel(docSvc, fileSvc, "Upload / download files", "") Rel(docSvc, docRepo, "Reads / writes documents", "") Rel(docSvc, docSpec, "Builds search predicates", "") - Rel(docSvc, personRepo, "Resolves sender / receivers", "") - Rel(docSvc, tagRepo, "Finds or creates tags", "") + Rel(docSvc, personSvc, "Resolves sender / receivers", "") + Rel(docSvc, tagSvc, "Finds or creates tags", "") Rel(massImport, excelSvc, "Parses Excel/ODS file", "") Rel(excelSvc, docSvc, "Creates / updates documents", "") Rel(minioConf, fileSvc, "Provides S3Client and S3Presigner beans", "") Rel(fileSvc, minio, "PUT / GET / presigned URL objects", "S3 API / HTTP") Rel(docRepo, db, "SQL queries", "JDBC") - Rel(personRepo, db, "SQL queries", "JDBC") - Rel(tagRepo, db, "SQL queries", "JDBC") ``` ### 3b.2 — Document Transcription Pipeline @@ -149,11 +147,18 @@ C4Component Component(commentCtrl, "CommentController", "Spring MVC — /api/documents/{id}/comments", "Threaded comment CRUD on transcription blocks with @mention support and notification triggers.") Component(transcriptionSvc, "TranscriptionService", "Spring Service", "Creates and updates transcription blocks from annotation regions. Tracks block versions, sanitizes text with an HTML allow-list, and triggers mentions.") - Component(transcriptionQueueSvc, "TranscriptionQueueService", "Spring Service", "Exposes segmentation, transcription, and review queue projections for the mission-control enrichment workflow.") + Component(transcriptionQueueSvc, "TranscriptionQueueService", "Spring Service", "Assembles segmentation, transcription, and review queue projections by delegating to DocumentService and AuditLogQueryService.") Component(annotationSvc, "AnnotationService", "Spring Service", "Manages document page annotations with polygon coordinates. Called by OcrAsyncRunner to persist OCR-generated block boundaries.") Component(commentSvc, "CommentService", "Spring Service", "Creates and manages threaded comments with @mention parsing. Triggers NotificationService for REPLY and MENTION events.") + + Component(blockRepo, "TranscriptionBlockRepository", "Spring Data JPA", "Reads and writes TranscriptionBlock and TranscriptionBlockVersion records.") + Component(annotationRepo, "AnnotationRepository", "Spring Data JPA", "Reads and writes DocumentAnnotation records.") + Component(commentRepo, "CommentRepository", "Spring Data JPA", "Reads and writes DocumentComment records.") } + Component(documentSvc, "DocumentService", "Spring Service", "See diagram 3b. Called by TranscriptionQueueService to assemble pipeline queue projections.") + Component(auditQuerySvc, "AuditLogQueryService", "Spring Service", "See diagram 3e. Called by TranscriptionQueueService for pipeline activity data.") + Rel(frontend, transcriptionCtrl, "Transcription block requests", "HTTP / JSON") Rel(frontend, annotationCtrl, "Annotation requests", "HTTP / JSON") Rel(frontend, commentCtrl, "Comment requests", "HTTP / JSON") @@ -161,10 +166,14 @@ C4Component Rel(transcriptionCtrl, transcriptionQueueSvc, "Queries pipeline queues", "") Rel(annotationCtrl, annotationSvc, "Delegates to", "") Rel(commentCtrl, commentSvc, "Delegates to", "") - Rel(transcriptionSvc, db, "Reads / writes blocks and versions", "JDBC") - Rel(annotationSvc, db, "Reads / writes annotations", "JDBC") - Rel(commentSvc, db, "Reads / writes comments", "JDBC") - Rel(transcriptionQueueSvc, db, "Queue projection queries", "JDBC") + Rel(transcriptionSvc, blockRepo, "Reads / writes blocks and versions", "") + Rel(annotationSvc, annotationRepo, "Reads / writes annotations", "") + Rel(commentSvc, commentRepo, "Reads / writes comments", "") + Rel(transcriptionQueueSvc, documentSvc, "Queries pipeline document state", "") + Rel(transcriptionQueueSvc, auditQuerySvc, "Queries pipeline activity data", "") + Rel(blockRepo, db, "SQL queries", "JDBC") + Rel(annotationRepo, db, "SQL queries", "JDBC") + Rel(commentRepo, db, "SQL queries", "JDBC") ``` ### 3c — Users, Groups & Administration -- 2.49.1 From 35d82814a633e63137e0236494de0464fadbb0e8 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 11:25:14 +0200 Subject: [PATCH 13/34] =?UTF-8?q?docs(c4):=20fix=203d=20OCR=20=E2=80=94=20?= =?UTF-8?q?route=20transcription/annotation=20through=20domain=20services?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OcrAsyncRunner injects TranscriptionService and AnnotationService; it only accesses the DB directly for OcrJob state (OcrJobRepository). The previous Rel arrow incorrectly showed direct JDBC access for transcription blocks and annotations, contradicting the component description. Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 1980b6e6..5e502d30 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -277,6 +277,9 @@ C4Component Component(ocrTraining, "OcrTrainingService", "Spring Service", "Orchestrates model training: exports training data as ZIP, calls Python /train or /segtrain, persists training metrics in OcrTrainingRunRepository.") } + Component(transcriptionSvc, "TranscriptionService", "Spring Service", "See diagram 3b.2. Called by OcrAsyncRunner to persist transcription blocks per page.") + Component(annotationSvc, "AnnotationService", "Spring Service", "See diagram 3b.2. Called by OcrAsyncRunner to persist OCR-generated annotation regions per page.") + Rel(frontend, ocrCtrl, "OCR trigger, status, and progress requests", "HTTP / JSON / SSE") Rel(ocrCtrl, ocrSvc, "Single-document jobs", "") Rel(ocrCtrl, ocrBatch, "Batch jobs", "") @@ -286,7 +289,9 @@ C4Component Rel(ocrAsync, ocrClient, "Streams OCR results page by page", "HTTP / NDJSON") Rel(ocrTraining, ocrClient, "Sends training data ZIP", "HTTP / multipart") Rel(ocrClient, ocrPy, "POST /ocr/stream, /train, /segtrain, /train-sender", "HTTP / REST") - Rel(ocrAsync, db, "Reads / writes job state, transcription blocks, annotations", "JDBC") + Rel(ocrAsync, transcriptionSvc, "Saves transcription blocks per page", "") + Rel(ocrAsync, annotationSvc, "Saves annotation regions per page", "") + Rel(ocrAsync, db, "Reads / writes OCR job state", "JDBC") Rel(ocrAsync, minio, "Generates presigned URLs for PDF fetch", "S3 API") Rel(ocrPy, minio, "Fetches PDF via presigned URL", "HTTP / S3 presigned") Rel(ocrTraining, db, "Persists training run metrics", "JDBC") -- 2.49.1 From a20a46b262609a3c99aa16392b8c3841fba8cca6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 11:25:57 +0200 Subject: [PATCH 14/34] =?UTF-8?q?docs(c4):=20fix=203c.2=20=E2=80=94=20add?= =?UTF-8?q?=20PersonRelationshipRepository,=20route=20through=20repo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both RelationshipService and RelationshipInferenceService inject PersonRelationshipRepository. The previous direct db arrows were inaccurate. Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 5e502d30..6467998d 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -242,6 +242,7 @@ C4Component Component(relInference, "RelationshipInferenceService", "Spring Service", "Computes transitive family relationships from explicit edges to infer grandparent/grandchild, aunt/uncle, and other extended-family links for the network graph.") Component(personRepo, "PersonRepository", "Spring Data JPA", "Queries persons with name search (including aliases), correspondent discovery, person summaries with document counts, and merge/reassignment helpers.") + Component(relRepo, "PersonRelationshipRepository", "Spring Data JPA", "Reads and writes PersonRelationship records. Supports lookup by person ID, by relation type, and existence checks for deduplication.") } Rel(frontend, personCtrl, "Person requests", "HTTP / JSON") @@ -250,9 +251,10 @@ C4Component Rel(relCtrl, relSvc, "Delegates to", "") Rel(relCtrl, relInference, "Queries inferred graph", "") Rel(personSvc, personRepo, "Reads / writes persons", "") - Rel(relSvc, db, "Reads / writes relationships", "JDBC") - Rel(relInference, db, "Reads relationships for inference", "JDBC") + Rel(relSvc, relRepo, "Reads / writes relationships", "") + Rel(relInference, relRepo, "Reads relationships for inference", "") Rel(personRepo, db, "SQL queries", "JDBC") + Rel(relRepo, db, "SQL queries", "JDBC") ``` ### 3d — OCR Orchestration -- 2.49.1 From 632fb9ef7b8e91b14b62d03b8dba33fdfa3c26fa Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 11:26:33 +0200 Subject: [PATCH 15/34] =?UTF-8?q?docs(c4):=20fix=203b=20frontend=20?= =?UTF-8?q?=E2=80=94=20correct=20docBulkEdit=20endpoint=20to=20/bulk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DocumentController maps the batch update to PATCH /api/documents/bulk, not /api/documents/batch. Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 6467998d..49b2edd5 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -415,7 +415,7 @@ C4Component Rel(docDetail, backend, "GET /api/documents/{id}, GET /api/documents/{id}/file", "HTTP / JSON + Binary") Rel(docEdit, backend, "PUT /api/documents/{id}", "HTTP / Multipart") Rel(docNew, backend, "GET /api/persons, POST /api/documents", "HTTP / JSON + Multipart") - Rel(docBulkEdit, backend, "GET /api/documents/incomplete, PATCH /api/documents/batch", "HTTP / JSON") + Rel(docBulkEdit, backend, "GET /api/documents/incomplete, PATCH /api/documents/bulk", "HTTP / JSON") Rel(enrichPage, backend, "GET/POST /api/transcription, POST /api/documents/{id}/annotations", "HTTP / JSON") Rel(homePage, typeahead, "Uses for sender/receiver filter", "") Rel(docEdit, typeahead, "Uses for sender/receiver selection", "") -- 2.49.1 From e3bfbde8e968a76cbacf02f9b6582e62263f2630 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 11:27:13 +0200 Subject: [PATCH 16/34] =?UTF-8?q?docs(c4):=20fix=203a=20=E2=80=94=20remove?= =?UTF-8?q?=20AOP=20@Around=20from=20secFilter=E2=86=92permAspect=20rel=20?= =?UTF-8?q?label?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The filter chain doesn't invoke the AOP aspect directly — Spring Security hands off to the servlet and AOP intercepts at the method level. The label implied a direct invocation chain that doesn't exist. Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 49b2edd5..92072070 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -80,7 +80,7 @@ C4Component } Rel(frontend, secFilter, "All requests", "HTTP / Basic Auth header") - Rel(secFilter, permAspect, "Authenticated requests proceed to guarded methods", "AOP @Around") + Rel(secFilter, permAspect, "Authenticated requests reach guarded service methods", "") Rel(secConf, userDetails, "Wires as UserDetailsService", "") Rel(userDetails, db, "Loads user by email", "JDBC") ``` -- 2.49.1 From bbded5b9216b9036df8dbcff0c8580651c0e19ae Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 11:28:03 +0200 Subject: [PATCH 17/34] =?UTF-8?q?docs(c4):=20fix=203d=20frontend=20?= =?UTF-8?q?=E2=80=94=20add=20User=20actor=20for=20/hilfe/transkription?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The help guide is used by all transcribers, not just administrators. Only showing admin as the actor was misleading about who accesses this route. Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 92072070..ec010c6d 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -471,6 +471,7 @@ C4Component title Component Diagram: Web Frontend — Administration & Help Person(admin, "Administrator") + Person(user, "User") Container(backend, "API Backend", "Spring Boot") System_Boundary(frontend, "Web Frontend (SvelteKit / SSR)") { @@ -483,6 +484,7 @@ C4Component } Rel(admin, adminUsers, "Manages users and invites", "HTTPS / Browser") + Rel(user, hilfe, "Views transcription style guide", "HTTPS / Browser") Rel(adminUsers, backend, "GET/POST/DELETE /api/users, POST /api/auth/invite", "HTTP / JSON") Rel(adminGroups, backend, "GET/POST/PUT/DELETE /api/groups", "HTTP / JSON") Rel(adminTags, backend, "GET/PUT/DELETE /api/tags", "HTTP / JSON") -- 2.49.1 From 61ca5aacf7ef274aec5ba033f92bfa04b632e006 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 12:16:07 +0200 Subject: [PATCH 18/34] =?UTF-8?q?docs(c4):=20fix=203a=20secFilter=20descri?= =?UTF-8?q?ption=20=E2=80=94=20BCrypt=20validation=20is=20in=20DaoAuthenti?= =?UTF-8?q?cationProvider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index ec010c6d..032897b0 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -73,7 +73,7 @@ C4Component ContainerDb(db, "PostgreSQL") System_Boundary(backend, "API Backend (Spring Boot)") { - Component(secFilter, "Security Filter Chain", "Spring Security", "Enforces authentication on all requests. Parses Basic Auth header and validates credentials via BCrypt. Permits password-reset, invite, and register endpoints without authentication.") + Component(secFilter, "Security Filter Chain", "Spring Security", "Enforces authentication on all requests. Parses Basic Auth header and constructs an Authentication token; delegates credential validation to DaoAuthenticationProvider via BCrypt. Permits password-reset, invite, and register endpoints without authentication.") Component(permAspect, "PermissionAspect", "Spring AOP", "Intercepts methods annotated with @RequirePermission. Checks user's granted authorities against the required permission. Throws 401/403 if denied.") Component(secConf, "SecurityConfig", "Spring @Configuration", "Configures filter chain: all routes require authentication, CSRF disabled, BCrypt password encoder, DaoAuthenticationProvider with CustomUserDetailsService.") Component(userDetails, "CustomUserDetailsService", "Spring Security UserDetailsService", "Loads AppUser by email from DB. Converts group permissions to Spring GrantedAuthority objects. Logs unknown permissions.") -- 2.49.1 From 83f4f8352c18454d7db182a585be62083b9e91c5 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 12:16:54 +0200 Subject: [PATCH 19/34] =?UTF-8?q?docs(c4):=20add=20OcrJobRepository=20inte?= =?UTF-8?q?rmediary=20in=203d=20=E2=80=94=20route=20ocrAsync=20through=20r?= =?UTF-8?q?epo,=20not=20bare=20db?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 032897b0..4645083c 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -277,6 +277,7 @@ C4Component Component(ocrAsync, "OcrAsyncRunner", "Spring Component — @Async", "Async worker that streams OCR results from Python page by page, persists transcription blocks and annotations via domain services, and emits progress via SSE.") Component(ocrClient, "RestClientOcrClient", "Spring Component", "HTTP client wrapping the Python service: POST /ocr/stream (NDJSON), /train, /segtrain, and /train-sender. Falls back from streaming to batch on 404.") Component(ocrTraining, "OcrTrainingService", "Spring Service", "Orchestrates model training: exports training data as ZIP, calls Python /train or /segtrain, persists training metrics in OcrTrainingRunRepository.") + Component(ocrJobRepo, "OcrJobRepository, OcrJobDocumentRepository", "Spring Data JPA", "Reads and writes OcrJob and OcrJobDocument records. Tracks job status (RUNNING/DONE/FAILED), per-document progress, page counts, and error messages.") } Component(transcriptionSvc, "TranscriptionService", "Spring Service", "See diagram 3b.2. Called by OcrAsyncRunner to persist transcription blocks per page.") @@ -293,7 +294,8 @@ C4Component Rel(ocrClient, ocrPy, "POST /ocr/stream, /train, /segtrain, /train-sender", "HTTP / REST") Rel(ocrAsync, transcriptionSvc, "Saves transcription blocks per page", "") Rel(ocrAsync, annotationSvc, "Saves annotation regions per page", "") - Rel(ocrAsync, db, "Reads / writes OCR job state", "JDBC") + Rel(ocrAsync, ocrJobRepo, "Reads / writes OCR job state", "") + Rel(ocrJobRepo, db, "SQL queries", "JDBC") Rel(ocrAsync, minio, "Generates presigned URLs for PDF fetch", "S3 API") Rel(ocrPy, minio, "Fetches PDF via presigned URL", "HTTP / S3 presigned") Rel(ocrTraining, db, "Persists training run metrics", "JDBC") -- 2.49.1 From 3fafaf29fc4ea19300fafd439ff4d8793a9c8b40 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 12:17:50 +0200 Subject: [PATCH 20/34] =?UTF-8?q?docs(c4):=20fix=203e=20DashboardService?= =?UTF-8?q?=20=E2=80=94=20add=20documentSvc=20and=20transcriptionSvc=20cro?= =?UTF-8?q?ss-domain=20stubs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DashboardService.getResume() calls DocumentService.getDocumentById() and TranscriptionService.listBlocks() — both missing from the diagram. Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 4645083c..a2f6f7db 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -319,7 +319,7 @@ C4Component 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(dashSvc, "DashboardService", "Spring Service", "Assembles the user dashboard: recent document resume (calls DocumentService + TranscriptionService), 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(notifSvc, "NotificationService", "Spring Service", "Creates REPLY and MENTION notifications, optionally sends email, marks as read, and pushes events to connected clients via SseEmitterRegistry.") @@ -331,6 +331,9 @@ C4Component Component(exHandler, "GlobalExceptionHandler", "Spring @RestControllerAdvice", "Converts DomainException, validation errors, and generic exceptions to ErrorResponse JSON with machine-readable ErrorCode and HTTP status.") } + Component(documentSvc, "DocumentService", "Spring Service", "See diagram 3b. Called by DashboardService to fetch document titles and resume data.") + Component(transcriptionSvc, "TranscriptionService", "Spring Service", "See diagram 3b.2. Called by DashboardService to fetch transcription block progress for resume.") + 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") @@ -339,6 +342,8 @@ C4Component Rel(statsCtrl, statsSvc, "Delegates to", "") Rel(statsSvc, db, "Reads aggregate counts", "JDBC") Rel(dashSvc, auditQuery, "Fetches activity feed and pulse stats", "") + Rel(dashSvc, documentSvc, "Fetches document titles and resume data", "") + Rel(dashSvc, transcriptionSvc, "Fetches transcription block progress for resume", "") Rel(notifCtrl, notifSvc, "Delegates to", "") Rel(notifCtrl, sseRegistry, "Registers client SSE connection", "") Rel(notifSvc, sseRegistry, "Broadcasts events to connected clients", "") -- 2.49.1 From 818246a26d3053346ba074c2a17b63ed265dc62d Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 12:18:42 +0200 Subject: [PATCH 21/34] =?UTF-8?q?docs(c4):=20add=20Email=20Service=20to=20?= =?UTF-8?q?L1=20and=20L2=20=E2=80=94=20NotificationService=20and=20Passwor?= =?UTF-8?q?dResetService=20send=20SMTP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index a2f6f7db..188eb7a2 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -14,9 +14,11 @@ C4Context Person(member, "Family Member", "Searches, browses, reads, and transcribes archived documents") System(familienarchiv, "Familienarchiv", "Web application for digitising, organising, and searching family documents") + System_Ext(mail, "Email Service", "SMTP server. Delivers notification emails (mentions, replies) and password-reset links.") Rel(admin, familienarchiv, "Manages via browser", "HTTPS") Rel(member, familienarchiv, "Searches, reads, and transcribes via browser", "HTTPS") + Rel(familienarchiv, mail, "Sends notification and password-reset emails (optional)", "SMTP") ``` --- @@ -30,6 +32,7 @@ C4Container title Container Diagram: Familienarchiv Person(user, "User", "Admin or family member") + System_Ext(mail, "Email Service", "SMTP server. Delivers notification and password-reset emails.") System_Boundary(archiv, "Familienarchiv (Docker Compose)") { Container(frontend, "Web Frontend", "SvelteKit / Node.js", "Server-side rendered UI. Handles auth session cookies, document search and viewer, transcription editor, annotation layer, family tree (Stammbaum), stories (Geschichten), activity feed (Chronik), enrichment workflow, and admin panel.") @@ -51,6 +54,7 @@ C4Container Rel(backend, db, "Reads and writes metadata and sessions", "JDBC / SQL") Rel(backend, storage, "Uploads and streams document files", "HTTP / S3 API (AWS SDK v2)") Rel(backend, ocr, "OCR job requests with presigned MinIO URL", "HTTP / REST / JSON") + Rel(backend, mail, "Sends notification and password-reset emails (optional)", "SMTP") Rel(ocr, storage, "Fetches PDF via presigned URL", "HTTP / S3 presigned") Rel(mc, storage, "Creates bucket on startup", "MinIO Client CLI") ``` -- 2.49.1 From 318d83d2fd778893aecd470bb03855580594cbac Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 12:52:09 +0200 Subject: [PATCH 22/34] =?UTF-8?q?fix(c4):=20correct=20docBulkEdit=20endpoi?= =?UTF-8?q?nt=20/batch=20=E2=86=92=20/bulk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DocumentController has @PatchMapping("/bulk"); the component description had the wrong path. The Rel in the same diagram was already correct. Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 188eb7a2..92e7b4df 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -412,7 +412,7 @@ C4Component Component(docDetail, "/documents/[id]", "SvelteKit Route", "Loader: GET /api/documents/{id}. Page: metadata panel, inline file viewer, transcription editor, annotation layer, and comment thread.") Component(docEdit, "/documents/[id]/edit", "SvelteKit Route", "Edit form with PersonTypeahead, TagInput, date/location fields. Form action: PUT /api/documents/{id}.") Component(docNew, "/documents/new", "SvelteKit Route", "Upload form for a new document. Loader: GET /api/persons. Form action: POST /api/documents with multipart file.") - Component(docBulkEdit, "/documents/bulk-edit", "SvelteKit Route", "Multi-document metadata editor. Loader: GET /api/documents/incomplete. Requires WRITE_ALL (redirects otherwise). Action: PATCH /api/documents/batch.") + Component(docBulkEdit, "/documents/bulk-edit", "SvelteKit Route", "Multi-document metadata editor. Loader: GET /api/documents/incomplete. Requires WRITE_ALL (redirects otherwise). Action: PATCH /api/documents/bulk.") Component(enrichPage, "/enrich/[id]", "SvelteKit Route", "Guided enrichment workflow. Loader: GET /api/documents/{id}. Progressively saves annotations and transcription blocks.") Component(apiPersons, "/api/persons (SvelteKit API)", "SvelteKit Server Route", "Proxies GET /api/persons?q=... to backend for PersonTypeahead suggestions.") -- 2.49.1 From af24d63176fee9e40c5f7222308266f197373037 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 12:52:55 +0200 Subject: [PATCH 23/34] =?UTF-8?q?fix(c4):=20loginPage=20=E2=80=94=20userna?= =?UTF-8?q?me=20=E2=86=92=20email=20in=20component=20description?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CustomUserDetailsService loads by email, not username. The component description had a stale "encodes username:password" label. Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 92e7b4df..b52455a1 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -379,7 +379,7 @@ C4Component Component(hooks, "hooks.server.ts", "SvelteKit Server Hook", "Four handle layers: (1) handleAuth — redirects unauthenticated users to /login; (2) userGroup — reads auth_token cookie, fetches /api/users/me, stores user in event.locals; (3) handleFetch — injects Authorization header on all outgoing /api/ calls; (4) handleLocaleDetection — sets language cookie from Accept-Language header.") Component(i18n, "hooks.ts (Paraglide)", "SvelteKit Client Hook", "Client-side i18n middleware. Detects language from URL and sets the active locale for Paraglide.js translation functions.") Component(layout, "+layout.server.ts", "SvelteKit Layout Loader", "Passes event.locals.user down to all child pages so every route has access to the authenticated user.") - Component(loginPage, "/login", "SvelteKit Route", "Form action: encodes username:password as Base64 Basic Auth token, POSTs to /api/users/me to validate, sets auth_token httpOnly cookie (SameSite=strict, maxAge=86400), redirects to /.") + Component(loginPage, "/login", "SvelteKit Route", "Form action: encodes email:password as Base64 Basic Auth token, POSTs to /api/users/me to validate, sets auth_token httpOnly cookie (SameSite=strict, maxAge=86400), redirects to /.") Component(logoutPage, "/logout", "SvelteKit Route (server-only)", "Clears the auth_token cookie and redirects to /login.") Component(registerPage, "/register", "SvelteKit Route", "Loader validates invite code via GET /api/auth/invite/{code}. Form action: POST /api/auth/register to create the user account.") Component(forgotPw, "/forgot-password", "SvelteKit Route", "Form action: POST /api/auth/forgot-password. Always responds with success to prevent email enumeration.") -- 2.49.1 From caf86b3225a7158c0a1f1c0d3b57795a94b29d34 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 12:53:32 +0200 Subject: [PATCH 24/34] =?UTF-8?q?fix(c4):=20sequence=20diagram=20=E2=80=94?= =?UTF-8?q?=20username=20=E2=86=92=20email=20in=20auth=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three stale references: "Enter username + password", Base64 encode "user:password", and SELECT WHERE username — all updated to email to match AppUserRepository.findByEmail() and CustomUserDetailsService. Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index b52455a1..ca2281df 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -517,12 +517,12 @@ sequenceDiagram participant Backend as Backend (Spring Boot) participant DB as PostgreSQL - User->>Browser: Enter username + password + User->>Browser: Enter email + password Browser->>Frontend: POST /login (form action) - Frontend->>Frontend: Base64 encode "user:password" + Frontend->>Frontend: Base64 encode "email:password" Frontend->>Backend: GET /api/users/me
Authorization: Basic Backend->>Backend: Spring Security parses Basic Auth - Backend->>DB: SELECT user WHERE username=? + Backend->>DB: SELECT user WHERE email=? DB-->>Backend: AppUser + groups + permissions Backend->>Backend: BCrypt.matches(password, hash) Backend-->>Frontend: 200 OK — UserDTO -- 2.49.1 From 9878810a2af4890aaea448b0fe99490d50895ca8 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 12:54:07 +0200 Subject: [PATCH 25/34] =?UTF-8?q?fix(c4):=20stammbaum=20=E2=80=94=20remove?= =?UTF-8?q?=20D3=20library=20detail=20from=20component=20description?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C4 L3 describes responsibility, not library choice. Removing the D3 reference keeps the description implementation-agnostic. Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index ca2281df..a272c01c 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -456,7 +456,7 @@ C4Component Component(aktivitaeten, "/aktivitaeten", "SvelteKit Route", "Unified activity feed (Chronik). Loader: GET /api/dashboard/activity and GET /api/notifications?read=false.") Component(geschichten, "/geschichten and /geschichten/[id]", "SvelteKit Routes", "Story list and detail pages. Loader: GET /api/geschichten?status=PUBLISHED.") Component(geschichtenEdit, "/geschichten/[id]/edit and /geschichten/new", "SvelteKit Routes", "Story editor with rich text, person and document linking. Actions: PUT/POST /api/geschichten. Requires BLOG_WRITE permission.") - Component(stammbaum, "/stammbaum", "SvelteKit Route", "Family tree visualisation. Loader: GET /api/network (nodes + edges). Renders interactive D3-based Stammbaum.") + Component(stammbaum, "/stammbaum", "SvelteKit Route", "Family tree visualisation. Loader: GET /api/network (nodes + edges). Renders interactive family tree from network graph data.") Component(profilePage, "/profile", "SvelteKit Route", "Current user profile settings. Loader: GET /api/users/me/notification-preferences. Actions: update name/password and notification preferences.") Component(userProfile, "/users/[id]", "SvelteKit Route", "Public user profile view. Loader: GET /api/users/{id}.") } -- 2.49.1 From 7ac39541ef709f8a733cc6db780237629b9326ea Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 12:54:43 +0200 Subject: [PATCH 26/34] docs(c4): add cross-diagram stub convention note to header The C4 standard doesn't define this pattern. Adding a one-sentence explanation so readers unfamiliar with the project's rendering convention understand what stub components outside System_Boundary blocks mean. Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index a272c01c..67e8c53c 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -1,6 +1,8 @@ # Familienarchiv — C4 Architecture Diagrams > For domain terminology used in these diagrams, see [docs/GLOSSARY.md](../GLOSSARY.md). +> +> **Cross-diagram stubs:** Components placed outside a `System_Boundary` block with a "See diagram X" annotation are reference stubs — they represent a component fully defined in another sub-diagram and appear here only to show the cross-domain dependency without duplicating the full definition. ## Level 1 — System Context -- 2.49.1 From b60ad668160360571f5a7b00121091094505e390 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 19:58:26 +0200 Subject: [PATCH 27/34] fix(c4): flatten decimal sub-diagram numbering; note invite gate at L1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename 3b.2→3c, 3c→3d, 3c.2→3e, 3d→3f, 3e→3g to eliminate decimal notation that read as version numbers rather than sub-levels - Update all seven "See diagram X" cross-references to match - Correct backend intro: "three focused views" → "seven focused sub-diagrams" - Add "Access by administrator invite." to L1 Family Member description to surface the invite-only registration constraint at the context level Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture/c4-diagrams.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/architecture/c4-diagrams.md b/docs/architecture/c4-diagrams.md index 67e8c53c..9585f735 100644 --- a/docs/architecture/c4-diagrams.md +++ b/docs/architecture/c4-diagrams.md @@ -13,7 +13,7 @@ C4Context title System Context: Familienarchiv Person(admin, "Administrator", "Manages users, triggers bulk imports, reviews and transcribes documents") - Person(member, "Family Member", "Searches, browses, reads, and transcribes archived documents") + Person(member, "Family Member", "Access by administrator invite. Searches, browses, reads, and transcribes archived documents.") System(familienarchiv, "Familienarchiv", "Web application for digitising, organising, and searching family documents") System_Ext(mail, "Email Service", "SMTP server. Delivers notification emails (mentions, replies) and password-reset links.") @@ -65,7 +65,7 @@ C4Container ## Level 3 — Components: API Backend -The internal structure of the Spring Boot backend, split into three focused views. +The internal structure of the Spring Boot backend, split into seven focused sub-diagrams. ### 3a — Security & Authentication @@ -117,8 +117,8 @@ C4Component Component(docSpec, "DocumentSpecifications", "JPA Criteria API", "Factory for composable predicates: hasText (full-text), hasSender, hasReceiver, isBetween (date range), hasTags (subquery AND/OR logic).") } - Component(personSvc, "PersonService", "Spring Service", "See diagram 3c.2. Called by DocumentService to resolve sender / receiver persons by ID.") - Component(tagSvc, "TagService", "Spring Service", "See diagram 3c. Called by DocumentService to find or create tags by name.") + Component(personSvc, "PersonService", "Spring Service", "See diagram 3e. Called by DocumentService to resolve sender / receiver persons by ID.") + Component(tagSvc, "TagService", "Spring Service", "See diagram 3d. Called by DocumentService to find or create tags by name.") Rel(frontend, docCtrl, "Document requests", "HTTP / JSON") Rel(frontend, adminCtrl, "Trigger import", "HTTP / JSON") @@ -136,7 +136,7 @@ C4Component Rel(docRepo, db, "SQL queries", "JDBC") ``` -### 3b.2 — Document Transcription Pipeline +### 3c — Document Transcription Pipeline Annotation-driven transcription: page markup, text blocks, versioning, and comment threads. @@ -163,7 +163,7 @@ C4Component } Component(documentSvc, "DocumentService", "Spring Service", "See diagram 3b. Called by TranscriptionQueueService to assemble pipeline queue projections.") - Component(auditQuerySvc, "AuditLogQueryService", "Spring Service", "See diagram 3e. Called by TranscriptionQueueService for pipeline activity data.") + Component(auditQuerySvc, "AuditLogQueryService", "Spring Service", "See diagram 3g. Called by TranscriptionQueueService for pipeline activity data.") Rel(frontend, transcriptionCtrl, "Transcription block requests", "HTTP / JSON") Rel(frontend, annotationCtrl, "Annotation requests", "HTTP / JSON") @@ -182,7 +182,7 @@ C4Component Rel(commentRepo, db, "SQL queries", "JDBC") ``` -### 3c — Users, Groups & Administration +### 3d — Users, Groups & Administration User lifecycle, permission groups, tag management, and authentication endpoints. @@ -228,7 +228,7 @@ C4Component Rel(tagRepo, db, "SQL queries", "JDBC") ``` -### 3c.2 — Persons & Family Graph +### 3e — Persons & Family Graph Person management including family relationship modelling and transitive inference. @@ -263,7 +263,7 @@ C4Component Rel(relRepo, db, "SQL queries", "JDBC") ``` -### 3d — OCR Orchestration +### 3f — OCR Orchestration How the Spring Boot backend manages OCR jobs, streams results, and trains recognition models. @@ -286,8 +286,8 @@ C4Component Component(ocrJobRepo, "OcrJobRepository, OcrJobDocumentRepository", "Spring Data JPA", "Reads and writes OcrJob and OcrJobDocument records. Tracks job status (RUNNING/DONE/FAILED), per-document progress, page counts, and error messages.") } - Component(transcriptionSvc, "TranscriptionService", "Spring Service", "See diagram 3b.2. Called by OcrAsyncRunner to persist transcription blocks per page.") - Component(annotationSvc, "AnnotationService", "Spring Service", "See diagram 3b.2. Called by OcrAsyncRunner to persist OCR-generated annotation regions per page.") + Component(transcriptionSvc, "TranscriptionService", "Spring Service", "See diagram 3c. Called by OcrAsyncRunner to persist transcription blocks per page.") + Component(annotationSvc, "AnnotationService", "Spring Service", "See diagram 3c. Called by OcrAsyncRunner to persist OCR-generated annotation regions per page.") Rel(frontend, ocrCtrl, "OCR trigger, status, and progress requests", "HTTP / JSON / SSE") Rel(ocrCtrl, ocrSvc, "Single-document jobs", "") @@ -307,7 +307,7 @@ C4Component Rel(ocrTraining, db, "Persists training run metrics", "JDBC") ``` -### 3e — Supporting Domains +### 3g — Supporting Domains Audit logging, dashboard stats, SSE notifications, stories (Geschichten), and cross-cutting exception handling. @@ -338,7 +338,7 @@ C4Component } Component(documentSvc, "DocumentService", "Spring Service", "See diagram 3b. Called by DashboardService to fetch document titles and resume data.") - Component(transcriptionSvc, "TranscriptionService", "Spring Service", "See diagram 3b.2. Called by DashboardService to fetch transcription block progress for resume.") + Component(transcriptionSvc, "TranscriptionService", "Spring Service", "See diagram 3c. Called by DashboardService to fetch transcription block progress for resume.") Rel(frontend, dashCtrl, "Dashboard requests", "HTTP / JSON") Rel(frontend, statsCtrl, "GET /api/stats", "HTTP / JSON") -- 2.49.1 From d2a4707c3e2fb7f799ab4ddd98959b5a06a79c63 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 21:23:26 +0200 Subject: [PATCH 28/34] docs(c4): add L1 context and L2 containers as C4-PlantUML files --- docs/architecture/c4/l1-context.puml | 16 ++++++++++++++ docs/architecture/c4/l2-containers.puml | 28 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 docs/architecture/c4/l1-context.puml create mode 100644 docs/architecture/c4/l2-containers.puml diff --git a/docs/architecture/c4/l1-context.puml b/docs/architecture/c4/l1-context.puml new file mode 100644 index 00000000..8d2efb8f --- /dev/null +++ b/docs/architecture/c4/l1-context.puml @@ -0,0 +1,16 @@ +@startuml +!include + +title System Context: Familienarchiv + +Person(admin, "Administrator", "Manages users, triggers bulk imports, reviews and transcribes documents") +Person(member, "Family Member", "Access by administrator invite. Searches, browses, reads, and transcribes archived documents.") + +System(familienarchiv, "Familienarchiv", "Web application for digitising, organising, and searching family documents") +System_Ext(mail, "Email Service", "SMTP server. Delivers notification emails (mentions, replies) and password-reset links.") + +Rel(admin, familienarchiv, "Manages via browser", "HTTPS") +Rel(member, familienarchiv, "Searches, reads, and transcribes via browser", "HTTPS") +Rel(familienarchiv, mail, "Sends notification and password-reset emails (optional)", "SMTP") + +@enduml diff --git a/docs/architecture/c4/l2-containers.puml b/docs/architecture/c4/l2-containers.puml new file mode 100644 index 00000000..bd187bca --- /dev/null +++ b/docs/architecture/c4/l2-containers.puml @@ -0,0 +1,28 @@ +@startuml +!include + +title Container Diagram: Familienarchiv + +Person(user, "User", "Admin or family member") +System_Ext(mail, "Email Service", "SMTP server. Delivers notification and password-reset emails.") + +System_Boundary(archiv, "Familienarchiv (Docker Compose)") { + Container(frontend, "Web Frontend", "SvelteKit / Node.js", "Server-side rendered UI. Handles auth session cookies, document search and viewer, transcription editor, annotation layer, family tree (Stammbaum), stories (Geschichten), activity feed (Chronik), enrichment workflow, and admin panel.") + Container(backend, "API Backend", "Spring Boot 4 / Java 21 / Jetty", "REST API. Implements document management, search, user auth, file upload/download, transcription, OCR orchestration, and SSE notifications.") + Container(ocr, "OCR Service", "Python FastAPI / port 8000", "Handwritten text recognition (HTR) and OCR microservice. Single-node by design — see ADR-001. Reachable only on the internal Docker network; no external port exposed.") + ContainerDb(db, "Relational Database", "PostgreSQL 16", "Stores document metadata, persons, users, permission groups, tags, transcription blocks, audit log, and Spring Session data.") + ContainerDb(storage, "Object Storage", "MinIO (S3-compatible)", "Stores the actual document files (PDFs, scans). Objects keyed as documents/{UUID}_{filename}.") + Container(mc, "Bucket Init Helper", "MinIO Client (mc)", "One-shot container on startup. Creates the archive bucket with private access policy.") +} + +Rel(user, frontend, "Uses", "HTTPS / Browser") +Rel(frontend, backend, "API requests with Basic Auth token", "HTTP / REST / JSON") +Rel(backend, user, "SSE notifications (server-sent events)", "HTTP / SSE — direct backend-to-browser") +Rel(backend, db, "Reads and writes metadata and sessions", "JDBC / SQL") +Rel(backend, storage, "Uploads and streams document files", "HTTP / S3 API (AWS SDK v2)") +Rel(backend, ocr, "OCR job requests with presigned MinIO URL", "HTTP / REST / JSON") +Rel(backend, mail, "Sends notification and password-reset emails (optional)", "SMTP") +Rel(ocr, storage, "Fetches PDF via presigned URL", "HTTP / S3 presigned") +Rel(mc, storage, "Creates bucket on startup", "MinIO Client CLI") + +@enduml -- 2.49.1 From f2bb0ca442aa4ecb1fe3891cd163e2f28c0f7a89 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 21:25:53 +0200 Subject: [PATCH 29/34] docs(c4): add L3 backend 3a security and 3b document management --- .../c4/l3-backend-3a-security.puml | 21 ++++++++++ .../c4/l3-backend-3b-document-management.puml | 40 +++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 docs/architecture/c4/l3-backend-3a-security.puml create mode 100644 docs/architecture/c4/l3-backend-3b-document-management.puml diff --git a/docs/architecture/c4/l3-backend-3a-security.puml b/docs/architecture/c4/l3-backend-3a-security.puml new file mode 100644 index 00000000..33e41dc9 --- /dev/null +++ b/docs/architecture/c4/l3-backend-3a-security.puml @@ -0,0 +1,21 @@ +@startuml +!include + +title Component Diagram: API Backend — Security & Authentication + +Container(frontend, "Web Frontend", "SvelteKit") +ContainerDb(db, "PostgreSQL", "PostgreSQL 16") + +System_Boundary(backend, "API Backend (Spring Boot)") { + Component(secFilter, "Security Filter Chain", "Spring Security", "Enforces authentication on all requests. Parses Basic Auth header and constructs an Authentication token; delegates credential validation to DaoAuthenticationProvider via BCrypt. Permits password-reset, invite, and register endpoints without authentication.") + Component(permAspect, "PermissionAspect", "Spring AOP", "Intercepts methods annotated with @RequirePermission. Checks user's granted authorities against the required permission. Throws 401/403 if denied.") + Component(secConf, "SecurityConfig", "Spring @Configuration", "Configures filter chain: all routes require authentication, CSRF disabled, BCrypt password encoder, DaoAuthenticationProvider with CustomUserDetailsService.") + Component(userDetails, "CustomUserDetailsService", "Spring Security UserDetailsService", "Loads AppUser by email from DB. Converts group permissions to Spring GrantedAuthority objects. Logs unknown permissions.") +} + +Rel(frontend, secFilter, "All requests", "HTTP / Basic Auth header") +Rel(secFilter, permAspect, "Authenticated requests reach guarded service methods") +Rel(secConf, userDetails, "Wires as UserDetailsService") +Rel(userDetails, db, "Loads user by email", "JDBC") + +@enduml diff --git a/docs/architecture/c4/l3-backend-3b-document-management.puml b/docs/architecture/c4/l3-backend-3b-document-management.puml new file mode 100644 index 00000000..1b68dbeb --- /dev/null +++ b/docs/architecture/c4/l3-backend-3b-document-management.puml @@ -0,0 +1,40 @@ +@startuml +!include + +title Component Diagram: API Backend — Document Management & Import + +Container(frontend, "Web Frontend", "SvelteKit") +ContainerDb(db, "PostgreSQL", "PostgreSQL 16") +ContainerDb(minio, "Object Storage", "MinIO (S3-compatible)") + +System_Boundary(backend, "API Backend (Spring Boot)") { + Component(docCtrl, "DocumentController", "Spring MVC — /api/documents", "CRUD for documents: search, get by ID, update metadata, upload/download file, conversation thread, and batch metadata updates.") + Component(adminCtrl, "AdminController", "Spring MVC — /api/admin", "Triggers asynchronous Excel/ODS mass import (requires ADMIN permission). Reports import state (IDLE/RUNNING/DONE/FAILED).") + Component(docSvc, "DocumentService", "Spring Service", "Core document business logic: store, update, search. Resolves persons and tags, delegates file I/O to FileService, builds dynamic JPA Specifications, and integrates with audit logging.") + Component(fileSvc, "FileService", "Spring Service", "Wraps AWS SDK v2 S3Client. Uploads files with UUID-keyed paths, computes SHA-256 hash, downloads with content-type detection, and generates presigned URLs for OCR access.") + Component(massImport, "MassImportService", "Spring Service — @Async", "Reads Excel/ODS files from /import mount. Tracks import state (IDLE/RUNNING/DONE/FAILED) and delegates to ExcelService. Returns immediately; processing runs asynchronously.") + Component(excelSvc, "ExcelService", "Spring Service", "Parses Excel/ODS workbooks (Apache POI). Column indices configurable via application.properties. Creates/updates document records per row.") + Component(minioConf, "MinioConfig", "Spring @Configuration", "Creates the S3Client and S3Presigner beans with path-style access for MinIO. Validates MinIO connectivity on startup.") + Component(docRepo, "DocumentRepository", "Spring Data JPA", "Queries documents with Specification-based dynamic search, bidirectional conversation thread queries, full-text search with ranking and match highlighting, and transcription pipeline queue projections.") + Component(docSpec, "DocumentSpecifications", "JPA Criteria API", "Factory for composable predicates: hasText (full-text), hasSender, hasReceiver, isBetween (date range), hasTags (subquery AND/OR logic).") +} + +Component(personSvc, "PersonService", "Spring Service", "See diagram 3e. Called by DocumentService to resolve sender / receiver persons by ID.") +Component(tagSvc, "TagService", "Spring Service", "See diagram 3d. Called by DocumentService to find or create tags by name.") + +Rel(frontend, docCtrl, "Document requests", "HTTP / JSON") +Rel(frontend, adminCtrl, "Trigger import", "HTTP / JSON") +Rel(docCtrl, docSvc, "Delegates to") +Rel(adminCtrl, massImport, "Triggers") +Rel(docSvc, fileSvc, "Upload / download files") +Rel(docSvc, docRepo, "Reads / writes documents") +Rel(docSvc, docSpec, "Builds search predicates") +Rel(docSvc, personSvc, "Resolves sender / receivers") +Rel(docSvc, tagSvc, "Finds or creates tags") +Rel(massImport, excelSvc, "Parses Excel/ODS file") +Rel(excelSvc, docSvc, "Creates / updates documents") +Rel(minioConf, fileSvc, "Provides S3Client and S3Presigner beans") +Rel(fileSvc, minio, "PUT / GET / presigned URL objects", "S3 API / HTTP") +Rel(docRepo, db, "SQL queries", "JDBC") + +@enduml -- 2.49.1 From 63d8065cd889881177e2c75fe80cbaef37d4fc64 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 21:28:02 +0200 Subject: [PATCH 30/34] docs(c4): add L3 backend 3c transcription and 3d users/groups --- .../c4/l3-backend-3c-transcription.puml | 41 +++++++++++++++++++ .../c4/l3-backend-3d-users-groups.puml | 41 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 docs/architecture/c4/l3-backend-3c-transcription.puml create mode 100644 docs/architecture/c4/l3-backend-3d-users-groups.puml diff --git a/docs/architecture/c4/l3-backend-3c-transcription.puml b/docs/architecture/c4/l3-backend-3c-transcription.puml new file mode 100644 index 00000000..cee54e97 --- /dev/null +++ b/docs/architecture/c4/l3-backend-3c-transcription.puml @@ -0,0 +1,41 @@ +@startuml +!include + +title Component Diagram: API Backend — Document Transcription Pipeline + +Container(frontend, "Web Frontend", "SvelteKit") +ContainerDb(db, "PostgreSQL", "PostgreSQL 16") + +System_Boundary(backend, "API Backend (Spring Boot)") { + Component(transcriptionCtrl, "TranscriptionBlockController", "Spring MVC — /api/transcription", "CRUD for transcription text blocks per document page. Manages sort order, review status, and block version history.") + Component(annotationCtrl, "AnnotationController", "Spring MVC — /api/documents/{id}/annotations", "CRUD for free-form page annotations with polygon coordinates, colour coding, and file-hash tracking.") + Component(commentCtrl, "CommentController", "Spring MVC — /api/documents/{id}/comments", "Threaded comment CRUD on transcription blocks with @mention support and notification triggers.") + Component(transcriptionSvc, "TranscriptionService", "Spring Service", "Creates and updates transcription blocks from annotation regions. Tracks block versions, sanitizes text with an HTML allow-list, and triggers mentions.") + Component(transcriptionQueueSvc, "TranscriptionQueueService", "Spring Service", "Assembles segmentation, transcription, and review queue projections by delegating to DocumentService and AuditLogQueryService.") + Component(annotationSvc, "AnnotationService", "Spring Service", "Manages document page annotations with polygon coordinates. Called by OcrAsyncRunner to persist OCR-generated block boundaries.") + Component(commentSvc, "CommentService", "Spring Service", "Creates and manages threaded comments with @mention parsing. Triggers NotificationService for REPLY and MENTION events.") + Component(blockRepo, "TranscriptionBlockRepository", "Spring Data JPA", "Reads and writes TranscriptionBlock and TranscriptionBlockVersion records.") + Component(annotationRepo, "AnnotationRepository", "Spring Data JPA", "Reads and writes DocumentAnnotation records.") + Component(commentRepo, "CommentRepository", "Spring Data JPA", "Reads and writes DocumentComment records.") +} + +Component(documentSvc, "DocumentService", "Spring Service", "See diagram 3b. Called by TranscriptionQueueService to assemble pipeline queue projections.") +Component(auditQuerySvc, "AuditLogQueryService", "Spring Service", "See diagram 3g. Called by TranscriptionQueueService for pipeline activity data.") + +Rel(frontend, transcriptionCtrl, "Transcription block requests", "HTTP / JSON") +Rel(frontend, annotationCtrl, "Annotation requests", "HTTP / JSON") +Rel(frontend, commentCtrl, "Comment requests", "HTTP / JSON") +Rel(transcriptionCtrl, transcriptionSvc, "Delegates to") +Rel(transcriptionCtrl, transcriptionQueueSvc, "Queries pipeline queues") +Rel(annotationCtrl, annotationSvc, "Delegates to") +Rel(commentCtrl, commentSvc, "Delegates to") +Rel(transcriptionSvc, blockRepo, "Reads / writes blocks and versions") +Rel(annotationSvc, annotationRepo, "Reads / writes annotations") +Rel(commentSvc, commentRepo, "Reads / writes comments") +Rel(transcriptionQueueSvc, documentSvc, "Queries pipeline document state") +Rel(transcriptionQueueSvc, auditQuerySvc, "Queries pipeline activity data") +Rel(blockRepo, db, "SQL queries", "JDBC") +Rel(annotationRepo, db, "SQL queries", "JDBC") +Rel(commentRepo, db, "SQL queries", "JDBC") + +@enduml diff --git a/docs/architecture/c4/l3-backend-3d-users-groups.puml b/docs/architecture/c4/l3-backend-3d-users-groups.puml new file mode 100644 index 00000000..d90711c8 --- /dev/null +++ b/docs/architecture/c4/l3-backend-3d-users-groups.puml @@ -0,0 +1,41 @@ +@startuml +!include + +title Component Diagram: API Backend — Users, Groups & Administration + +Container(frontend, "Web Frontend", "SvelteKit") +ContainerDb(db, "PostgreSQL", "PostgreSQL 16") + +System_Boundary(backend, "API Backend (Spring Boot)") { + Component(userCtrl, "UserController", "Spring MVC — /api/users", "Returns current user (/me), creates and deletes users (requires ADMIN_USER), supports user search and profile updates.") + Component(groupCtrl, "GroupController", "Spring MVC — /api/groups", "Lists and manages permission groups.") + Component(tagCtrl, "TagController", "Spring MVC — /api/tags", "Lists tags for typeahead, supports tag merge, tree structure, and subtree deletion.") + Component(inviteCtrl, "InviteController", "Spring MVC — /api/auth/invite", "Creates invite codes and validates them at registration time. Rate-limited via WebConfig interceptor.") + 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.") + Component(groupRepo, "UserGroupRepository", "Spring Data JPA", "Manages permission groups.") + Component(tagRepo, "TagRepository", "Spring Data JPA", "Finds or creates tags by name (case-insensitive). Supports recursive ancestor/descendant CTE queries and merge/reparent helpers.") +} + +Rel(frontend, userCtrl, "User requests", "HTTP / JSON") +Rel(frontend, groupCtrl, "Group requests", "HTTP / JSON") +Rel(frontend, tagCtrl, "Tag requests", "HTTP / JSON") +Rel(frontend, inviteCtrl, "Invite validation", "HTTP / JSON") +Rel(frontend, authCtrl, "Registration and password reset", "HTTP / JSON") +Rel(userCtrl, userSvc, "Delegates to") +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") +Rel(userSvc, groupRepo, "Assigns groups") +Rel(dataInit, db, "Seeds initial data", "JDBC") +Rel(userRepo, db, "SQL queries", "JDBC") +Rel(groupRepo, db, "SQL queries", "JDBC") +Rel(tagRepo, db, "SQL queries", "JDBC") + +@enduml -- 2.49.1 From f9549d7b642cf7d6923538179273e3525add1910 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 21:30:39 +0200 Subject: [PATCH 31/34] docs(c4): add L3 backend 3e persons, 3f OCR, 3g supporting domains --- .../c4/l3-backend-3e-persons.puml | 30 ++++++++++++ docs/architecture/c4/l3-backend-3f-ocr.puml | 41 +++++++++++++++++ .../c4/l3-backend-3g-supporting.puml | 46 +++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 docs/architecture/c4/l3-backend-3e-persons.puml create mode 100644 docs/architecture/c4/l3-backend-3f-ocr.puml create mode 100644 docs/architecture/c4/l3-backend-3g-supporting.puml diff --git a/docs/architecture/c4/l3-backend-3e-persons.puml b/docs/architecture/c4/l3-backend-3e-persons.puml new file mode 100644 index 00000000..47c884aa --- /dev/null +++ b/docs/architecture/c4/l3-backend-3e-persons.puml @@ -0,0 +1,30 @@ +@startuml +!include + +title Component Diagram: API Backend — Persons & Family Graph + +Container(frontend, "Web Frontend", "SvelteKit") +ContainerDb(db, "PostgreSQL", "PostgreSQL 16") + +System_Boundary(backend, "API Backend (Spring Boot)") { + Component(personCtrl, "PersonController", "Spring MVC — /api/persons", "Lists and searches family members. Returns documents sent by or received by a person, correspondent suggestions, and person summary with document counts.") + Component(relCtrl, "RelationshipController", "Spring MVC — /api/network, /api/persons/{id}/relationships", "CRUD for explicit person relationships and the full family network graph (nodes + edges) used by the Stammbaum view.") + Component(personSvc, "PersonService", "Spring Service", "Person CRUD, alias management, and merge operations (reassigns all document sender/receiver references before deleting duplicate persons).") + Component(relSvc, "RelationshipService", "Spring Service", "Manages explicit directional family relationships (PARENT_OF, SPOUSE_OF, SIBLING_OF, etc.) with optional date ranges and notes.") + Component(relInference, "RelationshipInferenceService", "Spring Service", "Computes transitive family relationships from explicit edges to infer grandparent/grandchild, aunt/uncle, and other extended-family links for the network graph.") + Component(personRepo, "PersonRepository", "Spring Data JPA", "Queries persons with name search (including aliases), correspondent discovery, person summaries with document counts, and merge/reassignment helpers.") + Component(relRepo, "PersonRelationshipRepository", "Spring Data JPA", "Reads and writes PersonRelationship records. Supports lookup by person ID, by relation type, and existence checks for deduplication.") +} + +Rel(frontend, personCtrl, "Person requests", "HTTP / JSON") +Rel(frontend, relCtrl, "Relationship and graph requests", "HTTP / JSON") +Rel(personCtrl, personSvc, "Delegates to") +Rel(relCtrl, relSvc, "Delegates to") +Rel(relCtrl, relInference, "Queries inferred graph") +Rel(personSvc, personRepo, "Reads / writes persons") +Rel(relSvc, relRepo, "Reads / writes relationships") +Rel(relInference, relRepo, "Reads relationships for inference") +Rel(personRepo, db, "SQL queries", "JDBC") +Rel(relRepo, db, "SQL queries", "JDBC") + +@enduml diff --git a/docs/architecture/c4/l3-backend-3f-ocr.puml b/docs/architecture/c4/l3-backend-3f-ocr.puml new file mode 100644 index 00000000..c465f72c --- /dev/null +++ b/docs/architecture/c4/l3-backend-3f-ocr.puml @@ -0,0 +1,41 @@ +@startuml +!include + +title Component Diagram: API Backend — OCR Orchestration + +Container(frontend, "Web Frontend", "SvelteKit") +ContainerDb(db, "PostgreSQL", "PostgreSQL 16") +ContainerDb(minio, "Object Storage", "MinIO (S3-compatible)") +Container(ocrPy, "OCR Service", "Python FastAPI") + +System_Boundary(backend, "API Backend (Spring Boot)") { + Component(ocrCtrl, "OcrController", "Spring MVC — /api/ocr", "REST entry point: trigger single or batch OCR jobs, stream progress via SSE, query job status, and manage training runs and per-sender models.") + Component(ocrSvc, "OcrService", "Spring Service", "Creates OcrJob and OcrJobDocument records, checks Python service health, and delegates async execution to OcrAsyncRunner.") + Component(ocrBatch, "OcrBatchService", "Spring Service", "Orchestrates multi-document OCR jobs, iterating documents and delegating each to OcrAsyncRunner.") + Component(ocrAsync, "OcrAsyncRunner", "Spring Component — @Async", "Async worker that streams OCR results from Python page by page, persists transcription blocks and annotations via domain services, and emits progress via SSE.") + Component(ocrClient, "RestClientOcrClient", "Spring Component", "HTTP client wrapping the Python service: POST /ocr/stream (NDJSON), /train, /segtrain, and /train-sender. Falls back from streaming to batch on 404.") + Component(ocrTraining, "OcrTrainingService", "Spring Service", "Orchestrates model training: exports training data as ZIP, calls Python /train or /segtrain, persists training metrics in OcrTrainingRunRepository.") + Component(ocrJobRepo, "OcrJobRepository, OcrJobDocumentRepository", "Spring Data JPA", "Reads and writes OcrJob and OcrJobDocument records. Tracks job status (RUNNING/DONE/FAILED), per-document progress, page counts, and error messages.") +} + +Component(transcriptionSvc, "TranscriptionService", "Spring Service", "See diagram 3c. Called by OcrAsyncRunner to persist transcription blocks per page.") +Component(annotationSvc, "AnnotationService", "Spring Service", "See diagram 3c. Called by OcrAsyncRunner to persist OCR-generated annotation regions per page.") + +Rel(frontend, ocrCtrl, "OCR trigger, status, and progress requests", "HTTP / JSON / SSE") +Rel(ocrCtrl, ocrSvc, "Single-document jobs") +Rel(ocrCtrl, ocrBatch, "Batch jobs") +Rel(ocrCtrl, ocrTraining, "Training runs") +Rel(ocrSvc, ocrAsync, "Delegates async execution") +Rel(ocrBatch, ocrAsync, "Delegates async execution") +Rel(ocrAsync, ocrClient, "Streams OCR results page by page", "HTTP / NDJSON") +Rel(ocrTraining, ocrClient, "Sends training data ZIP", "HTTP / multipart") +Rel(ocrClient, ocrPy, "POST /ocr/stream, /train, /segtrain, /train-sender", "HTTP / REST") +Rel(ocrAsync, transcriptionSvc, "Saves transcription blocks per page") +Rel(ocrAsync, annotationSvc, "Saves annotation regions per page") +Rel(ocrAsync, ocrJobRepo, "Reads / writes OCR job state") +Rel(ocrJobRepo, db, "SQL queries", "JDBC") +Rel(ocrAsync, minio, "Generates presigned URLs for PDF fetch", "S3 API") +Rel(ocrPy, minio, "Fetches PDF via presigned URL", "HTTP / S3 presigned") +Rel(ocrTraining, db, "Persists training run metrics", "JDBC") + +@enduml diff --git a/docs/architecture/c4/l3-backend-3g-supporting.puml b/docs/architecture/c4/l3-backend-3g-supporting.puml new file mode 100644 index 00000000..c1558c89 --- /dev/null +++ b/docs/architecture/c4/l3-backend-3g-supporting.puml @@ -0,0 +1,46 @@ +@startuml +!include + +title Component Diagram: API Backend — Supporting Domains + +Container(frontend, "Web Frontend", "SvelteKit") +ContainerDb(db, "PostgreSQL", "PostgreSQL 16") + +System_Boundary(backend, "API Backend (Spring Boot)") { + 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 (calls DocumentService + TranscriptionService), 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(notifSvc, "NotificationService", "Spring Service", "Creates REPLY and MENTION notifications, optionally sends email, marks as read, and pushes events to connected clients via SseEmitterRegistry.") + Component(sseRegistry, "SseEmitterRegistry", "Spring Component", "In-memory ConcurrentHashMap of Spring SseEmitter instances per user. Handles registration, deregistration, and JSON event broadcasts.") + Component(geschCtrl, "GeschichteController", "Spring MVC — /api/geschichten", "CRUD for publishable stories that link persons and documents. Requires BLOG_WRITE permission for write operations.") + Component(geschSvc, "GeschichteService", "Spring Service", "Manages story lifecycle (DRAFT → PUBLISHED with timestamp). Sanitizes HTML body with an allowlist policy.") + Component(exHandler, "GlobalExceptionHandler", "Spring @RestControllerAdvice", "Converts DomainException, validation errors, and generic exceptions to ErrorResponse JSON with machine-readable ErrorCode and HTTP status.") +} + +Component(documentSvc, "DocumentService", "Spring Service", "See diagram 3b. Called by DashboardService to fetch document titles and resume data.") +Component(transcriptionSvc, "TranscriptionService", "Spring Service", "See diagram 3c. Called by DashboardService to fetch transcription block progress for resume.") + +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(dashSvc, documentSvc, "Fetches document titles and resume data") +Rel(dashSvc, transcriptionSvc, "Fetches transcription block progress for resume") +Rel(notifCtrl, notifSvc, "Delegates to") +Rel(notifCtrl, sseRegistry, "Registers client SSE connection") +Rel(notifSvc, sseRegistry, "Broadcasts events to connected clients") +Rel(geschCtrl, geschSvc, "Delegates to") +Rel(auditSvc, db, "Writes audit_log", "JDBC") +Rel(auditQuery, db, "Reads audit_log", "JDBC") +Rel(notifSvc, db, "Reads / writes notifications", "JDBC") +Rel(geschSvc, db, "Reads / writes geschichten", "JDBC") + +@enduml -- 2.49.1 From e694b58a81b26a329cda3e7d3257183957fd22de Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 21:33:30 +0200 Subject: [PATCH 32/34] docs(c4): add L3 frontend 3a middleware/auth and 3b document workflows --- .../c4/l3-frontend-3a-middleware-auth.puml | 29 ++++++++++++++ .../c4/l3-frontend-3b-document-workflows.puml | 38 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 docs/architecture/c4/l3-frontend-3a-middleware-auth.puml create mode 100644 docs/architecture/c4/l3-frontend-3b-document-workflows.puml diff --git a/docs/architecture/c4/l3-frontend-3a-middleware-auth.puml b/docs/architecture/c4/l3-frontend-3a-middleware-auth.puml new file mode 100644 index 00000000..e02df734 --- /dev/null +++ b/docs/architecture/c4/l3-frontend-3a-middleware-auth.puml @@ -0,0 +1,29 @@ +@startuml +!include + +title Component Diagram: Web Frontend — Middleware, Auth & Layout + +Person(user, "User") +Container(backend, "API Backend", "Spring Boot") + +System_Boundary(frontend, "Web Frontend (SvelteKit / SSR)") { + Component(hooks, "hooks.server.ts", "SvelteKit Server Hook", "Four handle layers: (1) handleAuth — redirects unauthenticated users to /login; (2) userGroup — reads auth_token cookie, fetches /api/users/me, stores user in event.locals; (3) handleFetch — injects Authorization header on all outgoing /api/ calls; (4) handleLocaleDetection — sets language cookie from Accept-Language header.") + Component(i18n, "hooks.ts (Paraglide)", "SvelteKit Client Hook", "Client-side i18n middleware. Detects language from URL and sets the active locale for Paraglide.js translation functions.") + Component(layout, "+layout.server.ts", "SvelteKit Layout Loader", "Passes event.locals.user down to all child pages so every route has access to the authenticated user.") + Component(loginPage, "/login", "SvelteKit Route", "Form action: encodes email:password as Base64 Basic Auth token, POSTs to /api/users/me to validate, sets auth_token httpOnly cookie (SameSite=strict, maxAge=86400), redirects to /.") + Component(logoutPage, "/logout", "SvelteKit Route (server-only)", "Clears the auth_token cookie and redirects to /login.") + Component(registerPage, "/register", "SvelteKit Route", "Loader validates invite code via GET /api/auth/invite/{code}. Form action: POST /api/auth/register to create the user account.") + Component(forgotPw, "/forgot-password", "SvelteKit Route", "Form action: POST /api/auth/forgot-password. Always responds with success to prevent email enumeration.") + Component(resetPw, "/reset-password", "SvelteKit Route", "Form action: POST /api/auth/reset-password with the token from the query string.") +} + +Rel(user, hooks, "Every browser request", "HTTPS") +Rel(hooks, backend, "GET /api/users/me (session check)", "HTTP / Basic Auth") +Rel(hooks, loginPage, "Redirect if no token") +Rel(hooks, layout, "Stores authenticated user in event.locals") +Rel(loginPage, backend, "POST /api/users/me (auth check)", "HTTP / Basic Auth") +Rel(registerPage, backend, "GET /api/auth/invite/{code}, POST /api/auth/register", "HTTP / JSON") +Rel(forgotPw, backend, "POST /api/auth/forgot-password", "HTTP / JSON") +Rel(resetPw, backend, "POST /api/auth/reset-password", "HTTP / JSON") + +@enduml diff --git a/docs/architecture/c4/l3-frontend-3b-document-workflows.puml b/docs/architecture/c4/l3-frontend-3b-document-workflows.puml new file mode 100644 index 00000000..7ba2bb1e --- /dev/null +++ b/docs/architecture/c4/l3-frontend-3b-document-workflows.puml @@ -0,0 +1,38 @@ +@startuml +!include + +title Component Diagram: Web Frontend — Document Workflows + +Person(user, "User") +Container(backend, "API Backend", "Spring Boot") + +System_Boundary(frontend, "Web Frontend (SvelteKit / SSR)") { + Component(homePage, "/ (Home / Search)", "SvelteKit Route", "Loader: parses URL params (q, from, to, senderId, receiverId, tags), fetches /api/documents/search and /api/persons. Renders search form with full-text, date range, sender/receiver typeahead, and tag filters.") + Component(docDetail, "/documents/[id]", "SvelteKit Route", "Loader: GET /api/documents/{id}. Page: metadata panel, inline file viewer, transcription editor, annotation layer, and comment thread.") + Component(docEdit, "/documents/[id]/edit", "SvelteKit Route", "Edit form with PersonTypeahead, TagInput, date/location fields. Form action: PUT /api/documents/{id}.") + Component(docNew, "/documents/new", "SvelteKit Route", "Upload form for a new document. Loader: GET /api/persons. Form action: POST /api/documents with multipart file.") + Component(docBulkEdit, "/documents/bulk-edit", "SvelteKit Route", "Multi-document metadata editor. Loader: GET /api/documents/incomplete. Requires WRITE_ALL (redirects otherwise). Action: PATCH /api/documents/bulk.") + Component(enrichPage, "/enrich/[id]", "SvelteKit Route", "Guided enrichment workflow. Loader: GET /api/documents/{id}. Progressively saves annotations and transcription blocks.") + Component(apiPersons, "/api/persons (SvelteKit API)", "SvelteKit Server Route", "Proxies GET /api/persons?q=... to backend for PersonTypeahead suggestions.") + Component(apiTags, "/api/tags (SvelteKit API)", "SvelteKit Server Route", "Proxies GET /api/tags to backend for TagInput autocomplete.") + Component(typeahead, "PersonTypeahead.svelte", "Svelte Component", "Async autocomplete for selecting a person. Debounces input, calls /api/persons?q=.") + Component(tagInput, "TagInput.svelte", "Svelte Component", "Multi-tag input. Supports free-text entry and selecting existing tags from /api/tags.") +} + +Rel(user, homePage, "Searches and browses", "HTTPS / Browser") +Rel(homePage, backend, "GET /api/documents/search, GET /api/persons", "HTTP / JSON") +Rel(docDetail, backend, "GET /api/documents/{id}, GET /api/documents/{id}/file", "HTTP / JSON + Binary") +Rel(docEdit, backend, "PUT /api/documents/{id}", "HTTP / Multipart") +Rel(docNew, backend, "GET /api/persons, POST /api/documents", "HTTP / JSON + Multipart") +Rel(docBulkEdit, backend, "GET /api/documents/incomplete, PATCH /api/documents/bulk", "HTTP / JSON") +Rel(enrichPage, backend, "GET/POST /api/transcription, POST /api/documents/{id}/annotations", "HTTP / JSON") +Rel(homePage, typeahead, "Uses for sender/receiver filter") +Rel(docEdit, typeahead, "Uses for sender/receiver selection") +Rel(docNew, typeahead, "Uses for sender selection") +Rel(docEdit, tagInput, "Uses for tag management") +Rel(typeahead, apiPersons, "Fetches suggestions", "HTTP") +Rel(tagInput, apiTags, "Fetches existing tags", "HTTP") +Rel(apiPersons, backend, "GET /api/persons", "HTTP / JSON") +Rel(apiTags, backend, "GET /api/tags", "HTTP / JSON") + +@enduml -- 2.49.1 From e46f6cb0afd0d7e90409a666837b0eb1482c08e4 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 21:35:58 +0200 Subject: [PATCH 33/34] docs(c4): add L3 frontend 3c/3d and sequence diagrams --- .../c4/l3-frontend-3c-people-stories.puml | 32 +++++++++++++++++++ .../c4/l3-frontend-3d-administration.puml | 27 ++++++++++++++++ docs/architecture/c4/seq-auth-flow.puml | 26 +++++++++++++++ docs/architecture/c4/seq-document-upload.puml | 32 +++++++++++++++++++ 4 files changed, 117 insertions(+) create mode 100644 docs/architecture/c4/l3-frontend-3c-people-stories.puml create mode 100644 docs/architecture/c4/l3-frontend-3d-administration.puml create mode 100644 docs/architecture/c4/seq-auth-flow.puml create mode 100644 docs/architecture/c4/seq-document-upload.puml diff --git a/docs/architecture/c4/l3-frontend-3c-people-stories.puml b/docs/architecture/c4/l3-frontend-3c-people-stories.puml new file mode 100644 index 00000000..49526211 --- /dev/null +++ b/docs/architecture/c4/l3-frontend-3c-people-stories.puml @@ -0,0 +1,32 @@ +@startuml +!include + +title Component Diagram: Web Frontend — People, Stories & Discovery + +Person(user, "User") +Container(backend, "API Backend", "Spring Boot") + +System_Boundary(frontend, "Web Frontend (SvelteKit / SSR)") { + Component(personsPage, "/persons and /persons/[id]", "SvelteKit Routes", "Person directory and detail. Detail: metadata, document list sent/received, correspondents, explicit and inferred family relationships.") + Component(personEdit, "/persons/[id]/edit and /persons/new", "SvelteKit Routes", "Create and edit person forms. Edit: metadata, aliases, explicit relationships. Actions: PUT/POST /api/persons.") + Component(briefwechsel, "/briefwechsel", "SvelteKit Route", "Bilateral conversation timeline. Selects two persons via PersonTypeahead, fetches GET /api/documents/conversation, displays chronological exchange.") + Component(aktivitaeten, "/aktivitaeten", "SvelteKit Route", "Unified activity feed (Chronik). Loader: GET /api/dashboard/activity and GET /api/notifications?read=false.") + Component(geschichten, "/geschichten and /geschichten/[id]", "SvelteKit Routes", "Story list and detail pages. Loader: GET /api/geschichten?status=PUBLISHED.") + Component(geschichtenEdit, "/geschichten/[id]/edit and /geschichten/new", "SvelteKit Routes", "Story editor with rich text, person and document linking. Actions: PUT/POST /api/geschichten. Requires BLOG_WRITE permission.") + Component(stammbaum, "/stammbaum", "SvelteKit Route", "Family tree visualisation. Loader: GET /api/network (nodes + edges). Renders interactive family tree from network graph data.") + Component(profilePage, "/profile", "SvelteKit Route", "Current user profile settings. Loader: GET /api/users/me/notification-preferences. Actions: update name/password and notification preferences.") + Component(userProfile, "/users/[id]", "SvelteKit Route", "Public user profile view. Loader: GET /api/users/{id}.") +} + +Rel(user, personsPage, "Browses family members", "HTTPS / Browser") +Rel(personsPage, backend, "GET /api/persons, GET /api/persons/{id}", "HTTP / JSON") +Rel(personEdit, backend, "GET /api/persons/{id}, PUT /api/persons/{id}, POST /api/persons", "HTTP / JSON") +Rel(briefwechsel, backend, "GET /api/documents/conversation", "HTTP / JSON") +Rel(aktivitaeten, backend, "GET /api/dashboard/activity, GET /api/notifications", "HTTP / JSON") +Rel(geschichten, backend, "GET /api/geschichten", "HTTP / JSON") +Rel(geschichtenEdit, backend, "GET/PUT/POST /api/geschichten", "HTTP / JSON") +Rel(stammbaum, backend, "GET /api/network", "HTTP / JSON") +Rel(profilePage, backend, "GET/PUT /api/users/me, notification-preferences", "HTTP / JSON") +Rel(userProfile, backend, "GET /api/users/{id}", "HTTP / JSON") + +@enduml diff --git a/docs/architecture/c4/l3-frontend-3d-administration.puml b/docs/architecture/c4/l3-frontend-3d-administration.puml new file mode 100644 index 00000000..3f7c89ef --- /dev/null +++ b/docs/architecture/c4/l3-frontend-3d-administration.puml @@ -0,0 +1,27 @@ +@startuml +!include + +title Component Diagram: Web Frontend — Administration & Help + +Person(admin, "Administrator") +Person(user, "User") +Container(backend, "API Backend", "Spring Boot") + +System_Boundary(frontend, "Web Frontend (SvelteKit / SSR)") { + Component(adminUsers, "/admin/users, /admin/users/[id], /admin/users/new, /admin/invites", "SvelteKit Routes", "User directory, create/update/delete users, and manage invite codes. Requires ADMIN_USER permission.") + Component(adminGroups, "/admin/groups, /admin/groups/[id], /admin/groups/new", "SvelteKit Routes", "Permission group management: create/edit groups and their permission sets.") + Component(adminTags, "/admin/tags and /admin/tags/[id]", "SvelteKit Routes", "Tag administration: edit tag hierarchy, merge tags, delete subtrees.") + Component(adminOcr, "/admin/ocr and /admin/ocr/[personId]", "SvelteKit Routes", "Global and per-person OCR configuration. Manages script types and triggers sender model training.") + Component(adminSystem, "/admin/system", "SvelteKit Route", "System status panel. Triggers Excel/ODS mass import (POST /api/admin/trigger-import). Displays import state.") + Component(hilfe, "/hilfe/transkription", "SvelteKit Route", "Static transcription style guide for Kurrent and Sütterlin character recognition. No backend calls.") +} + +Rel(admin, adminUsers, "Manages users and invites", "HTTPS / Browser") +Rel(user, hilfe, "Views transcription style guide", "HTTPS / Browser") +Rel(adminUsers, backend, "GET/POST/DELETE /api/users, POST /api/auth/invite", "HTTP / JSON") +Rel(adminGroups, backend, "GET/POST/PUT/DELETE /api/groups", "HTTP / JSON") +Rel(adminTags, backend, "GET/PUT/DELETE /api/tags", "HTTP / JSON") +Rel(adminOcr, backend, "GET/POST /api/ocr (global config and sender training)", "HTTP / JSON") +Rel(adminSystem, backend, "POST /api/admin/trigger-import, GET /api/admin/import-status", "HTTP / JSON") + +@enduml diff --git a/docs/architecture/c4/seq-auth-flow.puml b/docs/architecture/c4/seq-auth-flow.puml new file mode 100644 index 00000000..bae4a831 --- /dev/null +++ b/docs/architecture/c4/seq-auth-flow.puml @@ -0,0 +1,26 @@ +@startuml +title Authentication Flow + +actor User +participant Browser +participant "Frontend (SvelteKit)" as Frontend +participant "Backend (Spring Boot)" as Backend +participant PostgreSQL as DB + +User -> Browser: Enter email + password +Browser -> Frontend: POST /login (form action) +Frontend -> Frontend: Base64 encode "email:password" +Frontend -> Backend: GET /api/users/me\nAuthorization: Basic +Backend -> Backend: Spring Security parses Basic Auth +Backend -> DB: SELECT user WHERE email=? +DB --> Backend: AppUser + groups + permissions +Backend -> Backend: BCrypt.matches(password, hash) +Backend --> Frontend: 200 OK — UserDTO +Frontend -> Browser: Set-Cookie: auth_token=\n(httpOnly, SameSite=strict, maxAge=86400) +Browser -> Frontend: GET / (next request) +Frontend -> Frontend: hooks.server.ts reads auth_token cookie +Frontend -> Backend: GET /api/users/me\nAuthorization: Basic +Backend --> Frontend: 200 OK — user in event.locals +Frontend --> Browser: Render page with user context + +@enduml diff --git a/docs/architecture/c4/seq-document-upload.puml b/docs/architecture/c4/seq-document-upload.puml new file mode 100644 index 00000000..474d1e8b --- /dev/null +++ b/docs/architecture/c4/seq-document-upload.puml @@ -0,0 +1,32 @@ +@startuml +title Document Upload Flow + +actor User +participant "Frontend (SvelteKit)" as Frontend +participant "Backend (Spring Boot)" as Backend +participant "PermissionAspect (AOP)" as Aspect +participant DocumentService as DocSvc +participant FileService as FileSvc +participant MinIO +participant PostgreSQL as DB + +User -> Frontend: Submit edit form (file + metadata) +Frontend -> Backend: PUT /api/documents/{id}\nmultipart/form-data + Authorization header +Backend -> Aspect: @RequirePermission(WRITE_ALL) check +Aspect -> Aspect: Verify user has WRITE_ALL authority +Aspect --> Backend: Proceed +Backend -> DocSvc: updateDocument(id, dto, file) +DocSvc -> DocSvc: Resolve sender Person by ID +DocSvc -> DocSvc: Resolve/create Tags +DocSvc -> FileSvc: uploadFile(file, filename) +FileSvc -> FileSvc: Generate key: documents/{UUID}_{filename} +FileSvc -> MinIO: PutObject(bucket, key, stream) +MinIO --> FileSvc: Success +FileSvc --> DocSvc: S3 key +DocSvc -> DB: UPDATE documents SET file_path=?, status='UPLOADED', ... +DB --> DocSvc: OK +DocSvc --> Backend: Updated Document entity +Backend --> Frontend: 200 OK — Document JSON +Frontend --> User: Refreshed document view + +@enduml -- 2.49.1 From 76eb8a1cf56676a29900490c8d3e69a466807d27 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 6 May 2026 21:38:05 +0200 Subject: [PATCH 34/34] docs(c4): add VS Code PlantUML server config and diagram index --- .vscode/settings.json | 4 +++- docs/architecture/c4/README.md | 39 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 docs/architecture/c4/README.md diff --git a/.vscode/settings.json b/.vscode/settings.json index b84f89c3..471d77c1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,6 @@ { "java.configuration.updateBuildConfiguration": "interactive", - "java.compile.nullAnalysis.mode": "automatic" + "java.compile.nullAnalysis.mode": "automatic", + "plantuml.render": "PlantUMLServer", + "plantuml.server": "http://heim-nas:8500" } \ No newline at end of file diff --git a/docs/architecture/c4/README.md b/docs/architecture/c4/README.md new file mode 100644 index 00000000..68b4f427 --- /dev/null +++ b/docs/architecture/c4/README.md @@ -0,0 +1,39 @@ +# C4-PlantUML Diagrams + +Architecture diagrams in C4-PlantUML format. These are the authoritative source for layout-accurate diagrams. The companion `c4-diagrams.md` in the parent directory keeps Mermaid versions for inline Gitea rendering. + +## Render in Gitea + +Gitea is configured to render `.puml` files as diagrams. Open any `.puml` file in the Gitea UI to see the rendered diagram. + +> **Note:** `plantuml` code fences inside Markdown files do **not** render inline in Gitea — this is a Gitea limitation unrelated to the server configuration. The `.md` files in this repo use Mermaid for that reason. + +## Render in VS Code + +Install the [PlantUML extension](https://marketplace.visualstudio.com/items?itemName=jebbs.plantuml) (`jebbs.plantuml`). The project's `.vscode/settings.json` already points at the shared server: + +``` +plantuml.server = http://heim-nas:8500 +``` + +Open any `.puml` file and press `Alt+D` to preview. + +## Files + +| File | Diagram | +|---|---| +| `l1-context.puml` | Level 1 — System Context | +| `l2-containers.puml` | Level 2 — Containers | +| `l3-backend-3a-security.puml` | L3 Backend: Security & Authentication | +| `l3-backend-3b-document-management.puml` | L3 Backend: Document Management & Import | +| `l3-backend-3c-transcription.puml` | L3 Backend: Document Transcription Pipeline | +| `l3-backend-3d-users-groups.puml` | L3 Backend: Users, Groups & Administration | +| `l3-backend-3e-persons.puml` | L3 Backend: Persons & Family Graph | +| `l3-backend-3f-ocr.puml` | L3 Backend: OCR Orchestration | +| `l3-backend-3g-supporting.puml` | L3 Backend: Supporting Domains | +| `l3-frontend-3a-middleware-auth.puml` | L3 Frontend: Middleware, Auth & Layout | +| `l3-frontend-3b-document-workflows.puml` | L3 Frontend: Document Workflows | +| `l3-frontend-3c-people-stories.puml` | L3 Frontend: People, Stories & Discovery | +| `l3-frontend-3d-administration.puml` | L3 Frontend: Administration & Help | +| `seq-auth-flow.puml` | Sequence: Authentication Flow | +| `seq-document-upload.puml` | Sequence: Document Upload Flow | -- 2.49.1