Add Windows SSH monitoring MVP #1
5 changed files with 75 additions and 19 deletions
|
|
@ -58,9 +58,10 @@
|
|||
|
||||
## Этап 5 — мониторинг и терминал
|
||||
|
||||
- [ ] swap, inode, network и выбранные systemd units;
|
||||
- [ ] предупреждения по диску, памяти и недоступности;
|
||||
- [ ] автоматическое обновление и короткая локальная история;
|
||||
- [x] swap, inode, network и состояние SSH/WireGuard;
|
||||
- [x] предупреждения по диску, памяти, inode и недоступности;
|
||||
- [x] автоматическое обновление каждые 30 секунд;
|
||||
- [ ] короткая локальная история;
|
||||
- [ ] графики и экспорт диагностики без секретов;
|
||||
- [ ] отдельный прямой SSH-терминал;
|
||||
- [ ] отдельная terminal identity и подтверждение пользователя;
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@
|
|||
<StackPanel Grid.Column="3" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding MemoryText}" />
|
||||
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{Binding DiskText}" />
|
||||
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{Binding HealthText}" />
|
||||
</StackPanel>
|
||||
<TextBlock
|
||||
Grid.Column="4"
|
||||
|
|
|
|||
|
|
@ -11,12 +11,15 @@ public sealed partial class MainPage : Page
|
|||
{
|
||||
private readonly ServerStorage _storage = new();
|
||||
private readonly SshMonitorService _ssh = new();
|
||||
private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromSeconds(30) };
|
||||
private readonly SemaphoreSlim _refreshLock = new(1, 1);
|
||||
private bool _loaded;
|
||||
|
||||
public MainPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
Loaded += MainPage_Loaded;
|
||||
_refreshTimer.Tick += async (_, _) => 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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue