Commit Graph

38 Commits

Author SHA1 Message Date
Marcel
6ef7b292cc fix(flyway): baseline existing schemas at V4 on first run
Local dev databases that existed before Flyway was introduced have tables
but no flyway_schema_history. Flyway refuses to migrate a non-empty schema
without a history table. baselineOnMigrate=true with baselineVersion=4
stamps those databases as already at V4 without re-running migrations.
Fresh databases (CI) have an empty schema so the baseline is never
triggered and all 4 migrations run normally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 12:03:14 +01:00
Marcel
a65cbf9bae fix(backend): switch base image from alpine to debian for bash compatibility
mvnw is a bash script; eclipse-temurin:21-jdk-alpine only provides ash
(busybox), causing the container to exit silently with code 0 before the
JVM starts. The Debian-based eclipse-temurin:21-jdk image includes bash.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 12:03:14 +01:00
Marcel
a60905674f fix(backend): explicit Flyway bean to bypass broken auto-configuration
Spring Boot 4.0 Flyway auto-configuration is not triggering in the CI
environment — confirmed by empty DB and no flyway_schema_history table.
Replace YAML-based auto-config with an explicit @Bean that creates and
runs Flyway directly on startup, independent of any auto-configuration
conditions. Disable the auto-config via spring.flyway.enabled=false to
prevent interference. Add @DependsOn("flyway") to DataInitializer to
enforce that CommandLineRunner beans are only registered after migrations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 12:03:14 +01:00
Marcel
e6db43850b fix(security): permit /actuator/health without authentication
The CI health check (curl -sf) and Docker Compose health check (wget)
both hit /actuator/health unauthenticated. With anyRequest().authenticated()
the endpoint returned 401, curl -f treated it as failure, and the health
check loop never exited successfully.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 12:03:14 +01:00
Marcel
802f1ab0e0 fix(backend): explicit Flyway config and DataInitializer null title fix
Adding explicit spring.flyway.* config (url/user/password) ensures Flyway
creates its own JDBC connection and runs migrations independently of the JPA
datasource initialization order in Spring Boot 4.0.

Fix DataInitializer creating a Document with title=null, which would hit the
NOT NULL constraint in the documents table once the admin user init succeeds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 12:03:14 +01:00
Marcel
9b67db74eb feat: auto-start Spring Boot backend via docker-compose
Replace the devcontainer (sleep infinity + VS Code image) with a proper
dev setup:
- Dockerfile: eclipse-temurin:21-jdk-alpine running ./mvnw spring-boot:run
- Source mounted at /app, Maven deps cached in named volume maven_cache
- Healthcheck on /actuator/health so frontend waits until backend is ready
- frontend depends_on backend: service_healthy (was service_started)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 12:03:14 +01:00
Marcel
225d6e44c9 test(backend): add service unit tests and controller slice tests
Some checks failed
CI / E2E Tests (push) Has been cancelled
CI / Unit & Component Tests (push) Has started running
CI / E2E Tests (pull_request) Has been cancelled
CI / Unit & Component Tests (pull_request) Has been cancelled
Service unit tests (Mockito, no Spring context):
- DocumentServiceTest — getById, updateDocument, deleteTagCascading, createPlaceholder
- PersonServiceTest — getById, findOrCreateByAlias, mergePersons
- TagServiceTest — getById, findOrCreate, update, delete
- UserServiceTest — findByUsername, deleteUser, createUserOrUpdate, getGroupById

Controller slice tests (@WebMvcTest):
- DocumentControllerTest — 401/403/200 for GET /search, POST /, PUT /{id}
- TagControllerTest — 401/403/200 for GET, PUT /{id}, DELETE /{id}

Also removes FamilienarchivApplicationTests (full @SpringBootTest
requires DB + S3 infrastructure; covered by e2e job instead).

65 tests total, all passing.
Refs #4

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 21:33:24 +01:00
Marcel
7cb20dec50 test: add e2e tests 2026-03-17 13:34:05 +00:00
Marcel
973620a097 build: add mvn properties 2026-03-17 13:33:02 +00:00
Marcel
25e095ea47 refactor: enforce Controller → Service → Repository layering throughout backend
- Created TagService: encapsulates all tag find/create/update/delete operations
- Extended PersonService: added findAll(), getById(), getAllById(), findOrCreateByAlias()
- Extended UserService: added createGroup(), updateGroup(), deleteGroup(), getGroupById()
- DocumentService: replaced direct PersonRepository/TagRepository access with
  PersonService/TagService calls; added getDocumentById(), getDocumentsBySender(),
  getConversationFiltered(), deleteTagCascading()
