Add dedicated desktop management pages

This commit is contained in:
Ochenstarik 2026-07-17 08:51:21 +07:00
parent 199e70c804
commit a8978c085e
13 changed files with 533 additions and 54 deletions

View file

@ -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.
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.

View file

@ -20,7 +20,7 @@
- [x] собрать и реально запустить x64-приложение;
- [x] редактирование и удаление серверов;
- [x] единственный изменяемый Hub;
- [ ] настоящие страницы Servers, Links, Sessions и Settings.
- [x] настоящие страницы Servers, Links, Sessions и Settings.
## Этап 2 — установщик Hub/Node

View file

@ -61,6 +61,7 @@
Text="SSH monitoring · серверы ещё не добавлены" />
</StackPanel>
<CommandBar
x:Name="OverviewCommandBar"
Grid.Column="1"
Background="Transparent"
DefaultLabelPosition="Right">
@ -425,22 +426,11 @@
</Grid>
</ScrollViewer>
<StackPanel
x:Name="NavigationPlaceholder"
<ContentPresenter
x:Name="NavigationPageHost"
Grid.Row="3"
Margin="0,36,0,0"
HorizontalAlignment="Center"
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>
Margin="0,20,0,0"
Visibility="Collapsed" />
</Grid>
</NavigationView>
</Page>

View file

@ -22,6 +22,10 @@ public sealed partial class MainPage : Page
private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromSeconds(30) };
private readonly SemaphoreSlim _refreshLock = new(1, 1);
private readonly CancellationTokenSource _controlCancellation = new();
private ServersPage? _serversPage;
private LinksPage? _linksPage;
private SessionsPage? _sessionsPage;
private SettingsPage? _settingsPage;
private bool _loaded;
private bool _controlListening;
@ -38,6 +42,62 @@ public sealed partial class MainPage : Page
public ObservableCollection<MeshNodeViewModel> MeshNodes { 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)
{
if (_loaded)
@ -1083,57 +1143,62 @@ public sealed partial class MainPage : Page
{
if (args.IsSettingsSelected)
{
ShowPlaceholder(
"Настройки",
"Профили серверов хранятся локально. Настройки ключей, интервалов обновления и подтверждений будут добавляться здесь.");
_settingsPage ??= new SettingsPage(this);
ShowNavigationPage(_settingsPage);
return;
}
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;
WorkspaceScroll.Visibility = Visibility.Visible;
ServerWorkspace.Visibility = tag is "overview" or "servers" ? Visibility.Visible : Visibility.Collapsed;
LinkInspector.Visibility = tag is "overview" or "links" ? Visibility.Visible : Visibility.Collapsed;
InspectorColumn.Width = tag == "overview" && ActualWidth >= 1080
? new GridLength(360)
: new GridLength(0);
if (tag == "links")
ServerWorkspace.Visibility = Visibility.Visible;
LinkInspector.Visibility = Visibility.Visible;
Grid.SetColumnSpan(LinkInspector, 1);
if (ActualWidth >= 1080)
{
Grid.SetRow(LinkInspector, 0);
Grid.SetColumn(LinkInspector, 0);
Grid.SetColumnSpan(LinkInspector, 2);
Grid.SetColumn(LinkInspector, 1);
InspectorColumn.Width = new GridLength(360);
}
else
{
Grid.SetColumnSpan(LinkInspector, 1);
if (ActualWidth >= 1080)
{
Grid.SetRow(LinkInspector, 0);
Grid.SetColumn(LinkInspector, 1);
}
else
{
Grid.SetRow(LinkInspector, 1);
Grid.SetColumn(LinkInspector, 0);
}
}
if (tag == "sessions")
{
ShowPlaceholder(
"SSH-сессии",
"Интерактивные терминалы будут использовать отдельную identity и не получат ключ мониторинга или права Mesh Hub.");
Grid.SetRow(LinkInspector, 1);
Grid.SetColumn(LinkInspector, 0);
InspectorColumn.Width = new GridLength(0);
}
}
private void ShowPlaceholder(string title, string description)
private void ShowNavigationPage(Page page)
{
OverviewCommandBar.Visibility = Visibility.Collapsed;
OverviewSummary.Visibility = Visibility.Collapsed;
WorkspaceScroll.Visibility = Visibility.Collapsed;
PlaceholderTitle.Text = title;
PlaceholderDescription.Text = description;
NavigationPlaceholder.Visibility = Visibility.Visible;
NavigationPageHost.Content = page;
NavigationPageHost.Visibility = Visibility.Visible;
}
}

View 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="&#xE8D7;" /></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="&#xE72A;" />
</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>

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

View 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>

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

View 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="&#xE756;" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Page>

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

View 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>

View 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.";
}

View file

@ -73,7 +73,8 @@
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
<!-- WinUI pages are activated through generated XAML metadata. Keep trimming disabled
until every desktop serialization and activation path has an explicit trim contract. -->
<PublishTrimmed>False</PublishTrimmed>
</PropertyGroup>
</Project>