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:
EngineeringSync
2026-03-26 21:52:26 +01:00
commit 04ae8a0aae
98 changed files with 8172 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
<Window x:Class="EngineeringSync.TrayApp.Views.PendingChangesWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Ausstehende Änderungen EngineeringSync"
Height="520" Width="860"
WindowStartupLocation="CenterScreen"
Background="#F5F5F5">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Projekt-Auswahl -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,8">
<TextBlock Text="Projekt:" VerticalAlignment="Center" Margin="0,0,8,0" FontWeight="SemiBold"/>
<ComboBox ItemsSource="{Binding Projects}"
SelectedItem="{Binding SelectedProject}"
DisplayMemberPath="Name"
Width="300" />
<Button Content="Aktualisieren" Margin="8,0,0,0" Padding="10,4"
Command="{Binding LoadChangesCommand}" />
</StackPanel>
<!-- Änderungsliste -->
<DataGrid Grid.Row="1"
ItemsSource="{Binding Changes}"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserDeleteRows="False"
SelectionMode="Single"
GridLinesVisibility="Horizontal"
Background="White"
BorderBrush="#DDDDDD"
BorderThickness="1">
<DataGrid.Columns>
<DataGridCheckBoxColumn Binding="{Binding IsSelected, UpdateSourceTrigger=PropertyChanged}"
Header="" Width="40" />
<DataGridTextColumn Binding="{Binding FileName}" Header="Datei" Width="200" IsReadOnly="True"/>
<DataGridTextColumn Binding="{Binding RelativePath}" Header="Pfad" Width="*" IsReadOnly="True"/>
<DataGridTextColumn Binding="{Binding ChangeTypeDisplay}" Header="Änderungstyp" Width="120" IsReadOnly="True"/>
<DataGridTextColumn Binding="{Binding CreatedAtDisplay}" Header="Zeitpunkt" Width="160" IsReadOnly="True"/>
</DataGrid.Columns>
</DataGrid>
<!-- Auswahl-Buttons -->
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,8,0,4">
<Button Content="Alle auswählen" Padding="8,4" Margin="0,0,4,0"
Command="{Binding SelectAllCommand}" />
<Button Content="Keine auswählen" Padding="8,4"
Command="{Binding SelectNoneCommand}" />
<TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center"
Margin="16,0,0,0" Foreground="Gray" />
</StackPanel>
<!-- Aktions-Buttons -->
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,4,0,0">
<Button Content="Ausgewählte ignorieren" Padding="10,6" Margin="0,0,8,0"
Command="{Binding IgnoreSelectedCommand}" />
<Button Content="Ausgewählte synchronisieren" Padding="10,6"
Background="#0078D4" Foreground="White" BorderThickness="0"
Command="{Binding SyncSelectedCommand}" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,13 @@
using System.Windows;
using EngineeringSync.TrayApp.ViewModels;
namespace EngineeringSync.TrayApp.Views;
public partial class PendingChangesWindow : Window
{
public PendingChangesWindow(PendingChangesViewModel vm)
{
InitializeComponent();
DataContext = vm;
}
}

View File