- MassImportService: replaced PersonRepository/TagRepository with PersonService/TagService
- PersonController: removed direct repo injections, delegates to PersonService/DocumentService
- DocumentController: removed DocumentRepository injection, delegates to DocumentService
- TagController: removed TagRepository/DocumentRepository, delegates to TagService/DocumentService
- GroupController: removed UserGroupRepository injection, delegates to UserService

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 08:49:33 +01:00
Marcel
97e5255d7f refactor: move createPerson logic into PersonService
Controller was directly calling personRepository.save() for person creation.
Extracted into PersonService.createPerson() to enforce Controller → Service → Repository layering.
Also documented the layering rules in CLAUDE.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-17 08:28:30 +01:00
Marcel
0123dffdc4 feat: add create person feature via web interface
- Backend: new POST /api/persons endpoint in PersonController
- Frontend: new /persons/new route with Vorname/Nachname/Alias form,
  redirects to the new person's detail page on success
- Persons list: subtle '+ Neue Person' link below the page title

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 16:23:05 +01:00
Marcel
6fcff3355d feat: add create document feature via web interface
- Backend: new POST /api/documents endpoint with DocumentService.createDocument()
  reusing DocumentUpdateDTO; handles file upload, tags, sender, receivers
- Frontend: new /documents/new route with same four-section form as edit page
  (Wer & Wann, Beschreibung, Transkription, Datei) but with empty fields
- Home page: subtle '+ Neues Dokument' link above the document list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 16:04:56 +01:00
Marcel
8a9e7bd9eb fix: resolve all 47 svelte-check errors and 10 a11y warnings
Root cause 1 — OpenAPI types: add @Schema(requiredMode=REQUIRED) to
non-nullable fields on Person, Tag, Document, AppUser, UserGroup;
regenerate api.ts so required fields are no longer optional.

Root cause 2 — Stale types: api.ts regenerated, picking up the Tag
endpoint fix from commit 62189d8 (List<Tag> instead of List<String>).

Root cause 3 — openapi-fetch error pattern: replace `if (apiError)`
(broken when error type is never/undefined) with `if (!result.response.ok)`
across all +page.server.ts files. Cast error via `unknown` to satisfy TS.

Root cause 4 — FormData casts: add `as string` / `as string[]` to
FormData.get() / FormData.getAll() calls in admin/+page.server.ts.

Standalone fixes:
- +page.server.ts: return error field so home page template compiles
- documents/[id]/+page.svelte: type loadFile param, remove invalid iframe `type`
- conversations: type documents as Document[] instead of unknown[]
- persons/[id]: non-null assert person data after ok-check

a11y: aria-label on all icon-only buttons in TagInput and admin page,
replace invalid <label> with <p> for compound controls, remove autofocus.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-16 12:08:25 +01:00
Marcel
62189d8bb3 fix: show names in admin tag panel 2026-03-15 21:08:06 +00:00
Marcel
ee279a29e5 feat: edit persons 2026-03-15 20:47:01 +00:00
Marcel
4dd4d81ca3 fix: replace WorkbookFactory with native XML/ZIP parser for ODS files
WorkbookFactory throws ODFNotOfficeXmlFileException on .ods files —
Apache POI does not support ODF format at all.

Replace ODS reading with a direct content.xml parser using Java's
built-in ZipFile + DOM API (no new dependency). ODS is a ZIP archive;
the spreadsheet lives in content.xml as standard ODF XML.

Also refactors the import pipeline to decouple file reading from import
logic: both ODS and XLSX paths now produce List<List<String>> which is
processed by format-agnostic row logic. XLSX date cells are now
converted to ISO strings before processing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 21:09:46 +01:00
Marcel
5cc4dcf7aa config: update import column mapping to match ODS structure
Rename app.import.excel.col.* → app.import.col.* and set correct
column indices for all fields in the ODS spreadsheet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 20:50:22 +01:00
Marcel
5abec093e5 feat: rewrite MassImportService for ODS import
- Use WorkbookFactory.create() to support .ods, .xlsx, and .xls
- Discover any spreadsheet file (not just .xlsx) in /import
- Fix column indices to match actual ODS structure (index=0, box=1,
  folder=2, sender=3, receivers=5, date=7, location=9, tags=10,
  summary=11, transcription=13)
- Append .pdf extension to bare index values (W-0001 → W-0001.pdf)
- Build German-format title: "W-0001 – 15. Februar 1888 – Rotterdam"
- Parse ISO date strings (col 7 is text in LibreOffice ODS)
- Resolve sender (col 3) and receivers (col 5) to Person entities via
  lookup-or-create by alias using PersonNameParser normalisation
