Files
PolyTraderSharp/docs/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md
T
RichardandClaude Opus 5 bf8048b82a Fruehjahrsputz: WinForms ausgebaut (P11/L5), LicenseLabrador abgeloest (D-6)
Das Repo enthaelt ab jetzt ausschliesslich plattformneutralen Code. Build gruen
(0 Fehler), 476 Tests gruen, Linux-Publish der Avalonia-App verifiziert.
Rueckfallebene ist der Tag winforms-final, der jetzt auch auf dem Server liegt.

WinForms endgueltig ausgebaut (P11/L5):
- PolyTrader.App.csproj aus PolyTraderSharp.sln genommen und geloescht
- Ui/, Models/, Licensing/, services/, Properties/, Resources/icons/,
  Program.cs, favicon.ico entfernt - rund 4.500 LOC generierter Designer-Code
- Root-appsettings.json entfernt: exaktes Duplikat der Avalonia-Kopie, die die
  App tatsaechlich liest. Zwei Dateien mit gleichem Inhalt laufen frueher oder
  spaeter auseinander.
- Kein net10.0-windows mehr im Repo; Build-Warnungen von 23 auf 15 gesunken
  (die CS0169 aus den Designer-Resten sind weg)

PNG-Symbole gerettet statt geloescht:
Die Avalonia-App band sie per ..\..\Resources\*.png aus dem Repo-Root ein und
haette sie mitverloren. Sie liegen jetzt in src/PolyTrader.App.Avalonia/Assets/,
wo sie hingehoeren. Der avares-Pfad "Assets/<datei>.png" bleibt unveraendert -
in der gebauten Assembly nachgeprueft.

LicenseLabrador abgeloest (D-6):
- LicenseLabrador.Client aus lib/nuget entfernt, Quellen-Mapping in
  NuGet.Config auf Deploymentcenter.* reduziert, lib/nuget/README.md neu
- Bewusste Abweichung vom Plan: die Watchdog*-Felder in ServerSettings bleiben.
  D-1 hat sie auf die Deploymentcenter-API umgewidmet statt sie zu ersetzen;
  sie werden aktiv benutzt. Die Planzeile stammte aus der Zeit davor.

Weitere Altlasten:
- agentspace/ (30 Dateien: WinForms-Designer-Patcher, fix_mongo.py nach der
  MySQL-Migration, tmp/test/scratch-Skripte) entfernt
- Ideen-fuer-Mittwoch.txt entfernt - Inhalt ist laengst umgesetzt
- tote .gitignore-Regel fuer agentspace/antigravity/ entfernt
- lokal entfernt (nicht versioniert): MongoDB/-Exporte, data.db, .bak-Datei

Dokumentation nachgezogen:
- ANALYSE-Linux-Portierung.md auf Revision 6, P11/L5 als erledigt
- LEITFADEN: Abschnitt B vollstaendig aufgeloest, A5 prueft ab jetzt gegen
  einen Worktree des Tags statt gegen den Arbeitsbaum
- UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration.md als ABGELOEST
  gekennzeichnet, D-6 im Deploymentcenter-Plan auf den Iststand gezogen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 10:58:43 +02:00

