Files
PolyTraderSharp/docs/ANALYSE-Linux-Portierung.md
T
RichardandClaude Opus 5 fd6d62618f Kultur-Bug behoben + Threema entfernt (INotificationSink)
- TraderMonitorService las API-Preise kulturabhaengig: unter de-DE wurde aus
  "0.53" der Wert 53 (Faktor-100-Fehler im Einstandspreis). Nutzt jetzt den
  bereits vorhandenen invarianten Helper ParseDecimal.
- Gleiche Fehlerklasse in PolymarketClobClient (6x) und MasterTraderAnalyticsJob
  vorsorglich auf InvariantCulture gestellt.
- Neuer Regressionstest ApiNumberParsingTests (10 Faelle unter erzwungener de-DE-Kultur).
- Threema komplett entfernt (Entscheidung Richard): ThreemaService, vendorte
  Bibliothek libs/Threema-MsgApi-Net-Core, ServerSettings-Block, DI-Verdrahtung.
- Ersetzt durch neutrale INotificationSink (No-Throw-Vertrag) + LogNotificationSink
  als Uebergang; RocketChat/Telegram folgen spaeter.
- Entfernt nebenbei libsodium 1.0.16, die einzige Registry-Nutzung im Build,
  den HttpListener-Webhook und System.Web.HttpUtility (alles Linux-Hindernisse).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:01:13 +02:00

42 KiB
Raw Blame History

Analyse & Umsetzungsrahmen: Linux-Fähigkeit von PolyTrader

Stand: 06.08.2026 · Revision 3 (Entscheidungen Richard eingearbeitet, P3b umgesetzt) Basis: 7 Projekte, ~24.250 LOC Produktivcode (ohne Tests/EF-Migrationen), 448 Tests


Entscheidungen (06.08.2026)

# Thema Entscheidung Auswirkung
1 Kultur-Bug Sofort gefixt Erledigt, siehe Abschnitt 4.1
2 Designer-Regel Neue Formulierung übernommen: Layout deklarativ in .axaml, nicht zur Laufzeit im Code ersetzt die WinForms-Designer-Regel
3 Windows-UI Variante 2: auf heutigem Stand eingefroren; soll später komplett aus dem Build verschwinden Strategie siehe 5.5
4 PropertyGrid wird durch normale Steuerelemente ersetzt +5 PT ggü. Fremdbibliothek, dafür bessere UX
5 Lizenz/Watchdog laufen jetzt über das Deploymentcenter, nicht mehr über Einzeldienste Linux-Blocker entfällt, siehe 5.2
6 Zeitzone einmalig bei der Installation festgelegt, nicht im laufenden Betrieb gewechselt vereinfacht die Umsetzung, siehe 3.3
7 Threema entfernt ; Nachfolger: RocketChat + Telegram umgesetzt am 06.08.2026, siehe 5.3
8 Charts LiveCharts2 bestätigt (nach Klarstellung, dass die Integration nicht komplex ist) +1 PT ggü. ScottPlot.Avalonia

Netto-Effekt auf den Gesamtaufwand: leicht gesunken auf 4161 PT. PropertyGrid (+5 PT) und LiveCharts2 (+1 PT) stehen gegen den bereits erledigten Threema-Ausbau (2 PT), die vereinfachte Zeitzonen-Anforderung (1 PT) und den entfallenden Lizenz-Umbau (2 PT).


0. Kurzfassung

Frage Antwort
Ist es machbar? Ja, ohne architektonische Sackgassen.
Was ist der Löwenanteil? Die UI. ~8.300 LOC (34 % des Produktivcodes) hängen an WinForms.
Was ist überraschend gut? Die gesamte Trading-Kernlogik ist bereits portabel: kein einziger DllImport, keine Registry, keine WMI, keine DPAPI, keine SpecialFolder, saubere Path.Combine-Nutzung. Nethereum, Pomelo/EF, HttpClient, ClientWebSocket, AES-GCM laufen unverändert.
Was ist der teuerste Einzelpunkt? Der PropertyGrid-Ersatz durch handgebaute Steuerelemente (6 Instanzen in 4 Fenstern).
Gesamtaufwand 4161 Personentage (davon 2 PT bereits erledigt)
Wichtigste Erkenntnis Für ~15 PT (≈ 25 %) bekommst du 80 % des Nutzens: Der Trading-Kern läuft headless auf Linux, die WinForms-UI bleibt als eingefrorener Fallback. Siehe Abschnitt 7.

1. Bestandsaufnahme

1.1 Projektstruktur und Zielframeworks

Projekt TargetFramework UseWindowsForms Warum WinForms?
PolyTrader.App net8.0-windows7.0 (WinExe) ja Shell, Launcher, 4 Core-Views
PolyTrader.Core net8.0-windows ja nur UI-Contract (ModuleUi.cs, WindowMenu.cs)
Modules.CopyTrading net8.0-windows ja 5 Views + TradeRowColoring (System.Drawing.Color)
Modules.ResolutionFarming net8.0-windows ja 1 Fenster
Modules.Supervisor net8.0-windows ja 1 Fenster
Modules.Accounting net8.0-windows ja 1 Fenster
PolyTrader.Tests net8.0-windows ja nur weil die Referenzen -windows sind
IcgSoftware.Threema.CoreMsgApi (lib) entfernt am 06.08.2026 (Entscheidung 7)

Alle verbleibenden Projekte sind -windows aber die Bindung ist dünn. Core und Module brauchen WinForms ausschließlich für den UI-Contract; die Services darunter sind plattformneutral.

1.2 Wo sitzt der Windows-Code?

WinForms/System.Drawing berührt 41 Dateien  davon:
  ├─ 33 reine UI-Dateien (Ui/-Ordner)          → müssen ohnehin neu
  └─  5 „Ausreißer" außerhalb der Ui-Ordner    → das ist die eigentliche Kopplung
       ├─ src/PolyTrader.Core/Modularity/ModuleUi.cs        (Func<Form>, Image)
       ├─ src/PolyTrader.Core/Modularity/WindowMenu.cs      (MenuStrip, Font)
       ├─ src/…CopyTrading/Logic/TradeRowColoring.cs        (Color)
       ├─ Models/DashboardRow.cs                            (using System.Drawing)
       └─ Program.cs                                        (ApplicationConfiguration.Initialize)

