feat(admin): add OCR training card to admin/system page

- TrainingHistory.svelte: responsive table with status badges
  (green/red/animated pulse), keyed iteration, empty-state row
- OcrTrainingCard.svelte: shows available blocks/docs, disabled states
  (< 5 blocks, service down), in-flight "…" state, 5s success message,
  embeds TrainingHistory
- Wired into admin/system/+page.svelte via fetchTrainingInfo() in $effect
- Regenerated api.ts with OcrTrainingRun + TrainingInfoResponse types
- TRAINING_ALREADY_RUNNING error code in errors.ts + de/en/es translations
- 7 OcrTrainingCard Vitest tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-13 14:58:13 +02:00
parent 88e005eb49
commit 4e08d31e01
10 changed files with 473 additions and 4 deletions

View File

@@ -228,6 +228,22 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/ocr/train": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["triggerTraining"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/ocr/batch": {
parameters: {
query?: never;
@@ -564,6 +580,22 @@ export interface paths {
patch: operations["updateGroup"];
trace?: never;
};
"/api/documents/{id}/training-labels": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch: operations["patchTrainingLabel"];
trace?: never;
};
"/api/documents/{documentId}/comments/{commentId}": {
parameters: {
query?: never;
@@ -676,6 +708,38 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/ocr/training-info": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["getTrainingInfo"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/ocr/training-data/export": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["exportTrainingData"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/ocr/jobs/{jobId}": {
parameters: {
query?: never;
@@ -1106,7 +1170,6 @@ export interface components {
receivers?: components["schemas"]["Person"][];
sender?: components["schemas"]["Person"];
tags?: components["schemas"]["Tag"][];
/** @enum {string} */
trainingLabels?: ("KURRENT_RECOGNITION" | "KURRENT_SEGMENTATION")[];
};
UpdateTranscriptionBlockDTO: {
@@ -1174,6 +1237,24 @@ export interface components {
/** Format: date-time */
createdAt: string;
};
OcrTrainingRun: {
/** Format: uuid */
id: string;
/** @enum {string} */
status: "RUNNING" | "DONE" | "FAILED";
/** Format: int32 */
blockCount: number;
/** Format: int32 */
documentCount: number;
modelName: string;
errorMessage?: string;
/** Format: uuid */
triggeredBy?: string;
/** Format: date-time */
createdAt: string;
/** Format: date-time */
completedAt?: string;
};
BatchOcrDTO: {
documentIds: string[];
};
@@ -1314,6 +1395,10 @@ export interface components {
actorName?: string;
documentTitle?: string;
};
TrainingLabelRequest: {
label?: string;
enrolled?: boolean;
};
StatsDTO: {
/** Format: int64 */
totalPersons?: number;
@@ -1325,8 +1410,6 @@ export interface components {
/** Format: uuid */
id?: string;
displayName?: string;
/** Format: int64 */
documentCount?: number;
firstName?: string;
lastName?: string;
/** Format: int32 */
@@ -1335,8 +1418,22 @@ export interface components {
deathYear?: number;
alias?: string;
notes?: string;
/** Format: int64 */
documentCount?: number;
personType?: string;
};
TrainingInfoResponse: {
/** Format: int32 */
availableBlocks?: number;
/** Format: int32 */
totalOcrBlocks?: number;
/** Format: int32 */
availableDocuments?: number;
ocrServiceAvailable?: boolean;
lastRun?: components["schemas"]["OcrTrainingRun"];
runs?: components["schemas"]["OcrTrainingRun"][];
};
StreamingResponseBody: unknown;
OcrJob: {
/** Format: uuid */
id: string;
@@ -1381,11 +1478,11 @@ export interface components {
empty?: boolean;
};
PageableObject: {
paged?: boolean;
/** Format: int32 */
pageNumber?: number;
/** Format: int32 */
pageSize?: number;
paged?: boolean;
/** Format: int64 */
offset?: number;
sort?: components["schemas"]["SortObject"];
@@ -2082,6 +2179,26 @@ export interface operations {
};
};
};
triggerTraining: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Created */
201: {
headers: {
[name: string]: unknown;
};
content: {
"*/*": components["schemas"]["OcrTrainingRun"];
};
};
};
};
triggerBatch: {
parameters: {
query?: never;
@@ -2743,6 +2860,30 @@ export interface operations {
};
};
};
patchTrainingLabel: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["TrainingLabelRequest"];
};
};
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
deleteComment: {
parameters: {
query?: never;
@@ -2923,6 +3064,46 @@ export interface operations {
};
};
};
getTrainingInfo: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"*/*": components["schemas"]["TrainingInfoResponse"];
};
};
};
};
exportTrainingData: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"*/*": components["schemas"]["StreamingResponseBody"];
};
};
};
};
getJobStatus: {
parameters: {
query?: never;