feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar

This commit is contained in:
Richard
2026-08-10 10:48:34 +02:00
parent a0e18d2a57
commit b5bf97ae74
187 changed files with 20054 additions and 882 deletions
+338
View File
@@ -0,0 +1,338 @@
# Deploymentcenter-Integration
Stand: 2026-08-08, Deploymentcenter **2.1**. Ersetzt den früheren
`Integrationsplan-WatchDog-LicenseLabrador.md`.
ClawdDotNet spricht das [Deploymentcenter](../../Deploymentcenter/docs/README.md) als
**eine** Gegenstelle an. Vorher waren es zwei Fremdprojekte mit je eigenem Server,
eigenem Schlüssel und eigener Anleitung:
| Vorher | Jetzt |
|---|---|
| WatchDog (`watchdog.mhdf.de`, `X-Watchdog-Key`) | Deploymentcenter-Modul Watchdog, `Authorization: Bearer` |
| LicenseLabrador (`license.mhdf.de`, Ed25519-Public-Key) | Deploymentcenter-Modul Lizenz |
| — | Update-Prüfung |
| — | Fehler-Stream (ungefangene Ausnahmen) |
| — | Bugtracker |
Eine Adresse, ein Token. Beides steht in den Anwendungseinstellungen.
---
## 1. Was der Betreiber einzutragen hat
| Ort | Wert |
|---|---|
| Einstellungen → Deploymentcenter → **Server-URL** | `https://dc.mhdf.de` (Vorgabe) |
| Einstellungen → Deploymentcenter → **Token** | Master-Token mit `watchdog:ping` + `bugtracker:report` |
| Einstellungen → Lizenz → **Lizenzschlüssel** | Der Schlüssel für das Projekt `clawddotnet` |
| Worker-Tab → Dienst **Instanz-Watchdog** | einschalten, greift beim nächsten Start der Instanz |
Das Token entsteht im WebUI unter **Token-Verwaltung → Master-Token erstellen**. Es
wird verschlüsselt (DPAPI) in `Settings.json` abgelegt.
> **Bis die Avalonia-Einstellungsansicht steht**, gibt es für diese Felder noch keine
> Oberfläche — die Seite „Einstellungen" ist ein Platzhalter. Die Werte kommen
> vorläufig von Hand in `Settings.json` (Ort steht beim Start im Protokoll:
> `%APPDATA%\ClawdDotNet\Settings.json`, unter Linux `$XDG_CONFIG_HOME`) bzw. in die
> `instance.json` der Instanz. Das Token wird beim ersten Speichern durch die Anwendung
> verschlüsselt; im Klartext eingetragen funktioniert es ebenfalls, weil der
> `SecretProtector` beide Richtungen verträgt.
**Serverseitig ist eine Sache Pflicht**, sonst ist die Überwachung wertlos: der
Evaluator-Cron. Ohne ihn ändert sich ein Monitor-Zustand nur beim Eintreffen eines
Heartbeats — eine abgestürzte Instanz bliebe dauerhaft grün.
```bash
* * * * * curl -fsS -H "Authorization: Bearer <SHARED_KEY>" https://dc.mhdf.de/api/watchdog/v1/evaluate > /dev/null
```
---
## 2. Watchdog — ein Monitor je Instanz
Das war die Vorgabe und ist jetzt sauber abgedeckt: Der Server führt Monitore über das
Paar `source` + `instance` (`UNIQUE KEY uq_monitor (source, instance)` in
`sql/schema.sql`). Alle Instanzen melden unter `source = "clawddotnet"` und tragen ihre
eigene `instance`. Fällt eine von dreien aus, fällt genau deren Monitor — und nur der
schlägt Alarm.
`instance` ist standardmäßig die `InstanceId` (stabil, aber im Dashboard nichtssagend).
In den Instanz-Einstellungen lässt sich stattdessen ein Name eintragen
([`WatchdogConfig.Instance`](../src/ClawdDotNet.Core/Config/WatchdogConfig.cs)); ein
späterer Wechsel legt allerdings einen neuen Monitor an.
**Eine Registrierung vorab gibt es nicht mehr.** Der Monitor entsteht beim ersten
Heartbeat von selbst (`INSERT … ON DUPLICATE KEY UPDATE`). Der frühere Weg über
`POST /api/register` hatte im Deploymentcenter nie ein Gegenstück — die alte Anbindung
lief in dieser Form also gegen einen Endpunkt, den es nicht gibt.
### Was der Heartbeat trägt
```
POST /api/watchdog/v1/ping
Authorization: Bearer <Instanz-Token>
```
| Feld | Inhalt |
|---|---|
| `source` / `instance` | `clawddotnet` / InstanceId bzw. eingestellter Name |
| `status` | `ok`, `warning`, `error` — beim Beenden `stopped` |
| `interval` | 60 s (Vorgabe). Daraus leitet der Evaluator ab: 2×`warning`, 4×`down` |
| `message` | Instanzname + Kurzbegründung |
| `os` | Betriebssystem + .NET-Version |
| `version` | Produktversion (2.1). Landet in `watchdog_monitors.app_version` — bei mehreren Instanzen der Unterschied zwischen „läuft" und „läuft noch auf der alten Fassung" |
| `checks` | `agents`, `scheduler`, `budget` — siehe unten |
| `metrics` | `agentCount`, `runningChats`, `todayCostUsd`, `todayTokens` |
### `checks` — der eigentliche Gewinn
Ein Heartbeat beweist nur, dass ein Faden läuft. Deshalb geht der selbst ermittelte
Zustand je Teilbereich mit; schlägt eine Prüfung fehl, stuft der Server einen als `ok`
gemeldeten Beat auf `warning` herab und nennt in der Antwort die betroffene.
| Prüfung | Fehlschlag bedeutet |
|---|---|
| `agents` | Kein OpenRouter-Key — die Instanz läuft, arbeitet aber nichts ab |
| `scheduler` | Die Taktschleife des Aufgaben-Scanners ist ausgestiegen |
| `budget` | Tagesgrenze für Kosten oder Token erreicht |
„Scanner noch nicht gestartet" gilt **nicht** als Fehlschlag: Er läuft erst nach der
Startabgleichung an, der erste Heartbeat geht sofort raus. Sonst gäbe es bei jedem
Start ein `warning_raised` und kurz darauf ein `recovered` — zwei Einträge im
Ereignisprotokoll für einen Normalvorgang.
### Metriken sind nur Zahlen
Der Server legt numerische Werte mit Zeitstempel ab (14 Tage) und vergleicht den
aktuellen Wert mit dem Sieben-Tage-Schnitt desselben Monitors. Nicht-numerische Werte
verwirft er dabei stillschweigend — `instanceId`, `instanceName` und `buildVersion`
standen früher in den Metriken und waren dort wirkungslos. Beschreibendes steht jetzt
in `message` und `os`.
### Angekündigtes Ende
Beim Herunterfahren geht ein Heartbeat mit `status: "stopped"` raus, danach das Ereignis
`stopped_graceful`. Der Evaluator lässt einen so gemeldeten Monitor in Ruhe. Ohne das
erzeugte jedes geplante Beenden wenige Minuten später einen Fehlalarm.
Nebenbei korrigiert: Die alte Anbindung schickte die Ereignisarten `start` und `stop`
beide stehen nicht auf der Liste des Servers und landeten stillschweigend als `started`.
Jetzt sind es `started` und `stopped_graceful`.
### Token je Instanz
Beim ersten Start tauscht die Instanz das anwendungsweite Token über
`POST /api/tokens/v1/provision` gegen ein eigenes, auf `watchdog:ping` und
`bugtracker:report` beschränktes Sub-Token und legt es verschlüsselt in der
Instanzkonfiguration ab. Danach liegt auf der Instanz nicht mehr das Master-Token, und
ein einzelner Zugang lässt sich widerrufen, ohne die anderen mitzunehmen.
Das ist derselbe Zweck, den die frühere Selbstregistrierung hatte. Scheitert es (etwa
weil das hinterlegte Token selbst ein Sub-Token ist und keine weiteren ausstellen darf),
wird mit dem hinterlegten Token gemeldet — Monitoring, das nur bei perfekter Rechtelage
läuft, ist genau dann still, wenn man es braucht.
---
## 3. Lizenz
Startprüfung in [`LicenseGate`](../src/ClawdDotNet.App/Services/LicenseGate.cs). Die
Offline-Gnadenfrist steckt im SDK: Es legt nach jeder erfolgreichen Prüfung einen mit
AES-GCM verschlüsselten, an die Hardware gebundenen Zwischenspeicher an (`LLS2`,
seit 2.1 Schema 3) und trägt damit über Ausfälle hinweg.
### Urteil und Fehlversuch sind zwei verschiedene Dinge
Das ist der Kern der 2.1-Anpassung. `LicenseValidationResult.IsTransient` unterscheidet:
| | Statuswerte | Folge |
|---|---|---|
| **Urteil des Servers** | `revoked`, `expired`, `not_found`, `activation_limit`, `suspended`, `clock_rollback` | Anwendung startet nicht bzw. beendet sich |
| **Kein Urteil erhalten** | `server_unavailable`, `cache_expired` | Warnung, Betrieb läuft weiter |
Nur das Urteil sperrt. Ein Serverausfall darf nicht jede Installation gleichzeitig
aussperren — und eine Drosselung (`429`) oder ein `500` sind Aussagen über den Server,
nicht über die Lizenz. Das gilt an beiden Stellen gleich: Startprüfung und laufende
Nachprüfung fragen dasselbe Merkmal ab.
> **Bewusst in Kauf genommen:** Ein Rechner, der die Gegenstelle nie erreicht, läuft
> damit auf Dauer mit Warnung weiter — auch nach Ablauf der Gnadenfrist
> (`cache_expired` ist laut SDK-Vertrag vorübergehend). Wer das anders will, prüft in
> [`LicenseGate`](../src/ClawdDotNet.App/Services/LicenseGate.cs) zusätzlich auf
> `cache_expired` und behandelt es als Urteil. Es sollte eine Entscheidung sein, nicht
> ein Nebeneffekt.
### Offline-Gnadenfrist ist echt begrenzt
Seit 2.1 wertet der Client `cache_ttl_hours` des Projekts aus (Vorgabe 168 h). Vorher
galt faktisch das Ablaufdatum der Lizenz — bei einer Lizenz bis 2040 also unbegrenzt.
Der verbleibende Rest steht in `CacheExpiresAt` und wird beim Start angezeigt, wenn die
Prüfung aus dem Zwischenspeicher kam.
`state.dat` steigt auf Schema 3; Schema 2 wird weiter gelesen. Ein Rückschritt auf ein
älteres SDK verwirft den Zwischenspeicher — dann ist einmal eine Online-Prüfung nötig.
### Kein Public-Key mehr
Die frühere Fassung führte einen Ed25519-Public-Key als „Vertrauensanker". Im
Deploymentcenter gibt es dazu keine Gegenseite — der Client liest ausschließlich das Feld
`status`. Ein Schlüssel, der nichts prüft, ist schlimmer als keiner: Er lässt Schutz
vermuten, wo keiner ist. Details in
[Deploymentcenter-Anbindung-Review](Deploymentcenter-Anbindung-Review.md), Abschnitt 2.1.
### Der Projekt-Slug ist `clawddotnet`
[`LicenseInfo.ProductSlug`](../src/ClawdDotNet.App/Services/LicenseInfo.cs) gilt für
**alle** Module — Lizenz, Bugtracker, Fehler-Stream und Update-Prüfung greifen auf
dieselbe Tabelle `dc_projects` zu.
Bis zur Umstellung stand hier `clawd`, der Name aus dem LicenseLabrador-Backend. Das
ist eine Falle mit langer Zündschnur: Der Server beantwortet ein unbekanntes Projekt mit
demselben `not_found` wie einen unbekannten Schlüssel — der Unterschied steht
ausschließlich in `message` (`"Project not found"` gegen `"Invalid license key"`). Wer
den Text nicht durchreicht, sucht den Fehler beim Lizenzschlüssel, während das Projekt
gar nicht existiert. Der Torwächter gibt die Serverantwort deshalb mit aus und schreibt
sie ins Protokoll.
### Deaktivieren läuft über das WebUI
`POST /api/license/v1/deactivate` verlangt den `shared_key` des Servers. Der gehört nicht
in eine ausgelieferte Anwendung, deshalb ist der Weg die Hardware-Liste im WebUI
(Schaltfläche „Freigeben").
### Laufende Nachprüfung
[`LicenseWatch`](../src/ClawdDotNet.App/Services/LicenseWatch.cs) prüft alle zwölf
Stunden nach — dieselbe Unterscheidung wie oben. Ohne das wirkt ein Widerruf erst beim
nächsten Start, bei einem wochenlang laufenden Dienst also praktisch nie.
---
## 4. Version und Updates
### Eine Stelle für die Version
`<Version>` in [Directory.Build.props](../Directory.Build.props) ist die Wahrheit.
`Deploymentcenter.BuildInfo.targets` (seit 2.1 einbindbar) erzeugt daraus zur
Übersetzungszeit `ClawdDotNet.App.ReleaseInfo` mit `Version`, `GitCommit`,
`GitCommitShort`, `BuildDateUtc`, `Channel` und `Summary`.
Der Wert geht an vier Stellen nach draußen, die vorher alle geraten haben:
| Stelle | Vorher |
|---|---|
| Aktivierungsliste (`app_version`) | fest `"1.0.0"` im SDK — jede Installation gleich |
| Heartbeat (`version`) | gab es nicht |
| Fehlermeldungen (`build`) | — |
| Versionsvergleich der Update-Prüfung | `0.0.<BuildInfo.Build>`, behelfsweise |
Die Klasse heißt bewusst `ReleaseInfo`, nicht `BuildInfo`: Diesen Namen trägt in
`ClawdDotNet.Core` schon ein von Hand geführter Zähler mit Änderungstext. Zwei
gleichnamige Klassen mit verschiedener Bedeutung wären eine Falle. Umgestellt über
`DeploymentcenterBuildInfoClass` in der csproj.
### Prüfung
Einmalig beim Start gegen `GET /api/updateservice/v1/check`, über
`Deploymentcenter.Client.UpdateClient`. Läuft nebenher und blockiert nichts; liegt eine
neuere Version vor, erscheint ein Hinweis mit Changelog. Ob und wann aktualisiert wird,
entscheidet der Benutzer — eine Anwendung, die sich beim Start selbst beendet, um sich zu
erneuern, ist genau dann im Weg, wenn man sie braucht.
Seit 2.1 liefern beide Wege vollständige Daten: die statische `latest.json` in camelCase,
die API in snake_case, jeweils über ein eigenes Modell (`VersionInfo` bzw.
`ApiReleaseInfo`). Vorher kam über den API-Zweig außer der Versionsnummer nichts an — und
der ist genau der Rückfall, wenn die `latest.json` fehlt. Die Download-Adresse wird
mitgeführt (`UpdateAvailability.DownloadUrl`), damit der `update-agent` später ohne
weitere Änderung anschließen kann.
Der `update-agent` ist noch **nicht** eingebunden, und solange kein Release über
`pack-and-deploy` veröffentlicht wird, hat die Prüfung nichts zu finden.
---
## 5. Fehler-Stream
Ungefangene Ausnahmen gehen an `POST /api/errors/v1/report`. Verdrahtet in
[`App.axaml.cs`](../src/ClawdDotNet.Desktop/App.axaml.cs) an drei Stellen:
`AppDomain.UnhandledException`, `TaskScheduler.UnobservedTaskException` und
`Dispatcher.UIThread.UnhandledException`.
Erst nach dem Aufbau verdrahtet, nicht in `Main`: Vorher gibt es weder Einstellungen
noch Token. Die Kehrseite ist bewusst in Kauf genommen — ein Absturz *während* des
Starts erreicht das Deploymentcenter nicht, steht aber im Protokoll.
**Eigene Drosselung** in
[`ErrorReporter`](../src/ClawdDotNet.Core/Deploymentcenter/ErrorReporter.cs): Derselbe
Fehler (Typ + oberste Stelle im Stacktrace) geht höchstens einmal alle fünf Minuten
raus. Der Server drosselt auch, aber erst, nachdem die Anfragen über die Leitung waren.
Die Fehlermeldung selbst gehört nicht zum Kennzeichen — sie enthält oft wechselnde
Werte, und dann wäre jeder Aufruf ein neuer Fehler.
Bekannte, harmlose Fehler lassen sich serverseitig unter **Bugtracker → Ignore-Regeln**
stummschalten. Sie werden weiter gezählt; der Zähler ist der Zweck: Dass ein bekannter
Fehler auftritt, ist normal — dass er plötzlich hundertmal so oft auftritt, bedeutet,
dass sich etwas geändert hat.
---
## 6. Bugtracker
[`BugtrackerClient`](../src/ClawdDotNet.Core/Deploymentcenter/BugtrackerClient.cs) für
bewusst formulierte Einträge (Fehler, Wunsch, Idee) mit Titel und Beschreibung, gegen
`POST /api/bugtracker/v1/report`. Der Absender wird serverseitig aus dem Token
abgeleitet und lässt sich nicht frei wählen.
Der Client ist da und über `AppHost.Deploymentcenter.Bugtracker` erreichbar; **eine
Oberfläche dafür fehlt noch** („Fehler melden"-Schaltfläche). Ein Agenten-Tool wäre der
nächste sinnvolle Schritt — Agenten könnten dann selbst Wünsche und Fehler eintragen,
und der Agenten-Workflow des Deploymentcenters (Claim/Lease über
`manage?action=next`) würde sie abarbeiten.
---
## 7. Wo was liegt
| Datei | Inhalt |
|---|---|
| [`Deploymentcenter/DeploymentcenterApi.cs`](../src/ClawdDotNet.Core/Deploymentcenter/DeploymentcenterApi.cs) | Gemeinsamer Unterbau: Bearer-Header, HTTPS-Pflicht, Umschlag auspacken, `DeploymentcenterException` mit stabilem `Code` |
| [`Deploymentcenter/Watchdog/`](../src/ClawdDotNet.Core/Deploymentcenter/Watchdog) | Heartbeat-Client, Zustandsermittlung, Takt-Dienst |
| [`Deploymentcenter/ErrorReporter.cs`](../src/ClawdDotNet.Core/Deploymentcenter/ErrorReporter.cs) | Fehler-Stream mit Drosselung |
| [`Deploymentcenter/BugtrackerClient.cs`](../src/ClawdDotNet.Core/Deploymentcenter/BugtrackerClient.cs) | Bugtracker-Einträge |
| [`Deploymentcenter/TokenProvisioner.cs`](../src/ClawdDotNet.Core/Deploymentcenter/TokenProvisioner.cs) | Sub-Token je Instanz |
| [`Services/DeploymentcenterService.cs`](../src/ClawdDotNet.App/Services/DeploymentcenterService.cs) | Verdrahtung: Token beschaffen, Heartbeat starten, Update prüfen |
| [`Services/LicenseGate.cs`](../src/ClawdDotNet.App/Services/LicenseGate.cs) | Startprüfung |
| [`Services/LicenseWatch.cs`](../src/ClawdDotNet.App/Services/LicenseWatch.cs) | Laufende Nachprüfung |
Die Lizenz läuft bewusst **nicht** über `DeploymentcenterApi`: Sie hat ein eigenes
Antwortformat (kein `status`/`error`-Umschlag — `status` trägt dort den Lizenzzustand),
einen eigenen Zwischenspeicher und muss vor allem anderen laufen.
Watchdog, Fehler-Stream und Bugtracker deckt das SDK `Deploymentcenter.Client` nicht ab;
dafür ist der eigene Unterbau da. Hardware-ID v2, Lizenz-Zwischenspeicher und
Update-Prüfung kommen aus dem SDK — die nachzubauen wäre Verdopplung.
---
## 8. Tests
[`tests/ClawdDotNet.Core.Tests/Deploymentcenter/`](../tests/ClawdDotNet.Core.Tests/Deploymentcenter):
Bearer-Header, Fehlerumschlag → Ausnahme mit Code (auch bei HTTP 200), HTTPS-Pflicht mit
Localhost-Ausnahme, Heartbeat-Pfad und -Rumpf, zwei Instanzen → zwei Monitore, Checks
und Metriken, Antwort-Auswertung, Drosselung des Fehler-Streams, Sub-Token-Bezug.
---
## 9. Offen
1. **Oberfläche für den Bugtracker** — Client vorhanden, Schaltfläche fehlt.
2. **Agenten-Tool für den Bugtracker** — würde den Agenten-Workflow des
Deploymentcenters nutzbar machen.
3. **Release-Strecke**`pack-and-deploy` aufrufen und `<Version>` dabei mitgeben,
danach `update-agent` einbinden. Die Versionsnummer selbst ist mit 2.1 erledigt.
4. **SDK als Git-Submodul** unter `external/` statt Cross-Repo-Pfad.
5. **Hierarchie** (`parent_source`): Läuft die Instanz auf einem Host, der selbst als
Monitor geführt wird, sollte sie ihn als übergeordnete Entität eingetragen bekommen —
sonst erzeugt ein Hostausfall eine Meldung je Instanz. Das ist im WebUI zu pflegen,
nicht im Client (siehe aber Anmerkung 2 in den Rückmeldungen).