Replaced the single shared sort control with per-section sort buttons placed
inline in each heading row (right-aligned via ml-auto). Each section now sorts
independently, which matches user expectation and keeps the control visually
anchored to the list it affects.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The floating stats bar was visually disconnected and showed a combined document
count already visible from the per-section badges. Replaced it with a year range
shown inline next to each section heading (e.g. "Gesendete Dokumente · 12 · 1921–1945"),
making the range contextually relevant per direction.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Client-side fetch('/api/documents/{id}/file') bypassed the handleFetch hook
that injects the Authorization header, causing the browser to receive a 401
with WWW-Authenticate: Basic and show a native auth dialog.
Added a SvelteKit server route at /api/documents/[id]/file that proxies the
request through the server, where handleFetch injects the auth cookie correctly.
Also fixed E2E default password (admin → admin123) to match application.yaml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add person_show_more key (DE/EN/ES)
- Limit sent/received document lists to 5 with a translated "show more" button
- Co-correspondent chips now link to /conversations?senderId=...&receiverId=...
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Gesendet/Empfangen badge is redundant since documents already appear
in separate Gesendete/Empfangene sections.
Refs #21
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Block direct URL navigation to /persons/new, /documents/new,
/documents/:id/edit for users without WRITE_ALL permission.
E2E tests verify admin user retains access to all write routes.
Closes#17
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wrap write-only elements with {#if data.canWrite} in:
- Home page: Neues Dokument link
- Persons list: Neue Person link
- Document detail: Bearbeiten button
- Person detail: edit button, edit form, merge section
Refs #17
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Derives canWrite from WRITE_ALL permission in user groups, available
as page.data.canWrite on every page without per-page boilerplate.
Refs #17
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Split document list into Gesendete / Empfangene Dokumente sections
- Add role badges (Gesendet / Empfangen) on each document card
- Add statistics strip showing total count and year range
- Add co-correspondents section with frequency-sorted chips
- Single sort toggle applies to both sections
Closes#1Closes#19Closes#21Closes#22
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split single documents load into sentDocuments and receivedDocuments,
fetched in parallel via Promise.all.
Refs #1
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>
- Add missing person_btn_conversations translation to de.json
- Fix birth/death year test: exclude /persons/new link + wait for hydration
- Fix lang test switching back to DE: wait for hydration + clear locale cookie
(headless Chromium doesn't reliably delete cookies via document.cookie)
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>
Extracted sortDocumentsByDate utility with full Vitest coverage (6 tests),
wired it into the person detail page with a DESC/ASC toggle button, and
added an E2E smoke test for the toggle interaction.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a "Konversationen anzeigen" link to the person detail page header
that navigates to /conversations?senderId={id}, pre-filling the person
as Person A. Includes i18n in de/en/es and an E2E test.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Paraglide's client-side setLocale writes the locale via document.cookie,
which silently fails for HttpOnly cookies. SvelteKit's cookies.set()
defaults to httpOnly: true, so locale switching never worked in tests.
Fix by setting httpOnly: false on the locale cookie (it's a UI preference,
not a credential — no security concern).
Add waitForSelector('[data-hydrated]') before any click that relies on
SvelteKit JavaScript event handlers. Without this, the click fires before
hydration and the onclick handler is not yet registered.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add exact: true to all language button selectors in lang.spec.ts to
prevent Playwright from matching "Abmelden" (contains "de") alongside
the DE language button
- Fix goto in conversations applyFilters to use absolute path
/conversations?... instead of relative ?... so URL updates correctly
- Set locale: 'de-DE' in playwright.config.ts so Accept-Language header
is consistent and locale detection defaults to German during tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract detectLocale() from hooks.server.ts into src/lib/server/locale.ts
so it can be tested in isolation. Add 7 unit tests covering:
- German, English, Spanish browser preferences
- Fallback when primary language is unsupported
- Quality value (q=) ordering
- Fully unsupported language → null
- Empty Accept-Language header → null
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- Switch unit-tests job to mcr.microsoft.com/playwright:v1.58.2-noble
container; Chromium and all system deps are pre-installed so the
browser install/cache dance is eliminated entirely (closes#13)
- Downgrade upload-artifact@v4 → v3 in both unit-tests and e2e-tests
jobs; v4 is not supported on Gitea (GHES) and was causing jobs to
report failure even when all tests passed, and prevented the
Playwright browser cache from ever being saved (closes#14)
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>
The runner's Docker client negotiates API 1.53 but the daemon on the
NAS only supports up to 1.43. Pin the version for all docker commands
in the e2e job, including the new network connect step.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The act_runner job runs inside a Docker container. Docker Compose port
mappings bind to the Docker host, not the job container's localhost —
so localhost:5433 was always refused.
Fix: after compose starts, connect the job container (identified by
/etc/hostname) to the archive-net compose network. Then switch the
backend startup args to use service names db:5432 and minio:9000
instead of host-mapped ports.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The e2e job was calling plain `docker compose up` without the CI
override file, so it used the base compose bind-mount for MinIO
(./data/minio) which doesn't exist on the runner. The CI override
replaces bind mounts with ephemeral named volumes.
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>
- Add unit-tests job using Playwright Docker image (no apt install needed)
- Add backend-unit-tests job with Java 21 + Maven
- Add e2e-tests job: PostgreSQL + MinIO via docker-compose, Spring Boot backend,
SvelteKit dev server, Playwright Chromium
- Use non-conflicting host ports (DB: 15432, MinIO: 19000/19001)
- Install Docker CLI via official Docker apt repo (Playwright image has no daemon)
- Connect job container to archive-net for direct DB/MinIO access
- Pin DOCKER_API_VERSION=1.43 for Docker socket compatibility
- Start backend with java -jar + health-check loop (curl /actuator/health)
- Use continue-on-error on cleanup step to handle SIGKILL gracefully
- Downgrade upload-artifact to v3 (v4 not supported on self-hosted Gitea)
- Always run npm ci unconditionally (actions/cache@v4 broken on this runner)
- Log /tmp/backend.log on startup timeout so Spring Boot errors are visible
- Add diagnostic steps for DB tables and Flyway schema history
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>
- 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 built-in cache: maven in setup-java@v4 does not reliably work
on self-hosted act runners. Replace with an explicit actions/cache@v4
on ~/.m2/repository keyed on pom.xml hash.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
node_modules and the Playwright Chromium binary were downloaded fresh
on every run, making setup account for ~99% of pipeline runtime.
- Cache frontend/node_modules keyed on package-lock.json hash
- Cache ~/.cache/ms-playwright keyed on package-lock.json hash
- On cache hit: skip npm ci and browser download, only reinstall
system deps (install-deps) which is much faster than a full install
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Avoids conflicts with any system services (PostgreSQL, MinIO) that
may already occupy 5432/9000/9001 on the runner host.
DB: 5433, MinIO API: 9100, MinIO console: 9101.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Port 5432 was already in use after docker compose cleanup because a
system-level PostgreSQL service on the runner host holds the port.
Also kill any stray containers binding to 5432/9000/9001.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>