Nur 5 Dateien stehen zwischen „Core/Module sind plattformneutral" und dem Ist-Zustand.

1.3 Code-Verteilung

Kategorie LOC Anteil
UI (Designer + Code-Behind), App + Module ~8.300 34 %
davon Designer-generiert (entfällt ersatzlos) ~4.500 19 %
Services / Logik / Persistenz / Modelle ~15.950 66 %
Tests (448 Testfälle, 48 Dateien) ~5.300
EF-Migrationen (unverändert übernehmbar) 6.891

Nur 1 von 48 Testdateien ist UI-abhängig (WindowMenuTests.cs).


2. Die UI-Portierung

2.1 Control-Inventar und die neue Layout-Regel

16 Fenster/UserControls, ~8.300 LOC. Aus den Designer-Dateien:

WinForms-Control Anzahl Avalonia-Ersatz Bewertung
DataGridViewTextBoxColumn 69 DataGridTextColumn mechanisch, viel Fleißarbeit
Label 49 TextBlock trivial
ToolStripButton 37 Button in StackPanel Layout neu
TabPage / TabControl 22 / 7 TabItem / TabControl trivial
DataGridView 20 DataGrid siehe unten
ToolStrip 16 StackPanel/DockPanel Layout neu
Panel / Button 16 / 16 direkt trivial
TextBox / ComboBox 9 / 9 direkt trivial
PropertyGrid 6 handgebaute Formulare (Entscheidung 4) teuerster Punkt, siehe 2.2
DateTimePicker 5 CalendarDatePicker + TimePicker (getrennt!) kleine Nacharbeit
GroupBox 4 HeaderedContentControl/Border trivial
RichTextBox 3 kein Äquivalent, siehe 2.3 mittel
PictureBox 3 CartesianChart (LiveCharts2) siehe 2.4
SplitContainer 2 GridSplitter Layout neu
MenuStrip / StatusStrip 1 / 1 Menu / DockPanel ToolStripItemAlignment.Right gibt es nicht → DockPanel

Entwarnung bei den DataGrids: Die Nutzung ist erfreulich schlicht 25× DataSource-Binding, 35× DefaultCellStyle, 8× AutoGenerateColumns, nur 4 SelectionChanged, 3 CellContentClick, 1 CellDoubleClick. Kein Virtual Mode, kein Custom Painting, keine CellFormatting. Direkt auf ItemsSource + Binding StringFormat abbildbar.

Die Zeilenfärbung (ClosedTradesView.cs, OpenTradesView.cs über DataBindingCompleterow.DefaultCellStyle.BackColor) wird zu einem IValueConverter auf DataGrid.LoadingRow sauberer als heute.

📐 Die neue Layout-Regel (Entscheidung 2)

Die bisherige Vorgabe „alle WinForms-Controls designerfähig (partial + .Designer.cs), nie code-only" wird ersetzt durch:

„Jede View besteht aus View.axaml (vollständiges Layout, deklarativ) und View.axaml.cs (nur Verdrahtung und Datenlogik). Steuerelemente und Layout werden NICHT zur Laufzeit im Code erzeugt."

Nutzen bleibt identisch: Layout ist vollständig inspizierbar, diffbar und an einer Stelle. Ein WYSIWYG-Designer existiert in Avalonia nicht stattdessen der XAML-Previewer (VS-/Rider-Extension) mit Live-Vorschau beim Tippen.

Praktische Konsequenz für die Portierung: WindowMenu.Populate() baut die Menüleiste heute komplett zur Laufzeit auf (menu.Items.Clear() + dynamisch erzeugte ToolStripMenuItem). Das verstößt gegen die neue Regel und wird zu einem deklarativen Menu mit ItemsSource-Bindung an eine ObservableCollection<WindowMenuEntry> Layout im XAML, nur die Daten kommen aus dem Code. Gleiches gilt für die dynamisch angehängte Menüleiste in ShellUiHost.AttachWindowMenu().

2.2 PropertyGrid → normale Steuerelemente (Entscheidung 4)

Betroffen sind 4 Fenster mit 6 Grid-Instanzen:

Datei Instanz Gebundenes Objekt
Ui/Views/SettingsView.Designer.cs:21,33 propertyGrid, pgAccount ServerSettings, Account-Daten
…CopyTrading/Ui/AccountSettingsView.Designer.cs:25 pgSettings CopyTradingAccountSettings
…CopyTrading/Ui/MasterTradersView.Designer.cs:41 pgDetail TrackedTrader
…ResolutionFarming/Ui/ResolutionFarmingMainForm.Designer.cs:38 pgSettings Farming-Settings

Vorarbeit, die sich jetzt auszahlt: Die Modelle tragen bereits [Description]-Attribute als Inline-Erklärungen (CopyTradingAccountSettings.cs:11, TrackedTrader.cs:10) und [Browsable(false)] zum Ausblenden von Geheimnissen (ServerSettings.cs:107,146 Watchdog-Token und Lizenzschlüssel). Diese Attribute sind die Spezifikation der neuen Formulare Beschriftung, Tooltip und Sichtbarkeit jedes Feldes stehen schon im Code. Das reduziert die Denkarbeit auf Layout und Validierung.

Was die handgebauten Formulare besser können als das PropertyGrid:

  • Eingabevalidierung vor dem Speichern statt erst beim Commit der Zelle (bei Risiko-Limits und Preisgrenzen relevant)
  • Einheiten direkt am Feld (USDC, %, bps, Minuten) statt im Beschreibungstext
  • Fachliche Gruppierung statt alphabetischer Reflection-Reihenfolge
  • Abhängige Felder ausgrauen (z. B. Ladder-Parameter nur bei aktivierter Sell-Ladder)

Aufwand: ~810 PT statt ~2 PT mit Fremdbibliothek. Der Aufschlag ist bewusst und kauft dauerhafte Wartbarkeit ohne Fremdabhängigkeit.

2.3 RichTextBox → Terminal-Fenster

TerminalView (378 + 195 LOC) und SupervisorMainForm (Chat) nutzen RichTextBox mit Farbausgabe pro Log-Zeile. Avalonia hat kein RichTextBox.

Empfehlung: ItemsControl über eine ObservableCollection<LogLine> mit Farb-Converter virtualisiert, deklarativ (passt zur neuen Layout-Regel), und schneller als heute. TerminalView.cs:186 implementiert bereits ein Auto-Clear bei RAM-Limit; der Performance-Druck ist bekannt, ein virtualisiertes ItemsControl löst ihn an der Wurzel.

