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; /// Exposes the terminal RichTextBox for the Serilog sink. 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; } /// /// Called after Serilog is configured. Initializes the embedded web server. /// 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(); } /// /// (Re-)creates the Watchdog heartbeat sender from the current settings. /// Called at startup and whenever settings change. /// 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"); } } }