Files
EngineeringSync/docs/superpowers/plans/2026-03-26-backup-settings.md

968 lines
34 KiB
Markdown
Raw Permalink Normal View History

# 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<ProjectConfig>(e => {...})` Konfiguration erweitern:
```csharp
modelBuilder.Entity<ProjectConfig>(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
<local:WizardPageBase x:Class="EngineeringSync.Setup.Views.Pages.BackupOptionsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:EngineeringSync.Setup.Views.Pages"
Background="White">
<Grid Margin="40,32,40,24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Backup-Einstellungen"
Style="{StaticResource PageTitleStyle}"/>
<TextBlock Grid.Row="1" Style="{StaticResource PageSubtitleStyle}"
Text="Legen Sie fest, ob und wie Sicherungskopien vor dem Überschreiben von Simulationsdateien erstellt werden."/>
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto">
<StackPanel>
<!-- Master-Toggle -->
<local:OptionCard Icon="&#xE72E;" Title="Backups aktivieren"
Description="Vor jeder Dateiüberschreibung wird automatisch eine .bak-Sicherungskopie erstellt"
IsChecked="{Binding BackupEnabled, Mode=TwoWay}"/>
<!-- Optionen (nur wenn Backup aktiviert) -->
<StackPanel Visibility="{Binding BackupEnabled,
Converter={StaticResource BoolToVisConverter}}">
<!-- Speicherort -->
<TextBlock Text="SPEICHERORT" Style="{StaticResource FieldLabelStyle}"
Margin="0,16,0,8"/>
<!-- Mode=OneWay: gegenseitiger Ausschluss läuft über GroupName + TwoWay am zweiten Radio -->
<RadioButton GroupName="BackupLocation"
Content="Gleicher Ordner wie die Simulationsdatei"
IsChecked="{Binding BackupUseCustomPath,
Converter={StaticResource BoolToInvVisConverter},
Mode=OneWay}"
FontFamily="Segoe UI" FontSize="13" Margin="0,0,0,8"/>
<RadioButton GroupName="BackupLocation"
Content="Eigener Backup-Ordner"
IsChecked="{Binding BackupUseCustomPath, Mode=TwoWay}"
FontFamily="Segoe UI" FontSize="13" Margin="0,0,0,8"/>
<!-- Pfad-Eingabe (nur bei eigenem Ordner) -->
<DockPanel Margin="0,0,0,16"
Visibility="{Binding BackupUseCustomPath,
Converter={StaticResource BoolToVisConverter}}">
<Button DockPanel.Dock="Right" Style="{StaticResource IconButtonStyle}"
Margin="6,0,0,0" Click="BrowseBackupPath_Click"
ToolTip="Backup-Verzeichnis wählen">
<TextBlock Text="&#xED25;" FontFamily="Segoe MDL2 Assets"
FontSize="14" Foreground="#0078D4"/>
</Button>
<TextBox Style="{StaticResource ModernTextBoxStyle}"
Text="{Binding BackupCustomPath, UpdateSourceTrigger=PropertyChanged}"
Height="36"/>
</DockPanel>
<!-- Aufbewahrung -->
<TextBlock Text="AUFBEWAHRUNG" Style="{StaticResource FieldLabelStyle}"
Margin="0,0,0,8"/>
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
<TextBlock Text="Maximal" FontFamily="Segoe UI" FontSize="13"
VerticalAlignment="Center" Margin="0,0,8,0"/>
<TextBox x:Name="MaxBackupsBox"
Width="60" Height="32" Padding="6,0"
FontFamily="Segoe UI" FontSize="13"
Text="{Binding MaxBackupsPerFile, UpdateSourceTrigger=PropertyChanged}"
VerticalContentAlignment="Center"/>
<TextBlock Text="Backups pro Datei" FontFamily="Segoe UI" FontSize="13"
VerticalAlignment="Center" Margin="8,0,0,0"/>
</StackPanel>
<TextBlock Text="(0 = unbegrenzt, alle Backups behalten)"
FontFamily="Segoe UI" FontSize="11" Foreground="#5F5F5F"
Margin="0,2,0,0"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</local:WizardPageBase>
```
- [ ] **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<WizardStep> 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 `</StackPanel>` des `ScrollViewer`, folgende Karte einfügen:
```xml
<!-- Backup -->
<Border Style="{StaticResource InfoCardStyle}">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,10">
<TextBlock Text="&#xE72E;" FontFamily="Segoe MDL2 Assets" FontSize="14"
Foreground="#0078D4" Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="Backup" FontFamily="Segoe UI" FontSize="13"
FontWeight="SemiBold" Foreground="#1A1A1A" VerticalAlignment="Center"/>
</StackPanel>
<local:SummaryBoolRow Label="Backups aktiviert" Value="{Binding BackupEnabled}"/>
<local:SummaryRow Label="Backup-Ordner"
Value="{Binding BackupCustomPath}"
Visibility="{Binding BackupUseCustomPath,
Converter={StaticResource BoolToVisConverter}}"/>
<!-- Aufbewahrung immer anzeigen wenn Backup aktiv (0 = unbegrenzt ist ein valider Wert) -->
<local:SummaryRow Label="Max. Backups/Datei"
Value="{Binding MaxBackupsPerFile,
StringFormat={}{0} (0&#x3D;unbegrenzt)}"
Visibility="{Binding BackupEnabled,
Converter={StaticResource BoolToVisConverter}}"/>
</StackPanel>
</Border>
```
- [ ] **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
<!-- BACKUP-Sektion -->
<Separator Margin="0,8,0,12"/>
<TextBlock Text="BACKUP" FontSize="10" FontWeight="Bold" Foreground="#888888"
LetterSpacing="1" Margin="0,0,0,8"/>
<CheckBox Content="Backups vor dem Überschreiben erstellen"
IsChecked="{Binding EditBackupEnabled}"
Margin="0,0,0,10"/>
<StackPanel Visibility="{Binding EditBackupEnabled,
Converter={StaticResource BoolToVisConverter}}">
<TextBlock Text="Speicherort:" FontWeight="SemiBold" Margin="0,0,0,6"/>
<!-- Mode=OneWay: gegenseitiger Ausschluss läuft über GroupName + TwoWay am zweiten Radio -->
<RadioButton GroupName="BackupLocation" Content="Gleicher Ordner wie Simulationsdatei"
IsChecked="{Binding EditBackupUseCustomPath,
Converter={StaticResource InverseBoolToVisConverter},
Mode=OneWay}"
Margin="0,0,0,4"/>
<RadioButton GroupName="BackupLocation" Content="Eigener Backup-Ordner"
IsChecked="{Binding EditBackupUseCustomPath, Mode=TwoWay}"
Margin="0,0,0,6"/>
<DockPanel Margin="0,0,0,10"
Visibility="{Binding EditBackupUseCustomPath,
Converter={StaticResource BoolToVisConverter}}">
<Button DockPanel.Dock="Right" Content="..." Width="32" Margin="4,0,0,0"
Click="BrowseBackup_Click"/>
<TextBox Text="{Binding EditBackupCustomPath, UpdateSourceTrigger=PropertyChanged}"
Padding="6"/>
</DockPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
<TextBlock Text="Max. Backups pro Datei:" FontWeight="SemiBold"
VerticalAlignment="Center" Margin="0,0,8,0"/>
<TextBox Width="50" Padding="4,2"
Text="{Binding EditMaxBackupsPerFile, UpdateSourceTrigger=PropertyChanged}"
VerticalContentAlignment="Center"/>
<TextBlock Text="(0 = unbegrenzt)" FontSize="11" Foreground="Gray"
VerticalAlignment="Center" Margin="6,0,0,0"/>
</StackPanel>
</StackPanel>
```
**Hinweis:** Das `ProjectManagementWindow` hat bereits `BooleanToVisibilityConverter` als `BoolToVisConverter`. Für die inverse Sichtbarkeit der RadioButton-Binding muss ein `InverseBoolToVisibilityConverter` in den Window-Ressourcen ergänzt werden **oder** es wird ein Code-Behind `IsChecked`-Binding auf `!EditBackupUseCustomPath` verwendet (nicht direkt in XAML). Einfachste Lösung: eine neue Converter-Klasse in `TrayApp/Converters/Converters.cs` und in `Window.Resources` registrieren (siehe Schritt 2).
- [ ] **Schritt 2: InverseBoolToVisibilityConverter in TrayApp hinzufügen**
In `EngineeringSync.TrayApp/Converters/Converters.cs` (bestehende Datei) einen neuen Converter hinzufügen:
```csharp
public class InverseBoolToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) =>
value is true ? Visibility.Collapsed : Visibility.Visible;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
throw new NotSupportedException();
}
```
Und in `ProjectManagementWindow.xaml`, in `<Window.Resources>` registrieren:
```xml
<conv:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisConverter"/>
```
**Alternative:** Den RadioButton für „Gleicher Ordner" ohne Converter binden: einfach keine Binding der zweite RadioButton (`EditBackupUseCustomPath = true`) und der erste sind in einer Gruppe; mit `GroupName` explizit koppeln und beide direkt binden:
```xml
<RadioButton GroupName="BackupLocation" Content="Gleicher Ordner wie Simulationsdatei"
IsChecked="{Binding EditBackupUseCustomPath,
Converter={StaticResource InverseBoolToVisConverter}, Mode=TwoWay}"/>
<RadioButton GroupName="BackupLocation" Content="Eigener Backup-Ordner"
IsChecked="{Binding EditBackupUseCustomPath, Mode=TwoWay}"/>
```
- [ ] **Schritt 3: Browse-Handler in Code-Behind hinzufügen**
In `ProjectManagementWindow.xaml.cs` (bestehende Datei), nach den vorhandenen Browse-Handlern einfügen:
```csharp
private void BrowseBackup_Click(object sender, RoutedEventArgs e)
{
var dlg = new OpenFolderDialog
{
Title = "Backup-Verzeichnis wählen",
InitialDirectory = string.IsNullOrEmpty(_vm.EditBackupCustomPath)
? null
: _vm.EditBackupCustomPath
};
if (dlg.ShowDialog() == true)
_vm.EditBackupCustomPath = dlg.FolderName;
}
```
- [ ] **Schritt 4: Build prüfen**
```bash
dotnet build EngineeringSync.TrayApp/EngineeringSync.TrayApp.csproj
```
Häufige Fehler: fehlender `using System.Globalization;` in Converters.cs, fehlende Namespaces in XAML.
- [ ] **Schritt 5: Commit**
```bash
git add EngineeringSync.TrayApp/Views/ProjectManagementWindow.xaml
git add EngineeringSync.TrayApp/Views/ProjectManagementWindow.xaml.cs
git add EngineeringSync.TrayApp/Converters/Converters.cs
git commit -m "feat(trayapp): BACKUP-Sektion in ProjectManagementWindow"
```
---
## Task 12: Gesamtbuild und manuelle Verifikation
- [ ] **Schritt 1: Gesamtlösung bauen**
```bash
dotnet build EngineeringSync.slnx
```
Erwartung: `Build succeeded` für alle 5 Projekte (Domain, Infrastructure, Service, TrayApp, Setup).
- [ ] **Schritt 2: Service starten und Migration prüfen**
```bash
dotnet run --project EngineeringSync.Service
```
Erwartung: Service startet auf `http://localhost:5050`. In der Konsole erscheint die Migration-Meldung. Die SQLite-DB unter `%ProgramData%\EngineeringSync\engineeringsync.db` hat nun die Spalten `BackupEnabled`, `BackupPath`, `MaxBackupsPerFile` in der `Projects`-Tabelle.
Prüfen mit:
```bash
# SQLite-Datei öffnen (DB Browser for SQLite oder sqlite3 CLI)
# SELECT * FROM Projects LIMIT 1;
# Alle 3 neuen Spalten müssen vorhanden sein.
```
- [ ] **Schritt 3: API-Endpunkt testen**
```bash
# Projekt erstellen mit Backup-Feldern
curl -X POST http://localhost:5050/api/projects \
-H "Content-Type: application/json" \
-d "{\"name\":\"Test\",\"engineeringPath\":\"C:\\\\Temp\\\\eng\",\"simulationPath\":\"C:\\\\Temp\\\\sim\",\"fileExtensions\":\".jt\",\"backupEnabled\":true,\"backupPath\":null,\"maxBackupsPerFile\":3}"
# Antwort muss Id, BackupEnabled=true, MaxBackupsPerFile=3 enthalten
```
- [ ] **Schritt 4: TrayApp starten und Backup-Sektion prüfen**
```bash
dotnet run --project EngineeringSync.TrayApp
```
- Projektverwaltung öffnen → Projekt bearbeiten → BACKUP-Sektion erscheint
- Toggle deaktivieren → Optionen verschwinden
- „Eigener Ordner" anklicken → Pfad-Eingabe und Browse erscheinen
- Speichern → API-Aufruf mit Backup-Feldern
- [ ] **Schritt 5: Setup-Wizard starten und Seite prüfen**
```bash
dotnet run --project EngineeringSync.Setup
```
- Durch alle Schritte navigieren → Schritt 4 „Backup" erscheint
- Backup deaktivieren → Optionen ausblenden
- Aktivieren + eigenen Ordner wählen + Max auf 5 setzen
- Zusammenfassung zeigt Backup-Karte korrekt
- [ ] **Schritt 6: Final Commit**
```bash
git add .
git commit -m "feat: Backup-Einstellungen pro Projekt vollständige Implementierung"
```