From 8acb830649050c41bd64d712d52fc3a22d21414b Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 18 Apr 2026 01:05:08 +0200 Subject: [PATCH] =?UTF-8?q?feat(admin):=20add=20OCR=20admin=20routes=20?= =?UTF-8?q?=E2=80=94=20overview,=20global=20history,=20sender=20detail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/admin/ocr/+page.server.ts | 16 +++++++ frontend/src/routes/admin/ocr/+page.svelte | 42 +++++++++++++++++++ .../admin/ocr/[personId]/+page.server.ts | 18 ++++++++ .../routes/admin/ocr/[personId]/+page.svelte | 31 ++++++++++++++ .../admin/ocr/[personId]/page.server.spec.ts | 33 +++++++++++++++ .../routes/admin/ocr/global/+page.server.ts | 16 +++++++ .../src/routes/admin/ocr/global/+page.svelte | 30 +++++++++++++ .../admin/ocr/global/page.server.spec.ts | 27 ++++++++++++ .../src/routes/admin/ocr/page.server.spec.ts | 28 +++++++++++++ 9 files changed, 241 insertions(+) create mode 100644 frontend/src/routes/admin/ocr/+page.server.ts create mode 100644 frontend/src/routes/admin/ocr/+page.svelte create mode 100644 frontend/src/routes/admin/ocr/[personId]/+page.server.ts create mode 100644 frontend/src/routes/admin/ocr/[personId]/+page.svelte create mode 100644 frontend/src/routes/admin/ocr/[personId]/page.server.spec.ts create mode 100644 frontend/src/routes/admin/ocr/global/+page.server.ts create mode 100644 frontend/src/routes/admin/ocr/global/+page.svelte create mode 100644 frontend/src/routes/admin/ocr/global/page.server.spec.ts create mode 100644 frontend/src/routes/admin/ocr/page.server.spec.ts 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

+ +
+ + + + + +
+
+

Sender Models

+ + Global history → + +
+ +
+
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 @@ + + +
+
+ + + OCR + +

+ {personName} +

+
+ + +
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 @@ + + +
+
+ + + OCR + +

+ Global History +

+
+ + +
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 }); + }); +});