diff --git a/.agents/rules/grundregeln.md b/.agents/rules/grundregeln.md
index 4011fa1..5b59d1c 100644
--- a/.agents/rules/grundregeln.md
+++ b/.agents/rules/grundregeln.md
@@ -17,24 +17,43 @@ Keine gegenseitigen Blockierungen – alles thread-sicher und performant
Technik (fest):
-C# .NET 10 WinForms
+C# .NET 10, **Avalonia** für die Oberfläche – plattformneutral (Windows und Linux).
+ KEIN WinForms und kein System.Drawing: beides bindet an Windows. Alle Projekte sind `net10.0`
+ ohne Plattform-Suffix; ein `net10.0-windows` irgendwo ist ein Fehler.
+ Avalonia bleibt auf der 11er-Linie (11.3.19 / DataGrid 11.3.13), bis LiveCharts2 Avalonia 12
+ unterstützt – sonst brechen die Diagramme der kommenden Module.
MySQL – Zugangsdaten NUR in settings.json (gitignored), NIE im Repo/Code/Doku hinterlegen
IBKR TWS/Gateway API (Paper: Port 4002, Live: Port 4001 – Umschaltung über TradingSettings.Mode)
Interne REST-API + lokaler Webserver (für späteres Web-UI)
Settings: settings.json (Vorlage: settings.example.json)
-Logging: RichTextBox (rtb_logs) + Dateien unter Logs\[Modul]\[Level]-dd-MM-yy.txt (Info/Warn/Error)
+Logging: LoggingService meldet Einträge über das Ereignis `EntryWritten` (die Oberfläche hängt sich
+ ein und färbt selbst) + Dateien unter Logs/[Modul]/[Level]-dd-MM-yy.txt sowie Logs/[Datum].jsonl
+Zeit: Zeitstempel IMMER in UTC persistieren. Für Anzeige, Tagesgrenzen und Zeitpläne
+ `AppTimeZone` verwenden, NIE `DateTime.Now` oder `DateTimeKind.Local` – wir betreiben Instanzen
+ in EU und US, die Ortszeit darf nicht am Rechner hängen.
+Kultur: Jede Zahl-/Datumsformatierung und jedes Parsen braucht einen ausdrücklichen
+ IFormatProvider (i. d. R. InvariantCulture). Ohne ihn hängt das Ergebnis an der Kultur des Hosts.
Tests: eigenes Projekt IBKRTrader.Tests (xUnit + NSubstitute + FluentAssertions), NUR Unit-Tests,
alles Externe (DB/IBKR/Scraper) gemockt. DoD jeder Phase: `dotnet test` grün + Build sauber.
UI-Grundmodell (Launcher-Prinzip nach Polytrader):
-LauncherForm = Basis-Fenster. Enthält: Core-Status/Steuerung ("Trading aktivieren", Paper/Live),
- Workers/Services (dgv_workerlist), Logs (rtb_logs), Settings (PropertyGrid) und eine Modul-Liste.
-Jedes Modul wird als EIGENSTÄNDIGES Fenster aus dem Launcher geöffnet (nicht als Tab).
- Der Launcher trackt offene Fenster (Key → Form) und fokussiert bei erneutem Öffnen.
-dgv_workerlist Spalten: Active | Type | Module | Workername | Last Runtime | Next Runtime | Run Every | Info
+LauncherWindow = Basis-Fenster mit einer Schaltfläche je registrierter Ansicht und dem
+ gemeinsamen Fenster-Menü. Die Inhalte (Dashboard, Workers, Logs, Settings, Modul-Fenster) sind
+ eigenständige Fenster, keine Tabs; je Ansicht höchstens eines, erneutes Öffnen fokussiert.
+Layout deklarativ in .axaml, nicht zur Laufzeit im Code. Kompilierte Bindings sind aktiv, jeder
+ Datenkontext braucht ein x:DataType – dadurch fallen Bindungsfehler beim Kompilieren auf.
+Module tragen KEINEN UI-Code: sonst müssten sie Avalonia referenzieren und wären nicht mehr
+ kopflos lauffähig. Ihr RegisterUi bleibt leer, die Fenster registriert die Shell zentral in
+ src/IBKRTrader.App/Shell/ModuleViews.cs.
+Workers-Ansicht, Spalten: Aktiv | Typ | Modul | Worker | Letzter Lauf | Nächster Lauf | Intervall | Info
Type = "Worker" oder "Service" (Service = permanent laufend)
+Betriebsformen (beide aus derselben Host-Zusammenstellung, src/IBKRTrader.Hosting):
+
+src/IBKRTrader.App – mit Oberfläche; `--smoke-ui` prüft die Fenster-Konstruktion ohne Anzeigegerät
+src/IBKRTrader.Daemon – kopflos für Linux/systemd; `--check` fährt die Startprüfungen ohne Dienste
+
Core-Worker (müssen zuerst):
Backup (alle 30 min)
diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml
index d1a8241..e950f66 100644
--- a/.gitea/workflows/build.yml
+++ b/.gitea/workflows/build.yml
@@ -44,7 +44,7 @@ jobs:
# Die Konstruktionsprüfung der Oberfläche braucht mit Avalonia KEIN Anzeigegerät mehr
# (SetupWithoutStarting). Mit WinForms war das auf einem Build-Server nicht möglich.
- name: Smoke-UI (Fenster-Konstruktion)
- run: dotnet run --project src/IBKRTrader.App.Avalonia --no-build -c Release -- --smoke-ui
+ run: dotnet run --project src/IBKRTrader.App --no-build -c Release -- --smoke-ui
# Trockenlauf des kopflosen Dienstes: Host bauen, Startprüfungen fahren, nichts starten.
- name: Daemon-Prüflauf
@@ -56,7 +56,7 @@ jobs:
if: matrix.os == 'ubuntu-latest'
run: |
dotnet publish src/IBKRTrader.Daemon -c Release -r linux-x64 --self-contained false -o out/daemon
- dotnet publish src/IBKRTrader.App.Avalonia -c Release -r linux-x64 --self-contained false -o out/gui
+ dotnet publish src/IBKRTrader.App -c Release -r linux-x64 --self-contained false -o out/gui
- name: Testergebnisse sichern
if: always()
diff --git a/IBKRTrader.slnx b/IBKRTrader.slnx
index d274988..3ec2cfe 100644
--- a/IBKRTrader.slnx
+++ b/IBKRTrader.slnx
@@ -2,7 +2,7 @@
-
+
diff --git a/README.md b/README.md
index 3f98174..d5a7e92 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ PolytraderSharp (nur IBKR statt Polymarket).
## Architektur (Kurzform)
```
-src/IBKRTrader.App.Avalonia Oberfläche (Avalonia, plattformneutral)
+src/IBKRTrader.App Oberfläche (Avalonia, plattformneutral)
src/IBKRTrader.Daemon kopfloser Dienst (systemd) – dieselbe Anwendung ohne Fenster
src/IBKRTrader.Hosting Host-Zusammenstellung, von beiden Einstiegspunkten geteilt
src/IBKRTrader.Core Contracts, EF-Persistenz, Trading-Kern, Worker, Security
@@ -36,12 +36,12 @@ dotnet test IBKRTrader.slnx
Anwendung starten:
```bash
-dotnet run --project src/IBKRTrader.App.Avalonia
+dotnet run --project src/IBKRTrader.App
```
Prüfläufe – beide ohne Anzeigegerät und ohne laufende Dienste, also CI-tauglich:
```bash
-dotnet run --project src/IBKRTrader.App.Avalonia -- --smoke-ui
+dotnet run --project src/IBKRTrader.App -- --smoke-ui
dotnet run --project src/IBKRTrader.Daemon -- --check
```
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 8565d47..8cabb12 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -14,12 +14,12 @@ nur dass statt Polymarket über IBKR gehandelt wird.
> **Stand seit der Linux-Portierung (2026-08-07):** Alle Projekte sind `net10.0` ohne
> Plattformbindung. Die WinForms-Shell ist entfernt; der letzte Stand liegt im Tag
-> `winforms-final`. Einstiegspunkte sind jetzt `IBKRTrader.App.Avalonia` (mit Oberfläche) und
+> `winforms-final`. Einstiegspunkte sind jetzt `IBKRTrader.App` (mit Oberfläche) und
> `IBKRTrader.Daemon` (kopflos, systemd); beide bauen ihren Host über `IBKRTrader.Hosting`.
> Analyse und Vorgehen: [konzepte/KONZEPT-Linux-Portierung.md](konzepte/KONZEPT-Linux-Portierung.md).
```
-src/IBKRTrader.App.Avalonia (WinExe, net10.0) – Oberfläche: Launcher, Shell, Core-Views, Modul-Fenster
+src/IBKRTrader.App (WinExe, net10.0) – Oberfläche: Launcher, Shell, Core-Views, Modul-Fenster
│ Program.cs: Host bauen (Hosting), Startprüfungen, dann Avalonia; --smoke-ui ohne Anzeigegerät
│ Shell/: AvaloniaUiHost, CoreViews, ModuleViews, ViewIcons, WindowMenu
│ Views/: LauncherWindow, Dashboard/Workers/Logs/Settings, Views/Modules/ (3 Modul-Fenster)
@@ -43,7 +43,7 @@ src/IBKRTrader.Hosting (classlib, net10.0) – AppHostBuilder + RunStartu
│ ├── CongressTradingModule : IModule
│ ├── Persistence/Ef/ eigener DbContext (ct_) + Repos
│ ├── Services/ Scraper + Jobs (IHostedService)
-│ └── Ui/ CongressTradingMainForm (Tabs) via RegisterUi
+│ └── (kein UI-Code – das Modul-Fenster liegt in der Shell, s. App/Shell/ModuleViews.cs)
│
└── tests/IBKRTrader.Tests (xUnit, referenziert Core + Module)
```
@@ -56,9 +56,10 @@ src/IBKRTrader.Hosting (classlib, net10.0) – AppHostBuilder + RunStartu
- **Config**: `appsettings.json` + `appsettings.Local.json` (gitignored, hält Connection-String/Secrets).
- **Modul-Vertrag** `IModule`: `Name`, `DbPrefix`, `RegisterServices(services, config)`,
`RegisterUi(host, sp)`, `StartAsync/StopAsync`, `GetActivationBlocker(config)`.
-- **UI = Shell + Views**: Core und Module registrieren `ModuleView`s beim `IModuleUiHost`.
- Der Launcher öffnet je View ein Fenster (Einzelinstanz, Re-Open fokussiert). Gemeinsames
- „Fenster"-Menü (`WindowMenu`) auf jedem Form. Views sind designbare Forms mit `Initialize(sp)`.
+- **UI = Shell + Views**: Der Core stellt den toolkit-neutralen Contract (`ModuleView`,
+ `IModuleUiHost`), die Shell setzt ihn in Avalonia um. Der Launcher öffnet je View ein Fenster
+ (Einzelinstanz, erneutes Öffnen fokussiert), jedes Fenster trägt das gemeinsame „Fenster"-Menü.
+ Layout deklarativ in `.axaml`; Module tragen keinen UI-Code.
- **Persistenz**: EF Core (Pomelo/MySQL), `AddDbContextFactory`, Repositories hinter Interfaces.
- **Sicherheit**: Master-Key + AES-256-GCM-Verschlüsselung von Credentials at-rest; TLS-Warnung.
- **Headless-Test**: `--smoke-ui` konstruiert jede View + Launcher ohne Message-Loop.
diff --git a/docs/konzepte/KONZEPT-Linux-Portierung.md b/docs/konzepte/KONZEPT-Linux-Portierung.md
index d37ccc8..895e35b 100644
--- a/docs/konzepte/KONZEPT-Linux-Portierung.md
+++ b/docs/konzepte/KONZEPT-Linux-Portierung.md
@@ -1,5 +1,20 @@
# Analyse: Linux-Fähigkeit des IBKRTrader
+> **UMGESETZT am 2026-08-07 (L0–L5).** Dieses Dokument ist die Analyse, die der Portierung
+> vorausging, und bleibt als Begründung erhalten – es beschreibt den Stand **vor** dem Umbau.
+> Was tatsächlich gebaut wurde, steht in der Phasen-Checkliste von
+> [../ARCHITECTURE.md](../ARCHITECTURE.md#l0l5--linux-portierung-avalonia-statt-winforms--2026-08-07);
+> die Pfadangaben im Fundstellenverzeichnis unten beziehen sich auf den alten Aufbau.
+>
+> Zwei Punkte sind gegenüber der Schätzung anders gekommen:
+> * Der Aufwand lag deutlich unter den veranschlagten 21–25 Personentagen, weil PolytraderSharp
+> dieselbe Portierung bereits durchlaufen hatte und als Vorlage diente (Avalonia-Pinnung,
+> toolkit-neutraler Contract, `AppTimeZone`).
+> * `InvariantGlobalization=true` – in der Analyse noch als Empfehlung für ein schlankes Image
+> genannt – wäre ein Fehler gewesen: ohne ICU fällt die Auflösung von Windows-Zeitzonen-IDs aus
+> und die feste `de-DE`-Formatierung des PDF-Exports kippt auf invariant. Beides lautlos.
+> Der Daemon setzt es deshalb ausdrücklich auf `false`.
+
> Stand: 2026-08-06. **Reine Analyse – es wurde kein Code geändert.**
> Grundlage ist der Commit `b96a207` (main): 154 C#-Dateien, ~16.200 LOC, 6 Projekte, 165 Tests.
> Alle Aussagen in Abschnitt 1–9 sind am Quelltext bzw. an einem Probe-Restore verifiziert;
diff --git a/docs/konzepte/KONZEPT-Modul-Accounting.md b/docs/konzepte/KONZEPT-Modul-Accounting.md
index f8cd3e0..148644e 100644
--- a/docs/konzepte/KONZEPT-Modul-Accounting.md
+++ b/docs/konzepte/KONZEPT-Modul-Accounting.md
@@ -56,7 +56,7 @@ Registrierung in `Program.cs`. Referenziert nur den Core. Eigener `AccountingDbC
`PdfExporter` (PDFsharp/MigraDoc, MIT).
- Realisierte GuV nutzt den Core-`RealizedPnlEngine` (FIFO) — kein Duplikat.
-## 5. UI (WinForms, ein Fenster mit Tabs)
+## 5. UI (Avalonia, ein Fenster mit Registerkarten)
Übersicht/BWA (KPI-Kacheln + Monatsvergleich, Zeitraum-/Konto-/Währungswahl), Ledger (filterbar),
Steuer (Platzhalter, s. u.), Abrechnung/Export (CSV/PDF), Abruf/Status (Ingest-Läufe, Soll-Ist, manueller
Trigger). DB-Zugriff nur auf Interaktion (Smoke-UI-sicher).
diff --git a/src/IBKRTrader.App.Avalonia/App.axaml b/src/IBKRTrader.App/App.axaml
similarity index 98%
rename from src/IBKRTrader.App.Avalonia/App.axaml
rename to src/IBKRTrader.App/App.axaml
index 079e7b6..0716de7 100644
--- a/src/IBKRTrader.App.Avalonia/App.axaml
+++ b/src/IBKRTrader.App/App.axaml
@@ -1,6 +1,6 @@
diff --git a/src/IBKRTrader.App.Avalonia/App.axaml.cs b/src/IBKRTrader.App/App.axaml.cs
similarity index 94%
rename from src/IBKRTrader.App.Avalonia/App.axaml.cs
rename to src/IBKRTrader.App/App.axaml.cs
index 40ca153..430e93f 100644
--- a/src/IBKRTrader.App.Avalonia/App.axaml.cs
+++ b/src/IBKRTrader.App/App.axaml.cs
@@ -2,12 +2,12 @@ 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.App.Shell;
+using IBKRTrader.App.Views;
using IBKRTrader.Core.Modularity;
using Microsoft.Extensions.DependencyInjection;
-namespace IBKRTrader.App.Avalonia;
+namespace IBKRTrader.App;
///
/// Avalonia-Anwendungsobjekt. Verbindet den bereits laufenden Generic Host (Trading-Dienste,
diff --git a/src/IBKRTrader.App.Avalonia/IBKRTrader.App.Avalonia.csproj b/src/IBKRTrader.App/IBKRTrader.App.csproj
similarity index 90%
rename from src/IBKRTrader.App.Avalonia/IBKRTrader.App.Avalonia.csproj
rename to src/IBKRTrader.App/IBKRTrader.App.csproj
index fb7e098..420f03d 100644
--- a/src/IBKRTrader.App.Avalonia/IBKRTrader.App.Avalonia.csproj
+++ b/src/IBKRTrader.App/IBKRTrader.App.csproj
@@ -1,15 +1,15 @@
-
+
WinExe
net10.0
enable
enable
- IBKRTrader.App.Avalonia
- IBKRTrader.App.Avalonia
+ IBKRTrader.App
+ IBKRTrader.App
en
+ (ModuleView/IModuleUiHost) ist toolkit-neutral (Func<object> statt eines Fenstertyps,
+ IconKey statt System.Drawing.Image) – hier hängt kein UI-Toolkit und kein
+ System.Drawing.Common, letzteres ist seit .NET 7 Windows-only und wirft auf Linux. -->
net10.0
enable
enable
diff --git a/src/IBKRTrader.Core/Logging/LogEntry.cs b/src/IBKRTrader.Core/Logging/LogEntry.cs
index db75338..fc08e47 100644
--- a/src/IBKRTrader.Core/Logging/LogEntry.cs
+++ b/src/IBKRTrader.Core/Logging/LogEntry.cs
@@ -1,6 +1,6 @@
namespace IBKRTrader.Core.Logging;
-/// Immutable log entry – wird in Datei und RichTextBox geschrieben.
+/// Unveränderlicher Logeintrag – geht in die Logdateien und an alle Senken.
public sealed record LogEntry(
DateTime Timestamp,
AppLogLevel Level,
diff --git a/src/IBKRTrader.Core/Modularity/ModuleView.cs b/src/IBKRTrader.Core/Modularity/ModuleView.cs
index dbba32b..82fb59e 100644
--- a/src/IBKRTrader.Core/Modularity/ModuleView.cs
+++ b/src/IBKRTrader.Core/Modularity/ModuleView.cs
@@ -9,8 +9,7 @@ namespace IBKRTrader.Core.Modularity;
/// Dadurch trägt der Core keine UI-Abhängigkeit und bleibt plattformneutral – Voraussetzung für den
/// kopflosen Linux-Betrieb. Insbesondere hängt hier kein System.Drawing.Image mehr:
/// System.Drawing.Common ist seit .NET 7 Windows-only und wirft auf Linux. Die jeweilige
-/// Shell kennt ihr Toolkit und castet – die WinForms-Shell auf Form, die Avalonia-Shell
-/// auf Window.
+/// Shell kennt ihr Toolkit und castet – die Avalonia-Shell auf Window.
///
public sealed class ModuleView
{
diff --git a/src/IBKRTrader.Core/Workers/IWorker.cs b/src/IBKRTrader.Core/Workers/IWorker.cs
index aa4a27f..86dfc00 100644
--- a/src/IBKRTrader.Core/Workers/IWorker.cs
+++ b/src/IBKRTrader.Core/Workers/IWorker.cs
@@ -16,7 +16,7 @@ public interface IWorker
/// Worker = periodisch / Service = dauerhaft.
WorkerType Type { get; }
- /// Live-Daten für die DataGridView-Zeile.
+ /// Live-Daten für die Zeile in der Workers-Ansicht.
WorkerInfo Info { get; }
/// Löst einen sofortigen, manuellen Run aus (unabhängig vom Zeitplan).
diff --git a/src/IBKRTrader.Core/Workers/WorkerEngine.cs b/src/IBKRTrader.Core/Workers/WorkerEngine.cs
index 50f1d55..7b99e7f 100644
--- a/src/IBKRTrader.Core/Workers/WorkerEngine.cs
+++ b/src/IBKRTrader.Core/Workers/WorkerEngine.cs
@@ -13,7 +13,11 @@ public class WorkerEngine
{
private readonly ConcurrentDictionary _registry = new();
- /// Live-bindbare Liste für die Workers-Ansicht (DataGridView).
+ ///
+ /// Live-bindbare Liste für die Workers-Ansicht. Die Liste selbst ändert sich zur Laufzeit
+ /// nicht – die Worker werden hier einmal registriert; was sich ändert, meldet
+ /// über INotifyPropertyChanged.
+ ///
public BindingList WorkerInfos { get; } = [];
public WorkerEngine(IEnumerable workers, LoggingService logger)
diff --git a/src/IBKRTrader.Core/Workers/WorkerInfo.cs b/src/IBKRTrader.Core/Workers/WorkerInfo.cs
index 1c27966..f601115 100644
--- a/src/IBKRTrader.Core/Workers/WorkerInfo.cs
+++ b/src/IBKRTrader.Core/Workers/WorkerInfo.cs
@@ -4,8 +4,8 @@ using System.Runtime.CompilerServices;
namespace IBKRTrader.Core.Workers;
///
-/// ViewModel-Objekt für eine Zeile in dgv_workerlist.
-/// Implementiert INotifyPropertyChanged für automatisches DataGridView-Binding.
+/// ViewModel-Objekt für eine Zeile der Workers-Ansicht.
+/// Implementiert INotifyPropertyChanged, damit die Oberfläche Änderungen ohne Zutun übernimmt.
///
public class WorkerInfo : INotifyPropertyChanged
{
@@ -32,8 +32,7 @@ public class WorkerInfo : INotifyPropertyChanged
public string RunEvery { get => _runEvery; set => Set(ref _runEvery, value); }
public string Info { get => _info; set => Set(ref _info, value); }
- /// Interner Status – wird nicht direkt als DGV-Spalte verwendet,
- /// aber steuert die Info-Spalte.
+ /// Interner Status – keine eigene Spalte, steuert aber den Info-Text.
public WorkerStatus Status
{
get => _status;