WinForms-Host durch die Avalonia-Shell abgeloest

Merge von main: die dort entstandene Deployment-Center-Integration (Lizenz,
Heartbeat mit DB-Health-Check, Fehler-Stream, UpdateService) ist jetzt Teil
des plattformneutralen Kerns. Predictalytics.WinFormsHost ist entfernt.

Nach Predictalytics.Hosting gezogen:
- DcConfig, DcApiClient, DcErrorSink, DcHeartbeatService, DcUpdateService
  unveraendert - sie waren bereits plattformneutral
- DcErrorReporter ohne Application.ThreadException und MessageBox; der
  UI-Thread-Handler liegt jetzt beim Host und ruft ReportUiThreadException
- LicenseGuard/LicenseSession ohne Dialog und ohne WinForms-Timer. Neu:
  TryUseCachedAsync, ActivateAsync, StartPeriodicRevalidation ueber
  PeriodicTimer. Die Unterscheidung transienter Fehler und die Warnung vor
  ablaufender Gnadenfrist sind unveraendert uebernommen.
- Dc-Einstellungen von AppSettings nach PredictalyticsOptions; die
  Watchdog-Einstellungen entfallen
- DcErrorSink im LoggingSetup, Startbanner nutzt DcConfig.AppVersion

BuildInfo.targets wird jetzt von Predictalytics.Hosting importiert.

In der Avalonia-Shell nachgezogen:
- Menue Deployment Center mit Update-Suche und Lizenzstatus
- Einstellungsgruppe Deployment Center statt Watchdog, Update-Kanal als
  ComboBox, Server-URL nur zur Anzeige
- Heartbeat-Snapshot mit SELECT-1-Probe wie in der WinForms-Fassung
- Update-Pruefung still beim Start und interaktiv ueber das Menue, mit
  NotifyStopping vor dem Start des Update-Agenten
- Lizenzfenster wertet IsTransient aus: bei fehlender Serververbindung wird
  nicht behauptet, die Lizenz sei ungueltig
- TextBox.Watermark auf PlaceholderText (in Avalonia 12 veraltet)

