Compare commits
24 Commits
feat/issue
...
b0aa3a6ffd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0aa3a6ffd | ||
|
|
d01b9a7508 | ||
|
|
d69a3abc3b | ||
|
|
5c72364899 | ||
|
|
50b18f0849 | ||
|
|
6cf5405b7a | ||
|
|
86c13a230c | ||
|
|
513fda2888 | ||
|
|
995c696c6a | ||
|
|
9b2ed48689 | ||
|
|
a1b89670c0 | ||
|
|
a3c17750cd | ||
|
|
83db80b867 | ||
|
|
a944563560 | ||
|
|
8225baf578 | ||
|
|
bab30fe29c | ||
|
|
69b564b34b | ||
|
|
fc53038af2 | ||
|
|
869885eb78 | ||
|
|
a9b8e19dea | ||
|
|
080e8eb55f | ||
|
|
a5f4b0df31 | ||
|
|
9dae044eec | ||
|
|
5302075124 |
@@ -38,10 +38,10 @@ Screen readers and search engines rely on landmarks to navigate. Every page need
|
|||||||
|
|
||||||
2. **Use CSS custom properties for all brand colors**
|
2. **Use CSS custom properties for all brand colors**
|
||||||
```css
|
```css
|
||||||
/* layout.css */
|
/* layout.css — semantic tokens backed by CSS variables (see --palette-* for raw values) */
|
||||||
--color-ink: #002850;
|
--color-ink: var(--c-ink);
|
||||||
--color-accent: #A6DAD8;
|
--color-accent: var(--c-accent);
|
||||||
--color-surface: #E4E2D7;
|
--color-surface: var(--c-surface);
|
||||||
```
|
```
|
||||||
```svelte
|
```svelte
|
||||||
<div class="text-ink bg-surface border-line">
|
<div class="text-ink bg-surface border-line">
|
||||||
@@ -103,9 +103,9 @@ unsaved work without warning.
|
|||||||
|
|
||||||
1. **Enforce WCAG AA contrast ratios**
|
1. **Enforce WCAG AA contrast ratios**
|
||||||
```
|
```
|
||||||
brand-navy (#002850) on white: 14.5:1 -- AAA pass
|
brand-navy (--palette-navy) on white: ~14.5:1 -- AAA pass (verify exact value in layout.css)
|
||||||
brand-mint (#A6DAD8) on navy: 7.2:1 -- AAA pass for large text
|
brand-mint (--palette-mint) on navy: ~7.2:1 -- AAA pass for large text
|
||||||
Gray-500 on white: check >= 4.5:1 -- AA minimum for body text
|
Gray-500 on white: check >= 4.5:1 -- AA minimum for body text
|
||||||
```
|
```
|
||||||
Always verify contrast with a tool. AA is the floor (4.5:1 normal text, 3:1 large text). Target AAA (7:1) for body copy.
|
Always verify contrast with a tool. AA is the floor (4.5:1 normal text, 3:1 large text). Target AAA (7:1) for body copy.
|
||||||
|
|
||||||
@@ -134,8 +134,8 @@ Color-blind users (8% of men) cannot distinguish status by color alone. Always p
|
|||||||
/* Silver #CACAC9 on white = 1.5:1 -- fails all WCAG levels */
|
/* Silver #CACAC9 on white = 1.5:1 -- fails all WCAG levels */
|
||||||
.caption { color: #CACAC9; }
|
.caption { color: #CACAC9; }
|
||||||
|
|
||||||
/* brand-mint on white = 2.8:1 -- fails AA for normal text */
|
/* brand-mint on white = ~2.8:1 -- fails AA for normal text */
|
||||||
.label { color: #A6DAD8; }
|
.label { color: var(--palette-mint); }
|
||||||
```
|
```
|
||||||
Test every text color against its background. Decorative palette colors are for borders and backgrounds, not text.
|
Test every text color against its background. Decorative palette colors are for borders and backgrounds, not text.
|
||||||
|
|
||||||
@@ -338,7 +338,7 @@ Test at 320px (small phone), 768px (tablet), and 1440px (desktop). Review diffs
|
|||||||
<table>
|
<table>
|
||||||
<tr><td>Section title</td><td><code>text-xs font-bold uppercase tracking-widest</code></td>
|
<tr><td>Section title</td><td><code>text-xs font-bold uppercase tracking-widest</code></td>
|
||||||
<td>12px / 700</td><td>Most commonly undersized</td></tr>
|
<td>12px / 700</td><td>Most commonly undersized</td></tr>
|
||||||
<tr><td>Card container</td><td><code>bg-white shadow-sm border border-brand-sand rounded-sm p-6</code></td>
|
<tr><td>Card container</td><td><code>bg-surface shadow-sm border border-line rounded-sm p-6</code></td>
|
||||||
<td>padding 24px</td><td>—</td></tr>
|
<td>padding 24px</td><td>—</td></tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -376,10 +376,10 @@ await page.setViewportSize({ width: 1440, height: 900 });
|
|||||||
## Domain Expertise
|
## Domain Expertise
|
||||||
|
|
||||||
### Brand Palette
|
### Brand Palette
|
||||||
- **Primary**: brand-navy `#002850` (text, buttons, headers), brand-mint `#A6DAD8` (accents, hover), brand-sand `#E4E2D7` (backgrounds, borders)
|
- **Primary**: `brand-navy` (`--palette-navy`) — text, buttons, headers; `brand-mint` (`--palette-mint`) — accents, hover; sand (`--palette-sand`) — page background (use `bg-canvas` or `bg-surface` as Tailwind utilities, not `bg-brand-sand`)
|
||||||
- **Typography**: `font-serif` (Merriweather) for body/titles, `font-sans` (Montserrat) for labels/UI chrome
|
- **Typography**: `font-serif` (Tinos) for body/titles, `font-sans` (Montserrat) for labels/UI chrome
|
||||||
- **Card pattern**: `bg-white shadow-sm border border-brand-sand rounded-sm p-6`
|
- **Card pattern**: `bg-surface shadow-sm border border-line rounded-sm p-6`
|
||||||
- **Section title**: `text-xs font-bold uppercase tracking-widest text-gray-400 mb-5`
|
- **Section title**: `text-xs font-bold uppercase tracking-widest text-ink-3 mb-5`
|
||||||
|
|
||||||
### Dual-Audience Design (25-42 AND 60+)
|
### Dual-Audience Design (25-42 AND 60+)
|
||||||
- Seniors: 16px minimum body text (prefer 18px), 44px touch targets (prefer 48px), redundant cues, calm layouts, persistent navigation, no timed interactions
|
- Seniors: 16px minimum body text (prefer 18px), 44px touch targets (prefer 48px), redundant cues, calm layouts, persistent navigation, no timed interactions
|
||||||
|
|||||||
@@ -1,96 +1,3 @@
|
|||||||
# Dev Container — Familienarchiv
|
# Dev Container
|
||||||
|
|
||||||
## Overview
|
→ See [.devcontainer/README.md](./README.md) for configuration, usage, and known limitations.
|
||||||
|
|
||||||
VS Code Dev Container configuration for a pre-configured development environment. Includes Java 21, Maven, and Node.js 24 — everything needed to work on both backend and frontend.
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
File: `.devcontainer/devcontainer.json`
|
|
||||||
|
|
||||||
### Included Features
|
|
||||||
|
|
||||||
| Feature | Version | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| Java | 21 | Spring Boot backend |
|
|
||||||
| Maven | bundled with Java feature | Build tool |
|
|
||||||
| Node.js | 24 | SvelteKit frontend |
|
|
||||||
|
|
||||||
### VS Code Extensions (Auto-installed)
|
|
||||||
|
|
||||||
| Extension | Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `vscjava.vscode-java-pack` | Java language support, debugging, testing |
|
|
||||||
| `vmware.vscode-spring-boot` | Spring Boot tooling |
|
|
||||||
| `gabrielbb.vscode-lombok` | Lombok annotation support |
|
|
||||||
| `humao.rest-client` | HTTP request files (for `backend/api_tests/`) |
|
|
||||||
|
|
||||||
### Ports
|
|
||||||
|
|
||||||
- `8080` forwarded to host — access backend at `http://localhost:8080`
|
|
||||||
|
|
||||||
### User
|
|
||||||
|
|
||||||
Runs as `vscode` user (not root) for security.
|
|
||||||
|
|
||||||
## How to Use
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- VS Code with the **Dev Containers** extension installed
|
|
||||||
- Docker running locally
|
|
||||||
|
|
||||||
### Open in Dev Container
|
|
||||||
|
|
||||||
1. Open the project in VS Code
|
|
||||||
2. Press `F1` → type "Dev Containers: Reopen in Container"
|
|
||||||
3. VS Code will:
|
|
||||||
- Build the container using the root `docker-compose.yml`
|
|
||||||
- Install Java 21, Maven, and Node 24
|
|
||||||
- Install the listed extensions
|
|
||||||
- Mount the workspace folder
|
|
||||||
|
|
||||||
### Working Inside the Container
|
|
||||||
|
|
||||||
Once inside the container, you have access to both stacks:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backend
|
|
||||||
cd backend
|
|
||||||
./mvnw spring-boot:run
|
|
||||||
|
|
||||||
# Frontend (in a new terminal)
|
|
||||||
cd frontend
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
The container reuses the `docker-compose.yml` services, so PostgreSQL and MinIO are available automatically.
|
|
||||||
|
|
||||||
### Forwarding Frontend Port
|
|
||||||
|
|
||||||
The devcontainer config only forwards port 8080 by default. To access the frontend dev server (port 5173 or 3000), either:
|
|
||||||
|
|
||||||
1. Add `5173` to `forwardPorts` in `devcontainer.json`, or
|
|
||||||
2. Use the VS Code "Ports" panel to forward it dynamically
|
|
||||||
|
|
||||||
## Limitations
|
|
||||||
|
|
||||||
- The devcontainer attaches to the `backend` service from `docker-compose.yml`, so it inherits those environment variables
|
|
||||||
- OCR service and other containers should be started separately via `docker-compose up -d`
|
|
||||||
- GPU passthrough for OCR training is not configured
|
|
||||||
|
|
||||||
## Customization
|
|
||||||
|
|
||||||
To add more tools or extensions, edit `.devcontainer/devcontainer.json`:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"features": {
|
|
||||||
"ghcr.io/devcontainers/features/python:1": {
|
|
||||||
"version": "3.11"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"forwardPorts": [8080, 5173, 3000]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|||||||
94
.devcontainer/README.md
Normal file
94
.devcontainer/README.md
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# Dev Container — Familienarchiv
|
||||||
|
|
||||||
|
VS Code Dev Container configuration for a pre-configured development environment. Includes Java 21, Maven, and Node.js 24 — everything needed to work on both backend and frontend.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
File: `.devcontainer/devcontainer.json`
|
||||||
|
|
||||||
|
### Included Features
|
||||||
|
|
||||||
|
| Feature | Version | Purpose |
|
||||||
|
| ------- | ------------------------- | ------------------- |
|
||||||
|
| Java | 21 | Spring Boot backend |
|
||||||
|
| Maven | bundled with Java feature | Build tool |
|
||||||
|
| Node.js | 24 | SvelteKit frontend |
|
||||||
|
|
||||||
|
### VS Code Extensions (Auto-installed)
|
||||||
|
|
||||||
|
| Extension | Purpose |
|
||||||
|
| --------------------------- | --------------------------------------------- |
|
||||||
|
| `vscjava.vscode-java-pack` | Java language support, debugging, testing |
|
||||||
|
| `vmware.vscode-spring-boot` | Spring Boot tooling |
|
||||||
|
| `gabrielbb.vscode-lombok` | Lombok annotation support |
|
||||||
|
| `humao.rest-client` | HTTP request files (for `backend/api_tests/`) |
|
||||||
|
|
||||||
|
### Ports
|
||||||
|
|
||||||
|
- `8080` forwarded to host — access backend at `http://localhost:8080`
|
||||||
|
|
||||||
|
### User
|
||||||
|
|
||||||
|
Runs as `vscode` user (not root) for security.
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- VS Code with the **Dev Containers** extension installed
|
||||||
|
- Docker running locally
|
||||||
|
|
||||||
|
### Open in Dev Container
|
||||||
|
|
||||||
|
1. Open the project in VS Code
|
||||||
|
2. Press `F1` → type "Dev Containers: Reopen in Container"
|
||||||
|
3. VS Code will:
|
||||||
|
- Build the container using the root `docker-compose.yml`
|
||||||
|
- Install Java 21, Maven, and Node 24
|
||||||
|
- Install the listed extensions
|
||||||
|
- Mount the workspace folder
|
||||||
|
|
||||||
|
### Working Inside the Container
|
||||||
|
|
||||||
|
Once inside the container, you have access to both stacks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
cd backend
|
||||||
|
./mvnw spring-boot:run
|
||||||
|
|
||||||
|
# Frontend (in a new terminal)
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The container reuses the `docker-compose.yml` services, so PostgreSQL and MinIO are available automatically.
|
||||||
|
|
||||||
|
### Forwarding Frontend Port
|
||||||
|
|
||||||
|
The devcontainer config only forwards port 8080 by default. To access the frontend dev server (port 5173 or 3000), either:
|
||||||
|
|
||||||
|
1. Add `5173` to `forwardPorts` in `devcontainer.json`, or
|
||||||
|
2. Use the VS Code "Ports" panel to forward it dynamically
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- The devcontainer attaches to the `backend` service from `docker-compose.yml`, so it inherits those environment variables
|
||||||
|
- OCR service and other containers should be started separately via `docker-compose up -d`
|
||||||
|
- GPU passthrough for OCR training is not configured
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
To add more tools or extensions, edit `.devcontainer/devcontainer.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/python:1": {
|
||||||
|
"version": "3.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forwardPorts": [8080, 5173, 3000]
|
||||||
|
}
|
||||||
|
```
|
||||||
241
CLAUDE.md
241
CLAUDE.md
@@ -1,7 +1,11 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
|
> For a human-readable project overview, see [README.md](./README.md).
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
> For a human-readable project overview, see [README.md](./README.md).
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
**Familienarchiv** is a family document archival system — a full-stack web app for digitizing, organizing, and searching family documents. Key features: file uploads (stored in MinIO/S3), metadata management, Excel/ODS batch import, full-text search, conversation threads between family members, and role-based access control.
|
**Familienarchiv** is a family document archival system — a full-stack web app for digitizing, organizing, and searching family documents. Key features: file uploads (stored in MinIO/S3), metadata management, Excel/ODS batch import, full-text search, conversation threads between family members, and role-based access control.
|
||||||
@@ -16,6 +20,8 @@ See [CODESTYLE.md](./CODESTYLE.md) for coding standards: Clean Code, DRY/KISS tr
|
|||||||
|
|
||||||
## Stack
|
## Stack
|
||||||
|
|
||||||
|
→ See [README.md §Tech Stack](./README.md#tech-stack)
|
||||||
|
|
||||||
- **Backend**: Spring Boot 4.0 (Java 21, Maven, Jetty, JPA/Hibernate, Flyway, Spring Security, Spring Session JDBC)
|
- **Backend**: Spring Boot 4.0 (Java 21, Maven, Jetty, JPA/Hibernate, Flyway, Spring Security, Spring Session JDBC)
|
||||||
- **Frontend**: SvelteKit 2 with Svelte 5, TypeScript, Tailwind CSS 4, Paraglide.js (i18n: de/en/es)
|
- **Frontend**: SvelteKit 2 with Svelte 5, TypeScript, Tailwind CSS 4, Paraglide.js (i18n: de/en/es)
|
||||||
- **Database**: PostgreSQL 16
|
- **Database**: PostgreSQL 16
|
||||||
@@ -25,12 +31,13 @@ See [CODESTYLE.md](./CODESTYLE.md) for coding standards: Clean Code, DRY/KISS tr
|
|||||||
## Common Commands
|
## Common Commands
|
||||||
|
|
||||||
### Running the Full Stack
|
### Running the Full Stack
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# From repo root — starts PostgreSQL, MinIO, and Spring Boot backend
|
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### Backend (Spring Boot)
|
### Backend (Spring Boot)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
|
|
||||||
@@ -42,11 +49,12 @@ cd backend
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Frontend (SvelteKit)
|
### Frontend (SvelteKit)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
|
|
||||||
npm install
|
npm install
|
||||||
npm run dev # Dev server (port 3000)
|
npm run dev # Dev server (port 5173)
|
||||||
npm run build # Production build
|
npm run build # Production build
|
||||||
npm run preview # Preview production build
|
npm run preview # Preview production build
|
||||||
|
|
||||||
@@ -64,7 +72,7 @@ npm run generate:api # Regenerate TypeScript API types from OpenAPI spec
|
|||||||
|
|
||||||
### Package Structure
|
### Package Structure
|
||||||
|
|
||||||
Package-by-domain: each domain owns its controller, service, repository, entities, and DTOs.
|
<!-- TODO: rewrite post-REFACTOR-1 — see Epic 4 -->
|
||||||
|
|
||||||
```
|
```
|
||||||
backend/src/main/java/org/raddatz/familienarchiv/
|
backend/src/main/java/org/raddatz/familienarchiv/
|
||||||
@@ -88,27 +96,21 @@ backend/src/main/java/org/raddatz/familienarchiv/
|
|||||||
└── user/ User domain — AppUser, UserGroup, UserService, auth controllers
|
└── user/ User domain — AppUser, UserGroup, UserService, auth controllers
|
||||||
```
|
```
|
||||||
|
|
||||||
### Layering Rules (strictly enforced)
|
### Layering Rules
|
||||||
|
|
||||||
```
|
→ See [docs/ARCHITECTURE.md §Layering rule](./docs/ARCHITECTURE.md#layering-rule)
|
||||||
Controller → Service → Repository → DB
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Controllers** never inject or call repositories directly.
|
**LLM reminder:** controllers never call repositories directly; services never reach into another domain's repository — always call the other domain's service instead.
|
||||||
- **Services** never reach into another domain's repository. Call the other domain's service instead.
|
|
||||||
- ✅ `DocumentService` → `PersonService.getById()` → `PersonRepository`
|
|
||||||
- ❌ `DocumentService` → `PersonRepository` directly
|
|
||||||
- This keeps domain boundaries clear and business logic testable in isolation.
|
|
||||||
|
|
||||||
### Domain Model
|
### Domain Model
|
||||||
|
|
||||||
| Entity | Table | Key relationships |
|
| Entity | Table | Key relationships |
|
||||||
|---|---|---|
|
| ----------- | ------------- | ------------------------------------------------------------------------------------- |
|
||||||
| `Document` | `documents` | ManyToOne `sender` (Person), ManyToMany `receivers` (Person), ManyToMany `tags` (Tag) |
|
| `Document` | `documents` | ManyToOne `sender` (Person), ManyToMany `receivers` (Person), ManyToMany `tags` (Tag) |
|
||||||
| `Person` | `persons` | Referenced by documents as sender/receiver |
|
| `Person` | `persons` | Referenced by documents as sender/receiver |
|
||||||
| `Tag` | `tag` | ManyToMany with documents via `document_tags` |
|
| `Tag` | `tag` | ManyToMany with documents via `document_tags` |
|
||||||
| `AppUser` | `app_users` | ManyToMany `groups` (UserGroup) |
|
| `AppUser` | `app_users` | ManyToMany `groups` (UserGroup) |
|
||||||
| `UserGroup` | `user_groups` | Has a `Set<String> permissions` |
|
| `UserGroup` | `user_groups` | Has a `Set<String> permissions` |
|
||||||
|
|
||||||
**`DocumentStatus` lifecycle:** `PLACEHOLDER → UPLOADED → TRANSCRIBED → REVIEWED → ARCHIVED`
|
**`DocumentStatus` lifecycle:** `PLACEHOLDER → UPLOADED → TRANSCRIBED → REVIEWED → ARCHIVED`
|
||||||
|
|
||||||
@@ -118,6 +120,7 @@ Controller → Service → Repository → DB
|
|||||||
### Entity Code Style
|
### Entity Code Style
|
||||||
|
|
||||||
All entities use these Lombok annotations:
|
All entities use these Lombok annotations:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "table_name")
|
@Table(name = "table_name")
|
||||||
@@ -146,65 +149,29 @@ Services are annotated with `@Service`, `@RequiredArgsConstructor`, and optional
|
|||||||
- Read methods are not annotated (default non-transactional is fine).
|
- Read methods are not annotated (default non-transactional is fine).
|
||||||
- Each service owns its domain's repository. Cross-domain data access goes through the other domain's service.
|
- Each service owns its domain's repository. Cross-domain data access goes through the other domain's service.
|
||||||
|
|
||||||
**Existing services:**
|
|
||||||
|
|
||||||
| Service | Responsibility |
|
|
||||||
|---|---|
|
|
||||||
| `DocumentService` | Document CRUD, search, tag cascade delete |
|
|
||||||
| `PersonService` | Person CRUD, find-or-create by alias |
|
|
||||||
| `TagService` | Tag find/create/update/delete |
|
|
||||||
| `UserService` | User and group CRUD |
|
|
||||||
| `FileService` | S3/MinIO upload and download |
|
|
||||||
| `MassImportService` | Async ODS/Excel import; delegates to PersonService and TagService |
|
|
||||||
|
|
||||||
### DTOs
|
### DTOs
|
||||||
|
|
||||||
Input DTOs live in `dto/`. Response types are the model entities themselves (no response DTOs).
|
Input DTOs live flat in the domain package. Response types are the model entities themselves (no response DTOs).
|
||||||
|
|
||||||
- `DocumentUpdateDTO` — used for both create and update (all fields optional)
|
- `@Schema(requiredMode = REQUIRED)` on every field the backend always populates — drives TypeScript generation.
|
||||||
- `CreateUserRequest` — user creation
|
|
||||||
- `GroupDTO` — group create/update
|
|
||||||
|
|
||||||
### Error Handling
|
### Error Handling
|
||||||
|
|
||||||
Use `DomainException` for all domain errors. Never throw raw exceptions from service methods.
|
→ See [CONTRIBUTING.md §Error handling](./CONTRIBUTING.md#error-handling)
|
||||||
|
|
||||||
```java
|
**LLM reminder:** use `DomainException.notFound/forbidden/conflict/internal()` from service methods — never throw raw exceptions. When adding a new `ErrorCode`: (1) add to `ErrorCode.java`, (2) mirror in `frontend/src/lib/shared/errors.ts`, (3) add i18n keys in `messages/{de,en,es}.json`.
|
||||||
// Static factories match common HTTP status codes:
|
|
||||||
DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id)
|
|
||||||
DomainException.forbidden("Access denied")
|
|
||||||
DomainException.conflict(ErrorCode.IMPORT_ALREADY_RUNNING, "Already running")
|
|
||||||
DomainException.internal(ErrorCode.FILE_UPLOAD_FAILED, "Upload failed: " + e.getMessage())
|
|
||||||
```
|
|
||||||
|
|
||||||
`ErrorCode` is an enum in `exception/ErrorCode.java`. When adding a new error case, add the value there **and** mirror it in the frontend's `src/lib/errors.ts` + add a Paraglide translation key.
|
|
||||||
|
|
||||||
For simple validation in controllers (not domain logic), `ResponseStatusException` is acceptable:
|
|
||||||
```java
|
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "firstName is required");
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security / Permissions
|
### Security / Permissions
|
||||||
|
|
||||||
Use `@RequirePermission` on controller methods (or the whole controller class):
|
→ See [docs/ARCHITECTURE.md §Permission system](./docs/ARCHITECTURE.md#permission-system)
|
||||||
|
|
||||||
```java
|
**LLM reminder:** `@RequirePermission(Permission.WRITE_ALL)` is **required** on every `POST`, `PUT`, `PATCH`, `DELETE` endpoint — not optional. Do not mix with Spring Security's `@PreAuthorize`. Available permissions: `READ_ALL`, `WRITE_ALL`, `ADMIN`, `ADMIN_USER`, `ADMIN_TAG`, `ADMIN_PERMISSION`, `ANNOTATE_ALL`, `BLOG_WRITE`.
|
||||||
@RequirePermission(Permission.WRITE_ALL)
|
|
||||||
public Document updateDocument(...) { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
Available permissions: `READ_ALL`, `WRITE_ALL`, `ADMIN`, `ADMIN_USER`, `ADMIN_TAG`, `ADMIN_PERMISSION`
|
|
||||||
|
|
||||||
`PermissionAspect` (AOP) checks the current user's `UserGroup.permissions` at runtime.
|
|
||||||
|
|
||||||
### OpenAPI / API Types
|
### OpenAPI / API Types
|
||||||
|
|
||||||
SpringDoc generates the spec at `/v3/api-docs` (only accessible when running with `--spring.profiles.active=dev`).
|
→ See [CONTRIBUTING.md §Walkthrough B — Add a new endpoint](./CONTRIBUTING.md#4-walkthrough-b--add-a-new-endpoint)
|
||||||
|
|
||||||
When changing any model field or endpoint:
|
**LLM reminder:** always run `npm run generate:api` in `frontend/` after any backend model or endpoint change — this is the most common cause of TypeScript type errors.
|
||||||
1. Rebuild the backend JAR with `-DskipTests`
|
|
||||||
2. Start it with `--spring.profiles.active=dev`
|
|
||||||
3. Run `npm run generate:api` in `frontend/`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -214,147 +181,99 @@ When changing any model field or endpoint:
|
|||||||
|
|
||||||
```
|
```
|
||||||
frontend/src/routes/
|
frontend/src/routes/
|
||||||
├── +layout.svelte Global header (sticky), nav links, logout
|
├── +layout.svelte / +layout.server.ts Global layout, auth cookie
|
||||||
├── +layout.server.ts Loads current user, injects auth cookie
|
├── +page.svelte / +page.server.ts Home / document search dashboard
|
||||||
├── +page.svelte Home / document search
|
|
||||||
├── +page.server.ts Load: search documents; no actions
|
|
||||||
├── documents/
|
├── documents/
|
||||||
│ ├── [id]/+page.svelte Document detail (view + file preview)
|
│ ├── [id]/ Document detail (view + file preview)
|
||||||
│ └── [id]/edit/ Edit form (all metadata + file upload)
|
│ ├── [id]/edit/ Edit form (all metadata + file upload)
|
||||||
│ └── new/ Create form (same fields, empty)
|
│ ├── new/ Upload form
|
||||||
|
│ └── bulk-edit/ Multi-document edit
|
||||||
├── persons/
|
├── persons/
|
||||||
│ ├── +page.svelte Person list with search
|
│ ├── [id]/ Person detail
|
||||||
│ ├── [id]/+page.svelte Person detail (inline edit + merge)
|
│ ├── [id]/edit/ Person edit form
|
||||||
│ └── new/ Create person form
|
│ └── new/ Create person form
|
||||||
├── conversations/ Bilateral conversation timeline
|
├── briefwechsel/ Bilateral conversation timeline (Briefwechsel)
|
||||||
├── admin/ User + group + tag management
|
├── aktivitaeten/ Unified activity feed (Chronik)
|
||||||
└── login/ logout/ Auth pages
|
├── geschichten/ Stories — list, [id], [id]/edit, new
|
||||||
|
├── stammbaum/ Family tree (Stammbaum)
|
||||||
|
├── enrich/ Enrichment workflow — [id], done
|
||||||
|
├── admin/ User, group, tag, OCR, system management
|
||||||
|
├── hilfe/transkription/ Transcription help page
|
||||||
|
├── profile/ User profile settings
|
||||||
|
├── users/[id]/ Public user profile page
|
||||||
|
├── login/ logout/ register/
|
||||||
|
├── forgot-password/ reset-password/
|
||||||
|
└── demo/ Dev-only demos
|
||||||
```
|
```
|
||||||
|
|
||||||
### API Client Pattern
|
### API Client Pattern
|
||||||
|
|
||||||
All server-side API calls use the typed client from `$lib/api.server.ts`:
|
→ See [CONTRIBUTING.md §Frontend API client](./CONTRIBUTING.md#frontend-api-client)
|
||||||
|
|
||||||
```typescript
|
**LLM reminder:** check `!result.response.ok` (not `result.error` — breaks when spec has no error responses defined); cast errors as `result.error as unknown as { code?: string }`; use `result.data!` after an ok check.
|
||||||
const api = createApiClient(fetch);
|
|
||||||
const result = await api.GET('/api/persons/{id}', { params: { path: { id } } });
|
|
||||||
|
|
||||||
// Always check via response.ok, NOT result.error
|
|
||||||
if (!result.response.ok) {
|
|
||||||
const code = (result.error as unknown as { code?: string })?.code;
|
|
||||||
throw error(result.response.status, getErrorMessage(code));
|
|
||||||
}
|
|
||||||
return { person: result.data! };
|
|
||||||
```
|
|
||||||
|
|
||||||
Key rules:
|
|
||||||
- Use `!result.response.ok` for error checking (not `if (result.error)` — this breaks when the spec has no error responses defined)
|
|
||||||
- Cast errors as `result.error as unknown as { code?: string }` to extract the backend error code
|
|
||||||
- Use `result.data!` (non-null assertion) after an ok check — TypeScript knows it's present
|
|
||||||
|
|
||||||
For multipart/form-data endpoints (file uploads), bypass the typed client and use raw `fetch`:
|
|
||||||
```typescript
|
|
||||||
const res = await fetch(`${baseUrl}/api/documents`, { method: 'POST', body: formData });
|
|
||||||
```
|
|
||||||
|
|
||||||
### Form Actions Pattern
|
### Form Actions Pattern
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// +page.server.ts
|
// +page.server.ts
|
||||||
export const actions = {
|
export const actions = {
|
||||||
default: async ({ request, fetch }) => {
|
default: async ({ request, fetch }) => {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const name = formData.get('name') as string; // cast needed — FormData returns FormDataEntryValue
|
const name = formData.get("name") as string;
|
||||||
// ...
|
// ...
|
||||||
return fail(400, { error: 'message' }); // on error
|
return fail(400, { error: "message" }); // on error
|
||||||
throw redirect(303, '/target'); // on success
|
throw redirect(303, "/target"); // on success
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Date Handling
|
### Date Handling
|
||||||
|
|
||||||
- **Forms**: German format `dd.mm.yyyy` with auto-dot insertion via `handleDateInput()`. A hidden `<input type="hidden" name="documentDate" value={dateIso}>` sends ISO format to the backend.
|
→ See [CONTRIBUTING.md §Date handling](./CONTRIBUTING.md#date-handling)
|
||||||
- **Display**: Always use `Intl.DateTimeFormat` with `T12:00:00` suffix to prevent UTC timezone off-by-one:
|
|
||||||
```typescript
|
**LLM reminder:** always append `T12:00:00` when constructing `new Date()` from an ISO date string — prevents UTC timezone off-by-one errors.
|
||||||
new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'long', year: 'numeric' })
|
|
||||||
.format(new Date(doc.documentDate + 'T12:00:00'))
|
|
||||||
```
|
|
||||||
|
|
||||||
### UI Component Library
|
### UI Component Library
|
||||||
|
|
||||||
Custom components in `src/lib/components/`:
|
→ See per-domain READMEs: [`frontend/src/lib/person/README.md`](./frontend/src/lib/person/README.md), [`frontend/src/lib/tag/README.md`](./frontend/src/lib/tag/README.md), [`frontend/src/lib/document/README.md`](./frontend/src/lib/document/README.md), [`frontend/src/lib/shared/README.md`](./frontend/src/lib/shared/README.md)
|
||||||
|
|
||||||
| Component | Props | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `PersonTypeahead` | `name`, `label`, `value`, `initialName`, `on:change` | Single-person selector with typeahead dropdown |
|
|
||||||
| `PersonMultiSelect` | `selectedPersons` (bind) | Chip-based multi-person selector |
|
|
||||||
| `TagInput` | `tags` (bind), `allowCreation?`, `on:change` | Tag chip input with typeahead |
|
|
||||||
|
|
||||||
### Styling Conventions (Tailwind CSS 4)
|
### Styling Conventions (Tailwind CSS 4)
|
||||||
|
|
||||||
Brand color utilities (defined in `layout.css`):
|
Brand color tokens (defined in `layout.css`):
|
||||||
|
|
||||||
| Class | Value | Usage |
|
| Token / Utility | CSS variable | Usage |
|
||||||
|---|---|---|
|
| ---------------- | ---------------- | ------------------------------------------------------- |
|
||||||
| `brand-navy` | `#002850` | Primary text, buttons, headers |
|
| `brand-navy` | `--palette-navy` | Tailwind utility — buttons, headers, primary text |
|
||||||
| `brand-mint` | `#A6DAD8` | Accents, hover underlines, icons |
|
| `brand-mint` | `--palette-mint` | Tailwind utility — accents, hover underlines, icons |
|
||||||
| `brand-sand` | `#E4E2D7` | Page background, card borders |
|
| `--palette-sand` | `--palette-sand` | Palette constant only — use `bg-canvas` or `bg-surface` |
|
||||||
|
|
||||||
Typography:
|
Typography:
|
||||||
- `font-serif` (Merriweather) — body text, document titles, names
|
|
||||||
|
- `font-serif` (Tinos) — body text, document titles, names
|
||||||
- `font-sans` (Montserrat) — labels, metadata, UI chrome
|
- `font-sans` (Montserrat) — labels, metadata, UI chrome
|
||||||
|
|
||||||
Card pattern for content sections:
|
Card pattern for content sections:
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
|
<div class="rounded-sm border border-line bg-surface shadow-sm p-6">
|
||||||
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Section Title</h2>
|
<h2 class="text-xs font-bold uppercase tracking-widest text-ink-3 mb-5">Section Title</h2>
|
||||||
<!-- content -->
|
<!-- content -->
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
Save bar pattern — use **sticky full-bleed** for long forms (edit document), **card-style with `mt-4`** for short forms (new person):
|
Back button pattern — use the shared `<BackButton>` component from `$lib/shared/primitives/BackButton.svelte`. Do not use a static `<a href>` for back navigation.
|
||||||
```svelte
|
|
||||||
<!-- Long forms: sticky, full-bleed -->
|
|
||||||
<div class="sticky bottom-0 z-10 -mx-4 px-6 py-4 bg-white border-t border-brand-sand shadow-[0_-2px_8px_rgba(0,0,0,0.06)] flex items-center justify-between">
|
|
||||||
|
|
||||||
<!-- Short forms: card, top margin -->
|
|
||||||
<div class="mt-4 flex items-center justify-between rounded-sm border border-brand-sand bg-white px-6 py-4 shadow-sm">
|
|
||||||
```
|
|
||||||
|
|
||||||
Back button pattern — use the shared `<BackButton>` component from `$lib/components/BackButton.svelte`:
|
|
||||||
```svelte
|
|
||||||
<script lang="ts">
|
|
||||||
import BackButton from '$lib/components/BackButton.svelte';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<BackButton />
|
|
||||||
```
|
|
||||||
The component calls `history.back()` so the user returns to wherever they came from. Label is always "Zurück" (no contextual suffix — destination is unknown). Touch target ≥ 44px and focus ring are built in. Do not use a static `<a href>` for back navigation.
|
|
||||||
|
|
||||||
Subtle action link (e.g. "new document/person"):
|
|
||||||
```svelte
|
|
||||||
<a href="/documents/new" class="inline-flex items-center gap-1 text-sm font-medium text-brand-navy/60 hover:text-brand-navy transition-colors">
|
|
||||||
<svg class="w-4 h-4" ...><!-- plus icon --></svg>
|
|
||||||
Neues Dokument
|
|
||||||
</a>
|
|
||||||
```
|
|
||||||
|
|
||||||
### Error Handling (Frontend)
|
### Error Handling (Frontend)
|
||||||
|
|
||||||
`src/lib/errors.ts` mirrors the backend `ErrorCode` enum and maps codes to Paraglide translation keys. When adding a new `ErrorCode` on the backend:
|
→ See [CONTRIBUTING.md §Error handling](./CONTRIBUTING.md#error-handling)
|
||||||
1. Add it to `ErrorCode.java`
|
|
||||||
2. Add it to the `ErrorCode` type in `errors.ts`
|
**LLM reminder:** when adding a new `ErrorCode`: (1) add to `ErrorCode.java`, (2) add to `ErrorCode` type in `frontend/src/lib/shared/errors.ts`, (3) add a `case` in `getErrorMessage()`, (4) add i18n keys in `messages/{de,en,es}.json`.
|
||||||
3. Add a `case` in `getErrorMessage()`
|
|
||||||
4. Add the translation key in `messages/de.json`, `en.json`, `es.json`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Infrastructure
|
## Infrastructure
|
||||||
|
|
||||||
The `docker-compose.yml` at the repo root orchestrates everything. A MinIO MC helper container runs at startup to create the `archive-documents` bucket. The backend container depends on both `db` and `minio` being healthy.
|
→ See [docs/DEPLOYMENT.md](./docs/DEPLOYMENT.md)
|
||||||
|
|
||||||
Database migrations live in `backend/src/main/resources/db/migration/` (Flyway, SQL files named `V{n}__{description}.sql`).
|
|
||||||
|
|
||||||
## API Testing
|
## API Testing
|
||||||
|
|
||||||
@@ -362,4 +281,4 @@ HTTP test files are in `backend/api_tests/` for use with the VS Code REST Client
|
|||||||
|
|
||||||
## Dev Container
|
## Dev Container
|
||||||
|
|
||||||
A `.devcontainer/` config is available (Java 21 + Node 24, ports 8080 and 3000 forwarded). Use VS Code's "Reopen in Container" for a pre-configured environment.
|
→ See [.devcontainer/README.md](./.devcontainer/README.md)
|
||||||
|
|||||||
@@ -180,6 +180,8 @@ When in doubt, commit more often rather than less.
|
|||||||
|
|
||||||
See [CODESTYLE.md](./CODESTYLE.md) for the full guide: Clean Code (Uncle Bob), DRY/KISS trade-offs, and SOLID principles applied to this stack.
|
See [CODESTYLE.md](./CODESTYLE.md) for the full guide: Clean Code (Uncle Bob), DRY/KISS trade-offs, and SOLID principles applied to this stack.
|
||||||
|
|
||||||
|
For domain terminology (Person vs AppUser, DocumentStatus lifecycle, Chronik vs Aktivität, etc.) see [docs/GLOSSARY.md](./docs/GLOSSARY.md).
|
||||||
|
|
||||||
Quick reminders:
|
Quick reminders:
|
||||||
- Pure functions over stateful helpers where possible
|
- Pure functions over stateful helpers where possible
|
||||||
- No premature abstractions — KISS beats DRY
|
- No premature abstractions — KISS beats DRY
|
||||||
|
|||||||
305
CONTRIBUTING.md
Normal file
305
CONTRIBUTING.md
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
# Contributing to Familienarchiv
|
||||||
|
|
||||||
|
For the full collaboration rules (issue workflow, PR process, Red/Green TDD, commit conventions) see [COLLABORATING.md](./COLLABORATING.md).
|
||||||
|
For coding style see [CODESTYLE.md](./CODESTYLE.md).
|
||||||
|
For the system architecture see [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) (introduced in DOC-2; until that PR merges, see [docs/architecture/c4-diagrams.md](./docs/architecture/c4-diagrams.md)).
|
||||||
|
For domain terminology see [docs/GLOSSARY.md](./docs/GLOSSARY.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Environment setup
|
||||||
|
|
||||||
|
**Prerequisites:** Java 21 (SDKMAN), Node 24 (nvm), Docker
|
||||||
|
|
||||||
|
**Activate SDKMAN and nvm before running `java`, `mvn`, `node`, or `npm`:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source "$HOME/.sdkman/bin/sdkman-init.sh"
|
||||||
|
export NVM_DIR="$HOME/.nvm" && [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Daily development workflow
|
||||||
|
|
||||||
|
**Startup order — services must start in this sequence:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Start PostgreSQL and MinIO
|
||||||
|
docker compose up -d db minio
|
||||||
|
|
||||||
|
# 2. Start the backend (separate terminal)
|
||||||
|
cd backend && ./mvnw spring-boot:run
|
||||||
|
|
||||||
|
# 3. Start the frontend (separate terminal)
|
||||||
|
cd frontend && npm install && npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
> `npm install` also wires up the Husky pre-commit hook via the `prepare` script.
|
||||||
|
> Run it before your first commit, or the hook will fail to execute.
|
||||||
|
|
||||||
|
> **Do not use `docker-compose.ci.yml` locally** — it disables the bind mounts that the dev workflow depends on.
|
||||||
|
|
||||||
|
**Regenerate TypeScript types after any backend API change:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend must be running with dev profile
|
||||||
|
cd frontend && npm run generate:api
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ Forgetting this step is the most common cause of "where did my TypeScript type go?" — always regenerate after changing models or endpoints.
|
||||||
|
|
||||||
|
**Test commands:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && ./mvnw test # backend unit + slice tests
|
||||||
|
cd frontend && npm run test # Vitest unit tests
|
||||||
|
cd frontend && npm run check # svelte-check (type errors)
|
||||||
|
cd frontend && npx playwright test # Playwright e2e tests
|
||||||
|
```
|
||||||
|
|
||||||
|
**Branch naming:** `<type>/<issue-number>-<short-description>`, e.g. `feat/398-contributing`
|
||||||
|
|
||||||
|
**Commits:** one logical change per commit; reference the Gitea issue:
|
||||||
|
|
||||||
|
```
|
||||||
|
feat(person): add aliases endpoint
|
||||||
|
|
||||||
|
Closes #42
|
||||||
|
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test-type decision matrix
|
||||||
|
|
||||||
|
| What you're testing | Test type | Tool |
|
||||||
|
|---|---|---|
|
||||||
|
| Service business logic, calculations | Unit test | JUnit + `@ExtendWith(MockitoExtension.class)` |
|
||||||
|
| HTTP contract, request validation, error codes | Controller slice test | `@WebMvcTest` |
|
||||||
|
| Server `load` function | Vitest unit | Import directly, mock `fetch` |
|
||||||
|
| Shared UI component | Vitest browser-mode | `render()` + `getByRole()` |
|
||||||
|
| Full user-facing flow, navigation, forms | E2E | Playwright |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Walkthrough A — Add a new domain
|
||||||
|
|
||||||
|
**Example:** adding a `citation` domain (formal references to documents).
|
||||||
|
|
||||||
|
Both the backend and frontend are organised **domain-first**. A new domain means adding a package on both sides under the same name.
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
1. Create `backend/src/main/java/org/raddatz/familienarchiv/citation/`
|
||||||
|
|
||||||
|
2. Add entity, repository, service, controller, and DTOs flat in the package:
|
||||||
|
- **Entity** `Citation.java` — annotate with `@Entity @Data @Builder @NoArgsConstructor @AllArgsConstructor`; use `@GeneratedValue(strategy = GenerationType.UUID)` for the `id` field; add `@Schema(requiredMode = REQUIRED)` on every field the backend always populates
|
||||||
|
- **Repository** `CitationRepository.java` — extends `JpaRepository<Citation, UUID>`
|
||||||
|
- **Service** `CitationService.java` — `@Service @RequiredArgsConstructor`; write methods `@Transactional`, read methods unannotated; cross-domain data goes through the other domain's service, never its repository
|
||||||
|
- **Controller** `CitationController.java` — `@RestController @RequestMapping("/api/citations")`
|
||||||
|
|
||||||
|
3. Add `@RequirePermission(Permission.WRITE_ALL)` on every `POST`, `PUT`, `PATCH`, and `DELETE` endpoint — **this is not optional**. Read-only `GET` endpoints stay unannotated.
|
||||||
|
|
||||||
|
4. Add a Flyway migration: `backend/src/main/resources/db/migration/V{n}__{description}.sql` (use the next sequential number after the highest existing one).
|
||||||
|
|
||||||
|
5. **Write failing tests before any implementation** (Red step):
|
||||||
|
- Service unit test for business logic (`@ExtendWith(MockitoExtension.class)`)
|
||||||
|
- `@WebMvcTest` slice test for each HTTP endpoint
|
||||||
|
|
||||||
|
6. Rebuild with `--spring.profiles.active=dev` and run `npm run generate:api` in `frontend/`.
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
7. Create `frontend/src/lib/citation/` — domain-specific Svelte components and TypeScript utilities go here.
|
||||||
|
|
||||||
|
8. Add routes under `frontend/src/routes/citations/` as needed.
|
||||||
|
|
||||||
|
9. Add a per-domain `README.md` in both the backend package folder and `frontend/src/lib/citation/` (per DOC-6).
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
10. Update `docs/ARCHITECTURE.md` Section 2 to include the new domain.
|
||||||
|
11. Update `docs/GLOSSARY.md` if new terms are introduced.
|
||||||
|
12. Update the ESLint boundary allow-list in `frontend/eslint.config.js` if the domain needs to import from another domain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Walkthrough B — Add a new endpoint
|
||||||
|
|
||||||
|
**Example:** `POST /api/persons/{id}/aliases` — attach a name alias to an existing person.
|
||||||
|
|
||||||
|
### Red (write failing tests first)
|
||||||
|
|
||||||
|
1. Write a failing `@WebMvcTest` controller slice test:
|
||||||
|
```java
|
||||||
|
@Test
|
||||||
|
void addAlias_returns201_whenAliasCreated() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Write a failing service unit test:
|
||||||
|
```java
|
||||||
|
@Test
|
||||||
|
void addAlias_throwsNotFound_whenPersonDoesNotExist() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Green (implement)
|
||||||
|
|
||||||
|
3. Add the service method in `PersonService.java`:
|
||||||
|
```java
|
||||||
|
@Transactional
|
||||||
|
public PersonNameAlias addAlias(UUID personId, PersonNameAliasDTO dto) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Add the controller method in `PersonController.java`:
|
||||||
|
```java
|
||||||
|
@PostMapping("/{id}/aliases")
|
||||||
|
@RequirePermission(Permission.WRITE_ALL)
|
||||||
|
public ResponseEntity<PersonNameAlias> addAlias(@PathVariable UUID id,
|
||||||
|
@RequestBody PersonNameAliasDTO dto) { ... }
|
||||||
|
```
|
||||||
|
`@RequirePermission(Permission.WRITE_ALL)` on every state-mutating endpoint — **not optional**.
|
||||||
|
|
||||||
|
5. Validate user-supplied inputs at the controller boundary:
|
||||||
|
```java
|
||||||
|
if (dto.name() == null || dto.name().isBlank())
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "name is required");
|
||||||
|
```
|
||||||
|
Validate at system boundaries; trust internal service code.
|
||||||
|
|
||||||
|
6. Use `DomainException` for domain errors:
|
||||||
|
```java
|
||||||
|
DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Person not found: " + id)
|
||||||
|
```
|
||||||
|
If you need a new error code, add it to `ErrorCode.java`, mirror it in
|
||||||
|
`frontend/src/lib/shared/errors.ts`, and add translation keys in `messages/{de,en,es}.json`.
|
||||||
|
|
||||||
|
7. Mark every field the backend always populates with `@Schema(requiredMode = REQUIRED)` — this drives TypeScript type generation.
|
||||||
|
|
||||||
|
### Types and tests
|
||||||
|
|
||||||
|
8. Rebuild with `--spring.profiles.active=dev`, then `npm run generate:api` in `frontend/`.
|
||||||
|
|
||||||
|
> ⚠️ **Always regenerate types after any API change.** This is the #1 cause of "where did my TypeScript type go?"
|
||||||
|
|
||||||
|
9. Run the full test suite — all green before committing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Walkthrough C — Add a new frontend page
|
||||||
|
|
||||||
|
**Example:** `/persons/[id]/timeline` — a chronological event timeline for one person.
|
||||||
|
|
||||||
|
### Red (write failing test first)
|
||||||
|
|
||||||
|
1. Write a failing Playwright E2E test for the user flow:
|
||||||
|
```typescript
|
||||||
|
test('timeline shows events in chronological order', async ({ page }) => {
|
||||||
|
await page.goto('/persons/1/timeline');
|
||||||
|
// assertions...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Green (implement)
|
||||||
|
|
||||||
|
2. Create `frontend/src/routes/persons/[id]/timeline/+page.svelte`
|
||||||
|
|
||||||
|
3. Add `frontend/src/routes/persons/[id]/timeline/+page.server.ts` for the SSR load:
|
||||||
|
```typescript
|
||||||
|
import { createApiClient } from '$lib/shared/api.server';
|
||||||
|
export const load: PageServerLoad = async ({ params, fetch }) => {
|
||||||
|
const api = createApiClient(fetch);
|
||||||
|
const result = await api.GET('/api/persons/{id}', { params: { path: { id: params.id } } });
|
||||||
|
if (!result.response.ok) throw error(result.response.status, '...');
|
||||||
|
return { person: result.data! };
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Domain-specific components (e.g. `TimelineEntry.svelte`) → `frontend/src/lib/person/`
|
||||||
|
|
||||||
|
5. Shared primitives (e.g. a generic date-range display) → `frontend/src/lib/shared/primitives/`
|
||||||
|
|
||||||
|
6. UI patterns to follow:
|
||||||
|
- Back navigation: `import BackButton from '$lib/shared/primitives/BackButton.svelte'`
|
||||||
|
- Date display: always append `T12:00:00` — `new Intl.DateTimeFormat('de-DE', …).format(new Date(val + 'T12:00:00'))` — prevents UTC off-by-one errors
|
||||||
|
- Brand colors: `brand-navy`, `brand-mint`, `brand-sand` (defined in `src/routes/layout.css`)
|
||||||
|
- Accessibility: touch targets ≥ 44 px (`min-h-[44px]`); focus rings (`focus-visible:ring-2 focus-visible:ring-brand-navy`); `aria-label` on icon-only buttons; `aria-live="polite"` on dynamic status messages
|
||||||
|
|
||||||
|
7. Add Paraglide i18n keys in `messages/de.json`, `messages/en.json`, `messages/es.json`.
|
||||||
|
|
||||||
|
8. If adding a new error code: mirror in `frontend/src/lib/shared/errors.ts` and add translation keys.
|
||||||
|
|
||||||
|
9. Make all tests green before committing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Conventions reference
|
||||||
|
|
||||||
|
### Error handling
|
||||||
|
|
||||||
|
| Scenario | Pattern |
|
||||||
|
|---|---|
|
||||||
|
| Domain entity not found | `DomainException.notFound(ErrorCode.X, "…")` |
|
||||||
|
| Permission denied | `DomainException.forbidden("…")` |
|
||||||
|
| Concurrent edit conflict | `DomainException.conflict(ErrorCode.X, "…")` |
|
||||||
|
| Infrastructure failure | `DomainException.internal(ErrorCode.X, "…")` |
|
||||||
|
| Simple controller validation | `throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "…")` |
|
||||||
|
|
||||||
|
New error code: `ErrorCode.java` → `frontend/src/lib/shared/errors.ts` → `messages/{de,en,es}.json`.
|
||||||
|
|
||||||
|
### DTOs
|
||||||
|
|
||||||
|
- Input DTOs live flat in the domain package (e.g. `PersonUpdateDTO.java`)
|
||||||
|
- Responses are the entity itself — no separate response DTOs
|
||||||
|
- `@Schema(requiredMode = REQUIRED)` on every field the backend always populates
|
||||||
|
|
||||||
|
### Frontend API client
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const api = createApiClient(fetch); // from $lib/shared/api.server
|
||||||
|
const result = await api.GET('/api/persons/{id}', { params: { path: { id } } });
|
||||||
|
if (!result.response.ok) {
|
||||||
|
const code = (result.error as unknown as { code?: string })?.code;
|
||||||
|
throw error(result.response.status, getErrorMessage(code));
|
||||||
|
}
|
||||||
|
return { person: result.data! }; // non-null assertion is safe after the ok check
|
||||||
|
```
|
||||||
|
|
||||||
|
For multipart/form-data (file uploads): bypass the typed client and use raw `fetch` — the client cannot handle it.
|
||||||
|
|
||||||
|
### Date handling
|
||||||
|
|
||||||
|
| Context | Pattern |
|
||||||
|
|---|---|
|
||||||
|
| Form display | German `dd.mm.yyyy` with auto-dot insertion via `handleDateInput()` |
|
||||||
|
| Wire format | ISO 8601 via a hidden `<input type="hidden" name="documentDate" value={dateIso}>` |
|
||||||
|
| Display | `new Intl.DateTimeFormat('de-DE', …).format(new Date(val + 'T12:00:00'))` |
|
||||||
|
|
||||||
|
### Security checklist (new endpoint)
|
||||||
|
|
||||||
|
- `@RequirePermission(Permission.WRITE_ALL)` on every `POST`, `PUT`, `PATCH`, `DELETE` — required, not optional
|
||||||
|
- Validate all user-supplied inputs at the controller boundary before passing to the service
|
||||||
|
- Parameterised queries only — never interpolate user input into JPQL/SQL strings
|
||||||
|
- No raw user input in log messages — use `{}` placeholders: `log.warn("Not found: {}", id)`
|
||||||
|
- Validate content-type and size on upload endpoints before reading the stream
|
||||||
|
|
||||||
|
### Accessibility baseline (new frontend page)
|
||||||
|
|
||||||
|
- Touch targets ≥ 44 px on all interactive elements (`min-h-[44px]`)
|
||||||
|
- Focus rings on all focusable elements (`focus-visible:ring-2 focus-visible:ring-brand-navy`)
|
||||||
|
- `aria-label` on every icon-only button
|
||||||
|
- `aria-live="polite"` on dynamic status messages
|
||||||
|
- Color is never the sole status indicator
|
||||||
|
|
||||||
|
Full WCAG 2.1 AA reference: [docs/STYLEGUIDE.md](./docs/STYLEGUIDE.md).
|
||||||
|
|
||||||
|
### Lint and format
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Frontend
|
||||||
|
cd frontend && npm run lint # Prettier + ESLint check
|
||||||
|
cd frontend && npm run format # Auto-fix formatting
|
||||||
|
cd frontend && npm run check # svelte-check (type errors)
|
||||||
|
|
||||||
|
# Backend — no standalone lint tool; compilation and test runs catch style issues
|
||||||
|
cd backend && ./mvnw test # compile + test
|
||||||
|
cd backend && ./mvnw clean package -DskipTests # compile-only check
|
||||||
|
```
|
||||||
93
README.md
Normal file
93
README.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Familienarchiv
|
||||||
|
|
||||||
|
Familienarchiv is a private web application for digitising, organising, and searching a family document collection — letters, postcards, and photographs from 1899 to 1950. Family members upload scans, transcribe handwritten text (Kurrent/Sütterlin), and read the archive from any device.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Subsystems
|
||||||
|
|
||||||
|
- `frontend/` — SvelteKit 2 / Svelte 5 / TypeScript / Tailwind 4 web app (server-side rendered)
|
||||||
|
- `backend/` — Spring Boot 4 (Java 21) REST API; handles documents, persons, search, and user management
|
||||||
|
- `ocr-service/` — Python FastAPI microservice for OCR and handwritten text recognition (HTR); single-node by design — see [ADR-001](docs/adr/001-ocr-python-microservice.md). Not part of the default dev stack (see Quick start below)
|
||||||
|
- `infra/` — Gitea Actions CI/CD config; future home for infrastructure-as-code
|
||||||
|
- `scripts/` — operational and data-pipeline helpers (`reset-db.sh`, `clean-e2e-data.sh`, import scripts)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
**Prerequisites:** Java 21, Node 24, Docker with the `docker compose` plugin (V2).
|
||||||
|
|
||||||
|
### 1. Configure environment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
# The defaults in .env.example work for local development without changes.
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Start infrastructure
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Starts PostgreSQL, MinIO (object storage), and Mailpit (dev mail catcher)
|
||||||
|
docker compose up -d db minio mailpit
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Start the backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
./mvnw spring-boot:run
|
||||||
|
# Starts on http://localhost:8080
|
||||||
|
# API docs (dev profile, auto-enabled): http://localhost:8080/v3/api-docs
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Start the frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
# Starts on http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
Open **http://localhost:5173** — you should see the Familienarchiv login screen.
|
||||||
|
|
||||||
|
Default development credentials:
|
||||||
|
|
||||||
|
```
|
||||||
|
# local dev only — change before any network-exposed deployment
|
||||||
|
Email: admin@familyarchive.local
|
||||||
|
Password: admin123
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Development setup only.** The default `docker compose` config exposes the database port and uses root MinIO credentials. Do not connect this to a network without first reading `docs/DEPLOYMENT.md` _(coming: [DOC-5, #399](http://heim-nas:3005/marcel/familienarchiv/issues/399))_.
|
||||||
|
|
||||||
|
### Running the full stack via Docker (optional)
|
||||||
|
|
||||||
|
To run everything including the backend and frontend in containers:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: the OCR service (`ocr-service/`) builds its Docker image locally and downloads ~6 GB of ML models on first start. Expect 30–60 minutes on a first run. The rest of the stack starts independently; OCR can be excluded with `--scale ocr-service=0` on memory-constrained machines (requires ≥ 12 GB RAM).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Where to go next
|
||||||
|
|
||||||
|
| Resource | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| [docs/architecture/c4-diagrams.md](docs/architecture/c4-diagrams.md) | C4 container and component diagrams (current system view) |
|
||||||
|
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) _(coming: [DOC-2, #396](http://heim-nas:3005/marcel/familienarchiv/issues/396))_ | Full architecture guide with domain list |
|
||||||
|
| [docs/GLOSSARY.md](docs/GLOSSARY.md) | Overloaded terms: Person vs AppUser, Chronik vs Aktivität, etc. |
|
||||||
|
| [CONTRIBUTING.md](CONTRIBUTING.md) _(coming: [DOC-4, #398](http://heim-nas:3005/marcel/familienarchiv/issues/398))_ | How to add a domain, endpoint, or SvelteKit route |
|
||||||
|
| [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) _(coming: [DOC-5, #399](http://heim-nas:3005/marcel/familienarchiv/issues/399))_ | Production deployment checklist and secrets guide |
|
||||||
|
| [docs/adr/](docs/adr/) | Architecture Decision Records — the "why" behind key choices |
|
||||||
|
| [Gitea issue tracker](http://heim-nas:3005/marcel/familienarchiv/issues) _(internal — home network only)_ | Bug reports, feature requests, and project planning |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Private project — all rights reserved. Not licensed for redistribution.
|
||||||
@@ -11,7 +11,7 @@ Spring Boot 4.0 monolith serving the Familienarchiv REST API. Handles document m
|
|||||||
- **Server**: Jetty (not Tomcat — excluded in pom.xml)
|
- **Server**: Jetty (not Tomcat — excluded in pom.xml)
|
||||||
- **Data**: PostgreSQL 16, JPA/Hibernate, Spring Data JPA
|
- **Data**: PostgreSQL 16, JPA/Hibernate, Spring Data JPA
|
||||||
- **Migrations**: Flyway (SQL files in `src/main/resources/db/migration/`)
|
- **Migrations**: Flyway (SQL files in `src/main/resources/db/migration/`)
|
||||||
- **Security**: Spring Security, Spring Session JDBC, JWT tokens
|
- **Security**: Spring Security, Spring Session JDBC
|
||||||
- **File Storage**: MinIO via AWS SDK v2 (S3-compatible)
|
- **File Storage**: MinIO via AWS SDK v2 (S3-compatible)
|
||||||
- **Spreadsheet Import**: Apache POI 5.5.0 (Excel/ODS)
|
- **Spreadsheet Import**: Apache POI 5.5.0 (Excel/ODS)
|
||||||
- **API Docs**: SpringDoc OpenAPI 3.x (`/v3/api-docs` — dev profile only)
|
- **API Docs**: SpringDoc OpenAPI 3.x (`/v3/api-docs` — dev profile only)
|
||||||
@@ -19,7 +19,7 @@ Spring Boot 4.0 monolith serving the Familienarchiv REST API. Handles document m
|
|||||||
|
|
||||||
## Package Structure
|
## Package Structure
|
||||||
|
|
||||||
Package-by-domain: each domain owns its controller, service, repository, entities, and DTOs.
|
<!-- TODO: rewrite post-REFACTOR-1 — see Epic 4 -->
|
||||||
|
|
||||||
```
|
```
|
||||||
src/main/java/org/raddatz/familienarchiv/
|
src/main/java/org/raddatz/familienarchiv/
|
||||||
@@ -43,31 +43,28 @@ src/main/java/org/raddatz/familienarchiv/
|
|||||||
└── user/ # User domain — AppUser, UserGroup, UserService, auth controllers
|
└── user/ # User domain — AppUser, UserGroup, UserService, auth controllers
|
||||||
```
|
```
|
||||||
|
|
||||||
## Layering Rules (Strict)
|
For per-domain ownership and public surface, see each domain's `README.md`.
|
||||||
|
|
||||||
```
|
## Layering Rules
|
||||||
Controller → Service → Repository → DB
|
|
||||||
```
|
|
||||||
|
|
||||||
- **Controllers never call repositories directly.**
|
→ See [docs/ARCHITECTURE.md §Layering rule](../docs/ARCHITECTURE.md#layering-rule)
|
||||||
- **Services never reach into another domain's repository.** Call the other domain's service instead.
|
|
||||||
- ✅ `DocumentService` → `PersonService.getById()` → `PersonRepository`
|
**LLM reminder:** controllers never call repositories directly; services never reach into another domain's repository — always call the other domain's service.
|
||||||
- ❌ `DocumentService` → `PersonRepository` directly
|
|
||||||
|
|
||||||
## Key Entities
|
## Key Entities
|
||||||
|
|
||||||
| Entity | Table | Key Relationships |
|
| Entity | Table | Key Relationships |
|
||||||
|---|---|---|
|
| --------------------------- | ------------------------------- | ------------------------------------------------------------------------------- |
|
||||||
| `Document` | `documents` | ManyToOne sender (Person), ManyToMany receivers (Person), ManyToMany tags (Tag) |
|
| `Document` | `documents` | ManyToOne sender (Person), ManyToMany receivers (Person), ManyToMany tags (Tag) |
|
||||||
| `Person` | `persons` | Referenced by documents as sender/receiver; name aliases table |
|
| `Person` | `persons` | Referenced by documents as sender/receiver; name aliases table |
|
||||||
| `Tag` | `tag` | ManyToMany with documents via `document_tags`; self-referencing parent for tree |
|
| `Tag` | `tag` | ManyToMany with documents via `document_tags`; self-referencing parent for tree |
|
||||||
| `AppUser` | `app_users` | ManyToMany groups (UserGroup) |
|
| `AppUser` | `app_users` | ManyToMany groups (UserGroup) |
|
||||||
| `UserGroup` | `user_groups` | Has a `Set<String> permissions` |
|
| `UserGroup` | `user_groups` | Has a `Set<String> permissions` |
|
||||||
| `TranscriptionBlock` | `transcription_blocks` | Per-document, per-page text blocks with polygons |
|
| `TranscriptionBlock` | `transcription_blocks` | Per-document, per-page text blocks with polygons |
|
||||||
| `DocumentAnnotation` | `document_annotations` | Free-form annotations on document pages |
|
| `DocumentAnnotation` | `document_annotations` | Free-form annotations on document pages |
|
||||||
| `Comment` | `document_comments` | Threaded comments with mentions |
|
| `Comment` | `document_comments` | Threaded comments with mentions |
|
||||||
| `Notification` | `notifications` | User notification feed |
|
| `Notification` | `notifications` | User notification feed |
|
||||||
| `OcrJob` / `OcrJobDocument` | `ocr_jobs`, `ocr_job_documents` | Batch OCR job tracking |
|
| `OcrJob` / `OcrJobDocument` | `ocr_jobs`, `ocr_job_documents` | Batch OCR job tracking |
|
||||||
|
|
||||||
**`DocumentStatus` lifecycle:** `PLACEHOLDER → UPLOADED → TRANSCRIBED → REVIEWED → ARCHIVED`
|
**`DocumentStatus` lifecycle:** `PLACEHOLDER → UPLOADED → TRANSCRIBED → REVIEWED → ARCHIVED`
|
||||||
|
|
||||||
@@ -104,32 +101,15 @@ public class MyEntity {
|
|||||||
|
|
||||||
## Error Handling
|
## Error Handling
|
||||||
|
|
||||||
Use `DomainException` for all domain errors:
|
→ See [CONTRIBUTING.md §Error handling](../CONTRIBUTING.md#error-handling)
|
||||||
|
|
||||||
```java
|
**LLM reminder:** use `DomainException.notFound/forbidden/conflict/internal()` — never throw raw exceptions from service methods. For simple controller validation (not domain logic), `ResponseStatusException` is acceptable: `throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "…")`. When adding a new `ErrorCode`: add to `ErrorCode.java`, mirror in `frontend/src/lib/shared/errors.ts`, add i18n keys in `messages/{de,en,es}.json`.
|
||||||
DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "...")
|
|
||||||
DomainException.forbidden("...")
|
|
||||||
DomainException.conflict(ErrorCode.IMPORT_ALREADY_RUNNING, "...")
|
|
||||||
DomainException.internal(ErrorCode.FILE_UPLOAD_FAILED, "...")
|
|
||||||
```
|
|
||||||
|
|
||||||
When adding a new `ErrorCode`:
|
|
||||||
1. Add to `ErrorCode.java`
|
|
||||||
2. Mirror in frontend `src/lib/errors.ts`
|
|
||||||
3. Add Paraglide translation key in `messages/{de,en,es}.json`
|
|
||||||
|
|
||||||
## Security / Permissions
|
## Security / Permissions
|
||||||
|
|
||||||
Use `@RequirePermission` on controller methods or classes:
|
→ See [docs/ARCHITECTURE.md §Permission system](../docs/ARCHITECTURE.md#permission-system)
|
||||||
|
|
||||||
```java
|
**LLM reminder:** `@RequirePermission(Permission.WRITE_ALL)` is **required** on every `POST`, `PUT`, `PATCH`, `DELETE` endpoint — not optional. Do not mix with Spring Security's `@PreAuthorize`. Available permissions: `READ_ALL`, `WRITE_ALL`, `ADMIN`, `ADMIN_USER`, `ADMIN_TAG`, `ADMIN_PERMISSION`, `ANNOTATE_ALL`, `BLOG_WRITE`.
|
||||||
@RequirePermission(Permission.WRITE_ALL)
|
|
||||||
public Document updateDocument(...) { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
Available permissions: `READ_ALL`, `WRITE_ALL`, `ADMIN`, `ADMIN_USER`, `ADMIN_TAG`, `ADMIN_PERMISSION`
|
|
||||||
|
|
||||||
`PermissionAspect` checks the current user's `UserGroup.permissions` at runtime.
|
|
||||||
|
|
||||||
## OCR Integration
|
## OCR Integration
|
||||||
|
|
||||||
@@ -141,49 +121,35 @@ The backend orchestrates OCR by calling the Python `ocr-service` microservice vi
|
|||||||
- `OcrBatchService` — handles batch/job workflows
|
- `OcrBatchService` — handles batch/job workflows
|
||||||
- `OcrAsyncRunner` — async execution of OCR jobs
|
- `OcrAsyncRunner` — async execution of OCR jobs
|
||||||
|
|
||||||
|
For ocr-service internals, see [`ocr-service/README.md`](../ocr-service/README.md).
|
||||||
|
|
||||||
## API Testing
|
## API Testing
|
||||||
|
|
||||||
HTTP test files in `backend/api_tests/` for the VS Code REST Client extension.
|
HTTP test files in `backend/api_tests/` for the VS Code REST Client extension.
|
||||||
|
|
||||||
## How to Run
|
## How to Run
|
||||||
|
|
||||||
### Local Development
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
|
|
||||||
# Run with dev profile (requires PostgreSQL + MinIO running via docker-compose)
|
./mvnw spring-boot:run # Run with dev profile (requires PostgreSQL + MinIO)
|
||||||
./mvnw spring-boot:run
|
./mvnw clean package # Build JAR (with tests)
|
||||||
|
|
||||||
# Build JAR (with tests)
|
|
||||||
./mvnw clean package
|
|
||||||
|
|
||||||
# Build JAR skipping tests
|
|
||||||
./mvnw clean package -DskipTests
|
./mvnw clean package -DskipTests
|
||||||
|
./mvnw test # Run all tests
|
||||||
# Run all tests
|
./mvnw test -Dtest=ClassName # Run a single test class
|
||||||
./mvnw test
|
./mvnw clean verify # Run with JaCoCo coverage report
|
||||||
|
|
||||||
# Run a single test class
|
|
||||||
./mvnw test -Dtest=ClassName
|
|
||||||
|
|
||||||
# Run with coverage (JaCoCo)
|
|
||||||
./mvnw clean verify
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### OpenAPI TypeScript Generation
|
**OpenAPI / TypeScript type generation:**
|
||||||
|
|
||||||
1. Build and start backend with `--spring.profiles.active=dev`
|
1. Start backend with `--spring.profiles.active=dev`
|
||||||
2. In `frontend/`, run: `npm run generate:api`
|
2. In `frontend/`: `npm run generate:api`
|
||||||
|
|
||||||
### Profiles
|
**LLM reminder:** always regenerate types after any model or endpoint change — the most common cause of "where did my TypeScript type go?"
|
||||||
|
|
||||||
- **dev** (default): Enables OpenAPI, dev configs, e2e seeds
|
|
||||||
- **prod**: Production profile — no dev endpoints
|
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
- Unit tests: Mockito + JUnit, pure in-memory
|
- Unit tests: Mockito + JUnit, pure in-memory
|
||||||
- Slice tests: `@WebMvcTest`, `@DataJpaTest` with Testcontainers PostgreSQL
|
- Slice tests: `@WebMvcTest`, `@DataJpaTest` with Testcontainers PostgreSQL
|
||||||
- Integration tests: Full Spring context with Testcontainers
|
- Integration tests: Full Spring context with Testcontainers
|
||||||
- Coverage gate: 88% branch coverage overall (JaCoCo)
|
- Coverage gate: 88% branch coverage (JaCoCo)
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# audit
|
||||||
|
|
||||||
|
Append-only event store for all domain mutations. Every write across the application produces an `audit_log` row. The activity feed and Family Pulse dashboard aggregate from this table.
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Table: `audit_log` (append-only by convention — no UPDATE or DELETE in application code).
|
||||||
|
Features: log mutations, query activity feed, query per-entity history.
|
||||||
|
|
||||||
|
**Admission criteria (why this is cross-cutting, not a Tier-1 domain):** consumed by 5+ domains; has no user-facing CRUD of its own; the data model is fixed (event log, not a business entity).
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
Nothing beyond the log table. `audit/` is an infrastructure layer, not a business domain.
|
||||||
|
|
||||||
|
## Public surface (called from other domains)
|
||||||
|
|
||||||
|
| Method | Consumer | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `logAfterCommit(event)` | document, person, user, ocr, geschichte | Record a mutation event after the DB transaction commits |
|
||||||
|
|
||||||
|
`logAfterCommit` is the only write-path. Query paths (`AuditLogQueryService`) are consumed by `dashboard/` and the activity feed route.
|
||||||
|
|
||||||
|
## Internal layout
|
||||||
|
|
||||||
|
- `AuditService` — `logAfterCommit()` (write)
|
||||||
|
- `AuditLogQueryService` — query by entity, by user, for the activity feed
|
||||||
|
- `AuditLog` (entity) → table `audit_log`
|
||||||
|
- `AuditLogRepository`
|
||||||
|
|
||||||
|
## Cross-domain dependencies
|
||||||
|
|
||||||
|
None. `audit/` is consumed by other domains; it does not call out to any of them.
|
||||||
|
|
||||||
|
## Frontend counterpart
|
||||||
|
|
||||||
|
No direct frontend counterpart. Audit data surfaces in the `activity/` and `conversation/` frontend domains via the dashboard API.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# dashboard
|
||||||
|
|
||||||
|
Stats aggregation for the admin dashboard and the Family Pulse widget. This is a derived domain — it has no tables of its own; all data is computed on-the-fly from Tier-1 domain data.
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
No entities. Routes: `/api/dashboard/*`, `/api/stats/*`.
|
||||||
|
Features: document counts, person counts, publication stats, weekly activity data, incomplete-document list, enrichment queue, Family Pulse widget data, admin statistics.
|
||||||
|
|
||||||
|
**Admission criteria (cross-cutting):** aggregates from 3+ domains; no owned entities.
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
None of the underlying data — it reads from `document/`, `person/`, `audit/`, `notification/`, `geschichte/`.
|
||||||
|
|
||||||
|
## Public surface
|
||||||
|
|
||||||
|
`dashboard/` is a leaf domain — no other domain calls its services. It is the aggregator, not the aggregated.
|
||||||
|
|
||||||
|
## Internal layout
|
||||||
|
|
||||||
|
- `StatsController` — REST under `/api/stats`
|
||||||
|
- `DashboardController` — REST under `/api/dashboard`
|
||||||
|
- `StatsService` — aggregated counts (documents, persons, geschichten, incomplete, etc.)
|
||||||
|
- `DashboardService` — activity feed composition, Family Pulse data
|
||||||
|
|
||||||
|
## Cross-domain dependencies
|
||||||
|
|
||||||
|
- `DocumentService.count()` — total document count (StatsService)
|
||||||
|
- `DocumentService.getDocumentById(UUID)` / `getDocumentsByIds(List<UUID>)` — document enrichment for activity feed (DashboardService)
|
||||||
|
- `PersonService.count()` — total person count (StatsService)
|
||||||
|
- `TranscriptionService.listBlocks(UUID)` — transcription block lookup for resume widget (DashboardService)
|
||||||
|
- `UserService.getById(UUID)` — actor name resolution in activity feed (DashboardService)
|
||||||
|
- `CommentService.findAnnotationIdsByIds(...)` — annotation context lookup for activity feed (DashboardService)
|
||||||
|
- `AuditLogQueryService.findMostRecentDocumentForUser()` / `getPulseStats()` / `findActivityFeed()` — audit-sourced feed rows (DashboardService)
|
||||||
|
|
||||||
|
## Frontend counterpart
|
||||||
|
|
||||||
|
Activity feed and Pulse widget are assembled in `frontend/src/lib/shared/dashboard/` and in the `aktivitaeten` route; no dedicated `dashboard/` lib folder.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# document
|
||||||
|
|
||||||
|
The archive's core concept. A `Document` represents one physical artefact (a letter, a postcard, a photo) stored in MinIO and described by metadata.
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Entities: `Document`, `DocumentVersion`, `TranscriptionBlock`, `DocumentAnnotation`, `DocumentComment`.
|
||||||
|
Features: document CRUD, file upload/download, full-text search, bulk editing, transcription workflows, annotation canvas, threaded comments, thumbnail generation (PDFBox).
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
- `Person` (sender / receivers) — referenced by ID, resolved via `PersonService`
|
||||||
|
- `Tag` — referenced by ID; the join is on the document side but tags are owned by `tag/`
|
||||||
|
- `AppUser` — comments reference `AppUser` IDs, but user management lives in `user/`
|
||||||
|
- OCR processing — `ocr/` orchestrates jobs; `ocr-service/` executes them
|
||||||
|
|
||||||
|
## Public surface (called from other domains)
|
||||||
|
|
||||||
|
| Method | Consumer | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `getDocumentById(UUID)` | ocr, notification | Fetch a single document |
|
||||||
|
| `getDocumentsByIds(List<UUID>)` | ocr | Bulk fetch for OCR job |
|
||||||
|
| `findByOriginalFilename(String)` | importing | Deduplication during mass import |
|
||||||
|
| `deleteTagCascading(UUID tagId)` | tag | Remove a tag from all documents before deleting it |
|
||||||
|
| `findWeeklyStats()` | dashboard | Activity data for Family Pulse widget |
|
||||||
|
| `count()` | dashboard | Total document count for stats |
|
||||||
|
| `addTrainingLabel(...)` | ocr | Attach a confirmed sender label to a document |
|
||||||
|
| `findSegmentationQueue(int limit)` / `findTranscriptionQueue(int limit)` / `findReadyToReadQueue(int limit)` | ocr | OCR pipeline queues |
|
||||||
|
|
||||||
|
## Internal layout
|
||||||
|
|
||||||
|
- `DocumentController` — REST under `/api/documents`
|
||||||
|
- `DocumentService` — CRUD, search (JPA Specifications), bulk edit
|
||||||
|
- `DocumentRepository` — includes bidirectional conversation-thread query
|
||||||
|
- `DocumentSpecifications` — composable `Specification` predicates for search
|
||||||
|
- `DocumentVersionService` / `DocumentVersionRepository` — append-only version history
|
||||||
|
- `ThumbnailService` + `ThumbnailAsyncRunner` — PDFBox thumbnail generation (separate thread pool)
|
||||||
|
- Sub-packages: `annotation/`, `comment/`, `transcription/`
|
||||||
|
|
||||||
|
## Cross-domain dependencies
|
||||||
|
|
||||||
|
- `PersonService.getById()` / `getAllById()` — resolve sender and receivers
|
||||||
|
- `TagService.expandTagNamesToDescendantIdSets()` — tag filter expansion
|
||||||
|
- `FileService.uploadFile()` / `downloadFile()` / `generatePresignedUrl()` — S3 I/O
|
||||||
|
- `NotificationService.notifyMentions()` / `.notifyReply()` — comment mentions
|
||||||
|
- `AuditService.logAfterCommit()` — every mutation is audited
|
||||||
|
|
||||||
|
## Frontend counterpart
|
||||||
|
|
||||||
|
`frontend/src/lib/document/README.md`
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# geschichte
|
||||||
|
|
||||||
|
Family stories — curated narrative pieces that weave together persons, documents, and commentary into a publishable article. German: *Geschichte* (story / history).
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Entity: `Geschichte`.
|
||||||
|
Lifecycle: `DRAFT → PUBLISHED` (only published stories are visible to non-authors).
|
||||||
|
Features: story CRUD, rich-text editing with person and document cross-references, publish/unpublish toggle, comment thread (shared component from `shared/discussion/`).
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
- `Person` or `Document` records — stories reference them by ID. Deleting a Person or Document does not cascade to Geschichte.
|
||||||
|
- Comment storage — shared comment infrastructure is in `document/comment/` (or `shared/discussion/` on the frontend).
|
||||||
|
|
||||||
|
## Public surface (called from other domains)
|
||||||
|
|
||||||
|
| Method | Consumer | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `getById(UUID)` | notification | Resolve story context in mention notifications |
|
||||||
|
| `list(...)` | dashboard | Recent stories for the activity feed |
|
||||||
|
| `count()` | dashboard | Published story count for stats |
|
||||||
|
|
||||||
|
## Internal layout
|
||||||
|
|
||||||
|
- `GeschichteController` — REST under `/api/geschichten`
|
||||||
|
- `GeschichteService` — CRUD, publish lifecycle
|
||||||
|
- `GeschichteRepository` — list by status, author
|
||||||
|
|
||||||
|
## Cross-domain dependencies
|
||||||
|
|
||||||
|
- `PersonService.getById()` / `getAllById()` — resolve person references in story body
|
||||||
|
- `DocumentService.getDocumentsByIds()` — resolve document references in story body
|
||||||
|
- `AuditService.logAfterCommit()` — story mutations are audited
|
||||||
|
|
||||||
|
## Frontend counterpart
|
||||||
|
|
||||||
|
`frontend/src/lib/geschichte/README.md`
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# notification
|
||||||
|
|
||||||
|
In-app messages delivered in real time via SSE and persisted in the bell-icon dropdown. Notifications are created by other domains in response to events (comment mentions, replies).
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Entity: `Notification`.
|
||||||
|
Features: create and deliver notifications, unread count, mark-read, SSE real-time push, per-user delivery preferences (stored as fields on `AppUser`, managed by `user/`).
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
- `AppUser` (recipient) — owned by `user/`
|
||||||
|
- `Document` or `Geschichte` (notification context) — referenced by ID only
|
||||||
|
|
||||||
|
## Public surface (called from other domains)
|
||||||
|
|
||||||
|
| Method | Consumer | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `notifyMentions(mentionedUserIds, comment)` | document (comment) | Push mention notifications when a comment contains @mentions |
|
||||||
|
| `notifyReply(reply, participantIds)` | document (comment) | Push reply notification to all thread participants |
|
||||||
|
| `countUnread(userId)` | user session | Unread badge count in the nav bar |
|
||||||
|
| `getNotifications(userId)` | dashboard / activity | Notification list for bell dropdown |
|
||||||
|
| `markRead(id)` / `markAllRead(userId)` | notification controller | User-driven read-state updates |
|
||||||
|
| `updatePreferences(userId, dto)` | notification controller | Per-user delivery preferences |
|
||||||
|
|
||||||
|
## Internal layout
|
||||||
|
|
||||||
|
- `NotificationController` — REST under `/api/notifications`
|
||||||
|
- `NotificationService` — create, query, mark-read
|
||||||
|
- `SseEmitterRegistry` — runtime-stateful component that keeps one `SseEmitter` per connected user. On `notifyMentions()` / `notifyReply()`, the service writes to `SseEmitterRegistry` to push real-time events. SSE connections go **backend → browser directly**, not via the SvelteKit SSR layer.
|
||||||
|
- `NotificationRepository` — persisted notification rows
|
||||||
|
- `NotificationPreferenceDTO` — read/write DTO for preference endpoints (prefs stored on `AppUser`)
|
||||||
|
|
||||||
|
## Cross-domain dependencies
|
||||||
|
|
||||||
|
**Outbound (this domain calls):**
|
||||||
|
- `DocumentService.findTitlesByIds(List<UUID>)` — enriches notification DTOs with document titles for display in the bell dropdown
|
||||||
|
|
||||||
|
## Frontend counterpart
|
||||||
|
|
||||||
|
`frontend/src/lib/notification/README.md`
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# ocr
|
||||||
|
|
||||||
|
OCR/HTR pipeline orchestration. This domain manages job lifecycle and result ingestion — it does **not** perform OCR. Actual text recognition runs in the Python `ocr-service/` container (port 8000, internal network only).
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Entities: `OcrJob`, `OcrJobDocument`, `SenderModel`.
|
||||||
|
Features: start OCR jobs, track job lifecycle (`PENDING → RUNNING → DONE / FAILED`), stream transcription blocks back into `document/transcription/`, sender-model training, segmentation training.
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
- Document content — `Document` and `TranscriptionBlock` are owned by `document/`
|
||||||
|
- File storage — presigned MinIO URLs are generated by `filestorage/FileService` and passed to the OCR service
|
||||||
|
- OCR processing — the Python `ocr-service/` executes Surya (typewritten) and Kraken (Kurrent/Sütterlin HTR) and streams results back
|
||||||
|
|
||||||
|
## Public surface (called from other domains)
|
||||||
|
|
||||||
|
| Method | Consumer | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `startOcr(documentId, ...)` | document | Trigger an OCR job for a document |
|
||||||
|
| `getJob(UUID)` | document | Fetch job status |
|
||||||
|
| `getDocumentOcrStatus(UUID)` | document | Per-document OCR status summary |
|
||||||
|
|
||||||
|
## Internal layout
|
||||||
|
|
||||||
|
- `OcrController` — REST under `/api/ocr`
|
||||||
|
- `OcrService` — job creation, presigned URL generation, result ingestion
|
||||||
|
- `OcrBatchService` — batch job workflows
|
||||||
|
- `OcrAsyncRunner` — `@Async` execution of OCR jobs
|
||||||
|
- `OcrTrainingService` — calls `/train` and `/segtrain` on the Python service (protected by `X-Training-Token` header)
|
||||||
|
- `OcrJobRepository` / `OcrJobDocumentRepository`
|
||||||
|
- `SenderModelRepository` — trained sender-recognition models
|
||||||
|
- `OcrClient` (interface) / `RestClientOcrClient` — HTTP client for the Python OCR service; mockable for tests
|
||||||
|
|
||||||
|
## Cross-domain dependencies
|
||||||
|
|
||||||
|
- `DocumentService.getDocumentById()` / `getDocumentsByIds()` — resolve target documents
|
||||||
|
- `DocumentService.addTrainingLabel()` — attach confirmed sender labels after training
|
||||||
|
- `FileService.generatePresignedUrl()` — generate MinIO presigned URLs passed to the OCR service (PDF bytes never flow through the backend)
|
||||||
|
- `AuditService.logAfterCommit()` — OCR job events are audited
|
||||||
|
|
||||||
|
## Frontend counterpart
|
||||||
|
|
||||||
|
`frontend/src/lib/ocr/README.md`
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# person
|
||||||
|
|
||||||
|
Historical individuals referenced by documents. A `Person` is a family member who appears as a sender or receiver in the archive — they are never login accounts.
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Entities: `Person`, `PersonNameAlias`, `PersonRelationship`.
|
||||||
|
Features: person CRUD, name alias management, person merge (deduplication), family-member designation, relationship graph, person type classification (FAMILY, CORRESPONDENT, INSTITUTION).
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
- `AppUser` — login accounts are in `user/`. A `Person` record has no login credentials. The separation is deliberate: a historical family member from 1905 is never a system user.
|
||||||
|
- Document content — `Person` records are referenced by documents (as sender/receiver), not the other way around.
|
||||||
|
- Relationship rendering — the Stammbaum view is derived by the frontend from `PersonRelationship` data.
|
||||||
|
|
||||||
|
## Public surface (called from other domains)
|
||||||
|
|
||||||
|
| Method | Consumer | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `getById(UUID)` | document, geschichte, ocr | Fetch one person by ID |
|
||||||
|
| `getAllById(List<UUID>)` | document | Bulk fetch for sender/receiver resolution |
|
||||||
|
| `findAll(String q)` | document, dashboard | List all persons |
|
||||||
|
| `findByName(String firstName, String lastName)` | document | Typeahead search |
|
||||||
|
| `findOrCreateByAlias(String rawName)` | importing | Idempotent create during mass import; type classification happens internally |
|
||||||
|
| `findAllFamilyMembers()` | dashboard | Family member list for stats |
|
||||||
|
| `findCorrespondents()` | document | Correspondent list for conversation filter |
|
||||||
|
| `count()` | dashboard | Total person count for stats |
|
||||||
|
|
||||||
|
## Internal layout
|
||||||
|
|
||||||
|
- `PersonController` — REST under `/api/persons`
|
||||||
|
- `PersonService` — CRUD, merge, alias management, family-member designation
|
||||||
|
- `PersonRepository` — sorted list, name search
|
||||||
|
- `PersonNameAlias` / `PersonNameAliasRepository` — alternative name spellings
|
||||||
|
- `PersonNameParser` / `PersonTypeClassifier` — name parsing utilities
|
||||||
|
- `PersonSummaryDTO` — lightweight DTO for typeahead / list views
|
||||||
|
- Sub-package: `relationship/` — `PersonRelationship`, `RelationshipService`, `RelationshipController`
|
||||||
|
|
||||||
|
## Cross-domain dependencies
|
||||||
|
|
||||||
|
- `AuditService.logAfterCommit()` — person mutations are audited
|
||||||
|
|
||||||
|
## Frontend counterpart
|
||||||
|
|
||||||
|
`frontend/src/lib/person/README.md`
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# tag
|
||||||
|
|
||||||
|
Hierarchical document categories. Tags form a tree via a self-referencing `parent_id` column and are applied to documents for filtering and browse navigation.
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Entity: `Tag` (self-referencing `parent_id` tree).
|
||||||
|
Features: tag CRUD, hierarchical deletion (cascade to descendants), tag typeahead, admin tag management (rename, reparent, merge).
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
- Documents — the `document_tags` join table is on the document side. `Tag` does not hold document references.
|
||||||
|
- Tag assignment — adding/removing a tag from a document is handled by `DocumentService`.
|
||||||
|
|
||||||
|
## Public surface (called from other domains)
|
||||||
|
|
||||||
|
| Method | Consumer | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `delete(UUID)` | document | Remove the tag record; called by `DocumentService.deleteTagCascading()` after all document references are unlinked |
|
||||||
|
| `deleteWithDescendants(UUID)` | admin tag UI | Recursive subtree deletion |
|
||||||
|
| `expandTagNamesToDescendantIdSets(List<String>)` | document | Expand tag filter to include descendant tags |
|
||||||
|
|
||||||
|
## Internal layout
|
||||||
|
|
||||||
|
- `TagController` — REST under `/api/tags`
|
||||||
|
- `TagService` — CRUD, hierarchy traversal, cascade-delete coordination
|
||||||
|
- `TagRepository` — find-or-create by name (case-insensitive), subtree queries
|
||||||
|
|
||||||
|
## Cross-domain dependencies
|
||||||
|
|
||||||
|
None. Documents reference tags; tags do not reference documents or other domains.
|
||||||
|
|
||||||
|
## Frontend counterpart
|
||||||
|
|
||||||
|
`frontend/src/lib/tag/README.md`
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# user
|
||||||
|
|
||||||
|
Login accounts and permission groups. An `AppUser` is a system user who can authenticate and act in the application — they are never a historical family member.
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Entities: `AppUser`, `UserGroup`, password-reset tokens, invite tokens.
|
||||||
|
Features: user CRUD, group CRUD, password change, password reset flow, invite links.
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
- `Person` records — historical family members. An `AppUser` is never linked to a `Person`. This separation is intentional: a person who digitized letters in 2024 is not the same entity as their great-grandmother who wrote them in 1912. See `docs/GLOSSARY.md`.
|
||||||
|
- Permission enforcement — `security/` owns `@RequirePermission` and `PermissionAspect`. `user/` only manages which permissions are stored on `UserGroup`.
|
||||||
|
|
||||||
|
## Public surface
|
||||||
|
|
||||||
|
`UserService` methods are consumed primarily by the security infrastructure and the admin UI. No other business-logic domain calls `UserService` directly.
|
||||||
|
|
||||||
|
The Spring Security chain (via `CustomUserDetailsService` in `security/`) calls `AppUserRepository.findByUsername()` on every authenticated request.
|
||||||
|
|
||||||
|
## Internal layout
|
||||||
|
|
||||||
|
- `UserController` — REST under `/api/users` (current user, CRUD)
|
||||||
|
- `AuthController` — password reset, invite flow
|
||||||
|
- `UserService` — BCrypt-encoded passwords, group assignment
|
||||||
|
- `AppUserRepository` — find by username (used by Spring Security)
|
||||||
|
- `UserGroupRepository` — group and permission management
|
||||||
|
|
||||||
|
## Cross-domain dependencies
|
||||||
|
|
||||||
|
- `AuditService.logAfterCommit()` — user-management mutations are audited
|
||||||
|
|
||||||
|
## Frontend counterpart
|
||||||
|
|
||||||
|
`frontend/src/lib/user/README.md`
|
||||||
146
docs/ARCHITECTURE.md
Normal file
146
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
<!-- Last reviewed: 2026-05-05 -->
|
||||||
|
|
||||||
|
# Familienarchiv — Architecture
|
||||||
|
|
||||||
|
**Target reader:** a PM-with-CS background who has read the README.
|
||||||
|
**Goal:** accurate mental model after one read — enough to sketch the system on a whiteboard.
|
||||||
|
|
||||||
|
For domain terminology, see [docs/GLOSSARY.md](GLOSSARY.md).
|
||||||
|
For security policies and hardening, see [docs/security-guide.md](security-guide.md).
|
||||||
|
For low-level ADR details, see [docs/adr/](adr/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. High-level diagram
|
||||||
|
|
||||||
|
The updated container diagram below shows all six deployable units and their communication paths.
|
||||||
|
|
||||||
|
See [docs/architecture/c4-diagrams.md](architecture/c4-diagrams.md) for the full C4 L1/L2/L3 diagrams (Mermaid, Gitea-rendered).
|
||||||
|
|
||||||
|
Key points not visible in the diagram:
|
||||||
|
|
||||||
|
- **OCR network boundary:** the OCR service has no external port — it is reachable only on the internal Docker Compose network. Only the backend calls it. The OCR service fetches PDF files from MinIO using a presigned URL that the backend generates and passes in the request body; the PDF bytes never pass through the backend.
|
||||||
|
- **SSE path:** server-sent event notifications go directly from the backend to the user's browser (not via the SvelteKit SSR layer) over a long-lived HTTP connection managed by `SseEmitterRegistry`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Domain set
|
||||||
|
|
||||||
|
Both stacks are organised **package-by-domain**: each domain owns its entities, service, controller, repository, and DTOs. Domain names are identical across `backend/src/main/java/.../` and `frontend/src/lib/`.
|
||||||
|
|
||||||
|
### Tier-1 domains — have entities and user-facing CRUD
|
||||||
|
|
||||||
|
**`document`** — the archive's core concept. Owns `Document`, `DocumentVersion`, `TranscriptionBlock`, `DocumentAnnotation`, `DocumentComment`. Does NOT own persons or tags (references them by ID). Cross-domain deps: `person` (sender/receivers), `tag` (labels), `ocr` (HTR pipeline), `notification` (comment mentions), `audit` (every mutation).
|
||||||
|
|
||||||
|
**`person`** — historical individuals referenced by documents. Owns `Person`, `PersonNameAlias`, `PersonRelationship`. Does NOT own `AppUser` (login accounts are a separate domain). Cross-domain deps: `document` (relationship queries).
|
||||||
|
|
||||||
|
**`tag`** — hierarchical document categories. Owns `Tag` (self-referencing `parent_id` tree). Does NOT own documents; the join is document-side. No cross-domain deps.
|
||||||
|
|
||||||
|
**`user`** — login accounts and permission groups. Owns `AppUser`, `UserGroup`, invite tokens. Does NOT own `Person` records. Cross-domain deps: `audit` (user management events).
|
||||||
|
|
||||||
|
**`geschichte`** — family stories. Owns `Geschichte` (`DRAFT → PUBLISHED` lifecycle). Cross-domain deps: `person`, `document` (linked entities in the story body).
|
||||||
|
|
||||||
|
**`notification`** — in-app messages. Owns `Notification`. Delivers via `SseEmitterRegistry` (live) and persisted rows (bell dropdown). Cross-domain deps: `user` (recipient), `document` (context).
|
||||||
|
|
||||||
|
**`ocr`** — OCR/HTR pipeline orchestration. Owns `OcrJob`, `OcrJobDocument`, `SenderModel`. Calls the Python OCR service; maps streamed transcription blocks back to `document`. Cross-domain deps: `document` (target), `filestorage` (presigned URLs).
|
||||||
|
|
||||||
|
### Tier-2 domains — derived (UI without dedicated tables)
|
||||||
|
|
||||||
|
A **derived domain** has its own routes and UI but no database tables of its own; it is assembled from data owned by Tier-1 domains.
|
||||||
|
|
||||||
|
**`conversation`** (route: `/briefwechsel`) — bilateral letter timeline between two `Person`s. Derived from `Document` sender/receiver relationships. The `DocumentRepository` bidirectional query is the only data source.
|
||||||
|
|
||||||
|
**`activity`** (route: `/aktivitaeten`) — family activity feed. Derived from `audit_log`, `notifications`, and document events. No aggregation table; computed on-the-fly by `DashboardService` and composed in the SvelteKit load function.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Cross-cutting layer
|
||||||
|
|
||||||
|
Members of the cross-cutting layer have no entity of their own, no user-facing CRUD, and are consumed by two or more domains — or are framework infrastructure that every domain depends on.
|
||||||
|
|
||||||
|
| Member (backend package) | Purpose | Admission criteria |
|
||||||
|
|---|---|---|
|
||||||
|
| `audit` | Append-only event store (`audit_log`) for all domain mutations. Feeds the activity feed and Family Pulse dashboard. | Consumed by 5+ domains; no user-facing CRUD of its own |
|
||||||
|
| `config` | Infrastructure bean definitions: `MinioConfig`, `AsyncConfig`, `WebConfig` | Framework infra; no business logic |
|
||||||
|
| `dashboard` | Stats aggregation for the admin dashboard and Family Pulse widget | Aggregates from 3+ domains; no owned entities |
|
||||||
|
| `exception` | `DomainException`, `ErrorCode` enum, `GlobalExceptionHandler` | Framework infra; consumed by every controller and service |
|
||||||
|
| `filestorage` | `FileService` — MinIO/S3 upload, download, presigned-URL generation | Generic service; consumed by `document` and `ocr` |
|
||||||
|
| `importing` | `MassImportService` — async ODS/Excel batch import | Orchestrates across `person`, `tag`, `document` |
|
||||||
|
| `security` | `SecurityConfig`, `Permission` enum, `@RequirePermission` annotation, `PermissionAspect` (AOP) | Framework infra; enforced globally across all controllers |
|
||||||
|
|
||||||
|
**Frontend `shared/`** follows the same admission criteria. Key members: `api.server.ts` (typed openapi-fetch client factory), `errors.ts` (backend `ErrorCode` → i18n mapping), `shared/primitives/` (generic UI components used across ≥2 domains), `shared/discussion/` (comment/mention editor used by `document` and `geschichte`), `shared/utils/` (pure date/sort/debounce utilities).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Stack-symmetry principle
|
||||||
|
|
||||||
|
**Rule:** a domain has the same name on both stacks.
|
||||||
|
|
||||||
|
| Backend | Frontend |
|
||||||
|
|---|---|
|
||||||
|
| `backend/src/main/java/.../document/` | `frontend/src/lib/document/` |
|
||||||
|
| `backend/src/main/java/.../person/` | `frontend/src/lib/person/` |
|
||||||
|
| … | … |
|
||||||
|
|
||||||
|
Adding a new Tier-1 domain means creating a package on **both** sides under the same name. Adding only a backend package without a corresponding frontend folder (or vice versa) is a red flag in code review.
|
||||||
|
|
||||||
|
The backend has been domain-first since the project started. The frontend `src/lib/` was restructured from flat-by-type to domain-first in issue #408 (May 2026).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Key architectural decisions
|
||||||
|
|
||||||
|
### ADR-001 — OCR as a Python microservice
|
||||||
|
The two OCR engines required (Surya for typewritten text, Kraken for Kurrent/Sütterlin HTR) exist only in the Python ecosystem. A separate `ocr-service` Python container exposes a simple HTTP API; the Spring Boot backend calls it via `RestClient`. All job tracking and business logic remain in Spring Boot. See [ADR-001](adr/001-ocr-python-microservice.md).
|
||||||
|
|
||||||
|
### ADR-002 — Polygon JSONB storage for annotations
|
||||||
|
Kraken outputs polygon boundaries for historical handwriting; axis-aligned bounding boxes approximate them poorly. Annotation and transcription-block positions are stored as `polygon JSONB` columns. Display-only — server-side geometry continues to use the AABB fields. See [ADR-002](adr/002-polygon-jsonb-storage.md).
|
||||||
|
|
||||||
|
### ADR-003 — Unified activity feed (Chronik/Aktivität)
|
||||||
|
Personal notifications and ambient activity (uploads, transcriptions, comments) are merged into one `/aktivitaeten` page. The SvelteKit load function composes data from `/api/dashboard/activity` and `/api/notifications` — no new backend orchestrator endpoint. See [ADR-003](adr/003-chronik-unified-activity-feed.md).
|
||||||
|
|
||||||
|
### ADR-004 — In-process PDFBox thumbnails
|
||||||
|
Thumbnails are rendered in Spring Boot using Apache PDFBox (already a dependency) rather than delegating to the OCR service. A dedicated `thumbnailExecutor` pool isolates the work. See [ADR-004](adr/004-pdfbox-thumbnails.md).
|
||||||
|
|
||||||
|
### ADR-005 — thumbnailAspect + pageCount
|
||||||
|
Aspect ratio (`PORTRAIT` / `LANDSCAPE`) and page count are persisted alongside the thumbnail JPEG at generation time — cheap to derive then, expensive to re-derive later. See [ADR-005](adr/005-thumbnail-aspect-and-page-count.md).
|
||||||
|
|
||||||
|
### ADR-006 — Synchronous domain events inside the publisher's transaction
|
||||||
|
When a `Person` display name changes, all `TranscriptionBlock` `@mention` text must be rewritten atomically. This is done via Spring `ApplicationEventPublisher` + `@EventListener @Transactional` to avoid a circular dependency between `PersonService` and `TranscriptionBlockService`. See [ADR-006](adr/006-synchronous-domain-events-in-transaction.md).
|
||||||
|
|
||||||
|
### Layering rule
|
||||||
|
```
|
||||||
|
Controller → Service → Repository → DB
|
||||||
|
```
|
||||||
|
Controllers never call repositories directly. Services never reach into another domain's repository — they call the other domain's service. This keeps domain boundaries clear and business logic testable without a running database.
|
||||||
|
|
||||||
|
### Permission system
|
||||||
|
Permissions are enforced via `@RequirePermission(Permission.X)` on controller methods, checked at runtime by `PermissionAspect` (Spring AOP). The `Permission` enum defines the available capabilities (`READ_ALL`, `WRITE_ALL`, `ADMIN`, `ADMIN_USER`, `ADMIN_TAG`, `ADMIN_PERMISSION`, `ANNOTATE_ALL`, `BLOG_WRITE`). This is not Spring Security's `@PreAuthorize` — do not mix the two mechanisms.
|
||||||
|
|
||||||
|
Sessions use a Base64-encoded Basic Auth token stored in an `httpOnly`, `SameSite=strict` cookie (`auth_token`, maxAge=86400 s). CSRF protection is disabled because this cookie configuration structurally prevents cross-origin credential theft. See [docs/security-guide.md](security-guide.md) for the full security reference.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Data flow walkthroughs
|
||||||
|
|
||||||
|
### Document upload
|
||||||
|
|
||||||
|
1. User submits the edit form (file + metadata) from the browser.
|
||||||
|
2. The SvelteKit server action sends `PUT /api/documents/{id}` as `multipart/form-data`. `hooks.server.ts` (`handleFetch`) transparently injects the `Authorization` header from the `auth_token` cookie — the action itself is unaware of auth.
|
||||||
|
3. `PermissionAspect` intercepts the controller method, verifies the user has `WRITE_ALL`, and proceeds.
|
||||||
|
4. `DocumentController` delegates to `DocumentService.updateDocument()`.
|
||||||
|
5. `DocumentService` resolves the `Person` sender by ID (via `PersonService`), resolves or creates `Tag`s (via `TagService`), then calls `FileService.uploadFile()`.
|
||||||
|
6. `FileService` generates a key (`documents/{UUID}_{filename}`), streams the file to MinIO via the AWS SDK v2 S3Client.
|
||||||
|
7. `DocumentService` persists the S3 key, sets `status = UPLOADED`, and saves to PostgreSQL.
|
||||||
|
8. `AuditService` writes an `UPLOADED` event to `audit_log` in the same transaction.
|
||||||
|
9. Backend returns the updated `Document` JSON; SvelteKit refreshes the document detail page.
|
||||||
|
|
||||||
|
### Transcription block autosave
|
||||||
|
|
||||||
|
1. The transcriber pauses typing; the frontend's `useBlockAutoSave` factory fires after a debounce interval.
|
||||||
|
2. The browser sends `PUT /api/documents/{documentId}/transcription-blocks/{blockId}` with the new text and the block's current `version` (optimistic lock). `hooks.server.ts` (`handleFetch`) injects the `Authorization` header from the cookie.
|
||||||
|
3. `TranscriptionService.saveBlock()` loads the block, checks the `@Version` field for concurrent edits, updates `block.text` and any `@mention` sidecars, and calls `saveAndFlush`.
|
||||||
|
4. If a concurrent save collides (version mismatch), the backend returns `409 Conflict`; the frontend's `saveBlockWithConflictRetry` helper re-fetches and retries.
|
||||||
|
5. On success, `AuditService` logs a `BLOCK_SAVED` event.
|
||||||
|
6. If the block text contains a new `@PersonName` mention, `NotificationService` creates a `Notification` row for the mentioned person's `AppUser`.
|
||||||
|
7. `SseEmitterRegistry` broadcasts the notification over the open SSE connection to that user's browser in real time.
|
||||||
@@ -1,97 +1,5 @@
|
|||||||
# Docs — Familienarchiv
|
# docs/
|
||||||
|
|
||||||
## Overview
|
→ See [docs/README.md](./README.md) for the folder structure and documentation guide.
|
||||||
|
|
||||||
Project documentation organized into four categories: architecture decision records (ADRs), system architecture diagrams, infrastructure runbooks, and detailed UI/UX specifications.
|
**LLM reminder:** ADRs are sequential — use the next number after the highest existing one in `docs/adr/`. When making a significant architectural change (new service, data model change, technology swap), write a new ADR before implementing.
|
||||||
|
|
||||||
## Folder Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
docs/
|
|
||||||
├── adr/ # Architecture Decision Records
|
|
||||||
├── architecture/ # C4 model diagrams and system architecture docs
|
|
||||||
├── infrastructure/ # Deployment, CI/CD, and ops guides
|
|
||||||
├── specs/ # UI/UX feature specifications (HTML)
|
|
||||||
├── app-analysis-*.md # Application analysis reports
|
|
||||||
├── mail.md # Mail system documentation
|
|
||||||
├── security-guide.md # Security policies and hardening guide
|
|
||||||
├── STYLEGUIDE.md # Coding and design style guide
|
|
||||||
├── TODO-backend.md # Backend backlog
|
|
||||||
└── TODO-frontend.md # Frontend backlog
|
|
||||||
```
|
|
||||||
|
|
||||||
## ADR (`adr/`)
|
|
||||||
|
|
||||||
Architecture Decision Records capture major technical decisions and their rationale.
|
|
||||||
|
|
||||||
| ADR | Title | Status |
|
|
||||||
|---|---|---|
|
|
||||||
| `001-ocr-python-microservice.md` | OCR as a separate Python container | Accepted |
|
|
||||||
| `002-polygon-jsonb-storage.md` | Polygon coordinates in JSONB columns | Accepted |
|
|
||||||
| `003-chronik-unified-activity-feed.md` | Unified activity feed (Chronik) | Accepted |
|
|
||||||
|
|
||||||
When making a significant architectural change (new service, data model change, technology swap), write a new ADR following the format:
|
|
||||||
- Status (Proposed / Accepted / Deprecated / Superseded)
|
|
||||||
- Context (forces at play)
|
|
||||||
- Decision (what we decided)
|
|
||||||
- Consequences (trade-offs)
|
|
||||||
- Alternatives Considered (table format)
|
|
||||||
|
|
||||||
## Architecture (`architecture/`)
|
|
||||||
|
|
||||||
Contains C4 model diagrams describing the system at different zoom levels:
|
|
||||||
|
|
||||||
- **Context diagram** — How Familienarchiv fits into the user and system ecosystem
|
|
||||||
- **Container diagram** — The high-level technology choices (Spring Boot, SvelteKit, PostgreSQL, MinIO, OCR service)
|
|
||||||
- **Component diagram** — Major structural components within the backend
|
|
||||||
|
|
||||||
Written in Markdown with embedded Mermaid or PlantUML diagrams (`c4-diagrams.md`).
|
|
||||||
|
|
||||||
## Infrastructure (`infrastructure/`)
|
|
||||||
|
|
||||||
Operational documentation for running Familienarchiv in production and CI.
|
|
||||||
|
|
||||||
| Document | Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `ci-gitea.md` | Gitea CI/CD pipeline configuration |
|
|
||||||
| `production-compose.md` | Production Docker Compose setup |
|
|
||||||
| `s3-migration.md` | Migrating documents between S3 buckets |
|
|
||||||
| `self-hosted-catalogue.md` | Self-hosted software catalogue |
|
|
||||||
|
|
||||||
## Specs (`specs/`)
|
|
||||||
|
|
||||||
High-fidelity UI/UX specifications written as standalone HTML files. These are design documents that describe exact layout, interactions, and responsive behavior before implementation.
|
|
||||||
|
|
||||||
Each spec typically includes:
|
|
||||||
- Visual mockups with CSS-in-HTML styling
|
|
||||||
- Interaction flows and state transitions
|
|
||||||
- Responsive breakpoint behavior
|
|
||||||
- Accessibility requirements
|
|
||||||
|
|
||||||
Examples of active spec areas:
|
|
||||||
- Document detail page (`document-topbar-*.html`, `documents-page-spec.html`)
|
|
||||||
- Admin interfaces (`admin-redesign-*.html`, `admin-tag-overhaul.html`)
|
|
||||||
- Transcription workflows (`inline-transcription-*.html`, `annotation-transcription-*.html`)
|
|
||||||
- Dashboard and activity feeds (`dashboard-*.html`, `chronik-spec.html`)
|
|
||||||
- OCR admin (`ocr-admin-spec.html`)
|
|
||||||
|
|
||||||
## How to Use
|
|
||||||
|
|
||||||
1. **Before implementing a feature**, check `specs/` for an existing specification.
|
|
||||||
2. **When proposing a new architecture**, draft an ADR in `adr/` and discuss before coding.
|
|
||||||
3. **When deploying**, follow `infrastructure/production-compose.md`.
|
|
||||||
4. **Keep TODO files updated** — they serve as lightweight backlogs.
|
|
||||||
|
|
||||||
## Style Guide
|
|
||||||
|
|
||||||
`STYLEGUIDE.md` covers:
|
|
||||||
- Code formatting and linting rules
|
|
||||||
- Component naming conventions
|
|
||||||
- Color palette and typography
|
|
||||||
- Accessibility standards (WCAG 2.1 AA)
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
- ADRs should be sequential (`NNN-descriptive-name.md`).
|
|
||||||
- Specs should be self-contained HTML files viewable in a browser.
|
|
||||||
- Infrastructure docs should include copy-pasteable commands.
|
|
||||||
|
|||||||
274
docs/DEPLOYMENT.md
Normal file
274
docs/DEPLOYMENT.md
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
<!-- Last reviewed: 2026-05-05 — reviewed at every milestone close -->
|
||||||
|
|
||||||
|
# Familienarchiv — Deployment Reference
|
||||||
|
|
||||||
|
> **If the app is down right now → jump to [§4 Logs](#4-logs--observability).**
|
||||||
|
|
||||||
|
This doc is the Day-1 checklist and operational reference. It links to the canonical infrastructure docs in `docs/infrastructure/` rather than duplicating them.
|
||||||
|
|
||||||
|
**Audience:** operator bringing up a fresh instance, or Successor-X debugging a live incident.
|
||||||
|
|
||||||
|
**Ownership:** project owner. Update this file in any PR that changes the container topology, env vars, or backup procedure.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
1. [Deployment topology](#1-deployment-topology)
|
||||||
|
2. [Environment variables](#2-environment-variables)
|
||||||
|
3. [Bootstrap from scratch](#3-bootstrap-from-scratch)
|
||||||
|
4. [Logs + observability](#4-logs--observability)
|
||||||
|
5. [Backup + recovery](#5-backup--recovery)
|
||||||
|
6. [Common operational tasks](#6-common-operational-tasks)
|
||||||
|
7. [Known limitations](#7-known-limitations)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Deployment topology
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
Browser -->|HTTPS| Caddy["Caddy (TLS termination)"]
|
||||||
|
Caddy -->|HTTP :5173| Frontend["Web Frontend\nSvelteKit / Node.js"]
|
||||||
|
Caddy -->|HTTP :8080| Backend["API Backend\nSpring Boot / Jetty :8080"]
|
||||||
|
Backend -->|JDBC :5432| DB[(PostgreSQL 16)]
|
||||||
|
Backend -->|S3 API :9000| MinIO[(MinIO / Hetzner OBS)]
|
||||||
|
Backend -->|HTTP :8000 internal| OCR["OCR Service\nPython FastAPI"]
|
||||||
|
OCR -->|presigned URL| MinIO
|
||||||
|
Browser -->|SSE direct| Backend
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key facts:**
|
||||||
|
- Caddy terminates TLS and reverse-proxies to frontend and backend. See the Caddyfile in [`docs/infrastructure/production-compose.md`](infrastructure/production-compose.md).
|
||||||
|
- The OCR service has **no external port** — reachable only on the internal Docker network from the backend.
|
||||||
|
- SSE notifications go directly backend → browser (not via the SvelteKit SSR layer).
|
||||||
|
- Management port 8081 (Spring Actuator / Prometheus scrape) is internal only — the Caddy config blocks `/actuator/*` externally.
|
||||||
|
|
||||||
|
### OCR memory requirements
|
||||||
|
|
||||||
|
The OCR service requires significant RAM for model loading. The dev compose sets `mem_limit: 12g`.
|
||||||
|
|
||||||
|
| Production target | RAM | Recommended OCR limit | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Hetzner CX42 | 16 GB | 12 GB | Recommended for OCR-enabled production |
|
||||||
|
| Hetzner CX32 | 8 GB | 6 GB | Accept reduced batch sizes and slower throughput |
|
||||||
|
| Hetzner CX22 | 4 GB | — | Disable the OCR service (`profiles: [ocr]`); run OCR on demand only |
|
||||||
|
|
||||||
|
A CX32 cannot honour a `mem_limit: 12g` — set it to `6g` in the prod overlay or use CX42.
|
||||||
|
|
||||||
|
### Dev vs production differences
|
||||||
|
|
||||||
|
| Concern | Dev compose | Prod overlay |
|
||||||
|
|---|---|---|
|
||||||
|
| MinIO image tag | `minio/minio:latest` (unpinned) | Pinned in prod overlay |
|
||||||
|
| Data persistence | Bind mounts `./data/postgres`, `./data/minio` | Named Docker volumes |
|
||||||
|
| Bucket creation | `create-buckets` helper container | Pre-created in Hetzner console |
|
||||||
|
| Spring profile | `dev,e2e` (enables OpenAPI + Swagger UI) | `prod` |
|
||||||
|
| Mail | Mailpit (local catcher) | Real SMTP |
|
||||||
|
|
||||||
|
Full prod overlay: [`docs/infrastructure/production-compose.md`](infrastructure/production-compose.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Environment variables
|
||||||
|
|
||||||
|
All vars are set in `.env` at the repo root (copy from `.env.example`). The backend resolves them via `application.yaml`; the Docker Compose file wires them into each container.
|
||||||
|
|
||||||
|
**Any var found in `docker-compose.yml` or `application*.yaml` that is not in this table is a blocking review comment on any PR that changes those files.**
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
|
||||||
|
| Variable | Purpose | Default | Required? | Sensitive? |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `SPRING_DATASOURCE_URL` | PostgreSQL JDBC URL | — | YES | — |
|
||||||
|
| `SPRING_DATASOURCE_USERNAME` | DB username | — | YES | — |
|
||||||
|
| `SPRING_DATASOURCE_PASSWORD` | DB password | — | YES | YES |
|
||||||
|
| `S3_ENDPOINT` | MinIO / OBS endpoint URL | — | YES | — |
|
||||||
|
| `S3_ACCESS_KEY` | MinIO access key (use service account, not root in prod) | — | YES | YES |
|
||||||
|
| `S3_SECRET_KEY` | MinIO secret key | — | YES | YES |
|
||||||
|
| `S3_BUCKET_NAME` | Target bucket name | — | YES | — |
|
||||||
|
| `S3_REGION` | S3 region string | `us-east-1` | YES | — |
|
||||||
|
| `APP_ADMIN_USERNAME` | Bootstrap admin username (⚠ not in .env.example) | `admin` | YES | — |
|
||||||
|
| `APP_ADMIN_PASSWORD` | Bootstrap admin password (⚠ ships as `admin123`) | `admin123` | YES | YES |
|
||||||
|
| `APP_BASE_URL` | Public-facing URL for email links | `http://localhost:3000` | YES (prod) | — |
|
||||||
|
| `APP_OCR_BASE_URL` | Internal URL of the OCR service | — | YES | — |
|
||||||
|
| `APP_OCR_TRAINING_TOKEN` | Secret token for OCR training endpoints | — | YES (prod) | YES |
|
||||||
|
| `MAIL_HOST` | SMTP host | `mailpit` (dev) | YES (prod) | — |
|
||||||
|
| `MAIL_PORT` | SMTP port | `1025` (dev) | YES (prod) | — |
|
||||||
|
| `MAIL_USERNAME` | SMTP username | — | YES (prod) | YES |
|
||||||
|
| `MAIL_PASSWORD` | SMTP password | — | YES (prod) | YES |
|
||||||
|
| `APP_MAIL_FROM` | From address for outbound mail | `noreply@familienarchiv.local` | YES (prod) | — |
|
||||||
|
| `MAIL_SMTP_AUTH` | SMTP auth enabled | `false` (dev) | YES (prod) | — |
|
||||||
|
| `MAIL_STARTTLS_ENABLE` | STARTTLS enabled | `false` (dev) | YES (prod) | — |
|
||||||
|
| `SPRING_PROFILES_ACTIVE` | Spring profile | `dev,e2e` (compose) | YES | — |
|
||||||
|
|
||||||
|
### PostgreSQL container
|
||||||
|
|
||||||
|
| Variable | Purpose | Default | Required? | Sensitive? |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `POSTGRES_USER` | DB superuser | `archive_user` | YES | — |
|
||||||
|
| `POSTGRES_PASSWORD` | DB password | `change-me` | YES | YES |
|
||||||
|
| `POSTGRES_DB` | Database name | `family_archive_db` | YES | — |
|
||||||
|
|
||||||
|
### MinIO container
|
||||||
|
|
||||||
|
| Variable | Purpose | Default | Required? | Sensitive? |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `MINIO_ROOT_USER` | MinIO root username | `minio_admin` | YES | — |
|
||||||
|
| `MINIO_ROOT_PASSWORD` | MinIO root password | `change-me` | YES | YES |
|
||||||
|
| `MINIO_DEFAULT_BUCKETS` | Bucket name | `archive-documents` | YES | — |
|
||||||
|
|
||||||
|
### OCR service
|
||||||
|
|
||||||
|
| Variable | Purpose | Default | Required? | Sensitive? |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `TRAINING_TOKEN` | Guards `/train` and `/segtrain` endpoints (accepts file uploads) | — | YES (prod) | YES |
|
||||||
|
| `ALLOWED_PDF_HOSTS` | SSRF protection — comma-separated list of allowed PDF source hosts. **Do not widen to `*`** | `minio,localhost,127.0.0.1` | YES | — |
|
||||||
|
| `KRAKEN_MODEL_PATH` | Directory containing Kraken HTR models (populated by `download-kraken-models.sh`) | `/app/models/` | — | — |
|
||||||
|
| `BLLA_MODEL_PATH` | Kraken baseline layout analysis model path | `/app/models/blla.mlmodel` | — | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Bootstrap from scratch
|
||||||
|
|
||||||
|
> Full VPS provisioning steps are in [`docs/infrastructure/production-compose.md`](infrastructure/production-compose.md). This section covers the sequence and the security-critical steps.
|
||||||
|
|
||||||
|
### Security checklist — complete before first boot
|
||||||
|
|
||||||
|
> ⚠️ **These defaults ship in `.env.example` and `application.yaml`. Change them or you will have an insecure installation.**
|
||||||
|
|
||||||
|
- [ ] Set `APP_ADMIN_PASSWORD` (default: `admin123` — change before starting the backend)
|
||||||
|
- [ ] Set `APP_ADMIN_USERNAME` if you want a non-default admin login name (add to `.env` — not in `.env.example`)
|
||||||
|
- [ ] Rotate `POSTGRES_PASSWORD` from `change-me`
|
||||||
|
- [ ] Rotate `MINIO_ROOT_PASSWORD` from `change-me`
|
||||||
|
- [ ] Set a strong `APP_OCR_TRAINING_TOKEN` (backend) and the matching `TRAINING_TOKEN` (OCR service) — both must be the same value (`python3 -c "import secrets; print(secrets.token_hex(32))"`)
|
||||||
|
- [ ] Confirm `ALLOWED_PDF_HOSTS` is locked to your MinIO/S3 hostname — widening to `*` opens SSRF
|
||||||
|
- [ ] Set `SPRING_PROFILES_ACTIVE=prod` in the prod overlay (not `dev,e2e` — that exposes Swagger UI and `/v3/api-docs`)
|
||||||
|
- [ ] Use a dedicated MinIO service account for `S3_ACCESS_KEY` / `S3_SECRET_KEY`, not the root credentials
|
||||||
|
|
||||||
|
### Bootstrap sequence
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Copy and fill the env file
|
||||||
|
cp .env.example .env
|
||||||
|
# edit .env — complete the security checklist above first
|
||||||
|
|
||||||
|
# 2. (Production only) Create the MinIO / Hetzner OBS bucket in the console
|
||||||
|
# The dev compose has a create-buckets helper; production does not.
|
||||||
|
# Create the bucket named $MINIO_DEFAULT_BUCKETS with private access.
|
||||||
|
|
||||||
|
# 3. Start the stack (prod overlay — see docs/infrastructure/production-compose.md)
|
||||||
|
# docker-compose.prod.yml is NOT committed — create it from the guide above
|
||||||
|
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||||
|
|
||||||
|
# 4. Flyway migrations run automatically on backend start.
|
||||||
|
# Watch the backend log to confirm:
|
||||||
|
docker compose logs --follow --tail=100 backend
|
||||||
|
|
||||||
|
# 5. Verify the stack is healthy
|
||||||
|
curl http://localhost:8080/actuator/health
|
||||||
|
# Expected: {"status":"UP"}
|
||||||
|
|
||||||
|
# 6. Open the app and log in with the admin credentials from .env
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Do not use `docker-compose.ci.yml` locally** — it disables bind mounts that the dev workflow depends on.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Logs + observability
|
||||||
|
|
||||||
|
### First-response commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stream backend logs (most useful first)
|
||||||
|
docker compose logs --follow --tail=100 backend
|
||||||
|
|
||||||
|
# Stream all services
|
||||||
|
docker compose logs --follow
|
||||||
|
|
||||||
|
# Single snapshot
|
||||||
|
docker compose logs --tail=200 <service>
|
||||||
|
# services: frontend, backend, db, minio, ocr-service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log locations
|
||||||
|
|
||||||
|
- **Backend application log**: stdout (captured by Docker). Access inside the container at `/app/logs/` via `docker exec`.
|
||||||
|
- **Spring Actuator health**: `http://localhost:8080/actuator/health` (internal only in prod — port 8081 for Prometheus scraping)
|
||||||
|
- **Prometheus scraping**: management port 8081, path `/actuator/prometheus`. Internal only; Caddy blocks `/actuator/*` externally.
|
||||||
|
|
||||||
|
### Future observability
|
||||||
|
|
||||||
|
Phase 7 of the Production v1 milestone adds Prometheus + Loki + Grafana. No monitoring infrastructure is in place yet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Backup + recovery
|
||||||
|
|
||||||
|
### Current state — no automated backup
|
||||||
|
|
||||||
|
No automated backup is configured. Manual procedure for a point-in-time backup:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# PostgreSQL dump
|
||||||
|
docker exec archive-db pg_dump -U ${POSTGRES_USER} ${POSTGRES_DB} > backup-$(date +%Y%m%d).sql
|
||||||
|
|
||||||
|
# MinIO data (bind-mounted in dev)
|
||||||
|
# Copy ./data/minio/ to external storage
|
||||||
|
```
|
||||||
|
|
||||||
|
Restoration:
|
||||||
|
```bash
|
||||||
|
# Restore Postgres
|
||||||
|
docker exec -i archive-db psql -U ${POSTGRES_USER} ${POSTGRES_DB} < backup-YYYYMMDD.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
### Planned — phase 5 of Production v1 milestone
|
||||||
|
|
||||||
|
Automated backup (PostgreSQL WAL archiving + MinIO bucket replication) is planned in the Production v1 milestone phase 5. Until that ships: **manual backups are the only recovery option.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Common operational tasks
|
||||||
|
|
||||||
|
### Reset dev database (truncates data, keeps schema)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/reset-db.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
> Truncates all data but does **not** drop the schema or re-run Flyway. Use for E2E test resets, not full reinstalls.
|
||||||
|
> ⚠️ Script hardcodes `DB_USER=archive_user` and `DB_NAME=family_archive_db` — if you customised these in `.env`, edit the script accordingly.
|
||||||
|
|
||||||
|
### Rebuild frontend container (clears node_modules volume)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/rebuild-frontend.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
> Assumes the Docker Compose volume is named `familienarchiv_frontend_node_modules`. If your project directory is not named `familienarchiv`, edit line 16 of the script.
|
||||||
|
|
||||||
|
### Download Kraken OCR models
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/download-kraken-models.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
> Downloads the Kurrent/Sütterlin HTR models. Run once after a fresh clone or when models are updated.
|
||||||
|
|
||||||
|
### Trigger a mass import (Excel/ODS)
|
||||||
|
|
||||||
|
1. Place the import file in the `import/` bind mount on the backend container.
|
||||||
|
2. Call `POST /api/admin/trigger-import` (requires `ADMIN` permission).
|
||||||
|
3. The import runs asynchronously — poll `GET /api/admin/import-status` or watch backend logs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Known limitations
|
||||||
|
|
||||||
|
| Limitation | Reason | Reference |
|
||||||
|
|---|---|---|
|
||||||
|
| **Single-node OCR service** | The two required OCR engines (Surya + Kraken) exist only in the Python ecosystem; horizontal scaling would require a job queue not currently implemented | [ADR-001](adr/001-ocr-python-microservice.md) |
|
||||||
|
| **No multi-tenancy** | Designed as a single-family private archive; all authenticated users share the same document space | Deliberate scope decision (family-only product frame) |
|
||||||
|
| **No multi-region** | Single PostgreSQL + MinIO instance; no replication or failover | Deliberate scope decision |
|
||||||
|
| **Max upload size** | 50 MB per file (500 MB per request for multi-file) | Configurable in `application.yaml` (`spring.servlet.multipart`) |
|
||||||
|
| **No automated backup** | Phase 5 of Production v1 milestone is not yet implemented | See §5 above |
|
||||||
113
docs/GLOSSARY.md
Normal file
113
docs/GLOSSARY.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# Familienarchiv — Glossary
|
||||||
|
|
||||||
|
Domain-specific and overloaded terms used in this codebase.
|
||||||
|
Each entry: **Term** — definition (≤ 2 sentences). Where two terms are easily confused, a _Not to be confused with_ note follows.
|
||||||
|
|
||||||
|
For architecture context see [`docs/architecture/c4-diagrams.md`](architecture/c4-diagrams.md).
|
||||||
|
For domain package structure see [`docs/ARCHITECTURE.md`](ARCHITECTURE.md) _(coming: DOC-2)_.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Identity Terms
|
||||||
|
|
||||||
|
**AppUser** (`AppUser`) — a real person who can log into the system (a family member or administrator). `AppUser` records carry login credentials, group memberships, and notification history.
|
||||||
|
_Not to be confused with [Person](#person-person)_ — an AppUser is never recorded as a document sender, receiver, or historical individual.
|
||||||
|
|
||||||
|
**Permission** — a discrete capability string assigned to a `UserGroup` (e.g. `READ_ALL`, `WRITE_ALL`, `ADMIN`, `ADMIN_USER`, `ADMIN_TAG`, `ADMIN_PERMISSION`). Enforced via the `@RequirePermission` AOP annotation on controller methods, checked at runtime by `PermissionAspect`; not via Spring Security's `@PreAuthorize`.
|
||||||
|
|
||||||
|
**Person** (`Person`) — a historical individual in the family archive (sender, receiver of letters, person mentioned in transcriptions). NEVER has a login account and NEVER appears as an `AppUser`.
|
||||||
|
_Not to be confused with [AppUser](#appuser-appuser)_ — `Person` is a historical record; `AppUser` is someone who can log in today.
|
||||||
|
|
||||||
|
**PersonNameAlias** (`PersonNameAlias`) — an alternate or historical name form associated with a `Person` (e.g. maiden name, nickname, abbreviated form). Used to locate `Person` records during mass import via `PersonNameAliasType`.
|
||||||
|
|
||||||
|
**UserGroup** (`UserGroup`) — a named permission bundle assigned to one or more `AppUser`s. A user's effective permissions are the union of all permissions across all groups they belong to.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Document-Related Terms
|
||||||
|
|
||||||
|
**Annotation** (`DocumentAnnotation`) — a free-form polygon or shape drawn over a document page image to highlight a region of interest. Always scoped to a specific page of a `Document`; stored as a polygon (JSONB).
|
||||||
|
_See also [TranscriptionBlock](#transcriptionblock-transcriptionblock)._
|
||||||
|
|
||||||
|
**Comment** (`DocumentComment`, table `document_comments`) — a threaded discussion message attached to a `Document`. Always scoped to a `Document`; optionally further contextualized by a specific `DocumentAnnotation` or `TranscriptionBlock`.
|
||||||
|
|
||||||
|
**Document** (`Document`) — a single archival item (letter, postcard, photograph) with a file stored in MinIO/S3 and associated metadata (sender, receivers, date, tags, transcription blocks).
|
||||||
|
|
||||||
|
**DocumentVersion** (`DocumentVersion`) — an append-only snapshot of a `Document`'s metadata at a point in time. Append-only by convention; no consumer-facing create or update endpoint exists. The entity uses Lombok `@Data` (which generates setters), so immutability is enforced by application convention, not at the Java level.
|
||||||
|
|
||||||
|
**Tag** (`Tag`) — a hierarchical category that can be applied to `Document`s. Tags are self-referencing via a `parent_id` foreign key, forming a tree structure.
|
||||||
|
|
||||||
|
**TranscriptionBlock** (`TranscriptionBlock`) — a paragraph-level segment of a `Document`'s transcribed text, with a polygon region (stored as JSONB) identifying its position on the page. One document can have many blocks across multiple pages.
|
||||||
|
_See also [Annotation](#annotation-documentannotation)._
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Terms
|
||||||
|
|
||||||
|
**DocumentStatus lifecycle** — the ordered states a `Document` moves through:
|
||||||
|
`PLACEHOLDER → UPLOADED → TRANSCRIBED → REVIEWED → ARCHIVED`
|
||||||
|
- `PLACEHOLDER`: created during mass import; no file attached yet.
|
||||||
|
- `UPLOADED`: a file has been stored in MinIO/S3.
|
||||||
|
- `TRANSCRIBED`: all transcription blocks have been marked done.
|
||||||
|
- `REVIEWED`: a reviewer has approved the transcription.
|
||||||
|
- `ARCHIVED`: the document is finalized and read-only.
|
||||||
|
|
||||||
|
**Mass import** — an asynchronous batch process (`MassImportService`) that reads an Excel or ODS file and creates `Person`s, `Tag`s, and `PLACEHOLDER` `Document`s in one shot. Only one import can run at a time (`IMPORT_ALREADY_RUNNING` error if attempted concurrently).
|
||||||
|
|
||||||
|
**Transcription queue** — the set of `Document`s and `TranscriptionBlock`s awaiting work, computed on-the-fly from `Document`/`Block` status. Three views: segmentation queue, transcription queue, ready-to-read queue. NOT a persistent entity — no `transcription_queues` table exists.
|
||||||
|
_See also [DocumentStatus lifecycle](#documentstatus-lifecycle)._
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OCR-Specific Terms
|
||||||
|
|
||||||
|
**HTR** — Handwritten Text Recognition. Recognizes cursive and historical handwriting (contrasted with OCR for printed/typewritten text). The primary mode used for letters in this archive.
|
||||||
|
|
||||||
|
**Kurrent** — Old German cursive handwriting style, the primary historical script appearing in letters from the 1899–1950 period covered by this archive.
|
||||||
|
|
||||||
|
**OCR** — Optical Character Recognition. Recognizes printed or typewritten text. Used for typed documents; HTR is used for handwritten ones.
|
||||||
|
|
||||||
|
**OcrJob** (`OcrJob`, table `ocr_jobs`) — a first-class persistent entity tracking a batch OCR run across one or more documents (`OcrJobDocument`, table `ocr_job_documents`). Distinct from the concept of "running OCR on a single document." Lifecycle: `PENDING → RUNNING → DONE / FAILED` (see `OcrJobStatus`).
|
||||||
|
|
||||||
|
**SenderModel** (`SenderModel`, table `sender_models`) — a fine-tuned Kraken HTR model trained on a specific historical correspondent's handwriting. Both an OCR-service concept (the model weights) and a persistent entity linking a `Person` to the path of their trained model file.
|
||||||
|
|
||||||
|
**Sütterlin** — A specific standardized style of Kurrent taught in German schools from 1915 to 1941.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Other Domain Terms
|
||||||
|
|
||||||
|
**Aktivität / Aktivitäten** `[user-facing]` — the family activity feed accessible at `/aktivitaeten`. Shows recent documents, transcriptions, comments, and Geschichten as a chronological timeline.
|
||||||
|
_See also [Chronik](#chronik-internal)._
|
||||||
|
|
||||||
|
**Briefwechsel** `[user-facing]` — the bilateral conversation timeline between two `Person`s, derived from `Document` sender/receiver relationships. Accessible at `/briefwechsel`. Not a persistent entity — data is computed from existing `Document` records.
|
||||||
|
_See also [Derived domain](#derived-domain)._
|
||||||
|
|
||||||
|
**Chronik** `[internal]` — the conceptual and code-level name for the unified activity feed (per ADR-003 `003-chronik-unified-activity-feed.md`). Used in code, architecture documents, and ADRs. The user-facing label for the same concept is [Aktivität](#aktivitat--aktivitaten-user-facing).
|
||||||
|
|
||||||
|
**Geschichte** (`Geschichte`) `[user-facing]` — a narrative story or article published in the archive, linking `Person`s and `Document`s. Lifecycle: `DRAFT → PUBLISHED` (see `GeschichteStatus`). DRAFT stories are hidden from users without the `BLOG_WRITE` permission.
|
||||||
|
|
||||||
|
**Notification** (`Notification`) — an in-app message delivered to an `AppUser`. No email or SMS delivery exists today. Delivered via Server-Sent Events (`SseEmitterRegistry`) and persisted in the `notifications` table.
|
||||||
|
|
||||||
|
**Audit log** (`AuditLog`, table `audit_log`) — an append-only event store recording domain-level activity (document edits, user actions, etc.). Append-only by application convention; a `REVOKE UPDATE, DELETE` is attempted at the DB layer (see migrations V46, V47) but is a no-op if the application role is the table owner in PostgreSQL. Do not rely on DB-enforced immutability — the constraint is application-layer only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architectural Terms
|
||||||
|
|
||||||
|
**Cross-cutting** — code that lives in `lib/shared/` (frontend) or cross-domain packages (backend) because it has no entity of its own, no user-facing CRUD, AND is used by two or more domains OR is framework infrastructure (error handling, API client, i18n utilities).
|
||||||
|
|
||||||
|
**Derived domain** — a Tier-2 frontend domain that has its own UI but no backend entities of its own. Data is computed from Tier-1 domain records. Current derived domains: `conversation` (from `Document` sender/receivers) and `activity` (from audit, notifications, document events).
|
||||||
|
_See also [Briefwechsel](#briefwechsel-user-facing)._
|
||||||
|
|
||||||
|
**Domain** — a Tier-1 bounded context with its own entities, controller, service, repository, and DTOs. Backend domains: `document`, `person`, `tag`, `user`, `geschichte`, `notification`, `ocr`, `audit`, `dashboard`. Frontend domains mirror this structure under `src/lib/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pending Terms
|
||||||
|
|
||||||
|
_Terms flagged as potentially ambiguous that have not yet been formally defined here. Add an entry above and remove it from this list when resolved._
|
||||||
|
|
||||||
|
- Terms surfaced by Epic 1 audit findings (#388–#392) — review audit reports under `docs/audits/` when available and add any term flagged as ambiguous.
|
||||||
|
- `OcrBatchService` vs `OcrAsyncRunner` — both handle async OCR orchestration; their division of responsibility should be clarified here.
|
||||||
|
- `Stammbaum` — the genealogy tree view; relationship to `PersonRelationship` entity.
|
||||||
85
docs/README.md
Normal file
85
docs/README.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# docs/
|
||||||
|
|
||||||
|
Project documentation organised into four categories: architecture decision records (ADRs), system architecture diagrams, infrastructure runbooks, and detailed UI/UX specifications.
|
||||||
|
|
||||||
|
## Folder structure
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/
|
||||||
|
├── adr/ # Architecture Decision Records
|
||||||
|
├── architecture/ # C4 model diagrams and system architecture docs
|
||||||
|
├── infrastructure/ # Deployment, CI/CD, and ops guides
|
||||||
|
├── specs/ # UI/UX feature specifications (HTML)
|
||||||
|
├── ARCHITECTURE.md # Human-readable architecture overview (DOC-2)
|
||||||
|
├── DEPLOYMENT.md # Day-1 checklist and operational reference (DOC-5)
|
||||||
|
├── GLOSSARY.md # Domain terminology (DOC-3)
|
||||||
|
├── security-guide.md # Security policies and hardening guide
|
||||||
|
└── STYLEGUIDE.md # Coding and design style guide
|
||||||
|
```
|
||||||
|
|
||||||
|
## ADR (`adr/`)
|
||||||
|
|
||||||
|
Architecture Decision Records capture major technical decisions and their rationale.
|
||||||
|
|
||||||
|
| ADR | Title | Status |
|
||||||
|
| -------------------------------------- | ------------------------------------ | -------- |
|
||||||
|
| `001-ocr-python-microservice.md` | OCR as a separate Python container | Accepted |
|
||||||
|
| `002-polygon-jsonb-storage.md` | Polygon coordinates in JSONB columns | Accepted |
|
||||||
|
| `003-chronik-unified-activity-feed.md` | Unified activity feed (Chronik) | Accepted |
|
||||||
|
|
||||||
|
When making a significant architectural change (new service, data model change, technology swap), write a new ADR:
|
||||||
|
|
||||||
|
- **Status** (Proposed / Accepted / Deprecated / Superseded)
|
||||||
|
- **Context** (forces at play)
|
||||||
|
- **Decision** (what we decided)
|
||||||
|
- **Consequences** (trade-offs)
|
||||||
|
- **Alternatives Considered** (table format)
|
||||||
|
|
||||||
|
ADRs are sequential (`NNN-descriptive-name.md`). Do not reuse numbers.
|
||||||
|
|
||||||
|
## Architecture (`architecture/`)
|
||||||
|
|
||||||
|
Contains C4 model diagrams describing the system at different zoom levels:
|
||||||
|
|
||||||
|
- **Context diagram** — How Familienarchiv fits into the user and system ecosystem
|
||||||
|
- **Container diagram** — The high-level technology choices (Spring Boot, SvelteKit, PostgreSQL, MinIO, OCR service)
|
||||||
|
- **Component diagram** — Major structural components within the backend
|
||||||
|
|
||||||
|
Written in Markdown with embedded Mermaid diagrams (`c4-diagrams.md`). Gitea renders these automatically.
|
||||||
|
|
||||||
|
For the human-readable architecture narrative, see [`docs/ARCHITECTURE.md`](ARCHITECTURE.md).
|
||||||
|
|
||||||
|
## Infrastructure (`infrastructure/`)
|
||||||
|
|
||||||
|
Operational documentation for running Familienarchiv in production and CI.
|
||||||
|
|
||||||
|
| Document | Purpose |
|
||||||
|
| -------------------------- | ---------------------------------------------------- |
|
||||||
|
| `ci-gitea.md` | Gitea CI/CD pipeline configuration |
|
||||||
|
| `production-compose.md` | Production Docker Compose setup and VPS provisioning |
|
||||||
|
| `s3-migration.md` | Migrating documents between S3 buckets |
|
||||||
|
| `self-hosted-catalogue.md` | Self-hosted software catalogue |
|
||||||
|
|
||||||
|
For the day-1 deployment checklist, see [`docs/DEPLOYMENT.md`](DEPLOYMENT.md).
|
||||||
|
|
||||||
|
## Specs (`specs/`)
|
||||||
|
|
||||||
|
High-fidelity UI/UX specifications written as standalone HTML files. These are design documents describing exact layout, interactions, and responsive behavior before implementation.
|
||||||
|
|
||||||
|
Each spec typically includes:
|
||||||
|
|
||||||
|
- Visual mockups with CSS-in-HTML styling
|
||||||
|
- Interaction flows and state transitions
|
||||||
|
- Responsive breakpoint behavior
|
||||||
|
- Accessibility requirements
|
||||||
|
|
||||||
|
Before implementing a feature, check `specs/` for an existing specification.
|
||||||
|
|
||||||
|
## Style Guide
|
||||||
|
|
||||||
|
[`docs/STYLEGUIDE.md`](STYLEGUIDE.md) covers:
|
||||||
|
|
||||||
|
- Code formatting and linting rules
|
||||||
|
- Component naming conventions
|
||||||
|
- Color palette and typography
|
||||||
|
- Accessibility standards (WCAG 2.1 AA)
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# Familienarchiv — C4 Architecture Diagrams
|
# Familienarchiv — C4 Architecture Diagrams
|
||||||
|
|
||||||
|
> For domain terminology used in these diagrams, see [docs/GLOSSARY.md](../GLOSSARY.md).
|
||||||
|
|
||||||
## Level 1 — System Context
|
## Level 1 — System Context
|
||||||
|
|
||||||
Who uses the system and what external systems does it interact with.
|
Who uses the system and what external systems does it interact with.
|
||||||
@@ -32,9 +34,11 @@ C4Container
|
|||||||
System_Boundary(archiv, "Familienarchiv (Docker Compose)") {
|
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 session cookies, search UI, document viewer, and admin panel.")
|
||||||
|
|
||||||
Container(backend, "API Backend", "Spring Boot 4 / Java 21 / Jetty", "REST API. Implements document management, search, user auth, file upload/download, and Excel import.")
|
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.")
|
||||||
|
|
||||||
ContainerDb(db, "Relational Database", "PostgreSQL 16", "Stores document metadata, persons, users, permission groups, tags, and Spring Session data.")
|
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}.")
|
ContainerDb(storage, "Object Storage", "MinIO (S3-compatible)", "Stores the actual document files (PDFs, scans). Objects keyed as documents/{UUID}_{filename}.")
|
||||||
|
|
||||||
@@ -43,8 +47,11 @@ C4Container
|
|||||||
|
|
||||||
Rel(user, frontend, "Uses", "HTTPS / Browser")
|
Rel(user, frontend, "Uses", "HTTPS / Browser")
|
||||||
Rel(frontend, backend, "API requests with Basic Auth token", "HTTP / REST / JSON")
|
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, 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, 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(ocr, storage, "Fetches PDF via presigned URL", "HTTP / S3 presigned")
|
||||||
Rel(mc, storage, "Creates bucket on startup", "MinIO Client CLI")
|
Rel(mc, storage, "Creates bucket on startup", "MinIO Client CLI")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
481
docs/specs/reader-dashboard-a-willkommen.html
Normal file
481
docs/specs/reader-dashboard-a-willkommen.html
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Reader Dashboard — Concept A · Herzlich Willkommen · Familienarchiv</title>
|
||||||
|
<style>
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:'Helvetica Neue',Arial,sans-serif;background:#ECEAE4;color:#1A1A1A;line-height:1.5}
|
||||||
|
.doc{max-width:1440px;margin:0 auto;padding:48px 32px}
|
||||||
|
|
||||||
|
/* ── Masthead ─── */
|
||||||
|
.mast{background:#0D2240;border-radius:10px;padding:32px 40px;margin-bottom:48px}
|
||||||
|
.mast-top{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;margin-bottom:16px}
|
||||||
|
.mast h1{font-size:22px;font-weight:900;color:#fff;letter-spacing:-.4px;margin-bottom:6px}
|
||||||
|
.mast p{font-size:12px;color:rgba(255,255,255,.5);max-width:660px;line-height:1.7}
|
||||||
|
.mast-badge{font-size:9px;font-weight:800;padding:3px 9px;border-radius:20px;text-transform:uppercase;letter-spacing:.8px;white-space:nowrap;flex-shrink:0;margin-top:4px;background:#A6DAD8;color:#002850}
|
||||||
|
.decisions{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px;border-top:1px solid rgba(255,255,255,.1);padding-top:16px}
|
||||||
|
.dec{background:rgba(255,255,255,.06);border-radius:6px;padding:10px 12px}
|
||||||
|
.dec-label{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:rgba(255,255,255,.35);margin-bottom:5px}
|
||||||
|
.dec-value{font-size:9.5px;font-weight:700;color:#fff;line-height:1.5}
|
||||||
|
|
||||||
|
/* ── Section headings ─── */
|
||||||
|
.sec{margin-bottom:64px}
|
||||||
|
.sec+.sec{border-top:2px dashed #C8C4BE;padding-top:56px}
|
||||||
|
.sec-h{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#888;margin-bottom:20px;display:flex;align-items:center;gap:10px}
|
||||||
|
.sec-h::after{content:'';flex:1;height:1px;background:#D8D4CE}
|
||||||
|
.sec-num{background:#0D2240;color:#fff;font-size:9px;font-weight:900;padding:2px 7px;border-radius:10px}
|
||||||
|
|
||||||
|
/* ── Screen grid ─── */
|
||||||
|
.sg{display:grid;gap:24px;align-items:start}
|
||||||
|
.sg-2{grid-template-columns:1fr 340px}
|
||||||
|
.sb{display:flex;flex-direction:column;gap:10px}
|
||||||
|
.sl{font-size:9px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;display:flex;align-items:center;gap:6px}
|
||||||
|
.sz{background:#E8E4DF;color:#666;padding:1px 5px;border-radius:3px;font-size:8px}
|
||||||
|
|
||||||
|
/* ── Annotation callouts ─── */
|
||||||
|
.ann-block{background:#FFF7ED;border:1px solid #FDBA74;border-radius:5px;padding:8px 10px;font-size:10px;color:#7C2D12;line-height:1.5}
|
||||||
|
.ann-block strong{font-weight:800}
|
||||||
|
.ann-block ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
.note{background:#F0F9FF;border:1px solid #BAE6FD;border-radius:5px;padding:8px 10px;font-size:10px;color:#0C4A6E;line-height:1.5}
|
||||||
|
.note strong{font-weight:800}
|
||||||
|
.note ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
.ok{background:#F0FDF4;border:1px solid #86EFAC;border-radius:5px;padding:8px 10px;font-size:10px;color:#166534;line-height:1.5}
|
||||||
|
.ok strong{font-weight:800}
|
||||||
|
.ok ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
|
||||||
|
/* ── Mock browser chrome ─── */
|
||||||
|
.wf{background:#fff;border:2px solid #B8B4AE;border-radius:10px;overflow:hidden;box-shadow:0 4px 18px rgba(0,0,0,.08)}
|
||||||
|
.wf-bar{height:24px;background:#E8E4DF;border-bottom:1px solid #C8C4BE;display:flex;align-items:center;padding:0 9px;gap:4px}
|
||||||
|
.dot{width:7px;height:7px;border-radius:50%}
|
||||||
|
.dot.r{background:#F87171}.dot.y{background:#FCD34D}.dot.g{background:#4ADE80}
|
||||||
|
.urlbar{flex:1;height:11px;background:#D8D4CE;border-radius:3px;margin-left:6px;display:flex;align-items:center;padding:0 5px}
|
||||||
|
.urlbar span{font-size:7.5px;color:#888;font-family:monospace}
|
||||||
|
|
||||||
|
/* ── Nav bar ─── */
|
||||||
|
.N{height:34px;background:#002850;display:flex;align-items:center;padding:0 14px;gap:10px;flex-shrink:0}
|
||||||
|
.N-accent{height:2px;background:#A6DAD8}
|
||||||
|
.logo{font-size:7.5px;font-weight:900;color:#fff;letter-spacing:1px;text-transform:uppercase}
|
||||||
|
.nl{font-size:6.5px;color:rgba(255,255,255,.45);font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:3px 6px}
|
||||||
|
.nl.on{color:#fff}
|
||||||
|
.nr{margin-left:auto;display:flex;gap:5px;align-items:center}
|
||||||
|
.av{width:18px;height:18px;background:#A6DAD8;border-radius:50%;font-size:5.5px;font-weight:900;color:#002850;display:flex;align-items:center;justify-content:center}
|
||||||
|
.nico{width:18px;height:18px;background:rgba(255,255,255,.1);border-radius:3px;display:flex;align-items:center;justify-content:center}
|
||||||
|
|
||||||
|
/* ── Page body ─── */
|
||||||
|
.MAIN{padding:12px 16px;background:#F5F4EF;display:flex;flex-direction:column;gap:8px}
|
||||||
|
|
||||||
|
/* ── Greeting card ─── */
|
||||||
|
.GREET{background:#FDFAF5;border:1px solid #E8E4DC;border-radius:3px;padding:12px 14px}
|
||||||
|
.GREET-time{font-size:6px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#C8B8A0;margin-bottom:4px}
|
||||||
|
.GREET-name{font-family:Georgia,serif;font-size:14px;color:#002850;line-height:1.2}
|
||||||
|
|
||||||
|
/* ── Stats strip ─── */
|
||||||
|
.STATS{display:grid;grid-template-columns:repeat(3,1fr);gap:6px}
|
||||||
|
.STAT{background:#fff;border:1px solid #E0DDD5;border-bottom:2px solid #A6DAD8;border-radius:3px;padding:9px 11px;text-decoration:none;display:block}
|
||||||
|
.STAT-num{font-size:18px;font-weight:900;color:#002850;line-height:1;margin-bottom:3px;font-family:'Helvetica Neue',Arial,sans-serif}
|
||||||
|
.STAT-label{font-size:6px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#B8B4AE}
|
||||||
|
|
||||||
|
/* ── Person chips ─── */
|
||||||
|
.PERSONS-WRAP{display:flex;flex-direction:column;gap:5px}
|
||||||
|
.PERSONS-HEADER{display:flex;align-items:center;justify-content:space-between}
|
||||||
|
.PERSONS-TITLE{font-size:6.5px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#AAA}
|
||||||
|
.PERSONS-ALL{font-size:6.5px;color:#002850;font-weight:600;text-decoration:none}
|
||||||
|
.CHIPS{display:flex;flex-wrap:wrap;gap:5px}
|
||||||
|
.CHIP{display:inline-flex;align-items:center;gap:4px;padding:3px 8px 3px 3px;background:#fff;border:1px solid #E0DDD5;border-radius:20px;font-size:7px;color:#002850;font-weight:600;text-decoration:none}
|
||||||
|
.CHIP-AV{width:16px;height:16px;border-radius:50%;color:#fff;display:flex;align-items:center;justify-content:center;font-size:5px;font-weight:900;flex-shrink:0}
|
||||||
|
.CHIP-COUNT{font-size:6px;color:#AAA;font-weight:400;margin-left:2px}
|
||||||
|
|
||||||
|
/* ── Two-column content row ─── */
|
||||||
|
.CONTENT-ROW{display:grid;grid-template-columns:3fr 2fr;gap:6px}
|
||||||
|
.CARD{background:#fff;border:1px solid #E0DDD5;border-radius:3px;overflow:hidden;display:flex;flex-direction:column}
|
||||||
|
.CARD-HEAD{display:flex;align-items:center;justify-content:space-between;padding:7px 10px 6px;border-bottom:1px solid #E0DDD5}
|
||||||
|
.CARD-HEAD h3{font-size:6.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#999}
|
||||||
|
.CARD-HEAD a{font-size:6.5px;font-weight:600;color:#002850;opacity:.4;text-decoration:none}
|
||||||
|
|
||||||
|
/* ── Doc rows ─── */
|
||||||
|
.DOC-ROW{display:flex;align-items:center;gap:7px;padding:5px 10px;border-bottom:1px solid #F0EDE6}
|
||||||
|
.DOC-ROW:last-child{border-bottom:none}
|
||||||
|
.DOC-THUMB{width:18px;height:18px;background:#F0EDE6;border-radius:2px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
|
||||||
|
.DOC-INFO{flex:1;min-width:0}
|
||||||
|
.DOC-TITLE{font-family:Georgia,serif;font-size:7.5px;color:#002850;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.DOC-META{font-size:6px;color:#AAA;margin-top:1px}
|
||||||
|
.DOC-DATE{font-size:6px;color:#C8C4BE;white-space:nowrap;flex-shrink:0}
|
||||||
|
|
||||||
|
/* ── Story rows ─── */
|
||||||
|
.STORY-ROW{padding:7px 10px;border-bottom:1px solid #F0EDE6}
|
||||||
|
.STORY-ROW:last-child{border-bottom:none}
|
||||||
|
.STORY-TITLE{font-family:Georgia,serif;font-size:8px;color:#002850;font-style:italic;margin-bottom:2px;line-height:1.4}
|
||||||
|
.STORY-META{font-size:6px;color:#B8B4AE}
|
||||||
|
|
||||||
|
/* ── Spec disclaimer ─── */
|
||||||
|
.spec-disclaimer{background:#FFF8E1;border:1.5px solid #FFC107;border-radius:6px;padding:11px 16px;font-size:11px;color:#6D4C00;margin-bottom:32px;line-height:1.6}
|
||||||
|
.spec-disclaimer strong{font-weight:800}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="doc">
|
||||||
|
|
||||||
|
<!-- ══ MASTHEAD ══ -->
|
||||||
|
<div class="mast">
|
||||||
|
<div class="mast-top">
|
||||||
|
<div>
|
||||||
|
<h1>Reader Dashboard — Konzept A · „Herzlich Willkommen"</h1>
|
||||||
|
<p>Warme, einladende Gestaltung mit klarer Hierarchie: Begrüßung → Statistik → Personen → Inhalte. Die Statistikkacheln sind großzügig bemessen und wirken als Einstiegspunkte. Personen erscheinen als kompakte Pills. Geschichten erhalten einen reduzierten, redaktionellen Stil ohne Auszug.</p>
|
||||||
|
</div>
|
||||||
|
<span class="mast-badge">Konzept A · Entwurf</span>
|
||||||
|
</div>
|
||||||
|
<div class="decisions">
|
||||||
|
<div class="dec">
|
||||||
|
<div class="dec-label">Schwerpunkt</div>
|
||||||
|
<div class="dec-value">Begrüßung & große Statistik als Einstieg</div>
|
||||||
|
</div>
|
||||||
|
<div class="dec">
|
||||||
|
<div class="dec-label">Personen-Chips</div>
|
||||||
|
<div class="dec-value">Schlanke Pills mit Initialen-Avatar</div>
|
||||||
|
</div>
|
||||||
|
<div class="dec">
|
||||||
|
<div class="dec-label">Geschichten-Spalte</div>
|
||||||
|
<div class="dec-value">Kursiver Serif-Titel · kein Auszug · schlicht</div>
|
||||||
|
</div>
|
||||||
|
<div class="dec">
|
||||||
|
<div class="dec-label">Spaltenbreite</div>
|
||||||
|
<div class="dec-value">3 : 2 — Dokumente : Geschichten</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spec-disclaimer">
|
||||||
|
<strong>📐 Mockup-Skalierung —</strong> alle Schriftgrößen, Abstände und Höhen im Mockup sind auf ca. 55 % der tatsächlichen Implementierungswerte skaliert. <strong>Werte nicht aus dem Mockup-CSS kopieren.</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══ SECTION 1: DESKTOP, REINER LESER ══ -->
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-h"><span class="sec-num">1</span> Desktop · Leser ohne BLOG_WRITE (READ_ALL only)</div>
|
||||||
|
|
||||||
|
<div class="sg sg-2">
|
||||||
|
<div class="sb">
|
||||||
|
<div class="sl">Vollansicht <span class="sz">≥ 1024 px</span></div>
|
||||||
|
|
||||||
|
<div class="wf">
|
||||||
|
<div class="wf-bar">
|
||||||
|
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
|
||||||
|
<div class="urlbar"><span>familienarchiv.local /</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nav -->
|
||||||
|
<div class="N">
|
||||||
|
<span class="logo">Familienarchiv</span>
|
||||||
|
<span class="nl on">Startseite</span>
|
||||||
|
<span class="nl">Dokumente</span>
|
||||||
|
<span class="nl">Personen</span>
|
||||||
|
<span class="nl">Geschichten</span>
|
||||||
|
<div class="nr">
|
||||||
|
<div class="nico">
|
||||||
|
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,.5)" stroke-width="2"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="av">BK</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="N-accent"></div>
|
||||||
|
|
||||||
|
<div class="MAIN">
|
||||||
|
|
||||||
|
<!-- Zone 1: Greeting -->
|
||||||
|
<div class="GREET">
|
||||||
|
<div class="GREET-time">Guten Abend</div>
|
||||||
|
<div class="GREET-name">Herzlich willkommen, Brigitte.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 2: Stats -->
|
||||||
|
<div class="STATS">
|
||||||
|
<a class="STAT" href="#">
|
||||||
|
<div class="STAT-num">847</div>
|
||||||
|
<div class="STAT-label">Dokumente</div>
|
||||||
|
</a>
|
||||||
|
<a class="STAT" href="#">
|
||||||
|
<div class="STAT-num">94</div>
|
||||||
|
<div class="STAT-label">Personen</div>
|
||||||
|
</a>
|
||||||
|
<a class="STAT" href="#">
|
||||||
|
<div class="STAT-num">12</div>
|
||||||
|
<div class="STAT-label">Geschichten</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 4: Person chips -->
|
||||||
|
<div class="PERSONS-WRAP">
|
||||||
|
<div class="PERSONS-HEADER">
|
||||||
|
<span class="PERSONS-TITLE">Personen im Fokus</span>
|
||||||
|
<a class="PERSONS-ALL" href="#">Alle 94 Personen →</a>
|
||||||
|
</div>
|
||||||
|
<div class="CHIPS">
|
||||||
|
<a class="CHIP" href="#">
|
||||||
|
<div class="CHIP-AV" style="background:#002850">KR</div>
|
||||||
|
Käthe Raddatz<span class="CHIP-COUNT">47 Dok.</span>
|
||||||
|
</a>
|
||||||
|
<a class="CHIP" href="#">
|
||||||
|
<div class="CHIP-AV" style="background:#1A4A6B">ER</div>
|
||||||
|
Ernst Raddatz<span class="CHIP-COUNT">31 Dok.</span>
|
||||||
|
</a>
|
||||||
|
<a class="CHIP" href="#">
|
||||||
|
<div class="CHIP-AV" style="background:#3D5A7A">FM</div>
|
||||||
|
Frieda Müller<span class="CHIP-COUNT">28 Dok.</span>
|
||||||
|
</a>
|
||||||
|
<a class="CHIP" href="#">
|
||||||
|
<div class="CHIP-AV" style="background:#4A7A5A">HW</div>
|
||||||
|
Heinrich Weber<span class="CHIP-COUNT">19 Dok.</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 5: Two-column content -->
|
||||||
|
<div class="CONTENT-ROW">
|
||||||
|
|
||||||
|
<!-- Left: Recent docs -->
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD">
|
||||||
|
<h3>Zuletzt aktualisiert</h3>
|
||||||
|
<a href="#">Alle Dokumente</a>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB">
|
||||||
|
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="#C8C4BE" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-INFO">
|
||||||
|
<div class="DOC-TITLE">Brief von Ernst an Käthe, März 1923</div>
|
||||||
|
<div class="DOC-META">Käthe Raddatz</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-DATE">vor 2 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB">
|
||||||
|
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="#C8C4BE" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-INFO">
|
||||||
|
<div class="DOC-TITLE">Heiratsurkunde Raddatz-Müller, 1898</div>
|
||||||
|
<div class="DOC-META" style="color:#DDD">—</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-DATE">vor 4 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB">
|
||||||
|
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="#C8C4BE" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-INFO">
|
||||||
|
<div class="DOC-TITLE">Familienfoto, Sommer 1928</div>
|
||||||
|
<div class="DOC-META">Ernst Raddatz</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-DATE">vor 1 Woche</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB">
|
||||||
|
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="#C8C4BE" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-INFO">
|
||||||
|
<div class="DOC-TITLE">Taufregister Heinrich Weber, 1902</div>
|
||||||
|
<div class="DOC-META" style="color:#DDD">—</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-DATE">vor 2 Wo.</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB">
|
||||||
|
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="#C8C4BE" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-INFO">
|
||||||
|
<div class="DOC-TITLE">Postkarte aus Berlin, 1930</div>
|
||||||
|
<div class="DOC-META">Frieda Müller</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-DATE">vor 3 Wo.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Stories -->
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD">
|
||||||
|
<h3>Geschichten</h3>
|
||||||
|
<a href="#">Alle Geschichten</a>
|
||||||
|
</div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-TITLE">Die Reise nach Berlin</div>
|
||||||
|
<div class="STORY-META">vor 3 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-TITLE">Sommer 1934 in Köln</div>
|
||||||
|
<div class="STORY-META">vor 2 Wochen</div>
|
||||||
|
</div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-TITLE">Briefe aus dem Krieg</div>
|
||||||
|
<div class="STORY-META">vor 1 Monat</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Annotations -->
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px;padding-top:40px">
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Zone 1 — Begrüßung</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Sand-getönter Hintergrund (#FDFAF5) grenzt die Karte vom Grau der Seite ab</li>
|
||||||
|
<li>Tageszeit-Label (z.B. „Guten Abend") in Capitalcase, 12 px, gedämpftes Beige</li>
|
||||||
|
<li>„Herzlich willkommen, Brigitte." in Georgia Serif, 28 px real (14 px skaliert)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Zone 2 — Statistik</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Jede Kachel ist ein vollflächiges <a>-Element</li>
|
||||||
|
<li>Mintfarbener Bottom-Border (2 px, #A6DAD8) als einziger visueller Akzent</li>
|
||||||
|
<li>Zahl: 48 px bold Navy — wirkt wie ein Einladungsschild</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="note">
|
||||||
|
<strong>Zone 4 — Personen-Chips</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Pill-Form: Initialen-Avatar (16 px) + Name + Dokumentzahl in gedämpftem Grau</li>
|
||||||
|
<li>Avatar-Hintergrundfarbe variiert innerhalb der Navy-Familie</li>
|
||||||
|
<li>Anzahl-Link „Alle 94 Personen →" rechtsbündig zum Section-Titel</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="note">
|
||||||
|
<strong>Zone 5 — Geschichten-Spalte</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Nur kursiver Titel + Datum — bewusst reduziert, literarisch</li>
|
||||||
|
<li>Kein Auszug: ruhigere Gestaltung, weniger Ablenkung</li>
|
||||||
|
<li>Passend für Leser, die Titel als Entscheidungsgrundlage kennen</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="ok">
|
||||||
|
<strong>Stärken dieses Konzepts</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Klar und vertraut — wenig kognitive Last für ältere Nutzer</li>
|
||||||
|
<li>Statistik-Kacheln motivieren zum Erkunden des Archivs</li>
|
||||||
|
<li>Konsistent mit bestehendem Card-Stil des Contributor-Dashboards</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══ SECTION 2: MIT ENTWÜRFE-MODUL ══ -->
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-h"><span class="sec-num">2</span> Variante · BLOG_WRITE — mit Zone 3 „Meine Entwürfe"</div>
|
||||||
|
|
||||||
|
<div class="sg sg-2">
|
||||||
|
<div class="sb">
|
||||||
|
<div class="sl">BLOG_WRITE-Nutzer <span class="sz">READ_ALL + BLOG_WRITE</span></div>
|
||||||
|
|
||||||
|
<div class="wf">
|
||||||
|
<div class="wf-bar">
|
||||||
|
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
|
||||||
|
<div class="urlbar"><span>familienarchiv.local /</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="N">
|
||||||
|
<span class="logo">Familienarchiv</span>
|
||||||
|
<span class="nl on">Startseite</span>
|
||||||
|
<span class="nl">Dokumente</span>
|
||||||
|
<span class="nl">Personen</span>
|
||||||
|
<span class="nl">Geschichten</span>
|
||||||
|
<div class="nr"><div class="av">MR</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="N-accent"></div>
|
||||||
|
<div class="MAIN">
|
||||||
|
|
||||||
|
<!-- Zone 1: Greeting -->
|
||||||
|
<div class="GREET">
|
||||||
|
<div class="GREET-time">Guten Morgen</div>
|
||||||
|
<div class="GREET-name">Herzlich willkommen, Marcel.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 2: Stats -->
|
||||||
|
<div class="STATS">
|
||||||
|
<a class="STAT" href="#"><div class="STAT-num">847</div><div class="STAT-label">Dokumente</div></a>
|
||||||
|
<a class="STAT" href="#"><div class="STAT-num">94</div><div class="STAT-label">Personen</div></a>
|
||||||
|
<a class="STAT" href="#"><div class="STAT-num">12</div><div class="STAT-label">Geschichten</div></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 3: Drafts — conditional -->
|
||||||
|
<div class="CARD" style="border-left:3px solid #A6DAD8">
|
||||||
|
<div class="CARD-HEAD">
|
||||||
|
<h3>Meine Entwürfe</h3>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 10px;border-bottom:1px solid #F0EDE6">
|
||||||
|
<div>
|
||||||
|
<div style="font-family:Georgia,serif;font-size:8px;color:#002850">Onkel Friedrichs Wanderjahre</div>
|
||||||
|
<div style="font-size:6px;color:#AAA;margin-top:1px">Entwurf · zuletzt bearbeitet vor 1 Tag</div>
|
||||||
|
</div>
|
||||||
|
<svg width="7" height="7" viewBox="0 0 24 24" fill="none" stroke="#CCC" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 10px">
|
||||||
|
<div>
|
||||||
|
<div style="font-family:Georgia,serif;font-size:8px;color:#002850">Die Raddatz-Kinder</div>
|
||||||
|
<div style="font-size:6px;color:#AAA;margin-top:1px">Entwurf · zuletzt bearbeitet vor 5 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<svg width="7" height="7" viewBox="0 0 24 24" fill="none" stroke="#CCC" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 4: Person chips -->
|
||||||
|
<div class="PERSONS-WRAP">
|
||||||
|
<div class="PERSONS-HEADER">
|
||||||
|
<span class="PERSONS-TITLE">Personen im Fokus</span>
|
||||||
|
<a class="PERSONS-ALL" href="#">Alle 94 Personen →</a>
|
||||||
|
</div>
|
||||||
|
<div class="CHIPS">
|
||||||
|
<a class="CHIP" href="#"><div class="CHIP-AV" style="background:#002850">KR</div>Käthe Raddatz<span class="CHIP-COUNT">47 Dok.</span></a>
|
||||||
|
<a class="CHIP" href="#"><div class="CHIP-AV" style="background:#1A4A6B">ER</div>Ernst Raddatz<span class="CHIP-COUNT">31 Dok.</span></a>
|
||||||
|
<a class="CHIP" href="#"><div class="CHIP-AV" style="background:#3D5A7A">FM</div>Frieda Müller<span class="CHIP-COUNT">28 Dok.</span></a>
|
||||||
|
<a class="CHIP" href="#"><div class="CHIP-AV" style="background:#4A7A5A">HW</div>Heinrich Weber<span class="CHIP-COUNT">19 Dok.</span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 5 (abbreviated) -->
|
||||||
|
<div class="CONTENT-ROW">
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Zuletzt aktualisiert</h3><a href="#">Alle Dokumente</a></div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="#C8C4BE" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Brief von Ernst an Käthe, März 1923</div><div class="DOC-META">Käthe Raddatz</div></div>
|
||||||
|
<div class="DOC-DATE">vor 2 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="#C8C4BE" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Heiratsurkunde Raddatz-Müller, 1898</div><div class="DOC-META" style="color:#DDD">—</div></div>
|
||||||
|
<div class="DOC-DATE">vor 4 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:5px 10px;font-size:6px;color:#C8C4BE;border-top:1px solid #F0EDE6">+ 3 weitere Dokumente …</div>
|
||||||
|
</div>
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Geschichten</h3><a href="#">Alle</a></div>
|
||||||
|
<div class="STORY-ROW"><div class="STORY-TITLE">Die Reise nach Berlin</div><div class="STORY-META">vor 3 Tagen</div></div>
|
||||||
|
<div class="STORY-ROW"><div class="STORY-TITLE">Sommer 1934 in Köln</div><div class="STORY-META">vor 2 Wochen</div></div>
|
||||||
|
<div class="STORY-ROW"><div class="STORY-TITLE">Briefe aus dem Krieg</div><div class="STORY-META">vor 1 Monat</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px;padding-top:40px">
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Zone 3 — Meine Entwürfe (nur BLOG_WRITE)</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Mintfarbener linker Rand (3 px solid #A6DAD8) hebt die Zone ohne visuellen Lärm hervor</li>
|
||||||
|
<li>Erscheint zwischen Stats (Zone 2) und Personen (Zone 4) — gemäß Issue #447</li>
|
||||||
|
<li>Jeder Entwurf: Serif-Titel + Metazeile → Link auf /geschichten/[id]/edit</li>
|
||||||
|
<li>Leer-Zustand: „Keine Entwürfe" — kein CTA benötigt</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
480
docs/specs/reader-dashboard-b-ueberblick.html
Normal file
480
docs/specs/reader-dashboard-b-ueberblick.html
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Reader Dashboard — Concept B · Überblick · Familienarchiv</title>
|
||||||
|
<style>
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:'Helvetica Neue',Arial,sans-serif;background:#ECEAE4;color:#1A1A1A;line-height:1.5}
|
||||||
|
.doc{max-width:1440px;margin:0 auto;padding:48px 32px}
|
||||||
|
|
||||||
|
/* ── Masthead ─── */
|
||||||
|
.mast{background:#0D2240;border-radius:10px;padding:32px 40px;margin-bottom:48px}
|
||||||
|
.mast-top{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;margin-bottom:16px}
|
||||||
|
.mast h1{font-size:22px;font-weight:900;color:#fff;letter-spacing:-.4px;margin-bottom:6px}
|
||||||
|
.mast p{font-size:12px;color:rgba(255,255,255,.5);max-width:660px;line-height:1.7}
|
||||||
|
.mast-badge{font-size:9px;font-weight:800;padding:3px 9px;border-radius:20px;text-transform:uppercase;letter-spacing:.8px;white-space:nowrap;flex-shrink:0;margin-top:4px;background:#A6DAD8;color:#002850}
|
||||||
|
.decisions{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px;border-top:1px solid rgba(255,255,255,.1);padding-top:16px}
|
||||||
|
.dec{background:rgba(255,255,255,.06);border-radius:6px;padding:10px 12px}
|
||||||
|
.dec-label{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:rgba(255,255,255,.35);margin-bottom:5px}
|
||||||
|
.dec-value{font-size:9.5px;font-weight:700;color:#fff;line-height:1.5}
|
||||||
|
|
||||||
|
/* ── Section headings ─── */
|
||||||
|
.sec{margin-bottom:64px}
|
||||||
|
.sec+.sec{border-top:2px dashed #C8C4BE;padding-top:56px}
|
||||||
|
.sec-h{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#888;margin-bottom:20px;display:flex;align-items:center;gap:10px}
|
||||||
|
.sec-h::after{content:'';flex:1;height:1px;background:#D8D4CE}
|
||||||
|
.sec-num{background:#0D2240;color:#fff;font-size:9px;font-weight:900;padding:2px 7px;border-radius:10px}
|
||||||
|
|
||||||
|
/* ── Screen grid ─── */
|
||||||
|
.sg{display:grid;gap:24px;align-items:start}
|
||||||
|
.sg-2{grid-template-columns:1fr 340px}
|
||||||
|
.sb{display:flex;flex-direction:column;gap:10px}
|
||||||
|
.sl{font-size:9px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;display:flex;align-items:center;gap:6px}
|
||||||
|
.sz{background:#E8E4DF;color:#666;padding:1px 5px;border-radius:3px;font-size:8px}
|
||||||
|
|
||||||
|
/* ── Annotation callouts ─── */
|
||||||
|
.ann-block{background:#FFF7ED;border:1px solid #FDBA74;border-radius:5px;padding:8px 10px;font-size:10px;color:#7C2D12;line-height:1.5}
|
||||||
|
.ann-block strong{font-weight:800}
|
||||||
|
.ann-block ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
.note{background:#F0F9FF;border:1px solid #BAE6FD;border-radius:5px;padding:8px 10px;font-size:10px;color:#0C4A6E;line-height:1.5}
|
||||||
|
.note strong{font-weight:800}
|
||||||
|
.note ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
.ok{background:#F0FDF4;border:1px solid #86EFAC;border-radius:5px;padding:8px 10px;font-size:10px;color:#166534;line-height:1.5}
|
||||||
|
.ok strong{font-weight:800}
|
||||||
|
.ok ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
|
||||||
|
/* ── Mock browser chrome ─── */
|
||||||
|
.wf{background:#fff;border:2px solid #B8B4AE;border-radius:10px;overflow:hidden;box-shadow:0 4px 18px rgba(0,0,0,.08)}
|
||||||
|
.wf-bar{height:24px;background:#E8E4DF;border-bottom:1px solid #C8C4BE;display:flex;align-items:center;padding:0 9px;gap:4px}
|
||||||
|
.dot{width:7px;height:7px;border-radius:50%}
|
||||||
|
.dot.r{background:#F87171}.dot.y{background:#FCD34D}.dot.g{background:#4ADE80}
|
||||||
|
.urlbar{flex:1;height:11px;background:#D8D4CE;border-radius:3px;margin-left:6px;display:flex;align-items:center;padding:0 5px}
|
||||||
|
.urlbar span{font-size:7.5px;color:#888;font-family:monospace}
|
||||||
|
|
||||||
|
/* ── Nav bar ─── */
|
||||||
|
.N{height:34px;background:#002850;display:flex;align-items:center;padding:0 14px;gap:10px;flex-shrink:0}
|
||||||
|
.N-accent{height:2px;background:#A6DAD8}
|
||||||
|
.logo{font-size:7.5px;font-weight:900;color:#fff;letter-spacing:1px;text-transform:uppercase}
|
||||||
|
.nl{font-size:6.5px;color:rgba(255,255,255,.45);font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:3px 6px}
|
||||||
|
.nl.on{color:#fff}
|
||||||
|
.nr{margin-left:auto;display:flex;gap:5px;align-items:center}
|
||||||
|
.av{width:18px;height:18px;background:#A6DAD8;border-radius:50%;font-size:5.5px;font-weight:900;color:#002850;display:flex;align-items:center;justify-content:center}
|
||||||
|
.nico{width:18px;height:18px;background:rgba(255,255,255,.1);border-radius:3px;display:flex;align-items:center;justify-content:center}
|
||||||
|
|
||||||
|
/* ── Page body ─── */
|
||||||
|
.MAIN{padding:12px 16px;background:#F5F4EF;display:flex;flex-direction:column;gap:8px}
|
||||||
|
|
||||||
|
/* ── Combined greeting + stats bar ─── */
|
||||||
|
.HEADER-BAR{background:#fff;border:1px solid #E0DDD5;border-radius:3px;padding:10px 14px;display:flex;align-items:center;justify-content:space-between;gap:16px}
|
||||||
|
.HEADER-LEFT{}
|
||||||
|
.HEADER-TIME{font-size:6px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#C8B8A0;margin-bottom:2px}
|
||||||
|
.HEADER-NAME{font-family:Georgia,serif;font-size:12px;color:#002850}
|
||||||
|
.HEADER-STATS{display:flex;gap:0;border-left:1px solid #F0EDE6;padding-left:14px}
|
||||||
|
.HSTAT{text-align:center;padding:0 12px;border-right:1px solid #F0EDE6}
|
||||||
|
.HSTAT:last-child{border-right:none;padding-right:0}
|
||||||
|
.HSTAT a{text-decoration:none;display:block}
|
||||||
|
.HSTAT-NUM{font-size:14px;font-weight:900;color:#002850;line-height:1;display:block}
|
||||||
|
.HSTAT-LABEL{font-size:5.5px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#B8B4AE;display:block;margin-top:2px}
|
||||||
|
.HSTAT a:hover .HSTAT-NUM{color:#A6DAD8}
|
||||||
|
|
||||||
|
/* ── Person cards (larger than pills) ─── */
|
||||||
|
.PERSON-CARDS{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
|
||||||
|
.PCARD{background:#fff;border:1px solid #E0DDD5;border-radius:3px;padding:8px 10px;text-decoration:none;display:flex;flex-direction:column;align-items:center;text-align:center;gap:4px}
|
||||||
|
.PCARD-AV{width:28px;height:28px;border-radius:50%;color:#fff;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:900;flex-shrink:0}
|
||||||
|
.PCARD-NAME{font-size:7px;font-weight:700;color:#002850;line-height:1.3}
|
||||||
|
.PCARD-COUNT{font-size:6px;color:#A6DAD8;font-weight:800;background:#E8F4F4;padding:1px 6px;border-radius:10px}
|
||||||
|
|
||||||
|
.PERSONS-FOOTER{display:flex;justify-content:flex-end;margin-top:2px}
|
||||||
|
.PERSONS-ALL-LINK{font-size:6.5px;color:#002850;font-weight:600;text-decoration:none;opacity:.6}
|
||||||
|
|
||||||
|
/* ── Two-column content row ─── */
|
||||||
|
.CONTENT-ROW{display:grid;grid-template-columns:1fr 1fr;gap:6px}
|
||||||
|
.CARD{background:#fff;border:1px solid #E0DDD5;border-radius:3px;overflow:hidden;display:flex;flex-direction:column}
|
||||||
|
.CARD-HEAD{display:flex;align-items:center;justify-content:space-between;padding:7px 10px 6px;border-bottom:1px solid #E0DDD5}
|
||||||
|
.CARD-HEAD h3{font-size:6.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#999}
|
||||||
|
.CARD-HEAD a{font-size:6.5px;font-weight:600;color:#002850;opacity:.4;text-decoration:none}
|
||||||
|
|
||||||
|
/* ── Doc rows with thumbnail ─── */
|
||||||
|
.DOC-ROW{display:flex;align-items:center;gap:7px;padding:5px 10px;border-bottom:1px solid #F0EDE6}
|
||||||
|
.DOC-ROW:last-child{border-bottom:none}
|
||||||
|
.DOC-THUMB{width:20px;height:24px;background:#ECEAE4;border-radius:2px;flex-shrink:0;display:flex;align-items:center;justify-content:center;border:1px solid #E0DDD5}
|
||||||
|
.DOC-INFO{flex:1;min-width:0}
|
||||||
|
.DOC-TITLE{font-family:Georgia,serif;font-size:7.5px;color:#002850;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.DOC-SENDER{font-size:6px;color:#888;margin-top:1px}
|
||||||
|
.DOC-SENDER a{color:#002850;text-decoration:none;opacity:.7}
|
||||||
|
.DOC-DATE{font-size:6px;color:#C8C4BE;white-space:nowrap;flex-shrink:0}
|
||||||
|
|
||||||
|
/* ── Story rows with excerpt ─── */
|
||||||
|
.STORY-ROW{padding:7px 10px;border-bottom:1px solid #F0EDE6}
|
||||||
|
.STORY-ROW:last-child{border-bottom:none}
|
||||||
|
.STORY-TITLE{font-family:Georgia,serif;font-size:8px;color:#002850;font-style:italic;margin-bottom:3px;line-height:1.3}
|
||||||
|
.STORY-EXCERPT{font-size:6.5px;color:#888;line-height:1.5;margin-bottom:3px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||||||
|
.STORY-META{font-size:6px;color:#B8B4AE}
|
||||||
|
|
||||||
|
/* ── Spec disclaimer ─── */
|
||||||
|
.spec-disclaimer{background:#FFF8E1;border:1.5px solid #FFC107;border-radius:6px;padding:11px 16px;font-size:11px;color:#6D4C00;margin-bottom:32px;line-height:1.6}
|
||||||
|
.spec-disclaimer strong{font-weight:800}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="doc">
|
||||||
|
|
||||||
|
<!-- ══ MASTHEAD ══ -->
|
||||||
|
<div class="mast">
|
||||||
|
<div class="mast-top">
|
||||||
|
<div>
|
||||||
|
<h1>Reader Dashboard — Konzept B · „Überblick"</h1>
|
||||||
|
<p>Kompakte, strukturierte Gestaltung. Begrüßung und Statistik werden in einem gemeinsamen Querbalken kombiniert — spart vertikalen Platz. Personen erscheinen als quadratische Portrait-Karten mit großem Avatar. Geschichten zeigen einen kurzen Auszug für schnelle Orientierung.</p>
|
||||||
|
</div>
|
||||||
|
<span class="mast-badge">Konzept B · Entwurf</span>
|
||||||
|
</div>
|
||||||
|
<div class="decisions">
|
||||||
|
<div class="dec">
|
||||||
|
<div class="dec-label">Schwerpunkt</div>
|
||||||
|
<div class="dec-value">Effizienz — alles auf einen Blick</div>
|
||||||
|
</div>
|
||||||
|
<div class="dec">
|
||||||
|
<div class="dec-label">Header</div>
|
||||||
|
<div class="dec-value">Gruß + Statistik in einem Querbalken</div>
|
||||||
|
</div>
|
||||||
|
<div class="dec">
|
||||||
|
<div class="dec-label">Personen</div>
|
||||||
|
<div class="dec-value">Portrait-Kacheln mit großem Avatar</div>
|
||||||
|
</div>
|
||||||
|
<div class="dec">
|
||||||
|
<div class="dec-label">Geschichten</div>
|
||||||
|
<div class="dec-value">Titel + Auszug (~2 Zeilen) + Datum</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spec-disclaimer">
|
||||||
|
<strong>📐 Mockup-Skalierung —</strong> alle Schriftgrößen, Abstände und Höhen im Mockup sind auf ca. 55 % der tatsächlichen Implementierungswerte skaliert. <strong>Werte nicht aus dem Mockup-CSS kopieren.</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══ SECTION 1: DESKTOP, REINER LESER ══ -->
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-h"><span class="sec-num">1</span> Desktop · Leser ohne BLOG_WRITE (READ_ALL only)</div>
|
||||||
|
|
||||||
|
<div class="sg sg-2">
|
||||||
|
<div class="sb">
|
||||||
|
<div class="sl">Vollansicht <span class="sz">≥ 1024 px</span></div>
|
||||||
|
|
||||||
|
<div class="wf">
|
||||||
|
<div class="wf-bar">
|
||||||
|
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
|
||||||
|
<div class="urlbar"><span>familienarchiv.local /</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nav -->
|
||||||
|
<div class="N">
|
||||||
|
<span class="logo">Familienarchiv</span>
|
||||||
|
<span class="nl on">Startseite</span>
|
||||||
|
<span class="nl">Dokumente</span>
|
||||||
|
<span class="nl">Personen</span>
|
||||||
|
<span class="nl">Geschichten</span>
|
||||||
|
<div class="nr">
|
||||||
|
<div class="nico">
|
||||||
|
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,.5)" stroke-width="2"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="av">BK</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="N-accent"></div>
|
||||||
|
|
||||||
|
<div class="MAIN">
|
||||||
|
|
||||||
|
<!-- Zone 1 + 2: Combined header bar -->
|
||||||
|
<div class="HEADER-BAR">
|
||||||
|
<div class="HEADER-LEFT">
|
||||||
|
<div class="HEADER-TIME">Guten Abend</div>
|
||||||
|
<div class="HEADER-NAME">Herzlich willkommen, Brigitte.</div>
|
||||||
|
</div>
|
||||||
|
<div class="HEADER-STATS">
|
||||||
|
<div class="HSTAT">
|
||||||
|
<a href="#">
|
||||||
|
<span class="HSTAT-NUM">847</span>
|
||||||
|
<span class="HSTAT-LABEL">Dokumente</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="HSTAT">
|
||||||
|
<a href="#">
|
||||||
|
<span class="HSTAT-NUM">94</span>
|
||||||
|
<span class="HSTAT-LABEL">Personen</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="HSTAT">
|
||||||
|
<a href="#">
|
||||||
|
<span class="HSTAT-NUM">12</span>
|
||||||
|
<span class="HSTAT-LABEL">Geschichten</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 4: Person cards -->
|
||||||
|
<div>
|
||||||
|
<div class="PERSON-CARDS">
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-AV" style="background:#002850">KR</div>
|
||||||
|
<div class="PCARD-NAME">Käthe Raddatz</div>
|
||||||
|
<div class="PCARD-COUNT">47 Dok.</div>
|
||||||
|
</a>
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-AV" style="background:#1A4A6B">ER</div>
|
||||||
|
<div class="PCARD-NAME">Ernst Raddatz</div>
|
||||||
|
<div class="PCARD-COUNT">31 Dok.</div>
|
||||||
|
</a>
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-AV" style="background:#3D5A7A">FM</div>
|
||||||
|
<div class="PCARD-NAME">Frieda Müller</div>
|
||||||
|
<div class="PCARD-COUNT">28 Dok.</div>
|
||||||
|
</a>
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-AV" style="background:#4A7A5A">HW</div>
|
||||||
|
<div class="PCARD-NAME">Heinrich Weber</div>
|
||||||
|
<div class="PCARD-COUNT">19 Dok.</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="PERSONS-FOOTER">
|
||||||
|
<a class="PERSONS-ALL-LINK" href="#">Alle 94 Personen →</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 5: Two-column (equal split) -->
|
||||||
|
<div class="CONTENT-ROW">
|
||||||
|
|
||||||
|
<!-- Left: Recent docs with thumbnail + sender -->
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD">
|
||||||
|
<h3>Zuletzt aktualisiert</h3>
|
||||||
|
<a href="#">Alle Dokumente</a>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB">
|
||||||
|
<svg width="8" height="10" viewBox="0 0 24 30" fill="none" stroke="#C8C4BE" stroke-width="2"><path d="M4 2h10l6 6v20H4V2z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-INFO">
|
||||||
|
<div class="DOC-TITLE">Brief von Ernst an Käthe, März 1923</div>
|
||||||
|
<div class="DOC-SENDER">von <a href="#">Käthe Raddatz</a></div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-DATE">vor 2 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB">
|
||||||
|
<svg width="8" height="10" viewBox="0 0 24 30" fill="none" stroke="#C8C4BE" stroke-width="2"><path d="M4 2h10l6 6v20H4V2z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-INFO">
|
||||||
|
<div class="DOC-TITLE">Heiratsurkunde Raddatz-Müller, 1898</div>
|
||||||
|
<div class="DOC-SENDER" style="color:#DDD">—</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-DATE">vor 4 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#A6DAD8" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-INFO">
|
||||||
|
<div class="DOC-TITLE">Familienfoto, Sommer 1928</div>
|
||||||
|
<div class="DOC-SENDER">von <a href="#">Ernst Raddatz</a></div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-DATE">vor 1 Woche</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB">
|
||||||
|
<svg width="8" height="10" viewBox="0 0 24 30" fill="none" stroke="#C8C4BE" stroke-width="2"><path d="M4 2h10l6 6v20H4V2z"/><polyline points="14 2 14 8 20 8"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-INFO">
|
||||||
|
<div class="DOC-TITLE">Taufregister Heinrich Weber, 1902</div>
|
||||||
|
<div class="DOC-SENDER" style="color:#DDD">—</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-DATE">vor 2 Wo.</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB">
|
||||||
|
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#A6DAD8" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-INFO">
|
||||||
|
<div class="DOC-TITLE">Postkarte aus Berlin, 1930</div>
|
||||||
|
<div class="DOC-SENDER">von <a href="#">Frieda Müller</a></div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-DATE">vor 3 Wo.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Stories with excerpt -->
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD">
|
||||||
|
<h3>Geschichten</h3>
|
||||||
|
<a href="#">Alle Geschichten</a>
|
||||||
|
</div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-TITLE">Die Reise nach Berlin</div>
|
||||||
|
<div class="STORY-EXCERPT">Im Frühjahr 1921 bestieg Ernst Raddatz erstmals den Zug nach Berlin, um Arbeit in der aufstrebenden Metropole zu finden …</div>
|
||||||
|
<div class="STORY-META">vor 3 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-TITLE">Sommer 1934 in Köln</div>
|
||||||
|
<div class="STORY-EXCERPT">Die Sommerferien 1934 verbrachte die Familie Raddatz bei Verwandten in Köln — die letzte unbeschwerte Reise vor dem Krieg …</div>
|
||||||
|
<div class="STORY-META">vor 2 Wochen</div>
|
||||||
|
</div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-TITLE">Briefe aus dem Krieg</div>
|
||||||
|
<div class="STORY-EXCERPT">Zwischen 1914 und 1918 schrieb Ernst Raddatz insgesamt 47 Briefe aus dem Feld an seine Frau Käthe …</div>
|
||||||
|
<div class="STORY-META">vor 1 Monat</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Annotations -->
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px;padding-top:40px">
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Zone 1 + 2 — Kombinierter Header-Balken</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Begrüßung links, drei Statistiken rechts — alles in einer Karte</li>
|
||||||
|
<li>Statistiken durch vertikale Trennlinien gegliedert, jede als <a>-Link</li>
|
||||||
|
<li>Spart vertikalen Platz: weniger Scrollen für Nutzer auf kleineren Bildschirmen</li>
|
||||||
|
<li>Stats-Zahlen: 24 px (real) — kleiner als Konzept A, aber immer noch gut lesbar</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Zone 4 — Portrait-Kacheln</strong>
|
||||||
|
<ul>
|
||||||
|
<li>4-spaltig gleichbreit; Avatar 28 px (skaliert), Name + Zahl darunter</li>
|
||||||
|
<li>Mint-farbene Zahl-Badge (#E8F4F4 / #A6DAD8) macht Dokumentanzahl prominent</li>
|
||||||
|
<li>Kacheln sind vollflächige <a>-Links — barrierefrei</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="note">
|
||||||
|
<strong>Zone 5 — Gleiches Spaltenbreite 1:1</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Beide Spalten erhalten gleich viel Platz — Parität zwischen Dokumente und Geschichten</li>
|
||||||
|
<li>Dokumente: Thumbnail-Platzhalter + Absender-Link in zweiter Zeile</li>
|
||||||
|
<li>Geschichten: Auszug mit 2-Zeilen-Clamp — gibt Lesern mehr Kontext zum Klicken</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="ok">
|
||||||
|
<strong>Stärken dieses Konzepts</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Kompakteste der drei Varianten — wenig Scrollbedarf</li>
|
||||||
|
<li>Portrait-Kacheln helfen, Personen visuell zu „erkennen" (Avatar wird vertraut)</li>
|
||||||
|
<li>Auszüge in der Geschichten-Spalte erhöhen die Klickwahrscheinlichkeit</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══ SECTION 2: BLOG_WRITE VARIANT ══ -->
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-h"><span class="sec-num">2</span> Variante · BLOG_WRITE — mit Zone 3 „Meine Entwürfe"</div>
|
||||||
|
|
||||||
|
<div class="sg sg-2">
|
||||||
|
<div class="sb">
|
||||||
|
<div class="sl">BLOG_WRITE-Nutzer <span class="sz">READ_ALL + BLOG_WRITE</span></div>
|
||||||
|
|
||||||
|
<div class="wf">
|
||||||
|
<div class="wf-bar">
|
||||||
|
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
|
||||||
|
<div class="urlbar"><span>familienarchiv.local /</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="N">
|
||||||
|
<span class="logo">Familienarchiv</span>
|
||||||
|
<span class="nl on">Startseite</span>
|
||||||
|
<span class="nl">Dokumente</span>
|
||||||
|
<span class="nl">Personen</span>
|
||||||
|
<span class="nl">Geschichten</span>
|
||||||
|
<div class="nr"><div class="av">MR</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="N-accent"></div>
|
||||||
|
<div class="MAIN">
|
||||||
|
|
||||||
|
<!-- Zone 1+2: Combined bar -->
|
||||||
|
<div class="HEADER-BAR">
|
||||||
|
<div class="HEADER-LEFT">
|
||||||
|
<div class="HEADER-TIME">Guten Morgen</div>
|
||||||
|
<div class="HEADER-NAME">Herzlich willkommen, Marcel.</div>
|
||||||
|
</div>
|
||||||
|
<div class="HEADER-STATS">
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">847</span><span class="HSTAT-LABEL">Dokumente</span></a></div>
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">94</span><span class="HSTAT-LABEL">Personen</span></a></div>
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">12</span><span class="HSTAT-LABEL">Geschichten</span></a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 3: Drafts -->
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD" style="border-left:3px solid #A6DAD8;padding-left:8px">
|
||||||
|
<h3>Meine Entwürfe</h3>
|
||||||
|
<a href="#" style="font-size:6px;color:#002850;opacity:.5;text-decoration:none">→ Alle Geschichten</a>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid #F0EDE6">
|
||||||
|
<div style="width:4px;height:4px;border-radius:50%;background:#A6DAD8;flex-shrink:0;margin-left:2px"></div>
|
||||||
|
<div style="flex:1;min-width:0">
|
||||||
|
<div style="font-family:Georgia,serif;font-size:8px;color:#002850">Onkel Friedrichs Wanderjahre</div>
|
||||||
|
<div style="font-size:6px;color:#AAA;margin-top:1px">Entwurf · zuletzt bearbeitet vor 1 Tag</div>
|
||||||
|
</div>
|
||||||
|
<svg width="7" height="7" viewBox="0 0 24 24" fill="none" stroke="#CCC" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px;padding:6px 10px">
|
||||||
|
<div style="width:4px;height:4px;border-radius:50%;background:#A6DAD8;flex-shrink:0;margin-left:2px"></div>
|
||||||
|
<div style="flex:1;min-width:0">
|
||||||
|
<div style="font-family:Georgia,serif;font-size:8px;color:#002850">Die Raddatz-Kinder</div>
|
||||||
|
<div style="font-size:6px;color:#AAA;margin-top:1px">Entwurf · zuletzt bearbeitet vor 5 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<svg width="7" height="7" viewBox="0 0 24 24" fill="none" stroke="#CCC" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 4 -->
|
||||||
|
<div>
|
||||||
|
<div class="PERSON-CARDS">
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-AV" style="background:#002850">KR</div><div class="PCARD-NAME">Käthe Raddatz</div><div class="PCARD-COUNT">47 Dok.</div></a>
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-AV" style="background:#1A4A6B">ER</div><div class="PCARD-NAME">Ernst Raddatz</div><div class="PCARD-COUNT">31 Dok.</div></a>
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-AV" style="background:#3D5A7A">FM</div><div class="PCARD-NAME">Frieda Müller</div><div class="PCARD-COUNT">28 Dok.</div></a>
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-AV" style="background:#4A7A5A">HW</div><div class="PCARD-NAME">Heinrich Weber</div><div class="PCARD-COUNT">19 Dok.</div></a>
|
||||||
|
</div>
|
||||||
|
<div class="PERSONS-FOOTER"><a class="PERSONS-ALL-LINK" href="#">Alle 94 Personen →</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 5 (abbreviated) -->
|
||||||
|
<div class="CONTENT-ROW">
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Zuletzt aktualisiert</h3><a href="#">Alle Dokumente</a></div>
|
||||||
|
<div class="DOC-ROW"><div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 24 30" fill="none" stroke="#C8C4BE" stroke-width="2"><path d="M4 2h10l6 6v20H4V2z"/><polyline points="14 2 14 8 20 8"/></svg></div><div class="DOC-INFO"><div class="DOC-TITLE">Brief von Ernst an Käthe, März 1923</div><div class="DOC-SENDER">von <a href="#">Käthe Raddatz</a></div></div><div class="DOC-DATE">vor 2 Tagen</div></div>
|
||||||
|
<div class="DOC-ROW"><div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 24 30" fill="none" stroke="#C8C4BE" stroke-width="2"><path d="M4 2h10l6 6v20H4V2z"/><polyline points="14 2 14 8 20 8"/></svg></div><div class="DOC-INFO"><div class="DOC-TITLE">Heiratsurkunde Raddatz-Müller, 1898</div><div class="DOC-SENDER" style="color:#DDD">—</div></div><div class="DOC-DATE">vor 4 Tagen</div></div>
|
||||||
|
<div style="padding:5px 10px;font-size:6px;color:#C8C4BE;border-top:1px solid #F0EDE6">+ 3 weitere Dokumente …</div>
|
||||||
|
</div>
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Geschichten</h3><a href="#">Alle</a></div>
|
||||||
|
<div class="STORY-ROW"><div class="STORY-TITLE">Die Reise nach Berlin</div><div class="STORY-EXCERPT">Im Frühjahr 1921 bestieg Ernst Raddatz erstmals den Zug nach Berlin …</div><div class="STORY-META">vor 3 Tagen</div></div>
|
||||||
|
<div class="STORY-ROW"><div class="STORY-TITLE">Sommer 1934 in Köln</div><div class="STORY-EXCERPT">Die Sommerferien 1934 verbrachte die Familie Raddatz bei Verwandten in Köln …</div><div class="STORY-META">vor 2 Wochen</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px;padding-top:40px">
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Zone 3 — Entwürfe mit Mint-Dot-Indikator</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Mint-Punkt vor jedem Entwurf signalisiert „unveröffentlicht" visuell</li>
|
||||||
|
<li>Mint-Randakzent links (3 px) ohne Störung des Card-Rahmens</li>
|
||||||
|
<li>Kompakter als Konzept A — fügt sich harmonisch zwischen Header und Personen ein</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
385
docs/specs/reader-dashboard-b1-hell.html
Normal file
385
docs/specs/reader-dashboard-b1-hell.html
Normal file
@@ -0,0 +1,385 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Reader Dashboard — B.1 · Hell & Klar · Familienarchiv</title>
|
||||||
|
<style>
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:'Helvetica Neue',Arial,sans-serif;background:#ECEAE4;color:#1A1A1A;line-height:1.5}
|
||||||
|
.doc{max-width:1440px;margin:0 auto;padding:48px 32px}
|
||||||
|
|
||||||
|
/* ── Masthead ─── */
|
||||||
|
.mast{background:#0D2240;border-radius:10px;padding:32px 40px;margin-bottom:48px}
|
||||||
|
.mast-top{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;margin-bottom:16px}
|
||||||
|
.mast h1{font-size:22px;font-weight:900;color:#fff;letter-spacing:-.4px;margin-bottom:6px}
|
||||||
|
.mast p{font-size:12px;color:rgba(255,255,255,.5);max-width:660px;line-height:1.7}
|
||||||
|
.mast-badge{font-size:9px;font-weight:800;padding:3px 9px;border-radius:20px;text-transform:uppercase;letter-spacing:.8px;white-space:nowrap;flex-shrink:0;margin-top:4px;background:#A6DAD8;color:#002850}
|
||||||
|
.decisions{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:20px;border-top:1px solid rgba(255,255,255,.1);padding-top:16px}
|
||||||
|
.dec{background:rgba(255,255,255,.06);border-radius:6px;padding:10px 12px}
|
||||||
|
.dec-label{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:rgba(255,255,255,.35);margin-bottom:5px}
|
||||||
|
.dec-value{font-size:9.5px;font-weight:700;color:#fff;line-height:1.5}
|
||||||
|
|
||||||
|
/* ── Section headings ─── */
|
||||||
|
.sec{margin-bottom:64px}
|
||||||
|
.sec+.sec{border-top:2px dashed #C8C4BE;padding-top:56px}
|
||||||
|
.sec-h{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#888;margin-bottom:20px;display:flex;align-items:center;gap:10px}
|
||||||
|
.sec-h::after{content:'';flex:1;height:1px;background:#D8D4CE}
|
||||||
|
.sec-num{background:#0D2240;color:#fff;font-size:9px;font-weight:900;padding:2px 7px;border-radius:10px}
|
||||||
|
|
||||||
|
/* ── Screen grid ─── */
|
||||||
|
.sg{display:grid;gap:24px;align-items:start}
|
||||||
|
.sg-2{grid-template-columns:1fr 340px}
|
||||||
|
.sb{display:flex;flex-direction:column;gap:10px}
|
||||||
|
.sl{font-size:9px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;display:flex;align-items:center;gap:6px}
|
||||||
|
.sz{background:#E8E4DF;color:#666;padding:1px 5px;border-radius:3px;font-size:8px}
|
||||||
|
|
||||||
|
/* ── Annotation callouts ─── */
|
||||||
|
.ann-block{background:#FFF7ED;border:1px solid #FDBA74;border-radius:5px;padding:8px 10px;font-size:10px;color:#7C2D12;line-height:1.5}
|
||||||
|
.ann-block strong{font-weight:800}
|
||||||
|
.ann-block ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
.note{background:#F0F9FF;border:1px solid #BAE6FD;border-radius:5px;padding:8px 10px;font-size:10px;color:#0C4A6E;line-height:1.5}
|
||||||
|
.note strong{font-weight:800}
|
||||||
|
.note ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
.ok{background:#F0FDF4;border:1px solid #86EFAC;border-radius:5px;padding:8px 10px;font-size:10px;color:#166534;line-height:1.5}
|
||||||
|
.ok strong{font-weight:800}
|
||||||
|
.ok ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
|
||||||
|
/* ── Mock browser chrome ─── */
|
||||||
|
.wf{background:#fff;border:2px solid #B8B4AE;border-radius:10px;overflow:hidden;box-shadow:0 4px 18px rgba(0,0,0,.08)}
|
||||||
|
.wf-bar{height:24px;background:#E8E4DF;border-bottom:1px solid #C8C4BE;display:flex;align-items:center;padding:0 9px;gap:4px}
|
||||||
|
.dot{width:7px;height:7px;border-radius:50%}
|
||||||
|
.dot.r{background:#F87171}.dot.y{background:#FCD34D}.dot.g{background:#4ADE80}
|
||||||
|
.urlbar{flex:1;height:11px;background:#D8D4CE;border-radius:3px;margin-left:6px;display:flex;align-items:center;padding:0 5px}
|
||||||
|
.urlbar span{font-size:7.5px;color:#888;font-family:monospace}
|
||||||
|
|
||||||
|
/* ── Nav bar ─── */
|
||||||
|
.N{height:34px;background:#002850;display:flex;align-items:center;padding:0 14px;gap:10px}
|
||||||
|
.N-accent{height:2px;background:#A6DAD8}
|
||||||
|
.logo{font-size:7.5px;font-weight:900;color:#fff;letter-spacing:1px;text-transform:uppercase}
|
||||||
|
.nl{font-size:6.5px;color:rgba(255,255,255,.45);font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:3px 6px}
|
||||||
|
.nl.on{color:#fff}
|
||||||
|
.nr{margin-left:auto;display:flex;gap:5px;align-items:center}
|
||||||
|
.av{width:18px;height:18px;background:#A6DAD8;border-radius:50%;font-size:5.5px;font-weight:900;color:#002850;display:flex;align-items:center;justify-content:center}
|
||||||
|
.nico{width:18px;height:18px;background:rgba(255,255,255,.1);border-radius:3px;display:flex;align-items:center;justify-content:center}
|
||||||
|
|
||||||
|
/* ── Page body ─── */
|
||||||
|
.MAIN{padding:12px 16px;background:#F5F4EF;display:flex;flex-direction:column;gap:8px}
|
||||||
|
|
||||||
|
/* ════════════════════════════════════
|
||||||
|
B.1 — HELL & KLAR
|
||||||
|
Header: pure white, divider line between greeting and stats
|
||||||
|
Person cards: round avatar, mint underline, white bg
|
||||||
|
Stories: clean 2-line excerpt
|
||||||
|
Columns: 1:1
|
||||||
|
════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* Combined header bar — white, clean divider */
|
||||||
|
.HEADER-BAR{background:#fff;border:1px solid #E0DDD5;border-radius:3px;padding:10px 14px;display:flex;align-items:center;gap:16px}
|
||||||
|
.HEADER-LEFT{flex:1;min-width:0}
|
||||||
|
.HEADER-TIME{font-size:6px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#C8B8A0;margin-bottom:2px}
|
||||||
|
.HEADER-NAME{font-family:Georgia,serif;font-size:12px;color:#002850}
|
||||||
|
.DIVIDER{width:1px;background:#E8E4DF;align-self:stretch;flex-shrink:0}
|
||||||
|
.HEADER-STATS{display:flex;align-items:center;gap:0}
|
||||||
|
.HSTAT{text-align:center;padding:0 12px;border-right:1px solid #F0EDE6}
|
||||||
|
.HSTAT:last-child{border-right:none;padding-right:0}
|
||||||
|
.HSTAT a{text-decoration:none;display:block}
|
||||||
|
.HSTAT-NUM{font-size:14px;font-weight:900;color:#002850;line-height:1;display:block}
|
||||||
|
.HSTAT-LABEL{font-size:5.5px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#B8B4AE;display:block;margin-top:2px}
|
||||||
|
|
||||||
|
/* Person portrait cards — round avatar, mint bottom-border on card */
|
||||||
|
.PERSON-GRID{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
|
||||||
|
.PCARD{background:#fff;border:1px solid #E0DDD5;border-bottom:2px solid #A6DAD8;border-radius:3px;padding:9px 10px 8px;text-decoration:none;display:flex;flex-direction:column;align-items:center;text-align:center;gap:5px}
|
||||||
|
.PCARD-AV{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:900;color:#fff;flex-shrink:0}
|
||||||
|
.PCARD-NAME{font-size:7px;font-weight:700;color:#002850;line-height:1.3}
|
||||||
|
.PCARD-COUNT{font-size:6px;color:#888;font-weight:400}
|
||||||
|
.PERSONS-FOOTER{text-align:right;margin-top:3px}
|
||||||
|
.PERSONS-ALL{font-size:6.5px;color:#002850;font-weight:600;text-decoration:none;opacity:.55}
|
||||||
|
|
||||||
|
/* Two-column 1:1 */
|
||||||
|
.CONTENT-ROW{display:grid;grid-template-columns:1fr 1fr;gap:6px}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.CARD{background:#fff;border:1px solid #E0DDD5;border-radius:3px;overflow:hidden;display:flex;flex-direction:column}
|
||||||
|
.CARD-HEAD{display:flex;align-items:center;justify-content:space-between;padding:7px 10px 6px;border-bottom:1px solid #E0DDD5}
|
||||||
|
.CARD-HEAD h3{font-size:6.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#999}
|
||||||
|
.CARD-HEAD a{font-size:6.5px;font-weight:600;color:#002850;opacity:.4;text-decoration:none}
|
||||||
|
|
||||||
|
/* Doc rows */
|
||||||
|
.DOC-ROW{display:flex;align-items:center;gap:7px;padding:5px 10px;border-bottom:1px solid #F0EDE6}
|
||||||
|
.DOC-ROW:last-child{border-bottom:none}
|
||||||
|
.DOC-THUMB{width:20px;height:24px;background:#ECEAE4;border:1px solid #E0DDD5;border-radius:2px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
|
||||||
|
.DOC-INFO{flex:1;min-width:0}
|
||||||
|
.DOC-TITLE{font-family:Georgia,serif;font-size:7.5px;color:#002850;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.DOC-SENDER{font-size:6px;color:#888;margin-top:1px}
|
||||||
|
.DOC-SENDER a{color:#002850;text-decoration:none;opacity:.7}
|
||||||
|
.DOC-DATE{font-size:6px;color:#C8C4BE;white-space:nowrap;flex-shrink:0}
|
||||||
|
|
||||||
|
/* Story rows — clean 2-line excerpt */
|
||||||
|
.STORY-ROW{padding:7px 10px;border-bottom:1px solid #F0EDE6}
|
||||||
|
.STORY-ROW:last-child{border-bottom:none}
|
||||||
|
.STORY-TITLE{font-family:Georgia,serif;font-size:8px;color:#002850;font-style:italic;margin-bottom:3px;line-height:1.3}
|
||||||
|
.STORY-EXCERPT{font-size:6.5px;color:#888;line-height:1.5;margin-bottom:3px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||||||
|
.STORY-META{font-size:6px;color:#B8B4AE}
|
||||||
|
|
||||||
|
/* Drafts card */
|
||||||
|
.DRAFTS-CARD{background:#fff;border:1px solid #E0DDD5;border-left:3px solid #A6DAD8;border-radius:3px;overflow:hidden}
|
||||||
|
.DRAFTS-HEAD{padding:6px 10px 5px;border-bottom:1px solid #F0EDE6;display:flex;align-items:center;justify-content:space-between}
|
||||||
|
.DRAFTS-HEAD h3{font-size:6.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#999}
|
||||||
|
.DRAFT-ROW{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;border-bottom:1px solid #F0EDE6}
|
||||||
|
.DRAFT-ROW:last-child{border-bottom:none}
|
||||||
|
.DRAFT-TITLE{font-family:Georgia,serif;font-size:8px;color:#002850}
|
||||||
|
.DRAFT-META{font-size:6px;color:#AAA;margin-top:1px}
|
||||||
|
|
||||||
|
/* Spec disclaimer */
|
||||||
|
.spec-disclaimer{background:#FFF8E1;border:1.5px solid #FFC107;border-radius:6px;padding:11px 16px;font-size:11px;color:#6D4C00;margin-bottom:32px;line-height:1.6}
|
||||||
|
.spec-disclaimer strong{font-weight:800}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="doc">
|
||||||
|
|
||||||
|
<div class="mast">
|
||||||
|
<div class="mast-top">
|
||||||
|
<div>
|
||||||
|
<h1>Reader Dashboard — B.1 · „Hell & Klar"</h1>
|
||||||
|
<p>Die reinste Iteration von Konzept B. Weißer Header-Balken mit sauberer vertikaler Trennlinie zwischen Gruß und Statistik. Personen-Kacheln mit mintfarbener Bottom-Border als einzigem Akzent. Gleiches 1:1-Spaltenraster. Geschichten mit 2-Zeilen-Auszug. Minimalste Chrome-Dichte.</p>
|
||||||
|
</div>
|
||||||
|
<span class="mast-badge">B.1 · Entwurf</span>
|
||||||
|
</div>
|
||||||
|
<div class="decisions">
|
||||||
|
<div class="dec"><div class="dec-label">Header-Bg</div><div class="dec-value">Weiß — maximale Helligkeit</div></div>
|
||||||
|
<div class="dec"><div class="dec-label">Stats-Trennlinie</div><div class="dec-value">Vertikale 1 px Linie zwischen Gruß + Stats</div></div>
|
||||||
|
<div class="dec"><div class="dec-label">Personen-Kacheln</div><div class="dec-value">Runder Avatar · Mint Bottom-Border</div></div>
|
||||||
|
<div class="dec"><div class="dec-label">Spaltenbreite</div><div class="dec-value">1 : 1 — gleichwertig</div></div>
|
||||||
|
<div class="dec"><div class="dec-label">Geschichten</div><div class="dec-value">Kursiver Titel + 2-Zeilen-Auszug</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spec-disclaimer">
|
||||||
|
<strong>📐 Mockup-Skalierung —</strong> alle Schriftgrößen, Abstände und Höhen sind auf ca. 55 % der Implementierungswerte skaliert.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══ SECTION 1: LESER ══ -->
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-h"><span class="sec-num">1</span> Desktop · READ_ALL — ohne BLOG_WRITE</div>
|
||||||
|
|
||||||
|
<div class="sg sg-2">
|
||||||
|
<div class="sb">
|
||||||
|
<div class="sl">Vollansicht <span class="sz">≥ 1024 px</span></div>
|
||||||
|
<div class="wf">
|
||||||
|
<div class="wf-bar">
|
||||||
|
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
|
||||||
|
<div class="urlbar"><span>familienarchiv.local /</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="N">
|
||||||
|
<span class="logo">Familienarchiv</span>
|
||||||
|
<span class="nl on">Startseite</span>
|
||||||
|
<span class="nl">Dokumente</span>
|
||||||
|
<span class="nl">Personen</span>
|
||||||
|
<span class="nl">Geschichten</span>
|
||||||
|
<div class="nr">
|
||||||
|
<div class="nico"><svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,.5)" stroke-width="2"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg></div>
|
||||||
|
<div class="av">BK</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="N-accent"></div>
|
||||||
|
<div class="MAIN">
|
||||||
|
|
||||||
|
<!-- Zone 1+2: Combined header — white, clean divider -->
|
||||||
|
<div class="HEADER-BAR">
|
||||||
|
<div class="HEADER-LEFT">
|
||||||
|
<div class="HEADER-TIME">Guten Abend</div>
|
||||||
|
<div class="HEADER-NAME">Herzlich willkommen, Brigitte.</div>
|
||||||
|
</div>
|
||||||
|
<div class="DIVIDER"></div>
|
||||||
|
<div class="HEADER-STATS">
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">847</span><span class="HSTAT-LABEL">Dokumente</span></a></div>
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">94</span><span class="HSTAT-LABEL">Personen</span></a></div>
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">12</span><span class="HSTAT-LABEL">Geschichten</span></a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 4: Person cards — mint bottom-border variant -->
|
||||||
|
<div>
|
||||||
|
<div class="PERSON-GRID">
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-AV" style="background:#002850">KR</div>
|
||||||
|
<div class="PCARD-NAME">Käthe Raddatz</div>
|
||||||
|
<div class="PCARD-COUNT">47 Dokumente</div>
|
||||||
|
</a>
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-AV" style="background:#1A4A6B">ER</div>
|
||||||
|
<div class="PCARD-NAME">Ernst Raddatz</div>
|
||||||
|
<div class="PCARD-COUNT">31 Dokumente</div>
|
||||||
|
</a>
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-AV" style="background:#3D5A7A">FM</div>
|
||||||
|
<div class="PCARD-NAME">Frieda Müller</div>
|
||||||
|
<div class="PCARD-COUNT">28 Dokumente</div>
|
||||||
|
</a>
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-AV" style="background:#4A7A5A">HW</div>
|
||||||
|
<div class="PCARD-NAME">Heinrich Weber</div>
|
||||||
|
<div class="PCARD-COUNT">19 Dokumente</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="PERSONS-FOOTER"><a class="PERSONS-ALL" href="#">Alle 94 Personen →</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 5: 1:1 split -->
|
||||||
|
<div class="CONTENT-ROW">
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Zuletzt aktualisiert</h3><a href="#">Alle Dokumente</a></div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Brief von Ernst an Käthe, März 1923</div><div class="DOC-SENDER">von <a href="#">Käthe Raddatz</a></div></div>
|
||||||
|
<div class="DOC-DATE">vor 2 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Heiratsurkunde Raddatz-Müller, 1898</div><div class="DOC-SENDER" style="color:#E0DDD5">—</div></div>
|
||||||
|
<div class="DOC-DATE">vor 4 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#A6DAD8" stroke-width="1.5"><rect x="2" y="2" width="20" height="20" rx="2"/><circle cx="8" cy="8" r="2"/><polyline points="22 14 16 9 4 22"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Familienfoto, Sommer 1928</div><div class="DOC-SENDER">von <a href="#">Ernst Raddatz</a></div></div>
|
||||||
|
<div class="DOC-DATE">vor 1 Woche</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Taufregister Heinrich Weber, 1902</div><div class="DOC-SENDER" style="color:#E0DDD5">—</div></div>
|
||||||
|
<div class="DOC-DATE">vor 2 Wo.</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#A6DAD8" stroke-width="1.5"><rect x="2" y="2" width="20" height="20" rx="2"/><circle cx="8" cy="8" r="2"/><polyline points="22 14 16 9 4 22"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Postkarte aus Berlin, 1930</div><div class="DOC-SENDER">von <a href="#">Frieda Müller</a></div></div>
|
||||||
|
<div class="DOC-DATE">vor 3 Wo.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Geschichten</h3><a href="#">Alle Geschichten</a></div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-TITLE">Die Reise nach Berlin</div>
|
||||||
|
<div class="STORY-EXCERPT">Im Frühjahr 1921 bestieg Ernst Raddatz erstmals den Zug nach Berlin, um Arbeit in der aufstrebenden Metropole zu finden …</div>
|
||||||
|
<div class="STORY-META">vor 3 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-TITLE">Sommer 1934 in Köln</div>
|
||||||
|
<div class="STORY-EXCERPT">Die Sommerferien 1934 verbrachte die Familie Raddatz bei Verwandten in Köln — die letzte unbeschwerte Reise vor dem Krieg …</div>
|
||||||
|
<div class="STORY-META">vor 2 Wochen</div>
|
||||||
|
</div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-TITLE">Briefe aus dem Krieg</div>
|
||||||
|
<div class="STORY-EXCERPT">Zwischen 1914 und 1918 schrieb Ernst Raddatz insgesamt 47 Briefe aus dem Feld an seine Frau Käthe in der Heimat …</div>
|
||||||
|
<div class="STORY-META">vor 1 Monat</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px;padding-top:40px">
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Header-Balken — weiß mit Trennlinie</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Kein Hintergrund-Farbton — pure weiß, gleicher Hintergrund wie alle Cards</li>
|
||||||
|
<li>1 px vertikale Trennlinie (#E8E4DF) trennt Gruß von Stats klar ohne schweren Rand</li>
|
||||||
|
<li>Stats: 24 px Zahlen (skaliert 14 px), 11 px uppercase Label darunter</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Personen-Kacheln — Mint Bottom-Border</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Einziger farbiger Akzent: 2 px mintfarbener Bottom-Border (#A6DAD8)</li>
|
||||||
|
<li>Runder Avatar in Navy-Farbfamilie — gleiche Logik wie bestehende Avatare in der App</li>
|
||||||
|
<li>Dokumentzahl in Grau, ohne Badge — so wenig Chrome wie möglich</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="note">
|
||||||
|
<strong>Stärken gegenüber Basis-B</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Höchste Konsistenz mit dem restlichen App-Stil (alle Cards weiß)</li>
|
||||||
|
<li>Leichteste visuelle Belastung — ideal für ältere Nutzer</li>
|
||||||
|
<li>Mint nur als funktionaler Akzent (Bottom-Border auf Personenkacheln)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══ SECTION 2: BLOG_WRITE ══ -->
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-h"><span class="sec-num">2</span> Variante · BLOG_WRITE — Zone 3 „Meine Entwürfe"</div>
|
||||||
|
<div class="sg sg-2">
|
||||||
|
<div class="sb">
|
||||||
|
<div class="sl">READ_ALL + BLOG_WRITE</div>
|
||||||
|
<div class="wf">
|
||||||
|
<div class="wf-bar"><div class="dot r"></div><div class="dot y"></div><div class="dot g"></div><div class="urlbar"><span>familienarchiv.local /</span></div></div>
|
||||||
|
<div class="N"><span class="logo">Familienarchiv</span><span class="nl on">Startseite</span><span class="nl">Dokumente</span><span class="nl">Personen</span><span class="nl">Geschichten</span><div class="nr"><div class="av">MR</div></div></div>
|
||||||
|
<div class="N-accent"></div>
|
||||||
|
<div class="MAIN">
|
||||||
|
<div class="HEADER-BAR">
|
||||||
|
<div class="HEADER-LEFT"><div class="HEADER-TIME">Guten Morgen</div><div class="HEADER-NAME">Herzlich willkommen, Marcel.</div></div>
|
||||||
|
<div class="DIVIDER"></div>
|
||||||
|
<div class="HEADER-STATS">
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">847</span><span class="HSTAT-LABEL">Dokumente</span></a></div>
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">94</span><span class="HSTAT-LABEL">Personen</span></a></div>
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">12</span><span class="HSTAT-LABEL">Geschichten</span></a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Drafts -->
|
||||||
|
<div class="DRAFTS-CARD">
|
||||||
|
<div class="DRAFTS-HEAD"><h3>Meine Entwürfe</h3></div>
|
||||||
|
<div class="DRAFT-ROW">
|
||||||
|
<div><div class="DRAFT-TITLE">Onkel Friedrichs Wanderjahre</div><div class="DRAFT-META">Entwurf · zuletzt bearbeitet vor 1 Tag</div></div>
|
||||||
|
<svg width="7" height="7" viewBox="0 0 24 24" fill="none" stroke="#CCC" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="DRAFT-ROW">
|
||||||
|
<div><div class="DRAFT-TITLE">Die Raddatz-Kinder</div><div class="DRAFT-META">Entwurf · zuletzt bearbeitet vor 5 Tagen</div></div>
|
||||||
|
<svg width="7" height="7" viewBox="0 0 24 24" fill="none" stroke="#CCC" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Persons -->
|
||||||
|
<div>
|
||||||
|
<div class="PERSON-GRID">
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-AV" style="background:#002850">KR</div><div class="PCARD-NAME">Käthe Raddatz</div><div class="PCARD-COUNT">47 Dok.</div></a>
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-AV" style="background:#1A4A6B">ER</div><div class="PCARD-NAME">Ernst Raddatz</div><div class="PCARD-COUNT">31 Dok.</div></a>
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-AV" style="background:#3D5A7A">FM</div><div class="PCARD-NAME">Frieda Müller</div><div class="PCARD-COUNT">28 Dok.</div></a>
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-AV" style="background:#4A7A5A">HW</div><div class="PCARD-NAME">Heinrich Weber</div><div class="PCARD-COUNT">19 Dok.</div></a>
|
||||||
|
</div>
|
||||||
|
<div class="PERSONS-FOOTER"><a class="PERSONS-ALL" href="#">Alle 94 Personen →</a></div>
|
||||||
|
</div>
|
||||||
|
<div class="CONTENT-ROW">
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Zuletzt aktualisiert</h3><a href="#">Alle Dokumente</a></div>
|
||||||
|
<div class="DOC-ROW"><div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div><div class="DOC-INFO"><div class="DOC-TITLE">Brief von Ernst an Käthe, März 1923</div><div class="DOC-SENDER">von <a href="#">Käthe Raddatz</a></div></div><div class="DOC-DATE">vor 2 Tagen</div></div>
|
||||||
|
<div class="DOC-ROW"><div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div><div class="DOC-INFO"><div class="DOC-TITLE">Heiratsurkunde Raddatz-Müller, 1898</div><div class="DOC-SENDER" style="color:#E0DDD5">—</div></div><div class="DOC-DATE">vor 4 Tagen</div></div>
|
||||||
|
<div style="padding:5px 10px;font-size:6px;color:#C8C4BE;border-top:1px solid #F0EDE6">+ 3 weitere Dokumente …</div>
|
||||||
|
</div>
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Geschichten</h3><a href="#">Alle</a></div>
|
||||||
|
<div class="STORY-ROW"><div class="STORY-TITLE">Die Reise nach Berlin</div><div class="STORY-EXCERPT">Im Frühjahr 1921 bestieg Ernst Raddatz erstmals den Zug nach Berlin …</div><div class="STORY-META">vor 3 Tagen</div></div>
|
||||||
|
<div class="STORY-ROW"><div class="STORY-TITLE">Sommer 1934 in Köln</div><div class="STORY-EXCERPT">Die Sommerferien 1934 verbrachte die Familie Raddatz bei Verwandten in Köln …</div><div class="STORY-META">vor 2 Wochen</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px;padding-top:40px">
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Entwürfe-Card — Mint Left-Border</strong>
|
||||||
|
<ul>
|
||||||
|
<li>3 px mintfarbener Left-Border auf Card-Ebene — harmoniert mit den Personen-Kacheln</li>
|
||||||
|
<li>Kein separater Card-Hintergrund — fügt sich natürlich in den White-Card-Stil ein</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
421
docs/specs/reader-dashboard-b2-warm.html
Normal file
421
docs/specs/reader-dashboard-b2-warm.html
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Reader Dashboard — B.2 · Sanft & Warm · Familienarchiv</title>
|
||||||
|
<style>
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:'Helvetica Neue',Arial,sans-serif;background:#ECEAE4;color:#1A1A1A;line-height:1.5}
|
||||||
|
.doc{max-width:1440px;margin:0 auto;padding:48px 32px}
|
||||||
|
|
||||||
|
/* ── Masthead ─── */
|
||||||
|
.mast{background:#0D2240;border-radius:10px;padding:32px 40px;margin-bottom:48px}
|
||||||
|
.mast-top{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;margin-bottom:16px}
|
||||||
|
.mast h1{font-size:22px;font-weight:900;color:#fff;letter-spacing:-.4px;margin-bottom:6px}
|
||||||
|
.mast p{font-size:12px;color:rgba(255,255,255,.5);max-width:660px;line-height:1.7}
|
||||||
|
.mast-badge{font-size:9px;font-weight:800;padding:3px 9px;border-radius:20px;text-transform:uppercase;letter-spacing:.8px;white-space:nowrap;flex-shrink:0;margin-top:4px;background:#A6DAD8;color:#002850}
|
||||||
|
.decisions{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:20px;border-top:1px solid rgba(255,255,255,.1);padding-top:16px}
|
||||||
|
.dec{background:rgba(255,255,255,.06);border-radius:6px;padding:10px 12px}
|
||||||
|
.dec-label{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:rgba(255,255,255,.35);margin-bottom:5px}
|
||||||
|
.dec-value{font-size:9.5px;font-weight:700;color:#fff;line-height:1.5}
|
||||||
|
|
||||||
|
/* ── Section headings ─── */
|
||||||
|
.sec{margin-bottom:64px}
|
||||||
|
.sec+.sec{border-top:2px dashed #C8C4BE;padding-top:56px}
|
||||||
|
.sec-h{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#888;margin-bottom:20px;display:flex;align-items:center;gap:10px}
|
||||||
|
.sec-h::after{content:'';flex:1;height:1px;background:#D8D4CE}
|
||||||
|
.sec-num{background:#0D2240;color:#fff;font-size:9px;font-weight:900;padding:2px 7px;border-radius:10px}
|
||||||
|
|
||||||
|
/* ── Screen grid ─── */
|
||||||
|
.sg{display:grid;gap:24px;align-items:start}
|
||||||
|
.sg-2{grid-template-columns:1fr 340px}
|
||||||
|
.sb{display:flex;flex-direction:column;gap:10px}
|
||||||
|
.sl{font-size:9px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;display:flex;align-items:center;gap:6px}
|
||||||
|
.sz{background:#E8E4DF;color:#666;padding:1px 5px;border-radius:3px;font-size:8px}
|
||||||
|
|
||||||
|
/* ── Annotation callouts ─── */
|
||||||
|
.ann-block{background:#FFF7ED;border:1px solid #FDBA74;border-radius:5px;padding:8px 10px;font-size:10px;color:#7C2D12;line-height:1.5}
|
||||||
|
.ann-block strong{font-weight:800}
|
||||||
|
.ann-block ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
.note{background:#F0F9FF;border:1px solid #BAE6FD;border-radius:5px;padding:8px 10px;font-size:10px;color:#0C4A6E;line-height:1.5}
|
||||||
|
.note strong{font-weight:800}
|
||||||
|
.note ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
.ok{background:#F0FDF4;border:1px solid #86EFAC;border-radius:5px;padding:8px 10px;font-size:10px;color:#166534;line-height:1.5}
|
||||||
|
.ok strong{font-weight:800}
|
||||||
|
.ok ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
|
||||||
|
/* ── Mock browser chrome ─── */
|
||||||
|
.wf{background:#fff;border:2px solid #B8B4AE;border-radius:10px;overflow:hidden;box-shadow:0 4px 18px rgba(0,0,0,.08)}
|
||||||
|
.wf-bar{height:24px;background:#E8E4DF;border-bottom:1px solid #C8C4BE;display:flex;align-items:center;padding:0 9px;gap:4px}
|
||||||
|
.dot{width:7px;height:7px;border-radius:50%}
|
||||||
|
.dot.r{background:#F87171}.dot.y{background:#FCD34D}.dot.g{background:#4ADE80}
|
||||||
|
.urlbar{flex:1;height:11px;background:#D8D4CE;border-radius:3px;margin-left:6px;display:flex;align-items:center;padding:0 5px}
|
||||||
|
.urlbar span{font-size:7.5px;color:#888;font-family:monospace}
|
||||||
|
|
||||||
|
/* ── Nav bar ─── */
|
||||||
|
.N{height:34px;background:#002850;display:flex;align-items:center;padding:0 14px;gap:10px}
|
||||||
|
.N-accent{height:2px;background:#A6DAD8}
|
||||||
|
.logo{font-size:7.5px;font-weight:900;color:#fff;letter-spacing:1px;text-transform:uppercase}
|
||||||
|
.nl{font-size:6.5px;color:rgba(255,255,255,.45);font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:3px 6px}
|
||||||
|
.nl.on{color:#fff}
|
||||||
|
.nr{margin-left:auto;display:flex;gap:5px;align-items:center}
|
||||||
|
.av{width:18px;height:18px;background:#A6DAD8;border-radius:50%;font-size:5.5px;font-weight:900;color:#002850;display:flex;align-items:center;justify-content:center}
|
||||||
|
.nico{width:18px;height:18px;background:rgba(255,255,255,.1);border-radius:3px;display:flex;align-items:center;justify-content:center}
|
||||||
|
|
||||||
|
/* ── Page body ─── */
|
||||||
|
.MAIN{padding:12px 16px;background:#F5F4EF;display:flex;flex-direction:column;gap:8px}
|
||||||
|
|
||||||
|
/* ════════════════════════════════════
|
||||||
|
B.2 — SANFT & WARM
|
||||||
|
Header: sand-warm background (#FDFAF5), rounded stat pills
|
||||||
|
Person cards: thin colored top accent strip per card
|
||||||
|
Stories: 2-line excerpt + serif "Geschichte" label
|
||||||
|
Columns: 3:2 docs wider
|
||||||
|
════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* Combined header — warm sand bg, rounded stat pills */
|
||||||
|
.HEADER-BAR{background:#FDFAF5;border:1px solid #E8E3D8;border-radius:3px;padding:10px 14px;display:flex;align-items:center;gap:16px}
|
||||||
|
.HEADER-LEFT{flex:1;min-width:0}
|
||||||
|
.HEADER-TIME{font-size:6px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#C8A87A;margin-bottom:2px}
|
||||||
|
.HEADER-NAME{font-family:Georgia,serif;font-size:12px;color:#002850}
|
||||||
|
.HEADER-STATS{display:flex;gap:5px;align-items:center}
|
||||||
|
.HSTAT-PILL{text-align:center;padding:5px 10px;border:1px solid #E0DDD5;border-radius:20px;background:#fff;text-decoration:none;display:flex;flex-direction:column;align-items:center;gap:1px}
|
||||||
|
.HSTAT-NUM{font-size:12px;font-weight:900;color:#002850;line-height:1;display:block}
|
||||||
|
.HSTAT-LABEL{font-size:5.5px;font-weight:800;text-transform:uppercase;letter-spacing:.7px;color:#B8B4AE;display:block}
|
||||||
|
|
||||||
|
/* Person cards — colored top accent strip */
|
||||||
|
.PERSON-GRID{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
|
||||||
|
.PCARD{background:#fff;border:1px solid #E0DDD5;border-radius:3px;overflow:hidden;text-decoration:none;display:flex;flex-direction:column;align-items:center;text-align:center}
|
||||||
|
.PCARD-STRIP{height:3px;width:100%;flex-shrink:0}
|
||||||
|
.PCARD-INNER{padding:8px 10px 9px;display:flex;flex-direction:column;align-items:center;gap:4px;width:100%}
|
||||||
|
.PCARD-AV{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:900;color:#fff;flex-shrink:0}
|
||||||
|
.PCARD-NAME{font-size:7px;font-weight:700;color:#002850;line-height:1.3}
|
||||||
|
.PCARD-COUNT{font-size:5.5px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:#AAA}
|
||||||
|
.PERSONS-FOOTER{text-align:right;margin-top:3px}
|
||||||
|
.PERSONS-ALL{font-size:6.5px;color:#002850;font-weight:600;text-decoration:none;opacity:.55}
|
||||||
|
|
||||||
|
/* Two-column 3:2 */
|
||||||
|
.CONTENT-ROW{display:grid;grid-template-columns:3fr 2fr;gap:6px}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.CARD{background:#fff;border:1px solid #E0DDD5;border-radius:3px;overflow:hidden;display:flex;flex-direction:column}
|
||||||
|
.CARD-HEAD{display:flex;align-items:center;justify-content:space-between;padding:7px 10px 6px;border-bottom:1px solid #E0DDD5}
|
||||||
|
.CARD-HEAD h3{font-size:6.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#999}
|
||||||
|
.CARD-HEAD a{font-size:6.5px;font-weight:600;color:#002850;opacity:.4;text-decoration:none}
|
||||||
|
|
||||||
|
/* Doc rows */
|
||||||
|
.DOC-ROW{display:flex;align-items:center;gap:7px;padding:5px 10px;border-bottom:1px solid #F0EDE6}
|
||||||
|
.DOC-ROW:last-child{border-bottom:none}
|
||||||
|
.DOC-THUMB{width:20px;height:24px;background:#ECEAE4;border:1px solid #E0DDD5;border-radius:2px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
|
||||||
|
.DOC-INFO{flex:1;min-width:0}
|
||||||
|
.DOC-TITLE{font-family:Georgia,serif;font-size:7.5px;color:#002850;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.DOC-SENDER{font-size:6px;color:#888;margin-top:1px}
|
||||||
|
.DOC-SENDER a{color:#002850;text-decoration:none;opacity:.7}
|
||||||
|
.DOC-DATE{font-size:6px;color:#C8C4BE;white-space:nowrap;flex-shrink:0}
|
||||||
|
|
||||||
|
/* Story rows — with "Geschichte" label */
|
||||||
|
.STORY-ROW{padding:7px 10px;border-bottom:1px solid #F0EDE6}
|
||||||
|
.STORY-ROW:last-child{border-bottom:none}
|
||||||
|
.STORY-LABEL{font-size:5.5px;font-weight:800;text-transform:uppercase;letter-spacing:.6px;color:#C8C4BE;margin-bottom:2px}
|
||||||
|
.STORY-TITLE{font-family:Georgia,serif;font-size:8px;color:#002850;font-style:italic;margin-bottom:3px;line-height:1.3}
|
||||||
|
.STORY-EXCERPT{font-size:6.5px;color:#888;line-height:1.5;margin-bottom:3px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||||||
|
.STORY-META{font-size:6px;color:#B8B4AE}
|
||||||
|
|
||||||
|
/* Drafts */
|
||||||
|
.DRAFTS-CARD{background:#FDFAF5;border:1px solid #E8E3D8;border-radius:3px;overflow:hidden}
|
||||||
|
.DRAFTS-HEAD{padding:6px 10px 5px;border-bottom:1px solid #EEE9E0;display:flex;align-items:center;gap:5px}
|
||||||
|
.DRAFTS-HEAD-DOT{width:6px;height:6px;border-radius:50%;background:#A6DAD8;flex-shrink:0}
|
||||||
|
.DRAFTS-HEAD h3{font-size:6.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#999}
|
||||||
|
.DRAFT-ROW{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;border-bottom:1px solid #EEE9E0}
|
||||||
|
.DRAFT-ROW:last-child{border-bottom:none}
|
||||||
|
.DRAFT-TITLE{font-family:Georgia,serif;font-size:8px;color:#002850}
|
||||||
|
.DRAFT-META{font-size:6px;color:#AAA;margin-top:1px}
|
||||||
|
|
||||||
|
/* Spec disclaimer */
|
||||||
|
.spec-disclaimer{background:#FFF8E1;border:1.5px solid #FFC107;border-radius:6px;padding:11px 16px;font-size:11px;color:#6D4C00;margin-bottom:32px;line-height:1.6}
|
||||||
|
.spec-disclaimer strong{font-weight:800}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="doc">
|
||||||
|
|
||||||
|
<div class="mast">
|
||||||
|
<div class="mast-top">
|
||||||
|
<div>
|
||||||
|
<h1>Reader Dashboard — B.2 · „Sanft & Warm"</h1>
|
||||||
|
<p>Wärmere Iteration von Konzept B. Der Header-Balken erhält einen Sand-Hintergrund (#FDFAF5) statt Weiß — dies erzeugt eine sanfte Abgrenzung zur Begrüßungszone ohne harte Kante. Statistiken erscheinen als abgerundete Pill-Kacheln. Jede Personenkachel bekommt einen schmalen farbigen Top-Akzentstreifen. Die Story-Spalte zeigt ein zusätzliches Kategorie-Label. Spalten 3:2 — Dokumente etwas breiter.</p>
|
||||||
|
</div>
|
||||||
|
<span class="mast-badge">B.2 · Entwurf</span>
|
||||||
|
</div>
|
||||||
|
<div class="decisions">
|
||||||
|
<div class="dec"><div class="dec-label">Header-Bg</div><div class="dec-value">Sand-warm #FDFAF5 — Begrüßungszone spürbar abgegrenzt</div></div>
|
||||||
|
<div class="dec"><div class="dec-label">Stats-Form</div><div class="dec-value">Abgerundete Pill-Kacheln (border-radius: 20 px)</div></div>
|
||||||
|
<div class="dec"><div class="dec-label">Personen</div><div class="dec-value">3 px Top-Akzentstreifen je Kachel (Avatarfarbe)</div></div>
|
||||||
|
<div class="dec"><div class="dec-label">Spaltenbreite</div><div class="dec-value">3 : 2 — Dokumente · Geschichten</div></div>
|
||||||
|
<div class="dec"><div class="dec-label">Geschichten</div><div class="dec-value">Label „Geschichte" + kursiver Titel + Auszug</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spec-disclaimer">
|
||||||
|
<strong>📐 Mockup-Skalierung —</strong> alle Schriftgrößen, Abstände und Höhen sind auf ca. 55 % der Implementierungswerte skaliert.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══ SECTION 1: LESER ══ -->
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-h"><span class="sec-num">1</span> Desktop · READ_ALL — ohne BLOG_WRITE</div>
|
||||||
|
|
||||||
|
<div class="sg sg-2">
|
||||||
|
<div class="sb">
|
||||||
|
<div class="sl">Vollansicht <span class="sz">≥ 1024 px</span></div>
|
||||||
|
<div class="wf">
|
||||||
|
<div class="wf-bar">
|
||||||
|
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
|
||||||
|
<div class="urlbar"><span>familienarchiv.local /</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="N">
|
||||||
|
<span class="logo">Familienarchiv</span>
|
||||||
|
<span class="nl on">Startseite</span>
|
||||||
|
<span class="nl">Dokumente</span>
|
||||||
|
<span class="nl">Personen</span>
|
||||||
|
<span class="nl">Geschichten</span>
|
||||||
|
<div class="nr">
|
||||||
|
<div class="nico"><svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,.5)" stroke-width="2"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg></div>
|
||||||
|
<div class="av">BK</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="N-accent"></div>
|
||||||
|
<div class="MAIN">
|
||||||
|
|
||||||
|
<!-- Zone 1+2: warm sand header + pill stats -->
|
||||||
|
<div class="HEADER-BAR">
|
||||||
|
<div class="HEADER-LEFT">
|
||||||
|
<div class="HEADER-TIME">Guten Abend</div>
|
||||||
|
<div class="HEADER-NAME">Herzlich willkommen, Brigitte.</div>
|
||||||
|
</div>
|
||||||
|
<div class="HEADER-STATS">
|
||||||
|
<a class="HSTAT-PILL" href="#">
|
||||||
|
<span class="HSTAT-NUM">847</span>
|
||||||
|
<span class="HSTAT-LABEL">Dokumente</span>
|
||||||
|
</a>
|
||||||
|
<a class="HSTAT-PILL" href="#">
|
||||||
|
<span class="HSTAT-NUM">94</span>
|
||||||
|
<span class="HSTAT-LABEL">Personen</span>
|
||||||
|
</a>
|
||||||
|
<a class="HSTAT-PILL" href="#">
|
||||||
|
<span class="HSTAT-NUM">12</span>
|
||||||
|
<span class="HSTAT-LABEL">Geschichten</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 4: person cards with colored top strip -->
|
||||||
|
<div>
|
||||||
|
<div class="PERSON-GRID">
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-STRIP" style="background:#002850"></div>
|
||||||
|
<div class="PCARD-INNER">
|
||||||
|
<div class="PCARD-AV" style="background:#002850">KR</div>
|
||||||
|
<div class="PCARD-NAME">Käthe Raddatz</div>
|
||||||
|
<div class="PCARD-COUNT">47 Dokumente</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-STRIP" style="background:#1A4A6B"></div>
|
||||||
|
<div class="PCARD-INNER">
|
||||||
|
<div class="PCARD-AV" style="background:#1A4A6B">ER</div>
|
||||||
|
<div class="PCARD-NAME">Ernst Raddatz</div>
|
||||||
|
<div class="PCARD-COUNT">31 Dokumente</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-STRIP" style="background:#3D5A7A"></div>
|
||||||
|
<div class="PCARD-INNER">
|
||||||
|
<div class="PCARD-AV" style="background:#3D5A7A">FM</div>
|
||||||
|
<div class="PCARD-NAME">Frieda Müller</div>
|
||||||
|
<div class="PCARD-COUNT">28 Dokumente</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-STRIP" style="background:#4A7A5A"></div>
|
||||||
|
<div class="PCARD-INNER">
|
||||||
|
<div class="PCARD-AV" style="background:#4A7A5A">HW</div>
|
||||||
|
<div class="PCARD-NAME">Heinrich Weber</div>
|
||||||
|
<div class="PCARD-COUNT">19 Dokumente</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="PERSONS-FOOTER"><a class="PERSONS-ALL" href="#">Alle 94 Personen →</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 5: 3:2 columns, stories with label -->
|
||||||
|
<div class="CONTENT-ROW">
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Zuletzt aktualisiert</h3><a href="#">Alle Dokumente</a></div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Brief von Ernst an Käthe, März 1923</div><div class="DOC-SENDER">von <a href="#">Käthe Raddatz</a></div></div>
|
||||||
|
<div class="DOC-DATE">vor 2 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Heiratsurkunde Raddatz-Müller, 1898</div><div class="DOC-SENDER" style="color:#E0DDD5">—</div></div>
|
||||||
|
<div class="DOC-DATE">vor 4 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#A6DAD8" stroke-width="1.5"><rect x="2" y="2" width="20" height="20" rx="2"/><circle cx="8" cy="8" r="2"/><polyline points="22 14 16 9 4 22"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Familienfoto, Sommer 1928</div><div class="DOC-SENDER">von <a href="#">Ernst Raddatz</a></div></div>
|
||||||
|
<div class="DOC-DATE">vor 1 Woche</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Taufregister Heinrich Weber, 1902</div><div class="DOC-SENDER" style="color:#E0DDD5">—</div></div>
|
||||||
|
<div class="DOC-DATE">vor 2 Wo.</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#A6DAD8" stroke-width="1.5"><rect x="2" y="2" width="20" height="20" rx="2"/><circle cx="8" cy="8" r="2"/><polyline points="22 14 16 9 4 22"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Postkarte aus Berlin, 1930</div><div class="DOC-SENDER">von <a href="#">Frieda Müller</a></div></div>
|
||||||
|
<div class="DOC-DATE">vor 3 Wo.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Geschichten</h3><a href="#">Alle Geschichten</a></div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-LABEL">Geschichte</div>
|
||||||
|
<div class="STORY-TITLE">Die Reise nach Berlin</div>
|
||||||
|
<div class="STORY-EXCERPT">Im Frühjahr 1921 bestieg Ernst Raddatz erstmals den Zug nach Berlin, um Arbeit in der aufstrebenden Metropole zu finden …</div>
|
||||||
|
<div class="STORY-META">vor 3 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-LABEL">Geschichte</div>
|
||||||
|
<div class="STORY-TITLE">Sommer 1934 in Köln</div>
|
||||||
|
<div class="STORY-EXCERPT">Die Sommerferien 1934 verbrachte die Familie Raddatz bei Verwandten in Köln — die letzte unbeschwerte Reise …</div>
|
||||||
|
<div class="STORY-META">vor 2 Wochen</div>
|
||||||
|
</div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-LABEL">Geschichte</div>
|
||||||
|
<div class="STORY-TITLE">Briefe aus dem Krieg</div>
|
||||||
|
<div class="STORY-EXCERPT">Zwischen 1914 und 1918 schrieb Ernst Raddatz insgesamt 47 Briefe aus dem Feld an seine Frau Käthe …</div>
|
||||||
|
<div class="STORY-META">vor 1 Monat</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px;padding-top:40px">
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Header-Balken — Sandfarben + Pill-Stats</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Hintergrund #FDFAF5 — kaum wahrnehmbar wärmer als Weiß, aber spürbar verschieden</li>
|
||||||
|
<li>Stats als Pill-Kacheln (border-radius: 20 px) — weicher, weniger „Tabellen"-Charakter</li>
|
||||||
|
<li>Pills sind vollständige <a>-Elemente; große Touch-Target auch auf Tablet</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Personen-Kacheln — Top-Akzentstreifen</strong>
|
||||||
|
<ul>
|
||||||
|
<li>3 px hoher Streifen oben: gleiche Farbe wie Avatar — verbindet Streifen und Bild visuell</li>
|
||||||
|
<li>Erzeugt ein „Lesezeichen"-Gefühl: jede Person hat ihre eigene Erkennungsfarbe</li>
|
||||||
|
<li>Umsetzung: border-top: 3px solid [avatarfarbe] auf .PCARD — kein extra Element nötig</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="note">
|
||||||
|
<strong>3:2 Spaltenbreite</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Dokumente bekommen mehr Platz (3) — als primäres Archiv-Element gewichtet</li>
|
||||||
|
<li>Geschichten-Spalte ist schmaler, aber durch das Story-Label visuell nicht schwächer</li>
|
||||||
|
<li>Auf < md stacken beide Spalten vertikal (wie alle anderen Varianten)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="ok">
|
||||||
|
<strong>Stärken dieser Iteration</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Warmster Charakter der drei B-Iterationen — Archiv fühlt sich familiärer an</li>
|
||||||
|
<li>Top-Streifen gibt Personenkacheln Persönlichkeit ohne Foto oder mehr Daten</li>
|
||||||
|
<li>Pill-Stats wirken weniger formal — besser für nicht-technische Leser</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══ SECTION 2: BLOG_WRITE ══ -->
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-h"><span class="sec-num">2</span> Variante · BLOG_WRITE — Zone 3 „Meine Entwürfe"</div>
|
||||||
|
<div class="sg sg-2">
|
||||||
|
<div class="sb">
|
||||||
|
<div class="sl">READ_ALL + BLOG_WRITE</div>
|
||||||
|
<div class="wf">
|
||||||
|
<div class="wf-bar"><div class="dot r"></div><div class="dot y"></div><div class="dot g"></div><div class="urlbar"><span>familienarchiv.local /</span></div></div>
|
||||||
|
<div class="N"><span class="logo">Familienarchiv</span><span class="nl on">Startseite</span><span class="nl">Dokumente</span><span class="nl">Personen</span><span class="nl">Geschichten</span><div class="nr"><div class="av">MR</div></div></div>
|
||||||
|
<div class="N-accent"></div>
|
||||||
|
<div class="MAIN">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="HEADER-BAR">
|
||||||
|
<div class="HEADER-LEFT"><div class="HEADER-TIME">Guten Morgen</div><div class="HEADER-NAME">Herzlich willkommen, Marcel.</div></div>
|
||||||
|
<div class="HEADER-STATS">
|
||||||
|
<a class="HSTAT-PILL" href="#"><span class="HSTAT-NUM">847</span><span class="HSTAT-LABEL">Dokumente</span></a>
|
||||||
|
<a class="HSTAT-PILL" href="#"><span class="HSTAT-NUM">94</span><span class="HSTAT-LABEL">Personen</span></a>
|
||||||
|
<a class="HSTAT-PILL" href="#"><span class="HSTAT-NUM">12</span><span class="HSTAT-LABEL">Geschichten</span></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Drafts — sand bg matches header -->
|
||||||
|
<div class="DRAFTS-CARD">
|
||||||
|
<div class="DRAFTS-HEAD">
|
||||||
|
<div class="DRAFTS-HEAD-DOT"></div>
|
||||||
|
<h3>Meine Entwürfe</h3>
|
||||||
|
</div>
|
||||||
|
<div class="DRAFT-ROW">
|
||||||
|
<div><div class="DRAFT-TITLE">Onkel Friedrichs Wanderjahre</div><div class="DRAFT-META">Entwurf · zuletzt bearbeitet vor 1 Tag</div></div>
|
||||||
|
<svg width="7" height="7" viewBox="0 0 24 24" fill="none" stroke="#CCC" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="DRAFT-ROW">
|
||||||
|
<div><div class="DRAFT-TITLE">Die Raddatz-Kinder</div><div class="DRAFT-META">Entwurf · zuletzt bearbeitet vor 5 Tagen</div></div>
|
||||||
|
<svg width="7" height="7" viewBox="0 0 24 24" fill="none" stroke="#CCC" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Persons -->
|
||||||
|
<div>
|
||||||
|
<div class="PERSON-GRID">
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-STRIP" style="background:#002850"></div><div class="PCARD-INNER"><div class="PCARD-AV" style="background:#002850">KR</div><div class="PCARD-NAME">Käthe Raddatz</div><div class="PCARD-COUNT">47 Dok.</div></div></a>
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-STRIP" style="background:#1A4A6B"></div><div class="PCARD-INNER"><div class="PCARD-AV" style="background:#1A4A6B">ER</div><div class="PCARD-NAME">Ernst Raddatz</div><div class="PCARD-COUNT">31 Dok.</div></div></a>
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-STRIP" style="background:#3D5A7A"></div><div class="PCARD-INNER"><div class="PCARD-AV" style="background:#3D5A7A">FM</div><div class="PCARD-NAME">Frieda Müller</div><div class="PCARD-COUNT">28 Dok.</div></div></a>
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-STRIP" style="background:#4A7A5A"></div><div class="PCARD-INNER"><div class="PCARD-AV" style="background:#4A7A5A">HW</div><div class="PCARD-NAME">Heinrich Weber</div><div class="PCARD-COUNT">19 Dok.</div></div></a>
|
||||||
|
</div>
|
||||||
|
<div class="PERSONS-FOOTER"><a class="PERSONS-ALL" href="#">Alle 94 Personen →</a></div>
|
||||||
|
</div>
|
||||||
|
<div class="CONTENT-ROW">
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Zuletzt aktualisiert</h3><a href="#">Alle Dokumente</a></div>
|
||||||
|
<div class="DOC-ROW"><div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div><div class="DOC-INFO"><div class="DOC-TITLE">Brief von Ernst an Käthe, März 1923</div><div class="DOC-SENDER">von <a href="#">Käthe Raddatz</a></div></div><div class="DOC-DATE">vor 2 Tagen</div></div>
|
||||||
|
<div class="DOC-ROW"><div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div><div class="DOC-INFO"><div class="DOC-TITLE">Heiratsurkunde Raddatz-Müller, 1898</div><div class="DOC-SENDER" style="color:#E0DDD5">—</div></div><div class="DOC-DATE">vor 4 Tagen</div></div>
|
||||||
|
<div style="padding:5px 10px;font-size:6px;color:#C8C4BE;border-top:1px solid #F0EDE6">+ 3 weitere …</div>
|
||||||
|
</div>
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Geschichten</h3><a href="#">Alle</a></div>
|
||||||
|
<div class="STORY-ROW"><div class="STORY-LABEL">Geschichte</div><div class="STORY-TITLE">Die Reise nach Berlin</div><div class="STORY-EXCERPT">Im Frühjahr 1921 bestieg Ernst Raddatz erstmals den Zug nach Berlin …</div><div class="STORY-META">vor 3 Tagen</div></div>
|
||||||
|
<div class="STORY-ROW"><div class="STORY-LABEL">Geschichte</div><div class="STORY-TITLE">Sommer 1934 in Köln</div><div class="STORY-EXCERPT">Die Sommerferien 1934 verbrachte die Familie Raddatz bei Verwandten …</div><div class="STORY-META">vor 2 Wochen</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px;padding-top:40px">
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Entwürfe-Card — Sand-Hintergrund + Mint-Dot</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Card-Hintergrund #FDFAF5 — gleich wie der Header-Balken: Entwürfe fühlen sich als Teil des „eigenen Bereichs" an</li>
|
||||||
|
<li>Mint-Dot vor dem Titel statt Border — subtiler, luftiger Akzent</li>
|
||||||
|
<li>Kontext: BLOG_WRITE-Nutzer schreibt Geschichten → sand-getönte Karte passt zur redaktionellen Rolle</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
437
docs/specs/reader-dashboard-b3-navy.html
Normal file
437
docs/specs/reader-dashboard-b3-navy.html
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Reader Dashboard — B.3 · Navy & Kontrast · Familienarchiv</title>
|
||||||
|
<style>
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:'Helvetica Neue',Arial,sans-serif;background:#ECEAE4;color:#1A1A1A;line-height:1.5}
|
||||||
|
.doc{max-width:1440px;margin:0 auto;padding:48px 32px}
|
||||||
|
|
||||||
|
/* ── Masthead ─── */
|
||||||
|
.mast{background:#0D2240;border-radius:10px;padding:32px 40px;margin-bottom:48px}
|
||||||
|
.mast-top{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;margin-bottom:16px}
|
||||||
|
.mast h1{font-size:22px;font-weight:900;color:#fff;letter-spacing:-.4px;margin-bottom:6px}
|
||||||
|
.mast p{font-size:12px;color:rgba(255,255,255,.5);max-width:660px;line-height:1.7}
|
||||||
|
.mast-badge{font-size:9px;font-weight:800;padding:3px 9px;border-radius:20px;text-transform:uppercase;letter-spacing:.8px;white-space:nowrap;flex-shrink:0;margin-top:4px;background:#A6DAD8;color:#002850}
|
||||||
|
.decisions{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:20px;border-top:1px solid rgba(255,255,255,.1);padding-top:16px}
|
||||||
|
.dec{background:rgba(255,255,255,.06);border-radius:6px;padding:10px 12px}
|
||||||
|
.dec-label{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:rgba(255,255,255,.35);margin-bottom:5px}
|
||||||
|
.dec-value{font-size:9.5px;font-weight:700;color:#fff;line-height:1.5}
|
||||||
|
|
||||||
|
/* ── Section headings ─── */
|
||||||
|
.sec{margin-bottom:64px}
|
||||||
|
.sec+.sec{border-top:2px dashed #C8C4BE;padding-top:56px}
|
||||||
|
.sec-h{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#888;margin-bottom:20px;display:flex;align-items:center;gap:10px}
|
||||||
|
.sec-h::after{content:'';flex:1;height:1px;background:#D8D4CE}
|
||||||
|
.sec-num{background:#0D2240;color:#fff;font-size:9px;font-weight:900;padding:2px 7px;border-radius:10px}
|
||||||
|
|
||||||
|
/* ── Screen grid ─── */
|
||||||
|
.sg{display:grid;gap:24px;align-items:start}
|
||||||
|
.sg-2{grid-template-columns:1fr 340px}
|
||||||
|
.sb{display:flex;flex-direction:column;gap:10px}
|
||||||
|
.sl{font-size:9px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;display:flex;align-items:center;gap:6px}
|
||||||
|
.sz{background:#E8E4DF;color:#666;padding:1px 5px;border-radius:3px;font-size:8px}
|
||||||
|
|
||||||
|
/* ── Annotation callouts ─── */
|
||||||
|
.ann-block{background:#FFF7ED;border:1px solid #FDBA74;border-radius:5px;padding:8px 10px;font-size:10px;color:#7C2D12;line-height:1.5}
|
||||||
|
.ann-block strong{font-weight:800}
|
||||||
|
.ann-block ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
.note{background:#F0F9FF;border:1px solid #BAE6FD;border-radius:5px;padding:8px 10px;font-size:10px;color:#0C4A6E;line-height:1.5}
|
||||||
|
.note strong{font-weight:800}
|
||||||
|
.note ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
.ok{background:#F0FDF4;border:1px solid #86EFAC;border-radius:5px;padding:8px 10px;font-size:10px;color:#166534;line-height:1.5}
|
||||||
|
.ok strong{font-weight:800}
|
||||||
|
.ok ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
|
||||||
|
/* ── Mock browser chrome ─── */
|
||||||
|
.wf{background:#fff;border:2px solid #B8B4AE;border-radius:10px;overflow:hidden;box-shadow:0 4px 18px rgba(0,0,0,.08)}
|
||||||
|
.wf-bar{height:24px;background:#E8E4DF;border-bottom:1px solid #C8C4BE;display:flex;align-items:center;padding:0 9px;gap:4px}
|
||||||
|
.dot{width:7px;height:7px;border-radius:50%}
|
||||||
|
.dot.r{background:#F87171}.dot.y{background:#FCD34D}.dot.g{background:#4ADE80}
|
||||||
|
.urlbar{flex:1;height:11px;background:#D8D4CE;border-radius:3px;margin-left:6px;display:flex;align-items:center;padding:0 5px}
|
||||||
|
.urlbar span{font-size:7.5px;color:#888;font-family:monospace}
|
||||||
|
|
||||||
|
/* ── Nav bar ─── */
|
||||||
|
.N{height:34px;background:#002850;display:flex;align-items:center;padding:0 14px;gap:10px}
|
||||||
|
.N-accent{height:2px;background:#A6DAD8}
|
||||||
|
.logo{font-size:7.5px;font-weight:900;color:#fff;letter-spacing:1px;text-transform:uppercase}
|
||||||
|
.nl{font-size:6.5px;color:rgba(255,255,255,.45);font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:3px 6px}
|
||||||
|
.nl.on{color:#fff}
|
||||||
|
.nr{margin-left:auto;display:flex;gap:5px;align-items:center}
|
||||||
|
.av{width:18px;height:18px;background:#A6DAD8;border-radius:50%;font-size:5.5px;font-weight:900;color:#002850;display:flex;align-items:center;justify-content:center}
|
||||||
|
.nico{width:18px;height:18px;background:rgba(255,255,255,.1);border-radius:3px;display:flex;align-items:center;justify-content:center}
|
||||||
|
|
||||||
|
/* ── Page body ─── */
|
||||||
|
.MAIN{padding:0;background:#F5F4EF;display:flex;flex-direction:column;gap:0}
|
||||||
|
|
||||||
|
/* ════════════════════════════════════
|
||||||
|
B.3 — NAVY & KONTRAST
|
||||||
|
Header: full navy bg (#002850), white greeting, mint stats
|
||||||
|
Person cards: larger avatar (34px), mint count badge prominent
|
||||||
|
Stories: 2-line excerpt, mint left accent on hover row
|
||||||
|
Columns: 1:1 equal
|
||||||
|
Note: MAIN has no padding — navy header bleeds to edges
|
||||||
|
════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* Combined header — full navy, white text, mint stats */
|
||||||
|
.HEADER-NAVY{background:#002850;padding:12px 16px;display:flex;align-items:center;gap:16px}
|
||||||
|
.HEADER-LEFT{flex:1;min-width:0}
|
||||||
|
.HEADER-TIME{font-size:6px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:rgba(166,218,216,.6);margin-bottom:3px}
|
||||||
|
.HEADER-NAME{font-family:Georgia,serif;font-size:12px;color:#fff;line-height:1.2}
|
||||||
|
.HEADER-DIVIDER{width:1px;background:rgba(255,255,255,.12);align-self:stretch;flex-shrink:0}
|
||||||
|
.HEADER-STATS{display:flex;align-items:center;gap:0}
|
||||||
|
.HSTAT{text-align:center;padding:0 12px;border-right:1px solid rgba(255,255,255,.1)}
|
||||||
|
.HSTAT:last-child{border-right:none;padding-right:0}
|
||||||
|
.HSTAT a{text-decoration:none;display:block}
|
||||||
|
.HSTAT-NUM{font-size:14px;font-weight:900;color:#A6DAD8;line-height:1;display:block}
|
||||||
|
.HSTAT-LABEL{font-size:5.5px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:rgba(255,255,255,.4);display:block;margin-top:2px}
|
||||||
|
|
||||||
|
/* Thin mint separator between header and page body */
|
||||||
|
.HEADER-ACCENT{height:2px;background:#A6DAD8;flex-shrink:0}
|
||||||
|
|
||||||
|
/* Inner page padding (below the navy header) */
|
||||||
|
.INNER{padding:12px 16px;display:flex;flex-direction:column;gap:8px}
|
||||||
|
|
||||||
|
/* Person cards — larger avatar, mint count badge */
|
||||||
|
.PERSON-GRID{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
|
||||||
|
.PCARD{background:#fff;border:1px solid #E0DDD5;border-radius:3px;padding:10px 10px 9px;text-decoration:none;display:flex;flex-direction:column;align-items:center;text-align:center;gap:5px;box-shadow:0 1px 3px rgba(0,40,80,.04)}
|
||||||
|
.PCARD-AV{width:34px;height:34px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:900;color:#fff;flex-shrink:0;box-shadow:0 2px 6px rgba(0,40,80,.2)}
|
||||||
|
.PCARD-NAME{font-size:7px;font-weight:700;color:#002850;line-height:1.3}
|
||||||
|
.PCARD-BADGE{font-size:5.5px;font-weight:800;color:#002850;background:#D4F0EE;padding:1px 6px;border-radius:10px}
|
||||||
|
.PERSONS-FOOTER{text-align:right;margin-top:3px}
|
||||||
|
.PERSONS-ALL{font-size:6.5px;color:#002850;font-weight:600;text-decoration:none;opacity:.55}
|
||||||
|
|
||||||
|
/* Two-column 1:1 */
|
||||||
|
.CONTENT-ROW{display:grid;grid-template-columns:1fr 1fr;gap:6px}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.CARD{background:#fff;border:1px solid #E0DDD5;border-radius:3px;overflow:hidden;display:flex;flex-direction:column;box-shadow:0 1px 3px rgba(0,40,80,.04)}
|
||||||
|
.CARD-HEAD{display:flex;align-items:center;justify-content:space-between;padding:7px 10px 6px;border-bottom:1px solid #E0DDD5}
|
||||||
|
.CARD-HEAD h3{font-size:6.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#999}
|
||||||
|
.CARD-HEAD a{font-size:6.5px;font-weight:600;color:#002850;opacity:.4;text-decoration:none}
|
||||||
|
|
||||||
|
/* Doc rows */
|
||||||
|
.DOC-ROW{display:flex;align-items:center;gap:7px;padding:5px 10px;border-bottom:1px solid #F0EDE6}
|
||||||
|
.DOC-ROW:last-child{border-bottom:none}
|
||||||
|
.DOC-THUMB{width:20px;height:24px;background:#ECEAE4;border:1px solid #E0DDD5;border-radius:2px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
|
||||||
|
.DOC-INFO{flex:1;min-width:0}
|
||||||
|
.DOC-TITLE{font-family:Georgia,serif;font-size:7.5px;color:#002850;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.DOC-SENDER{font-size:6px;color:#888;margin-top:1px}
|
||||||
|
.DOC-SENDER a{color:#002850;text-decoration:none;opacity:.7}
|
||||||
|
.DOC-DATE{font-size:6px;color:#C8C4BE;white-space:nowrap;flex-shrink:0}
|
||||||
|
|
||||||
|
/* Story rows — mint left accent strip */
|
||||||
|
.STORY-ROW{display:flex;gap:0;border-bottom:1px solid #F0EDE6}
|
||||||
|
.STORY-ROW:last-child{border-bottom:none}
|
||||||
|
.STORY-ACCENT{width:2px;background:#A6DAD8;flex-shrink:0;margin:6px 0}
|
||||||
|
.STORY-BODY{padding:6px 10px;flex:1;min-width:0}
|
||||||
|
.STORY-TITLE{font-family:Georgia,serif;font-size:8px;color:#002850;font-style:italic;margin-bottom:3px;line-height:1.3}
|
||||||
|
.STORY-EXCERPT{font-size:6.5px;color:#888;line-height:1.5;margin-bottom:3px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
|
||||||
|
.STORY-META{font-size:6px;color:#B8B4AE}
|
||||||
|
|
||||||
|
/* Drafts — navy tint card */
|
||||||
|
.DRAFTS-CARD{background:#fff;border:1px solid #E0DDD5;border-radius:3px;overflow:hidden;box-shadow:0 1px 3px rgba(0,40,80,.04)}
|
||||||
|
.DRAFTS-HEAD{background:#002850;padding:6px 10px 5px;display:flex;align-items:center;justify-content:space-between}
|
||||||
|
.DRAFTS-HEAD h3{font-size:6.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:rgba(166,218,216,.8)}
|
||||||
|
.DRAFT-ROW{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid #F0EDE6}
|
||||||
|
.DRAFT-ROW:last-child{border-bottom:none}
|
||||||
|
.DRAFT-DOT{width:5px;height:5px;border-radius:50%;background:#A6DAD8;flex-shrink:0}
|
||||||
|
.DRAFT-INFO{flex:1;min-width:0}
|
||||||
|
.DRAFT-TITLE{font-family:Georgia,serif;font-size:8px;color:#002850}
|
||||||
|
.DRAFT-META{font-size:6px;color:#AAA;margin-top:1px}
|
||||||
|
|
||||||
|
/* Spec disclaimer */
|
||||||
|
.spec-disclaimer{background:#FFF8E1;border:1.5px solid #FFC107;border-radius:6px;padding:11px 16px;font-size:11px;color:#6D4C00;margin-bottom:32px;line-height:1.6}
|
||||||
|
.spec-disclaimer strong{font-weight:800}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="doc">
|
||||||
|
|
||||||
|
<div class="mast">
|
||||||
|
<div class="mast-top">
|
||||||
|
<div>
|
||||||
|
<h1>Reader Dashboard — B.3 · „Navy & Kontrast"</h1>
|
||||||
|
<p>Markanteste Iteration von Konzept B. Der Header-Balken übernimmt die volle Navy-Farbe der App-Navigation (#002850) — Begrüßung in Weiß, Statistik-Zahlen in Mintgrün. Die Kacheln im Seitenbereich (weiß + Schatten) heben sich stark ab. Personen-Avatare sind größer (34 px) mit leichtem Schatten. Story-Zeilen haben einen fixen mintgrünen Akzentstreifen links.</p>
|
||||||
|
</div>
|
||||||
|
<span class="mast-badge">B.3 · Entwurf</span>
|
||||||
|
</div>
|
||||||
|
<div class="decisions">
|
||||||
|
<div class="dec"><div class="dec-label">Header-Bg</div><div class="dec-value">Navy #002850 — gespiegelt aus App-Nav</div></div>
|
||||||
|
<div class="dec"><div class="dec-label">Stats-Farbe</div><div class="dec-value">Mint #A6DAD8 auf Navy — maximaler Kontrast</div></div>
|
||||||
|
<div class="dec"><div class="dec-label">Personen</div><div class="dec-value">Größerer Avatar (34 px) + Badge mit mint Bg</div></div>
|
||||||
|
<div class="dec"><div class="dec-label">Geschichten</div><div class="dec-value">2 px Mint-Streifen links + 2-Zeilen-Auszug</div></div>
|
||||||
|
<div class="dec"><div class="dec-label">Spaltenbreite</div><div class="dec-value">1 : 1 gleichwertig</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spec-disclaimer">
|
||||||
|
<strong>📐 Mockup-Skalierung —</strong> alle Schriftgrößen, Abstände und Höhen sind auf ca. 55 % der Implementierungswerte skaliert.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══ SECTION 1: LESER ══ -->
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-h"><span class="sec-num">1</span> Desktop · READ_ALL — ohne BLOG_WRITE</div>
|
||||||
|
|
||||||
|
<div class="sg sg-2">
|
||||||
|
<div class="sb">
|
||||||
|
<div class="sl">Vollansicht <span class="sz">≥ 1024 px</span></div>
|
||||||
|
<div class="wf">
|
||||||
|
<div class="wf-bar">
|
||||||
|
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
|
||||||
|
<div class="urlbar"><span>familienarchiv.local /</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nav bar -->
|
||||||
|
<div class="N">
|
||||||
|
<span class="logo">Familienarchiv</span>
|
||||||
|
<span class="nl on">Startseite</span>
|
||||||
|
<span class="nl">Dokumente</span>
|
||||||
|
<span class="nl">Personen</span>
|
||||||
|
<span class="nl">Geschichten</span>
|
||||||
|
<div class="nr">
|
||||||
|
<div class="nico"><svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,.5)" stroke-width="2"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg></div>
|
||||||
|
<div class="av">BK</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Note: no N-accent here — header bleeds directly into navy header bar -->
|
||||||
|
|
||||||
|
<div class="MAIN">
|
||||||
|
|
||||||
|
<!-- Zone 1+2: full navy header — blends with nav visually -->
|
||||||
|
<div class="HEADER-NAVY">
|
||||||
|
<div class="HEADER-LEFT">
|
||||||
|
<div class="HEADER-TIME">Guten Abend</div>
|
||||||
|
<div class="HEADER-NAME">Herzlich willkommen, Brigitte.</div>
|
||||||
|
</div>
|
||||||
|
<div class="HEADER-DIVIDER"></div>
|
||||||
|
<div class="HEADER-STATS">
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">847</span><span class="HSTAT-LABEL">Dokumente</span></a></div>
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">94</span><span class="HSTAT-LABEL">Personen</span></a></div>
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">12</span><span class="HSTAT-LABEL">Geschichten</span></a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="HEADER-ACCENT"></div>
|
||||||
|
|
||||||
|
<div class="INNER">
|
||||||
|
|
||||||
|
<!-- Zone 4: Person cards — larger avatar, mint badge -->
|
||||||
|
<div>
|
||||||
|
<div class="PERSON-GRID">
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-AV" style="background:#002850">KR</div>
|
||||||
|
<div class="PCARD-NAME">Käthe Raddatz</div>
|
||||||
|
<div class="PCARD-BADGE">47 Dok.</div>
|
||||||
|
</a>
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-AV" style="background:#1A4A6B">ER</div>
|
||||||
|
<div class="PCARD-NAME">Ernst Raddatz</div>
|
||||||
|
<div class="PCARD-BADGE">31 Dok.</div>
|
||||||
|
</a>
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-AV" style="background:#3D5A7A">FM</div>
|
||||||
|
<div class="PCARD-NAME">Frieda Müller</div>
|
||||||
|
<div class="PCARD-BADGE">28 Dok.</div>
|
||||||
|
</a>
|
||||||
|
<a class="PCARD" href="#">
|
||||||
|
<div class="PCARD-AV" style="background:#4A7A5A">HW</div>
|
||||||
|
<div class="PCARD-NAME">Heinrich Weber</div>
|
||||||
|
<div class="PCARD-BADGE">19 Dok.</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="PERSONS-FOOTER"><a class="PERSONS-ALL" href="#">Alle 94 Personen →</a></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 5: 1:1, stories with mint accent strip -->
|
||||||
|
<div class="CONTENT-ROW">
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Zuletzt aktualisiert</h3><a href="#">Alle Dokumente</a></div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Brief von Ernst an Käthe, März 1923</div><div class="DOC-SENDER">von <a href="#">Käthe Raddatz</a></div></div>
|
||||||
|
<div class="DOC-DATE">vor 2 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Heiratsurkunde Raddatz-Müller, 1898</div><div class="DOC-SENDER" style="color:#E0DDD5">—</div></div>
|
||||||
|
<div class="DOC-DATE">vor 4 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#A6DAD8" stroke-width="1.5"><rect x="2" y="2" width="20" height="20" rx="2"/><circle cx="8" cy="8" r="2"/><polyline points="22 14 16 9 4 22"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Familienfoto, Sommer 1928</div><div class="DOC-SENDER">von <a href="#">Ernst Raddatz</a></div></div>
|
||||||
|
<div class="DOC-DATE">vor 1 Woche</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Taufregister Heinrich Weber, 1902</div><div class="DOC-SENDER" style="color:#E0DDD5">—</div></div>
|
||||||
|
<div class="DOC-DATE">vor 2 Wo.</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW">
|
||||||
|
<div class="DOC-THUMB"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#A6DAD8" stroke-width="1.5"><rect x="2" y="2" width="20" height="20" rx="2"/><circle cx="8" cy="8" r="2"/><polyline points="22 14 16 9 4 22"/></svg></div>
|
||||||
|
<div class="DOC-INFO"><div class="DOC-TITLE">Postkarte aus Berlin, 1930</div><div class="DOC-SENDER">von <a href="#">Frieda Müller</a></div></div>
|
||||||
|
<div class="DOC-DATE">vor 3 Wo.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Geschichten</h3><a href="#">Alle Geschichten</a></div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-ACCENT"></div>
|
||||||
|
<div class="STORY-BODY">
|
||||||
|
<div class="STORY-TITLE">Die Reise nach Berlin</div>
|
||||||
|
<div class="STORY-EXCERPT">Im Frühjahr 1921 bestieg Ernst Raddatz erstmals den Zug nach Berlin, um Arbeit in der aufstrebenden Metropole zu finden …</div>
|
||||||
|
<div class="STORY-META">vor 3 Tagen</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-ACCENT"></div>
|
||||||
|
<div class="STORY-BODY">
|
||||||
|
<div class="STORY-TITLE">Sommer 1934 in Köln</div>
|
||||||
|
<div class="STORY-EXCERPT">Die Sommerferien 1934 verbrachte die Familie Raddatz bei Verwandten in Köln — die letzte unbeschwerte Reise vor dem Krieg …</div>
|
||||||
|
<div class="STORY-META">vor 2 Wochen</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="STORY-ROW">
|
||||||
|
<div class="STORY-ACCENT"></div>
|
||||||
|
<div class="STORY-BODY">
|
||||||
|
<div class="STORY-TITLE">Briefe aus dem Krieg</div>
|
||||||
|
<div class="STORY-EXCERPT">Zwischen 1914 und 1918 schrieb Ernst Raddatz insgesamt 47 Briefe aus dem Feld an seine Frau Käthe in der Heimat …</div>
|
||||||
|
<div class="STORY-META">vor 1 Monat</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /INNER -->
|
||||||
|
</div><!-- /MAIN -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px;padding-top:40px">
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Header-Balken — Vollflächiges Navy</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Gleiche Farbe (#002850) wie die App-Navigation direkt darüber</li>
|
||||||
|
<li>Effekt: Nav und Header verschmelzen optisch zu einer „Kopfzone" der Seite</li>
|
||||||
|
<li>Mint-Trennlinie (2 px, wie N-accent) trennt Header sauber vom Canvas</li>
|
||||||
|
<li>Begrüßungstext weiß → maximale Lesbarkeit, serif bleibt erhalten</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Stats in Mint auf Navy</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Zahlen in #A6DAD8 — gleiche Farbe wie N-accent und bestehende Mint-Akzente</li>
|
||||||
|
<li>Sehr hoher Kontrast; klar als Zahlen erkennbar ohne Tooltip oder Label-Hilfe</li>
|
||||||
|
<li>Labels in rgba(255,255,255,.4) — gedämpft, aber lesbar</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="note">
|
||||||
|
<strong>Personen-Avatar — 34 px mit Schatten</strong>
|
||||||
|
<ul>
|
||||||
|
<li>5 px größerer Avatar als B.1/B.2 — gibt Kacheln mehr „Gesicht"</li>
|
||||||
|
<li>Leichter box-shadow auf Avatar macht ihn dreidimensionaler, erinnert an Foto</li>
|
||||||
|
<li>Mint-Badge (#D4F0EE / #002850) statt einfachem Grau-Text für Dokumentzahl</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="ok">
|
||||||
|
<strong>Stärken dieser Iteration</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Stärkste Markenpräsenz — Navy + Mint als Leitfarben treten klar hervor</li>
|
||||||
|
<li>Header-Zone kommuniziert „Willkommen im Archiv" — persönlicher Empfang</li>
|
||||||
|
<li>Mint-Akzent auf Story-Zeilen verankert den Geschichten-Bereich im Brand</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══ SECTION 2: BLOG_WRITE ══ -->
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-h"><span class="sec-num">2</span> Variante · BLOG_WRITE — Zone 3 „Meine Entwürfe"</div>
|
||||||
|
<div class="sg sg-2">
|
||||||
|
<div class="sb">
|
||||||
|
<div class="sl">READ_ALL + BLOG_WRITE</div>
|
||||||
|
<div class="wf">
|
||||||
|
<div class="wf-bar"><div class="dot r"></div><div class="dot y"></div><div class="dot g"></div><div class="urlbar"><span>familienarchiv.local /</span></div></div>
|
||||||
|
<div class="N"><span class="logo">Familienarchiv</span><span class="nl on">Startseite</span><span class="nl">Dokumente</span><span class="nl">Personen</span><span class="nl">Geschichten</span><div class="nr"><div class="av">MR</div></div></div>
|
||||||
|
<div class="MAIN">
|
||||||
|
<!-- Navy header -->
|
||||||
|
<div class="HEADER-NAVY">
|
||||||
|
<div class="HEADER-LEFT"><div class="HEADER-TIME">Guten Morgen</div><div class="HEADER-NAME">Herzlich willkommen, Marcel.</div></div>
|
||||||
|
<div class="HEADER-DIVIDER"></div>
|
||||||
|
<div class="HEADER-STATS">
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">847</span><span class="HSTAT-LABEL">Dokumente</span></a></div>
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">94</span><span class="HSTAT-LABEL">Personen</span></a></div>
|
||||||
|
<div class="HSTAT"><a href="#"><span class="HSTAT-NUM">12</span><span class="HSTAT-LABEL">Geschichten</span></a></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="HEADER-ACCENT"></div>
|
||||||
|
<div class="INNER">
|
||||||
|
<!-- Drafts — navy header on card mirrors main header -->
|
||||||
|
<div class="DRAFTS-CARD">
|
||||||
|
<div class="DRAFTS-HEAD">
|
||||||
|
<h3>Meine Entwürfe</h3>
|
||||||
|
</div>
|
||||||
|
<div class="DRAFT-ROW">
|
||||||
|
<div class="DRAFT-DOT"></div>
|
||||||
|
<div class="DRAFT-INFO">
|
||||||
|
<div class="DRAFT-TITLE">Onkel Friedrichs Wanderjahre</div>
|
||||||
|
<div class="DRAFT-META">Entwurf · zuletzt bearbeitet vor 1 Tag</div>
|
||||||
|
</div>
|
||||||
|
<svg width="7" height="7" viewBox="0 0 24 24" fill="none" stroke="#CCC" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="DRAFT-ROW">
|
||||||
|
<div class="DRAFT-DOT"></div>
|
||||||
|
<div class="DRAFT-INFO">
|
||||||
|
<div class="DRAFT-TITLE">Die Raddatz-Kinder</div>
|
||||||
|
<div class="DRAFT-META">Entwurf · zuletzt bearbeitet vor 5 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<svg width="7" height="7" viewBox="0 0 24 24" fill="none" stroke="#CCC" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Persons -->
|
||||||
|
<div>
|
||||||
|
<div class="PERSON-GRID">
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-AV" style="background:#002850">KR</div><div class="PCARD-NAME">Käthe Raddatz</div><div class="PCARD-BADGE">47 Dok.</div></a>
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-AV" style="background:#1A4A6B">ER</div><div class="PCARD-NAME">Ernst Raddatz</div><div class="PCARD-BADGE">31 Dok.</div></a>
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-AV" style="background:#3D5A7A">FM</div><div class="PCARD-NAME">Frieda Müller</div><div class="PCARD-BADGE">28 Dok.</div></a>
|
||||||
|
<a class="PCARD" href="#"><div class="PCARD-AV" style="background:#4A7A5A">HW</div><div class="PCARD-NAME">Heinrich Weber</div><div class="PCARD-BADGE">19 Dok.</div></a>
|
||||||
|
</div>
|
||||||
|
<div class="PERSONS-FOOTER"><a class="PERSONS-ALL" href="#">Alle 94 Personen →</a></div>
|
||||||
|
</div>
|
||||||
|
<!-- Content row (abbreviated) -->
|
||||||
|
<div class="CONTENT-ROW">
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Zuletzt aktualisiert</h3><a href="#">Alle Dokumente</a></div>
|
||||||
|
<div class="DOC-ROW"><div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div><div class="DOC-INFO"><div class="DOC-TITLE">Brief von Ernst an Käthe, März 1923</div><div class="DOC-SENDER">von <a href="#">Käthe Raddatz</a></div></div><div class="DOC-DATE">vor 2 Tagen</div></div>
|
||||||
|
<div class="DOC-ROW"><div class="DOC-THUMB"><svg width="8" height="10" viewBox="0 0 20 26" fill="none" stroke="#C8C4BE" stroke-width="1.5"><path d="M3 1h9l5 5v19H3V1z"/><polyline points="12 1 12 6 18 6"/></svg></div><div class="DOC-INFO"><div class="DOC-TITLE">Heiratsurkunde Raddatz-Müller, 1898</div><div class="DOC-SENDER" style="color:#E0DDD5">—</div></div><div class="DOC-DATE">vor 4 Tagen</div></div>
|
||||||
|
<div style="padding:5px 10px;font-size:6px;color:#C8C4BE;border-top:1px solid #F0EDE6">+ 3 weitere …</div>
|
||||||
|
</div>
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Geschichten</h3><a href="#">Alle</a></div>
|
||||||
|
<div class="STORY-ROW"><div class="STORY-ACCENT"></div><div class="STORY-BODY"><div class="STORY-TITLE">Die Reise nach Berlin</div><div class="STORY-EXCERPT">Im Frühjahr 1921 bestieg Ernst Raddatz erstmals den Zug nach Berlin …</div><div class="STORY-META">vor 3 Tagen</div></div></div>
|
||||||
|
<div class="STORY-ROW"><div class="STORY-ACCENT"></div><div class="STORY-BODY"><div class="STORY-TITLE">Sommer 1934 in Köln</div><div class="STORY-EXCERPT">Die Sommerferien 1934 verbrachte die Familie Raddatz bei Verwandten …</div><div class="STORY-META">vor 2 Wochen</div></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px;padding-top:40px">
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Entwürfe-Card — Navy Header auf Card</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Card-Header in #002850 mit Mint-Label — spiegelt den Haupt-Header-Balken</li>
|
||||||
|
<li>Schafft visuelle Verbindung: „Meine Entwürfe" gehört zur Autorenrolle, die auch im Gruß adressiert wird</li>
|
||||||
|
<li>Mint-Dot pro Entwurf-Zeile als einziges farbiges Element im White-Bereich der Card</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
480
docs/specs/reader-dashboard-c-entdecken.html
Normal file
480
docs/specs/reader-dashboard-c-entdecken.html
Normal file
@@ -0,0 +1,480 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Reader Dashboard — Concept C · Entdecken · Familienarchiv</title>
|
||||||
|
<style>
|
||||||
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||||
|
body{font-family:'Helvetica Neue',Arial,sans-serif;background:#ECEAE4;color:#1A1A1A;line-height:1.5}
|
||||||
|
.doc{max-width:1440px;margin:0 auto;padding:48px 32px}
|
||||||
|
|
||||||
|
/* ── Masthead ─── */
|
||||||
|
.mast{background:#0D2240;border-radius:10px;padding:32px 40px;margin-bottom:48px}
|
||||||
|
.mast-top{display:flex;align-items:flex-start;justify-content:space-between;gap:24px;margin-bottom:16px}
|
||||||
|
.mast h1{font-size:22px;font-weight:900;color:#fff;letter-spacing:-.4px;margin-bottom:6px}
|
||||||
|
.mast p{font-size:12px;color:rgba(255,255,255,.5);max-width:660px;line-height:1.7}
|
||||||
|
.mast-badge{font-size:9px;font-weight:800;padding:3px 9px;border-radius:20px;text-transform:uppercase;letter-spacing:.8px;white-space:nowrap;flex-shrink:0;margin-top:4px;background:#A6DAD8;color:#002850}
|
||||||
|
.decisions{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:20px;border-top:1px solid rgba(255,255,255,.1);padding-top:16px}
|
||||||
|
.dec{background:rgba(255,255,255,.06);border-radius:6px;padding:10px 12px}
|
||||||
|
.dec-label{font-size:7px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:rgba(255,255,255,.35);margin-bottom:5px}
|
||||||
|
.dec-value{font-size:9.5px;font-weight:700;color:#fff;line-height:1.5}
|
||||||
|
|
||||||
|
/* ── Section headings ─── */
|
||||||
|
.sec{margin-bottom:64px}
|
||||||
|
.sec+.sec{border-top:2px dashed #C8C4BE;padding-top:56px}
|
||||||
|
.sec-h{font-size:11px;font-weight:800;text-transform:uppercase;letter-spacing:1.2px;color:#888;margin-bottom:20px;display:flex;align-items:center;gap:10px}
|
||||||
|
.sec-h::after{content:'';flex:1;height:1px;background:#D8D4CE}
|
||||||
|
.sec-num{background:#0D2240;color:#fff;font-size:9px;font-weight:900;padding:2px 7px;border-radius:10px}
|
||||||
|
|
||||||
|
/* ── Screen grid ─── */
|
||||||
|
.sg{display:grid;gap:24px;align-items:start}
|
||||||
|
.sg-2{grid-template-columns:1fr 340px}
|
||||||
|
.sb{display:flex;flex-direction:column;gap:10px}
|
||||||
|
.sl{font-size:9px;font-weight:800;color:#888;text-transform:uppercase;letter-spacing:1.5px;margin-bottom:6px;display:flex;align-items:center;gap:6px}
|
||||||
|
.sz{background:#E8E4DF;color:#666;padding:1px 5px;border-radius:3px;font-size:8px}
|
||||||
|
|
||||||
|
/* ── Annotation callouts ─── */
|
||||||
|
.ann-block{background:#FFF7ED;border:1px solid #FDBA74;border-radius:5px;padding:8px 10px;font-size:10px;color:#7C2D12;line-height:1.5}
|
||||||
|
.ann-block strong{font-weight:800}
|
||||||
|
.ann-block ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
.note{background:#F0F9FF;border:1px solid #BAE6FD;border-radius:5px;padding:8px 10px;font-size:10px;color:#0C4A6E;line-height:1.5}
|
||||||
|
.note strong{font-weight:800}
|
||||||
|
.note ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
.ok{background:#F0FDF4;border:1px solid #86EFAC;border-radius:5px;padding:8px 10px;font-size:10px;color:#166534;line-height:1.5}
|
||||||
|
.ok strong{font-weight:800}
|
||||||
|
.ok ul{padding-left:14px;display:flex;flex-direction:column;gap:3px;margin-top:5px}
|
||||||
|
|
||||||
|
/* ── Mock browser chrome ─── */
|
||||||
|
.wf{background:#fff;border:2px solid #B8B4AE;border-radius:10px;overflow:hidden;box-shadow:0 4px 18px rgba(0,0,0,.08)}
|
||||||
|
.wf-bar{height:24px;background:#E8E4DF;border-bottom:1px solid #C8C4BE;display:flex;align-items:center;padding:0 9px;gap:4px}
|
||||||
|
.dot{width:7px;height:7px;border-radius:50%}
|
||||||
|
.dot.r{background:#F87171}.dot.y{background:#FCD34D}.dot.g{background:#4ADE80}
|
||||||
|
.urlbar{flex:1;height:11px;background:#D8D4CE;border-radius:3px;margin-left:6px;display:flex;align-items:center;padding:0 5px}
|
||||||
|
.urlbar span{font-size:7.5px;color:#888;font-family:monospace}
|
||||||
|
|
||||||
|
/* ── Nav bar ─── */
|
||||||
|
.N{height:34px;background:#002850;display:flex;align-items:center;padding:0 14px;gap:10px;flex-shrink:0}
|
||||||
|
.N-accent{height:2px;background:#A6DAD8}
|
||||||
|
.logo{font-size:7.5px;font-weight:900;color:#fff;letter-spacing:1px;text-transform:uppercase}
|
||||||
|
.nl{font-size:6.5px;color:rgba(255,255,255,.45);font-weight:700;text-transform:uppercase;letter-spacing:.5px;padding:3px 6px}
|
||||||
|
.nl.on{color:#fff}
|
||||||
|
.nr{margin-left:auto;display:flex;gap:5px;align-items:center}
|
||||||
|
.av{width:18px;height:18px;background:#A6DAD8;border-radius:50%;font-size:5.5px;font-weight:900;color:#002850;display:flex;align-items:center;justify-content:center}
|
||||||
|
.nico{width:18px;height:18px;background:rgba(255,255,255,.1);border-radius:3px;display:flex;align-items:center;justify-content:center}
|
||||||
|
|
||||||
|
/* ── Page body ─── */
|
||||||
|
.MAIN{padding:12px 16px;background:#F5F4EF;display:flex;flex-direction:column;gap:8px}
|
||||||
|
|
||||||
|
/* ── Greeting (inline, minimal) ─── */
|
||||||
|
.GREET-INLINE{display:flex;align-items:baseline;justify-content:space-between;padding:0 2px}
|
||||||
|
.GREET-TEXT{font-family:Georgia,serif;font-size:11px;color:#002850}
|
||||||
|
.GREET-TEXT span{color:#AAA;font-family:'Helvetica Neue',Arial,sans-serif;font-size:6.5px;margin-right:4px}
|
||||||
|
|
||||||
|
/* ── Stats strip (compact inline) ─── */
|
||||||
|
.STATS-SLIM{background:#fff;border:1px solid #E0DDD5;border-radius:3px;padding:6px 12px;display:flex;align-items:center;gap:0}
|
||||||
|
.SSTAT{display:flex;align-items:baseline;gap:4px;padding:0 12px;border-right:1px solid #F0EDE6;text-decoration:none}
|
||||||
|
.SSTAT:first-child{padding-left:0}
|
||||||
|
.SSTAT:last-child{border-right:none}
|
||||||
|
.SSTAT-NUM{font-size:13px;font-weight:900;color:#002850;line-height:1}
|
||||||
|
.SSTAT-LABEL{font-size:6px;font-weight:700;text-transform:uppercase;letter-spacing:.6px;color:#B8B4AE}
|
||||||
|
|
||||||
|
/* ── Person chips (horizontal, colored fill) ─── */
|
||||||
|
.PERSONS-BAND{display:flex;flex-direction:column;gap:5px}
|
||||||
|
.PERSONS-HEADER{display:flex;align-items:center;justify-content:space-between}
|
||||||
|
.PERSONS-TITLE{font-size:6.5px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#AAA}
|
||||||
|
.PERSONS-ALL-LINK{font-size:6.5px;color:#002850;font-weight:600;text-decoration:none;opacity:.6}
|
||||||
|
.CHIPS-COLORED{display:flex;flex-wrap:wrap;gap:5px}
|
||||||
|
.CHIP-COL{display:inline-flex;align-items:center;gap:5px;padding:4px 10px 4px 4px;border-radius:3px;text-decoration:none;background:#fff;border:1px solid #E0DDD5}
|
||||||
|
.CHIP-COL-AV{width:20px;height:20px;border-radius:2px;color:#fff;display:flex;align-items:center;justify-content:center;font-size:6px;font-weight:900;flex-shrink:0}
|
||||||
|
.CHIP-COL-BODY{}
|
||||||
|
.CHIP-COL-NAME{font-size:7px;font-weight:700;color:#002850;display:block;line-height:1.2}
|
||||||
|
.CHIP-COL-COUNT{font-size:5.5px;color:#AAA;display:block}
|
||||||
|
|
||||||
|
/* ── Two-column content row ─── */
|
||||||
|
.CONTENT-ROW{display:grid;grid-template-columns:2fr 3fr;gap:6px}
|
||||||
|
|
||||||
|
/* ── Cards ─── */
|
||||||
|
.CARD{background:#fff;border:1px solid #E0DDD5;border-radius:3px;overflow:hidden;display:flex;flex-direction:column}
|
||||||
|
.CARD-HEAD{display:flex;align-items:center;justify-content:space-between;padding:7px 10px 6px;border-bottom:1px solid #E0DDD5}
|
||||||
|
.CARD-HEAD h3{font-size:6.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#999}
|
||||||
|
.CARD-HEAD a{font-size:6.5px;font-weight:600;color:#002850;opacity:.4;text-decoration:none}
|
||||||
|
|
||||||
|
/* ── Doc rows (slim) ─── */
|
||||||
|
.DOC-ROW-SLIM{display:flex;align-items:baseline;justify-content:space-between;gap:6px;padding:5px 10px;border-bottom:1px solid #F0EDE6}
|
||||||
|
.DOC-ROW-SLIM:last-child{border-bottom:none}
|
||||||
|
.DOC-TITLE-SLIM{font-family:Georgia,serif;font-size:7.5px;color:#002850;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.DOC-DATE-SLIM{font-size:6px;color:#C8C4BE;white-space:nowrap;flex-shrink:0}
|
||||||
|
|
||||||
|
/* ── Stories: Featured + slim list ─── */
|
||||||
|
.STORY-FEATURED{padding:9px 10px;border-bottom:2px solid #F0EDE6;background:#FDFAF5}
|
||||||
|
.STORY-FEATURED-LABEL{font-size:5.5px;font-weight:800;text-transform:uppercase;letter-spacing:.8px;color:#A6DAD8;margin-bottom:4px}
|
||||||
|
.STORY-FEATURED-TITLE{font-family:Georgia,serif;font-size:10px;color:#002850;font-style:italic;margin-bottom:4px;line-height:1.4}
|
||||||
|
.STORY-FEATURED-EXCERPT{font-size:7px;color:#888;line-height:1.55;margin-bottom:4px;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
|
||||||
|
.STORY-FEATURED-META{font-size:6px;color:#B8B4AE}
|
||||||
|
|
||||||
|
.STORY-ROW-SLIM{display:flex;align-items:baseline;gap:6px;padding:5px 10px;border-bottom:1px solid #F0EDE6}
|
||||||
|
.STORY-ROW-SLIM:last-child{border-bottom:none}
|
||||||
|
.STORY-TITLE-SLIM{font-family:Georgia,serif;font-size:7.5px;color:#002850;font-style:italic;flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.STORY-DATE-SLIM{font-size:6px;color:#C8C4BE;white-space:nowrap;flex-shrink:0}
|
||||||
|
|
||||||
|
/* ── Spec disclaimer ─── */
|
||||||
|
.spec-disclaimer{background:#FFF8E1;border:1.5px solid #FFC107;border-radius:6px;padding:11px 16px;font-size:11px;color:#6D4C00;margin-bottom:32px;line-height:1.6}
|
||||||
|
.spec-disclaimer strong{font-weight:800}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="doc">
|
||||||
|
|
||||||
|
<!-- ══ MASTHEAD ══ -->
|
||||||
|
<div class="mast">
|
||||||
|
<div class="mast-top">
|
||||||
|
<div>
|
||||||
|
<h1>Reader Dashboard — Konzept C · „Entdecken"</h1>
|
||||||
|
<p>Leseorientierte Gestaltung — Geschichten stehen im Mittelpunkt. Die Statistik ist auf ein schlankes Querband reduziert. Personen erscheinen als flache quadratische Chips mit vollfarbigem Initialen-Feld. Die aktuellste Geschichte erhält einen eigenen Featured-Block mit Auszug; zwei weitere folgen als kompakte Liste.</p>
|
||||||
|
</div>
|
||||||
|
<span class="mast-badge">Konzept C · Entwurf</span>
|
||||||
|
</div>
|
||||||
|
<div class="decisions">
|
||||||
|
<div class="dec">
|
||||||
|
<div class="dec-label">Schwerpunkt</div>
|
||||||
|
<div class="dec-value">Geschichten als primäres Erlebnis</div>
|
||||||
|
</div>
|
||||||
|
<div class="dec">
|
||||||
|
<div class="dec-label">Statistik</div>
|
||||||
|
<div class="dec-value">Inline-Strip — Text statt Kacheln</div>
|
||||||
|
</div>
|
||||||
|
<div class="dec">
|
||||||
|
<div class="dec-label">Personen-Chips</div>
|
||||||
|
<div class="dec-value">Quadratischer Avatar + Name + Zahl</div>
|
||||||
|
</div>
|
||||||
|
<div class="dec">
|
||||||
|
<div class="dec-label">Geschichten</div>
|
||||||
|
<div class="dec-value">Featured Block (3 Zeilen Auszug) + 2 slim</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="spec-disclaimer">
|
||||||
|
<strong>📐 Mockup-Skalierung —</strong> alle Schriftgrößen, Abstände und Höhen im Mockup sind auf ca. 55 % der tatsächlichen Implementierungswerte skaliert. <strong>Werte nicht aus dem Mockup-CSS kopieren.</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══ SECTION 1: DESKTOP, REINER LESER ══ -->
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-h"><span class="sec-num">1</span> Desktop · Leser ohne BLOG_WRITE (READ_ALL only)</div>
|
||||||
|
|
||||||
|
<div class="sg sg-2">
|
||||||
|
<div class="sb">
|
||||||
|
<div class="sl">Vollansicht <span class="sz">≥ 1024 px</span></div>
|
||||||
|
|
||||||
|
<div class="wf">
|
||||||
|
<div class="wf-bar">
|
||||||
|
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
|
||||||
|
<div class="urlbar"><span>familienarchiv.local /</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nav -->
|
||||||
|
<div class="N">
|
||||||
|
<span class="logo">Familienarchiv</span>
|
||||||
|
<span class="nl on">Startseite</span>
|
||||||
|
<span class="nl">Dokumente</span>
|
||||||
|
<span class="nl">Personen</span>
|
||||||
|
<span class="nl">Geschichten</span>
|
||||||
|
<div class="nr">
|
||||||
|
<div class="nico">
|
||||||
|
<svg width="9" height="9" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,.5)" stroke-width="2"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="av">BK</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="N-accent"></div>
|
||||||
|
|
||||||
|
<div class="MAIN">
|
||||||
|
|
||||||
|
<!-- Zone 1: Greeting (inline minimal) -->
|
||||||
|
<div class="GREET-INLINE">
|
||||||
|
<div class="GREET-TEXT">
|
||||||
|
<span>Guten Abend —</span>Herzlich willkommen, Brigitte.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 2: Stats (compact band) -->
|
||||||
|
<div class="STATS-SLIM">
|
||||||
|
<a class="SSTAT" href="#">
|
||||||
|
<span class="SSTAT-NUM">847</span>
|
||||||
|
<span class="SSTAT-LABEL">Dokumente</span>
|
||||||
|
</a>
|
||||||
|
<a class="SSTAT" href="#">
|
||||||
|
<span class="SSTAT-NUM">94</span>
|
||||||
|
<span class="SSTAT-LABEL">Personen</span>
|
||||||
|
</a>
|
||||||
|
<a class="SSTAT" href="#">
|
||||||
|
<span class="SSTAT-NUM">12</span>
|
||||||
|
<span class="SSTAT-LABEL">Geschichten</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 4: Person chips (colored square avatar) -->
|
||||||
|
<div class="PERSONS-BAND">
|
||||||
|
<div class="PERSONS-HEADER">
|
||||||
|
<span class="PERSONS-TITLE">Personen im Fokus</span>
|
||||||
|
<a class="PERSONS-ALL-LINK" href="#">Alle 94 Personen →</a>
|
||||||
|
</div>
|
||||||
|
<div class="CHIPS-COLORED">
|
||||||
|
<a class="CHIP-COL" href="#">
|
||||||
|
<div class="CHIP-COL-AV" style="background:#002850">KR</div>
|
||||||
|
<div class="CHIP-COL-BODY">
|
||||||
|
<span class="CHIP-COL-NAME">Käthe Raddatz</span>
|
||||||
|
<span class="CHIP-COL-COUNT">47 Dok.</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="CHIP-COL" href="#">
|
||||||
|
<div class="CHIP-COL-AV" style="background:#1A4A6B">ER</div>
|
||||||
|
<div class="CHIP-COL-BODY">
|
||||||
|
<span class="CHIP-COL-NAME">Ernst Raddatz</span>
|
||||||
|
<span class="CHIP-COL-COUNT">31 Dok.</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="CHIP-COL" href="#">
|
||||||
|
<div class="CHIP-COL-AV" style="background:#3D5A7A">FM</div>
|
||||||
|
<div class="CHIP-COL-BODY">
|
||||||
|
<span class="CHIP-COL-NAME">Frieda Müller</span>
|
||||||
|
<span class="CHIP-COL-COUNT">28 Dok.</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="CHIP-COL" href="#">
|
||||||
|
<div class="CHIP-COL-AV" style="background:#4A7A5A">HW</div>
|
||||||
|
<div class="CHIP-COL-BODY">
|
||||||
|
<span class="CHIP-COL-NAME">Heinrich Weber</span>
|
||||||
|
<span class="CHIP-COL-COUNT">19 Dok.</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 5: Docs LEFT (slim), Stories RIGHT (featured) — 2:3 -->
|
||||||
|
<div class="CONTENT-ROW">
|
||||||
|
|
||||||
|
<!-- Left: Docs slim list -->
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD">
|
||||||
|
<h3>Zuletzt aktualisiert</h3>
|
||||||
|
<a href="#">Alle Dokumente</a>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW-SLIM">
|
||||||
|
<div class="DOC-TITLE-SLIM">Brief von Ernst an Käthe, März 1923</div>
|
||||||
|
<div class="DOC-DATE-SLIM">vor 2 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW-SLIM">
|
||||||
|
<div class="DOC-TITLE-SLIM">Heiratsurkunde Raddatz-Müller, 1898</div>
|
||||||
|
<div class="DOC-DATE-SLIM">vor 4 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW-SLIM">
|
||||||
|
<div class="DOC-TITLE-SLIM">Familienfoto, Sommer 1928</div>
|
||||||
|
<div class="DOC-DATE-SLIM">vor 1 Woche</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW-SLIM">
|
||||||
|
<div class="DOC-TITLE-SLIM">Taufregister Heinrich Weber, 1902</div>
|
||||||
|
<div class="DOC-DATE-SLIM">vor 2 Wo.</div>
|
||||||
|
</div>
|
||||||
|
<div class="DOC-ROW-SLIM">
|
||||||
|
<div class="DOC-TITLE-SLIM">Postkarte aus Berlin, 1930</div>
|
||||||
|
<div class="DOC-DATE-SLIM">vor 3 Wo.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Stories — Featured + slim -->
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD">
|
||||||
|
<h3>Geschichten</h3>
|
||||||
|
<a href="#">Alle Geschichten</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Featured story -->
|
||||||
|
<div class="STORY-FEATURED">
|
||||||
|
<div class="STORY-FEATURED-LABEL">Neueste Geschichte</div>
|
||||||
|
<div class="STORY-FEATURED-TITLE">Die Reise nach Berlin</div>
|
||||||
|
<div class="STORY-FEATURED-EXCERPT">Im Frühjahr 1921 bestieg Ernst Raddatz erstmals den Zug nach Berlin, um Arbeit in der aufstrebenden Metropole zu finden. Was als abenteuerliche Reise begann, wurde zur Schicksalswende für die gesamte Familie …</div>
|
||||||
|
<div class="STORY-FEATURED-META">vor 3 Tagen</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Slim list -->
|
||||||
|
<div class="STORY-ROW-SLIM">
|
||||||
|
<div class="STORY-TITLE-SLIM">Sommer 1934 in Köln</div>
|
||||||
|
<div class="STORY-DATE-SLIM">vor 2 Wochen</div>
|
||||||
|
</div>
|
||||||
|
<div class="STORY-ROW-SLIM">
|
||||||
|
<div class="STORY-TITLE-SLIM">Briefe aus dem Krieg</div>
|
||||||
|
<div class="STORY-DATE-SLIM">vor 1 Monat</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Annotations -->
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px;padding-top:40px">
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Zone 1 — Minimale Begrüßung</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Kein eigener Card-Container — reine Textzeile, platzsparend</li>
|
||||||
|
<li>„Guten Abend —" in gedämpftem Grau vor Serif-Begrüßungstext</li>
|
||||||
|
<li>Beibehaltung der Tageszeit-Dynamik ohne visuelle Lautstärke</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Zone 2 — Kompakter Stats-Strip</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Zahlen inline mit Label — keine separaten Kacheln</li>
|
||||||
|
<li>Gleicher Informationsgehalt wie Konzept A/B, aber ~60 % weniger Höhe</li>
|
||||||
|
<li>Klickbar als vollständige <a>-Elemente</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="note">
|
||||||
|
<strong>Zone 4 — Quadratische Personen-Chips</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Quadratischer Avatar (border-radius: 2 px statt 50 %) — wirkt archiv-artig, leicht dokumentarisch</li>
|
||||||
|
<li>Name + Dokumentzahl übereinander — kompakt und lesbar</li>
|
||||||
|
<li>Chips wrappen bei schmalem Viewport automatisch</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="note">
|
||||||
|
<strong>Zone 5 — Geschichten als Featured Block</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Neueste Geschichte erhält sand-getönten Hintergrund + 3-Zeilen-Auszug</li>
|
||||||
|
<li>Mint-Label „Neueste Geschichte" differenziert vom restlichen Geschichten-Content</li>
|
||||||
|
<li>Die zwei weiteren Geschichten folgen als slim-Zeilen → reduziert auf Titel + Datum</li>
|
||||||
|
<li>Dokumente-Spalte ist bewusst schlanker (2:3) — gibt Geschichten Raum</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="ok">
|
||||||
|
<strong>Stärken dieses Konzepts</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Stärkt die narrative Dimension des Archivs — Geschichten als Hauptfenster</li>
|
||||||
|
<li>Featured-Block lädt zum Lesen ein, ohne dass ein gesonderter Story-Bereich nötig ist</li>
|
||||||
|
<li>Kompaktester Header → mehr Inhalt „above the fold"</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ══ SECTION 2: BLOG_WRITE VARIANT ══ -->
|
||||||
|
<div class="sec">
|
||||||
|
<div class="sec-h"><span class="sec-num">2</span> Variante · BLOG_WRITE — mit Zone 3 „Meine Entwürfe"</div>
|
||||||
|
|
||||||
|
<div class="sg sg-2">
|
||||||
|
<div class="sb">
|
||||||
|
<div class="sl">BLOG_WRITE-Nutzer <span class="sz">READ_ALL + BLOG_WRITE</span></div>
|
||||||
|
|
||||||
|
<div class="wf">
|
||||||
|
<div class="wf-bar">
|
||||||
|
<div class="dot r"></div><div class="dot y"></div><div class="dot g"></div>
|
||||||
|
<div class="urlbar"><span>familienarchiv.local /</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="N">
|
||||||
|
<span class="logo">Familienarchiv</span>
|
||||||
|
<span class="nl on">Startseite</span>
|
||||||
|
<span class="nl">Dokumente</span>
|
||||||
|
<span class="nl">Personen</span>
|
||||||
|
<span class="nl">Geschichten</span>
|
||||||
|
<div class="nr"><div class="av">MR</div></div>
|
||||||
|
</div>
|
||||||
|
<div class="N-accent"></div>
|
||||||
|
<div class="MAIN">
|
||||||
|
|
||||||
|
<!-- Zone 1 inline -->
|
||||||
|
<div class="GREET-INLINE">
|
||||||
|
<div class="GREET-TEXT"><span>Guten Morgen —</span>Herzlich willkommen, Marcel.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 2 stats -->
|
||||||
|
<div class="STATS-SLIM">
|
||||||
|
<a class="SSTAT" href="#"><span class="SSTAT-NUM">847</span><span class="SSTAT-LABEL">Dokumente</span></a>
|
||||||
|
<a class="SSTAT" href="#"><span class="SSTAT-NUM">94</span><span class="SSTAT-LABEL">Personen</span></a>
|
||||||
|
<a class="SSTAT" href="#"><span class="SSTAT-NUM">12</span><span class="SSTAT-LABEL">Geschichten</span></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 3: Drafts -->
|
||||||
|
<div style="background:#fff;border:1px solid #E0DDD5;border-radius:3px;overflow:hidden">
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:6px 10px 5px;border-bottom:1px solid #E0DDD5;background:rgba(166,218,216,.08)">
|
||||||
|
<div style="display:flex;align-items:center;gap:5px">
|
||||||
|
<div style="width:2px;height:12px;background:#A6DAD8;border-radius:1px"></div>
|
||||||
|
<span style="font-size:6.5px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:#999">Meine Entwürfe</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:5px 10px;border-bottom:1px solid #F0EDE6">
|
||||||
|
<div>
|
||||||
|
<div style="font-family:Georgia,serif;font-size:8px;color:#002850">Onkel Friedrichs Wanderjahre</div>
|
||||||
|
<div style="font-size:6px;color:#AAA;margin-top:1px">Entwurf · zuletzt bearbeitet vor 1 Tag</div>
|
||||||
|
</div>
|
||||||
|
<svg width="7" height="7" viewBox="0 0 24 24" fill="none" stroke="#CCC" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;padding:5px 10px">
|
||||||
|
<div>
|
||||||
|
<div style="font-family:Georgia,serif;font-size:8px;color:#002850">Die Raddatz-Kinder</div>
|
||||||
|
<div style="font-size:6px;color:#AAA;margin-top:1px">Entwurf · zuletzt bearbeitet vor 5 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<svg width="7" height="7" viewBox="0 0 24 24" fill="none" stroke="#CCC" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 4 -->
|
||||||
|
<div class="PERSONS-BAND">
|
||||||
|
<div class="PERSONS-HEADER">
|
||||||
|
<span class="PERSONS-TITLE">Personen im Fokus</span>
|
||||||
|
<a class="PERSONS-ALL-LINK" href="#">Alle 94 Personen →</a>
|
||||||
|
</div>
|
||||||
|
<div class="CHIPS-COLORED">
|
||||||
|
<a class="CHIP-COL" href="#"><div class="CHIP-COL-AV" style="background:#002850">KR</div><div class="CHIP-COL-BODY"><span class="CHIP-COL-NAME">Käthe Raddatz</span><span class="CHIP-COL-COUNT">47 Dok.</span></div></a>
|
||||||
|
<a class="CHIP-COL" href="#"><div class="CHIP-COL-AV" style="background:#1A4A6B">ER</div><div class="CHIP-COL-BODY"><span class="CHIP-COL-NAME">Ernst Raddatz</span><span class="CHIP-COL-COUNT">31 Dok.</span></div></a>
|
||||||
|
<a class="CHIP-COL" href="#"><div class="CHIP-COL-AV" style="background:#3D5A7A">FM</div><div class="CHIP-COL-BODY"><span class="CHIP-COL-NAME">Frieda Müller</span><span class="CHIP-COL-COUNT">28 Dok.</span></div></a>
|
||||||
|
<a class="CHIP-COL" href="#"><div class="CHIP-COL-AV" style="background:#4A7A5A">HW</div><div class="CHIP-COL-BODY"><span class="CHIP-COL-NAME">Heinrich Weber</span><span class="CHIP-COL-COUNT">19 Dok.</span></div></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Zone 5 (abbreviated) -->
|
||||||
|
<div class="CONTENT-ROW">
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Zuletzt aktualisiert</h3><a href="#">Alle Dokumente</a></div>
|
||||||
|
<div class="DOC-ROW-SLIM"><div class="DOC-TITLE-SLIM">Brief von Ernst an Käthe, März 1923</div><div class="DOC-DATE-SLIM">vor 2 Tagen</div></div>
|
||||||
|
<div class="DOC-ROW-SLIM"><div class="DOC-TITLE-SLIM">Heiratsurkunde Raddatz-Müller, 1898</div><div class="DOC-DATE-SLIM">vor 4 Tagen</div></div>
|
||||||
|
<div class="DOC-ROW-SLIM"><div class="DOC-TITLE-SLIM">Familienfoto, Sommer 1928</div><div class="DOC-DATE-SLIM">vor 1 Woche</div></div>
|
||||||
|
<div style="padding:5px 10px;font-size:6px;color:#C8C4BE;border-top:1px solid #F0EDE6">+ 2 weitere …</div>
|
||||||
|
</div>
|
||||||
|
<div class="CARD">
|
||||||
|
<div class="CARD-HEAD"><h3>Geschichten</h3><a href="#">Alle</a></div>
|
||||||
|
<div class="STORY-FEATURED" style="background:#FDFAF5">
|
||||||
|
<div class="STORY-FEATURED-LABEL">Neueste Geschichte</div>
|
||||||
|
<div class="STORY-FEATURED-TITLE">Die Reise nach Berlin</div>
|
||||||
|
<div class="STORY-FEATURED-EXCERPT">Im Frühjahr 1921 bestieg Ernst Raddatz erstmals den Zug nach Berlin, um Arbeit in der aufstrebenden Metropole zu finden …</div>
|
||||||
|
<div class="STORY-FEATURED-META">vor 3 Tagen</div>
|
||||||
|
</div>
|
||||||
|
<div class="STORY-ROW-SLIM"><div class="STORY-TITLE-SLIM">Sommer 1934 in Köln</div><div class="STORY-DATE-SLIM">vor 2 Wochen</div></div>
|
||||||
|
<div class="STORY-ROW-SLIM"><div class="STORY-TITLE-SLIM">Briefe aus dem Krieg</div><div class="STORY-DATE-SLIM">vor 1 Monat</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-direction:column;gap:10px;padding-top:40px">
|
||||||
|
<div class="ann-block">
|
||||||
|
<strong>Zone 3 — Entwürfe mit Mint-Vertikallinie</strong>
|
||||||
|
<ul>
|
||||||
|
<li>Mintfarbene 2 px Vertikallinie links neben dem Card-Titel — diskreter als ein Randakzent</li>
|
||||||
|
<li>Leicht mintfarbenes Card-Header-Background (rgba(166,218,216,.08)) grenzt Zone ab</li>
|
||||||
|
<li>Gut integriert in den kompakten Gesamtstil von Konzept C</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1055
docs/specs/reader-dashboard-final.html
Normal file
1055
docs/specs/reader-dashboard-final.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -30,11 +30,18 @@ src/
|
|||||||
│ ├── documents/ # Document CRUD, detail, edit, upload
|
│ ├── documents/ # Document CRUD, detail, edit, upload
|
||||||
│ ├── persons/ # Person directory, detail, edit, merge
|
│ ├── persons/ # Person directory, detail, edit, merge
|
||||||
│ ├── briefwechsel/ # Bilateral conversation timeline
|
│ ├── briefwechsel/ # Bilateral conversation timeline
|
||||||
│ ├── chronik/ # Unified activity feed
|
│ ├── aktivitaeten/ # Unified activity feed (Chronik)
|
||||||
│ ├── admin/ # User, group, tag, OCR, system management
|
│ ├── admin/ # User, group, tag, OCR, system management
|
||||||
│ ├── api/ # Internal API proxies (server-side only)
|
│ ├── api/ # Internal API proxies (server-side only)
|
||||||
│ ├── login/ logout/ # Auth pages
|
│ ├── geschichten/ # Stories (list, [id], [id]/edit, new)
|
||||||
│ └── ...
|
│ ├── stammbaum/ # Family tree
|
||||||
|
│ ├── enrich/ # Enrichment workflow ([id], done)
|
||||||
|
│ ├── hilfe/transkription/ # Transcription help page
|
||||||
|
│ ├── profile/ # User profile settings
|
||||||
|
│ ├── users/[id]/ # Public user profile page
|
||||||
|
│ ├── login/ logout/ register/
|
||||||
|
│ ├── forgot-password/ reset-password/
|
||||||
|
│ └── demo/ # Dev-only demos
|
||||||
├── lib/ # Domain-based package structure (mirrors backend)
|
├── lib/ # Domain-based package structure (mirrors backend)
|
||||||
│ ├── document/ # Document domain: components, stores, services, utils
|
│ ├── document/ # Document domain: components, stores, services, utils
|
||||||
│ │ ├── annotation/ # Annotation overlay components
|
│ │ ├── annotation/ # Annotation overlay components
|
||||||
@@ -71,29 +78,13 @@ src/
|
|||||||
└── ... # Other SvelteKit config files
|
└── ... # Other SvelteKit config files
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For per-domain component inventories, see the domain READMEs in `src/lib/<domain>/README.md`.
|
||||||
|
|
||||||
## API Client Pattern
|
## API Client Pattern
|
||||||
|
|
||||||
All server-side API calls use the typed client from `$lib/api.server.ts`:
|
→ See [CONTRIBUTING.md §Frontend API client](../CONTRIBUTING.md#frontend-api-client)
|
||||||
|
|
||||||
```typescript
|
**LLM reminder:** check `!result.response.ok` (not `result.error` — breaks when spec has no error responses); cast errors as `result.error as unknown as { code?: string }`; use `result.data!` after an ok check. For multipart/form-data (file uploads), bypass the typed client and use raw `fetch`.
|
||||||
const api = createApiClient(fetch);
|
|
||||||
const result = await api.GET('/api/persons/{id}', { params: { path: { id } } });
|
|
||||||
|
|
||||||
// Always check via response.ok, NOT result.error
|
|
||||||
if (!result.response.ok) {
|
|
||||||
const code = (result.error as unknown as { code?: string })?.code;
|
|
||||||
throw error(result.response.status, getErrorMessage(code));
|
|
||||||
}
|
|
||||||
return { person: result.data! };
|
|
||||||
```
|
|
||||||
|
|
||||||
Key rules:
|
|
||||||
|
|
||||||
- Use `!result.response.ok` for error checking (not `if (result.error)` — breaks when spec has no error responses defined)
|
|
||||||
- Cast errors as `result.error as unknown as { code?: string }` to extract backend error code
|
|
||||||
- Use `result.data!` after an ok check
|
|
||||||
|
|
||||||
For multipart/form-data (file uploads), bypass the typed client and use raw `fetch`.
|
|
||||||
|
|
||||||
## Form Actions Pattern
|
## Form Actions Pattern
|
||||||
|
|
||||||
@@ -102,7 +93,7 @@ For multipart/form-data (file uploads), bypass the typed client and use raw `fet
|
|||||||
export const actions = {
|
export const actions = {
|
||||||
default: async ({ request, fetch }) => {
|
default: async ({ request, fetch }) => {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const name = formData.get('name') as string;
|
const name = formData.get('name') as string; // cast needed — FormData returns FormDataEntryValue
|
||||||
// ...
|
// ...
|
||||||
return fail(400, { error: 'message' }); // on error
|
return fail(400, { error: 'message' }); // on error
|
||||||
throw redirect(303, '/target'); // on success
|
throw redirect(303, '/target'); // on success
|
||||||
@@ -112,49 +103,39 @@ export const actions = {
|
|||||||
|
|
||||||
## Date Handling
|
## Date Handling
|
||||||
|
|
||||||
- **Forms**: German format `dd.mm.yyyy` with auto-dot insertion via `handleDateInput()`. A hidden `<input type="hidden" name="documentDate" value={dateIso}>` sends ISO to the backend.
|
→ See [CONTRIBUTING.md §Date handling](../CONTRIBUTING.md#date-handling)
|
||||||
- **Display**: Always use `Intl.DateTimeFormat` with `T12:00:00` suffix to prevent UTC off-by-one:
|
|
||||||
```typescript
|
**LLM reminder:** always append `T12:00:00` when constructing `new Date()` from an ISO date string — prevents UTC timezone off-by-one errors. Forms use German `dd.mm.yyyy` format via `handleDateInput()` with a hidden ISO input.
|
||||||
new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'long', year: 'numeric' }).format(
|
|
||||||
new Date(doc.documentDate + 'T12:00:00')
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Styling Conventions (Tailwind CSS 4)
|
## Styling Conventions (Tailwind CSS 4)
|
||||||
|
|
||||||
Brand color utilities (defined in `layout.css`):
|
Brand color tokens (defined in `layout.css`):
|
||||||
|
|
||||||
| Class | Value | Usage |
|
| Token / Utility | CSS variable | Usage |
|
||||||
| ------------ | --------- | -------------------------------- |
|
| ---------------- | ---------------- | ------------------------------------------------------- |
|
||||||
| `brand-navy` | `#002850` | Primary text, buttons, headers |
|
| `brand-navy` | `--palette-navy` | Tailwind utility — buttons, headers, primary text |
|
||||||
| `brand-mint` | `#A6DAD8` | Accents, hover underlines, icons |
|
| `brand-mint` | `--palette-mint` | Tailwind utility — accents, hover underlines, icons |
|
||||||
| `brand-sand` | `#E4E2D7` | Page background, card borders |
|
| `--palette-sand` | `--palette-sand` | Palette constant only — use `bg-canvas` or `bg-surface` |
|
||||||
|
|
||||||
Typography:
|
Typography:
|
||||||
|
|
||||||
- `font-serif` (Merriweather) — body text, document titles, names
|
- `font-serif` (Tinos) — body text, document titles, names
|
||||||
- `font-sans` (Montserrat) — labels, metadata, UI chrome
|
- `font-sans` (Montserrat) — labels, metadata, UI chrome
|
||||||
|
|
||||||
Card pattern for content sections:
|
Card pattern for content sections:
|
||||||
|
|
||||||
```svelte
|
```svelte
|
||||||
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
|
<div class="rounded-sm border border-line bg-surface shadow-sm p-6">
|
||||||
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Section</h2>
|
<h2 class="text-xs font-bold uppercase tracking-widest text-ink-3 mb-5">Section</h2>
|
||||||
<!-- content -->
|
<!-- content -->
|
||||||
</div>
|
</div>
|
||||||
```
|
```
|
||||||
|
|
||||||
## Key UI Components
|
## Key UI Components
|
||||||
|
|
||||||
| Component | Location | Props | Description |
|
→ See per-domain READMEs: [`src/lib/person/README.md`](src/lib/person/README.md), [`src/lib/tag/README.md`](src/lib/tag/README.md), [`src/lib/document/README.md`](src/lib/document/README.md), [`src/lib/shared/README.md`](src/lib/shared/README.md)
|
||||||
| -------------------- | ------------------------------ | --------------------------------------- | ------------------------------------------ |
|
|
||||||
| `PersonTypeahead` | `$lib/person/` | `name`, `label`, `value`, `initialName` | Single-person selector with typeahead |
|
**LLM reminder:** `BackButton` is at `$lib/shared/primitives/BackButton.svelte` — use it for all back navigation; never a static `<a href>`. API client is at `$lib/shared/api.server`.
|
||||||
| `PersonMultiSelect` | `$lib/person/` | `selectedPersons` (bind) | Chip-based multi-person selector |
|
|
||||||
| `TagInput` | `$lib/tag/` | `tags` (bind), `allowCreation?` | Tag chip input with typeahead |
|
|
||||||
| `PdfViewer` | `$lib/document/` | `url`, `annotations` | PDF rendering with annotation overlay |
|
|
||||||
| `TranscriptionBlock` | `$lib/document/transcription/` | `block`, `mode` | Read/edit transcription block |
|
|
||||||
| `DocumentTopBar` | `$lib/document/` | `document` | Responsive document metadata header |
|
|
||||||
| `BackButton` | `$lib/shared/primitives/` | — | Calls `history.back()`; 44 px touch target |
|
|
||||||
|
|
||||||
## How to Run
|
## How to Run
|
||||||
|
|
||||||
@@ -163,7 +144,7 @@ Card pattern for content sections:
|
|||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install
|
npm install
|
||||||
npm run dev # Dev server on port 5173 (or 3000 if --port 3000)
|
npm run dev # Dev server on port 5173
|
||||||
```
|
```
|
||||||
|
|
||||||
### Build & Preview
|
### Build & Preview
|
||||||
|
|||||||
36
frontend/src/lib/document/README.md
Normal file
36
frontend/src/lib/document/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# document (frontend)
|
||||||
|
|
||||||
|
UI for the archive's core concept: viewing, uploading, editing, searching, bulk-selecting, and transcribing documents.
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Components: `DocumentRow`, `DocumentThumbnail`, `DocumentTopBar`, `DocumentViewer`, `DocumentMetadataDrawer`, `DocumentEditLayout`, `DocumentStatusChip`, `UploadZone`, `BulkSelectionBar`, `BulkDropZone`.
|
||||||
|
Utilities: `search.ts` (search-param helpers), `filename.ts` (filename formatting), `documentStatusLabel.ts` (i18n label mapping), `validateFile.ts` (upload validation), `groupDocuments.ts` (list grouping).
|
||||||
|
Sub-folders: `annotation/`, `transcription/`, `viewer/`.
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
- Person typeahead — `person/PersonTypeahead.svelte` (cross-domain import, allowed by ESLint rule)
|
||||||
|
- Tag input — `tag/TagInput.svelte` (cross-domain import, allowed)
|
||||||
|
- Shared discussion — `shared/discussion/` (comment/mention editor)
|
||||||
|
|
||||||
|
## Key components
|
||||||
|
|
||||||
|
| Component | Route used in | Notes |
|
||||||
|
| --------------------------- | ---------------------------------- | ------------------------------------ |
|
||||||
|
| `DocumentRow.svelte` | `/` (search results), admin queues | Compact document card with thumbnail |
|
||||||
|
| `DocumentViewer.svelte` | `/documents/[id]` | PDF/image inline viewer |
|
||||||
|
| `DocumentEditLayout.svelte` | `/documents/[id]/edit` | Full edit form with sticky save bar |
|
||||||
|
| `UploadZone.svelte` | `/documents/new`, bulk upload | Drag-and-drop file drop area |
|
||||||
|
| `BulkSelectionBar.svelte` | `/documents` bulk mode | Multi-select action bar |
|
||||||
|
|
||||||
|
## Cross-domain imports
|
||||||
|
|
||||||
|
- `person/PersonTypeahead.svelte` — sender / receiver selection
|
||||||
|
- `tag/TagInput.svelte` — tag chip input
|
||||||
|
- `ocr/OcrProgress.svelte` — job status indicator in the document header
|
||||||
|
- `shared/primitives/BackButton.svelte`, `shared/discussion/` — shared UI
|
||||||
|
|
||||||
|
## Backend counterpart
|
||||||
|
|
||||||
|
`backend/src/main/java/org/raddatz/familienarchiv/document/README.md`
|
||||||
34
frontend/src/lib/geschichte/README.md
Normal file
34
frontend/src/lib/geschichte/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# geschichte (frontend)
|
||||||
|
|
||||||
|
UI for family stories: the rich-text editor, story cards, and story list view.
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Components: `GeschichteEditor.svelte`, `GeschichtenCard.svelte`.
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
- Comment/discussion UI — shared via `shared/discussion/` (same component used for document comments)
|
||||||
|
- Person display — `person/PersonChip.svelte` is used inside story content (cross-domain import)
|
||||||
|
- Document display — document references in stories use components from `document/`
|
||||||
|
|
||||||
|
## Key components
|
||||||
|
|
||||||
|
| Component | Used in | Notes |
|
||||||
|
| ------------------------- | -------------------------------------------- | ------------------------------------------------------------------ |
|
||||||
|
| `GeschichteEditor.svelte` | `/geschichten/new`, `/geschichten/[id]/edit` | Rich-text editor with person/document @-mentions and inline embeds |
|
||||||
|
| `GeschichtenCard.svelte` | `/geschichten` (list), dashboard | Story preview card with cover image and publish status |
|
||||||
|
|
||||||
|
## Audience note
|
||||||
|
|
||||||
|
The `/geschichten` route primarily serves readers (younger family members on mobile). Cards must have ≥ 44 px touch targets. Status must not rely on color alone.
|
||||||
|
|
||||||
|
## Cross-domain imports
|
||||||
|
|
||||||
|
- `person/PersonChip.svelte` — inline person references in story content
|
||||||
|
- `document/DocumentThumbnail.svelte` — inline document references
|
||||||
|
- `shared/discussion/` — comment thread below published stories
|
||||||
|
|
||||||
|
## Backend counterpart
|
||||||
|
|
||||||
|
`backend/src/main/java/org/raddatz/familienarchiv/geschichte/README.md`
|
||||||
36
frontend/src/lib/notification/README.md
Normal file
36
frontend/src/lib/notification/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# notification (frontend)
|
||||||
|
|
||||||
|
Bell-icon dropdown and real-time SSE connection for in-app notifications.
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Components: `NotificationBell.svelte`, `NotificationDropdown.svelte`.
|
||||||
|
Utilities: `notifications.svelte.ts` (Svelte 5 reactive store), `notifications.ts` (API helpers).
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
- SSE infrastructure — the backend's `SseEmitterRegistry` manages the server-side emitter. The frontend establishes one `EventSource` connection per session. Connection management lives in `notifications.svelte.ts`.
|
||||||
|
- Notification content rendering — notification payloads contain a `contextUrl`; the frontend navigates there on click.
|
||||||
|
|
||||||
|
## Key design: SSE connection
|
||||||
|
|
||||||
|
The SSE path is **backend → browser directly** (not proxied through SvelteKit SSR). The `EventSource` connects to `/api/notifications/stream`. On receive, the reactive store updates the unread count and the bell dropdown in real time.
|
||||||
|
|
||||||
|
```
|
||||||
|
Backend SseEmitterRegistry → /api/notifications/stream → EventSource in browser
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key components
|
||||||
|
|
||||||
|
| Component | Used in | Notes |
|
||||||
|
| ----------------------------- | ----------------------------- | --------------------------------------------------------- |
|
||||||
|
| `NotificationBell.svelte` | global nav (`+layout.svelte`) | Bell icon with unread badge; opens `NotificationDropdown` |
|
||||||
|
| `NotificationDropdown.svelte` | global nav | Scrollable list of recent notifications with mark-read |
|
||||||
|
|
||||||
|
## Cross-domain imports
|
||||||
|
|
||||||
|
- `shared/primitives/` — icon, button primitives only
|
||||||
|
|
||||||
|
## Backend counterpart
|
||||||
|
|
||||||
|
`backend/src/main/java/org/raddatz/familienarchiv/notification/README.md`
|
||||||
27
frontend/src/lib/ocr/README.md
Normal file
27
frontend/src/lib/ocr/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# ocr (frontend)
|
||||||
|
|
||||||
|
UI for OCR job management, progress display, and sender-model training in the admin/enrichment panel.
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Components: `OcrProgress.svelte`, `OcrTrigger.svelte`, `OcrTrainingCard.svelte`, `SegmentationTrainingCard.svelte`, `TrainingHistory.svelte`.
|
||||||
|
Utilities: `translateOcrProgress.ts` (progress-state → display-string mapping), `training.ts` (training API helpers).
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
- OCR processing — all text recognition runs in the Python `ocr-service/` container. The frontend shows job state; it does not run OCR.
|
||||||
|
- Transcription block display — rendered by `document/transcription/` components.
|
||||||
|
|
||||||
|
## Key components
|
||||||
|
|
||||||
|
| Component | Used in | Notes |
|
||||||
|
| --------------------------------- | ----------------------------- | -------------------------------------------------------- |
|
||||||
|
| `OcrProgress.svelte` | document header, enrich panel | Progress bar and status label for an active OCR job |
|
||||||
|
| `OcrTrigger.svelte` | enrich panel, document detail | Button to start an OCR job; disabled when one is running |
|
||||||
|
| `OcrTrainingCard.svelte` | `/admin/ocr` | Trigger sender-model training; shows training history |
|
||||||
|
| `SegmentationTrainingCard.svelte` | `/admin/ocr` | Trigger segmentation training |
|
||||||
|
| `TrainingHistory.svelte` | `/admin/ocr` | List of past training runs with status |
|
||||||
|
|
||||||
|
## Backend counterpart
|
||||||
|
|
||||||
|
`backend/src/main/java/org/raddatz/familienarchiv/ocr/README.md`
|
||||||
37
frontend/src/lib/person/README.md
Normal file
37
frontend/src/lib/person/README.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# person (frontend)
|
||||||
|
|
||||||
|
UI for historical family members: typeahead selection, chip display, hover cards, genealogy graph, relationship management.
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Components: `PersonTypeahead.svelte`, `PersonMultiSelect.svelte`, `PersonChip.svelte`, `PersonChipRow.svelte`, `PersonHoverCard.svelte`, `PersonTypeBadge.svelte`, `PersonTypeSelector.svelte`.
|
||||||
|
Utilities: `personFormat.ts` (full-name formatting), `personLifeDates.ts` (birth/death display), `person-validation.ts` (form validation), `personHoverCard.ts` (hover-card controller).
|
||||||
|
Sub-folders: `genealogy/` (Stammbaum view components), `relationship/` (relationship graph components).
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
- Document content — displayed in `document/`
|
||||||
|
- AppUser accounts — managed in `user/`
|
||||||
|
|
||||||
|
## Key components
|
||||||
|
|
||||||
|
| Component | Used in | Notes |
|
||||||
|
| -------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||||
|
| `PersonTypeahead.svelte` | document edit, geschichte, search filters | Single-person selector with debounced typeahead. Exported for use by other domains. |
|
||||||
|
| `PersonMultiSelect.svelte` | document edit (receivers) | Chip-based multi-person selector |
|
||||||
|
| `PersonChip.svelte` | document rows, conversation view | Compact display chip with link and hover card |
|
||||||
|
| `PersonHoverCard.svelte` | person chips | Floating card with person summary on hover |
|
||||||
|
|
||||||
|
## Cross-domain imports
|
||||||
|
|
||||||
|
- `shared/primitives/` — generic UI primitives
|
||||||
|
- `shared/hooks/useTypeahead.svelte.ts` — typeahead keyboard/focus logic
|
||||||
|
|
||||||
|
## Accessibility notes
|
||||||
|
|
||||||
|
- `PersonChip` focus ring: `focus-visible:ring-2 focus-visible:ring-brand-navy`
|
||||||
|
- `PersonTypeahead` dropdown navigable via keyboard (↑↓ Enter Escape)
|
||||||
|
|
||||||
|
## Backend counterpart
|
||||||
|
|
||||||
|
`backend/src/main/java/org/raddatz/familienarchiv/person/README.md`
|
||||||
40
frontend/src/lib/shared/README.md
Normal file
40
frontend/src/lib/shared/README.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# shared (frontend)
|
||||||
|
|
||||||
|
Cross-domain utilities and UI primitives. Any file here is consumed by two or more domain folders and has no domain identity of its own.
|
||||||
|
|
||||||
|
## Admission criteria (what belongs here)
|
||||||
|
|
||||||
|
A file belongs in `shared/` if it meets **all three** conditions:
|
||||||
|
|
||||||
|
1. No domain identity — it does not represent a `Document`, `Person`, `Tag`, etc.
|
||||||
|
2. Consumed by ≥ 2 domain folders — or is framework infrastructure that every domain depends on.
|
||||||
|
3. Generic — could work in a different SvelteKit project with zero business-logic changes.
|
||||||
|
|
||||||
|
If any condition fails, the file belongs in the domain folder of its primary consumer.
|
||||||
|
|
||||||
|
## What this folder owns
|
||||||
|
|
||||||
|
| Sub-folder / file | Purpose |
|
||||||
|
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `api.server.ts` | Typed `openapi-fetch` client factory — the standard entry point for all backend API calls in server-side load functions and actions |
|
||||||
|
| `errors.ts` | Mirror of the backend `ErrorCode` enum + `getErrorMessage()` → Paraglide i18n key mapping |
|
||||||
|
| `types.ts` | Cross-domain TypeScript interfaces |
|
||||||
|
| `utils.ts` | Pure utility functions (date formatting, sorting, debounce) |
|
||||||
|
| `relativeTime.ts` | Human-relative time formatting (`"2 days ago"`) |
|
||||||
|
| `primitives/` | Generic UI components: `BackButton.svelte`, form inputs, pagination, layout shells |
|
||||||
|
| `discussion/` | Comment/mention editor shared by `document/` and `geschichte/` |
|
||||||
|
| `dashboard/` | Family Pulse widget and recent-activity components assembled in the `/` route |
|
||||||
|
| `hooks/` | Svelte 5 reactive hooks: `useTypeahead`, `useUnsavedWarning` |
|
||||||
|
| `services/` | Generic client-side service helpers |
|
||||||
|
| `actions/` | Shared SvelteKit form action utilities |
|
||||||
|
| `server/` | Server-only shared utilities (load function helpers) |
|
||||||
|
| `help/` | Coach marks and empty-state components used across multiple domains |
|
||||||
|
|
||||||
|
## What does NOT belong here
|
||||||
|
|
||||||
|
- Components owned by one domain — move to that domain's folder.
|
||||||
|
- Domain-specific business logic — even if shared, it belongs in the owning domain's public surface.
|
||||||
|
|
||||||
|
## Adding to shared/
|
||||||
|
|
||||||
|
If you need to add a file here, confirm it meets all three admission criteria. If it's domain-adjacent, check whether the owning domain should export it as part of its public surface instead.
|
||||||
28
frontend/src/lib/tag/README.md
Normal file
28
frontend/src/lib/tag/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# tag (frontend)
|
||||||
|
|
||||||
|
UI for hierarchical document categories: tag chip lists, tag input with typeahead, and the admin tag-tree editor.
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Components: `TagInput.svelte`, `TagChipList.svelte`, `TagParentPicker.svelte`.
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
- Tag data management — CRUD is handled via the backend `tag/` domain
|
||||||
|
- Document association — adding/removing tags from documents is in `document/`
|
||||||
|
|
||||||
|
## Key components
|
||||||
|
|
||||||
|
| Component | Used in | Notes |
|
||||||
|
| ------------------------ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `TagInput.svelte` | document edit form | Multi-tag chip input with typeahead. Supports free-text creation and selecting existing tags. Exported for use by other domains. |
|
||||||
|
| `TagChipList.svelte` | document rows, detail pages | Read-only display of a tag set |
|
||||||
|
| `TagParentPicker.svelte` | admin tag editor | Tree-aware parent selection |
|
||||||
|
|
||||||
|
## Cross-domain imports
|
||||||
|
|
||||||
|
- `shared/hooks/useTypeahead.svelte.ts` — shared typeahead logic for `TagInput`
|
||||||
|
|
||||||
|
## Backend counterpart
|
||||||
|
|
||||||
|
`backend/src/main/java/org/raddatz/familienarchiv/tag/README.md`
|
||||||
28
frontend/src/lib/user/README.md
Normal file
28
frontend/src/lib/user/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# user (frontend)
|
||||||
|
|
||||||
|
UI for account management: profile editing, password change, and permission group management in the admin panel.
|
||||||
|
|
||||||
|
## What this domain owns
|
||||||
|
|
||||||
|
Components: `UserProfileSection.svelte`, `UserPasswordSection.svelte`, `UserGroupsSection.svelte`.
|
||||||
|
|
||||||
|
## What this domain does NOT own
|
||||||
|
|
||||||
|
- `Person` records — historical family members are entirely separate from login accounts. A user editing their profile is an `AppUser`; the historical persons in documents are `Person` entities. They are never linked.
|
||||||
|
- User list or admin creation UI — those live in the `/admin` route, which assembles views from multiple domains.
|
||||||
|
|
||||||
|
## Key components
|
||||||
|
|
||||||
|
| Component | Used in | Notes |
|
||||||
|
| ---------------------------- | --------------------------- | ------------------------------------ |
|
||||||
|
| `UserProfileSection.svelte` | `/settings` or profile page | Display name, email editing |
|
||||||
|
| `UserPasswordSection.svelte` | `/settings` | Password change form |
|
||||||
|
| `UserGroupsSection.svelte` | `/admin` | Per-user permission group assignment |
|
||||||
|
|
||||||
|
## Cross-domain imports
|
||||||
|
|
||||||
|
- `shared/primitives/` — generic UI primitives only
|
||||||
|
|
||||||
|
## Backend counterpart
|
||||||
|
|
||||||
|
`backend/src/main/java/org/raddatz/familienarchiv/user/README.md`
|
||||||
@@ -1,154 +1,7 @@
|
|||||||
# OCR Service — Familienarchiv
|
# OCR Service
|
||||||
|
|
||||||
## Overview
|
→ See [ocr-service/README.md](./README.md) for tech stack, architecture, endpoints, environment variables, local development, testing, and training.
|
||||||
|
|
||||||
Python FastAPI microservice that performs OCR (Optical Character Recognition) and HTR (Handwritten Text Recognition) on historical family documents. It exposes a simple HTTP API consumed by the Spring Boot backend. The service is stateless — all job tracking and business logic remain in Java.
|
**LLM reminder:** the OCR service is a **single-node container** — training reloads the model in-process, so multiple replicas cause model-state divergence (see ADR-001). All job tracking and business logic stay in Spring Boot; the Python service is stateless OCR only.
|
||||||
|
|
||||||
## Tech Stack
|
**LLM reminder:** `ALLOWED_PDF_HOSTS` must never be set to `*` — that opens SSRF. The default (`minio,localhost,127.0.0.1`) is correct for dev.
|
||||||
|
|
||||||
- **Framework**: FastAPI 0.115.6 (Python 3.11)
|
|
||||||
- **OCR Engines**:
|
|
||||||
- **Surya** (`surya-ocr`) — Transformer-based, handles typewritten and modern Latin handwriting
|
|
||||||
- **Kraken** (`kraken==7.0`) — Historical HTR model support, required for pre-1941 German Kurrent/Sütterlin scripts
|
|
||||||
- **ML**: PyTorch 2.7.1 (CPU-only), torchvision, transformers
|
|
||||||
- **PDF Processing**: `pypdfium2` (rendering), `pillow`
|
|
||||||
- **Image Processing**: `opencv-python-headless`, `pyvips`
|
|
||||||
- **Spell Checking**: `pyspellchecker`
|
|
||||||
- **HTTP Client**: `httpx`
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
The service is a single-node container (see ADR-001). OCR training reloads the model in-process after each run, so multiple replicas would cause training conflicts and model-state divergence.
|
|
||||||
|
|
||||||
### Interface Contract
|
|
||||||
|
|
||||||
**Request:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"pdfUrl": "http://minio:9000/archive-documents/abc.pdf?presigned...",
|
|
||||||
"scriptType": "HANDWRITING_KURRENT",
|
|
||||||
"language": "de"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:** Array of `OcrBlock` objects:
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"pageNumber": 0,
|
|
||||||
"x": 0.12, "y": 0.08, "width": 0.76, "height": 0.04,
|
|
||||||
"polygon": [[0.12,0.08],[0.88,0.09],[0.87,0.12],[0.13,0.11]],
|
|
||||||
"text": "Sehr geehrter Herr ..."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Coordinates are normalized (0-1) relative to page dimensions.
|
|
||||||
|
|
||||||
### File Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
ocr-service/
|
|
||||||
├── main.py # FastAPI app, endpoints, request handling
|
|
||||||
├── models.py # Pydantic models (OcrRequest, OcrBlock)
|
|
||||||
├── engines/
|
|
||||||
│ ├── __init__.py
|
|
||||||
│ ├── kraken.py # Kraken engine wrapper (Kurrent models)
|
|
||||||
│ └── surya.py # Surya engine wrapper (typewritten/Latin)
|
|
||||||
├── preprocessing.py # Image preprocessing (CLAHE, deskew, denoise)
|
|
||||||
├── confidence.py # Confidence scoring and thresholding
|
|
||||||
├── spell_check.py # Post-OCR spell correction
|
|
||||||
├── ensure_blla_model.py # Model download / verification helper
|
|
||||||
├── dictionaries/ # Historical word lists for spell checking
|
|
||||||
├── requirements.txt # Python dependencies
|
|
||||||
├── Dockerfile # Production container image
|
|
||||||
└── entrypoint.sh # Container startup script
|
|
||||||
```
|
|
||||||
|
|
||||||
### Key Endpoints
|
|
||||||
|
|
||||||
| Endpoint | Method | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `/health` | GET | Returns 200 only after models are loaded |
|
|
||||||
| `/ocr` | POST | Extract text blocks from a PDF URL |
|
|
||||||
| `/ocr/stream` | POST | Streaming OCR with SSE-style progress events |
|
|
||||||
| `/training/submit` | POST | Submit training data for model fine-tuning |
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
|
||||||
|---|---|---|
|
|
||||||
| `KRAKEN_MODEL_PATH` | `/app/models/german_kurrent.mlmodel` | Path to Kraken model file |
|
|
||||||
| `TRAINING_TOKEN` | `""` | Bearer token required for training endpoints |
|
|
||||||
| `OCR_CONFIDENCE_THRESHOLD` | `0.3` | Minimum confidence for Latin scripts |
|
|
||||||
| `OCR_CONFIDENCE_THRESHOLD_KURRENT` | `0.5` | Minimum confidence for Kurrent scripts |
|
|
||||||
| `RECOGNITION_BATCH_SIZE` | `16` | Kraken recognition batch size |
|
|
||||||
| `DETECTOR_BATCH_SIZE` | `8` | Surya detector batch size |
|
|
||||||
| `OCR_CLAHE_CLIP_LIMIT` | `2.0` | CLAHE contrast enhancement limit |
|
|
||||||
| `OCR_CLAHE_TILE_SIZE` | `8` | CLAHE tile grid size |
|
|
||||||
| `OCR_MAX_CACHED_MODELS` | `2` | LRU model cache size (~500 MB each) |
|
|
||||||
| `ALLOWED_PDF_HOSTS` | `minio,localhost,127.0.0.1` | SSRF protection — allowed PDF URL hosts |
|
|
||||||
|
|
||||||
## How to Run
|
|
||||||
|
|
||||||
### Local Development (Python venv)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ocr-service
|
|
||||||
python -m venv .venv
|
|
||||||
source .venv/bin/activate
|
|
||||||
|
|
||||||
# Install PyTorch CPU first (saves ~2 GB vs CUDA)
|
|
||||||
pip install torch==2.7.1 torchvision==0.22.1 --index-url https://download.pytorch.org/whl/cpu
|
|
||||||
|
|
||||||
# Install remaining dependencies
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
# Run development server
|
|
||||||
fastapi dev main.py --host 0.0.0.0 --port 8000
|
|
||||||
|
|
||||||
# Or production mode
|
|
||||||
uvicorn main:app --host 0.0.0.0 --port 8000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker (via docker-compose)
|
|
||||||
|
|
||||||
The OCR service is included in the root `docker-compose.yml`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d ocr-service
|
|
||||||
```
|
|
||||||
|
|
||||||
The container:
|
|
||||||
- Exposes port 8000 internally (not mapped to host by default)
|
|
||||||
- Mounts `ocr_models` and `ocr_cache` volumes for persistence
|
|
||||||
- Has a 120-second startup grace period for model loading
|
|
||||||
- Memory limit: 12 GB
|
|
||||||
|
|
||||||
### Model Downloads
|
|
||||||
|
|
||||||
Use the helper script to download Kraken models:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./scripts/download-kraken-models.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Models are stored in the `ocr_models` Docker volume or `./ocr-service/models/` locally.
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Only a subset of tests can run without the full ML stack:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd ocr-service
|
|
||||||
pip install pytest pytest-asyncio pyspellchecker
|
|
||||||
|
|
||||||
# No ML required — pure logic tests
|
|
||||||
python -m pytest test_spell_check.py test_confidence.py test_sender_registry.py -v
|
|
||||||
```
|
|
||||||
|
|
||||||
Tests requiring PyTorch/Kraken/Surya (e.g., `test_engines.py`) must be run in the Docker container or a fully provisioned venv.
|
|
||||||
|
|
||||||
## Training
|
|
||||||
|
|
||||||
The service supports in-process model fine-tuning via Kraken's `ketos` training pipeline. Training endpoints require the `TRAINING_TOKEN` bearer token. After training completes, the model is reloaded in-process — this is why only a single replica is supported.
|
|
||||||
|
|||||||
51
ocr-service/README.md
Normal file
51
ocr-service/README.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# ocr-service
|
||||||
|
|
||||||
|
Python FastAPI microservice that performs the actual handwritten text recognition (HTR) and OCR. The Spring Boot backend orchestrates jobs; this service executes them.
|
||||||
|
|
||||||
|
## What this service owns
|
||||||
|
|
||||||
|
- Text recognition: Surya (typewritten text) and Kraken (Kurrent/Sütterlin historical handwriting)
|
||||||
|
- Baseline layout analysis: Kraken BLLA model
|
||||||
|
- Sender recognition: trained per-archive sender models
|
||||||
|
- HTTP API at port 8000 (internal Docker network — no external port)
|
||||||
|
|
||||||
|
## What this service does NOT own
|
||||||
|
|
||||||
|
- Job lifecycle — tracked in the backend's `ocr/` domain
|
||||||
|
- MinIO storage — the service fetches PDFs via presigned URLs generated by the backend; it does not hold credentials
|
||||||
|
- Transcription block storage — results are streamed back to the backend, which writes them to PostgreSQL
|
||||||
|
|
||||||
|
## API endpoints
|
||||||
|
|
||||||
|
| Endpoint | Auth | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `POST /ocr` | None (internal network only) | Run OCR on a PDF (presigned MinIO URL in request body) |
|
||||||
|
| `POST /train` | `X-Training-Token` header | Trigger sender-model training |
|
||||||
|
| `POST /segtrain` | `X-Training-Token` header | Trigger segmentation training |
|
||||||
|
| `GET /health` | None | Health check |
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Default | Required? | Sensitive? | Purpose |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `TRAINING_TOKEN` | — | YES (prod) | YES | Guards `/train` and `/segtrain`. Do not leave empty in production. |
|
||||||
|
| `ALLOWED_PDF_HOSTS` | `minio,localhost,127.0.0.1` | YES | — | SSRF protection — comma-separated allowed PDF source hosts. Never set to `*`. |
|
||||||
|
| `KRAKEN_MODEL_PATH` | `/app/models/` | — | — | Directory where Kraken HTR models are stored (populated by `download-kraken-models.sh`) |
|
||||||
|
| `BLLA_MODEL_PATH` | `/app/models/blla.mlmodel` | — | — | Kraken baseline layout analysis model. Auto-downloaded via `ensure_blla_model.py` on startup if missing. |
|
||||||
|
|
||||||
|
## Key files
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `main.py` | FastAPI app, endpoint definitions, SSRF validation |
|
||||||
|
| `engines/` | Surya and Kraken engine wrappers |
|
||||||
|
| `models.py` | Pydantic request/response models |
|
||||||
|
| `preprocessing.py` | PDF-to-image conversion before OCR |
|
||||||
|
| `confidence.py` | Per-block confidence scoring |
|
||||||
|
| `spell_check.py` | Post-OCR spell correction using historical dictionaries |
|
||||||
|
| `ensure_blla_model.py` | Startup script that downloads the BLLA model if missing |
|
||||||
|
| `entrypoint.sh` | Docker entrypoint — runs `ensure_blla_model.py` then starts the server |
|
||||||
|
|
||||||
|
## Backend counterpart
|
||||||
|
|
||||||
|
`backend/src/main/java/org/raddatz/familienarchiv/ocr/README.md`
|
||||||
@@ -1,144 +1,5 @@
|
|||||||
# Scripts — Familienarchiv
|
# scripts/
|
||||||
|
|
||||||
## Overview
|
→ See [scripts/README.md](./README.md) for the full list of scripts, their purpose, and usage.
|
||||||
|
|
||||||
Utility scripts for development, data management, model downloads, and database operations. These are standalone shell and Python scripts used outside the normal application runtime.
|
**LLM reminder:** when adding a new script, document it in `scripts/README.md` (not here).
|
||||||
|
|
||||||
## Scripts
|
|
||||||
|
|
||||||
### `reset-db.sh`
|
|
||||||
**Purpose**: Hard-reset the development database, wiping all documents, persons, tags, and related data.
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```bash
|
|
||||||
./scripts/reset-db.sh
|
|
||||||
# Type 'yes' to confirm
|
|
||||||
```
|
|
||||||
|
|
||||||
**What it truncates:**
|
|
||||||
- `transcription_block_versions`
|
|
||||||
- `transcription_blocks`
|
|
||||||
- `comment_mentions`
|
|
||||||
- `document_comments`
|
|
||||||
- `document_annotations`
|
|
||||||
- `document_versions`
|
|
||||||
- `notifications`
|
|
||||||
- `documents`
|
|
||||||
- `person_name_aliases`
|
|
||||||
- `persons`
|
|
||||||
- `tag`
|
|
||||||
|
|
||||||
> ⚠️ **Destructive operation** — only for development!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `rebuild-frontend.sh`
|
|
||||||
**Purpose**: Force a clean rebuild of the frontend Docker container.
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```bash
|
|
||||||
./scripts/rebuild-frontend.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `download-kraken-models.sh`
|
|
||||||
**Purpose**: Download Kraken HTR models for German Kurrent and Sütterlin scripts.
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```bash
|
|
||||||
./scripts/download-kraken-models.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Downloads models into `./ocr-service/models/` or the `ocr_models` Docker volume. Models are ~100-500 MB each.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `download-paperless.sh`
|
|
||||||
**Purpose**: Download exported documents from a Paperless-ngx instance.
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```bash
|
|
||||||
./scripts/download-paperless.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
Requires environment variables or config for the Paperless API endpoint and token.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `flatten-paperless.sh`
|
|
||||||
**Purpose**: Flatten nested Paperless export directories into a single import-ready structure.
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```bash
|
|
||||||
./scripts/flatten-paperless.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `generate_data.py`
|
|
||||||
**Purpose**: Generate synthetic test data for development.
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```bash
|
|
||||||
python scripts/generate_data.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Generates fake documents, persons, and tags suitable for load testing or UI development.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `prepare_historical_dict.py`
|
|
||||||
**Purpose**: Build a historical German word dictionary for the OCR spell-checker.
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```bash
|
|
||||||
python scripts/prepare_historical_dict.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Processes raw word lists into the format expected by `ocr-service/spell_check.py`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `schema.sql`
|
|
||||||
**Purpose**: Complete database schema dump for reference.
|
|
||||||
|
|
||||||
**Note**: Flyway migrations in `backend/src/main/resources/db/migration/` are the source of truth for schema evolution. `schema.sql` is a snapshot for quick reference only.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### `large-data.sql`
|
|
||||||
**Purpose**: Pre-seeded dataset with a large number of documents for performance testing.
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```bash
|
|
||||||
# Import into PostgreSQL
|
|
||||||
docker exec -i archive-db psql -U archive_user -d family_archive_db < scripts/large-data.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## How to Use
|
|
||||||
|
|
||||||
Most scripts should be run from the **repository root**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Database reset
|
|
||||||
./scripts/reset-db.sh
|
|
||||||
|
|
||||||
# Model download
|
|
||||||
./scripts/download-kraken-models.sh
|
|
||||||
|
|
||||||
# Data generation
|
|
||||||
cd scripts && python generate_data.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Ensure scripts are executable:
|
|
||||||
```bash
|
|
||||||
chmod +x scripts/*.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Adding New Scripts
|
|
||||||
|
|
||||||
1. Place the script in `scripts/`
|
|
||||||
2. Add a header comment describing purpose and usage
|
|
||||||
3. Make it executable (`chmod +x`)
|
|
||||||
4. Document it in this `CLAUDE.md`
|
|
||||||
|
|||||||
161
scripts/README.md
Normal file
161
scripts/README.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# scripts/
|
||||||
|
|
||||||
|
Utility scripts for development, data management, model downloads, and database operations. These are standalone shell and Python scripts used outside the normal application runtime.
|
||||||
|
|
||||||
|
## Scripts
|
||||||
|
|
||||||
|
### `reset-db.sh`
|
||||||
|
|
||||||
|
**Purpose**: Hard-reset the development database, wiping all documents, persons, tags, and related data.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/reset-db.sh
|
||||||
|
# Type 'yes' to confirm
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it truncates:**
|
||||||
|
|
||||||
|
- `transcription_block_versions`
|
||||||
|
- `transcription_blocks`
|
||||||
|
- `comment_mentions`
|
||||||
|
- `document_comments`
|
||||||
|
- `document_annotations`
|
||||||
|
- `document_versions`
|
||||||
|
- `notifications`
|
||||||
|
- `documents`
|
||||||
|
- `person_name_aliases`
|
||||||
|
- `persons`
|
||||||
|
- `tag`
|
||||||
|
|
||||||
|
> ⚠️ **Destructive operation — only for development!** This wipes ALL data. Not reversible without a backup.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `rebuild-frontend.sh`
|
||||||
|
|
||||||
|
**Purpose**: Force a clean rebuild of the frontend Docker container.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/rebuild-frontend.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `download-kraken-models.sh`
|
||||||
|
|
||||||
|
**Purpose**: Download Kraken HTR models for German Kurrent and Sütterlin scripts.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/download-kraken-models.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Downloads models into `./ocr-service/models/` or the `ocr_models` Docker volume. Models are ~100–500 MB each.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `download-paperless.sh`
|
||||||
|
|
||||||
|
**Purpose**: Download exported documents from a Paperless-ngx instance.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/download-paperless.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires environment variables or config for the Paperless API endpoint and token.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `flatten-paperless.sh`
|
||||||
|
|
||||||
|
**Purpose**: Flatten nested Paperless export directories into a single import-ready structure.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/flatten-paperless.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `generate_data.py`
|
||||||
|
|
||||||
|
**Purpose**: Generate synthetic test data for development.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/generate_data.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Generates fake documents, persons, and tags suitable for load testing or UI development.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `prepare_historical_dict.py`
|
||||||
|
|
||||||
|
**Purpose**: Build a historical German word dictionary for the OCR spell-checker.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/prepare_historical_dict.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Processes raw word lists into the format expected by `ocr-service/spell_check.py`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `schema.sql`
|
||||||
|
|
||||||
|
**Purpose**: Complete database schema dump for reference.
|
||||||
|
|
||||||
|
**Note**: Flyway migrations in `backend/src/main/resources/db/migration/` are the source of truth for schema evolution. `schema.sql` is a snapshot for quick reference only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `large-data.sql`
|
||||||
|
|
||||||
|
**Purpose**: Pre-seeded dataset with a large number of documents for performance testing.
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Import into PostgreSQL
|
||||||
|
docker exec -i archive-db psql -U archive_user -d family_archive_db < scripts/large-data.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
Most scripts should be run from the **repository root**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Database reset
|
||||||
|
./scripts/reset-db.sh
|
||||||
|
|
||||||
|
# Model download
|
||||||
|
./scripts/download-kraken-models.sh
|
||||||
|
|
||||||
|
# Data generation
|
||||||
|
cd scripts && python generate_data.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Ensure scripts are executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x scripts/*.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding New Scripts
|
||||||
|
|
||||||
|
1. Place the script in `scripts/`
|
||||||
|
2. Add a header comment describing purpose and usage
|
||||||
|
3. Make it executable (`chmod +x`)
|
||||||
|
4. Document it in this `README.md`
|
||||||
Reference in New Issue
Block a user