Covers naming, function design, guard clauses, comment policy, command-query separation, DRY vs KISS trade-offs (KISS wins), and SOLID applied to the Java backend and TypeScript/Svelte frontend. Linked from CLAUDE.md and COLLABORATING.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
353 lines
13 KiB
Markdown
353 lines
13 KiB
Markdown
# 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<String> 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 `<input type="hidden" name="documentDate" value={dateIso}>` 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
|
|
<div class="bg-white shadow-sm border border-brand-sand rounded-sm p-6">
|
|
<h2 class="text-xs font-bold uppercase tracking-widest text-gray-400 mb-5">Section Title</h2>
|
|
<!-- content -->
|
|
</div>
|
|
```
|
|
|
|
Save bar pattern — use **sticky full-bleed** for long forms (edit document), **card-style with `mt-4`** for short forms (new person):
|
|
```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 link pattern:
|
|
```svelte
|
|
<a href="/persons" class="inline-flex items-center text-xs font-bold uppercase tracking-widest text-gray-500 hover:text-brand-navy transition-colors group mb-4">
|
|
<svg class="w-4 h-4 mr-2 transform group-hover:-translate-x-1 transition-transform" .../>
|
|
Zurück zur Übersicht
|
|
</a>
|
|
```
|
|
|
|
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)
|
|
|
|
`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.
|