WebUI Redesign and Component 1: category mapper fixes

This commit is contained in:
Richard
2026-07-19 10:51:07 +02:00
parent c3e66d6cca
commit 7045002ca3
40 changed files with 7037 additions and 971 deletions
+144
View File
@@ -0,0 +1,144 @@
# Plan: Drei-Schichten-Architektur (Core / Lokal / Public)
> Stand: 2026-07-12 · Status: **Architektur bestätigt** (Nutzer-Entscheidung), Implementierung
> schrittweise. Kein Code in diesem Schritt.
> Kontext: Neues WebUI-Design (mit Claude Design vorbereitet) wird nach der Schicht-Trennung
> schrittweise implementiert. Public-Release ist noch fern, aber alles wird dafür vorbereitet.
## 0. Bestätigte Architektur
Drei Schichten mit **genau einer** Integrationsschnittstelle zwischen intern und öffentlich:
**der Datenbank**.
```
┌─────────────────────────── LOKAL (eigener Rechner/LAN) ───────────────────────────┐
│ │
│ Schicht 1: CORE Schicht 2: LOKALE API + WebUI │
│ ───────────────── ─────────────────────────── │
│ Crawler, Worker, PnL-Engine, volle API (Read + Control), │
│ Analyse, Ingest keine Auth (localhost), │
│ │ schreibt serviert WebUI-SPA, │
│ ▼ zum Testen von Analysen & Design │
│ ┌──────────────────┐ liest/schreibt ▲ │
│ │ Analyse-DB │◄────────────────────────┘ │
│ └────────┬─────────┘ │
│ │ (nur SELECT-Grant) │
└────────────┼──────────────────────────────────────────────────────────────────────┘
│ ← EINZIGE Verbindung nach außen: read-only DB-Zugriff
┌────────────┼──────────────────────── EXTERNER WEBSERVER ───────────────────────────┐
│ ▼ │
│ Schicht 3: PUBLIC API + WebUI │
│ ──────────────────────────── │
│ nur Read-Endpunkte (identisch zur lokalen Read-Seite), │
│ Auth + Accounts, öffentliches Rate-Limiting, TLS, │
│ serviert dasselbe WebUI-SPA │
│ │ read-only │ read/write │
│ ▼ ▼ │
│ [Analyse-DB, RO] [User-DB (Accounts), separat] │
└────────────────────────────────────────────────────────────────────────────────────┘
```
**Kernprinzipien (aus der Nutzer-Entscheidung):**
- **Core ist rein lokal** und die einzige Instanz, die in die Analyse-DB **schreibt**.
- **Public spricht ausschließlich mit der Datenbank** — nie mit dem Core-Prozess, nie mit der
lokalen API. Public bekommt einen **read-only** DB-User auf die Analyse-DB und eine
**separate User-DB** für Accounts.
- **Lokale und Public WebUI sind optisch UND funktional gleich** (auf der Analyse-/Ansichtsseite),
damit neue Analysen und Designs lokal getestet werden und 1:1 öffentlich aussehen.
## 1. Wie „identisch" technisch erreicht wird
Damit beide WebUIs garantiert gleich sind, wird die **Read-/Analyse-Seite genau einmal** gebaut
und von beiden Hosts wiederverwendet — kein Duplizieren:
1. **Geteilte Read-API-Library.** `Predictalytics.Api` ist bereits eine Library. Die
Endpoint-Registrierung wird in zwei Gruppen gespalten:
- `MapReadEndpoints()`**die Analyse-/Ansichts-Endpunkte** (Trader-Liste/-Detail/-Profil/
-Positionen, Markets, Dashboard, Search, Watchlist-Ansicht, Traits). Wird von **beiden**
Hosts registriert.
- `MapControlEndpoints()`**schreibende/steuernde** Endpunkte (Jobs-Trigger, `/api/dev`,
Trader-Anlage, Watchlist-Mutation, Priority, AI-Analyse-Trigger). Wird **nur lokal**
registriert.
2. **Ein einziges WebUI-SPA-Artefakt.** Dasselbe Build wird von der lokalen und der öffentlichen
Schicht ausgeliefert. Es ist reiner API-Client (kein Server-Code, keine DB-Kenntnis).
3. **Capability-gesteuerte Admin-Bedienelemente.** Da die Control-Endpunkte öffentlich fehlen,
fragt das SPA beim Start eine `GET /api/capabilities` ab (`{ canControl: bool, authRequired: bool }`)
und blendet Admin-Aktionen (Sync/Analyze/Deep-Resync/Trader-Anlegen) nur ein, wenn vorhanden.
→ Analyse-Ansichten sind überall identisch; nur die lokalen Steuer-Buttons fehlen öffentlich.
> **Wichtiger Audit-Punkt vor dem Teilen:** Manche „Read"-Endpunkte haben heute versteckte
> Nebenwirkungen. Beispiel: `GET /api/traders/{id}/deep-dive` **holt Preis-Historie vom Provider
> und speichert Snapshots** — also ein Schreibzugriff **und** ein externer API-Call. Solche
> Endpunkte sind **nicht** public-tauglich (read-only DB verbietet den Write, und Public darf
> Polymarket nicht anrufen). Vor dem Aufnehmen in `MapReadEndpoints()` jeden Endpunkt auf
> versteckte Writes/Provider-Calls prüfen; die Public-Read-Seite muss **rein aus persistierten
> Daten** bedienbar sein (das `/profile`-Design aus FIXPLAN E ist genau deshalb „persisted-only").
## 2. Was heute wo läuft (Ist → Ziel)
- **Heute:** Core + Lokale API + WebUI sind **ein** Prozess (WinFormsHost/EmbeddedWebServer:
Worker + volle API + `wwwroot`-SPA, localhost, keine Auth, CORS `*`). Public existiert nicht.
- **Ziel Lokal:** bleibt bequem **ein** Prozess (Core-Worker + Lokale API + WebUI zusammen ist
zum Testen praktisch). Wichtig ist nur die **Code-Schichtung** (Read-Library getrennt von
Control/Core), damit Public die Read-Library **ohne** Core/Worker/Control referenzieren kann.
- **Ziel Public:** eigener schlanker Host (neues Projekt `Predictalytics.PublicApi`), der
`MapReadEndpoints()` + Auth registriert, das SPA ausliefert, und **nur** die read-only
Analyse-DB + die User-DB kennt.
## 3. Implementierungsplan (schrittweise)
### Phase 0 — Endpunkte klassifizieren & Read/Control trennen (Fundament, klein)
- [ ] Jeden Endpunkt in `docs/API.md` als **Read** (public-tauglich) oder **Control** (nur lokal)
markieren. Kandidaten Control: alle `POST/PUT/DELETE`, `/api/dev/*`, Job-Trigger.
- [ ] Auf **versteckte Writes/Provider-Calls** in Read-Endpunkten prüfen (Deep-Dive!) und
bereinigen oder als Control einstufen.
- [ ] `MapPredictalyticsEndpoints()` in `MapReadEndpoints()` + `MapControlEndpoints()` splitten
(rein struktureller Refactor, Verhalten unverändert; bestehende Tests bleiben grün).
- [ ] `GET /api/capabilities` einführen.
### Phase 1 — WebUI als sauberes, gemeinsames SPA (parallel zum neuen Design)
- [ ] Neues Design als **ein statisches Artefakt** bauen, reiner API-Client, `API_BASE`
konfigurierbar (heute `''` = gleiche Origin bleibt lokal gültig).
- [ ] Admin-Aktionen an `capabilities.canControl` koppeln.
- [ ] Trait-Chips/-Filter im Design vorsehen — **erscheinen erst, wenn Backend-D2 steht**
(kein UI-Bug, das Backend liefert Traits noch nicht).
### Phase 2 — Lokale Grenze härten (klein, jetzt schon sinnvoll)
- [ ] CORS von `AllowAnyOrigin` auf konfigurierte Origin-Whitelist umstellen.
- [ ] Bind-Adresse explizit (localhost/LAN), nie `0.0.0.0` ohne Firewall; Konfig-Kommentar
„NIEMALS ins Internet — das ist die interne Schicht".
### Phase 3 — DB für Public vorbereiten (mittel, kann früh passieren)
- [ ] **Read-only DB-User** auf der Analyse-DB anlegen (nur `SELECT`). Der Nutzer führt das SQL
selbst aus (Grant-Statements liefern wir).
- [ ] `AppDbContext` public-seitig read-only konfigurieren (read-only Connection; keine
Migrations, kein SaveChanges). Sicherstellen, dass die Read-Endpunkte ohne Writes auskommen.
- [ ] **Schema-Kopplung dokumentieren:** Weil Public direkt auf der Analyse-DB liest, ist das
DB-Schema jetzt eine **Vertragsfläche**. Schema-Änderungen im Core müssen die Public-Read-
Views berücksichtigen (Views/stabile Spalten als Puffer erwägen, damit interne
Refactorings die öffentliche Sicht nicht brechen).
### Phase 4 — Public-Host bauen (groß, wenn Release näher rückt)
- [ ] Neues Projekt `Predictalytics.PublicApi` (eigener Host, TLS, öffentliche Bind-Adresse).
- [ ] `MapReadEndpoints()` + **Auth/Accounts** (separate **User-DB**; Passwörter/Sessions nach
Stand der Technik — Secret-Handling lebt **nur hier**, nie im Core).
- [ ] Öffentliches **Rate-Limiting & Quotas pro Account** (getrennt vom internen Polymarket-Limiter).
- [ ] Mandanten-Sicht: Kunde sieht seine Watchlist/kopierten Master, nicht das gesamte Universum.
- [ ] Dasselbe WebUI-SPA ausliefern.
## 4. Explizite Nicht-Ziele / Fallen
- ❌ Public bekommt **keinen** Zugriff auf Core-Prozess, lokale API oder Control-Endpunkte.
- ❌ Public bekommt **keinen** Schreibzugriff auf die Analyse-DB (nur `SELECT`).
-**Kein** Nachrüsten von Kunden-Auth auf die lokale Vollschicht — Auth lebt nur im Public-Host.
- ❌ Read-Endpunkte machen **keine** Provider-Calls und **keine** Writes (sonst public-untauglich).
- ❌ WebUI kennt die DB nicht — immer nur über die API.
## 5. Reihenfolge / Aufwand
1. **Phase 0** (klein): Read/Control-Split + Capabilities + Deep-Dive-Audit. **Jetzt** — es ist
die Voraussetzung dafür, dass das neue Design von Anfang an sauber sitzt.
2. **Phase 1+2** (klein, parallel): neues SPA als gemeinsames Artefakt, lokale Grenze härten.
3. **Phase 3** (mittel): read-only DB-User + Schema-als-Vertrag dokumentieren.
4. **Phase 4** (groß, später): Public-Host mit Auth/User-DB.
Phase 02 begleiten das neue Design sofort. Phase 3 kann jederzeit vorgezogen werden (billig).
Phase 4 erst zum Release — aber durch 03 ist dann nichts mehr umzubauen, nur zu ergänzen.
+137
View File
@@ -0,0 +1,137 @@
# Plan: Daten-Ingest skalieren (Rate-Limits, Egress-Kanäle, Blockchain)
> Stand: 2026-07-12 · Status: **Richtung bestätigt** (Nutzer: beide Egress-Wege umschaltbar
> umsetzen, sofern Aufwand vertretbar). Kein Code in diesem Schritt.
> Problem: Polymarket-Rate-Limits bremsen den Datenimport teils extrem aus.
## 0. Ist-Zustand (verifiziert)
- `RateLimiterService` = **globaler Singleton-Token-Bucket**, feste Delays je Endpoint-Gruppe
(Gamma ≈28/s, Data ≈18/s, Clob ≈66/s).
- `PolymarketApiClient` = Singleton mit 3 benannten HttpClients, **ohne** Proxy/IP-Konfig →
ein einziger Ausgangs-IP, ein globales Budget. Das ist die echte Bremse.
**Zwei Hebel:** Nachfrage senken (Abschnitt 1, größter Gewinn) und Angebot erhöhen (Abschnitt 2,
Egress-Kanäle). Der Nutzer möchte in Abschnitt 2 **beide Egress-Arten (eigene IPs UND Proxys)
umschaltbar** — und das ist billig, weil beide **derselbe HttpClient-Seam** sind (Abschnitt 2).
---
## 1. Nachfrage senken — größter Hebel (zuerst)
### 1a. Historische Massendaten aus Blockchain/Subgraph statt REST
Polymarket-Trades sind On-Chain-Events auf Polygon. Für **Backfill/Deep-Resync** (größter
REST-Verbraucher) ist `/activity` die falsche Quelle.
- [ ] The-Graph-/Goldsky-Subgraph für Polymarket evaluieren: ein GraphQL-Call → tausende Fills
eines Wallets, statt seitenweiser rate-limitierter REST-Abrufe.
- [ ] Neue `IHistoricalTradeSource` neben dem REST-Provider; DeepResync (FIXPLAN A6) zieht Bulk
hierüber. **Effekt:** nimmt die teuerste Last komplett von der REST-API.
### 1b. Markt-Stammdaten aggressiv cachen
- [ ] Metadaten (Frage, Kategorie, Outcomes, `ConditionId``TokenId`) einmal ziehen, lange cachen;
nur Volume/Liquidity/Auflösung periodisch aktualisieren. Geschlossene Märkte nie voll re-syncen.
### 1c. Ingest-Tiering (FIXPLAN D3)
- [ ] Ultra-HF-Trader im `SnapshotOnly`-Modus erzeugen null Trade-Calls (PnL aus `/positions` +
Leaderboard, wöchentliche Biopsie). Entfernt genau die Wallets, die das Budget auffressen.
### 1d. Redundanz vermeiden
- [ ] Worker-übergreifender Kurzzeit-Cache „gerade geholt", damit nicht mehrere Worker denselben
Markt/Trader kurz hintereinander abrufen.
---
## 2. Angebot erhöhen: umschaltbare **Egress-Kanäle** (eigene IPs UND Proxys — eine Mechanik)
**Die Schlüssel-Einsicht:** Ob eine Anfrage über eine **eigene Quell-IP** oder über einen
**Proxy** rausgeht, ist im HttpClient nur eine andere Konfiguration desselben `SocketsHttpHandler`.
Deshalb wird **nicht** zweimal gebaut, sondern **ein** Konzept: der **Egress-Kanal**. Umschalten =
Konfiguration, nicht Code. Damit bekommt der Nutzer „beides, umschaltbar" zu geringen Kosten.
### 2a. Abstraktion `EgressChannel`
Ein Kanal ist genau eine Ausgangsroute, per Config als einer von zwei Typen definiert:
```jsonc
"Egress": {
"Channels": [
{ "id": "ip-a", "type": "SourceIp", "value": "203.0.113.10" }, // eigene IP
{ "id": "ip-b", "type": "SourceIp", "value": "203.0.113.11" },
{ "id": "prox-1", "type": "Proxy", "value": "http://user:pass@proxy.example:8080" }
]
}
```
- [ ] Pro Kanal **ein** `SocketsHttpHandler`:
- `SourceIp``ConnectCallback`, Socket vor Connect an die lokale IP binden
(`socket.Bind(new IPEndPoint(ip, 0))`).
- `Proxy``handler.Proxy = new WebProxy(url); handler.UseProxy = true;`.
- [ ] `IEgressPool` verteilt Requests round-robin/least-loaded über die aktiven Kanäle.
Leere/❑ Kanalliste = heutiges Verhalten (ein Default-Ausgang).
- [ ] Umschalten „nur eigene IPs" ↔ „nur Proxys" ↔ „Mix" = Config ändern, kein Deploy-Umbau.
### 2b. Rate-Limiter **pro Kanal** (Generalisierung des globalen Limiters)
- [ ] Limiter-Schlüssel wird `{platform}-{endpointGroup}-{channelId}`. Jeder Kanal hält sein
eigenes Budget → N Kanäle ≈ N× Durchsatz, jeder Kanal bleibt unter dem Per-Route-Limit.
- [ ] 429 sperrt **nur den betroffenen Kanal** kurz, nicht alle.
### 2c. Betrieb
- [ ] Bei eigenen IPs prüfen: sind es echte getrennte Egress-IPs (multi-homed), nicht NAT hinter einer.
- [ ] Health/Statistik je Kanal (Erfolg, 429-Rate, Latenz), damit tote Proxys automatisch pausiert werden.
**Aufwand:** moderat und **einmalig** — durch die gemeinsame Abstraktion kostet „beides
umschaltbar" kaum mehr als „nur IPs".
---
## 3. Proxys & Header-Rotation — Einordnung (korrigiert)
**Klarstellung (Korrektur einer früheren Fassung):** Predictalytics betreibt **kein Trading und
keine Wallet** — es ist reine Analysesoftware, die ausschließlich **öffentliche** Marktdaten liest.
Das frühere „Ban gefährdet die Trading-Wallet"-Argument gehört zu **PolyTrader** (getrenntes
Projekt) und trifft hier **nicht** zu. Damit ist die Proxy-Wahl eine reine Engineering-Entscheidung
des Nutzers — freie Proxys eingeschlossen.
Was real bleibt (ehrliche Hinweise, keine Blocker — Entscheidung liegt beim Nutzer):
- **ToS-Grauzone:** Rate-Limit-Umgehung per Routen-/Header-Rotation widerspricht vermutlich
Polymarkets Nutzungsbedingungen. Realistische Konsequenz **hier**: einzelne IPs/Proxys werden
geblockt und müssen ersetzt werden — mehr nicht (kein Kapital, keine Wallet betroffen).
- **Datenintegrität (der eigentlich relevante Punkt):** Ein kaputter/bösartiger (v. a. gratis)
Proxy kann Antworten **verfälschen** → korrupte Analyse („garbage in, garbage out"). Da die
gesamte Auswertung darauf aufbaut, lohnt sich eine **Plausibilitätsprüfung** der Antworten
(Feldtypen, Wertebereiche, z. B. Preise ∈ [0,1]) und — wo Genauigkeit zählt — das Bevorzugen
kontrollierter Proxys/eigener IPs. Rein informativ, kein Zwang.
- **DB-Verbindung NIE über Proxy** (ausdrücklicher Nutzer-Wille): Die MySQL-Verbindung läuft
**immer direkt**. Der Egress-Pool gilt **ausschließlich** für ausgehende Polymarket-HTTP-Calls,
niemals für die DB.
### 3a. Header-Rotation (separat aktivierbar, standardmäßig AUS)
Auf Wunsch integriert, bewusst opt-in:
- [ ] Config `Egress.HeaderRotation.Enabled`**Default `false`**. Bei `false` wird ein einziger,
konsistenter Standard-Header-Satz gesendet (heutiges Verhalten).
- [ ] Rotiert **kohärente Header-SETS**, nicht nur den User-Agent isoliert: je Eintrag ein
zusammenpassendes Bündel (`User-Agent` + `Accept` + `Accept-Language` + `Sec-CH-UA`…), damit
die Kombination realistisch bleibt — ein moderner UA mit widersprüchlichen Accept-Headern
fällt eher auf als gar keine Rotation. Set-Pool aus Config ladbar.
- [ ] Auswahl pro Kanal **oder** pro Request (konfigurierbar).
- [ ] Greift nur für Polymarket-Read-Calls; unabhängig vom Kanaltyp (IP oder Proxy) nutzbar.
---
## 4. Reihenfolge
1. **1b + 1d** (Caching/Dedup) — sofort, klein, spürbar.
2. **2a2c** (Egress-Kanäle + Per-Kanal-Limiter) — moderat, liefert „eigene IPs UND Proxys umschaltbar".
3. **1c** (Tiering) — hängt an FIXPLAN D3.
4. **1a** (Blockchain/Subgraph-Bulk) — größter struktureller Hebel; eigener Rechercheschritt
(welcher Subgraph deckt Fills sauber ab?), dann als `IHistoricalTradeSource`.
## 5. Tests / Abnahme
- [ ] Per-Kanal-Limiter: unabhängige Budgets (kein globales Blocken); 429 sperrt nur einen Kanal.
- [ ] Egress-Binding: Smoke gegen einen Dienst, der die Quell-IP zurückgibt → Round-Robin nutzt
wirklich verschiedene IPs; Proxy-Kanal geht über den Proxy.
- [ ] Kanal-Health: toter Proxy wird automatisch pausiert, Pool weicht aus.
- [ ] Blockchain-Quelle: Stichproben-Abgleich Bulk-Historie ↔ REST-`/activity` eines Wallets,
bevor sie produktiv wird.
- [ ] **Header-Rotation:** bei `Enabled=false` wird genau ein konsistenter Standard-Satz gesendet
(kein Rotieren); bei `Enabled=true` stammt jeder gesendete Header-Satz **unverändert** aus dem
Pool (kohärent, keine zusammengewürfelten Felder).
- [ ] **DB-Guard:** die MySQL-Verbindung nutzt nie den Egress-Pool/Proxy (Regressionsschutz).
- [ ] **Response-Plausibilität:** grob unplausible Provider-Antworten (z. B. Preis außerhalb
[0,1]) werden erkannt und verworfen statt in die DB zu wandern.