- /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>
When navigating from the conversations page via the 'New document in this
correspondence' link, the senderId and receiverId query params are now read
in the server load, resolved to person names, and used to pre-populate the
sender typeahead and receiver multi-select on the form.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers: empty state, swap button (visible/hidden, goto called with
swapped params), summary content, year dividers, and new document link
visibility gated by canWrite.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The guard was lost when the button was moved into the grid between the
two person inputs. Without it the button rendered even when no persons
were selected, breaking the UX and the E2E assertion.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The link navigates to a page that requires WRITE_ALL. Guard it with
data.canWrite (supplied by the layout) so read-only users never see a
link that leads to a 403.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On desktop the button sits between the two typeaheads as an icon-only
button (icon rotated 90° to point left/right) aligned to the input
baseline. On mobile it renders full-width with the label text between
the stacked fields.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a link next to the summary that navigates to the new-document form
with senderId and receiverId pre-filled from the current conversation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renders a horizontal rule with the year label between consecutive
documents that belong to different years.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows a summary line above the conversation listing with total document
count and the year span, e.g. "4 Dokumente · 1923–1965".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a button between the two person typeaheads that swaps sender and
receiver, then reloads the conversation view.
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>
The sync $effect on the home page unconditionally overwrote the local `q`
state with the URL value after every navigation. When users typed faster
than a navigation round-trip (debounce fires → goto() → data reloads),
the completed navigation wrote the stale URL value back into the input,
dropping the characters typed in the interim.
Guard the `q` assignment in the effect with a `qFocused` flag (set via
onfocus/onblur on the text input). Covers issue #34.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Document the three key rules enforced by eslint-plugin-svelte:
- svelte/require-each-key: why position-based tracking silently corrupts state
- svelte/prefer-writable-derived: why $state+$effect is wrong for computed values
- svelte/prefer-svelte-reactivity: why SvelteMap/SvelteURLSearchParams are needed
Each rule includes bad/good code examples and a technical reason.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
## Pre-commit hook
- Add .husky/pre-commit at repo root: runs `cd frontend && npm run lint`
- Update prepare script in package.json to auto-configure git hooks path
on npm install (git -C .. config core.hooksPath .husky)
- Add lint step to CI unit-tests job so it catches issues before tests run
- Add generated dirs to .prettierignore (paraglide_bak*, test-results, .auth)
- Add src/lib/paraglide_bak* to .gitignore so ESLint can ignore them
## ESLint fixes (all pre-existing)
- Disable svelte/no-navigation-without-resolve: false positive in SvelteKit
(rule targets Svelte 5 standalone routing, not SvelteKit <a href>)
- Fix svelte/require-each-key: add (item.id)/(item) keys to all {#each} blocks
across 10 files — improves Svelte reconciliation performance
- Fix svelte/prefer-writable-derived in PersonTypeahead: $state+$effect → $derived
- Fix svelte/prefer-svelte-reactivity: URLSearchParams → SvelteURLSearchParams,
Map → SvelteMap (enables Svelte reactive tracking)
- Fix @typescript-eslint/no-unused-vars: remove dead imports/variables
## Prettier
- Run npm run format to bring all source files in line with .prettierrc
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Standard JSON does not allow trailing commas. The comma after the last
key in de/en/es.json caused paraglide to fail compilation, which meant
messages.js was never generated and all component tests crashed on import.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace 5 inline Intl.DateTimeFormat blocks with formatDate() across
home, conversations, persons detail, and document detail pages
- Fix coCorrespondents: $derived(() => ...) → $derived.by(...) —
the old form typed the value as a function, breaking template call sites
- Persons list: throw error on API failure instead of silently returning []
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Admin page: replace 7 identical error-handling blocks with a single
toActionResult() helper — DRY without over-abstraction
- New date.ts util: formatDate(isoDate) centralises the T12:00:00
timezone guard and Intl.DateTimeFormat locale config
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>
Covers naming, function design, guard clauses, comment policy, command-query
separation, DRY vs KISS trade-offs (KISS wins), and SOLID applied to the
Java backend and TypeScript/Svelte frontend. Linked from CLAUDE.md and
COLLABORATING.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>