Files
familienarchiv/CLAUDE.md
Marcel c0d8704d6d docs: remove stale ExcelService from CLAUDE.md
ExcelService was deleted in fa60c5be. Both the root and backend
CLAUDE.md still listed it under importing/ and in the services table.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 13:25:40 +02:00

14 KiB

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 for the full rules: issue tracking workflow, commit message conventions, and the Research → Plan → Implement → Validate cycle.

See 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

# From repo root — starts PostgreSQL, MinIO, and Spring Boot backend
docker-compose up -d

Backend (Spring Boot)

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)

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

Package-by-domain: each domain owns its controller, service, repository, entities, and DTOs.

backend/src/main/java/org/raddatz/familienarchiv/
├── audit/               Audit logging
├── config/              Infrastructure config (Minio, Async, Web)
├── dashboard/           Dashboard analytics + StatsController/StatsService
├── document/            Document domain (entities, controller, service, repository, DTOs)
│   ├── annotation/      DocumentAnnotation, AnnotationService, AnnotationController
│   ├── comment/         DocumentComment, CommentService, CommentController
│   └── transcription/   TranscriptionBlock, TranscriptionService, TranscriptionBlockQueryService
├── exception/           DomainException, ErrorCode, GlobalExceptionHandler
├── filestorage/         FileService (S3/MinIO)
├── geschichte/          Geschichte (story) domain
├── importing/           MassImportService
├── notification/        Notification domain + SseEmitterRegistry
├── ocr/                 OCR domain — OcrService, OcrBatchService, training
├── person/              Person domain
│   └── relationship/    PersonRelationship sub-domain
├── security/            SecurityConfig, Permission, @RequirePermission, PermissionAspect
├── tag/                 Tag domain
└── user/                User domain — AppUser, UserGroup, UserService, auth controllers

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.
    • DocumentServicePersonService.getById()PersonRepository
    • DocumentServicePersonRepository 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:

@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

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.

// 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:

throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "firstName is required");

Security / Permissions

Use @RequirePermission on controller methods (or the whole controller class):

@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:

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:

const res = await fetch(`${baseUrl}/api/documents`, { method: 'POST', body: formData });

Form Actions Pattern

// +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:
    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:

<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):

<!-- 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:

<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"):

<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.