Files
PolyTraderSharp/docs/umsetzungsplaene/UMSETZUNGSPLAN-Deploymentcenter-Integration.md
T
RichardandClaude Opus 5 dd8da3fd3f 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 <noreply@anthropic.com>
2026-08-22 10:42:22 +02:00

55 KiB
Raw Blame History

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 (Watchdog + LicenseLabrador als Einzeldienste). Ersetzt in ANALYSE-Linux-Portierung.md den Punkt P3c und präzisiert dort §5.2.


0. Was sich gegenüber der bisherigen Planung geändert hat

Das Deploymentcenter ist nicht mehr „Lizenz + Watchdog an einem Ort", sondern deckt inzwischen vier Bereiche ab. Zwei davon standen in unserer Planung bisher gar nicht:

Bereich Zustand bei uns
Lizenz vorhanden, aber gegen LicenseLabrador gebaut (LicenseGate, Ui/LicenseDialog) — und in der Avalonia-App gar nicht verdrahtet, dort läuft derzeit alles ungeprüft
Watchdog vorhanden (WatchdogHeartbeatService), aber gegen die alte Watchdog-API gebaut — URL, Header und Payload passen nicht
Error-Reporting / Bugtracker existiert bei uns nicht. Es gibt im gesamten Repo weder AppDomain.UnhandledException noch TaskScheduler.UnobservedTaskException
UpdateService + Erstinstallation existiert bei uns nicht. Kein Paket, kein Release-Weg, keine Versionsdisziplin (<Version> steht in keiner einzigen .csproj)

Die Schätzung aus der Analyse (24 PT) bezog sich auf die ersten beiden Punkte. Mit Error-Reporting, Auslieferung und Erstinstallation liegt der Umfang bei 47 PT.


1. Befunde — Probleme, Bugs und Konzeptfehler

Sortiert nach Wirkung auf uns. Spalte „Wo" trennt, was wir lösen und was im Deploymentcenter gelöst werden müsste.

D-1 · Der Update-Weg beendet die Anwendung hart — für einen Trading-Bot unzulässig · hoch · Wo: wir + DC

UpdateClient.LaunchUpdateAgent ruft Environment.Exit(0) (client-dotnet/Deploymentcenter.Client/UpdateClient.cs:401), und exitCurrentApp: true ist die Vorgabe. Genau das haben wir bei der Lizenz schon einmal ausgeschlossen: Eine Anwendung mit offenen Positionen, laufenden CLOB-Aufrufen und einem 10-Sekunden-Bestätigungsdialog darf nicht mitten im Lauf abgeschossen werden.

Lösbar auf unserer Seite (exitCurrentApp: false und danach der bestehende geordnete Shutdown-Pfad). Nicht lösbar auf unserer Seite: Der Agent wartet über --wait-for-pid auf das Ende der Anwendung, aber nur --wait-timeout Sekunden — Vorgabe 60 (Deploymentcenter.UpdateAgent/Program.cs:70). LaunchUpdateAgent hat für diesen Wert keinen Parameter (UpdateClient.cs:330-342). Unser Herunterfahren nimmt sich allein für host.StopAsync bis zu 30 s, dazu Avalonia-Teardown und die Modul-Stopps. Reißt die Frist, tut der Agent nichts — immerhin sicher, aber ohne Rückmeldung und ohne Stellschraube.

Wunsch an DC: waitTimeoutSeconds in LaunchUpdateAgent durchreichen. Bis dahin bei uns: den Agenten selbst starten (eigener ProcessStartInfo) statt über LaunchUpdateAgent — dann setzen wir --wait-timeout selbst.

D-2 · Der Lizenzschlüssel wandert in die Kommandozeile · hoch · Wo: DC

Der Agent nimmt die Zugangsdaten der Release-Ablage ausschließlich als Argument entgegen: --license-key bzw. --download-user/--download-password (Deploymentcenter.UpdateAgent/Program.cs:70-79), und UpdateClient.LaunchUpdateAgent hängt sie ebenso an (UpdateClient.cs:366-369). Umgebungsvariablen liest der Agent an keiner Stelle.

Auf einem Mehrbenutzersystem zeigt ps (bzw. der Task-Manager) die Argumente jedes laufenden Prozesses. Das ist wörtlich dieselbe Begründung, mit der UPGRADE.md §5 den curl-Aufruf im Crontab verwirft — an dieser Stelle wird sie nicht angewandt. Für uns kommt hinzu: „keine Secrets in argv" ist bei PolyTrader ein bereits umgesetzter Sicherheitsbefund; diese Integration würde ihn zurücknehmen.

Wunsch an DC: Zugangsdaten zusätzlich über DC_LICENSE_KEY / DC_DOWNLOAD_PASSWORD oder über stdin annehmen. Sauberer Workaround auf unserer Seite gibt es nicht.

D-3 · Das Lizenzurteil ist nicht signiert — Rückschritt gegenüber LicenseLabrador · mittelhoch · Wo: DC

