diff --git a/Dashboard/CollectorScheduleWindow.xaml.cs b/Dashboard/CollectorScheduleWindow.xaml.cs
index a74f402..0a0a912 100644
--- a/Dashboard/CollectorScheduleWindow.xaml.cs
+++ b/Dashboard/CollectorScheduleWindow.xaml.cs
@@ -123,12 +123,17 @@ public partial class CollectorScheduleWindow : Window
}
};
- public CollectorScheduleWindow(DatabaseService databaseService)
+ public CollectorScheduleWindow(DatabaseService databaseService, string? serverDisplayName = null)
{
InitializeComponent();
_databaseService = databaseService;
Loaded += CollectorScheduleWindow_Loaded;
Closing += CollectorScheduleWindow_Closing;
+
+ if (!string.IsNullOrEmpty(serverDisplayName))
+ {
+ Title = $"Collector Schedules - {serverDisplayName}";
+ }
}
private void CollectorScheduleWindow_Closing(object? sender, CancelEventArgs e)
diff --git a/Dashboard/ServerTab.xaml.cs b/Dashboard/ServerTab.xaml.cs
index af1aebe..f23356a 100644
--- a/Dashboard/ServerTab.xaml.cs
+++ b/Dashboard/ServerTab.xaml.cs
@@ -1665,7 +1665,7 @@ private void HealthDataGrid_MouseDoubleClick(object sender, System.Windows.Input
private void EditSchedules_Click(object sender, RoutedEventArgs e)
{
- var scheduleWindow = new CollectorScheduleWindow(_databaseService);
+ var scheduleWindow = new CollectorScheduleWindow(_databaseService, _serverConnection.DisplayName);
scheduleWindow.Owner = Window.GetWindow(this);
scheduleWindow.ShowDialog();
}
diff --git a/Lite/MainWindow.xaml.cs b/Lite/MainWindow.xaml.cs
index f86b1a4..2647faf 100644
--- a/Lite/MainWindow.xaml.cs
+++ b/Lite/MainWindow.xaml.cs
@@ -557,7 +557,7 @@ private async void ConnectToServer(ServerConnection server)
{
if (_collectorService != null)
{
- var onLoadCollectors = _scheduleManager.GetOnLoadCollectors();
+ var onLoadCollectors = _scheduleManager.GetOnLoadCollectorsForServer(server.Id);
foreach (var collector in onLoadCollectors)
{
try
@@ -848,7 +848,7 @@ private void ManageServersButton_Click(object sender, RoutedEventArgs e)
private async void SettingsButton_Click(object sender, RoutedEventArgs e)
{
- var window = new SettingsWindow(_scheduleManager, _backgroundService, _mcpService, _muteRuleService) { Owner = this };
+ var window = new SettingsWindow(_scheduleManager, _serverManager, _backgroundService, _mcpService, _muteRuleService) { Owner = this };
window.ShowDialog();
UpdateStatusBar();
diff --git a/Lite/Models/ServerScheduleOverride.cs b/Lite/Models/ServerScheduleOverride.cs
new file mode 100644
index 0000000..d661220
--- /dev/null
+++ b/Lite/Models/ServerScheduleOverride.cs
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor Lite.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace PerformanceMonitorLite.Models;
+
+///
+/// Per-server schedule override. Contains the full collector list for one server.
+/// When present, replaces the default schedule entirely for that server.
+///
+public class ServerScheduleOverride
+{
+ [JsonPropertyName("collectors")]
+ public List Collectors { get; set; } = new();
+}
diff --git a/Lite/Services/RemoteCollectorService.BlockedProcessReport.cs b/Lite/Services/RemoteCollectorService.BlockedProcessReport.cs
index cd5c90a..5ed567f 100644
--- a/Lite/Services/RemoteCollectorService.BlockedProcessReport.cs
+++ b/Lite/Services/RemoteCollectorService.BlockedProcessReport.cs
@@ -32,7 +32,7 @@ public partial class RemoteCollectorService
public async Task EnsureBlockedProcessXeSessionAsync(ServerConnection server, int engineEdition = 0, CancellationToken cancellationToken = default)
{
/* Skip if the blocked_process_report collector is disabled */
- var schedule = _scheduleManager.GetSchedule("blocked_process_report");
+ var schedule = _scheduleManager.GetScheduleForServer(server.Id, "blocked_process_report");
if (schedule == null || !schedule.Enabled)
{
return;
diff --git a/Lite/Services/RemoteCollectorService.Deadlocks.cs b/Lite/Services/RemoteCollectorService.Deadlocks.cs
index 22c8e4d..68bd656 100644
--- a/Lite/Services/RemoteCollectorService.Deadlocks.cs
+++ b/Lite/Services/RemoteCollectorService.Deadlocks.cs
@@ -32,7 +32,7 @@ public partial class RemoteCollectorService
public async Task EnsureDeadlockXeSessionAsync(ServerConnection server, int engineEdition = 0, CancellationToken cancellationToken = default)
{
/* Skip if the deadlock collector is disabled */
- var schedule = _scheduleManager.GetSchedule("deadlocks");
+ var schedule = _scheduleManager.GetScheduleForServer(server.Id, "deadlocks");
if (schedule == null || !schedule.Enabled)
{
return;
diff --git a/Lite/Services/RemoteCollectorService.cs b/Lite/Services/RemoteCollectorService.cs
index afbd489..be1f957 100644
--- a/Lite/Services/RemoteCollectorService.cs
+++ b/Lite/Services/RemoteCollectorService.cs
@@ -235,10 +235,9 @@ Record the error message so the user can see what's wrong. */
///
public async Task RunDueCollectorsAsync(CancellationToken cancellationToken = default)
{
- var dueCollectors = _scheduleManager.GetDueCollectors();
var enabledServers = _serverManager.GetEnabledServers();
- if (dueCollectors.Count == 0 || enabledServers.Count == 0)
+ if (enabledServers.Count == 0)
{
return;
}
@@ -258,15 +257,22 @@ public async Task RunDueCollectorsAsync(CancellationToken cancellationToken = de
onlineServers.Add(server);
}
- _logger?.LogInformation("Running {CollectorCount} collectors for {OnlineCount}/{TotalCount} servers ({SkippedCount} offline, skipped)",
- dueCollectors.Count, onlineServers.Count, enabledServers.Count, skippedOffline);
+ if (onlineServers.Count == 0)
+ {
+ return;
+ }
+
+ _logger?.LogInformation("Checking per-server schedules for {OnlineCount}/{TotalCount} servers ({SkippedCount} offline, skipped)",
+ onlineServers.Count, enabledServers.Count, skippedOffline);
/* Run servers in parallel, but collectors within each server sequentially.
DuckDB is single-writer; running all collectors in parallel causes spin-wait
contention (50%+ CPU, multi-second stalls). Sequential per-server eliminates
- this while still allowing multi-server parallelism. */
+ this while still allowing multi-server parallelism.
+ Each server gets its own due-collector list from per-server schedules. */
var serverTasks = onlineServers.Select(server => Task.Run(async () =>
{
+ var dueCollectors = _scheduleManager.GetDueCollectorsForServer(server.Id);
foreach (var collector in dueCollectors)
{
await RunCollectorAsync(server, collector.Name, cancellationToken);
@@ -299,8 +305,8 @@ CHECKPOINT reorganizes/truncates the database file. */
///
public async Task RunAllCollectorsForServerAsync(ServerConnection server, CancellationToken cancellationToken = default)
{
- var enabledSchedules = _scheduleManager.GetEnabledSchedules()
- .Concat(_scheduleManager.GetOnLoadCollectors())
+ var enabledSchedules = _scheduleManager.GetSchedulesForServer(server.Id)
+ .Where(s => s.Enabled)
.ToList();
/* Ensure XE sessions are set up before collecting */
@@ -396,7 +402,7 @@ public async Task RunCollectorAsync(ServerConnection server, string collectorNam
_ => throw new ArgumentException($"Unknown collector: {collectorName}")
};
- _scheduleManager.MarkCollectorRun(collectorName, startTime);
+ _scheduleManager.MarkCollectorRunForServer(server.Id, collectorName, startTime);
var elapsed = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
AppLogger.Info("Collector", $" [{server.DisplayName}] {collectorName} => {rowsCollected} rows in {elapsed}ms (sql:{_lastSqlMs}ms, duck:{_lastDuckDbMs}ms)");
diff --git a/Lite/Services/ScheduleManager.cs b/Lite/Services/ScheduleManager.cs
index 6fd8c84..d6a2bbd 100644
--- a/Lite/Services/ScheduleManager.cs
+++ b/Lite/Services/ScheduleManager.cs
@@ -19,6 +19,7 @@ namespace PerformanceMonitorLite.Services;
///
/// Manages collector schedules and determines when each collector should run.
+/// Supports per-server schedule overrides (v2 config format).
///
public class ScheduleManager
{
@@ -63,82 +64,96 @@ public class ScheduleManager
private readonly string _schedulePath;
private readonly ILogger? _logger;
- private List _schedules;
private readonly object _lock = new();
+ private List _defaultSchedule;
+ private Dictionary _serverOverrides;
+
+ ///
+ /// Per-server runtime state: serverId → (collectorName → lastRunTime).
+ /// Kept separate from config because runtime state is not persisted to JSON.
+ ///
+ private readonly Dictionary> _serverRunState = new();
+
public ScheduleManager(string configDirectory, ILogger? logger = null)
{
_schedulePath = Path.Combine(configDirectory, "collection_schedule.json");
_logger = logger;
- _schedules = new List();
+ _defaultSchedule = new List();
+ _serverOverrides = new Dictionary();
LoadSchedules();
}
+ // ──────────────────────────────────────────────────────────────────
+ // Existing public API — operates on the default schedule.
+ // These methods are unchanged from v1 so existing callers keep working.
+ // ──────────────────────────────────────────────────────────────────
+
///
- /// Gets all configured collector schedules.
+ /// Gets all configured collector schedules (default schedule).
///
public IReadOnlyList GetAllSchedules()
{
lock (_lock)
{
- return _schedules.ToList();
+ return _defaultSchedule.ToList();
}
}
///
- /// Gets only enabled and scheduled collectors.
+ /// Gets only enabled and scheduled collectors (default schedule).
///
public IReadOnlyList GetEnabledSchedules()
{
lock (_lock)
{
- return _schedules.Where(s => s.Enabled && s.IsScheduled).ToList();
+ return _defaultSchedule.Where(s => s.Enabled && s.IsScheduled).ToList();
}
}
///
- /// Gets collectors that are due to run.
+ /// Gets collectors that are due to run (default schedule, global run state).
///
public IReadOnlyList GetDueCollectors()
{
lock (_lock)
{
- return _schedules.Where(s => s.IsDue).ToList();
+ return _defaultSchedule.Where(s => s.IsDue).ToList();
}
}
///
- /// Gets on-load only collectors (frequency = 0).
+ /// Gets on-load only collectors (frequency = 0) from the default schedule.
///
public IReadOnlyList GetOnLoadCollectors()
{
lock (_lock)
{
- return _schedules.Where(s => s.Enabled && !s.IsScheduled).ToList();
+ return _defaultSchedule.Where(s => s.Enabled && !s.IsScheduled).ToList();
}
}
///
- /// Gets a specific collector schedule by name.
+ /// Gets a specific collector schedule by name (default schedule).
///
public CollectorSchedule? GetSchedule(string collectorName)
{
lock (_lock)
{
- return _schedules.FirstOrDefault(s =>
+ return _defaultSchedule.FirstOrDefault(s =>
s.Name.Equals(collectorName, StringComparison.OrdinalIgnoreCase));
}
}
///
- /// Marks a collector as having been run.
+ /// Marks a collector as having been run (default schedule, global run state).
///
public void MarkCollectorRun(string collectorName, DateTime runTime)
{
lock (_lock)
{
- var schedule = _schedules.FirstOrDefault(s =>
+ var schedule = _defaultSchedule.FirstOrDefault(s =>
s.Name.Equals(collectorName, StringComparison.OrdinalIgnoreCase));
if (schedule != null)
@@ -157,13 +172,13 @@ public void MarkCollectorRun(string collectorName, DateTime runTime)
}
///
- /// Updates a collector's schedule settings.
+ /// Updates a collector's schedule settings (default schedule).
///
public void UpdateSchedule(string collectorName, bool? enabled = null, int? frequencyMinutes = null, int? retentionDays = null)
{
lock (_lock)
{
- var schedule = _schedules.FirstOrDefault(s =>
+ var schedule = _defaultSchedule.FirstOrDefault(s =>
s.Name.Equals(collectorName, StringComparison.OrdinalIgnoreCase));
if (schedule == null)
@@ -194,33 +209,18 @@ public void UpdateSchedule(string collectorName, bool? enabled = null, int? freq
}
///
- /// Detects which preset matches the current intervals, or returns "Custom".
+ /// Detects which preset matches the current default schedule intervals, or returns "Custom".
///
public string GetActivePreset()
{
lock (_lock)
{
- foreach (var (presetName, intervals) in s_presets)
- {
- bool matches = true;
- foreach (var (collector, freq) in intervals)
- {
- var schedule = _schedules.FirstOrDefault(s =>
- s.Name.Equals(collector, StringComparison.OrdinalIgnoreCase));
- if (schedule != null && schedule.FrequencyMinutes != freq)
- {
- matches = false;
- break;
- }
- }
- if (matches) return presetName;
- }
- return "Custom";
+ return DetectPreset(_defaultSchedule);
}
}
///
- /// Applies a named preset, changing all scheduled collector frequencies.
+ /// Applies a named preset to the default schedule.
/// Does not modify enabled/disabled state or on-load (frequency=0) collectors.
///
public void ApplyPreset(string presetName)
@@ -232,31 +232,287 @@ public void ApplyPreset(string presetName)
lock (_lock)
{
- foreach (var (collector, freq) in intervals)
+ ApplyPresetToList(_defaultSchedule, intervals);
+ SaveSchedules();
+
+ _logger?.LogInformation("Applied collection preset '{Preset}' to default schedule", presetName);
+ }
+ }
+
+ // ──────────────────────────────────────────────────────────────────
+ // New per-server API (v2)
+ // ──────────────────────────────────────────────────────────────────
+
+ ///
+ /// Returns the default schedule list.
+ ///
+ public IReadOnlyList GetDefaultSchedule()
+ {
+ lock (_lock)
+ {
+ return _defaultSchedule.ToList();
+ }
+ }
+
+ ///
+ /// Returns the schedule for a specific server.
+ /// If the server has an override, returns those collectors; otherwise returns a copy of the default.
+ ///
+ public IReadOnlyList GetSchedulesForServer(string serverId)
+ {
+ lock (_lock)
+ {
+ if (_serverOverrides.TryGetValue(serverId, out var over))
{
- var schedule = _schedules.FirstOrDefault(s =>
- s.Name.Equals(collector, StringComparison.OrdinalIgnoreCase));
- if (schedule != null)
+ return over.Collectors.ToList();
+ }
+
+ return CloneScheduleList(_defaultSchedule);
+ }
+ }
+
+ ///
+ /// Gets collectors that are due to run for a specific server, using per-server run state.
+ ///
+ public IReadOnlyList GetDueCollectorsForServer(string serverId)
+ {
+ lock (_lock)
+ {
+ var schedules = _serverOverrides.TryGetValue(serverId, out var over)
+ ? over.Collectors
+ : _defaultSchedule;
+
+ _serverRunState.TryGetValue(serverId, out var runState);
+
+ var due = new List();
+ foreach (var s in schedules)
+ {
+ if (!s.Enabled || !s.IsScheduled)
+ continue;
+
+ if (runState == null || !runState.TryGetValue(s.Name, out var lastRun))
{
- schedule.FrequencyMinutes = freq;
+ due.Add(s); // never run — due immediately
+ continue;
}
+
+ var elapsed = DateTime.UtcNow - lastRun;
+ if (elapsed.TotalMinutes >= s.FrequencyMinutes)
+ {
+ due.Add(s);
+ }
+ }
+
+ return due;
+ }
+ }
+
+ ///
+ /// Gets on-load only collectors for a specific server.
+ ///
+ public IReadOnlyList GetOnLoadCollectorsForServer(string serverId)
+ {
+ lock (_lock)
+ {
+ var schedules = _serverOverrides.TryGetValue(serverId, out var over)
+ ? over.Collectors
+ : _defaultSchedule;
+
+ return schedules.Where(s => s.Enabled && !s.IsScheduled).ToList();
+ }
+ }
+
+ ///
+ /// Gets a specific collector schedule by name for a server.
+ ///
+ public CollectorSchedule? GetScheduleForServer(string serverId, string collectorName)
+ {
+ lock (_lock)
+ {
+ var schedules = _serverOverrides.TryGetValue(serverId, out var over)
+ ? over.Collectors
+ : _defaultSchedule;
+
+ return schedules.FirstOrDefault(s =>
+ s.Name.Equals(collectorName, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+
+ ///
+ /// Records a collector run for a specific server.
+ ///
+ public void MarkCollectorRunForServer(string serverId, string collectorName, DateTime runTime)
+ {
+ lock (_lock)
+ {
+ if (!_serverRunState.TryGetValue(serverId, out var runState))
+ {
+ runState = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ _serverRunState[serverId] = runState;
}
+ runState[collectorName] = runTime;
+
+ _logger?.LogDebug("Marked collector '{Name}' as run for server {ServerId} at {Time}",
+ collectorName, serverId, runTime);
+ }
+ }
+
+ ///
+ /// Creates or updates a per-server schedule override.
+ ///
+ public void SetScheduleForServer(string serverId, List schedules)
+ {
+ lock (_lock)
+ {
+ _serverOverrides[serverId] = new ServerScheduleOverride { Collectors = schedules };
SaveSchedules();
- _logger?.LogInformation("Applied collection preset '{Preset}'", presetName);
+ _logger?.LogInformation("Set schedule override for server {ServerId} ({Count} collectors)",
+ serverId, schedules.Count);
}
}
///
- /// Loads schedules from the JSON config file.
+ /// Removes a server's schedule override, reverting it to the default.
+ ///
+ public void RemoveServerOverride(string serverId)
+ {
+ lock (_lock)
+ {
+ if (_serverOverrides.Remove(serverId))
+ {
+ SaveSchedules();
+ _logger?.LogInformation("Removed schedule override for server {ServerId}", serverId);
+ }
+ }
+ }
+
+ ///
+ /// Returns true if the server has a custom schedule override.
+ ///
+ public bool HasServerOverride(string serverId)
+ {
+ lock (_lock)
+ {
+ return _serverOverrides.ContainsKey(serverId);
+ }
+ }
+
+ ///
+ /// Applies a preset to a single server's schedule.
+ /// Creates an override if one doesn't exist (copies default first).
+ ///
+ public void ApplyPresetForServer(string serverId, string presetName)
+ {
+ if (!s_presets.TryGetValue(presetName, out var intervals))
+ {
+ throw new ArgumentException($"Unknown preset: {presetName}");
+ }
+
+ lock (_lock)
+ {
+ if (!_serverOverrides.TryGetValue(serverId, out var over))
+ {
+ over = new ServerScheduleOverride { Collectors = CloneScheduleList(_defaultSchedule) };
+ _serverOverrides[serverId] = over;
+ }
+
+ ApplyPresetToList(over.Collectors, intervals);
+ SaveSchedules();
+
+ _logger?.LogInformation("Applied preset '{Preset}' to server {ServerId}", presetName, serverId);
+ }
+ }
+
+ ///
+ /// Applies a preset to the default schedule (alias for ApplyPreset).
+ ///
+ public void ApplyPresetToDefault(string presetName)
+ {
+ ApplyPreset(presetName);
+ }
+
+ ///
+ /// Detects which preset matches a server's active schedule.
+ ///
+ public string GetActivePresetForServer(string serverId)
+ {
+ lock (_lock)
+ {
+ var schedules = _serverOverrides.TryGetValue(serverId, out var over)
+ ? over.Collectors
+ : _defaultSchedule;
+
+ return DetectPreset(schedules);
+ }
+ }
+
+ ///
+ /// Removes orphaned overrides for servers that no longer exist.
+ ///
+ public void CleanupRemovedServers(IEnumerable activeServerIds)
+ {
+ lock (_lock)
+ {
+ var activeSet = new HashSet(activeServerIds);
+ var orphaned = _serverOverrides.Keys.Where(id => !activeSet.Contains(id)).ToList();
+
+ if (orphaned.Count == 0)
+ return;
+
+ foreach (var id in orphaned)
+ {
+ _serverOverrides.Remove(id);
+ _serverRunState.Remove(id);
+ }
+
+ SaveSchedules();
+
+ _logger?.LogInformation("Cleaned up {Count} orphaned server schedule override(s)", orphaned.Count);
+ }
+ }
+
+ // ──────────────────────────────────────────────────────────────────
+ // Persistence
+ // ──────────────────────────────────────────────────────────────────
+
+ ///
+ /// Saves schedules to the JSON config file (v2 format).
+ ///
+ public void SaveSchedules()
+ {
+ lock (_lock)
+ {
+ try
+ {
+ var config = new ScheduleConfigV2
+ {
+ Version = 2,
+ DefaultSchedule = _defaultSchedule,
+ ServerOverrides = _serverOverrides
+ };
+ string json = JsonSerializer.Serialize(config, s_jsonOptions);
+ File.WriteAllText(_schedulePath, json);
+ }
+ catch (Exception ex)
+ {
+ _logger?.LogError(ex, "Failed to save collection_schedule.json");
+ throw;
+ }
+ }
+ }
+
+ ///
+ /// Loads schedules from the JSON config file, handling v1→v2 migration.
///
private void LoadSchedules()
{
if (!File.Exists(_schedulePath))
{
_logger?.LogInformation("Schedule file not found, using defaults");
- _schedules = GetDefaultSchedules();
+ _defaultSchedule = GetDefaultSchedules();
+ _serverOverrides = new Dictionary();
SaveSchedules();
return;
}
@@ -264,16 +520,35 @@ private void LoadSchedules()
try
{
string json = File.ReadAllText(_schedulePath);
- var config = JsonSerializer.Deserialize(json);
- _schedules = config?.Collectors ?? GetDefaultSchedules();
- /* Create backup of valid config */
- try { File.Copy(_schedulePath, _schedulePath + ".bak", overwrite: true); }
- catch { /* best effort */ }
+ if (TryLoadV2(json))
+ {
+ /* Create backup of valid config */
+ try { File.Copy(_schedulePath, _schedulePath + ".bak", overwrite: true); }
+ catch { /* best effort */ }
+
+ _logger?.LogInformation(
+ "Loaded v2 schedule config: {DefaultCount} default collectors, {OverrideCount} server override(s)",
+ _defaultSchedule.Count, _serverOverrides.Count);
+ }
+ else
+ {
+ /* v1 format — migrate */
+ var v1Config = JsonSerializer.Deserialize(json);
+ _defaultSchedule = v1Config?.Collectors ?? GetDefaultSchedules();
+ _serverOverrides = new Dictionary();
- _logger?.LogInformation("Loaded {Count} collector schedules from configuration", _schedules.Count);
+ /* Backup the v1 file before overwriting */
+ try { File.Copy(_schedulePath, _schedulePath + ".v1.bak", overwrite: true); }
+ catch { /* best effort */ }
+
+ SaveSchedules();
+
+ _logger?.LogInformation(
+ "Migrated v1 schedule config to v2: {Count} collectors moved to default_schedule",
+ _defaultSchedule.Count);
+ }
- /* Add any new default collectors that are missing from the saved config */
MergeNewDefaults();
}
catch (Exception ex)
@@ -287,79 +562,167 @@ private void LoadSchedules()
try
{
string bakJson = File.ReadAllText(bakPath);
- var bakConfig = JsonSerializer.Deserialize(bakJson);
- _schedules = bakConfig?.Collectors ?? GetDefaultSchedules();
- _logger?.LogInformation("Restored schedules from backup file");
+ if (TryLoadV2(bakJson))
+ {
+ _logger?.LogInformation("Restored schedules from backup file");
+ return;
+ }
+
+ var bakConfig = JsonSerializer.Deserialize(bakJson);
+ _defaultSchedule = bakConfig?.Collectors ?? GetDefaultSchedules();
+ _serverOverrides = new Dictionary();
+ _logger?.LogInformation("Restored v1 schedules from backup file");
return;
}
catch { /* backup also corrupt, fall through to defaults */ }
}
- _schedules = GetDefaultSchedules();
+ _defaultSchedule = GetDefaultSchedules();
+ _serverOverrides = new Dictionary();
SaveSchedules();
}
}
///
- /// Merges any new default collectors that are missing from the loaded config,
- /// and removes any obsolete/renamed collectors that no longer have a dispatch case.
- /// This handles the case where new collectors are added to the code but the user
- /// has an existing config file from an older version.
+ /// Attempts to load JSON as v2 format. Returns true if successful.
+ ///
+ private bool TryLoadV2(string json)
+ {
+ using var doc = JsonDocument.Parse(json);
+ if (!doc.RootElement.TryGetProperty("version", out var versionProp) || versionProp.GetInt32() < 2)
+ return false;
+
+ var config = JsonSerializer.Deserialize(json);
+ if (config == null)
+ return false;
+
+ _defaultSchedule = config.DefaultSchedule ?? GetDefaultSchedules();
+ _serverOverrides = config.ServerOverrides ?? new Dictionary();
+ return true;
+ }
+
+ ///
+ /// Merges any new default collectors into the default schedule and all server overrides.
+ /// Also removes obsolete collectors that no longer have a dispatch case.
///
private void MergeNewDefaults()
{
var defaults = GetDefaultSchedules();
var defaultNames = new HashSet(defaults.Select(s => s.Name), StringComparer.OrdinalIgnoreCase);
- var loadedNames = new HashSet(_schedules.Select(s => s.Name), StringComparer.OrdinalIgnoreCase);
var changed = false;
- /* Remove obsolete collectors that are no longer in the defaults
- (e.g., blocking_snapshot was renamed to blocked_process_report) */
- var removed = _schedules.RemoveAll(s => !defaultNames.Contains(s.Name));
+ /* Merge into default schedule */
+ changed |= MergeIntoList(_defaultSchedule, defaults, defaultNames);
+
+ /* Merge into each server override */
+ foreach (var over in _serverOverrides.Values)
+ {
+ changed |= MergeIntoList(over.Collectors, defaults, defaultNames);
+ }
+
+ if (changed)
+ {
+ SaveSchedules();
+ }
+ }
+
+ ///
+ /// Merges new defaults into a collector list. Removes obsolete, adds missing.
+ /// Returns true if any changes were made.
+ ///
+ private bool MergeIntoList(List list, List defaults, HashSet defaultNames)
+ {
+ var loadedNames = new HashSet(list.Select(s => s.Name), StringComparer.OrdinalIgnoreCase);
+ var changed = false;
+
+ /* Remove obsolete collectors */
+ var removed = list.RemoveAll(s => !defaultNames.Contains(s.Name));
if (removed > 0)
{
_logger?.LogInformation("Removed {Count} obsolete collector(s) from schedule", removed);
changed = true;
}
- /* Add any new default collectors that are missing */
+ /* Add missing collectors */
foreach (var defaultSchedule in defaults)
{
if (!loadedNames.Contains(defaultSchedule.Name))
{
- _schedules.Add(defaultSchedule);
+ list.Add(CloneSchedule(defaultSchedule));
_logger?.LogInformation("Added missing collector '{Name}' from defaults", defaultSchedule.Name);
changed = true;
}
}
- if (changed)
- {
- SaveSchedules();
- }
+ return changed;
}
+ // ──────────────────────────────────────────────────────────────────
+ // Helpers
+ // ──────────────────────────────────────────────────────────────────
+
///
- /// Saves schedules to the JSON config file.
+ /// Detects which preset matches a list of collector schedules.
///
- public void SaveSchedules()
+ private static string DetectPreset(List schedules)
{
- lock (_lock)
+ foreach (var (presetName, intervals) in s_presets)
{
- try
+ bool matches = true;
+ foreach (var (collector, freq) in intervals)
{
- var config = new ScheduleConfig { Collectors = _schedules };
- string json = JsonSerializer.Serialize(config, s_jsonOptions);
- File.WriteAllText(_schedulePath, json);
+ var schedule = schedules.FirstOrDefault(s =>
+ s.Name.Equals(collector, StringComparison.OrdinalIgnoreCase));
+ if (schedule != null && schedule.FrequencyMinutes != freq)
+ {
+ matches = false;
+ break;
+ }
}
- catch (Exception ex)
+ if (matches) return presetName;
+ }
+ return "Custom";
+ }
+
+ ///
+ /// Applies preset intervals to a collector list. Does not touch enabled/disabled or on-load collectors.
+ ///
+ private static void ApplyPresetToList(List schedules, Dictionary intervals)
+ {
+ foreach (var (collector, freq) in intervals)
+ {
+ var schedule = schedules.FirstOrDefault(s =>
+ s.Name.Equals(collector, StringComparison.OrdinalIgnoreCase));
+ if (schedule != null)
{
- _logger?.LogError(ex, "Failed to save collection_schedule.json");
- throw;
+ schedule.FrequencyMinutes = freq;
}
}
}
+ ///
+ /// Deep-clones a schedule list (for creating overrides from defaults).
+ ///
+ private static List CloneScheduleList(List source)
+ {
+ return source.Select(CloneSchedule).ToList();
+ }
+
+ ///
+ /// Deep-clones a single CollectorSchedule (config properties only, not runtime state).
+ ///
+ private static CollectorSchedule CloneSchedule(CollectorSchedule s)
+ {
+ return new CollectorSchedule
+ {
+ Name = s.Name,
+ Enabled = s.Enabled,
+ FrequencyMinutes = s.FrequencyMinutes,
+ RetentionDays = s.RetentionDays,
+ Description = s.Description
+ };
+ }
+
///
/// Gets the default collector schedules.
///
@@ -393,12 +756,31 @@ private static List GetDefaultSchedules()
};
}
+ // ──────────────────────────────────────────────────────────────────
+ // JSON config models
+ // ──────────────────────────────────────────────────────────────────
+
///
- /// JSON wrapper for schedules list.
+ /// v1 JSON format: { "collectors": [...] }
///
- private class ScheduleConfig
+ private class ScheduleConfigV1
{
[JsonPropertyName("collectors")]
public List Collectors { get; set; } = new();
}
+
+ ///
+ /// v2 JSON format: { "version": 2, "default_schedule": [...], "server_overrides": { "guid": { "collectors": [...] } } }
+ ///
+ private class ScheduleConfigV2
+ {
+ [JsonPropertyName("version")]
+ public int Version { get; set; } = 2;
+
+ [JsonPropertyName("default_schedule")]
+ public List DefaultSchedule { get; set; } = new();
+
+ [JsonPropertyName("server_overrides")]
+ public Dictionary ServerOverrides { get; set; } = new();
+ }
}
diff --git a/Lite/Windows/CollectorScheduleEditorWindow.xaml b/Lite/Windows/CollectorScheduleEditorWindow.xaml
new file mode 100644
index 0000000..d3aa581
--- /dev/null
+++ b/Lite/Windows/CollectorScheduleEditorWindow.xaml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Lite/Windows/CollectorScheduleEditorWindow.xaml.cs b/Lite/Windows/CollectorScheduleEditorWindow.xaml.cs
new file mode 100644
index 0000000..3243d05
--- /dev/null
+++ b/Lite/Windows/CollectorScheduleEditorWindow.xaml.cs
@@ -0,0 +1,345 @@
+/*
+ * Copyright (c) 2026 Erik Darling, Darling Data LLC
+ *
+ * This file is part of the SQL Server Performance Monitor Lite.
+ *
+ * Licensed under the MIT License. See LICENSE file in the project root for full license information.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
+using System.Windows.Controls;
+using PerformanceMonitorLite.Models;
+using PerformanceMonitorLite.Services;
+
+namespace PerformanceMonitorLite.Windows;
+
+public partial class CollectorScheduleEditorWindow : Window
+{
+ private readonly ScheduleManager _scheduleManager;
+ private readonly ServerManager _serverManager;
+ private readonly string? _serverId;
+ private readonly string? _serverDisplayName;
+ private List _editingSchedules = new();
+ private bool _suppressPresetChange;
+ private bool _isEditingDefault;
+
+ ///
+ /// True if the user saved changes.
+ ///
+ public bool Saved { get; private set; }
+
+ ///
+ /// Opens the editor for a specific server's schedule.
+ ///
+ public CollectorScheduleEditorWindow(
+ ScheduleManager scheduleManager,
+ ServerManager serverManager,
+ string serverId,
+ string serverDisplayName)
+ {
+ InitializeComponent();
+ _scheduleManager = scheduleManager;
+ _serverManager = serverManager;
+ _serverId = serverId;
+ _serverDisplayName = serverDisplayName;
+ _isEditingDefault = false;
+
+ Title = $"Collector Schedules - {serverDisplayName}";
+ HeaderText.Text = $"Collector Schedules - {serverDisplayName}";
+ SubHeaderText.Text = $"Server: {serverDisplayName}";
+
+ SetupCopyFromServerCombo();
+ LoadServerSchedule();
+ }
+
+ ///
+ /// Opens the editor for the default schedule.
+ ///
+ public CollectorScheduleEditorWindow(
+ ScheduleManager scheduleManager,
+ ServerManager serverManager)
+ {
+ InitializeComponent();
+ _scheduleManager = scheduleManager;
+ _serverManager = serverManager;
+ _isEditingDefault = true;
+
+ Title = "Default Collector Schedule";
+ HeaderText.Text = "Default Collector Schedule";
+ SubHeaderText.Text = "This schedule applies to all servers without a custom override.";
+
+ /* Hide server-specific controls */
+ UseDefaultCheckBox.Visibility = Visibility.Collapsed;
+ CopyFromDefaultButton.Visibility = Visibility.Collapsed;
+
+ _editingSchedules = CloneScheduleList(_scheduleManager.GetDefaultSchedule());
+ ScheduleGrid.ItemsSource = _editingSchedules;
+ DetectActivePreset();
+ }
+
+ private void SetupCopyFromServerCombo()
+ {
+ var servers = _serverManager.GetAllServers()
+ .Where(s => s.Id != _serverId)
+ .ToList();
+
+ if (servers.Count > 0)
+ {
+ CopyFromServerCombo.Visibility = Visibility.Visible;
+ CopyFromServerButton.Visibility = Visibility.Visible;
+ CopyFromServerCombo.DisplayMemberPath = "DisplayName";
+ CopyFromServerCombo.SelectedValuePath = "Id";
+ CopyFromServerCombo.ItemsSource = servers;
+ CopyFromServerCombo.SelectedIndex = 0;
+ }
+ }
+
+ private void LoadServerSchedule()
+ {
+ bool usesDefault = !_scheduleManager.HasServerOverride(_serverId!);
+ UseDefaultCheckBox.IsChecked = usesDefault;
+
+ _editingSchedules = CloneScheduleList(_scheduleManager.GetSchedulesForServer(_serverId!));
+ ScheduleGrid.ItemsSource = _editingSchedules;
+ UpdateEditableState(usesDefault);
+ DetectActivePreset();
+ }
+
+ private void UpdateEditableState(bool usesDefault)
+ {
+ bool editable = !usesDefault;
+ ScheduleGrid.IsReadOnly = usesDefault;
+ ScheduleGrid.Opacity = usesDefault ? 0.6 : 1.0;
+ PresetComboBox.IsEnabled = editable;
+ CopyFromDefaultButton.IsEnabled = editable;
+ CopyFromServerButton.IsEnabled = editable;
+ CopyFromServerCombo.IsEnabled = editable;
+
+ StatusText.Text = usesDefault
+ ? "Using default schedule (read-only). Uncheck 'Use default schedule' to customize this server."
+ : "Custom schedule. Changes apply only to this server.";
+ }
+
+ private void UseDefaultCheckBox_Changed(object sender, RoutedEventArgs e)
+ {
+ if (_isEditingDefault) return;
+
+ bool usesDefault = UseDefaultCheckBox.IsChecked == true;
+
+ if (!usesDefault)
+ {
+ /* Switching from default to custom — copy current defaults as starting point */
+ _editingSchedules = CloneScheduleList(_scheduleManager.GetDefaultSchedule());
+ ScheduleGrid.ItemsSource = _editingSchedules;
+ }
+ else
+ {
+ /* Switching to default — show the default schedule (read-only) */
+ _editingSchedules = CloneScheduleList(_scheduleManager.GetDefaultSchedule());
+ ScheduleGrid.ItemsSource = _editingSchedules;
+ }
+
+ UpdateEditableState(usesDefault);
+ DetectActivePreset();
+ }
+
+ private void DetectActivePreset()
+ {
+ _suppressPresetChange = true;
+ try
+ {
+ string active = DetectPresetForList(_editingSchedules);
+ for (int i = 0; i < PresetComboBox.Items.Count; i++)
+ {
+ if (PresetComboBox.Items[i] is ComboBoxItem item &&
+ string.Equals(item.Content?.ToString(), active, StringComparison.OrdinalIgnoreCase))
+ {
+ PresetComboBox.SelectedIndex = i;
+ return;
+ }
+ }
+ PresetComboBox.SelectedIndex = 0;
+ }
+ finally
+ {
+ _suppressPresetChange = false;
+ }
+ }
+
+ private void PresetComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_suppressPresetChange) return;
+ if (PresetComboBox.SelectedItem is not ComboBoxItem selected) return;
+
+ string presetName = selected.Content?.ToString() ?? "";
+ if (presetName == "Custom") return;
+
+ var result = MessageBox.Show(
+ $"Apply the \"{presetName}\" preset?\n\nThis will change all collector frequencies. Enabled/disabled state and retention settings are not affected.",
+ "Apply Collection Preset",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question);
+
+ if (result != MessageBoxResult.Yes)
+ {
+ DetectActivePreset();
+ return;
+ }
+
+ ApplyPresetToList(_editingSchedules, presetName);
+ ScheduleGrid.ItemsSource = null;
+ ScheduleGrid.ItemsSource = _editingSchedules;
+ DetectActivePreset();
+ }
+
+ private void CopyFromDefault_Click(object sender, RoutedEventArgs e)
+ {
+ var result = MessageBox.Show(
+ "Replace this server's schedule with a copy of the default schedule?",
+ "Copy from Default",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question);
+
+ if (result != MessageBoxResult.Yes) return;
+
+ _editingSchedules = CloneScheduleList(_scheduleManager.GetDefaultSchedule());
+ ScheduleGrid.ItemsSource = _editingSchedules;
+ DetectActivePreset();
+ }
+
+ private void CopyFromServer_Click(object sender, RoutedEventArgs e)
+ {
+ if (CopyFromServerCombo.SelectedItem is not Models.ServerConnection selected) return;
+ var sourceServerId = selected.Id;
+
+ var result = MessageBox.Show(
+ $"Replace this server's schedule with a copy of {selected.DisplayName}'s schedule?",
+ "Copy from Server",
+ MessageBoxButton.YesNo,
+ MessageBoxImage.Question);
+
+ if (result != MessageBoxResult.Yes) return;
+
+ _editingSchedules = CloneScheduleList(_scheduleManager.GetSchedulesForServer(sourceServerId));
+ ScheduleGrid.ItemsSource = _editingSchedules;
+ DetectActivePreset();
+ }
+
+ private void SaveButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (_isEditingDefault)
+ {
+ /* Save to default schedule */
+ foreach (var edited in _editingSchedules)
+ {
+ _scheduleManager.UpdateSchedule(edited.Name,
+ enabled: edited.Enabled,
+ frequencyMinutes: edited.FrequencyMinutes,
+ retentionDays: edited.RetentionDays);
+ }
+ }
+ else if (UseDefaultCheckBox.IsChecked == true)
+ {
+ /* Revert to default — remove override */
+ _scheduleManager.RemoveServerOverride(_serverId!);
+ }
+ else
+ {
+ /* Save per-server override */
+ _scheduleManager.SetScheduleForServer(_serverId!, _editingSchedules);
+ }
+
+ Saved = true;
+ Close();
+ }
+
+ private void CancelButton_Click(object sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+
+ // ──────────────────────────────────────────────────────────────────
+ // Helpers
+ // ──────────────────────────────────────────────────────────────────
+
+ private static readonly Dictionary> s_presets = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["Aggressive"] = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["wait_stats"] = 1, ["query_stats"] = 1, ["procedure_stats"] = 1,
+ ["query_store"] = 2, ["query_snapshots"] = 1, ["cpu_utilization"] = 1,
+ ["file_io_stats"] = 1, ["memory_stats"] = 1, ["memory_clerks"] = 2,
+ ["tempdb_stats"] = 1, ["perfmon_stats"] = 1, ["deadlocks"] = 1,
+ ["memory_grant_stats"] = 1, ["waiting_tasks"] = 1,
+ ["blocked_process_report"] = 1, ["running_jobs"] = 2
+ },
+ ["Balanced"] = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["wait_stats"] = 1, ["query_stats"] = 1, ["procedure_stats"] = 1,
+ ["query_store"] = 5, ["query_snapshots"] = 1, ["cpu_utilization"] = 1,
+ ["file_io_stats"] = 1, ["memory_stats"] = 1, ["memory_clerks"] = 5,
+ ["tempdb_stats"] = 1, ["perfmon_stats"] = 1, ["deadlocks"] = 1,
+ ["memory_grant_stats"] = 1, ["waiting_tasks"] = 1,
+ ["blocked_process_report"] = 1, ["running_jobs"] = 5
+ },
+ ["Low-Impact"] = new(StringComparer.OrdinalIgnoreCase)
+ {
+ ["wait_stats"] = 5, ["query_stats"] = 10, ["procedure_stats"] = 10,
+ ["query_store"] = 30, ["query_snapshots"] = 5, ["cpu_utilization"] = 5,
+ ["file_io_stats"] = 10, ["memory_stats"] = 10, ["memory_clerks"] = 30,
+ ["tempdb_stats"] = 5, ["perfmon_stats"] = 5, ["deadlocks"] = 5,
+ ["memory_grant_stats"] = 5, ["waiting_tasks"] = 5,
+ ["blocked_process_report"] = 5, ["running_jobs"] = 30
+ }
+ };
+
+ private static string DetectPresetForList(List schedules)
+ {
+ foreach (var (presetName, intervals) in s_presets)
+ {
+ bool matches = true;
+ foreach (var (collector, freq) in intervals)
+ {
+ var schedule = schedules.FirstOrDefault(s =>
+ s.Name.Equals(collector, StringComparison.OrdinalIgnoreCase));
+ if (schedule != null && schedule.FrequencyMinutes != freq)
+ {
+ matches = false;
+ break;
+ }
+ }
+ if (matches) return presetName;
+ }
+ return "Custom";
+ }
+
+ private static void ApplyPresetToList(List schedules, string presetName)
+ {
+ if (!s_presets.TryGetValue(presetName, out var intervals)) return;
+
+ foreach (var (collector, freq) in intervals)
+ {
+ var schedule = schedules.FirstOrDefault(s =>
+ s.Name.Equals(collector, StringComparison.OrdinalIgnoreCase));
+ if (schedule != null)
+ {
+ schedule.FrequencyMinutes = freq;
+ }
+ }
+ }
+
+ private static List CloneScheduleList(IReadOnlyList source)
+ {
+ return source.Select(s => new CollectorSchedule
+ {
+ Name = s.Name,
+ Enabled = s.Enabled,
+ FrequencyMinutes = s.FrequencyMinutes,
+ RetentionDays = s.RetentionDays,
+ Description = s.Description
+ }).ToList();
+ }
+}
diff --git a/Lite/Windows/SettingsWindow.xaml b/Lite/Windows/SettingsWindow.xaml
index b9beaee..8a9c3fb 100644
--- a/Lite/Windows/SettingsWindow.xaml
+++ b/Lite/Windows/SettingsWindow.xaml
@@ -332,49 +332,47 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/Lite/Windows/SettingsWindow.xaml.cs b/Lite/Windows/SettingsWindow.xaml.cs
index 64d5ef0..cdd066e 100644
--- a/Lite/Windows/SettingsWindow.xaml.cs
+++ b/Lite/Windows/SettingsWindow.xaml.cs
@@ -7,6 +7,7 @@
*/
using System;
+using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
@@ -15,6 +16,7 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
+using System.Windows.Input;
using PerformanceMonitorLite.Mcp;
using PerformanceMonitorLite.Services;
@@ -23,23 +25,26 @@ namespace PerformanceMonitorLite.Windows;
public partial class SettingsWindow : Window
{
private readonly ScheduleManager _scheduleManager;
+ private readonly ServerManager _serverManager;
private readonly CollectionBackgroundService? _backgroundService;
private readonly McpHostService? _mcpService;
private readonly MuteRuleService? _muteRuleService;
public SettingsWindow(
ScheduleManager scheduleManager,
+ ServerManager serverManager,
CollectionBackgroundService? backgroundService = null,
McpHostService? mcpService = null,
MuteRuleService? muteRuleService = null)
{
InitializeComponent();
_scheduleManager = scheduleManager;
+ _serverManager = serverManager;
_backgroundService = backgroundService;
_mcpService = mcpService;
_muteRuleService = muteRuleService;
- LoadSchedules();
+ LoadServerScheduleSummary();
UpdateCollectionStatus();
LoadMcpSettings();
UpdateMcpStatus();
@@ -52,62 +57,91 @@ public SettingsWindow(
LoadSmtpSettings();
}
- private bool _suppressPresetChange;
+ private void LoadServerScheduleSummary()
+ {
+ var servers = _serverManager.GetAllServers();
+ var rows = servers.Select(s => new ServerScheduleRow
+ {
+ ServerId = s.Id,
+ ServerName = s.DisplayName,
+ Preset = _scheduleManager.GetActivePresetForServer(s.Id),
+ Status = _scheduleManager.HasServerOverride(s.Id) ? "Customized" : "Default"
+ }).ToList();
+
+ ServerScheduleGrid.ItemsSource = rows;
+ DefaultPresetText.Text = _scheduleManager.GetActivePreset();
+ }
- private void LoadSchedules()
+ private void EditServerSchedule_Click(object sender, RoutedEventArgs e)
{
- ScheduleGrid.ItemsSource = _scheduleManager.GetAllSchedules();
- DetectActivePreset();
+ OpenServerScheduleEditor();
}
- private void DetectActivePreset()
+ private void ServerScheduleGrid_DoubleClick(object sender, MouseButtonEventArgs e)
{
- _suppressPresetChange = true;
- try
+ OpenServerScheduleEditor();
+ }
+
+ private void OpenServerScheduleEditor()
+ {
+ if (ServerScheduleGrid.SelectedItem is not ServerScheduleRow row) return;
+
+ var server = _serverManager.GetAllServers().FirstOrDefault(s => s.Id == row.ServerId);
+ if (server == null) return;
+
+ var editor = new CollectorScheduleEditorWindow(_scheduleManager, _serverManager, server.Id, server.DisplayName) { Owner = this };
+ editor.ShowDialog();
+
+ if (editor.Saved)
{
- string active = _scheduleManager.GetActivePreset();
- for (int i = 0; i < PresetComboBox.Items.Count; i++)
- {
- if (PresetComboBox.Items[i] is ComboBoxItem item &&
- string.Equals(item.Content?.ToString(), active, StringComparison.OrdinalIgnoreCase))
- {
- PresetComboBox.SelectedIndex = i;
- return;
- }
- }
- PresetComboBox.SelectedIndex = 0;
+ LoadServerScheduleSummary();
}
- finally
+ }
+
+ private void EditDefaultSchedule_Click(object sender, RoutedEventArgs e)
+ {
+ var editor = new CollectorScheduleEditorWindow(_scheduleManager, _serverManager) { Owner = this };
+ editor.ShowDialog();
+
+ if (editor.Saved)
{
- _suppressPresetChange = false;
+ LoadServerScheduleSummary();
}
}
- private void PresetComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+ private void ApplyDefaultToAll_Click(object sender, RoutedEventArgs e)
{
- if (_suppressPresetChange) return;
- if (PresetComboBox.SelectedItem is not ComboBoxItem selected) return;
+ var servers = _serverManager.GetAllServers();
+ var customCount = servers.Count(s => _scheduleManager.HasServerOverride(s.Id));
- string presetName = selected.Content?.ToString() ?? "";
- if (presetName == "Custom") return;
+ if (customCount == 0)
+ {
+ MessageBox.Show("All servers are already using the default schedule.", "Apply Default", MessageBoxButton.OK, MessageBoxImage.Information);
+ return;
+ }
var result = MessageBox.Show(
- $"Apply the \"{presetName}\" preset?\n\nThis will change all collector frequencies. Enabled/disabled state and retention settings are not affected.",
- "Apply Collection Preset",
+ $"Remove custom schedules from {customCount} server(s) and revert them to the default schedule?\n\nThis cannot be undone.",
+ "Apply Default to All",
MessageBoxButton.YesNo,
- MessageBoxImage.Question
- );
+ MessageBoxImage.Warning);
- if (result != MessageBoxResult.Yes)
+ if (result != MessageBoxResult.Yes) return;
+
+ foreach (var server in servers)
{
- DetectActivePreset();
- return;
+ _scheduleManager.RemoveServerOverride(server.Id);
}
- _scheduleManager.ApplyPreset(presetName);
- ScheduleGrid.ItemsSource = null;
- ScheduleGrid.ItemsSource = _scheduleManager.GetAllSchedules();
- DetectActivePreset();
+ LoadServerScheduleSummary();
+ }
+
+ private class ServerScheduleRow
+ {
+ public string ServerId { get; set; } = "";
+ public string ServerName { get; set; } = "";
+ public string Preset { get; set; } = "";
+ public string Status { get; set; } = "";
}
private void UpdateCollectionStatus()
@@ -169,7 +203,6 @@ private void PauseResumeButton_Click(object sender, RoutedEventArgs e)
private async void SaveButton_Click(object sender, RoutedEventArgs e)
{
- _scheduleManager.SaveSchedules();
var (mcpChanged, mcpValid) = await SaveMcpSettingsAsync();
SaveDefaultTimeRange();
SaveConnectionTimeout();