- Import tag (col 10) via lookup-or-create
- Import summary from col 11 (Inhalt)
- Import archiveBox (col 1) and archiveFolder (col 2)
- Inject PersonRepository and TagRepository

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 20:50:06 +01:00
Marcel
6e5761840c feat: add findByAliasIgnoreCase to PersonRepository
Required by MassImportService to look up persons by their full name
stored as alias before deciding to create a new Person record.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 20:47:38 +01:00
Marcel
cbdc48a061 feat: add PersonNameParser utility for ODS name normalisation
Pure static utility that parses raw name strings from the ODS into
structured Person data. Handles multi-receiver patterns like
"Walter und Eugenie de Gruyter" → [Walter de Gruyter, Eugenie de Gruyter],
parenthesised last names, "geb." maiden-name stripping, and
"Familie" filtering. Includes unit tests for all patterns found in the data.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 20:47:10 +01:00
Marcel
a6acc11fc0 feat: add archiveBox and archiveFolder fields to Document
Maps cols 1 (Box) and 2 (Mappe) from the ODS to the Document entity.
These are physical archival location identifiers needed to locate
original documents in the physical archive.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 20:44:09 +01:00
Marcel
fa60c5be90 feat: remove ExcelService and web-upload import path
The ODS-from-filesystem mass import is the sole import workflow.
ExcelService (web-upload Excel) is deleted, and
DocumentService.updateOrCreateFromExcel() which it exclusively called
is removed along with it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 20:43:48 +01:00
Marcel
79eccd5598 fix: store content type at upload time instead of guessing from extension
Previously FileService fell back to extension-based MIME detection, causing
TIFF, HEIC, DOCX and other unlisted types to be served as octet-stream
(forced download instead of inline display).

- Add content_type column to documents (V3 migration)
- Store file.getContentType() in DocumentService on upload and file replace
- MassImportService uses Files.probeContentType() for local files
- DocumentController prefers doc.getContentType() over S3-reported type
- FileService: remove extension-based fallback (no longer needed)
- DocumentService: replace leftover ResponseStatusException with DomainException

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 14:42:19 +01:00
Marcel
6c723aeb8c fix: remove spring-boot-starter-data-rest (security vulnerability)
Data REST was auto-exposing raw JPA repository endpoints (/appUsers,
/documents, /persons, /userGroups, etc.) that completely bypass the
@RequirePermission AOP checks — effectively making the entire database
readable and writable without authentication.

All API needs are covered by the custom controllers. The generated
api.ts is reverted to the stub until npm run generate:api is re-run
against the cleaned backend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 14:22:41 +01:00
Marcel
251d865ddc fix: permit OpenAPI/Swagger endpoints in dev profile
Spring Security was blocking /v3/api-docs with 401, preventing
npm run generate:api from fetching the spec. The springdoc paths are
now whitelisted only when the dev Spring profile is active.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 14:13:36 +01:00
Marcel
d76248cffd refactor: migrate all page.server.ts files to typed API client
All server-side fetch calls now go through createApiClient() from
$lib/api.server.ts, which wraps openapi-fetch with the generated OpenAPI
types. This means backend changes are reflected in the frontend after
running npm run generate:api.

