Static import of pdfjs-dist fails during SSR because DOMMatrix and
other browser globals are unavailable in Node.js. Move the import into
onMount so it only ever executes in the browser. A plain pdfjsLib
variable holds the module; a $state boolean pdfjsReady triggers the
load-document effect once the library is available.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Install pdfjs-dist v5 and add optimizeDeps pre-bundle config
- New PdfViewer.svelte component: renders each page on a <canvas> with
correct device-pixel-ratio scaling, overlays a text layer (enables
text selection; foundation for annotations in #40), prev/next
navigation, zoom controls, and lazy page rendering (only current ±1
pre-fetched — avoids freezing on multi-page documents)
- Replace the <iframe> in documents/[id]/+page.svelte with PdfViewer;
image attachments continue to use <img>; detection now uses
doc.contentType instead of filename extension
- Unit tests for navigation controls and page counter (pdfjs mocked)
- E2E tests: PDF renders as canvas (not iframe), nav controls visible,
image fallback stays as <img>; minimal.pdf fixture for upload tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Browser-side fetch('/api/...') calls bypass SvelteKit's handleFetch hook
(which adds the Authorization header from the auth_token cookie for SSR).
As a result, client-side API calls in the dev server always got a 401.
Add a proxy configure hook that extracts the auth_token cookie from incoming
requests and sets it as the Authorization header before forwarding to the
backend. This makes browser-side fetches (history panel, file preview, etc.)
work correctly in dev mode.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All three history tests navigated to the doc page but didn't wait for
SvelteKit hydration, so the toggle onclick wasn't registered yet. Also
wait for versions to load (API call) before asserting on version items
or the compare button.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds ExpandableText.svelte which clamps text to 10 lines and shows a
toggle button only when the content actually overflows. Applied to the
summary and transcription fields on the document detail page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PDF viewer: append #zoom=page-width to iframe src so A4 letters fill
the panel width instead of leaving large grey gutters
- Diff view: trim unchanged context to 4 words either side of each
change, replacing long runs with '…' so edits are easy to spot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Admin can trigger an initial history snapshot for all documents without
version history. Shows count of backfilled documents after completion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds POST /api/admin/backfill-versions which creates an initial snapshot
(editorName="Datenimport", changedFields=[]) for every document that has
no version entry yet, using the document's createdAt as the version timestamp.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
versions array is ascending (oldest first), so the previous version
is at idx-1, not idx+1. Using idx+1 caused added/removed to be swapped,
showing new text as red and old text as green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three scenarios: versions list appears after edits, diff shows changed
field, compare mode displays diff between two selected versions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a collapsible history section to the document detail view, showing
all saved versions with changed-field labels, word-level diff between
adjacent versions, and a compare mode for any two arbitrary versions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Spring Boot 4 auto-configures a tools.jackson.databind.ObjectMapper bean.
The service was importing the Jackson 2 package, causing a no-qualifying-bean
error at startup.
Refs #38
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DocumentService now calls documentVersionService.recordVersion() after
createDocument and updateDocument. DocumentController exposes two new
read-only endpoints: GET /{id}/versions and GET /{id}/versions/{versionId}.
Refs #38
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Creates the document_versions table (V9) with JSONB snapshot and
changed_fields columns. DocumentVersionService records a version on
every create/update, resolves the editor name from the security context,
and computes changedFields by diffing against the previous snapshot.
Refs #38
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
waitForURL('/') resolves as soon as the URL changes but before SvelteKit
finishes hydrating — the avatar button's onclick is not yet registered,
so the click has no effect and the dropdown never opens.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers dev (Mailpit), production SMTP, all env vars with defaults,
common provider settings, and how to disable mail entirely.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a Mailpit container that catches all outgoing emails locally so
password reset links can be tested without a real SMTP server.
- Backend defaults to MAIL_HOST=mailpit / MAIL_PORT=1025 in compose
- SMTP auth and STARTTLS disabled for Mailpit (no credentials needed)
- Web inbox available at http://localhost:8025
- Production SMTP still works by overriding MAIL_HOST, MAIL_PORT,
MAIL_USERNAME, MAIL_SMTP_AUTH, and MAIL_STARTTLS_ENABLE in .env
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- /forgot-password: email form → sends POST /api/auth/forgot-password → success banner
- /reset-password: password form reads token from URL → sends POST /api/auth/reset-password
- Login page: add "Passwort vergessen?" link
- hooks.server.ts: add /forgot-password and /reset-password to PUBLIC_PATHS; skip auth
injection for public auth API endpoints
- errors.ts: add INVALID_RESET_TOKEN error code
- i18n: add all new message keys in de/en/es
- playwright.config.ts: use E2E_BASE_URL for webServer check URL (allows reusing docker
dev server at port 5173 locally)
- ci.yml: pass E2E_BACKEND_URL=http://localhost:8080 to E2E test step
- e2e/password-reset.spec.ts: 5 tests (4 pass locally, full flow requires e2e profile in CI)
- Regenerated OpenAPI types including new /api/auth/* endpoints
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>
admin.spec.ts: after clicking "Schlagwort bearbeiten", Svelte's {#if editingTagId}
replaces the span with a form, so familieRow (filtered by the span) no longer matches.
Find input[name="name"] and the save button directly instead.
auth.spec.ts: dropdown opens via {#if userMenuOpen} which renders asynchronously.
Wait for the Abmelden button to be visible before clicking to prevent a race condition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
throw error(403) kept the URL at /documents/new (the error page renders
in-place). Changed to throw redirect(303, '/') so the URL actually changes,
matching the E2E test expectation that a read-only user is redirected away.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After the layout load function started injecting user+canWrite into all
page data, the admin spec files failed svelte-check with missing property
errors. Add user:undefined, canWrite:true, and form:null to all fixture
data objects.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- admin: add exact:true to tab button assertions to avoid strict-mode
violations from "Benutzer löschen" title buttons matching "Benutzer"
- admin: change tag-row locator from hasText regex on <li> to has: span
filter (more robust against whitespace differences); add waitForSelector
after tab click to ensure panel is rendered before hovering
- auth: replace page.request.get('/api/users/me') with a profile page
navigation — direct browser requests don't carry Basic Auth, only
server-side SvelteKit fetches do
- documents: use getByRole('heading') instead of getByText to avoid strict
mode violation when the title appears in both h1 and breadcrumb
- persons: same heading fix for person creation landing page
- profile: remove success-message assertion after password change; the
auth_token cookie still holds old credentials so use:enhance's update()
immediately gets a 401 and redirects to /login before the message renders
— test now asserts the redirect directly, then re-logs in
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
E2E tests run on CI anyway — running them locally before every push
adds too much friction. Removed the hook; CI remains the safety net.
Refs #48
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Logs in as the seeded "reader" user (READ_ALL only) and asserts
that all write controls are absent from every page.
Refs #48
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Full lifecycle: create group → create user → edit user → reset
password → verify login → delete user → delete group → rename tag.
Self-contained: everything created is also deleted.
Refs #48
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Includes self-healing password change test that restores admin123
at the end so the shared session remains valid for subsequent specs.
Refs #48
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Guards against regressions where the session cookie is set but
the backend rejects it — a URL redirect alone is not enough.
Refs #48
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>
waitForURL(/senderId=/) resolved immediately because the URL already
contained senderId= before the swap navigation. Use a predicate that
waits for the specific swapped ID value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The logout action was moved into a user avatar dropdown in the nav.
The E2E test was clicking the now-hidden button directly.
Refs #35
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Every feature issue must include a User Journey and E2E Scenarios
section before implementation begins.
Refs #48
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a Husky pre-push hook so `npm run test:e2e` must pass before any
push is accepted. The login regression in 8f5c13f would have been caught
immediately had this gate been in place.
Closes#48 (enforcement side — coverage gaps tracked separately).
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>
The login action sends Basic auth via an explicit Authorization header.
handleFetch was intercepting this request and returning 401 because no
auth_token cookie exists yet (the user isn't logged in), never forwarding
the credentials to the backend.
Fix: if the outgoing request already has an Authorization header, pass it
through unchanged. Only inject the cookie-based token for requests that
don't provide their own auth.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace Playwright locator .click() calls with native DOM element.click()
for all tests that trigger Svelte 5 delegated onclick handlers ($.delegated).
Playwright's CDP-based synthetic events don't propagate through Svelte 5's
document-level handle_event_propagation delegation mechanism, while native
DOM .click() does.
Also replace locator.click() with element.focus() for onfocus handler tests,
and add cleanup() to afterEach in all spec files missing it to prevent test
pollution between runs. Fix TagInput.svelte to use untrack() when reading
bindable state after an await to avoid track_reactivity_loss errors.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the browser-native type="date" picker with a text input using
the same german format (dd.mm.yyyy with auto-dot insertion) as the
document date fields. A hidden input sends the ISO value to the server.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add user: undefined to baseData in conversations and documents/new specs.
Change null to undefined for filePath/transcription in makeDoc fixture.
Add form: null to render calls missing it.
Fix birthYear conversion from string to number in persons/[id] server action.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The handleFetch hook previously skipped auth headers for all URLs
containing /api/users/me. Since the hook's own user-load call uses
globalThis.fetch (bypassing handleFetch), it is safe to remove this
exception — enabling profile update and password change actions to
authenticate properly.
Also regenerates API types with new profile endpoints and AppUser fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/profile: two-card layout with personal info form (name, birth date,
email, contact) and password change form, each with independent actions.
/users/[id]: read-only public view showing name, username, email, contact
with avatar circle initials.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Show user initials (e.g. MM) in a circular button when name is set,
or a fallback person icon. Clicking opens a dropdown with links to
/profile and a logout form.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add profile_* message keys for the profile page forms in de/en/es.
Add EMAIL_ALREADY_IN_USE and WRONG_CURRENT_PASSWORD to ErrorCode type and
getErrorMessage switch.
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>
The persons list search input used value={data.q || ''} bound directly to
server data, so every navigation completion would reset it to the URL value
mid-typing, dropping keystrokes just like issue #34 on the home page.
Apply the same focus-guard fix: introduce local `q` state, a `qFocused`
flag, and a guarded $effect that only syncs URL → state when the input is
not focused. Adds a regression test matching the home-page pattern.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The swap button was conditionally removed from the DOM with {#if}, which
caused the receiver input to collapse into the narrow auto column of the
grid-cols-[1fr_auto_1fr] layout on desktop when no persons were selected.
The button is now always rendered. On desktop it becomes invisible
(visibility:hidden) when no persons are selected, preserving the middle
column width so both 1fr columns stay equal. On mobile it remains hidden
(display:none) via the hidden class so no empty gap appears between the
stacked inputs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>