# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## 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. ## Collaboration See [COLLABORATING.md](./COLLABORATING.md) for the full rules: issue tracking workflow, commit message conventions, and the Research → Plan → Implement → Validate cycle. See [CODESTYLE.md](./CODESTYLE.md) for coding standards: Clean Code, DRY/KISS trade-offs (KISS wins), and SOLID principles applied to this stack. --- ## Stack - **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) - **Database**: PostgreSQL 16 - **Object Storage**: MinIO (S3-compatible) - **Infrastructure**: Docker Compose ## Common Commands ### Running the Full Stack ```bash # From repo root — starts PostgreSQL, MinIO, and Spring Boot backend docker-compose up -d ``` ### Backend (Spring Boot) ```bash cd backend ./mvnw spring-boot:run # Run locally ./mvnw clean package # Build JAR (with tests) ./mvnw clean package -DskipTests ./mvnw test # Run all tests ./mvnw test -Dtest=ClassName # Run a single test class ``` ### Frontend (SvelteKit) ```bash cd frontend npm install npm run dev # Dev server (port 3000) npm run build # Production build npm run preview # Preview production build npm run lint # Prettier + ESLint check npm run format # Auto-fix formatting npm run check # svelte-check (type checking) npm run test # Vitest unit tests npm run generate:api # Regenerate TypeScript API types from OpenAPI spec # (requires backend running with --spring.profiles.active=dev) ``` --- ## Backend Architecture ### Package Structure ``` backend/src/main/java/org/raddatz/familienarchiv/ ├── controller/ REST endpoints — thin, delegate everything to services ├── service/ Business logic — the only place that touches repositories ├── repository/ Spring Data JPA interfaces ├── model/ JPA entities ├── dto/ Input objects (request bodies/form data) ├── exception/ DomainException + ErrorCode enum ├── security/ SecurityConfig, Permission enum, @RequirePermission, PermissionAspect └── config/ MinioConfig, AsyncConfig ``` ### Layering Rules (strictly enforced) ``` Controller → Service → Repository → DB ``` - **Controllers** never inject or call repositories directly. - **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 | Entity | Table | Key relationships | |---|---|---| | `Document` | `documents` | ManyToOne `sender` (Person), ManyToMany `receivers` (Person), ManyToMany `tags` (Tag) | | `Person` | `persons` | Referenced by documents as sender/receiver | | `Tag` | `tag` | ManyToMany with documents via `document_tags` | | `AppUser` | `app_users` | ManyToMany `groups` (UserGroup) | | `UserGroup` | `user_groups` | Has a `Set permissions` | **`DocumentStatus` lifecycle:** `PLACEHOLDER → UPLOADED → TRANSCRIBED → REVIEWED → ARCHIVED` - `PLACEHOLDER`: created during Excel import, no file yet - `UPLOADED`: file has been stored in S3 ### Entity Code Style All entities use these Lombok annotations: ```java @Entity @Table(name = "table_name") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class MyEntity { @Id @GeneratedValue(strategy = GenerationType.UUID) @Schema(requiredMode = Schema.RequiredMode.REQUIRED) // marks field as required in OpenAPI spec private UUID id; // ... } ``` - `@Schema(requiredMode = REQUIRED)` must be added to every field the backend always populates (id, non-null fields). This drives the TypeScript type generation. - Collections use `@Builder.Default` with `new HashSet<>()` as the default. - Timestamps use `@CreationTimestamp` / `@UpdateTimestamp`. ### Services Services are annotated with `@Service`, `@RequiredArgsConstructor`, and optionally `@Slf4j`. - Write methods are annotated `@Transactional`. - 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. **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 | | `ExcelService` | Lower-level spreadsheet parsing | ### DTOs Input DTOs live in `dto/`. Response types are the model entities themselves (no response DTOs). - `DocumentUpdateDTO` — used for both create and update (all fields optional) - `CreateUserRequest` — user creation - `GroupDTO` — group create/update ### Error Handling Use `DomainException` for all domain errors. Never throw raw exceptions from service methods. ```java // 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 Use `@RequirePermission` on controller methods (or the whole controller class): ```java @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 SpringDoc generates the spec at `/v3/api-docs` (only accessible when running with `--spring.profiles.active=dev`). When changing any model field or endpoint: 1. Rebuild the backend JAR with `-DskipTests` 2. Start it with `--spring.profiles.active=dev` 3. Run `npm run generate:api` in `frontend/` --- ## Frontend Architecture ### Route Structure ``` frontend/src/routes/ ├── +layout.svelte Global header (sticky), nav links, logout ├── +layout.server.ts Loads current user, injects auth cookie ├── +page.svelte Home / document search ├── +page.server.ts Load: search documents; no actions ├── documents/ │ ├── [id]/+page.svelte Document detail (view + file preview) │ └── [id]/edit/ Edit form (all metadata + file upload) │ └── new/ Create form (same fields, empty) ├── persons/ │ ├── +page.svelte Person list with search │ ├── [id]/+page.svelte Person detail (inline edit + merge) │ └── new/ Create person form ├── conversations/ Bilateral conversation timeline ├── admin/ User + group + tag management └── login/ logout/ Auth pages ``` ### API Client Pattern All server-side API calls use the typed client from `$lib/api.server.ts`: ```typescript 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 ```typescript // +page.server.ts export const actions = { default: async ({ request, fetch }) => { const formData = await request.formData(); const name = formData.get('name') as string; // cast needed — FormData returns FormDataEntryValue // ... return fail(400, { error: 'message' }); // on error throw redirect(303, '/target'); // on success } }; ``` ### Date Handling - **Forms**: German format `dd.mm.yyyy` with auto-dot insertion via `handleDateInput()`. A hidden `` sends ISO format to the backend. - **Display**: Always use `Intl.DateTimeFormat` with `T12:00:00` suffix to prevent UTC timezone off-by-one: ```typescript new Intl.DateTimeFormat('de-DE', { day: 'numeric', month: 'long', year: 'numeric' }) .format(new Date(doc.documentDate + 'T12:00:00')) ``` ### UI Component Library Custom components in `src/lib/components/`: | 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) Brand color utilities (defined in `layout.css`): | Class | Value | Usage | |---|---|---| | `brand-navy` | `#002850` | Primary text, buttons, headers | | `brand-mint` | `#A6DAD8` | Accents, hover underlines, icons | | `brand-sand` | `#E4E2D7` | Page background, card borders | Typography: - `font-serif` (Merriweather) — body text, document titles, names - `font-sans` (Montserrat) — labels, metadata, UI chrome Card pattern for content sections: ```svelte

Section Title

``` Save bar pattern — use **sticky full-bleed** for long forms (edit document), **card-style with `mt-4`** for short forms (new person): ```svelte
``` Back link pattern: ```svelte Zurück zur Übersicht ``` Subtle action link (e.g. "new document/person"): ```svelte Neues Dokument ``` ### 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: 1. Add it to `ErrorCode.java` 2. Add it to the `ErrorCode` type in `errors.ts` 3. Add a `case` in `getErrorMessage()` 4. Add the translation key in `messages/de.json`, `en.json`, `es.json` --- ## 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. Database migrations live in `backend/src/main/resources/db/migration/` (Flyway, SQL files named `V{n}__{description}.sql`). ## API Testing HTTP test files are in `backend/api_tests/` for use with the VS Code REST Client extension. ## 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.