Commit Graph

2744 Commits

Author SHA1 Message Date
Marcel
51cb8e7e22 test(transcription): polish @mention test docstrings and tighten clip assert
Round-4 polish from Sara (#11199) and Felix (#11186):
- Replace setTimeout(50) in stale-response race with tick() — matches
  round-3 pattern Sara verified in the sticky-takeover test.
- Add intent comment above the "clear input" wait — it is a negative
  assertion that must not be optimised away.
- Tighten displayName-clip assert from <=100 to ===100 so the test
  discriminates "clip works" from "clip works AND nothing weakened it".
- JSDoc POST_DEBOUNCE_SLACK_MS with the calibration rationale.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
3c5fc6907c chore(lint): forbid *.test-fixture.svelte imports from production code
Add ESLint no-restricted-imports rule banning *.test-fixture.svelte from
non-test files. Tree-shaking already keeps test fixtures out of the
production bundle, but making the boundary lint-enforced catches an
accidental autocomplete-driven import in a route or component. Test
files and the fixtures themselves are exempt. Nora #2 on PR #629
round 3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
790f870cac a11y(transcription): persistent aria-live region for @mention dropdown
The aria-live region previously lived inside {#if items.length === 0} so
it remounted whenever items transitioned between empty and populated —
VoiceOver in particular swallows announcements from freshly-mounted live
regions, and the "N persons found" announcement was missing entirely on
the populated branch. Move the live region above the conditional so the
element persists, and announce a localized "1 person found" / "N persons
found" count on the populated branch. The visible empty-state <p> stays
as a visual cue (no aria-live). Leonie #3 on PR #629 round 3.

Adds person_mention_results_count_singular / _plural in de/en/es.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
e0b2db061b i18n(transcription): align @mention search label verb-number across locales
de + es already use singular ("Person suchen", "Buscar persona"); en
was plural ("Search persons"). Switch en to "Search for a person" so
all three locales announce a singular search control to screen-reader
users — cross-locale parity polish. Leonie #1 on PR #629 round 3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
d4cfd4c8fd test(transcription): make @mention onKeyDown tests consistent
Wrap all four onKeyDown unit tests (ArrowDown/ArrowUp/Enter/Escape) in
flushSync uniformly so the next reader doesn't have to figure out why
some are wrapped and others aren't. Felix #1 on PR #629 round 3.

Also add a comment above the describe block calling out that these unit
tests do NOT exercise the Tiptap forwarding chain — that is covered by
the 'ArrowDown moves the highlight' integration test. Sara #3 on PR #629
round 3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
8a77e64421 refactor(test): complete .test-host -> .test-fixture rename sweep
Round 2 renamed only MentionDropdown's fixture; three siblings retained
the old suffix. Rename PersonMentionEditor, confirm, and TranscriptionBlock
test hosts to the .test-fixture suffix and update the three importers so
the boundary is uniform across the repo. Felix #1 / Tobi #1 on PR #629.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
d2f0eef45e test(transcription): replace setTimeout(50) with tick() in sticky-takeover
Sara on PR #629 round 3: the magic 50 ms in the @mention sticky-takeover
test was anchored to nothing and read as a race-fix it wasn't. Replace
with await tick() so the intent ("flush pending Svelte reactivity") is
explicit. The expect.element polling already covers timing drift.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
5c588caf26 test(transcription): restore strong one-fetch regression guard
Sara on PR #629 round 3: the round-2 fix captured the fetch count AFTER
typing '@', so a regression that re-introduced the legacy per-keystroke
items() callback would have its '@'-keystroke fetch silently absorbed
into the baseline. Drop the baseline subtraction and count every
/api/persons fetch since render — typing '@' + fill('Walter') must
total exactly one fetch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
4b75ca80cc fix(transcription): clip @mention displayName to MAX_QUERY_LENGTH
The dropdown's editor-mirror clips at 100 chars (CWE-400, Nora #1), but
the host editor previously fed renderProps.query directly to displayName
on selection — so a 200-char @-suffix would search the first 100 chars
but insert 200 chars. Clip once in updateState and use the clipped value
for both the inserted displayName and the dropdown's editorQuery mirror,
keeping "what I searched" and "what got inserted" in sync. Felix #3 on
PR #629.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
5f6b896bd2 refactor(transcription): hoist @mention constants to shared module
Single source of truth for MAX_QUERY_LENGTH, SEARCH_DEBOUNCE_MS, and
SEARCH_RESULT_LIMIT — MentionDropdown imports MAX_QUERY_LENGTH;
PersonMentionEditor imports the debounce + result-limit; the spec's
mirror now imports SEARCH_DEBOUNCE_MS so it can never drift. Unblocks
the displayName length-cap fix (Felix #3 on PR #629).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
cb0ce90fd6 a11y(transcription): bump @mention search input to text-base (16 px floor)
The senior-audience body-text floor is 16 px (CLAUDE.md
§Dual-Audience). The search input was the smallest non-metadata
text in the dropdown at text-sm (14 px), even though it is the
primary write surface a 60+ transcriber types into. Bumping to
text-base costs ~2 px of popover header height and closes the
"I can't read what I'm typing" complaint that historically tops
senior-usability tests of search bars. Leonie FINDING-MENTION-006
on PR #629.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
2057a31018 a11y(transcription): cap @mention listbox width at viewport-1rem (WCAG 1.4.10)
w-72 (288 px) listbox can overflow horizontally on a 320 px viewport
when the caret sits near the right edge — the existing flip logic
only handles vertical overflow. max-w-[calc(100vw-1rem)] adds a
defensive horizontal cap so a senior on a 320 px phone never sees
the dropdown clip off-screen. Leonie FINDING-MENTION-005 on PR #629.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
85770aceac a11y(transcription): give @mention search input its own sr-only label
The sr-only label for the search input was reusing the listbox
"Link person" label — but the input filters a candidate list, it does
not link anything. Screen readers heard a verb mismatch between the
listbox announce and the search-input focus event. New
person_mention_search_label key in de/en/es. The listbox aria-label
stays person_mention_btn_label since that labels the listbox itself.
Leonie FINDING-MENTION-004 on PR #629.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
5580fe9545 fix(transcription): clip @mention editor-mirror to 100 chars (CWE-400 layered)
The <input maxlength=100> attribute capped direct user edits but did
not cover the Tiptap editor-mirror path. A 5000-char @-suffix in the
contenteditable would mirror unchanged into searchQuery and reach
runSearch. Clipping at the mirror keeps both paths bounded. The
literal in the maxlength attribute is also bound to the new
MAX_QUERY_LENGTH constant so the two stay in sync. Server-side cap
tracked separately. Nora #1 on PR #629.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
0f06e959d1 test(transcription): unit-test @mention dropdown onKeyDown export
Tiptap intercepts ArrowDown/ArrowUp/Enter at the editor level and
forwards them via the dropdown's exported onKeyDown — the dropdown
itself has no DOM keydown listener. These tests exercise the same
export directly (the full focus-chain E2E is deferred to a separate
Playwright issue). Sara #3 on PR #629.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
b72ac15fec test(transcription): characterize @mention silent failure on 500 / network error
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>
2026-05-20 12:36:45 +02:00
Marcel
cc4a8aed14 test(transcription): de-flake one-fetch @mention test via searchbox fill
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>
2026-05-20 12:36:45 +02:00
Marcel
ed430252d1 refactor(transcription): rename @mention test-host to test-fixture
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>
2026-05-20 12:36:45 +02:00
Marcel
88722e04cd docs(transcription): explain why @mention mirror uses \$state+\$effect
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>
2026-05-20 12:36:45 +02:00
Marcel
6a82f4b5b0 fix(transcription): cancel pending @mention debounce in onExit
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>
2026-05-20 12:36:45 +02:00
Marcel
4d875e7e67 docs(debounce): clarify that cancel() drops, never flushes, the trailing call
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>
2026-05-20 12:36:45 +02:00
Marcel
2ca512fc9d refactor(test): drop double-cast on Person fixtures
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>
2026-05-20 12:36:45 +02:00
Marcel
fb414183be refactor(test): name the debounce slack and harden against CI jitter
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>
2026-05-20 12:36:45 +02:00
Marcel
57be10ccb5 fix(transcription): defensively cap @mention fetch with limit=5
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>
2026-05-20 12:36:45 +02:00
Marcel
dd423a22db test(transcription): pin sticky search-input takeover behaviour
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>
2026-05-20 12:36:45 +02:00
Marcel
9b6561562d a11y(transcription): announce @mention empty state via aria-live
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>
2026-05-20 12:36:45 +02:00
Marcel
6a5083f9bc a11y(transcription): enlarge @mention magnifier and darken contrast
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>
2026-05-20 12:36:45 +02:00
Marcel
d0f4642bb9 fix(transcription): cap @mention search input at maxlength=100
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>
2026-05-20 12:36:45 +02:00
Marcel
b6bdebc449 fix(transcription): guard @mention fetch against stale responses
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>
2026-05-20 12:36:45 +02:00
Marcel
fd323191dc fix(transcription): neutralize legacy items() to dedupe @mention fetch
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>
2026-05-20 12:36:45 +02:00
Marcel
729a99f8c5 refactor(transcription): consolidate MentionDropdown test files
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>
2026-05-20 12:36:45 +02:00
Marcel
f651a41d18 feat(transcription): drive @mention fetch through the dropdown search input
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>
2026-05-20 12:36:45 +02:00
Marcel
344f5cac77 fix(transcription): add noreferrer to mention dropdown create-new link
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>
2026-05-20 12:36:45 +02:00
Marcel
c5ad2c366c test(transcription): cover 44px touch target on mention search input
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>
2026-05-20 12:36:45 +02:00
Marcel
bdc0b112b6 feat(transcription): wire dropdown search input to editor @-text
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>
2026-05-20 12:36:45 +02:00
Marcel
ff3e8fb755 test(transcription): cover MentionDropdown onSearch callback wiring
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>
2026-05-20 12:36:45 +02:00
Marcel
38b87f6a9f feat(transcription): add data-test-search-input hook for E2E selectors
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>
2026-05-20 12:36:45 +02:00
Marcel
fa7fe42363 feat(transcription): add search input with initialQuery prefill to MentionDropdown
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>
2026-05-20 12:36:45 +02:00
Marcel
c667a5add8 feat(i18n): add person_mention_search_prompt message key
For issue #380 — the new search input inside the @mention dropdown
needs an empty-state prompt distinct from "no results found".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 12:36:45 +02:00
Marcel
9a460b3c90 fix(document): add trainingLabels to Document.full entity graph (#642)
All checks were successful
CI / Semgrep Security Scan (push) Successful in 19s
CI / Compose Bucket Idempotency (push) Successful in 59s
CI / Unit & Component Tests (push) Successful in 3m28s
CI / OCR Service Tests (push) Successful in 20s
CI / Backend Unit Tests (push) Successful in 3m22s
CI / fail2ban Regex (push) Successful in 49s
trainingLabels was switched to LAZY fetch in #467 but not added to the
Document.full @NamedEntityGraph. DocumentRepository.findById() uses
Document.full to eagerly load sender/receivers/tags, but the Hibernate
session closes before Jackson serializes the response. Accessing
trainingLabels outside the session throws LazyInitializationException,
causing GET /api/documents/{id} to return HTTP 500.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 12:36:27 +02:00
Marcel
cdc3e2e4c8 fix(deploy): wire VITE_SENTRY_DSN as Docker build arg for frontend GlitchTip (#645)
All checks were successful
CI / Backend Unit Tests (pull_request) Successful in 3m18s
CI / fail2ban Regex (pull_request) Successful in 42s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m0s
CI / Unit & Component Tests (push) Successful in 3m19s
CI / OCR Service Tests (push) Successful in 19s
CI / Backend Unit Tests (push) Successful in 3m26s
CI / fail2ban Regex (push) Successful in 43s
CI / Semgrep Security Scan (push) Successful in 18s
CI / Compose Bucket Idempotency (push) Successful in 1m0s
CI / Unit & Component Tests (pull_request) Successful in 3m29s
CI / OCR Service Tests (pull_request) Successful in 19s
VITE_SENTRY_DSN is a Vite build-time variable baked into the JS bundle.
Without an ARG/ENV in the Dockerfile build stage and a build.args entry in
docker-compose.prod.yml, the SDK initialised with enabled=false regardless
of the Gitea secret value.

- frontend/Dockerfile: add ARG VITE_SENTRY_DSN + ENV before npm run build
- docker-compose.prod.yml: add build.args.VITE_SENTRY_DSN with empty fallback
- nightly.yml: write VITE_SENTRY_DSN secret into .env.staging

Requires Gitea secret VITE_SENTRY_DSN to be set to the GlitchTip project #1 DSN.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 09:54:04 +02:00
Marcel
e89a90ff66 fix(deploy): wire SENTRY_DSN and enable ECS JSON logging for prod (#641)
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m27s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m22s
CI / fail2ban Regex (pull_request) Successful in 1m19s
CI / Semgrep Security Scan (pull_request) Successful in 19s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m0s
CI / Unit & Component Tests (push) Successful in 3m21s
CI / OCR Service Tests (push) Successful in 18s
CI / Backend Unit Tests (push) Successful in 3m33s
CI / fail2ban Regex (push) Successful in 43s
CI / Semgrep Security Scan (push) Successful in 20s
CI / Compose Bucket Idempotency (push) Successful in 59s
Pass SENTRY_DSN env var through to the backend container so the Sentry SDK
actually ships exceptions to GlitchTip — the variable was written to
.env.staging by nightly.yml but never forwarded into the container.

Enable Spring Boot 4.0 ECS structured logging (LOGGING_STRUCTURED_FORMAT_CONSOLE=ecs)
so Loki receives single-entry JSON log lines with parsed log.level, enabling
detected_level filtering in Grafana instead of 50-line unlinked stack trace blobs.

Update Grafana Loki dashboard query from | logfmt to | json to match the new format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 08:16:00 +02:00
Marcel
0c0a4830cd ux(transcription): bump dismiss button icon from red-500 to red-600
All checks were successful
nightly / deploy-staging (push) Successful in 4m32s
CI / Unit & Component Tests (pull_request) Successful in 3m19s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m27s
CI / fail2ban Regex (pull_request) Successful in 41s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 58s
CI / Unit & Component Tests (push) Successful in 3m30s
CI / OCR Service Tests (push) Successful in 19s
CI / Backend Unit Tests (push) Successful in 3m20s
CI / fail2ban Regex (push) Successful in 41s
CI / Semgrep Security Scan (push) Successful in 18s
CI / Compose Bucket Idempotency (push) Successful in 58s
text-red-500 on bg-red-50 gives ~3.8:1 contrast (passes AA for UI
components at 3:1 but leaves no margin). text-red-600 gives ~5.0:1,
comfortably above the AA threshold with no visual downgrade.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:32:47 +02:00
Marcel
dd843d76c2 a11y(transcription): remove redundant aria-live="polite" from alert div
role="alert" already implies aria-live="assertive". The polite override
caused screen readers to wait for the current announcement to finish
before reading the error — too gentle for a failure state the user just
triggered. Dropping the attribute restores the implicit assertive
behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:31:57 +02:00
Marcel
9601974db0 ux(transcription): bump error banner font size to text-sm for readability
text-xs (12px) is at the lower bound for the 60+ transcriber cohort.
text-sm (14px) matches the visual weight of the progress counter label
above and is more comfortable to read under stress (failed operation).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:30:54 +02:00
Marcel
1782526c99 test(transcription): gate second click on button re-enabled to fix race
Adds an await for the button to become non-disabled between the two
dispatchEvent calls in 'clears error on next successful call'. This
ensures the first async rejection has fully settled and Svelte has
flushed markingAllReviewed before the second click fires.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:29:31 +02:00
Marcel
76ef54e064 test(transcription): cover non-JSON fallback in markAllReviewed error path
Adds a test for when the server returns a non-JSON body (e.g. an nginx
502 HTML page). Confirms the res.json().catch(() => ({})) fallback
produces 'INTERNAL_ERROR' as the thrown message and leaves blocks intact.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:28:39 +02:00
Marcel
f1d1ac3f1a test(transcription): assert error banner shows domain-specific message
Adds toHaveTextContent(m.transcription_mark_all_reviewed_error()) to the
error-present test. The previous check only asserted presence via
role="alert", which would not have caught the dead key bug — the banner
was showing the generic fallback rather than the operation-specific copy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:27:29 +02:00
Marcel
0f48ffede5 fix(transcription): use domain-specific message in markAllReviewed catch
Removes the getErrorMessage() indirection and calls
m.transcription_mark_all_reviewed_error() directly in the catch block.
The previous implementation routed through getErrorMessage(code) which
mapped any error code to the generic m.error_internal_error() fallback,
leaving the domain-specific key unreachable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 21:23:59 +02:00
Marcel
3e72157ee1 test(transcription): update markAllReviewed non-OK test to expect throw
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m14s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / Backend Unit Tests (pull_request) Successful in 3m22s
CI / fail2ban Regex (pull_request) Successful in 41s
CI / Semgrep Security Scan (pull_request) Successful in 18s
CI / Compose Bucket Idempotency (pull_request) Successful in 58s
The function now throws instead of silently returning on failure.
Update the test name and assertion to match the new behaviour, and
verify blocks remain unchanged after the error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 20:43:21 +02:00