API-First als Regel ausgeschrieben, Anleitung fuer die WebUI-Umsetzung
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>
This commit is contained in:
@@ -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
|
||||
<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ü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]/`.
|
||||
Reference in New Issue
Block a user