diff --git a/docs/roadmap.md b/docs/roadmap.md
index e7e9885..4bc7e04 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -18,8 +18,8 @@
- [x] сохранять профили локально без паролей;
- [x] получать CPU/load, RAM, disk, uptime и latency;
- [x] собрать и реально запустить x64-приложение;
-- [ ] редактирование и удаление серверов;
-- [ ] единственный изменяемый Hub;
+- [x] редактирование и удаление серверов;
+- [x] единственный изменяемый Hub;
- [ ] настоящие страницы Servers, Links, Sessions и Settings.
## Этап 2 — установщик Hub/Node
diff --git a/src/ServerMonitorManager.Desktop/MainPage.xaml b/src/ServerMonitorManager.Desktop/MainPage.xaml
index d37793a..687b736 100644
--- a/src/ServerMonitorManager.Desktop/MainPage.xaml
+++ b/src/ServerMonitorManager.Desktop/MainPage.xaml
@@ -79,6 +79,16 @@
Click="AddServerButton_Click"
Icon="Add"
Label="Добавить сервер" />
+
+
diff --git a/src/ServerMonitorManager.Desktop/MainPage.xaml.cs b/src/ServerMonitorManager.Desktop/MainPage.xaml.cs
index a3f1b50..2dff0cf 100644
--- a/src/ServerMonitorManager.Desktop/MainPage.xaml.cs
+++ b/src/ServerMonitorManager.Desktop/MainPage.xaml.cs
@@ -154,6 +154,11 @@ public sealed partial class MainPage : Page
{
return;
}
+ if (hubBox.IsChecked == true && Servers.Any(server => server.IsHub))
+ {
+ ShowInfo("Hub уже выбран", "Измените существующий Hub или снимите эту отметку.", InfoBarSeverity.Warning);
+ return;
+ }
var profile = new ServerProfileData(
Guid.NewGuid().ToString("N"),
@@ -169,6 +174,99 @@ public sealed partial class MainPage : Page
await RefreshServerAsync(server);
}
+ private async void EditServerButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (ServerList.SelectedItem is not ServerViewModel selected)
+ {
+ ShowInfo("Сервер не выбран", "Выберите сервер в списке для изменения.", InfoBarSeverity.Warning);
+ return;
+ }
+
+ var nameBox = new TextBox { Header = "Название", Text = selected.Profile.Name };
+ var hostBox = new TextBox { Header = "IP или домен", Text = selected.Profile.Host };
+ var portBox = new NumberBox
+ {
+ Header = "SSH-порт",
+ Value = selected.Profile.Port,
+ Minimum = 1,
+ Maximum = 65535,
+ SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Compact
+ };
+ var userBox = new TextBox { Header = "Пользователь", Text = selected.Profile.User };
+ var hubBox = new CheckBox { Content = "Это главный Mesh Hub", IsChecked = selected.IsHub };
+ AutomationProperties.SetName(hubBox, "Использовать сервер как главный Mesh Hub");
+ var dialog = new ContentDialog
+ {
+ XamlRoot = XamlRoot,
+ Title = "Изменить сервер",
+ Content = new StackPanel
+ {
+ Spacing = 12,
+ MinWidth = 420,
+ Children = { nameBox, hostBox, portBox, userBox, hubBox }
+ },
+ PrimaryButtonText = "Сохранить",
+ CloseButtonText = "Отмена",
+ DefaultButton = ContentDialogButton.Primary
+ };
+ if (await dialog.ShowAsync() != ContentDialogResult.Primary)
+ {
+ return;
+ }
+ if (string.IsNullOrWhiteSpace(nameBox.Text)
+ || string.IsNullOrWhiteSpace(hostBox.Text)
+ || string.IsNullOrWhiteSpace(userBox.Text)
+ || double.IsNaN(portBox.Value))
+ {
+ ShowInfo("Данные не сохранены", "Название, адрес, порт и пользователь обязательны.", InfoBarSeverity.Warning);
+ return;
+ }
+ if (hubBox.IsChecked == true && Servers.Any(server => server.IsHub && server != selected))
+ {
+ ShowInfo("Hub уже выбран", "В конфигурации может быть только один главный Mesh Hub.", InfoBarSeverity.Warning);
+ return;
+ }
+
+ var index = Servers.IndexOf(selected);
+ var updated = new ServerViewModel(new ServerProfileData(
+ selected.Profile.Id,
+ nameBox.Text.Trim(),
+ hostBox.Text.Trim(),
+ checked((int)portBox.Value),
+ userBox.Text.Trim(),
+ hubBox.IsChecked == true));
+ Servers[index] = updated;
+ await SaveProfilesAsync();
+ await RefreshServerAsync(updated);
+ ShowInfo("Сервер изменён", updated.Name, InfoBarSeverity.Success);
+ }
+
+ private async void DeleteServerButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (ServerList.SelectedItem is not ServerViewModel selected)
+ {
+ ShowInfo("Сервер не выбран", "Выберите сервер в списке для удаления.", InfoBarSeverity.Warning);
+ return;
+ }
+ var dialog = new ContentDialog
+ {
+ XamlRoot = XamlRoot,
+ Title = $"Удалить {selected.Name}?",
+ Content = "Удаляется только локальный профиль. Серверная часть и WireGuard Node останутся установленными.",
+ PrimaryButtonText = "Удалить профиль",
+ CloseButtonText = "Отмена",
+ DefaultButton = ContentDialogButton.Close
+ };
+ if (await dialog.ShowAsync() != ContentDialogResult.Primary)
+ {
+ return;
+ }
+ Servers.Remove(selected);
+ await SaveProfilesAsync();
+ UpdateEmptyState();
+ ShowInfo("Профиль удалён", selected.Name, InfoBarSeverity.Success);
+ }
+
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
=> await RefreshAllAsync();