Files
PolyTraderSharp/docs/archiv/umsetzungsplaene/UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration.md
T
RichardandClaude Opus 5 6218a04fe4 Eine Roadmap statt fuenfzehn Plandokumente; Altbestand ins Archiv
Der Status des Projekts stand verstreut in elf Umsetzungsplaenen, drei
Konzepten, der Linux-Analyse und dem Projektstand - teils widersprechend, teils
wochenlang veraltet. Ab jetzt gibt es genau eine Statusquelle.

docs/ROADMAP.md (neu):
- Alle Vorhaben in vier Stufen A bis D, plus technische Schuld und Verlauf.
  Die Stufen sind eine Reihenfolge, keine Termine: jede schafft die
  Voraussetzung fuer die naechste.
- Statuszeichen: erledigt / offen / blockiert (mit Ursache) / bewusst
  zurueckgestellt / Idee, nicht beschlossen. Damit ist das, was wir NICHT bauen
  wollen, sichtbar vorgehalten statt unauffindbar in einem Plan zu schlummern.
- Inhaltlich getragen, nicht nur verlinkt: je Vorhaben Ziel, Phasen,
  Akzeptanzkriterien, offene Entscheidungen und Leitplanken aus den Quelldokumenten.
- Sichtbar gemacht, was vorher zwischen den Dokumenten verborgen lag:
  CopyTrading Phase 1 ist der Engpass der gesamten Roadmap (MarketMaking und
  BundleArbitrage haben harte Voraussetzungen darauf), und die Sniper-Metriken
  aus Phase 3.2 sind ein Spezialfall des StrategieDrift-Fingerprints - zusammen
  bauen statt doppelt.

Archiv (docs/archiv/):
- 15 Dokumente verschoben (11 Umsetzungsplaene, 3 Konzepte, ANALYSE-Linux-Portierung).
  Sie bleiben die Bauanleitungen mit Code-Bezuegen, Risikotabellen und
  Begruendungen - eingefroren ist nur ihr Status.
- archiv/README.md ordnet jedes Dokument seinem Roadmap-Punkt zu.

Verweise nachgezogen - der eigentliche Aufwand:
- 25 Markdown-Links repariert. 15 davon verschiebungsbedingt (eine Ebene
  tiefer), der Rest war schon vorher falsch: die Ideensammlung verlinkte
  Quellcode relativ zum Repo-Wurzelverzeichnis statt zu docs/.
- 12 Dateien ausserhalb von docs/ verwiesen in Kommentaren auf die Plaene
  (csproj, props, setup.json, sechs Quelldateien) - alle auf archiv/ umgebogen.
- Verweise auf Dateien, die der Fruehjahrsputz geloescht hat (Ui/,
  Program.cs, WindowMenuBar), zu Klartext entschaerft statt tote Links zu lassen.
- Gegenprobe: 85 Links geprueft, 0 kaputt. Build gruen, 476 Tests gruen.

PROJEKTSTAND.md entdoppelt: Abschnitt "Offen" verweist jetzt auf die Roadmap.
Arbeitsteilung ist damit klar - Projektstand sagt was IST, Roadmap was KOMMT.

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

456 lines
33 KiB
Markdown
Raw 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.
# Umsetzungsplan: Watchdog & LicenseLabrador in PolyTrader einbinden
> # ⛔ ABGELÖST — nicht mehr umsetzen
>
> **Dieser Plan wird nicht ausgeführt.** Watchdog und LicenseLabrador als getrennte Dienste
> sind durch das **Deploymentcenter** ersetzt, das Lizenz, Monitoring, Fehler-Reporting und
> Updates in einem Dienst zusammenfasst.
>
> **Maßgeblich ist stattdessen:**
> [UMSETZUNGSPLAN-Deploymentcenter-Integration.md](./UMSETZUNGSPLAN-Deploymentcenter-Integration.md)
>
> **Vollzogen am 22.08.2026** (Schnitt D-6): `LicenseLabrador.Client` ist aus `lib/nuget` und
> aus dem Quellen-Mapping in `NuGet.Config` entfernt, `Licensing/LicenseGate.cs` und
> `Ui/LicenseDialog.cs` sind mit dem WinForms-Ausbau gelöscht. Im Code findet sich
> LicenseLabrador nur noch als historische Erwähnung in Kommentaren und Plandokumenten.
> Die `Watchdog*`-Felder in `ServerSettings` sind **bewusst geblieben**: Schnitt D-1 hat sie
> auf die Deploymentcenter-API **umgewidmet** statt sie zu ersetzen — sie sind in Gebrauch.
>
> **Serverseitig noch offen:** `watchdog.mhdf.de` und `license.mhdf.de` abschalten. Das darf
> erst geschehen, wenn die Live-Abnahme des Deploymentcenters steht — vorher fehlt die
> Rückfallebene. Mit dem Abschalten erledigen sich auch die beiden Auflagen aus diesem Plan
> (Watchdog-Secrets rotieren, UTC/`NOW()`-Mix).
>
> Der Text unten bleibt als **Verlaufsdokument** stehen. Die Dateiverweise darin zeigen auf
> inzwischen gelöschte Pfade; ihr Inhalt liegt im Git-Tag `winforms-final`.
---
> Stand: 2026-07-28 · Grundlage: Review von `J:\Softwareprojekte\WatchDog` und
> `J:\Softwareprojekte\LicenseLabrador`.
> **Reihenfolge war 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 `false``valid_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](../../../src/PolyTrader.Core/Services/WatchdogHeartbeatService.cs)
mit 21 Tests in [WatchdogHeartbeatTests.cs](../../../tests/PolyTrader.Tests/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`;
`Warning``status=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` (`SendEventAsync``POST /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](../../../src/PolyTrader.Core/Models/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.