diff --git a/frontend/src/routes/admin/ocr/+page.server.ts b/frontend/src/routes/admin/ocr/+page.server.ts
new file mode 100644
index 00000000..06e356be
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/+page.server.ts
@@ -0,0 +1,16 @@
+import { error } from '@sveltejs/kit';
+import type { PageServerLoad } from './$types';
+import { createApiClient } from '$lib/api.server';
+import { getErrorMessage } from '$lib/errors';
+
+export const load: PageServerLoad = async ({ fetch }) => {
+ const api = createApiClient(fetch);
+ const result = await api.GET('/api/ocr/training-info');
+
+ if (!result.response.ok) {
+ const code = (result.error as unknown as { code?: string })?.code;
+ throw error(result.response.status, getErrorMessage(code));
+ }
+
+ return { trainingInfo: result.data! };
+};
diff --git a/frontend/src/routes/admin/ocr/+page.svelte b/frontend/src/routes/admin/ocr/+page.svelte
new file mode 100644
index 00000000..cc063db2
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/+page.svelte
@@ -0,0 +1,42 @@
+
+
+
+
+
+
OCR
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/routes/admin/ocr/[personId]/+page.server.ts b/frontend/src/routes/admin/ocr/[personId]/+page.server.ts
new file mode 100644
index 00000000..70b1fc47
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/[personId]/+page.server.ts
@@ -0,0 +1,18 @@
+import { error } from '@sveltejs/kit';
+import type { PageServerLoad } from './$types';
+import { createApiClient } from '$lib/api.server';
+import { getErrorMessage } from '$lib/errors';
+
+export const load: PageServerLoad = async ({ params, fetch }) => {
+ const api = createApiClient(fetch);
+ const result = await api.GET('/api/ocr/training-info/{personId}', {
+ params: { path: { personId: params.personId } }
+ });
+
+ if (!result.response.ok) {
+ const code = (result.error as unknown as { code?: string })?.code;
+ throw error(result.response.status, getErrorMessage(code));
+ }
+
+ return { history: result.data! };
+};
diff --git a/frontend/src/routes/admin/ocr/[personId]/+page.svelte b/frontend/src/routes/admin/ocr/[personId]/+page.svelte
new file mode 100644
index 00000000..272a1b16
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/[personId]/+page.svelte
@@ -0,0 +1,31 @@
+
+
+
diff --git a/frontend/src/routes/admin/ocr/[personId]/page.server.spec.ts b/frontend/src/routes/admin/ocr/[personId]/page.server.spec.ts
new file mode 100644
index 00000000..5e939a4a
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/[personId]/page.server.spec.ts
@@ -0,0 +1,33 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { load } from './+page.server';
+
+const mockApi = { GET: vi.fn() };
+
+vi.mock('$lib/api.server', () => ({ createApiClient: () => mockApi }));
+
+beforeEach(() => vi.clearAllMocks());
+
+describe('admin/ocr/[personId] — load', () => {
+ it('returns sender history from API', async () => {
+ const personId = '123e4567-e89b-12d3-a456-426614174000';
+ mockApi.GET.mockResolvedValue({
+ response: { ok: true },
+ data: { runs: [], personNames: { [personId]: 'Anna Müller' } }
+ });
+
+ const result = (await load({ params: { personId }, fetch } as never))!;
+
+ expect(result.history.personNames?.[personId]).toBe('Anna Müller');
+ });
+
+ it('throws 404 when person not found', async () => {
+ mockApi.GET.mockResolvedValue({
+ response: { ok: false, status: 404 },
+ error: { code: 'PERSON_NOT_FOUND' }
+ });
+
+ await expect(
+ load({ params: { personId: 'unknown-id' }, fetch } as never)
+ ).rejects.toMatchObject({ status: 404 });
+ });
+});
diff --git a/frontend/src/routes/admin/ocr/global/+page.server.ts b/frontend/src/routes/admin/ocr/global/+page.server.ts
new file mode 100644
index 00000000..fb1d8dfc
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/global/+page.server.ts
@@ -0,0 +1,16 @@
+import { error } from '@sveltejs/kit';
+import type { PageServerLoad } from './$types';
+import { createApiClient } from '$lib/api.server';
+import { getErrorMessage } from '$lib/errors';
+
+export const load: PageServerLoad = async ({ fetch }) => {
+ const api = createApiClient(fetch);
+ const result = await api.GET('/api/ocr/training-info/global');
+
+ if (!result.response.ok) {
+ const code = (result.error as unknown as { code?: string })?.code;
+ throw error(result.response.status, getErrorMessage(code));
+ }
+
+ return { history: result.data! };
+};
diff --git a/frontend/src/routes/admin/ocr/global/+page.svelte b/frontend/src/routes/admin/ocr/global/+page.svelte
new file mode 100644
index 00000000..1f2c9b22
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/global/+page.svelte
@@ -0,0 +1,30 @@
+
+
+
diff --git a/frontend/src/routes/admin/ocr/global/page.server.spec.ts b/frontend/src/routes/admin/ocr/global/page.server.spec.ts
new file mode 100644
index 00000000..cbc78eb0
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/global/page.server.spec.ts
@@ -0,0 +1,27 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { load } from './+page.server';
+
+const mockApi = { GET: vi.fn() };
+
+vi.mock('$lib/api.server', () => ({ createApiClient: () => mockApi }));
+
+beforeEach(() => vi.clearAllMocks());
+
+describe('admin/ocr/global — load', () => {
+ it('returns history from API', async () => {
+ mockApi.GET.mockResolvedValue({
+ response: { ok: true },
+ data: { runs: [{ id: 'run1' }], personNames: {} }
+ });
+
+ const result = (await load({ fetch } as never))!;
+
+ expect(result.history.runs).toHaveLength(1);
+ });
+
+ it('throws error when API call fails', async () => {
+ mockApi.GET.mockResolvedValue({ response: { ok: false, status: 500 }, error: {} });
+
+ await expect(load({ fetch } as never)).rejects.toMatchObject({ status: 500 });
+ });
+});
diff --git a/frontend/src/routes/admin/ocr/page.server.spec.ts b/frontend/src/routes/admin/ocr/page.server.spec.ts
new file mode 100644
index 00000000..b3b27a03
--- /dev/null
+++ b/frontend/src/routes/admin/ocr/page.server.spec.ts
@@ -0,0 +1,28 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { load } from './+page.server';
+
+const mockApi = { GET: vi.fn() };
+
+vi.mock('$lib/api.server', () => ({ createApiClient: () => mockApi }));
+
+beforeEach(() => vi.clearAllMocks());
+
+describe('admin/ocr — load', () => {
+ it('returns trainingInfo from API', async () => {
+ mockApi.GET.mockResolvedValue({
+ response: { ok: true },
+ data: { availableBlocks: 10, ocrServiceAvailable: true, senderModels: [] }
+ });
+
+ const result = (await load({ fetch } as never))!;
+
+ expect(result.trainingInfo.availableBlocks).toBe(10);
+ expect(result.trainingInfo.ocrServiceAvailable).toBe(true);
+ });
+
+ it('throws 503 when OCR API call fails', async () => {
+ mockApi.GET.mockResolvedValue({ response: { ok: false, status: 503 }, error: {} });
+
+ await expect(load({ fetch } as never)).rejects.toMatchObject({ status: 503 });
+ });
+});