From 27577ef1b2c422077ac48b4043f5949376fd5428 Mon Sep 17 00:00:00 2001 From: Richard Date: Thu, 6 Aug 2026 22:26:49 +0200 Subject: [PATCH] @ Doku: Analyse Linux-Portierung (Avalonia + net10, Aufwand, Fundstellen) Bestandsaufnahme gegen b96a207, kein Code geaendert. Kernbefunde: - Der nicht-UI-Teil ist portabel: kein DllImport, keine Registry, kein WMI, kein DPAPI, keine Windows-Zeitzonen-IDs. WinForms-Kopplung im Core sind genau 3 Dateien, je Modul genau eine Form-Datei. - Daraus die Empfehlung: erst headless (~6,5 PT), dann Avalonia (~12 PT). LiveCharts2 ist Feature-Neubau, kein Migrationsposten (kein Diagramm im Bestand). - Kritisch: ParseExecutionTime verwirft die Zeitzone (Kind=Unspecified) und DailyReportService nutzt DateTimeKind.Local - auf einem UTC-Host liefert derselbe Code lautlos andere Werte. - NuGet.config ist bereits heute kaputt: PDFsharp-MigraDoc, Microsoft.EntityFrameworkCore (Glob matcht nicht ohne Suffix) und Microsoft.CodeAnalysis.CSharp.Workspaces fehlen im packageSourceMapping. Gegen leeren Paket-Ordner verifiziert (NU1100). - Weiter: mysqldump.exe + PATH-Split(';') im BackupWorker, "Segoe UI" und kulturabhaengige Betragsformatierung im PdfExporter, Schreibzugriff neben der Binaerdatei (FHS), Fundstellenverzeichnis mit Datei+Zeile im Anhang. Co-Authored-By: Claude Opus 5 --- docs/konzepte/KONZEPT-Linux-Portierung.md | 689 ++++++++++++++++++++++ 1 file changed, 689 insertions(+) create mode 100644 docs/konzepte/KONZEPT-Linux-Portierung.md diff --git a/docs/konzepte/KONZEPT-Linux-Portierung.md b/docs/konzepte/KONZEPT-Linux-Portierung.md new file mode 100644 index 0000000..d37ccc8 --- /dev/null +++ b/docs/konzepte/KONZEPT-Linux-Portierung.md @@ -0,0 +1,689 @@ +# Analyse: Linux-Fähigkeit des IBKRTrader + +> 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; +> Stellen, die nur plausibel und ungeprüft sind, stehen ausdrücklich als solche gekennzeichnet. + +--- + +## 0. Kurzfassung + +**Die gute Nachricht:** Der portabilitätskritische Teil der Anwendung ist bereits sauber. Kein +einziges `DllImport`, keine Registry, kein WMI, kein DPAPI, kein `TimeZoneInfo.FindSystemTimeZoneById`. +Die Verschlüsselung (`AesGcm`), die Datenbank (Pomelo/EF Core) und der TWS-Adapter laufen ohne +Änderung auf Linux. Die WinForms-Kopplung im **Core** beschränkt sich auf **drei Dateien**, und +jedes Modul trägt **genau eine** Form-Datei. + +**Die eigentliche Arbeit** liegt an zwei Stellen und sie sind unterschiedlich groß: + +| | Umfang | Aufwand | +|---|---|---| +| **A. Headless-Linux** (Worker, Trading, Accounting, Supervisor, REST/MCP – ohne UI) | ~15 Fundstellen, 3 Core-Dateien entkoppeln | **5–7 Personentage** | +| **B. Desktop-Linux** (zusätzlich die komplette UI auf Avalonia) | ~1.300 LOC WinForms neu bauen | **+10–14 Personentage** | +| **C. LiveCharts2** (heute existiert **kein einziges** Diagramm) | Neubau, keine Migration | **+1–3 Personentage** | + +**Empfehlung:** Die beiden Schritte trennen. Eine headless Linux-Variante ist mit ~1 Woche +erreichbar und liefert sofort den größten praktischen Nutzen (Dauerbetrieb auf einem Server statt +auf dem Windows-Desktop). Der Avalonia-Umbau ist danach eine unabhängige Etappe, die man ohne +Zeitdruck und ohne laufenden Betrieb zu gefährden angehen kann. Details in Abschnitt 11. + +**Ein Punkt ist unabhängig von der UI der gefährlichste:** die Zeitzonen-Behandlung (Abschnitt 6). +Der Code mischt heute `DateTime.Now` und `DateTime.UtcNow` und verwirft in `ParseExecutionTime` +bewusst die Zeitzone. Auf einem Windows-Rechner mit `Europe/Berlin` und in einem Linux-Container +mit `UTC` liefert **derselbe Code unterschiedliche Werte** – lautlos, ohne Fehler, in +Buchungszeitstempeln. Das muss vor der Portierung geklärt werden, nicht danach. + +--- + +## 1. Was **nicht** geändert werden muss + +Diese Prüfungen sind negativ ausgefallen – das sind ersparte Personentage: + +| Geprüft | Ergebnis | +|---|---| +| `DllImport` / `LibraryImport` / `Marshal` | **keine Fundstelle** | +| Windows Registry, WMI, EventLog, `WindowsIdentity` | **keine Fundstelle** | +| DPAPI / `ProtectedData` | **keine Fundstelle** – `SecretProtection` nutzt `AesGcm` + `SHA256`, voll portabel | +| `TimeZoneInfo.FindSystemTimeZoneById` (Windows- vs. IANA-IDs) | **keine Fundstelle** | +| Hartkodierte Laufwerksbuchstaben im Produktivpfad | nur in `BackupWorker` (s. 7.1) | +| EF Core / Pomelo / MySqlConnector | voll portabel, Migrationen unberührt | +| WinForms-Designer-Dateien (`*.Designer.cs`, Layout-`.resx`) | **keine** – die gesamte UI ist handgeschriebener Code-Behind. Das erspart die übliche Designer-Konvertierung vollständig. | + +### 1.1 Die TWS-API läuft auf Linux (geprüft) + +Das Paket `IB.TWS.CSharpApi 9.76.1` liefert `lib/net45/CSharpAPI.dll` – deshalb steht heute +`NoWarn="NU1701"` im Core-csproj. Die Assembly-Referenzen wurden ausgelesen: + +``` +mscorlib 4.0.0.0 +System 4.0.0.0 +System.Core 4.0.0.0 +``` + +Nur diese drei, alle auf .NET 10 vollständig typweitergeleitet. Es gibt keine Abhängigkeit auf +`System.Configuration`, `System.Web` oder sonst etwas Windows-Gebundenes. Die DLL wird auf Linux +laufen. **Verbleibendes Restrisiko: gering, aber ungeprüft** – ein Verbindungs-Smoke-Test gegen +das Gateway von einem Linux-Host aus gehört in die erste Etappe. + +> **Sauberere Alternative:** IBKR liefert im offiziellen TWS-API-Download den C#-Quelltext mit. +> Den als eigenes `netstandard2.0`-Projekt in `src/` aufzunehmen, ersetzt das NuGet-Mirror-Paket, +> beseitigt `NU1701` und macht die Herkunft nachvollziehbar. Halber Tag, optional. + +--- + +## 2. Sofort-Blocker: der Restore schlägt heute auf **jedem** frischen Rechner fehl + +Das ist keine Linux-Besonderheit, aber es ist das Erste, worüber man auf einer neuen Maschine +stolpert – und deshalb Teil dieser Analyse. `NuGet.config` nutzt `packageSourceMapping` als +Allowlist mit ``. Drei Pakete haben kein passendes Muster. Verifiziert mit einem Restore +gegen einen leeren Paket-Ordner: + +``` +error NU1100: "PDFsharp-MigraDoc (>= 6.2.4)" kann für "net10.0-windows" nicht aufgelöst werden. +error NU1100: "Microsoft.EntityFrameworkCore (>= 8.0.13)" kann für "net10.0-windows" nicht aufgelöst werden. +error NU1100: "Microsoft.CodeAnalysis.CSharp.Workspaces (>= 4.5.0)" kann für "net10.0-windows" nicht aufgelöst werden. +``` + +Auf dem Entwicklungsrechner fällt das nicht auf, weil alle drei längst im globalen Paket-Cache +liegen. Die Ursachen: + +- `PDFsharp-MigraDoc` – gar kein Muster vorhanden. +- `Microsoft.EntityFrameworkCore` – das Muster lautet `Microsoft.EntityFrameworkCore.*`; der Glob + matcht das Paket **ohne** Suffix nicht. +- `Microsoft.CodeAnalysis.CSharp.Workspaces` – transitiv über `EntityFrameworkCore.Design`, kein Muster. + +**Aufwand: 10 Minuten.** Muss vor allem anderen erledigt sein, sonst startet der erste +Linux-Build nicht. + +--- + +## 3. Block A – Projekt- und Build-Ebene + +**Alle sechs Projekte** stehen heute auf `net10.0-windows` mit `UseWindowsForms=true` – auch der +Core, alle drei Module und das Testprojekt. + +| Projekt | heute | Ziel | +|---|---|---| +| `IBKRTrader.Core` | `net10.0-windows`, WinForms | `net10.0`, **keine** UI-Abhängigkeit | +| `Modules.CongressTrading` | `net10.0-windows`, WinForms | `net10.0` | +| `Modules.Accounting` | `net10.0-windows`, WinForms | `net10.0` | +| `Modules.Supervisor` | `net10.0-windows`, WinForms | `net10.0` | +| `IBKRTrader.Tests` | `net10.0-windows`, WinForms | `net10.0` | +| `IBKRTrader.App` | `WinExe`, `net10.0-windows` | neu: `IBKRTrader.Desktop` (Avalonia) + `IBKRTrader.Daemon` (Konsole) | + +Zwei Nebenwirkungen, die man kennen muss: + +1. **`ImplicitUsings` + `UseWindowsForms` fügt `System.Windows.Forms` und `System.Drawing` als + globale Usings hinzu.** Sobald das wegfällt, brechen Dateien, die unbemerkt `Point`, `Size`, + `Color`, `Font` oder `Padding` benutzt haben. Das ist *nützlich* – der Compiler findet die + Arbeit für uns – aber es erklärt, warum die erste Umstellung mehr Fehler wirft als die 3 + bekannten Core-Dateien vermuten lassen. +2. `ApplicationHighDpiMode`, `Properties/Resources.resx` (24 `System.Drawing.Bitmap`-Icons) und + `ApplicationConfiguration.Initialize()` verschwinden mit dem App-Projekt. Die 24 PNGs unter + `Resources/` bleiben brauchbar, werden aber in Avalonia über `AvaloniaResource` + `Bitmap` + eingebunden statt über den `ResourceManager`. + +**Aufwand Block A: 0,5 Tage.** + +--- + +## 4. Block B – den Core von WinForms lösen + +Das ist der architektonisch wichtigste Schritt und erfreulich klein. **Genau drei Dateien** im +Core kennen WinForms: + +### 4.1 `Core/Logging/LoggingService.cs` (139 LOC, davon ~40 betroffen) + +Hält direkt ein `RichTextBox?`, benutzt `System.Drawing.Color` und marshallt selbst per +`InvokeRequired`/`BeginInvoke`: + +```csharp +private RichTextBox? _rtb; +public void AttachRichTextBox(RichTextBox rtb) => _rtb = rtb; +private static readonly Color ColorInfo = Color.FromArgb(150, 210, 150); +``` + +**Umbau:** `AttachRichTextBox` durch ein `event Action? EntryWritten` bzw. ein +`ILogSink`-Interface ersetzen. Das Einfärben und das Thread-Marshalling wandern in die UI-Schicht +(Avalonia: `Dispatcher.UIThread.Post`). Der `LogEntry`-Record ist bereits sauber und braucht +keine Änderung. + +Nebenbei zu bereinigen: `e.Level.ToString().ToUpper()` in Zeile 115 ist kulturabhängig +(s. Abschnitt 5). + +### 4.2 `Core/Modularity/ModuleView.cs` (57 LOC) + +```csharp +public System.Drawing.Image? Icon { get; set; } +public Func
CreateForm { get; init; } = () => new Form(); +``` + +**Umbau:** Der Contract muss toolkit-neutral werden. Zwei Wege: + +- **Pragmatisch:** `Func CreateWindow` + `string IconKey` (Ressourcenname statt Bitmap). + Der Core kennt dann keine UI-Typen mehr, die Shell castet. +- **Sauber:** ein `IModuleWindow`-Marker-Interface, das die Desktop-Schicht auf `Window` abbildet. + +Das `IModuleUiHost`-Interface selbst (`RegisterView`, `IsOpen`, `OpenView`, `ActivateMain`, +`RequestShutdown`, `OpenStateChanged`) ist **bereits toolkit-neutral** und kann unverändert bleiben. + +### 4.3 `Core/Modularity/WindowMenu.cs` (79 LOC) + +Vollständig WinForms (`MenuStrip`, `ToolStripMenuItem`, `Font`, `FontStyle`). Wird ersatzlos +gelöscht und in der Avalonia-Schicht neu gebaut. Die *Logik* dahinter (Launcher + alle Views + +kontextabhängige rechte Aktion) ist trivial und in ~60 Zeilen XAML/C# nachgebaut. + +### 4.4 Die Module + +**Je Modul genau eine betroffene Datei:** + +- `Modules.Accounting/Ui/AccountingMainForm.cs` (302 LOC) +- `Modules.CongressTrading/UI/CongressTradingForm.cs` (129 LOC) +- `Modules.Supervisor/Ui/SupervisorMainForm.cs` (188 LOC) + +`Modules.Accounting/Logic/PdfExporter.cs` sieht in einer naiven Suche nach WinForms aus, ist es +aber nicht – `Font` und `Colors` stammen dort aus `MigraDoc.DocumentObjectModel`. (Der PdfExporter +hat ein *anderes* Linux-Problem, s. 7.2.) + +**Das heißt:** Zieht man diese drei Dateien heraus, sind Core und alle Module sofort headless-fähig. +Genau darauf baut die Empfehlung in Abschnitt 11 auf. + +**Aufwand Block B: 1 Tag.** + +--- + +## 5. Block C – Kultur und Stringformatierung + +Der Bestand ist überwiegend gut: `StringComparison.Ordinal`/`OrdinalIgnoreCase` wird konsequent +benutzt, der `IbkrMapping` und der `CsvExporter` arbeiten korrekt mit `CultureInfo.InvariantCulture`. +Es gibt aber fünf konkrete Fundstellen. + +### 5.1 `PdfExporter` formatiert Geldbeträge kulturabhängig — **relevant, weil Finanzdokument** + +`src/IBKRTrader.Modules.Accounting/Logic/PdfExporter.cs:25` + +```csharp +string M(decimal baseAmount) => V(baseAmount).ToString("N2") + " " + currencyCode; +``` + +Ebenso Zeile 86: `e.Quantity.ToString("0.###")`, `e.PriceNative.ToString("0.###")`. + +Alle ohne `IFormatProvider`, also **CurrentCulture**. Auf dem heutigen deutschen Windows kommt +`1.234,56` heraus. In einem Linux-Container mit `LANG=C` oder mit +`InvariantGlobalization=true` wird daraus `1,234.56` – dieselbe Zahl, andere Bedeutung für einen +Leser, und der PDF-Export ist ausdrücklich als *prüfbare Aufstellung* gedacht. + +Auffällig: der `CsvExporter` im selben Modul macht es richtig (`CultureInfo.InvariantCulture`). +Der PDF-Export sollte bewusst festgelegt werden – entweder fest `de-DE` (Leserfreundlichkeit) oder +fest invariant (Maschinenlesbarkeit), aber nicht "was der Host gerade meint". + +### 5.2 `CapitolTradesScraper.ParseDate` parst kulturabhängig + +`src/IBKRTrader.Modules.CongressTrading/Scraper/CapitolTradesScraper.cs:333` + +```csharp +return DateOnly.TryParse(datePart, out var d) ? d : null; +``` + +Quelle ist capitoltrades.com mit ISO-Datum, das unter den meisten Kulturen durchgeht – aber +`TryParse` ohne `IFormatProvider` ist bei einem externen, unkontrollierten Eingabeformat die +falsche Wahl. `TryParseExact("yyyy-MM-dd", InvariantCulture)` ist hier auch fachlich richtiger: +ein Formatwechsel bei der Quelle soll *auffallen*, nicht stillschweigend zu einem falschen Datum +werden. Gleiches gilt für `int.Parse(match.Groups[1].Value)` in Zeile 124. + +### 5.3 Kulturabhängiges `ToUpper()` / `ToLower()` + +- `Core/Logging/LoggingService.cs:115` – `e.Level.ToString().ToUpper()` +- `Core/IBKR/IBKRGatewayService.cs:247` – `outsideRth.ToString().ToLower()`, fließt in eine URL + +Beide sind das klassische Türkisch-I-Problem und beide mit `…Invariant()` in einer Minute erledigt. +Der zweite ist der unangenehmere, weil er in einen HTTP-Query-String geht. + +### 5.4 Die Grundsatzentscheidung: ICU oder Invariant? + +Auf Linux kommt die Kulturdatenbank aus **ICU** (`libicu`). Das muss entschieden und im csproj +festgeschrieben werden: + +- **Mit ICU** (`libicu` im Image installieren): Kulturen verhalten sich weitgehend wie auf + Windows ab .NET 5, das ebenfalls ICU benutzt. Größeres Image. +- **`InvariantGlobalization=true`**: schlankes Image, keine ICU-Abhängigkeit – aber *jede* + kulturabhängige Formatierung wird lautlos invariant. Genau dann schlagen 5.1 und 5.2 durch. + +Solange 5.1/5.2 nicht behoben sind, ist die Wahl sicherheitsrelevant. Danach ist sie beliebig. +Empfehlung: Fundstellen explizit machen, dann `InvariantGlobalization=true` (schlank und +deterministisch). + +**Aufwand Block C: 0,5 Tage.** + +--- + +## 6. Block D – Zeit und Zeitzonen ⚠ **der kritischste Punkt** + +Hier liegt das einzige Risiko, das *stillschweigend falsche Daten* erzeugt statt einen Fehler. + +### 6.1 Die Ausgangslage + +Der Code mischt drei Konventionen: + +| Konvention | Fundstellen (Auswahl) | +|---|---| +| `DateTime.UtcNow` – korrekt für Persistenz | ~25 Stellen: alle EF-Entitäten, `ExecutionService`, `PortfolioService`, `TradeHistoryService`, `AccountingIngestService`, `BudgetService` | +| `DateTime.Now` – Ortszeit des Hosts | `LoggingService:51`, `BackupWorker:34`, `WorkerBase:114/118/146`, `DailyReportService:44`, drei UI-Statuszeilen, `PdfExporter:51` | +| `Kind = Unspecified` – weder noch | `IbkrMapping.ParseExecutionTime` | + +### 6.2 `ParseExecutionTime` verwirft die Zeitzone – bewusst, aber jetzt folgenreich + +`src/IBKRTrader.Core/Trading/Ibkr/IbkrMapping.cs:99` (aus R10, gerade committet): + +```csharp +/// Die Zeitzone wird verworfen – der Wert bleibt Ortszeit der Börse, wie ihn TWS meldet. +``` + +TWS liefert je nach Aufruf `20260804 17:52:56` oder `20260804 17:52:56 Europe/Berlin`. Der +Suffix wird abgeschnitten, es entsteht ein `DateTime` mit `Kind = Unspecified`. Dieser Wert +landet in `BrokerExecution.Time` und von dort perspektivisch in der Buchführung – wo er neben +`DateTime.UtcNow`-Feldern liegt. + +Solange alles auf **einem** Windows-Rechner mit `Europe/Berlin` läuft, ist das konsistent genug, +um nicht aufzufallen. Auf einem Linux-Container mit `TZ=UTC` bedeutet derselbe abgeschnittene +Zeitstempel plötzlich etwas anderes als vorher – **ohne dass sich eine Zeile Code ändert.** Es +gibt keine Exception, keinen Log-Eintrag, nur um 1–2 Stunden verschobene Ausführungszeiten. + +**Das ist zu klären, bevor die erste Zeile portiert wird.** Der saubere Weg: die von TWS +gemeldete Zeitzone *nicht* verwerfen, sondern über `TimeZoneInfo` (IANA-IDs, die TWS liefert +bereits `Europe/Berlin`-Format) nach UTC konvertieren und als `DateTimeOffset` führen. Das ist +auch unabhängig von Linux die bessere Lösung, weil Ausführungen an US-Börsen sonst +Berliner Ortszeit tragen. + +> **Nebenbefund:** IANA-IDs (`Europe/Berlin`) funktionieren mit `TimeZoneInfo` auf .NET 6+ auch +> auf Windows. Es braucht also keine ID-Übersetzung – ein Problem, das man bei solchen +> Portierungen sonst regelmäßig hat, entfällt hier. + +### 6.3 `DailyReportService` feuert zur falschen Uhrzeit + +`src/IBKRTrader.Modules.Supervisor/Services/DailyReportService.cs:56` + +```csharp +var candidate = new DateTime(now.Year, now.Month, now.Day, hour, 0, 0, DateTimeKind.Local); +``` + +`IBKRTRADER_SUPERVISOR_DAILY=18` heißt heute "18 Uhr deutscher Zeit". Auf einem UTC-Container +heißt es "20 Uhr deutscher Zeit" (Sommerzeit). Fachlich meint man aber eine Börsen- bzw. +Ortszeit. Lösung: eine explizite Report-Zeitzone konfigurierbar machen statt `Local` zu benutzen. +Der Test `NextRun` existiert bereits und lässt sich mitziehen. + +### 6.4 Logdatei- und Backup-Namen + +`LoggingService` benennt Dateien nach `DateTime.Now` (`{Level}-dd-MM-yy.txt` bzw. +`{yyyy-MM-dd}.jsonl`), `BackupWorker` nach `DateTime.Now` (`yyyy-MM-dd_HH-mm`). Beim Umzug auf +UTC entsteht ein einmaliger Bruch in der Dateibenennung: der Tageswechsel liegt woanders, es +kann für einen Tag zwei Teil-Dateien geben. Nicht kritisch, aber der `SupervisorTools`-Zugriff +`Logs/{date}.jsonl` und `DossierService` lesen genau diese Namen – man sollte es wissen und +bewusst umstellen, statt es zu entdecken. + +### 6.5 Datenbank + +MariaDB speichert `DATETIME` ohne Offset. Solange die App UTC schreibt und UTC liest, ist der +Server-`time_zone` egal. Wandert die DB später auch, ist das der Punkt, an dem man `SET time_zone` +prüfen muss. **Ungeprüft** – für den reinen App-Umzug nicht relevant. + +**Aufwand Block D: 1–2 Tage**, davon der größere Teil Audit und Tests, nicht Code. + +--- + +## 7. Block E – Dateisystem, Pfade, Prozesse + +### 7.1 `BackupWorker` ist der einzige echt Windows-gebundene Codeteil + +`src/IBKRTrader.Core/Workers/BuiltIn/BackupWorker.cs:95-118` + +```csharp +var candidates = new[] { + "mysqldump.exe", + @"C:\Program Files\MySQL\MySQL Server 8.0\bin\mysqldump.exe", + @"C:\Program Files\MySQL\MySQL Server 8.4\bin\mysqldump.exe", + @"C:\xampp\mysql\bin\mysqldump.exe" +}; +foreach (var dir in pathVar.Split(';')) + var full = Path.Combine(dir.Trim(), "mysqldump.exe"); +``` + +Drei Fehler auf einmal: `.exe`-Endung, Windows-Installationspfade, und `PATH` mit `;` getrennt – +Linux nutzt `:`. Letzteres ist `Path.PathSeparator`. Lösung: Kandidatenliste und Endung über +`OperatingSystem.IsWindows()` verzweigen, `mariadb-dump` als Kandidat aufnehmen (heißt auf +aktuellen MariaDB-Versionen so), Trennzeichen aus `Path.PathSeparator`. + +> **Zusätzlicher Sicherheitsbefund, der erst auf Linux entsteht:** Zeile 66 übergibt das +> DB-Passwort als Kommandozeilenargument (`--password={db.Password}`). Unter Linux ist +> `/proc//cmdline` für **jeden lokalen Nutzer lesbar** – das Passwort steht damit für die +> Dauer des Dumps offen im Prozessbaum. Auf Windows ist das weniger exponiert. Beim Umzug also +> gleich auf `MYSQL_PWD` (Umgebungsvariable) oder eine temporäre Options-Datei mit `chmod 600` +> umstellen. Halber Tag, und unabhängig von Linux ohnehin die bessere Lösung. + +### 7.2 PDF-Export findet auf Linux keine Schriftart + +`src/IBKRTrader.Modules.Accounting/Logic/PdfExporter.cs:30` + +```csharp +style.Font.Name = "Segoe UI"; +``` + +"Segoe UI" ist eine Windows-Schrift und auf Linux nicht vorhanden. PDFsharp 6.x löst Schriften +auf Nicht-Windows-Plattformen nicht automatisch auf – es braucht einen eigenen +`GlobalFontSettings.FontResolver` (`IFontResolver`), der die Schriftdatei liefert. Ohne ihn +scheitert der Export zur Laufzeit. + +Lösung: eine freie Schrift (z. B. DejaVu Sans oder Inter) als `EmbeddedResource` ins +Accounting-Modul legen und einen kleinen `IFontResolver` implementieren. Das macht den PDF-Export +gleichzeitig **plattformunabhängig reproduzierbar** – dasselbe Dokument sieht auf jedem Host +identisch aus, was für ein prüfbares Finanzdokument ein Gewinn ist. Rund ein halber Tag. + +### 7.3 Schreibzugriff neben der Binärdatei + +`Logs/`, `Backups/`, `settings.json`, `master.key` und `openrouter.key` liegen alle unter +`AppDomain.CurrentDomain.BaseDirectory` bzw. `AppContext.BaseDirectory`. Auf Windows neben der +`.exe` üblich. Auf Linux liegt eine Anwendung typischerweise unter `/opt/…` oder `/usr/local/…` +und der Dienstbenutzer hat dort **keinen Schreibzugriff**. Erwartet werden `/var/log/ibkrtrader`, +`/var/lib/ibkrtrader`, `/etc/ibkrtrader`. + +Das ist kein Einzeiler, sondern eine kleine Entwurfsentscheidung: die vier Pfade sollten aus einer +zentralen `IAppPaths`-Abstraktion kommen, die unter Windows das heutige Verhalten beibehält und +unter Linux den FHS-Konventionen folgt (oder per Umgebungsvariable überschreibbar ist). +Betroffen sind `LoggingService:18`, `SettingsService:13`, `BackupWorker:35/126`, +`Program.cs:227`, `OpenRouterClient:39`, `SupervisorTools:30`, `DossierService:29`. + +Dazu: `master.key` und `openrouter.key` brauchen auf Linux `chmod 600`. Ein Startup-Check, der +zu weite Rechte meldet, wäre angemessen – Windows-ACLs übertragen sich nicht. + +### 7.4 Groß-/Kleinschreibung + +Linux-Dateisysteme sind case-sensitiv. Zwei Stellen sind zu beachten: + +- Der Ordner heißt bei CongressTrading `UI/`, bei Accounting und Supervisor `Ui/`. MSBuild-Globbing + stört das nicht, aber es ist eine Inkonsistenz, die man bei der Gelegenheit begradigen sollte. +- `LoggingService:63` baut Log-Verzeichnisse aus dem `Module`-String: `Logs/Core`, `Logs/IBKR`, + `Logs/CT`, `Logs/Supervisor`, `Logs/Accounting`, `Logs/AI`. Diese Strings sind über den Code + verstreut (~90 Aufrufstellen). Auf Windows wären `Logs/CT` und `Logs/ct` dasselbe Verzeichnis, + auf Linux zwei. Die Schreibweisen sind heute konsistent – aber es ist eine Fußangel, die eine + Konstantenklasse (`LogModules.Core` etc.) endgültig entschärfen würde. + +### 7.5 Kleinigkeiten + +- `LoggingService:69/72` schreibt hartkodiert `"\r\n"` in die `.txt`-Logs. Auf Linux kosmetisch + störend; `Environment.NewLine` wäre richtig. (Das JSONL nutzt korrekt `"\n"`.) +- `.gitattributes` ist vorhanden und korrekt konfiguriert (`* text=auto`, Binärdateien ausgenommen). + **Kein Handlungsbedarf** – gemischte Zeilenenden werden beim Arbeiten von Linux aus nicht churnen. +- `scripts/provision-db.ps1` ist PowerShell. `pwsh` gibt es auf Linux, aber ein `.sh`-Pendant wäre + freundlicher. Optional, 1 Stunde. +- Der Code mischt `AppDomain.CurrentDomain.BaseDirectory` (ältere Dateien) und + `AppContext.BaseDirectory` (neuere). Identischer Wert, rein kosmetisch – erledigt sich mit 7.3. + +**Aufwand Block E: 1–1,5 Tage.** + +--- + +## 8. Block F – Netzwerk und Dienste + +Drei `HttpListener`-basierte Dienste: + +| Datei | Prefix | +|---|---| +| `Core/Workers/BuiltIn/WebserverService.cs:38` | `http://localhost:{port}/` | +| `Core/Workers/BuiltIn/WebApiService.cs:46` | `http://localhost:{port}/api/` | +| `Modules.Supervisor/Mcp/McpLightServer.cs:44` | `http://127.0.0.1:{port}/mcp/` | + +`HttpListener` **funktioniert auf Linux** (dort als verwaltete Socket-Implementierung statt über +`http.sys`). Zu beachten: + +- Ports unter 1024 brauchen root. Genutzt werden 5001 und ein per Env gesetzter MCP-Port, der + bereits auf `1024–65535` geprüft wird – **passt**. +- Kein HTTPS ohne Zusatzarbeit. Alle drei binden auf localhost, also derzeit kein Thema. +- Auf Linux entfällt die `netsh urlacl`-Registrierung – eine Erleichterung, kein Problem. + +`IBKRGatewayService:39` setzt `ServerCertificateCustomValidationCallback = (_,_,_,_) => true`, akzeptiert +also jedes Zertifikat. Das ist für den lokalen Client-Portal-Gateway mit Selbstsignat gedacht und +funktioniert auf Linux identisch. Es ist unabhängig von dieser Portierung eine Stelle, die man +irgendwann auf Pinning des Gateway-Zertifikats einengen sollte – hier nur der Vollständigkeit halber. + +**Aufwand Block F: 0 Tage** (nur Verifikation). Langfristig wäre ein Umstieg von `HttpListener` +auf Kestrel/Minimal-API sauberer – `HttpListener` gilt als Altlast –, das ist aber **nicht** +Voraussetzung für Linux. + +--- + +## 9. Block G – Die UI: Avalonia + LiveCharts2 + +### 9.1 Umfang + +| Datei | LOC | +|---|---| +| `LauncherForm.cs` | 162 | +| `UI/ShellUiHost.cs` | 107 | +| `UI/WorkerListBindingSource.cs` | 77 | +| `UI/LogPanelController.cs` | 33 | +| `UI/Views/DashboardView.cs` | 98 | +| `UI/Views/{Logs,Settings,Workers}View.cs` | 64 | +| `Core/Modularity/WindowMenu.cs` + `ModuleView.cs` | 136 | +| `Modules.Accounting/Ui/AccountingMainForm.cs` | 302 | +| `Modules.Supervisor/Ui/SupervisorMainForm.cs` | 188 | +| `Modules.CongressTrading/UI/CongressTradingForm.cs` | 129 | +| **Summe** | **~1.300** | + +Erfahrungsgemäß wächst das bei einer Umsetzung mit XAML + ViewModels auf 2.000–2.500 LOC, weil +MVVM Struktur kostet, die im Code-Behind heute implizit ist. + +**Kein einziges Diagramm im Bestand.** Die Suche nach `Chart`/`Series`/`Plot` liefert null Treffer. +LiveCharts2 ist damit **kein Migrationsposten, sondern ein Feature-Neubau** – und sollte auch so +geplant und geschätzt werden, getrennt vom Rest. + +### 9.2 Was gut übertragbar ist + +Der Shell-Entwurf passt bemerkenswert gut auf Avalonia: + +- `ShellUiHost` (Dictionary offener Fenster, eine Instanz je View, `OpenStateChanged`-Event) ist + **fast vollständig toolkit-neutral**. Nur der Typ `Form` und `MessageBox.Show` müssen getauscht + werden. Die 107 LOC überleben zu ~80 %. +- Mehrere gleichrangige Top-Level-Fenster + Launcher entsprechen exakt Avalonias + `IClassicDesktopStyleApplicationLifetime` mit mehreren `Window`-Instanzen. +- `WorkerInfo` implementiert bereits `INotifyPropertyChanged` – das ist genau das, was Avalonias + Binding erwartet. `WorkerListBindingSource` (`BindingList`) wird zu `ObservableCollection`. +- Das UI-Thread-Marshalling (`InvokeRequired`/`BeginInvoke`, 6 Stellen) wird zu + `Dispatcher.UIThread.Post` – ein mechanischer 1:1-Ersatz. + +### 9.3 Was echte Arbeit ist + +| WinForms | Avalonia | Anmerkung | +|---|---|---| +| `DataGridView` (4×) | `DataGrid` | **eigenes Paket** `Avalonia.Controls.DataGrid` + Theme-Include in `App.axaml`. `AccountingMainForm:187` bindet heute anonyme Typen – die müssen zu echten Record-ViewModels werden. | +| `MessageBox.Show` (`ShellUiHost:46`) | – | Avalonia hat **keine** eingebaute MessageBox. Eigener Dialog oder Zusatzpaket. | +| `SaveFileDialog` (2×) | `IStorageProvider.SaveFilePickerAsync` | asynchron, anderer API-Zuschnitt | +| `RichTextBox` mit `SelectionColor` | `ItemsControl`/`SelectableTextBlock` | Das farbige Log-Panel muss anders gebaut werden (eingefärbte Items statt Selection-Färbung) – im Ergebnis sauberer. | +| `ToolStrip`/`MenuStrip`/`StatusStrip` | `Menu` + Panel-Layout | Direkte Entsprechungen fehlen; wird handgebaut. | +| `DateTimePicker`, `ComboBox` | `DatePicker`, `ComboBox` | unkritisch | +| `SystemColors.GrayText` etc. | Theme-Ressourcen | Fluent-Theme, gleichzeitig Hell/Dunkel möglich | +| `Dock`/`Anchor`, absolute `Point`-Positionen | Grid/DockPanel/StackPanel | Layout muss neu gedacht, nicht übersetzt werden | + +Die Steuerelement-Inventur über alle UI-Dateien: 12 `Label`, 10 `Button`, 9 `Panel`, +4 `FlowLayoutPanel`, 4 `DataGridView`, 2 `ToolStrip`, 2 `TabControl`, 2 `SaveFileDialog`, +2 `MenuStrip`, 1 `StatusStrip`, 1 `RichTextBox`, 1 `BindingSource`. Überschaubar – es gibt keine +exotischen Controls und kein Custom-Drawing. + +### 9.4 Lizenz und Laufzeitabhängigkeiten + +- **Avalonia ist MIT-lizenziert**, ohne Kosten und ohne Umsatzschwelle. "Avalonia Accelerate" ist + nur ein optionales kommerzielles Support-/Tooling-Paket. Für das Projekt entstehen keine + Lizenzkosten. (Das passt zur bereits getroffenen Linie – PDFsharp wurde ausdrücklich statt + QuestPDF gewählt, um Umsatzschwellen zu vermeiden.) +- **LiveCharts2** (`LiveChartsCore.SkiaSharpView.Avalonia`) ist ebenfalls MIT. +- Beide bringen **SkiaSharp** mit. Auf Linux braucht das `libfontconfig1` und (für die Desktop-UI) + X11- oder Wayland-Bibliotheken im Image. Bei einer headless Variante entfällt das komplett – + ein weiteres Argument für die Trennung in Abschnitt 11. +- Zusätzliche `packageSourceMapping`-Muster: `Avalonia*`, `LiveChartsCore*`, `SkiaSharp*`, + `HarfBuzzSharp*`, `Tmds.DBus*`, `MicroCom*`. + +**Aufwand Block G: 10–14 Personentage** für jemanden, der Avalonia kennt. Ohne Vorerfahrung +realistisch +3–5 Tage Einarbeitung. LiveCharts2 zusätzlich 1–3 Tage je nach gewünschtem Umfang. + +--- + +## 10. Block H – Tests, Smoke-Check und CI + +165 Tests, 2.302 LOC. Das Testprojekt steht auf `net10.0-windows` + WinForms – **wegen genau einer +Datei**: `UiConstructionTests.cs` (87 LOC), die die Modul-Fenster auf einem STA-Thread konstruiert. +Dieselbe Idee steckt hinter dem `--smoke-ui`-Schalter in `Program.cs:270`. + +Nach der Portierung: + +- Die restlichen ~2.200 LOC Tests laufen **unverändert** auf `net10.0`/Linux. Sie testen Mapping, + Risiko, Portfolio, Accounting, Supervisor, Krypto, Logging – alles portabel, mit + EF-InMemory statt echter DB. +- `UiConstructionTests` und `--smoke-ui` werden auf `Avalonia.Headless` umgestellt. **Das ist ein + Gewinn, kein Verlust:** WinForms lässt sich in CI ohne Desktop-Session nicht sinnvoll + instanziieren, Avalonia.Headless ist genau dafür gebaut. Der Smoke-Check wird damit CI-fähig, + was er heute nicht ist. +- Eine Test-Fixture hängt an einem Pfad: `tests/…csproj` bindet `..\..\ct_raw.html` mit + Backslashes ein. MSBuild normalisiert das – **kein Problem**. + +Empfehlung für die CI: eine GitHub-Actions-/Gitea-Actions-Matrix `ubuntu-latest` + `windows-latest` +einrichten, sobald Etappe 1 steht. Das hält die Portabilität dauerhaft und fängt Rückfälle +(neues `DateTime.Now`, neues `.ToString("N2")`) sofort ab. + +**Aufwand Block H: 1–2 Tage.** + +--- + +## 11. Betrieb: der Punkt, den man leicht übersieht + +**Die App auf Linux zu bringen, macht das IB Gateway nicht Linux-tauglich.** Das ist eine getrennte +Baustelle: + +- TWS bzw. IB Gateway ist eine Java-Anwendung und läuft auf Linux – aber mit GUI. Für den + Dauerbetrieb ohne Bildschirm braucht es **IBC** (IBController) plus **Xvfb** als virtuellen + X-Server, dazu die Behandlung des täglichen Auto-Restarts und des 2FA-Handlings. +- Das ist erfahrungsgemäß **1–2 Tage** eigener Arbeit und hat mit dem C#-Code nichts zu tun. +- Alternative: Gateway bleibt auf dem Windows-Rechner, die Linux-App verbindet sich über das Netz + auf Port 4002. Dann muss in der TWS-Konfiguration die erlaubte Client-IP eingetragen werden + (heute steht in `settings.example.json` `127.0.0.1`) – und der TWS-API-Verkehr ist unverschlüsselt, + gehört also nicht über ein unvertrautes Netz. + +Weitere Betriebspunkte: `systemd`-Unit statt Autostart, Log-Rotation über `logrotate` statt +`BackupWorker`-Kopien, Dienstbenutzer ohne Login-Shell. + +**Aufwand Block I: 1–3 Tage**, je nachdem ob das Gateway mitwandert. + +--- + +## 12. Aufwandsübersicht + +| Block | Inhalt | Tage | +|---|---|---:| +| **0** | `NuGet.config` reparieren (blockiert alles andere) | 0,1 | +| **A** | Zielframeworks, csproj-Aufteilung, Projektstruktur | 0,5 | +| **B** | Core von WinForms lösen (3 Dateien + 3 Modul-Forms herauslösen) | 1,0 | +| **C** | Kultur/Formatierung (5 Fundstellen + Globalisierungsentscheidung) | 0,5 | +| **D** | Zeit/Zeitzonen ⚠ (Audit, `ParseExecutionTime`, `DailyReportService`, Tests) | 1,5 | +| **E** | Dateisystem (`BackupWorker`, PDF-Fonts, `IAppPaths`, Secrets-Rechte) | 1,5 | +| **F** | Netzwerk/Dienste (nur Verifikation) | 0,0 | +| **H** | Tests auf `net10.0`, Smoke-Check headless, CI-Matrix | 1,5 | +| | **Zwischensumme: headless Linux lauffähig** | **~6,5** | +| **G1** | Avalonia: Shell, Launcher, 4 Core-Views | 5,0 | +| **G2** | Avalonia: 3 Modul-Fenster (Accounting ist das größte) | 5,0 | +| **G3** | Theming, Feinschliff, Dialoge, Icons | 2,0 | +| | **Zwischensumme: Desktop-Linux** | **~12** | +| **G4** | LiveCharts2 – Neubau, kein Bestand vorhanden | 1–3 | +| **I** | Betrieb: systemd, Deployment, ggf. IB Gateway headless (IBC/Xvfb) | 1–3 | +| | **Gesamt** | **~21–25 Personentage** | + +Ohne Avalonia-Vorerfahrung auf Block G realistisch **+3–5 Tage** aufschlagen. + +Zum Vergleich in Wochen: **headless in gut einer Woche**, **komplett mit Desktop-UI in etwa +4–5 Wochen** Vollzeit. + +--- + +## 13. Vorschlag: in zwei Etappen, nicht in einem Zug + +### Etappe 1 – Headless Linux (~1,5 Wochen) + +Ziel: Core + alle drei Module + Worker + REST/MCP laufen als `systemd`-Dienst auf Linux. Die +Windows-UI bleibt **unverändert bestehen** und läuft weiter. + +1. `NuGet.config` reparieren. +2. Die drei Core-Dateien und die drei Modul-Forms herauslösen; Core und Module auf `net10.0`. +3. Neues Projekt `IBKRTrader.Daemon` (Konsole, `net10.0`) – nimmt `Program.cs` ab Zeile 51 + (`Host.CreateDefaultBuilder`) fast unverändert auf. Der Generic Host und die + `IHostedService`-Worker sind dafür bereits die richtige Grundlage; das wurde in R4 gelegt. +4. Blöcke C, D, E abarbeiten. +5. Testprojekt auf `net10.0`, CI-Matrix Linux + Windows. + +Danach ist die App auf Linux **im Dauerbetrieb einsatzfähig** – ohne dass ein einziges Fenster +angefasst wurde. Für ein System, das rund um die Uhr Marktdaten zieht und Signale verarbeitet, ist +das der eigentliche Gewinn. + +### Etappe 2 – Avalonia-Desktop (~2,5–3 Wochen) + +Ziel: `IBKRTrader.Desktop` ersetzt `IBKRTrader.App` und läuft auf beiden Plattformen. + +6. Shell + Launcher + Core-Views. +7. Die drei Modul-Fenster. +8. Erst danach LiveCharts2 – als eigenständiges Feature mit eigener Anforderung, nicht als + Nebenprodukt der Portierung. + +**Warum diese Reihenfolge:** Etappe 1 bringt den vollen Betriebsnutzen bei einem Sechstel des +Aufwands, und sie ist reversibel – wenn Etappe 2 liegen bleibt, steht trotzdem ein funktionierendes +System da. Umgekehrt (erst UI) hätte man nach drei Wochen eine schöne Oberfläche und immer noch +keinen Linux-Betrieb. + +--- + +## 14. Was du entscheiden musst + +Vier Punkte, die nicht aus dem Code ableitbar sind: + +1. **Zeitzonen-Konvention.** Alles UTC in der Persistenz und nur an der Oberfläche umrechnen? Oder + eine feste "Betriebszeitzone"? Das entscheidet den Zuschnitt von Block D – und es entscheidet, + wie `ParseExecutionTime` künftig aussieht. **Das ist der einzige Punkt, der vor dem ersten + Handgriff geklärt sein muss.** +2. **Wandert das IB Gateway mit auf Linux** (IBC + Xvfb, eigene 1–2 Tage) oder bleibt es auf dem + Windows-Rechner und die Linux-App verbindet sich über das lokale Netz? +3. **Soll die Windows-Desktop-Version erhalten bleiben?** Avalonia läuft auf beiden Plattformen – + die Frage ist nur, ob Windows weiter *getestet* werden muss (CI-Matrix) oder ob Linux das + alleinige Ziel wird. +4. **Zahlenformat im PDF-Export:** fest deutsch oder fest invariant? Für ein prüfbares + Finanzdokument sollte es festgelegt und nicht vom Host abhängig sein (5.1). + +--- + +## Anhang: Fundstellenverzeichnis + +Kurzliste aller konkret zu ändernden Stellen außerhalb der UI, nach Datei sortiert: + +| Datei | Zeile | Befund | Block | +|---|---|---|---| +| `NuGet.config` | – | 3 fehlende `packageSourceMapping`-Muster → Restore schlägt fehl | 0 | +| alle 6 `*.csproj` | – | `net10.0-windows` + `UseWindowsForms` | A | +| `Core/Logging/LoggingService.cs` | 12, 22, 100–138 | `RichTextBox`, `System.Drawing.Color` | B | +| `Core/Logging/LoggingService.cs` | 115 | `.ToUpper()` kulturabhängig | C | +| `Core/Logging/LoggingService.cs` | 18, 63, 66, 88 | Pfade unter `BaseDirectory`, Modulnamen als Verzeichnisse | E | +| `Core/Logging/LoggingService.cs` | 69, 72 | hartkodiertes `\r\n` | E | +| `Core/Modularity/ModuleView.cs` | 27, 30 | `System.Drawing.Image`, `Func` | B | +| `Core/Modularity/WindowMenu.cs` | gesamt | vollständig WinForms, wird ersetzt | B | +| `Core/Trading/Ibkr/IbkrMapping.cs` | 99–113 | verwirft Zeitzone, `Kind = Unspecified` ⚠ | D | +| `Core/IBKR/IBKRGatewayService.cs` | 247 | `.ToLower()` kulturabhängig, geht in URL | C | +| `Core/Workers/BuiltIn/BackupWorker.cs` | 95–118 | `mysqldump.exe`, `C:\`-Pfade, `PATH.Split(';')` | E | +| `Core/Workers/BuiltIn/BackupWorker.cs` | 66 | DB-Passwort in der Kommandozeile (auf Linux exponiert) | E | +| `Core/Workers/BuiltIn/BackupWorker.cs` | 34, 35, 126 | `DateTime.Now`, `BaseDirectory` | D/E | +| `Core/Workers/WorkerBase.cs` | 114, 118, 146 | `DateTime.Now` in der Ablaufsteuerung | D | +| `Core/Settings/SettingsService.cs` | 13 | `settings.json` neben der Binärdatei | E | +| `Modules.Accounting/Logic/PdfExporter.cs` | 30 | `"Segoe UI"` – auf Linux nicht vorhanden | E | +| `Modules.Accounting/Logic/PdfExporter.cs` | 25, 86 | `ToString("N2")`/`("0.###")` kulturabhängig | C | +| `Modules.Accounting/Ui/AccountingMainForm.cs` | gesamt (302) | WinForms, 3 `DataGridView`, 2 `SaveFileDialog` | G | +| `Modules.CongressTrading/Scraper/CapitolTradesScraper.cs` | 124, 333 | `int.Parse`/`DateOnly.TryParse` ohne `IFormatProvider` | C | +| `Modules.CongressTrading/UI/CongressTradingForm.cs` | gesamt (129) | WinForms | G | +| `Modules.Supervisor/Services/DailyReportService.cs` | 44, 56 | `DateTime.Now` + `DateTimeKind.Local` → falsche Uhrzeit auf UTC-Host | D | +| `Modules.Supervisor/Agent/OpenRouterClient.cs` | 39 | `openrouter.key` neben der Binärdatei, Dateirechte | E | +| `Modules.Supervisor/Ui/SupervisorMainForm.cs` | gesamt (188) | WinForms, `RichTextBox` | G | +| `Program.cs` | 31, 48, 227, 270–320 | `[STAThread]`, `ApplicationConfiguration`, `master.key`, Smoke-UI | A/B/E | +| `LauncherForm.cs`, `UI/**` | gesamt (541) | WinForms-Shell | G | +| `tests/…csproj` + `UiConstructionTests.cs` | – | `net10.0-windows` nur wegen einer Datei | H |