Files
Predictalytics/docs/PLAN-Linux-Portierung.md
T
RichardandClaude Opus 5 1f230fe12c Fruehjahrsputz 2/2: Dokumentation auf den tatsaechlichen Stand gebracht
Die Plandokumente waren durchweg veraltet: 85 von 91 Punkten im
UMSETZUNGSPLAN standen auf offen, obwohl der Code sie enthielt, und in
FIXPLAN-UI-Ranglisten war keine einzige der 18 Aufgaben abgehakt, obwohl
beide zugehoerigen Commits laengst im Zweig stecken. Alles abschnittsweise
gegen den Code geprueft und die Haekchen gesetzt - mit Belegstellen, damit
die naechste Pruefung nicht wieder bei null anfaengt.

Neu: STATUS.md als Einstiegsseite - wo das Projekt steht, was fertig ist,
was offen ist, und was beim Aufraeumen bewusst stehengeblieben ist. CLAUDE.md
verweist darauf.

Nachgezogen:
* UMSETZUNGSPLAN.md - 66 Punkte abgehakt. Offen bleiben A4 (Strategie-
  Klassifikation), B2 (Secrets) sowie C3/D1/D2 (SQL-Arbeiten des Nutzers).
* FIXPLAN-UI-Ranglisten.md - abgeschlossen bis auf den als "Optional"
  markierten Pfeilrichtungs-Punkt.
* FIXPLAN-TODO.md - Teil D und F abgehakt; die Test-Baseline "16 gruen"
  auf die heutigen 126 korrigiert. Offen: F5 und zwei Tests aus F6.
* FIXPLAN-G-Speicher.md - G1 bis G4 als erledigt vermerkt, Baseline "39/1"
  korrigiert, Pfad auf die nicht mehr existierende WinFormsHost/appsettings.json
  richtiggestellt.
* docs/PLAN-Linux-Portierung.md - der Watchdog-Warnhinweis war ueberholt
  (DcHeartbeatService meldet an /api/watchdog/v1/ping). Abschnitt 11 empfahl
  noch, mit Phase 0 zu beginnen; jetzt benennt er Phase 5 und 6 als das, was
  wirklich aussteht. Die Randnotiz zu Zugangsdaten in LicenseGuard.cs ist
  gegenstandslos, die Datei laeuft ueber den Deploymentcenter.Client.

Zwei Befunde, die keine Aufraeumarbeit sind und deshalb nur dokumentiert
wurden - beide in STATUS.md Abschnitt 3:

1. In src/Predictalytics.Api/appsettings.json steht ein echter
   OpenRouter-API-Key im Klartext, versioniert seit 7045002. Nicht
   eigenmaechtig entfernt: ohne Rotation beim Anbieter bringt das nichts
   (die History behaelt ihn), wuerde aber die KI-Analyse abschalten.
   Der Schluessel muss zurueckgezogen und neu ausgestellt werden.
2. Die Portierung ist auf keinem Linux-System je ausgefuehrt worden. Die
   Verifikation vom 2026-08-08 lief unter Windows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 12:30:42 +02:00

767 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).