Files
EngineeringSync/docs/superpowers/plans/2026-03-26-backup-settings.md
EngineeringSync 04ae8a0aae Initial commit: EngineeringSync v1.0.0
Vollständige Implementierung des EngineeringSync-Middleware-Tools:
- Windows Service (Kestrel :5050) mit FileSystemWatcher + SignalR
- WPF Tray-App mit PendingChanges- und Projektverwaltungs-Fenster
- Setup-Wizard (8-Schritte-Installer)
- SQLite/EF Core Datenschicht (WAL-Modus)
- SHA-256-basiertes Debouncing (2s Fenster)
- Backup-System mit konfigurierbarer Aufbewahrung

Bugfixes & Verbesserungen:
- BUG-1: AppDbContext OnConfiguring invertierte Bedingung behoben
- BUG-2: Event-Handler-Leak in TrayApp (Fenster-Singleton-Pattern)
- BUG-3: ProjectConfigChanged SignalR-Signal in allen CRUD-Endpoints
- BUG-5: Rename-Sync löscht alte Datei im Simulations-Ordner
- BUG-6: Doppeltes Dispose von SignalR verhindert
- BUG-7: Registry-Deinstallation nur EngineeringSync-Eintrag entfernt
- S1: Path-Traversal-Schutz via SafeCombine() im SyncManager
- E1: FSW Buffer 64KB + automatischer Re-Scan bei Overflow
- E2: Retry-Logik (3x) für gesperrte Dateien mit exponentiellem Backoff
- E4: Channel.Writer.TryComplete() beim Shutdown
- C2: HubMethodNames-Konstanten statt Magic Strings
- E3: Pagination in Changes-API (page/pageSize Query-Parameter)
- A1: Fire-and-Forget mit try/catch + Logging

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-26 21:52:26 +01:00

968 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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"
```