2.4 Charts: ScottPlot → LiveCharts2 (Entscheidung 8)

Nur 3 Charts, alle in DashboardView.RenderCharts() (Equity-Kurve, PnL je Modul, PnL je Tag). Die Auswertungslogik liegt bereits pur in TradeAnalytics und bleibt komplett unverändert.

Der heutige Weg (plot.GetImage(w,h).GetImageBytes()System.Drawing.BitmapPictureBox, DashboardView.cs:207-227) entfällt und wird zu drei deklarativen CartesianChart-Elementen im XAML was der neuen Layout-Regel ohnehin besser entspricht als das heutige Rendern im Code.

Zur Einordnung deiner Sorge: Der Unterschied ist kleiner als befürchtet. ScottPlot.Avalonia wäre ~1 PT günstiger gewesen, nicht mehr bei drei Charts fällt der Integrationsvorteil kaum ins Gewicht. LiveCharts2 bringt dafür Animationen, Tooltips und Zoom out of the box, die du bei ScottPlot nachbauen müsstest. Die Entscheidung für LiveCharts2 ist bei dieser Chart-Anzahl gut begründet du zahlst dafür keinen nennenswerten Aufpreis.

2.5 System.Drawing.Common muss raus

Seit .NET 7 ist System.Drawing.Common Windows-only und wirft auf Linux PlatformNotSupportedException. Betroffen:

  • ModuleUi.cs:32 System.Drawing.Image? IconAvalonia.Media.IImage
  • TradeRowColoring.cs:11-13 Color.FromArgb(...) → eigenes Farbtupel oder Avalonia.Media.Color
  • WindowMenu.cs Font, FontStyle.Bold
  • Models/DashboardRow.cs using System.Drawing (vermutlich verwaist, prüfen)
  • DashboardView.cs:222-225 entfällt mit LiveCharts2

TradeRowColoringTests.cs testet die Farben direkt → zieht mit (klein).

2.6 PDF-Export: PDFsharp-MigraDoc-GDI → Core-Build

Modules.Accounting referenziert PDFsharp-MigraDoc-GDI 6.2.4 der GDI+-Build ist Windows-only. Ersatz: PDFsharp-MigraDoc (Core-Build), API-kompatibel.

Aber: Der Core-Build findet auf Linux keine Systemschriften. PdfExporter.cs:34 setzt style.Font.Name = "Segoe UI" existiert auf Linux nicht. Nötig ist ein eigener IFontResolver plus eine als EmbeddedResource mitgelieferte Schrift (DejaVu Sans oder Open Sans, beide frei lizenziert). Einziger nennenswerter Umbau am Accounting-Modul.

AccountingReportTests.cs:131 rendert PDFs headless im Test → zieht mit.


3. Zeitzonen

3.1 Der harte Punkt

Ui/Views/TerminalView.cs:150:

TimeZoneInfo berlinTz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");

Eine Windows-Zeitzonen-ID. Seit .NET 6 mappt FindSystemTimeZoneById Windows-IDs auf Linux über ICU funktioniert also meistens. Es bricht, sobald InvariantGlobalization=true gesetzt ist oder die Distro kein vollständiges ICU mitbringt (Alpine ohne icu-libs). Dann fliegt beim Öffnen des Terminal-Fensters eine TimeZoneNotFoundException.

Mit Entscheidung 6 verschwindet diese Stelle ohnehin die Zeitzone kommt künftig aus der Konfiguration, nicht mehr hartcodiert.

3.2 Die Now/UtcNow-Mischung

122× DateTime.UtcNow gegen 23× DateTime.Now. Konsistent, solange der Rechner auf Europe/Berlin steht. Linux-Server laufen praktisch immer auf UTC. Betroffene Stellen:

Stelle Auswirkung bei Server-TZ = UTC
TerminalLogger.cs:23 Timestamp = DateTime.Now Log-Zeitstempel 12 h versetzt
TerminalLogger.cs:122, LauncherWidgetsPanel.cs:112 Logdatei {DateTime.Now:yyyy-MM-dd}.jsonl Tagesgrenze der Logdateien verschiebt sich
DossierService.cs:81, SupervisorTools.cs:144 suchen Logdateien per Datum müssen dieselbe Konvention nutzen, sonst leere Dossiers
DailyReportService.cs:61 Tagesbericht schneidet an anderer Stelle
AccountingMainForm.cs:57 Periodenvorbelegung steuerlich relevant Monats-/Quartalsgrenzen
PdfExporter.cs:55 „Erstellt: …" Belegdatum im Steuer-PDF
MasterTraderAnalyticsJob.cs mischt Now (Anzeige) und UtcNow (Cutoffs) funktioniert, ist aber verwirrend

3.3 Anforderung: konfigurierbare Zeitzone (Entscheidung 6)

Ziel (präzisiert): Die Zeitzone wird einmalig bei der Installation festgelegt — wie in einem Setup — und im laufenden Betrieb normalerweise nicht mehr gewechselt.

