feat(app): add overview and main window view models

This commit is contained in:
dualfroz committed 2026-08-26 18:42:19 +02:00
1 parent 419316efef
commit 3d474bb927
2 files changed
+218

No files matched your search

@@ -0,0 +1,176 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Vantage.Core.Alerts;
using Vantage.Core.Configuration;
using Vantage.Core.Docker;
using Vantage.Core.Health;
using Vantage.Core.History;
using Vantage.Core.Metrics;
using Vantage.Core.Models;
namespace Vantage.App.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
private readonly IMetricsProvider metricsProvider;
private readonly IHealthChecker healthChecker;
private readonly IConfigStore configStore;
private readonly IMetricsStore? metricsStore;
private readonly AlertEngine? alertEngine;
private VantageConfig config = new();
private CancellationTokenSource? monitorCts;
[ObservableProperty] private HostViewModel? selectedHost;
[ObservableProperty] private string statusLine = "Idle";
[ObservableProperty] private bool isMonitoring;
[ObservableProperty] private int firingAlertCount;
public MainWindowViewModel(
IMetricsProvider metricsProvider,
IDockerProvider dockerProvider,
IHealthChecker healthChecker,
IConfigStore configStore,
IMetricsStore? metricsStore = null,
AlertEngine? alertEngine = null)
{
this.metricsProvider = metricsProvider;
this.healthChecker = healthChecker;
this.configStore = configStore;
this.metricsStore = metricsStore;
this.alertEngine = alertEngine;
Docker = new DockerViewModel(dockerProvider);
Overview = new OverviewViewModel(Hosts);
}
public ObservableCollection<HostViewModel> Hosts { get; } = new();
public ObservableCollection<HealthCheckViewModel> HealthChecks { get; } = new();
public ObservableCollection<AlertStatus> Alerts { get; } = new();
public DockerViewModel Docker { get; }
public OverviewViewModel Overview { get; }
public async Task InitializeAsync(CancellationToken cancellationToken = default)
{
config = await configStore.LoadAsync(cancellationToken).ConfigureAwait(false);
if (config.Hosts.Count == 0 && config.HealthChecks.Count == 0)
config = DemoData.Build();
Hosts.Clear();
foreach (var host in config.Hosts)
Hosts.Add(new HostViewModel(host, config.HistoryLength));
HealthChecks.Clear();
foreach (var check in config.HealthChecks)
HealthChecks.Add(new HealthCheckViewModel(check, healthChecker));
if (alertEngine is not null)
{
alertEngine.SetRules(config.AlertRules);
if (!string.IsNullOrWhiteSpace(config.AlertWebhookUrl))
alertEngine.AddSink(new WebhookAlertSink(new HttpClient(), new Uri(config.AlertWebhookUrl)));
}
SelectedHost = Hosts.FirstOrDefault();
Overview.Refresh();
}
partial void OnSelectedHostChanged(HostViewModel? value)
{
Docker.SetHost(value?.Config);
}
[RelayCommand]
public async Task RefreshSelectedHostAsync(CancellationToken cancellationToken)
{
var host = SelectedHost;
if (host is null)
return;
try
{
var metrics = await metricsProvider.GetMetricsAsync(host.Config, cancellationToken).ConfigureAwait(false);
host.ApplyMetrics(metrics);
StatusLine = $"Updated {host.Name} at {metrics.CollectedAt.ToLocalTime():HH:mm:ss}";
if (metricsStore is not null)
await metricsStore.RecordHostMetricsAsync(host.Config.Id, metrics, cancellationToken).ConfigureAwait(false);
if (alertEngine is not null)
{
await alertEngine.IngestAsync(host.Config.Id, metrics, metrics.CollectedAt, cancellationToken).ConfigureAwait(false);
UpdateAlerts();
}
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
host.ApplyError(ex.Message);
StatusLine = $"{host.Name}: {ex.Message}";
}
Overview.Refresh();
}
private void UpdateAlerts()
{
if (alertEngine is null)
return;
Alerts.Clear();
foreach (var status in alertEngine.Statuses())
Alerts.Add(status);
FiringAlertCount = alertEngine.ActiveAlerts().Count;
Overview.FiringAlerts = FiringAlertCount;
}
[RelayCommand]
public async Task RunAllHealthAsync(CancellationToken cancellationToken)
{
var checks = HealthChecks.Select(check => check.RunAsync(cancellationToken));
await Task.WhenAll(checks).ConfigureAwait(false);
}
public void StartMonitoring()
{
if (IsMonitoring)
return;
monitorCts = new CancellationTokenSource();
IsMonitoring = true;
_ = MonitorLoopAsync(monitorCts.Token);
}
public void StopMonitoring()
{
monitorCts?.Cancel();
monitorCts?.Dispose();
monitorCts = null;
IsMonitoring = false;
}
private async Task MonitorLoopAsync(CancellationToken cancellationToken)
{
var interval = TimeSpan.FromSeconds(Math.Max(1, config.RefreshIntervalSeconds));
using var timer = new PeriodicTimer(interval);
await RefreshSelectedHostAsync(cancellationToken);
await RunAllHealthAsync(cancellationToken);
try
{
while (await timer.WaitForNextTickAsync(cancellationToken))
{
await RefreshSelectedHostAsync(cancellationToken);
await RunAllHealthAsync(cancellationToken);
}
}
catch (OperationCanceledException)
{
}
}
}
@@ -0,0 +1,42 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Vantage.App.ViewModels;
public partial class OverviewViewModel : ViewModelBase
{
private readonly ObservableCollection<HostViewModel> hosts;
[ObservableProperty] private int totalHosts;
[ObservableProperty] private int onlineHosts;
[ObservableProperty] private int offlineHosts;
[ObservableProperty] private double averageCpuPercent;
[ObservableProperty] private double peakCpuPercent;
[ObservableProperty] private string peakCpuHost = "-";
[ObservableProperty] private double peakMemoryPercent;
[ObservableProperty] private string peakMemoryHost = "-";
[ObservableProperty] private int firingAlerts;
public OverviewViewModel(ObservableCollection<HostViewModel> hosts)
{
this.hosts = hosts;
}
public void Refresh()
{
TotalHosts = hosts.Count;
OnlineHosts = hosts.Count(host => host.IsOnline);
OfflineHosts = TotalHosts - OnlineHosts;
var online = hosts.Where(host => host.IsOnline).ToArray();
AverageCpuPercent = online.Length == 0 ? 0 : Math.Round(online.Average(host => host.CpuUsagePercent), 1);
var hottestCpu = online.OrderByDescending(host => host.CpuUsagePercent).FirstOrDefault();
PeakCpuPercent = hottestCpu?.CpuUsagePercent ?? 0;
PeakCpuHost = hottestCpu?.Name ?? "-";
var hottestMemory = online.OrderByDescending(host => host.MemoryUsedPercent).FirstOrDefault();
PeakMemoryPercent = hottestMemory?.MemoryUsedPercent ?? 0;
PeakMemoryHost = hottestMemory?.Name ?? "-";
}
}