diff --git a/.agents/rules/grundregeln.md b/.agents/rules/grundregeln.md index 5b59d1c..4dfbb9f 100644 --- a/.agents/rules/grundregeln.md +++ b/.agents/rules/grundregeln.md @@ -36,6 +36,22 @@ Kultur: Jede Zahl-/Datumsformatierung und jedes Parsen braucht einen ausdrückli Tests: eigenes Projekt IBKRTrader.Tests (xUnit + NSubstitute + FluentAssertions), NUR Unit-Tests, alles Externe (DB/IBKR/Scraper) gemockt. DoD jeder Phase: `dotnet test` grün + Build sauber. +API-First (gilt seit dem Umbau auf Avalonia, seit 2026-09-06 ausgeschrieben): + +Der Ausgabeweg ist wählbar – Desktop-UI, Web-UI oder beide gleichzeitig. Daraus folgt genau + eine harte Regel: **Fachlichkeit lebt in einem Core-Dienst, nie in einer Ansicht.** Ein Fenster + und ein REST-Endpunkt sind zwei Zugänge auf denselben Dienst, und beide dürfen nur formatieren. +Prüffrage vor jedem UI-Code: Könnte ein zweiter Ausgabeweg dasselbe tun, ohne diese Zeile zu + kopieren? Lautet die Antwort nein, gehört die Zeile in den Core. +Jede neue Funktion bekommt ihren Core-Dienst UND ihren Endpunkt, im selben Zug wie ihr Fenster. + Nicht nachträglich: Endpunkte, die später in einem Block nachgezogen werden, kosten mehr und + weichen ab. Vorbild ist `Core/Positions/PositionManager.cs` – ein Dienst, drei Zugänge + (Shell-Fenster, Daemon-Schalter, `GET /api/positions`). +Der Daemon ist die Gegenprobe: Lässt sich eine Funktion kopflos nicht bedienen, hängt zu viel + am Fenster. Der Testbetrieb läuft als systemd-Dienst – dort gibt es keines. +Schreibende Endpunkte erst mit Anmeldung. Ohne sie wäre ein Eingriff in die Bücher möglich, den + niemand belegen kann; solange keine steht, bleibt die REST-Schnittstelle lesend. + UI-Grundmodell (Launcher-Prinzip nach Polytrader): LauncherWindow = Basis-Fenster mit einer Schaltfläche je registrierter Ansicht und dem diff --git a/docs/ANLEITUNG-WebUI.md b/docs/ANLEITUNG-WebUI.md new file mode 100644 index 0000000..6a4e176 --- /dev/null +++ b/docs/ANLEITUNG-WebUI.md @@ -0,0 +1,152 @@ +# 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]/`.