# 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.
