diff --git a/IBKRTrader.slnx b/IBKRTrader.slnx
index 4b57690..3a81a0c 100644
--- a/IBKRTrader.slnx
+++ b/IBKRTrader.slnx
@@ -3,6 +3,7 @@
+
diff --git a/NuGet.config b/NuGet.config
index 2cc6ca5..5ced69e 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -22,6 +22,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/IBKRTrader.App.Avalonia/App.axaml.cs b/src/IBKRTrader.App.Avalonia/App.axaml.cs
new file mode 100644
index 0000000..40ca153
--- /dev/null
+++ b/src/IBKRTrader.App.Avalonia/App.axaml.cs
@@ -0,0 +1,50 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using IBKRTrader.App.Avalonia.Shell;
+using IBKRTrader.App.Avalonia.Views;
+using IBKRTrader.Core.Modularity;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace IBKRTrader.App.Avalonia;
+
+///
+/// Avalonia-Anwendungsobjekt. Verbindet den bereits laufenden Generic Host (Trading-Dienste,
+/// Module, Persistenz) mit der Oberfläche: beim Start wird der Launcher erzeugt und die Views von
+/// Core und Modulen werden bei der Shell registriert.
+///
+public partial class App : Application
+{
+ ///
+ /// Wird von vor StartWithClassicDesktopLifetime gesetzt.
+ /// Bewusst statisch: Avalonia erzeugt die Application-Instanz selbst, ein Konstruktorparameter
+ /// ist deshalb nicht möglich.
+ ///
+ public static IServiceProvider? Services { get; set; }
+
+ public override void Initialize() => AvaloniaXamlLoader.Load(this);
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && Services != null)
+ {
+ var uiHost = Services.GetRequiredService();
+
+ CoreViews.Register(uiHost, Services);
+ foreach (var module in Services.GetServices())
+ module.RegisterUi(uiHost, Services);
+ ModuleViews.Register(uiHost, Services);
+ ViewIcons.AssignDefaults(uiHost);
+
+ var launcher = new LauncherWindow(uiHost, Services);
+ uiHost.SetMainWindow(launcher);
+ desktop.MainWindow = launcher;
+
+ // Das Schließen des Launchers läuft über die Sicherheitsabfrage (wie bisher das X).
+ desktop.ShutdownMode = ShutdownMode.OnMainWindowClose;
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+}
diff --git a/src/IBKRTrader.App.Avalonia/IBKRTrader.App.Avalonia.csproj b/src/IBKRTrader.App.Avalonia/IBKRTrader.App.Avalonia.csproj
new file mode 100644
index 0000000..ec9f17d
--- /dev/null
+++ b/src/IBKRTrader.App.Avalonia/IBKRTrader.App.Avalonia.csproj
@@ -0,0 +1,56 @@
+
+
+
+
+ WinExe
+ net10.0
+ enable
+ enable
+ IBKRTrader.App.Avalonia
+ IBKRTrader.App.Avalonia
+ en
+
+ true
+
+ $(NoWarn);AVLN3001
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/IBKRTrader.App.Avalonia/Program.cs b/src/IBKRTrader.App.Avalonia/Program.cs
new file mode 100644
index 0000000..a6e7407
--- /dev/null
+++ b/src/IBKRTrader.App.Avalonia/Program.cs
@@ -0,0 +1,81 @@
+using Avalonia;
+using IBKRTrader.App.Avalonia.Shell;
+using IBKRTrader.Core.Logging;
+using IBKRTrader.Core.Modularity;
+using IBKRTrader.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+namespace IBKRTrader.App.Avalonia;
+
+///
+/// Einstiegspunkt der plattformneutralen Oberfläche.
+///
+/// Bewusst zweigeteilt: stellt den Host mit Persistenz,
+/// Diensten und Modulen zusammen – ohne jeden Bezug zur Oberfläche. Erst danach wird Avalonia
+/// daran gehängt. Derselbe Host trägt den kopflosen Linux-Dienst
+/// (IBKRTrader.Daemon).
+///
+internal static class Program
+{
+ [STAThread]
+ public static int Main(string[] args)
+ {
+ // Konstruktionsprüfung aller Fenster ohne Message-Loop und ohne laufende Dienste.
+ if (HasFlag(args, "--smoke-ui")) return SmokeUi.Run();
+
+ var modules = AppHostBuilder.CreateModules();
+ using var host = AppHostBuilder.Build(modules, ShellServices.Register);
+
+ AppHostBuilder.RunStartupChecks(host.Services);
+
+ var logger = host.Services.GetRequiredService();
+ logger.Info("Core", "=== IBKRTrader startet ===");
+ logger.Info("Core", $"Version: 1.0.0 | .NET {Environment.Version}");
+
+ host.Start();
+ StartModules(modules, logger);
+
+ try
+ {
+ App.Services = host.Services;
+ BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
+ return 0;
+ }
+ finally
+ {
+ // Geordnetes Herunterfahren – erst nachdem die Oberfläche beendet ist.
+ StopModules(modules, logger);
+ host.StopAsync(TimeSpan.FromSeconds(30)).GetAwaiter().GetResult();
+ }
+ }
+
+ /// Von Avalonia erwartete Fabrikmethode (auch vom XAML-Previewer genutzt).
+ public static AppBuilder BuildAvaloniaApp() =>
+ AppBuilder.Configure()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace();
+
+ private static void StartModules(IReadOnlyList modules, LoggingService logger)
+ {
+ foreach (var module in modules)
+ {
+ try { module.StartAsync(default).GetAwaiter().GetResult(); }
+ catch (Exception ex) { logger.Error(module.Name, $"{module.Name}: Start fehlgeschlagen.", ex); }
+ }
+ logger.Info("Core", "IBKRTrader bereit.");
+ }
+
+ private static void StopModules(IReadOnlyList modules, LoggingService logger)
+ {
+ foreach (var module in modules)
+ {
+ try { module.StopAsync(default).GetAwaiter().GetResult(); }
+ catch (Exception ex) { logger.Warn(module.Name, $"{module.Name}: Stopp fehlgeschlagen: {ex.Message}"); }
+ }
+ }
+
+ private static bool HasFlag(string[] args, string flag) =>
+ args.Any(a => string.Equals(a, flag, StringComparison.OrdinalIgnoreCase));
+}
diff --git a/src/IBKRTrader.App.Avalonia/Shell/AvaloniaUiHost.cs b/src/IBKRTrader.App.Avalonia/Shell/AvaloniaUiHost.cs
new file mode 100644
index 0000000..98eee24
--- /dev/null
+++ b/src/IBKRTrader.App.Avalonia/Shell/AvaloniaUiHost.cs
@@ -0,0 +1,119 @@
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using IBKRTrader.App.Avalonia.Views;
+using IBKRTrader.Core.Modularity;
+
+namespace IBKRTrader.App.Avalonia.Shell;
+
+///
+/// Avalonia-Umsetzung von . Verhält sich wie der frühere
+/// WinForms-ShellUiHost: je View höchstens ein Fenster, ein bereits offenes wird nach vorn
+/// geholt, der Offen-Status wird gemeldet (für die Markierung im Launcher).
+///
+/// Der Core-Contract ist toolkit-neutral – liefert
+/// . Hier wird auf gecastet: ein anderer Typ ist ein
+/// Programmierfehler und soll laut scheitern, nicht still ein leeres Fenster ergeben.
+///
+public sealed class AvaloniaUiHost : IModuleUiHost
+{
+ private readonly List _views = [];
+ private readonly Dictionary _open = new(StringComparer.OrdinalIgnoreCase);
+
+ private Window? _mainWindow;
+ private bool _shutdownDialogOpen;
+
+ /// True, sobald das Herunterfahren über die Sicherheitsabfrage bestätigt wurde.
+ public bool ShutdownConfirmed { get; private set; }
+
+ public event Action? OpenStateChanged;
+
+ public IReadOnlyList Views => _views;
+
+ public void RegisterView(ModuleView view) => _views.Add(view);
+
+ /// Setzt das Hauptfenster (Launcher) – Ziel für .
+ public void SetMainWindow(Window main) => _mainWindow = main;
+
+ public bool IsOpen(string viewId) => _open.ContainsKey(viewId);
+
+ public void ActivateMain()
+ {
+ if (_mainWindow is null) return;
+ if (_mainWindow.WindowState == WindowState.Minimized)
+ _mainWindow.WindowState = WindowState.Normal;
+ _mainWindow.Activate();
+ }
+
+ public void OpenView(string viewId)
+ {
+ var view = _views.FirstOrDefault(v => v.Id == viewId);
+ if (view is not null) OpenView(view);
+ }
+
+ private void OpenView(ModuleView view)
+ {
+ if (_open.TryGetValue(view.Id, out var existing))
+ {
+ if (existing.WindowState == WindowState.Minimized)
+ existing.WindowState = WindowState.Normal;
+ existing.Activate();
+ return;
+ }
+
+ var window = (Window)view.CreateView();
+ if (string.IsNullOrEmpty(window.Title)) window.Title = view.Title;
+ window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
+
+ _open[view.Id] = window;
+ window.Closed += (_, _) =>
+ {
+ _open.Remove(view.Id);
+ OpenStateChanged?.Invoke();
+ };
+
+ window.Show();
+ OpenStateChanged?.Invoke();
+ }
+
+ ///
+ /// Zeigt die Sicherheitsabfrage und fährt bei Bestätigung herunter. Aus jedem Fenster
+ /// aufrufbar – auch aus Modul-Fenstern, die nur den Core-Contract kennen.
+ ///
+ /// async void ist hier korrekt: die Methode ist ein Ereignis-Handler hinter dem
+ /// synchronen Contract , und der Dialog muss
+ /// erwartet werden. Ausnahmen können nicht entweichen – der Dialog wirft nicht, und der
+ /// finally-Block gibt die Sperre in jedem Fall frei.
+ ///
+ public async void RequestShutdown()
+ {
+ if (ShutdownConfirmed || _shutdownDialogOpen) return;
+ _shutdownDialogOpen = true;
+ try
+ {
+ var owner = _mainWindow;
+ if (owner is null) return;
+
+ var confirmed = await new ShutdownConfirmWindow().ShowDialog(owner);
+ if (!confirmed) return;
+
+ ShutdownConfirmed = true;
+ CloseAllViews();
+
+ if (global::Avalonia.Application.Current?.ApplicationLifetime
+ is IClassicDesktopStyleApplicationLifetime desktop)
+ desktop.Shutdown();
+ }
+ finally
+ {
+ _shutdownDialogOpen = false;
+ }
+ }
+
+ /// Schließt alle offenen View-Fenster (beim Herunterfahren).
+ public void CloseAllViews()
+ {
+ foreach (var window in _open.Values.ToList())
+ window.Close();
+ _open.Clear();
+ }
+}
diff --git a/src/IBKRTrader.App.Avalonia/Shell/CoreViews.cs b/src/IBKRTrader.App.Avalonia/Shell/CoreViews.cs
new file mode 100644
index 0000000..65c357c
--- /dev/null
+++ b/src/IBKRTrader.App.Avalonia/Shell/CoreViews.cs
@@ -0,0 +1,47 @@
+using IBKRTrader.App.Avalonia.Views;
+using IBKRTrader.Core.Logging;
+using IBKRTrader.Core.Modularity;
+using IBKRTrader.Core.Settings;
+using IBKRTrader.Core.Trading;
+using IBKRTrader.Core.Workers;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace IBKRTrader.App.Avalonia.Shell;
+
+/// Registriert die Core-Ansichten (Dashboard, Workers, Logs, Settings) bei der Shell.
+public static class CoreViews
+{
+ public static void Register(IModuleUiHost host, IServiceProvider sp)
+ {
+ host.RegisterView(new ModuleView
+ {
+ Id = "core.dashboard", Title = "Dashboard", Group = "Core", Order = 5,
+ CreateView = () => new DashboardWindow(
+ host,
+ sp.GetRequiredService(),
+ sp.GetRequiredService(),
+ sp.GetServices(),
+ sp.GetRequiredService(),
+ sp.GetServices())
+ });
+
+ host.RegisterView(new ModuleView
+ {
+ Id = "core.workers", Title = "Workers / Services", Group = "Core", Order = 10,
+ CreateView = () => new WorkersWindow(host, sp.GetRequiredService())
+ });
+
+ host.RegisterView(new ModuleView
+ {
+ Id = "core.logs", Title = "Logs", Group = "Core", Order = 20,
+ CreateView = () => new LogsWindow(host, sp.GetRequiredService())
+ });
+
+ host.RegisterView(new ModuleView
+ {
+ Id = "core.settings", Title = "Settings", Group = "Core", Order = 30,
+ CreateView = () => new SettingsWindow(host, sp.GetRequiredService())
+ });
+ }
+}
diff --git a/src/IBKRTrader.App.Avalonia/Shell/ModuleViews.cs b/src/IBKRTrader.App.Avalonia/Shell/ModuleViews.cs
new file mode 100644
index 0000000..9c139cf
--- /dev/null
+++ b/src/IBKRTrader.App.Avalonia/Shell/ModuleViews.cs
@@ -0,0 +1,32 @@
+using IBKRTrader.Core.Modularity;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace IBKRTrader.App.Avalonia.Shell;
+
+///
+/// Registriert die Fenster der Module bei der Shell.
+///
+/// Warum hier und nicht im Modul? Ein Modul, das sein eigenes Fenster erzeugt, müsste
+/// Avalonia referenzieren – und wäre damit nicht mehr kopflos auf Linux lauffähig. Die
+/// Modulprojekte bleiben deshalb frei von UI-Code; ihr RegisterUi ist leer, und die Shell
+/// verdrahtet die Fenster zentral. Die Modul-Dienste kommen unverändert aus dem DI-Container.
+///
+/// Stand: noch leer – die drei Modul-Fenster (CongressTrading, Supervisor,
+/// Accounting) werden in der nächsten Etappe portiert. Bis dahin zeigt die Avalonia-Shell nur die
+/// Core-Ansichten; die WinForms-Shell bleibt daneben vollständig nutzbar.
+///
+public static class ModuleViews
+{
+ public static void Register(IModuleUiHost host, IServiceProvider sp)
+ {
+ // Noch keine portierten Modul-Fenster. Das Muster steht in der WinForms-Fassung
+ // (UI/ModuleViews.cs) und wird hier eins zu eins übernommen.
+ }
+
+ /// Registriert die Ansicht nur, wenn das Modul in dieser Sitzung geladen ist.
+ internal static void RegisterIfLoaded(IServiceProvider sp, string moduleName, Action register)
+ {
+ if (sp.GetServices().Any(m => string.Equals(m.Name, moduleName, StringComparison.OrdinalIgnoreCase)))
+ register();
+ }
+}
diff --git a/src/IBKRTrader.App.Avalonia/Shell/ShellServices.cs b/src/IBKRTrader.App.Avalonia/Shell/ShellServices.cs
new file mode 100644
index 0000000..18004da
--- /dev/null
+++ b/src/IBKRTrader.App.Avalonia/Shell/ShellServices.cs
@@ -0,0 +1,18 @@
+using IBKRTrader.Core.Modularity;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace IBKRTrader.App.Avalonia.Shell;
+
+///
+/// Die Dienste, die es nur mit Oberfläche gibt. Alles andere kommt aus
+/// AppHostBuilder und ist mit dem kopflosen Daemon geteilt.
+///
+public static class ShellServices
+{
+ public static void Register(IServiceCollection services, IConfiguration configuration)
+ {
+ services.AddSingleton();
+ services.AddSingleton(sp => sp.GetRequiredService());
+ }
+}
diff --git a/src/IBKRTrader.App.Avalonia/Shell/SmokeUi.cs b/src/IBKRTrader.App.Avalonia/Shell/SmokeUi.cs
new file mode 100644
index 0000000..9144ff1
--- /dev/null
+++ b/src/IBKRTrader.App.Avalonia/Shell/SmokeUi.cs
@@ -0,0 +1,95 @@
+using IBKRTrader.App.Avalonia.ViewModels;
+using IBKRTrader.App.Avalonia.Views;
+using IBKRTrader.Core.Modularity;
+using IBKRTrader.Core.Settings;
+using IBKRTrader.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace IBKRTrader.App.Avalonia.Shell;
+
+///
+/// Konstruktionsprüfung aller Fenster – ohne Message-Loop und ohne laufende Dienste.
+///
+/// Nachfolger des --smoke-ui-Laufs der WinForms-Shell, der Konstruktionsfehler
+/// zuverlässig gefangen hat. Der Host wird bewusst nicht gestartet: sonst liefen Worker,
+/// Broker-Verbindungen und Marktdaten-Abrufe gegen die echten Endpunkte an – für eine reine
+/// Konstruktionsprüfung unerwünscht, auf einem Build-Server schlicht falsch.
+///
+/// Im Gegensatz zu WinForms braucht Avalonia dafür kein Anzeigegerät:
+/// SetupWithoutStarting initialisiert das Framework, ohne ein Fenster zu zeigen. Damit ist
+/// diese Prüfung erstmals CI-tauglich.
+///
+public static class SmokeUi
+{
+ public static int Run()
+ {
+ Program.BuildAvaloniaApp().SetupWithoutStarting();
+
+ var modules = AppHostBuilder.CreateModules();
+ using var host = AppHostBuilder.Build(modules, ShellServices.Register);
+ App.Services = host.Services;
+
+ var uiHost = host.Services.GetRequiredService();
+ CoreViews.Register(uiHost, host.Services);
+ foreach (var module in modules)
+ module.RegisterUi(uiHost, host.Services);
+ ModuleViews.Register(uiHost, host.Services);
+ ViewIcons.AssignDefaults(uiHost);
+
+ var failures = 0;
+ Console.WriteLine("=== Smoke-UI: Fenster-Konstruktion (Avalonia) ===");
+
+ foreach (var view in uiHost.Views)
+ failures += Check(view.Id, view.Title, () => view.CreateView());
+
+ failures += Check("shell.launcher", "Launcher", () => new LauncherWindow(uiHost, host.Services));
+ failures += Check("shell.shutdown", "Beenden-Abfrage", () => new ShutdownConfirmWindow());
+
+ // Die Einstellungsmaske entsteht aus den Attributen von AppSettings. Ein Fenster kann
+ // fehlerfrei konstruieren und trotzdem leer sein, wenn die Attribute verlorengehen –
+ // deshalb hier gegen die tatsächliche Feldzahl prüfen.
+ failures += CheckSettingsForm(host.Services.GetRequiredService());
+
+ Console.WriteLine(failures == 0 ? "=== Smoke-UI OK ===" : $"=== Smoke-UI: {failures} Fehler ===");
+ return failures == 0 ? 0 : 1;
+ }
+
+ private static int Check(string id, string title, Func