diff --git a/Dashboard/Services/DatabaseService.NocHealth.cs b/Dashboard/Services/DatabaseService.NocHealth.cs index f678a3c3..33a68263 100644 --- a/Dashboard/Services/DatabaseService.NocHealth.cs +++ b/Dashboard/Services/DatabaseService.NocHealth.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; +using System.Data; using System.Threading.Tasks; using Microsoft.Data.SqlClient; using PerformanceMonitorDashboard.Helpers; @@ -631,7 +632,7 @@ ORDER BY r.total_elapsed_time DESC { using var cmd = new SqlCommand(query, connection); cmd.CommandTimeout = 10; - cmd.Parameters.AddWithValue("@thresholdMs", (long)thresholdMinutes * 60 * 1000); + cmd.Parameters.Add(new SqlParameter("@thresholdMs", SqlDbType.BigInt) { Value = (long)thresholdMinutes * 60 * 1000 }); using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) @@ -685,7 +686,7 @@ ORDER BY percent_of_average DESC { using var cmd = new SqlCommand(query, connection); cmd.CommandTimeout = 10; - cmd.Parameters.AddWithValue("@thresholdPercent", thresholdPercent); + cmd.Parameters.Add(new SqlParameter("@thresholdPercent", SqlDbType.Int) { Value = thresholdPercent }); using var reader = await cmd.ExecuteReaderAsync(); while (await reader.ReadAsync()) diff --git a/Dashboard/Services/DatabaseService.cs b/Dashboard/Services/DatabaseService.cs index ffcd89d7..f255a10b 100644 --- a/Dashboard/Services/DatabaseService.cs +++ b/Dashboard/Services/DatabaseService.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; +using System.Data; using System.Globalization; using System.Threading; using System.Threading.Tasks; @@ -302,10 +303,10 @@ public async Task UpdateCollectorScheduleAsync(int scheduleId, bool enabled, int using var command = new SqlCommand(query, connection); command.CommandTimeout = 120; - command.Parameters.AddWithValue("@schedule_id", scheduleId); - command.Parameters.AddWithValue("@enabled", enabled); - command.Parameters.AddWithValue("@frequency_minutes", frequencyMinutes); - command.Parameters.AddWithValue("@retention_days", retentionDays); + command.Parameters.Add(new SqlParameter("@schedule_id", SqlDbType.Int) { Value = scheduleId }); + command.Parameters.Add(new SqlParameter("@enabled", SqlDbType.Bit) { Value = enabled }); + command.Parameters.Add(new SqlParameter("@frequency_minutes", SqlDbType.Int) { Value = frequencyMinutes }); + command.Parameters.Add(new SqlParameter("@retention_days", SqlDbType.Int) { Value = retentionDays }); await command.ExecuteNonQueryAsync(); } diff --git a/Installer/Program.cs b/Installer/Program.cs index a3b7c97e..db6bceac 100644 --- a/Installer/Program.cs +++ b/Installer/Program.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; +using System.Data; using System.IO; using System.Linq; using System.Net.Http; @@ -1341,16 +1342,16 @@ INSERT INTO PerformanceMonitor.config.installation_history using (var insertCmd = new SqlCommand(insertSql, connection)) { - insertCmd.Parameters.AddWithValue("@installer_version", assemblyVersion); - insertCmd.Parameters.AddWithValue("@installer_info_version", (object?)infoVersion ?? DBNull.Value); - insertCmd.Parameters.AddWithValue("@sql_server_version", sqlVersion); - insertCmd.Parameters.AddWithValue("@sql_server_edition", sqlEdition); - insertCmd.Parameters.AddWithValue("@installation_type", installationType); - insertCmd.Parameters.AddWithValue("@previous_version", (object?)previousVersion ?? DBNull.Value); - insertCmd.Parameters.AddWithValue("@installation_status", status); - insertCmd.Parameters.AddWithValue("@files_executed", filesExecuted); - insertCmd.Parameters.AddWithValue("@files_failed", filesFailed); - insertCmd.Parameters.AddWithValue("@installation_duration_ms", durationMs); + insertCmd.Parameters.Add(new SqlParameter("@installer_version", SqlDbType.NVarChar, 50) { Value = assemblyVersion }); + insertCmd.Parameters.Add(new SqlParameter("@installer_info_version", SqlDbType.NVarChar, 100) { Value = (object?)infoVersion ?? DBNull.Value }); + insertCmd.Parameters.Add(new SqlParameter("@sql_server_version", SqlDbType.NVarChar, 500) { Value = sqlVersion }); + insertCmd.Parameters.Add(new SqlParameter("@sql_server_edition", SqlDbType.NVarChar, 128) { Value = sqlEdition }); + insertCmd.Parameters.Add(new SqlParameter("@installation_type", SqlDbType.VarChar, 20) { Value = installationType }); + insertCmd.Parameters.Add(new SqlParameter("@previous_version", SqlDbType.NVarChar, 50) { Value = (object?)previousVersion ?? DBNull.Value }); + insertCmd.Parameters.Add(new SqlParameter("@installation_status", SqlDbType.VarChar, 20) { Value = status }); + insertCmd.Parameters.Add(new SqlParameter("@files_executed", SqlDbType.Int) { Value = filesExecuted }); + insertCmd.Parameters.Add(new SqlParameter("@files_failed", SqlDbType.Int) { Value = filesFailed }); + insertCmd.Parameters.Add(new SqlParameter("@installation_duration_ms", SqlDbType.BigInt) { Value = durationMs }); await insertCmd.ExecuteNonQueryAsync().ConfigureAwait(false); } diff --git a/Lite/Controls/ServerTab.xaml.cs b/Lite/Controls/ServerTab.xaml.cs index 4b771b0f..b14035fb 100644 --- a/Lite/Controls/ServerTab.xaml.cs +++ b/Lite/Controls/ServerTab.xaml.cs @@ -536,8 +536,8 @@ Include the latest event timestamp so acknowledgement is only DateTime? latestEventTime = null; if (blockingCount > 0 || deadlockCount > 0) { - var latestBlocking = blockedProcessTask.Result.Max(r => (DateTime?)r.CollectionTime); - var latestDeadlock = deadlockTask.Result.Max(r => (DateTime?)r.CollectionTime); + var latestBlocking = blockedProcessTask.Result.Max(r => (DateTime?)r.EventTime); + var latestDeadlock = deadlockTask.Result.Max(r => (DateTime?)r.DeadlockTime); latestEventTime = latestBlocking > latestDeadlock ? latestBlocking : latestDeadlock; } AlertCountsChanged?.Invoke(blockingCount, deadlockCount, latestEventTime); @@ -849,12 +849,19 @@ private void UpdateBlockingTrendChart(List data, int hoursBack, Date if (data.Count == 0) { - /* Show empty chart with correct time range */ + /* No blocking events — show a flat line at zero so the chart looks active */ + var zeroLine = BlockingTrendChart.Plot.Add.Scatter( + new[] { rangeStart.ToOADate(), rangeEnd.ToOADate() }, + new[] { 0.0, 0.0 }); + zeroLine.LegendText = "Blocking Incidents"; + zeroLine.Color = ScottPlot.Color.FromHex("#E57373"); + zeroLine.MarkerSize = 0; BlockingTrendChart.Plot.Axes.DateTimeTicksBottom(); BlockingTrendChart.Plot.Axes.SetLimitsX(rangeStart.ToOADate(), rangeEnd.ToOADate()); - BlockingTrendChart.Plot.Axes.SetLimitsY(0, 1); ReapplyAxisColors(BlockingTrendChart); BlockingTrendChart.Plot.YLabel("Blocking Incidents"); + SetChartYLimitsWithLegendPadding(BlockingTrendChart, 0, 1); + ShowChartLegend(BlockingTrendChart); BlockingTrendChart.Refresh(); return; } @@ -919,12 +926,19 @@ private void UpdateDeadlockTrendChart(List data, int hoursBack, Date if (data.Count == 0) { - /* Show empty chart with correct time range */ + /* No deadlocks — show a flat line at zero so the chart looks active */ + var zeroLine = DeadlockTrendChart.Plot.Add.Scatter( + new[] { rangeStart.ToOADate(), rangeEnd.ToOADate() }, + new[] { 0.0, 0.0 }); + zeroLine.LegendText = "Deadlocks"; + zeroLine.Color = ScottPlot.Color.FromHex("#FFB74D"); + zeroLine.MarkerSize = 0; DeadlockTrendChart.Plot.Axes.DateTimeTicksBottom(); DeadlockTrendChart.Plot.Axes.SetLimitsX(rangeStart.ToOADate(), rangeEnd.ToOADate()); - DeadlockTrendChart.Plot.Axes.SetLimitsY(0, 1); ReapplyAxisColors(DeadlockTrendChart); DeadlockTrendChart.Plot.YLabel("Deadlocks"); + SetChartYLimitsWithLegendPadding(DeadlockTrendChart, 0, 1); + ShowChartLegend(DeadlockTrendChart); DeadlockTrendChart.Refresh(); return; } @@ -1495,13 +1509,13 @@ private static void SetChartYLimitsWithLegendPadding(ScottPlot.WPF.WpfPlot chart dataYMin = limits.Bottom; dataYMax = limits.Top; } - if (dataYMax <= dataYMin) dataYMax = dataYMin + 100; + if (dataYMax <= dataYMin) dataYMax = dataYMin + 1; double range = dataYMax - dataYMin; double topPadding = range * 0.05; - /* Only add bottom padding if dataYMin is above zero - don't go negative */ - double yMin = dataYMin >= 0 ? 0 : dataYMin - (range * 0.10); + /* Add small bottom margin when dataYMin is zero so flat lines at Y=0 are visible above the axis */ + double yMin = dataYMin > 0 ? 0 : dataYMin == 0 ? -(range * 0.05) : dataYMin - (range * 0.10); double yMax = dataYMax + topPadding; chart.Plot.Axes.SetLimitsY(yMin, yMax); diff --git a/Lite/Database/DuckDbInitializer.cs b/Lite/Database/DuckDbInitializer.cs index 809fb1ea..a288429a 100644 --- a/Lite/Database/DuckDbInitializer.cs +++ b/Lite/Database/DuckDbInitializer.cs @@ -19,7 +19,7 @@ public class DuckDbInitializer /// /// Current schema version. Increment this when schema changes require table rebuilds. /// - private const int CurrentSchemaVersion = 9; + private const int CurrentSchemaVersion = 10; public DuckDbInitializer(string databasePath, ILogger? logger = null) { @@ -328,6 +328,21 @@ Safe to ALTER because this table uses INSERT (not appender). */ /* Table doesn't exist yet — will be created with correct schema below */ } } + + if (fromVersion < 10) + { + /* v10: Added server_name column to collection_log so log entries + can be identified by server without needing a lookup table. */ + _logger?.LogInformation("Running migration to v10: adding server_name column to collection_log"); + try + { + await ExecuteNonQueryAsync(connection, "ALTER TABLE collection_log ADD COLUMN IF NOT EXISTS server_name VARCHAR"); + } + catch + { + /* Table doesn't exist yet — will be created with correct schema below */ + } + } } /// diff --git a/Lite/Database/Schema.cs b/Lite/Database/Schema.cs index 658325b3..24f46ab5 100644 --- a/Lite/Database/Schema.cs +++ b/Lite/Database/Schema.cs @@ -38,6 +38,7 @@ modified_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP CREATE TABLE IF NOT EXISTS collection_log ( log_id BIGINT PRIMARY KEY, server_id INTEGER NOT NULL, + server_name VARCHAR, collector_name VARCHAR NOT NULL, collection_time TIMESTAMP NOT NULL, duration_ms INTEGER, diff --git a/Lite/MainWindow.xaml b/Lite/MainWindow.xaml index 55d89105..c57bb020 100644 --- a/Lite/MainWindow.xaml +++ b/Lite/MainWindow.xaml @@ -97,10 +97,10 @@