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);
}