Files
Predictalytics/src/Predictalytics.WinFormsHost/MainForm.cs
T
RichardandClaude Opus 5 260dff1700 Phase 2: Plattformneutralen Hosting-Kern extrahiert
Neues Projekt Predictalytics.Hosting nimmt auf, was bisher im
windows-gebundenen WinFormsHost feststeckte, aber portabel ist:

- PredictalyticsHost (aus EmbeddedWebServer): Kestrel- und Worker-Lifecycle,
  Wartungsaktionen, DB-Groesse. Meldet Zustandswechsel ueber StateChanged.
- PredictalyticsOptions (aus AppSettings): ohne WinForms-Bezug. Die
  System.ComponentModel-Attribute sind plattformneutral und bleiben, damit
  das PropertyGrid Gruppen und Beschreibungen behaelt.
- LoggingSetup (aus Program.cs): Serilog-Aufbau, Terminal-Sink als optionale
  Action statt fester RichTextBox.
- LicenseGuard: GUI-frei. Periodische Revalidierung ueber PeriodicTimer statt
  WinForms-Timer, Abbruch ueber Callback statt Application.Exit. Der
  interaktive Dialogaufruf bleibt als LicenseGate im WinForms-Host.
- WatchdogHeartbeatService unveraendert verschoben.

Infrastructure: RichTextBoxSink -> DelegateSink umbenannt (war nie
WinForms-abhaengig, nur missverstaendlich benannt).

Einstellungen liegen jetzt unter %APPDATA%/Predictalytics bzw.
~/.config/Predictalytics statt neben der Programmdatei, mit einmaliger
Uebernahme aus dem alten Ort. Das Installationsverzeichnis ist unter Linux
ueblicherweise nicht beschreibbar.

wwwroot wird ueber einen Content-Eintrag neben die Programmdatei kopiert;
die frueheren Pfad-Heuristiken entfallen.

Hosting und WinFormsHost nutzen Microsoft.NET.Sdk statt Sdk.Web: der Web-SDK
globbt wwwroot automatisch als Static Web Asset und kollidiert mit dem
Content-Eintrag. WebApplication kommt ueber FrameworkReference.

Neu konfigurierbar (verhaltensgleiche Defaults): WebserverHost fuer die
Kestrel-Bind-Adresse, DbSslMode fuer die MySQL-Verschluesselung.

explorer.exe-Aufrufe durch ProcessStartInfo mit UseShellExecute ersetzt —
funktioniert unter Windows und Linux.

Build: 0 Fehler. Tests: 100 bestanden, 0 Fehler, 1 uebersprungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 23:13:04 +02:00

321 lines
12 KiB
C#