LicenseClient liest den Status aus reinem JSON (LicenseClient.cs:191-237). Der mitgeschickte nonce (:113) wird in der Antwort nie geprüft, eine Signatur gibt es nicht. LicenseLabrador verifizierte die Serverantwort per Ed25519 gegen einen einkompilierten Public Key und reflektierte die Nonce — deshalb konnte niemand ohne den privaten Schlüssel ein „valid" erfinden.

Jetzt trägt allein TLS. Wer DNS oder die hosts-Datei umbiegt und ein eigenes Zertifikat in den Trust Store legt — also der Besitzer der Maschine — kann sich ein {"status":"valid"} selbst ausstellen. Für unser erklärtes Bedrohungsmodell („Schutz gegen einfaches Kopieren auf ein fremdes System") reicht das; sobald Dritte die Software bekommen, ist es die schwächste Stelle der Kette.

Bemerkenswert: Für Release-Pakete führt UPDATESERVICE_INTEGRATION_GUIDE §6 eine ausführliche und richtige Begründung, warum dort asymmetrisch signiert werden muss („ein Update wird auf dem Zielsystem geprüft") — beim Lizenzurteil gilt dasselbe Argument und fehlt die Umsetzung.

Zusatz: Der Client erzwingt kein https:// (serverBaseUrl wird ungeprüft übernommen, LicenseClient.cs:118). Das prüfen wir wie beim Heartbeat selbst.

D-4 · ILicensePrompt ist eine tote Naht · mittel · Wo: DC (Doku) + unsere Analyse

LicenseClient nimmt ein ILicensePrompt entgegen und legt es in _prompt ab (LicenseClient.cs:62,69-72) — benutzt wird es nirgends, im ganzen SDK gibt es keine weitere Fundstelle. Unsere ANALYSE-Linux-Portierung.md (§5.2 und Risiko R6) baut die headless-Story darauf auf, dass diese Abstraktion den Start auf einem Server ohne Display trägt. Sie tut nichts.

Kein Beinbruch — die Ablauflogik gehört ohnehin zu uns (LicenseGate hat sie bereits) —, aber die Analyse muss korrigiert werden, und die Abstraktion bauen wir selbst.

D-5 · Reihenfolgefalle beim Zugangsschutz trifft uns in der scharfen Variante · mittel · Wo: wir

UPGRADE.md §16.1 beschreibt für bestehende Produkte „erst ausliefern, dann scharfschalten". Für neue Produkte gilt das ausdrücklich nicht: /releases/polytrader/ entsteht erst mit dem ersten Upload und wird beim nächsten cli/tick.php-Lauf geschützt. Es gibt also kein Zeitfenster, in dem sich ein noch ungeschütztes Paket ziehen ließe.

Der allererste ausgelieferte Build muss die Zugangsdaten bereits mitbringen. Veröffentlichen wir zuerst ein Release, dessen Anwendung credentials noch nicht übergibt, fällt die Tür hinter ihm zu und jede Erstinstallation bekommt 401.

Zweitens: Der Schutz beruht auf .htaccess und trägt nur unter Apache mit AllowOverride. Die Anzeige „GESCHÜTZT" beweist nichts — nur der Selbsttest tut es. Gehört in unsere Abnahme, nicht in die Zuversicht.

D-6 · Der Packager würde unsere Secrets mitveröffentlichen · hoch · Wo: wir

pack-and-deploy verpackt das Publish-Verzeichnis. Bei uns liegt dort:

Datei Inhalt Wie sie dorthin kommt
appsettings.Local.json MySQL-Verbindung mit Passwort CopyToOutputDirectory in PolyTrader.App.Avalonia.csproj
master.key AES-Master-Key aller at-rest-Secrets wird neben der Anwendung erwartet (Program.cs:309)
openrouter.key, server_settings.xml, data.db, Logs/ API-Schlüssel, Watchdog-Token, Lizenzschlüssel, Betriebsdaten Betrieb

Ohne strikte excludePatterns landet all das im Paket und damit auf dem Server. Der Zugangsschutz macht es nicht harmlos: UPDATESERVICE §5A/Grenzen sagt es selbst — dann ist es eben statt öffentlich „für jeden Lizenznehmer sichtbar". Der Packager warnt inzwischen bei zugangsdatenverdächtigen Dateien, aber die Warnung ist keine Sperre.

Gegenmaßnahme: eigenes Publish-Profil in ein sauberes Verzeichnis, explizite excludePatterns, und als Abnahmekriterium ein Blick in das fertige Archiv.

D-7 · BuildInfo.targets kommt bei unserer Auslieferungsart nicht mit · mittel · Wo: DC + wir

Deploymentcenter.Client.csproj setzt weder PackageId noch Version und packt die Deploymentcenter.BuildInfo.targets nicht ins NuGet-Paket (kein <None Pack="true" PackagePath="build/">). Der Guide empfiehlt stattdessen einen relativen <Import> ins Nachbar-Repo — genau die Kopplung, die wir bei LicenseLabrador bewusst aufgegeben haben (Begründung in lib/nuget/README.md: PolyTrader muss ohne das Schwester-Repo bauen).

Wunsch an DC: PackageId/Version setzen und die Targets als build/Deploymentcenter.Client.targets mitpacken — dann greift sie beim Paketverweis von selbst. Bis dahin bei uns: die Datei einmalig nach build/ kopieren, mit Herkunfts- und Aktualisierungsvermerk.

D-8 · Wir haben keine Versionsdisziplin · mittel · Wo: wir

In keiner .csproj steht ein <Version>, eine Directory.Build.props gibt es nicht. Damit trägt jede Assembly 1.0.0.0.

Der Packager prüft --version gegen die Hauptassembly und bricht bei Abweichung ab — das ist gut so, denn der Fehler dahinter ist übel: Wird 1.0.1 als 1.0.2 veröffentlicht, aktualisieren alle Installationen, melden weiterhin 1.0.1, halten das Release erneut für neu und aktualisieren bei jedem Start wieder. Ohne zentrale Version ist ein Release entweder unmöglich oder gefährlich. Muss vor dem ersten Paket stehen.

D-9 · setup.json kann unsere Konfiguration nicht schreiben · mittel · Wo: wir

Der Installer beherrscht die Formate json und env (SETUP_INTEGRATION_GUIDE §3/§4). Unsere Betriebseinstellungen liegen in server_settings.xml — eigenes XML-Format, das er nicht kennt.

Konsequenz: Entweder verzichtet die Erstinstallation auf jede Vorbelegung, oder der Deploymentcenter-Block (BaseUrl, Token, Kanal, Source, Lizenzschlüssel) zieht nach appsettings.json. Zweiteres ist ohnehin sauberer — die Anwendung liest bereits appsettings.Local.json über die IConfiguration, und nur so kann die Erstinstallation das Anwendungstoken per source: "provision" selbst ziehen, ohne dass jemand ein Token abtippt.

D-10 · Die Erstinstallation kann den Master-Key nicht lösen · mittel · Wo: wir

Der Installer schreibt ausdrücklich Klartext und verschlüsselt nichts (SETUP_INTEGRATION_GUIDE §3/Dateirechte). Einen POLYTRADER_MASTER_KEY kann er nicht erzeugen. Eine frisch installierte Instanz hätte also alle Secrets unverschlüsselt liegen, bis jemand von Hand nachhilft — auf einem Zielsystem im Ausland der wahrscheinlichste Ort für „später".

Optionen: (a) Master-Key als type: "secret"-Feld abfragen, (b) PolyTrader erzeugt beim ersten Start selbst einen und legt ihn mit 0600 ab, (c) über die systemd-Unit als Umgebungsvariable. Empfehlung: (b) + (c), (a) wäre eine weitere Stelle, an der jemand ein Geheimnis abtippt.

Positiv: Die im Guide geforderte Weiche „Entschlüsselung reicht Klartext unverändert durch" haben wir bereits (SecretProtection.Unprotect). Der Guide warnt zu Recht, sie nicht eines Tages als tote Altlast zu entfernen — sie gehört als aktiv genutzt dokumentiert.

D-11 · Pfad-Inkonsistenz, die erst beim Installieren auffällt · klein · Wo: wir

ServerSettings.Load("server_settings.xml") ist relativ zum Arbeitsverzeichnis, master.key wird dagegen aus AppContext.BaseDirectory gelesen (Program.cs:309). Solange man aus dem Projektordner startet, fällt das nicht auf. Nach einer Installation nach /opt/polytrader mit einer systemd-Unit, deren WorkingDirectory woanders zeigt, liest die Anwendung eine andere Datei, als der Installer geschrieben hat. Vor D-6/L2 klären.

D-12 · Der bestehende Heartbeat passt nicht — mehr als „Header und URL" · klein · Wo: wir

WatchdogHeartbeatService sendet an {base}/api/heartbeat mit X-Watchdog-Key (WatchdogHeartbeatService.cs:280,428). Das Deploymentcenter erwartet {base}/api/watchdog/v1/ping mit Authorization: Bearer (alternativ X-Agent-Token), Events gehen nach /api/watchdog/v1/event.

ANALYSE-Linux-Portierung.md §5.2 nennt das „nur Header und Ziel-URL". Tatsächlich kommen dazu: version (sonst lässt sich ein Ausfall keinem Rollout zuordnen), os, group, das ganze checks-Konzept, metrics — und status: "stopped", das die Fehlalarm-Vermeidung beim geordneten Beenden übernimmt. Unser metadata-Feld liest der Server dagegen nicht; es entfällt.

D-13 · Error-Reporting braucht einen Handler, den es bei uns nicht gibt · mittel · Wo: wir

Weder AppDomain.CurrentDomain.UnhandledException noch TaskScheduler.UnobservedTaskException kommen im Repo vor. Beim Bau lauern zwei Fallen:

  1. Rückkopplung. Der Reporter meldet einen Fehlschlag über TerminalLogger.Error — was den nächsten Report auslöst. Der Reporter darf seine eigenen Fehler niemals über den eskalierenden Pfad melden. (DC drosselt bei 60 Reports/Minute/IP; darauf verlassen wir uns nicht.)
  2. Datenabfluss. Unsere Fehlermeldungen enthalten Wallet-Adressen, Marktkennungen und Beträge, der DC-Server steht beim Hoster. Das vorhandene Sanitize() des Heartbeats muss auch auf Meldung und Stacktrace des Error-Reports laufen.

D-14 · Kein Update ohne gültige Lizenz — auch nicht das reparierende · klein, bewusst · Wo: Entscheidung

Der Zugangsschutz koppelt Updates an eine gültige Lizenz. Eine Installation mit abgelaufener Lizenz kann sich also nicht mehr selbst reparieren. Für eigene Systeme belanglos; sobald Kunden im Spiel sind, ist es eine Entscheidung, keine Nebenwirkung.

D-16 · environment im Fehler-Report ist heimlich ein Auth-Gate · kleinmittel, überraschend · Wo: DC (Doku + Fehlermeldung)

Beim Live-Test von D-3 lieferte /api/errors/v1/report mit einem funktionierenden, bugtracker:report-berechtigten Token beharrlich 401 unauthorized: "Erwartet wird ein Token mit dem Recht \"bugtracker:report\"" — obwohl der Token genau dieses Recht trug (per /api/tokens/v1/provision selbst bestätigt).

Ursache: report.php liest environment aus dem Request-Body und reicht es an ApiAuth::requireScope($db, 'bugtracker:report', $environment, false) durch; TokenManager::validateToken lehnt danach ab, wenn token['environment'] !== 'all' && token['environment'] !== $environment (TokenManager.php:317). Unser Token war bei der Provisionierung auf environment: "development" festgelegt, der Report schickte "environment": "production" — Mismatch, Ablehnung.

Zwei Dinge daran sind problematisch:

  1. Kein Dokument erwähnt diese Kopplung. bugtracker.md §3 führt environment als reine Metadatum-Angabe für /report (Bugtracker), nicht als Zugriffs-Gate. Für /api/errors/v1/report steht es nirgends. Andere Endpunkte (watchdog:ping, watchdog:read/evaluate, license:deactivate) übergeben $environment gar nicht erst an requireScope — die Kopplung gilt also inkonsistent nur für diesen einen Endpunkt.
  2. Die Fehlermeldung nennt die falsche Ursache. 401 mit "Token mit dem Recht bugtracker:report" legt einen fehlenden Scope nahe. Der tatsächliche Grund (Environment- Mismatch) taucht in der Antwort nirgends auf — von der Client-Seite aus ist das ohne Blick in den Serverquelltext nicht diagnostizierbar.

Für uns gelöst: Token mit environment: "all" neu provisioniert (dc_sub_109c1a03…), gilt für jeden gemeldeten Umgebungswert. Wunsch an DC: entweder den wahren Grund in der Fehlerantwort nennen (z.B. eigener Code environment_mismatch), oder die Kopplung ganz aus /errors/v1/report entfernen und environment wie bei den anderen Endpunkten rein deskriptiv behandeln.

D-15 · Unauthorized wird nur im statischen Zweig erkannt · klein · Wo: DC

CheckForUpdateAsync setzt result.Unauthorized nur, wenn die statische latest.json mit 401 antwortet (UpdateClient.cs:105-111). Fehlt die latest.json, fällt die Prüfung auf die offene API zurück — eine tote Lizenz sieht dann aus wie „kein Update vorhanden". Für die Fehlersuche im Feld relevant.


2. Leitentscheidungen

  1. Das SDK kommt als lokales NuGet-Paket in lib/nuget, wie schon LicenseLabrador.Client — keine Projektreferenz ins Nachbar-Repo. Begründung unverändert: PolyTrader muss ohne das Schwester-Repo bauen (Build-Server, Zielland-System).
  2. Der Heartbeat bleibt unsere eigene Implementierung. Der Service ist erprobt (Live-Reload, Entprellung, TLS-Zwang, No-Throw-Vertrag, 21 Tests); umgestellt werden Endpunkt, Auth und Payload — nicht die Mechanik.
  3. Kein Environment.Exit, nirgends. Weder bei ungültiger Lizenz (Entscheidung vom 31.07.2026) noch beim Update (D-1). Ein Update läuft über denselben geordneten Weg wie das Beenden über die Menüleiste, inklusive Bestätigungsdialog.
  4. Updates werden nie automatisch installiert. Die Anwendung prüft beim Start, meldet im Launcher und installiert erst auf ausdrückliche Bestätigung. Auch ein is_critical-Release erzwingt nichts — offene Positionen schlagen jedes Sicherheitsargument.
  5. Revidiert bei der Umsetzung (14.08.2026): Ursprünglich geplant war, dass der gesamte Deploymentcenter-Block nach appsettings.json zieht. Für Watchdog-Felder (WatchdogUrl, WatchdogToken, WatchdogSource, WatchdogInstance, WatchdogIntervalSeconds) blieb es bei server_settings.xml/ServerSettings — die bestehende UI (maskierte Eingabe, Live-Reload) deckt den Bedarf bereits ab, ein zweites Konfigurationssystem dafür wäre unnötig gewesen. Nur Werte ohne bestehende UI-Heimat (ProjectSlug, Channel, Feature-Schalter für D-3/D-4) liegen in appsettings.json/DeploymentcenterOptions; Secrets darin (Token, LicenseKey für D-2) in der gitignorierten appsettings.Local.json, zusätzlich über SecretProtection verschlüsselbar wo die bestehende Ablage (ServerSettings.LicenseKey) das schon leistet.
  6. Ausgeliefert wird ausschließlich PolyTrader.App.Avalonia, für win-x64 und linux-x64 (Entscheidung Richard, 14.08.2026: WinForms verschwindet demnächst vollständig, L5). Folge für diesen Plan: Lizenzdialog und Startgate werden nur noch in Avalonia gebaut, nicht in beiden Anwendungen. Licensing/LicenseGate.cs und Ui/LicenseDialog.cs im WinForms-Projekt werden nicht portiert, sondern ersetzt und gelöscht. Das spart eine komplette zweite Verdrahtung — und die WinForms-Fassung wäre ohnehin die einzige mit Lizenzprüfung gewesen, während die tatsächlich benutzte Anwendung ungeprüft läuft.
  7. Watchdog und LicenseLabrador werden vollständig ausgebaut (Entscheidung Richard, 14.08.2026). Kein Parallelbetrieb, keine Rückfallebene: Nach D-1/D-2 gibt es genau einen Gegenüber, das Deploymentcenter. Siehe Schnitt D-6.
  8. Ein Anwendungstoken je Installation, per source: "provision" beim Setup gezogen und auf watchdog:ping + bugtracker:report beschränkt. Ein Master-Token wird nie ausgeliefert.
  9. Produkt-Slug ist polytrader (Entscheidung Richard, 14.08.2026). Das alte pt aus dem LicenseLabrador-Admin entfällt mit diesem Umzug ersatzlos.
  10. Der Master-Key wird bei der Erstinstallation erzeugt (Entscheidung Richard, 14.08.2026), nicht abgefragt — Einzelheiten in D-5.

3. Umsetzung in Schnitten

D-0 · Fundament (ohne Serverkontakt) ERLEDIGT (14.08.2026)

Umgesetzt wie geplant: Directory.Build.props mit <Version>0.1.0</Version> für alle Projekte, Deploymentcenter.Client 2.5.0 als lokales NuGet-Paket in lib/nuget (PackageReference in PolyTrader.Core.csproj, BuildInfo.targets kommt jetzt automatisch über build/ aus dem Paket D-7 war zu diesem Zeitpunkt bereits vom Deploymentcenter-Team behoben, keine manuelle Übernahme nötig), nuget.config-Mapping um Deploymentcenter.* ergänzt, DeploymentcenterOptions + Deploymentcenter-Sektion in appsettings.json (Secrets in appsettings.Local.json, gitignored). PolyTrader.Core.BuildInfo.Version erzeugt sich korrekt bei jedem Build. LicenseLabrador-Paket bewusst noch nicht entfernt (das ist D-6, erst nach Live-Abnahme).

D-0-Rest · Zwischenverifikation der Befunde (14.08.2026)

Vor dem Start Richards Hinweis geprüft, dass die Befunde behoben sein sollten: 5 von 6 bestätigt behoben (Commit 1967b49, Deploymentcenter-Team) D-1 (Exit/--wait-timeout), D-2 (argv-Key, jetzt DC_LICENSE_KEY-Umgebungsvariable), D-4 (ILicensePrompt jetzt über EnsureLicensedAsync() genutzt), D-6 (Packager bricht bei Secrets ab statt nur zu warnen), D-7 (Paket jetzt IsPackable, Version 2.5.0). Als Bonus zusätzlich behoben: D-15 (Unauthorized jetzt auch im API-Rückfallzweig erkannt). D-3 (unsigniertes Lizenzurteil) bleibt bestätigt offen vom Deploymentcenter-Team selbst so benannt („bewusst nicht Teil dieses Commits"), deckt sich mit unserer Einordnung in §6.

D-1 · Watchdog auf die Deploymentcenter-API umstellen ERLEDIGT (14.08.2026)

Umgesetzt in WatchdogHeartbeatService.cs. 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, Licensing/LicenseCli.cs und den neuen Lizenz-Schaltflächen in SettingsWindow.axaml(.cs). Zwei Abweichungen von der ursprünglichen Planung, beide vereinfachend:

  • Kein eigenes ILicensePrompt, kein eigener modaler Lizenzdialog. Für die GUI wird die bereits vorhandene, erprobte Settings-Oberfläche wiederverwendet — dieselbe maskierte Prompt/Save-Mechanik wie beim Watchdog-Token (btnLicenseKey = „Lizenzschlüssel setzen …", speichert verschlüsselbar in server_settings.xml) plus ein neuer btnCheckLicense = „Lizenz jetzt prüfen", der den gespeicherten Schlüssel gegen den Server validiert und Status + Hardware-ID in einem Info-Dialog anzeigt. Ein zusätzliches, eigenes Dialogfenster nur für die Lizenz wäre eine zweite, redundante Eingabe-Oberfläche gewesen. Für --headless/CLI genügt die vom SDK mitgelieferte ConsoleLicensePrompt (wird hier gar nicht gebraucht, siehe nächster Punkt) — eine eigene Fassung hätte keinen Mehrwert geboten. Grund für den Verzicht auf einen echten Avalonia-Dialog vor dem Hauptfenster: Avalonias Startreihenfolge (AppBuilder.Setup() darf pro Prozess nur einmal laufen) macht einen zweiten, vorgeschalteten Dialog vor StartWithClassicDesktopLifetime unnötig fragil; ein im laufenden Fenster erreichbarer Button ist die robustere Lösung und deckt denselben Bedarf.

  • Modulentscheidung läuft still, ohne Dialog, vor dem Host-Aufbau. LicenseGate.CheckUsable validiert den in server_settings.xml gespeicherten Schlüssel synchron (wie einst die WinForms-Fassung) und entscheidet, welche Module überhaupt registriert werden nicht über EnsureLicensedAsync/ILicensePrompt des SDK (die interaktive Korrektur läuft über die Settings-UI, s.o., nicht über eine erneute Abfrage an dieser Stelle). Eine per Dialog neu eingetragene Lizenz wirkt für Module deshalb erst beim nächsten Start bewusst, konsistent mit der Entscheidung vom 31.07.2026 (keine Laufzeit-Revalidierung).

  • DeploymentcenterOptions wird jetzt vor dem Host-Aufbau eigenständig aus appsettings.json/appsettings.Local.json geladen (Program.LoadDeploymentcenterOptions), weil die Modulliste schon vor dem eigentlichen IHost feststehen muss. Dieselbe Instanz wird anschließend auch reguär in die DI registriert.

  • --smoke-ui bewusst ausgenommen: enforceLicense: !smoke — die Konstruktionsprüfung soll auf einem (nie lizenzierten) Build-Server weiterhin ALLE Modul-Fenster testen, nicht nur die Core-Shell. Verifiziert: --smoke-ui konstruiert weiterhin alle 4 Modul-Fenster plus das Einstellungsfenster mit den neuen Lizenz-Feldern (7 statt 6 Settings-Abschnitte).

  • TLS wird — anders als im SDK (Befund D-3) — im Gate selbst erzwungen, exakt wie beim Watchdog.

  • CLI-Schalter --license-status, --license-set-key <schlüssel>, --license-deactivate nach der Konvention aus LICENSE_INTEGRATION_GUIDE §4. --license-deactivate verlangt DC_SHARED_KEY aus der Umgebung (Administrationsrechner) und verweigert sich mit klarer Meldung ohne ihn — der Shared Key gehört nicht in die ausgelieferte Anwendung.

  • Keine eigenen Unit-Tests angelegt (wie schon bei der WinForms-Fassung dokumentiert begründet): PolyTrader.Tests referenziert PolyTrader.App.Avalonia nicht, der Gate ist dünner Leim über dem SDK. Abdeckung über --smoke-ui (Konstruktion) und die Live-Verifikation unten.

Live-Verifikation (15.08.2026, echter Server, Dev-Lizenz mit 9999 Aktivierungen von Richard):

> PolyTrader.App.Avalonia.exe --license-set-key LLAB2-12345-67890-ABCDE-FGHIJ
valid: License is valid
Lizenz gültig und in server_settings.xml gespeichert (Klartext).

> PolyTrader.App.Avalonia.exe --license-status
HWID v2:   2:win:a765bd47c2458fd518a7a1ad8a9d1d4b654c91085dca4667f33e7cad4427bf93 (machine-guid)
Produkt:   polytrader
Server:    https://dc.mhdf.de
Cache:     valid
Offline noch bis: 2026-08-18 07:56:54Z
Gespeicherter Schlüssel (server_settings.xml): gesetzt (Klartext)

Damit ist derselbe Codepfad geprüft, den LicenseGate.CheckUsable beim Start benutzt (LicenseClient.ValidateAsync) nicht nur ein isolierter Testaufruf. Die volle GUI/--headless wurde bewusst nicht gestartet, um keine echten Handelsverbindungen (Polymarket-WSS, Alchemy-Listener) aus dieser Sitzung heraus zu öffnen; das bleibt Richards manuelle Abnahme. max_activations war bereits großzügig gesetzt (Richards Hinweis), keine Admin-Änderung nötig.

D-3 · Fehler-Reporting ERLEDIGT (17.08.2026)

Umgesetzt in DeploymentcenterErrorReporter.cs und SecretRedactor.cs (die Secret-Muster-Prüfung aus WatchdogHeartbeatService.Sanitize wurde dorthin ausgelagert, damit Heartbeat-Meldung — kurz, einzeilig — und Stacktrace — lang, mehrzeilig — dieselbe Musterprüfung, aber unterschiedliche Kürzungsregeln bekommen, ohne Code zu verdoppeln).

  • Drei Quellen wie geplant: AppDomain.UnhandledException (level: "fatal", synchron mit 3-s-Frist gesendet, da der Prozess unmittelbar danach stirbt), TaskScheduler.UnobservedTaskException ("warning", SetObserved() verhindert einen Prozessabbruch durch die schon behandelte Ausnahme), TerminalLogger-Fehler ("error", Fire-and-forget wie beim Heartbeat).
  • No-Throw + keine Rückkopplung wie gefordert: eigene Sendefehler laufen ausschließlich über TerminalLogger.Warning (niemals .Error sonst würde der Reporter sich selbst erneut auslösen, da er genau auf .Error hört).
  • Eigene Ratenbegrenzung: 20 Meldungen/Minute (gleitendes Fenster, kein Hintergrund-Timer), deutlich unter dem Server-Limit von 60/Minute/IP Absicht ist, lokal zu drosseln, bevor eine Fehlerschleife den Server oder andere Dienste auf derselben IP überhaupt erreicht.
  • Sanitize (Nachricht, ≤200 Zeichen, einzeilig) und die neue SanitizeStackTrace (≤4000 Zeichen, mehrzeilig bleibt erhalten ein einzeiliger Stacktrace wäre zum Debuggen wertlos) laufen beide vor jedem Versand.
  • Korrektur gegenüber der Planung: kein client_ref/Idempotenz-Schlüssel implementiert anders als beim Bugtracker-/report dedupliziert /api/errors/v1/report bereits serverseitig über einen inhaltsbasierten Gruppenschlüssel (bestätigt im Live-Test unten, occurrence_count stieg ohne jedes Zutun). Ein zusätzlicher client_ref hätte hier keinen Zweck gehabt.
  • Registrierung als BackgroundService wie WatchdogHeartbeatService (rein ereignisgesteuert, ExecuteAsync wartet nur auf Abbruch) konsistent mit dem etablierten Muster, obwohl kein eigener Takt gebraucht wird.
  • Konfiguration über DeploymentcenterOptions.Token/.ErrorReportingEnabled (aus appsettings.Local.json) anders als beim Watchdog gibt es hierfür keine bestehende UI-Heimat in ServerSettings, passt also zur in D-1 begründeten Aufteilung.
  • 14 neue Tests (Payload, Auth-Header, Secrets aus Nachricht UND Stacktrace, Ratenbegrenzung, alle drei Quellen inkl. internal gemachter Handler-Methoden für direkte Testbarkeit ohne echte Prozessabstürze zu provozieren). Gesamt 468 Tests grün.

Live-Verifikation (17.08.2026, echter Server): Zwei identische Meldungen über /api/errors/v1/report mit dem exakten Payload-Format des neuen Codes → erste Meldung is_new: true, occurrence_count: 1, zweite is_new: false, occurrence_count: 2 serverseitige Deduplizierung bestätigt.

Nebenbefund beim Live-Test: D-16 (neu, siehe §1) das environment-Feld im Report-Body ist ein undokumentiertes Auth-Gate gegen die Provisionierung des Tokens; die Fehlermeldung bei einem Mismatch nennt fälschlich einen fehlenden Scope. Für uns gelöst durch Neuprovisionierung mit environment: "all" (Token in appsettings.Local.json aktualisiert, betrifft auch den Watchdog-Token-Kommentar dort).


D-0 · Fundament (ohne Serverkontakt) — ursprüngliche Planung

  • Directory.Build.props mit einer <Version> für alle Projekte (D-8).
  • Deploymentcenter.BuildInfo.targets nach build/ übernehmen und einbinden; BuildInfo.Version ersetzt das heutige Assembly.GetName().Version im Heartbeat (D-7).
  • SDK paketieren: dotnet pack Deploymentcenter.Client -c Release -o lib/nuget; nuget.config um das Quellen-Mapping Deploymentcenter.*local ergänzen; LicenseLabrador.Client samt .nupkg entfernen.
  • Neuer Konfigurationsabschnitt Deploymentcenter in appsettings.json + DeploymentcenterOptions (BaseUrl, ProjectSlug, Channel, Token, LicenseKey, Watchdog-Source/-Instance/-Intervall, Schalter für Heartbeat / Fehlermeldungen / Update-Prüfung).

Abnahme: Solution baut, Tests grün, BuildInfo.Version stimmt mit <Version> überein, lib/nuget enthält nur noch das DC-Paket.

D-1 · Watchdog auf die Deploymentcenter-API umstellen — ursprüngliche Planung

  • Endpunkte …/api/watchdog/v1/ping und …/api/watchdog/v1/event, Header Authorization: Bearer. TLS-Zwang und Loopback-Ausnahme bleiben.
  • Payload ergänzen: version (aus BuildInfo), os, group.
  • checks füllen — das ist der eigentliche Gewinn gegenüber heute: db (MySQL erreichbar), market_feed (Alter des letzten Ticks), vpn (Mullvad-Zustand), modules (laufen die aktiven Module). Ein ok-Heartbeat mit fehlschlagender Prüfung wird serverseitig auf warning gestuft.
  • metrics schlank halten (Jobs in der Warteschlange, offene Orders als Anzahl). Weiterhin keine Kontostände, Wallet-Adressen oder Trader-Namen.
  • Beim Herunterfahren status: "stopped" senden (zusätzlich zum bestehenden stopped_graceful-Event) — das ist der vom Evaluator ausgewertete Weg.
  • Konfiguration aus DeploymentcenterOptions; die alten Watchdog*-Felder aus ServerSettings entfernen (mit einmaliger Übernahme beim Start, damit niemand neu tippt).
  • WatchdogHeartbeatTests nachziehen.

Abnahme: Monitor erscheint auf dc.mhdf.de mit korrekter version; Beenden über die Menüleiste → Monitor stopped, kein Alarm; Prozess abschießen → nach 4× Intervall genau ein Down-Alarm. Voraussetzung, die vorher zu prüfen ist: cli/tick.php läuft dort tatsächlich als Cron — ohne ihn sind alle Monitorzustände wertlos (/api/healthchecks.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): 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=produpdate_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):

  • 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".
  • Beide Antworten enthalten ein gefülltes manifest_signature-Feld — signiert.
  • Zugangsschutz: nicht sofort aktivcurl -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.
  • Vollständige Selbsttest-Sequenz danach grün: ohne Zugangsdaten → 401 (beide Plattformen); mit dem Dev-Lizenzschlüssel als Basic-Auth (lic_<sha256-präfix>:<schlüssel>) → 200; .htpasswd direkt abgerufen → 403.

Reihenfolge ist hier bindend — siehe D-5.

Ursprüngliche Planung (jetzt umgesetzt, s.o.):

  1. packager.config.json mit strikten excludePatterns (appsettings.Local.json, master.key, openrouter.key, server_settings.xml, *.db, Logs/**, *.pdb) und preservePatterns (appsettings.json).
  2. Sauberes Publish-Verzeichnis je Plattform (win-x64, linux-x64), nicht das Debug-Output.
  3. In der Anwendung: Update-Prüfung beim Start mit credentials: ReleaseCredentials.FromLicenseKey(...), Anzeige im Launcher, Installation nur auf Bestätigung und über den geordneten Shutdown-Pfad (D-1).
  4. Erst danach das erste Release veröffentlichen.
  5. Zugangsschutz erzeugen und Selbsttest laufen lassen; zusätzlich von Hand: curl -I …/releases/polytrader/prod/win-x64/<version>/package.tar.gz → 401.

Abnahme: Update auf einem Testsystem behält appsettings.Local.json, server_settings.xml, master.key und data.db; eine im neuen Release entfallene DLL ist verschwunden; ein Abbruch mitten im Anwenden hinterlässt eine lauffähige Installation (Rollback); und der Blick ins Archiv:

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 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 (neu, testbar in PolyTrader.Core statt direkt in Program.cs verdrahtet) + gemeinsamer 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 (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,51 PT 14.08.2026, live verifiziert
D-2 Lizenz umstellen + Prompt-Naht (nur Avalonia) D-0 1 PT 15.08.2026, live verifiziert
D-3 Fehler-Reporting D-0 0,51 PT 17.08.2026, live verifiziert
D-4 Auslieferung D-0, D-2 (Schlüssel für den Paketabruf) 12 PT 20.08.2026, polytrader 0.1.0 live für win-x64+linux-x64, signiert, live geprüft
D-5 Erstinstallation + Master-Key-Erzeugung D-4 0,51 PT Code 17.08.2026; Release liegt jetzt vor — Live-Install fehlt nur noch das Installationskonto (Richard)
D-6 Watchdog/LicenseLabrador ausbauen Live-Abnahme von D-1 und D-2 0,5 PT offen — beide Vorbedingungen live , wartet auf Richards manuelle GUI-Abnahme

Summe 47 PT. D-1, D-2 und D-3 sind untereinander unabhängig und können in beliebiger Reihenfolge laufen. Zwei Reihenfolgen sind dagegen bindend:

  • D-2 vor D-4 — ohne Lizenzschlüssel im Update-Weg sperrt uns der Zugangsschutz mit dem ersten Release aus (Befund D-5).
  • D-6 nach der Live-Abnahme, nicht nach dem Bau.

Serverseitige Voraussetzungen (vor D-1)

  • Projekt polytrader im Deploymentcenter angelegt (Slug festgelegt, id 2, aus dem Seed-Bestand)
  • cli/tick.php läuft als Cron — /api/health meldet checks.evaluator.ok: true (14.08.2026 geprüft)
  • 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
  • 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).