From ebb405ab9bf36d32c558f1ee421b0f6f4ee244c9 Mon Sep 17 00:00:00 2001 From: ARIA Date: Mon, 6 Jul 2026 10:29:44 +0200 Subject: [PATCH] Support multiple standalone Proxmox hosts with shared credentials Comma-separated host list logs into every host, merges VM lists tagged by origin host, and routes SPICE connections to the right one. Also adds a username format hint (user@realm) on the login form. --- package.json | 2 +- src/main.js | 82 +++++++++++++++++++++++++++++++++-------- src/renderer/login.html | 6 ++- src/renderer/styles.css | 7 ++++ src/renderer/vms.js | 13 ++++++- 5 files changed, 90 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 4d7a382..4e8128e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxmox-spice-client", - "version": "1.0.1", + "version": "1.0.2", "description": "VDI SPICE Client f\u00fcr Proxmox", "main": "src/main.js", "scripts": { diff --git a/src/main.js b/src/main.js index 79553e0..9e5df82 100644 --- a/src/main.js +++ b/src/main.js @@ -8,7 +8,10 @@ const ProxmoxClient = require('./proxmox'); const store = new Store({ name: 'proxmox-spice-client' }); let mainWindow; -let client; // ProxmoxClient instance (persists across login → VM views) +// One ProxmoxClient per logged-in host — several standalone Proxmox hosts can +// share the same root@pam credentials without being a real PVE cluster, so we +// log into every host the user entered and merge their VM lists. +let clients = []; // ---------- Window management ---------- @@ -120,34 +123,83 @@ ipcMain.handle('credentials:clear', () => { // ---------- IPC: Proxmox ---------- ipcMain.handle('proxmox:login', async (_e, { host, username, password }) => { - try { - client = new ProxmoxClient(host); - await client.login(username, password); - createWindow('vms.html', 680, 520, true); - return { success: true }; - } catch (err) { - return { success: false, error: err.message }; + const hosts = String(host || '') + .split(',') + .map((h) => h.trim()) + .filter(Boolean); + + if (hosts.length === 0) { + return { success: false, error: 'Bitte mindestens einen Host angeben.' }; } + + const attempts = await Promise.all( + hosts.map(async (h) => { + const c = new ProxmoxClient(h); + try { + await c.login(username, password); + return { host: h, client: c, success: true }; + } catch (err) { + return { host: h, success: false, error: err.message }; + } + }) + ); + + const succeeded = attempts.filter((a) => a.success); + const failed = attempts.filter((a) => !a.success); + + if (succeeded.length === 0) { + // Single host → show the real error. Multiple hosts, all failing → the + // per-host errors are usually identical (wrong creds), so the first is enough. + return { success: false, error: attempts[0].error }; + } + + clients = succeeded; + createWindow('vms.html', 680, 520, true); + + if (failed.length > 0) { + setImmediate(() => { + dialog.showMessageBox(mainWindow, { + type: 'warning', + title: 'Nicht alle Hosts erreichbar', + message: `${failed.length} von ${hosts.length} Host(s) konnten nicht angemeldet werden.`, + detail: failed.map((f) => `${f.host}: ${f.error}`).join('\n'), + buttons: ['OK'], + }); + }); + } + + return { success: true }; }); ipcMain.handle('proxmox:getVMs', async () => { try { - const vms = await client.getSpiceVMs(); - return { success: true, vms }; + const perHost = await Promise.all( + clients.map(async ({ host, client }) => { + try { + const vms = await client.getSpiceVMs(); + return vms.map((vm) => ({ ...vm, host })); + } catch { + return []; + } + }) + ); + return { success: true, vms: perHost.flat() }; } catch (err) { return { success: false, error: err.message }; } }); ipcMain.handle('proxmox:logout', () => { - client = null; + clients = []; createWindow('login.html', 460, 500, false); }); -ipcMain.handle('proxmox:connect', async (_e, { node, vmid }) => { +ipcMain.handle('proxmox:connect', async (_e, { host, node, vmid }) => { try { - const params = await client.getSpiceTicket(node, vmid); - const vvPath = buildVVFile(params, 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); launchRemoteViewer(vvPath); return { success: true }; } catch (err) { @@ -179,7 +231,7 @@ ipcMain.handle('settings:browseViewerPath', async () => { // ---------- SPICE helpers ---------- -function buildVVFile(params, vmid) { +function buildVVFile(params, vmid, client) { const lines = ['[virt-viewer]', `type=${params.type || 'spice'}`]; const host = params.proxy || params.host || client.host.split(':')[0]; diff --git a/src/renderer/login.html b/src/renderer/login.html index a78190f..76ea8d4 100644 --- a/src/renderer/login.html +++ b/src/renderer/login.html @@ -17,12 +17,14 @@
- + +
Mehrere eigenständige Hosts mit denselben Zugangsdaten? Mit Komma trennen, z. B. pve1.example.com, pve2.example.com
- + +
Format: Benutzer@Realm — z. B. root@pam
diff --git a/src/renderer/styles.css b/src/renderer/styles.css index ccc54d3..917f579 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -76,6 +76,13 @@ body.login-page { margin-bottom: 0.4rem; } +.field-hint { + font-size: 0.72rem; + color: var(--muted); + margin-top: 0.35rem; + line-height: 1.4; +} + input[type="text"], input[type="password"] { width: 100%; diff --git a/src/renderer/vms.js b/src/renderer/vms.js index a83e19d..47f92bb 100644 --- a/src/renderer/vms.js +++ b/src/renderer/vms.js @@ -91,6 +91,9 @@ loadVMs(); // ── Render ──────────────────────────────────────────────── function renderList(vms) { + // Only show the host when there's more than one — single-host setups don't need the noise. + const multiHost = new Set(vms.map((vm) => vm.host)).size > 1; + listEl.innerHTML = vms .map( (vm) => ` @@ -99,12 +102,13 @@ function renderList(vms) {
${esc(vm.name)}
- Läuft  ·  Node: ${esc(vm.node)}  ·  ID: ${vm.vmid} + Läuft  ·  ${multiHost ? `Host: ${esc(vm.host)}  ·  ` : ''}Node: ${esc(vm.node)}  ·  ID: ${vm.vmid}
@@ -115,7 +119,12 @@ function renderList(vms) { listEl.querySelectorAll('.btn-connect').forEach((btn) => { btn.addEventListener('click', () => - connectVM({ vmid: btn.dataset.vmid, node: btn.dataset.node, name: btn.dataset.name }) + connectVM({ + vmid: btn.dataset.vmid, + host: btn.dataset.host, + node: btn.dataset.node, + name: btn.dataset.name, + }) ); });