diff --git a/frontend/src/routes/admin/ocr/OcrHealthBar.svelte b/frontend/src/routes/admin/ocr/OcrHealthBar.svelte
new file mode 100644
index 00000000..8319fc6c
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/OcrHealthBar.svelte
@@ -0,0 +1,14 @@
+
+
+
+
+
+ {ocrServiceAvailable ? 'Online' : 'Offline'}
+
+
diff --git a/frontend/src/routes/admin/ocr/OcrHealthBar.svelte.spec.ts b/frontend/src/routes/admin/ocr/OcrHealthBar.svelte.spec.ts
new file mode 100644
index 00000000..5e0a561b
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/OcrHealthBar.svelte.spec.ts
@@ -0,0 +1,18 @@
+import { afterEach, describe, it, expect } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+import OcrHealthBar from './OcrHealthBar.svelte';
+
+afterEach(cleanup);
+
+describe('OcrHealthBar', () => {
+ it('shows online status when OCR service is available', async () => {
+ render(OcrHealthBar, { ocrServiceAvailable: true });
+ await expect.element(page.getByText(/online/i)).toBeInTheDocument();
+ });
+
+ it('shows offline status when OCR service is unavailable', async () => {
+ render(OcrHealthBar, { ocrServiceAvailable: false });
+ await expect.element(page.getByText(/offline/i)).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/routes/admin/ocr/OcrModelsTable.svelte b/frontend/src/routes/admin/ocr/OcrModelsTable.svelte
new file mode 100644
index 00000000..785c96b0
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/OcrModelsTable.svelte
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+ | Person |
+ CER |
+ Accuracy |
+ Lines |
+ Actions |
+
+
+
+ {#each senderModels as model (model.id)}
+
+ |
+
+ {personNames[model.personId] ?? model.personId}
+
+ |
+
+ {model.cer != null ? (model.cer * 100).toFixed(1) + '%' : '—'}
+ |
+
+ {model.accuracy != null ? (model.accuracy * 100).toFixed(1) + '%' : '—'}
+ |
+
+ {model.correctedLinesAtTraining}
+ |
+
+ Details
+ |
+
+ {/each}
+
+
+
diff --git a/frontend/src/routes/admin/ocr/OcrModelsTable.svelte.spec.ts b/frontend/src/routes/admin/ocr/OcrModelsTable.svelte.spec.ts
new file mode 100644
index 00000000..67ff1e83
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/OcrModelsTable.svelte.spec.ts
@@ -0,0 +1,44 @@
+import { afterEach, describe, it, expect } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+import OcrModelsTable from './OcrModelsTable.svelte';
+import type { components } from '$lib/generated/api';
+
+afterEach(cleanup);
+
+type SenderModel = components['schemas']['SenderModel'];
+
+const personId = '123e4567-e89b-12d3-a456-426614174000';
+const model: SenderModel = {
+ id: 'aaa00000-0000-0000-0000-000000000001',
+ personId,
+ correctedLinesAtTraining: 120,
+ createdAt: '2024-01-01T00:00:00Z',
+ updatedAt: '2024-06-01T00:00:00Z',
+ cer: 0.04,
+ accuracy: 0.96
+};
+
+describe('OcrModelsTable', () => {
+ it('shows person name when provided', async () => {
+ render(OcrModelsTable, {
+ senderModels: [model],
+ personNames: { [personId]: 'Anna Müller' }
+ });
+ await expect.element(page.getByText('Anna Müller')).toBeInTheDocument();
+ });
+
+ it('shows person ID when name is missing', async () => {
+ render(OcrModelsTable, {
+ senderModels: [model],
+ personNames: {}
+ });
+ await expect.element(page.getByText(personId)).toBeInTheDocument();
+ });
+
+ it('shows empty state when no models', async () => {
+ render(OcrModelsTable, { senderModels: [], personNames: {} });
+ const rows = document.querySelectorAll('tbody tr');
+ expect(rows.length).toBe(0);
+ });
+});
diff --git a/frontend/src/routes/admin/ocr/OcrStatCards.svelte b/frontend/src/routes/admin/ocr/OcrStatCards.svelte
new file mode 100644
index 00000000..6375bdd5
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/OcrStatCards.svelte
@@ -0,0 +1,36 @@
+
+
+
+
+
{availableBlocks}
+
+ Training blocks
+
+
+
+
{totalOcrBlocks}
+
Total blocks
+
+
+
{availableDocuments}
+
Documents
+
+
+
{availableSegBlocks}
+
Seg. blocks
+
+
diff --git a/frontend/src/routes/admin/ocr/OcrStatCards.svelte.spec.ts b/frontend/src/routes/admin/ocr/OcrStatCards.svelte.spec.ts
new file mode 100644
index 00000000..f471c57f
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/OcrStatCards.svelte.spec.ts
@@ -0,0 +1,30 @@
+import { afterEach, describe, it, expect } from 'vitest';
+import { cleanup, render } from 'vitest-browser-svelte';
+import { page } from 'vitest/browser';
+import OcrStatCards from './OcrStatCards.svelte';
+
+afterEach(cleanup);
+
+const stats = {
+ availableBlocks: 42,
+ totalOcrBlocks: 200,
+ availableDocuments: 15,
+ availableSegBlocks: 8
+};
+
+describe('OcrStatCards', () => {
+ it('shows available block count', async () => {
+ render(OcrStatCards, stats);
+ await expect.element(page.getByText('42')).toBeInTheDocument();
+ });
+
+ it('shows total OCR block count', async () => {
+ render(OcrStatCards, stats);
+ await expect.element(page.getByText('200')).toBeInTheDocument();
+ });
+
+ it('shows available document count', async () => {
+ render(OcrStatCards, stats);
+ await expect.element(page.getByText('15')).toBeInTheDocument();
+ });
+});