# Spec: Pro-Projekt Backup-Einstellungen **Datum:** 2026-03-26 **Status:** Draft v3 **Scope:** EngineeringSync – Backup-Konfiguration pro Projekt im Setup-Wizard und TrayApp --- ## Übersicht Beim Sync erstellt der `SyncManager` aktuell immer eine `.bak`-Datei im gleichen Ordner wie die Zieldatei, ohne dass der User Kontrolle darüber hat. Ziel ist es, dem User pro Projekt volle Kontrolle über das Backup-Verhalten zu geben: aktivieren/deaktivieren, eigenen Backup-Ordner wählen, und eine Aufbewahrungsregel (max. N Backups pro Datei) festlegen. --- ## Anforderungen ### Funktional - Der User kann pro Projekt Backups **aktivieren oder deaktivieren**. - Bei aktivierten Backups kann der User wählen zwischen: - **Gleicher Ordner** wie die Zieldatei (bisheriges Verhalten) - **Eigener Backup-Ordner** (frei wählbarer Pfad) - Der User kann eine **Aufbewahrungsregel** festlegen: - `0` = unbegrenzt (alle Backups behalten) - `N > 0` = maximal N Backups pro Datei; älteste werden automatisch gelöscht - Die Einstellungen sind konfigurierbar: - Im **Setup-Wizard** (neue Seite zwischen „Erstes Projekt" und „Optionen") - Im **TrayApp `ProjectManagementWindow`** (neue BACKUP-Sektion beim Bearbeiten) ### Nicht-funktional - Standardwerte: `BackupEnabled = true`, `BackupPath = null` (gleicher Ordner), `MaxBackupsPerFile = 0` - Rückwärtskompatibilität: bestehende Projekte erhalten die Standardwerte per DB-Migration - Backup-Dateien folgen weiterhin dem Schema: `{name}_{yyyyMMdd_HHmmss}{ext}.bak` - Der Timestamp im Backup-Dateinamen verwendet `DateTime.Now` (Ortszeit), um für den User lesbar zu sein. Dies ist eine bewusste Abweichung von der UTC-Konvention im Rest der Anwendung. --- ## Architektur ### Ausgewählter Ansatz: Backup-Felder direkt in `ProjectConfig` Drei neue Felder in der bestehenden `ProjectConfig`-Entity – keine neue Tabelle. Konsistent mit dem Muster des Projekts (alle Projekteinstellungen zentral in einer Entity). **Verworfene Alternativen:** - Separate `ProjectBackupSettings`-Entity (1:1): Mehr Boilerplate für nur 3 Felder - JSON-Column: Verlust von EF-Typsicherheit, Overkill für 3 Felder --- ## Komponenten & Änderungen ### 1. Domain – `ProjectConfig` ```csharp public bool BackupEnabled { get; set; } = true; public string? BackupPath { get; set; } = null; // null = gleicher Ordner wie Zieldatei public int MaxBackupsPerFile { get; set; } = 0; // 0 = unbegrenzt ``` ### 2. Infrastructure – EF Migration - Migration: `AddBackupSettingsToProjectConfig` - Standardwerte per `HasDefaultValue()` in `OnModelCreating` für bestehende Rows: - `BackupEnabled`: `true` - `BackupPath`: `null` - `MaxBackupsPerFile`: `0` ### 3. Service – `SyncManager` **`BackupFile()` wird von `static` zur Instanzmethode** und erhält `project` als zweiten Parameter: `BackupFile(string targetPath, ProjectConfig project)`. **Ablauf in `BackupFile(targetPath, project)`:** 1. `if (!project.BackupEnabled) return;` — kein Backup, sofort zurück 2. `backupDir = project.BackupPath ?? Path.GetDirectoryName(targetPath)` 3. `Directory.CreateDirectory(backupDir)` — **nur** Backup-Verzeichnis (unabhängig vom Ziel-Verzeichnis) 4. Backup-Dateiname: `{name}_{DateTime.Now:yyyyMMdd_HHmmss}{ext}.bak` 5. Backup-Datei verschieben (laufwerk-übergreifend sicher): - Versuch: `File.Move(targetPath, backupPath)` (schnell, gleiche Volume) - Falls `IOException` (z.B. andere Volume, Netzlaufwerk): `File.Copy` + `File.Delete` als Fallback 6. `if (project.MaxBackupsPerFile > 0) CleanupOldBackups(backupDir, baseName, ext, project.MaxBackupsPerFile)` **`ProcessChangeAsync(change, ct)` – beide Call-Sites werden aktualisiert:** Die Methode hat zwei Stellen, an denen `BackupFile` aufgerufen wird: ``` // Stelle 1: Deleted-Branch (Zeile ~57) if (File.Exists(targetPath)) { BackupFile(targetPath, project); // ← project hinzufügen File.Delete(targetPath); } // Stelle 2: Overwrite-Branch (Zeile ~69) if (File.Exists(targetPath)) BackupFile(targetPath, project); // ← project hinzufügen ``` **`Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!)` bleibt unverändert** (Zeile ~66 im aktuellen Code) – dies ist die Zielverzeichnis-Erstellung und ist von der Backup-Verzeichnis-Erstellung in `BackupFile()` unabhängig. **Verhalten bei `ChangeType.Deleted`:** - `BackupEnabled = true`: Zieldatei wird **gesichert** (`.bak`), dann gelöscht. (Konsistent mit bisherigem Verhalten.) - `BackupEnabled = false`: Zieldatei wird **unwiederbringlich gelöscht**. Dies ist gewünschtes Verhalten – der User hat Backups explizit deaktiviert. **Neue `CleanupOldBackups()`-Methode:** ``` CleanupOldBackups(backupDir, baseName, ext, max): 1. Suche alle Dateien: {baseName}_*{ext}.bak im backupDir 2. Sortiere alphabetisch aufsteigend nach Dateiname (Format yyyyMMdd_HHmmss ist lexikografisch geordnet → korrekte zeitliche Reihenfolge) NICHT nach Filesystem-Erstelldatum sortieren (File.Move behält das Original-Erstelldatum) 3. Lösche alle Einträge über dem Limit (älteste zuerst = vorne in der sortierten Liste) 4. Fehler beim Löschen: LogWarning, kein Abbruch ``` **Rename-Szenario (`ChangeType.Renamed`):** Backups, die unter dem alten Dateinamen angelegt wurden, werden von `CleanupOldBackups` nicht erfasst (anderer `baseName`). Diese orphaned Backups akkumulieren sich unter dem alten Namen. Dieses Verhalten ist **explizit akzeptiert** und liegt außerhalb des Scopes dieser Änderung. ### 4. Service – API-Modelle `CreateProjectRequest` und `UpdateProjectRequest` erhalten: ```csharp bool BackupEnabled = true string? BackupPath = null int MaxBackupsPerFile = 0 ``` **Validierungsregel für `BackupPath` in `ProjectsApi`:** `BackupPath` wird bei POST/PUT **nicht** auf Existenz geprüft. Fehlende Verzeichnisse werden beim ersten Sync per `Directory.CreateDirectory()` automatisch erzeugt. (Abweichend von `EngineeringPath`/`SimulationPath`, die auf Existenz geprüft werden.) `ProjectsApi` mappt die neuen Felder beim Erstellen/Aktualisieren der `ProjectConfig`. ### 5. Setup-Wizard **Neue Datei:** `EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml` + `.cs` **`WizardState` – neue Properties:** ```csharp [ObservableProperty] private bool _backupEnabled = true; [ObservableProperty] private bool _backupUseCustomPath = false; [ObservableProperty] private string _backupCustomPath = string.Empty; [ObservableProperty] private int _maxBackupsPerFile = 0; ``` **`WizardViewModel` – Wizard-Schritte steigen von 7 auf 8:** ``` Index 0: Willkommen Index 1: Installation Index 2: Erstes Projekt Index 3: Backup ← NEU WizardStep("Backup", "\uE72E") Index 4: Optionen Index 5: Zusammenfassung Index 6: Installation Index 7: Fertig ``` `WizardViewModel.Steps` erhält `WizardStep("Backup", "\uE72E")` an Position 3. `_pageFactories` erhält `() => new BackupOptionsPage(this)` an Position 3. `IsLastStep = Steps.Count - 2` bleibt unverändert (ergibt mit 8 Schritten korrekt Index 6 = „Zusammenfassung"). **`BackupOptionsPage`-Inhalt:** - `OptionCard` / `CheckBox`: „Backups vor dem Überschreiben aktivieren" (Standard: an) - Wenn aktiviert (Visibility via `BoolToVisibilityConverter`): - Radio: „Gleicher Ordner wie Simulationsdatei" *(Standard)* - Radio: „Eigener Backup-Ordner" → TextBox + Browse-Button (`FolderBrowserDialog`) - TextBox + Label: „Maximal ___ Backups pro Datei (0 = unbegrenzt)" **`InstallerService.WriteFirstRunConfig()`** schreibt Backup-Felder als neuen `Backup`-Abschnitt in `firstrun-config.json`: ```json { "FirstRun": { ... }, "Backup": { "BackupEnabled": true, "BackupPath": null, "MaxBackupsPerFile": 0 } } ``` **`Program.cs` – `ProcessFirstRunConfigAsync()`-Funktion** (NICHT `Worker.cs` – die Verarbeitung der `firstrun-config.json` liegt als lokale Funktion in `Program.cs`, Zeile ~42): - Liest den neuen `Backup`-Abschnitt via `JsonDocument` (mit `TryGetProperty` für Rückwärtskompatibilität, falls `Backup`-Abschnitt fehlt → Standardwerte) - Setzt `BackupEnabled`, `BackupPath`, `MaxBackupsPerFile` beim Erstellen der `ProjectConfig` **`SummaryPage`** zeigt: - `SummaryBoolRow`: „Backups aktiviert" - `SummaryRow` (conditional, wenn `BackupUseCustomPath`): „Backup-Ordner: {Pfad}" - `SummaryRow` (conditional, wenn `MaxBackupsPerFile > 0`): „Max. Backups pro Datei: {N}" ### 6. TrayApp – `ProjectManagementWindow` **`ProjectManagementViewModel` – neue Properties:** ```csharp [ObservableProperty] private bool _backupEnabled; [ObservableProperty] private bool _backupUseCustomPath; [ObservableProperty] private string _backupCustomPath = string.Empty; [ObservableProperty] private int _maxBackupsPerFile; ``` **XAML – neue BACKUP-Sektion** im Bearbeitungs-Formular (analog zu bestehenden Sektionen): ``` BACKUP ├── CheckBox: „Backups vor dem Überschreiben erstellen" ├── (sichtbar wenn aktiviert, via BoolToVisibilityConverter): │ ├── RadioButton: „Gleicher Ordner wie Simulationsdatei" │ ├── RadioButton: „Eigener Ordner:" + TextBox + [Durchsuchen] │ └── TextBox: „Maximal ___ Backups pro Datei (0 = unbegrenzt)" ``` **TrayApp `ApiClient` – DTOs:** `CreateProjectDto` und `UpdateProjectDto` in `ApiClient.cs` erhalten: ```csharp bool BackupEnabled = true string? BackupPath = null int MaxBackupsPerFile = 0 ``` `ApiClient.CreateProjectAsync()` und `UpdateProjectAsync()` senden die Backup-Felder mit. --- ## Datenfluss ``` [Wizard BackupOptionsPage] ↓ WizardState [InstallerService] → firstrun-config.json (Backup-Abschnitt) ↓ [Program.cs ProcessFirstRunConfigAsync()] → ProjectConfig (mit Backup-Feldern) in SQLite [TrayApp ProjectManagementWindow] ↓ PUT /api/projects/{id} (mit Backup-Feldern in UpdateProjectDto) [ProjectsApi] → ProjectConfig in SQLite ↓ [SyncManager.ProcessChangeAsync] ├─ Directory.CreateDirectory(targetDir) ← Zielverzeichnis └─ BackupFile(targetPath, project) ├─ if !BackupEnabled → return ├─ backupDir = BackupPath ?? targetDir ├─ Directory.CreateDirectory(backupDir) ← Backup-Verzeichnis ├─ File.Move(targetPath, backupPath) └─ CleanupOldBackups() wenn MaxBackupsPerFile > 0 ``` --- ## Fehlerbehandlung | Szenario | Verhalten | |---|---| | Backup-Ordner existiert nicht | `Directory.CreateDirectory()` in `BackupFile()` erzeugt ihn beim ersten Sync | | Backup-Ordner nicht beschreibbar | Exception → von `ProcessChangeAsync` gefangen → `failed`-Zähler | | Cleanup schlägt fehl | `logger.LogWarning`, kein Sync-Abbruch | | `BackupEnabled=false` + `Deleted` | Zieldatei wird unwiederbringlich gelöscht (gewünscht) | | `BackupEnabled=true` + `Deleted` | Backup wird erstellt, dann Zieldatei gelöscht (bisheriges Verhalten) | | `BackupPath` bei POST/PUT nicht existent | Kein API-Fehler; Verzeichnis wird beim ersten Sync erstellt | | Rename-Szenario: alte Backups | Akkumulieren unter altem Dateinamen; kein automatisches Cleanup (out of scope) | --- ## Nicht im Scope - Backups wiederherstellen (Restore-UI) - Backup-Komprimierung (.zip) - Backup-Statistiken / Speicherplatz-Anzeige - Globale (projektübergreifende) Backup-Einstellungen - Cleanup von Backups unter altem Dateinamen nach Rename