@@ -0,0 +1,215 @@
<Window x:Class="EngineeringSync.TrayApp.Views.ProjectManagementWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:EngineeringSync.TrayApp.Converters"
Title="Projektverwaltung EngineeringSync"
Height="800" Width="800"
WindowStartupLocation="CenterScreen"
Background="#F5F5F5">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BoolToVisConverter"/>
<conv:NewEditHeaderConverter x:Key="NewEditHeaderConverter"/>
<conv:StringToVisibilityConverter x:Key="StringToVisConverter"/>
<conv:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisConverter"/>
</Window.Resources>
<Grid Margin="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="350" />
</Grid.ColumnDefinitions>
<!-- Linke Spalte: Projektliste -->
<Grid Grid.Column="0" Margin="0,0,12,0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="Überwachte Projekte" FontWeight="Bold" FontSize="14" Margin="0,0,0,8"/>
<ListView Grid.Row="1" ItemsSource="{Binding Projects}"
SelectedItem="{Binding SelectedProject}"
Background="White" BorderBrush="#DDDDDD">
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel Margin="4">
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" />
<TextBlock Text="{Binding EngineeringPath}" FontSize="11" Foreground="Gray" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="Aktiv: " FontSize="11" Foreground="Gray"/>
<TextBlock Text="{Binding IsActive}" FontSize="11" Foreground="Gray"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="0,8,0,0">
<Button Content="+ Neues Projekt" Padding="10,6" Margin="0,0,8,0"
Command="{Binding StartNewProjectCommand}" />
<Button Content="Bearbeiten" Padding="10,6" Margin="0,0,8,0"
Command="{Binding EditProjectCommand}"
CommandParameter="{Binding SelectedProject}" />
<Button Content="Löschen" Padding="10,6" Foreground="Red"
Command="{Binding DeleteCommand}"
CommandParameter="{Binding SelectedProject}" />
</StackPanel>
</Grid>
<!-- Rechte Spalte: Editierformular -->
<Border Grid.Column="1"
Background="White"
BorderBrush="#DDDDDD"
BorderThickness="1"
CornerRadius="4"
Padding="16"
Visibility="{Binding IsEditing, Converter={StaticResource BoolToVisConverter}}">
<StackPanel>
<TextBlock Text="{Binding IsNewProject, Converter={StaticResource NewEditHeaderConverter}}"
FontWeight="Bold" FontSize="14" Margin="0,0,0,16"/>
<TextBlock Text="Name:" Margin="0,0,0,4" FontWeight="SemiBold"/>
<TextBox Text="{Binding EditName, UpdateSourceTrigger=PropertyChanged}"
Margin="0,0,0,12" Padding="6"/>
<TextBlock Text="Engineering-Pfad:" Margin="0,0,0,4" FontWeight="SemiBold"/>
<DockPanel Margin="0,0,0,12">
<Button DockPanel.Dock="Right" Content="..." Width="32" Margin="4,0,0,0"
Click="BrowseEngineering_Click"/>
<TextBox Text="{Binding EditEngineeringPath, UpdateSourceTrigger=PropertyChanged}" Padding="6"/>
</DockPanel>
<TextBlock Text="Simulations-Pfad:" Margin="0,0,0,4" FontWeight="SemiBold"/>
<DockPanel Margin="0,0,0,12">
<Button DockPanel.Dock="Right" Content="..." Width="32" Margin="4,0,0,0"
Click="BrowseSimulation_Click"/>
<TextBox Text="{Binding EditSimulationPath, UpdateSourceTrigger=PropertyChanged}" Padding="6"/>
</DockPanel>
<TextBlock Text="Dateiendungen (komma-getrennt):" Margin="0,0,0,4" FontWeight="SemiBold"/>
<TextBox Text="{Binding EditFileExtensions, UpdateSourceTrigger=PropertyChanged}"
Margin="0,0,0,12" Padding="6">
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Text" Value="">
<Setter Property="Background">
<Setter.Value>
<VisualBrush AlignmentX="Left" AlignmentY="Center" Stretch="None">
<VisualBrush.Visual>
<TextBlock Text="z.B. .jt,.cojt (leer = alle Dateien)" Foreground="#999999" Margin="6,0" FontSize="11"/>
</VisualBrush.Visual>
</VisualBrush>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<CheckBox Content="Aktiv (Watcher läuft)"
IsChecked="{Binding EditIsActive}"
Margin="0,0,0,16"/>
<!-- ORDNER-PRÜFUNG -->
<Border Background="#E3F2FD" BorderBrush="#90CAF9" BorderThickness="1"
CornerRadius="4" Padding="12" Margin="0,0,0,16">
<StackPanel>
<TextBlock Text="Unterschiede prüfen" FontWeight="SemiBold" Margin="0,0,0,6"/>
<TextBlock Text="Prüft beide Ordner und zeigt Unterschiede vor dem Speichern."
FontSize="11" Foreground="#555555" Margin="0,0,0,8" TextWrapping="Wrap"/>
<Button Content="Ordner prüfen..." Padding="12,6"
Command="{Binding ScanFoldersCommand}"
HorizontalAlignment="Left"/>
<TextBlock Text="{Binding ScanStatus}" FontSize="11" Foreground="#1976D2"
Margin="0,4,0,0" TextWrapping="Wrap"
Visibility="{Binding ScanStatus, Converter={StaticResource StringToVisConverter}}"/>
</StackPanel>
</Border>
<!-- Scan-Ergebnisse -->
<ItemsControl ItemsSource="{Binding ScanResults}" Margin="0,0,0,16"
Visibility="{Binding HasScanResults, Converter={StaticResource BoolToVisConverter}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="#FFF8E1" BorderBrush="#FFD54F" BorderThickness="1"
CornerRadius="4" Padding="8" Margin="0,4">
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding ChangeType}" FontWeight="SemiBold"
Foreground="#F57C00" Width="70"/>
<TextBlock Text="{Binding RelativePath}" FontWeight="SemiBold"/>
</StackPanel>
<TextBlock FontSize="11" Foreground="#666666">
<Run Text="{Binding Size, StringFormat='{}{0:N0} Bytes'}"/>
<Run Text=" • "/>
<Run Text="{Binding LastModified, StringFormat='yyyy-MM-dd HH:mm'}"/>
</TextBlock>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- BACKUP-Sektion -->
<Separator Margin="0,8,0,12"/>
<TextBlock Text="BACKUP" FontSize="10" FontWeight="Bold" Foreground="#888888"
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>
<TextBlock Text="{Binding StatusMessage}" Foreground="OrangeRed"
TextWrapping="Wrap" Margin="0,0,0,8"
Visibility="{Binding StatusMessage, Converter={StaticResource StringToVisConverter}}"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Abbrechen" Padding="10,6" Margin="0,0,8,0"
Command="{Binding CancelCommand}" />
<Button Content="Speichern" Padding="10,6"
Background="#0078D4" Foreground="White" BorderThickness="0"
Command="{Binding SaveCommand}" />
</StackPanel>
</StackPanel>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,52 @@
using System.Windows;
using EngineeringSync.TrayApp.ViewModels;
using Microsoft.Win32;
namespace EngineeringSync.TrayApp.Views;
public partial class ProjectManagementWindow : Window
{
private readonly ProjectManagementViewModel _vm;
public ProjectManagementWindow(ProjectManagementViewModel vm)
{
InitializeComponent();
_vm = vm;
DataContext = vm;
}
private void BrowseEngineering_Click(object sender, RoutedEventArgs e)
{
var dialog = new OpenFolderDialog
{
Title = "Engineering-Quellpfad wählen",
InitialDirectory = _vm.EditEngineeringPath
};
if (dialog.ShowDialog() == true)
_vm.EditEngineeringPath = dialog.FolderName;
}
private void BrowseSimulation_Click(object sender, RoutedEventArgs e)
{
var dialog = new OpenFolderDialog
{
Title = "Simulations-Zielpfad wählen",
InitialDirectory = _vm.EditSimulationPath
};
if (dialog.ShowDialog() == true)
_vm.EditSimulationPath = dialog.FolderName;
}
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;
}
}