Files
unraid-podman/webui/plugins/podman/javascript/terminal.js
T
maggesandClaude Sonnet 5 46a8503498
Build Packages / Build .txz packages (push) Failing after 8m53s
Lint / ShellCheck (push) Successful in 43s
Lint / Validate .plg XML (push) Successful in 11s
Lint / EditorConfig (push) Failing after 6s
Rework Terminal into a real live console; polish danger buttons and Settings
Terminal panel now opens a genuinely interactive shell (ttyd bound to a
unix socket, proxied through Unraid's own /logterminal/ nginx location —
the same mechanism Unraid's own Docker "Console" button uses) instead of
one-shot exec calls, shown inline with a Disconnect action; bash is the
default shell. Container/shell selectors and action buttons are now
correctly bottom-aligned (root cause: Unraid's theme puts a 10px margin
on every <button>, never reset before).

Destructive actions (Disconnect, Compose/Template Delete, Volumes/Images/
Networks Remove) get a consistent, solid red treatment at rest instead of
only tinting on hover, via new --bad-strong/--bad-contrast tokens.

Settings panel restructured: a real save toolbar instead of a button
buried in an empty-label row, card subtitles, a toggle switch instead of
a bare checkbox, and installed-package versions shown as chips.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-12 21:19:47 +00:00

98 lines
3.9 KiB
JavaScript

/**
* javascript/terminal.js
*
* Terminal panel: opens a real, fully interactive terminal inline (as an
* <iframe>, not a popup window) — the same mechanism Unraid's own webGui
* uses for its System Terminal and for `docker exec` (see ajax/exec.php's
* header comment for the full explanation). This module's own job is just:
*
* 1. Ask ajax/exec.php to spawn a ttyd instance wrapping
* `podman exec -it <container> <shell>`, bound to a unix socket.
* 2. Point an <iframe> at /logterminal/<sockName>/ — nginx's own
* "logterminal" location block (already installed system-wide by
* Unraid, not something this plugin configures) proxies that,
* WebSocket upgrade included, straight to ttyd's socket.
* 3. Track which container's session (if any) is currently open, so
* "Disconnect" — or opening a different container/shell — can kill
* the right ttyd process server-side instead of just discarding the
* iframe and leaving it running.
*/
(function () {
'use strict';
const P = window.Podman;
let openName = null;
function populateContainerSelect(list) {
const select = P.el('term-container-select');
if (!select) return;
const running = list.filter(function (c) { return c.state === 'running'; });
select.innerHTML = running
.map(function (c) { return '<option value="' + P.escapeHtml(c.name) + '">' + P.escapeHtml(c.name) + '</option>'; })
.join('') || '<option value="">No running containers</option>';
}
function loadContainers() {
return P.get('containers', 'list').then(populateContainerSelect);
}
function resetFrame(message) {
P.el('term-frame-wrap').innerHTML = '<p class="podman-empty-note">' + message + '</p>';
P.el('term-disconnect-btn').disabled = true;
openName = null;
}
/** Best-effort: tells the backend to kill the ttyd/podman-exec session, if any is open. Never rejects. */
function closeCurrent() {
if (!openName) return Promise.resolve();
const name = openName;
return P.post('exec', 'close', { name: name }).catch(function () {});
}
function openLiveTerminal() {
const name = P.el('term-container-select').value;
if (!name) return;
const shell = P.el('term-shell-select').value;
const wrap = P.el('term-frame-wrap');
const btn = P.el('term-open-btn');
wrap.innerHTML = '<p class="podman-empty-note">Opening terminal…</p>';
btn.disabled = true;
closeCurrent().then(function () {
return P.post('exec', 'open', { name: name, shell: shell });
}).then(function (data) {
openName = name;
P.el('term-disconnect-btn').disabled = false;
// Matches the ~200ms delay Unraid's own openTerminal() uses between
// asking the backend to spawn ttyd and navigating to its socket —
// ttyd needs a brief moment to bind before nginx can proxy to it.
setTimeout(function () {
wrap.innerHTML = '<iframe class="podman-term-frame" src="/logterminal/' + encodeURIComponent(data.sockName) + '/"></iframe>';
}, 200);
}).catch(function (err) {
resetFrame('Could not open terminal: ' + P.escapeHtml(err.message));
}).finally(function () {
btn.disabled = false;
});
}
function disconnect() {
if (!openName) return;
const btn = P.el('term-disconnect-btn');
btn.disabled = true;
closeCurrent().finally(function () {
resetFrame('Disconnected. Pick a container and click "Open Terminal" to start a new session.');
});
}
function init() {
P.el('term-open-btn').addEventListener('click', openLiveTerminal);
P.el('term-disconnect-btn').addEventListener('click', disconnect);
return loadContainers();
}
// refresh() only repopulates the container select — it must never touch
// #term-frame-wrap, or an already-open terminal would be torn down out
// from under the user just by switching tabs and back.
P.registerPanel('terminal', { init: init, refresh: loadContainers });
})();