Phase 0: UTC-Datumsauswertung korrigiert, Portierungsanalyse und -plan
Vorarbeiten fuer die Linux-/NET-10-Portierung (siehe docs/PLAN-Linux-Portierung.md). DateTime.TryParse ohne DateTimeStyles liefert bei Z-Zeitstempeln Kind=Local und rechnet in Lokalzeit um. An 13 Stellen in PolymarketProvider und LimitlessProvider wurden so ISO-Zeitstempel der Plattform-APIs eingelesen, waehrend der Fallback derselben Zuweisungen (DateTime.UtcNow) korrektes UTC schrieb - die Spalten enthielten also gemischt verschobene und korrekte Werte. Auf einem Server mit TZ=UTC haette derselbe Code andere Werte erzeugt als unter Windows. Neuer Helfer Infrastructure/Helpers/DateParsing.cs kapselt das Parsen mit InvariantCulture und AdjustToUniversal|AssumeUniversal; alle Fundstellen umgestellt. Ausserdem: - LimitlessProvider: Volume-Parsing auf InvariantCulture umgestellt - EmbeddedWebServer: hartkodierten j:\-Absolutpfad als wwwroot-Fallback entfernt Ein Backfill der Altdaten erfolgt bewusst nicht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,407 @@
|
||||
# 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 1–2 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. | klein–mittel |
|
||||
| **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 | 2–3 | niedrig — Composition Root existiert bereits |
|
||||
| **B** Konfigurationsmigration `AppSettings`/`PropertyGrid` → `appsettings.json` + Env + `IOptions` | 1 | niedrig |
|
||||
| **C** Manuelle Wartungsaktionen → CLI-Verben bzw. Admin-Endpunkte | 1–1,5 | niedrig |
|
||||
| **D** Lizenzierung headless: nicht-interaktive Aktivierung, StorageDirectory, Timer, Cache-Absicherung | 1,5–3 | **mittel–hoch** — berührt LicenseLabrador, serverseitig zu verifizieren |
|
||||
| **E** Kestrel-Binding + CORS + Absicherung der Control-Endpunkte | 0,5–1 | mittel (Sicherheitsentscheidung nötig) |
|
||||
| **F** Kleinkram: `wwwroot`-Deployment, Log-Pfad, Culture-Bug, TFM-Vereinheitlichung, Solution-Split, eingecheckte Logs | 0,5–1 | niedrig |
|
||||
| **G** Deployment: systemd/Docker, Reverse Proxy, Secrets, CI | 1–2 | niedrig–mittel |
|
||||
| **H** Verifikation auf einer echten Linux-Instanz (Worker-Dauerlauf, DB, Egress, Watchdog, Lizenz) | 2 | mittel |
|
||||
| **Summe** | **~9,5–14,5 PT** | |
|
||||
|
||||
**Realistisch: 2–3 Wochen bei Teilzeit, ca. 2 Wochen fokussiert.**
|
||||
|
||||
Zum Vergleich: Der ursprünglich angenommene Weg über **Avalonia + LiveCharts2** hätte
|
||||
grob **4–8 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,5–14,5 Personentage**, mit dem einzigen nennenswerten Risiko im
|
||||
Lizenzierungspfad.
|
||||
@@ -0,0 +1,436 @@
|
||||
# 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 +3–5 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
|
||||
1–2 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 1–2-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 (1,5–2,5 PT)
|
||||
|
||||
Bewusst **vor** dem Avalonia-Umbau: eine Baustelle nach der anderen, und das Shell-Projekt
|
||||
entsteht dann direkt auf dem Zielframework.
|
||||
|
||||
### AP 1.1 — Zentrale Frameworkverwaltung
|
||||
`Directory.Build.props` erweitern:
|
||||
```xml
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
```
|
||||
`<TargetFramework>` aus allen `.csproj` entfernen (Ausnahme: WinFormsHost →
|
||||
`net10.0-windows`). Empfohlen: zugleich `Directory.Packages.props` mit
|
||||
`ManagePackageVersionsCentrally` einführen — bei 7+ Projekten mit überlappenden Paketen
|
||||
verhindert das künftige Versionsdrift.
|
||||
|
||||
### 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 | |
|
||||
|
||||
### AP 1.3 — Erwartete Bruchstellen
|
||||
* **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 (1,5–2 PT)
|
||||
|
||||
**Der Schlüsselschritt.** Reiner Verschiebe- und Entkopplungsvorgang, keine neue Logik.
|
||||
|
||||
### 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 (4–6 PT)
|
||||
|
||||
### 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** | **mittel–groß** |
|
||||
|
||||
**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. ~1–1,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.
|
||||
|
||||
---
|
||||
|
||||
## 6. Phase 4 — Aufräumen und Absichern (1–1,5 PT)
|
||||
|
||||
### 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 (1–2 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 Watchdog-API-Key liegen heute im Klartext in `settings.json`.
|
||||
Unter Linux → EnvironmentFile mit `0600` oder Secret-Store.
|
||||
*Randnotiz:* In `LicenseGuard.cs:16-17` stehen Basic-Auth-Zugangsdaten im Quelltext —
|
||||
unabhängig von dieser Portierung überdenkenswert, erst recht mit Blick auf Monetarisierung.
|
||||
* CI: Build + Test auf Linux **und** Windows.
|
||||
* `docs/BETRIEB-Watchdog-Lizenz.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,5–2,5 |
|
||||
| 2 | Hosting-Kern extrahieren | 1,5–2 |
|
||||
| 3 | Avalonia-Shell **inkl. Headless-Modus (AP 3.4)** | 4,5–6,5 |
|
||||
| 4 | Aufräumen, Binding, Absicherung | 1–1,5 |
|
||||
| 5 | Deployment + CI (Desktop **und** systemd/Container) | 1,5–2,5 |
|
||||
| 6 | Verifikation (beide Betriebsarten) | 2 |
|
||||
| | **Summe** | **12,5–17,5 PT** |
|
||||
|
||||
**≈ 2,5–3,5 Wochen fokussiert.**
|
||||
|
||||
Phasen 0–2 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
|
||||
|
||||
Alle Vorentscheidungen sind getroffen (Abschnitt 0). Umsetzung kann beginnen:
|
||||
|
||||
1. **Phase 0** — 0,5 PT, sofort, unabhängig vom Rest. Ergebnis ist ein aufgeräumter
|
||||
Ausgangspunkt auf `feature/linux-net10`.
|
||||
2. **Phase 1** — .NET 10 + Paketmatrix. Hier zeigt sich früh, wie groß der
|
||||
Swashbuckle-Umbau tatsächlich ist.
|
||||
3. Parallel dazu: **Lizenzaktivierung ohne Dialog** mit der laufenden HardwareID-Arbeit
|
||||
abstimmen — das ist der einzige Punkt im Plan, der von außen abhängt und dadurch spät
|
||||
teuer werden kann.
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Predictalytics.Infrastructure.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Parses timestamp strings from platform APIs as UTC.
|
||||
/// <para>
|
||||
/// The platforms deliver ISO-8601 strings; the domain stores UTC throughout — every call
|
||||
/// site falls back to <see cref="DateTime.UtcNow"/>, and the neighbouring DbCreatedAt is
|
||||
/// always UTC. A plain <c>DateTime.TryParse</c> breaks that in two ways: it honours the
|
||||
/// machine's locale, and it converts a "Z"-suffixed timestamp to <em>local</em> time. The
|
||||
/// same API payload would therefore land in the database differently on a Berlin
|
||||
/// workstation than on a UTC server.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class DateParsing
|
||||
{
|
||||
private const DateTimeStyles UtcStyles =
|
||||
DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal;
|
||||
|
||||
/// <summary>
|
||||
/// Parses an API timestamp as UTC. Returns null when absent or unparseable.
|
||||
/// Timestamps without a zone designator are treated as UTC.
|
||||
/// </summary>
|
||||
public static DateTime? ParseUtc(string? value) =>
|
||||
DateTime.TryParse(value, CultureInfo.InvariantCulture, UtcStyles, out var parsed)
|
||||
? parsed
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// Parses an API timestamp as UTC, falling back to the current UTC time.
|
||||
/// </summary>
|
||||
public static DateTime ParseUtcOrNow(string? value) => ParseUtc(value) ?? DateTime.UtcNow;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using System.Globalization;
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Predictalytics.Infrastructure.Helpers;
|
||||
using Predictalytics.Infrastructure.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -235,7 +237,7 @@ public class LimitlessProvider : IPlatformProvider
|
||||
Price = (decimal)(e.Price ?? 0),
|
||||
Size = (decimal)(e.Size ?? 0),
|
||||
Amount = (decimal)(e.Price * e.Size ?? 0),
|
||||
ExecutedAt = DateTime.TryParse(e.CreatedAt, out var dt) ? dt : DateTime.UtcNow,
|
||||
ExecutedAt = DateParsing.ParseUtcOrNow(e.CreatedAt),
|
||||
TransactionHash = e.TxHash,
|
||||
TransientWallet = wallet,
|
||||
};
|
||||
@@ -265,11 +267,12 @@ public class LimitlessProvider : IPlatformProvider
|
||||
Category = catMap.Category,
|
||||
Subcategory = catMap.Subcategory,
|
||||
ImageUrl = raw.ImageUrl ?? "",
|
||||
Volume = decimal.TryParse(raw.VolumeFormatted?.Replace(" USDC", ""), out var vol) ? vol : 0,
|
||||
Volume = decimal.TryParse(raw.VolumeFormatted?.Replace(" USDC", ""),
|
||||
NumberStyles.Any, CultureInfo.InvariantCulture, out var vol) ? vol : 0,
|
||||
Liquidity = (decimal)(raw.Liquidity ?? 0),
|
||||
EndDate = (raw.ExpirationTimestamp.HasValue && raw.ExpirationTimestamp.Value > 0 && raw.ExpirationTimestamp.Value < 253402300799)
|
||||
? DateTimeOffset.FromUnixTimeSeconds(raw.ExpirationTimestamp.Value).UtcDateTime
|
||||
: (DateTime.TryParse(raw.ExpirationDate, out var ed) ? ed : null),
|
||||
? DateTimeOffset.FromUnixTimeSeconds(raw.ExpirationTimestamp.Value).UtcDateTime
|
||||
: DateParsing.ParseUtc(raw.ExpirationDate),
|
||||
IsResolved = raw.Closed,
|
||||
LastUpdatedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Text.Json;
|
||||
using Predictalytics.Domain.Entities;
|
||||
using Predictalytics.Domain.Enums;
|
||||
using Predictalytics.Domain.Interfaces;
|
||||
using Predictalytics.Infrastructure.Helpers;
|
||||
using Predictalytics.Infrastructure.Logging;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@@ -216,9 +217,9 @@ public class PolymarketProvider : IPlatformProvider
|
||||
Title = rawEv.Title,
|
||||
Description = rawEv.Description,
|
||||
ImageUrl = rawEv.Image,
|
||||
StartDate = DateTime.TryParse(rawEv.StartDate, out var esd) ? esd : null,
|
||||
EndDate = DateTime.TryParse(rawEv.EndDate, out var eed) ? eed : null,
|
||||
CreatedAt = DateTime.TryParse(rawEv.CreatedAt, out var ecd) ? ecd : DateTime.UtcNow,
|
||||
StartDate = DateParsing.ParseUtc(rawEv.StartDate),
|
||||
EndDate = DateParsing.ParseUtc(rawEv.EndDate),
|
||||
CreatedAt = DateParsing.ParseUtcOrNow(rawEv.CreatedAt),
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
IsActive = rawEv.Active,
|
||||
IsClosed = rawEv.Closed,
|
||||
@@ -312,9 +313,9 @@ public class PolymarketProvider : IPlatformProvider
|
||||
Title = rawEvent.Title,
|
||||
Description = rawEvent.Description,
|
||||
ImageUrl = rawEvent.Image,
|
||||
StartDate = DateTime.TryParse(rawEvent.StartDate, out var sd) ? sd : null,
|
||||
EndDate = DateTime.TryParse(rawEvent.EndDate, out var ed) ? ed : null,
|
||||
CreatedAt = DateTime.TryParse(rawEvent.CreatedAt, out var cd) ? cd : DateTime.UtcNow,
|
||||
StartDate = DateParsing.ParseUtc(rawEvent.StartDate),
|
||||
EndDate = DateParsing.ParseUtc(rawEvent.EndDate),
|
||||
CreatedAt = DateParsing.ParseUtcOrNow(rawEvent.CreatedAt),
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
IsActive = rawEvent.Active,
|
||||
IsClosed = rawEvent.Closed,
|
||||
@@ -359,14 +360,14 @@ public class PolymarketProvider : IPlatformProvider
|
||||
Volume = (decimal)raw.Volume,
|
||||
Volume24h = (decimal)raw.Volume24hr,
|
||||
Liquidity = (decimal)raw.Liquidity,
|
||||
StartDate = DateTime.TryParse(raw.StartDate, out var msd) ? msd : null,
|
||||
EndDate = DateTime.TryParse(raw.EndDate ?? raw.EndDateIso, out var med) ? med : null,
|
||||
CreatedAt = DateTime.TryParse(raw.CreatedAt, out var mcd) ? mcd : DateTime.UtcNow,
|
||||
StartDate = DateParsing.ParseUtc(raw.StartDate),
|
||||
EndDate = DateParsing.ParseUtc(raw.EndDate ?? raw.EndDateIso),
|
||||
CreatedAt = DateParsing.ParseUtcOrNow(raw.CreatedAt),
|
||||
DbCreatedAt = DateTime.UtcNow,
|
||||
LastUpdatedAt = DateTime.UtcNow,
|
||||
FeeRateBps = (decimal)(raw.TakerFee * 10000),
|
||||
IsNegRisk = raw.NegRisk,
|
||||
ClosedAt = DateTime.TryParse(raw.ClosedTime, out var mct) ? mct : null
|
||||
ClosedAt = DateParsing.ParseUtc(raw.ClosedTime)
|
||||
};
|
||||
|
||||
// Parse outcomes, prices, and token IDs from JSON strings
|
||||
|
||||
@@ -315,9 +315,7 @@ public class EmbeddedWebServer
|
||||
Path.Combine(baseDir, "wwwroot"),
|
||||
// From bin/Debug/net8.0-windows/ up to src/, then into Api/wwwroot
|
||||
Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "Predictalytics.Api", "wwwroot")),
|
||||
Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "..", "src", "Predictalytics.Api", "wwwroot")),
|
||||
// Absolute fallback for this specific workspace
|
||||
@"j:\Softwareprojekte\Predictalytics\Predictalytics\src\Predictalytics.Api\wwwroot"
|
||||
Path.GetFullPath(Path.Combine(baseDir, "..", "..", "..", "..", "..", "src", "Predictalytics.Api", "wwwroot"))
|
||||
};
|
||||
|
||||
foreach (var path in candidates)
|
||||
|
||||
Reference in New Issue
Block a user