828 lines
55 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: Deploymentcenter in PolyTrader einbinden (P3c)
> Stand: 14.08.2026 · Grundlage: `J:\Softwareprojekte\Deploymentcenter\docs` (Fassung 2.4) und die
> SDK-Quellen unter `client-dotnet\Deploymentcenter.Client`.
> Löst ab: [UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration.md](./UMSETZUNGSPLAN-Watchdog-LicenseLabrador-Integration.md)
> (Watchdog + LicenseLabrador als Einzeldienste).
> Ersetzt in `ANALYSE-Linux-Portierung.md` den Punkt **P3c** und präzisiert dort §5.2.
---
## 0. Was sich gegenüber der bisherigen Planung geändert hat
Das Deploymentcenter ist nicht mehr „Lizenz + Watchdog an einem Ort", sondern deckt inzwischen
**vier** Bereiche ab. Zwei davon standen in unserer Planung bisher gar nicht:
| Bereich | Zustand bei uns |
|---|---|
| **Lizenz** | vorhanden, aber gegen LicenseLabrador gebaut (`LicenseGate`, `Ui/LicenseDialog`) — **und in der Avalonia-App gar nicht verdrahtet**, dort läuft derzeit alles ungeprüft |
| **Watchdog** | vorhanden (`WatchdogHeartbeatService`), aber gegen die **alte** Watchdog-API gebaut — URL, Header und Payload passen nicht |
| **Error-Reporting / Bugtracker** | **existiert bei uns nicht**. Es gibt im gesamten Repo weder `AppDomain.UnhandledException` noch `TaskScheduler.UnobservedTaskException` |
| **UpdateService + Erstinstallation** | **existiert bei uns nicht**. Kein Paket, kein Release-Weg, keine Versionsdisziplin (`<Version>` steht in keiner einzigen `.csproj`) |
Die Schätzung aus der Analyse (24 PT) bezog sich auf die ersten beiden Punkte. Mit
Error-Reporting, Auslieferung und Erstinstallation liegt der Umfang bei **47 PT**.
---
## 1. Befunde — Probleme, Bugs und Konzeptfehler
Sortiert nach Wirkung auf uns. Spalte „Wo" trennt, was **wir** lösen und was im
**Deploymentcenter** gelöst werden müsste.
### D-1 · Der Update-Weg beendet die Anwendung hart — für einen Trading-Bot unzulässig · **hoch** · Wo: wir + DC
`UpdateClient.LaunchUpdateAgent` ruft `Environment.Exit(0)`
(`client-dotnet/Deploymentcenter.Client/UpdateClient.cs:401`), und `exitCurrentApp: true` ist die
**Vorgabe**. Genau das haben wir bei der Lizenz schon einmal ausgeschlossen: Eine Anwendung mit
offenen Positionen, laufenden CLOB-Aufrufen und einem 10-Sekunden-Bestätigungsdialog darf nicht
mitten im Lauf abgeschossen werden.
Lösbar auf unserer Seite (`exitCurrentApp: false` und danach der bestehende geordnete
Shutdown-Pfad). **Nicht lösbar auf unserer Seite:** Der Agent wartet über `--wait-for-pid` auf das
Ende der Anwendung, aber nur `--wait-timeout` Sekunden — Vorgabe **60**
(`Deploymentcenter.UpdateAgent/Program.cs:70`). `LaunchUpdateAgent` hat für diesen Wert
**keinen Parameter** (`UpdateClient.cs:330-342`). Unser Herunterfahren nimmt sich allein für
`host.StopAsync` bis zu 30 s, dazu Avalonia-Teardown und die Modul-Stopps. Reißt die Frist, tut der
Agent **nichts** — immerhin sicher, aber ohne Rückmeldung und ohne Stellschraube.
**Wunsch an DC:** `waitTimeoutSeconds` in `LaunchUpdateAgent` durchreichen.
**Bis dahin bei uns:** den Agenten selbst starten (eigener `ProcessStartInfo`) statt über
`LaunchUpdateAgent` — dann setzen wir `--wait-timeout` selbst.
### D-2 · Der Lizenzschlüssel wandert in die Kommandozeile · **hoch** · Wo: DC
Der Agent nimmt die Zugangsdaten der Release-Ablage ausschließlich als Argument entgegen:
`--license-key` bzw. `--download-user/--download-password`
(`Deploymentcenter.UpdateAgent/Program.cs:70-79`), und `UpdateClient.LaunchUpdateAgent` hängt sie
ebenso an (`UpdateClient.cs:366-369`). Umgebungsvariablen liest der Agent **an keiner Stelle**.
Auf einem Mehrbenutzersystem zeigt `ps` (bzw. der Task-Manager) die Argumente jedes laufenden
Prozesses. Das ist wörtlich dieselbe Begründung, mit der `UPGRADE.md §5` den `curl`-Aufruf im
Crontab verwirft — an dieser Stelle wird sie nicht angewandt. Für uns kommt hinzu: „keine Secrets
in argv" ist bei PolyTrader ein bereits **umgesetzter** Sicherheitsbefund; diese Integration würde
ihn zurücknehmen.
**Wunsch an DC:** Zugangsdaten zusätzlich über `DC_LICENSE_KEY` / `DC_DOWNLOAD_PASSWORD` oder über
stdin annehmen. Sauberer Workaround auf unserer Seite gibt es nicht.
### D-3 · Das Lizenzurteil ist nicht signiert — Rückschritt gegenüber LicenseLabrador · **mittelhoch** · Wo: DC
`LicenseClient` liest den Status aus reinem JSON (`LicenseClient.cs:191-237`). Der mitgeschickte
`nonce` (`:113`) wird in der Antwort **nie geprüft**, eine Signatur gibt es nicht. LicenseLabrador
verifizierte die Serverantwort per Ed25519 gegen einen einkompilierten Public Key und reflektierte
die Nonce — deshalb konnte niemand ohne den privaten Schlüssel ein „valid" erfinden.
Jetzt trägt allein TLS. Wer DNS oder die `hosts`-Datei umbiegt und ein eigenes Zertifikat in den
Trust Store legt — also der Besitzer der Maschine — kann sich ein `{"status":"valid"}` selbst
ausstellen. Für unser erklärtes Bedrohungsmodell („Schutz gegen einfaches Kopieren auf ein fremdes
System") reicht das; sobald Dritte die Software bekommen, ist es die schwächste Stelle der Kette.
Bemerkenswert: Für **Release-Pakete** führt `UPDATESERVICE_INTEGRATION_GUIDE §6` eine ausführliche
und richtige Begründung, warum dort asymmetrisch signiert werden muss („ein Update wird auf dem
Zielsystem geprüft") — beim Lizenzurteil gilt dasselbe Argument und fehlt die Umsetzung.
Zusatz: Der Client erzwingt **kein** `https://` (`serverBaseUrl` wird ungeprüft übernommen,
`LicenseClient.cs:118`). Das prüfen wir wie beim Heartbeat selbst.
### D-4 · `ILicensePrompt` ist eine tote Naht · **mittel** · Wo: DC (Doku) + unsere Analyse
`LicenseClient` nimmt ein `ILicensePrompt` entgegen und legt es in `_prompt` ab
(`LicenseClient.cs:62,69-72`) — **benutzt wird es nirgends**, im ganzen SDK gibt es keine weitere
Fundstelle. Unsere `ANALYSE-Linux-Portierung.md` (§5.2 und Risiko R6) baut die headless-Story
darauf auf, dass diese Abstraktion den Start auf einem Server ohne Display trägt. Sie tut nichts.
Kein Beinbruch — die Ablauflogik gehört ohnehin zu uns (`LicenseGate` hat sie bereits) —, aber die
Analyse muss korrigiert werden, und die Abstraktion bauen wir selbst.
### D-5 · Reihenfolgefalle beim Zugangsschutz trifft uns in der scharfen Variante · **mittel** · Wo: wir
`UPGRADE.md §16.1` beschreibt für bestehende Produkte „erst ausliefern, dann scharfschalten". Für
**neue** Produkte gilt das ausdrücklich nicht: `/releases/polytrader/` entsteht erst mit dem ersten
Upload und wird beim nächsten `cli/tick.php`-Lauf geschützt. Es gibt also kein Zeitfenster, in dem
sich ein noch ungeschütztes Paket ziehen ließe.
**Der allererste ausgelieferte Build muss die Zugangsdaten bereits mitbringen.** Veröffentlichen wir
zuerst ein Release, dessen Anwendung `credentials` noch nicht übergibt, fällt die Tür hinter ihm zu
und jede Erstinstallation bekommt 401.
Zweitens: Der Schutz beruht auf `.htaccess` und trägt nur unter Apache mit `AllowOverride`. Die
Anzeige „GESCHÜTZT" beweist nichts — nur der **Selbsttest** tut es. Gehört in unsere Abnahme, nicht
in die Zuversicht.
### D-6 · Der Packager würde unsere Secrets mitveröffentlichen · **hoch** · Wo: wir
`pack-and-deploy` verpackt das Publish-Verzeichnis. Bei uns liegt dort:
| Datei | Inhalt | Wie sie dorthin kommt |
|---|---|---|
| `appsettings.Local.json` | **MySQL-Verbindung mit Passwort** | `CopyToOutputDirectory` in `PolyTrader.App.Avalonia.csproj` |
| `master.key` | AES-Master-Key aller at-rest-Secrets | wird neben der Anwendung erwartet (`Program.cs:309`) |
| `openrouter.key`, `server_settings.xml`, `data.db`, `Logs/` | API-Schlüssel, Watchdog-Token, Lizenzschlüssel, Betriebsdaten | Betrieb |
Ohne strikte `excludePatterns` landet all das im Paket und damit auf dem Server. Der Zugangsschutz
macht es nicht harmlos: `UPDATESERVICE §5A/Grenzen` sagt es selbst — dann ist es eben statt
öffentlich „für jeden Lizenznehmer sichtbar". Der Packager warnt inzwischen bei
zugangsdatenverdächtigen Dateien, aber die Warnung ist **keine Sperre**.
Gegenmaßnahme: eigenes Publish-Profil in ein sauberes Verzeichnis, explizite `excludePatterns`,
und als Abnahmekriterium ein Blick in das fertige Archiv.
### D-7 · `BuildInfo.targets` kommt bei unserer Auslieferungsart nicht mit · **mittel** · Wo: DC + wir
`Deploymentcenter.Client.csproj` setzt weder `PackageId` noch `Version` und packt die
`Deploymentcenter.BuildInfo.targets` **nicht** ins NuGet-Paket (kein
`<None Pack="true" PackagePath="build/">`). Der Guide empfiehlt stattdessen einen relativen
`<Import>` ins Nachbar-Repo — genau die Kopplung, die wir bei LicenseLabrador bewusst aufgegeben
haben (Begründung in `lib/nuget/README.md`: PolyTrader muss ohne das Schwester-Repo bauen).
**Wunsch an DC:** `PackageId`/`Version` setzen und die Targets als `build/Deploymentcenter.Client.targets`
mitpacken — dann greift sie beim Paketverweis von selbst.
**Bis dahin bei uns:** die Datei einmalig nach `build/` kopieren, mit Herkunfts- und
Aktualisierungsvermerk.
### D-8 · Wir haben keine Versionsdisziplin · **mittel** · Wo: wir
In keiner `.csproj` steht ein `<Version>`, eine `Directory.Build.props` gibt es nicht. Damit trägt
jede Assembly `1.0.0.0`.
Der Packager prüft `--version` gegen die Hauptassembly und bricht bei Abweichung ab — das ist gut
so, denn der Fehler dahinter ist übel: Wird `1.0.1` als `1.0.2` veröffentlicht, aktualisieren alle
Installationen, melden weiterhin `1.0.1`, halten das Release erneut für neu und aktualisieren bei
**jedem Start** wieder. Ohne zentrale Version ist ein Release entweder unmöglich oder gefährlich.
Muss **vor** dem ersten Paket stehen.
### D-9 · `setup.json` kann unsere Konfiguration nicht schreiben · **mittel** · Wo: wir
Der Installer beherrscht die Formate `json` und `env` (`SETUP_INTEGRATION_GUIDE §3/§4`). Unsere
Betriebseinstellungen liegen in `server_settings.xml` — eigenes XML-Format, das er nicht kennt.
Konsequenz: Entweder verzichtet die Erstinstallation auf jede Vorbelegung, oder der
Deploymentcenter-Block (BaseUrl, Token, Kanal, Source, Lizenzschlüssel) zieht nach
`appsettings.json`. Zweiteres ist ohnehin sauberer — die Anwendung liest bereits
`appsettings.Local.json` über die `IConfiguration`, und nur so kann die Erstinstallation das
Anwendungstoken per `source: "provision"` selbst ziehen, ohne dass jemand ein Token abtippt.
### D-10 · Die Erstinstallation kann den Master-Key nicht lösen · **mittel** · Wo: wir
Der Installer schreibt ausdrücklich **Klartext** und verschlüsselt nichts
(`SETUP_INTEGRATION_GUIDE §3/Dateirechte`). Einen `POLYTRADER_MASTER_KEY` kann er nicht erzeugen.
Eine frisch installierte Instanz hätte also alle Secrets unverschlüsselt liegen, bis jemand von
Hand nachhilft — auf einem Zielsystem im Ausland der wahrscheinlichste Ort für „später".
Optionen: (a) Master-Key als `type: "secret"`-Feld abfragen, (b) PolyTrader erzeugt beim ersten
Start selbst einen und legt ihn mit `0600` ab, (c) über die systemd-Unit als Umgebungsvariable.
**Empfehlung: (b) + (c)**, (a) wäre eine weitere Stelle, an der jemand ein Geheimnis abtippt.
Positiv: Die im Guide geforderte Weiche „Entschlüsselung reicht Klartext unverändert durch" haben
wir bereits (`SecretProtection.Unprotect`). Der Guide warnt zu Recht, sie nicht eines Tages als
tote Altlast zu entfernen — sie gehört als **aktiv genutzt** dokumentiert.
### D-11 · Pfad-Inkonsistenz, die erst beim Installieren auffällt · **klein** · Wo: wir
`ServerSettings.Load("server_settings.xml")` ist **relativ zum Arbeitsverzeichnis**, `master.key`
wird dagegen aus `AppContext.BaseDirectory` gelesen (`Program.cs:309`). Solange man aus dem
Projektordner startet, fällt das nicht auf. Nach einer Installation nach `/opt/polytrader` mit
einer systemd-Unit, deren `WorkingDirectory` woanders zeigt, liest die Anwendung eine andere Datei,
als der Installer geschrieben hat. Vor D-6/L2 klären.
### D-12 · Der bestehende Heartbeat passt nicht — mehr als „Header und URL" · **klein** · Wo: wir
`WatchdogHeartbeatService` sendet an `{base}/api/heartbeat` mit `X-Watchdog-Key`
(`WatchdogHeartbeatService.cs:280,428`). Das Deploymentcenter erwartet
`{base}/api/watchdog/v1/ping` mit `Authorization: Bearer` (alternativ `X-Agent-Token`), Events
gehen nach `/api/watchdog/v1/event`.
`ANALYSE-Linux-Portierung.md §5.2` nennt das „nur Header und Ziel-URL". Tatsächlich kommen dazu:
`version` (sonst lässt sich ein Ausfall keinem Rollout zuordnen), `os`, `group`, das ganze
`checks`-Konzept, `metrics` — und `status: "stopped"`, das die Fehlalarm-Vermeidung beim geordneten
Beenden übernimmt. Unser `metadata`-Feld liest der Server dagegen nicht; es entfällt.
### D-13 · Error-Reporting braucht einen Handler, den es bei uns nicht gibt · **mittel** · Wo: wir
Weder `AppDomain.CurrentDomain.UnhandledException` noch `TaskScheduler.UnobservedTaskException`
kommen im Repo vor. Beim Bau lauern zwei Fallen:
1. **Rückkopplung.** Der Reporter meldet einen Fehlschlag über `TerminalLogger.Error` — was den
nächsten Report auslöst. Der Reporter darf seine eigenen Fehler niemals über den
eskalierenden Pfad melden. (DC drosselt bei 60 Reports/Minute/IP; darauf verlassen wir uns nicht.)
2. **Datenabfluss.** Unsere Fehlermeldungen enthalten Wallet-Adressen, Marktkennungen und Beträge,
der DC-Server steht beim Hoster. Das vorhandene `Sanitize()` des Heartbeats muss auch auf
Meldung **und Stacktrace** des Error-Reports laufen.
### D-14 · Kein Update ohne gültige Lizenz — auch nicht das reparierende · **klein, bewusst** · Wo: Entscheidung
Der Zugangsschutz koppelt Updates an eine gültige Lizenz. Eine Installation mit abgelaufener Lizenz
kann sich also nicht mehr selbst reparieren. Für eigene Systeme belanglos; sobald Kunden im Spiel
sind, ist es eine Entscheidung, keine Nebenwirkung.
### D-16 · `environment` im Fehler-Report ist heimlich ein Auth-Gate · **kleinmittel, überraschend** · Wo: DC (Doku + Fehlermeldung)
Beim Live-Test von D-3 lieferte `/api/errors/v1/report` mit einem funktionierenden,
`bugtracker:report`-berechtigten Token beharrlich `401 unauthorized: "Erwartet wird ein Token mit
dem Recht \"bugtracker:report\""` — obwohl der Token genau dieses Recht trug (per
`/api/tokens/v1/provision` selbst bestätigt).
Ursache: `report.php` liest `environment` aus dem **Request-Body** und reicht es an
`ApiAuth::requireScope($db, 'bugtracker:report', $environment, false)` durch;
`TokenManager::validateToken` lehnt danach ab, wenn `token['environment'] !== 'all' &&
token['environment'] !== $environment` (`TokenManager.php:317`). Unser Token war bei der
Provisionierung auf `environment: "development"` festgelegt, der Report schickte
`"environment": "production"` — Mismatch, Ablehnung.
Zwei Dinge daran sind problematisch:
1. **Kein Dokument erwähnt diese Kopplung.** `bugtracker.md §3` führt `environment` als reine
Metadatum-Angabe für `/report` (Bugtracker), nicht als Zugriffs-Gate. Für `/api/errors/v1/report`
steht es nirgends. Andere Endpunkte (`watchdog:ping`, `watchdog:read/evaluate`,
`license:deactivate`) übergeben `$environment` gar nicht erst an `requireScope` — die Kopplung
gilt also inkonsistent nur für diesen einen Endpunkt.
2. **Die Fehlermeldung nennt die falsche Ursache.** `401` mit `"Token mit dem Recht
bugtracker:report"` legt einen fehlenden Scope nahe. Der tatsächliche Grund (Environment-
Mismatch) taucht in der Antwort nirgends auf — von der Client-Seite aus ist das ohne Blick in den
Serverquelltext nicht diagnostizierbar.
**Für uns gelöst:** Token mit `environment: "all"` neu provisioniert (`dc_sub_109c1a03…`), gilt für
jeden gemeldeten Umgebungswert. **Wunsch an DC:** entweder den wahren Grund in der Fehlerantwort
nennen (z.B. eigener Code `environment_mismatch`), oder die Kopplung ganz aus `/errors/v1/report`
entfernen und `environment` wie bei den anderen Endpunkten rein deskriptiv behandeln.
### D-15 · `Unauthorized` wird nur im statischen Zweig erkannt · **klein** · Wo: DC
`CheckForUpdateAsync` setzt `result.Unauthorized` nur, wenn die statische `latest.json` mit 401
antwortet (`UpdateClient.cs:105-111`). Fehlt die `latest.json`, fällt die Prüfung auf die
**offene** API zurück — eine tote Lizenz sieht dann aus wie „kein Update vorhanden". Für die
Fehlersuche im Feld relevant.
---
## 2. Leitentscheidungen
1. **Das SDK kommt als lokales NuGet-Paket** in `lib/nuget`, wie schon `LicenseLabrador.Client` —
keine Projektreferenz ins Nachbar-Repo. Begründung unverändert: PolyTrader muss ohne das
Schwester-Repo bauen (Build-Server, Zielland-System).
2. **Der Heartbeat bleibt unsere eigene Implementierung.** Der Service ist erprobt (Live-Reload,
Entprellung, TLS-Zwang, No-Throw-Vertrag, 21 Tests); umgestellt werden Endpunkt, Auth und
Payload — nicht die Mechanik.
3. **Kein `Environment.Exit`, nirgends.** Weder bei ungültiger Lizenz (Entscheidung vom 31.07.2026)
noch beim Update (D-1). Ein Update läuft über denselben geordneten Weg wie das Beenden über die
Menüleiste, inklusive Bestätigungsdialog.
4. **Updates werden nie automatisch installiert.** Die Anwendung prüft beim Start, meldet im
Launcher und installiert erst auf ausdrückliche Bestätigung. Auch ein `is_critical`-Release
erzwingt nichts — offene Positionen schlagen jedes Sicherheitsargument.
5. **Revidiert bei der Umsetzung (14.08.2026):** Ursprünglich geplant war, dass der gesamte
Deploymentcenter-Block nach `appsettings.json` zieht. Für Watchdog-Felder (`WatchdogUrl`,
`WatchdogToken`, `WatchdogSource`, `WatchdogInstance`, `WatchdogIntervalSeconds`) blieb es bei
`server_settings.xml`/`ServerSettings` — die bestehende UI (maskierte Eingabe, Live-Reload) deckt
den Bedarf bereits ab, ein zweites Konfigurationssystem dafür wäre unnötig gewesen. Nur Werte
**ohne** bestehende UI-Heimat (ProjectSlug, Channel, Feature-Schalter für D-3/D-4) liegen in
`appsettings.json`/`DeploymentcenterOptions`; Secrets darin (Token, LicenseKey für D-2) in der
gitignorierten `appsettings.Local.json`, zusätzlich über `SecretProtection` verschlüsselbar wo
die bestehende Ablage (`ServerSettings.LicenseKey`) das schon leistet.
6. **Ausgeliefert wird ausschließlich `PolyTrader.App.Avalonia`**, für `win-x64` und `linux-x64`
(Entscheidung Richard, 14.08.2026: WinForms verschwindet demnächst vollständig, L5).
**Folge für diesen Plan:** Lizenzdialog und Startgate werden **nur noch in Avalonia** gebaut,
nicht in beiden Anwendungen. `Licensing/LicenseGate.cs` und `Ui/LicenseDialog.cs` im
WinForms-Projekt werden nicht portiert, sondern ersetzt und gelöscht. Das spart eine komplette
zweite Verdrahtung — und die WinForms-Fassung wäre ohnehin die einzige mit Lizenzprüfung
gewesen, während die tatsächlich benutzte Anwendung ungeprüft läuft.
7. **Watchdog und LicenseLabrador werden vollständig ausgebaut** (Entscheidung Richard,
14.08.2026). Kein Parallelbetrieb, keine Rückfallebene: Nach D-1/D-2 gibt es genau einen
Gegenüber, das Deploymentcenter. Siehe Schnitt **D-6**.
8. **Ein Anwendungstoken je Installation**, per `source: "provision"` beim Setup gezogen und auf
`watchdog:ping` + `bugtracker:report` beschränkt. Ein Master-Token wird **nie** ausgeliefert.
9. **Produkt-Slug ist `polytrader`** (Entscheidung Richard, 14.08.2026). Das alte `pt` aus dem
LicenseLabrador-Admin entfällt mit diesem Umzug ersatzlos.
10. **Der Master-Key wird bei der Erstinstallation erzeugt** (Entscheidung Richard, 14.08.2026),
nicht abgefragt — Einzelheiten in **D-5**.
---
## 3. Umsetzung in Schnitten
### D-0 · Fundament (ohne Serverkontakt) ✅ ERLEDIGT (14.08.2026)
Umgesetzt wie geplant: `Directory.Build.props` mit `<Version>0.1.0</Version>` für alle Projekte,
`Deploymentcenter.Client` 2.5.0 als lokales NuGet-Paket in `lib/nuget` (PackageReference in
`PolyTrader.Core.csproj`, `BuildInfo.targets` kommt jetzt automatisch über `build/` aus dem Paket
D-7 war zu diesem Zeitpunkt bereits vom Deploymentcenter-Team behoben, keine manuelle Übernahme
nötig), `nuget.config`-Mapping um `Deploymentcenter.*` ergänzt, `DeploymentcenterOptions` +
`Deploymentcenter`-Sektion in `appsettings.json` (Secrets in `appsettings.Local.json`, gitignored).
`PolyTrader.Core.BuildInfo.Version` erzeugt sich korrekt bei jedem Build. LicenseLabrador-Paket
bewusst noch nicht entfernt (das ist D-6, erst nach Live-Abnahme).
### D-0-Rest · Zwischenverifikation der Befunde (14.08.2026)
Vor dem Start Richards Hinweis geprüft, dass die Befunde behoben sein sollten: **5 von 6 bestätigt
behoben** (Commit `1967b49`, Deploymentcenter-Team) D-1 (Exit/`--wait-timeout`), D-2 (argv-Key,
jetzt `DC_LICENSE_KEY`-Umgebungsvariable), D-4 (`ILicensePrompt` jetzt über `EnsureLicensedAsync()`
genutzt), D-6 (Packager bricht bei Secrets ab statt nur zu warnen), D-7 (Paket jetzt `IsPackable`,
Version 2.5.0). Als Bonus zusätzlich behoben: D-15 (`Unauthorized` jetzt auch im API-Rückfallzweig
erkannt). **D-3 (unsigniertes Lizenzurteil) bleibt bestätigt offen** vom Deploymentcenter-Team
selbst so benannt („bewusst nicht Teil dieses Commits"), deckt sich mit unserer Einordnung in §6.
### D-1 · Watchdog auf die Deploymentcenter-API umstellen ✅ ERLEDIGT (14.08.2026)
Umgesetzt in [WatchdogHeartbeatService.cs](../../src/PolyTrader.Core/Services/WatchdogHeartbeatService.cs).
Abweichung von der ursprünglichen Planung: **kein neues Konfigurationsschema.** `ServerSettings`
(XML, Live-Reload, maskierte Token-Eingabe bereits erprobte Mechanik) bleibt die Quelle für
`WatchdogUrl`/`WatchdogToken`/`WatchdogSource`/`WatchdogInstance`/`WatchdogIntervalSeconds`; nur
ihre Bedeutung ändert sich (zeigen jetzt auf das Deploymentcenter statt auf watchdog.mhdf.de).
`DeploymentcenterOptions`/`appsettings.json` wäre hier ein zweites, unnötiges Konfigurationssystem
gewesen die bestehende UI/Masking/Live-Reload-Maschinerie deckt genau denselben Bedarf ab.
`DeploymentcenterOptions` bleibt für D-2/D-3/D-4 reserviert (Werte ohne bestehende UI-Heimat:
ProjectSlug, Channel, Feature-Schalter).
- Endpunkte `…/api/watchdog/v1/ping` und `…/api/watchdog/v1/event`, Header `Authorization: Bearer`
statt `X-Watchdog-Key`. `TryBuildEndpoint` nimmt jetzt einen relativen Pfad statt eines
Aktionsnamens (`PingPath`/`EventPath`-Konstanten).
- Payload ergänzt: `version` (aus `PolyTrader.Core.BuildInfo.Version`, nicht mehr aus der
Assembly-Version stimmt jetzt mit `Directory.Build.props` überein), `os`
(`RuntimeInformation.OSDescription`). Das bisherige `metadata`-Feld entfällt (der Server liest es
nicht, siehe Befund D-12).
- **`checks`-Mechanik neu:** `RegisterCheck(name, Func<(bool ok, string? message)>)` /
`UnregisterCheck(name)` Composition Root registriert die konkreten Prüfungen, der Dienst selbst
kennt ihre Bedeutung nicht. Eine werfende Prüfung meldet sich selbst als fehlgeschlagen statt den
Heartbeat zu verhindern. In `Program.cs` (Avalonia) verdrahtet: `vpn`-Prüfung gegen
`MullvadVpnService.IsConnected`. **`db`/`market_feed`-Prüfungen sind bewusst noch nicht verdrahtet**
die jeweiligen Dienste geben aktuell kein Gesundheitssignal nach außen; das ist ein Fast-Follow,
kein offener Blocker.
- Beim Herunterfahren sendet `StopAsync` jetzt **zwei** Dinge (vorher nur das Event): einen
Heartbeat mit `status:"stopped"` (das ist, was der Evaluator tatsächlich auswertet) **und**
weiterhin das `stopped_graceful`-Event ins Ereignisprotokoll.
- Tests: 8 neue/geänderte Fälle (Endpunkte, Bearer-Header, `checks`-Payload inkl. Fehlerfall),
gesamt 454 Tests grün.
**Live-Abnahme (14.08.2026, gegen den echten Server, Dev-Sub-Token):**
`POST /api/watchdog/v1/ping` mit dem neuen Payload → `state: "up"`, `app_version` korrekt übernommen.
`POST /api/watchdog/v1/event` (`stopped_graceful`) → angenommen. Erneuter Ping mit
`status:"stopped"` → Monitor wechselt zu `state: "stopped"`, `state_changed: true`. Test-Monitor
`polytrader-dev-probe` bleibt im Deploymentcenter stehen (Zustand `stopped`, alarmiert nicht).
### D-2 · Lizenz auf das Deploymentcenter umstellen ✅ ERLEDIGT (15.08.2026)
Umgesetzt in [Licensing/LicenseGate.cs](../../src/PolyTrader.App.Avalonia/Licensing/LicenseGate.cs),
[Licensing/LicenseCli.cs](../../src/PolyTrader.App.Avalonia/Licensing/LicenseCli.cs) und den neuen
Lizenz-Schaltflächen in [SettingsWindow.axaml(.cs)](../../src/PolyTrader.App.Avalonia/Views/SettingsWindow.axaml).
Zwei Abweichungen von der ursprünglichen Planung, beide vereinfachend:
- **Kein eigenes `ILicensePrompt`, kein eigener modaler Lizenzdialog.** Für die GUI wird die
bereits vorhandene, erprobte Settings-Oberfläche wiederverwendet — dieselbe maskierte
Prompt/Save-Mechanik wie beim Watchdog-Token (`btnLicenseKey` = „Lizenzschlüssel setzen …",
speichert verschlüsselbar in `server_settings.xml`) plus ein neuer `btnCheckLicense` = „Lizenz
jetzt prüfen", der den gespeicherten Schlüssel gegen den Server validiert und Status + Hardware-ID
in einem Info-Dialog anzeigt. Ein zusätzliches, eigenes Dialogfenster nur für die Lizenz wäre eine
zweite, redundante Eingabe-Oberfläche gewesen. Für `--headless`/CLI genügt die vom SDK
mitgelieferte `ConsoleLicensePrompt` (wird hier gar nicht gebraucht, siehe nächster Punkt) —
eine eigene Fassung hätte keinen Mehrwert geboten.
Grund für den Verzicht auf einen echten Avalonia-Dialog **vor** dem Hauptfenster: Avalonias
Startreihenfolge (`AppBuilder.Setup()` darf pro Prozess nur einmal laufen) macht einen zweiten,
vorgeschalteten Dialog vor `StartWithClassicDesktopLifetime` unnötig fragil; ein im laufenden
Fenster erreichbarer Button ist die robustere Lösung und deckt denselben Bedarf.
- **Modulentscheidung läuft still, ohne Dialog, vor dem Host-Aufbau.** `LicenseGate.CheckUsable`
validiert den in `server_settings.xml` gespeicherten Schlüssel synchron (wie einst die
WinForms-Fassung) und entscheidet, welche Module überhaupt registriert werden **nicht** über
`EnsureLicensedAsync`/`ILicensePrompt` des SDK (die interaktive Korrektur läuft über die
Settings-UI, s.o., nicht über eine erneute Abfrage an dieser Stelle). Eine per Dialog neu
eingetragene Lizenz wirkt für Module deshalb **erst beim nächsten Start** bewusst, konsistent
mit der Entscheidung vom 31.07.2026 (keine Laufzeit-Revalidierung).
- `DeploymentcenterOptions` wird jetzt **vor** dem Host-Aufbau eigenständig aus
`appsettings.json`/`appsettings.Local.json` geladen (`Program.LoadDeploymentcenterOptions`), weil
die Modulliste schon vor dem eigentlichen `IHost` feststehen muss. Dieselbe Instanz wird
anschließend auch reguär in die DI registriert.
- **`--smoke-ui` bewusst ausgenommen:** `enforceLicense: !smoke` — die Konstruktionsprüfung soll auf
einem (nie lizenzierten) Build-Server weiterhin ALLE Modul-Fenster testen, nicht nur die
Core-Shell. Verifiziert: `--smoke-ui` konstruiert weiterhin alle 4 Modul-Fenster plus das
Einstellungsfenster mit den neuen Lizenz-Feldern (7 statt 6 Settings-Abschnitte).
- TLS wird — anders als im SDK (Befund D-3) — im Gate selbst erzwungen, exakt wie beim Watchdog.
- CLI-Schalter `--license-status`, `--license-set-key <schlüssel>`, `--license-deactivate` nach der
Konvention aus LICENSE_INTEGRATION_GUIDE §4. `--license-deactivate` verlangt `DC_SHARED_KEY` aus
der Umgebung (Administrationsrechner) und verweigert sich mit klarer Meldung ohne ihn — der
Shared Key gehört nicht in die ausgelieferte Anwendung.
- **Keine eigenen Unit-Tests angelegt** (wie schon bei der WinForms-Fassung dokumentiert
begründet): `PolyTrader.Tests` referenziert `PolyTrader.App.Avalonia` nicht, der Gate ist dünner
Leim über dem SDK. Abdeckung über `--smoke-ui` (Konstruktion) und die Live-Verifikation unten.
**Live-Verifikation (15.08.2026, echter Server, Dev-Lizenz mit 9999 Aktivierungen von Richard):**
```
> PolyTrader.App.Avalonia.exe --license-set-key LLAB2-12345-67890-ABCDE-FGHIJ
valid: License is valid
Lizenz gültig und in server_settings.xml gespeichert (Klartext).
> PolyTrader.App.Avalonia.exe --license-status
HWID v2: 2:win:a765bd47c2458fd518a7a1ad8a9d1d4b654c91085dca4667f33e7cad4427bf93 (machine-guid)
Produkt: polytrader
Server: https://dc.mhdf.de
Cache: valid
Offline noch bis: 2026-08-18 07:56:54Z
Gespeicherter Schlüssel (server_settings.xml): gesetzt (Klartext)
```
Damit ist derselbe Codepfad geprüft, den `LicenseGate.CheckUsable` beim Start benutzt
(`LicenseClient.ValidateAsync`) nicht nur ein isolierter Testaufruf. Die volle GUI/`--headless`
wurde **bewusst nicht** gestartet, um keine echten Handelsverbindungen (Polymarket-WSS,
Alchemy-Listener) aus dieser Sitzung heraus zu öffnen; das bleibt Richards manuelle Abnahme.
`max_activations` war bereits großzügig gesetzt (Richards Hinweis), keine Admin-Änderung nötig.
### D-3 · Fehler-Reporting ✅ ERLEDIGT (17.08.2026)
Umgesetzt in [DeploymentcenterErrorReporter.cs](../../src/PolyTrader.Core/Services/DeploymentcenterErrorReporter.cs)
und [SecretRedactor.cs](../../src/PolyTrader.Core/Security/SecretRedactor.cs) (die
Secret-Muster-Prüfung aus `WatchdogHeartbeatService.Sanitize` wurde dorthin ausgelagert, damit
Heartbeat-Meldung — kurz, einzeilig — und Stacktrace — lang, mehrzeilig — dieselbe Musterprüfung,
aber unterschiedliche Kürzungsregeln bekommen, ohne Code zu verdoppeln).
- Drei Quellen wie geplant: `AppDomain.UnhandledException` (`level: "fatal"`, synchron mit 3-s-Frist
gesendet, da der Prozess unmittelbar danach stirbt), `TaskScheduler.UnobservedTaskException`
(`"warning"`, `SetObserved()` verhindert einen Prozessabbruch durch die schon behandelte Ausnahme),
`TerminalLogger`-Fehler (`"error"`, Fire-and-forget wie beim Heartbeat).
- **No-Throw + keine Rückkopplung** wie gefordert: eigene Sendefehler laufen ausschließlich über
`TerminalLogger.Warning` (niemals `.Error` sonst würde der Reporter sich selbst erneut auslösen,
da er genau auf `.Error` hört).
- Eigene Ratenbegrenzung: 20 Meldungen/Minute (gleitendes Fenster, kein Hintergrund-Timer), deutlich
unter dem Server-Limit von 60/Minute/IP Absicht ist, lokal zu drosseln, bevor eine
Fehlerschleife den Server oder andere Dienste auf derselben IP überhaupt erreicht.
- `Sanitize` (Nachricht, ≤200 Zeichen, einzeilig) und die neue `SanitizeStackTrace` (≤4000 Zeichen,
**mehrzeilig bleibt erhalten** ein einzeiliger Stacktrace wäre zum Debuggen wertlos) laufen
beide vor jedem Versand.
- **Korrektur gegenüber der Planung:** kein `client_ref`/Idempotenz-Schlüssel implementiert anders
als beim Bugtracker-`/report` dedupliziert `/api/errors/v1/report` bereits serverseitig über einen
inhaltsbasierten Gruppenschlüssel (bestätigt im Live-Test unten, `occurrence_count` stieg ohne
jedes Zutun). Ein zusätzlicher `client_ref` hätte hier keinen Zweck gehabt.
- Registrierung als `BackgroundService` wie `WatchdogHeartbeatService` (rein ereignisgesteuert,
`ExecuteAsync` wartet nur auf Abbruch) konsistent mit dem etablierten Muster, obwohl kein
eigener Takt gebraucht wird.
- Konfiguration über `DeploymentcenterOptions.Token`/`.ErrorReportingEnabled` (aus
`appsettings.Local.json`) anders als beim Watchdog gibt es hierfür keine bestehende
UI-Heimat in `ServerSettings`, passt also zur in D-1 begründeten Aufteilung.
- 14 neue Tests (Payload, Auth-Header, Secrets aus Nachricht UND Stacktrace, Ratenbegrenzung,
alle drei Quellen inkl. `internal` gemachter Handler-Methoden für direkte Testbarkeit ohne echte
Prozessabstürze zu provozieren). Gesamt 468 Tests grün.
**Live-Verifikation (17.08.2026, echter Server):** Zwei identische Meldungen über
`/api/errors/v1/report` mit dem exakten Payload-Format des neuen Codes → erste Meldung
`is_new: true, occurrence_count: 1`, zweite `is_new: false, occurrence_count: 2` serverseitige
Deduplizierung bestätigt.
**Nebenbefund beim Live-Test: D-16** (neu, siehe §1) das `environment`-Feld im Report-Body ist
ein undokumentiertes Auth-Gate gegen die Provisionierung des Tokens; die Fehlermeldung bei einem
Mismatch nennt fälschlich einen fehlenden Scope. Für uns gelöst durch Neuprovisionierung mit
`environment: "all"` (Token in `appsettings.Local.json` aktualisiert, betrifft auch den
Watchdog-Token-Kommentar dort).
---
### D-0 · Fundament (ohne Serverkontakt) — ursprüngliche Planung
- `Directory.Build.props` mit **einer** `<Version>` für alle Projekte (D-8).
- `Deploymentcenter.BuildInfo.targets` nach `build/` übernehmen und einbinden; `BuildInfo.Version`
ersetzt das heutige `Assembly.GetName().Version` im Heartbeat (D-7).
- SDK paketieren: `dotnet pack Deploymentcenter.Client -c Release -o lib/nuget`; `nuget.config` um
das Quellen-Mapping `Deploymentcenter.*` → `local` ergänzen; `LicenseLabrador.Client` samt
`.nupkg` entfernen.
- Neuer Konfigurationsabschnitt `Deploymentcenter` in `appsettings.json` + `DeploymentcenterOptions`
(BaseUrl, ProjectSlug, Channel, Token, LicenseKey, Watchdog-Source/-Instance/-Intervall,
Schalter für Heartbeat / Fehlermeldungen / Update-Prüfung).
**Abnahme:** Solution baut, Tests grün, `BuildInfo.Version` stimmt mit `<Version>` überein,
`lib/nuget` enthält nur noch das DC-Paket.
### D-1 · Watchdog auf die Deploymentcenter-API umstellen — ursprüngliche Planung
- Endpunkte `…/api/watchdog/v1/ping` und `…/api/watchdog/v1/event`, Header
`Authorization: Bearer`. TLS-Zwang und Loopback-Ausnahme bleiben.
- Payload ergänzen: `version` (aus `BuildInfo`), `os`, `group`.
- `checks` füllen — das ist der eigentliche Gewinn gegenüber heute: `db` (MySQL erreichbar),
`market_feed` (Alter des letzten Ticks), `vpn` (Mullvad-Zustand), `modules` (laufen die aktiven
Module). Ein `ok`-Heartbeat mit fehlschlagender Prüfung wird serverseitig auf `warning` gestuft.
- `metrics` schlank halten (Jobs in der Warteschlange, offene Orders als **Anzahl**). Weiterhin
**keine** Kontostände, Wallet-Adressen oder Trader-Namen.
- Beim Herunterfahren `status: "stopped"` senden (zusätzlich zum bestehenden
`stopped_graceful`-Event) — das ist der vom Evaluator ausgewertete Weg.
- Konfiguration aus `DeploymentcenterOptions`; die alten `Watchdog*`-Felder aus `ServerSettings`
entfernen (mit einmaliger Übernahme beim Start, damit niemand neu tippt).
- `WatchdogHeartbeatTests` nachziehen.
**Abnahme:** Monitor erscheint auf `dc.mhdf.de` mit korrekter `version`; Beenden über die
Menüleiste → Monitor `stopped`, **kein** Alarm; Prozess abschießen → nach 4× Intervall genau **ein**
Down-Alarm. Voraussetzung, die vorher zu prüfen ist: `cli/tick.php` läuft dort tatsächlich als
Cron — ohne ihn sind alle Monitorzustände wertlos (`/api/health` → `checks.evaluator.ok`).
### D-2 · Lizenz auf das Deploymentcenter umstellen — ursprüngliche Planung
- `LicenseGate` auf `Deploymentcenter.Client.LicenseClient` umbauen:
`ValidateAsync(slug, key, baseUrl, appVersion)`, Ergebnis `LicenseValidationResult`.
`LicenseClient.DefaultAppVersion = BuildInfo.Version` einmal beim Start.
- Statusabbildung: `IsValid` → voller Start; **`IsTransient` → weiterlaufen und nur warnen**
(Netzstörung ist kein Urteil); alles andere → eingeschränkter Modus ohne Module.
`https://` selbst erzwingen (D-3).
- Eigene Prompt-Abstraktion bauen (D-4): Avalonia-Dialog für den Desktop, Konsolenvariante für
`--headless`. Der Dialog zeigt Hardware-ID (kopierbar), Status und Offline-Restlaufzeit.
- **Gate und Dialog entstehen im Avalonia-Projekt** — dort läuft heute gar keine Prüfung, und die
WinForms-Fassung wird nicht mitgezogen (Leitentscheidung 6). `Licensing/LicenseGate.cs` und
`Ui/LicenseDialog.cs` werden mit D-6 gelöscht, nicht portiert.
- CLI-Schalter `--license-status`, `--license-set-key`, `--license-deactivate` nach der Konvention
des Guides.
**Abnahme:** gültige Lizenz → alle Module; Netz getrennt → `valid_offline` mit sichtbarer
Restlaufzeit; Schlüssel entfernt → nur Core-Shell, kein Absturz; `--headless` ohne Display
blockiert nicht.
**Vor dem ersten echten Start im DC einstellen:** Produkt `polytrader` mit großzügigem
`max_activations` (ein Windows-Neuinstall erzeugt eine neue MachineGuid = neuer Platz) und
`default_cache_ttl_hours` als Offline-Kulanz für eigene Maschinen.
> **Ein Aktivierungsplatz wird beim ersten echten Start verbraucht.** Die Prüfung deshalb nicht
> „mal eben" von einer Entwicklermaschine aus ausprobieren, sondern dort, wo die Installation
> hingehört — so wie beim letzten Mal gehandhabt.
### D-3 · Fehler-Reporting — ursprüngliche Planung
- `DeploymentcenterErrorReporter` in `PolyTrader.Core`: `POST /api/errors/v1/report`, Scope
`bugtracker:report`.
- Quellen: `AppDomain.UnhandledException` (`level: "fatal"`),
`TaskScheduler.UnobservedTaskException` (`warning`), `TerminalLogger`-Fehler (`error`).
- Pflichtmerkmale: No-Throw, keine Rückkopplung (D-13), eigene Ratenbegrenzung,
`Sanitize()` auf Meldung **und** Stacktrace, abschaltbar über die Konfiguration.
- `client_ref` je Prozessstart+Fehlergruppe setzen, damit ein Neuversuch kein Duplikat anlegt.
- Serverseitig Ignore-Regeln für bekanntes Rauschen anlegen (Polymarket-429, VPN-Reconnect) —
jeweils **mit** Alarmschwelle, denn der Zähler ist der eigentliche Zweck.
**Abnahme:** erzwungener Testfehler erscheint als Gruppe; zweites Auftreten erhöht
`occurrence_count`, statt ein zweites Item anzulegen; bei nicht erreichbarem DC läuft PolyTrader
unverändert weiter und erzeugt **keine** Fehlerschleife.
### D-4 · Auslieferung (UpdateService) ✅ VOLLSTÄNDIG ERLEDIGT (20.08.2026)
**Umgesetzt (Schritte 1 und 3 der ursprünglichen Planung):**
- Update-Prüfung beim Start in [LauncherWindow.axaml(.cs)](../../src/PolyTrader.App.Avalonia/Views/LauncherWindow.axaml):
fire-and-forget, nur aktiv bei `Deploymentcenter:UpdateCheckEnabled` (Vorgabe `false`), nutzt
`ReleaseCredentials.FromLicenseKey(...)` mit dem entschlüsselten `ServerSettings.LicenseKey`.
Ein gefundenes Update erscheint als Banner (Version, Changelog, `is_critical`-Hinweis) mit
„Jetzt installieren …" und „Später". **Installiert nie automatisch** — deckt sich mit
Leitentscheidung 4. `result.Unauthorized` wird erkannt und bewusst still behandelt (die
Lizenzprüfung aus D-2 hat eine tote Lizenz bereits gemeldet, ein zweiter Hinweis wäre Rauschen).
- Installation nach Bestätigungsdialog über `UpdateClient.LaunchUpdateAgent(..., exitCurrentApp:
false, waitForCurrentProcess: true, waitTimeoutSeconds: 40, licenseKey: ...)`, danach
`_uiHost.RequestShutdown()` — **derselbe** geordnete Weg wie „Beenden" über die Menüleiste,
inklusive der 10-Sekunden-Sicherheitsabfrage. Kein Sonderpfad, der die Bestätigung umgeht.
- `deploy/packager.config.example.json` (Schritt 1) mit den `excludePatterns`/`preservePatterns`
aus der Planung, plus Nachtrag `*.pfx/*.key/*.pem/*.p12` (der Packager selbst prüft seit
Version 2.5.0 zusätzlich Dateiname UND Inhalt, siehe D-6 der Befunde — diese Liste ist die
erste, nicht die einzige Verteidigungslinie). `deploy/packager.config.json` (echte FTP-Zugangsdaten
+ `updateservice:publish`-Token) neu in `.gitignore`.
- **Live geprüft (HTTP-Vertrag, per curl):**
`GET /api/updateservice/v1/check?product=polytrader&version=0.1.0&channel=prod` →
`update_available: false, latest_version: "0.1.0"` — bestätigt, dass für `polytrader` aktuell
**kein** Release auf dem Server liegt (die `2.0.1`-Zeile in `sql/schema.sql` ist Beispiel-Seed-Daten
aus dem Deploymentcenter-Repo, nicht live). Der Code-Pfad selbst nutzt dieselbe SDK-Methode, die
bereits in D-1/D-2/D-3 erprobt ist.
**Nachgezogen (20.08.2026, auf Richards ausdrücklichen Wunsch — „lass uns einen Win und Linux
Release erstellen und hochladen"):**
- Signierschlüssel serverseitig bestätigt: `GET /api/updateservice/v1/pubkey` liefert einen
RSA-Key (Fingerprint `ed99a38d…`) — die zuvor offene Checkliste-Voraussetzung war in Wahrheit
schon erfüllt.
- `dotnet publish -c Release --self-contained true` für `win-x64` (124 MB) und `linux-x64`
(120 MB) in saubere `/tmp`-Verzeichnisse, **nicht** das Debug-Output. Self-contained bewusst
gewählt, weil der Zweck genau der Erstinstallations-Test auf einem frischen System ist, das
nicht zwingend eine .NET-Laufzeit mitbringt.
- **Fund dabei:** `appsettings.Local.json` (mein echtes lokales MySQL-Passwort + Dev-Token) landete
im Publish-Output — die `CopyToOutputDirectory`-Regel in der `.csproj`, die genau für diesen
Fall in D-6 der Befunde und im eigenen `packager.config.example.json`-Kommentar dokumentiert ist.
Vor jedem weiteren Schritt von Hand entfernt, dann zusätzlich mit einem eigenen Grep nach
gefüllten `Password=`-Feldern, `dc_master_…`/`dc_sub_…`-Tokens und privaten Schlüsseln über beide
Verzeichnisse geprüft (leer). Der Packager selbst hätte es beim `--allow-secrets`-losen Lauf
ohnehin blockiert — das war die zweite, nicht die einzige Verteidigungslinie.
- **`deploy/packager.config.json`** (gitignored) angelegt. FTP-Zugangsdaten kamen auf Richards
Anweisung aus `Deploymentcenter/scripts/deploy_config.json` (dieselbe Hosting-Zugangsdaten, mit
denen auch das Deploymentcenter selbst deployt wird). **Remote-Pfad per read-only FTPS-Listing
verifiziert statt geraten:** `/releases/` liegt als eigenständiges Verzeichnis auf FTP-Root-Ebene
(nicht unter `/public_html/releases/`, wie das Beispiel im UPDATESERVICE-Guide nahelegt) —
bestätigt durch die dort bereits vorhandenen Verzeichnisse `clawddotnet/` und `predictalytics/`
echter Nachbarprojekte. `ftpRemoteBaseDir: "/releases"` entsprechend gesetzt.
- Ein `updateservice:publish`-Token separat provisioniert (`environment: "all"`, wie schon bei D-3
gelernt) — nicht das Dev-Token aus `appsettings.Local.json` wiederverwendet, das hat nur
`watchdog:ping`/`bugtracker:report`.
- **Beide Releases veröffentlicht** über `pack-and-deploy`: `polytrader 0.1.0 (prod, win-x64)` und
`polytrader 0.1.0 (prod, linux-x64)`. Versionsgegenprobe lief automatisch durch (Assembly-Version
stimmte mit `--version` überein). `appsettings.json` korrekt als „GESCHÜTZT" (preservePatterns)
markiert, `*.pdb` je Projekt korrekt ausgeschlossen.
**Live-Abnahme (20.08.2026):**
- [x] `GET /api/updateservice/v1/check?product=polytrader&version=0.0.0&channel=prod&platform=win-x64`
**und** `...&platform=linux-x64` → beide `update_available: true, latest_version: "0.1.0"`.
- [x] Beide Antworten enthalten ein gefülltes `manifest_signature`-Feld — signiert.
- [x] Zugangsschutz: **nicht sofort aktiv** — `curl -I .../package.tar.gz` lieferte direkt nach dem
Publish noch `200` statt `401`. Deckt sich mit der Dokumentation: für ein *neues* Produkt
entsteht `/releases/polytrader/` erst mit dem ersten Upload, der Schutz erst mit dem
nächsten `cli/tick.php`-Durchlauf. Per Hintergrund-Poll auf `401` gewartet statt einfach
anzunehmen, dass er greift — nach rund zwei Minuten aktiv geworden.
- [x] **Vollständige Selbsttest-Sequenz danach grün:** ohne Zugangsdaten → `401` (beide
Plattformen); mit dem Dev-Lizenzschlüssel als Basic-Auth (`lic_<sha256-präfix>:<schlüssel>`)
→ `200`; `.htpasswd` direkt abgerufen → `403`.
Reihenfolge ist hier bindend — siehe D-5.
**Ursprüngliche Planung (jetzt umgesetzt, s.o.):**
1. `packager.config.json` mit strikten `excludePatterns` (`appsettings.Local.json`, `master.key`,
`openrouter.key`, `server_settings.xml`, `*.db`, `Logs/**`, `*.pdb`) und `preservePatterns`
(`appsettings.json`).
2. Sauberes Publish-Verzeichnis je Plattform (`win-x64`, `linux-x64`), **nicht** das Debug-Output.
3. In der Anwendung: Update-Prüfung beim Start mit
`credentials: ReleaseCredentials.FromLicenseKey(...)`, Anzeige im Launcher, Installation nur auf
Bestätigung und über den geordneten Shutdown-Pfad (D-1).
4. **Erst danach** das erste Release veröffentlichen.
5. Zugangsschutz erzeugen und **Selbsttest** laufen lassen; zusätzlich von Hand:
`curl -I …/releases/polytrader/prod/win-x64/<version>/package.tar.gz` → 401.
**Abnahme:** Update auf einem Testsystem behält `appsettings.Local.json`, `server_settings.xml`,
`master.key` und `data.db`; eine im neuen Release entfallene DLL ist verschwunden; ein Abbruch
mitten im Anwenden hinterlässt eine lauffähige Installation (Rollback); und der Blick ins Archiv:
```bash
tar -tzf package.tar.gz | grep -Ei 'local|master\.key|openrouter|settings\.xml|\.db$'
```
Diese Ausgabe muss **leer** sein.
### D-5 · Erstinstallation (`setup.json`) ✅ Code-seitig erledigt (17.08.2026), Live-Abnahme offen
**Umgesetzt:**
- [setup.json](../../src/PolyTrader.App.Avalonia/setup.json) im App-Projekt (per `CopyToOutputDirectory`
im Publish-Paket) mit zwei Zielen: `appsettings.json` (Deploymentcenter-Block: `BaseUrl` via
`detect:baseurl`, `ProjectSlug` via `detect:project`, `Channel`, `Token` via `provision` mit den
Scopes `watchdog:ping`+`bugtracker:report`) und `appsettings.Local.json` (`Database:MySqlConnectionString`
als `secret`).
**Korrektur gegenüber der ursprünglichen Planung:** kein `Watchdog:Source`-Feld — die
Watchdog-Konfiguration liegt seit D-1 bewusst in `server_settings.xml` (maskierte UI,
Live-Reload), und `setup.json` kann nur `json`/`env`-Ziele schreiben, kein XML. Aktivierung von
Watchdog **und** Eintragen des Lizenzschlüssels bleiben deshalb bewusste Post-Install-Schritte
über das Einstellungsfenster — beide sind seit D-1/D-2 genau dafür gebaut (maskierte
Eingabe-Buttons, sofort wirksam ohne Neustart). Im `setup.json`-Kommentar dokumentiert, damit das
nicht als vergessenes Feld missverstanden wird.
- **Master-Key-Erzeugung (D-10)** in [MasterKeyResolver.cs](../../src/PolyTrader.Core/Security/MasterKeyResolver.cs)
(neu, testbar in `PolyTrader.Core` statt direkt in `Program.cs` verdrahtet) + gemeinsamer
[FilePermissions.cs](../../src/PolyTrader.Core/Security/FilePermissions.cs)-Helfer (ersetzt die
bisher in `SettingsWindow.axaml.cs` duplizierte `RestrictToOwner`-Logik). Reihenfolge:
Umgebungsvariable → vorhandene Datei → **nur wenn beides fehlt** neu erzeugen. 8 Tests, u.a.
explizit „ein zweiter/dritter Aufruf erzeugt niemals neu" und „ein fehlgeschlagener Schreibversuch
wirft nie, sondern warnt". Live geprüft: frisches Verzeichnis ohne `master.key` →
`--smoke-ui`-Lauf erzeugt die Datei mit der erwarteten Log-Meldung, `SecretProtection` aktiviert
sich sofort danach in derselben Sitzung.
- [polytrader.service](../../deploy/polytrader.service) (systemd-Unit-Vorlage) löst **D-11**: setzt
`WorkingDirectory` ausdrücklich auf das Installationsverzeichnis, mit Begründung im Kommentar
(sonst laden `server_settings.xml`, relativ zum Arbeitsverzeichnis, und `master.key`, relativ zu
`AppContext.BaseDirectory`, aus unterschiedlichen Orten). `SIGTERM` mit 45 s Frist für den
geordneten Shutdown-Pfad statt `SIGKILL`.
**Bewusst nicht gemacht:**
- **Installationskonto im DC anlegen** — das Anlegen eines Kontos mit Zugangsdaten ist eine
Handlung, die grundsätzlich beim Menschen bleibt, nicht beim Agenten. Richard legt es im WebUI
unter *System → 👤 Benutzer* an (Rolle `installer`, siehe SETUP_INTEGRATION_GUIDE §2).
- **Volle Live-Abnahme** (`update-agent --action install` auf einem echten System) — braucht ein
veröffentlichtes Release (D-4, noch offen, siehe dort) und das Installationskonto von oben. Ohne
beides lässt sich nur die App-seitige Hälfte prüfen, was hiermit geschehen ist.
Ursprünglich geplant, jetzt überholt:
- **Master-Key beim ersten Start selbst erzeugen** (Entscheidung Richard, 14.08.2026):
`ConfigureSecretProtection` legt, wenn weder `POLYTRADER_MASTER_KEY` noch `master.key` vorliegen,
einen kryptografisch zufälligen Schlüssel an und schreibt ihn mit `File.SetUnixFileMode(…,
UserRead | UserWrite)` — sonst entsteht er unter Linux mit `644`, also world-readable.
Drei Punkte, die dabei nicht untergehen dürfen:
- **Nur erzeugen, wenn wirklich keiner da ist.** Ein versehentliches Überschreiben macht jedes
bereits verschlüsselte Secret in der Datenbank unlesbar — das ist der einzige wirklich
unumkehrbare Fehler in diesem ganzen Plan. Erzeugung deshalb ausschließlich beim Nichtvorhandensein
der Datei, mit deutlichem Log-Eintrag und einem Hinweis, den Schlüssel zu sichern.
- **Ort ist `AppContext.BaseDirectory`** — dieselbe Stelle, an der er heute gelesen wird. Zusammen
mit D-11 klären, dass die Anwendung nach der Installation dieselbe Datei findet.
- **`master.key` gehört in `excludePatterns`** (D-6) und darf nie in ein Paket geraten.
- Installationskonto im DC anlegen (Rolle `installer`, **kein** Admin).
- systemd-Unit schreiben — der Installer legt bewusst keinen Dienst an. Dabei D-11 auflösen
(`WorkingDirectory` = Installationsverzeichnis oder absolute Pfade).
**Abnahme:** `update-agent --action install` auf einem frischen Linux-System führt zu einer
lauffähigen Installation, ohne dass eine Datei von Hand bearbeitet wird; `master.key` entsteht mit
`600`; ein zweiter Start erzeugt **keinen** neuen Schlüssel.
### D-6 · Watchdog und LicenseLabrador ausbauen ✅ Code-seitig erledigt (22.08.2026), Serverabschaltung offen
Entscheidung Richard (14.08.2026): kein Parallelbetrieb. Läuft **nach** D-1 und D-2, damit zu
keinem Zeitpunkt beides fehlt. Umgesetzt im Frühjahrsputz vom 22.08.2026, gemeinsam mit dem
WinForms-Ausbau (P11/L5) — beide Schnitte betrafen dieselben Dateien.
- [x] `LicenseLabrador.Client` aus `lib/nuget` entfernt; `PolyTrader.App.csproj` existiert nicht
mehr. Quellen-Mapping in `NuGet.Config` trägt nur noch `Deploymentcenter.*`, `lib/nuget/README.md`
beschreibt jetzt das Deploymentcenter-SDK.
- [x] `Licensing/LicenseGate.cs` und `Ui/LicenseDialog.cs` gelöscht — ersetzt durch die
Avalonia-Fassung aus D-2.
- [x] Beide Altpläne als **abgelöst** gekennzeichnet;
[ANALYSE-Linux-Portierung.md](../ANALYSE-Linux-Portierung.md) auf Revision 6 gezogen (P11/L5
erledigt), Leitfaden-Abschnitte B und C aufgelöst.
- [x] **Abweichung vom Plan, bewusst:** Die `Watchdog*`-Felder in `ServerSettings` bleiben stehen.
Die Planzeile „alte Watchdog-Felder entfernen" stammt aus der Zeit vor D-1 — D-1 hat die Felder
jedoch **umgewidmet** (sie zeigen jetzt auf die Deploymentcenter-API) statt sie zu ersetzen. Sie
werden von `WatchdogHeartbeatService`, `DeploymentcenterOptions`, dem Einstellungsfenster und den
Tests aktiv benutzt; ein Entfernen wäre ein Rückschritt gewesen.
- [ ] **Serverseitig offen:** `watchdog.mhdf.de` und `license.mhdf.de` abschalten, sobald die
Live-Abnahme steht. Damit erledigen sich die beiden offenen Auflagen aus dem Altplan (Secrets
rotieren, UTC/`NOW()`) von selbst — **nicht** vorher abschalten, sonst fehlt die Rückfallebene
während der Abnahme.
**Abnahme-Kriterium erfüllt:** Die Solution baut ohne LicenseLabrador-Paket (0 Fehler, 476 Tests
grün), und `grep -ri licenselabrador` findet im Code nur noch historische Erwähnungen in
Kommentaren und Plandokumenten.
**Abnahme:** Die Solution baut ohne LicenseLabrador-Paket, `grep -ri licenselabrador` findet im
Code nur noch historische Erwähnungen in Plandokumenten, und `server_settings.xml` enthält keine
Watchdog-Felder mehr.
> **Reihenfolge:** Die alten Dienste erst abschalten, wenn D-1 und D-2 **live** abgenommen sind —
> nicht, wenn sie gebaut sind.
---
## 4. Reihenfolge, Abhängigkeiten, Aufwand
| Schritt | Inhalt | hängt an | Aufwand | Stand |
|---|---|---|---|---|
| D-0 | Fundament: Version, SDK-Paket, Optionen | — | 0,5 PT | ✅ 14.08.2026 |
| D-1 | Watchdog umstellen | D-0 | 0,51 PT | ✅ 14.08.2026, live verifiziert |
| D-2 | Lizenz umstellen + Prompt-Naht (nur Avalonia) | D-0 | 1 PT | ✅ 15.08.2026, live verifiziert |
| D-3 | Fehler-Reporting | D-0 | 0,51 PT | ✅ 17.08.2026, live verifiziert |
| D-4 | Auslieferung | D-0, D-2 (Schlüssel für den Paketabruf) | 12 PT | ✅ 20.08.2026, `polytrader 0.1.0` live für win-x64+linux-x64, signiert, live geprüft |
| D-5 | Erstinstallation + Master-Key-Erzeugung | D-4 | 0,51 PT | Code ✅ 17.08.2026; Release liegt jetzt vor — Live-Install fehlt nur noch das Installationskonto (Richard) |
| D-6 | Watchdog/LicenseLabrador ausbauen | Live-Abnahme von D-1 und D-2 | 0,5 PT | offen — beide Vorbedingungen live ✅, wartet auf Richards manuelle GUI-Abnahme |
**Summe 47 PT.** D-1, D-2 und D-3 sind untereinander unabhängig und können in beliebiger
Reihenfolge laufen. Zwei Reihenfolgen sind dagegen bindend:
- **D-2 vor D-4** — ohne Lizenzschlüssel im Update-Weg sperrt uns der Zugangsschutz mit dem ersten
Release aus (Befund D-5).
- **D-6 nach der Live-Abnahme**, nicht nach dem Bau.
### Serverseitige Voraussetzungen (vor D-1)
- [x] Projekt `polytrader` im Deploymentcenter angelegt (Slug festgelegt, id 2, aus dem Seed-Bestand)
- [x] `cli/tick.php` läuft als Cron — `/api/health` meldet `checks.evaluator.ok: true` (14.08.2026 geprüft)
- [x] Master-Token vorhanden; Dev-Sub-Token für `polytrader` provisioniert (Scopes `watchdog:ping`,
`bugtracker:report`) — für den Produktivbetrieb ein eigenes Anwendungstoken je Installation
nachziehen, siehe Leitentscheidung 8
- [ ] Release-Signierschlüssel hinterlegt (`/api/updateservice/v1/pubkey` antwortet) — vor D-4
- [x] `max_activations` für `polytrader` großzügig gesetzt (Richard, vor 15.08.2026: Dev-Lizenz mit
9999 Aktivierungen) — `default_cache_ttl_hours` ungeprüft übernommen (Live-Test zeigte
plausible ~72h Offline-Kulanz), bei Bedarf im Admin nachjustieren
- [ ] Installationskonto (Rolle `installer`) angelegt — vor D-5
---
## 5. Entscheidungen (Richard, 14.08.2026)
| Frage | Entscheidung | Wirkt auf |
|---|---|---|
| Produkt-Slug | **`polytrader`**, `pt` entfällt | D-2, D-4, Serverseite |
| Zielarchitektur | **Alles auf das Deploymentcenter.** Watchdog und LicenseLabrador verschwinden, kein Parallelbetrieb | D-6 |
| Oberfläche | **Nur noch Avalonia**, WinForms wird demnächst vollständig ausgebaut | Leitentscheidung 6, D-2, D-4 |
| Master-Key | **Bei der Erstinstallation erzeugen** | D-5, D-10 |
## 6. Weiterhin offen
**Stand 17.08.2026:** Von den ursprünglich zwei DC-seitigen Wünschen ist einer erledigt
(Zugangsdaten aus argv → `DC_LICENSE_KEY`-Umgebungsvariable, `waitTimeoutSeconds` einstellbar,
`BuildInfo.targets` jetzt im Paket — alles Commit `1967b49`, siehe §0). **Offen bleibt nur D-3 der
Befunde** (unsigniertes Lizenzurteil) — vom Deploymentcenter-Team selbst bestätigt und bewusst
zurückgestellt. Trägt für den Eigenbetrieb, wird relevant, sobald Dritte die Software bekommen.
Kein Blocker für diesen Plan.
Dazu neu **D-16** (§1): das `environment`-Feld in `/api/errors/v1/report` ist ein undokumentiertes
Auth-Gate mit irreführender Fehlermeldung — für uns durch Neuprovisionierung gelöst (Token mit
`environment: "all"`), als Wunsch an DC offen (entweder die echte Ursache in der Fehlerantwort
nennen, oder die Kopplung entfernen).
### Entscheidung nötig: erstes Release veröffentlichen (D-4)
D-4 ist bis auf die eigentliche Auslieferung fertig — Update-Prüfung, Installations-Bestätigung und
der geordnete Neustart-Weg sind gebaut und laufen über denselben Pfad wie „Beenden". **Nicht
durchgeführt:** ein Paket schnüren und veröffentlichen. Drei Dinge sprechen dafür, das nicht
nebenbei zu tun, sondern mit Richard abzustimmen:
1. Ein veröffentlichtes Release ist auf einem **von mehreren Projekten geteilten** Server
persistent, nicht so folgenlos rückgängig zu machen wie ein lokaler Build.
2. Der Zugangsschutz (§5A des UpdateService-Guides) verändert `.htaccess`/`.htpasswd` in
`/releases/` — einem Verzeichnisbaum, den auch andere Produkte nutzen.
3. Der **Release-Signierschlüssel ist serverseitig noch nicht bestätigt** (offener Punkt in der
Checkliste §4). Ein erstes, unsigniertes Release wäre vermeidbar.
**Sobald Richard grünes Licht gibt:** `deploy/packager.config.json` aus der Vorlage anlegen (echtes
`updateservice:publish`-Token, FTP-Zugangsdaten), `dotnet publish` für `win-x64` und `linux-x64` in
saubere Verzeichnisse (nicht das Debug-Output), `pack-and-deploy` je Plattform aufrufen, danach
Zugangsschutz + Selbsttest prüfen (§4B der Planung).