Vorgeschlagene Umsetzung:

  1. Neue Einstellung ServerSettings.ApplicationTimeZoneId (Default "Europe/Berlin"). IANA-IDs verwenden .NET akzeptiert sie seit Version 6 auf beiden Plattformen, Windows-IDs dagegen nur eingeschränkt auf Linux.

  2. Zentraler Zugriffspunkt statt verstreuter DateTime.Now-Aufrufe:

    IAppClock
      ├─ DateTime UtcNow          → für alle Domänen-/Persistenzwerte
      ├─ DateTime Local           → für Anzeige und Logdatei-Namen
      └─ TimeZoneInfo TimeZone    → aufgelöst aus ApplicationTimeZoneId, mit Fallback auf UTC + Warnung
    

    Damit ist die Zeitzone an einer Stelle wirksam statt an 23.

  3. Festlegung bei der Erstinstallation, danach schreibgeschützt-mit-Bestätigung:

    • Beim ersten Start ohne gesetzte Zeitzone wird sie abgefragt (UI-Dialog bzw. CLI-Schalter --set-timezone für den headless Betrieb) und in server_settings.xml festgeschrieben.
    • Eine spätere Änderung ist möglich, aber bewusst unbequem: Bestätigungsdialog mit Klartext-Folgen („Logdatei-Tagesgrenzen verschieben sich um X Stunden. Bereits geschriebene Logs und Buchungen bleiben unverändert und sind danach in der neuen Zeitzone zu lesen.") und Wirksamkeit erst nach Neustart.
    • Protokolleintrag mit alter und neuer Zeitzone, damit die Verschiebung später in den Logs nachvollziehbar ist.

    Weil der Regelfall die einmalige Festlegung ist, entfällt jede Komplexität für einen Wechsel zur Laufzeit — kein Neu-Verdrahten laufender Jobs, kein Umschalten offener Fenster. Das ist der eigentliche Gewinn dieser Entscheidung.

  4. Zusätzlich TZ-Umgebungsvariable in der systemd-Unit setzen, damit auch die Ausgaben außerhalb der Anwendung (Logrotate, Cron, Systemlogs) zusammenpassen.

Offene fachliche Frage: Soll die Zeitzone im Accounting mitwandern oder dort fix auf der steuerlich maßgeblichen Zeitzone stehen bleiben? Ein Wechsel des Betriebsstandorts ändert nicht zwingend den steuerlichen Bezugsrahmen. Empfehlung: zwei getrennte Einstellungen ApplicationTimeZoneId (Anzeige, Logs, Jobs) und AccountingTimeZoneId (Buchungsperioden, Steuerbelege), Default identisch. Kostet fast nichts, verhindert aber, dass ein Standortwechsel rückwirkend die Buchungsperioden verschiebt.

Aufwand: ~2 PT (Einstellung, IAppClock, 23 Aufrufstellen umziehen, Erstinstallations-Abfrage, Tests). Durch die Beschränkung auf die Installationszeit ~1 PT günstiger als in Revision 2 angesetzt.

3.4 MySQL

Pomelo speichert DateTime als DATETIME ohne Zeitzoneninformation. Bestehende Daten ändern sich beim Plattformwechsel nicht aber die Interpretation neuer Werte ändert sich. Prüfen: SELECT @@global.time_zone, @@session.time_zone; auf dem Ziel-Server. Bei gemischt geschriebenen Daten entstehen im Accounting stumme Lücken/Überlappungen an der Tagesgrenze. Der IAppClock aus 3.3 löst das, indem er die Persistenz konsequent auf UTC festlegt.


4. String- und Zahlenformatierung

4.1 ERLEDIGT der aktive Bug ist behoben

TraderMonitorService.cs parste API-Preise kulturabhängig. Empirisch verifiziert:

de-DE      "0.53" -> 53       "1234.5" -> 12345
Invariant  "0.53" -> 0,53     "1234.5" -> 1234,5

Ein Faktor-100-Fehler im Einstandspreis der Master-Trader-Auswertung.

Behoben am 06.08.2026:

  • TraderMonitorService.cs:1276-1280 nutzt jetzt den bereits vorhandenen, korrekten Helper ParseDecimal statt duplizierter Inline-Logik. Der Helper ist internal static und dokumentiert.
  • PolymarketClobClient.cs:770, 784, 796, 820, 822, 827 alle TryParse auf InvariantCulture (Regex-Gruppen sind zwar ziffernrein und waren damit faktisch sicher, aber die Klasse Fehler soll nirgends mehr im Code stehen).
  • MasterTraderAnalyticsJob.cs:140 Unix-Timestamp-String invariant.
  • Neuer Regressionstest tests/PolyTrader.Tests/ApiNumberParsingTests.cs 10 Fälle, die unter erzwungener de-DE-Kultur laufen und fehlschlagen, sobald jemand die Invariant-Angabe entfernt.

Verifikation: Build fehlerfrei, 448 Tests grün (438 vorher + 10 neue).

4.2 Ausgabeformatierung (offen)

35× DefaultCellStyle.Format, 19× ToString("…"), dazu zahlreiche :N2/:F2/:F3 in Log- und UI-Strings alle nach CultureInfo.CurrentCulture.

Auf Windows ist das de-DE. Auf Linux ist es LANG/LC_ALL unter systemd oft gar nicht gesetzt → .NET fällt auf Invariant zurück → aus 1.234,56 USDC wird 1,234.56 USDC. Kein Datenfehler, aber sofort sichtbar.

Empfehlung:

  1. Anzeige-Kultur beim Start explizit pinnen: CultureInfo.DefaultThreadCurrentCulture = new CultureInfo("de-DE") in Program.Main. Damit ist die Darstellung unabhängig von LANG. (Sinnvollerweise ebenfalls konfigurierbar, analog zur Zeitzone dieselbe Begründung: Betrieb im Ausland.)
  2. Globalisierungs-Analyzer aktivieren (CA1304, CA1305, CA1307, CA1310). Sie hätten den Bug aus 4.1 gefunden.

4.3 Weitere Kulturthemen

  • ICU vs. NLS: .NET nutzt seit Version 5 auf beiden Plattformen ICU Sortier- und Vergleichssemantik bleibt gleich. Voraussetzung: kein InvariantGlobalization=true im Publish-Profil, libicu auf dem Zielsystem (Debian/Ubuntu Standard, Alpine nachinstallieren).
  • String-Vergleiche: durchgängig StringComparison.OrdinalIgnoreCase vorbildlich, keine Nacharbeit.

5. Weitere plattformspezifische Punkte

5.1 Dateisystem erfreulich sauber

Prüfpunkt Befund
Path.Combine 17 von 17 korrekt, durchgängig über AppContext.BaseDirectory
Environment.SpecialFolder 0 Treffer
Harte Windows-Pfade genau 1: ServerSettings.cs:73 MullvadCliPath (konfigurierbar, nur der Default ist Windows)
DllImport / LibraryImport 0 Treffer
Registry / WMI / DPAPI / EventLog 0 Treffer im eigenen Code

Groß-/Kleinschreibung: Linux ist case-sensitive. Kritisch: server_settings.xml, appsettings.Local.json, master.key, openrouter.key, Logs/, favicon.ico, die 24 PNGs in Resources/. Im Code konsistent trotzdem beim ersten Linux-Build gezielt prüfen, ebenso die \-Pfade in den .csproj (MSBuild normalisiert sie, aber es ist eine bekannte Stolperstelle).

Dateirechte: master.key und openrouter.key liegen als Klartext neben der Anwendung (SettingsView.cs:301,356, Program.cs:393, OpenRouterClient.cs:46). Auf Linux erzeugt File.WriteAllText mit Standard-umask 644 world-readable. Nach jedem Schreiben File.SetUnixFileMode(path, UserRead | UserWrite) (.NET 7+). Ergänzt Befund F1 des Sicherheitskonzepts.

Das Deploymentcenter macht es beim Lizenz-Cache bereits richtig (chmod 600 state.dat, siehe LICENSE_INTEGRATION_GUIDE Abschnitt 3) dieselbe Konvention für die eigenen Schlüsseldateien übernehmen.

Zeilenenden: Environment.NewLine wechselt von \r\n auf \n. CsvExporter.cs nutzt bereits explizites '\n' korrekt.

5.2 Lizenz & Watchdog jetzt über das Deploymentcenter (Entscheidung 5)

Das war in Revision 1 ein Blocker (R4) er ist gelöst. Aus J:\Softwareprojekte\Deploymentcenter\docs:

Thema Alt (LicenseLabrador.Client 1.0.0) Neu (Deploymentcenter, HWID v2)
Hardware-ID Windows: Registry MachineGuid; Linux: /etc/machine-idplattformabhängig verschieden 2:<plattform>:<sha256> mit dokumentierter Quellen-Priorisierung je Plattform
Container jede Instanz eine neue ID → Lizenz invalidiert bei jedem Neustart DEPLOYMENTCENTER_HWID-Override gewinnt plattformweit
Rechnername im Hash bewusst nicht enthalten, Umbenennen kostet keinen Aktivierungsplatz
Migration Client sendet hardware_id (v2) und legacy_hardware_id (v1); Server zieht die Aktivierung lautlos um, ohne zusätzlichen Platz
Headless modaler Dialog blockiert den Start CLI-Schalter --license-status, --license-set-key, --license-deactivate plus ILicensePrompt/ConsoleLicensePrompt als UI-Abstraktion
Cache LLS2-Envelope, AES-256-GCM, HKDF aus HWID+Slug, Linux 0600, kein Klartext-Rückfall
SDK netstandard2.0 Deploymentcenter.Client, Multi-Targeting netstandard2.0;net8.0

Damit entfallen zwei Risiken vollständig: die Neubindung der Lizenz beim Plattformwechsel (Migration v1→v2 ist serverseitig gelöst) und der Container-Neustart-Effekt.

Verbleibende Arbeit ein Umbau, kein Blocker:

  1. Lizenz: Licensing/LicenseGate.cs und Ui/LicenseDialog.cs von LicenseLabrador.Client auf Deploymentcenter.Client umstellen. Die API unterscheidet sich: LicenseResult/LicenseState/GetHardwareId()LicenseValidationResult (IsValid, Status, IsCached, Message) / HardwareId.GetHardwareId(slug) / ValidateAsync(slug, key, serverUrl). ILicensePrompt implementieren statt den Dialog direkt aufzurufen das ist genau die Naht, die den headless-Betrieb und später die Avalonia-UI trägt. Das lokale NuGet-Paket in lib/nuget/ wird ersetzt.
  2. Watchdog: WatchdogHeartbeatService sendet bereits source, instance, interval das Payload-Schema passt schon. Zu ändern sind nur Header (X-Watchdog-KeyX-Agent-Token) und Ziel-URL (watchdog.mhdf.dedc.mhdf.de/api/watchdog/v1/ping). Ergänzen: os- und group-Feld, sowie der im Guide empfohlene Abschluss-Ping beim geordneten Herunterfahren.

Aufwand: ~23 PT. Empfehlung: in Stufe L1 mitnehmen, nicht später der headless-Betrieb (L2) hängt an ILicensePrompt.

Sicherheitshinweis am Rande: server_settings.xml enthält den Watchdog-Token und den Lizenzschlüssel im Klartext (WatchdogToken, LicenseKey). Beim Umbau auf das Deploymentcenter ist der passende Moment, beide über die SecretProtection laufen zu lassen oder auf Umgebungsvariablen umzustellen passend zur bereits offenen Auflage, die Watchdog-Secrets zu rotieren.

5.3 Threema entfernt (Entscheidung 7)

Am 06.08.2026 umgesetzt. Nachfolgekanäle werden RocketChat und Telegram.

Was entfernt wurde:

Entfernt Umfang
src/PolyTrader.Core/Services/ThreemaService.cs 1 Datei (BackgroundService + Webhook-Listener)
libs/Threema-MsgApi-Net-Core (vendorte Fremdbibliothek) 35 Dateien, aus Solution und Git entfernt (History bleibt)
Projektreferenz in PolyTrader.Core.csproj 1
Threema-Block in ServerSettings (7 Properties, [Category("Threema Notifications")]) 36 Zeilen
DI-Registrierungen in Program.cs (Singleton + HostedService + View-Injektion) 3
ThreemaService-Verdrahtung in SettingsView 3 Stellen

Was an die Stelle getreten ist — INotificationSink:

PolyTrader.Core/Notifications/
  ├─ INotificationSink.cs    Schnittstelle + NotificationSeverity (Info/Warning/Critical)
  └─ LogNotificationSink.cs  Übergangs-Implementierung: schreibt ins Terminal-Log

Die drei Fachdienste kennen jetzt nur noch die Schnittstelle:

Dienst Meldung Severity
MasterTraderAnalyticsJob Auto-Pause eines Master-Traders Warning
SellLadderService SELL-Floor ohne Fill erreicht, Position wird gehalten Critical
DailyReportService täglicher Supervisor-Kurzbericht Info

Zwei Entwurfsentscheidungen, die sich später auszahlen:

  1. No-Throw-Vertrag. SendAsync wirft laut Schnittstellen-Doku niemals — Kanalfehler werden intern abgefangen und protokolliert. Das ist genau die Best-Practice aus dem Watchdog-Guide („Fange Netzwerkfehler stets stumm ab, damit der Ausfall des Monitoring-Servers niemals den Hauptanwendungsfluss unterbricht"). Die Aufrufer verzichten deshalb bewusst auf eigene try/catch-Blöcke — der Code wurde dabei kürzer, nicht länger. Bindend für die kommenden RocketChat- und Telegram-Implementierungen.
  2. NotificationSeverity ist von Anfang an dabei, obwohl die Log-Implementierung sie kaum nutzt. Bei zwei Kanälen (RocketChat für alles, Telegram als Fallback für Kritisches — oder umgekehrt) ist die Einstufung die natürliche Routing-Information. Sie nachträglich einzuziehen hätte alle Aufrufstellen erneut angefasst.

Nebeneffekte — vier Linux-Themen sind mit weggefallen:

Entfallen War
libsodium 1.0.16 (Baujahr 2018) Risiko R7: .so gegen alte glibc gelinkt, Ladefehler auf Ubuntu 24.04 möglich
Microsoft.Win32.Registry (E2EHelper.cs, #if CoreWinOnly) die einzige Registry-Nutzung im gesamten Build
HttpListener auf http://*:{port}/ (Webhook) Port-Bindung + netsh-Fehlermeldung mit Windows-Semantik
System.Web.HttpUtility

Verifikation: Clean-Build fehlerfrei, 448 Tests grün, Compiler-Warnungen von 17 auf 9 gesunken (die 8 Warnungen der Threema-Bibliothek — 4× SYSLIB0014, 3× CA1416, 1× CS0472 — sind weg).

Testverbesserung nebenbei: Die drei Tests, die bisher new ThreemaService(logger, new JobManager()) als Wegwerf-Instanz injizierten, nutzen jetzt FakeNotificationSink — damit ist prüfbar, ob und mit welcher Severity gemeldet wurde. Vorher war das nicht beobachtbar.

Offen für später: RocketChatNotificationSink und TelegramNotificationSink implementieren und in Program.cs registrieren — eine Klasse je Kanal, keine Änderung an der Fachlogik. Bei mehreren aktiven Kanälen bietet sich ein CompositeNotificationSink an (leitet an alle weiter, jeder Kanal fängt seine Fehler selbst ab).

5.4 Verbleibende Netzwerkdienste

  • HttpListener nach dem Threema-Ausbau nur noch der MCP-Light-Server (127.0.0.1:{port}/mcp). Läuft auf Linux über die managed Implementierung, kein netsh nötig. Unkritisch.
  • MullvadVpnService: Der CLI-Aufruf ist portabel (UseShellExecute = false, sauber). Auf Linux heißt die CLI mullvad (typisch /usr/bin/mullvad). Achtung: HealthCheckAsync() prüft status.Contains("Connected") der Wortlaut der Linux-CLI weicht ab. Muss gegen die echte CLI verifiziert werden, sonst reconnectet der Dienst dauerhaft ins Leere und meldet nach 3 Fehlversuchen „unrecoverable".
  • Nethereum 6.1.0, Pomelo/EF Core 8, HttpClient, ClientWebSocket, System.Text.Json, AES-GCM (OpenSSL): plattformneutral, keine Anpassung nötig. Das ist der gesamte Trading-Pfad.

5.5 Windows-UI als Fallback: Variante 2 (Entscheidung 3)

Heute laufen Generic Host und Application.Run im selben Prozess; darunter 19 BackgroundService-Instanzen. Auf Linux gibt es kein „WinExe" eine Avalonia-App braucht X11/Wayland, ein Trading-Server hat typischerweise kein Display.

Entschieden: Variante 2 eingefrorener Stand. Die WinForms-UI bleibt auf dem heutigen Stand und wird nicht weiterentwickelt; später verschwindet sie vollständig aus dem Build, damit keine Altlast mitgeschleppt wird.

Was das konkret heißt:

Einfrieren Vor Beginn von L3 einen Git-Tag setzen (Vorschlag: winforms-final) und im Repo dokumentieren. Ab da keine Feature-Arbeit mehr an Ui/ und den Ui/-Ordnern der Module.
Fallback im Ernstfall Tag auschecken, bauen, starten. Greift auf dieselbe MySQL zu — Anzeige und Einstellungen bleiben voll brauchbar.
Weiterentwicklung Läuft unverändert weiter — nur eben im Trading-Kern, der ab L2 headless auf Linux läuft. Eingefroren ist die Oberfläche, nicht die Funktion.
Endgültiger Ausbau Nach Abschluss von L4: Ui/-Ordner, PolyTrader.App als WinExe, alle .Designer.cs/.resx, UseWindowsForms, favicon.ico-Einbindung und ApplicationConfiguration.Initialize() entfernen. ~4.500 LOC generierter Designer-Code fallen ersatzlos weg.

Wichtig für die Reihenfolge: Damit der eingefrorene Stand überhaupt lauffähig bleibt, darf P1 die WinForms-UI nicht kaputtmachen. Beim Entkoppeln von Core und Modulen (Zielframework net8.0, System.Drawing raus) muss der WinForms-Zweig bis zum Tag baubar bleiben — praktisch heisst das: erst taggen, dann P1 durchziehen. Sonst ist der Fallback genau in dem Moment weg, in dem man ihn braucht.

Konkreter Vorschlag zur Absicherung: Vor P1 einmal dotnet publish -c Release des heutigen Stands erzeugen und das Ergebnis zusammen mit dem Tag archivieren. Ein fertig gebautes Verzeichnis ist im Ernstfall schneller einsatzbereit als ein Checkout, der erst wieder kompiliert werden muss — und beweist nebenbei, dass der eingefrorene Stand tatsächlich läuft.

Unabhängig davon nötig: headless-fähiger Einstiegspunkt für den Linux-Betrieb, systemd-Unit mit Restart=always und Environment=TZ=…, Master-Key über EnvironmentFile mit chmod 600, Logrotate für Logs/*.jsonl.

5.6 Tests und CI

  • Testprojekt auf net8.0 (fällt mit den Referenzen).
  • WindowMenuTests.cs einziger UI-Test, neu schreiben (~0,5 PT).
  • TradeRowColoringTests.cs zieht mit dem Farbtyp-Wechsel mit (~0,2 PT).
  • AccountingReportTests.cs:131 braucht den neuen FontResolver.
  • --smoke-ui (Program.cs:500-570): konstruiert alle Views headless und hat laut Kommentar bereits Designer-Regressionen gefangen. Unbedingt erhalten Avalonia bietet Avalonia.Headless, das Muster überträgt sich gut.
  • Es gibt keine CI. Die Portierung ist der richtige Anlass, dotnet build + dotnet test auf einem Linux-Runner einzurichten sonst schleicht sich die Windows-Abhängigkeit unbemerkt zurück.

6. Aufwandsschätzung (Revision 2)

# Paket Änderung ggü. Rev. 1 Aufwand
L0 Kultur-Bug erledigt 0,5 PT
P0 Zielbild, Avalonia-Spike an einer echten View, Layout-Konventionen festzurren 23 PT
P1 Core/Module von WinForms entkoppeln, TargetFramework auf net8.0, System.Drawing raus 23 PT
P2 Kultur & Zeit: IAppClock, Zeitzone bei Installation festlegen, Kultur pinnen, Analyzer 1 PT (Entscheidung 6 vereinfacht) 23 PT
P3 Plattform-Kleinkram: Mullvad-Pfad + Status-Parsing, Unix-Dateirechte 0,5 PT (Threema entfällt) 1 PT
P3b Threema-Ausbau + INotificationSink erledigt 06.08.2026 2 PT
P3c Lizenz/Watchdog auf Deploymentcenter umstellen neu (Entscheidung 5) 23 PT
P4 PDF-Export: PDFsharp Core + IFontResolver + eingebettete Schrift 12 PT
P5 Charts auf LiveCharts2 +1 PT (Entscheidung 8) 2 PT
P6 UI-Shell: Launcher, Fenstermenü (deklarativ!), ShellUiHost, Shutdown-, Lizenz-Dialog, Widgets 57 PT
P7 Core-Views: Dashboard, Settings, Jobs, Terminal (inkl. 2 PropertyGrid-Ersätze + RichTextBox) +3 PT (Entscheidung 4) 1115 PT
P8 Modul-Fenster: CopyTrading (5), ResolutionFarming, Supervisor, Accounting (inkl. 4 PropertyGrid-Ersätze, 20 DataGrids) +2 PT (Entscheidung 4) 1420 PT
P9 Tests, --smoke-ui auf Avalonia.Headless, Linux-CI 34 PT
P10 Deployment: systemd, TZ, Logrotate, Feldtest im Zielland 0,5 PT (Lizenz-Neubindung entfällt) 34 PT
P11 WinForms endgültig ausbauen (nach L4): Ui/, Designer-Dateien, UseWindowsForms, WinExe neu (Entscheidung 3) 1 PT
Summe (offen) 4161 PT

Personentage entwickelnder Arbeit. Mit Agentenunterstützung sinkt die Kalenderzeit deutlich; der Review- und Feldtest-Anteil (P9, P10) bleibt weitgehend gleich bei einer Anwendung, die echtes Geld bewegt, ist das die richtige Reihenfolge.


7. Fahrplan

Die zentrale Erkenntnis bleibt

P1P4 brauchen kein Avalonia. Sie sind auf Windows entwickelbar und verifizierbar. Zusammen mit einem headless-Einstiegspunkt hast du für ~15 PT von rund 50 den eigentlichen Nutzen:

Der Bot läuft auf Linux. Die WinForms-UI bleibt als eingefrorener Fallback (Entscheidung 3) und greift über dieselbe MySQL auf dieselben Daten zu.

Stufe Inhalt Aufwand Ergebnis
L0 Kultur-Bug erledigt Faktor-100-Preisfehler behoben, 10 Regressionstests
L0b Threema-Ausbau + INotificationSink erledigt 36 Dateien raus, 448 Tests grün, 8 Warnungen weniger
L1 Tag winforms-final setzen → dann P0 + P1 + P2 + P3 + P3c + P4 1115 PT Core/Module sind net8.0 und plattformneutral; Zeitzone bei Installation festlegbar; Deploymentcenter angebunden; Tests laufen auf Linux
L2 Headless-Einstiegspunkt + systemd + Feldtest im Zielland 35 PT Trading läuft headless auf Linux. Meilenstein
L3 P5 + P6 + P7 (Charts, Shell, Core-Views) 1824 PT Avalonia-Grundgerüst steht; Dashboard, Settings, Terminal nutzbar
L4 P8 + P9 (Modul-Fenster, Tests, CI) 1724 PT WinForms funktional abgelöst
L5 P11 WinForms endgültig aus dem Build entfernen 1 PT Keine Altlast mehr; ~4.500 LOC Designer-Code fallen weg

Reihenfolge-Begründung

  • Der Tag winforms-final kommt VOR P1. P1 entkoppelt Core und Module von WinForms — danach ist der alte Stand nicht mehr aus dem Hauptzweig baubar. Erst taggen (und einmal dotnet publish archivieren), dann umbauen. Sonst ist der Fallback genau dann weg, wenn man ihn braucht.
  • P3c (Deploymentcenter) gehört nach L1, nicht später der headless-Betrieb in L2 hängt an der ILicensePrompt-Abstraktion. Ohne sie blockiert der Lizenzdialog den Start auf einem Server ohne Display.
  • P2 vor L2 die Zeitzone muss feststehen, bevor der erste Linux-Server Logs und Buchungen schreibt. Nachträglich umzustellen bedeutet gemischte Daten an der Tagesgrenze.

Noch offene Punkte

  1. Accounting-Zeitzone getrennt von der Anwendungszeitzone? (3.3) Empfehlung ja, verhindert rückwirkend verschobene Buchungsperioden bei einem Standortwechsel.
  2. Anzeige-Kultur ebenfalls bei der Installation festlegbar? (4.2) naheliegend, da dieselbe Begründung wie bei der Zeitzone (Auslandsbetrieb).
  3. RocketChat und Telegram: Wann sollen die echten INotificationSink-Implementierungen kommen? Bis dahin landen Auto-Pause- und Eskalationsmeldungen nur im Terminal-Log. Für den unbeaufsichtigten Linux-Betrieb (L2) wäre mindestens ein echter Kanal wichtig — sonst bemerkst du eine Auto-Pause erst beim nächsten Blick ins Log.

8. Risikoliste (Revision 3)

# Risiko Status Wirkung / Gegenmaßnahme
R1 Kulturabhängiger Parse, Faktor-100-Preisfehler behoben gefixt + 10 Regressionstests unter de-DE
R4 Lizenz-Hardware-ID ändert sich, in Containern instabil gelöst Deploymentcenter HWID v2 mit DEPLOYMENTCENTER_HWID-Override und v1→v2-Migration ohne Platzverlust
R7 libsodium 1.0.16 gegen moderne glibc entfallen mit dem Threema-Ausbau am 06.08.2026
R2 Kein WYSIWYG-Designer; Layout-Regel ist neu offen Im Spike (P0) an einer echten View erproben, bevor 16 Views folgen
R3 PropertyGrid-Ersatz durch handgebaute Formulare offen Teuerster Posten (~10 PT). [Description]-Attribute liefern die Spezifikation
R5 Zeitzonen-Semantikwechsel trifft Logs, Tagesberichte, Accounting-Perioden offen P2 vor L2; IAppClock; Wirksamkeit erst nach Neustart
R6 Headless-Server ohne Display offen ILicensePrompt/ConsoleLicensePrompt aus dem Deploymentcenter-SDK; headless-Einstiegspunkt in L2
R8 Anzeigeformate kippen auf Invariant (LANG nicht gesetzt) offen Kultur in Program.Main pinnen
R9 Keine CI Windows-Abhängigkeiten schleichen zurück offen Linux-Build in Gitea Actions ab L1
R10 Mullvad-CLI meldet auf Linux anderen Statustext offen Gegen die echte CLI im Zielland verifizieren (5.4)
R11 Doppelpflege beider UIs entfällt Variante 2 gewählt (eingefrorener Stand)
R12 Benachrichtigungen fallen still entschärft INotificationSink eingezogen; Meldungen landen im Terminal-Log
R13 Kein echter Benachrichtigungskanal bis RocketChat/Telegram da sind offen Für den unbeaufsichtigten Betrieb (L2) mindestens einen Kanal implementieren — sonst bleibt eine Auto-Pause bis zum nächsten Log-Blick unbemerkt
R14 Fallback-Stand nicht mehr baubar, weil P1 vor dem Tag lief offen Tag winforms-final + archiviertes dotnet publish vor P1 (5.5)

9. Was nicht angepasst werden muss

  • Der gesamte Trading-Pfad: PolymarketClobClient (EIP-712 über Nethereum), PolymarketApiService, PolymarketWssClient, AlchemyWssClient, CopyTradingEngine, SellLadderService, TraderMonitorService
  • Persistenz: EF Core 8 + Pomelo/MySQL, alle 15 Migrationen, EncryptedStringConverter
  • Sicherheit: SecretProtection (AES-256-GCM läuft auf Linux über OpenSSL), Master-Key-Ableitung
  • Alle Analytics: TradeAnalytics, DossierBuilder, Supervisor-Agent, OpenRouter-Client, MCP-Light-Server
  • Accounting-Logik: Ingest, Klassifikation, BWA, CSV-Export (nur der PDF-Export braucht den FontResolver)
  • Watchdog-Payload-Schema (source/instance/interval passen bereits zur Deploymentcenter-API), Modularitäts-Infrastruktur
  • 438 von 448 Tests

Das ist der Grund, warum die Schätzung trotz 24.000 LOC bei 4161 PT landet und nicht bei 150: Zwei Drittel des Codes sind bereits plattformneutral, und die Trennung ist sauber.


10. Anhang: verifizierte Fundstellen

Behoben am 06.08.2026

Thema Datei:Zeile
Kulturabhängiger Preis-Parse (Faktor 100) …CopyTrading/Services/TraderMonitorService.cs:1276-1280 → nutzt jetzt ParseDecimal
ParseDecimal testbar + dokumentiert …CopyTrading/Services/TraderMonitorService.cs:1223-1234
Kulturabhängige Parses (Regex-Gruppen) …Core/Services/PolymarketClobClient.cs:770, 784, 796, 820, 822, 827
Kulturabhängiger Unix-Timestamp …CopyTrading/Services/MasterTraderAnalyticsJob.cs:140
Neuer Regressionstest (10 Fälle, de-DE) tests/PolyTrader.Tests/ApiNumberParsingTests.cs
Threema vollständig entfernt ThreemaService.cs, libs/Threema-MsgApi-Net-Core/ (35 Dateien), ServerSettings (7 Properties), Program.cs, SettingsView.cs, PolyTrader.Core.csproj, PolyTraderSharp.sln
Neutraler Benachrichtigungsausgang src/PolyTrader.Core/Notifications/INotificationSink.cs, LogNotificationSink.cs
Fachdienste auf INotificationSink umgestellt MasterTraderAnalyticsJob.cs, SellLadderService.cs, DailyReportService.cs
Testdouble mit Assertion-Möglichkeit tests/PolyTrader.Tests/Fakes/FakeNotificationSink.cs

Offen

Thema Datei:Zeile
Windows-Zeitzonen-ID hartcodiert Ui/Views/TerminalView.cs:150
Harter Windows-Pfad (Mullvad) src/PolyTrader.Core/Models/ServerSettings.cs:73
Mullvad-Statusprüfung (Wortlaut) src/PolyTrader.Core/Services/MullvadVpnService.cs:HealthCheckAsync
UI-Contract mit WinForms-Typen src/PolyTrader.Core/Modularity/ModuleUi.cs:32, 36
Menü wird zur Laufzeit gebaut (neue Layout-Regel) src/PolyTrader.Core/Modularity/WindowMenu.cs:Populate, Ui/ShellUiHost.cs:AttachWindowMenu
System.Drawing.Color in Modul-Logik …CopyTrading/Logic/TradeRowColoring.cs:11-13
ScottPlot → System.Drawing.Bitmap Ui/Views/DashboardView.cs:207-227
Windows-only Schrift im PDF …Accounting/Logic/PdfExporter.cs:34
Schlüsseldateien ohne Rechteschutz Ui/Views/SettingsView.cs:301, 356; Program.cs:393
Headless-Smoke-Test (erhaltenswert) Program.cs:500-570
Lizenz-Gate mit modalem Dialog Licensing/LicenseGate.cs, Program.cs:98
Secrets im Klartext server_settings.xml (WatchdogToken, LicenseKey)

Referenzen

  • J:\Softwareprojekte\Deploymentcenter\docs\LICENSE_INTEGRATION_GUIDE.md Hardware-ID v2, LLS2-Cache, CLI-Schalter, v1→v2-Migration
  • J:\Softwareprojekte\Deploymentcenter\docs\WATCHDOG_INTEGRATION_GUIDE.md Ping-API, X-Agent-Token, Payload-Schema
  • J:\Softwareprojekte\Deploymentcenter\client-dotnet\Deploymentcenter.Client SDK-Quellen (LicenseClient, HardwareId, ILicensePrompt, StateStore)