# Backup-Einstellungen pro Projekt – Implementierungsplan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Pro-Projekt konfigurierbare Backup-Einstellungen (an/aus, eigener Ordner, Aufbewahrungsregel) im Setup-Wizard und TrayApp. **Architecture:** Drei neue Felder direkt in `ProjectConfig` (BackupEnabled, BackupPath, MaxBackupsPerFile). `SyncManager.BackupFile` wird zur Instanzmethode mit cross-volume-sicherem Verschieben und automatischem Cleanup. Setup-Wizard erhält eine neue `BackupOptionsPage`, TrayApp eine neue BACKUP-Sektion im `ProjectManagementWindow`. **Tech Stack:** .NET 10, EF Core 10 + SQLite, ASP.NET Core Minimal API, WPF + CommunityToolkit.Mvvm, `OpenFolderDialog` (Microsoft.Win32) --- ## Dateiübersicht | Aktion | Datei | |--------|-------| | Modify | `EngineeringSync.Domain/Entities/ProjectConfig.cs` | | Modify | `EngineeringSync.Infrastructure/AppDbContext.cs` | | **Generate** | `EngineeringSync.Infrastructure/Migrations/..._AddBackupSettings.cs` (via `dotnet ef`) | | Modify | `EngineeringSync.Service/Services/SyncManager.cs` | | Modify | `EngineeringSync.Service/Models/ApiModels.cs` | | Modify | `EngineeringSync.Service/Api/ProjectsApi.cs` | | Modify | `EngineeringSync.Service/Program.cs` | | Modify | `EngineeringSync.Setup/ViewModels/WizardState.cs` | | **Create** | `EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml` | | **Create** | `EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml.cs` | | Modify | `EngineeringSync.Setup/ViewModels/WizardViewModel.cs` | | Modify | `EngineeringSync.Setup/Services/InstallerService.cs` | | Modify | `EngineeringSync.Setup/Views/Pages/SummaryPage.xaml` | | Modify | `EngineeringSync.TrayApp/Services/ApiClient.cs` | | Modify | `EngineeringSync.TrayApp/ViewModels/ProjectManagementViewModel.cs` | | Modify | `EngineeringSync.TrayApp/Views/ProjectManagementWindow.xaml` | | Modify | `EngineeringSync.TrayApp/Views/ProjectManagementWindow.xaml.cs` | --- ## Task 1: Domain – Backup-Felder in ProjectConfig **Files:** - Modify: `EngineeringSync.Domain/Entities/ProjectConfig.cs` - [ ] **Schritt 1: Drei Felder nach `IsActive` einfügen** ```csharp // In ProjectConfig.cs – nach "public bool IsActive" einfügen: public bool BackupEnabled { get; set; } = true; public string? BackupPath { get; set; } = null; // null = gleicher Ordner public int MaxBackupsPerFile { get; set; } = 0; // 0 = unbegrenzt ``` - [ ] **Schritt 2: Build prüfen** ```bash dotnet build EngineeringSync.Domain/EngineeringSync.Domain.csproj ``` Erwartung: `Build succeeded` ohne Fehler. - [ ] **Schritt 3: Commit** ```bash git add EngineeringSync.Domain/Entities/ProjectConfig.cs git commit -m "feat(domain): add BackupEnabled, BackupPath, MaxBackupsPerFile to ProjectConfig" ``` --- ## Task 2: Infrastructure – EF-Migration **Files:** - Modify: `EngineeringSync.Infrastructure/AppDbContext.cs` - Generate: `EngineeringSync.Infrastructure/Migrations/..._AddBackupSettings.cs` - [ ] **Schritt 1: HasDefaultValue in AppDbContext eintragen** In `AppDbContext.cs`, die bestehende `modelBuilder.Entity(e => {...})` Konfiguration erweitern: ```csharp modelBuilder.Entity(e => { e.HasKey(p => p.Id); e.Property(p => p.Name).IsRequired().HasMaxLength(200); e.Property(p => p.EngineeringPath).IsRequired(); e.Property(p => p.SimulationPath).IsRequired(); // NEU: Standardwerte für Backup-Felder (für bestehende Datenbankzeilen) e.Property(p => p.BackupEnabled).HasDefaultValue(true); e.Property(p => p.BackupPath).HasDefaultValue(null); e.Property(p => p.MaxBackupsPerFile).HasDefaultValue(0); }); ``` - [ ] **Schritt 2: Migration generieren** ```bash cd D:/001_Projekte/021_KON-SIM dotnet ef migrations add AddBackupSettingsToProjectConfig \ --project EngineeringSync.Infrastructure \ --startup-project EngineeringSync.Service ``` Erwartung: Neue Migrationsdatei in `EngineeringSync.Infrastructure/Migrations/` mit drei `AddColumn`-Aufrufen für `BackupEnabled`, `BackupPath`, `MaxBackupsPerFile`. - [ ] **Schritt 3: Generierte Migration kurz prüfen** Die generierte `.cs`-Datei öffnen und sicherstellen, dass `defaultValue: true` (BackupEnabled) und `defaultValue: 0` (MaxBackupsPerFile) enthalten sind. Falls Werte fehlen: manuell ergänzen. - [ ] **Schritt 4: Build prüfen** ```bash dotnet build EngineeringSync.Infrastructure/EngineeringSync.Infrastructure.csproj ``` Erwartung: `Build succeeded`. - [ ] **Schritt 5: Commit** ```bash git add EngineeringSync.Infrastructure/AppDbContext.cs git add EngineeringSync.Infrastructure/Migrations/ git commit -m "feat(infra): migration AddBackupSettingsToProjectConfig" ``` --- ## Task 3: Service – SyncManager refaktorieren **Files:** - Modify: `EngineeringSync.Service/Services/SyncManager.cs` - [ ] **Schritt 1: `ProcessChangeAsync` – `static` entfernen und beide BackupFile-Aufrufe anpassen** Die Methode `ProcessChangeAsync` ist aktuell `static` (Zeile 47). Da sie gleich eine Instanzmethode (`BackupFile`) aufrufen soll, muss `static` entfernt werden: ```csharp // Vorher: private static async Task ProcessChangeAsync(PendingChange change, CancellationToken ct) // Nachher: private async Task ProcessChangeAsync(PendingChange change, CancellationToken ct) ``` Dann beide BackupFile-Aufrufe anpassen: - Zeile ~57: im `Deleted`-Branch: `BackupFile(targetPath);` → `BackupFile(targetPath, project);` - Zeile ~69: im Overwrite-Branch: `BackupFile(targetPath);` → `BackupFile(targetPath, project);` - [ ] **Schritt 2: `BackupFile` von `static` zur Instanzmethode umbauen** ```csharp private void BackupFile(string targetPath, ProjectConfig project) { if (!project.BackupEnabled) return; var backupDir = project.BackupPath ?? Path.GetDirectoryName(targetPath)!; Directory.CreateDirectory(backupDir); var nameWithoutExt = Path.GetFileNameWithoutExtension(targetPath); var ext = Path.GetExtension(targetPath); var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); var backupPath = Path.Combine(backupDir, $"{nameWithoutExt}_{timestamp}{ext}.bak"); // Cross-volume-sicheres Verschieben (z.B. Netzlaufwerke, andere Laufwerksbuchstaben) try { File.Move(targetPath, backupPath); } catch (IOException) { File.Copy(targetPath, backupPath, overwrite: false); File.Delete(targetPath); } if (project.MaxBackupsPerFile > 0) CleanupOldBackups(backupDir, nameWithoutExt, ext, project.MaxBackupsPerFile); } ``` - [ ] **Schritt 3: `CleanupOldBackups` als private Instanzmethode hinzufügen** ```csharp private void CleanupOldBackups(string backupDir, string baseName, string ext, int max) { try { var pattern = $"{baseName}_*{ext}.bak"; var backups = Directory.GetFiles(backupDir, pattern) .OrderBy(f => Path.GetFileName(f)) // yyyyMMdd_HHmmss → lexikografisch = zeitlich .ToList(); while (backups.Count > max) { File.Delete(backups[0]); backups.RemoveAt(0); } } catch (Exception ex) { logger.LogWarning(ex, "Cleanup alter Backups fehlgeschlagen in {Dir}", backupDir); } } ``` - [ ] **Schritt 4: Build prüfen** ```bash dotnet build EngineeringSync.Service/EngineeringSync.Service.csproj ``` Erwartung: `Build succeeded`. Bei Compiler-Fehler `static member cannot be used with instance receiver`: sicherstellen dass `BackupFile` nicht mehr `static` ist. - [ ] **Schritt 5: Commit** ```bash git add EngineeringSync.Service/Services/SyncManager.cs git commit -m "feat(service): BackupFile als Instanzmethode mit cross-volume-Support und CleanupOldBackups" ``` --- ## Task 4: Service – API-Modelle und ProjectsApi **Files:** - Modify: `EngineeringSync.Service/Models/ApiModels.cs` - Modify: `EngineeringSync.Service/Api/ProjectsApi.cs` - [ ] **Schritt 1: API-Modelle um Backup-Felder erweitern** `CreateProjectRequest` und `UpdateProjectRequest` in `ApiModels.cs` ersetzen durch: ```csharp public record CreateProjectRequest( string Name, string EngineeringPath, string SimulationPath, string FileExtensions, bool IsActive = true, bool BackupEnabled = true, string? BackupPath = null, int MaxBackupsPerFile = 0 ); public record UpdateProjectRequest( string Name, string EngineeringPath, string SimulationPath, string FileExtensions, bool IsActive, bool BackupEnabled = true, string? BackupPath = null, int MaxBackupsPerFile = 0 ); ``` - [ ] **Schritt 2: ProjectsApi – neue Felder beim Erstellen mappen** In `ProjectsApi.cs`, den `MapPost`-Handler: beim Erstellen der `ProjectConfig` die neuen Felder setzen: ```csharp var project = new ProjectConfig { Name = req.Name, EngineeringPath = req.EngineeringPath, SimulationPath = req.SimulationPath, FileExtensions = req.FileExtensions, IsActive = req.IsActive, BackupEnabled = req.BackupEnabled, // NEU BackupPath = req.BackupPath, // NEU MaxBackupsPerFile = req.MaxBackupsPerFile // NEU }; ``` - [ ] **Schritt 3: ProjectsApi – neue Felder beim Aktualisieren mappen** Im `MapPut`-Handler, nach den bestehenden Zuweisungen ergänzen: ```csharp project.Name = req.Name; project.EngineeringPath = req.EngineeringPath; project.SimulationPath = req.SimulationPath; project.FileExtensions = req.FileExtensions; project.IsActive = req.IsActive; project.BackupEnabled = req.BackupEnabled; // NEU project.BackupPath = req.BackupPath; // NEU project.MaxBackupsPerFile = req.MaxBackupsPerFile; // NEU ``` - [ ] **Schritt 4: Build prüfen** ```bash dotnet build EngineeringSync.Service/EngineeringSync.Service.csproj ``` Erwartung: `Build succeeded`. - [ ] **Schritt 5: Commit** ```bash git add EngineeringSync.Service/Models/ApiModels.cs EngineeringSync.Service/Api/ProjectsApi.cs git commit -m "feat(service): Backup-Felder in API-Modelle und ProjectsApi" ``` --- ## Task 5: Service – Program.cs ProcessFirstRunConfigAsync **Files:** - Modify: `EngineeringSync.Service/Program.cs` - [ ] **Schritt 1: Backup-Abschnitt aus firstrun-config.json lesen** In `Program.cs`, in `ProcessFirstRunConfigAsync()`, direkt nach dem Auslesen von `watchAllFiles` (ca. Zeile 58) folgenden Block einfügen: ```csharp // Backup-Einstellungen (optional, mit Standardwerten für Rückwärtskompatibilität) var backupEnabled = true; string? backupPath = null; var maxBackupsPerFile = 0; if (root.RootElement.TryGetProperty("Backup", out var backupElement)) { if (backupElement.TryGetProperty("BackupEnabled", out var be)) backupEnabled = be.GetBoolean(); if (backupElement.TryGetProperty("BackupPath", out var bp) && bp.ValueKind != JsonValueKind.Null) backupPath = bp.GetString(); if (backupElement.TryGetProperty("MaxBackupsPerFile", out var mb)) maxBackupsPerFile = mb.GetInt32(); } ``` - [ ] **Schritt 2: Backup-Felder beim Erstellen der ProjectConfig setzen** Im selben `ProcessFirstRunConfigAsync`, beim `new ProjectConfig { ... }` Block die neuen Felder ergänzen: ```csharp var project = new EngineeringSync.Domain.Entities.ProjectConfig { Name = projectName, EngineeringPath = engineeringPath, SimulationPath = simulationPath, FileExtensions = fileExtensions, IsActive = true, CreatedAt = DateTime.UtcNow, BackupEnabled = backupEnabled, // NEU BackupPath = backupPath, // NEU MaxBackupsPerFile = maxBackupsPerFile // NEU }; ``` - [ ] **Schritt 3: Build prüfen** ```bash dotnet build EngineeringSync.Service/EngineeringSync.Service.csproj ``` Erwartung: `Build succeeded`. - [ ] **Schritt 4: Commit** ```bash git add EngineeringSync.Service/Program.cs git commit -m "feat(service): Backup-Felder aus firstrun-config.json lesen" ``` --- ## Task 6: Setup-Wizard – WizardState **Files:** - Modify: `EngineeringSync.Setup/ViewModels/WizardState.cs` - [ ] **Schritt 1: Vier neue Properties im Abschnitt „Erstes Projekt" hinzufügen** ```csharp // --- Backup-Einstellungen --- [ObservableProperty] private bool _backupEnabled = true; [ObservableProperty] private bool _backupUseCustomPath = false; [ObservableProperty] private string _backupCustomPath = string.Empty; [ObservableProperty] private int _maxBackupsPerFile = 0; ``` - [ ] **Schritt 2: Build prüfen** ```bash dotnet build EngineeringSync.Setup/EngineeringSync.Setup.csproj ``` - [ ] **Schritt 3: Commit** ```bash git add EngineeringSync.Setup/ViewModels/WizardState.cs git commit -m "feat(setup): Backup-Properties in WizardState" ``` --- ## Task 7: Setup-Wizard – BackupOptionsPage erstellen **Files:** - Create: `EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml` - Create: `EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml.cs` - [ ] **Schritt 1: BackupOptionsPage.xaml erstellen** ```xml ``` - [ ] **Schritt 2: BackupOptionsPage.xaml.cs erstellen** ```csharp using System.Windows; using EngineeringSync.Setup.ViewModels; using Microsoft.Win32; namespace EngineeringSync.Setup.Views.Pages; public partial class BackupOptionsPage : WizardPageBase { public BackupOptionsPage(WizardViewModel wizard) : base(wizard) { InitializeComponent(); } private void BrowseBackupPath_Click(object sender, RoutedEventArgs e) { var dlg = new OpenFolderDialog { Title = "Backup-Verzeichnis wählen", InitialDirectory = string.IsNullOrEmpty(Wizard.State.BackupCustomPath) ? null : Wizard.State.BackupCustomPath }; if (dlg.ShowDialog() == true) Wizard.State.BackupCustomPath = dlg.FolderName; } public override bool Validate() { if (Wizard.State.BackupEnabled && Wizard.State.BackupUseCustomPath && string.IsNullOrWhiteSpace(Wizard.State.BackupCustomPath)) { MessageBox.Show("Bitte wählen Sie einen Backup-Ordner oder deaktivieren Sie die Option 'Eigener Backup-Ordner'.", "Validierung", MessageBoxButton.OK, MessageBoxImage.Warning); return false; } return true; } } ``` - [ ] **Schritt 3: Build prüfen** ```bash dotnet build EngineeringSync.Setup/EngineeringSync.Setup.csproj ``` Erwartung: `Build succeeded`. Häufiger Fehler: fehlendes `x:Class`-Attribut oder falsche Namespace-Angaben → prüfen ob `xmlns:local` und `x:Class` übereinstimmen. - [ ] **Schritt 4: Commit** ```bash git add EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml git add EngineeringSync.Setup/Views/Pages/BackupOptionsPage.xaml.cs git commit -m "feat(setup): BackupOptionsPage erstellen" ``` --- ## Task 8: Setup-Wizard – WizardViewModel und InstallerService **Files:** - Modify: `EngineeringSync.Setup/ViewModels/WizardViewModel.cs` - Modify: `EngineeringSync.Setup/Services/InstallerService.cs` - Modify: `EngineeringSync.Setup/Views/Pages/SummaryPage.xaml` - [ ] **Schritt 1: WizardViewModel – neuen Schritt und neue Page einfügen** In `WizardViewModel.cs`, die `Steps`-Liste: neuen Eintrag an Index 3 einfügen: ```csharp public ObservableCollection Steps { get; } = [ new("Willkommen", "\uE80F"), new("Installation", "\uE7B7"), new("Erstes Projekt", "\uE8B7"), new("Backup", "\uE72E"), // NEU – Index 3 new("Optionen", "\uE713"), new("Zusammenfassung","\uE8A9"), new("Installation", "\uE896"), new("Fertig", "\uE930"), ]; ``` Und `_pageFactories`: `() => new BackupOptionsPage(this)` an Index 3 einfügen: ```csharp _pageFactories = [ () => new WelcomePage(this), () => new InstallPathPage(this), () => new FirstProjectPage(this), () => new BackupOptionsPage(this), // NEU – Index 3 () => new ServiceOptionsPage(this), () => new SummaryPage(this), () => new InstallingPage(this, _installer), () => new CompletionPage(this), ]; ``` - [ ] **Schritt 2: InstallerService – Backup-Abschnitt in firstrun-config.json schreiben** In `InstallerService.cs`, in der Methode `WriteFirstRunConfig()`: Das anonyme Objekt `config` erweitern um einen `Backup`-Abschnitt: ```csharp var config = new { FirstRun = new { ProjectName = state.ProjectName, EngineeringPath = state.EngineeringPath, SimulationPath = state.SimulationPath, FileExtensions = state.WatchAllFiles ? "*" : state.FileExtensions, WatchAllFiles = state.WatchAllFiles }, Backup = new // NEU { BackupEnabled = state.BackupEnabled, BackupPath = state.BackupUseCustomPath ? state.BackupCustomPath : (string?)null, MaxBackupsPerFile = state.MaxBackupsPerFile } }; ``` - [ ] **Schritt 3: SummaryPage.xaml – Backup-Karte hinzufügen** In `SummaryPage.xaml`, nach dem bestehenden „Optionen"-Block, vor dem abschließenden `` des `ScrollViewer`, folgende Karte einfügen: ```xml ``` - [ ] **Schritt 4: Build prüfen** ```bash dotnet build EngineeringSync.Setup/EngineeringSync.Setup.csproj ``` - [ ] **Schritt 5: Commit** ```bash git add EngineeringSync.Setup/ViewModels/WizardViewModel.cs git add EngineeringSync.Setup/Services/InstallerService.cs git add EngineeringSync.Setup/Views/Pages/SummaryPage.xaml git commit -m "feat(setup): BackupOptionsPage in Wizard einbinden, InstallerService und SummaryPage" ``` --- ## Task 9: TrayApp – ApiClient-DTOs **Files:** - Modify: `EngineeringSync.TrayApp/Services/ApiClient.cs` - [ ] **Schritt 1: DTOs um Backup-Felder erweitern** `CreateProjectDto` und `UpdateProjectDto` in `ApiClient.cs` ersetzen: ```csharp public record CreateProjectDto( string Name, string EngineeringPath, string SimulationPath, string FileExtensions, bool IsActive = true, bool BackupEnabled = true, string? BackupPath = null, int MaxBackupsPerFile = 0 ); public record UpdateProjectDto( string Name, string EngineeringPath, string SimulationPath, string FileExtensions, bool IsActive, bool BackupEnabled = true, string? BackupPath = null, int MaxBackupsPerFile = 0 ); ``` - [ ] **Schritt 2: Build prüfen** ```bash dotnet build EngineeringSync.TrayApp/EngineeringSync.TrayApp.csproj ``` - [ ] **Schritt 3: Commit** ```bash git add EngineeringSync.TrayApp/Services/ApiClient.cs git commit -m "feat(trayapp): Backup-Felder in CreateProjectDto und UpdateProjectDto" ``` --- ## Task 10: TrayApp – ProjectManagementViewModel **Files:** - Modify: `EngineeringSync.TrayApp/ViewModels/ProjectManagementViewModel.cs` - [ ] **Schritt 1: Vier neue ObservableProperty-Felder hinzufügen** Nach `_editIsActive` einfügen: ```csharp [ObservableProperty] private bool _editBackupEnabled = true; [ObservableProperty] private bool _editBackupUseCustomPath = false; [ObservableProperty] private string _editBackupCustomPath = string.Empty; [ObservableProperty] private int _editMaxBackupsPerFile = 0; ``` - [ ] **Schritt 2: `StartNewProject()` – Standardwerte setzen** Nach `EditIsActive = true;` einfügen: ```csharp EditBackupEnabled = true; EditBackupUseCustomPath = false; EditBackupCustomPath = string.Empty; EditMaxBackupsPerFile = 0; ``` - [ ] **Schritt 3: `EditProject()` – Werte aus ProjectConfig laden** Nach `EditIsActive = project.IsActive;` einfügen: ```csharp EditBackupEnabled = project.BackupEnabled; EditBackupUseCustomPath = project.BackupPath is not null; EditBackupCustomPath = project.BackupPath ?? string.Empty; EditMaxBackupsPerFile = project.MaxBackupsPerFile; ``` - [ ] **Schritt 4: `SaveAsync()` – Backup-Felder in DTOs mitsenden** In `SaveAsync()`, die beiden DTO-Konstruktoraufrufe ersetzen: ```csharp // CreateProjectDto: await api.CreateProjectAsync(new CreateProjectDto( EditName, EditEngineeringPath, EditSimulationPath, EditFileExtensions, EditIsActive, EditBackupEnabled, EditBackupUseCustomPath ? EditBackupCustomPath : null, EditMaxBackupsPerFile)); // UpdateProjectDto: await api.UpdateProjectAsync(SelectedProject!.Id, new UpdateProjectDto( EditName, EditEngineeringPath, EditSimulationPath, EditFileExtensions, EditIsActive, EditBackupEnabled, EditBackupUseCustomPath ? EditBackupCustomPath : null, EditMaxBackupsPerFile)); ``` - [ ] **Schritt 5: Build prüfen** ```bash dotnet build EngineeringSync.TrayApp/EngineeringSync.TrayApp.csproj ``` - [ ] **Schritt 6: Commit** ```bash git add EngineeringSync.TrayApp/ViewModels/ProjectManagementViewModel.cs git commit -m "feat(trayapp): Backup-Properties in ProjectManagementViewModel" ``` --- ## Task 11: TrayApp – ProjectManagementWindow XAML + Code-Behind **Files:** - Modify: `EngineeringSync.TrayApp/Views/ProjectManagementWindow.xaml` - Modify: `EngineeringSync.TrayApp/Views/ProjectManagementWindow.xaml.cs` - [ ] **Schritt 1: BACKUP-Sektion in das Editierformular einfügen** In `ProjectManagementWindow.xaml`, direkt **vor** der `StatusMessage`-TextBlock-Zeile (ca. Zeile 116) folgende Sektion einfügen: ```xml