Files
IBKRTrader/docs/konzepte/KONZEPT-Deploymentcenter-Integration.md
T
RichardandClaude Opus 5 0a72ea7e28 Deploymentcenter-Integration (Schritte 0-8 des Konzepts)
Bindet Lizenz, Watchdog, Fehler-Stream, UpdateService und Erstinstallation an
das Deploymentcenter 2.5.1 an. Einbauort ist IBKRTrader.Hosting - den Host
teilen sich Shell und Daemon.

Projekt-Befunde aus dem Konzept vorab bereinigt:
  P1  Echte DB-Zugangsdaten als Vorgabewerte in AppSettings -> Platzhalter.
      Das alte Passwort steht weiterhin in der Git-Historie und ist als
      kompromittiert zu behandeln (Rotation ist Nutzer-Aktion).
  P2  AppPaths fiel unter Windows auf /etc/ibkrtrader zurueck, was .NET zu
      C:\etc\ibkrtrader aufloest. Jetzt %ProgramData%\IBKRTrader.
  P3  Globale Ausnahmebehandler (AppDomain / TaskScheduler) - vorher gab es
      keinen Logeintrag, wenn der Prozess unbehandelt wegbrach.
  P4  Version einmal zentral in Directory.Build.props statt zweimal hartkodiert.

Bewusste Abweichungen vom DC-Leitfaden, beide fuer ein handelndes System:
  - Lizenz-Urteil fuehrt zum Sperrbetrieb (TradingEnabled=false) statt zu
    Environment.Exit(1). Keine neuen Einstiege, aber Risiko-, Exit- und
    Buchhaltungslogik laufen weiter.
  - exitCurrentApp bleibt immer false; der Aufrufer beendet geordnet.

Das SDK haengt als Cross-Repo-ProjectReference am Schwester-Repo
Deploymentcenter (Interim, siehe Konzept 2.2). Damit ist P5 offen: die
Gitea-CI checkt das Schwester-Repo nicht aus und wird rot, bis der Bezug auf
ein NuGet-Paket umgestellt ist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 10:44:56 +02:00

653 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `<Version>`, 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 `<Version>`
```xml
<Project>
<PropertyGroup>
<Version>1.0.0</Version>
</PropertyGroup>
</Project>
```
`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 `<Import>` 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 `<Import>` 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, `<Import>` durch `<PackageReference Include="Deploymentcenter.Client" Version="2.5.1" />` 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/<slug>` 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 `<slug>/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 <csproj> -r <rid>` 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 (`<None Update
Condition="Exists(...)">` 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-14:** Alle sieben ursprünglich an den Deploymentcenter-Entwickler
> gemeldeten Befunde (D1D7) 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 (P1P6) 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\<slug>` 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.
**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/<slug>` statt ins
Installationsverzeichnis. (b) `SetStorageDirectory` hängt jetzt selbst
`<slug>/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 | — | ⬜ dieser Durchlauf |
| 1 | **P2** `AppPaths`-Rückfall plattformabhängig, **P4** `Directory.Build.props` | — | ⬜ dieser Durchlauf |
| 2 | SDK-Bezug Stufe 1: Cross-Repo-`ProjectReference` (§2.2) | — | ⬜ dieser Durchlauf |
| 3 | `DcConfig`, `DeploymentcenterSettings`, `DcApiClient`, `BuildInfo` | 1, 2 | ⬜ dieser Durchlauf |
| 4 | **P3** globale Handler + Fehler-Stream | 3 | ⬜ dieser Durchlauf |
| 5 | Watchdog-Heartbeat inkl. `stopped` | 3 | ⬜ dieser Durchlauf |
| 6 | Lizenz mit Sperrbetrieb statt Abbruch (§5.2), `EnsureLicensedAsync` | 3 | ⬜ dieser Durchlauf |
| 7 | Update-Prüfung **mit** `ReleaseCredentials`, `exitCurrentApp:false` bei offenem Zustand | 6 (braucht den Schlüssel) | ⬜ dieser Durchlauf |
| 8 | `setup.json`, `preserve`/`exclude`, Release-Pipeline (Vorlage per `curl` von `/docs/release-template/`) | 17 | ⬜ dieser Durchlauf |
| 9 | Bugtracker-Baustein nach `AGENTS.md` / `.agents/rules` | Projekt-Slug angelegt | ⬜ separat (Projekt `ibkrtrader` muss im WebUI erst angelegt werden) |
| 10 | **Folgeschritt, nicht Teil dieses Durchlaufs:** `Deploymentcenter.Client` als NuGet-Paket in die Gitea-Registry pushen, `<Import>` 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.