From dd8da3fd3fff4f3277228148813e8226ee6fb501 Mon Sep 17 00:00:00 2001 From: Richard Date: Sat, 22 Aug 2026 10:42:22 +0200 Subject: [PATCH] Arbeitsstand gesichert: Deploymentcenter-Integration (D-0 bis D-5) und Einfenster-Shell Sicherungscommit vor dem Aufraeumen des Repos, damit nachvollziehbar bleibt, welcher Stand vor der Bereinigung galt. Build gruen, 476 Tests gruen. Zwei Straenge, die sich ueber .csproj, Program.cs und appsettings.json ueberschneiden und darum gemeinsam abgelegt werden: Deploymentcenter-Integration (P3c, Plan D-0 bis D-5 code-seitig fertig): - Deploymentcenter.Client 2.5.0 als lokales Paket, Source-Mapping erweitert - DeploymentcenterOptions, DeploymentcenterErrorReporter, LicenseGate/LicenseCli - WatchdogHeartbeatService auf die Deploymentcenter-API umgestellt (version, os, checks, metrics, status stopped) - Security: MasterKeyResolver, SecretRedactor, FilePermissions - Directory.Build.props mit zentraler Version 0.1.0 (Packager-Versionsdisziplin) - deploy/: Packager-Vorlage und systemd-Unit; echte Zugangsdaten bleiben ueber .gitignore aussen vor - setup.json fuer die Erstinstallation - UMSETZUNGSPLAN-Deploymentcenter-Integration.md; ANALYSE-Linux-Portierung.md verweist auf den neuen Plan Einfenster-Shell (UI-Redesign): - ShellWindow + ShellNavModel als Seitenleisten-Shell - WindowMenuBar und WindowMenuModel entfallen - Modul- und Kernfenster auf die Shell-Einbettung angepasst Co-Authored-By: Claude Opus 5 --- .gitignore | 3 + Directory.Build.props | 23 + NuGet.Config | 16 +- appsettings.json | 14 + deploy/packager.config.example.json | 40 + deploy/polytrader.service | 39 + docs/ANALYSE-Linux-Portierung.md | 9 + ...TZUNGSPLAN-Deploymentcenter-Integration.md | 816 ++++++++++++++++++ lib/nuget/Deploymentcenter.Client.2.5.0.nupkg | Bin 0 -> 82065 bytes src/PolyTrader.App.Avalonia/App.axaml | 422 +++++++-- src/PolyTrader.App.Avalonia/App.axaml.cs | 12 +- .../Controls/WindowMenuBar.axaml | 51 -- .../Controls/WindowMenuBar.axaml.cs | 30 - .../Licensing/LicenseCli.cs | 143 +++ .../Licensing/LicenseGate.cs | 105 +++ .../PolyTrader.App.Avalonia.csproj | 3 + src/PolyTrader.App.Avalonia/Program.cs | 112 ++- .../Shell/AvaloniaUiHost.cs | 98 ++- .../Shell/CoreViews.cs | 22 +- .../Shell/ModuleViews.cs | 74 +- .../Shell/ShellNavModel.cs | 247 ++++++ .../Shell/ViewIcons.cs | 4 +- .../Shell/WindowMenuModel.cs | 133 --- .../Views/DashboardWindow.axaml | 19 +- .../Views/DashboardWindow.axaml.cs | 11 +- .../Views/DialogWindow.axaml.cs | 42 +- .../Views/JobsWindow.axaml | 15 +- .../Views/JobsWindow.axaml.cs | 8 +- .../Views/LauncherWindow.axaml | 54 +- .../Views/LauncherWindow.axaml.cs | 185 +++- .../Views/Modules/AccountingWindow.axaml | 17 +- .../Views/Modules/AccountingWindow.axaml.cs | 11 +- .../Views/Modules/CopyTradingWindow.axaml | 18 +- .../Views/Modules/CopyTradingWindow.axaml.cs | 7 +- .../Modules/ResolutionFarmingWindow.axaml | 16 +- .../Modules/ResolutionFarmingWindow.axaml.cs | 4 +- .../Views/Modules/SupervisorWindow.axaml | 21 +- .../Views/Modules/SupervisorWindow.axaml.cs | 4 +- .../Views/SettingsWindow.axaml | 21 +- .../Views/SettingsWindow.axaml.cs | 135 ++- .../Views/ShellWindow.axaml | 194 +++++ .../Views/ShellWindow.axaml.cs | 188 ++++ .../Views/TerminalWindow.axaml | 15 +- .../Views/TerminalWindow.axaml.cs | 17 +- src/PolyTrader.App.Avalonia/appsettings.json | 14 + src/PolyTrader.App.Avalonia/setup.json | 53 ++ .../Configuration/DeploymentcenterOptions.cs | 60 ++ src/PolyTrader.Core/Models/ServerSettings.cs | 14 +- src/PolyTrader.Core/Modularity/ModuleUi.cs | 43 +- src/PolyTrader.Core/PolyTrader.Core.csproj | 7 + .../Security/FilePermissions.cs | 29 + .../Security/MasterKeyResolver.cs | 64 ++ .../Security/SecretRedactor.cs | 22 + .../Services/DeploymentcenterErrorReporter.cs | 260 ++++++ .../Services/WatchdogHeartbeatService.cs | 139 ++- .../DeploymentcenterErrorReporterTests.cs | 264 ++++++ .../MasterKeyResolverTests.cs | 115 +++ .../WatchdogHeartbeatTests.cs | 80 +- 58 files changed, 3932 insertions(+), 650 deletions(-) create mode 100644 Directory.Build.props create mode 100644 deploy/packager.config.example.json create mode 100644 deploy/polytrader.service create mode 100644 docs/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md create mode 100644 lib/nuget/Deploymentcenter.Client.2.5.0.nupkg delete mode 100644 src/PolyTrader.App.Avalonia/Controls/WindowMenuBar.axaml delete mode 100644 src/PolyTrader.App.Avalonia/Controls/WindowMenuBar.axaml.cs create mode 100644 src/PolyTrader.App.Avalonia/Licensing/LicenseCli.cs create mode 100644 src/PolyTrader.App.Avalonia/Licensing/LicenseGate.cs create mode 100644 src/PolyTrader.App.Avalonia/Shell/ShellNavModel.cs delete mode 100644 src/PolyTrader.App.Avalonia/Shell/WindowMenuModel.cs create mode 100644 src/PolyTrader.App.Avalonia/Views/ShellWindow.axaml create mode 100644 src/PolyTrader.App.Avalonia/Views/ShellWindow.axaml.cs create mode 100644 src/PolyTrader.App.Avalonia/setup.json create mode 100644 src/PolyTrader.Core/Configuration/DeploymentcenterOptions.cs create mode 100644 src/PolyTrader.Core/Security/FilePermissions.cs create mode 100644 src/PolyTrader.Core/Security/MasterKeyResolver.cs create mode 100644 src/PolyTrader.Core/Security/SecretRedactor.cs create mode 100644 src/PolyTrader.Core/Services/DeploymentcenterErrorReporter.cs create mode 100644 tests/PolyTrader.Tests/DeploymentcenterErrorReporterTests.cs create mode 100644 tests/PolyTrader.Tests/MasterKeyResolverTests.cs 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 0000000000000000000000000000000000000000..e3287e37f9df87adbdb5a12b10c7b20710a90bdd GIT binary patch literal 82065 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

`AXol_N0j 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 literal 0 HcmV?d00001 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 @@ - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +