Eine Roadmap statt neun Plandokumente; alte Plaene ins Archiv

Die offenen Punkte lagen ueber neun Dokumente verstreut, teils widersprechend,
teils mit Punkten, die laengst umgesetzt waren. ROADMAP.md fuehrt sie zusammen:
sechs Stufen, jeder Punkt gegen den Code geprueft.

Markierung ueber eine Legende, damit Konzepte nicht mit Aufgaben verwechselt
werden: dringend, eingeplant, Backlog, Konzept (durchdacht, aber bewusst nicht
eingeplant), liegt beim Nutzer, verworfen.

Stufen: 0 Sofort (OpenRouter-Key) - 1 Aufraeumen abschliessen (Merge nach main,
Nullable-Warnungen, Startup-Backfill) - 2 Portierung abschliessen (Linux-Erstlauf
und Verifikation, dann Deployment/CI) - 3 Analytik schaerfen (Strategie-
Klassifikation, Backtest-Harness, Track-Record im Score, Ranking) - 4 Daten-
haushalt (SQL des Nutzers) - 5 Ingest skalieren - 6 Monetarisierung vorbereiten.

Ein Anhang haelt fest, was geprueft und bewusst NICHT auf die Roadmap kam, damit
es nicht versehentlich wieder als Aufgabe auftaucht (Azuro/Limitless, die
Marketing-Seiten, Kaltarchiv, EF Core 10).

Archiv: FIXPLAN-DONE, FIXPLAN-G-Speicher, FIXPLAN-TODO, FIXPLAN-UI-Ranglisten,
UMSETZUNGSPLAN sowie ANALYSE-Linux-Portierung, PLAN-Linux-Portierung,
PLAN-Architektur-WebUI-Backend und PLAN-DatenIngest-Skalierung liegen jetzt
unter docs/archiv/ mit einer README, die jedes Dokument einordnet. Sie bleiben
als Begruendungs- und Detailquelle - die Roadmap nennt jeden Punkt knapp, die
Herleitung steht dort.

Dabei aufgefallen: die beiden nie gepflegten Plaene (Architektur, DatenIngest)
zeigten 40 offene Punkte, von denen die Haelfte umgesetzt war - Read/Control-
Split, /api/capabilities, CORS-Whitelist, Egress-Kanaele mit Cooldown und
Per-Kanal-Limiter, Ingest-Tiering. Nachgeprueft, abgehakt und mit Statusblock
eingeordnet, sonst waere das Archiv selbst eine Fehlerquelle.

Was dabei nur teilweise umgesetzt war, ist als Roadmap 5.3 aufgenommen
(Health-Statistik je Kanal, multi-homed pruefen, DB-Guard, Plausibilitaets-
pruefung), ebenso der nie durchgefuehrte Audit auf versteckte Writes in
Read-Endpunkten (6.1). Header-Rotation ist als verworfen markiert statt offen
zu bleiben: Verschleierung gegenueber Polymarket riskiert genau den Zugang, auf
dem das Projekt aufsetzt.

STATUS.md beschreibt jetzt nur noch den Ist-Stand und verweist fuer die offenen
Punkte auf die Roadmap, damit nichts doppelt gepflegt wird. CLAUDE.md nennt
beide Einstiege.

