Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4828f73b9 | ||
|
|
ebb405ab9b |
+3
-3
@@ -18,7 +18,7 @@ Jetzt automatisch herunterladen und installieren (ca. 30 MB)?" \
|
|||||||
DetailPrint "Lade VirtViewer herunter..."
|
DetailPrint "Lade VirtViewer herunter..."
|
||||||
nsExec::ExecToLog 'powershell.exe -NonInteractive -Command \
|
nsExec::ExecToLog 'powershell.exe -NonInteractive -Command \
|
||||||
"Invoke-WebRequest \
|
"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\" \
|
-OutFile \"$TEMP\virt-viewer-setup.msi\" \
|
||||||
-UseBasicParsing"'
|
-UseBasicParsing"'
|
||||||
Pop $0
|
Pop $0
|
||||||
@@ -26,7 +26,7 @@ Jetzt automatisch herunterladen und installieren (ca. 30 MB)?" \
|
|||||||
MessageBox MB_OK|MB_ICONEXCLAMATION \
|
MessageBox MB_OK|MB_ICONEXCLAMATION \
|
||||||
"Download fehlgeschlagen (Fehlercode: $0).$\n$\n\
|
"Download fehlgeschlagen (Fehlercode: $0).$\n$\n\
|
||||||
Bitte VirtViewer nach der Installation manuell installieren:$\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
|
Goto vv_skip
|
||||||
${EndIf}
|
${EndIf}
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ https://virt-manager.org/download/"
|
|||||||
MessageBox MB_OK|MB_ICONEXCLAMATION \
|
MessageBox MB_OK|MB_ICONEXCLAMATION \
|
||||||
"VirtViewer-Installation schlug fehl (Code: $0).$\n$\n\
|
"VirtViewer-Installation schlug fehl (Code: $0).$\n$\n\
|
||||||
Bitte nach Abschluss manuell installieren:$\n\
|
Bitte nach Abschluss manuell installieren:$\n\
|
||||||
https://virt-manager.org/download/"
|
https://gitlab.com/virt-viewer/virt-viewer/-/releases/v11.0"
|
||||||
${EndIf}
|
${EndIf}
|
||||||
Goto vv_skip
|
Goto vv_skip
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "proxmox-spice-client",
|
"name": "proxmox-spice-client",
|
||||||
"version": "1.0.1",
|
"version": "1.0.2",
|
||||||
"description": "VDI SPICE Client f\u00fcr Proxmox",
|
"description": "VDI SPICE Client f\u00fcr Proxmox",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+68
-16
@@ -8,7 +8,10 @@ const ProxmoxClient = require('./proxmox');
|
|||||||
|
|
||||||
const store = new Store({ name: 'proxmox-spice-client' });
|
const store = new Store({ name: 'proxmox-spice-client' });
|
||||||
let mainWindow;
|
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 ----------
|
// ---------- Window management ----------
|
||||||
|
|
||||||
@@ -120,34 +123,83 @@ ipcMain.handle('credentials:clear', () => {
|
|||||||
// ---------- IPC: Proxmox ----------
|
// ---------- IPC: Proxmox ----------
|
||||||
|
|
||||||
ipcMain.handle('proxmox:login', async (_e, { host, username, password }) => {
|
ipcMain.handle('proxmox:login', async (_e, { host, username, password }) => {
|
||||||
try {
|
const hosts = String(host || '')
|
||||||
client = new ProxmoxClient(host);
|
.split(',')
|
||||||
await client.login(username, password);
|
.map((h) => h.trim())
|
||||||
createWindow('vms.html', 680, 520, true);
|
.filter(Boolean);
|
||||||
return { success: true };
|
|
||||||
} catch (err) {
|
if (hosts.length === 0) {
|
||||||
return { success: false, error: err.message };
|
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 () => {
|
ipcMain.handle('proxmox:getVMs', async () => {
|
||||||
try {
|
try {
|
||||||
const vms = await client.getSpiceVMs();
|
const perHost = await Promise.all(
|
||||||
return { success: true, vms };
|
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) {
|
} catch (err) {
|
||||||
return { success: false, error: err.message };
|
return { success: false, error: err.message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('proxmox:logout', () => {
|
ipcMain.handle('proxmox:logout', () => {
|
||||||
client = null;
|
clients = [];
|
||||||
createWindow('login.html', 460, 500, false);
|
createWindow('login.html', 460, 500, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('proxmox:connect', async (_e, { node, vmid }) => {
|
ipcMain.handle('proxmox:connect', async (_e, { host, node, vmid }) => {
|
||||||
try {
|
try {
|
||||||
const params = await client.getSpiceTicket(node, vmid);
|
const entry = clients.find((c) => c.host === host) || clients[0];
|
||||||
const vvPath = buildVVFile(params, vmid);
|
if (!entry) throw new Error('Nicht angemeldet.');
|
||||||
|
const params = await entry.client.getSpiceTicket(node, vmid);
|
||||||
|
const vvPath = buildVVFile(params, vmid, entry.client);
|
||||||
launchRemoteViewer(vvPath);
|
launchRemoteViewer(vvPath);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -179,7 +231,7 @@ ipcMain.handle('settings:browseViewerPath', async () => {
|
|||||||
|
|
||||||
// ---------- SPICE helpers ----------
|
// ---------- SPICE helpers ----------
|
||||||
|
|
||||||
function buildVVFile(params, vmid) {
|
function buildVVFile(params, vmid, client) {
|
||||||
const lines = ['[virt-viewer]', `type=${params.type || 'spice'}`];
|
const lines = ['[virt-viewer]', `type=${params.type || 'spice'}`];
|
||||||
|
|
||||||
const host = params.proxy || params.host || client.host.split(':')[0];
|
const host = params.proxy || params.host || client.host.split(':')[0];
|
||||||
@@ -251,7 +303,7 @@ function launchRemoteViewer(vvPath) {
|
|||||||
if (i >= candidates.length) {
|
if (i >= candidates.length) {
|
||||||
const hint =
|
const hint =
|
||||||
process.platform === 'win32'
|
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, ' +
|
'Falls VirtViewer an einem nicht-standardmäßigen Ort installiert ist, ' +
|
||||||
'trage den Pfad unter „Einstellungen → Viewer-Pfad" ein.'
|
'trage den Pfad unter „Einstellungen → Viewer-Pfad" ein.'
|
||||||
: 'Linux: sudo apt install virt-viewer';
|
: 'Linux: sudo apt install virt-viewer';
|
||||||
|
|||||||
@@ -17,12 +17,14 @@
|
|||||||
|
|
||||||
<form id="loginForm" autocomplete="on">
|
<form id="loginForm" autocomplete="on">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="host">Proxmox Host</label>
|
<label for="host">Proxmox Host(s)</label>
|
||||||
<input type="text" id="host" placeholder="pve.example.com" autocomplete="url" spellcheck="false">
|
<input type="text" id="host" placeholder="pve.example.com" autocomplete="url" spellcheck="false">
|
||||||
|
<div class="field-hint">Mehrere eigenständige Hosts mit denselben Zugangsdaten? Mit Komma trennen, z. B. pve1.example.com, pve2.example.com</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="username">Benutzer</label>
|
<label for="username">Benutzer</label>
|
||||||
<input type="text" id="username" placeholder="user@pam" autocomplete="username" spellcheck="false">
|
<input type="text" id="username" placeholder="root@pam" autocomplete="username" spellcheck="false">
|
||||||
|
<div class="field-hint">Format: Benutzer@Realm — z. B. root@pam</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="password">Passwort</label>
|
<label for="password">Passwort</label>
|
||||||
|
|||||||
@@ -76,6 +76,13 @@ body.login-page {
|
|||||||
margin-bottom: 0.4rem;
|
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="text"],
|
||||||
input[type="password"] {
|
input[type="password"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
+11
-2
@@ -91,6 +91,9 @@ loadVMs();
|
|||||||
// ── Render ────────────────────────────────────────────────
|
// ── Render ────────────────────────────────────────────────
|
||||||
|
|
||||||
function renderList(vms) {
|
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
|
listEl.innerHTML = vms
|
||||||
.map(
|
.map(
|
||||||
(vm) => `
|
(vm) => `
|
||||||
@@ -99,12 +102,13 @@ function renderList(vms) {
|
|||||||
<div class="vm-name">${esc(vm.name)}</div>
|
<div class="vm-name">${esc(vm.name)}</div>
|
||||||
<div class="vm-meta">
|
<div class="vm-meta">
|
||||||
<span class="status-dot"></span>
|
<span class="status-dot"></span>
|
||||||
Läuft · Node: ${esc(vm.node)} · ID: ${vm.vmid}
|
Läuft · ${multiHost ? `Host: ${esc(vm.host)} · ` : ''}Node: ${esc(vm.node)} · ID: ${vm.vmid}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="btn-connect"
|
class="btn-connect"
|
||||||
data-vmid="${vm.vmid}"
|
data-vmid="${vm.vmid}"
|
||||||
|
data-host="${esc(vm.host)}"
|
||||||
data-node="${esc(vm.node)}"
|
data-node="${esc(vm.node)}"
|
||||||
data-name="${esc(vm.name)}"
|
data-name="${esc(vm.name)}"
|
||||||
>Verbinden</button>
|
>Verbinden</button>
|
||||||
@@ -115,7 +119,12 @@ function renderList(vms) {
|
|||||||
|
|
||||||
listEl.querySelectorAll('.btn-connect').forEach((btn) => {
|
listEl.querySelectorAll('.btn-connect').forEach((btn) => {
|
||||||
btn.addEventListener('click', () =>
|
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,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user