Add dedicated desktop management pages
This commit is contained in:
parent
199e70c804
commit
a8978c085e
13 changed files with 533 additions and 54 deletions
|
|
@ -122,7 +122,7 @@ In the application, generate or copy the monitoring SSH key, add the Hub profile
|
||||||
|
|
||||||
`v0.1.0-alpha.4` is an early testing release, not a production security appliance. Windows and Linux builds, control-plane tests, Bash syntax checks, self-contained `linux-x64`/`linux-arm64` artifacts, and checksums are automated in GitHub Actions.
|
`v0.1.0-alpha.4` is an early testing release, not a production security appliance. Windows and Linux builds, control-plane tests, Bash syntax checks, self-contained `linux-x64`/`linux-arm64` artifacts, and checksums are automated in GitHub Actions.
|
||||||
|
|
||||||
The current development branch implements Windows SSH monitoring, the Hub/Node WireGuard installer, directional Links, one-time enrollment, separate mTLS Agent, Operator, and source-scoped Automation identities, certificate revocation/re-enrollment, SQLite control state, audit, authenticated event streaming, Windows Control API integration, and a bounded durable Agent buffer with downsampling.
|
The current development branch implements dedicated Windows pages for Servers, Links, Sessions, and Settings; SSH monitoring; the Hub/Node WireGuard installer; directional Links; one-time enrollment; separate mTLS Agent, Operator, and source-scoped Automation identities; certificate revocation/re-enrollment; SQLite control state; audit; authenticated event streaming; Windows Control API integration; and a bounded durable Agent buffer with downsampling.
|
||||||
|
|
||||||
Reconnect reconciliation is implemented with a durable SQLite marker: after a Node returns, the Hub reapplies the latest effective disabled policies and clears the marker only after the firewall confirms success. Linux CI exercises the real Control-to-helper process boundary, including a helper failure and Control process reconstruction over the same SQLite database. CI also runs a 100-Node concurrent heartbeat and replay scenario against one Hub store. Still planned: end-to-end nftables and host-reboot tests with the installer, a signed Windows installer, and desktop/mobile clients for additional platforms.
|
Reconnect reconciliation is implemented with a durable SQLite marker: after a Node returns, the Hub reapplies the latest effective disabled policies and clears the marker only after the firewall confirms success. Linux CI exercises the real Control-to-helper process boundary, including a helper failure and Control process reconstruction over the same SQLite database. CI also runs a 100-Node concurrent heartbeat and replay scenario against one Hub store. Still planned: end-to-end nftables and host-reboot tests with the installer, a signed Windows installer, and desktop/mobile clients for additional platforms.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
- [x] собрать и реально запустить x64-приложение;
|
- [x] собрать и реально запустить x64-приложение;
|
||||||
- [x] редактирование и удаление серверов;
|
- [x] редактирование и удаление серверов;
|
||||||
- [x] единственный изменяемый Hub;
|
- [x] единственный изменяемый Hub;
|
||||||
- [ ] настоящие страницы Servers, Links, Sessions и Settings.
|
- [x] настоящие страницы Servers, Links, Sessions и Settings.
|
||||||
|
|
||||||
## Этап 2 — установщик Hub/Node
|
## Этап 2 — установщик Hub/Node
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@
|
||||||
Text="SSH monitoring · серверы ещё не добавлены" />
|
Text="SSH monitoring · серверы ещё не добавлены" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<CommandBar
|
<CommandBar
|
||||||
|
x:Name="OverviewCommandBar"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
DefaultLabelPosition="Right">
|
DefaultLabelPosition="Right">
|
||||||
|
|
@ -425,22 +426,11 @@
|
||||||
</Grid>
|
</Grid>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
<StackPanel
|
<ContentPresenter
|
||||||
x:Name="NavigationPlaceholder"
|
x:Name="NavigationPageHost"
|
||||||
Grid.Row="3"
|
Grid.Row="3"
|
||||||
Margin="0,36,0,0"
|
Margin="0,20,0,0"
|
||||||
HorizontalAlignment="Center"
|
Visibility="Collapsed" />
|
||||||
VerticalAlignment="Top"
|
|
||||||
MaxWidth="560"
|
|
||||||
Spacing="12"
|
|
||||||
Visibility="Collapsed">
|
|
||||||
<TextBlock x:Name="PlaceholderTitle" HorizontalAlignment="Center" Style="{StaticResource TitleTextBlockStyle}" />
|
|
||||||
<TextBlock
|
|
||||||
x:Name="PlaceholderDescription"
|
|
||||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
|
||||||
TextAlignment="Center"
|
|
||||||
TextWrapping="Wrap" />
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</NavigationView>
|
</NavigationView>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ public sealed partial class MainPage : Page
|
||||||
private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromSeconds(30) };
|
private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromSeconds(30) };
|
||||||
private readonly SemaphoreSlim _refreshLock = new(1, 1);
|
private readonly SemaphoreSlim _refreshLock = new(1, 1);
|
||||||
private readonly CancellationTokenSource _controlCancellation = new();
|
private readonly CancellationTokenSource _controlCancellation = new();
|
||||||
|
private ServersPage? _serversPage;
|
||||||
|
private LinksPage? _linksPage;
|
||||||
|
private SessionsPage? _sessionsPage;
|
||||||
|
private SettingsPage? _settingsPage;
|
||||||
private bool _loaded;
|
private bool _loaded;
|
||||||
private bool _controlListening;
|
private bool _controlListening;
|
||||||
|
|
||||||
|
|
@ -38,6 +42,62 @@ public sealed partial class MainPage : Page
|
||||||
public ObservableCollection<MeshNodeViewModel> MeshNodes { get; } = [];
|
public ObservableCollection<MeshNodeViewModel> MeshNodes { get; } = [];
|
||||||
public ObservableCollection<MeshLinkViewModel> MeshLinks { get; } = [];
|
public ObservableCollection<MeshLinkViewModel> MeshLinks { get; } = [];
|
||||||
|
|
||||||
|
internal bool IsControlConfigured => _control.IsConfigured;
|
||||||
|
|
||||||
|
internal void AddServerFromPage() => AddServerButton_Click(this, new RoutedEventArgs());
|
||||||
|
|
||||||
|
internal void EditServerFromPage(ServerViewModel? server)
|
||||||
|
{
|
||||||
|
ServerList.SelectedItem = server;
|
||||||
|
EditServerButton_Click(this, new RoutedEventArgs());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void DeleteServerFromPage(ServerViewModel? server)
|
||||||
|
{
|
||||||
|
ServerList.SelectedItem = server;
|
||||||
|
DeleteServerButton_Click(this, new RoutedEventArgs());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void OpenTerminalFromPage(ServerViewModel? server)
|
||||||
|
{
|
||||||
|
ServerList.SelectedItem = server;
|
||||||
|
TerminalButton_Click(this, new RoutedEventArgs());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task RefreshServersFromPageAsync()
|
||||||
|
=> await RefreshAllAsync();
|
||||||
|
|
||||||
|
internal async Task RefreshLinksFromPageAsync()
|
||||||
|
=> await RefreshMeshAsync();
|
||||||
|
|
||||||
|
internal async Task ChangeLinkFromPageAsync(
|
||||||
|
MeshNodeViewModel? source,
|
||||||
|
MeshNodeViewModel? target,
|
||||||
|
string protocol,
|
||||||
|
int port,
|
||||||
|
int ttlMinutes,
|
||||||
|
bool enable)
|
||||||
|
{
|
||||||
|
SourceNodeBox.SelectedItem = source;
|
||||||
|
TargetNodeBox.SelectedItem = target;
|
||||||
|
LinkProtocolBox.SelectedIndex = string.Equals(protocol, "udp", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
|
||||||
|
LinkPortBox.Value = port;
|
||||||
|
LinkTtlBox.Value = ttlMinutes;
|
||||||
|
await ChangeLinkAsync(enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ReenrollNodeFromPage(MeshNodeViewModel? source)
|
||||||
|
{
|
||||||
|
SourceNodeBox.SelectedItem = source;
|
||||||
|
ReenrollNodeButton_Click(this, new RoutedEventArgs());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ShowSshKeyFromPage() => SshKeyButton_Click(this, new RoutedEventArgs());
|
||||||
|
|
||||||
|
internal void ConnectControlHubFromPage() => ControlHubButton_Click(this, new RoutedEventArgs());
|
||||||
|
|
||||||
|
internal void ExportDiagnosticsFromPage() => ExportDiagnosticsButton_Click(this, new RoutedEventArgs());
|
||||||
|
|
||||||
private async void MainPage_Loaded(object sender, RoutedEventArgs e)
|
private async void MainPage_Loaded(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (_loaded)
|
if (_loaded)
|
||||||
|
|
@ -1083,57 +1143,62 @@ public sealed partial class MainPage : Page
|
||||||
{
|
{
|
||||||
if (args.IsSettingsSelected)
|
if (args.IsSettingsSelected)
|
||||||
{
|
{
|
||||||
ShowPlaceholder(
|
_settingsPage ??= new SettingsPage(this);
|
||||||
"Настройки",
|
ShowNavigationPage(_settingsPage);
|
||||||
"Профили серверов хранятся локально. Настройки ключей, интервалов обновления и подтверждений будут добавляться здесь.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var tag = (args.SelectedItem as NavigationViewItem)?.Tag?.ToString() ?? "overview";
|
var tag = (args.SelectedItem as NavigationViewItem)?.Tag?.ToString() ?? "overview";
|
||||||
NavigationPlaceholder.Visibility = Visibility.Collapsed;
|
switch (tag)
|
||||||
|
{
|
||||||
|
case "servers":
|
||||||
|
_serversPage ??= new ServersPage(this);
|
||||||
|
ShowNavigationPage(_serversPage);
|
||||||
|
break;
|
||||||
|
case "links":
|
||||||
|
_linksPage ??= new LinksPage(this);
|
||||||
|
ShowNavigationPage(_linksPage);
|
||||||
|
break;
|
||||||
|
case "sessions":
|
||||||
|
_sessionsPage ??= new SessionsPage(this);
|
||||||
|
ShowNavigationPage(_sessionsPage);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ShowOverview();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowOverview()
|
||||||
|
{
|
||||||
|
NavigationPageHost.Visibility = Visibility.Collapsed;
|
||||||
|
NavigationPageHost.Content = null;
|
||||||
|
OverviewCommandBar.Visibility = Visibility.Visible;
|
||||||
OverviewSummary.Visibility = Visibility.Visible;
|
OverviewSummary.Visibility = Visibility.Visible;
|
||||||
WorkspaceScroll.Visibility = Visibility.Visible;
|
WorkspaceScroll.Visibility = Visibility.Visible;
|
||||||
ServerWorkspace.Visibility = tag is "overview" or "servers" ? Visibility.Visible : Visibility.Collapsed;
|
ServerWorkspace.Visibility = Visibility.Visible;
|
||||||
LinkInspector.Visibility = tag is "overview" or "links" ? Visibility.Visible : Visibility.Collapsed;
|
LinkInspector.Visibility = Visibility.Visible;
|
||||||
InspectorColumn.Width = tag == "overview" && ActualWidth >= 1080
|
|
||||||
? new GridLength(360)
|
|
||||||
: new GridLength(0);
|
|
||||||
|
|
||||||
if (tag == "links")
|
|
||||||
{
|
|
||||||
Grid.SetRow(LinkInspector, 0);
|
|
||||||
Grid.SetColumn(LinkInspector, 0);
|
|
||||||
Grid.SetColumnSpan(LinkInspector, 2);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Grid.SetColumnSpan(LinkInspector, 1);
|
Grid.SetColumnSpan(LinkInspector, 1);
|
||||||
if (ActualWidth >= 1080)
|
if (ActualWidth >= 1080)
|
||||||
{
|
{
|
||||||
Grid.SetRow(LinkInspector, 0);
|
Grid.SetRow(LinkInspector, 0);
|
||||||
Grid.SetColumn(LinkInspector, 1);
|
Grid.SetColumn(LinkInspector, 1);
|
||||||
|
InspectorColumn.Width = new GridLength(360);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Grid.SetRow(LinkInspector, 1);
|
Grid.SetRow(LinkInspector, 1);
|
||||||
Grid.SetColumn(LinkInspector, 0);
|
Grid.SetColumn(LinkInspector, 0);
|
||||||
|
InspectorColumn.Width = new GridLength(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tag == "sessions")
|
private void ShowNavigationPage(Page page)
|
||||||
{
|
|
||||||
ShowPlaceholder(
|
|
||||||
"SSH-сессии",
|
|
||||||
"Интерактивные терминалы будут использовать отдельную identity и не получат ключ мониторинга или права Mesh Hub.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowPlaceholder(string title, string description)
|
|
||||||
{
|
{
|
||||||
|
OverviewCommandBar.Visibility = Visibility.Collapsed;
|
||||||
OverviewSummary.Visibility = Visibility.Collapsed;
|
OverviewSummary.Visibility = Visibility.Collapsed;
|
||||||
WorkspaceScroll.Visibility = Visibility.Collapsed;
|
WorkspaceScroll.Visibility = Visibility.Collapsed;
|
||||||
PlaceholderTitle.Text = title;
|
NavigationPageHost.Content = page;
|
||||||
PlaceholderDescription.Text = description;
|
NavigationPageHost.Visibility = Visibility.Visible;
|
||||||
NavigationPlaceholder.Visibility = Visibility.Visible;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
104
src/ServerMonitorManager.Desktop/Pages/LinksPage.xaml
Normal file
104
src/ServerMonitorManager.Desktop/Pages/LinksPage.xaml
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<Page
|
||||||
|
x:Class="ServerMonitorManager_Desktop.LinksPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:ServerMonitorManager_Desktop">
|
||||||
|
|
||||||
|
<Grid RowSpacing="16">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid ColumnSpacing="12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Spacing="3">
|
||||||
|
<TextBlock Style="{StaticResource TitleTextBlockStyle}" Text="Связи серверов" />
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Направленные разрешения source → destination с ручным kill switch" />
|
||||||
|
</StackPanel>
|
||||||
|
<CommandBar Grid.Column="1" Background="Transparent" DefaultLabelPosition="Right">
|
||||||
|
<AppBarButton AutomationProperties.Name="Обновить узлы и связи" Click="RefreshButton_Click" Icon="Refresh" Label="Обновить" />
|
||||||
|
<AppBarButton AutomationProperties.Name="Перерегистрировать выбранный исходный Node" Click="ReenrollButton_Click" Label="Перерегистрировать">
|
||||||
|
<AppBarButton.Icon><FontIcon Glyph="" /></AppBarButton.Icon>
|
||||||
|
</AppBarButton>
|
||||||
|
</CommandBar>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid x:Name="LinksWorkspace" Grid.Row="1" ColumnSpacing="20" RowSpacing="20">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition x:Name="EditorColumn" Width="*" />
|
||||||
|
<ColumnDefinition x:Name="ListColumn" Width="0" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
x:Name="LinkEditor"
|
||||||
|
Padding="18"
|
||||||
|
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||||
|
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="8">
|
||||||
|
<StackPanel Spacing="14">
|
||||||
|
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="Управление доступом" />
|
||||||
|
<ComboBox x:Name="SourceNodeBox" AutomationProperties.Name="Источник связи" DisplayMemberPath="Label" Header="Источник доступа" ItemsSource="{x:Bind Nodes}" />
|
||||||
|
<ComboBox x:Name="TargetNodeBox" AutomationProperties.Name="Цель связи" DisplayMemberPath="Label" Header="Целевой сервер" ItemsSource="{x:Bind Nodes}" />
|
||||||
|
<Grid ColumnSpacing="10">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions>
|
||||||
|
<ComboBox x:Name="ProtocolBox" Header="Протокол" SelectedIndex="0">
|
||||||
|
<ComboBoxItem Content="TCP" Tag="tcp" />
|
||||||
|
<ComboBoxItem Content="UDP" Tag="udp" />
|
||||||
|
</ComboBox>
|
||||||
|
<NumberBox x:Name="PortBox" Grid.Column="1" Header="Порт" Maximum="65535" Minimum="1" SpinButtonPlacementMode="Compact" Value="22" />
|
||||||
|
</Grid>
|
||||||
|
<NumberBox x:Name="TtlBox" Header="TTL, минут (0 — вручную)" Maximum="525600" Minimum="0" SpinButtonPlacementMode="Compact" Value="120" />
|
||||||
|
<Grid ColumnSpacing="10">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions>
|
||||||
|
<Button HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Click="ConnectButton_Click" Content="Разрешить" Style="{StaticResource AccentButtonStyle}" />
|
||||||
|
<Button Grid.Column="1" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" Click="DisconnectButton_Click" Content="Отключить" />
|
||||||
|
</Grid>
|
||||||
|
<InfoBar IsClosable="False" IsOpen="True" Message="Обратное направление не включается автоматически. Отключение Link удаляет разрешение nftables на Hub." Severity="Informational" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Grid x:Name="LinksListPanel" Grid.Row="1" RowSpacing="10">
|
||||||
|
<Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="*" /></Grid.RowDefinitions>
|
||||||
|
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="Разрешённые направления" />
|
||||||
|
<ListView x:Name="LinksList" Grid.Row="1" AutomationProperties.Name="Список связей" ItemsSource="{x:Bind Links}" SelectionMode="Single">
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="local:MeshLinkViewModel">
|
||||||
|
<Grid MinHeight="62" ColumnSpacing="12">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="Auto" /></Grid.ColumnDefinitions>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind Label}" TextWrapping="Wrap" />
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Направленная политика доступа" />
|
||||||
|
</StackPanel>
|
||||||
|
<FontIcon Grid.Column="1" VerticalAlignment="Center" Glyph="" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<VisualStateManager.VisualStateGroups>
|
||||||
|
<VisualStateGroup>
|
||||||
|
<VisualState x:Name="NarrowState" />
|
||||||
|
<VisualState x:Name="WideState">
|
||||||
|
<VisualState.StateTriggers><AdaptiveTrigger MinWindowWidth="820" /></VisualState.StateTriggers>
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Target="EditorColumn.Width" Value="360" />
|
||||||
|
<Setter Target="ListColumn.Width" Value="*" />
|
||||||
|
<Setter Target="LinksListPanel.(Grid.Row)" Value="0" />
|
||||||
|
<Setter Target="LinksListPanel.(Grid.Column)" Value="1" />
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
</VisualStateManager.VisualStateGroups>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
44
src/ServerMonitorManager.Desktop/Pages/LinksPage.xaml.cs
Normal file
44
src/ServerMonitorManager.Desktop/Pages/LinksPage.xaml.cs
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace ServerMonitorManager_Desktop;
|
||||||
|
|
||||||
|
public sealed partial class LinksPage : Page
|
||||||
|
{
|
||||||
|
private readonly MainPage _host;
|
||||||
|
|
||||||
|
internal LinksPage(MainPage host)
|
||||||
|
{
|
||||||
|
_host = host;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<MeshNodeViewModel> Nodes => _host.MeshNodes;
|
||||||
|
|
||||||
|
public ObservableCollection<MeshLinkViewModel> Links => _host.MeshLinks;
|
||||||
|
|
||||||
|
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> await _host.RefreshLinksFromPageAsync();
|
||||||
|
|
||||||
|
private async void ConnectButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> await ChangeLinkAsync(enable: true);
|
||||||
|
|
||||||
|
private async void DisconnectButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> await ChangeLinkAsync(enable: false);
|
||||||
|
|
||||||
|
private void ReenrollButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> _host.ReenrollNodeFromPage(SourceNodeBox.SelectedItem as MeshNodeViewModel);
|
||||||
|
|
||||||
|
private async Task ChangeLinkAsync(bool enable)
|
||||||
|
{
|
||||||
|
var protocol = (ProtocolBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "tcp";
|
||||||
|
await _host.ChangeLinkFromPageAsync(
|
||||||
|
SourceNodeBox.SelectedItem as MeshNodeViewModel,
|
||||||
|
TargetNodeBox.SelectedItem as MeshNodeViewModel,
|
||||||
|
protocol,
|
||||||
|
double.IsNaN(PortBox.Value) ? 0 : checked((int)PortBox.Value),
|
||||||
|
double.IsNaN(TtlBox.Value) ? 0 : checked((int)TtlBox.Value),
|
||||||
|
enable);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/ServerMonitorManager.Desktop/Pages/ServersPage.xaml
Normal file
75
src/ServerMonitorManager.Desktop/Pages/ServersPage.xaml
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<Page
|
||||||
|
x:Class="ServerMonitorManager_Desktop.ServersPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:ServerMonitorManager_Desktop">
|
||||||
|
|
||||||
|
<Grid RowSpacing="12">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Grid ColumnSpacing="12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Spacing="3">
|
||||||
|
<TextBlock Style="{StaticResource TitleTextBlockStyle}" Text="Серверы" />
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Локальные SSH-профили и текущее состояние ресурсов" />
|
||||||
|
</StackPanel>
|
||||||
|
<CommandBar Grid.Column="1" Background="Transparent" DefaultLabelPosition="Right">
|
||||||
|
<AppBarButton AutomationProperties.Name="Обновить все серверы" Click="RefreshButton_Click" Icon="Refresh" Label="Обновить" />
|
||||||
|
<AppBarButton AutomationProperties.Name="Добавить сервер" Click="AddButton_Click" Icon="Add" Label="Добавить" />
|
||||||
|
<AppBarButton AutomationProperties.Name="Изменить выбранный сервер" Click="EditButton_Click" Icon="Edit" Label="Изменить" />
|
||||||
|
<AppBarButton AutomationProperties.Name="Удалить выбранный сервер" Click="DeleteButton_Click" Icon="Delete" Label="Удалить" />
|
||||||
|
</CommandBar>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<InfoBar
|
||||||
|
x:Name="EmptyServersInfo"
|
||||||
|
Grid.Row="1"
|
||||||
|
IsClosable="False"
|
||||||
|
Message="Добавьте первый сервер. Пароли не сохраняются; мониторинг использует отдельный ограниченный SSH-ключ."
|
||||||
|
Severity="Informational"
|
||||||
|
Title="Список серверов пуст" />
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
x:Name="ServerList"
|
||||||
|
Grid.Row="2"
|
||||||
|
AutomationProperties.Name="Список серверов"
|
||||||
|
ItemsSource="{x:Bind Servers}"
|
||||||
|
SelectionMode="Single">
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="local:ServerViewModel">
|
||||||
|
<Grid MinHeight="88" Padding="4" ColumnSpacing="18">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="2*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel VerticalAlignment="Center" Spacing="2">
|
||||||
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind Name}" />
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind Endpoint}" />
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind Status, Mode=OneWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="1" VerticalAlignment="Center" Spacing="6">
|
||||||
|
<TextBlock Text="{x:Bind CpuText, Mode=OneWay}" />
|
||||||
|
<ProgressBar Maximum="100" Value="{x:Bind CpuPercent, Mode=OneWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" VerticalAlignment="Center" Spacing="2">
|
||||||
|
<TextBlock Text="{x:Bind MemoryText, Mode=OneWay}" />
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind DiskText, Mode=OneWay}" />
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind HealthText, Mode=OneWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Grid.Column="3" VerticalAlignment="Center" Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind LatencyText, Mode=OneWay}" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
33
src/ServerMonitorManager.Desktop/Pages/ServersPage.xaml.cs
Normal file
33
src/ServerMonitorManager.Desktop/Pages/ServersPage.xaml.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace ServerMonitorManager_Desktop;
|
||||||
|
|
||||||
|
public sealed partial class ServersPage : Page
|
||||||
|
{
|
||||||
|
private readonly MainPage _host;
|
||||||
|
|
||||||
|
internal ServersPage(MainPage host)
|
||||||
|
{
|
||||||
|
_host = host;
|
||||||
|
InitializeComponent();
|
||||||
|
Servers.CollectionChanged += (_, _) => UpdateEmptyState();
|
||||||
|
Loaded += (_, _) => UpdateEmptyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<ServerViewModel> Servers => _host.Servers;
|
||||||
|
|
||||||
|
private ServerViewModel? SelectedServer => ServerList.SelectedItem as ServerViewModel;
|
||||||
|
|
||||||
|
private void AddButton_Click(object sender, RoutedEventArgs e) => _host.AddServerFromPage();
|
||||||
|
|
||||||
|
private void EditButton_Click(object sender, RoutedEventArgs e) => _host.EditServerFromPage(SelectedServer);
|
||||||
|
|
||||||
|
private void DeleteButton_Click(object sender, RoutedEventArgs e) => _host.DeleteServerFromPage(SelectedServer);
|
||||||
|
|
||||||
|
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> await _host.RefreshServersFromPageAsync();
|
||||||
|
|
||||||
|
private void UpdateEmptyState() => EmptyServersInfo.IsOpen = Servers.Count == 0;
|
||||||
|
}
|
||||||
59
src/ServerMonitorManager.Desktop/Pages/SessionsPage.xaml
Normal file
59
src/ServerMonitorManager.Desktop/Pages/SessionsPage.xaml
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<Page
|
||||||
|
x:Class="ServerMonitorManager_Desktop.SessionsPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:ServerMonitorManager_Desktop">
|
||||||
|
|
||||||
|
<Grid RowSpacing="16">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid ColumnSpacing="12">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel Spacing="3">
|
||||||
|
<TextBlock Style="{StaticResource TitleTextBlockStyle}" Text="SSH-сессии" />
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Прямые интерактивные подключения через системный OpenSSH" />
|
||||||
|
</StackPanel>
|
||||||
|
<CommandBar Grid.Column="1" Background="Transparent" DefaultLabelPosition="Right">
|
||||||
|
<AppBarButton AutomationProperties.Name="Открыть SSH-терминал для выбранного сервера" Click="OpenTerminalButton_Click" Icon="OpenFile" Label="Открыть терминал" />
|
||||||
|
</CommandBar>
|
||||||
|
</Grid>
|
||||||
|
<InfoBar
|
||||||
|
Grid.Row="1"
|
||||||
|
IsClosable="False"
|
||||||
|
IsOpen="True"
|
||||||
|
Message="Интерактивный терминал использует ваши обычные SSH-ключи. Ограниченный ключ мониторинга, Operator-сертификат и Automation identity в терминал не передаются."
|
||||||
|
Severity="Informational"
|
||||||
|
Title="Разделение полномочий" />
|
||||||
|
<ListView
|
||||||
|
x:Name="SessionTargetsList"
|
||||||
|
Grid.Row="2"
|
||||||
|
AutomationProperties.Name="Серверы для SSH-сессии"
|
||||||
|
ItemsSource="{x:Bind Servers}"
|
||||||
|
SelectionMode="Single">
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="local:ServerViewModel">
|
||||||
|
<Grid MinHeight="68" ColumnSpacing="16">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="2*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind Name}" />
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="{x:Bind Endpoint}" />
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Grid.Column="1" VerticalAlignment="Center" Text="{x:Bind Status, Mode=OneWay}" />
|
||||||
|
<FontIcon Grid.Column="2" VerticalAlignment="Center" Glyph="" />
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
21
src/ServerMonitorManager.Desktop/Pages/SessionsPage.xaml.cs
Normal file
21
src/ServerMonitorManager.Desktop/Pages/SessionsPage.xaml.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace ServerMonitorManager_Desktop;
|
||||||
|
|
||||||
|
public sealed partial class SessionsPage : Page
|
||||||
|
{
|
||||||
|
private readonly MainPage _host;
|
||||||
|
|
||||||
|
internal SessionsPage(MainPage host)
|
||||||
|
{
|
||||||
|
_host = host;
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<ServerViewModel> Servers => _host.Servers;
|
||||||
|
|
||||||
|
private void OpenTerminalButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> _host.OpenTerminalFromPage(SessionTargetsList.SelectedItem as ServerViewModel);
|
||||||
|
}
|
||||||
54
src/ServerMonitorManager.Desktop/Pages/SettingsPage.xaml
Normal file
54
src/ServerMonitorManager.Desktop/Pages/SettingsPage.xaml
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<Page
|
||||||
|
x:Class="ServerMonitorManager_Desktop.SettingsPage"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<ScrollViewer>
|
||||||
|
<StackPanel MaxWidth="840" HorizontalAlignment="Left" Spacing="20">
|
||||||
|
<StackPanel Spacing="3">
|
||||||
|
<TextBlock Style="{StaticResource TitleTextBlockStyle}" Text="Настройки" />
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Подключения, локальные ключи и обслуживание приложения" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid ColumnSpacing="16" RowSpacing="16">
|
||||||
|
<Grid.ColumnDefinitions><ColumnDefinition Width="*" /><ColumnDefinition Width="*" /></Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions><RowDefinition Height="Auto" /><RowDefinition Height="Auto" /></Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<Border Padding="18" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1" CornerRadius="8">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="Control Hub" />
|
||||||
|
<TextBlock x:Name="ControlStatusText" Foreground="{ThemeResource TextFillColorSecondaryBrush}" TextWrapping="Wrap" />
|
||||||
|
<Button HorizontalAlignment="Left" Click="ConnectControlButton_Click" Content="Подключить по SMMDEV1" Style="{StaticResource AccentButtonStyle}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Column="1" Padding="18" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1" CornerRadius="8">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="SSH-ключ мониторинга" />
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Ed25519 private key защищён Windows DPAPI и не передаётся Hub." TextWrapping="Wrap" />
|
||||||
|
<Button HorizontalAlignment="Left" Click="ShowSshKeyButton_Click" Content="Показать публичный ключ" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Row="1" Padding="18" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1" CornerRadius="8">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="Мониторинг" />
|
||||||
|
<TextBlock Text="Интервал обновления: 30 секунд" />
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="История: до 240 точек на сервер. Старые offline-точки уплотняются Linux Agent." TextWrapping="Wrap" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Row="1" Grid.Column="1" Padding="18" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1" CornerRadius="8">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<TextBlock Style="{StaticResource SubtitleTextBlockStyle}" Text="Диагностика" />
|
||||||
|
<TextBlock Foreground="{ThemeResource TextFillColorSecondaryBrush}" Text="Экспорт не содержит адресов, пользователей, ключей, сертификатов или токенов." TextWrapping="Wrap" />
|
||||||
|
<Button HorizontalAlignment="Left" Click="ExportDiagnosticsButton_Click" Content="Экспортировать JSON" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<InfoBar IsClosable="False" IsOpen="True" Message="Agent, Operator, terminal и Automation используют разные identities. Изменять Links может только Operator." Severity="Informational" Title="Безопасность" />
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Page>
|
||||||
33
src/ServerMonitorManager.Desktop/Pages/SettingsPage.xaml.cs
Normal file
33
src/ServerMonitorManager.Desktop/Pages/SettingsPage.xaml.cs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace ServerMonitorManager_Desktop;
|
||||||
|
|
||||||
|
public sealed partial class SettingsPage : Page
|
||||||
|
{
|
||||||
|
private readonly MainPage _host;
|
||||||
|
|
||||||
|
internal SettingsPage(MainPage host)
|
||||||
|
{
|
||||||
|
_host = host;
|
||||||
|
InitializeComponent();
|
||||||
|
Loaded += (_, _) => UpdateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConnectControlButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_host.ConnectControlHubFromPage();
|
||||||
|
UpdateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowSshKeyButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> _host.ShowSshKeyFromPage();
|
||||||
|
|
||||||
|
private void ExportDiagnosticsButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
=> _host.ExportDiagnosticsFromPage();
|
||||||
|
|
||||||
|
private void UpdateStatus()
|
||||||
|
=> ControlStatusText.Text = _host.IsControlConfigured
|
||||||
|
? "Operator-сертификат установлен и защищён Windows DPAPI."
|
||||||
|
: "Control Hub ещё не подключён. Создайте одноразовый device code на Hub.";
|
||||||
|
}
|
||||||
|
|
@ -73,7 +73,8 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
|
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
|
||||||
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
||||||
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
|
<!-- WinUI pages are activated through generated XAML metadata. Keep trimming disabled
|
||||||
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
|
until every desktop serialization and activation path has an explicit trim contract. -->
|
||||||
|
<PublishTrimmed>False</PublishTrimmed>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue