Files
PolyTraderSharp/docs/umsetzungsplaene/UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration.md
T
RichardandClaude Opus 5 ca750a06f5 Watchdog-Heartbeat + LicenseLabrador-Lizenzpruefung eingebunden
Slices W-A/W-B/W-C und L-A aus UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration.

Watchdog (Dead-Man's-Switch, externer Server):
- WatchdogHeartbeatService als BackgroundService + DI-Singleton; jeder Sendeversuch
  gekapselt, ein Ausfall des Watchdogs beeintraechtigt PolyTrader nie.
- Eigene Implementierung statt Test-Client des Fremdprojekts: TLS-Pruefung bleibt
  aktiv, http:// nur fuer localhost (Agent-Token nicht im Klartext ins Netz).
- Status aus dem App-Log abgeleitet (Error mit 5-Minuten-Sticky-Fenster, entprellt),
  Lifecycle-Events started/stopping.
- Konfiguration in ServerSettings; Agent-Token [Browsable(false)] mit maskierter
  Eingabe + Statusanzeige, bei gesetztem Master-Key verschluesselt (enc:v1:).

Lizenz (LicenseLabrador, Ed25519):
- LicenseGate.RunStartupGate prueft beim Start; bei ungueltiger Lizenz wird die
  Modulliste leer gebaut, sodass nur die Core-Shell (Terminal/Einstellungen)
  startet. Bewusst kein Environment.Exit - ein Trading-Bot darf nicht mitten im
  Lauf hart sterben. TamperSuspected gilt als nicht nutzbar.
- LicenseDialog (partial + .Designer.cs) fuer Start- und Verwalten-Modus, mit
  Hardware-ID zum Kopieren; Smoke-UI konstruiert beide Modi headless.
- Master-Key wird jetzt VOR dem Host-Build geladen, da auch der Lizenzschluessel
  entschluesselt werden muss; derselbe TerminalLogger wird als Singleton
  weitergereicht, damit die Startmeldungen im Terminal-Fenster erscheinen.

Der Lizenz-SDK-Client wird per Cross-Repo-Projektreferenz auf
..\..\LicenseLabrador eingebunden, damit SDK-Fixes ohne Kopie einfliessen.

484 Zeilen Tests fuer den Heartbeat; Suite gruen (438 Tests).

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

32 KiB
Raw Blame History

Umsetzungsplan: Watchdog & LicenseLabrador in PolyTrader einbinden

Stand: 2026-07-28 · Grundlage: Review von J:\Softwareprojekte\WatchDog und J:\Softwareprojekte\LicenseLabrador. Reihenfolge ist bindend: Teil A (Blocker in den Fremdprojekten) vor Teil B (Einbindung). Wer B ohne A baut, bindet bekannte Lücken fest in den Produktivbetrieb ein.


0. Kurzfassung der Bewertung

LicenseLabrador (Server) ist handwerklich gut: keine Secrets im Repo, Argon2id, CSRF in allen Admin-Formularen, durchgängig Prepared Statements, Ed25519 über den Base64-Payload (umgeht das Kanonikalisierungsproblem elegant), Nonce-Reflexion, Rate-Limit plus Failed-Key-Sperre, Audit-Log. Der Server kann so bleiben. Die Schwächen liegen im C#-Client.

Watchdog ist funktional weit, hat aber Produktiv-Secrets im Git, ein hartkodiertes Admin-Passwort und eine Autorisierung, die jeden Agent-Token wie einen Admin behandelt. Vor der Anbindung von PolyTrader muss dort aufgeräumt werden.

Kritischster Einzelbefund für unseren Betrieb: die Hardware-ID von LicenseLabrador ist auf einem Rechner mit Mullvad-VPN nicht stabil (siehe L-1). Ohne Fix sperrt PolyTrader sich selbst aus.


0a. Nachprüfung vom 29.07.2026

Beide Projekte wurden nach dem ersten Review nachgebessert. Stand jetzt:

LicenseLabrador — integrationsbereit. Die vier Blocker sind behoben (Änderungen liegen noch uncommitted im Arbeitsverzeichnis, sollten also committet werden):

  • L-1 erledigt. HardwareId nutzt jetzt MachineGuid (bzw. /etc/machine-id) als Primärquelle, ProcessorCount ist raus, der MAC-Fallback filtert virtuelle Adapter (Mullvad, WireGuard, Docker, Hyper-V, WSL) und sortiert deterministisch. Damit ändert ein VPN-Wechsel die Hardware-ID nicht mehr — die Selbst-Aussperrung ist vom Tisch.
  • L-2 erledigt. Der gecachte Envelope wird gegen die lokale hardware_id geprüft; kopierte state.dat-Dateien werden verworfen.
  • L-3 erledigt. signatureTampered liefert jetzt TamperSuspected statt still in die Offline-Kulanz zu fallen.
  • L-5 erledigt. Das HMAC-Secret wird aus der Hardware-ID abgeleitet, und docs/SECURITY.md führt es korrekt als Laufzeit-Schutz gegen Property-Patching, nicht als kryptografische Sicherheit.
  • Das SDK baut fehlerfrei (eine CS8604-Warnung in LicenseClient.cs:47).
  • Offen: L-4 (gecachte Endpoints haben weiter Vorrang vor der Konfiguration, http:// wird weiter akzeptiert) sowie L-6 bis L-13. L-4 entschärfen wir auf unserer Seite, indem wir ausschließlich https://-Endpoints konfigurieren — da die Antwort signaturgeprüft ist, kann ein untergeschobener Endpoint keine gültige Lizenz erfinden, sondern uns höchstens in die Offline- Kulanz drängen. Kein Blocker mehr, aber Nachtrag wert.

Watchdog — integrationsbereit mit zwei Auflagen. Commits 1e453f8 und 404e184 haben die Blocker abgearbeitet; nachgeprüft am 29.07.2026:

  • Erledigt in 1e453f8: W-2 (Backdoor entfernt, nur noch password_verify), W-3 (CSRF in admin.php und index.php), W-4 (alle XSS-Vektoren escaped, inklusive e.message), W-5 (Token-Scoping greift: validateToken() wird benutzt, $requireAdmin() schützt monitor/edit, monitor/delete, icon/upload), W-6 (Status-API nicht mehr offen), W-13 (Alarm nur noch bei Zustandswechsel, zusätzlich 120 s Dämpfung pro Quelle und Art).
  • W-14 erledigt in 404e184. Evaluator.php setzt einen Monitor nur noch auf up, wenn last_status === 'ok' und die Metriken unauffällig sind. Zustandsmaschine durchgespielt: dauerhaftes status=error bleibt auf error (ein Alarm, danach Ruhe); Rückkehr auf ok erzeugt genau eine Entwarnung; Absturz → genau ein Down-Alarm; down + Rückkehr mit status=error → ein hard_error. Die Flapping-Schleife ist weg.
  • W-22 erledigt. escapeHtml liegt jetzt auf Dateiebene (index.php:630) vor allen Verwendungen. Monitore ohne Metriken — unser Fall — brechen im Detail-Fenster nicht mehr ab.
  • W-23 erledigt. session_start() steht am Anfang von api.php; der Admin-Session-Zweig funktioniert.
  • W-24 erledigt. display_errors und Stack-Traces hängen an WATCHDOG_DEBUG=1; API-Fehler liefern sauberes JSON ohne interne Details.
  • W-1 teilweise erledigt — Auflage 1. Aus dem getrackten config/config.php sind die echten Werte raus (Platzhalter), config.example.php ist ergänzt, config.local.php war schon gitignoriert und die Merge-Logik ist intakt. Aber die Secrets stehen weiter in der Git-History: git show 1e453f8:config/config.php liefert DB-Passwort, shared_key und setup_token im Klartext. Das lässt sich durch Bearbeiten von HEAD nicht heilen — alle drei müssen rotiert werden (das FTP-Passwort aus deploy_config.json gleich mit, es ist dasselbe).
  • Auflage 2 — W-16 (UTC vs. NOW()). Unverändert: cli/tick.php:39,44,106, ProxmoxPoller.php:56,117 und admin.php:115,157 mischen UTC-Strings aus PHP mit dem NOW() der DB-Session. Steht die MySQL-Zeitzone nicht auf UTC, verschieben sich Job-Fälligkeit, Lock-Leases und der Log-Cleanup um den Offset. Entweder in der DB-Session SET time_zone = '+00:00' setzen oder NOW() durchgängig durch UTC-Parameter ersetzen. Nicht integrationsblockend, aber vor dem Produktivbetrieb zu klären.
  • Weiterhin offen, ohne Einfluss auf unsere Anbindung: W-11 (api.php:157 gibt im eigenen catch weiter $t->getMessage() heraus — der neue Handler in Autoload.php macht es richtig, wird hier aber umgangen), W-15 (intervalSeconds der PS-/Bash-Agents wird weiter ignoriert; unser C#-Client sendet interval und ist korrekt), W-17 (self_ping ist weiter ein echo-Stub — es gibt also keinen Alarm, wenn der Watchdog selbst stirbt), W-7 bis W-10, W-12, W-18 bis W-21.

Konsequenz für die Reihenfolge: Schritt 1 und 2 sind erledigt. Teil B kann vollständig beginnen — B.1 bis B.3 (Heartbeat) und B.4/B.5 (Lizenz) sind nicht mehr blockiert. Parallel abzuarbeiten: Secrets rotieren, W-16, W-17.


Teil A — Blocker in den Fremdprojekten

A.1 Watchdog — Sicherheit

# Befund Datei Maßnahme
W-1 DB-Passwort, shared_key, setup_token als Defaults im getrackten config/config.php (auch in der Git-History) config/config.php:15,19,20 Datei nach config.php.example umbenennen (Muster von LicenseLabrador), aus dem Index nehmen, alle drei Secrets rotieren
W-2 Hartkodiertes Admin-Passwort, vor der DB-Prüfung ausgewertet public/admin.php:31 Ersatzlos entfernen; nur noch users-Tabelle + password_verify
W-3 Kein CSRF-Schutz im Admin — Monitor löschen, Proxmox-Target anlegen, Token widerrufen sind cross-site auslösbar public/admin.php (alle POST-Zweige) Csrf-Klasse aus LicenseLabrador übernehmen (1:1 portierbar)
W-4 Stored XSS: Heartbeat-message und Disk-Namen landen ungefiltert per innerHTML im Dashboard public/index.php:707,731,744,770 textContent statt innerHTML, oder clientseitig escapen
W-5 Jeder gültige Agent-Token autorisiert jeden Endpunkt — auch /api/monitor/delete, /api/monitor/edit, /api/link, /api/icon/upload public/api.php:66-77 Zwei Berechtigungsstufen trennen: Agent-Token nur für heartbeat/event; verwaltende Routen nur mit Admin-Session oder shared_key. TokenManager::validateToken() (prüft die Source-Bindung) wird derzeit nirgends aufgerufen — einbinden
W-6 /api/status, /api/events, /api/lineage sind bewusst unauthentifiziert und zusätzlich von der Basic-Auth ausgenommen public/api.php:60, .htaccess:6 Für uns nicht akzeptabel: „polytrader ist seit 14:03 down" ist eine verwertbare Information. Lesezugriff hinter Session oder eigenen Read-Token legen
W-7 Installer-Download läuft vor dem Login-Check; {{TOKEN}}/{{SOURCE}} werden ungefiltert in ein PowerShell-Skript interpoliert, das als SYSTEM ausgeführt wird public/admin.php:58-84 Hinter $isLoggedIn verschieben, Platzhalter strikt validieren ([A-Za-z0-9_-]), Host nicht aus HTTP_HOST ableiten
W-8 Agent-Token als maschinenweite Umgebungsvariable, Default-URL http:// agents/windows/install.ps1.tmpl:104, agents/*/watchdog-agent.*:9,4 Token in eine ACL-geschützte Datei unter C:\ProgramData\Watchdog; HTTPS erzwingen
W-9 SVG-Upload in denselben Origin, mkdir(0777) public/api.php:284-296, admin.php:240 SVG entfernen oder als Content-Disposition: attachment ausliefern; 0755
W-10 Proxmox: allowSelfSigned = true als Default, cert_fingerprint nie benutzt, Token-Secret im Klartext in der DB src/ProxmoxClient.php:18,76, schema:87 Fingerprint-Pinning umsetzen (steht so in HANDOFF §8), Secret verschlüsseln
W-11 500-Antwort gibt $t->getMessage() an den Client (DB-Fehlerdetails) public/api.php:141-144 Generische Meldung, Details ins event_log
W-12 FTP-Deploy-Passwort im Klartext, identisch mit DB- und Admin-Passwort scripts/deploy_config.json Eigenes Passwort, rotieren

A.2 Watchdog — funktionale Fehler

# Befund Wirkung
W-13 api.php:120 alarmiert bei jedem Heartbeat mit status=error/warning sofort Ein dauerhaft fehlerhafter Dienst im 30-s-Takt erzeugt 120 Telegram-Nachrichten pro Stunde. Widerspricht §6 des Umsetzungsplans („nur bei Zustandswechsel")
W-14 upsertHeartbeat erzwingt state = IF(state="down","up",…), der Evaluator setzt bei frischem Heartbeat zusätzlich error → up („recovered") Ein Dienst, der dauerhaft status=error meldet, pendelt im Minutentakt zwischen „recovered" und „hard_error" — mit Alarm bei jedem Wechsel
W-15 Agents senden intervalSeconds, die API liest nur interval/expected_interval_sec Der gemeldete Takt wird still verworfen, es bleibt bei 60 s. Der C#-Client sendet interval und ist zufällig korrekt
W-16 PHP schreibt UTC-Strings, MySQL vergleicht mit NOW() (Session-Zeitzone) Job-Fälligkeit, Lock-Leases und Log-Cleanup verschieben sich um den UTC-Offset, sofern die DB nicht auf UTC steht. Betrifft cli/tick.php:39,44,106 und admin.php:108
W-17 self_ping ist ein echo-Stub, proxmox_smart hat keinen Handler, first_contact_deadline_utc / expect_running / ack_until_utc werden nie ausgewertet Der „Wer wacht über den Wächter"-Mechanismus fehlt komplett; der Nie-Start-Alarm ebenfalls. Beides sind Abnahmekriterien aus dem Plan
W-18 warning_digest schickt HTML durch notifyImmediate, das per htmlspecialchars escaped Im Telegram erscheinen <b>-Tags als Text
W-19 Nur der Heartbeat-Zweig liegt im try/catch, danach läuft der Code ohne Fehlerbehandlung weiter Uneinheitliche Fehlerbilder
W-20 Kein Rate-Limit auf /api/heartbeat (Plan §14 fordert es) Ein durchdrehender Client flutet die DB
W-21 Keine Tests im gesamten Projekt (Plan §13 fordert Unit-Tests der Zustandsmaschine) W-13/W-14 wären mit einem Test aufgefallen

Mindestumfang vor der Einbindung: W-1, W-2, W-5, W-13, W-14, W-16. Ohne W-13/W-14 ist der Watchdog für einen produktiven Trading-Bot unbrauchbar, weil jeder echte Fehler in einer Nachrichtenflut untergeht.

A.3 LicenseLabrador — Client

# Befund Datei Maßnahme
L-1 Hardware-ID ist instabil. Die „erste" aktive Netzwerkkarte wird per FirstOrDefault ohne stabile Sortierung gewählt, dazu ProcessorCount HardwareId.cs:47, :40 Blocker für uns. Mullvad, Hyper-V und Docker legen virtuelle Adapter an und wieder ab — jede Änderung erzeugt eine neue HWID, damit eine neue Aktivierung, damit max_activations (Default 2) erschöpft: PolyTrader sperrt sich selbst aus. Fix: nur MachineGuid als Primärquelle; MAC nur als Fallback, wenn die Registry nichts liefert, und dann sortiert und auf physische Adapter gefiltert; ProcessorCount entfernen
L-2 Offline-Cache ist maschinen-portabel: geprüft wird nur payload.license_key == licenseKey, nicht hardware_id und nicht product LicenseClient.cs:195 Wer state.dat von einer aktivierten Maschine kopiert und das Netz blockiert, läuft bis zu 168 h auf beliebig vielen Rechnern. Der wirksamste Angriff auf das gesamte System — und mit zwei Vergleichen behoben
L-3 Erkannte Signaturmanipulation (signatureTampered) wird verworfen und fällt still in die Offline-Kulanz LicenseClient.cs:132-135 LicenseState.TamperSuspected zurückgeben und protokollieren
L-4 Kein TLS-Zwang; Endpoints aus dem Cache haben Vorrang vor der Konfiguration LicenseClient.cs:276-292, :342 http://-Endpoints ablehnen; konfigurierte Endpoints zuerst versuchen, gecachte nur als Ergänzung
L-5 HMAC-„Härtung" mit Klartext-Konstante im Assembly, geprüft mit demselben Prozess-Secret LicenseClient.cs:23,172 Schützt gegen nichts, was ein Angreifer nicht in derselben Sitzung mitpatcht. Als Obfuskation behalten, aber in docs/SECURITY.md nicht mehr als Schutzmaßnahme führen
L-6 Offline-.lic ohne Hardware-Bindung bzw. ohne Key gilt für jede Maschine bzw. jeden Key LicenseClient.cs:232,242, offline_license.php:62 Im Admin-Dialog hardware_id verpflichtend machen (Ausnahme nur mit expliziter Warnung); ein geleaktes Notfall-File ist sonst ein Universal-Crack bis valid_until
L-7 Session-Cookie ohne secure-Flag server/src/Auth.php:11 'secure' => true + HTTPS erzwingen
L-8 key_id wird signiert und übertragen, vom Client aber ignoriert Signature.cs Ohne Auswertung ist keine Schlüsselrotation möglich, ohne alle Clients gleichzeitig zu tauschen. Für uns momentan verschmerzbar, aber notieren
L-9 LicenseClient erzeugt einen HttpClient pro Instanz und ist nicht IDisposable LicenseClient.cs:28 Socket-Leak bei wiederholter Instanziierung; HttpClient injizierbar machen
L-10 app_version hartkodiert "1.0.0" LicenseClient.cs:325 Aus LicenseConfig befüllen — der Server pflegt die Spalte bereits
L-11 strtotime() liefert bei ungültigem Datum falsevalid_until: false im JSON → Deserialisierung wirft → stilles „keine Lizenz" offline_license.php:55 Datum validieren
L-12 Nur 5 Unit-Tests; EvaluateOfflineFallback, Statusabbildung und Grace-Ablauf sind ungetestet ClientTests.cs Tests für L-1/L-2 mitliefern
L-13 create_admin.php: Minimum 6 Zeichen, Passwort wird im Klartext angezeigt server/tools/create_admin.php Minimum 12, Eingabe verdecken

Mindestumfang vor der Einbindung: L-1, L-2, L-3, L-4.


Teil B — Einbindung in PolyTrader

B.0 Leitentscheidungen

  1. Eigene Client-Implementierung für den Watchdog. Der mitgelieferte WatchdogTestClient deaktiviert die Zertifikatsprüfung vollständig (WatchdogClient.cs:42). Wir schreiben einen schlanken WatchdogHeartbeatService in PolyTrader.Core — der HTTP-Vertrag ist trivial (ein POST mit JSON und einem Header) und wir behalten die Kontrolle über TLS, Timeouts und Fehlerverhalten.
  2. LicenseLabrador.Client als Projektreferenz, nicht als kopierter Code — dann fließen Fixes aus Teil A.3 automatisch ein. Voraussetzung: L-1 bis L-4 sind dort behoben.
  3. Kein Environment.Exit bei ungültiger Lizenz. docs/INTEGRATION.md schlägt das vor; für einen Trading-Bot mit offenen Positionen ist ein harter Abbruch gefährlich. Stattdessen: Trading-Module gar nicht erst starten (Startfall) bzw. geordnet stoppen (Laufzeitfall).
  4. Secrets über SecretProtection. Watchdog-Token und Lizenzschlüssel werden mit dem bestehenden AES-GCM-Master-Key (POLYTRADER_MASTER_KEY) verschlüsselt abgelegt — dieselbe Mechanik wie für Wallet-Keys.
  5. Heartbeat-Metriken bleiben schlank. Version, aktive Module, Uptime. Keine Kontostände, Positionen, Wallet-Adressen oder Trader-Namen — der Watchdog-Server liegt beim Hoster und seine Status-API war bis W-6 öffentlich lesbar.

B.1 Slice W-A — Heartbeat-Grundgerüst ERLEDIGT (29.07.2026)

Umgesetzt in WatchdogHeartbeatService.cs mit 21 Tests in WatchdogHeartbeatTests.cs. Abweichungen und Ergänzungen gegenüber der Planung unten:

  • TLS-Zwang implementiert: TryBuildEndpoint akzeptiert nur https://; http:// ausschließlich für Loopback-Adressen (lokales Testen). Damit kann der Agent-Token nicht versehentlich im Klartext übertragen werden — der Grund, warum der mitgelieferte Testclient nicht verwendet wird.
  • Intervall wird nach unten begrenzt (10 s), damit ein Fehlkonfigurierter Client den Watchdog nicht flutet (W-20 ist serverseitig weiter offen).
  • Log-Entprellung: nur der erste Fehlversuch und die Erholung werden protokolliert, nicht jeder Tick. Sonst hätte ein längerer Watchdog-Ausfall das Terminal geflutet.
  • Settings-Quelle injizierbar (Func<ServerSettings>), damit der Dienst ohne Datei testbar ist.
  • Token darf verschlüsselt sein: SecretProtection.Unprotect reicht Klartext unverändert durch und entschlüsselt enc:v1:…, wenn ein Master-Key gesetzt ist. Kein neuer Fehlerpfad, wenn kein Master-Key existiert.
  • UI — vollständig über das Settings-Fenster bedienbar: Enabled, URL, Source, Instance und Intervall erscheinen automatisch im bestehenden PropertyGrid (Kategorie „Watchdog Monitoring"). Der Agent-Token ist bewusst [Browsable(false)] und wird über den Designer-Button „Watchdog-Token setzen …" mit maskierter Eingabe gepflegt — kein Secret offen im Fenster und kein Handanlegen an der server_settings.xml. Bei gesetztem Master-Key wird er über SecretProtection.Protect verschlüsselt abgelegt, sonst mit deutlichem Hinweis im Klartext. Die schreibgeschützte Anzeige WatchdogTokenStatus zeigt „nicht gesetzt" / „gesetzt (verschlüsselt)" / „gesetzt (Klartext)". Zweiter Button: „Test-Heartbeat senden" — nutzt die gespeicherten Werte, damit ein Erfolg keine Konfiguration bestätigt, die nicht auf der Platte liegt.
  • Änderungen greifen ohne Neustart. ExecuteAsync wertet die Konfiguration in jedem Durchlauf neu aus (1-s-Poll, Sendezeitpunkt über eine Fälligkeit, der Takt bleibt also exakt) statt einen PeriodicTimer mit fest verdrahtetem Intervall zu benutzen. Ein-/Ausschalten, Takt-, URL- und Token-Änderungen wirken sofort nach dem Speichern; Zustandswechsel werden je Ursache genau einmal geloggt. Zwei Tests fixieren das Live-Ein- und -Ausschalten.

Ursprüngliche Planung:

Neu: src/PolyTrader.Core/Services/WatchdogHeartbeatService.cs (BackgroundService)

  • ExecuteAsync: PeriodicTimer im konfigurierten Takt (Default 60 s). Jeder Sendeversuch in try/catch — ein nicht erreichbarer Watchdog darf PolyTrader niemals beeinträchtigen.
  • Payload: source, instance, type: "heartbeat", status, message, interval (nicht intervalSeconds, siehe W-15), group, metadata.
  • Header X-Watchdog-Key mit dem entschlüsselten Agent-Token.
  • HttpClient aus SocketsHttpHandler wie die übrigen Clients in Program.cs, mit Standard-Zertifikatsprüfung und 10 s Timeout.
  • Registrierung in Program.cs neben MarketSyncService: services.AddHostedService<WatchdogHeartbeatService>();

Konfiguration in ServerSettings (src/PolyTrader.Core/Models/ServerSettings.cs), analog zu den Threema-/Mullvad-Feldern:

WatchdogEnabled          bool    = false
WatchdogUrl              string  = ""          // https:// erzwungen
WatchdogSource           string  = "polytrader"
WatchdogInstance         string  = "prod-1"
WatchdogIntervalSeconds  int     = 60
WatchdogToken            string  = ""          // via SecretProtection verschlüsselt

Bedienung über die bestehende SettingsView (Designer-fähig, partial + .Designer.cs — siehe UI-Regel), mit einem „Test-Heartbeat senden"-Button.

Abnahme: Monitor erscheint im Watchdog-Dashboard, last_seen_utc läuft mit. PolyTrader startet und arbeitet unverändert, wenn WatchdogUrl falsch oder der Server aus ist.

B.2 Slice W-B — Status aus dem Log ableiten ERLEDIGT (31.07.2026)

Umgesetzt im WatchdogHeartbeatService. Abweichungen gegenüber der Planung:

  • Nur Error eskaliert, keine Warnungs-Eskalation: App-Warnungen (VPN-Reconnect, „kein Master-Key" …) sind zu häufig, um daraus einen Monitor-Alarm zu rechtfertigen. Warning/Info lassen den Status auf ok. Das vermeidet auch eine Rückkopplung, wenn der Dienst selbst warnt.
  • Entprellung über ein Nachwirkfenster (ErrorStickyWindow = 5 min): Nach einem Error meldet der Heartbeat so lange status=error, bis 5 Minuten ohne neuen Fehler vergangen sind — dann wieder ok. Kein Flackern error→ok→error, Erholung wird genau einmal gemeldet. (Der 30-Minuten- Notbehelf aus der Planung entfällt, weil W-13 im Watchdog behoben ist und ohnehin nur bei Zustandswechsel alarmiert wird.)
  • Sanitize() entfernt Wallet-Adressen/Keys (0x…-Hex) und verschlüsselte Secrets (enc:v1:…) und kürzt auf 200 Zeichen, bevor eine Fehlermeldung an den (extern gehosteten) Watchdog geht. Beträge werden bewusst nicht gestrippt (würde die Meldung entwerten; der Kanal ist nur für den Betreiber).
  • Angedockt über TerminalLogger.OnLogMessage (synchron), Abmeldung in Dispose.

Ursprüngliche Planung: Der Heartbeat soll status=error melden, wenn PolyTrader in Not ist.

  • TerminalLogger.OnLogMessage abonnieren, Fehler der letzten n Intervalle zählen.
  • Ableitung: Error im Fenster → status=error mit der letzten Fehlermeldung als message; Warningstatus=warning; sonst ok.
  • Entprellen ist Pflicht, solange W-13 im Watchdog nicht behoben ist: sonst löst jeder Heartbeat eine eigene Telegram-Nachricht aus. Bis dahin status=error frühestens alle 30 Minuten erneut senden.
  • Meldungstexte kürzen (≤ 200 Zeichen) und keine Wallet-Adressen, Keys oder Beträge durchreichen. Ein Filter über die bekannten Secret-Präfixe gehört dazu.

B.3 Slice W-C — Lifecycle-Events ERLEDIGT (31.07.2026)

Umgesetzt im WatchdogHeartbeatService (SendEventAsyncPOST /api/event):

  • Start: kind: "started" mit App-Version, genau einmal, sobald der Dienst aktiv wird (in der Sendeschleife, nach AppHost.Start()), vor dem ersten Heartbeat.
  • Geplantes Beenden: StopAsync sendet kind: "stopped_graceful" (best effort, 3-s-Timeout, blockiert das Herunterfahren nie). Läuft über den bestehenden Pfad AppHost.StopAsync() nach Application.Run + den 10-Sekunden-Bestätigungsdialog.
  • Damit unterscheidet der Watchdog „sauber beendet" (Monitor → stopped, kein Alarm) von „abgestürzt" (Staleness → genau ein Down-Alarm) — der eigentliche Mehrwert.
  • Tests decken „started"→„stopped_graceful" über den Dienst-Lebenszyklus sowie den /api/event-Vertrag ab.

Abnahme (manuell, live): PolyTrader über die Menüleiste beenden → Monitor geht auf stopped, kein Alarm. PolyTrader per Task-Manager abschießen → nach Interval × Faktor + Grace genau ein Down-Alarm.


Stand 31.07.2026: Watchdog-Seite (B.1/B.2/B.3) und Lizenz-Seite (B.4, B.5 vereinfacht) umgesetzt; 438 Tests grün, --smoke-ui ok. Offen nur noch die live-Abnahme (echter Server) und die Fremdprojekt-Auflagen (Watchdog-Secrets rotieren, UTC/NOW(), self_ping-Stub).

B.4 Slice L-A — Lizenzprüfung beim Start ERLEDIGT (31.07.2026)

Umgesetzt in Licensing/LicenseGate.cs, Startdialog in Ui/LicenseDialog.cs (+.Designer.cs), Verdrahtung in Program.cs, Schlüssel-/Statusfeld in ServerSettings.cs. Abweichungen und Ergänzungen gegenüber der ursprünglichen Planung unten:

  • Ort: LicenseGate liegt im App-Projekt (PolyTraderSharp.Licensing), nicht in Core. Grund: Program.Main (App) ist der einzige Aufrufer, und so bleibt die (cross-repo) SDK-Referenz auf PolyTrader.App beschränkt statt in Core alle Module zu belasten.
  • SDK-Einbindung: LicenseLabrador.Client als Cross-Repo-Projektreferenz von PolyTrader.App auf ..\..\LicenseLabrador\... (Fixes am SDK fließen ohne Kopie ein). Baut sauber, kein BouncyCastle-Konflikt mit Nethereum.
  • Slug pt (nicht polytrader), Endpoint https://license.mhdf.de, eingebetteter Public-Key. End-to-End verifiziert (ohne Aktivierung zu verbrauchen): validate mit bewusst ungültigem Schlüssel liefert signiertes not_found/„Invalid license key" (⇒ Slug pt bestätigt, Produkt existiert), Nonce wird reflektiert, und die Server-Signatur verifiziert gegen den eingebetteten Public-Key (openssl Ed25519 „Verified Successfully"). Der Client wird echte Antworten also akzeptieren.
  • Startablauf: Master-Key wird jetzt vor dem Host-Build geladen (ConfigureSecretProtection(bootLog)), dann der Gate. Bei IsUsable == false → modaler Dialog (Schlüssel eingeben, Hardware-ID anzeigen + kopieren, „eingeschränkt starten"); bleibt es ungültig, wird die Modulliste leer gebaut → nur die Core-Shell (Terminal/Settings) startet. Kein Environment.Exit. TamperSuspected wird als nicht nutzbar behandelt und im Dialog benannt.
  • Konfiguration wie beim Watchdog-Token: ServerSettings.LicenseKey ([Browsable(false)], bei Master-Key verschlüsselt) + Statusanzeige. Der Schlüssel 9KHEZ-… ist bereits in server_settings.xml hinterlegt → beim ersten echten Start validiert der Gate automatisch und aktiviert auf dieser Maschine (kein Dialog nötig, falls gültig).
  • Ein Dialog für beide Wege (31.07.2026): Der Settings-Toolstrip-Button „Lizenz prüfen / setzen …" öffnet denselben LicenseDialog wie der Start, nur im Verwalten-Modus (startupContext:false, Button „Schließen" statt „eingeschränkt starten"). Damit validiert man auch aus den laufenden Einstellungen gegen den Server (Hardware-ID sichtbar/kopierbar); der frühere einfache Text-Prompt entfällt. Ein gültig validierter Schlüssel wird verschlüsselt gespeichert und gilt ab dem nächsten Start.
  • Verifikation: Solution baut fehlerfrei, 430 Tests grün, --smoke-ui konstruiert zusätzlich license.dialog. Eigene Unit-Tests für den Gate wurden bewusst nicht angelegt: er ist dünner Leim über dem SDK (das seine eigenen Krypto-Tests mitbringt), und ein Test bräuchte eine Referenz vom Testprojekt auf die WinExe. Stattdessen deckt der Smoke-Test Konstruktion/Compile ab.
  • Offen (bewusst): L-4 bleibt serverseitige Hygiene — sicherstellen, dass die endpoints- Einstellung im Lizenz-Admin https oder leer ist (gecachte Endpoints hätten sonst im SDK Vorrang). Und die eigentliche erfolgreiche Aktivierung mit dem echten Schlüssel passiert erst beim realen Start aus PolyTrader (mit dessen Hardware-ID) — von hier aus bewusst nicht getestet, um keinen Aktivierungs-Slot auf der falschen Maschine zu verbrennen.

Ursprüngliche Planung:

Neu: src/PolyTrader.Core/Security/LicenseGate.cs

  • Wird in Program.Main nach ConfigureSecretProtection und vor AppHost.Start() aufgerufen — der Lizenzschlüssel liegt verschlüsselt und braucht den Master-Key.
  • LicenseConfig: ProductSlug = "polytrader", eingebetteter PublicKeyBase64, Endpoints ausschließlich https://.
  • Ergebnis:
    • IsUsable == true → normaler Start. Bei ValidOffline zusätzlich eine sichtbare Restlaufzeit im Launcher.
    • IsUsable == false → Lizenzdialog (Schlüssel eingeben, Hardware-ID anzeigen und kopierbar machen), Trading-Module werden nicht registriert. Der Rest der Shell darf starten, damit Terminal und Settings erreichbar bleiben.
  • TamperSuspected (nach L-3) wird deutlich angezeigt und wie „nicht nutzbar" behandelt.

Wichtig: Der Schlüssel gehört in die verschlüsselte Ablage, nicht in appsettings.json.

B.5 Slice L-B — Revalidierung im Betrieb VERWORFEN (Entscheidung 31.07.2026)

Es gibt bewusst KEINE Laufzeit-Revalidierung. Richards Vorgabe: im laufenden Betrieb darf lizenzseitig gar nichts passieren — die Software darf niemals mitten in einer Sitzung aufhören zu arbeiten (offene Positionen, laufende API-Aufrufe). Die einzige Durchsetzung ist der Startup-Gate (B.4): beim nächsten Start einer ungültigen/widerrufenen/kopierten Lizenz startet die Software nicht mehr voll, sondern zeigt die Aufforderung, eine (neue) gültige Lizenz anzugeben. Kein BackgroundService, kein 12-h-Timer, kein Netzwerkaufruf während der Sitzung.

Warum das reicht (Bedrohungsmodell = Schutz vor einfachem Kopieren auf ein fremdes System): Die Software läuft überwiegend auf eigenen Systemen. Geschützt werden soll nur gegen „jemand kopiert die Software auf ein eigenes System und macht dort weiter". Das leistet der Startup-Gate durch drei Mechanismen des SDK, alle beim Start:

  1. Hardware-gebundene Aktivierung — ein fremdes System hat eine andere MachineGuid ⇒ andere Hardware-ID ⇒ braucht einen eigenen Aktivierungs-Slot.
  2. max_activations — sind alle Slots belegt, liefert der Server activation_limit ⇒ die Kopie startet nicht (nur die Aufforderung erscheint).
  3. Hardware-gebundener Offline-Cache (L-2-Fix) — ein mitkopierter state.dat gehört zu einer fremden Hardware-ID und wird offline verworfen ⇒ die Kopie kann sich auch ohne Netz nicht „durchmogeln".

Zwei Stellschrauben im Lizenz-Admin (Produkt pt):

  • max_activations großzügig auf die Zahl der eigenen Maschinen + Reserve setzen (ein Windows-Neuinstall erzeugt eine neue MachineGuid = neuer Slot; alte Aktivierungen im Admin freigeben). Sonst sperrt man sich bei einem Reinstall selbst aus.
  • default_cache_ttl_hours (Offline-Kulanz, Default 168 h/7 Tage) = wie lange eine eigene, bereits aktivierte Maschine ohne Serverkontakt neu starten darf. Für rein eigene Systeme darf das gern länger sein. Die Kopie profitiert davon nicht (Punkt 3).

Verhalten bei fehlender/ungültiger Lizenz beim Start (aktuell umgesetzt): modaler Dialog (Schlüssel eingeben / Hardware-ID kopieren / „eingeschränkt starten"). Ohne gültige Lizenz startet nur die Core-Shell (Terminal + Einstellungen), keine Module — die Software „arbeitet" also nicht, bleibt aber bedienbar genug, um eine Lizenz einzutragen. Wer stattdessen ein hartes Beenden statt des eingeschränkten Modus will, ist eine Ein-Zeilen-Änderung im Dialog (offen).

B.6 Slice L-C — Predictalytics

Beide Dienste sind mandantenfähig ausgelegt: ein zweites Produkt predictalytics im LicenseLabrador-Admin, ein zweiter Monitor im Watchdog. Der Heartbeat-Service ist bewusst projektunabhängig geschrieben und lässt sich unverändert übernehmen. Fällt an, sobald Predictalytics produktiv geht.


Reihenfolge und Aufwand

Schritt Inhalt Abhängigkeit
1 Watchdog A.1 (W-1, W-2, W-5) + A.2 (W-13, W-14, W-16)
2 LicenseLabrador A.3 (L-1 bis L-4) + Tests
3 B.1 Heartbeat-Grundgerüst + Settings-UI 1
4 B.2 Status aus dem Log 3
5 B.3 Lifecycle-Events 3
6 B.4 Lizenzprüfung beim Start 2
7 B.5 Revalidierung 6
8 Restliche Befunde aus A.1/A.2/A.3
9 B.6 Predictalytics 3, 6

Schritte 1 und 2 sind unabhängig voneinander und können parallel laufen.


Offene Entscheidungen für Richard

  1. Wo läuft der Watchdog? Aktuell ein Shared-Webserver bei derselben Hosting-Firma. Für den Zweck („Außensicht auf einen Server-Crash") reicht das, solange es nicht dieselbe Maschine ist. Der self_ping an healthchecks.io (W-17) fehlt aber noch komplett — bis dahin gibt es keinen Alarm, wenn der Watchdog selbst stirbt.
  2. Lizenzierung für uns selbst oder für Kunden? Wenn PolyTrader vorerst nur auf unseren eigenen Maschinen läuft, ist die Offline-Kulanz von 168 h großzügig und L-2 entsprechend entspannter. Sobald Dritte die Software bekommen, sind L-1, L-2 und L-6 harte Blocker.
  3. max_activations sollte für unsere eigene Lizenz großzügig gesetzt werden (5+), solange L-1 nicht behoben ist — sonst reicht ein VPN-Wechsel für eine Aussperrung.