diff --git a/webui/html/assets/css/app.css b/webui/html/assets/css/app.css index 718d0f8..3ef6af8 100644 --- a/webui/html/assets/css/app.css +++ b/webui/html/assets/css/app.css @@ -456,6 +456,40 @@ pre{ .menuItem.ok{ border-color: rgba(45,212,191,.35); } .menuItem.warn{ border-color: rgba(251,191,36,.35); } .menuItem.bad{ border-color: rgba(251,113,133,.35); } +.menuItem.info{ border-color: rgba(96,165,250,.45); } + +/* Exec terminal modal */ +.modalExec{ + width: min(1100px, 100%); +} +.execBody{ + display: flex; + flex-direction: column; + gap: 10px; +} +.execOutput{ + margin: 0; + white-space: pre-wrap; + word-break: break-word; + font-family: var(--mono); + font-size: 12.5px; + color: var(--pre-text); + background: var(--pre-bg); + border: 1px solid var(--soft-line); + border-radius: 12px; + padding: 12px; + min-height: 48vh; + max-height: 64vh; + overflow: auto; +} +.execInputRow{ + display: flex; + align-items: center; + gap: 8px; +} +.execInputRow .input{ + flex: 1; +} /* css voor herziene Netwerken pagina */ /* Toolbar controls */ diff --git a/webui/html/assets/js/tabs/containers.js b/webui/html/assets/js/tabs/containers.js index fa6c999..a161a8e 100644 --- a/webui/html/assets/js/tabs/containers.js +++ b/webui/html/assets/js/tabs/containers.js @@ -29,13 +29,17 @@ function _setCollapsed(pod, v) { localStorage.setItem('pod_group_collapsed:' + pod, v ? '1' : '0'); } -function renderActionsDropdown(menuId, actionFn, targetEsc) { +function renderActionsDropdown(menuId, actionFn, targetEsc, includeExec = false) { // actionFn is string: "containerAction" of "podAction" // targetEsc is al esc(...) dus veilig in onclick + const execBtn = includeExec + ? `` + : ''; return ` ` : '-'} @@ -395,3 +399,177 @@ function formatBytes(b) { if (n >= 1024) return (n / 1024).toFixed(0) + "KiB"; return n + "B"; } + +let execTerminalState = { + container: '', + sessionId: '', + source: null, + resizeTimer: null, + resizeObserver: null, +}; + +function _execEls() { + return { + back: document.getElementById('execModalBack'), + title: document.getElementById('execModalTitle'), + output: document.getElementById('execTerminalOutput'), + input: document.getElementById('execTerminalInput'), + status: document.getElementById('execTerminalStatus'), + modal: document.getElementById('execTerminalModal'), + }; +} + +function _execSetStatus(msg) { + const { status } = _execEls(); + if (status) status.textContent = msg || ''; +} + +function _execAppendOutput(txt) { + const { output } = _execEls(); + if (!output || !txt) return; + output.textContent += txt; + output.scrollTop = output.scrollHeight; +} + +function _execClearOutput() { + const { output } = _execEls(); + if (output) output.textContent = ''; +} + +function _execCloseEventSource() { + if (execTerminalState.source) { + try { execTerminalState.source.close(); } catch (_) {} + execTerminalState.source = null; + } +} + +function _execComputeSize() { + const { output } = _execEls(); + if (!output) return { rows: 24, cols: 80 }; + const w = output.clientWidth || 640; + const h = output.clientHeight || 360; + const cols = Math.max(20, Math.floor(w / 8)); + const rows = Math.max(8, Math.floor(h / 18)); + return { rows, cols }; +} + +async function _execResizeNow() { + const sid = execTerminalState.sessionId; + if (!sid) return; + const { rows, cols } = _execComputeSize(); + try { + await api(`/containers/exec/${encodeURIComponent(sid)}/resize`, 'POST', { rows, cols }); + } catch (_) { + // ignore while session is not fully running yet + } +} + +function _execScheduleResize() { + if (execTerminalState.resizeTimer) clearTimeout(execTerminalState.resizeTimer); + execTerminalState.resizeTimer = setTimeout(() => { + execTerminalState.resizeTimer = null; + _execResizeNow(); + }, 120); +} + +async function containerExecOpen(name) { + const els = _execEls(); + execTerminalState.container = name; + execTerminalState.sessionId = ''; + _execCloseEventSource(); + _execClearOutput(); + _execSetStatus('Starting exec session...'); + + if (els.title) els.title.textContent = `Exec: ${name}`; + if (els.back) els.back.style.display = 'flex'; + + try { + const res = await api(`/containers/${encodeURIComponent(name)}/exec/start`, 'POST', { cmd: ['/bin/sh'], tty: true }); + const sid = res?.session_id; + if (!sid) throw new Error('Geen session_id ontvangen'); + execTerminalState.sessionId = sid; + _execSetStatus(`Connected (${sid})`); + + const es = new EventSource(`/api/containers/exec/${encodeURIComponent(sid)}/stream`); + execTerminalState.source = es; + + es.addEventListener('exec', (ev) => { + try { + const msg = JSON.parse(ev.data || '{}'); + if (msg?.type === 'stdout') _execAppendOutput(msg.data || ''); + if (msg?.type === 'closed') _execSetStatus(`Session closed: ${msg.data || 'closed'}`); + } catch (_) {} + }); + + es.addEventListener('ping', () => {}); + + es.onerror = () => { + _execSetStatus('Stream disconnected'); + }; + + _execScheduleResize(); + + if (execTerminalState.resizeObserver) { + try { execTerminalState.resizeObserver.disconnect(); } catch (_) {} + execTerminalState.resizeObserver = null; + } + if (typeof ResizeObserver !== 'undefined' && els.modal) { + execTerminalState.resizeObserver = new ResizeObserver(() => _execScheduleResize()); + execTerminalState.resizeObserver.observe(els.modal); + } + + if (els.input) els.input.focus(); + } catch (e) { + _execSetStatus('Start failed'); + _execAppendOutput(`\n[ERROR] ${e.message || e}\n`); + } +} + +async function containerExecSendInput() { + const els = _execEls(); + const sid = execTerminalState.sessionId; + if (!sid || !els.input) return; + const raw = els.input.value || ''; + if (!raw) return; + els.input.value = ''; + try { + await api(`/containers/exec/${encodeURIComponent(sid)}/input`, 'POST', { data: raw + '\n' }); + } catch (e) { + _execAppendOutput(`\n[ERROR] ${e.message || e}\n`); + } +} + +function containerExecHandleInputKey(ev) { + if (ev.key !== 'Enter') return; + ev.preventDefault(); + containerExecSendInput(); +} + +async function containerExecClose() { + const els = _execEls(); + const sid = execTerminalState.sessionId; + + _execCloseEventSource(); + if (execTerminalState.resizeObserver) { + try { execTerminalState.resizeObserver.disconnect(); } catch (_) {} + execTerminalState.resizeObserver = null; + } + if (execTerminalState.resizeTimer) { + clearTimeout(execTerminalState.resizeTimer); + execTerminalState.resizeTimer = null; + } + + execTerminalState.sessionId = ''; + if (els.back) els.back.style.display = 'none'; + + if (sid) { + try { + await api(`/containers/exec/${encodeURIComponent(sid)}/stop`, 'POST'); + } catch (_) {} + } +} + +function closeExecModal(ev) { + if (ev.target.id !== 'execModalBack') return; + containerExecClose(); +} diff --git a/webui/html/index.html b/webui/html/index.html index 58bdec7..63e394b 100644 --- a/webui/html/index.html +++ b/webui/html/index.html @@ -359,6 +359,31 @@ + + +