Files
Predictalytics/docs/archiv/ANALYSE-Linux-Portierung.md
RichardandClaude Opus 5 6975720dc6 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>
2026-08-23 18:46:00 +02:00

408 lines
21 KiB
Markdown
Raw Permalink 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.
# Analyse: Linux-Fähigkeit von Predictalytics
**Stand:** 2026-08-06 · Branch `main` @ `aa19a89` · reine Ist-Analyse, keine Codeänderungen
---
## 0. Kernbefund vorab
> **Avalonia UI und LiveCharts2 werden nicht benötigt.**
Die Annahme im Auftrag war, dass eine WinForms-Oberfläche auf Avalonia + LiveCharts2 portiert
werden muss. Das trifft auf dieses Projekt nicht zu:
* Die **eigentliche Analyse-Oberfläche ist bereits eine Browser-Anwendung**
`src/Predictalytics.Api/wwwroot/` (3.745 Zeilen HTML/CSS/JS), Charts über
**Chart.js 4.4.4** (`index.html:9`, CDN). Sie ist damit heute schon plattformunabhängig.
* `Predictalytics.WinFormsHost` ist **kein UI-Projekt im fachlichen Sinne**, sondern eine
Bedien-Hülle für den Betrieb: Start/Stop der Worker, Start/Stop des Kestrel-Webservers,
ein `PropertyGrid` für Einstellungen, eine `RichTextBox` als Log-Terminal und vier
manuelle Wartungsaktionen. **1.698 LOC inkl. Designer-Datei.** Kein einziges Diagramm.
Die Aufgabe lautet damit nicht „UI-Framework migrieren", sondern
**„Bedien-Hülle durch einen headless Host ersetzen"** — eine Größenordnung weniger Aufwand.
---
## 1. Portierungsstatus je Projekt
| Projekt | TFM | Status | Bemerkung |
|---|---|---|---|
| `Predictalytics.Domain` | `net8.0` | ✅ portabel | 42 Dateien, reine Fachlogik |
| `Predictalytics.Application` | `net8.0` | ✅ portabel | 39 Dateien |
| `Predictalytics.Infrastructure` | `net8.0` | ✅ portabel | 94 Dateien, EF Core + Pomelo MySQL |
| `Predictalytics.Api` | `net8.0` | ✅ portabel | Minimal-APIs + wwwroot |
| `Predictalytics.Worker` | `net8.0` | ✅ portabel | 13 Worker-Services |
| `Predictalytics.Application.Tests` | `net10.0` | ⚠️ Inkonsistenz | s. Punkt 4.8 |
| `Predictalytics.WinFormsHost` | `net8.0-windows` | ❌ **muss ersetzt werden** | einziger Blocker |
| `LicenseLabrador.Client` (Schwester-Repo) | `netstandard2.0` | ⚠️ teilportabel | s. Punkt 3.2 |
**54.056 Zeilen C# gesamt. Davon sind 1.698 (3,1 %) windows-gebunden — und davon ist
etwa die Hälfte (`EmbeddedWebServer.cs`, Serilog-Setup) inhaltlich bereits portabel und
nur im falschen Projekt beheimatet.**
---
## 2. Was bereits sauber ist (und keinen Aufwand erzeugt)
Diese Punkte wurden explizit geprüft, weil sie erfahrungsgemäß die teuren Überraschungen
einer Linux-Portierung sind. Der Code ist hier durchweg in gutem Zustand:
### 2.1 Zeitzonen — überwiegend sauber, mit einer wichtigen Ausnahme ⚠️
> **Korrektur (2026-08-06, nach Phase 0):** Die erste Fassung dieser Analyse stufte den
> Punkt als „vollständig sauber, Aufwand 0" ein. Das war falsch — siehe unten. Die
> explizite Zeitzonen-*API*-Nutzung ist sauber, die *implizite* über `DateTime.TryParse`
> war es nicht.
```
166 × DateTime.UtcNow / DateTimeOffset.UtcNow
0 × DateTime.Now · DateTime.Today · DateTimeOffset.Now
0 × ToLocalTime() · DateTimeKind.Local
0 × TimeZoneInfo / FindSystemTimeZoneById
13 × DateTime.TryParse ohne Culture/Styles ← Problem
```
Der klassische Linux-Killer `TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time")`
(Windows-IDs existieren unter Linux nicht, dort gilt IANA `Europe/Berlin`) **kommt nicht
vor**. Insoweit ist die Codebasis sauber.
**Übersehen hatte ich die implizite Variante:** `DateTime.TryParse("...Z")` ohne
`DateTimeStyles` liefert `Kind=Local` und **rechnet den Wert in Lokalzeit um**. In beiden
Providern wurden so ISO-Zeitstempel der Plattform-APIs eingelesen:
* `PolymarketProvider.cs``StartDate`, `EndDate`, `CreatedAt`, `ClosedAt` (10 Stellen)
* `LimitlessProvider.cs``ExecutedAt`, `ExpirationDate` (2 Stellen)
Dass UTC gemeint war, belegen die Fallbacks: jede dieser Zuweisungen fällt bei
Parse-Fehler auf `DateTime.UtcNow` zurück, und direkt daneben steht `DbCreatedAt =
DateTime.UtcNow`. Auf der Berliner Entwicklungsmaschine landeten die geparsten Werte
dadurch um 12 Stunden verschoben in der Datenbank, die Fallback-Werte dagegen korrekt.
Für die Portierung ist das doppelt relevant: derselbe Code hätte auf einem Linux-Server
mit `TZ=UTC` **andere Werte** produziert als auf dem Windows-Rechner.
**Behoben in Phase 0** über `Infrastructure/Helpers/DateParsing.cs`
(`InvariantCulture` + `AdjustToUniversal | AssumeUniversal`).
⚠️ **Offen: Altdaten.** Bereits gespeicherte Zeilen tragen die Verschiebung weiter. Ob ein
Backfill nötig ist, hängt daran, wie stark `StartDate`/`EndDate`/`ClosedAt` in die
Auswertung eingehen — siehe „Nächste Schritte" im Portierungsplan.
### 2.2 Kulturabhängige Zahlenformatierung — bis auf eine Stelle sauber ✅
Alle Parser für Fremd-APIs (Polymarket, Limitless) verwenden explizit `InvariantCulture`:
* `LimitlessProvider.cs:59,60,110,113,114,115`
* `PolymarketModels.cs:17` · `PolymarketProvider.cs:386`
* `TradeHistoryWorker.cs:466,467`
**Eine Ausnahme (echter Latent-Bug, plattformunabhängig):**
```csharp
// Predictalytics.Infrastructure/Providers/Limitless/LimitlessProvider.cs:268
Volume = decimal.TryParse(raw.VolumeFormatted?.Replace(" USDC", ""), out var vol) ? vol : 0,
```
Ohne `InvariantCulture`. Auf einem System mit deutscher Locale (`de_DE.UTF-8`, unter Linux
sehr üblich) wird `"1234.56"` als **123456** interpretiert — Faktor 100.000 zu hoch.
Der Fehler existiert schon heute auf einem deutschsprachigen Windows; Linux macht ihn nur
wahrscheinlicher, weil die Locale dort oft explizit gesetzt wird.
**Aufwand: 5 Minuten. Sollte unabhängig von der Portierung gefixt werden.**
Zusätzliche Absicherung empfohlen: `InvariantGlobalization` bzw. ein explizites
`CultureInfo.DefaultThreadCurrentCulture = CultureInfo.InvariantCulture` im neuen Host.
### 2.3 Pfade und Dateisystem ✅
* Keine hartkodierten Laufwerksbuchstaben im Code — **eine** Ausnahme, siehe 4.5.
* `Path.Combine()` wird konsequent verwendet, keine manuell zusammengesetzten `\`-Pfade.
* **Kein einziger Datei-/Verzeichniszugriff** in `Infrastructure`, `Worker`, `Api`,
`Application`, `Domain`. Sämtliches File-IO liegt in `WinFormsHost` (Settings, Logs)
und im Lizenz-Client.
### 2.4 Groß-/Kleinschreibung (case-sensitive Dateisystem) ✅
Linux-Dateisysteme sind case-sensitiv, Windows nicht — ein häufiger Stolperstein bei
statischen Web-Assets. Geprüft:
| Referenz | Datei | Passt |
|---|---|---|
| `href="css/style.css?v=20260715"` | `wwwroot/css/style.css` | ✅ |
| `src="js/app.js?v=20260715"` | `wwwroot/js/app.js` | ✅ |
| `href="./docs.html"` | `wwwroot/docs.html` | ✅ |
| `href="./index.html"` | `wwwroot/index.html` | ✅ |
Alles durchgängig kleingeschrieben und deckungsgleich. **Aufwand: 0.**
Hinweis: MySQL-Tabellennamen sind unter Linux abhängig von `lower_case_table_names`
ebenfalls case-sensitiv. Da Prod-DB und Dev-DB bereits auf Linux-Servern laufen
(`lqf7.your-database.de`) und der Code über EF Core mit konsistenter Schreibweise
zugreift, besteht hier kein zusätzliches Risiko durch die Client-Portierung.
### 2.5 Keine Windows-spezifischen APIs außerhalb des Hosts ✅
Geprüft auf `Registry`, `Microsoft.Win32`, `ManagementObject`/WMI, `ProtectedData`,
`DllImport`, `System.Drawing`, `Environment.OSVersion`: **keine Treffer** außerhalb von
`Predictalytics.WinFormsHost`. Auch `Encoding.Default` (unter Linux immer UTF-8, unter
Windows Codepage) kommt nirgends vor.
### 2.6 NuGet-Abhängigkeiten ✅
Alle verwendeten Pakete sind plattformneutral: EF Core 8 + Pomelo MySQL, Serilog (+ File/
Console/Compact), Swashbuckle, `Microsoft.Extensions.*`, xunit, Moq. Keine
windows-exklusiven Pakete außerhalb des WinForms-Hosts.
### 2.7 Ein bereits vorhandener Vorteil: der Composition Root existiert schon ✅
`Predictalytics.Api/ApiConfiguration.cs` (`ConfigureApi`) ist eine **vollständige,
eigenständige Host-Konfiguration** — DI, CORS, Swagger, Static Files, Endpoint-Mapping.
Ihr fehlt nur eine `Main`-Methode. Der neue headless Host kann darauf direkt aufsetzen,
statt die Verdrahtung neu zu schreiben.
Ebenso ist `Infrastructure/Logging/RichTextBoxSink.cs` trotz des Namens **vollständig
portabel** — es ist ein reiner Delegate-Sink (`Action<string, LogEventLevel>`) ohne
WinForms-Referenz. Lediglich umbenennen (z. B. `DelegateSink`) wäre sinnvoll.
---
## 3. Die tatsächlichen Blocker
### 3.1 `Predictalytics.WinFormsHost` — Ersatz durch headless Host ⛔ **Hauptaufwand**
Das Projekt ist der einzige Einstiegspunkt der Anwendung (`static void Main` existiert nur
hier). Es leistet fünf Dinge, die im neuen Host anders gelöst werden müssen:
| Funktion heute | Ersatz unter Linux | Aufwand |
|---|---|---|
| **Serilog-Setup** (`Program.cs:35-148`) — Datei-Sinks nach Level und Plattform | 1:1 übernehmbar, ist bereits portabel. Nur `RichTextBoxSink` entfällt bzw. wird Console-Sink. | trivial |
| **`EmbeddedWebServer.cs`** (365 Z.) — Kestrel- und Worker-Lifecycle | Inhaltlich portabel. Wird zum normalen `IHost` mit `WebApplication` + `AddWorkerServices()`. Vereinfacht sich sogar deutlich, weil Start/Stop-Buttons entfallen. | klein |
| **`AppSettings.cs` + `PropertyGrid`** — Laufzeit-Konfiguration in `settings.json` | `appsettings.json` + Umgebungsvariablen + `IOptions<T>`. Die `[Category]`/`[DisplayName]`-Attribute entfallen ersatzlos. | kleinmittel |
| **Manuelle Aktionen** — „Sync Markets", „DB Update", „Recalculate All Traders" (Bestätigungsdialoge + `MessageBox`) | Entweder CLI-Verben (`--sync-markets`, `--recalc-all`) oder geschützte Admin-Endpunkte in der bestehenden Web-UI. Die Rückfrage-Logik („nicht während laufender Worker") muss erhalten bleiben. | mittel |
| **Log-Terminal** (`RichTextBox`) + DB-Größen-Anzeige + Build-Version | Entfällt / Console + `journalctl`. Die DB-Größenabfrage (`MainForm.cs:317-343`) kann als Health-Endpunkt weiterleben. | klein |
`WatchdogHeartbeatService.cs` (141 Z.) ist reines `HttpClient`-Verhalten und **vollständig
portabel** — sollte nach `Infrastructure` oder in den neuen Host wandern.
**Empfehlung:** WinFormsHost **nicht löschen**, sondern beide Hosts parallel führen.
Er ist die etablierte Betriebsoberfläche unter Windows; beide teilen sich denselben
Composition Root. Der Wartungsaufwand für die dünne Hülle ist gering.
### 3.2 Lizenzierung — der unterschätzte Blocker ⛔
Der `LicenseLabrador.Client` ist erfreulich gut vorbereitet, aber die **Aktivierung** ist es nicht.
**Was bereits funktioniert:**
* `HardwareId.cs:52-84` behandelt Linux explizit: liest `/etc/machine-id` bzw.
`/var/lib/dbus/machine-id`, mit MAC-Adressen-Fallback. Sauber gelöst.
* Zielframework `netstandard2.0`, `Microsoft.Win32.Registry` wird per `IsWindows()`-Guard
nur unter Windows angefasst.
**Was nicht funktioniert:**
1. **`LicenseDialog` ist ein blockierender WinForms-Modaldialog.**
`Program.cs:16``LicenseGuard.EnsureLicensed()``dialog.ShowDialog()`.
Auf einem headless Server gibt es niemanden, der einen Key eintippt. Es braucht einen
**nicht-interaktiven Aktivierungspfad**: Lizenzschlüssel aus Umgebungsvariable oder
Konfigurationsdatei, Aktivierung beim ersten Start, sauberer Abbruch mit Exit-Code und
klarer Logmeldung, wenn keine gültige Lizenz vorliegt.
2. **`StateStore.cs:43,92` — DPAPI-Verschlüsselung fällt unter Linux still auf Klartext zurück.**
```csharp
try { dataToSave = ProtectedData.Protect(plainBytes, null, DataProtectionScope.CurrentUser); }
catch { dataToSave = plainBytes; } // ← Linux landet immer hier
```
Der Lizenz-Cache (inkl. Lizenzschlüssel) liegt dann unverschlüsselt auf der Platte.
Funktional unkritisch, aber eine bewusste Entscheidung — mindestens sollten die
Dateirechte (`0600`) und das Verzeichnis abgesichert werden.
3. **`LicenseConfig.StorageDirectory`** nutzt `Environment.SpecialFolder.ApplicationData`
→ unter Linux `~/.config`. Unter systemd mit `DynamicUser=yes` oder ohne `$HOME`
existiert das nicht. Muss explizit gesetzt werden (z. B. `/var/lib/predictalytics`).
4. **`LicenseGuard.StartPeriodicRevalidation`** verwendet `System.Windows.Forms.Timer`
→ `PeriodicTimer` oder `IHostedService`. `Application.Exit()` → `IHostApplicationLifetime.StopApplication()`.
**Das ist der Punkt mit der größten Restunsicherheit**, weil er das Schwester-Repo
LicenseLabrador berührt und serverseitig geprüft werden muss (Aktivierung ohne UI).
### 3.3 Kestrel bindet nur auf `localhost` ⚠️
```csharp
// EmbeddedWebServer.cs:122
builder.WebHost.UseUrls($"http://localhost:{port}");
```
Das ist für einen Desktop-Host korrekt, für einen Linux-Server aber falsch: von außen
nicht erreichbar. Muss konfigurierbar werden (`0.0.0.0` bzw. `ASPNETCORE_URLS`), inkl.
Anpassung der CORS-`AllowedOrigins` (heute fest `http://localhost:5000`).
**Sicherheitshinweis:** Sobald der Dienst nicht mehr nur auf `localhost` lauscht, ist die
bisher implizite Absicherung weg. `MapPredictalyticsControlEndpoints()` (schreibende
Operationen) und Swagger sind dann exponiert. Es gibt bereits ein `CanControl`/`AuthRequired`-
Konzept (`ApiConfiguration.cs:66`) — das muss vor dem Linux-Deployment scharf geschaltet
werden, plus Reverse Proxy (nginx/Caddy) mit TLS davor.
---
## 4. Weitere Fundstellen (klein, aber notwendig)
### 4.1 `Process.Start("explorer.exe", …)` — 3 Stellen
`MainForm.cs:192,207,209` (Browser öffnen, Log-Ordner öffnen). Betrifft nur den
WinForms-Host; im headless Host entfallen die Aktionen ersatzlos.
### 4.2 `System.Windows.Forms.Timer` — 2 Stellen
`MainForm.cs:71` (DB-Größe), `LicenseGuard.cs:70` (Revalidierung).
→ `PeriodicTimer` / `IHostedService`.
### 4.3 Schriftart `Cascadia Code`
`MainForm.cs:25`. Nur WinForms-relevant, entfällt.
### 4.4 Log-Verzeichnis
`Program.cs:33`: `Path.Combine(AppContext.BaseDirectory, "logs")`.
Technisch portabel, aber unter Linux ist das Installationsverzeichnis üblicherweise nicht
beschreibbar. → konfigurierbar machen, Default `/var/log/predictalytics` bzw.
`journalctl` über den Console-Sink.
Nebenbefund: `src/Predictalytics.WinFormsHost/logs/Predictalytics-20260428.json` ist
offenbar **versehentlich eingecheckt** und enthält Stacktraces mit lokalen Pfaden. Sollte
aus der Versionierung entfernt und `.gitignore` ergänzt werden.
### 4.5 `FindWwwrootPath()` — hartkodierter Absolutpfad ⚠️
```csharp
// EmbeddedWebServer.cs:314-321
var candidates = new[] {
Path.Combine(baseDir, "wwwroot"),
Path.GetFullPath(Path.Combine(baseDir, "..","..","..","..","Predictalytics.Api","wwwroot")),
Path.GetFullPath(Path.Combine(baseDir, "..","..","..","..","..","src","Predictalytics.Api","wwwroot")),
@"j:\Softwareprojekte\Predictalytics\Predictalytics\src\Predictalytics.Api\wwwroot" // ←
};
```
Die Suchheuristik über Build-Output-Verzeichnisse funktioniert im Deployment nicht
zuverlässig, der `j:\`-Fallback gar nicht. Sauber: `wwwroot` als `Content` mit
`CopyToOutputDirectory` in die Host-`.csproj` aufnehmen (in der Api-`.csproj` bereits
vorhanden) und nur noch `Path.Combine(AppContext.BaseDirectory, "wwwroot")` verwenden.
### 4.6 MySQL-Verbindung: `SslMode = None` fest verdrahtet ⚠️
`AppSettings.cs:100`. Bei einem lokal laufenden Windows-Client zu einer entfernten DB war
das schon fragwürdig; bei einem Serverbetrieb sollte SSL konfigurierbar (und
standardmäßig aktiv) sein. Kein Linux-spezifisches Problem, fällt aber bei der
Konfigurationsmigration ohnehin an.
### 4.7 Egress-Kanäle (`SourceIp`) — verhält sich unter Linux anders ⚠️
`EgressPoolService.cs:70-88` bindet einen Socket per `socket.Bind(new IPEndPoint(ip, 0))`
an eine Quell-IP. Das funktioniert unter Linux grundsätzlich, aber:
* Die IP muss auf einem Interface tatsächlich konfiguriert sein (sonst `EADDRNOTAVAIL`);
Windows ist hier toleranter.
* Für nicht-lokale IPs bräuchte es `IP_FREEBIND`/`CAP_NET_ADMIN`.
Falls Egress-Kanäle produktiv genutzt werden, ist das ein expliziter Testpunkt.
Proxy-Kanäle (`WebProxy`) sind unproblematisch.
### 4.8 Zielframework-Inkonsistenz ⚠️
`Predictalytics.Application.Tests` steht auf **`net10.0`**, alle anderen auf `net8.0`.
Das funktioniert (Test-Assembly darf höher liegen), erzwingt aber ein .NET-10-SDK auf dem
Build-Server. Bei einer CI-Einrichtung für Linux vereinheitlichen — entweder alles auf
`net8.0` oder ein bewusster Sprung der gesamten Solution auf `net10.0`.
### 4.9 `Predictalytics.slnx` und Build
Die Solution referenziert den WinFormsHost. Auf Linux schlägt ein `dotnet build` der
Gesamtsolution fehl (`net8.0-windows` + `UseWindowsForms`). Optionen:
* Den WinFormsHost mit `<TargetFrameworks>` bzw. einer Condition auf
`$([MSBuild]::IsOSPlatform('Windows'))` versehen, oder
* eine zweite Solution-Datei für Linux-Builds pflegen.
Zusätzlich: Die `.csproj` des Hosts referenziert das Schwester-Repo über einen
**relativen Pfad mit vier Ebenen nach oben** (`..\..\..\..\LicenseLabrador\…`). Das setzt
eine feste Verzeichnisstruktur voraus und ist für CI/Container fragil — besser über ein
NuGet-Paket oder ein Git-Submodul lösen.
---
## 5. Was zusätzlich neu entsteht (Deployment)
Kein Portierungsproblem im engeren Sinn, aber Teil der Aufgabe „Linux-fähig":
* **systemd-Unit** (`Restart=always`, `User=`, `WorkingDirectory=`, `EnvironmentFile=`)
oder alternativ **Dockerfile** + `docker-compose` (es existiert bereits ein
`J:\Softwareprojekte\dockerCompose`-Verzeichnis — vermutlich anknüpfbar).
* **Reverse Proxy** (nginx/Caddy) mit TLS vor Kestrel.
* **Secrets-Handling**: DB-Passwort und Watchdog-API-Key liegen heute im Klartext in
`settings.json` neben der Exe. Unter Linux → Environment-File mit `0600` oder
Secret-Store.
*Randnotiz:* In `LicenseGuard.cs:16-17` stehen Basic-Auth-Zugangsdaten im Quelltext.
Bei einem Server-Deployment (und erst recht bei der angedachten Monetarisierung) sollte
das überdacht werden — unabhängig von dieser Portierung.
* **EF-Migrationen**: `EnsureDatabaseAsync()` existiert bereits und wird beim Start
aufgerufen — für den Serverbetrieb ist zu entscheiden, ob Migrationen automatisch
laufen sollen oder als separater Schritt.
* **CI**: Build/Test auf Linux, Publish als `linux-x64` (ggf. self-contained).
* `docs/BETRIEB-Watchdog-Lizenz.md` muss um den Linux-Betrieb ergänzt werden.
---
## 6. Aufwandsschätzung
| Arbeitspaket | Aufwand (PT) | Risiko |
|---|---|---|
| **A** Neues Host-Projekt `Predictalytics.Host` (net8.0): Serilog, Kestrel, Worker, Lifecycle | 23 | niedrig — Composition Root existiert bereits |
| **B** Konfigurationsmigration `AppSettings`/`PropertyGrid``appsettings.json` + Env + `IOptions` | 1 | niedrig |
| **C** Manuelle Wartungsaktionen → CLI-Verben bzw. Admin-Endpunkte | 11,5 | niedrig |
| **D** Lizenzierung headless: nicht-interaktive Aktivierung, StorageDirectory, Timer, Cache-Absicherung | 1,53 | **mittelhoch** — berührt LicenseLabrador, serverseitig zu verifizieren |
| **E** Kestrel-Binding + CORS + Absicherung der Control-Endpunkte | 0,51 | mittel (Sicherheitsentscheidung nötig) |
| **F** Kleinkram: `wwwroot`-Deployment, Log-Pfad, Culture-Bug, TFM-Vereinheitlichung, Solution-Split, eingecheckte Logs | 0,51 | niedrig |
| **G** Deployment: systemd/Docker, Reverse Proxy, Secrets, CI | 12 | niedrigmittel |
| **H** Verifikation auf einer echten Linux-Instanz (Worker-Dauerlauf, DB, Egress, Watchdog, Lizenz) | 2 | mittel |
| **Summe** | **~9,514,5 PT** | |
**Realistisch: 23 Wochen bei Teilzeit, ca. 2 Wochen fokussiert.**
Zum Vergleich: Der ursprünglich angenommene Weg über **Avalonia + LiveCharts2** hätte
grob **48 Wochen** gekostet — für eine Oberfläche, die es fachlich bereits als Web-UI
gibt. Diese Arbeit wäre vollständig verloren gewesen.
---
## 7. Empfohlenes Vorgehen
1. **F zuerst** (halber Tag): Culture-Bug, TFM, eingecheckte Logs, `wwwroot`-Deployment.
Wirkt sofort, unabhängig von der Portierung, reduziert Rauschen.
2. **A + B parallel** — das neue Host-Projekt entsteht neben dem WinFormsHost, nicht
an seiner Stelle. Beide teilen sich `ApiConfiguration` und `AddWorkerServices()`.
Ab hier läuft der Stack unter Linux (noch ohne Lizenz-Gate).
3. **E** — Binding und Absicherung, bevor irgendetwas exponiert wird.
4. **D** — Lizenzierung. Am besten früh anfangen zu klären, weil es das Schwester-Repo
und ggf. Serveränderungen betrifft; die Umsetzung kann dann parallel laufen.
5. **C**, **G**, **H** zum Abschluss.
**Zwei Entscheidungen sind vorab zu treffen:**
* **Bleibt der WinFormsHost erhalten?** Empfehlung: **ja**, als Windows-Betriebsoberfläche.
Er ist dünn, funktioniert und beide Hosts teilen sich denselben Kern. Der Web-UI-Zugang
ist ohnehin identisch.
* **Wie wird headless lizenziert?** Lizenzschlüssel per Umgebungsvariable/Config und
Aktivierung beim ersten Start ist der pragmatische Weg — muss aber mit LicenseLabrador
abgestimmt werden.
---
## 8. Zusammenfassung in drei Sätzen
Predictalytics ist in deutlich besserem Zustand für eine Linux-Portierung als erwartet:
96,9 % des Codes sind bereits plattformneutral, es gibt keine Windows-Zeitzonen-IDs und
keine Windows-APIs außerhalb des Hosts, und die Analyse-Oberfläche ist längst eine
Browser-Anwendung mit Chart.js — LiveCharts2 wird nicht gebraucht.
Zwei Defekte an der Grenze zu den Plattform-APIs (13 × lokalzeit-verzerrendes
Datums-Parsing, 1 × kulturabhängiges Zahlen-Parsing) waren allerdings vorhanden und sind
in Phase 0 behoben.
Der eigentliche Aufwand liegt im Ersetzen der 1.698 Zeilen umfassenden WinForms-
Bedienhülle durch einen headless Host und im Umbau der Lizenzaktivierung, die heute
zwingend einen Modaldialog erfordert.
Geschätzt **9,514,5 Personentage**, mit dem einzigen nennenswerten Risiko im
Lizenzierungspfad.