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

34 KiB
Raw Blame History

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, 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.
  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>

<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)

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 HandlerAppDomain.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:

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 — 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:

{
  "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; 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:

{
  "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.