@ 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 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-06 22:26:49 +02:00
co-authored by Claude Opus 5
parent b96a20788c
commit ae79ae9dca
+689
View File
@@ -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 19 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 | **57 Personentage** |
| **B. Desktop-Linux** (zusätzlich die komplette UI auf Avalonia) | ~1.300 LOC WinForms neu bauen | **+1014 Personentage** |
| **C. LiveCharts2** (heute existiert **kein einziges** Diagramm) | Neubau, keine Migration | **+13 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 `<clear/>`. 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<LogEntry>? 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<Form> CreateForm { get; init; } = () => new Form();
```
**Umbau:** Der Contract muss toolkit-neutral werden. Zwei Wege:
- **Pragmatisch:** `Func<object> 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 12 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: 12 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/<pid>/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: 11,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 `102465535` 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.0002.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<T>`) wird zu `ObservableCollection<T>`.
- 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: 1014 Personentage** für jemanden, der Avalonia kennt. Ohne Vorerfahrung
realistisch +35 Tage Einarbeitung. LiveCharts2 zusätzlich 13 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: 12 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äß **12 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: 13 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 | 13 |
| **I** | Betrieb: systemd, Deployment, ggf. IB Gateway headless (IBC/Xvfb) | 13 |
| | **Gesamt** | **~2125 Personentage** |
Ohne Avalonia-Vorerfahrung auf Block G realistisch **+35 Tage** aufschlagen.
Zum Vergleich in Wochen: **headless in gut einer Woche**, **komplett mit Desktop-UI in etwa
45 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,53 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 12 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, 100138 | `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<Form>` | B |
| `Core/Modularity/WindowMenu.cs` | gesamt | vollständig WinForms, wird ersetzt | B |
| `Core/Trading/Ibkr/IbkrMapping.cs` | 99113 | verwirft Zeitzone, `Kind = Unspecified` ⚠ | D |
| `Core/IBKR/IBKRGatewayService.cs` | 247 | `.ToLower()` kulturabhängig, geht in URL | C |
| `Core/Workers/BuiltIn/BackupWorker.cs` | 95118 | `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, 270320 | `[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 |