Add Docker/Podman container management over SSH: container list with status, ports, CPU and memory; shell and live logs in terminal tabs; start, stop, restart and remove

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-25 22:11:26 +02:00
co-authored by Claude Opus 5.5
parent 039d89b9f7
commit 70a15eddbc
7 changed files with 322 additions and 7 deletions
+17 -1
View File
@@ -12,6 +12,7 @@ const { createEmbed, embedSupported } = require('./rdp-embed');
const i18n = require('../i18n');
const fido = require('./fido');
const { VpnManager } = require('./vpn');
const { DockerManager } = require('./docker');
const applyLanguage = () => i18n.setLanguage(store.get().settings.language, app.getLocale());
let win;
@@ -61,6 +62,7 @@ function askSecret(sessionId, req) {
const ssh = new SshManager(store, confirmHostKey, askSecret);
const updater = new Updater(store, send);
const vpn = new VpnManager(store, app.getPath('userData'));
const docker = new DockerManager(ssh);
function createWindow() {
win = new BrowserWindow({
@@ -194,6 +196,18 @@ handle('vault:remove', async (col, id) => {
return store.remove(col, id);
});
// ---------- Docker ----------
handle('docker:open', async (id, hostRef) => {
const host = hostWithOverrides(hostRef);
if (await vpn.ensureForHost(host)) send('vpn:changed');
return docker.open(id, host, () => send('docker:closed', id));
});
handle('docker:list', (id) => docker.list(id));
handle('docker:stats', (id) => docker.stats(id));
handle('docker:action', (id, action, cid) => docker.action(id, action, cid));
handle('docker:command', (id, kind, cid) => docker.command(id, kind, cid));
handle('docker:close', (id) => docker.close(id));
// ---------- VPN ----------
handle('vpn:status', () => vpn.status());
handle('vpn:up', (id) => vpn.up(id));
@@ -288,12 +302,14 @@ function hostWithOverrides(hostOrId) {
if (!h) throw new Error(i18n.t('Host not found'));
return h;
}
// Gespeicherter Host mit Zusätzen, z. B. { ref, execCommand } für Container-Shells
if (hostOrId?.ref) return { ...hostWithOverrides(hostOrId.ref), execCommand: hostOrId.execCommand, label: hostOrId.label };
return hostOrId; // Quick-Connect: temporärer Host
}
handle('ssh:open', async (sessionId, hostRef, size) => {
const host = hostWithOverrides(hostRef);
if (host.id) store.addHistory({ hostId: host.id, at: Date.now() });
if (host.id && !host.execCommand) store.addHistory({ hostId: host.id, at: Date.now() });
const onEvent = (type, payload) => send('ssh:event', sessionId, type, payload);
if (await vpn.ensureForHost(host, (m) => onEvent('status', m))) send('vpn:changed');
await ssh.openShell(sessionId, host, size, (type, payload) => send('ssh:event', sessionId, type, payload));