Build gruen, 126 Tests gruen, keine toten Links.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-23 18:46:00 +02:00
co-authored by Claude Opus 5
parent 1f230fe12c
commit 6975720dc6
14 changed files with 429 additions and 73 deletions
+766
View File
@@ -0,0 +1,766 @@
# Implementierungsplan: Predictalytics auf Windows + Linux
**Stand:** 2026-08-06 · Basis: [ANALYSE-Linux-Portierung.md](ANALYSE-Linux-Portierung.md)
**Ziel:** Ein Code-Stand, der unter Windows und Linux identisch läuft — Bedienhülle in
Avalonia, gesamte Solution auf .NET 10.
---
## 0. Drei Entscheidungen vorab — **alle getroffen (2026-08-06)**
| # | Entscheidung | Ergebnis |
|---|---|---|
| 0.1 | EF Core | **Weg A** — TFM `net10.0`, EF Core `9.0.18` + Pomelo `9.0.0` |
| 0.2 | UI-Framework | **Avalonia 12.1.1**, kein LiveCharts2 |
| 0.3 | Linux-Ziel | **Beides** — Avalonia-GUI *und* Headless-Modus (AP 3.4 verbindlich) |
### 0.1 ✅ EF Core: Version 9 behalten, nicht auf 10 gehen — **entschieden: Weg A**
Das ist der einzige Punkt, an dem „alles auf .NET 10" nicht sauber aufgeht.
**Befund (verifiziert über nuget.org am 2026-08-06):**
```
Pomelo.EntityFrameworkCore.MySql → neueste stabile Version: 9.0.0 (2025-08-17)
nuspec: targetFramework net8.0
Microsoft.EntityFrameworkCore.Relational [9.0.0, 9.0.999] ← harte Obergrenze
```
**Es gibt keinen Pomelo-Provider für EF Core 10.** Die Versionsspanne ist nach oben
geschlossen; ein Upgrade auf EF Core 10 ist mit Pomelo technisch unmöglich.
| Weg | Konsequenz |
|---|---|
| **A — empfohlen: TFM `net10.0`, EF Core `9.0.18`, Pomelo `9.0.0`** | Alle Projekte laufen auf .NET 10. EF-Core-9-Pakete (net8.0) laufen problemlos auf der .NET-10-Runtime. **Kein Provider-Wechsel, kein DB-Risiko.** Nur +1 EF-Major statt +2. |
| **B — EF Core `10.0.10` + `MySql.EntityFrameworkCore 10.0.9` (Oracle)** | Erzwingt den Wechsel des Datenbankproviders. Andere SQL-Generierung, andere Migrations-Historie, abweichendes Verhalten bei Typ-Mapping und Bulk-Operationen — **gegen die Produktiv-DB.** Realistisch +35 PT plus Regressionsrisiko in 94 Infrastructure-Dateien und 30+ Migrationen. |
**→ Gewählt: Weg A.** „Auf .NET 10 gleichbleiben" ist das eigentliche Ziel und wird
vollständig erreicht — das Zielframework aller Projekte ist `net10.0`. Dass die
EF-Core-*Bibliothek* bei 9.0.18 bleibt, ist ein Implementierungsdetail ohne Auswirkung auf
Runtime, Sprachfeatures oder Toolchain. Sobald Pomelo einen EF-Core-10-Provider
veröffentlicht, ist der Nachzug ein Einzeiler.
Weg B entfällt; ein Providerwechsel gegen die Produktiv-DB findet nicht statt.
### 0.2 Avalonia 12.1.1 — entschieden ✅
```
Avalonia 12.1.1 (2026-07-29), lib-TFMs: net8.0, net10.0 ← natives .NET 10
Avalonia.Desktop 12.1.1 · Avalonia.Themes.Fluent 12.1.1 · Avalonia.Controls.DataGrid 12.1.2
```
Eine Einschränkung: **`Avalonia.Diagnostics` liegt noch bei 11.3.18**, es gibt keine
12.x-Version. Das ist das DevTools-Inspektorfenster (F12) — reines Entwicklerwerkzeug, für
die Laufzeit irrelevant. Falls beim Layouten vermisst: temporär eine 11.3.18-Referenz nur
in der Debug-Konfiguration, oder ohne auskommen.
**LiveCharts2 wird nicht benötigt.** Die Bedienhülle enthält kein einziges Diagramm; alle
Charts liegen in der Web-UI (Chart.js). Keine Chart-Bibliothek im Shell-Projekt.
### 0.3 ✅ Wo läuft die Linux-Instanz? — **entschieden: beides**
Eine Avalonia-GUI braucht unter Linux eine Desktop-Umgebung: X11 (bzw. Wayland über XWayland),
dazu `libx11`, `libice`, `libsm`, `libfontconfig1` und installierte Fonts. Auf einem
**headless Server ohne Desktop startet sie nicht.**
**→ Gewählt: beides in einem Binary.** Die Avalonia-Shell für Desktop-Betrieb (Windows und
Linux), zusätzlich ein Schalter `--headless` für Server ohne Desktop-Session.
Damit ist **AP 3.4 verbindlicher Bestandteil**, nicht optional. Nach der
Hosting-Extraktion (Phase 2) kostet er ca. 0,5 PT, weil der gesamte Kern dann bereits
GUI-frei ist. Konsequenzen, die dadurch fix werden:
* Der Lizenzschlüssel muss **auch ohne Dialog** beziehbar sein (Konfiguration /
Umgebungsvariable) — koppelt an die laufende HardwareID-Arbeit.
* Das Deployment liefert zwei Betriebsarten: Desktop-Start und systemd/Container.
* Phase 6 verifiziert beide Modi.
---
## 1. Zielarchitektur
Der entscheidende Umbau ist nicht die GUI, sondern das **Herausziehen eines
plattformneutralen Hosting-Kerns**. Heute liegen ~870 portable Zeilen (EmbeddedWebServer,
Watchdog, Serilog-Setup, Settings) im windows-gebundenen Projekt fest. Danach ist die
GUI nur noch eine dünne Schale darüber — und der Headless-Modus fällt fast von selbst ab.
```
src/
Predictalytics.Domain net10.0 unverändert
Predictalytics.Application net10.0 unverändert
Predictalytics.Infrastructure net10.0 unverändert
Predictalytics.Api net10.0 unverändert
Predictalytics.Worker net10.0 unverändert
Predictalytics.Hosting NEU net10.0 ← plattformneutraler Kern
Predictalytics.Shell NEU net10.0 ← Avalonia-Bedienhülle (Win + Linux)
Predictalytics.Application.Tests net10.0 net10.0 statt bisher net10.0 (bleibt)
Predictalytics.WinFormsHost net10.0-windows ← Übergang, s. 6.2
```
**`Predictalytics.Hosting`** (neu, GUI-frei) nimmt auf:
| Herkunft | Ziel |
|---|---|
| `WinFormsHost/Services/EmbeddedWebServer.cs` (365 Z.) | `Hosting/PredictalyticsHost.cs` — unverändert bis auf Binding + wwwroot |
| `WinFormsHost/AppSettings.cs` (137 Z.) | `Hosting/PredictalyticsOptions.cs` — ohne `[Category]`/`[Editor]`-Attribute |
| `WinFormsHost/Services/WatchdogHeartbeatService.cs` (141 Z.) | 1:1, ist bereits portabel |
| `WinFormsHost/Program.cs:33-148` (Serilog-Setup) | `Hosting/LoggingSetup.cs` — 1:1, ist bereits portabel |
| `WinFormsHost/Services/LicenseGuard.cs` (102 Z.) | `Hosting/LicenseGuard.cs` — ohne WinForms-Timer/Dialog |
| `Infrastructure/Logging/RichTextBoxSink.cs` | umbenennen → `DelegateSink.cs` (ist schon portabel) |
**`Predictalytics.Shell`** (neu, Avalonia) enthält nur noch View + ViewModel.
---
## 2. Phase 0 — Vorarbeiten ✅ **erledigt (2026-08-06)**
| # | Aufgabe | Ergebnis |
|---|---|---|
| 0.1 | `InvariantCulture` ergänzen | ✅ `LimitlessProvider.cs:270` (Volume) |
| 0.1b | **Nachtrag:** 13 × lokalzeit-verzerrendes `DateTime.TryParse` | ✅ neuer Helfer `Infrastructure/Helpers/DateParsing.cs`, alle Fundstellen umgestellt |
| 0.2 | Eingecheckte Logdatei entfernen | ✅ nicht nötig — Datei ist **nicht** getrackt, `.gitignore:14 logs/` greift bereits |
| 0.3 | `j:\`-Absolutpfad-Fallback entfernen | ✅ `EmbeddedWebServer.cs` |
| 0.4 | Branch anlegen | ✅ `feature/linux-net10` |
**Abnahme erreicht:** `dotnet build` → 0 Fehler, 10 Warnungen (alle vorbestehend).
`dotnet test`**100 bestanden, 0 Fehler, 1 übersprungen**.
### Nachtrag 0.1b — Umfang und offener Punkt
`DateTime.TryParse("2026-08-06T12:00:00Z")` ohne `DateTimeStyles` liefert `Kind=Local` und
rechnet in Lokalzeit um. Betroffen waren in beiden Providern `StartDate`, `EndDate`,
`CreatedAt`, `ClosedAt`, `ExecutedAt`, `ExpirationDate` — während der Fallback derselben
Zuweisungen (`DateTime.UtcNow`) korrektes UTC schrieb. Auf der Berliner Maschine also
12 h Versatz, auf einem Linux-Server mit `TZ=UTC` hingegen nicht: derselbe Code hätte
plattformabhängig unterschiedliche Werte erzeugt.
**Altdaten-Backfill: entfällt (Entscheidung 2026-08-06).** Der Fix wirkt nur für neue
Schreibvorgänge; bereits gespeicherte Zeilen tragen den 12-h-Versatz weiter. Bewusst
akzeptiert — die Daten verlieren mit steigendem Alter ohnehin an Relevanz, und die
betroffenen Felder werden bei jedem Markt-Sync überschrieben.
**Konsequenz für Phase 6:** Beim Vergleich Linux-Lauf ↔ Windows-Referenz (Prüfung 8.3)
dürfen historische Zeilen nicht als Referenz dienen — nur frisch geschriebene Datensätze
sind aussagekräftig.
---
## 3. Phase 1 — .NET 10 ✅ **erledigt (2026-08-06)**
**Abnahme erreicht:** `dotnet build` → 0 Fehler, 8 Warnungen (alle vorbestehend, 2 weniger
als vorher). `dotnet test`**100 bestanden, 0 Fehler, 1 übersprungen**, Testhost
`net10.0`. Alle Projekte laufen auf `net10.0`, WinFormsHost auf `net10.0-windows`.
Ergebnis gegenüber der Planung:
| Erwartung | Tatsächlich |
|---|---|
| Swashbuckle 6.9 → 10.x „wahrscheinlichster Reibungspunkt", ggf. Wechsel auf `AddOpenApi()` | **Keine Bruchstelle.** Der genutzte API-Ausschnitt (`AddSwaggerGen`/`SwaggerDoc`/`UseSwagger`/`UseSwaggerUI`) ist stabil geblieben. Kein Umbau nötig. |
| EF Core 8 → 9 „überschaubar" | Bestätigt, keine Anpassung nötig |
| SQLite-Sicherheitswarnung „löst sich mit dem EF-Bump von selbst" | **Falsch** — EF Core 9.0.18 zieht SQLitePCLRaw 2.1.10, weiterhin von GHSA-2m69-gcr7-jv3q betroffen. Behoben über einen expliziten transitiven Pin auf 2.1.12. |
Bewusst **vor** dem Avalonia-Umbau erledigt: eine Baustelle nach der anderen, und das
Shell-Projekt entsteht direkt auf dem Zielframework.
### AP 1.1 — Zentrale Frameworkverwaltung ✅
`Directory.Build.props` setzt `TargetFramework=net10.0` zentral; `<TargetFramework>` aus
allen `.csproj` entfernt (Ausnahme: WinFormsHost → `net10.0-windows`). Zusätzlich zentral:
`SatelliteResourceLanguages=en`, das bisher in vier Projekten einzeln stand.
`Directory.Packages.props` mit `ManagePackageVersionsCentrally` eingeführt — 21 Pakete,
Versionen aus allen `.csproj` entfernt. Zusätzlich
`CentralPackageTransitivePinningEnabled`, um Sicherheits-Pins auf transitive
Abhängigkeiten setzen zu können.
**Nicht angefasst:** `Spike/Spike.csproj` — leeres Restprojekt ohne Quelldateien, nicht in
der Solution, baut auch im Ist-Zustand nicht (`CS5001: keine Main-Methode`). Kandidat zum
Löschen, aber nicht Teil dieser Portierung.
### AP 1.2 — Paketmatrix
| Paket | heute | Ziel | Anmerkung |
|---|---|---|---|
| `Microsoft.EntityFrameworkCore(.Relational/.Design)` | 8.0.11 | **9.0.18** | Obergrenze durch Pomelo |
| `Pomelo.EntityFrameworkCore.MySql` | 8.0.2 | **9.0.0** | zieht MySqlConnector 2.4.0 |
| `Microsoft.EntityFrameworkCore.InMemory/.Sqlite` (Tests) | 8.0.11 | **9.0.18** | muss zu EF Core passen |
| `Swashbuckle.AspNetCore` | 6.9.0 | **10.2.3** | ⚠️ 6.x ist nicht ASP.NET-Core-10-tauglich |
| `Serilog` | 4.2.0 | 4.4.0 | |
| `Serilog.AspNetCore` | 8.0.3 | 10.0.0 | |
| `Serilog.Sinks.File` | 6.0.0 | 7.0.0 | |
| `Serilog.Extensions.Logging` / `.Sinks.Console` / `.Formatting.Compact` | 8.0.0 / 6.0.0 / 3.0.0 | jeweils aktuell | |
| `Microsoft.Extensions.Http` / `.Hosting.Abstractions` / `.Logging.Abstractions` | 8.0.x | 10.0.x | |
| `Microsoft.NET.Test.Sdk` / `xunit` / `Moq` / `coverlet` | aktuell | prüfen | |
Tatsächlich aufgelöste Versionen (verifiziert über `dotnet list package`): EF Core 9.0.18,
Pomelo 9.0.0, Serilog 4.4.0 / Extensions.Logging 10.0.0 / Sinks.File 7.0.0 /
Sinks.Console 6.1.1, Swashbuckle 10.2.3, Test-SDK 18.8.1, coverlet 10.0.1.
Sicherheits-Pin: SQLitePCLRaw 2.1.12 (statt transitiv 2.1.10).
### AP 1.3 — Erwartete Bruchstellen *(Rückblick: keine eingetreten)*
* **Swashbuckle 6.9 → 10.x** ist der wahrscheinlichste Reibungspunkt: geänderte API-Oberfläche,
und ASP.NET Core 10 bringt mit `Microsoft.AspNetCore.OpenApi` eine eigene
OpenAPI-Generierung mit. Betrifft `Api/ApiConfiguration.cs:19-20` und
`WinFormsHost/Services/EmbeddedWebServer.cs:143-144,154-155`.
*Alternative:* auf das eingebaute `AddOpenApi()`/`MapOpenApi()` umsteigen und Swashbuckle
ganz ablösen — sauberer, aber die Swagger-UI müsste ersetzt werden (z. B. Scalar).
* **EF Core 8 → 9:** überschaubar. Zu prüfen sind Migrationsverhalten und die
`ExecuteSqlRawAsync`-Aufrufe in `EmbeddedWebServer.cs:56-100`.
* `Predictalytics.Application.Tests` steht bereits auf `net10.0` — hier wird die
Inkonsistenz gerade aufgelöst statt vergrößert.
**Abnahme:** `dotnet build` + `dotnet test` grün; WinFormsHost startet, Worker laufen,
Web-UI erreichbar; **Migrationen gegen die Dev-DB (`bergisnu_db0`) verifiziert, nicht gegen Prod.**
---
## 4. Phase 2 — Hosting-Kern extrahieren ✅ **erledigt (2026-08-06)**
**Abnahme erreicht:** `dotnet build` → 0 Fehler. `dotnet test` → **100 bestanden, 0 Fehler,
1 übersprungen**. `Predictalytics.Hosting` baut ohne Windows-Bezug; `WinFormsHost` enthält
nur noch UI-Code (`MainForm`, `LicenseDialog`, `TerminalHelper`, `Program`) und referenziert
Api/Worker/Infrastructure/LicenseLabrador nur noch transitiv über Hosting.
### ⚠️ Betrieblicher Hinweis: `settings.json` ging verloren
Beim Aufräumen der Build-Ausgabe in Phase 1 wurde `bin/` gelöscht — und dort lag die zur
Laufzeit erzeugte `settings.json` mit DB-Zugangsdaten und Watchdog-API-Key. Die Datei war
nie versioniert (`.gitignore:11`) und ist nicht wiederherstellbar.
**Beim nächsten Start neu einzutragen:** DB Server/Database/User/Password, Watchdog API Key,
ggf. Egress-Kanäle.
Der Fehler kann sich nicht wiederholen: durch AP 2.3 liegen die Einstellungen jetzt unter
`%APPDATA%\Predictalytics\settings.json` bzw. `~/.config/Predictalytics/settings.json`
außerhalb des Build-Verzeichnisses. `dotnet clean` oder ein Löschen von `bin/` sind damit
folgenlos.
### Abweichungen von der Planung
| Geplant | Tatsächlich |
|---|---|
| Hosting als `Microsoft.NET.Sdk.Web` mit `OutputType=Library` (analog Api) | **`Microsoft.NET.Sdk` + `<FrameworkReference Include="Microsoft.AspNetCore.App" />`.** Der Web-SDK globbt `wwwroot/**` automatisch als Static Web Asset und kollidierte mit dem Content-Eintrag aus AP 2.5 (`DiscoverPrecompressedAssets`: doppelter Schlüssel). Gilt auch für den Host: WinFormsHost ist jetzt ebenfalls normaler SDK. Für die Avalonia-Shell gleich so anlegen. |
| `Process.Start("explorer.exe", …)` erst in AP 3.3 ersetzen | Vorgezogen — `MainForm` wurde ohnehin umgeschrieben. Jetzt `ProcessStartInfo { UseShellExecute = true }`, funktioniert unter Windows und Linux. |
| `LicenseGuard` komplett nach Hosting | Aufgeteilt: der plattformneutrale Teil liegt in `Hosting/LicenseGuard.cs`, der interaktive Dialogaufruf in `WinFormsHost/Services/LicenseGate.cs`. Genau die Naht, an der Phase 3 den Avalonia-Dialog bzw. den Headless-Pfad einhängt. |
| `[Editor]`-Attribut für den mehrzeiligen Egress-Editor erhalten | **Entfallen.** Es verwies auf die .NET-Framework-Assembly `System.Design`, die es in .NET (Core) nicht gibt — der Editor dürfte schon vorher nicht gegriffen haben. Die Avalonia-Settings-View bekommt ein echtes mehrzeiliges Textfeld. Alle übrigen `System.ComponentModel`-Attribute sind plattformneutral und bleiben, das PropertyGrid behält Gruppen und Beschreibungen. |
### Zusätzlich mitgenommen
* `PredictalyticsOptions.WebserverHost` — Bind-Adresse konfigurierbar (Vorbereitung AP 4.1),
Default `localhost`, also verhaltensgleich.
* `PredictalyticsOptions.DbSslMode` — Default bewusst `None`, damit sich das Verbindungs-
verhalten in einem reinen Refactoring nicht ändert. Umstellung auf `Preferred` ist
jetzt eine reine Konfigurationsfrage.
* `PredictalyticsHost.GetDatabaseSizeMbAsync()` — die DB-Abfrage lag vorher direkt im
`MainForm`.
* `RichTextBoxSink``DelegateSink` umbenannt (war nie WinForms-abhängig).
**Reiner Verschiebe- und Entkopplungsvorgang, keine neue Fachlogik.**
### AP 2.1 — Projekt `Predictalytics.Hosting` anlegen
`net10.0`, Referenzen auf `Api`, `Worker`, `Infrastructure`, `LicenseLabrador.Client`.
### AP 2.2 — Verschieben (Tabelle in Abschnitt 1)
`EmbeddedWebServer``PredictalyticsHost`, Watchdog, Serilog-Setup, LicenseGuard.
`RichTextBoxSink``DelegateSink` umbenennen (der Sink ist ein reiner
`Action<string, LogEventLevel>`-Delegat und war nie WinForms-abhängig).
### AP 2.3 — Konfiguration entkoppeln
`AppSettings``PredictalyticsOptions` ohne `System.ComponentModel`-Attribute.
Ladereihenfolge: `appsettings.json``appsettings.{Environment}.json`
Umgebungsvariablen (`PREDICTALYTICS_`) → `settings.json` im Benutzerprofil (Kompatibilität).
Ablageort plattformabhängig über `Environment.SpecialFolder.ApplicationData`
(Windows: `%APPDATA%`, Linux: `~/.config`) statt neben der Executable — das
Installationsverzeichnis ist unter Linux üblicherweise nicht beschreibbar.
⚠️ Beim Umbau ist `SslMode = MySqlSslMode.None` (`AppSettings.cs:100`) fest verdrahtet.
Da die Zugangsdaten künftig auch über Netz zu einer entfernten DB gehen, sollte das
konfigurierbar und standardmäßig aktiv werden.
### AP 2.4 — Lifecycle GUI-frei machen
* `System.Windows.Forms.Timer``PeriodicTimer` bzw. `IHostedService`
(`MainForm.cs:71` DB-Größe, `LicenseGuard.cs:70` Revalidierung)
* `Application.Exit()``IHostApplicationLifetime.StopApplication()`
* `PredictalyticsHost` bekommt Events/Observables (`StatusChanged`, `DbSizeChanged`),
an die sich später sowohl das Avalonia-ViewModel als auch der Headless-Modus hängen.
### AP 2.5 — wwwroot sauber deployen
`FindWwwrootPath()` (`EmbeddedWebServer.cs:308-335`) mit der Verzeichnis-Heuristik ersatzlos
streichen. Stattdessen in den Host-`.csproj`:
```xml
<Content Include="..\Predictalytics.Api\wwwroot\**"
Link="wwwroot\%(RecursiveDir)%(Filename)%(Extension)"
CopyToOutputDirectory="PreserveNewest" />
```
und im Code nur noch `Path.Combine(AppContext.BaseDirectory, "wwwroot")`.
**Abnahme:** WinFormsHost benutzt nur noch `Predictalytics.Hosting` und enthält
ausschließlich UI-Code. `Predictalytics.Hosting` baut ohne Windows-Referenzen.
---
## 5. Phase 3 — Avalonia-Bedienhülle ✅ **erledigt (2026-08-07)**
**Abnahme:** `dotnet build` → 0 Fehler, 0 Warnungen. `dotnet test` → 100 bestanden.
Beide Betriebsarten laufen an:
| Prüfung | Ergebnis |
|---|---|
| `Predictalytics.Shell.exe --headless` | Logging läuft, Lizenzprüfung greift, sauberer Abbruch mit **Exit-Code 2** und handlungsfähiger Meldung |
| `Predictalytics.Shell.exe` (GUI) | startet, Lizenzfenster erscheint, kein Absturz |
| Konsolen-Kodierung | UTF-8 explizit gesetzt — ohne das zeigt die Windows-Konsole nur Fragezeichen statt Rahmen und Emoji |
### Nachtrag 2026-08-07: Hauptfenster verifiziert ✅
Zunächst blockierte eine abgelaufene Lizenz die Prüfung. Nach der Umstellung auf das
Deploymentcenter (Abschnitt 5a) startet die GUI vollständig durch — belegt durch die
Logdateien des Laufs:
```
[INF] Shell initialisiert. Bereit.
[INF] 'Start Webserver' startet die WebUI auf http://localhost:5000
[INF] Einstellungen: C:\Users\bergm\AppData\Roaming\Predictalytics\settings.json
```
Damit sind Lizenzschranke, Serilog-Aufbau mit Terminal-Sink, `MainWindowViewModel.Start()`
und der neue Einstellungspfad nachgewiesen. **Noch nicht durchgeklickt**: die einzelnen
Wartungsaktionen und die Einstellungsansicht — dafür fehlen die in Phase 1 verlorenen
DB-Zugangsdaten.
### Umsetzung
| Datei | Zweck |
|---|---|
| `Program.cs` | Einstiegspunkt. Argumentauswertung **vor** jeder Avalonia-Initialisierung — sonst stirbt der Prozess auf einem Server ohne X11, bevor `--headless` greift. |
| `App.axaml(.cs)` | Lizenzschranke, dann Hauptfenster. `ShutdownMode.OnExplicitShutdown` während der Aktivierung, danach `OnMainWindowClose`. |
| `ViewModels/MainWindowViewModel.cs` | Bindungsziel, `CommunityToolkit.Mvvm`-Quellgeneratoren, kapselt `PredictalyticsHost` |
| `ViewModels/LogLine.cs` | Terminalzeile mit Einfärbung nach Loglevel (Farbschema aus WinForms übernommen) |
| `Views/MainWindow.axaml` | Menü, Werkzeugleiste, Terminal-Tab, Einstellungen-Tab, Statusleiste |
| `Views/LicenseWindow.axaml` | Aktivierungsfenster, ersetzt `LicenseDialog` |
| `Views/Dialogs.cs` | Ersatz für `MessageBox.Show` — im Code aufgebaut, kein Drittanbieterpaket |
| `Services/HeadlessRunner.cs` | AP 3.4: Betrieb ohne Oberfläche |
| `Services/ConsoleAttach.cs` | Windows-Konsole anhängen + UTF-8 |
### Abweichungen und Entscheidungen
* **`PropertyGrid`-Ersatz handgeschrieben**, wie empfohlen. Vier Gruppen als
`HeaderedContentControl`, Passwortfelder mit `PasswordChar`, **echtes mehrzeiliges
Textfeld** für die Egress-Kanäle (besser als das in Phase 2 entfallene `[Editor]`-Attribut).
SSL-Modus als `ComboBox`; bewusst nur `None`/`Preferred`/`Required``VerifyCA`/`VerifyFull`
bräuchten hinterlegte Zertifikate und würden hier nur zu Fehlkonfiguration einladen.
* **Speichern ist jetzt explizit** („Speichern & übernehmen") statt wie beim `PropertyGrid`
bei jeder Einzeländerung. Klarer, und erspart `INotifyPropertyChanged` auf `PredictalyticsOptions`.
* **Feste Fenstergröße aufgelöst** (vorher `1886×1088` fix): jetzt `MinWidth`/`MinHeight`
mit skalierendem `DockPanel`/`Grid`.
* **Schrift-Fallbackkette** `Cascadia Code, DejaVu Sans Mono, Liberation Mono, Consolas, monospace`
fürs Terminal; `Avalonia.Fonts.Inter` als mitgelieferte UI-Schrift, weil Linux-Distributionen
sehr unterschiedliche Standardfonts haben.
* **Ringpuffer statt Leeren**: die WinForms-Fassung verwarf bei 500 Zeilen das gesamte
Terminal (`rtb.Clear()`), jetzt fällt jeweils nur die älteste Zeile heraus.
* **`Avalonia.Diagnostics` nicht referenziert** — gibt es nur bis 11.3.18, nicht für 12.x.
* **`global::Avalonia.Application`**: innerhalb von `Predictalytics.*` löst der kurze Name
`Application` auf den eigenen Namespace `Predictalytics.Application` auf. Betrifft jede
künftige Avalonia-Klasse in diesem Projekt.
* **`Microsoft.NET.Sdk` statt `Sdk.Web`**, wie in Phase 2 gelernt (Static-Web-Assets-Kollision
mit dem `wwwroot`-Content-Eintrag).
### Headless-Betrieb
Lizenz über `PREDICTALYTICS_LICENSE_KEY`; ohne nutzbare Lizenz Abbruch mit Exit-Code 2
statt eines Dialogs, den niemand sieht. `SIGTERM` und `SIGINT` werden über
`PosixSignalRegistration` abgefangen, damit systemd sauber stoppen kann.
Exit-Codes: `0` planmäßig beendet · `2` keine nutzbare Lizenz · `3` Abbruch mit Fehler.
### AP 3.1 — Projekt `Predictalytics.Shell`
```xml
<PackageReference Include="Avalonia" Version="12.1.1" />
<PackageReference Include="Avalonia.Desktop" Version="12.1.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.1" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
```
MVVM mit `CommunityToolkit.Mvvm` (Source-Generatoren) — bei ~15 Settings-Properties plus
Status-Bindings spart das spürbar Boilerplate gegenüber handgeschriebenem `INotifyPropertyChanged`.
### AP 3.2 — Control-Mapping
| WinForms (heute) | Avalonia | Aufwand |
|---|---|---|
| `MenuStrip` + 3 Menüs | `Menu` / `NativeMenuBar` | klein |
| `ToolStrip` + 2 Buttons | `StackPanel` + `Button` | klein |
| `TabControl` / `TabPage` | `TabControl` / `TabItem` | klein |
| `StatusStrip` + 3 Labels | `DockPanel` unten + `TextBlock` | klein |
| `MessageBox.Show` (7 Aufrufe) | eigener Dialog oder `MsBox.Avalonia` | klein |
| **`RichTextBox` (farbiges Log)** | `ListBox`/`ItemsControl` über `ObservableCollection<LogLine>`, `DataTemplate` mit `Foreground`-Binding | **mittel** |
| **`PropertyGrid`** | **kein Äquivalent in Avalonia Core** | **mittelgroß** |
**Zum Log-View:** die bisherige Ringpuffer-Logik (`TerminalHelper.cs:26-31` — bei 500 Zeilen
`rtb.Clear()`) wird zur `ObservableCollection` mit `RemoveAt(0)` — angenehmer, weil die
Historie nicht mehr komplett verworfen wird. Marshalling über `Dispatcher.UIThread.Post`
statt `Control.BeginInvoke`. Farbschema aus `TerminalHelper.cs:33-39` direkt übernehmbar.
**Zum PropertyGrid** — das ist der einzige echte UI-Entwurfsaufwand. Drei Optionen:
| Option | Bewertung |
|---|---|
| **Handgeschriebene Settings-View** (empfohlen) | 4 Gruppen / 15 Properties (Webserver 2, Egress 1, Watchdog 6, Database 4 + abgeleiteter ConnectionString). Als `Expander` je Gruppe mit `TextBox`/`CheckBox`/`NumericUpDown`. Volle Kontrolle über Passwortfelder (`PasswordChar`) und Mehrzeilen-Editor für Egress. ~11,5 PT. |
| Drittanbieter `Avalonia.PropertyGrid` | Spart Zeit, bringt aber eine Abhängigkeit fragwürdiger Pflegelage und passt selten zum Fluent-Theme. |
| Reflection-basierter Generator | Elegant, aber Overengineering für 15 Properties. |
### AP 3.3 — Plattformdetails
* **Fixe Fenstergröße auflösen:** `MinimumSize`/`MaximumSize = 1886×1088`
(`MainForm.Designer.cs:233-234`) ist bei abweichender DPI/Auflösung unter Linux
untauglich. → skalierendes Grid-Layout, sinnvolles `MinWidth`/`MinHeight`.
* **Schriftart:** `"Cascadia Code"` (`MainForm.cs:25`) existiert unter Linux meist nicht.
→ Fallback-Kette `Cascadia Code, DejaVu Sans Mono, monospace`.
* **`Process.Start("explorer.exe", …)`** (3×, `MainForm.cs:192,207,209`) →
```csharp
Process.Start(new ProcessStartInfo(target) { UseShellExecute = true });
```
funktioniert auf Windows und Linux (dort über `xdg-open`).
* **Lizenzdialog** (`LicenseDialog.cs`, 118 Z.) in Avalonia nachbauen: Label, `TextBox`
(Großschreibung — `CharacterCasing.Upper` hat kein direktes Pendant, per
`TextInput`-Handler oder Converter lösen), Status-Label, zwei Buttons. Der
Aktivierungsablauf (`ValidateAsync` + `VerifyChecksum`) bleibt unverändert.
*Hinweis: greift auf die HardwareID-Arbeit zu, die separat läuft.*
### AP 3.4 — Headless-Modus *(verbindlich, 0,5 PT — Entscheidung 0.3)*
Schalter `--headless` in `Program.cs` der Shell: statt `BuildAvaloniaApp().StartWithClassicDesktopLifetime()`
direkt `PredictalyticsHost` starten und auf `IHostApplicationLifetime` warten, Logs nur auf
Console. Nach Phase 2 ist das reine Verdrahtung, weil der Kern bereits GUI-frei ist.
Zwingend mitzudenken:
* **Lizenz ohne Dialog:** Schlüssel aus `PREDICTALYTICS_LICENSE_KEY` bzw. Konfiguration;
bei fehlender/ungültiger Lizenz sauberer Abbruch mit Exit-Code ≠ 0 und klarer Logzeile
statt eines Dialogs, den niemand sieht.
* **Keine Avalonia-Initialisierung** vor der Argumentauswertung — sonst versucht der
Prozess auf einem Server ohne X11 zu starten und stirbt, bevor der Schalter greift.
* SIGTERM sauber behandeln (`IHostApplicationLifetime`), damit systemd korrekt stoppt.
**Abnahme:** Shell startet unter Windows **und** Linux-Desktop mit GUI; derselbe Build
startet mit `--headless` auf einem Server ohne X11. In beiden Modi: Worker laufen, Web-UI
erreichbar, Settings persistieren; im GUI-Modus zusätzlich farbiger Log-View und
Lizenzdialog.
---
## 5b. WinForms-Host abgelöst ✅ (2026-08-08)
`main` brachte eine vollständige Deployment-Center-Integration mit (Lizenz, Heartbeat mit
DB-Health-Check, Fehler-Stream, UpdateService), die im WinForms-Host entstanden war. Diese
wurde nach `feature/linux-net10` gemerged und dabei in den plattformneutralen Kern gezogen.
**`Predictalytics.WinFormsHost` ist entfernt** — die Avalonia-Shell ist ab jetzt die einzige
Bedienoberfläche.
### Was nach `Predictalytics.Hosting` wanderte
| Datei | Anmerkung |
|---|---|
| `DcConfig`, `DcApiClient`, `DcErrorSink`, `DcHeartbeatService`, `DcUpdateService` | unverändert, waren bereits plattformneutral |
| `DcErrorReporter` | `Application.ThreadException` + `MessageBox` entfernt; stattdessen `ReportUiThreadException(Exception)`, das der Host aufruft. Avalonia hängt sich über `Dispatcher.UIThread.UnhandledException` ein. |
| `LicenseGuard` + `LicenseSession` | Dialogaufruf und `System.Windows.Forms.Timer` entfernt. Neu: `TryUseCachedAsync()`, `ActivateAsync()` und `StartPeriodicRevalidation(session, onUnusable)` über `PeriodicTimer`. Die Unterscheidung transienter Fehler und die Gnadenfrist-Warnung sind unverändert übernommen. |
| Dc-Einstellungen | aus `AppSettings` nach `PredictalyticsOptions`; die Watchdog-Einstellungen entfielen |
| `DcErrorSink` in Serilog | in `LoggingSetup` aufgenommen, Startbanner nutzt jetzt `DcConfig.AppVersion` |
`Deploymentcenter.BuildInfo.targets` wird nun von `Predictalytics.Hosting` importiert;
`BuildInfo` entsteht damit im Namensraum `Predictalytics.Hosting`.
### Was in der Avalonia-Shell nachgezogen wurde
* Menü **Deployment Center** mit „Nach Updates suchen" und „Lizenzstatus anzeigen"
* Einstellungsgruppe **Watchdog → Deployment Center** (Token, Heartbeat, Fehler-Reporting,
Update-Kanal als ComboBox; die Server-URL wird nur angezeigt, nicht editiert)
* Heartbeat-Snapshot samt `SELECT 1`-DB-Probe, identisch zur WinForms-Fassung
* Update-Prüfung beim Start (still) und über das Menü (interaktiv), inklusive
`NotifyStopping()` vor dem Start des Update-Agenten
* Lizenzfenster nutzt `IsTransient`: bei fehlender Serververbindung wird nicht behauptet,
die Lizenz sei ungültig
### Verifiziert
```
🔑 Lizenz geprüft: valid (HWID 2:win:a765bd47…, Quelle machine-guid)
Shell initialisiert. Bereit.
🐕 Heartbeat ist aktiviert, aber es fehlt das Deployment-Center-Token
Update-Prüfung: v1.0.0 ist aktuell (Kanal prod)
```
`--license-status` meldet gültig samt Offline-Gnadenfrist. Build 0 Fehler, 100 Tests grün.
**Noch offen:** Heartbeat und Fehler-Stream laufen erst mit gesetztem DC-Token; die
Wartungsaktionen brauchen DB-Zugangsdaten.
---
## 5a. Zusatz — Umstellung auf das Deploymentcenter-Lizenzsystem ✅ (2026-08-07)
> **Überholt durch 5b.** Der hier beschriebene eigene `LicenseGuard` gegen
> `Deploymentcenter.Client` war ein Provisorium und wurde durch die reifere Fassung aus
> `main` ersetzt (`LicenseSession`, `IsTransient`, `TryGetCachedKey`, Gnadenfrist-Warnung).
> Die Begründungen zur Hardware-ID v2 und zum AES-GCM-Cache gelten unverändert.
**Nicht ursprünglich geplant.** Beim Aktivieren der neuen Lizenz stellte sich heraus, dass
Predictalytics noch am alten Lizenzsystem hing (LicenseLabrador, `license.mhdf.de`),
während die Lizenzen inzwischen im Deploymentcenter (`dc.mhdf.de`) gepflegt werden.
Vor dem Umbau mit einem Wegwerf-Probelauf gegen den echten Client abgesichert:
Slug `predictalytics`, HWID v2 aus `machine-guid`, Server meldet `valid`.
### Was sich geändert hat
| | vorher (LicenseLabrador) | jetzt (Deploymentcenter) |
|---|---|---|
| Endpunkt | `license.mhdf.de/public/api/v1` | `dc.mhdf.de/api/license/v1` |
| Client | `LicenseLabrador.Client` (netstandard2.0) | `Deploymentcenter.Client` (netstandard2.0;net8.0) |
| Authentifizierung | Ed25519-Signaturprüfung + Basic Auth im Quelltext | serverseitig, kein Geheimnis im Client |
| Hardware-ID | SHA-256 aus MachineGuid **+ Rechnername** | **v2**: `2:<plattform>:<hex>`, ohne Rechnername |
| Linux-HWID | `/etc/machine-id`, sonst MAC | `/etc/machine-id` → dbus → DMI-UUID → MAC → Schlüsseldatei |
| Container | nicht vorgesehen | `DEPLOYMENTCENTER_HWID` überschreibt |
| Cache | DPAPI, **stiller Klartext-Rückfall unter Linux** | AES-256-GCM, HKDF aus HWID+Slug, **kein Klartext-Rückfall**, `0600` unter Linux |
| Ablageort | `%APPDATA%` | zusätzlich `XDG_CONFIG_HOME` unter Linux |
**Damit erledigt sich Analyse-Punkt 3.2 Nr. 2 von selbst:** der stille Klartext-Rückfall
des Lizenz-Caches unter Linux existiert im neuen Client nicht mehr. Der Rechnername steckt
außerdem nicht mehr im Hash — Umbenennen verbraucht keinen Aktivierungsplatz.
### Neue CLI-Schalter (`Services/LicenseCli.cs`)
Aus dem Deploymentcenter-Integrationsleitfaden übernommen, für Server ohne Oberfläche:
```bash
Predictalytics.Shell --license-status # HWID, Cache-Ablage, Serverprüfung
Predictalytics.Shell --license-set-key <SCHLÜSSEL> # einmalig aktivieren
Predictalytics.Shell --license-deactivate # Aktivierungsplatz freigeben
```
Die Schalter werden **vor** jeder Avalonia-Initialisierung ausgewertet, wie `--headless`.
Der Schlüssel wird in der Ausgabe maskiert (`LLAB3-*****-*****-*****-AABB2`).
### ✅ Nachgetragen: Watchdog ist migriert (Stand 2026-08-23)
Der hier notierte Rest — `WatchdogHeartbeatService` gegen `watchdog.mhdf.de` mit dem
Header `X-Watchdog-Key` — ist erledigt. An seiner Stelle steht
`Hosting/DcHeartbeatService.cs`, das gegen `POST /api/watchdog/v1/ping` am
Deployment Center meldet (`:105,143`). Damit laufen Lizenz, Watchdog, UpdateService,
Fehler-Stream und Bugtracker über dieselbe Anbindung; `watchdog.mhdf.de` wird
nicht mehr angesprochen.
---
## 6. Phase 4 — Aufräumen und Absichern ✅ **erledigt (2026-08-08)**
**Abnahme:** Build 0 Fehler, 8 Warnungen (alle vorbestehend).
`dotnet test` → **124 bestanden** (vorher 100; 24 neue Tests zur Absicherung).
### AP 4.1 — Steuerendpunkte werden jetzt tatsächlich geschützt ✅
Der Befund war schlimmer als in der Analyse notiert: `/api/capabilities` **meldete**
`CanControl`/`AuthRequired` aus der Konfiguration, **erzwungen wurde nichts**.
`MapPredictalyticsControlEndpoints()` mappte Jobs, Trader-Steuerung und die Dev-Endpunkte
bedingungslos und ungeschützt.
Neu:
| Bindung | Token | Verhalten |
|---|---|---|
| localhost | — | Steuerendpunkte aktiv, ungeschützt (das Betriebssystem schützt) |
| localhost | gesetzt | Steuerendpunkte aktiv, Token wird verlangt |
| **extern** | **fehlt** | **Steuerendpunkte werden gar nicht gemappt**, dazu ein `Log.Error` |
| extern | gesetzt | Steuerendpunkte verlangen `X-Predictalytics-Key` |
Bewusst fail-safe: eine unbedachte Umstellung der Bind-Adresse öffnet nichts stillschweigend.
`ApiTokenFilter` vergleicht laufzeitkonstant (`CryptographicOperations.FixedTimeEquals`),
damit die Antwortzeit nicht verrät, wie viele Zeichen stimmen.
Swagger ist bei externer Bindung abgeschaltet — es legt die gesamte API-Oberfläche offen.
`/api/capabilities` meldet nun den tatsächlichen Zustand statt einer Konfigurationsabsicht.
### AP 4.2 — Globalisierung ✅
`RuntimeSetup.UseInvariantCulture()`, aufgerufen als Erstes in beiden Startpfaden.
Bewusst **nicht** über `InvariantGlobalization` im Projekt: das änderte zusätzlich die
Semantik von Zeichenkettenvergleichen und wäre ein größerer Eingriff als nötig.
### AP 4.3 — Log-Verzeichnis ✅
Standard ist jetzt `%APPDATA%\Predictalytics\logs` bzw. `~/.config/Predictalytics/logs` —
neben den Einstellungen statt neben der Programmdatei, die unter Linux üblicherweise nicht
beschreibbar ist und beim Update überschrieben würde. Übersteuerbar über
`PREDICTALYTICS_LOG_DIR`, etwa auf `/var/log/predictalytics` im systemd-Betrieb.
### AP 4.4 — Solution und Repo-Kopplung ✅
Der WinFormsHost-Sonderfall entfällt, weil das Projekt entfernt ist — die Solution baut
unter Linux ohne Bedingung durch. Die relative Referenz auf das Schwester-Repo bleibt
(vier Ebenen aufwärts), scheitert jetzt aber mit einer verständlichen Meldung samt
Klon-URL statt mit einem Fehler über eine fehlende Projektdatei.
**Offen:** Git-Submodul oder NuGet-Paket statt Pfadreferenz — für CI und Container relevant,
aber eine Entscheidung über Repo-Grenzen hinweg.
### AP 4.5 — Egress `SourceIp` ✅ (dokumentiert)
Code ist korrekt; der Unterschied ist zur Laufzeit. Linux verlangt, dass die Adresse
tatsächlich auf einem Interface konfiguriert ist (sonst `EADDRNOTAVAIL`), Windows ist
toleranter. Als Kommentar an der Bind-Stelle hinterlegt. Praktischer Test steht in Phase 6 aus.
### Neue Tests
* `PredictalyticsOptionsTests` — `IsPubliclyBound` für Loopback-Schreibweisen, Leerwerte
und externe Adressen; `WebserverUrl`; SSL-Modus im Connection-String; und dass leere
Eingaben bestehende DB-Zugangsdaten **nicht** überschreiben.
* `ApiTokenFilterTests` — richtiger Token passiert, fehlender/falscher/zu kurzer/zu langer
sowie abweichend geschriebener Token werden abgewiesen.
### ✅ Ende-zu-Ende verifiziert (2026-08-08, gegen die echte Datenbank)
Nachgeholt, sobald die DB-Zugangsdaten wieder vorlagen. Geprüft wurde mit einem
Wegwerf-Host, der **ausschließlich den Webserver** startet — die Worker blieben aus, weil
parallel eine ältere Instanz auf derselben Datenbank läuft. Zusätzlich mit
`ApiSettings__ReadOnlyDatabase=true`, sodass weder Migrationen noch Seeding liefen
(im Log bestätigt).
Als „externe" Bindung diente `127.0.0.2`: die Adresse gilt der Prüfung als öffentlich,
bleibt aber Loopback — der Test öffnet also nichts im Netz.
| Fall | `/api/capabilities` | `/api/jobs` | Swagger |
|---|---|---|---|
| localhost, kein Token | `canControl:true, authRequired:false` | 200 | 200 |
| **extern, kein Token** | `canControl:false` | **404** | **404** |
| extern, mit Token | `canControl:true, authRequired:true, tokenHeader:"X-Predictalytics-Key"` | ohne Header **401**, falscher Token **401**, richtiger **200** | **404** |
Damit ist belegt: eine externe Bindung ohne Token stellt die Steuerendpunkte gar nicht
erst bereit, und Swagger ist in beiden externen Fällen abgeschaltet.
**Gleichzeitig verifiziert** (Vorgriff auf Phase 6):
* Datenbankverbindung steht — 37.803 MB, `SELECT`-Pfad über `GetDatabaseSizeMbAsync`
* Web-UI und Assets werden ausgeliefert: `/`, `/css/style.css`, `/js/app.js` je 200
* `/api/dashboard` liefert echte Daten: 151.094 Trader, 38.187.946 Trades
---
## 6a. Phase 4 — ursprüngliche Planung
### AP 4.1 — Kestrel-Binding und Absicherung ⚠️
`UseUrls($"http://localhost:{port}")` (`EmbeddedWebServer.cs:122`) konfigurierbar machen
(`ASPNETCORE_URLS` bzw. Options), CORS-`AllowedOrigins` (heute fest `http://localhost:5000`)
mitziehen.
**Sobald nicht mehr nur auf `localhost` gelauscht wird, entfällt die bisher implizite
Absicherung.** `MapPredictalyticsControlEndpoints()` (schreibende Operationen) und Swagger
wären dann offen erreichbar. Das vorhandene `CanControl`/`AuthRequired`-Konzept
(`Api/ApiConfiguration.cs:66`) muss davor scharf geschaltet werden.
### AP 4.2 — Globalisierung
Im Host-Bootstrap `CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture`
setzen, damit Zahlenformatierung unabhängig von der Systemlocale ist.
`InvariantGlobalization=true` (spart `libicu` im Container) erst später erwägen — es ändert
String-Vergleichssemantik und sollte nicht im selben Sprung passieren.
### AP 4.3 — Log-Verzeichnis
`Path.Combine(AppContext.BaseDirectory, "logs")` (`Program.cs:33`) konfigurierbar machen.
Default: Windows `%APPDATA%`, Linux `~/.local/share` bzw. `/var/log/predictalytics`.
### AP 4.4 — Solution und Repo-Kopplung
* `Predictalytics.slnx`: WinFormsHost mit
`Condition="$([MSBuild]::IsOSPlatform('Windows'))"` versehen, damit
`dotnet build` der Gesamtsolution unter Linux durchläuft.
* Die `..\..\..\..\LicenseLabrador\…`-Projektreferenz (4 Ebenen aufwärts) ist für CI und
Container fragil → Git-Submodul oder NuGet-Paket.
### AP 4.5 — Egress-Kanäle prüfen ⚠️
`EgressPoolService.cs:70-88` bindet Sockets an eine Quell-IP. Unter Linux muss die IP
tatsächlich auf einem Interface konfiguriert sein (sonst `EADDRNOTAVAIL`); Windows ist
toleranter. Für nicht-lokale IPs bräuchte es `IP_FREEBIND`/`CAP_NET_ADMIN`.
Nur relevant, falls Egress-Kanäle produktiv genutzt werden. Proxy-Kanäle sind unkritisch.
---
## 7. Phase 5 — Deployment und CI (12 PT)
* `dotnet publish -r linux-x64` (und `win-x64`); bei self-contained an
`--self-contained` + Trimming-Verhalten von Avalonia denken.
* **Systemabhängigkeiten Linux (GUI):** `libx11-6`, `libice6`, `libsm6`, `libfontconfig1`,
`libharfbuzz0b`, Monospace-Fonts (z. B. `fonts-dejavu`). **(GUI-Variante braucht eine
Desktop-Session — siehe 0.3.)**
* systemd-Unit bzw. Dockerfile (für den Headless-Modus); es existiert bereits ein
`J:\Softwareprojekte\dockerCompose` — vermutlich anknüpfbar.
* Reverse Proxy (nginx/Caddy) mit TLS vor Kestrel.
* **Secrets:** DB-Passwort und Deployment-Center-Token liegen im Klartext in
`settings.json` (nicht versioniert). Unter Linux → EnvironmentFile mit `0600` oder
Secret-Store.
*Erledigt:* Die früher hier genannten Basic-Auth-Zugangsdaten in `LicenseGuard.cs`
gibt es nicht mehr — die Datei läuft vollständig über den `Deploymentcenter.Client`.
*Offen und dringlicher:* In `src/Predictalytics.Api/appsettings.json` steht ein
**echter OpenRouter-API-Key im Klartext**, und diese Datei **ist** versioniert.
Schlüssel zurückziehen, neu ausstellen, künftig über `OpenRouter__ApiKey` setzen.
* CI: Build + Test auf Linux **und** Windows.
* `docs/BETRIEB-Deploymentcenter.md` um den Linux-Betrieb ergänzen.
---
## 8. Phase 6 — Verifikation (2 PT)
| # | Prüfung |
|---|---|
| 8.1 | Worker-Dauerlauf ≥ 24 h unter Linux gegen die **Dev-DB**, Logvergleich mit einem Windows-Lauf |
| 8.2 | EF-Migrationen von Null gegen Dev-DB (Weg A: nur EF-9-Regression) |
| 8.3 | Zahlenformatierung: Lauf unter `LANG=de_DE.UTF-8` — Volumina/Preise/PnL gegen Windows-Referenz |
| 8.4 | Web-UI vollständig durchklicken (case-sensitive Assets, alle Endpunkte) |
| 8.5 | Lizenz: Erstaktivierung, Neustart aus Cache, Offline-Gnadenfrist, Revalidierung |
| 8.6 | Watchdog-Heartbeats erscheinen unter `watchdog.mhdf.de` |
| 8.7 | Egress-Kanäle (falls genutzt) |
| 8.8 | Shell unter Windows **und** Linux-Desktop: alle Buttons, Menüs, Settings-Persistenz |
| 8.9 | Derselbe Build mit `--headless` auf einem Server ohne X11: Start, Lizenz aus Konfiguration, SIGTERM-Stopp |
---
## 9. Zeitplan
| Phase | Inhalt | PT |
|---|---|---|
| 0 | Vorarbeiten | 0,5 |
| 1 | .NET 10 + Paketmatrix | 1,52,5 |
| 2 | Hosting-Kern extrahieren | 1,52 |
| 3 | Avalonia-Shell **inkl. Headless-Modus (AP 3.4)** | 4,56,5 |
| 4 | Aufräumen, Binding, Absicherung | 11,5 |
| 5 | Deployment + CI (Desktop **und** systemd/Container) | 1,52,5 |
| 6 | Verifikation (beide Betriebsarten) | 2 |
| | **Summe** | **12,517,5 PT** |
**≈ 2,53,5 Wochen fokussiert.**
Phasen 02 sind risikoarm und liefern bereits Wert (aufgeräumte Architektur, aktuelles
Framework). Phase 3 ist der Hauptaufwand. Die Phasen sind strikt sequenziell — jede baut
auf der vorigen auf; einzig AP 5 (Deployment) kann parallel zu Phase 3 vorbereitet werden.
---
## 10. Risiken
| Risiko | Eintritt | Wirkung | Gegenmaßnahme |
|---|---|---|---|
| **Pomelo hat keinen EF-Core-10-Provider** | **sicher** | blockiert EF Core 10 | ✅ gelöst durch Weg A: EF Core 9 auf net10.0 |
| Swashbuckle 6.9 → 10.x mit Brüchen | wahrscheinlich | halber bis ganzer Tag | ggf. auf eingebautes `AddOpenApi()` wechseln |
| PropertyGrid-Ersatz wird aufwendiger als geschätzt | mittel | +1 PT | handgeschriebene View, 15 Properties sind überschaubar |
| Avalonia-GUI läuft auf dem Linux-Ziel nicht (kein Desktop) | sicher auf Servern | Shell unbrauchbar | ✅ abgedeckt durch AP 3.4 (verbindlich) |
| `Avalonia.Diagnostics` fehlt für 12.x | sicher | nur Komfort | ohne DevTools arbeiten oder 11.3.18 in Debug |
| **Lizenzaktivierung headless** | **sicher (AP 3.4)** | mittel | koppelt an laufende HardwareID-Arbeit — früh abstimmen |
| Egress `SourceIp` unter Linux | niedrig | mittel | AP 4.5, früh testen |
| Regression in der Datenschicht | niedrig (Weg A) | hoch | ausschließlich Dev-DB, nie Prod |
---
## 11. Nächster Schritt
**Stand 2026-08-23: Phase 0 bis 4 sind abgeschlossen** (jeweils oben mit ✅ vermerkt),
ebenso der Zusatz 5a (Deploymentcenter-Lizenzsystem) und 5b (WinForms-Host abgelöst).
Der ursprüngliche Text an dieser Stelle empfahl noch, mit Phase 0 zu beginnen — das ist
überholt und deshalb ersetzt.
**Offen sind Phase 5 und Phase 6:**
1. **Phase 5 — Deployment und CI** (Abschnitt 7). Nichts davon ist bisher angefasst: kein
`dotnet publish -r linux-x64`, keine systemd-Unit, kein Dockerfile, kein Reverse Proxy,
keine CI. Das ist der eigentliche verbleibende Arbeitsblock.
2. **Phase 6 — Verifikation** (Abschnitt 8, Punkte 8.18.9). Der Dauerlauf unter Linux
(8.1) und die Prüfung unter `LANG=de_DE.UTF-8` (8.3) stehen aus. **Bisher wurde die
Anwendung noch auf keinem Linux-System ausgeführt** — die Portierung ist zwar
vollständig gebaut, aber unter Linux unbewiesen.
3. Unabhängig davon und vorgelagert: den **OpenRouter-API-Key** aus
`src/Predictalytics.Api/appsettings.json` zurückziehen und neu ausstellen (siehe
Abschnitt 7, Secrets).