Build: 0 Fehler, 8 Warnungen (alle vorbestehend).
Tests: 100 bestanden, 0 Fehler, 1 uebersprungen.
Verifiziert: --license-status meldet gueltig samt Gnadenfrist; die GUI
startet durch, prueft die Lizenz und laeuft gegen den UpdateService.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-08 21:07:13 +02:00
co-authored by Claude Opus 5
35 changed files with 1694 additions and 1578 deletions
+232
View File
@@ -0,0 +1,232 @@
# Betrieb: Deployment Center
Das **Deployment Center** (`https://dc.mhdf.de`, Schwester-Repo
`J:\Softwareprojekte\Deploymentcenter`) hat die beiden früheren Einzelserver abgelöst:
| Vorher | Jetzt |
|---|---|
| WatchDog auf `watchdog.mhdf.de`, `POST /api/heartbeat`, Header `X-Watchdog-Key` | Deployment Center, `POST /api/watchdog/v1/ping`, `Authorization: Bearer` |
| LicenseLabrador auf `license.mhdf.de`, Ed25519-signierte Envelopes | Deployment Center, `POST /api/license/v1/validate`, Hardware-ID v2 |
| — | UpdateService, Fehler-Stream und Bugtracker (neu) |
Die Anbindung sitzt im `Predictalytics.WinFormsHost` und referenziert das
C#-SDK `Deploymentcenter.Client` aus dem Schwester-Repo:
```
..\..\..\..\Deploymentcenter\client-dotnet\Deploymentcenter.Client\Deploymentcenter.Client.csproj
```
Das Repo muss also neben dem Predictalytics-Checkout liegen. `LicenseLabrador`
und `WatchDog` werden nicht mehr gebraucht.
---
## 0. Was einmalig zu tun ist
1. Im Deployment Center unter **Token-Verwaltung → Master-Token erstellen** ein
Token mit den Rechten **`watchdog:ping`** und **`bugtracker:report`** anlegen.
2. Im Host unter **Settings → Deployment Center → API Token** eintragen.
Der alte `X-Watchdog-Key` funktioniert nicht mehr — es ist ein anderer Server.
3. Beim ersten Start erscheint einmalig der Lizenzdialog: der Lizenzschlüssel muss
erneut eingegeben werden, weil die Aktivierung jetzt am Deployment Center hängt.
4. Serverseitig muss der **Evaluator-Cron** laufen — ohne ihn bleibt ein
abgestürzter Dienst dauerhaft grün:
```
* * * * * curl -fsS -H "Authorization: Bearer <SHARED_KEY>" https://dc.mhdf.de/api/watchdog/v1/evaluate > /dev/null
```
Der Token landet in der `settings.json` neben der Exe (nicht im Git).
---
## 1. Konfiguration (PropertyGrid, Kategorie „Deployment Center")
| Feld | Default | Bedeutung |
|---|---|---|
| `Server URL` | `https://dc.mhdf.de` | **schreibgeschützt**, siehe unten |
| `API Token` | *(leer)* | `Authorization: Bearer`. Ohne Token: keine Heartbeats, keine Fehlermeldungen. |
| `Heartbeat aktiv` | `true` | Dead-Man's-Switch an/aus |
| `Monitor Source` | `Predictalytics` | Monitor-Name im Dashboard (Auto-Registrierung beim ersten Ping) |
| `Monitor Instance` | `default` | falls mehrere Instanzen laufen |
| `Heartbeat-Intervall (Sekunden)` | `60` | `warning` nach 2×, `down` nach 4× |
| `Fehler melden` | `true` | Error/Fatal an den Fehler-Stream |
| `Update-Prüfung beim Start` | `true` | prüft nur, installiert nichts von selbst |
| `Update-Kanal` | `prod` | `prod`, `beta` oder `dev` |
**Warum ist die Server-URL nicht einstellbar?** Sie entscheidet, wohin die
Lizenzprüfung geht und woher Update-Pakete kommen. Ein einstellbarer Endpoint
würde erlauben, die App auf einen gefälschten Lizenz- oder Update-Server zu
zeigen. Sie steht als Konstante in `Services/DcConfig.cs`.
---
## 2. Watchdog (Dead-Man's-Switch)
`Services/DcHeartbeatService.cs` sendet alle *n* Sekunden einen
`POST /api/watchdog/v1/ping`. Beim regulären Schließen geht ein Ping mit
`status: "stopped"` raus — der Evaluator lässt einen so beendeten Monitor in
Ruhe, bis wieder ein normaler Heartbeat eintrifft. Ohne das folgt wenige Minuten
nach jedem geordneten Beenden ein Fehlalarm.
**Ein Ausfall des Deployment Centers darf Predictalytics nie beeinträchtigen.**
Alle Aufrufe sind best effort; der erste Fehlschlag wird als Warnung geloggt,
Folgefehler nur noch auf Debug-Level (keine Log-Flut). Wird das Token abgelehnt
(`401`/`403`), stellt der Dienst die Versuche ganz ein — Wiederholen kann daran
nichts ändern.
### Mitgesendeter Gesundheitszustand
Ein Heartbeat beweist nur, dass ein Timer läuft. Deshalb schickt die App ihren
selbst ermittelten Zustand mit:
| `checks` | Bedeutung |
|---|---|
| `db` | `SELECT 1` gegen die konfigurierte MySQL, gedeckelt auf 5 s. Schlägt sie fehl, stuft der Server den Heartbeat auf `warning`. |
| `metrics` | Bedeutung |
|---|---|
| `uptime_sec` | Laufzeit des Prozesses |
| `workers_running` | 0/1 — Hintergrund-Worker gestartet |
| `webserver_running` | 0/1 — eingebetteter Kestrel gestartet |
| `memory_mb` | verwalteter Heap |
| `db_size_mb` | zuletzt ermittelte Datenbankgröße |
Zusätzlich geht seit Server 2.1 das Feld `version` mit — im Dashboard ist damit
sichtbar, welcher Build läuft, und ein Ausfall lässt sich einem Rollout zuordnen.
Gestoppte Worker sind **bewusst kein** fehlgeschlagener Check: das ist ein
gewollter Betriebszustand und würde den Monitor sonst dauerhaft auf `warning`
halten. Der Zustand steckt als Metrik drin und lässt sich dort auswerten.
Metriken werden serverseitig 14 Tage lang mit Verlauf gehalten
(`GET /api/watchdog/v1/metrics?source=Predictalytics`).
### Maschine mit überwachen
Der Heartbeat deckt nur *diesen* Prozess ab. Für die Maschine selbst gehört
zusätzlich ein OS-Agent auf den Host (siehe
`Deploymentcenter/docs/WATCHDOG_INTEGRATION_GUIDE.md`). Läuft beides, sollte im
WebUI unter **WatchDog → System-Hierarchie** die *Übergeordnete Entität* des
Predictalytics-Monitors auf den Host gesetzt werden — dann erzeugt ein
Maschinenausfall eine Meldung statt zwei.
---
## 3. Lizenzierung
`Services/LicenseGuard.cs` prüft beim Start, ob eine nutzbare Lizenz vorliegt;
`Program.Main` bricht sonst ab, bevor die MainForm entsteht. Ohne gültige Lizenz
erscheint `Services/LicenseDialog.cs` zur Key-Eingabe.
- **Produkt-Slug:** `predictalytics`
- **Hardware-ID v2:** `2:win:<sha256>` aus `HKLM\...\Cryptography\MachineGuid`.
Der Rechnername steckt **nicht** im Hash — Umbenennen kostet keinen
Aktivierungsplatz.
- **Offline-Cache:** AES-256-GCM + DPAPI unter `%AppData%\predictalytics\license\state.dat`
(Schema 3 seit SDK 2.1, Schema 2 wird noch gelesen). Dort liegt auch der
Lizenzschlüssel — `LicenseClient.TryGetCachedKey()` holt ihn beim Start, damit
ohne Dialog revalidiert werden kann.
- **Revalidierung zur Laufzeit:** alle 12 h.
- **Gemeldete Version:** `LicenseClient.DefaultAppVersion` wird in `Program.Main`
auf `BuildInfo.Version` gesetzt; ohne das trüge jede Installation in der
Aktivierungsliste dieselbe „1.0.0".
### Offline-Gnadenfrist
Seit Server 2.1 ist sie **echt begrenzt**: der Server liefert `cache_ttl_hours`
(je Projekt im WebUI, Standard 168 h = 7 Tage). Vorher galt faktisch das
Lizenz-Ablaufdatum, bei einer Lizenz bis 2040 also unbegrenzt.
Läuft die Frist in weniger als 48 h ab, warnt der Host beim Start und bei jeder
Revalidierung. Die verbleibende Frist zeigt **Deployment Center → Lizenzstatus
anzeigen**. Falls Predictalytics planmäßig länger offline laufen soll, muss die
TTL im WebUI für das Projekt `predictalytics` heraufgesetzt werden.
### Wann die App sich beendet — und wann nicht
Maßgeblich ist `LicenseValidationResult.IsTransient`:
| | Zustände | Reaktion |
|---|---|---|
| `IsValid` | `valid`, `valid_offline` | weiter |
| `IsTransient` | `server_unavailable`, `cache_expired` | **kein Urteil, nur eine gescheiterte Verbindung** — weiterlaufen, beim nächsten Durchlauf erneut versuchen |
| sonst | `revoked`, `expired`, `not_found`, `activation_limit`, `suspended`, `clock_rollback` | Meldung und Beenden |
`cache_expired` beendet die laufende Sitzung also nicht — der **nächste Start**
bleibt aber am Lizenzdialog hängen, weil `IsValid` dann falsch ist. Der Host
loggt diesen Fall deshalb als Error, nicht bloß als Warnung.
### Deaktivierung bei PC-Wechsel
`POST /api/license/v1/deactivate` verlangt den `shared_key` aus der
Server-Konfiguration. Der gehört **nicht** in die ausgelieferte Anwendung,
deshalb ist die Funktion hier nicht verdrahtet. Übliche Route: im WebUI unter
**Lizenzen → Hardware-Liste → „Freigeben"**. Die Hardware-ID zeigt der Host unter
**Deployment Center → Lizenzstatus anzeigen**.
---
## 4. Fehler-Stream
`Services/DcErrorReporter.cs` meldet Laufzeitfehler an
`POST /api/errors/v1/report`. Zwei Wege führen dorthin:
1. **Globale Handler** — `Application.ThreadException` (UI-Thread),
`AppDomain.UnhandledException` (Level `fatal`, wird blockierend gesendet, weil
der Prozess gleich weg ist) und `TaskScheduler.UnobservedTaskException`.
2. **Serilog-Sink** (`Services/DcErrorSink.cs`) — jedes `Log.Error`/`Log.Fatal`,
auch aus Worker und Kestrel.
Lokale Bremse, damit das Rate-Limit (60/min und IP) nicht verbrannt wird:
höchstens 20 Meldungen pro Minute, derselbe Fehler höchstens alle 5 Minuten.
Serverseitig werden gleiche Fehler ohnehin gruppiert und hochgezählt;
betriebsbedingtes Rauschen gehört in die **Ignore-Regeln** des WebUI — ein
Treffer zählt weiter, meldet aber nicht.
> Der Serilog-Filter in `Program.cs` wirft `Duplicate entry`-Ausnahmen schon
> vorher weg; die erreichen den Stream also gar nicht.
---
## 5. Updates
`Services/DcUpdateService.cs` fragt beim Start
`GET /api/updateservice/v1/check` ab (ohne Token). Gefunden wird nur — installiert
wird nichts von selbst:
- Kein Update: Info ins Log.
- Update verfügbar: Warnung ins Log, Hinweis in der Statusleiste.
- **Kritisches** Update: zusätzlich ein Dialog.
- Menü **Deployment Center → Nach Updates suchen** prüft jederzeit von Hand.
Liegt `update-agent.exe` neben der Anwendung, bietet der Dialog an, ihn zu
starten; die Anwendung meldet dann vorher `stopped` und beendet sich. Fehlt der
Agent, weist der Dialog auf die manuelle Installation hin.
Seit SDK 2.1 liefert auch der API-Zweig vollständige Release-Daten
(Download-Adresse, Prüfsumme, Changelog, Kritikalität) — der Dialog zeigt die
Release Notes also auch dann, wenn die statische `latest.json` fehlt.
### Versionsstand
`Predictalytics.WinFormsHost.csproj` importiert `Deploymentcenter.BuildInfo.targets`
und erzeugt daraus zur Übersetzungszeit `Predictalytics.WinFormsHost.BuildInfo`
mit `Version`, `GitCommit`, `GitCommitShort`, `BuildDateUtc`, `Channel` und
`Summary`. Quelle ist `<Version>` in derselben csproj — beim Release dort
hochziehen und mit
```
pack-and-deploy --project predictalytics --version <x> --channel prod
```
veröffentlichen. Dieselbe Version geht an die Lizenz-Aktivierungsliste, an den
Heartbeat und als `build` an den Fehler-Stream; `GitCommitShort` reist im
`context` jeder Fehlermeldung mit.
---
## 6. Bugtracker
Der Agenten-Workflow ist in [`../CLAUDE.md`](../CLAUDE.md) beschrieben.
Projekt-Slug: `predictalytics`. Items, deren `resolved_in_build` einer
veröffentlichten Version entspricht, schließen sich beim Publish von selbst.
-82
View File
@@ -1,82 +0,0 @@
# Betrieb: Watchdog-Überwachung & LicenseLabrador-Lizenzierung
Beide Integrationen sitzen im `Predictalytics.WinFormsHost` (dem Produktiv-Host) und
binden zwei eigenständige Schwester-Projekte an:
| Projekt | Pfad | Rolle |
|---|---|---|
| Watchdog | `J:\Softwareprojekte\WatchDog` | PHP/MySQL-Server auf `watchdog.mhdf.de`, empfängt Heartbeats |
| LicenseLabrador | `J:\Softwareprojekte\LicenseLabrador` | PHP-Lizenzserver auf `license.mhdf.de` + C#-SDK |
---
## 1. Watchdog (Dead-Man's-Switch)
`Services/WatchdogHeartbeatService.cs` sendet alle *n* Sekunden einen
`POST /api/heartbeat` an den Watchdog. Bleiben die Heartbeats aus — weil die App
abgestürzt ist oder die ganze Maschine weg ist — schlägt der Watchdog Alarm.
Beim regulären Schließen geht ein `POST /api/event` mit `kind=stopped_graceful`
raus, damit ein geplantes Beenden nicht als Crash alarmiert wird.
> **Nicht auf `kind=stopping` ändern.** Der Watchdog akzeptiert den Wert im
> Router, aber `event_log.kind` ist ein ENUM ohne `stopping`. Der Server setzt
> dann zwar noch den Zustand auf `stopped`, scheitert aber am Insert und liefert
> HTTP 500 — das Event fehlt in der Historie. Verifiziert am 2026-07-30.
**Wichtig:** Ein Ausfall des Watchdogs darf Predictalytics nie beeinträchtigen.
Alle Aufrufe sind best effort; der erste Fehlschlag wird als Warnung geloggt,
Folgefehler nur noch auf Debug-Level (keine Log-Flut).
### Konfiguration (PropertyGrid im Host, Kategorie „Watchdog")
| Feld | Default | Bedeutung |
|---|---|---|
| `Enabled` | `true` | Heartbeats an/aus |
| `Server URL` | `https://watchdog.mhdf.de` | Basis-URL |
| `API Key` | *(leer)* | `X-Watchdog-Key` — Shared Key **oder** Agent-Token. Ohne Key passiert nichts. |
| `Source` | `Predictalytics` | Monitor-Name im Dashboard (Auto-Registrierung beim ersten Beat) |
| `Instance` | `default` | falls mehrere Instanzen laufen |
| `Interval (Sekunden)` | `60` | Sende-Takt; Alarm nach ca. `Intervall × 1,5 + 30 s` |
Der Key landet in der `settings.json` neben der Exe (nicht im Git).
### PolyTrader-Maschine mit überwachen
Der Heartbeat aus Predictalytics deckt nur *diesen* Prozess ab. Damit auch die
Maschine überwacht wird, auf der PolyTrader läuft, gehört dort zusätzlich der
OS-Agent hin: `WatchDog\agents\windows\watchdog-agent.ps1` als Scheduled Task
(inkl. Shutdown-Hook), bzw. `agents/linux/watchdog-agent.sh` per systemd-Timer.
---
## 2. LicenseLabrador (Kopierschutz)
`Services/LicenseGuard.cs` prüft beim Start, ob eine nutzbare Lizenz vorliegt
(`Program.Main` bricht sonst ab, bevor die MainForm überhaupt entsteht).
Ohne gültige Lizenz erscheint `Services/LicenseDialog.cs` zur Key-Eingabe.
Produkt-Slug, Endpoint und der Ed25519-Public-Key sind **bewusst einkompiliert**
und nicht konfigurierbar — ein einstellbarer Endpoint würde erlauben, die App auf
einen gefälschten Lizenzserver zu zeigen.
- **Produkt-Slug:** `predictalytics`
- **Offline-Gnadenfrist:** 168 h (7 Tage) — danach ist Serverkontakt nötig
- **Revalidierung zur Laufzeit:** alle 12 h; bei Widerruf/Ablauf beendet sich die App
- **Härtung:** `VerifyChecksum` (HMAC über State + Key + Hardware-ID) gegen Memory-Patches;
Nonce-Reflexion und Signaturprüfung übernimmt das SDK
### Status
Produkt `predictalytics` und Lizenzschlüssel sind angelegt; die Aktivierung wurde
am 2026-07-30 gegen den Produktivserver verifiziert (`Valid`, gültig bis
2040-12-31, Checksum-Prüfung bestanden). Der einkompilierte Public Key passt zum
`signing.pub` des Servers — sonst käme `TamperSuspected` statt `Valid`.
Die Aktivierung ist an die Hardware-ID der jeweiligen Maschine gebunden. Auf
einem neuen Rechner erscheint einmalig der Dialog; der Cache liegt danach unter
`%AppData%\predictalytics\license`.
### Deaktivierung bei PC-Wechsel
`LicenseClient.DeactivateAsync()` gibt die Aktivierung wieder frei. Aktuell nicht
in der UI verdrahtet — bei Bedarf als Menüpunkt ergänzen.
+52
View File
@@ -443,8 +443,60 @@ Lizenzdialog.
---
## 5b. WinForms-Host abgelöst ✅ (2026-08-08)
`main` brachte eine vollständige Deployment-Center-Integration mit (Lizenz, Heartbeat mit
DB-Health-Check, Fehler-Stream, UpdateService), die im WinForms-Host entstanden war. Diese
wurde nach `feature/linux-net10` gemerged und dabei in den plattformneutralen Kern gezogen.
**`Predictalytics.WinFormsHost` ist entfernt** — die Avalonia-Shell ist ab jetzt die einzige
Bedienoberfläche.
### Was nach `Predictalytics.Hosting` wanderte
| Datei | Anmerkung |
|---|---|
| `DcConfig`, `DcApiClient`, `DcErrorSink`, `DcHeartbeatService`, `DcUpdateService` | unverändert, waren bereits plattformneutral |
| `DcErrorReporter` | `Application.ThreadException` + `MessageBox` entfernt; stattdessen `ReportUiThreadException(Exception)`, das der Host aufruft. Avalonia hängt sich über `Dispatcher.UIThread.UnhandledException` ein. |
| `LicenseGuard` + `LicenseSession` | Dialogaufruf und `System.Windows.Forms.Timer` entfernt. Neu: `TryUseCachedAsync()`, `ActivateAsync()` und `StartPeriodicRevalidation(session, onUnusable)` über `PeriodicTimer`. Die Unterscheidung transienter Fehler und die Gnadenfrist-Warnung sind unverändert übernommen. |
| Dc-Einstellungen | aus `AppSettings` nach `PredictalyticsOptions`; die Watchdog-Einstellungen entfielen |
| `DcErrorSink` in Serilog | in `LoggingSetup` aufgenommen, Startbanner nutzt jetzt `DcConfig.AppVersion` |
`Deploymentcenter.BuildInfo.targets` wird nun von `Predictalytics.Hosting` importiert;
`BuildInfo` entsteht damit im Namensraum `Predictalytics.Hosting`.
### Was in der Avalonia-Shell nachgezogen wurde
* Menü **Deployment Center** mit „Nach Updates suchen" und „Lizenzstatus anzeigen"
* Einstellungsgruppe **Watchdog → Deployment Center** (Token, Heartbeat, Fehler-Reporting,
Update-Kanal als ComboBox; die Server-URL wird nur angezeigt, nicht editiert)
* Heartbeat-Snapshot samt `SELECT 1`-DB-Probe, identisch zur WinForms-Fassung
* Update-Prüfung beim Start (still) und über das Menü (interaktiv), inklusive
`NotifyStopping()` vor dem Start des Update-Agenten
* Lizenzfenster nutzt `IsTransient`: bei fehlender Serververbindung wird nicht behauptet,
die Lizenz sei ungültig
### Verifiziert
```
🔑 Lizenz geprüft: valid (HWID 2:win:a765bd47…, Quelle machine-guid)
Shell initialisiert. Bereit.
🐕 Heartbeat ist aktiviert, aber es fehlt das Deployment-Center-Token
Update-Prüfung: v1.0.0 ist aktuell (Kanal prod)
```
`--license-status` meldet gültig samt Offline-Gnadenfrist. Build 0 Fehler, 100 Tests grün.
**Noch offen:** Heartbeat und Fehler-Stream laufen erst mit gesetztem DC-Token; die
Wartungsaktionen brauchen DB-Zugangsdaten.
---
## 5a. Zusatz — Umstellung auf das Deploymentcenter-Lizenzsystem ✅ (2026-08-07)
> **Überholt durch 5b.** Der hier beschriebene eigene `LicenseGuard` gegen
> `Deploymentcenter.Client` war ein Provisorium und wurde durch die reifere Fassung aus
> `main` ersetzt (`LicenseSession`, `IsTransient`, `TryGetCachedKey`, Gnadenfrist-Warnung).
> Die Begründungen zur Hardware-ID v2 und zum AES-GCM-Cache gelten unverändert.
**Nicht ursprünglich geplant.** Beim Aktivieren der neuen Lizenz stellte sich heraus, dass
Predictalytics noch am alten Lizenzsystem hing (LicenseLabrador, `license.mhdf.de`),
während die Lizenzen inzwischen im Deploymentcenter (`dc.mhdf.de`) gepflegt werden.