When the e2e profile is active, initE2EData (which creates a reader user)
can run before initAdminUser. The old count() == 0 guard then skips admin
creation entirely, causing every login test to fail with 401.
Switch to findByUsername(adminUsername).isEmpty() so the admin is created
regardless of which CommandLineRunner runs first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add PasswordResetToken entity, repository (Flyway V8 migration)
- PasswordResetService: token generation, validation, nightly cleanup
- AuthController: POST /api/auth/forgot-password and /api/auth/reset-password (both permitAll)
- AuthE2EController (@Profile("e2e")): GET /api/auth/reset-token-for-test for CI testing
- spring-boot-starter-mail dependency; JavaMailSender optional (@Autowired required=false)
- mail health indicator disabled; mail config via MAIL_HOST/PORT/USERNAME/PASSWORD env vars
- 5 unit tests written TDD-style (all pass)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a "Leser" group (READ_ALL only) and "reader" / "reader123"
user to the deterministic e2e seed so the permissions spec can log
in as a read-only user without relying on admin-created test data.
Refs #48
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New GET /admin/users/new page: create user with all profile fields
(login, password, firstName, lastName, birthDate, email, contact, groups)
- New GET /admin/users/[id] page: edit user profile, groups, and
optional password change without requiring current password
- New PUT /api/users/{id} backend endpoint (ADMIN_USER permission)
with AdminUpdateUserRequest DTO for admin-override user updates
- Refactored admin users tab: replaced inline editing with edit links
to dedicated routes; create button now links to /admin/users/new
- Extended CreateUserRequest with profile fields so new users can be
created with full profile data in a single request
- Added 28 component tests across 3 new spec files (TDD)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add firstName, lastName, birthDate, contact to AppUser via V7 migration.
Add PUT /api/users/me and POST /api/users/me/password endpoints.
Add GET /api/users/{id} for public profile lookup.
Add EMAIL_ALREADY_IN_USE and WRONG_CURRENT_PASSWORD error codes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes#29
Backend:
- Add PersonRepository.findCorrespondents / findCorrespondentsWithFilter
(native SQL, orders by shared document count DESC, limit 10)
- Add PersonService.findCorrespondents(personId, q) delegating to the
correct repository method based on whether a query string is present
- Expose GET /api/persons/{id}/correspondents?q= in PersonController
Frontend:
- Add optional restrictToCorrespondentsOf prop to PersonTypeahead
- On focus with the prop set, fetch correspondents immediately (no typing
required) — opens the dropdown showing top correspondents
- On input with the prop set, hit the correspondents endpoint with q= param
- Without the prop, keep existing /api/persons?q= behaviour unchanged
- Wire the prop bidirectionally in /conversations: sender restricts receiver
and vice versa
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reduces parameter count from 7 to 2 (id + dto), keeping all validation
and trimming logic in the service. Controller now binds request JSON
directly to the DTO via @RequestBody.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- UserService: remove debug log dumping all DB groups ("Groupds in DB"),
fix indentation of createUserOrUpdate, clean up log messages
- DocumentService: fix typo reciever → receiver in searchDocuments parameter,
remove broken log.info("Tags", tags) with missing format specifier,
replace bare orElseThrow() with DomainException in updateDocumentTags
and createDocument, remove what-comments on Lombok annotations
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add findByReceiversId to DocumentRepository, getDocumentsByReceiver
to DocumentService, and GET /api/persons/{id}/received-documents
to PersonController. Tests added for both service and controller layers.
Closes#1
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Avoids Flyway errors when columns already exist in the DB due to
migration history mismatches from parallel feature branches.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolves merge conflicts with main (feat/person-notes merged first).
Combines both features: birth/death years and notes field on person detail.
Renames migration V5__add_birth_death_years to V6 to avoid Flyway conflict.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
V5 Flyway migration adds TEXT notes column; Person entity, service, and
controller updated to persist notes. Frontend edit form adds textarea and
view mode renders the notes section. Backed by 2 new service unit tests
(persist + blank clears).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
V5 Flyway migration adds birth_year and death_year INTEGER columns.
Service validates birthYear <= deathYear (400 otherwise). Frontend edit
form adds year number inputs; view mode renders * year / † year. Backed
by 3 backend service tests and 1 E2E test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>