diff --git a/.gitignore b/.gitignore
index 490b87e..dcaa3df 100644
--- a/.gitignore
+++ b/.gitignore
@@ -33,6 +33,9 @@ MongoDB/
master.key
openrouter.key
+# Packager-Zugangsdaten (FTP, updateservice:publish-Token) – Vorlage in deploy/packager.config.example.json
+deploy/packager.config.json
+
# ── Logs & temporäre Dateien ─────────────────────
*.log
*.tmp
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..dd01794
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,23 @@
+
+
+
+
+ 0.1.0
+
+
+
diff --git a/NuGet.Config b/NuGet.Config
index 6e49632..0e81abb 100644
--- a/NuGet.Config
+++ b/NuGet.Config
@@ -3,19 +3,23 @@
-
+ Neues SDK übernehmen: siehe lib/nuget/README.md.
+ LicenseLabrador.Client wird mit der Deploymentcenter-Integration abgelöst (siehe
+ docs/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md, Schnitt D-6) und
+ erst nach der Live-Abnahme von Watchdog/Lizenz entfernt. -->
-
+
+
diff --git a/appsettings.json b/appsettings.json
index 5e64f5a..20f0e5a 100644
--- a/appsettings.json
+++ b/appsettings.json
@@ -3,6 +3,20 @@
"_comment": "MySqlConnectionString liegt in der gitignorierten appsettings.Local.json.",
"MySqlConnectionString": ""
},
+ "Deploymentcenter": {
+ "_comment": "Token und LicenseKey liegen in der gitignorierten appsettings.Local.json (Secrets gehören nicht in die versionierte Datei).",
+ "BaseUrl": "https://dc.mhdf.de",
+ "ProjectSlug": "polytrader",
+ "Channel": "prod",
+ "Token": "",
+ "LicenseKey": "",
+ "WatchdogSource": "polytrader",
+ "WatchdogInstance": "prod-1",
+ "WatchdogIntervalSeconds": 60,
+ "WatchdogEnabled": false,
+ "ErrorReportingEnabled": false,
+ "UpdateCheckEnabled": false
+ },
"Logging": {
"LogLevel": {
"Default": "Information",
diff --git a/deploy/packager.config.example.json b/deploy/packager.config.example.json
new file mode 100644
index 0000000..39503d3
--- /dev/null
+++ b/deploy/packager.config.example.json
@@ -0,0 +1,40 @@
+{
+ "_comment": "Kopie als packager.config.json in diesem Ordner anlegen und ausfuellen. packager.config.json ist per .gitignore ausgeschlossen. Alternativ ueber Umgebungsvariablen: DC_FTP_HOST, DC_FTP_PORT, DC_FTP_USER, DC_FTP_PASS, DC_API_URL, DC_TOKEN. Siehe docs/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md, Schnitt D-4.",
+
+ "ftpHost": "ftp.example.com",
+ "ftpPort": 21,
+ "ftpUser": "ftp-user",
+ "ftpPass": "ftp-password",
+ "ftpRemoteBaseDir": "/public_html/releases",
+
+ "apiBaseUrl": "https://dc.mhdf.de",
+ "_apiToken_comment": "Token mit dem Recht updateservice:publish. Im Deploymentcenter-WebUI unter Token-Verwaltung erzeugen - NICHT das Dev-Sub-Token aus appsettings.Local.json wiederverwenden (das hat nur watchdog:ping/bugtracker:report).",
+ "apiToken": "",
+
+ "_excludePatterns_comment": "Kommt gar nicht ins Paket. D-6 des Umsetzungsplans: appsettings.Local.json enthaelt das MySQL-Passwort im Klartext, server_settings.xml den Watchdog-Token und den Lizenzschluessel, master.key den AES-Master-Key aller verschluesselten Secrets. Der Packager bricht seit Version 2.5.0 zusaetzlich bei erkannten Zugangsdaten ab (Datei- UND Inhaltspruefung) - diese Liste ist die erste, nicht die einzige Verteidigungslinie.",
+ "excludePatterns": [
+ "appsettings.Local.json",
+ "appsettings.*.Local.json",
+ "*.local.json",
+ "master.key",
+ "openrouter.key",
+ "server_settings.xml",
+ "*.pfx",
+ "*.key",
+ "*.pem",
+ "*.p12",
+ "*.db",
+ "*.sqlite",
+ "*.sqlite3",
+ "Logs/**",
+ "*.pdb",
+ "*.xml",
+ "*.log",
+ "scratch/**"
+ ],
+
+ "_preservePatterns_comment": "Wird ausgeliefert, ersetzt am Ziel aber niemals eine vorhandene Datei. appsettings.json enthaelt den Deploymentcenter-Konfigurationsblock (BaseUrl, ProjectSlug, ...) - eine Erstinstallation bekommt die Vorlage, ein Update laesst die vor Ort eingerichteten Werte in Ruhe.",
+ "preservePatterns": [
+ "appsettings.json"
+ ]
+}
diff --git a/deploy/polytrader.service b/deploy/polytrader.service
new file mode 100644
index 0000000..06ddcf6
--- /dev/null
+++ b/deploy/polytrader.service
@@ -0,0 +1,39 @@
+# systemd-Unit für den kopflosen Linux-Betrieb (--headless, Stufe L2 der Linux-Portierung).
+# Der Deploymentcenter-Installer legt selbst keinen Dienst an (siehe SETUP_INTEGRATION_GUIDE §7,
+# "Dienstregistrierung fehlt") - diese Datei ist unser Ersatz dafür.
+#
+# Einrichtung:
+# sudo cp deploy/polytrader.service /etc/systemd/system/polytrader.service
+# sudo systemctl daemon-reload
+# sudo systemctl enable --now polytrader
+#
+# WICHTIG (D-11 aus dem Umsetzungsplan): server_settings.xml wird relativ zum Arbeitsverzeichnis
+# geladen, master.key dagegen relativ zur ausführbaren Datei (AppContext.BaseDirectory). Beides
+# fällt nur zusammen, wenn WorkingDirectory exakt das Installationsverzeichnis ist - deshalb hier
+# ausdrücklich gesetzt statt dem systemd-Standard (Root des Dienstbenutzers) überlassen. Eine
+# abweichende WorkingDirectory liest server_settings.xml aus dem falschen Ordner, ohne dass das
+# irgendwo auffällt (ServerSettings.Load() legt bei fehlender Datei kommentarlos eine neue an).
+
+[Unit]
+Description=PolyTrader (Trading- und Analyse-Suite fuer Polymarket)
+After=network-online.target mysql.service
+Wants=network-online.target
+
+[Service]
+Type=simple
+# Anpassen: Installationsverzeichnis und Dienstkonto. Nicht als root betreiben - das
+# Installationsverzeichnis (inkl. master.key, server_settings.xml, data.db) gehört diesem Konto.
+User=polytrader
+Group=polytrader
+WorkingDirectory=/opt/polytrader
+ExecStart=/opt/polytrader/PolyTrader.App.Avalonia --headless
+Restart=on-failure
+RestartSec=10
+
+# Absturz vom sauberen Beenden unterscheiden: SIGTERM statt SIGKILL, mit Frist fuer den
+# geordneten Shutdown-Pfad (offene Positionen, Watchdog-"stopped_graceful", DB-Flush).
+KillSignal=SIGTERM
+TimeoutStopSec=45
+
+[Install]
+WantedBy=multi-user.target
diff --git a/docs/ANALYSE-Linux-Portierung.md b/docs/ANALYSE-Linux-Portierung.md
index 748e815..a6f34da 100644
--- a/docs/ANALYSE-Linux-Portierung.md
+++ b/docs/ANALYSE-Linux-Portierung.md
@@ -7,6 +7,15 @@
> **Lizenzverwaltung, Watchdog und Error-Reporting** grundlegend. Alles, was in diesem Dokument zu
> LicenseLabrador oder zum Watchdog-Heartbeat steht, ist damit Übergangszustand — dort nichts mehr
> ausbauen.
+>
+> **P3c hat seit dem 14.08.2026 einen eigenen Plan:**
+> **[UMSETZUNGSPLAN-Deploymentcenter-Integration.md](./umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md)**
+> — er ist ab jetzt maßgeblich und korrigiert zwei Aussagen dieses Dokuments:
+> §5.2 unterschätzt die Watchdog-Umstellung („nur Header und URL"; tatsächlich kommen `version`,
+> `os`, `checks`, `metrics` und `status: "stopped"` dazu), und §5.2 wie Risiko **R6** stützen die
+> headless-Lizenzprüfung auf `ILicensePrompt` aus dem SDK — diese Schnittstelle wird dort jedoch
+> **nirgends benutzt** und trägt nichts. Der Umfang liegt bei **4–7 PT** statt 2–4 PT, weil
+> Error-Reporting, Auslieferung und Erstinstallation hier noch nicht enthalten waren.
---
diff --git a/docs/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md b/docs/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md
new file mode 100644
index 0000000..7218bed
--- /dev/null
+++ b/docs/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md
@@ -0,0 +1,816 @@
+# 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).
diff --git a/lib/nuget/Deploymentcenter.Client.2.5.0.nupkg b/lib/nuget/Deploymentcenter.Client.2.5.0.nupkg
new file mode 100644
index 0000000..e3287e3
Binary files /dev/null and b/lib/nuget/Deploymentcenter.Client.2.5.0.nupkg differ
diff --git a/src/PolyTrader.App.Avalonia/App.axaml b/src/PolyTrader.App.Avalonia/App.axaml
index 86d9486..63087f7 100644
--- a/src/PolyTrader.App.Avalonia/App.axaml
+++ b/src/PolyTrader.App.Avalonia/App.axaml
@@ -3,71 +3,112 @@
x:Class="PolyTrader.App.Avalonia.App">
+
+
+ Barlow, Inter, Segoe UI, sans-serif
+ Barlow Condensed, Barlow, Inter, Segoe UI, sans-serif
+ Consolas, Menlo, DejaVu Sans Mono, monospace
+
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
+
-
-
-
+
+
+
-
-
+
+
-
-
+
+
@@ -77,62 +118,275 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PolyTrader.App.Avalonia/App.axaml.cs b/src/PolyTrader.App.Avalonia/App.axaml.cs
index 750ed81..ea5581f 100644
--- a/src/PolyTrader.App.Avalonia/App.axaml.cs
+++ b/src/PolyTrader.App.Avalonia/App.axaml.cs
@@ -42,12 +42,16 @@ namespace PolyTrader.App.Avalonia
ModuleViews.Register(uiHost, Services);
ViewIcons.AssignDefaults(uiHost);
- var launcher = new LauncherWindow(uiHost, Services);
- uiHost.SetMainWindow(launcher);
- desktop.MainWindow = launcher;
+ // Einfenster-Shell: ein Fenster, die Views hängen als Seiten darin.
+ var shell = new ShellWindow(uiHost, Services);
+ uiHost.SetShell(shell);
+ desktop.MainWindow = shell;
- // Schliessen des Launchers laeuft ueber die Sicherheitsabfrage (wie bisher das X).
+ // Schliessen der Shell laeuft ueber die Sicherheitsabfrage (wie bisher das X).
desktop.ShutdownMode = global::Avalonia.Controls.ShutdownMode.OnMainWindowClose;
+
+ // Erste Seite: die Startseite (frühere LauncherForm).
+ uiHost.ActivateMain();
}
base.OnFrameworkInitializationCompleted();
diff --git a/src/PolyTrader.App.Avalonia/Controls/WindowMenuBar.axaml b/src/PolyTrader.App.Avalonia/Controls/WindowMenuBar.axaml
deleted file mode 100644
index 65c3148..0000000
--- a/src/PolyTrader.App.Avalonia/Controls/WindowMenuBar.axaml
+++ /dev/null
@@ -1,51 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/PolyTrader.App.Avalonia/Controls/WindowMenuBar.axaml.cs b/src/PolyTrader.App.Avalonia/Controls/WindowMenuBar.axaml.cs
deleted file mode 100644
index 71c981c..0000000
--- a/src/PolyTrader.App.Avalonia/Controls/WindowMenuBar.axaml.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
-using PolyTrader.App.Avalonia.Shell;
-using PolyTrader.Core.Modularity;
-
-namespace PolyTrader.App.Avalonia.Controls
-{
- ///
- /// Code-Behind der Fenster-Menüleiste: nur Verdrahtung, kein Layout (siehe Layout-Regel).
- /// Jedes Fenster ruft auf und bekommt damit dieselbe Leiste.
- ///
- public partial class WindowMenuBar : UserControl
- {
- private WindowMenuModel? _model;
-
- public WindowMenuBar() => AvaloniaXamlLoader.Load(this);
-
- ///
- /// Verbindet die Leiste mit der Shell. ist null für den
- /// Launcher (rechte Aktion „Beenden"), sonst die View-ID (rechte Aktion „Fenster schließen").
- /// Die Event-Registrierung wird beim Entladen wieder gelöst – kein Leak.
- ///
- public void Attach(IModuleUiHost host, string? currentViewId, Window owner)
- {
- _model = new WindowMenuModel(host, currentViewId, owner.Close);
- DataContext = _model;
- owner.Closed += (_, _) => _model?.Detach();
- }
- }
-}
diff --git a/src/PolyTrader.App.Avalonia/Licensing/LicenseCli.cs b/src/PolyTrader.App.Avalonia/Licensing/LicenseCli.cs
new file mode 100644
index 0000000..8b53c5e
--- /dev/null
+++ b/src/PolyTrader.App.Avalonia/Licensing/LicenseCli.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Linq;
+using Deploymentcenter.Client;
+using PolyTrader.Core;
+using PolyTrader.Core.Security;
+using PolyTraderSharp.Models;
+
+namespace PolyTrader.App.Avalonia.Licensing
+{
+ ///
+ /// Kopflose Lizenzverwaltung von der Kommandozeile aus (D-2), nach der Konvention aus
+ /// LICENSE_INTEGRATION_GUIDE §4: --license-status, --license-set-key,
+ /// --license-deactivate. Diese Schalter baut die Anwendung selbst, das SDK bringt nur die
+ /// Bausteine mit. Läuft VOR dem Host-Aufbau und beendet den Prozess über den Rückgabewert.
+ ///
+ internal static class LicenseCli
+ {
+ /// Liefert true, wenn einer der Lizenz-Schalter erkannt und abgearbeitet wurde.
+ public static bool TryHandle(string[] args, out int exitCode)
+ {
+ exitCode = 0;
+
+ if (args.Any(a => string.Equals(a, "--license-status", StringComparison.OrdinalIgnoreCase)))
+ {
+ exitCode = Status();
+ return true;
+ }
+
+ int setIdx = Array.FindIndex(args, a => string.Equals(a, "--license-set-key", StringComparison.OrdinalIgnoreCase));
+ if (setIdx >= 0)
+ {
+ string? key = setIdx + 1 < args.Length ? args[setIdx + 1] : null;
+ exitCode = SetKey(key);
+ return true;
+ }
+
+ if (args.Any(a => string.Equals(a, "--license-deactivate", StringComparison.OrdinalIgnoreCase)))
+ {
+ exitCode = Deactivate();
+ return true;
+ }
+
+ return false;
+ }
+
+ private static int Status()
+ {
+ var options = Program.LoadDeploymentcenterOptions();
+ var hw = HardwareId.GetHardwareId(options.ProjectSlug);
+ var cached = LicenseClient.TryGetCachedState(options.ProjectSlug);
+
+ Console.WriteLine($"HWID v2: {hw.HardwareId} ({hw.HwidSource})");
+ Console.WriteLine($"Produkt: {options.ProjectSlug}");
+ Console.WriteLine($"Server: {(string.IsNullOrWhiteSpace(options.BaseUrl) ? "(keine URL konfiguriert)" : options.BaseUrl)}");
+ Console.WriteLine($"Cache: {(cached == null ? "leer" : cached.Status)}");
+
+ if (cached != null && cached.CacheExpiresAt > 0)
+ {
+ var until = DateTimeOffset.FromUnixTimeSeconds(cached.CacheExpiresAt);
+ Console.WriteLine($"Offline noch bis: {until:u}");
+ }
+
+ var settings = ServerSettings.Load("server_settings.xml");
+ Console.WriteLine($"Gespeicherter Schlüssel (server_settings.xml): {(string.IsNullOrWhiteSpace(settings.LicenseKey) ? "keiner" : settings.LicenseKeyStatus)}");
+ return 0;
+ }
+
+ private static int SetKey(string? key)
+ {
+ if (string.IsNullOrWhiteSpace(key))
+ {
+ Console.Error.WriteLine("Fehler: --license-set-key erwartet den Schlüssel als nächstes Argument.");
+ return 1;
+ }
+
+ var options = Program.LoadDeploymentcenterOptions();
+ if (string.IsNullOrWhiteSpace(options.BaseUrl))
+ {
+ Console.Error.WriteLine("Fehler: Keine Deploymentcenter-URL konfiguriert (appsettings.json: Deploymentcenter:BaseUrl).");
+ return 1;
+ }
+
+ LicenseClient.DefaultAppVersion = BuildInfo.Version;
+ var client = new LicenseClient();
+ LicenseValidationResult result = client
+ .ValidateAsync(options.ProjectSlug, key.Trim(), options.BaseUrl, BuildInfo.Version)
+ .GetAwaiter().GetResult();
+
+ Console.WriteLine($"{result.Status}: {result.Message}");
+ if (!result.IsValid)
+ {
+ return 1;
+ }
+
+ const string settingsPath = "server_settings.xml";
+ var settings = ServerSettings.Load(settingsPath);
+ settings.LicenseKey = SecretProtection.Protect(key.Trim());
+ settings.Save(settingsPath);
+ Console.WriteLine($"Lizenz gültig und in {settingsPath} gespeichert " +
+ $"({(SecretProtection.IsEncrypted(settings.LicenseKey) ? "verschlüsselt" : "Klartext")}).");
+ return 0;
+ }
+
+ private static int Deactivate()
+ {
+ var options = Program.LoadDeploymentcenterOptions();
+ var settings = ServerSettings.Load("server_settings.xml");
+
+ string key;
+ try { key = SecretProtection.Unprotect(settings.LicenseKey); }
+ catch (Exception ex)
+ {
+ Console.Error.WriteLine($"Fehler: gespeicherter Schlüssel nicht lesbar: {ex.Message}");
+ return 1;
+ }
+
+ if (string.IsNullOrWhiteSpace(key))
+ {
+ Console.Error.WriteLine("Fehler: kein Lizenzschlüssel hinterlegt.");
+ return 1;
+ }
+
+ // Die Deaktivierung gibt einen Aktivierungsplatz frei und ist deshalb geschützt - der
+ // shared_key gehört auf den Administrationsrechner, nicht in die ausgelieferte
+ // Anwendung (siehe LICENSE_INTEGRATION_GUIDE). Ohne ihn: klarer Hinweis statt Versuch.
+ string? sharedKey = Environment.GetEnvironmentVariable("DC_SHARED_KEY");
+ if (string.IsNullOrWhiteSpace(sharedKey))
+ {
+ Console.Error.WriteLine("Fehler: DC_SHARED_KEY nicht gesetzt. Die Deaktivierung braucht den " +
+ "administrativen Shared Key - alternativ im Deploymentcenter-WebUI unter " +
+ "der Hardware-Liste auf „Freigeben\" klicken.");
+ return 1;
+ }
+
+ var client = new LicenseClient();
+ bool ok = client.DeactivateAsync(options.ProjectSlug, key, options.BaseUrl, sharedKey)
+ .GetAwaiter().GetResult();
+
+ Console.WriteLine(ok ? "Aktivierung freigegeben." : "Deaktivierung fehlgeschlagen.");
+ return ok ? 0 : 1;
+ }
+ }
+}
diff --git a/src/PolyTrader.App.Avalonia/Licensing/LicenseGate.cs b/src/PolyTrader.App.Avalonia/Licensing/LicenseGate.cs
new file mode 100644
index 0000000..1ccc6d6
--- /dev/null
+++ b/src/PolyTrader.App.Avalonia/Licensing/LicenseGate.cs
@@ -0,0 +1,105 @@
+using System;
+using Deploymentcenter.Client;
+using PolyTrader.Core;
+using PolyTrader.Core.Configuration;
+using PolyTrader.Core.Security;
+using PolyTraderSharp.Models;
+using PolyTraderSharp.Services;
+
+namespace PolyTrader.App.Avalonia.Licensing
+{
+ ///
+ /// Lizenz-Torwächter für den Programmstart (D-2). Ersetzt den früheren, gegen
+ /// LicenseLabrador.Client gebauten Licensing/LicenseGate.cs im WinForms-Projekt
+ /// (siehe docs/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md) – dort läuft
+ /// ohnehin keine Lizenzprüfung, weil Avalonia die tatsächlich benutzte Anwendung ist.
+ ///
+ /// **Grundsatz unverändert:** kein bei ungültiger Lizenz. Ein
+ /// Trading-Bot mit offenen Positionen darf nicht mitten im Lauf abgeschossen werden. Bei
+ /// fehlender/ungültiger Lizenz startet die Core-Shell (Terminal, Einstellungen) ohne die
+ /// Trading-/Analyse-Module; der Schlüssel lässt sich jederzeit über den Button
+ /// „Lizenzschlüssel setzen …" im Einstellungsfenster nachtragen (wirkt ab dem nächsten Start –
+ /// eine Laufzeit-Revalidierung gibt es bewusst nicht, siehe Entscheidung vom 31.07.2026).
+ ///
+ /// **Bewusst kein eigenes :** Der interaktive Weg läuft über die
+ /// bereits vorhandene, erprobte Settings-UI (derselbe maskierte Prompt-Button wie beim
+ /// Watchdog-Token); ein zusätzlicher Dialog nur für die Lizenz wäre eine zweite, redundante
+ /// Eingabe-Oberfläche. Für den Kopflos-Betrieb (--headless, CLI-Schalter) genügt die vom
+ /// SDK mitgelieferte – eine eigene Fassung böte keinen Mehrwert.
+ ///
+ public static class LicenseGate
+ {
+ ///
+ /// Prüft still (ohne Dialog), ob die gespeicherte Lizenz aktuell nutzbar ist. Entscheidet
+ /// darüber, ob die Trading-/Analyse-Module überhaupt registriert werden – muss deshalb vor
+ /// dem Host-Aufbau laufen. Wirft nie: jeder Fehler in der Prüfung führt zum eingeschränkten
+ /// Modus, nicht zum Absturz.
+ ///
+ public static bool CheckUsable(DeploymentcenterOptions options, ServerSettings settings, TerminalLogger logger)
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(options.BaseUrl))
+ {
+ logger.Warning("⚠️ Keine Deploymentcenter-URL konfiguriert. Start im eingeschränkten Modus (nur Core-Shell).");
+ return false;
+ }
+
+ // Befund D-3 aus dem Umsetzungsplan: das SDK erzwingt selbst kein TLS. Ohne diese
+ // Prüfung ginge der Lizenzschlüssel im Klartext übers Netz, wenn die URL versehentlich
+ // auf http:// steht.
+ if (!Uri.TryCreate(options.BaseUrl, UriKind.Absolute, out var parsed) ||
+ !(parsed.Scheme == Uri.UriSchemeHttps || (parsed.Scheme == Uri.UriSchemeHttp && parsed.IsLoopback)))
+ {
+ logger.Warning($"⚠️ Deploymentcenter-URL muss HTTPS verwenden (http:// nur für localhost): '{options.BaseUrl}'. " +
+ "Start im eingeschränkten Modus.");
+ return false;
+ }
+
+ string key;
+ try
+ {
+ // Erlaubt einen mit dem Master-Key verschlüsselten Schlüssel (enc:v1:…);
+ // Klartext wird unverändert durchgereicht.
+ key = SecretProtection.Unprotect(settings.LicenseKey);
+ }
+ catch (Exception ex)
+ {
+ logger.Warning($"⚠️ Gespeicherter Lizenzschlüssel nicht lesbar ({ex.Message}) – behandle als 'keine Lizenz'.");
+ return false;
+ }
+
+ if (string.IsNullOrWhiteSpace(key))
+ {
+ logger.Warning("⚠️ Kein Lizenzschlüssel gesetzt. Start im eingeschränkten Modus (nur Core-Shell). " +
+ "Schlüssel über „Lizenzschlüssel setzen …\" im Einstellungsfenster eintragen.");
+ return false;
+ }
+
+ LicenseClient.DefaultAppVersion = BuildInfo.Version;
+ var client = new LicenseClient();
+ LicenseValidationResult result = client
+ .ValidateAsync(options.ProjectSlug, key, options.BaseUrl, BuildInfo.Version)
+ .GetAwaiter().GetResult();
+
+ if (result.IsValid)
+ {
+ logger.Info(result.IsCached
+ ? $"🔑 Lizenz im Offline-Kulanzmodus gültig ({result.Status})."
+ : "🔑 Lizenz gültig (online geprüft).");
+ return true;
+ }
+
+ logger.Warning($"⚠️ Keine gültige Lizenz ({result.Status}: {result.Message}). " +
+ "Start im eingeschränkten Modus (nur Core-Shell, keine Module). " +
+ "Schlüssel über „Lizenzschlüssel setzen …\" im Einstellungsfenster prüfen.");
+ return false;
+ }
+ catch (Exception ex)
+ {
+ logger.Error($"❌ Lizenzprüfung unerwartet fehlgeschlagen: {ex.Message}. Start im eingeschränkten Modus.");
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/PolyTrader.App.Avalonia/PolyTrader.App.Avalonia.csproj b/src/PolyTrader.App.Avalonia/PolyTrader.App.Avalonia.csproj
index 4dd3915..eac8dec 100644
--- a/src/PolyTrader.App.Avalonia/PolyTrader.App.Avalonia.csproj
+++ b/src/PolyTrader.App.Avalonia/PolyTrader.App.Avalonia.csproj
@@ -50,6 +50,9 @@
+
+
diff --git a/src/PolyTrader.App.Avalonia/Program.cs b/src/PolyTrader.App.Avalonia/Program.cs
index 1dfa84b..d55bfa0 100644
--- a/src/PolyTrader.App.Avalonia/Program.cs
+++ b/src/PolyTrader.App.Avalonia/Program.cs
@@ -41,7 +41,18 @@ namespace PolyTrader.App.Avalonia
bool headless = args.Any(a => string.Equals(a, "--headless", StringComparison.OrdinalIgnoreCase));
bool smoke = args.Any(a => string.Equals(a, "--smoke-ui", StringComparison.OrdinalIgnoreCase));
- var host = BuildHost(out var bootLog);
+ // D-2: Lizenz-CLI-Schalter (--license-status/--license-set-key/--license-deactivate)
+ // laufen VOR dem Host-Aufbau und beenden sich selbst - kein Sinn, dafür Trading-Dienste
+ // hochzufahren.
+ if (Licensing.LicenseCli.TryHandle(args, out int cliExitCode))
+ return cliExitCode;
+
+ var dcOptions = LoadDeploymentcenterOptions();
+
+ // Konstruktionsprüfung (--smoke-ui) testet Fensteraufbau, keine Lizenzlogik - dort
+ // bewusst ALLE Module registrieren, sonst würde die Prüfung auf einem Build-Server
+ // (nie lizenziert) still einen Großteil ihrer Abdeckung verlieren.
+ var host = BuildHost(dcOptions, enforceLicense: !smoke, out var bootLog);
// Konstruktionspruefung der Fenster: Host NICHT starten. Sonst laufen Trading-Engine,
// WSS-Clients und Jobs gegen die echten Boersen-Endpunkte an - fuer einen reinen
@@ -82,11 +93,35 @@ namespace PolyTrader.App.Avalonia
.WithInterFont()
.LogToTrace();
+ ///
+ /// Lädt NUR den Deploymentcenter-Konfigurationsblock, bevor der Generic Host existiert –
+ /// die Lizenzprüfung (D-2) muss VOR der Modul-Liste laufen, also bevor
+ /// seine eigene baut.
+ /// Liest dieselben Dateien wie der Host später (appsettings.json + appsettings.Local.json),
+ /// nur eigenständig und minimal. Von mitbenutzt.
+ ///
+ internal static DeploymentcenterOptions LoadDeploymentcenterOptions()
+ {
+ var config = new ConfigurationBuilder()
+ .SetBasePath(AppContext.BaseDirectory)
+ .AddJsonFile("appsettings.json", optional: true)
+ .AddJsonFile("appsettings.Local.json", optional: true)
+ .Build();
+
+ var options = new DeploymentcenterOptions();
+ config.GetSection(DeploymentcenterOptions.SectionName).Bind(options);
+ return options;
+ }
+
///
/// Baut den Anwendungs-Host: Konfiguration, Master-Key, Persistenz, Core-Dienste und die
/// aktiven Module. Kennt die Oberfläche nicht.
///
- private static IHost BuildHost(out TerminalLogger bootLog)
+ ///
+ /// D-2: Wenn false (nur --smoke-ui), werden immer alle Module registriert –
+ /// die Konstruktionsprüfung testet Fensteraufbau, keine Lizenzlogik.
+ ///
+ private static IHost BuildHost(DeploymentcenterOptions dcOptions, bool enforceLicense, out TerminalLogger bootLog)
{
var serverSettings = ServerSettings.Load(ServerSettingsPath);
var logger = new TerminalLogger();
@@ -115,6 +150,16 @@ namespace PolyTrader.App.Avalonia
new AccountingModule()
};
var disabled = new HashSet(serverSettings.DisabledModules, StringComparer.OrdinalIgnoreCase);
+
+ // D-2: ohne gültige Lizenz startet nur die Core-Shell (Terminal, Einstellungen) - alle
+ // Trading-/Analyse-Module bleiben unregistriert. Kein Environment.Exit (Entscheidung
+ // 31.07.2026): der Schlüssel lässt sich im Einstellungsfenster nachtragen.
+ bool licensed = !enforceLicense || Licensing.LicenseGate.CheckUsable(dcOptions, serverSettings, logger);
+ if (!licensed)
+ {
+ foreach (var m in allModules) disabled.Add(m.Name);
+ }
+
var modules = allModules.Where(m => !disabled.Contains(m.Name)).ToList();
return Host.CreateDefaultBuilder()
@@ -123,6 +168,7 @@ namespace PolyTrader.App.Avalonia
config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: false))
.ConfigureServices((context, services) =>
{
+ services.AddSingleton(dcOptions);
var databaseOptions = new DatabaseOptions
{
MySqlConnectionString = context.Configuration["Database:MySqlConnectionString"] ?? string.Empty
@@ -147,7 +193,22 @@ namespace PolyTrader.App.Avalonia
services.AddSingleton(
sp => new LogNotificationSink(sp.GetRequiredService()));
- services.AddSingleton(sp => new WatchdogHeartbeatService(sp.GetRequiredService()));
+ services.AddSingleton(sp =>
+ {
+ var watchdog = new WatchdogHeartbeatService(sp.GetRequiredService());
+
+ // D-1: erster "checks"-Eintrag - beweist die Mechanik mit einem real
+ // verfügbaren Signal. db/market_feed folgen, sobald die jeweiligen Dienste
+ // ein Gesundheitssignal nach außen geben (siehe
+ // docs/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md, D-1).
+ var vpn = sp.GetRequiredService();
+ var serverSettings = sp.GetRequiredService();
+ watchdog.RegisterCheck("vpn", () => serverSettings.VpnEnabled
+ ? (vpn.IsConnected, vpn.IsConnected ? null : "Mullvad VPN nicht verbunden")
+ : (true, null)); // VPN bewusst aus -> kein Fehlalarm
+
+ return watchdog;
+ });
// MUSS vor den Modulen registriert werden: hydriert die Accounts, bevor die
// Trading-Dienste anlaufen (Reihenfolge = Registrierungsreihenfolge).
@@ -159,9 +220,15 @@ namespace PolyTrader.App.Avalonia
module.RegisterServices(services, context.Configuration);
}
+ // D-3: Fehler-Reporting - rein ereignisgesteuert (AppDomain.UnhandledException,
+ // TaskScheduler.UnobservedTaskException, TerminalLogger-Fehler), kein eigener Takt.
+ services.AddSingleton(sp => new DeploymentcenterErrorReporter(
+ sp.GetRequiredService(), sp.GetRequiredService()));
+
services.AddHostedService();
services.AddHostedService(sp => sp.GetRequiredService());
services.AddHostedService(sp => sp.GetRequiredService());
+ services.AddHostedService(sp => sp.GetRequiredService());
// Shell-Zustand (Fensterverwaltung). Auch im kopflosen Betrieb harmlos.
services.AddSingleton();
@@ -189,7 +256,7 @@ namespace PolyTrader.App.Avalonia
ViewIcons.AssignDefaults(uiHost);
int failures = 0;
- Console.WriteLine("=== Smoke-UI: Fenster-Konstruktion (Avalonia) ===");
+ Console.WriteLine("=== Smoke-UI: Seiten-Konstruktion (Avalonia) ===");
foreach (var view in uiHost.Views)
{
try
@@ -204,15 +271,17 @@ namespace PolyTrader.App.Avalonia
}
}
+ // Die Startseite steckt seit der Einfenster-Shell in der Schleife oben (core.start);
+ // separat geprueft wird nur noch das Shell-Fenster selbst.
try
{
- _ = new Views.LauncherWindow(uiHost, host.Services);
- Console.WriteLine("[OK] LauncherWindow konstruiert");
+ _ = new Views.ShellWindow(uiHost, host.Services);
+ Console.WriteLine("[OK] ShellWindow konstruiert");
}
catch (Exception ex)
{
failures++;
- Console.WriteLine($"[FEHLER] LauncherWindow: {ex.GetType().Name}: {ex.Message}");
+ Console.WriteLine($"[FEHLER] ShellWindow: {ex.GetType().Name}: {ex.Message}");
}
try
@@ -255,9 +324,12 @@ namespace PolyTrader.App.Avalonia
// Schluessel faellt sonst nicht auf - ThemeManager.Brush liefert dann still Grau.
string[] themeKeys =
{
- "AppSurfaceBrush", "AppSurfaceAltBrush", "AppCardBrush", "AppBorderBrush",
- "AppMutedTextBrush", "AppCaptionTextBrush", "AppReadOnlyTextBrush",
+ "AppBgBrush", "AppSurfaceBrush", "AppSurfaceAltBrush", "AppCardBrush",
+ "AppBorderBrush", "AppDividerBrush",
+ "AppTextBrush", "AppMutedTextBrush", "AppCaptionTextBrush", "AppReadOnlyTextBrush",
+ "AppAccentBrush", "AppAccentAltBrush", "AppAccentStrongBrush", "AppAccentSoftBrush",
"AppPositiveBrush", "AppNegativeBrush", "AppWarningBrush",
+ "AppNotifyWarnBrush", "AppNotifyErrorBrush",
"AppTradeLossBrush", "AppTradeSmallWinBrush", "AppTradeBigWinBrush",
"AppToggleActiveBrush", "AppToggleSellOnlyBrush", "AppToggleInactiveBrush",
"AppChatUserBrush", "AppChatAgentBrush"
@@ -298,17 +370,23 @@ namespace PolyTrader.App.Avalonia
};
///
- /// Lädt den Master-Key (Umgebungsvariable, sonst gitignorierte master.key) und aktiviert
- /// die at-rest-Verschlüsselung. Muss VOR jeder Credential-Entschlüsselung laufen.
+ /// Lädt den Master-Key (Umgebungsvariable, sonst gitignorierte master.key, sonst wird
+ /// einer erzeugt – D-5/D-10) und aktiviert die at-rest-Verschlüsselung. Muss VOR jeder
+ /// Credential-Entschlüsselung laufen.
///
private static void ConfigureSecretProtection(TerminalLogger logger)
{
- string? masterKey = Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY");
- if (string.IsNullOrWhiteSpace(masterKey))
- {
- string keyFile = Path.Combine(AppContext.BaseDirectory, "master.key");
- if (File.Exists(keyFile)) masterKey = File.ReadAllText(keyFile).Trim();
- }
+ string keyFile = Path.Combine(AppContext.BaseDirectory, "master.key");
+ string? masterKey = MasterKeyResolver.Resolve(
+ keyFile,
+ () => Environment.GetEnvironmentVariable("POLYTRADER_MASTER_KEY"),
+ onGenerated: _ => logger.Warning(
+ "🔐 Kein Master-Key gefunden – ein neuer wurde erzeugt und in \"master.key\" " +
+ "gespeichert (App-Ordner, gitignored). WICHTIG: Diese Datei SOFORT separat sichern " +
+ "(Passwort-Manager/Offline-Backup) – ihr Verlust macht jedes ab jetzt verschlüsselte " +
+ "Secret unwiederbringlich unlesbar."),
+ onWarning: msg => logger.Warning($"⚠️ {msg}"));
+
SecretProtection.Configure(masterKey);
if (SecretProtection.IsConfigured)
diff --git a/src/PolyTrader.App.Avalonia/Shell/AvaloniaUiHost.cs b/src/PolyTrader.App.Avalonia/Shell/AvaloniaUiHost.cs
index 56dd856..238d97b 100644
--- a/src/PolyTrader.App.Avalonia/Shell/AvaloniaUiHost.cs
+++ b/src/PolyTrader.App.Avalonia/Shell/AvaloniaUiHost.cs
@@ -7,19 +7,29 @@ using PolyTrader.Core.Modularity;
namespace PolyTrader.App.Avalonia.Shell
{
///
- /// Avalonia-Umsetzung von . Verhält sich wie der frühere
- /// WinForms-ShellUiHost: je View höchstens ein Fenster, ein bereits offenes wird nach vorn
- /// geholt, alle Fenster öffnen maximiert.
+ /// Avalonia-Umsetzung von .
+ ///
+ /// Umstellung auf die Einfenster-Shell: Früher öffnete jede View ein eigenes
+ /// Fenster. Jetzt gibt es genau ein Anwendungsfenster (); eine
+ /// View ist eine -Seite, die in dessen Inhaltsbereich gehängt wird.
+ /// Eine Seite wird beim ersten Aufruf erzeugt und bleibt danach für die Sitzung bestehen –
+ /// das entspricht dem alten „Fenster bleibt offen": Timer, Log-Abos und der Bildlaufstand
+ /// überleben den Seitenwechsel.
///
/// Der Core-Contract ist toolkit-neutral ( liefert
- /// ) – hier wird auf gecastet. Ein falscher Typ ist ein
- /// Programmierfehler und soll laut scheitern, nicht still ein leeres Fenster erzeugen.
+ /// ) – hier wird auf gecastet. Ein falscher Typ ist ein
+ /// Programmierfehler und soll laut scheitern, nicht still eine leere Seite erzeugen.
///
public sealed class AvaloniaUiHost : IModuleUiHost
{
+ /// ID der Startseite (frühere LauncherForm/LauncherWindow).
+ public const string StartViewId = "core.start";
+
private readonly List _views = new();
- private readonly Dictionary _open = new();
- private Window? _mainWindow;
+ private readonly List _unavailable = new();
+ private readonly Dictionary _pages = new();
+ private Views.ShellWindow? _shell;
+ private string? _activeViewId;
private bool _shutdownDialogOpen;
/// True, sobald das Herunterfahren über die Sicherheitsabfrage bestätigt wurde.
@@ -29,20 +39,43 @@ namespace PolyTrader.App.Avalonia.Shell
public IReadOnlyList Views => _views;
+ /// ID der gerade angezeigten Seite – die Seitenleiste hebt sie hervor.
+ public string? ActiveViewId => _activeViewId;
+
public void RegisterView(ModuleView view) => _views.Add(view);
- /// Setzt das Hauptfenster (Launcher) – Ziel für .
- public void SetMainWindow(Window main) => _mainWindow = main;
+ ///
+ /// Ein bekanntes, in dieser Sitzung aber nicht geladenes Modul. Solche Module haben keine
+ /// Seite – die Seitenleiste zeigt sie trotzdem, ausgegraut und mit dem Grund als Hinweis.
+ ///
+ /// Warum das Modul fehlt (Lizenz, Abschaltung, fehlender API-Key …).
+ public sealed record UnavailableView(string Title, string? IconKey, int Order, string Reason);
- public bool IsOpen(string viewId) => _open.ContainsKey(viewId);
+ ///
+ /// Meldet ein nicht geladenes Modul an. Ohne das verschwände es spurlos aus der Oberfläche –
+ /// in der Mehrfenster-Fassung fiel das kaum auf, in einer Seitenleiste ist eine unerklärt
+ /// leere Rubrik dagegen schlicht ein Fehler.
+ ///
+ public void RegisterUnavailable(UnavailableView view) => _unavailable.Add(view);
- public void ActivateMain()
- {
- if (_mainWindow == null) return;
- if (_mainWindow.WindowState == WindowState.Minimized)
- _mainWindow.WindowState = WindowState.Maximized;
- _mainWindow.Activate();
- }
+ /// Bekannte, aber nicht geladene Module (siehe ).
+ public IReadOnlyList UnavailableViews => _unavailable;
+
+ /// Setzt das Shell-Fenster – Ziel aller Seitenwechsel und Besitzer der Dialoge.
+ public void SetShell(Views.ShellWindow shell) => _shell = shell;
+
+ /// Das Shell-Fenster, oder null vor dessen Aufbau (z.B. im Smoke-Lauf).
+ public Window? ShellWindow => _shell;
+
+ ///
+ /// Wurde die Seite in dieser Sitzung schon einmal aufgerufen? Erbt die Bedeutung des früheren
+ /// „Fenster ist offen": die Seite existiert und arbeitet weiter, auch wenn gerade eine andere
+ /// angezeigt wird.
+ ///
+ public bool IsOpen(string viewId) => _pages.ContainsKey(viewId);
+
+ /// Zurück auf die Startseite.
+ public void ActivateMain() => OpenView(StartViewId);
public void OpenView(string viewId)
{
@@ -52,35 +85,20 @@ namespace PolyTrader.App.Avalonia.Shell
public void OpenView(ModuleView view)
{
- if (_open.TryGetValue(view.Id, out var existing))
+ if (!_pages.TryGetValue(view.Id, out var page))
{
- if (existing.WindowState == WindowState.Minimized)
- existing.WindowState = WindowState.Normal;
- existing.Activate();
- return;
+ page = (Control)view.CreateView();
+ _pages[view.Id] = page;
}
- var window = (Window)view.CreateView();
- if (string.IsNullOrEmpty(window.Title)) window.Title = view.Title;
-
- // Vorgabe aus der WinForms-Oberflaeche uebernommen: alle Fenster maximiert (Full-HD-Ziel).
- window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
- window.WindowState = WindowState.Maximized;
-
- _open[view.Id] = window;
- window.Closed += (_, _) =>
- {
- _open.Remove(view.Id);
- OpenStateChanged?.Invoke();
- };
-
- window.Show();
+ _activeViewId = view.Id;
+ _shell?.ShowPage(view.Title, page);
OpenStateChanged?.Invoke();
}
///
/// Zeigt die Sicherheitsabfrage (Beenden-Schaltfläche 10 s gesperrt) und fährt bei Bestätigung
- /// herunter. Aus jedem Fenster aufrufbar – auch aus Modul-Fenstern, die die App nicht kennen.
+ /// herunter. Aus jeder Seite aufrufbar – auch aus Modul-Seiten, die die App nicht kennen.
///
public async void RequestShutdown()
{
@@ -89,8 +107,8 @@ namespace PolyTrader.App.Avalonia.Shell
try
{
var dlg = new Views.ShutdownConfirmWindow();
- bool confirmed = _mainWindow != null
- ? await dlg.ShowDialog(_mainWindow)
+ bool confirmed = _shell != null
+ ? await dlg.ShowDialog(_shell)
: await dlg.ShowDialog(new Window());
if (!confirmed) return;
diff --git a/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs b/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs
index 76d9488..69308db 100644
--- a/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs
+++ b/src/PolyTrader.App.Avalonia/Shell/CoreViews.cs
@@ -10,7 +10,7 @@ using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia.Shell
{
///
- /// Registriert die Fenster, die der Core selbst beisteuert (Dashboard, Einstellungen,
+ /// Registriert die Seiten, die der Core selbst beisteuert (Start, Dashboard, Einstellungen,
/// Terminal, Jobs). Gegenstück zur entsprechenden Registrierung im WinForms-Program.cs –
/// die View-IDs sind bewusst identisch, damit Symbole und Reihenfolge unverändert greifen.
///
@@ -21,10 +21,25 @@ namespace PolyTrader.App.Avalonia.Shell
public static void Register(IModuleUiHost host, IServiceProvider services)
{
+ // Die Startseite ist seit der Einfenster-Shell eine View wie jede andere: sie steht in
+ // der Seitenleiste, wird über OpenView aufgerufen und beim Start als erste angezeigt.
+ // Sie heißt „Dashboard", weil sie genau das ist: Kennzahlen, Konten und Live-Überblick.
+ host.RegisterView(new ModuleView
+ {
+ Id = AvaloniaUiHost.StartViewId,
+ Title = "Dashboard",
+ Group = "Core",
+ Order = 0,
+ CreateView = () => new Views.LauncherWindow(host, services)
+ });
+
host.RegisterView(new ModuleView
{
Id = "core.dashboard",
- Title = "Dashboard",
+ // Umbenannt: „Dashboard" heißt jetzt die Startseite; diese Auswertungsansicht
+ // (KPIs, Diagramme, Tradehistorie) läuft unter „Analytics". Die View-ID bleibt
+ // bewusst unverändert – daran hängen Symbole, Einstellungen und die Gruppierung.
+ Title = "Analytics",
Group = "Core",
Order = 10,
CreateView = () => new Views.DashboardWindow(
@@ -47,7 +62,8 @@ namespace PolyTrader.App.Avalonia.Shell
services.GetRequiredService(),
services.GetRequiredService(),
services.GetService(),
- services.GetService())
+ services.GetService(),
+ services.GetService())
});
host.RegisterView(new ModuleView
diff --git a/src/PolyTrader.App.Avalonia/Shell/ModuleViews.cs b/src/PolyTrader.App.Avalonia/Shell/ModuleViews.cs
index b318e76..d16fb00 100644
--- a/src/PolyTrader.App.Avalonia/Shell/ModuleViews.cs
+++ b/src/PolyTrader.App.Avalonia/Shell/ModuleViews.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.Core.Modularity;
@@ -10,15 +11,17 @@ namespace PolyTrader.App.Avalonia.Shell
/// Begründung in Views/Modules/README.md: die Modulprojekte bleiben dadurch frei von
/// Avalonia, was für den kopflosen Linux-Betrieb (--headless) den Ausschlag gibt.
///
- /// Registriert wird nur, was auch geladen ist: ist ein Modul über
- /// DisabledModules abgeschaltet, fehlen seine Dienste im Container – dann entfällt auch
- /// sein Fenster, und der Launcher zeigt es gar nicht erst an.
+ /// Eine Seite bekommt nur, was auch geladen ist: ist ein Modul über
+ /// DisabledModules abgeschaltet oder fehlt die Lizenz, fehlen seine Dienste im Container.
+ /// Ein solches Modul wird dann als gemeldet und
+ /// erscheint in der Seitenleiste ausgegraut mit dem Grund – lautlos verschwinden darf es nicht.
///
public static class ModuleViews
{
- public static void Register(IModuleUiHost host, IServiceProvider services)
+ public static void Register(AvaloniaUiHost host, IServiceProvider services)
{
- RegisterIfLoaded(services, "CopyTrading", () => host.RegisterView(new ModuleView
+ RegisterIfLoaded(host, services, "CopyTrading", "Copytrading", "copytrading", 100,
+ () => host.RegisterView(new ModuleView
{
Id = "copytrading.main",
Title = "Copytrading",
@@ -33,7 +36,8 @@ namespace PolyTrader.App.Avalonia.Shell
services.GetRequiredService())
}));
- RegisterIfLoaded(services, "ResolutionFarming", () => host.RegisterView(new ModuleView
+ RegisterIfLoaded(host, services, "ResolutionFarming", "ResolutionFarming", "resolutionfarming", 200,
+ () => host.RegisterView(new ModuleView
{
Id = "resolutionfarming.main",
Title = "ResolutionFarming",
@@ -48,7 +52,8 @@ namespace PolyTrader.App.Avalonia.Shell
services.GetRequiredService())
}));
- RegisterIfLoaded(services, "Supervisor", () => host.RegisterView(new ModuleView
+ RegisterIfLoaded(host, services, "Supervisor", "Supervisor", "supervisor", 300,
+ () => host.RegisterView(new ModuleView
{
Id = "supervisor.main",
Title = "Supervisor",
@@ -63,7 +68,8 @@ namespace PolyTrader.App.Avalonia.Shell
services.GetRequiredService())
}));
- RegisterIfLoaded(services, "Accounting", () => host.RegisterView(new ModuleView
+ RegisterIfLoaded(host, services, "Accounting", "Accounting", "accounting", 400,
+ () => host.RegisterView(new ModuleView
{
Id = "accounting.main",
Title = "Accounting",
@@ -80,12 +86,58 @@ namespace PolyTrader.App.Avalonia.Shell
}));
}
- /// Registriert die Ansicht nur, wenn das Modul in dieser Sitzung geladen ist.
- private static void RegisterIfLoaded(IServiceProvider services, string moduleName, Action register)
+ ///
+ /// Registriert die Seite, wenn das Modul in dieser Sitzung geladen ist – sonst einen
+ /// ausgegrauten Platzhalter samt Grund.
+ ///
+ private static void RegisterIfLoaded(AvaloniaUiHost host, IServiceProvider services,
+ string moduleName, string title, string iconKey, int order, Action register)
{
bool loaded = services.GetServices()
.Any(m => string.Equals(m.Name, moduleName, StringComparison.OrdinalIgnoreCase));
- if (loaded) register();
+ if (loaded)
+ {
+ register();
+ return;
+ }
+
+ host.RegisterUnavailable(new AvaloniaUiHost.UnavailableView(
+ title, iconKey, order, BlockerOf(moduleName, services)));
+ }
+
+ ///
+ /// Warum fehlt das Modul? Ein modul-eigener Grund (z.B. fehlender API-Key) ist die genaueste
+ /// Auskunft; sonst bleibt der allgemeine Fall: Lizenz oder DisabledModules.
+ ///
+ private static string BlockerOf(string moduleName, IServiceProvider services)
+ {
+ var config = services.GetService();
+ if (config != null)
+ {
+ IPolyTraderModule? known = moduleName.ToLowerInvariant() switch
+ {
+ "copytrading" => new Modules.CopyTrading.CopyTradingModule(),
+ "resolutionfarming" => new Modules.ResolutionFarming.ResolutionFarmingModule(),
+ "supervisor" => new Modules.Supervisor.SupervisorModule(),
+ "accounting" => new Modules.Accounting.AccountingModule(),
+ _ => null
+ };
+
+ try
+ {
+ string? blocker = known?.GetActivationBlocker(config);
+ if (!string.IsNullOrWhiteSpace(blocker))
+ return $"Modul nicht geladen: {blocker}";
+ }
+ catch
+ {
+ // Die Auskunft ist ein Komfort, kein Muss – ein Fehler darin darf den Start
+ // nicht aufhalten. Dann greift der allgemeine Text.
+ }
+ }
+
+ return "Modul in dieser Sitzung nicht geladen – Lizenz pruefen oder unter " +
+ "Einstellungen aktivieren (wirkt nach einem Neustart).";
}
}
}
diff --git a/src/PolyTrader.App.Avalonia/Shell/ShellNavModel.cs b/src/PolyTrader.App.Avalonia/Shell/ShellNavModel.cs
new file mode 100644
index 0000000..e87e58b
--- /dev/null
+++ b/src/PolyTrader.App.Avalonia/Shell/ShellNavModel.cs
@@ -0,0 +1,247 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Linq;
+using System.Windows.Input;
+using Avalonia.Media.Imaging;
+using Avalonia.Threading;
+using PolyTrader.Core.Modularity;
+
+namespace PolyTrader.App.Avalonia.Shell
+{
+ /// Schlanker Befehl ohne MVVM-Fremdbibliothek.
+ public sealed class RelayCommand : ICommand
+ {
+ private readonly Action _execute;
+ public RelayCommand(Action execute) => _execute = execute;
+ public bool CanExecute(object? parameter) => true;
+ public void Execute(object? parameter) => _execute();
+ public event EventHandler? CanExecuteChanged { add { } remove { } }
+ }
+
+ ///
+ /// Eine große Kachel auf der Startseite. Früher WindowMenuEntry – seit der Umstellung auf
+ /// die Einfenster-Shell öffnet sie keine Fenster mehr, sondern schaltet die Seite um.
+ ///
+ public sealed class ViewTile
+ {
+ public string Title { get; init; } = "";
+ public string? ToolTip { get; init; }
+ public Bitmap? Icon { get; init; }
+
+ /// Aktuell angezeigte Seite – wird fett dargestellt.
+ public bool IsCurrent { get; init; }
+
+ /// Seite wurde in dieser Sitzung schon aufgerufen – bekommt einen Haken.
+ public bool IsChecked { get; init; }
+
+ public ICommand Command { get; init; } = new RelayCommand(() => { });
+
+ // Von den Bindungen im XAML genutzt (kein Konverter nötig).
+ public string FontWeight => IsCurrent ? "Bold" : "Normal";
+ public string CheckMark => IsChecked ? "✓ " : "";
+ }
+
+ /// Ein Eintrag der Seitenleiste.
+ public sealed class ShellNavEntry
+ {
+ public string Title { get; init; } = "";
+ public string? ToolTip { get; init; }
+ public Bitmap? Icon { get; init; }
+
+ /// Diese Seite wird gerade angezeigt – Hinterlegung im Akzent.
+ public bool IsActive { get; init; }
+
+ ///
+ /// false für bekannte, aber nicht geladene Module: der Eintrag steht ausgegraut da
+ /// und nennt im Hinweistext den Grund, statt einfach zu fehlen.
+ ///
+ public bool IsAvailable { get; init; } = true;
+
+ ///
+ /// Beschriftung anzeigen? Steht bewusst am Eintrag und nicht nur am Modell: so kommt die
+ /// Vorlage mit einer geprüften Bindung auf ihren eigenen Datentyp aus und braucht keinen
+ /// Umweg über den DataContext des Fensters (der wäre nur ungeprüft bindbar).
+ ///
+ public bool LabelsVisible { get; init; } = true;
+
+ /// Eingeklappte Leiste: nur das Symbol, mittig gesetzt (Klassenbindung im XAML).
+ public bool IconOnly => !LabelsVisible;
+
+ public ICommand Command { get; init; } = new RelayCommand(() => { });
+ }
+
+ /// Ein Abschnitt der Seitenleiste („ÜBERSICHT", „MODULE", „SYSTEM").
+ public sealed class ShellNavGroup
+ {
+ public string Title { get; init; } = "";
+ public bool LabelsVisible { get; init; } = true;
+ public IReadOnlyList Entries { get; init; } = Array.Empty();
+ }
+
+ ///
+ /// Ein Eintrag der Benachrichtigungsleiste über dem Inhalt. Gespeist aus den Warnungen und
+ /// Fehlern des TerminalLogger – dieselbe Quelle wie das Widget „Warnungen & Fehler"
+ /// auf der Startseite, nur live und wegklickbar.
+ ///
+ public sealed class ShellNotification
+ {
+ public string Source { get; init; } = "";
+ public string Message { get; init; } = "";
+
+ /// Fehler statt Warnung – färbt die Kante rot (Klassenbindung im XAML).
+ public bool IsError { get; init; }
+
+ ///
+ /// Setzbar (nicht init), weil der Befehl den Eintrag selbst aus der Liste nimmt und
+ /// ihn dafür kennen muss – er wird direkt nach dem Erzeugen zugewiesen.
+ ///
+ public ICommand Dismiss { get; set; } = new RelayCommand(() => { });
+ }
+
+ ///
+ /// Datenmodell der Seitenleiste der Einfenster-Shell. Ersetzt das frühere
+ /// WindowMenuModel, das auf JEDEM Fenster eine Menüleiste aufgespannt hat.
+ ///
+ /// Layout-Regel: Das Modell liefert nur Daten (, Breite,
+ /// Sichtbarkeit der Beschriftungen). Wie ein Eintrag aussieht, steht deklarativ in
+ /// Views/ShellWindow.axaml – hier wird nichts zur Laufzeit zusammengebaut.
+ ///
+ public sealed class ShellNavModel : INotifyPropertyChanged
+ {
+ /// Abschnitte der Seitenleiste in Anzeigereihenfolge.
+ private const string GroupOverview = "ÜBERSICHT";
+ private const string GroupModules = "MODULE";
+ private const string GroupSystem = "SYSTEM";
+
+ private static readonly string[] GroupOrder = { GroupOverview, GroupModules, GroupSystem };
+
+ private readonly AvaloniaUiHost _host;
+ private bool _expanded = true;
+ private string _pageTitle = "";
+
+ public ObservableCollection Groups { get; } = new();
+
+ public ShellNavModel(AvaloniaUiHost host)
+ {
+ _host = host;
+ Rebuild();
+ _host.OpenStateChanged += OnOpenStateChanged;
+ }
+
+ /// Registrierung lösen, wenn die Shell geht – kein Event-Leak über die Host-Lebensdauer.
+ public void Detach() => _host.OpenStateChanged -= OnOpenStateChanged;
+
+ // ===== Seitenleiste ein-/ausklappen =====
+
+ public bool IsExpanded => _expanded;
+
+ /// Breite der Seitenleiste. Eingeklappt bleiben nur die Symbole stehen.
+ public double SidebarWidth => _expanded ? 236 : 62;
+
+ /// Beschriftungen (Marke, Abschnittstitel, Eintragstexte, Fußzeile) nur ausgeklappt.
+ public bool LabelsVisible => _expanded;
+
+ public string CollapseGlyph => _expanded ? "‹" : "›";
+ public string CollapseToolTip => _expanded ? "Seitenleiste einklappen" : "Seitenleiste ausklappen";
+
+ public void ToggleSidebar()
+ {
+ _expanded = !_expanded;
+ Raise(nameof(IsExpanded));
+ Raise(nameof(SidebarWidth));
+ Raise(nameof(LabelsVisible));
+ Raise(nameof(CollapseGlyph));
+ Raise(nameof(CollapseToolTip));
+
+ // Die Einträge tragen die Sichtbarkeit ihrer Beschriftung selbst (siehe ShellNavEntry) –
+ // deshalb muss die Liste beim Ein-/Ausklappen neu aufgebaut werden.
+ Rebuild();
+ }
+
+ // ===== Kopfzeile =====
+
+ /// Titel der angezeigten Seite – steht groß in der Kopfzeile.
+ public string PageTitle
+ {
+ get => _pageTitle;
+ set { _pageTitle = value; Raise(nameof(PageTitle)); }
+ }
+
+ // ===== Navigation =====
+
+ private void OnOpenStateChanged()
+ {
+ if (Dispatcher.UIThread.CheckAccess()) Rebuild();
+ else Dispatcher.UIThread.Post(Rebuild);
+ }
+
+ private void Rebuild()
+ {
+ // Geladene Seiten und nicht geladene Module kommen aus zwei Quellen, sollen in der
+ // Leiste aber gemeinsam nach Order stehen – deshalb erst zu einer Liste zusammenführen.
+ var entries = _host.Views
+ .Select(v => (
+ Group: GroupOf(v.Id),
+ v.Order,
+ v.Title,
+ Entry: new ShellNavEntry
+ {
+ Title = v.Title,
+ ToolTip = v.Title,
+ Icon = ViewIcons.Resolve(v.IconKey),
+ IsActive = v.Id == _host.ActiveViewId,
+ LabelsVisible = _expanded,
+ Command = new RelayCommand(() => _host.OpenView(v.Id))
+ }))
+ .Concat(_host.UnavailableViews.Select(u => (
+ Group: GroupModules,
+ u.Order,
+ u.Title,
+ Entry: new ShellNavEntry
+ {
+ Title = u.Title,
+ ToolTip = u.Reason,
+ Icon = ViewIcons.Resolve(u.IconKey),
+ IsAvailable = false,
+ LabelsVisible = _expanded
+ })))
+ .OrderBy(x => x.Order).ThenBy(x => x.Title)
+ .GroupBy(x => x.Group)
+ .ToDictionary(g => g.Key, g => g.Select(x => x.Entry).ToList());
+
+ Groups.Clear();
+ foreach (var groupTitle in GroupOrder)
+ {
+ if (!entries.TryGetValue(groupTitle, out var groupEntries)) continue;
+
+ Groups.Add(new ShellNavGroup
+ {
+ Title = groupTitle,
+ LabelsVisible = _expanded,
+ Entries = groupEntries
+ });
+ }
+
+ Raise(nameof(Groups));
+ }
+
+ ///
+ /// Ordnet eine View einem Abschnitt der Seitenleiste zu. Bewusst über die View-ID und nicht
+ /// über : dort steht der Modulname („CopyTrading"), was in der
+ /// Seitenleiste zu einem Abschnitt je Modul führen würde.
+ ///
+ private static string GroupOf(string viewId) => viewId switch
+ {
+ AvaloniaUiHost.StartViewId or "core.dashboard" => GroupOverview,
+ "core.jobs" or "core.terminal" or "core.settings" => GroupSystem,
+ _ => GroupModules
+ };
+
+ private void Raise(string name) =>
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+ }
+}
diff --git a/src/PolyTrader.App.Avalonia/Shell/ViewIcons.cs b/src/PolyTrader.App.Avalonia/Shell/ViewIcons.cs
index b5587f7..21169f8 100644
--- a/src/PolyTrader.App.Avalonia/Shell/ViewIcons.cs
+++ b/src/PolyTrader.App.Avalonia/Shell/ViewIcons.cs
@@ -17,7 +17,8 @@ namespace PolyTrader.App.Avalonia.Shell
/// Symbol-Schlüssel → Dateiname in Resources/ (als Avalonia-Asset eingebettet).
private static readonly Dictionary FileByKey = new()
{
- ["dashboard"] = "dashboard.png",
+ ["start"] = "dashboard.png",
+ ["dashboard"] = "token_quantifier.png",
["settings"] = "setting_tools.png",
["terminal"] = "error_log.png",
["jobs"] = "system_time.png",
@@ -30,6 +31,7 @@ namespace PolyTrader.App.Avalonia.Shell
/// Standard-Symbolschlüssel je View-ID – identisch zur WinForms-Shell.
private static readonly Dictionary DefaultKeyByViewId = new()
{
+ [AvaloniaUiHost.StartViewId] = "start",
["core.dashboard"] = "dashboard",
["core.settings"] = "settings",
["core.terminal"] = "terminal",
diff --git a/src/PolyTrader.App.Avalonia/Shell/WindowMenuModel.cs b/src/PolyTrader.App.Avalonia/Shell/WindowMenuModel.cs
deleted file mode 100644
index 64dea76..0000000
--- a/src/PolyTrader.App.Avalonia/Shell/WindowMenuModel.cs
+++ /dev/null
@@ -1,133 +0,0 @@
-using System;
-using System.Collections.ObjectModel;
-using System.ComponentModel;
-using System.Linq;
-using System.Runtime.CompilerServices;
-using System.Windows.Input;
-using Avalonia.Media.Imaging;
-using Avalonia.Threading;
-using PolyTrader.Core.Modularity;
-
-namespace PolyTrader.App.Avalonia.Shell
-{
- /// Schlanker Befehl ohne MVVM-Fremdbibliothek.
- public sealed class RelayCommand : ICommand
- {
- private readonly Action _execute;
- public RelayCommand(Action execute) => _execute = execute;
- public bool CanExecute(object? parameter) => true;
- public void Execute(object? parameter) => _execute();
- public event EventHandler? CanExecuteChanged { add { } remove { } }
- }
-
- /// Ein Eintrag der Fenster-Menüleiste (Launcher, eine View, oder die rechte Aktion).
- public sealed class WindowMenuEntry
- {
- public string Title { get; init; } = "";
- public string? ToolTip { get; init; }
- public Bitmap? Icon { get; init; }
-
- /// Aktuelles Fenster – wird fett dargestellt.
- public bool IsCurrent { get; init; }
-
- /// Fenster ist offen (oder das aktuelle) – bekommt einen Haken.
- public bool IsChecked { get; init; }
-
- public ICommand Command { get; init; } = new RelayCommand(() => { });
-
- // Von den Bindungen im XAML genutzt (kein Konverter nötig).
- public string FontWeight => IsCurrent ? "Bold" : "Normal";
- public string CheckMark => IsChecked ? "✓ " : "";
- }
-
- ///
- /// Datenmodell der gemeinsamen Fenster-Menüleiste, die auf JEDEM PolyTrader-Fenster erscheint.
- ///
- /// Umsetzung der neuen Layout-Regel: Die WinForms-Fassung baute das Menü zur Laufzeit
- /// auf (menu.Items.Clear() + dynamisch erzeugte ToolStripMenuItem). Hier liefert das
- /// Modell nur noch ; das Layout steht deklarativ in
- /// Controls/WindowMenuBar.axaml.
- ///
- public sealed class WindowMenuModel : INotifyPropertyChanged
- {
- private readonly IModuleUiHost _host;
- private readonly string? _currentViewId;
- private readonly Action _closeThisWindow;
-
- public ObservableCollection Entries { get; } = new();
-
- ///
- /// null für den Launcher (dort heisst die rechte Aktion „Beenden"), sonst die View-ID
- /// des Fensters (dann „Fenster schließen").
- ///
- public WindowMenuModel(IModuleUiHost host, string? currentViewId, Action closeThisWindow)
- {
- _host = host;
- _currentViewId = currentViewId;
- _closeThisWindow = closeThisWindow;
-
- Rebuild();
- _host.OpenStateChanged += OnOpenStateChanged;
- ThemeManager.ThemeChanged += OnOpenStateChanged; // Symbol des Umschalters dreht sich
- }
-
- /// Registrierung lösen, wenn das Fenster geht – kein Event-Leak über die Host-Lebensdauer.
- public void Detach()
- {
- _host.OpenStateChanged -= OnOpenStateChanged;
- ThemeManager.ThemeChanged -= OnOpenStateChanged;
- }
-
- private void OnOpenStateChanged()
- {
- if (Dispatcher.UIThread.CheckAccess()) Rebuild();
- else Dispatcher.UIThread.Post(Rebuild);
- }
-
- private void Rebuild()
- {
- Entries.Clear();
-
- Entries.Add(new WindowMenuEntry
- {
- Title = "Launcher",
- IsCurrent = _currentViewId == null,
- IsChecked = _currentViewId == null,
- Command = new RelayCommand(_host.ActivateMain)
- });
-
- foreach (var view in _host.Views.OrderBy(v => v.Order).ThenBy(v => v.Title))
- {
- string id = view.Id;
- bool isCurrent = id == _currentViewId;
- Entries.Add(new WindowMenuEntry
- {
- Title = view.Title,
- Icon = ViewIcons.Resolve(view.IconKey),
- IsCurrent = isCurrent,
- IsChecked = isCurrent || _host.IsOpen(id),
- Command = new RelayCommand(() => _host.OpenView(id))
- });
- }
-
- // Farbschema-Umschalter: sitzt auf JEDEM Fenster, damit er ueberall erreichbar ist.
- // Sonne = ins Helle wechseln, Mond = ins Dunkle.
- Entries.Add(new WindowMenuEntry
- {
- Title = ThemeManager.IsDark ? "☀" : "🌙",
- ToolTip = ThemeManager.IsDark ? "Zum hellen Farbschema wechseln" : "Zum dunklen Farbschema wechseln",
- Command = new RelayCommand(ThemeManager.Toggle)
- });
-
- // Kontextabhaengige rechte Aktion – wie in der WinForms-Fassung: nur der Launcher darf die
- // App beenden (mit Sicherheitsabfrage), jedes andere Fenster schliesst nur sich selbst.
- Entries.Add(_currentViewId == null
- ? new WindowMenuEntry { Title = "Beenden", Command = new RelayCommand(_host.RequestShutdown) }
- : new WindowMenuEntry { Title = "Fenster schließen", Command = new RelayCommand(_closeThisWindow) });
-
- PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Entries)));
- }
-
- public event PropertyChangedEventHandler? PropertyChanged;
- }
-}
diff --git a/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml b/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml
index f12c815..f1d0cd5 100644
--- a/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml
+++ b/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml
@@ -1,12 +1,9 @@
-
+
-
-
@@ -168,4 +163,4 @@
-
+
diff --git a/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml.cs b/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml.cs
index 254d41f..be77bd5 100644
--- a/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml.cs
+++ b/src/PolyTrader.App.Avalonia/Views/DashboardWindow.axaml.cs
@@ -19,7 +19,7 @@ namespace PolyTrader.App.Avalonia.Views
/// die Diagramme jetzt aus (LiveCharts2).
/// Layout vollständig in DashboardWindow.axaml.
///
- public partial class DashboardWindow : Window
+ public partial class DashboardWindow : UserControl
{
private readonly DashboardChartsModel _charts = new();
private readonly ObservableCollection _history = new();
@@ -43,8 +43,6 @@ namespace PolyTrader.App.Avalonia.Views
public DashboardWindow(IModuleUiHost host, ITradeLogRepository tradeLog, TradingState state,
IReadOnlyList? moduleInfos = null, string? settingsPath = null) : this()
{
- this.FindControl("menuBar")!.Attach(host, "core.dashboard", this);
-
_tradeLog = tradeLog;
_state = state;
_moduleInfos = moduleInfos;
@@ -75,9 +73,10 @@ namespace PolyTrader.App.Avalonia.Views
InitModulesTab();
// Die KPI-Farbe wird im Code gesetzt und wechselt daher nicht von selbst mit dem Thema.
- void OnTheme() => ApplyScope();
- Shell.ThemeManager.ThemeChanged += OnTheme;
- Closed += (_, _) => Shell.ThemeManager.ThemeChanged -= OnTheme;
+ // Kein Abmelden mehr: Seiten werden in der Einfenster-Shell einmal erzeugt und leben
+ // bis zum Programmende (siehe AvaloniaUiHost) – ein Gegenstück zu Window.Closed gibt es
+ // hier nicht, und Unloaded würde bei jedem Seitenwechsel feuern.
+ Shell.ThemeManager.ThemeChanged += ApplyScope;
}
// ===== Daten laden / Scope =====
diff --git a/src/PolyTrader.App.Avalonia/Views/DialogWindow.axaml.cs b/src/PolyTrader.App.Avalonia/Views/DialogWindow.axaml.cs
index 647bfa3..338f494 100644
--- a/src/PolyTrader.App.Avalonia/Views/DialogWindow.axaml.cs
+++ b/src/PolyTrader.App.Avalonia/Views/DialogWindow.axaml.cs
@@ -1,5 +1,6 @@
using System.Threading.Tasks;
using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace PolyTrader.App.Avalonia.Views
@@ -7,6 +8,10 @@ namespace PolyTrader.App.Avalonia.Views
///
/// Kleiner Allzweck-Dialog: Hinweis, Ja/Nein-Rückfrage und (maskierte) Eingabe.
/// Ersetzt die MessageBox-/Prompt-Aufrufe der WinForms-Oberfläche.
+ ///
+ /// Seit der Umstellung auf die Einfenster-Shell rufen die Seiten (UserControls) hier auf
+ /// und können kein mehr als Besitzer übergeben. Die Helfer nehmen deshalb
+ /// ein beliebiges und suchen sich das Fenster selbst.
///
public partial class DialogWindow : Window
{
@@ -35,31 +40,48 @@ namespace PolyTrader.App.Avalonia.Views
cancel.Click += (_, _) => Close(false);
}
- /// Reiner Hinweis mit OK.
- public static async Task Info(Window owner, string title, string message)
+ ///
+ /// Fenster zum aufrufenden Steuerelement. Fällt auf das Hauptfenster zurück, falls das
+ /// Steuerelement (noch) nicht im Baum hängt – etwa während der Konstruktionsprüfung.
+ ///
+ private static Window? OwnerOf(Control owner)
{
- var dlg = new DialogWindow();
- dlg.Setup(title, message, withInput: false, masked: false, okText: "OK", withCancel: false);
- await dlg.ShowDialog(owner);
+ if (TopLevel.GetTopLevel(owner) is Window window) return window;
+ return (global::Avalonia.Application.Current?.ApplicationLifetime
+ as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
}
- /// Ja/Nein-Rückfrage. Liefert true bei Bestätigung.
- public static async Task Confirm(Window owner, string title, string message, string okText = "Ja")
+ /// Reiner Hinweis mit OK.
+ public static async Task Info(Control owner, string title, string message)
{
+ if (OwnerOf(owner) is not { } window) return;
+ var dlg = new DialogWindow();
+ dlg.Setup(title, message, withInput: false, masked: false, okText: "OK", withCancel: false);
+ await dlg.ShowDialog(window);
+ }
+
+ ///
+ /// Ja/Nein-Rückfrage. Liefert true bei Bestätigung – und false, wenn sich gar
+ /// kein Fenster finden lässt: ohne Oberfläche gilt nichts als bestätigt.
+ ///
+ public static async Task Confirm(Control owner, string title, string message, string okText = "Ja")
+ {
+ if (OwnerOf(owner) is not { } window) return false;
var dlg = new DialogWindow();
dlg.Setup(title, message, withInput: false, masked: false, okText: okText, withCancel: true);
- return await dlg.ShowDialog(owner);
+ return await dlg.ShowDialog(window);
}
///
/// Eingabe. Liefert null bei Abbruch – leerer String bedeutet bewusst „Wert entfernen"
/// (dieselbe Unterscheidung wie in der bisherigen Oberfläche).
///
- public static async Task Prompt(Window owner, string title, string message, bool masked = true)
+ public static async Task Prompt(Control owner, string title, string message, bool masked = true)
{
+ if (OwnerOf(owner) is not { } window) return null;
var dlg = new DialogWindow();
dlg.Setup(title, message, withInput: true, masked: masked, okText: "OK", withCancel: true);
- bool ok = await dlg.ShowDialog(owner);
+ bool ok = await dlg.ShowDialog(window);
return ok ? dlg.FindControl("txtInput")!.Text ?? string.Empty : null;
}
}
diff --git a/src/PolyTrader.App.Avalonia/Views/JobsWindow.axaml b/src/PolyTrader.App.Avalonia/Views/JobsWindow.axaml
index 45c4550..f3ae230 100644
--- a/src/PolyTrader.App.Avalonia/Views/JobsWindow.axaml
+++ b/src/PolyTrader.App.Avalonia/Views/JobsWindow.axaml
@@ -1,10 +1,7 @@
-
+
-
-
@@ -51,4 +46,4 @@
-
+
diff --git a/src/PolyTrader.App.Avalonia/Views/JobsWindow.axaml.cs b/src/PolyTrader.App.Avalonia/Views/JobsWindow.axaml.cs
index cb8254b..e509097 100644
--- a/src/PolyTrader.App.Avalonia/Views/JobsWindow.axaml.cs
+++ b/src/PolyTrader.App.Avalonia/Views/JobsWindow.axaml.cs
@@ -12,14 +12,16 @@ namespace PolyTrader.App.Avalonia.Views
/// Hintergrund-Jobs (JobManager). Verhalten wie bisher: „Run Now" löst den ManualTrigger aus,
/// „Aktiv" schaltet den Job über die Bindung. Layout vollständig in JobsWindow.axaml.
///
- public partial class JobsWindow : Window
+ public partial class JobsWindow : UserControl
{
public JobsWindow() => AvaloniaXamlLoader.Load(this);
+ ///
+ /// Wird für diese Seite nicht mehr gebraucht (die Navigation sitzt in der Shell), bleibt
+ /// aber im Vertrag, weil ModuleView.CreateView alle Seiten gleich erzeugt.
+ ///
public JobsWindow(IModuleUiHost host, JobManager jobManager) : this()
{
- this.FindControl("menuBar")!.Attach(host, "core.jobs", this);
-
var grid = this.FindControl("gridJobs")!;
grid.ItemsSource = jobManager.Jobs;
diff --git a/src/PolyTrader.App.Avalonia/Views/LauncherWindow.axaml b/src/PolyTrader.App.Avalonia/Views/LauncherWindow.axaml
index d9e66d2..4051fd2 100644
--- a/src/PolyTrader.App.Avalonia/Views/LauncherWindow.axaml
+++ b/src/PolyTrader.App.Avalonia/Views/LauncherWindow.axaml
@@ -1,21 +1,19 @@
-
+
-
-
@@ -23,17 +21,25 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
@@ -42,7 +48,7 @@
-
+
-
+
diff --git a/src/PolyTrader.App.Avalonia/Views/LauncherWindow.axaml.cs b/src/PolyTrader.App.Avalonia/Views/LauncherWindow.axaml.cs
index 0405119..78c5f83 100644
--- a/src/PolyTrader.App.Avalonia/Views/LauncherWindow.axaml.cs
+++ b/src/PolyTrader.App.Avalonia/Views/LauncherWindow.axaml.cs
@@ -3,17 +3,22 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
+using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
+using Deploymentcenter.Client;
using Microsoft.Extensions.DependencyInjection;
using PolyTrader.App.Avalonia.Shell;
using PolyTrader.App.Avalonia.ViewModels;
+using PolyTrader.Core;
using PolyTrader.Core.Analytics;
+using PolyTrader.Core.Configuration;
using PolyTrader.Core.Modularity;
using PolyTrader.Core.Persistence;
+using PolyTrader.Core.Security;
using PolyTrader.Modules.Supervisor.Persistence;
using PolyTraderSharp;
using PolyTraderSharp.Models;
@@ -22,17 +27,23 @@ using PolyTraderSharp.Services;
namespace PolyTrader.App.Avalonia.Views
{
///
- /// „Startleiste" der Anwendung – Gegenstück zur bisherigen LauncherForm. Öffnet die Fenster,
- /// spiegelt deren Offen-Status, zeigt Kernkennzahlen und beinhaltet die Live-Überblick-Widgets.
+ /// Startseite der Anwendung – Gegenstück zur bisherigen LauncherForm. Führt zu den übrigen
+ /// Ansichten, zeigt Kernkennzahlen und beinhaltet die Live-Überblick-Widgets.
/// Layout vollständig in LauncherWindow.axaml.
+ ///
+ /// Seit der Umstellung auf die Einfenster-Shell ist dies ein
+ /// und kein Fenster mehr; der Klassenname bleibt vorerst, um die parallele
+ /// Deploymentcenter-Arbeit an dieser Datei nicht mit einer Umbenennung zu kollidieren.
+ /// Die Statusleiste (Trading/Module/API) und das Beenden liegen jetzt in
+ /// .
///
- public partial class LauncherWindow : Window
+ public partial class LauncherWindow : UserControl
{
- private readonly AvaloniaUiHost _uiHost;
+ private readonly IModuleUiHost _uiHost;
private readonly IServiceProvider _services;
private readonly TradingState _state;
private readonly DispatcherTimer _statusTimer = new() { Interval = TimeSpan.FromSeconds(1) };
- private readonly ObservableCollection _windowButtons = new();
+ private readonly ObservableCollection _windowButtons = new();
private readonly ObservableCollection _accountRows = new();
private readonly ObservableCollection _kpiTiles = new();
private readonly ObservableCollection _notableRows = new();
@@ -40,17 +51,17 @@ namespace PolyTrader.App.Avalonia.Views
private int _statusTicks;
+ /// Ergebnis der letzten Update-Prüfung – für den Klick auf „Jetzt installieren …".
+ private UpdateCheckResult? _updateCheck;
+
public LauncherWindow() => AvaloniaXamlLoader.Load(this);
- public LauncherWindow(AvaloniaUiHost uiHost, IServiceProvider services) : this()
+ public LauncherWindow(IModuleUiHost uiHost, IServiceProvider services) : this()
{
_uiHost = uiHost;
_services = services;
_state = services.GetRequiredService();
- // currentViewId == null => dies ist der Launcher (rechte Menueaktion "Beenden").
- this.FindControl("menuBar")!.Attach(uiHost, null, this);
-
this.FindControl("windowButtons")!.ItemsSource = _windowButtons;
this.FindControl("gridAccounts")!.ItemsSource = _accountRows;
this.FindControl("kpiTiles")!.ItemsSource = _kpiTiles;
@@ -71,27 +82,24 @@ namespace PolyTrader.App.Avalonia.Views
UpdateTradingToggles();
};
+ this.FindControl