/** * javascript/logs.js * * Logs panel: container selector sidebar + log pane, backed by * ajax/containers.php?action=logs. "Follow" polls on an interval rather * than opening a persistent stream — see javascript/terminal.js for the * same underlying constraint (PHP-FPM's request lifecycle) applied to a * different panel. */ (function () { 'use strict'; const P = window.Podman; let containers = []; let selectedId = null; let follow = true; let pollHandle = null; function levelClass(line) { if (/\berror\b/i.test(line)) return 'lvl-error'; if (/\bwarn(ing)?\b/i.test(line)) return 'lvl-warn'; return ''; } function renderLog(text) { const pane = P.el('log-pane'); const atBottom = pane.scrollTop + pane.clientHeight >= pane.scrollHeight - 20; const lines = text.split('\n').filter(function (l) { return l.length > 0; }); pane.innerHTML = lines.map(function (line) { const cls = levelClass(line); return '
' + P.escapeHtml(line) + '
'; }).join('') || '
(no output)
'; if (atBottom) pane.scrollTop = pane.scrollHeight; } function loadLogs() { if (!selectedId) return Promise.resolve(); return P.get('containers', 'logs', { id: selectedId, tail: 300 }).then(function (data) { renderLog(data.text); }).catch(function (err) { P.el('log-pane').innerHTML = '
' + P.escapeHtml(err.message) + '
'; }); } function renderSidebar() { const side = P.el('logs-sidebar'); side.innerHTML = containers.map(function (c) { return '
' + ' ' + P.escapeHtml(c.name) + '
'; }).join(''); } function selectContainer(id) { selectedId = id; renderSidebar(); loadLogs(); } function setPolling(enabled) { follow = enabled; if (pollHandle) clearInterval(pollHandle); if (follow) { pollHandle = setInterval(loadLogs, 4000); } } function init() { P.el('logs-sidebar').addEventListener('click', function (e) { const item = e.target.closest('.item[data-id]'); if (item) selectContainer(item.dataset.id); }); document.querySelectorAll('#logs-follow-toggle button').forEach(function (btn) { btn.addEventListener('click', function () { document.querySelectorAll('#logs-follow-toggle button').forEach(function (b) { b.classList.remove('active'); }); btn.classList.add('active'); setPolling(btn.dataset.follow === 'true'); }); }); P.el('logs-filter').addEventListener('input', function (e) { const term = e.target.value.toLowerCase(); P.el('log-pane').querySelectorAll('.l').forEach(function (line) { line.style.display = term === '' || line.textContent.toLowerCase().includes(term) ? '' : 'none'; }); }); return P.get('containers', 'list').then(function (data) { containers = data; if (containers.length > 0) { selectedId = containers[0].id; } renderSidebar(); setPolling(true); return loadLogs(); }).catch(function (err) { P.el('logs-sidebar').innerHTML = '
' + P.escapeHtml(err.message) + '
'; }); } P.registerPanel('logs', { init: init, refresh: loadLogs }); })();