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>
98 lines
3.9 KiB
JavaScript
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 });
|
|
})();
|