# KONZEPT: Deploymentcenter-Integration > Stand: 2026-08-14 · Deploymentcenter-Version **2.5.1** · Quelle: `J:\Softwareprojekte\Deploymentcenter\docs`, > `GET /api/updateservice/v1/changelog?since=2.4` > Zielprojekt: IBKRTrader (App + Daemon), Projekt-Slug **`ibkrtrader`** > SDK-Zielversion: `Deploymentcenter.Client` **2.5.1** (`DeploymentcenterSdk.Version` mitführen, damit > künftige Anbindungen über `?since=2.5.1` direkt sehen, was sich seither geändert hat) > **Nachtrag 2026-08-14:** Die ursprüngliche Fassung dieses Dokuments (Stand DC 2.4) enthielt sieben > Befunde/Bedenken in §8, die dem Deploymentcenter-Entwickler mitgeteilt wurden. Alle sieben sind mit > 2.5.0/2.5.1 behoben — Einzelheiten weiterhin in §8, jetzt mit Fix-Vermerk. Zwei Neuerungen aus > derselben Runde gehen über die ursprünglichen Befunde hinaus und sind in §5.1, §7.2 und §2.2 > eingearbeitet: `exitCurrentApp` ist jetzt als harter Kill dokumentiert, und `BuildInfo.targets` liegt > im NuGet-Paket selbst. Das Deploymentcenter (`https://dc.mhdf.de`) bündelt fünf Dienste, die IBKRTrader bisher alle nicht hat: **Lizenz**, **Watchdog**, **Error-Reporting**, **Bugtracker** und **UpdateService inkl. Erstinstallation**. Dieses Dokument beschreibt, in welcher Reihenfolge sie angebunden werden, was dabei am Projekt vorher aufgeräumt werden muss und wo die vom Deploymentcenter vorgeschlagenen Muster für ein *handelndes* System nicht ohne Weiteres passen. --- ## 0. Ausgangslage | | Zustand heute | |---|---| | DC-Anbindung | **keine** — kein Treffer für `Deploymentcenter`, `dc.mhdf.de` oder `DC_TOKEN` im Quelltext | | Version | Zeichenkette `"1.0.0"` in `App/Program.cs:34` und `Daemon/Program.cs:43`, keine ``, keine `Directory.Build.props` | | Globale Fehlerbehandlung | **keine** — weder `AppDomain.UnhandledException` noch `TaskScheduler.UnobservedTaskException` | | Auslieferung | `dotnet publish` von Hand, `deploy/README.md`, systemd-Unit | | CI | Gitea Actions, Matrix `ubuntu-latest` / `windows-latest`, Checkout **nur** von IBKRTrader | Referenzintegration ist **Predictalytics** (`src/Predictalytics.Hosting/Dc*.cs`, `docs/BETRIEB-Deploymentcenter.md`). Sie liegt auf SDK-Stand 2.2 und ist damit unvollständig — siehe [§8](#8-befunde-und-offene-punkte), Punkt D2. Übernommen wird das Muster, nicht der Code eins zu eins. Der Einbauort ist **`IBKRTrader.Hosting`**: den Host teilen sich Shell und Daemon (`AppHostBuilder`), damit gilt jede Anbindung automatisch für beide Betriebsformen. `IBKRTrader.Core` bleibt frei davon — der Core soll keine Netzabhängigkeit zu einem Verwaltungsserver bekommen. --- ## 1. Voraussetzungen im Deploymentcenter (vor jedem Code) Ohne diese Schritte läuft nichts von dem, was danach kommt: 1. **Projekt `ibkrtrader` anlegen** (WebUI → Projekte). Slug wird von Lizenz, UpdateService, Fehler-Stream und Bugtracker gemeinsam benutzt. 2. **Offline-Gnadenfrist festlegen** (`default_cache_ttl_hours`). Vorgabe 168 h. Für einen Handelsdienst, der auch bei gestörter Internetverbindung weiterlaufen soll, ist das die entscheidende Stellschraube — siehe [§5](#5-lizenz). 3. **Lizenz für `ibkrtrader` ausstellen.** Ohne sie kommt die Anwendung nach dem ersten Release nicht mehr an ihre Updates (Zugangsschutz seit 2.4). 4. **Master-Token** mit `watchdog:ping`, `bugtracker:report` — für die Anwendung. Zweites Token mit `updateservice:publish` — für die Release-Pipeline, gehört **nicht** in die Anwendung. 5. **Installationskonto** (Rolle `installer`) für die Erstinstallation. 6. **Evaluator-Cron prüfen.** `* * * * * php cli/tick.php --quiet`. Ohne ihn bleibt ein abgestürzter Trading-Daemon im Dashboard dauerhaft grün — der Watchdog wäre dekorativ. Derselbe Job erzeugt auch den Release-Zugangsschutz. --- ## 2. Build-Grundlage ### 2.1 `Directory.Build.props` mit `` ```xml 1.0.0 ``` `pack-and-deploy` liest die Version aus der Hauptassembly und **bricht bei einer Abweichung zu `--version` ab**. Der Grund ist unangenehm genug dafür: Wird `1.0.1` als `1.0.2` veröffentlicht, aktualisiert die gesamte Installationsbasis, meldet danach weiterhin `1.0.1`, hält das Release erneut für neu — und aktualisiert bei jedem Start wieder. Gleichzeitig entfallen die beiden hartkodierten `"1.0.0"` in den `Program.cs`; sie werden durch `BuildInfo.Version` ersetzt. ### 2.2 SDK-Anbindung — entschieden, in zwei Stufen `Deploymentcenter.Client` (`netstandard2.0;net8.0`) wird von `net10.0` problemlos konsumiert. Seit SDK 2.5.0 liegt `BuildInfo.targets` im Paket selbst (`build/`-Ordner, von NuGet automatisch importiert) — der frühere `` per Cross-Repo-Pfad ist damit **nicht mehr nötig**. Das entscheidet die Frage grundsätzlich zugunsten eines echten NuGet-Bezugs. Geprüft (2026-08-14): Weder `https://dc.mhdf.de/nuget/` noch die Gitea-Package-Registry unter `http://192.168.178.10:8418/api/packages/Richard/nuget/` führen aktuell ein Paket `Deploymentcenter.Client` — es gibt also noch keine Quelle, gegen die eine `PackageReference` auflösen könnte. **Deshalb zweistufig:** | Stufe | Weg | Zustand | |---|---|---| | **Jetzt (umgesetzt)** | Cross-Repo-`ProjectReference` (Muster Predictalytics), `Deploymentcenter.BuildInfo.targets` weiterhin per `` eingebunden — der pfadbasierte Import funktioniert laut Leitfaden unverändert weiter, auch wenn er nicht mehr nötig wäre | funktioniert sofort, keine neue Infrastruktur, keine Push-Entscheidung nötig | | **Folgeschritt (offen)** | `dotnet pack` im Deploymentcenter-Repo, Push nach `.../api/packages/Richard/nuget/`, `NuGet.config` um Quelle `gitea` mit `packageSourceMapping` auf `Deploymentcenter.*` ergänzen, `` durch `` ersetzen | braucht einen Push in ein fremdes Repo (Deploymentcenter) und Package-Registry-Zugangsdaten — bewusst nicht Teil dieses Durchlaufs | Der Cross-Repo-Bezug bricht die Gitea-CI von IBKRTrader (der Runner checkt nur dieses eine Repo aus) — siehe **P5**. Das ist mit der Wahl von Stufe 1 bewusst in Kauf genommen, weil eine funktionierende lokale Integration wichtiger ist als eine grüne CI, die eine noch nicht existierende Paketquelle voraussetzt. ### 2.3 `BuildInfo` `Deploymentcenter.BuildInfo.targets` in die csproj von `IBKRTrader.Hosting` importieren. Die Klasse entsteht in `$(RootNamespace)`, also `IBKRTrader.Hosting.BuildInfo` — erreichbar ohne `using`. Liefert `Version`, `GitCommit`, `GitCommitShort`, `BuildDateUtc`, `Channel`, `Summary`. > **Erledigt mit 2.5.1** (vormals §8 D4): `BuildDateUtc` trug die volle Uhrzeit, > `WriteOnlyWhenDifferent` griff dadurch nie, und jedes importierende Projekt wurde > bei jedem Build neu übersetzt. Seit 2.5.1 ist das Feld tagesgenau — höchstens ein > Neubau pro Tag. Vorbedingung: die Targets-Datei im Schwester-Repo muss auf 2.5.1 > oder neuer stehen (`git -C ../Deploymentcenter log -1 --oneline -- client-dotnet/Deploymentcenter.Client/Deploymentcenter.BuildInfo.targets`). --- ## 3. Konfiguration und gemeinsame Bausteine ### 3.1 `DcConfig` (kompiliert, nicht einstellbar) ```csharp namespace IBKRTrader.Hosting.Deploymentcenter; public static class DcConfig { public const string BaseUrl = "https://dc.mhdf.de"; // bewusst KEINE Einstellung public const string ProductSlug = "ibkrtrader"; public const string MonitorGroup = "Trading"; public static string AppVersion => BuildInfo.Version; public static string GitCommitShort => BuildInfo.GitCommitShort; } ``` Die Basis-URL entscheidet, wohin die Lizenzprüfung geht und woher ausführbarer Code kommt. Einstellbar wäre sie eine Einladung, die Anwendung auf einen gefälschten Update-Server zu zeigen. ### 3.2 Neuer Abschnitt in `AppSettings` Ergänzt `src/IBKRTrader.Core/Settings/AppSettings.cs` um `DeploymentcenterSettings` (gleiches Muster wie `LoggingSettings`, mit `Category`/`DisplayName`/`Description`, damit es in der Settings-Ansicht auftaucht): | Feld | Vorgabe | Bedeutung | |---|---|---| | `ApiToken` | *(leer)* | `Authorization: Bearer`. Leer = kein Heartbeat, keine Fehlermeldung. `[PasswordPropertyText]` | | `HeartbeatEnabled` | `true` | Dead-Man's-Switch | | `MonitorSource` | *(Hostname)* | Monitorname im Dashboard | | `MonitorInstance` | `default` | mehrere Instanzen (EU/US) desselben Dienstes | | `HeartbeatIntervalSeconds` | `60` | `warning` nach 2×, `down` nach 4× | | `ReportErrors` | `true` | Error/Fatal an den Fehler-Stream | | `CheckUpdatesOnStart` | `true` | prüft nur, installiert nie von selbst | | `UpdateChannel` | `prod` | `prod`, `beta`, `dev` | | `LicenseKey` | *(leer)* | nur Rückfall; Regelfall ist der verschlüsselte Cache | `ApiToken` und `LicenseKey` sind schutzbedürftig und laufen über `SecretProtection` (AES-256-GCM at rest), wie die übrigen Geheimnisse des Projekts. ### 3.3 `DcApiClient` Schlanker JSON-Client für Heartbeat und Fehler-Stream (Lizenz und Update bringen ihren eigenen mit). Übernimmt von Predictalytics: - `DcApiException` mit stabilem `error.code` aus dem Umschlag `{"status":"error","error":{"code":"…"}}` — reagiert wird auf den Code, nie auf den Text. - `IsPermanent` für `401`/`403`/`unauthorized`: danach wird nicht weiter versucht. - Zeitgrenze 10 s. **Ein nicht erreichbares Deploymentcenter darf den Handel niemals beeinflussen** — jeder Aufruf ist best effort. --- ## 4. Watchdog Neuer Worker `DcHeartbeatWorker` in `IBKRTrader.Hosting`, registriert wie die übrigen (`IWorker` + `AddHostedService`), damit er in der Worker-Ansicht auftaucht und manuell auslösbar ist. `POST /api/watchdog/v1/ping` alle *n* Sekunden mit `source`, `instance`, `interval`, `os`, `version = BuildInfo.Version`, `group = "Trading"`. ### 4.1 `checks` — was IBKRTrader wirklich über sich weiß Ein Heartbeat beweist nur, dass ein Timer läuft. Der klassische Fehlerfall dieses Projekts ist ein anderer: der Prozess lebt, aber die TWS-Verbindung ist seit einer Stunde tot und es kommen keine Kurse mehr. | Check | Quelle | `ok` wenn | |---|---|---| | `db` | EF/`MySqlConnector`, `SELECT 1`, gedeckelt auf 5 s | Verbindung steht | | `ibkr_gateway` | `IBKRGatewayService` | verbunden **oder** `UseTwsApi = false` (NullBroker ist ein gewollter Zustand, kein Fehler) | | `market_data` | `IBKRMarketDataRepository` | letzter Tick jünger als *x* min — nur wenn TWS aktiv | | `worker_engine` | `WorkerEngine` | kein Worker im Fehlerzustand | Bewusst **kein** Check auf „Trading aktiv": `TradingEnabled = false` ist ein gewollter Betriebszustand und würde den Monitor sonst dauerhaft auf `warning` halten. Er wandert als Metrik mit. ### 4.2 `metrics` `uptime_sec`, `trading_enabled` (0/1), `open_positions`, `orders_today`, `account_value`, `workers_running`, `memory_mb`, `decision_journal_lag_sec`. Serverseitig 14 Tage Verlauf mit Abweichung zum 7-Tage-Mittel desselben Monitors — damit lässt sich „heute plötzlich 40 Orders statt 3" erkennen, ohne im Deploymentcenter Schwellwerte für jedes Projekt zu pflegen. ### 4.3 Sauberes Beenden Beim geordneten Herunterfahren (SIGTERM/systemd, Fenster schließen) geht ein Ping mit `status: "stopped"` raus. Ohne ihn folgt wenige Minuten nach jedem planmäßigen Neustart ein Fehlalarm. Der Aufruf gehört in `StopAsync` **vor** das Beenden der übrigen Dienste und braucht eine eigene, kurze Zeitgrenze (3 s), damit ein hängender Monitoring-Server das Herunterfahren nicht blockiert. ### 4.4 Hierarchie Läuft der Daemon auf einem eigenen Host, gehört im WebUI unter **WatchDog → System-Hierarchie** die *Übergeordnete Entität* auf diesen Host gesetzt. Sonst erzeugt ein Maschinenausfall zwei Meldungen für ein Problem. --- ## 5. Lizenz `LicenseGuard` in `IBKRTrader.Hosting`, aufgerufen aus `RunStartupChecks` — nach der Zeitzone (Logausgaben) und **vor** dem Start der Worker. - Produkt-Slug `ibkrtrader`, Hardware-ID v2 (`2:win:…` / `2:lin:…`). - `LicenseClient.DefaultAppVersion = BuildInfo.Version` einmalig beim Start, sonst trägt jede Installation in der Aktivierungsliste dieselbe `1.0.0`. - Revalidierung zur Laufzeit alle 12 h; Warnung, wenn die Gnadenfrist in unter 48 h abläuft. - **`LicenseConfig.SetStorageDirectory(AppPaths.ConfigFile("license"))`** — ausdrücklich setzen. Die Vorgabe des SDK löst unter Linux über `$XDG_CONFIG_HOME` bzw. `$HOME` auf; ein systemd-Dienst mit `User=ibkrtrader` und ohne Heimatverzeichnis landet im Rückfall auf `AppContext.BaseDirectory/license`, also unter `/opt` — dort ist nichts schreibbar, und es gäbe **keinen Offline-Cache**. Mit `AppPaths` fügt sich die Ablage in die bereits vorhandene Verzeichnisauflösung des Projekts ein. **Erledigt/entschärft mit 2.5.1** (vormals §8 D4): der Rückfall ohne `$HOME` zeigt jetzt serverseitig auf `/var/lib/` statt ins Installationsverzeichnis — unser eigenes explizites Setzen bleibt trotzdem die robustere Wahl und ist unabhängig vom SDK-Rückfall. ⚠️ **Breaking Change 2.5.1:** `SetStorageDirectory` hängt jetzt selbst `/license` an (vorher wurde der Pfad roh benutzt). Der Aufruf wird zu `LicenseConfig.SetStorageDirectory(AppPaths.Config)` — **ohne** das eigene `"license"`-Suffix, sonst entstünde `…/license/ibkrtrader/license`. ### 5.1 Kopflose Schalter Konvention aus dem Lizenz-Leitfaden, im Daemon umzusetzen (das SDK bringt keine CLI mit): ``` IBKRTrader.Daemon --license-status IBKRTrader.Daemon --license-set-key XXXXX-XXXXX-XXXXX-XXXXX-XXXXX ``` `--license-deactivate` **nicht** verdrahten: es verlangt den `shared_key` des Servers, der gehört nicht in eine ausgelieferte Anwendung. Freigeben läuft über das WebUI (Hardware-Liste → „Freigeben"). > **Geändert mit 2.5.0:** der `update-agent` nimmt den Lizenzschlüssel nicht mehr > als `--license-key`-Argument entgegen — `ps` zeigt Prozessargumente jedem Nutzer > der Maschine, dieselbe Begründung wie beim Crontab-Verzicht in UPGRADE §5. Der > Weg ist jetzt die Umgebungsvariable `DC_LICENSE_KEY` (`LaunchUpdateAgent` setzt > sie selbst, wenn `licenseKey` übergeben wird — kein eigener Code nötig). Unsere > `--license-set-key`-Implementierung ruft also `LicenseClient.ValidateAsync` > direkt auf und schreibt **nicht** `--license-key` in einen eigenen Aufruf des > Agenten. > > Ebenfalls neu: `EnsureLicensedAsync()` auf `LicenseClient` — nimmt den > zwischengespeicherten Schlüssel, fragt bei Bedarf über `ILicensePrompt` nach, > prüft, fragt bei Ablehnung erneut. `allowPrompt:false` lehnt ohne Cache ab, statt > im Daemon auf eine Eingabe zu warten, die nie kommt — **das ist für den Daemon > die richtige Einstiegsmethode**, nicht das manuelle Zusammensetzen aus > `TryGetCachedKey` + `ValidateAsync`. ### 5.2 Reaktion — hier weicht IBKRTrader bewusst vom Leitfaden ab Die DC-Doku empfiehlt bei einem endgültigen Urteil `Environment.Exit(1)`. **Für einen Dienst mit offenen Positionen ist das die falsche Reaktion.** Ein abgelaufener Vertrag ist kein Grund, ein Portfolio unbeaufsichtigt zu lassen — Stop-Loss- und Exit-Logik müssen weiterlaufen, sonst richtet die Lizenzdurchsetzung mehr Schaden an als der unlizenzierte Betrieb. Vorschlag — drei Stufen statt zwei: | Ergebnis | Zustände | Reaktion | |---|---|---| | `IsValid` | `valid`, `valid_offline` | Normalbetrieb | | `IsTransient` | `server_unavailable`, `cache_expired` | **kein Urteil, nur keine Auskunft.** Warnen, weiterlaufen, beim nächsten Durchlauf erneut fragen | | Urteil | `revoked`, `expired`, `not_found`, `activation_limit`, `suspended`, `clock_rollback` | **Sperrbetrieb:** `TradingEnabled` hart auf `false`, keine neuen Einstiege, Risiko-/Exit-Worker und Buchhaltung laufen weiter, Meldung an Watchdog (`status: "warning"`) und Fehler-Stream. Beendet wird erst, wenn keine Position mehr offen ist — oder auf ausdrückliche Anweisung | Bei der Desktop-Shell darf es beim Start weiterhin ein Lizenzfenster geben; der **Daemon** darf niemals auf eine Eingabe warten. --- ## 6. Error-Reporting Zwei Wege in `POST /api/errors/v1/report`, beide in `IBKRTrader.Hosting`: 1. **Senke am `LoggingService`.** `LoggingService.EntryWritten` existiert bereits und ist genau dafür gedacht (die Oberfläche hängt schon dran). Ein `DcErrorSink` filtert auf `AppLogLevel.Error` und meldet mit `level: "error"`, `build: BuildInfo.Version`, `file`/`line` aus dem Stacktrace, `context` mit `GitCommitShort` und `CorrelationId`/`SignalId`, wenn vorhanden. Die `SignalId` mitzuschicken ist der eigentliche Gewinn: eine Fehlermeldung lässt sich damit im Deploymentcenter direkt gegen das Entscheidungsjournal halten. 2. **Globale Handler** — `AppDomain.UnhandledException` (`level: "fatal"`, **blockierend** gesendet, der Prozess ist gleich weg) und `TaskScheduler.UnobservedTaskException`. Beide fehlen im Projekt heute vollständig; sie müssen sowieso her. ### 6.1 Bremse Verpflichtend, sonst verbrennt ein Fehler in einer Handelsschleife das serverseitige Rate-Limit binnen Sekunden: - höchstens 20 Meldungen pro Minute, - derselbe Fehler höchstens alle 5 Minuten, - bei `401`/`403` dauerhaft abschalten, - bei `429` Intervall verdoppeln. Betriebsbedingtes Rauschen gehört nicht in diese Bremse, sondern in die **Ignore-Regeln** des WebUI: dort wird weitergezählt, aber nicht gemeldet — und eine Alarmschwelle schlägt an, wenn ein bekannter Fehler plötzlich hundertmal so oft auftritt. > **Nicht melden:** alles, was Kontodaten, Positionen oder Schlüsselmaterial im > Text führen könnte. Der Fehler-Stream ist eine externe Ablage. Vor dem Senden > wird der Text durch dieselbe Maskierung geschickt, die schon für die Logs gilt. --- ## 7. UpdateService, Erstinstallation und Release-Pipeline ### 7.1 Prüfen, nicht installieren `DcUpdateService.CheckAsync` beim Start (`GET /api/updateservice/v1/check`). **Mit Zugangsdaten**, sonst gibt es seit 2.4 nur noch `401`: ```csharp var key = settings.Deploymentcenter.LicenseKey is { Length: > 0 } k ? k : LicenseClient.TryGetCachedKey(DcConfig.ProductSlug); var check = await new UpdateClient().CheckForUpdateAsync( baseUrl: DcConfig.BaseUrl, projectId: DcConfig.ProductSlug, currentVersion: BuildInfo.Version, channel: settings.Deploymentcenter.UpdateChannel, credentials: ReleaseCredentials.FromLicenseKey(key), cancellationToken: ct); if (check.Unauthorized) { /* Lizenzproblem, KEIN Netzwerkfehler — eigene Meldung */ } ``` Gefunden wird nur, installiert wird nichts von selbst. Ein Trading-Dienst, der sich unangekündigt neu startet, ist keine akzeptable Vorgabe. - Kein Update → Info ins Log. - Update verfügbar → Warnung + Hinweis in der Shell. - Kritisch → zusätzlich Dialog (Shell) bzw. Watchdog-Event (Daemon). ### 7.2 Anwenden Über `UpdateClient.LaunchUpdateAgent(..., licenseKey: key)`; der Agent bekommt `--restart`, `--wait-for-pid`, `--platform` von selbst, den Lizenzschlüssel seit 2.5.0 **nicht mehr** als `--license-key`-Argument, sondern über die Umgebungsvariable `DC_LICENSE_KEY` (siehe §5.1). Vorher geht ein `status: "stopped"` an den Watchdog. **Vorbedingung, die die Anwendung selbst prüfen muss:** kein offener Auftrag, keine laufende Order. Ein Update, das den Prozess zwischen Order-Absendung und Statusrückmeldung abräumt, hinterlässt eine Order bei IBKR ohne Gegenstück im Journal. Das Update wird also erst nach dem Handelsschluss bzw. bei leerem Auftragsbuch angeboten. > **Verschärft mit 2.5.0:** `exitCurrentApp: true` ist jetzt ausdrücklich als > **harter Kill** dokumentiert — laufende `finally`-Blöcke, `IHostApplicationLifetime` > und Destruktoren kommen nicht mehr zum Zug. Für IBKRTrader heißt das: Solange > die Vorbedingung oben nicht sicher geprüft ist (offenes Auftragsbuch, laufende > Buchungstransaktion), muss `exitCurrentApp: false` gesetzt werden. Der Agent > wartet über `--wait-for-pid` ohnehin auf das Prozessende; das eigene > Herunterfahren läuft dann über `host.StopAsync(...)` wie in `App/Program.cs` > bereits vorhanden — **nicht** über `LaunchUpdateAgent` selbst. Zusätzlich > `waitTimeoutSeconds` heraufsetzen, falls das geordnete Beenden (EF-Flush, > Kestrel, Log-Flush, DB-Backup-Worker) länger als die Vorgabe von 60 s dauern > kann — läuft die Zeit ab, bricht der Agent ohne Änderung ab. Für den Linux-Dienst siehe [§8 D3](#8-befunde-und-offene-punkte) — dafür gibt es im Deploymentcenter derzeit keinen fertigen Weg. ### 7.3 Was ausgeliefert wird — Ausschließen vs. Schützen Das ist die Stelle, an der Updates in anderen Projekten schon Konfigurationen zerstört haben. Für IBKRTrader gilt: | Datei | Liste | Begründung | |---|---|---| | `settings.example.json` | *(im Paket)* | Vorlage mit Platzhaltern — Erstinstallation braucht sie | | `settings.json` | `preservePatterns` | enthält DB-Zugang, DC-Token, Flex-Token. Muss ein Update überleben | | `appsettings.Local.json` | `preservePatterns` | Verbindungszeichenfolge des Zielsystems | | `master.key`, `openrouter.key` | `excludePatterns` | **darf nie ins Paket** | | `Logs/**`, `Backups/**` | `excludePatterns` | Laufzeitdaten | | `*.pdb`, `*.xml` | `excludePatterns` | Build-Artefakte | | `update-agent*` | bewusst entscheiden | mitliefern = aktualisiert sich selbst mit | `pack-and-deploy` warnt von sich aus bei Dateien, die nach Zugangsdaten aussehen. Die Warnung ist ernst zu nehmen: Release-Pakete liegen zwar hinter einem Zugangsschutz, aber jeder lizenzierte Kunde kann sie auspacken. ### 7.4 `setup.json` Beschreibt der Erstinstallation, was abzufragen ist; wird ins Paket kopiert. Entwurf für IBKRTrader: ```json { "schema": 1, "displayName": "IBKRTrader", "targets": [ { "id": "app", "file": "settings.json", "location": "install", "format": "json" } ], "fields": [ { "key": "Database:Host", "label": "MySQL-Host", "type": "string" }, { "key": "Database:Database", "label": "Datenbank", "type": "string" }, { "key": "Database:User", "label": "Benutzer", "type": "string" }, { "key": "Database:Password", "label": "Passwort", "type": "secret" }, { "key": "IBKR:Host", "label": "TWS/Gateway", "default": "127.0.0.1" }, { "key": "IBKR:Port", "label": "Port (Paper 4002 / Live 4001)", "type": "int", "default": "4002" }, { "key": "Trading:ApplicationTimeZoneId", "label": "Betriebszeitzone", "default": "Europe/Berlin" }, { "key": "Deploymentcenter:BaseUrl", "source": "detect:baseurl", "type": "url" }, { "key": "Deploymentcenter:ApiToken", "label": "Token dieser Installation", "source": "provision", "scopes": ["watchdog:ping", "bugtracker:report"] }, { "key": "Deploymentcenter:MonitorSource", "source": "detect:hostname" } ] } ``` Zwei Punkte dazu: - **`location: "install"` ist nur unter Windows richtig** — und auch dort nur, solange nicht nach `C:\Program Files` installiert wird. Siehe [§8 P2](#8-befunde-und-offene-punkte); solange `AppPaths` nicht korrigiert ist, passen `setup.json` und die tatsächliche Leseposition der Anwendung unter Linux nicht zusammen. **Dieser Punkt blockiert die Erstinstallation und gehört vor §7.4.** - Der Installer schreibt **Klartext**. `SecretProtection` muss also Klartext unverändert durchreichen und beim ersten Speichern verschlüsseln — die Weiche ist zu prüfen und im Test ausdrücklich als *aktiv genutzt* zu beschreiben, damit sie nicht eines Tages als tot entfernt wird. ### 7.5 Release-Pipeline `scripts/release.ps1` und `scripts/release.sh` sind per `curl` von `/docs/release-template/` geholt (wie `release.md` empfiehlt, statt aus dem lokalen Schwester-Repo kopiert — damit ist sichergestellt, dass die kanonische Fassung im Projekt liegt) und **unverändert** gelassen. Nur `scripts/release.config.json` ist ausgefüllt: ```json { "project": "ibkrtrader", "csproj": "src/IBKRTrader.Daemon/IBKRTrader.Daemon.csproj", "runtimes": ["win-x64", "linux-x64"], "selfContained": true, "setupJson": "setup.json" } ``` **Entschieden: nur der Daemon, keine Zwei-Produkte-Aufteilung.** IBKRTrader hat zwei Einstiegspunkte (`App` = Avalonia-Shell, `Daemon` = kopflos). Die Vorlage nimmt aber genau **eine** `csproj` für alle Runtimes in der Liste — sie ruft `dotnet publish -r ` in einer Schleife auf, es gibt keine Möglichkeit, App für `win-x64` und Daemon für `linux-x64` in **demselben** Lauf zu veröffentlichen, ohne das (laut Anleitung unveränderte) Skript anzufassen. Ursprünglich in diesem Dokument vorgeschlagen war genau das ("Windows = App, Linux = Daemon") — das ist mit der Vorlage so nicht umsetzbar und wurde beim Umsetzen korrigiert. `IBKRTrader.Daemon` ist der bewusste Kandidat: laut README „plattformneutral" (`net10.0` ohne Plattformbindung), läuft kopflos auf beiden Zielsystemen und ist ohnehin die für den Serverbetrieb vorgesehene Form (`deploy/README.md`). Eine automatisierte Auslieferung der Avalonia-Shell (`App`) als Windows-Desktop-Distribution ist damit **nicht** Teil dieses Durchlaufs — dafür bräuchte es entweder einen zweiten Produkt-Slug (`ibkrtrader-app`, doppelte Lizenz-/Update-Pflege) oder ein angepasstes Release-Skript. Zurückgestellt, bis tatsächlich Bedarf an einer automatisierten Desktop-Auslieferung besteht — bis dahin bleibt `dotnet run --project src/IBKRTrader.App` bzw. eine manuelle `dotnet publish` der übliche Weg für die Shell. Zugangsdaten kommen aus `DC_FTP_HOST` / `DC_FTP_USER` / `DC_FTP_PASS` / `DC_TOKEN`, `.dc-tools/` und `artifacts/` sind in der `.gitignore`. **`packager.config.json` (`excludePatterns`/`preservePatterns`) ist nicht Teil dieses Durchlaufs.** Sie liegt "neben dem Werkzeug", also unter `.dc-tools/` — das entsteht erst, wenn `pack-and-deploy` beim ersten echten Lauf von `release.ps1`/`release.sh` selbst heruntergeladen wird (Prüfsummen-geprüft). Ein erster Release-Lauf braucht `DC_FTP_*`/`DC_TOKEN` und ist damit eine credentialierte, vom Betreiber auszulösende Aktion. **Sicherheitsnetz bis dahin:** Der Packager bricht seit 2.5.0 ohnehin hart ab, wenn er im Publish-Verzeichnis nach Zugangsdaten aussehende Dateien findet (Name *und* Inhalt) — die in §7.3 beschriebenen Dateien (`settings.json`, `appsettings.Local.json`) sind ohnehin gitignored und landen nur dann im Publish-Output, wenn sie lokal im Projektverzeichnis liegen (`` in den `.csproj`-Dateien kopiert sie dann mit). Bricht `release.ps1` mit einem Hinweis auf eine vermutete Zugangsdatei ab, ist das der Packager, der genau das abfängt — kein Fehlschlag, sondern die Absicherung, die 2.5.0 dafür eingeführt hat. --- ## 8. Befunde und offene Punkte > **Status 2026-08-23:** P1–P4 sind umgesetzt (Einzelheiten in §9). **P5 ist eingetreten**, > P6 entscheidet sich erst beim ersten Release. Der ursprüngliche Vermerk von 2026-08-14: > > **Status 2026-08-14:** Alle sieben ursprünglich an den Deploymentcenter-Entwickler > gemeldeten Befunde (D1–D7) sind mit den Versionen 2.5.0/2.5.1 behoben — bestätigt über > `GET /api/updateservice/v1/changelog?since=2.4`. Der Abschnitt bleibt als Nachweis > stehen, jeder Punkt trägt jetzt einen Fix-Vermerk. Die Projekt-Befunde (P1–P6) sind > davon unabhängig und weiterhin offen — sie werden in diesem Durchlauf abgearbeitet. ### Am Projekt (P) — muss vor der Integration bereinigt werden **P1 · Echte Zugangsdaten als Vorgabewerte im Quelltext.** `src/IBKRTrader.Core/Settings/AppSettings.cs:14-35` trägt Host, Datenbankname, Benutzer und **Passwort** einer produktiven MariaDB als Vorgabe. Die Datei ist versioniert, die Werte stehen damit auch in der Git-Historie — `settings.json` im `.gitignore` hilft dagegen nicht. Das Passwort ist als kompromittiert zu behandeln und zu wechseln; die Vorgaben gehören auf Platzhalter. Zusätzlich brisant, weil die Vorlage künftig im Release-Paket ausgeliefert wird. > **Verschärft mit 2.5.0:** der Packager **bricht jetzt hart ab**, wenn er > Zugangsdaten im Publish-Verzeichnis erkennt (Dateiname *und* Inhalt, u. a. > gefülltes `Password=`) — vorher nur eine Warnung, die auf der Preserve-Liste > sogar unterdrückt wurde. Damit ist P1 nicht mehr „sollte bereinigt werden", > sondern **release-blockierend**: Ohne Fix lässt sich `settings.example.json` > gar nicht mehr veröffentlichen. **P2 · `AppPaths` fällt unter Windows auf einen Linux-Pfad zurück.** `Configuration/AppPaths.cs:57-71`: ist das Binärverzeichnis nicht beschreibbar, gilt der FHS-Rückfall `/etc/ibkrtrader`. Unter Windows löst .NET das zu `C:\etc\ibkrtrader` auf. Genau dieser Fall tritt ein, sobald der DC-Installer seinen Windows-Vorschlag `Programme\` benutzt — dann liegen `settings.json`, `master.key` und `Logs/` in `C:\etc\`. Nötig: plattformabhängiger Rückfall (`%ProgramData%\IBKRTrader` bzw. `%APPDATA%`). **P3 · Keine globalen Ausnahmebehandler.** Ohne `AppDomain.UnhandledException` und `TaskScheduler.UnobservedTaskException` gibt es keine `fatal`-Meldungen — und heute auch keinen Logeintrag, wenn der Prozess wegbricht. Unabhängig vom Deploymentcenter überfällig. **P4 · Version an zwei Stellen hartkodiert.** `App/Program.cs:34` und `Daemon/Program.cs:43`. Ohne `Directory.Build.props` bricht `pack-and-deploy` beim ersten Release ab — zu Recht. **P5 · CI kennt das Schwester-Repo nicht.** `.gitea/workflows/build.yml` checkt nur IBKRTrader aus. Eine `ProjectReference` nach `..\..\..\..\Deploymentcenter\…` macht den Build auf beiden Matrix-Zielen rot. Siehe die Entscheidung in §2.2. > **Seit 2026-08-23 eingetreten, nicht mehr nur vorhergesagt.** Die > `ProjectReference` ist mit der Integration auf `main` gelandet, die CI ist > damit rot. Das war die bewusst in Kauf genommene Folge der Interimslösung aus > §2.2 — lokal baut die Projektmappe, sobald das Schwester-Repo daneben liegt. > Auflösung ist Schritt 10: SDK als NuGet-Paket in die Gitea-Registry, dann > `PackageReference` statt Cross-Repo-Pfad. **P6 · Konfiguration liegt im Installationsverzeichnis.** Unter Windows schreibt `AppPaths` neben die Binärdatei. Das ist mit `preservePatterns` beherrschbar (§7.3), muss aber beim ersten Release stimmen — ein Update, das `settings.json` überschreibt, nimmt einer laufenden Installation Datenbank, Token und Flex-Zugang gleichzeitig. ### Am Deploymentcenter (D) — gemeldet, Stand vor 2.5.0 **D1 · Der Lizenzschlüssel als Update-Zugang war nirgends vollständig erklärt.** ✅ **Behoben (2.5.1, Doku).** Laut Changelog explizit als „Lückenschluss" genannt: „Woher die Anwendung den Lizenzschlüssel für den Update-Zugang nimmt (`TryGetCachedKey`)". Für uns bleibt zusätzlich `EnsureLicensedAsync()` (neu in 2.5.0) relevant — siehe §5.1. **D2 · Predictalytics wurde vom Zugangsschutz ausgesperrt.** ✅ **Bestätigt und behoben (2.5.1).** Der Changelog bestätigt den Verdacht wörtlich: „bei Predictalytics ist genau das passiert". Fix: Zugangsschutz ist jetzt je Produkt abschaltbar (WebUI → UpdateService → Zugangsschutz) — für Installationsbasen ohne SDK 2.4 lässt sich das Produkt ausnehmen, mit Schlüssel neu ausliefern, danach wieder einschalten. **Für IBKRTrader heißt das:** genau diese Falle von Anfang an vermeiden, indem der erste ausgelieferte Build bereits `licenseKey` mitgibt (§7.5, §9 Reihenfolge). **D3 · Für einen systemd-Dienst gab es keinen dokumentierten Update-Weg.** ✅ **Behoben (2.5.1, Doku).** Laut Changelog ergänzt: „wie ein laufender systemd-Dienst aktualisiert wird". Bleibt in §7.2/§8-P laufend zu beobachten, ob die neue Beschreibung mit unserem tatsächlichen Vorgehen (`--target-dir`, Dienstbenutzer-Rechte) übereinstimmt — bei der Umsetzung von Aufgabe „Update-Prüfung mit ReleaseCredentials" gegenprüfen. **D4 · `LicenseConfig.GetStorageDirectory`/`SetStorageDirectory` war für Dienste unglücklich.** ✅ **Behoben, mit Breaking Change (2.5.1).** Zwei Korrekturen: (a) Rückfall ohne `$HOME` zeigt jetzt auf `/var/lib/` statt ins Installationsverzeichnis. (b) `SetStorageDirectory` hängt jetzt selbst `/license` an, statt den Pfad roh zu verwenden — **das ändert unseren Aufruf**, siehe §5 (`AppPaths.Config` statt `AppPaths.ConfigFile("license")`). **D5 · `BuildInfo.targets` verhinderte inkrementelle Builds.** ✅ **Behoben (2.5.1).** `BuildDateUtc` ist jetzt tagesgenau statt mit voller Uhrzeit, `WriteOnlyWhenDifferent` greift wieder. Siehe §2.3. **D6 · Die Empfehlung „bei einem Urteil beenden" passte für handelnde Systeme nicht.** ✅ **Behoben (2.5.1, Doku)** — als Lückenschluss genannt: „was statt `Environment.Exit(1)` zu tun ist, wenn Beenden die gefährlichere Option wäre". Unser Sperrbetrieb-Ansatz (§5.2) bleibt die konkrete Umsetzung für IBKRTrader. **Zusätzlich, über D6 hinausgehend:** 2.5.0 dokumentiert `exitCurrentApp: true` jetzt als harten Kill ohne `finally`/Lifecycle-Hooks — praktisch relevanter für uns als die Lizenz-Frage allein, siehe §7.2. **D7 · Kleinigkeiten** (README-Verweis auf `AGENT_RELEASE_GUIDE.md`, Rate-Limits nicht durchgängig beziffert). ✅ **Behoben (2.5.1, Doku)** — Changelog nennt „die Drosselungsgrenzen aller Endpunkte" als ergänzt; `docs/README.md` führt jetzt `/docs/release.md` und den Changelog-Endpunkt im Index. --- ## 9. Reihenfolge | Schritt | Inhalt | Abhängig von | Stand | |---|---|---|---| | 0 | **P1** Zugangsdaten wechseln, Vorgaben auf Platzhalter | — | ✅ Vorgaben sind Platzhalter. **Die Rotation des geleakten Passworts steht weiterhin aus** (Nutzer-Aktion) | | 1 | **P2** `AppPaths`-Rückfall plattformabhängig, **P4** `Directory.Build.props` | — | ✅ 2026-08-23 | | 2 | SDK-Bezug Stufe 1: Cross-Repo-`ProjectReference` (§2.2) | — | ✅ 2026-08-23, inkl. Prüf-Target mit lesbarer Fehlermeldung | | 3 | `DcConfig`, `DeploymentcenterSettings`, `DcApiClient`, `BuildInfo` | 1, 2 | ✅ 2026-08-23 | | 4 | **P3** globale Handler + Fehler-Stream | 3 | ✅ 2026-08-23 (`DcCrashHandlers`, `DcErrorSink`, `DcErrorReporter`) | | 5 | Watchdog-Heartbeat inkl. `stopped` | 3 | ✅ 2026-08-23 (`DcHeartbeatWorker`) | | 6 | Lizenz mit Sperrbetrieb statt Abbruch (§5.2), `EnsureLicensedAsync` | 3 | ✅ 2026-08-23 (`LicenseGuard`) | | 7 | Update-Prüfung **mit** `ReleaseCredentials`, `exitCurrentApp:false` bei offenem Zustand | 6 (braucht den Schlüssel) | 🔶 Die **Prüfung** läuft beim Start. Das **Anwenden** ist es nicht: `LaunchAgent` ist fertig, aber kein Aufrufer fährt danach geordnet herunter | | 8 | `setup.json`, `preserve`/`exclude`, Release-Pipeline (Vorlage per `curl` von `/docs/release-template/`) | 1–7 | ✅ 2026-08-23 (`setup.json`, `scripts/release.*`). **Noch nie ausgeführt** – das erste Release steht aus | | 9 | Bugtracker-Baustein nach `AGENTS.md` / `.agents/rules` | Projekt-Slug angelegt | ⬜ offen — Projekt `ibkrtrader` muss im WebUI erst angelegt werden | | 10 | **Folgeschritt, nicht Teil dieses Durchlaufs:** `Deploymentcenter.Client` als NuGet-Paket in die Gitea-Registry pushen, `` durch `PackageReference` ersetzen, Gitea-CI (**P5**) auf den Paketbezug umstellen | 2 | ⬜ offen — siehe §2.2 | Schritt 7 und 8 hängen zusammen: **der erste ausgelieferte Build muss die Zugangsdaten schon mitbringen.** Für ein Produkt, das noch nie veröffentlicht hat, entsteht `/releases/ibkrtrader/` mit dem ersten Upload und wird vom nächsten `tick.php`-Lauf geschützt — es gibt kein Zeitfenster, das SDK danach nachzurüsten. Wird Schritt 7 übersprungen, fällt die Tür hinter dem ersten Release zu (genau der Fall, der laut Changelog bei Predictalytics bereits eingetreten ist, siehe D2). Schritt 9 (Bugtracker) setzt voraus, dass das Projekt `ibkrtrader` im Deploymentcenter-WebUI angelegt ist und ein Token mit `bugtracker:report` vorliegt — beides serverseitige Handarbeit, die dieser Durchlauf nicht auslösen kann. Wird zurückgestellt.