From f27e2d33a5e07ee39b64fce4ede1265f04d1df3e Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 19 May 2026 20:35:56 +0200 Subject: [PATCH 01/15] test(transcription): add failing tests for markAllReviewed error display RED phase: 4 new Vitest browser tests that fail because the error banner and catch block don't exist yet. Co-Authored-By: Claude Sonnet 4.6 --- .../TranscriptionEditView.svelte.spec.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/frontend/src/lib/document/transcription/TranscriptionEditView.svelte.spec.ts b/frontend/src/lib/document/transcription/TranscriptionEditView.svelte.spec.ts index ec53e911..ad320cd5 100644 --- a/frontend/src/lib/document/transcription/TranscriptionEditView.svelte.spec.ts +++ b/frontend/src/lib/document/transcription/TranscriptionEditView.svelte.spec.ts @@ -3,6 +3,7 @@ import { cleanup, render } from 'vitest-browser-svelte'; import { page, userEvent } from 'vitest/browser'; import TranscriptionEditView from './TranscriptionEditView.svelte'; import { createConfirmService, CONFIRM_KEY } from '$lib/shared/services/confirm.svelte.js'; +import { m } from '$lib/paraglide/messages.js'; afterEach(cleanup); @@ -369,4 +370,67 @@ describe('TranscriptionEditView — mark all reviewed', () => { .toBeDisabled(); resolveMarkAll(); }); + + it('shows error message when onMarkAllReviewed callback rejects', async () => { + const onMarkAllReviewed = vi.fn().mockRejectedValue(new Error('INTERNAL_ERROR')); + renderView({ blocks: [unreviewedBlock1, unreviewedBlock2], onMarkAllReviewed }); + + const btnEl = (await page + .getByRole('button', { name: /Alle als fertig markieren/ }) + .element()) as HTMLButtonElement; + btnEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + + await expect.element(page.getByRole('alert')).toBeInTheDocument(); + }); + + it('clears error when dismiss button is clicked', async () => { + const onMarkAllReviewed = vi.fn().mockRejectedValue(new Error('INTERNAL_ERROR')); + renderView({ blocks: [unreviewedBlock1, unreviewedBlock2], onMarkAllReviewed }); + + const btnEl = (await page + .getByRole('button', { name: /Alle als fertig markieren/ }) + .element()) as HTMLButtonElement; + btnEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + await expect.element(page.getByRole('alert')).toBeInTheDocument(); + + const dismissEl = (await page + .getByRole('button', { name: m.comp_dismiss() }) + .element()) as HTMLButtonElement; + dismissEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + + await expect.element(page.getByRole('alert')).not.toBeInTheDocument(); + }); + + it('clears error on next successful markAllReviewed call', async () => { + const onMarkAllReviewed = vi + .fn() + .mockRejectedValueOnce(new Error('INTERNAL_ERROR')) + .mockResolvedValue(undefined); + renderView({ blocks: [unreviewedBlock1, unreviewedBlock2], onMarkAllReviewed }); + + const btnEl = (await page + .getByRole('button', { name: /Alle als fertig markieren/ }) + .element()) as HTMLButtonElement; + btnEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + await expect.element(page.getByRole('alert')).toBeInTheDocument(); + + btnEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + + await expect.element(page.getByRole('alert')).not.toBeInTheDocument(); + }); + + it('re-enables button after markAllReviewed failure', async () => { + const onMarkAllReviewed = vi.fn().mockRejectedValue(new Error('INTERNAL_ERROR')); + renderView({ blocks: [unreviewedBlock1, unreviewedBlock2], onMarkAllReviewed }); + + const btnEl = (await page + .getByRole('button', { name: /Alle als fertig markieren/ }) + .element()) as HTMLButtonElement; + btnEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + await expect.element(page.getByRole('alert')).toBeInTheDocument(); + + await expect + .element(page.getByRole('button', { name: /Alle als fertig markieren/ })) + .not.toBeDisabled(); + }); }); -- 2.49.1 From 907a6a6b53f23f7794013432079e3da17e0553e9 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 19 May 2026 20:36:44 +0200 Subject: [PATCH 02/15] feat(i18n): add transcription_mark_all_reviewed_error message key Co-Authored-By: Claude Sonnet 4.6 --- frontend/messages/de.json | 1 + frontend/messages/en.json | 1 + frontend/messages/es.json | 1 + 3 files changed, 3 insertions(+) diff --git a/frontend/messages/de.json b/frontend/messages/de.json index dcd08246..bf3f41d2 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -633,6 +633,7 @@ "transcription_block_review": "Als geprüft markieren", "transcription_block_unreview": "Markierung aufheben", "transcription_reviewed_count": "{reviewed} von {total} geprüft", + "transcription_mark_all_reviewed_error": "Markierung fehlgeschlagen. Bitte versuchen Sie es erneut.", "training_ocr_heading": "Kurrent-Erkennung trainieren", "training_ocr_description": "Starte ein neues Training mit den bisher geprüften OCR-Blöcken, um die Erkennungsgenauigkeit für Kurrentschrift zu verbessern.", "training_ocr_blocks_ready": "{blocks} geprüfte Blöcke bereit / {docs} Dokumente", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index f915633e..b6c35a82 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -633,6 +633,7 @@ "transcription_block_review": "Mark as reviewed", "transcription_block_unreview": "Unmark as reviewed", "transcription_reviewed_count": "{reviewed} of {total} reviewed", + "transcription_mark_all_reviewed_error": "Failed to mark all as reviewed. Please try again.", "training_ocr_heading": "Train Kurrent recognition", "training_ocr_description": "Start a new training run using the reviewed OCR blocks to improve recognition accuracy for Kurrent script.", "training_ocr_blocks_ready": "{blocks} reviewed blocks ready / {docs} documents", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 1f1dec02..fe6308e8 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -633,6 +633,7 @@ "transcription_block_review": "Marcar como revisado", "transcription_block_unreview": "Desmarcar como revisado", "transcription_reviewed_count": "{reviewed} de {total} revisados", + "transcription_mark_all_reviewed_error": "Error al marcar como revisado. Intente de nuevo.", "training_ocr_heading": "Entrenar reconocimiento Kurrent", "training_ocr_description": "Inicia un nuevo entrenamiento con los bloques OCR revisados para mejorar la precisión de reconocimiento del script Kurrent.", "training_ocr_blocks_ready": "{blocks} bloques revisados listos / {docs} documentos", -- 2.49.1 From e3e83735264bde20eafd99d4fb8545a974939770 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 19 May 2026 20:37:21 +0200 Subject: [PATCH 03/15] fix(transcription): throw error from markAllReviewed() on non-2xx response Previously the function silently returned on failure, leaving no way for callers to detect or surface the error to the user. Co-Authored-By: Claude Sonnet 4.6 --- .../document/transcription/useTranscriptionBlocks.svelte.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/document/transcription/useTranscriptionBlocks.svelte.ts b/frontend/src/lib/document/transcription/useTranscriptionBlocks.svelte.ts index 3a601f1a..da864ad8 100644 --- a/frontend/src/lib/document/transcription/useTranscriptionBlocks.svelte.ts +++ b/frontend/src/lib/document/transcription/useTranscriptionBlocks.svelte.ts @@ -119,7 +119,11 @@ export function createTranscriptionBlocks( const res = await fetchImpl(`/api/documents/${documentId()}/transcription-blocks/review-all`, { method: 'PUT' }); - if (!res.ok) return; + if (!res.ok) { + const body = await res.json().catch(() => ({})); + // Never render body.message — route through getErrorMessage() to prevent leaking backend internals + throw new Error((body as { code?: string })?.code ?? 'INTERNAL_ERROR'); + } const updated = (await res.json()) as { id: string; reviewed: boolean }[]; for (const b of updated) { const existing = blocks.find((x) => x.id === b.id); -- 2.49.1 From 6b53cbfc5b744e263111c9967cd6e4d6fc909bcc Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 19 May 2026 20:38:28 +0200 Subject: [PATCH 04/15] feat(transcription): show dismissible error banner when markAllReviewed fails Adds markAllError state and catch block to handleMarkAllReviewed. Error banner renders below the review progress bar with role="alert" and aria-live="polite" for screen reader announcement. Dismiss button clears the error; next successful call also clears it automatically. Co-Authored-By: Claude Sonnet 4.6 --- .../TranscriptionEditView.svelte | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/frontend/src/lib/document/transcription/TranscriptionEditView.svelte b/frontend/src/lib/document/transcription/TranscriptionEditView.svelte index 9aeb473e..8986d11d 100644 --- a/frontend/src/lib/document/transcription/TranscriptionEditView.svelte +++ b/frontend/src/lib/document/transcription/TranscriptionEditView.svelte @@ -1,5 +1,6 @@