using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using OTSSignsOrchestrator.Core.Data; using OTSSignsOrchestrator.Core.Models.Entities; using OTSSignsOrchestrator.Core.Services; using OTSSignsOrchestrator.Desktop.Models; using OTSSignsOrchestrator.Desktop.Services; namespace OTSSignsOrchestrator.Desktop.ViewModels; /// /// ViewModel for listing, viewing, and managing CMS instances. /// All data is fetched live from Docker Swarm hosts — nothing stored locally. /// public partial class InstancesViewModel : ObservableObject { private readonly IServiceProvider _services; [ObservableProperty] private ObservableCollection _instances = new(); [ObservableProperty] private LiveStackItem? _selectedInstance; [ObservableProperty] private string _statusMessage = string.Empty; [ObservableProperty] private bool _isBusy; [ObservableProperty] private string _filterText = string.Empty; [ObservableProperty] private ObservableCollection _selectedServices = new(); // Available SSH hosts — loaded for display and used to scope operations [ObservableProperty] private ObservableCollection _availableHosts = new(); [ObservableProperty] private SshHost? _selectedSshHost; /// Raised when the instance details modal should be opened for the given ViewModel. public event Action? OpenDetailsRequested; public InstancesViewModel(IServiceProvider services) { _services = services; _ = RefreshAllAsync(); } /// /// Enumerates all SSH hosts, then calls docker stack ls on each to build the /// live instance list. Only stacks matching *-cms-stack are shown. /// [RelayCommand] private async Task LoadInstancesAsync() => await RefreshAllAsync(); private async Task RefreshAllAsync() { IsBusy = true; StatusMessage = "Loading live instances from all hosts..."; SelectedServices = new ObservableCollection(); try { using var scope = _services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var hosts = await db.SshHosts.OrderBy(h => h.Label).ToListAsync(); AvailableHosts = new ObservableCollection(hosts); var dockerCli = _services.GetRequiredService(); var all = new List(); var errors = new List(); foreach (var host in hosts) { try { dockerCli.SetHost(host); var stacks = await dockerCli.ListStacksAsync(); foreach (var stack in stacks.Where(s => s.Name.EndsWith("-cms-stack"))) { all.Add(new LiveStackItem { StackName = stack.Name, CustomerAbbrev = stack.Name[..^10], ServiceCount = stack.ServiceCount, Host = host, }); } } catch (Exception ex) { errors.Add($"{host.Label}: {ex.Message}"); } } if (!string.IsNullOrWhiteSpace(FilterText)) all = all.Where(i => i.StackName.Contains(FilterText, StringComparison.OrdinalIgnoreCase) || i.CustomerAbbrev.Contains(FilterText, StringComparison.OrdinalIgnoreCase) || i.HostLabel.Contains(FilterText, StringComparison.OrdinalIgnoreCase)).ToList(); Instances = new ObservableCollection(all); var msg = $"Found {all.Count} instance(s) across {hosts.Count} host(s)."; if (errors.Count > 0) msg += $" | Errors: {string.Join(" | ", errors)}"; StatusMessage = msg; } catch (Exception ex) { StatusMessage = $"Error: {ex.Message}"; } finally { IsBusy = false; } } [RelayCommand] private async Task InspectInstanceAsync() { if (SelectedInstance == null) return; IsBusy = true; StatusMessage = $"Inspecting '{SelectedInstance.StackName}'..."; try { var dockerCli = _services.GetRequiredService(); dockerCli.SetHost(SelectedInstance.Host); var services = await dockerCli.InspectStackServicesAsync(SelectedInstance.StackName); SelectedServices = new ObservableCollection(services); StatusMessage = $"Found {services.Count} service(s) in stack '{SelectedInstance.StackName}'."; } catch (Exception ex) { StatusMessage = $"Error inspecting: {ex.Message}"; } finally { IsBusy = false; } } [RelayCommand] private async Task DeleteInstanceAsync() { if (SelectedInstance == null) return; IsBusy = true; StatusMessage = $"Deleting {SelectedInstance.StackName}..."; try { var dockerCli = _services.GetRequiredService(); dockerCli.SetHost(SelectedInstance.Host); var dockerSecrets = _services.GetRequiredService(); dockerSecrets.SetHost(SelectedInstance.Host); using var scope = _services.CreateScope(); var instanceSvc = scope.ServiceProvider.GetRequiredService(); var result = await instanceSvc.DeleteInstanceAsync( SelectedInstance.StackName, SelectedInstance.CustomerAbbrev); StatusMessage = result.Success ? $"Instance '{SelectedInstance.StackName}' deleted." : $"Delete failed: {result.ErrorMessage}"; await RefreshAllAsync(); } catch (Exception ex) { StatusMessage = $"Error deleting: {ex.Message}"; } finally { IsBusy = false; } } [RelayCommand] private async Task RotateMySqlPasswordAsync() { if (SelectedInstance == null) return; IsBusy = true; StatusMessage = $"Rotating MySQL password for {SelectedInstance.StackName}..."; try { var dockerCli = _services.GetRequiredService(); dockerCli.SetHost(SelectedInstance.Host); var dockerSecrets = _services.GetRequiredService(); dockerSecrets.SetHost(SelectedInstance.Host); using var scope = _services.CreateScope(); var instanceSvc = scope.ServiceProvider.GetRequiredService(); var (ok, msg) = await instanceSvc.RotateMySqlPasswordAsync(SelectedInstance.StackName); StatusMessage = ok ? $"Done: {msg}" : $"Failed: {msg}"; await RefreshAllAsync(); } catch (Exception ex) { StatusMessage = $"Error rotating password: {ex.Message}"; } finally { IsBusy = false; } } [RelayCommand] private async Task OpenDetailsAsync() { if (SelectedInstance == null) return; IsBusy = true; StatusMessage = $"Loading details for '{SelectedInstance.StackName}'..."; try { // Set the SSH host on singleton Docker services so modal operations target the right host var dockerCli = _services.GetRequiredService(); dockerCli.SetHost(SelectedInstance.Host); var dockerSecrets = _services.GetRequiredService(); dockerSecrets.SetHost(SelectedInstance.Host); var detailsVm = _services.GetRequiredService(); await detailsVm.LoadAsync(SelectedInstance); OpenDetailsRequested?.Invoke(detailsVm); StatusMessage = string.Empty; } catch (Exception ex) { StatusMessage = $"Error opening details: {ex.Message}"; } finally { IsBusy = false; } } }