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

34 KiB
Raw Permalink Blame 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

// 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
dotnet build EngineeringSync.Domain/EngineeringSync.Domain.csproj

Erwartung: Build succeeded ohne Fehler.

  • Schritt 3: Commit
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:

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
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
dotnet build EngineeringSync.Infrastructure/EngineeringSync.Infrastructure.csproj

Erwartung: Build succeeded.

  • Schritt 5: Commit
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:

// 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

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
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
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
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:

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:

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:

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
dotnet build EngineeringSync.Service/EngineeringSync.Service.csproj

Erwartung: Build succeeded.

  • Schritt 5: Commit
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:

// 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:

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
dotnet build EngineeringSync.Service/EngineeringSync.Service.csproj

Erwartung: Build succeeded.

  • Schritt 4: Commit
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

// --- 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
dotnet build EngineeringSync.Setup/EngineeringSync.Setup.csproj
  • Schritt 3: Commit
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

<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
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
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
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:

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:

_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:

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:

<!-- 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
dotnet build EngineeringSync.Setup/EngineeringSync.Setup.csproj
  • Schritt 5: Commit
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:

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
dotnet build EngineeringSync.TrayApp/EngineeringSync.TrayApp.csproj
  • Schritt 3: Commit
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:

[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:

EditBackupEnabled       = true;
EditBackupUseCustomPath = false;
EditBackupCustomPath    = string.Empty;
EditMaxBackupsPerFile   = 0;
  • Schritt 3: EditProject() Werte aus ProjectConfig laden

Nach EditIsActive = project.IsActive; einfügen:

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:

// 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
dotnet build EngineeringSync.TrayApp/EngineeringSync.TrayApp.csproj
  • Schritt 6: Commit
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:

<!-- 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:

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:

<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:

<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:

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
dotnet build EngineeringSync.TrayApp/EngineeringSync.TrayApp.csproj

Häufige Fehler: fehlender using System.Globalization; in Converters.cs, fehlende Namespaces in XAML.

  • Schritt 5: Commit
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
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
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:

# 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
# 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
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

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

git add .
git commit -m "feat: Backup-Einstellungen pro Projekt  vollständige Implementierung"