diff --git a/Proxmox Desktop/Api/ApiClient.cs b/Proxmox Desktop/Api/ApiClient.cs
index 215c2b9..5f52d96 100644
--- a/Proxmox Desktop/Api/ApiClient.cs
+++ b/Proxmox Desktop/Api/ApiClient.cs
@@ -41,6 +41,9 @@ public ApiClient(ServerInfo info)
public ApiClient(string server, string port, bool skipSsl)
: this(new ServerInfo(server, int.Parse(port), skipSsl)) { }
+ /// Hostname/IP this client is connected to — used as the friendly server name.
+ public string Host => _http.BaseAddress!.Host;
+
// ─── Auth ─────────────────────────────────────────────────────────────────────────
public async Task> GetRealmsAsync(CancellationToken ct = default)
diff --git a/Proxmox Desktop/Api/Models/MachineData.cs b/Proxmox Desktop/Api/Models/MachineData.cs
index c1c01b8..1a9e77e 100644
--- a/Proxmox Desktop/Api/Models/MachineData.cs
+++ b/Proxmox Desktop/Api/Models/MachineData.cs
@@ -19,9 +19,10 @@ public record MachineData
[JsonPropertyName("serial")] public int Serial { get; init; }
[JsonPropertyName("tags")] public string? Tags { get; init; }
- // Set by ApiClient after deserialization
- public string NodeName { get; init; } = string.Empty;
- public string Type { get; init; } = string.Empty;
+ // Set by ApiClient / ViewModel after deserialization
+ public string NodeName { get; init; } = string.Empty;
+ public string Type { get; init; } = string.Empty;
+ public string ServerName { get; init; } = string.Empty;
// Computed
public bool IsRunning => Status == "running";
diff --git a/Proxmox Desktop/App.xaml.cs b/Proxmox Desktop/App.xaml.cs
index 7d3c90d..06bd184 100644
--- a/Proxmox Desktop/App.xaml.cs
+++ b/Proxmox Desktop/App.xaml.cs
@@ -1,4 +1,6 @@
using System.Windows;
+using MaterialDesignThemes.Wpf;
+using ProxmoxDesktop.Config;
using ProxmoxDesktop.Services;
namespace ProxmoxDesktop;
@@ -9,9 +11,23 @@ protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
NotificationService.Enable();
+ ApplySavedTheme();
new Views.LoginWindow().Show();
}
+ private static void ApplySavedTheme()
+ {
+ try
+ {
+ var cfg = new ConfigurationService().Config;
+ var helper = new PaletteHelper();
+ var theme = helper.GetTheme();
+ theme.SetBaseTheme(cfg.IsDarkTheme ? BaseTheme.Dark : BaseTheme.Light);
+ helper.SetTheme(theme);
+ }
+ catch { /* fall back to the default theme from App.xaml */ }
+ }
+
protected override void OnExit(ExitEventArgs e)
{
NotificationService.Disable();
diff --git a/Proxmox Desktop/Controls/MachineCard.xaml b/Proxmox Desktop/Controls/MachineCard.xaml
index 2e4b7e5..199a44f 100644
--- a/Proxmox Desktop/Controls/MachineCard.xaml
+++ b/Proxmox Desktop/Controls/MachineCard.xaml
@@ -5,10 +5,12 @@
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:ProxmoxDesktop.Converters">
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -47,12 +63,25 @@
FontWeight="Medium" VerticalAlignment="Center"/>
+
+
+
+
@@ -62,8 +91,8 @@
-
-
-
+
+
+
-
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Proxmox Desktop/Views/MainWindow.xaml.cs b/Proxmox Desktop/Views/MainWindow.xaml.cs
index 56cce82..b7cf3c6 100644
--- a/Proxmox Desktop/Views/MainWindow.xaml.cs
+++ b/Proxmox Desktop/Views/MainWindow.xaml.cs
@@ -1,7 +1,8 @@
using System.Windows;
+using System.Windows.Input;
using MaterialDesignThemes.Wpf;
using ProxmoxDesktop.Api;
-using ProxmoxDesktop.Api.Models;
+using ProxmoxDesktop.Config;
using ProxmoxDesktop.Console;
using ProxmoxDesktop.ViewModels;
@@ -12,18 +13,31 @@ public partial class MainWindow : Window
public static MainWindow? Instance { get; private set; }
public ISnackbarMessageQueue SnackbarService => MainSnackbar.MessageQueue!;
- private readonly MainViewModel _vm;
- private bool _isDark = true;
+ private readonly MainViewModel _vm;
+ private readonly ConfigurationService _config = new();
+ private bool _isDark;
public MainWindow(IApiClient api)
{
InitializeComponent();
- Instance = this;
- _vm = new MainViewModel((ApiClient)api);
+ Instance = this;
+
+ _isDark = _config.Config.IsDarkTheme;
+ ApplyTheme();
+
+ _vm = new MainViewModel((ApiClient)api, _config.Config.RefreshIntervalSeconds);
DataContext = _vm;
+
_vm.OnLogout += () => { _vm.Dispose(); Instance = null; new LoginWindow().Show(); Close(); };
_vm.OnOpenConsole += (m, url) => new ConsoleWindow(m, url).Show();
_vm.OnOpenSpice += async cfg => await SpiceLauncher.LaunchAsync(cfg);
+ _vm.OnNotify += msg => Dispatcher.Invoke(() => SnackbarService.Enqueue(msg));
+ _vm.OnRequestAddServer += () =>
+ {
+ var dlg = new LoginWindow(connectedApi => _vm.AddConnection(connectedApi)) { Owner = this };
+ dlg.ShowDialog();
+ };
+
Loaded += async (_, _) => await _vm.RefreshAsync();
Closed += (_, _) => { _vm.Dispose(); Instance = null; };
}
@@ -31,10 +45,27 @@ public MainWindow(IApiClient api)
private void OnThemeToggle(object sender, RoutedEventArgs e)
{
_isDark = !_isDark;
+ ApplyTheme();
+ _config.Config.IsDarkTheme = _isDark;
+ _config.Save();
+ }
+
+ private void ApplyTheme()
+ {
var helper = new PaletteHelper();
var theme = helper.GetTheme();
theme.SetBaseTheme(_isDark ? BaseTheme.Dark : BaseTheme.Light);
helper.SetTheme(theme);
ThemeIcon.Kind = _isDark ? PackIconKind.WeatherNight : PackIconKind.WeatherSunny;
}
+
+ private void OnWindowPreviewKeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.F && Keyboard.Modifiers == ModifierKeys.Control)
+ {
+ SearchBox.Focus();
+ SearchBox.SelectAll();
+ e.Handled = true;
+ }
+ }
}
diff --git a/README.md b/README.md
index d5a7ff5..e87b9a6 100644
--- a/README.md
+++ b/README.md
@@ -29,16 +29,21 @@ Built with **WPF** and **Material Design**, using an **MVVM** architecture (Comm
## Features
-- 🖥️ **VM/LXC Dashboard** — Grid view of all machines with real-time status, CPU%, RAM%, uptime
+- 🖥️ **VM/LXC Dashboard** — Grid **or list** view of all machines with real-time status, CPU%, RAM%, uptime
+- 🌐 **Multi-server** — Connect several Proxmox clusters at once and see them side by side
- 📊 **Stats bar** — Total / Running / Stopped / VMs / LXC at a glance
-- 🗂️ **Node sidebar** — Filter by node with a single click
-- 🔍 **Search** — Filter by name, VMID or node
+- 🗂️ **Server & node sidebar** — Filter by cluster or node with a single click
+- 🏷️ **Proxmox tags** — Shown on each card, click a tag to filter
+- 🧭 **Sorting** — By name, VMID, CPU, RAM, status or uptime
+- 🕑 **Activity log** — Side panel recording power actions, state changes and errors
+- 🔍 **Search** — Filter by name, VMID, node or server
- 🖥️ **Integrated console** — NoVNC, xTermJS and SPICE (Virt-Viewer)
- ⚡ **Power control** — Start / Shutdown / Reboot / Suspend / Hibernate / Force Stop / Reset
- 🔐 **Dual authentication** — Classic login + TOTP, or Proxmox API Token
- 🔔 **Windows notifications** — Native toast on VM state changes
-- 🔄 **Auto-refresh** — Every 60 seconds, ticket renewed automatically every 90 min
-- 🌙 **Dark / Light theme** — Toggle in one click
+- ⌨️ **Keyboard shortcuts** — `F5` refresh · `Ctrl+F` search · `Esc` clear
+- 🔄 **Auto-refresh** — Configurable interval, ticket renewed automatically every 90 min
+- 🌙 **Dark / Light theme** — Toggle in one click, remembered between sessions
---
@@ -199,10 +204,12 @@ Proxmox-Desktop/
- [x] Group by node
- [x] Windows Toast notifications
- [x] Dark / Light theme
-- [ ] Multi-server support (multiple Proxmox clusters)
-- [ ] List view in addition to grid view
-- [ ] Proxmox tags displayed on cards
-- [ ] Action history / logs
+- [x] Multi-server support (multiple Proxmox clusters)
+- [x] List view in addition to grid view
+- [x] Proxmox tags displayed on cards
+- [x] Action history / logs
+- [ ] Bulk power actions on multiple machines
+- [ ] Per-VM resource graphs (history)
---