# 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 (`` steht in keiner einzigen `.csproj`) | Die Schätzung aus der Analyse (2–4 PT) bezog sich auf die ersten beiden Punkte. Mit Error-Reporting, Auslieferung und Erstinstallation liegt der Umfang bei **4–7 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 · **mittel–hoch** · 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 ``). Der Guide empfiehlt stattdessen einen relativen `` 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 ``, 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 · **klein–mittel, ü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 `0.1.0` 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 `, `--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** `` 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 `` ü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_:`) → `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//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 Entscheidung Richard (14.08.2026): kein Parallelbetrieb. Läuft **nach** D-1 und D-2, damit zu keinem Zeitpunkt beides fehlt. - `LicenseLabrador.Client` aus `PolyTrader.App.csproj` und `lib/nuget` entfernen, Quellen-Mapping in `nuget.config` von `LicenseLabrador.*` auf `Deploymentcenter.*` umstellen. - `Licensing/LicenseGate.cs`, `Ui/LicenseDialog.cs` (+ `.Designer.cs`) löschen — ersetzt durch die Avalonia-Fassung aus D-2. - Alte `Watchdog*`-Felder aus `ServerSettings` und der Settings-Oberfläche entfernen (der Token-Button und die Statusanzeige wandern zum Deploymentcenter-Block). - Beide Umsetzungspläne als **abgelöst** kennzeichnen, `ANALYSE-Linux-Portierung.md` §5.2 und die Risikozeile R6 korrigieren (D-4 der Befunde: `ILicensePrompt` trägt die headless-Story nicht). - Serverseitig: `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:** 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,5–1 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,5–1 PT | ✅ 17.08.2026, live verifiziert | | D-4 | Auslieferung | D-0, D-2 (Schlüssel für den Paketabruf) | 1–2 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,5–1 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 4–7 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).