2 Commits
Author SHA1 Message Date
aria.hacker a299eaf925 Always render VM list; surface SPICE connect target for diagnosis
- Auto-connect (single-VM shortcut and autostart match) now renders the
  VM list first instead of skipping straight to the connect attempt.
  With cluster dedup collapsing 3 duplicate entries down to 1, the VM
  vanished from the screen entirely whenever the connection failed.
- proxmox:connect now returns the resolved host:port it handed to
  remote-viewer, shown in the success notice, so connection failures
  (e.g. Proxmox handing out a cluster-internal node address the client
  cannot route to) can be diagnosed without racing delete-this-file.
2026-07-06 20:45:50 +02:00
aria.hacker 0968468978 Dedup VMs across real Proxmox cluster nodes (was listing each VM once per entered host) 2026-07-06 20:02:57 +02:00
4 changed files with 54 additions and 13 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "proxmox-spice-client",
"version": "1.0.4",
"version": "1.0.6",
"description": "VDI SPICE Client f\u00fcr Proxmox",
"main": "src/main.js",
"scripts": {
+32 -4
View File
@@ -153,7 +153,30 @@ ipcMain.handle('proxmox:login', async (_e, { host, username, password }) => {
return { success: false, error: attempts[0].error };
}
clients = succeeded;
// Several hostnames can point at nodes of the SAME real Proxmox cluster
// (e.g. Stefan's setup) — in that case /cluster/resources returns the
// identical cluster-wide VM list from every one of them, so keeping all
// logged-in clients would just show each VM 3x. Detect real cluster
// membership and keep only one representative client per cluster.
const seenGroups = new Set();
const dedupedClients = [];
await Promise.all(
succeeded.map(async (entry) => {
try {
entry.clusterId = await entry.client.getClusterId();
} catch {
entry.clusterId = null;
}
})
);
for (const entry of succeeded) {
const group = entry.clusterId || `standalone:${entry.host}`;
if (seenGroups.has(group)) continue;
seenGroups.add(group);
dedupedClients.push(entry);
}
clients = dedupedClients;
createWindow('vms.html', 680, 520, true);
if (failed.length > 0) {
@@ -199,9 +222,9 @@ ipcMain.handle('proxmox:connect', async (_e, { host, node, vmid }) => {
const entry = clients.find((c) => c.host === host) || clients[0];
if (!entry) throw new Error('Nicht angemeldet.');
const params = await entry.client.getSpiceTicket(node, vmid);
const vvPath = buildVVFile(params, vmid, entry.client);
const { vvPath, target } = buildVVFile(params, vmid, entry.client);
launchRemoteViewer(vvPath);
return { success: true };
return { success: true, target };
} catch (err) {
return { success: false, error: err.message };
}
@@ -267,7 +290,12 @@ function buildVVFile(params, vmid, client) {
const vvPath = path.join(os.tmpdir(), `spice-${vmid}-${Date.now()}.vv`);
fs.writeFileSync(vvPath, lines.join('\n') + '\n', { mode: 0o600 });
return vvPath;
// Surfaced to the renderer so a failed connection (e.g. Proxmox handing out
// a cluster-internal node address the client can't route to) is diagnosable
// without having to catch the .vv file before remote-viewer deletes it.
const port = params['tls-port'] || params.port || '?';
return { vvPath, target: `${host}:${port}` };
}
// VirtViewer's Windows installer names its folder after the bundled version
+10
View File
@@ -121,6 +121,16 @@ class ProxmoxClient {
return vga.startsWith('qxl') || vga === 'virtio-vga-gl';
}
// Real Proxmox clusters: every member node returns the identical
// cluster-wide resource list, so logging into several nodes of the same
// cluster would just duplicate every VM. Returns the cluster's name, or
// null if this host isn't part of a cluster (standalone).
async getClusterId() {
const status = await this.request('GET', '/cluster/status');
const cluster = (status || []).find((s) => s.type === 'cluster');
return cluster ? cluster.name || cluster.id || null : null;
}
async getSpiceTicket(node, vmid) {
// proxy = address clients should connect to for SPICE traffic
const proxy = this.host.split(':')[0];
+11 -8
View File
@@ -76,6 +76,15 @@ async function loadVMs() {
return;
}
const autoConnect = await api.settings.getAutoConnect();
const autoMatch = autoConnect && vms.find(
(vm) => vm.host === autoConnect.host && String(vm.vmid) === String(autoConnect.vmid)
);
// Render the list first — even when we're about to auto-connect, Stefan
// needs to see the VM (and reach Settings/retry) if that connection fails.
renderList(vms, autoConnect);
// Auto-connect when there is exactly one SPICE VM
if (vms.length === 1) {
showNotice(`Nur eine VM verfügbar — verbinde mit „${vms[0].name}" …`);
@@ -84,18 +93,11 @@ async function loadVMs() {
}
// Auto-connect the VM the user marked for autostart (only one at a time)
const autoConnect = await api.settings.getAutoConnect();
const autoMatch = autoConnect && vms.find(
(vm) => vm.host === autoConnect.host && String(vm.vmid) === String(autoConnect.vmid)
);
if (autoMatch) {
showNotice(`Autostart-VM „${autoMatch.name}" markiert — verbinde …`);
await connectVM(autoMatch);
return;
}
renderList(vms, autoConnect);
}
loadVMs();
@@ -187,7 +189,8 @@ async function connectVM(vm) {
if (!result.success) {
showError(result.error || 'Verbindung fehlgeschlagen.');
} else {
showNotice(`${vm.name}" — SPICE-Sitzung gestartet.`);
const targetInfo = result.target ? ` (Ziel: ${result.target})` : '';
showNotice(`${vm.name}" — SPICE-Sitzung gestartet${targetInfo}.`);
}
}