On first visit (no PARAGLIDE_LOCALE cookie), parse the Accept-Language
request header and set the cookie to the best matching supported locale
(de/en/es). The user's manual choice via the switcher always takes
precedence since the detection is skipped when the cookie exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract all hardcoded German strings from every .svelte file and component
into Paraglide message keys. Add complete translations for all keys in
messages/en.json (English) and messages/es.json (Spanish/Mexico).
Changes:
- messages/de.json: 100+ keys covering navigation, buttons, form labels,
placeholders, section headings, empty states, and error messages
- messages/en.json, messages/es.json: complete translations for all keys
- project.inlang/settings.json: change baseLocale from "en" to "de"
- +layout.svelte: add DE/EN/ES language selector in header using setLocale();
active language is bold, choice persists via Paraglide cookie strategy
- All 10 route pages + 3 shared components: replace hardcoded German with m.key()
- e2e/lang.spec.ts: E2E tests for language selector visibility, switching,
persistence across navigation, and active state highlighting
Closes#2
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both SvelteKit API proxy routes were hardcoding http://localhost:8080,
breaking typeahead search in Docker environments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The userGroup hook was hardcoding http://localhost:8080 instead of
reading API_INTERNAL_URL from the environment. In Docker this caused
the /api/users/me fetch to fail silently, leaving event.locals.user
unset and triggering the handleAuth guard to redirect every page to
/login — including the login form action itself, creating an infinite
redirect loop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace __dirname with fileURLToPath(import.meta.url) for ESM compatibility
- Start SvelteKit dev server on port 3000 with 120s webServer timeout
- Add data-hydrated attribute (set in onMount) so tests wait for hydration
- Fix nav active class assertions: text-brand-navy (not border-brand-navy)
- Fix filter button selector: exact match to avoid matching "Alle Filter löschen"
- Fix date validation test: use pressSequentially('99') to trigger dateInvalid
- Fix person/document search: navigate directly to URL with query param
(avoids debounced oninput → goto race condition in CI)
- Fix heading selector: level: 1 to avoid strict-mode with h1+h2 on page
- Fix auth redirect: return 401 from handleFetch instead of throwing redirect
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- frontend/Dockerfile: Node 20 Alpine image running npm run dev
- docker-compose: frontend service with depends_on db/minio/backend,
source mounted as volume, named volume for node_modules to avoid
OS binary conflicts between host and container
- vite.config.ts: make API proxy target configurable via
API_PROXY_TARGET env var (defaults to localhost:8080 for local dev,
set to http://backend:8080 inside Docker)
- .env: update PORT_FRONTEND to 5173 (actual vite dev server port)
Usage:
docker compose up frontend # starts frontend + all dependencies
docker compose up # starts everything
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The logo was changed from an SVG to a plain <span>, causing
getByText('Familienarchiv') to match both the logo and the footer.
- Update test to use getByRole('link') for precision
- Remove "De Gruyter" from footer text to align with branding change
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace `export let` with `$props()` and `$bindable()` across all components
- Replace `$:` reactive statements with `$derived()` and `$effect()`
- Replace `createEventDispatcher` with callback props (e.g. `onchange`)
- Replace `on:event` directives with inline event handlers (`onclick`, `oninput`, etc.)
- Replace `<slot />` with `{@render children()}` in layout
- Use `untrack()` for SSR-safe $state initialization from reactive props
- Replace `blur` + `setTimeout` anti-pattern in TagInput with `clickOutside` action
- Fix `page` store usage in layout to use `$app/state` directly
- 0 errors, 0 warnings after svelte-check
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaced sticky full-bleed bar with a regular card-style row,
matching the form card width and adding mt-4 top margin.
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>
Added white background, explicit border, and rounded corners to make
the search field clearly visible against the sand-colored page background.
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>
Use Intl.DateTimeFormat with de-DE locale to show full German month
name. Appending T12:00:00 to the ISO string avoids UTC-midnight
timezone shifts misrendering the day.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Show a red border and format hint ("TT.MM.JJJJ") only when the user
has typed something but the date is not yet complete. Focuses-only
or empty fields show no error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace native date picker (mm/dd/yyyy) with a plain text input that
auto-inserts dots as the user types (20122026 → 20.12.2026). A hidden
field transforms the display value back to ISO (yyyy-mm-dd) before
the form is submitted to the backend.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full UX overhaul of the edit page targeting non-technical users:
- Grouped fields into four labelled sections (Wer & Wann, Beschreibung, Transkription, Datei)
- Replaced date text field with native <input type="date">
- Replaced sender <select> with PersonTypeahead component
- Replaced receiver multi-select with new PersonMultiSelect (chip-based)
- Sticky save bar at bottom with cancel/save actions
- Aligned all colours to brand palette (no more blue-*)
- Fixed a11y: replaced invalid <label> on compound components with <p>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the native multi-select pattern with a typeahead + dismissible
chips UI. Uses fixed dropdown positioning (same getBoundingClientRect
trick as PersonTypeahead) to escape overflow:hidden parents.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Now contains only the 16 intentional /api/* endpoints. TypeScript will
enforce path and parameter correctness for all typed client calls.
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>