diff --git a/docs/roadmap.md b/docs/roadmap.md index 4bc7e04..303dd83 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -58,9 +58,10 @@ ## Этап 5 — мониторинг и терминал -- [ ] swap, inode, network и выбранные systemd units; -- [ ] предупреждения по диску, памяти и недоступности; -- [ ] автоматическое обновление и короткая локальная история; +- [x] swap, inode, network и состояние SSH/WireGuard; +- [x] предупреждения по диску, памяти, inode и недоступности; +- [x] автоматическое обновление каждые 30 секунд; +- [ ] короткая локальная история; - [ ] графики и экспорт диагностики без секретов; - [ ] отдельный прямой SSH-терминал; - [ ] отдельная terminal identity и подтверждение пользователя; diff --git a/src/ServerMonitorManager.Desktop/MainPage.xaml b/src/ServerMonitorManager.Desktop/MainPage.xaml index 03b9812..a040abf 100644 --- a/src/ServerMonitorManager.Desktop/MainPage.xaml +++ b/src/ServerMonitorManager.Desktop/MainPage.xaml @@ -202,6 +202,7 @@ + await RefreshAllAsync(); MainNavigation.SelectedItem = MainNavigation.MenuItems[0]; } @@ -32,6 +35,7 @@ public sealed partial class MainPage : Page } _loaded = true; + _refreshTimer.Start(); foreach (var profile in await _storage.LoadAsync()) { Servers.Add(new ServerViewModel(profile)); @@ -272,26 +276,37 @@ public sealed partial class MainPage : Page private async Task RefreshAllAsync() { - if (Servers.Count == 0) + if (!await _refreshLock.WaitAsync(0)) { - ShowInfo("Серверы не добавлены", "Сначала создайте SSH-ключ и установите его на сервере.", InfoBarSeverity.Informational); return; } - - await Task.WhenAll(Servers.Select(RefreshServerAsync)); - var online = Servers.Count(server => server.IsOnline); - var warnings = Servers.Count - online; - AvailabilityValueText.Text = $"{online} / {Servers.Count}"; - AvailabilityDetailText.Text = warnings == 0 ? "Все серверы доступны" : $"Недоступно: {warnings}"; - AverageLoadValueText.Text = online == 0 - ? "—" - : $"{Servers.Where(server => server.IsOnline).Average(server => server.CpuPercent):F0}%"; - WarningValueText.Text = warnings.ToString(CultureInfo.InvariantCulture); - WarningDetailText.Text = warnings == 0 ? "Нет предупреждений" : "Проверьте SSH и ключ"; - HeaderStatusText.Text = $"SSH monitoring · {Servers.Count} сервер(а) · обновлено {DateTime.Now:HH:mm:ss}"; - if (Servers.Any(server => server.IsHub)) + try { - await RefreshMeshAsync(showSuccess: false); + if (Servers.Count == 0) + { + ShowInfo("Серверы не добавлены", "Сначала создайте SSH-ключ и установите его на сервере.", InfoBarSeverity.Informational); + return; + } + + await Task.WhenAll(Servers.Select(RefreshServerAsync)); + var online = Servers.Count(server => server.IsOnline); + var warnings = Servers.Count(server => !server.IsOnline || server.HasWarning); + AvailabilityValueText.Text = $"{online} / {Servers.Count}"; + AvailabilityDetailText.Text = warnings == 0 ? "Все серверы доступны" : $"Недоступно: {warnings}"; + AverageLoadValueText.Text = online == 0 + ? "—" + : $"{Servers.Where(server => server.IsOnline).Average(server => server.CpuPercent):F0}%"; + WarningValueText.Text = warnings.ToString(CultureInfo.InvariantCulture); + WarningDetailText.Text = warnings == 0 ? "Нет предупреждений" : "Проверьте доступность и ресурсы"; + HeaderStatusText.Text = $"SSH monitoring · {Servers.Count} сервер(а) · обновлено {DateTime.Now:HH:mm:ss}"; + if (Servers.Any(server => server.IsHub)) + { + await RefreshMeshAsync(showSuccess: false); + } + } + finally + { + _refreshLock.Release(); } } @@ -306,6 +321,11 @@ public sealed partial class MainPage : Page server.CpuText = $"{metrics.CpuPercent:F0}% load"; server.MemoryText = $"{FormatSize(metrics.MemoryUsedKb)} / {FormatSize(metrics.MemoryTotalKb)} RAM"; server.DiskText = $"{FormatSize(metrics.DiskUsedKb)} / {FormatSize(metrics.DiskTotalKb)} disk"; + var memoryPercent = Percent(metrics.MemoryUsedKb, metrics.MemoryTotalKb); + var diskPercent = Percent(metrics.DiskUsedKb, metrics.DiskTotalKb); + var inodePercent = Percent(metrics.InodesUsed, metrics.InodesTotal); + server.HasWarning = memoryPercent >= 90 || diskPercent >= 90 || inodePercent >= 90 || metrics.SshState != "active"; + server.HealthText = $"swap {FormatSize(metrics.SwapUsedKb)}/{FormatSize(metrics.SwapTotalKb)} · inode {inodePercent:F0}% · net ↓{FormatBytes(metrics.NetworkRxBytes)} ↑{FormatBytes(metrics.NetworkTxBytes)} · SSH {metrics.SshState}"; server.LatencyText = $"{metrics.Latency.TotalMilliseconds:F0} ms"; server.Status = $"Онлайн · uptime {FormatUptime(metrics.Uptime)}"; server.IsOnline = true; @@ -319,6 +339,8 @@ public sealed partial class MainPage : Page server.DiskText = "—"; server.LatencyText = "offline"; server.IsOnline = false; + server.HasWarning = true; + server.HealthText = "Нет данных о службах и сети"; } } @@ -351,6 +373,14 @@ public sealed partial class MainPage : Page ? $"{uptime.TotalDays:F0} дн." : $"{uptime.TotalHours:F0} ч."; + private static double Percent(long used, long total) + => total <= 0 ? 0 : Math.Clamp(used * 100d / total, 0, 100); + + private static string FormatBytes(long bytes) + => bytes >= 1024L * 1024 * 1024 + ? $"{bytes / 1024d / 1024d / 1024d:F1} GB" + : $"{bytes / 1024d / 1024d:F1} MB"; + private static string CompactError(Exception exception) { var firstLine = exception.Message.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); diff --git a/src/ServerMonitorManager.Desktop/ServerViewModel.cs b/src/ServerMonitorManager.Desktop/ServerViewModel.cs index 194c6b6..8053620 100644 --- a/src/ServerMonitorManager.Desktop/ServerViewModel.cs +++ b/src/ServerMonitorManager.Desktop/ServerViewModel.cs @@ -19,7 +19,9 @@ public sealed class ServerViewModel : INotifyPropertyChanged private string _memoryText = "—"; private string _diskText = "—"; private string _latencyText = "—"; + private string _healthText = "—"; private bool _isOnline; + private bool _hasWarning; public ServerViewModel(ServerProfileData profile) => Profile = profile; @@ -33,7 +35,9 @@ public sealed class ServerViewModel : INotifyPropertyChanged public string MemoryText { get => _memoryText; set => Set(ref _memoryText, value); } public string DiskText { get => _diskText; set => Set(ref _diskText, value); } public string LatencyText { get => _latencyText; set => Set(ref _latencyText, value); } + public string HealthText { get => _healthText; set => Set(ref _healthText, value); } public bool IsOnline { get => _isOnline; set => Set(ref _isOnline, value); } + public bool HasWarning { get => _hasWarning; set => Set(ref _hasWarning, value); } public event PropertyChangedEventHandler? PropertyChanged; diff --git a/src/ServerMonitorManager.Desktop/SshMonitorService.cs b/src/ServerMonitorManager.Desktop/SshMonitorService.cs index 0e5eac9..4613b75 100644 --- a/src/ServerMonitorManager.Desktop/SshMonitorService.cs +++ b/src/ServerMonitorManager.Desktop/SshMonitorService.cs @@ -12,6 +12,14 @@ public sealed record ServerMetrics( long MemoryTotalKb, long DiskUsedKb, long DiskTotalKb, + long SwapUsedKb, + long SwapTotalKb, + long InodesUsed, + long InodesTotal, + long NetworkRxBytes, + long NetworkTxBytes, + string SshState, + string WireGuardState, TimeSpan Uptime, TimeSpan Latency); @@ -61,6 +69,10 @@ public sealed partial class SshMonitorService var memoryAvailable = ReadLong(values, "MEM_AVAILABLE_KB"); var diskTotal = ReadLong(values, "DISK_TOTAL_KB"); var diskAvailable = ReadLong(values, "DISK_AVAILABLE_KB"); + var swapTotal = ReadLong(values, "SWAP_TOTAL_KB"); + var swapFree = ReadLong(values, "SWAP_FREE_KB"); + var inodesTotal = ReadLong(values, "DISK_INODES_TOTAL"); + var inodesFree = ReadLong(values, "DISK_INODES_FREE"); return new ServerMetrics( values.GetValueOrDefault("HOSTNAME", profile.Name), @@ -69,6 +81,14 @@ public sealed partial class SshMonitorService memoryTotal, Math.Max(0, diskTotal - diskAvailable), diskTotal, + Math.Max(0, swapTotal - swapFree), + swapTotal, + Math.Max(0, inodesTotal - inodesFree), + inodesTotal, + ReadLong(values, "NETWORK_RX_BYTES"), + ReadLong(values, "NETWORK_TX_BYTES"), + values.GetValueOrDefault("SYSTEMD_SSH", "unknown"), + values.GetValueOrDefault("SYSTEMD_WIREGUARD", "unknown"), TimeSpan.FromSeconds(ReadLong(values, "UPTIME_SECONDS")), stopwatch.Elapsed); }