/** * API-Anbindung der Sicherungsaufträge (SYNCOVA_API.md §9). * * Das Modul kennt nur den Vertrag nach außen. Die Gestalt der Anfrage folgt * dem Backend und nicht der Oberfläche: Der Wizard führt seinen eigenen * Entwurf und übersetzt ihn erst beim Anlegen. Andernfalls müsste jede * Änderung an der API sofort die Maske umbauen. */ import { requestApi } from '../../api/client'; /** Art eines Zeitplans (packages/scheduler). */ export type ScheduleType = | 'manual' | 'interval' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'cron'; /** Dringlichkeit eines Auftrags. */ export type JobPriority = 'critical' | 'high' | 'normal' | 'low'; /** Art einer Sicherungsquelle. */ export type SourceType = | 'filesystem' | 'proxmox_vm' | 'proxmox_container' | 'windows_system' | 'linux_system'; /** Zeitplan im Anfrage- und Antwortformat der API. */ export interface ScheduleDescriptor { /** Art des Zeitplans. */ type: ScheduleType; /** Abstand in Sekunden bei type=interval. */ interval_seconds?: number; /** Uhrzeit im Format HH:MM. */ time?: string; /** Wochentage, 0 = Sonntag. */ weekdays?: number[]; /** Tage des Monats; -1 bedeutet Monatsletzter. */ month_days?: number[]; /** Cron-Ausdruck bei type=cron. */ cron_expression?: string; /** Zeitzone der Uhrzeiten. */ time_zone?: string; } /** Quelle im Anfrage- und Antwortformat der API. */ export interface SourceDescriptor { /** Art der Quelle. */ type: SourceType; /** Kennung innerhalb ihrer Art, etwa ein Pfad. */ id: string; /** Sprechende Bezeichnung. */ name?: string; /** Einzuschließende Muster. */ include_patterns?: string[]; /** Auszuschließende Muster. */ exclude_patterns?: string[]; } /** Rumpf beim Anlegen eines Auftrags. */ export interface CreateJobRequest { /** Eindeutige Bezeichnung. */ name: string; /** Erläuterung des Zwecks. */ description?: string; /** Dringlichkeit. */ priority?: JobPriority; /** Zeitplan. */ schedule: ScheduleDescriptor; /** Zu sichernde Quellen. */ sources: SourceDescriptor[]; /** Ziel-Repository. */ repository_id: string; /** Zulässiger Datenverlust in Sekunden. */ rpo_seconds?: number; /** Zulässige Wiederherstellungsdauer in Sekunden. */ rto_seconds?: number; /** Bandbreitengrenze in Byte je Sekunde. */ bandwidth_limit_bps?: number; } /** Auftrag in der Antwort der API. */ export interface BackupJob { /** Öffentlicher Bezeichner. */ id: string; /** Bezeichnung. */ name: string; /** Erläuterung. */ description?: string; /** Zustand. */ status: string; /** Dringlichkeit. */ priority: JobPriority; /** Zeitplan. */ schedule: ScheduleDescriptor; /** Erklärung des Zeitplans in einem Satz. */ schedule_description: string; /** Quellen. */ sources: SourceDescriptor[]; /** Ziel-Repository. */ repository_id: string; /** Nächster Zeitpunkt in UTC. */ next_run_at?: string; /** Beginn des letzten Laufs in UTC. */ last_run_at?: string; /** Ausgang des letzten Laufs. */ last_outcome?: string; /** Bandbreitengrenze in Byte je Sekunde. */ bandwidth_limit_bps?: number; } /** Sicherungsziel in der Antwort der API. */ export interface BackupRepository { /** Öffentlicher Bezeichner. */ id: string; /** Sprechende Bezeichnung. */ name: string; /** Ablageart. */ repository_type: string; /** Pfad oder Adresse der Ablage. */ location: string; /** Betriebszustand. */ status: string; /** * Meldet, ob dieses Ziel Sicherungen annimmt. * * Die Auskunft kommt vom Server. Die Oberfläche müsste sonst wissen, welche * Zustände schreibend sind - eine Regel, die dort nicht hingehört. */ accepts_backups: boolean; /** Meldet den gehärteten Modus. */ hardened: boolean; } /** * Lädt die bekannten Sicherungsziele. * * Das Abbruchsignal wird nur gesetzt, wenn es vorliegt: Bei * exactOptionalPropertyTypes ist ein ausdrückliches undefined etwas anderes * als ein fehlendes Feld. */ export async function listRepositories(abortSignal?: AbortSignal): Promise { return requestApi('/repositories', abortSignal ? { signal: abortSignal } : {}); } /** Lädt die vorhandenen Sicherungsaufträge. */ export async function listJobs(abortSignal?: AbortSignal): Promise { return requestApi('/jobs?page_size=100', abortSignal ? { signal: abortSignal } : {}); } /** Legt einen Sicherungsauftrag an. */ export async function createJob(jobRequest: CreateJobRequest): Promise { return requestApi('/jobs', { method: 'POST', body: jobRequest }); } // --------------------------------------------------------------------------- // Bedienung der Aufträge // --------------------------------------------------------------------------- // // Bis hierher konnte die Oberfläche einen Auftrag anlegen und ansehen — mehr // nicht. Eine Liste, deren Einträge sich nicht bedienen lassen, ist ein // Bericht, keine Konsole. /** Ein einzelner Lauf eines Auftrags. */ export interface BackupJobRun { /** Öffentlicher Bezeichner. */ id: string; /** Zugehöriger Auftrag. */ job_id: string; /** Zustand: queued, running, succeeded, partial_failure, failed, cancelled. */ status: string; /** Beginn in UTC. */ started_at?: string; /** Ende in UTC. */ completed_at?: string; /** Gelesene Bytes. */ bytes_processed?: number; /** Tatsächlich abgelegte Bytes nach Deduplizierung. */ bytes_written?: number; /** Anzahl erfasster Dateien. */ files_processed?: number; /** * Übergangene Objekte. * * Größer als null bedeutet Teilfehler — die Datenbank lässt * "erfolgreich mit übergangenen Objekten" per CHECK gar nicht zu. */ files_skipped?: number; /** Durchsatz in Byte je Sekunde. */ throughput_bps?: number; /** Fehlercode bei nicht erfolgreichem Ausgang. */ error_code?: string; /** Fehlermeldung. */ error_message?: string; /** Klasse des Fehlers: transient, permanent, integrity, auth, … */ error_class?: string; } /** Lädt einen einzelnen Auftrag. */ export async function getJob(jobIdentifier: string, abortSignal?: AbortSignal): Promise { return requestApi( `/jobs/${encodeURIComponent(jobIdentifier)}`, abortSignal ? { signal: abortSignal } : {}, ); } /** Lädt die Laufhistorie eines Auftrags. */ export async function listJobRuns( jobIdentifier: string, abortSignal?: AbortSignal, ): Promise { return requestApi( `/jobs/${encodeURIComponent(jobIdentifier)}/runs?page_size=50`, abortSignal ? { signal: abortSignal } : {}, ); } /** * Stößt einen Lauf an. * * Die Antwort ist 202, nicht 201: Der Lauf ist eingereiht, die Sicherung hat * nicht begonnen. Ein zweiter Anstoß bei laufendem Auftrag ergibt 409 — das * ist eine Auskunft, kein Fehler, und wird in der Oberfläche als solche * gezeigt. */ export async function runJob(jobIdentifier: string): Promise<{ run_id?: string }> { return requestApi<{ run_id?: string }>(`/jobs/${encodeURIComponent(jobIdentifier)}/run`, { method: 'POST', idempotencyKey: true, }); } /** Hält einen Auftrag an. Laufende Sicherungen bleiben unberührt. */ export async function pauseJob(jobIdentifier: string): Promise { return requestApi(`/jobs/${encodeURIComponent(jobIdentifier)}/pause`, { method: 'POST', }); } /** Nimmt einen angehaltenen Auftrag wieder auf. */ export async function resumeJob(jobIdentifier: string): Promise { return requestApi(`/jobs/${encodeURIComponent(jobIdentifier)}/resume`, { method: 'POST', }); } /** * Löscht einen Auftrag. * * Die bereits erzeugten Wiederherstellungspunkte bleiben bestehen — sie * gehören zum Repository, nicht zum Auftrag. Das muss die Oberfläche sagen, * sonst löscht jemand einen Auftrag in der Annahme, damit Platz zu schaffen. */ export async function deleteJob(jobIdentifier: string): Promise { return requestApi(`/jobs/${encodeURIComponent(jobIdentifier)}`, { method: 'DELETE', idempotencyKey: true, }); } /** Bricht einen laufenden Sicherungslauf ab. */ export async function cancelJobRun(runIdentifier: string): Promise { return requestApi(`/backup-runs/${encodeURIComponent(runIdentifier)}/cancel`, { method: 'POST', }); }