Die Regel galt seit dem Umbau auf Avalonia, stand aber nirgends: Der Ausgabeweg ist waehlbar - Desktop, Web oder beides gleichzeitig -, und daraus folgt genau eine harte Vorgabe: Fachlichkeit lebt in einem Core-Dienst, nie in einer Ansicht. Ein Fenster und ein Endpunkt sind zwei Zugaenge auf denselben Dienst. In den Grundregeln steht jetzt die Prueffrage dazu (koennte ein zweiter Ausgabeweg dasselbe tun, ohne diese Zeile zu kopieren?), die Vorgabe, dass Dienst und Endpunkt im selben Zug wie das Fenster entstehen, und der Daemon als Gegenprobe: Was sich kopflos nicht bedienen laesst, haengt zu sehr am Fenster. docs/ANLEITUNG-WebUI.md richtet sich an den Agenten, der #490 uebernimmt. Enthaelt den gemessenen Bestand, die Reihenfolge der drei Bloecke, die Tabelle der 24 Endpunkte mit ihren Diensten, die beiden nicht-mechanischen Stellen (Logs als Push, Settings mit Zugangsdaten) und die Grenzen - vor allem: keine Fachlogik anfassen, keine schreibenden Endpunkte ohne Anmeldung. Ein Punkt darin ist Architektur und keine Geschmacksfrage: Der Core darf die Module nicht kennen, also koennen Modul-Routen nicht im Core registriert werden. Sie gehoeren ins Hosting-Projekt, das die Module ohnehin kennt - so wie die Modulfenster zentral in App/Shell/ModuleViews.cs stehen und nicht im Modul. 473 Tests gruen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.5 KiB
Anleitung: Web-UI als zweiter Ausgabeweg
Für den Agenten, der diese Aufgabe übernimmt. Bugtracker-Item #490 im Deploymentcenter,
Projekt ibkrtrader. Lies zuerst .agents/rules/grundregeln.md — die Regeln dort haben Vorrang
vor allem, was hier steht.
Der Auftrag in einem Satz
IBKRTrader hat heute eine Avalonia-Desktopoberfläche. Es soll daneben eine Weboberfläche geben — wählbar, auch beide gleichzeitig. Kein Ersatz: Die Shell bleibt.
Die eine Regel, aus der alles folgt
Fachlichkeit lebt in einem Core-Dienst, nie in einer Ansicht.
Ein Fenster und ein REST-Endpunkt sind zwei Zugänge auf denselben Dienst. Beide dürfen formatieren, sonst nichts. Wenn du beim Bauen einer Route merkst, dass du eine Berechnung, eine Prüfung oder eine Regel schreibst — halt an. Die gehört in den Core, und dann kann das Avalonia-Fenster sie auch benutzen.
Die Prüffrage: Könnte ein zweiter Ausgabeweg dasselbe tun, ohne diese Zeile zu kopieren?
Vorbild ist src/IBKRTrader.Core/Positions/PositionManager.cs: ein Dienst, drei Zugänge —
Shell-Fenster, Daemon-Schalter, GET /api/positions. Sieh dir die drei an, bevor du anfängst.
Der Endpunkt darin ist rund dreißig Zeilen; so soll jeder aussehen.
Was schon da ist
Core/Workers/BuiltIn/WebApiService.cs |
HttpListener, Port Webserver.Port + 1, 3 Routen |
Core/Workers/BuiltIn/WebserverService.cs |
zweiter HttpListener auf Webserver.Port, liefert nur einen Status-JSON |
WebserverSettings |
Port (Vorgabe 5001), Enabled (Vorgabe false) |
| Routen | GET /api/status, GET /api/workers, GET /api/positions |
Beide Listener sind Worker vom Typ Service und laufen in beiden Betriebsformen — Shell und
Daemon. Das ist gut so und soll so bleiben: Die Weboberfläche muss auf der kopflosen Instanz
funktionieren, dort ist sie am meisten wert.
Die Oberfläche selbst umfasst heute rund 585 Zeilen AXAML und 1.370 Zeilen Code-behind in zehn Fenstern. Sie sind dünn — jedes holt Daten aus einem Core-Dienst und verdrahtet Steuerelemente. Der teure Teil einer Portierung ist damit schon erledigt. Du brauchst keine Fachlogik zu verstehen oder nachzubauen, sondern nur einen zweiten Zugang zu bestehenden Diensten.
Reihenfolge
1. HTTP-Unterbau ersetzen
Der HttpListener mit handgeschriebenem switch-Router trägt drei Routen, aber keine Oberfläche:
localhost-gebunden, ohne Anmeldung, ohne statische Dateien, mit catch { } um jede Antwort.
Nimm ASP.NET Core Minimal APIs. Im Hosting-Projekt genügt
<FrameworkReference Include="Microsoft.AspNetCore.App" />
— das SDK bleibt Microsoft.NET.Sdk, das Projekt wird kein Web-Projekt. Achte darauf, dass
dotnet publish für den Daemon weiter durchläuft; die Zielmaschine braucht dann die
ASP.NET-Core-Runtime (bei self-contained wird sie mitgeliefert).
Was dabei zusammenkommt: Die beiden Listener werden einer. Ein Host liefert die statischen Dateien der Oberfläche und die API.
Was hier hineingehört, bevor irgendein schreibender Endpunkt entsteht: eine Anmeldung. Solange keine steht, bleibt die Schnittstelle lesend — ohne sie wäre ein Eingriff in die Bücher möglich, den niemand belegen kann.
2. Endpunkte
Rund 24 gezählt, 3 vorhanden. Sie sind mechanisch: Der Dienst existiert jeweils schon.
| Ansicht | Dienst | lesend | auslösend |
|---|---|---|---|
| Dashboard | DashboardService.GetSnapshotAsync |
1 | — |
| Workers | WorkerEngine.WorkerInfos |
1 | TriggerWorkerAsync |
| Positionen | PositionManager.GetOverviewAsync |
✔ fertig | ReassignAsync, SetLockAsync, SetRuleAsync |
| Orders | IBrokerOrderBook.GetOpenOrdersAsync |
1 | CancelOrderAsync |
| Logs | LoggingService |
1 + Strom | — |
| Settings | SettingsService |
1 | speichern |
| Accounting | AccountingReportService, ILedgerRepository, IIngestRunRepository |
~4 | IngestAllAsync |
| CongressTrading | CongressRepository, IPortfolioService |
2 | TriggerWorkerAsync |
| Supervisor | DossierService, ISupervisorReportRepository |
2 | SupervisorAgent.AskAsync |
Wichtig für die Modul-Endpunkte: Der Core darf die Module nicht kennen — das ist eine harte
Architekturregel. IBKRTrader.Hosting kennt sie (siehe die ProjectReference-Liste in
IBKRTrader.Hosting.csproj) und ist deshalb der richtige Ort, um Modul-Routen zu registrieren.
Das spiegelt, wie es bei den Fenstern gelöst ist: zentral in App/Shell/ModuleViews.cs, nicht im
Modul. Leg eine entsprechende Stelle für Routen an; Core-Routen kommen aus dem Core, Modul-Routen
aus dem Hosting.
3. Frontend
Hier liegt das Volumen. Neun Ansichten, überwiegend Tabelle plus Werkzeugleiste. Es braucht kein Rahmenwerk, das darüber hinausgeht. Halte dich an die Spalten, die die Fenster heute zeigen — die Workers-Ansicht hat ihre Spalten sogar in den Grundregeln festgeschrieben.
Die beiden Stellen, die nicht mechanisch sind
Logs. LoggingService meldet Einträge über das Ereignis EntryWritten; die Shell hängt sich
ein und färbt selbst. Das ist ein Push. Polling wäre ein Rückschritt gegenüber dem, was die Shell
heute kann — nimm Server-Sent Events. Entscheide dabei ausdrücklich, was bei mehreren offenen
Browsern passiert, und schreib die Entscheidung hin.
Settings. Schreibt settings.json, und darin stehen Zugangsdaten zu Datenbank und Broker.
Dieser Endpunkt darf ohne Anmeldung nicht existieren. Er ist der Grund, aus dem Schritt 1 vor
Schritt 2 kommt. Gib Geheimnisse niemals lesend heraus — auch nicht maskiert, wenn sich daraus
etwas ableiten lässt.
Grenzen
- Fass keine Fachlogik an. Kapitalmodell, Risikopfad, Orderstrecke, Abgleich: nicht dein Auftrag. Fehlt dir dort etwas, melde es im Deploymentcenter, statt es zu umgehen.
- Module bleiben unberührt. Sie dürfen weder Avalonia noch einen Webstack referenzieren, und
sie müssen kopflos lauffähig bleiben. Es gibt einen Architekturtest, der das prüft
(
tests/IBKRTrader.Tests/Modules/ModuleBoundaryTests.cs) — wenn er anschlägt, hat er recht. - Keine schreibenden Endpunkte ohne Anmeldung. Auch nicht „vorläufig".
- Zeit und Kultur: Zeitstempel immer UTC persistieren, für Anzeige
AppTimeZone. Jede Formatierung braucht einen ausdrücklichenIFormatProvider. In JSON gehört ISO-8601 in UTC. Achtung:CA1305greift nicht bei interpolierten Zeichenketten — dort musst du selbst aufpassen (FormattableString.Invariant). Das hat uns schon Logdateien mit dem Jahr 1405 eingebracht. - Der Daemon ist die Gegenprobe. Was du baust, muss auf der kopflosen Instanz laufen.
Fertig ist es, wenn
dotnet testgrün und der Build ohne Warnungen ist (das ist die DoD jeder Phase),dotnet publishfür Shell und Daemon durchläuft,- die Oberfläche auf der Testmaschine erreichbar ist und dieselben Zahlen zeigt wie die Shell,
- kein schreibender Endpunkt ohne Anmeldung existiert,
- und für jede Ansicht gilt: Die Shell tut es weiterhin, unverändert.
Arbeitsweise
Der Planungsstand liegt im Deploymentcenter (https://dc.mhdf.de, Projekt ibkrtrader), nicht in
Dateien. Vor der Arbeit dc_roadmap, Item #490 übernehmen, Fortschritt mit dc_comment
festhalten. Findest du unterwegs einen Fehler außerhalb deines Auftrags: melden, nicht nebenbei
beheben. Einzelheiten stehen in CLAUDE.md.
Eine Warnung aus eigener Erfahrung mit dieser Codebasis: Der Daemon schreibt derzeit jede
SQL-Anweisung von EF Core auf stdout (#392). journalctl ist dadurch praktisch unbrauchbar —
nimm die Logdateien unter Logs/[Modul]/.