Eine Roadmap statt fuenfzehn Plandokumente; Altbestand ins Archiv

Der Status des Projekts stand verstreut in elf Umsetzungsplaenen, drei
Konzepten, der Linux-Analyse und dem Projektstand - teils widersprechend, teils
wochenlang veraltet. Ab jetzt gibt es genau eine Statusquelle.

docs/ROADMAP.md (neu):
- Alle Vorhaben in vier Stufen A bis D, plus technische Schuld und Verlauf.
  Die Stufen sind eine Reihenfolge, keine Termine: jede schafft die
  Voraussetzung fuer die naechste.
- Statuszeichen: erledigt / offen / blockiert (mit Ursache) / bewusst
  zurueckgestellt / Idee, nicht beschlossen. Damit ist das, was wir NICHT bauen
  wollen, sichtbar vorgehalten statt unauffindbar in einem Plan zu schlummern.
- Inhaltlich getragen, nicht nur verlinkt: je Vorhaben Ziel, Phasen,
  Akzeptanzkriterien, offene Entscheidungen und Leitplanken aus den Quelldokumenten.
- Sichtbar gemacht, was vorher zwischen den Dokumenten verborgen lag:
  CopyTrading Phase 1 ist der Engpass der gesamten Roadmap (MarketMaking und
  BundleArbitrage haben harte Voraussetzungen darauf), und die Sniper-Metriken
  aus Phase 3.2 sind ein Spezialfall des StrategieDrift-Fingerprints - zusammen
  bauen statt doppelt.

Archiv (docs/archiv/):
- 15 Dokumente verschoben (11 Umsetzungsplaene, 3 Konzepte, ANALYSE-Linux-Portierung).
  Sie bleiben die Bauanleitungen mit Code-Bezuegen, Risikotabellen und
  Begruendungen - eingefroren ist nur ihr Status.
- archiv/README.md ordnet jedes Dokument seinem Roadmap-Punkt zu.

Verweise nachgezogen - der eigentliche Aufwand:
- 25 Markdown-Links repariert. 15 davon verschiebungsbedingt (eine Ebene
  tiefer), der Rest war schon vorher falsch: die Ideensammlung verlinkte
  Quellcode relativ zum Repo-Wurzelverzeichnis statt zu docs/.
- 12 Dateien ausserhalb von docs/ verwiesen in Kommentaren auf die Plaene
  (csproj, props, setup.json, sechs Quelldateien) - alle auf archiv/ umgebogen.
- Verweise auf Dateien, die der Fruehjahrsputz geloescht hat (Ui/,
  Program.cs, WindowMenuBar), zu Klartext entschaerft statt tote Links zu lassen.
- Gegenprobe: 85 Links geprueft, 0 kaputt. Build gruen, 476 Tests gruen.

PROJEKTSTAND.md entdoppelt: Abschnitt "Offen" verweist jetzt auf die Roadmap.
Arbeitsteilung ist damit klar - Projektstand sagt was IST, Roadmap was KOMMT.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-23 18:56:50 +02:00
co-authored by Claude Opus 5
parent 5507db3e32
commit 6218a04fe4
33 changed files with 569 additions and 129 deletions
@@ -0,0 +1,827 @@
# 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).