Files
IBKRTrader/docs/archiv/KONZEPT-Linux-Portierung.md
RichardandClaude Opus 5 9f66183f1c
Build & Test / build (ubuntu-latest) (push) Waiting to run
Build & Test / build (windows-latest) (push) Waiting to run
Eine Roadmap statt sieben Konzepte; Quelldokumente ins Archiv
Der offene Stand lag ueber sieben Konzepte, zwei Referenzdokumente und die
Phasen-Checkliste der Architektur verteilt. Dieselbe Aufgabe stand teils
doppelt unter zwei Namen - die asynchrone Fill-Verfolgung etwa als "bekannte
Grenze" in IBKR-Integration.md und zugleich als W-2 im OptionsWheel-Konzept.
Wer wissen wollte, was als Naechstes ansteht, musste alles neun lesen.

docs/ROADMAP.md fuehrt das zusammen:
  - Fuenf Stufen in Abhaengigkeitsreihenfolge, von "Fundament schliessen" bis
    zum OptionsWheel, dazu vier laufende Bahnen (Auslieferung, Accounting,
    Supervisor, technische Schulden).
  - Jede Zeile traegt ihre Herkunft (W-2, P5, Kapitalmodell 5, ...), damit die
    Herleitung im Archiv auffindbar bleibt.
  - Eigene Abschnitte fuer Zurueckgestelltes und Verworfenes. Zurueckgestellte
    Ideen nennen ausdruecklich, WAS sie wieder aktuell macht; verworfene nennen
    den Grund, damit sie nicht in sechs Monaten erneut vorgeschlagen werden.
  - Erledigtes bleibt als Zeile mit Datum stehen statt zu verschwinden.

Archiv (git erkennt alle sieben als Umbenennung, Historie bleibt):
  docs/konzepte/*         -> docs/archiv/
  docs/Kapital-und-Buchmodell.md -> docs/archiv/
Jedes archivierte Dokument bekommt oben einen Vermerk, warum es erhalten bleibt
und wo der lebende Stand steht. Inhaltlich ist keines veraendert.

Weiter gepflegt werden ARCHITECTURE.md (Aufbau + Phasen-Historie),
IBKR-Integration.md (Adapter-Design und Grenzen) und die TWS-Setup-Checkliste -
das sind Referenzen, keine Planung. Ihre eigenen Offen-Listen verweisen jetzt
mit Roadmap-Kennung dorthin, statt einen zweiten Stand zu fuehren.

Alle 63 relativen Markdown-Links geprueft, keiner tot. Fuenf Pfadverweise in
Code, csproj und systemd-Unit mitgezogen; die Unit zeigte auf die
Portierungsanalyse, die ausdruecklich den Stand VOR dem Umbau beschreibt - jetzt
auf ARCHITECTURE.md. Build 0 Warnungen, 198/198 Tests gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 18:14:51 +02:00

715 lines
37 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
> ### 📦 Archiviert am 2026-08-23
> Dieses Dokument wird **nicht mehr gepflegt**. Was davon noch offen ist, steht in der
> [Roadmap](../ROADMAP.md) dort und nur dort wird der Stand nachgeführt.
>
> Es bleibt erhalten, weil es die Analyse vor dem Umbau ist: Fundstellenverzeichnis, Aufwandsschaetzung und
> was anders kam als geschaetzt. Die Portierung selbst ist abgeschlossen (L0L6). Zum Nachschlagen also weiterhin richtig,
> als Aufgabenliste nicht mehr.
---
# Analyse: Linux-Fähigkeit des IBKRTrader
> **UMGESETZT am 2026-08-07 (L0L5).** 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 2125 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 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 |