diff --git a/docs/roadmap.md b/docs/roadmap.md index 6abbfb8..544f238 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -51,9 +51,9 @@ - [x] политики по целевому `/32`, TCP/UDP и порту; - [x] TTL и автоматическое истечение; - [ ] состояния Connecting, Active, Disconnecting, Partial, Disabled и Failed; -- [ ] версия политики и подтверждение применения; +- [x] версия политики и подтверждение применения на Hub; - [ ] обязательное отключение после reconnect; -- [ ] append-only аудит; +- [x] локальный append-only JSONL-аудит операций Link; - [ ] интеграционные тесты kill switch и частичных отказов. ## Этап 5 — мониторинг и терминал diff --git a/src/ServerMonitorManager.Desktop/MainPage.xaml.cs b/src/ServerMonitorManager.Desktop/MainPage.xaml.cs index 05b53a2..a3f1b50 100644 --- a/src/ServerMonitorManager.Desktop/MainPage.xaml.cs +++ b/src/ServerMonitorManager.Desktop/MainPage.xaml.cs @@ -306,9 +306,10 @@ public sealed partial class MainPage : Page continue; } var fields = line[5..].Split('|'); - if (fields.Length >= 7 + if (fields.Length >= 8 && int.TryParse(fields[4], CultureInfo.InvariantCulture, out var port) - && long.TryParse(fields[5], CultureInfo.InvariantCulture, out var expiresUnix)) + && long.TryParse(fields[5], CultureInfo.InvariantCulture, out var expiresUnix) + && long.TryParse(fields[7], CultureInfo.InvariantCulture, out var version)) { MeshLinks.Add(new MeshLinkViewModel( fields[0], @@ -316,7 +317,9 @@ public sealed partial class MainPage : Page fields[2], fields[3], port, - expiresUnix)); + expiresUnix, + fields[6], + version)); } } @@ -411,14 +414,21 @@ public sealed partial class MainPage : Page ? $"{protocol} {port} {ttlMinutes}" : $"{protocol} {port}"; using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(15)); - await _ssh.RunRestrictedCommandAsync( + var commandOutput = await _ssh.RunRestrictedCommandAsync( hub.Profile, $"mesh {action} {source.Name} {target.Name} {policyArguments}", timeout.Token); + var confirmation = commandOutput + .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) + .FirstOrDefault(line => line.StartsWith("LINK_STATE=", StringComparison.Ordinal)); + if (confirmation is null) + { + throw new InvalidOperationException("Hub не подтвердил фактическое состояние Link."); + } await RefreshMeshAsync(showSuccess: false); ShowInfo( - enable ? "Связь включена" : "Связь отключена", - $"{source.Name} → {target.Name} · {protocol.ToUpperInvariant()}/{port}", + enable ? "Hub подтвердил Active" : "Hub подтвердил Disabled", + $"{source.Name} → {target.Name} · {protocol.ToUpperInvariant()}/{port} · {confirmation[11..]}", enable ? InfoBarSeverity.Success : InfoBarSeverity.Warning); } catch (Exception exception) diff --git a/src/ServerMonitorManager.Desktop/MeshModels.cs b/src/ServerMonitorManager.Desktop/MeshModels.cs index 60f6c2f..b57352d 100644 --- a/src/ServerMonitorManager.Desktop/MeshModels.cs +++ b/src/ServerMonitorManager.Desktop/MeshModels.cs @@ -25,7 +25,9 @@ public sealed class MeshLinkViewModel string cidr, string protocol, int port, - long expiresUnix) + long expiresUnix, + string state, + long version) { Source = source; Target = target; @@ -33,6 +35,8 @@ public sealed class MeshLinkViewModel Protocol = protocol; Port = port; ExpiresUnix = expiresUnix; + State = state; + Version = version; } public string Source { get; set; } @@ -41,8 +45,10 @@ public sealed class MeshLinkViewModel public string Protocol { get; set; } public int Port { get; set; } public long ExpiresUnix { get; set; } + public string State { get; set; } + public long Version { get; set; } public string ExpirationText => ExpiresUnix == 0 ? "вручную" : $"до {DateTimeOffset.FromUnixTimeSeconds(ExpiresUnix).ToLocalTime():dd.MM HH:mm}"; - public string Label => $"{Source} → {Target} · {Protocol.ToUpperInvariant()}/{Port} · {Cidr} · {ExpirationText}"; + public string Label => $"{Source} → {Target} · {Protocol.ToUpperInvariant()}/{Port} · {Cidr} · {ExpirationText} · {State} v{Version}"; }