Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a299eaf925 | ||
|
|
0968468978 | ||
|
|
7fbac7c18a | ||
|
|
5c4e3781fd | ||
|
|
f4828f73b9 |
+3
-3
@@ -18,7 +18,7 @@ Jetzt automatisch herunterladen und installieren (ca. 30 MB)?" \
|
||||
DetailPrint "Lade VirtViewer herunter..."
|
||||
nsExec::ExecToLog 'powershell.exe -NonInteractive -Command \
|
||||
"Invoke-WebRequest \
|
||||
-Uri \"https://virt-manager.org/download/virt-viewer-x64.msi\" \
|
||||
-Uri \"https://gitlab.com/virt-viewer/virt-viewer/-/releases/v11.0/downloads/virt-viewer-x64-11.0-1.0.msi\" \
|
||||
-OutFile \"$TEMP\virt-viewer-setup.msi\" \
|
||||
-UseBasicParsing"'
|
||||
Pop $0
|
||||
@@ -26,7 +26,7 @@ Jetzt automatisch herunterladen und installieren (ca. 30 MB)?" \
|
||||
MessageBox MB_OK|MB_ICONEXCLAMATION \
|
||||
"Download fehlgeschlagen (Fehlercode: $0).$\n$\n\
|
||||
Bitte VirtViewer nach der Installation manuell installieren:$\n\
|
||||
https://virt-manager.org/download/"
|
||||
https://gitlab.com/virt-viewer/virt-viewer/-/releases/v11.0"
|
||||
Goto vv_skip
|
||||
${EndIf}
|
||||
|
||||
@@ -40,7 +40,7 @@ https://virt-manager.org/download/"
|
||||
MessageBox MB_OK|MB_ICONEXCLAMATION \
|
||||
"VirtViewer-Installation schlug fehl (Code: $0).$\n$\n\
|
||||
Bitte nach Abschluss manuell installieren:$\n\
|
||||
https://virt-manager.org/download/"
|
||||
https://gitlab.com/virt-viewer/virt-viewer/-/releases/v11.0"
|
||||
${EndIf}
|
||||
Goto vv_skip
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "proxmox-spice-client",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.6",
|
||||
"description": "VDI SPICE Client f\u00fcr Proxmox",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
|
||||
+42
-5
@@ -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 };
|
||||
}
|
||||
@@ -229,6 +252,15 @@ ipcMain.handle('settings:browseViewerPath', async () => {
|
||||
return chosen;
|
||||
});
|
||||
|
||||
// Only one VM can be marked for autostart at a time — identified by
|
||||
// host+vmid since the same vmid can exist on several hosts.
|
||||
ipcMain.handle('settings:getAutoConnect', () => store.get('autoConnect', null));
|
||||
|
||||
ipcMain.handle('settings:setAutoConnect', (_e, target) => {
|
||||
if (target) store.set('autoConnect', target);
|
||||
else store.delete('autoConnect');
|
||||
});
|
||||
|
||||
// ---------- SPICE helpers ----------
|
||||
|
||||
function buildVVFile(params, vmid, client) {
|
||||
@@ -258,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
|
||||
@@ -303,7 +340,7 @@ function launchRemoteViewer(vvPath) {
|
||||
if (i >= candidates.length) {
|
||||
const hint =
|
||||
process.platform === 'win32'
|
||||
? 'Windows: https://virt-manager.org/download/\n\n' +
|
||||
? 'Windows: https://gitlab.com/virt-viewer/virt-viewer/-/releases/v11.0\n\n' +
|
||||
'Falls VirtViewer an einem nicht-standardmäßigen Ort installiert ist, ' +
|
||||
'trage den Pfad unter „Einstellungen → Viewer-Pfad" ein.'
|
||||
: 'Linux: sudo apt install virt-viewer';
|
||||
|
||||
@@ -17,5 +17,7 @@ contextBridge.exposeInMainWorld('api', {
|
||||
getViewerPath: () => ipcRenderer.invoke('settings:getViewerPath'),
|
||||
browseViewerPath: () => ipcRenderer.invoke('settings:browseViewerPath'),
|
||||
clearViewerPath: () => ipcRenderer.invoke('settings:clearViewerPath'),
|
||||
getAutoConnect: () => ipcRenderer.invoke('settings:getAutoConnect'),
|
||||
setAutoConnect: (target) => ipcRenderer.invoke('settings:setAutoConnect', target),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -250,6 +250,20 @@ body.vms-page {
|
||||
|
||||
.vm-info { flex: 1; min-width: 0; }
|
||||
|
||||
.vm-autostart {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.vm-autostart input { cursor: pointer; }
|
||||
|
||||
.vm-name {
|
||||
font-size: 0.97rem;
|
||||
font-weight: 600;
|
||||
|
||||
+53
-7
@@ -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}" …`);
|
||||
@@ -83,20 +92,30 @@ async function loadVMs() {
|
||||
return;
|
||||
}
|
||||
|
||||
renderList(vms);
|
||||
// Auto-connect the VM the user marked for autostart (only one at a time)
|
||||
if (autoMatch) {
|
||||
showNotice(`Autostart-VM „${autoMatch.name}" markiert — verbinde …`);
|
||||
await connectVM(autoMatch);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
loadVMs();
|
||||
|
||||
// ── Render ────────────────────────────────────────────────
|
||||
|
||||
function renderList(vms) {
|
||||
function renderList(vms, autoConnect) {
|
||||
// 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) => `
|
||||
.map((vm) => {
|
||||
const isAutostart = !!(
|
||||
autoConnect &&
|
||||
autoConnect.host === vm.host &&
|
||||
String(autoConnect.vmid) === String(vm.vmid)
|
||||
);
|
||||
return `
|
||||
<div class="vm-card" data-vmid="${vm.vmid}">
|
||||
<div class="vm-info">
|
||||
<div class="vm-name">${esc(vm.name)}</div>
|
||||
@@ -105,6 +124,17 @@ function renderList(vms) {
|
||||
Läuft · ${multiHost ? `Host: ${esc(vm.host)} · ` : ''}Node: ${esc(vm.node)} · ID: ${vm.vmid}
|
||||
</div>
|
||||
</div>
|
||||
<label class="vm-autostart" title="Beim Start automatisch mit dieser VM verbinden">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="autostart-checkbox"
|
||||
data-vmid="${vm.vmid}"
|
||||
data-host="${esc(vm.host)}"
|
||||
data-name="${esc(vm.name)}"
|
||||
${isAutostart ? 'checked' : ''}
|
||||
>
|
||||
Autostart
|
||||
</label>
|
||||
<button
|
||||
class="btn-connect"
|
||||
data-vmid="${vm.vmid}"
|
||||
@@ -113,8 +143,8 @@ function renderList(vms) {
|
||||
data-name="${esc(vm.name)}"
|
||||
>Verbinden</button>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
listEl.querySelectorAll('.btn-connect').forEach((btn) => {
|
||||
@@ -128,6 +158,21 @@ function renderList(vms) {
|
||||
);
|
||||
});
|
||||
|
||||
listEl.querySelectorAll('.autostart-checkbox').forEach((cb) => {
|
||||
cb.addEventListener('change', async () => {
|
||||
if (cb.checked) {
|
||||
// Only one VM can be the autostart VM — uncheck any other.
|
||||
listEl.querySelectorAll('.autostart-checkbox').forEach((other) => {
|
||||
if (other !== cb) other.checked = false;
|
||||
});
|
||||
await api.settings.setAutoConnect({ host: cb.dataset.host, vmid: cb.dataset.vmid });
|
||||
showNotice(`„${cb.dataset.name}" wird beim nächsten Start automatisch verbunden.`);
|
||||
} else {
|
||||
await api.settings.setAutoConnect(null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
listEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
@@ -144,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}.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user