# 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 ```xml ``` — 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ücklichen `IFormatProvider`. In JSON gehört ISO-8601 in UTC. Achtung: `CA1305` greift **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 test` grün und der Build ohne Warnungen ist (das ist die DoD jeder Phase), - `dotnet publish` fü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]/`.