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; /// 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 application host. /// 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(); } /// /// (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 = _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"); } } /// /// 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. /// 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: โ€”"); } }