runSearch swallows non-OK responses and fetch rejections to an empty
items list. The user sees "Keine Personen gefunden" identically to a
genuine empty result. These two tests pin that behaviour so a future
distinct-error-UX implementer is forced to update the assertions.
Sara #2 on PR #629.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
userEvent.type(@Walter) types 7 keys; CI jitter can space the gaps past
the 150 ms debounce and fire 2+ fetches, even though the request-token
guard discards the stale response. fill() collapses the input into one
event so the assertion (exactly 1 fetch) becomes deterministic.
Sara #1 on PR #629.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Test-only helper colocated with production code now has a visible
.test-fixture.svelte boundary so eslint-boundaries and code search
do not confuse it for a production component. The internal alias was
also bumped from *Host to *Fixture for consistency. No behaviour
change. Felix #3 / Nora #3 on PR #629.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The mirror effect on the dropdown's searchQuery looks like it should be
\$derived but it cannot be: bind:value on the <input> writes to the same
state, so it must remain mutable. Felix #2 on PR #629.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Without this, a closed dropdown's trailing runSearch could fire against
the next dropdown's state and silently overwrite its items before its
own fetch resolved. Felix #1 on PR #629.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Markus on PR #629 — the cancel-not-flush contract is what the
PersonMentionEditor onDestroy path relies on. Spell it out so future
callers can rely on the same guarantee.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drops the `as unknown as Person` double-cast in makePerson and on
AUGUSTE/ANNA in favor of plain return-typed object literals; this
restores the type-system safety net Felix flagged on PR #629 — a
future required field on Person now fails compilation in the fixture
instead of silently slipping through.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Extracts SEARCH_DEBOUNCE_MS + POST_DEBOUNCE_SLACK_MS at the top of the
spec and bumps the post-debounce wait from 250/300 ms to 500 ms.
Addresses Felix's "magic number" suggestion and Sara's flake-risk
concern on PR #629. (Sara's fake-timer alternative collides with
userEvent + vi.waitFor in vitest-browser; the slack bump achieves the
same deterministic outcome with no fragility.)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds &limit=5 to the /api/persons request so the client signals its
intent and stays consistent with the SEARCH_RESULT_LIMIT slice. Backend
enforcement (and the broader PersonSummaryDTO response-shape audit) is
tracked separately. Markus on PR #629.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Once the user edits the dropdown search input, subsequent editorQuery
changes from the host editor must not overwrite it. Felix on PR #629.
Adds a small test host that exposes a setter for editorQuery so the
test can drive reactive prop changes.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Collapse the two empty-state branches into a single p[aria-live=polite]
whose text derives from the search query. Screen readers now hear the
transition between "Namen eingeben…" and "Keine Personen gefunden".
Leonie FINDING-MENTION-002 on PR #629.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bump h-4 w-4 to h-5 w-5 and text-ink-3 to text-ink-2 so the icon
carries enough visual weight to identify the input region without a
visible text label. Leonie FINDING-MENTION-001 on PR #629.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Soft-cap on the client side mitigates CWE-400 query amplification
(server-side cap remains a separate backend PR).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tag each runSearch with an incrementing requestId; discard responses
whose id no longer matches the latest onSearch. Prevents a slow fetch
from repopulating the dropdown after the user has cleared the search.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tiptap's suggestion items() callback fired a fetch on every keystroke
after `@`, in parallel with the debounced search-input fetch. Its result
was discarded by updateState, so it was pure waste — doubling the load
on /api/persons and confusing the debounce.
Returning [] from items() routes the entire fetch flow through the
search-input -> debounced onSearch path. New test pins @Walter to
exactly one fetch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
For issue #380. Drops the redundant MentionDropdown.svelte.spec.ts that
was added earlier in this branch and folds its search-input coverage
into the long-established MentionDropdown.svelte.test.ts. Same
test surface, single file.
While there:
- Updates the empty-state test to match the new behaviour: an empty
search field shows the "Namen eingeben…" prompt; "Keine Personen
gefunden" only appears when a query is entered but nothing matches.
- Fixes pre-existing Person-type drift in makePerson (missing
personType, familyMember).
- Stricten the create-new link rel assertion to cover the new
noreferrer addition.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
For issue #380 (AC-2, AC-3, AC-4 + NFR debounce).
The search input is now the single fetch trigger. The dropdown's
searchQuery reactivity calls onSearch on every change — whether sourced
from the editor mirror or the user's own input. PersonMentionEditor
debounces these calls at 150 ms, short-circuits on empty queries (no
fetch, items cleared), and tears down pending timers on destroy.
The Tiptap suggestion plugin's items() now returns [] — per-keystroke
fetches in the editor are gone. The same /api/persons?q= endpoint is
used; the difference is in when and how often the request fires.
Adds a cancel() method to the debounce utility so destroyed editors
don't leave trailing fetches alive (which previously polluted the test
ledger and would have wasted bandwidth in production tab-close races).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
For issue #380 (Nora CWE-116). The "Neue Person anlegen" link opens in
a new tab and was missing `noreferrer` — the new tab could read
window.opener and the referrer leaked the transcription URL. Same-origin
risk is low but the omission was unintentional.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
For issue #380 NFR. The transcriber audience is 60+ on laptops/tablets;
the search input must meet WCAG 2.2 AA touch target dimensions just like
the existing person result rows.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
For issue #380. The search input mirrors the @-text the user types until
the user takes ownership by typing into the input itself. After that,
the input owns its own state and editor typing no longer overrides it.
Two empty states now exist:
- "Namen eingeben…" when the search input is empty (AC-4)
- "Keine Personen gefunden" when the search input has a query but the
list is empty (existing behavior)
The dropdown reads editorQuery through the shared $state proxy via a
getter prop, matching the established pattern for model.items.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
For issue #380. Asserts that typing in the search input invokes the
onSearch prop with the current value — characterising the boundary that
PersonMentionEditor relies on for its debounced fetch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
For issue #380. Adds an explicit Playwright selector attribute on the
mention search input so E2E tests target a stable hook instead of a
fragile CSS class string.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
For issue #380. The dropdown now renders a dedicated search input at the
top, pre-filled with the text typed after @. This decouples the lookup
from the display text — the transcriber can edit the search field to
find a person whose stored name differs from what was typed.
The fetch wiring (onSearch callback) is consumed by PersonMentionEditor
in a follow-up commit; this commit only introduces the input UI and the
prop surface.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces hardcoded visible text 'Für Training vormerken' with
m.transcribe_mark_for_training() so the label translates in EN and ES.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces hardcoded 'Menü öffnen'/'Menü schließen' ternary with
m.layout_menu_open()/m.layout_menu_close() so the mobile nav toggle
announces correctly in EN and ES locales.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces hardcoded Zurück/Weiter/Verkleinern/Vergrößern aria-label strings
with m.viewer_previous_page(), m.viewer_next_page(), m.viewer_zoom_out(),
and m.viewer_zoom_in() so viewer controls translate in EN and ES locales.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds 7 Paraglide keys (viewer_previous_page, viewer_next_page,
viewer_zoom_out, viewer_zoom_in, transcribe_mark_for_training,
layout_menu_open, layout_menu_close) to de/en/es.json.
Adds messages.spec.ts to enforce key parity across all three locales.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After adding @Schema(requiredMode=REQUIRED) to InviteListItemDTO.shareableUrl,
npm run generate:api now emits shareableUrl as required. Replace the hand-rolled
InviteListItem interface with a type alias to the generated InviteListItemDTO
and remove the two 'as unknown as InviteListItem' casts + TODO comments.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix VALID_STATUSES fallback to use uppercase enum value
- Add TODO comment on InviteListItem cast pending type regeneration
- Guard revoke action against null id (returns fail 400)
- Add request: to delete action mock events for Sentry consistency
- Add expiresAt forwarding test for create action
- Add null-id guard test for revoke action
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add load() unit tests for admin/users/[id] (permission gate, 404, success)
- Rename .test.ts → .spec.ts for consistency with rest of suite
- Add @Schema(requiredMode=REQUIRED) to InviteListItem.shareableUrl
- Add client-side allowlist for invite status query param
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`@sentry/sveltekit` wraps load functions and reads `event.request.method` and
`event.url.pathname`. Mock events that omitted `request` or `url` threw
`TypeError: Cannot read properties of undefined` on every invocation, silently
masking 86 test failures on main.
Two root causes fixed:
- Added `request: new Request(...)` (and `url: new URL(...)` where absent) to
all mock event objects in 14 `*.server.spec.ts` files
- Changed `;` to `&&` in the `test:coverage` npm script so a failing server
run propagates its exit code instead of being swallowed by the client run
All 576 server-project tests now pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace fetch('/api/users/${id}', { method: 'PUT', ... }) + inline JSON
error parsing with createApiClient(fetch).PUT('/api/users/{id}', ...) and
the standard result.error cast pattern.
Also fix pre-existing Sentry mock event failures in layout.server.spec.ts
by adding request and url to the test event object.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace manual fetch(${apiUrl}/api/...) calls in load, create, and revoke
with createApiClient(fetch) so auth injection is handled by handleFetch
and the typed API contract is enforced at compile time.
Also fix pre-existing load test failures caused by Sentry's load wrapper
reading event.request.method (add request to the mock event object).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add min-h-[44px] py-2 to <summary> in ImportStatusCard for 44 px touch target
- Add SkippedFile and skipped count entries to docs/GLOSSARY.md
- Add MassImportServiceTest case: ALREADY_EXISTS fires before file I/O when doc is UPLOADED and file is present on disk
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Change importSingleDocument return type from boolean to Optional<String>
so callers in processRows receive the skip reason on every non-success path.
S3 upload failures now surface as "S3_UPLOAD_FAILED" and already-imported
documents as "ALREADY_EXISTS" in the skippedFiles list shown in the admin UI.
- Add two new tests: runImportAsync_addsS3UploadFailed_toSkippedFiles and
runImportAsync_addsAlreadyExists_toSkippedFiles; update
importSingleDocument_skips_whenDocumentAlreadyUploadedNotPlaceholder and
the S3-failure test to assert on the Optional return value.
- Add i18n keys for S3_UPLOAD_FAILED and ALREADY_EXISTS in de/en/es messages.
- Svelte ImportStatusCard: add aria-hidden="true" to SVG chevron, wrap
conditional warning section in aria-live="polite" div, add max-h-64
overflow-y-auto to skipped-files <ul> to cap height on large batches.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add comment to openFileStream() explaining package-private visibility
is intentional (Mockito spy seam for IOException test)
- Key {#each} skippedFiles by filename instead of array index
- Add test: skipped section hidden when state is FAILED
- Add test: reasonLabel returns raw code for unknown reason strings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use loop index as each key (handles duplicate filenames)
- Increase skipped filename font from text-xs to text-sm
- Add motion-safe guard to details chevron transition
- Replace text-warning with text-amber-900 to meet WCAG AA contrast
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- remove duplicate List import in AdminControllerTest
- derive skipped() from skippedFiles.size() — drop redundant int field
- use machine codes for SkippedFile.reason (INVALID_PDF_SIGNATURE, FILE_READ_ERROR)
- map reason codes to i18n strings in ImportStatusCard (de/en/es)
- replace raw amber Tailwind classes with warning semantic token
- fix <summary> accessibility: replace list-none with rotating chevron SVG
- replace <p> with <span> inside <summary> (phrasing content rule)
- extract setupOneValidOneFakeImport() helper — remove 3x copy-paste
- add lenient mock to short-file test for defensive coverage
- add IOException path test for isPdfMagicBytes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds SkippedFile to the local ImportStatus type and updates
ImportStatusCard to show an amber skipped-count section with a
collapsible filename list in the DONE state. Only rendered when
skipped > 0. i18n keys added for de/en/es.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- frontend/hooks.server.ts: replace request.url.includes('/api/') with
new URL(request.url).pathname.startsWith('/api/') so a page named
/my-api/something cannot accidentally match the API gate
- DomainException: add optional retryAfterSeconds field and a new
tooManyRequests() factory overload that carries the value
- LoginRateLimiter: pass windowMinutes * 60 as retryAfterSeconds when
throwing TOO_MANY_LOGIN_ATTEMPTS (RFC 6585 §4 SHOULD)
- GlobalExceptionHandler: emit Retry-After header when retryAfterSeconds
is set on a DomainException
- RateLimitInterceptor: emit Retry-After: 60 on 429 responses (1-min
window matches the existing MAX_REQUESTS_PER_MINUTE logic)
- LoginRateLimiterTest: assert retryAfterSeconds equals window duration
- RateLimitInterceptorTest: assert Retry-After header is set on 429
- JdbcSessionRevocationAdapterIntegrationTest: new @SpringBootTest +
Testcontainers test verifying revokeAll deletes all spring_session rows
and revokeOther leaves the current session intact
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove unreachable `&& !xsrfToken` condition from `handleFetch` guard;
simplify the redundant `cookieParts.length > 0` check that follows it
- Add `TOO_MANY_LOGIN_ATTEMPTS` to both Error Handling sections in CLAUDE.md
(backend and frontend) so LLMs are aware of the code without looking it up
- Add reverse-proxy IP trust and IPv6 address-cycling caveats to ADR-022
Consequences section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renders LoginPage with form.rateLimited=true and asserts that the
role="alert" div (clock icon + error message) is visible in the browser.
Previously only the form action's rateLimited=true return value was tested;
now the rendered UI is also verified.
Addresses Sara Concern 4 / Elicit open question from PR #617 review.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Regular error div was missing role="alert" — screen readers did not
announce it on dynamic display. Rate-limited clock icon used text-ink-3
(muted grey) instead of text-red-600, visually inconsistent with the
surrounding error text. Also removes the erroneous aria-invalid="true"
from the rate-limit alert div (not a permitted attribute on role=alert).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- handleFetch injects X-XSRF-TOKEN + XSRF-TOKEN cookie on all mutating
backend API requests (double-submit cookie pattern); generates a fresh
UUID when no XSRF-TOKEN cookie exists yet
- ErrorCode union gains CSRF_TOKEN_MISSING and TOO_MANY_LOGIN_ATTEMPTS;
getErrorMessage maps both to i18n keys
- de/en/es messages add error_csrf_token_missing and
error_too_many_login_attempts translations
- Login action maps HTTP 429 to fail(429, { ..., rateLimited: true });
page shows a muted clock icon with aria-invalid on rate-limit errors
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
text-xs (12px) is below Leonie's body-copy floor for the senior reader cohort
who hit /login?reason=expired on a phone in sunlight after being logged out.
text-sm (14px) restores legibility without breaking the visual hierarchy with
the heading. Addresses PR #612 / Leonie L3.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Color-blind reader cohort (8% of men) on a phone in sunlight cannot rely on
amber alone to parse the banner as a warning. Add a Heroicons-style
exclamation-triangle SVG, aria-hidden because the heading text already
conveys the meaning to assistive tech. Addresses PR #612 / Leonie L2.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace text-amber-900/text-amber-800 with the existing --color-warning
utility from layout.css. The amber soft fill stays (matching the precedent
of the green "registered" banner; a full surface-token pair is out of scope
for this PR). Addresses PR #612 / Leonie L1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Four new tests against the composed handle (with sequence stubbed to return
the head function): backend 401 on a private path redirects to
/login?reason=expired; backend 401 on /login does NOT redirect (no loop);
missing fa_session passes through without a backend call; 200 attaches the
user to event.locals. Closes the hook-coverage gap flagged by Sara S1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>