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