diff --git a/CLAUDE.md b/CLAUDE.md index ededd67e..75e83228 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,23 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## 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 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. -## Collaboration Principles +## Collaboration -**Be honest and objective**: Evaluate all suggestions, ideas, and feedback on their technical merits. Don't be overly complimentary or sycophantic. If something doesn't make sense, doesn't align with best practices, or could be improved, say so directly and constructively. Technical accuracy and project quality take precedence over being agreeable. +See [COLLABORATING.md](./COLLABORATING.md) for the full rules: issue tracking workflow, commit message conventions, the Research → Plan → Implement → Validate cycle, and code style expectations. -## Core Workflow: Research → Plan → Implement → Validate - -**Start every feature with:** "Let me research the codebase and create a plan before implementing." - -1. **Research** - Understand existing patterns and architecture -2. **Plan** - Propose approach and verify with you -3. **Implement** - Build with tests and error handling -4. **Validate** - ALWAYS run formatters, linters, and tests after implementation - -- Whenever working on a feature or issue, let's always come up with a plan first, then save it to a file called `/.agent/current-plan.md`, before getting started with code changes. Update this file as the work progresses. -- Let's use pure functions where possible to improve readability and testing. +--- ## Stack @@ -62,62 +52,299 @@ 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) ``` -## Architecture +--- -### Backend (`backend/src/main/java/org/raddatz/familienarchiv/`) +## Backend Architecture -Layered architecture: +### Package Structure -- **`model/`** — JPA entities: `Document`, `Person`, `AppUser`, `UserGroup`, `Tag`, `DocumentStatus` (enum: PLACEHOLDER → UPLOADED → TRANSCRIBED → REVIEWED → ARCHIVED) -- **`repository/`** — Spring Data repositories + `DocumentSpecifications` for complex filtered queries -- **`service/`** — Core business logic: `DocumentService` (uploads, search, Excel import), `FileService` (MinIO/S3), `ExcelService`, `MassImportService`, `UserService` -- **`controller/`** — REST endpoints: `DocumentController`, `PersonController`, `UserController`, `AdminController`, `GroupController`, `TagController` -- **`security/`** — `SecurityConfig` (HTTP Basic + form login, CSRF disabled), `PermissionAspect` (AOP enforcement of `@RequirePermission`), `CustomUserDetailsService` -- **`config/`** — `MinioConfig` (creates S3Client, validates connectivity on startup), `AsyncConfig` +``` +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 +``` -Database migrations live in `src/main/resources/db/migration/` (Flyway). Configuration in `src/main/resources/application.properties` — most values injected from environment variables (DB credentials, MinIO endpoint/credentials/bucket, upload limits, Excel column mappings). - -### Frontend (`backend/workspaces/frontend/src/`) - -- **`hooks.server.ts`** — Central middleware: reads `auth_token` cookie, injects it into all API calls to the backend, loads current user context -- **`routes/`** — File-based routing. Main pages: `/` (search/home), `/documents/[id]`, `/documents/[id]/edit`, `/persons`, `/persons/[id]`, `/conversations`, `/admin`, `/login` -- **`routes/api/`** — SvelteKit API endpoints for typeahead (persons, tags) — these call the Spring Boot backend -- **`lib/components/`** — `PersonTypeahead.svelte`, `TagInput.svelte` -- **`messages/`** — Paraglide.js translation files (`de.json`, `en.json`, `es.json`) - -Authentication: form login → backend sets session → `auth_token` cookie → hooks.server.ts injects into all backend requests. - -### Backend Layering Rules - -Strict layering must be respected at all times: +### Layering Rules (strictly enforced) ``` Controller → Service → Repository → DB ``` -- **Controllers** must never inject or call repositories directly. All business logic goes through a service. -- **Services** must never reach into another domain's repository directly. If Service A needs data owned by domain B, it calls Service B — not Repository B. - - ✅ `DocumentService` → `PersonService.findById()` → `PersonRepository` - - ❌ `DocumentService` → `PersonRepository` (bypasses the service layer) +- **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. -### Key Design Patterns +### Domain Model -- **Search**: `DocumentSpecifications` (Spring Data JPA Specification pattern) enables composable, dynamic query building for the document search endpoint -- **Permissions**: `@RequirePermission` annotation processed by `PermissionAspect` (AOP) — checks user's `UserGroup` permissions at the method level -- **Excel Import**: Configurable column index mapping in `application.properties`; `ExcelService` parses → `MassImportService` upserts documents -- **File Storage**: `FileService` wraps AWS SDK v2 `S3Client` with path-style access for MinIO compatibility +| 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` | -### Infrastructure +**`DocumentStatus` lifecycle:** `PLACEHOLDER → UPLOADED → TRANSCRIBED → REVIEWED → ARCHIVED` -The `docker-compose.yml` at the repo root orchestrates everything. A MinIO MC helper container runs at startup to create the `archive-documents` bucket and set permissions. The backend container depends on both `db` and `minio` being healthy before starting. +- `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/` (`Document.http`, `User.http`) for use with the VS Code REST Client extension. +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, with ports 8080 and 3000 forwarded). Use VS Code's "Reopen in Container" to get a pre-configured environment with Spring Boot Tools, Lombok support, and database/MinIO services running. +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. diff --git a/COLLABORATING.md b/COLLABORATING.md new file mode 100644 index 00000000..45cd6984 --- /dev/null +++ b/COLLABORATING.md @@ -0,0 +1,80 @@ +# Collaboration Rules + +How we work together on this project. + +## Honesty and Objectivity + +Evaluate all suggestions on their technical merits. No sycophancy — if something doesn't make sense, doesn't align with best practices, or could be improved, say so directly and constructively. Technical accuracy and project quality take precedence over being agreeable. + +## Core Workflow: Research → Plan → Implement → Validate + +Every non-trivial feature or bug fix follows this sequence: + +1. **Research** — Read the relevant code. Understand existing patterns before touching anything. +2. **Plan** — Write a plan to `/.agent/current-plan.md` and align with the user before writing code. Update the plan as work progresses. +3. **Implement** — Build with tests and error handling. Use pure functions where possible. +4. **Validate** — Run formatters, linters, and tests after every implementation step. + +Never start writing code without having read the relevant files first. + +## Issue Tracking (Gitea) + +All work is tracked in **Gitea** at `http://192.168.178.71:3005` (repo `marcel/familienarchiv`). Never use todo files or CLAUDE.md notes as a substitute. + +Create an issue whenever work is identified that isn't being done in the current session. + +### Issue title formats + +**`feature` label** — user story format: +``` +As a [role] I want [capability] so/because [reason] +``` +Examples: +- "As a user I want to search documents so I can find a specific document faster" +- "As an admin I want to add a new user so I don't have to restart the server" + +**`bug` label** — user-facing impact, not the technical cause: +``` +[What breaks] when [trigger] +``` +Examples: +- "Document list shows blank page when no results found" +- "Upload fails silently when file exceeds 50MB" + +**`devops` label** — infrastructure, CI/CD, deployment, tooling: +- "Fix CI checkout failing due to unresolvable hostname" +- "Add E2E test seed data for runner" + +### Priority labels + +- `priority: high` — blocking or urgent +- `priority: medium` — normal +- `priority: low` — nice to have + +### Other labels + +- `needs-discussion` — decision needed before work starts +- `wontfix` — acknowledged, not addressing + +## Commit Messages + +Every commit must reference the relevant Gitea issue. + +- `Closes #12` — commit fully resolves the issue (Gitea auto-closes it) +- `Refs #12` — commit is related but doesn't fully close the issue + +Place the reference at the end of the commit body: + +``` +feat: add person typeahead to document edit form + +Closes #7 +Co-Authored-By: Claude Sonnet 4.6 +``` + +## Code Style Reminders + +- Pure functions over stateful helpers where possible +- No premature abstractions — solve the problem in front of you +- No backwards-compatibility shims for code that has no callers +- Validate at system boundaries only (user input, external APIs)