feat(import): RennmausPro-III-Backup-Importer (analyze+execute, Dedup, Fotos)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 22:43:19 +02:00
parent 5e14124322
commit 3ad276637c
19 changed files with 2620 additions and 0 deletions

View File

@@ -0,0 +1,103 @@
/**
* RPRO3: API-Client für den RennmausPro-III-Backup-Import.
* POST /import/rpro3/analyze -> multipart 'backup' (+ optional 'images') -> Auswertung (kein Schreiben)
* POST /import/rpro3/execute -> derselbe Upload -> idempotenter Import
*
* Upload via FormData (nicht api.post — das erzwingt application/json); der Browser setzt
* den multipart-Boundary-Header selbst.
*/
import { API_BASE_URL, ApiError } from './client'
import { de } from '../strings/de'
export interface Rpro3Counts {
ownAnimals: number
externalRaw: number
externalAfterDedup: number
litters: number
contacts: number
genotypes: number
duplicatesMerged: number
mergeClusters: number
}
export interface Rpro3AmbiguousVariant {
count: number
dob: string
farbe: string
origin: string
isOwn: boolean
}
export interface Rpro3AmbiguousName {
name: string
variants: Rpro3AmbiguousVariant[]
bareCount: number
}
export interface Rpro3MergeSample {
name: string
recordCount: number
dob: string
farbe: string
origin: string
}
export interface Rpro3AnalyzeResult {
counts: Rpro3Counts
newVsCurrentNew: number
newVsCurrentExisting: number
topMerges: Rpro3MergeSample[]
ambiguousNames: Rpro3AmbiguousName[]
photosProvided: boolean
photoFilesAvailable: number
}
export interface Rpro3ExecuteResult {
gerbilsImported: number
littersImported: number
contactsImported: number
healthRecordsImported: number
weightRecordsImported: number
photosImported: number
message: string
}
async function postUpload<T>(path: string, backup: File, images: File | null): Promise<T> {
const form = new FormData()
form.append('backup', backup)
if (images) form.append('images', images)
let response: Response
try {
response = await fetch(`${API_BASE_URL}${path}`, { method: 'POST', body: form })
} catch {
throw new ApiError(de.api.errors.network, null, path)
}
if (!response.ok) {
// 400 trägt eine deutsche Klartext-Fehlermeldung (z. B. „keine _rpro3.db gefunden").
let body: unknown
let text: string | null = null
try {
text = await response.clone().text()
body = text ? JSON.parse(text) : undefined
} catch {
body = undefined
}
const message =
response.status === 400 && text
? text.replace(/^"|"$/g, '')
: response.status >= 500
? de.api.errors.server
: de.api.errors.unknown
throw new ApiError(message, response.status, path, body)
}
return (await response.json()) as T
}
export function analyzeRpro3(backup: File, images: File | null): Promise<Rpro3AnalyzeResult> {
return postUpload<Rpro3AnalyzeResult>('/import/rpro3/analyze', backup, images)
}
export function executeRpro3(backup: File, images: File | null): Promise<Rpro3ExecuteResult> {
return postUpload<Rpro3ExecuteResult>('/import/rpro3/execute', backup, images)
}