using Predictalytics.Hosting;
using Serilog;
namespace Predictalytics.WinFormsHost;
public partial class MainForm : Form
{
private PredictalyticsHost _host = null!;
private CancellationTokenSource? _workerCts;
private PredictalyticsOptions _settings = null!;
private WatchdogHeartbeatService? _watchdog;
/// <summary>Exposes the terminal RichTextBox for the Serilog sink.</summary>
public RichTextBox Terminal => rtb_terminal;
public MainForm()
{
InitializeComponent();
this.Text = "Predictalytics Analytics — Backend Server";
rtb_terminal.BackColor = System.Drawing.Color.FromArgb(15, 15, 20);
rtb_terminal.ForeColor = System.Drawing.Color.FromArgb(180, 180, 180);
rtb_terminal.Font = new Font("Cascadia Code", 9.5f, FontStyle.Regular);
rtb_terminal.ReadOnly = true;
}
/// <summary>
/// Called after Serilog is configured. Initializes the application host.
/// </summary>
public void Initialize()
{
_settings = PredictalyticsOptions.Load();
// Der Host haelt dieselbe Options-Instanz — Aenderungen im PropertyGrid
// wirken damit ohne weitere Weitergabe beim naechsten Start.
_host = new PredictalyticsHost(_settings);
_host.StateChanged += () => BeginInvoke(UpdateStatusBar);
pg_settings.SelectedObject = _settings;
pg_settings.PropertyValueChanged += (s, e) =>
{
_settings.Save();
RestartWatchdog();
};
// Build Version (Date of compilation/file creation)
try
{
var buildDate = new FileInfo(this.GetType().Assembly.Location).LastWriteTime;
label_buildVersion.Text = $"Build: {buildDate:yyyy-MM-dd HH:mm:ss}";
}
catch
{
label_buildVersion.Text = "Build: Unknown";
}
UpdateStatusBar();
// Wire up button events
btn_serverstart.Click += Btn_serverstart_Click;
btn_localWebserver.Click += Btn_localWebserver_Click;
Log.Information("MainForm initialized. Ready.");
Log.Information("Press 'Start Server' to begin polling & discovery.");
Log.Information("Press 'Start Local Webserver' to launch the WebUI on {Url}", _settings.WebserverUrl);
Log.Information("Settings: {Path}", PredictalyticsOptions.SettingsFilePath);
_ = UpdateDbSizeAsync();
var dbSizeTimer = new System.Windows.Forms.Timer { Interval = 6 * 60 * 60 * 1000 };
dbSizeTimer.Tick += async (s, e) => await UpdateDbSizeAsync();
dbSizeTimer.Start();
RestartWatchdog();
}
/// <summary>
/// (Re-)creates the Watchdog heartbeat sender from the current settings.
/// Called at startup and whenever settings change.
/// </summary>
private void RestartWatchdog()
{
_watchdog?.Dispose();
_watchdog = null;
if (!_settings.WatchdogEnabled) return;
if (string.IsNullOrWhiteSpace(_settings.WatchdogApiKey) || string.IsNullOrWhiteSpace(_settings.WatchdogUrl))
{
Log.Information("🐕 Watchdog ist aktiviert, aber URL/API Key fehlen — bitte in den Settings eintragen.");
return;
}
_watchdog = new WatchdogHeartbeatService(
_settings.WatchdogUrl,
_settings.WatchdogApiKey,
_settings.WatchdogSource,
_settings.WatchdogInstance,
_settings.WatchdogIntervalSeconds,
metadataProvider: () => new
{
workersRunning = _host.WorkersRunning,
webserverRunning = _host.WebServerRunning
});
_watchdog.Start();
}
private async void Btn_serverstart_Click(object? sender, EventArgs e)
{
if (!_host.WorkersRunning)
{
// Start workers
_workerCts = new CancellationTokenSource();
btn_serverstart.Text = "⏹ Stop Server";
Log.Information("🚀 Starting background workers...");
try
{
await _host.StartWorkersAsync(_workerCts.Token);
}
catch (OperationCanceledException) { }
catch (Exception ex) { Log.Error(ex, "Worker error"); }
}
else
{
// Stop workers
Log.Information("⏹ Stopping background workers...");
_workerCts?.Cancel();
btn_serverstart.Text = "▶ Start Server";
Log.Information("Workers stopped.");
}
UpdateStatusBar();
}
private async void Btn_localWebserver_Click(object? sender, EventArgs e)
{
if (!_host.WebServerRunning)
{
try
{
Log.Information("🌐 Starting embedded Kestrel webserver on {Url}...", _settings.WebserverUrl);
await _host.StartWebServerAsync();
btn_localWebserver.Text = "⏹ Stop Webserver";
Log.Information("✅ WebUI available at {Url}", _settings.WebserverUrl);
Log.Information("📄 Swagger API docs at {Url}/swagger", _settings.WebserverUrl);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to start webserver");
}
}
else
{
Log.Information("⏹ Stopping webserver...");
await _host.StopWebServerAsync();
btn_localWebserver.Text = "▶ Start Webserver";
Log.Information("Webserver stopped.");
}
UpdateStatusBar();
}
private void UpdateStatusBar()
{
var workerStatus = _host.WorkersRunning ? "[RUNNING] Workers" : "[STOPPED] Workers";
var serverStatus = _host.WebServerRunning
? $"[RUNNING] Webserver :{_settings.WebserverPort}"
: "[STOPPED] Webserver";
this.Text = $"Predictalytics Analytics — {workerStatus} | {serverStatus}";
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
_watchdog?.NotifyStopping();
_watchdog?.Dispose();
_watchdog = null;
_workerCts?.Cancel();
_host?.StopWebServerAsync().GetAwaiter().GetResult();
base.OnFormClosing(e);
}
private void btn_openbrowser_Click(object sender, EventArgs e)
{
try
{
OpenInShell(_settings.WebserverUrl);
}
catch (Exception ex)
{
Log.Error(ex, "Fehler beim Öffnen des Browsers");
MessageBox.Show("Browser konnte nicht gestartet werden.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
private void btn_logfolder_Click(object sender, EventArgs e)
{
try
{
var logPath = LoggingSetup.DefaultLogDirectory;
OpenInShell(Directory.Exists(logPath) ? logPath : Environment.CurrentDirectory);
}
catch (Exception ex)
{
Log.Error(ex, "Fehler beim Öffnen des Log-Ordners");
}
}
/// <summary>
/// Oeffnet Pfad oder URL mit der Standardanwendung. UseShellExecute funktioniert
/// unter Windows wie unter Linux (dort ueber xdg-open) — im Gegensatz zum
/// vorherigen direkten Aufruf von explorer.exe.
/// </summary>
private static void OpenInShell(string target) =>
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(target) { UseShellExecute = true });
private async void syncMarketsaToolStripMenuItem_Click(object? sender, EventArgs e)
{
if (_host.WorkersRunning)
{
MessageBox.Show("Market sync cannot be started while background workers are running.",
"Workers Busy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
btn_syncmarkets.Enabled = false;
Log.Information("Manual market sync triggered...");
// Use a temporary CTS for this operation
using var cts = new CancellationTokenSource();
await _host.RunSingleMarketSyncAsync(cts.Token);
Log.Information("Manual market sync completed successfully.");
MessageBox.Show("Market sync completed.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
Log.Error(ex, "Manual market sync failed");
MessageBox.Show($"Error syncing markets: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
btn_syncmarkets.Enabled = true;
}
}
private async void btn_dbUpdate_Click(object? sender, EventArgs e)
{
if (_host.WorkersRunning)
{
MessageBox.Show("Database update cannot be run while background workers are running.",
"Workers Busy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try
{
btn_dbUpdate.Enabled = false;
Log.Information("Manual database update triggered...");
await _host.UpdateDatabaseAsync();
Log.Information("Database updated successfully.");
MessageBox.Show("Database update completed successfully.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
Log.Error(ex, "Manual database update failed");
MessageBox.Show($"Database update failed: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
btn_dbUpdate.Enabled = true;
}
}
private async void btn_recalcAll_Click(object? sender, EventArgs e)
{
if (_host.WorkersRunning)
{
MessageBox.Show("Recalculation cannot be started while background workers are running. Stop the server first.",
"Workers Busy", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var confirm = MessageBox.Show(
"This deletes all DERIVED analytics data (positions, daily snapshots, category stats) " +
"and marks every trader for full recalculation.\n\n" +
"Raw trades and markets are NOT touched.\n\n" +
"After this, start the server: the analytics worker rebuilds every trader with the current " +
"engine (runs in the background, can take several hours for large trader counts).\n\nContinue?",
"Recalculate All Traders", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
if (confirm != DialogResult.Yes) return;
try
{
btn_recalcAll.Enabled = false;
Log.Information("Manual full recalculation reset triggered...");
using var cts = new CancellationTokenSource();
var summary = await _host.RunRecalculateAllTradersAsync(cts.Token);
MessageBox.Show($"Reset complete:\n\n{summary}\n\nNow start the server to rebuild the analytics.",
"Recalculate All Traders", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
Log.Error(ex, "Full recalculation reset failed");
MessageBox.Show($"Recalculation reset failed: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
btn_recalcAll.Enabled = true;
}
}
private async Task UpdateDbSizeAsync()
{
var sizeMb = await _host.GetDatabaseSizeMbAsync();
if (IsDisposed) return;
this.Invoke(() => label_dbSize.Text = sizeMb.HasValue
? $"DB Size: {sizeMb.Value:F2} MB"
: "DB Size: —");
}
}