/** * Backup-Wizard in zehn Schritten (SYNCOVA_IMPLEMENTATION_PLAN.md §10). * * Der Assistent fuehrt von Name bis Anlegen. Die Pruefung geschieht je Schritt, * damit ein Fehler dort auffaellt, wo er entsteht - nicht erst nach dem letzten * Schritt. Rueckwaerts geht es ohne Datenverlust: Der Entwurf lebt in einem * Zustand, nicht in den Eingabefeldern. */ import { useEffect, useState } from 'react'; import { ApiError } from '../../api/client'; import { createJob, listRepositories } from './jobsApi'; import type { BackupJob, BackupRepository, SourceType } from './jobsApi'; import { buildCreateRequest, createEmptyDraft, describeDraftSchedule, splitPatternList, STEP_TITLES, UNAVAILABLE_STEPS, validateStep, WEEKDAY_NAMES, WIZARD_STEPS, } from './wizardModel'; import type { DraftSource, JobDraft, WizardStep } from './wizardModel'; /** Eigenschaften des Assistenten. */ export interface BackupWizardProps { /** Wird nach dem Anlegen mit dem erzeugten Auftrag aufgerufen. */ onJobCreated: (createdJob: BackupJob) => void; /** Bricht den Assistenten ab. */ onCancel: () => void; } /** Quellarten, die der Executor derzeit sichern kann. */ const SUPPORTED_SOURCE_TYPES: readonly SourceType[] = ['filesystem']; /** Beschriftungen der Quellarten. */ const SOURCE_TYPE_LABELS: Record = { filesystem: 'Verzeichnis', proxmox_vm: 'Proxmox-VM', proxmox_container: 'Proxmox-Container', windows_system: 'Windows-System', linux_system: 'Linux-System', }; /** Zeigt den Assistenten zum Anlegen eines Sicherungsauftrags. */ export function BackupWizard({ onJobCreated, onCancel }: BackupWizardProps): React.JSX.Element { const [currentStepIndex, setCurrentStepIndex] = useState(0); const [jobDraft, setJobDraft] = useState(createEmptyDraft); const [repositories, setRepositories] = useState([]); const [repositoryError, setRepositoryError] = useState(null); const [submitError, setSubmitError] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const [showProblems, setShowProblems] = useState(false); const currentStep = WIZARD_STEPS[currentStepIndex] as WizardStep; const stepValidation = validateStep(currentStep, jobDraft); // Die Ziele werden einmal geladen. Ein Abbruchsignal verhindert, dass eine // Antwort nach dem Schliessen des Assistenten noch Zustand setzt. useEffect(() => { const abortController = new AbortController(); listRepositories(abortController.signal) .then(setRepositories) .catch((caughtError: unknown) => { if (abortController.signal.aborted) { return; } setRepositoryError(describeError(caughtError, 'Die Sicherungsziele konnten nicht geladen werden.')); }); return () => abortController.abort(); }, []); /** Aendert ein Feld des Entwurfs. */ function updateDraft(changedFields: Partial): void { setJobDraft((previousDraft) => ({ ...previousDraft, ...changedFields })); setShowProblems(false); } /** Geht einen Schritt vor, sofern der aktuelle vollstaendig ist. */ function goToNextStep(): void { if (!stepValidation.isComplete) { // Die Probleme erscheinen erst beim Versuch weiterzugehen. Sie von Anfang // an zu zeigen hiesse, ein leeres Formular als fehlerhaft zu markieren. setShowProblems(true); return; } setShowProblems(false); setCurrentStepIndex((previousIndex) => Math.min(previousIndex + 1, WIZARD_STEPS.length - 1)); } /** Geht einen Schritt zurueck. */ function goToPreviousStep(): void { setShowProblems(false); setCurrentStepIndex((previousIndex) => Math.max(previousIndex - 1, 0)); } /** Springt zu einem bereits erreichten Schritt. */ function jumpToStep(targetIndex: number): void { if (targetIndex > currentStepIndex) { return; } setShowProblems(false); setCurrentStepIndex(targetIndex); } /** Legt den Auftrag an. */ async function handleCreate(): Promise { setSubmitError(null); setIsSubmitting(true); try { const createdJob = await createJob(buildCreateRequest(jobDraft)); onJobCreated(createdJob); } catch (caughtError: unknown) { // Die Meldung des Servers wird wortgetreu gezeigt: Sie nennt den // eigentlichen Grund, etwa einen Zeitplan, der den geforderten // Wiederherstellungspunkt nicht einhalten kann. setSubmitError(describeError(caughtError, 'Der Auftrag konnte nicht angelegt werden.')); } finally { setIsSubmitting(false); } } return (
    {WIZARD_STEPS.map((wizardStep, stepIndex) => (
  1. ))}

Schritt {currentStepIndex + 1} von {WIZARD_STEPS.length}: {STEP_TITLES[currentStep]}

{renderStepContent()} {showProblems && stepValidation.problems.length > 0 && (
    {stepValidation.problems.map((problemText) => (
  • {problemText}
  • ))}
)}
{currentStepIndex > 0 && ( )} {currentStep !== 'create' ? ( ) : ( )}
); /** Waehlt den Inhalt des aktuellen Schrittes. */ function renderStepContent(): React.JSX.Element { const unavailableExplanation = UNAVAILABLE_STEPS[currentStep]; if (unavailableExplanation !== undefined) { return renderUnavailableStep(unavailableExplanation); } switch (currentStep) { case 'name': return renderNameStep(); case 'source': return renderSourceStep(); case 'schedule': return renderScheduleStep(); case 'repository': return renderRepositoryStep(); case 'security': return renderSecurityStep(); case 'review': return renderReviewStep(); case 'create': return renderCreateStep(); default: return

Unbekannter Schritt.

; } } /** * Zeigt einen Schritt ohne Umsetzung im Backend. * * Es werden keine Eingaben abgefragt. Eine Maske, die Werte sammelt, die * niemand auswertet, ist ein vorgetaeuschtes Funktionsversprechen * (PROMPT.md §138) - stattdessen steht hier, was ohne diese Einstellung * tatsaechlich geschieht. */ function renderUnavailableStep(explanationText: string): React.JSX.Element { return (

Noch nicht verfuegbar

{explanationText}

); } /** Schritt 1: Name und Beschreibung. */ function renderNameStep(): React.JSX.Element { return (