Bindet die beiden neuen Betriebsprojekte an: - WatchdogHeartbeatService: periodischer POST /api/heartbeat an watchdog.mhdf.de (Dead-Man's-Switch), stopping-Event beim Beenden, Konfiguration ueber AppSettings-Kategorie "Watchdog". Fehler sind best effort und beeintraechtigen die App nie. - LicenseGuard + LicenseDialog: Lizenzpruefung vor dem Start der MainForm, 12h-Revalidierung zur Laufzeit, Checksum-Haertung. Produkt-Slug/Endpoint/Public-Key sind einkompiliert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
345 lines
13 KiB
C#
345 lines
13 KiB
C#
using System.Reflection;
|
|
using Predictalytics.WinFormsHost.Services;
|
|
using Serilog;
|
|
|
|
namespace Predictalytics.WinFormsHost;
|
|
|
|
public partial class MainForm : Form
|
|
{
|
|
private EmbeddedWebServer? _webServer;
|
|
private CancellationTokenSource? _workerCts;
|
|
private bool _workerRunning;
|
|
private bool _webServerRunning;
|
|
private AppSettings _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 embedded web server.
|
|
/// </summary>
|
|
public void Initialize()
|
|
{
|
|
_settings = AppSettings.Load();
|
|
pg_settings.SelectedObject = _settings;
|
|
pg_settings.PropertyValueChanged += (s, e) => {
|
|
_settings.Save();
|
|
if (_webServer != null)
|
|
{
|
|
_webServer.ConnectionString = _settings.ConnectionString;
|
|
_webServer.DbConnectionDebug = _settings.DbConnectionDebug;
|
|
_webServer.EgressChannelsText = _settings.EgressChannelsText;
|
|
}
|
|
RestartWatchdog();
|
|
};
|
|
|
|
_webServer = new EmbeddedWebServer();
|
|
_webServer.ConnectionString = _settings.ConnectionString;
|
|
_webServer.DbConnectionDebug = _settings.DbConnectionDebug;
|
|
_webServer.EgressChannelsText = _settings.EgressChannelsText;
|
|
|
|
// 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 http://localhost:{Port}", _settings.WebserverPort);
|
|
|
|
_ = 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 = _workerRunning,
|
|
webserverRunning = _webServerRunning
|
|
});
|
|
_watchdog.Start();
|
|
}
|
|
|
|
private async void Btn_serverstart_Click(object? sender, EventArgs e)
|
|
{
|
|
if (!_workerRunning)
|
|
{
|
|
// Start workers
|
|
_workerCts = new CancellationTokenSource();
|
|
_workerRunning = true;
|
|
btn_serverstart.Text = "⏹ Stop Server";
|
|
Log.Information("🚀 Starting background workers...");
|
|
|
|
try
|
|
{
|
|
_webServer!.ConnectionString = _settings.ConnectionString;
|
|
_webServer!.DbConnectionDebug = _settings.DbConnectionDebug;
|
|
_webServer!.EgressChannelsText = _settings.EgressChannelsText;
|
|
await _webServer!.StartWorkersAsync(_workerCts.Token);
|
|
}
|
|
catch (OperationCanceledException) { }
|
|
catch (Exception ex) { Log.Error(ex, "Worker error"); }
|
|
}
|
|
else
|
|
{
|
|
// Stop workers
|
|
Log.Information("⏹ Stopping background workers...");
|
|
_workerCts?.Cancel();
|
|
_workerRunning = false;
|
|
btn_serverstart.Text = "▶ Start Server";
|
|
Log.Information("Workers stopped.");
|
|
}
|
|
UpdateStatusBar();
|
|
}
|
|
|
|
private async void Btn_localWebserver_Click(object? sender, EventArgs e)
|
|
{
|
|
if (!_webServerRunning)
|
|
{
|
|
try
|
|
{
|
|
Log.Information("🌐 Starting embedded Kestrel webserver on http://localhost:{Port}...", _settings.WebserverPort);
|
|
await _webServer!.StartWebServerAsync(_settings.WebserverPort);
|
|
_webServerRunning = true;
|
|
btn_localWebserver.Text = "⏹ Stop Webserver";
|
|
Log.Information("✅ WebUI available at http://localhost:{Port}", _settings.WebserverPort);
|
|
Log.Information("📄 Swagger API docs at http://localhost:{Port}/swagger", _settings.WebserverPort);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.Error(ex, "Failed to start webserver");
|
|
_webServerRunning = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log.Information("⏹ Stopping webserver...");
|
|
await _webServer!.StopWebServerAsync();
|
|
_webServerRunning = false;
|
|
btn_localWebserver.Text = "▶ Start Webserver";
|
|
Log.Information("Webserver stopped.");
|
|
}
|
|
UpdateStatusBar();
|
|
}
|
|
|
|
private void UpdateStatusBar()
|
|
{
|
|
var workerStatus = _workerRunning ? "[RUNNING] Workers" : "[STOPPED] Workers";
|
|
var serverStatus = _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();
|
|
_webServer?.StopWebServerAsync().GetAwaiter().GetResult();
|
|
base.OnFormClosing(e);
|
|
}
|
|
|
|
private void btn_openbrowser_Click(object sender, EventArgs e)
|
|
{
|
|
try
|
|
{
|
|
System.Diagnostics.Process.Start("explorer.exe", $"\"http://localhost:{_settings.WebserverPort}\"");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Serilog.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 = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs");
|
|
if (Directory.Exists(logPath))
|
|
System.Diagnostics.Process.Start("explorer.exe", logPath);
|
|
else
|
|
System.Diagnostics.Process.Start("explorer.exe", Environment.CurrentDirectory);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Serilog.Log.Error(ex, "Fehler beim Öffnen des Log-Ordners");
|
|
}
|
|
}
|
|
|
|
private async void syncMarketsaToolStripMenuItem_Click(object? sender, EventArgs e)
|
|
{
|
|
if (_workerRunning)
|
|
{
|
|
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 _webServer!.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 (_workerRunning)
|
|
{
|
|
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 _webServer!.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 (_workerRunning)
|
|
{
|
|
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 _webServer!.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()
|
|
{
|
|
try
|
|
{
|
|
// Build the connection string
|
|
var csBuilder = new MySqlConnector.MySqlConnectionStringBuilder(_settings.ConnectionString);
|
|
if (string.IsNullOrWhiteSpace(csBuilder.Database))
|
|
return; // Not ready or valid yet
|
|
|
|
using var conn = new MySqlConnector.MySqlConnection(_settings.ConnectionString);
|
|
await conn.OpenAsync();
|
|
using var cmd = conn.CreateCommand();
|
|
cmd.CommandText = "SELECT SUM(data_length + index_length) / 1024 / 1024 FROM information_schema.tables WHERE table_schema = DATABASE();";
|
|
|
|
var result = await cmd.ExecuteScalarAsync();
|
|
if (result != DBNull.Value && result != null)
|
|
{
|
|
var sizeMb = Convert.ToDouble(result);
|
|
this.Invoke(() => label_dbSize.Text = $"DB Size: {sizeMb:F2} MB");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
this.Invoke(() => label_dbSize.Text = "DB Size: Error");
|
|
Log.Debug(ex, "Failed to fetch DB size for status bar");
|
|
}
|
|
}
|
|
}
|