# 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 (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
`<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 · **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 `<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

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