# 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 in `Predictalytics.Hosting` 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 " 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:` 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.Hosting.csproj` importiert `Deploymentcenter.BuildInfo.targets` und erzeugt daraus zur Übersetzungszeit `Predictalytics.Hosting.BuildInfo` mit `Version`, `GitCommit`, `GitCommitShort`, `BuildDateUtc`, `Channel` und `Summary`. Quelle ist `` in `Directory.Build.props` — beim Release dort hochziehen und mit ``` pack-and-deploy --project predictalytics --version --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.