using System.Collections.ObjectModel; using System.Runtime.Versioning; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using WebhookServer.Core.Ipc; using WebhookServer.Gui.Services; namespace WebhookServer.Gui.ViewModels; [SupportedOSPlatform("windows")] public sealed partial class ConfigCheckpointsViewModel : ObservableObject { private readonly AdminPipeClient _client; public ObservableCollection Checkpoints { get; } = new(); [ObservableProperty] private BackupEntry? _selected; [ObservableProperty] private string _statusMessage = ""; public ConfigCheckpointsViewModel(AdminPipeClient client) { _client = client; } [RelayCommand] public async Task RefreshAsync() { try { var list = await _client.ListBackupsAsync().ConfigureAwait(false); Application.Current.Dispatcher.Invoke(() => { Checkpoints.Clear(); foreach (var b in list) Checkpoints.Add(b); StatusMessage = list.Count == 0 ? "No checkpoints yet. Save the config or click Take Checkpoint Now." : $"{list.Count} checkpoint{(list.Count == 1 ? "" : "s")}."; }); } catch (Exception ex) { Application.Current.Dispatcher.Invoke(() => StatusMessage = $"Could not load: {ex.Message}"); } } [RelayCommand] private async Task TakeCheckpointAsync() { // Prompt for an optional description on the UI thread. string? description = null; var prompted = Application.Current.Dispatcher.Invoke(() => { var dlg = new Views.TakeCheckpointDialog { Owner = Application.Current.MainWindow }; if (dlg.ShowDialog() != true) return false; description = string.IsNullOrWhiteSpace(dlg.Description) ? null : dlg.Description; return true; }); if (!prompted) return; try { var entry = await _client.CreateCheckpointAsync(description).ConfigureAwait(false); await RefreshAsync().ConfigureAwait(false); if (entry is not null) { Application.Current.Dispatcher.Invoke(() => { Selected = Checkpoints.FirstOrDefault(c => c.FileName == entry.FileName); StatusMessage = $"Created {entry.FileName}"; }); } } catch (Exception ex) { Application.Current.Dispatcher.Invoke(() => MessageBox.Show(ex.Message, "Take checkpoint failed", MessageBoxButton.OK, MessageBoxImage.Error)); } } [RelayCommand] private async Task RollbackAsync() { if (Selected is null) return; // Capture before the refresh; the ObservableCollection.Clear() in // RefreshAsync nulls Selected (the original instance is gone from the // collection so the SelectedItem binding clears). var fileName = Selected.FileName; var savedAt = Selected.SavedAt; var ok = MessageBox.Show( $"Roll the configuration back to the checkpoint from {savedAt.ToLocalTime():yyyy-MM-dd HH:mm:ss}?\n\nThe current configuration is automatically saved as a new checkpoint first, so you can roll forward again.", "Confirm rollback", MessageBoxButton.OKCancel, MessageBoxImage.Warning); if (ok != MessageBoxResult.OK) return; try { await _client.RestoreBackupAsync(fileName).ConfigureAwait(false); await RefreshAsync().ConfigureAwait(false); Application.Current.Dispatcher.Invoke(() => StatusMessage = $"Rolled back to {fileName}."); } catch (Exception ex) { Application.Current.Dispatcher.Invoke(() => MessageBox.Show(ex.Message, "Rollback failed", MessageBoxButton.OK, MessageBoxImage.Error)); } } }