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>