- Add stub src/lib/generated/api.ts (replaced by generate:api output)
- Fix GroupController: missing /api prefix and ResponseStatusException
- Root, conversations, persons, documents pages all use typed client
- Error handling uses apiError.code directly (no parseBackendError needed)
- Edit page load uses typed client; PUT action keeps raw fetch (multipart)
- Login keeps raw fetch (explicit Authorization header, not cookie auth)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 13:39:15 +01:00
Marcel
5d356cd694 feat: add OpenAPI spec (dev only) with typed frontend client
- Add springdoc-openapi 3.0.2 (supports Spring Boot 4) to backend
- Disable api-docs/swagger-ui in application.yaml (prod default)
- Enable both in application-dev.yaml; Swagger UI at /swagger-ui.html
- Add openapi-fetch (runtime) and openapi-typescript (dev) to frontend
- Add generate:api npm script — run with backend up to regenerate types
- Add src/lib/api.server.ts typed client factory (uses SvelteKit fetch)
- Gitignore src/lib/generated/api.ts (generated artifact)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 13:30:57 +01:00
Marcel
4cc86de143 feat: replace raw error messages with structured error codes
Backend now returns { code: ErrorCode, message: string } for all errors,
making it language-agnostic. Frontend maps codes to localised strings via
Paraglide (en/de/es), so translations live in messages/*.json.

- Add ErrorCode enum and DomainException with static factory methods
- Update GlobalExceptionHandler to return ErrorResponse(code, message)
- Replace ResponseStatusException throughout controllers/services/aspects
- Add frontend errors.ts with parseBackendError() and getErrorMessage()
- getErrorMessage() delegates to Paraglide m.error_*() functions
- Add error_* keys to messages/en.json, de.json, es.json
- Update all page.server.ts files to use the new error utilities
- Fix hardcoded localhost URLs in admin and login pages
- Fix missing baseUrl in deleteTag action

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 13:15:28 +01:00
Marcel
ace57e9fc7 feat: add import status tracking to MassImportService
The async import previously ran fire-and-forget with no way to know
if it succeeded, failed, or was still running.

- Add ImportStatus record (state, message, processed count, startedAt)
  and a volatile currentStatus field updated throughout the async run
- POST /api/admin/trigger-import now returns 202 Accepted with initial status
- GET /api/admin/import-status lets callers poll for the current state

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 12:37:09 +01:00
Marcel
82974170e9 fix: make Permission enum complete and grant all permissions to admin group
Three related issues:
- READ_ALL was stored in the DB but missing from the Permission enum
- ADMIN_USER, ADMIN_TAG and ADMIN_PERMISSION were in the enum and used
  in controllers but never granted to any user, making those endpoints
  permanently inaccessible
- No runtime signal when a DB permission string drifts from the enum

Changes:
- Add READ_ALL to Permission enum
- Grant all six permissions to the Administrators group in DataInitializer
- Warn in CustomUserDetailsService when a DB permission string has no
  matching Permission enum value

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 12:35:42 +01:00
Marcel
35b998a0e8 refactor: remove unused Spring Session JDBC dependency and tables
Spring Session was pulled in as a dependency but never used — auth is
stateless HTTP Basic, so sessions are never written. Removed:
- spring-boot-starter-session-jdbc (and test variant) from pom.xml
- spring_session and spring_session_attributes tables/indexes/constraints
  from V1__initial_schema.sql

Added V2 migration to drop the tables on existing databases that already
ran V1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 12:32:38 +01:00
Marcel
e89dd5dc3c docs: document why CSRF is intentionally disabled in SecurityConfig
The previous comment implied CSRF was disabled as a temporary dev
convenience. Replaced it with an explanation of why it is safe with
the current Authorization-header-based auth scheme, and added a
clear note on when it must be re-enabled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 12:27:50 +01:00
Marcel
d50cfc7718 fix: remove duplicate 'Admins' group from dev data initializer
initData was creating an 'Admins' group with identical permissions to
the 'Administrators' group already created by initAdminUser, resulting
in two redundant groups on every fresh start. Removed the duplicate,
dropped the now-unused groupRepo parameter, and corrected the log
message which claimed 50 persons were created when only 4 were.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 12:26:22 +01:00
Marcel
adee746b23 refactor: migrate to YAML config and add Maven dev/prod profiles
- Replace application.properties with application.yaml (base/prod config)
  and application-dev.yaml (dev overrides: show-sql=true)
- Add Maven 'dev' profile (activeByDefault) and 'prod' profile to pom.xml;
  spring-boot:run picks up the active Spring profile automatically
- Guard DataInitializer.initData with @Profile("dev") so test data is
  never seeded in production

Local dev: ./mvnw spring-boot:run (dev profile active by default)
Production: SPRING_PROFILES_ACTIVE env var controls the Spring profile;
            Maven profiles are irrelevant for the packaged JAR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 12:21:16 +01:00
Marcel
107cec8a2f fix: remove plaintext password from startup log in DataInitializer
The log statement revealed the default admin password in application
logs. Now only the username is logged, using the resolved variable
instead of a hardcoded string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 12:16:27 +01:00
Marcel
09ec2103c8 fix: correct malformed @Value annotations in DataInitializer
Missing closing braces caused Spring to inject the literal placeholder
string instead of resolving the property, silently ignoring any
app.admin.username / app.admin.password env-var overrides.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 12:16:00 +01:00
Marcel
e63adb964d restructure: flatten workspace nesting, move devcontainer to root
- backend/workspaces/backend/ → backend/
- backend/workspaces/frontend/ → frontend/
- backend/.devcontainer/ + .vscode/ → repo root (where VS Code expects them)
- loose scripts/SQL files → scripts/
- replace nested git repo with single repo at project root
- update docker-compose.yml build context and devcontainer.json path
- add root .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-15 11:47:58 +01:00