refactor(ui)!: verwijderen pods/systemd tab en samenvoegen functionaliteit in containers

This commit is contained in:
kodi
2026-02-20 15:46:25 +01:00
parent a7e32d08f0
commit 881382602b
+34 -252
View File
@@ -49,9 +49,6 @@
<div class="tab" id="tab-containers" onclick="setTab('containers')" title="Containers"> <div class="tab" id="tab-containers" onclick="setTab('containers')" title="Containers">
<span class="navIcon">📦</span><span class="navLabel">Containers</span> <span class="navIcon">📦</span><span class="navLabel">Containers</span>
</div> </div>
<div class="tab" id="tab-systemd" onclick="setTab('systemd')" title="Systemd">
<span class="navIcon">⚙️</span><span class="navLabel">Systemd</span>
</div>
<div class="tab" id="tab-files" onclick="setTab('files')" title="Files"> <div class="tab" id="tab-files" onclick="setTab('files')" title="Files">
<span class="navIcon">📁</span><span class="navLabel">Files</span> <span class="navIcon">📁</span><span class="navLabel">Files</span>
</div> </div>
@@ -74,7 +71,6 @@
<div class="flex"> <div class="flex">
<span class="pill"><span class="b" id="countPods">-</span> pods</span> <span class="pill"><span class="b" id="countPods">-</span> pods</span>
<span class="pill"><span class="b" id="countContainers">-</span> containers</span> <span class="pill"><span class="b" id="countContainers">-</span> containers</span>
<span class="pill"><span class="b" id="countSystemd">-</span> units (UI)</span>
</div> </div>
<div class="hint"> <div class="hint">
Deze UI gebruikt jouw API endpoints onder <span class="mono">/api</span> (same origin). Deze UI gebruikt jouw API endpoints onder <span class="mono">/api</span> (same origin).
@@ -82,20 +78,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card half">
<div class="cardHeader">
<div class="cardTitle">Systemd units (uit UI lijst)</div>
<div class="flex">
<button class="btn" onclick="systemdRefresh()">Ververs status</button>
</div>
</div>
<div class="cardBody">
<div id="systemdMini" class="muted">Nog geen data.</div>
</div>
</div>
</div> </div>
<div id="view-containers" class="grid" style="display:none"> <div id="view-containers" class="grid" style="display:none">
<div class="card" style="grid-column: 1 / -1;"> <div class="card" style="grid-column: 1 / -1;">
<div class="cardHeader"> <div class="cardHeader">
@@ -125,64 +108,6 @@
</div> </div>
</div> </div>
<div id="view-systemd" class="grid" style="display:none">
<div class="card" style="grid-column: 1 / -1;">
<div class="cardHeader">
<div class="cardTitle">Systemd (allowlist via UI)</div>
<div class="flex">
<button class="btn ok" onclick="daemonReload()">daemon-reload</button>
<button class="btn" onclick="systemdRefresh()">Ververs status</button>
</div>
</div>
<div class="cardBody">
<div class="split">
<div>
<div class="muted" style="margin-bottom:8px">
Units (één per regel). Deze lijst wordt opgeslagen in je browser (localStorage).
</div>
<textarea id="systemdUnits" class="textarea" spellcheck="false"></textarea>
<div class="flex" style="margin-top:10px">
<button class="btn" onclick="saveSystemdUnits()">Opslaan</button>
<button class="btn ghost" onclick="loadDefaultUnits()">Standaard</button>
<span class="pill">Gebruik allowlist op server om te beperken.</span>
</div>
<div class="hint">
De server enforcet jouw allowlist. Als je hier een unit invult die niet toegestaan is, krijg je 403.
</div>
</div>
<div>
<div class="muted" style="margin-bottom:8px">
Snelle actie op één unit:
</div>
<input id="systemdOne" class="input mono" placeholder="bijv. sonarr.service" />
<div class="flex" style="margin-top:10px">
<button class="btn" onclick="systemdActionSingle('status')">Status</button>
<button class="btn ok" onclick="systemdActionSingle('start')">Start</button>
<button class="btn warn" onclick="systemdActionSingle('restart')">Restart</button>
<button class="btn bad" onclick="systemdActionSingle('stop')">Stop</button>
</div>
<div class="hint">
Tip: gebruik <span class="mono">demo1.service</span>, <span class="mono">demo2.service</span>, <span class="mono">sonarr.service</span> om te testen.
</div>
</div>
</div>
<div style="margin-top:16px">
<table>
<thead>
<tr>
<th>Unit</th>
<th>Laatste status (API output)</th>
<th>Acties</th>
</tr>
</thead>
<tbody id="systemdTbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div id="view-files" class="grid" style="display:none"> <div id="view-files" class="grid" style="display:none">
<div class="card" style="grid-column: 1 / -1;"> <div class="card" style="grid-column: 1 / -1;">
<div class="cardHeader"> <div class="cardHeader">
@@ -214,7 +139,7 @@
</div> </div>
<textarea id="filesEditor" class="textarea mono" spellcheck="false" placeholder="Selecteer links een bestand..."></textarea> <textarea id="filesEditor" class="textarea mono" spellcheck="false" placeholder="Selecteer links een bestand..."></textarea>
<div class="hint"> <div class="hint">
Na wijzigen van <span class="mono">*.container</span> moet je meestal <span class="mono">daemon-reload</span> doen (kan via Systemd-tab of dashboard-knop). Na wijzigen van <span class="mono">*.container</span> moet je meestal <span class="mono">daemon-reload</span> doen (via de dashboard-knop).
</div> </div>
</div> </div>
</div> </div>
@@ -354,7 +279,7 @@
async function pingApi() { async function pingApi() {
try { try {
// simpele ping: pods ophalen // simpele ping: pods ophalen
await api('/pods', 'GET'); await api('/pods-dashboard', 'GET');
setApiState(true, 'API: OK'); setApiState(true, 'API: OK');
} catch (e) { } catch (e) {
setApiState(false, 'API: fout (' + e.message + ')'); setApiState(false, 'API: fout (' + e.message + ')');
@@ -372,22 +297,16 @@
async function refreshActive() { async function refreshActive() {
try { try {
if (currentTab === 'containers') await fetchContainers(); if (currentTab === 'containers') await fetchContainers();
else if (currentTab === 'systemd') await systemdRefresh();
else { else {
// dashboard: haal in achtergrond counts + mini systemd
const [pods, containers] = await Promise.all([ const [pods, containers] = await Promise.all([
api('/pods-dashboard','GET'), api('/pods-dashboard','GET'),
api('/containers','GET') api('/containers-dashboard','GET')
]); ]);
document.getElementById('countPods').textContent = (pods || []).length; document.getElementById('countPods').textContent = (pods || []).length;
// containers list kan array of object zijn; jij gebruikt array
const cCount = Array.isArray(containers) ? containers.length : (containers?.length || 0); const list = Array.isArray(containers) ? containers : (containers?.containers || []);
const cCount = list.length;
document.getElementById('countContainers').textContent = cCount; document.getElementById('countContainers').textContent = cCount;
const units = await getSystemdUnitsFromServer();
document.getElementById('countSystemd').textContent = units.length;
await systemdMiniRefresh();
} }
setApiState(true, 'API: OK'); setApiState(true, 'API: OK');
} catch (e) { } catch (e) {
@@ -396,49 +315,14 @@
} }
// ---- Pods ---- // ---- Pods ----
async function fetchPods() {
const pods = await api('/pods-dashboard','GET');
document.getElementById('countPods').textContent = (pods || []).length;
const tbody = document.getElementById('podsTbody');
if (!tbody) return; // Pods-tab verwijderd: alleen countPods updaten, geen rendering
tbody.innerHTML = (pods || []).map(p => {
const name = p.Name || p.name || '';
const status = p.Status || p.status || '';
const containers = (Array.isArray(p.Containers) ? p.Containers : [])
.map(c => {
if (typeof c === 'string') return c; // jouw nieuwe API: list of strings
const n = c?.Names;
if (Array.isArray(n)) return n[0] || '';
if (typeof n === 'string') return n;
return c?.Name || c?.name || '';
})
.filter(Boolean)
.join(', ');
return `
<tr>
<td><strong>${esc(name)}</strong></td>
<td>${badgeFromStatus(status)}</td>
<td class="muted">${esc(containers || '')}</td>
<td>
<div class="flex">
<button class="btn small ok" onclick="podAction('start','${esc(name)}')">Start</button>
<button class="btn small warn" onclick="podAction('restart','${esc(name)}')">Restart</button>
<button class="btn small bad" onclick="podAction('stop','${esc(name)}')">Stop</button>
</div>
</td>
</tr>
`;
}).join('');
}
async function podAction(action, name) { async function podAction(action, name) {
try { try {
const res = await api(`/pods/actions/${encodeURIComponent(action)}/${encodeURIComponent(name)}`, 'POST'); const res = await api(`/pods/actions/${encodeURIComponent(action)}/${encodeURIComponent(name)}`, 'POST');
showModal(`Pod ${action}: ${name}`, JSON.stringify(res, null, 2)); showModal(`Pod ${action}: ${name}`, JSON.stringify(res, null, 2));
await fetchPods();
if (currentTab === 'containers') { await fetchContainers(); // refresh pods + containers view in één keer
await fetchContainers();
}
} catch (e) { } catch (e) {
showModal(`Pod ${action} fout`, e.stack || e.message); showModal(`Pod ${action} fout`, e.stack || e.message);
} }
@@ -511,15 +395,15 @@
<td>${ports || '-'}</td> <td>${ports || '-'}</td>
<td> <td>
<div class="flex"> <div class="flex">
<button class="btn icon" title="Inspect" onclick="containerInspect('${esc(name)}')">🔍</button> <button class="btn icon" title="Inspect" onclick="containerInspect(decodeURIComponent('${encodeURIComponent(name)}'))">🔍</button>
<button class="btn icon" title="Logs" onclick="containerLogs('${esc(name)}')">📄</button> <button class="btn icon" title="Logs" onclick="containerLogs(decodeURIComponent('${encodeURIComponent(name)}'))">📄</button>
${renderActionsDropdown(menuId, 'containerAction', esc(name))} ${renderActionsDropdown(menuId, 'containerAction', esc(name))}
</div> </div>
</td> </td>
</tr> </tr>
`; `;
} }
let containersC2P = new Map();
function renderContainersGrouped(list, tbody, podStatus) { function renderContainersGrouped(list, tbody, podStatus) {
const groups = new Map(); const groups = new Map();
for (const c of (list || [])) { for (const c of (list || [])) {
@@ -538,8 +422,6 @@
return _cmpStr(a, b); return _cmpStr(a, b);
}); });
const table = tbody.closest('table');
const colCount = table ? table.querySelectorAll('thead th').length : 9;
containersC2P = new Map(); containersC2P = new Map();
let html = ''; let html = '';
@@ -589,7 +471,7 @@
html += ` html += `
<tr class="pod-group-row"> <tr class="pod-group-row">
<td> <td>
<span class="pod-toggle" data-pod="${pod}" style="cursor:pointer; user-select:none;"> <span class="pod-toggle" data-pod="${encodeURIComponent(pod)}" style="cursor:pointer; user-select:none;">
${collapsed ? '▶' : '▼'} <b>${esc(pod)}</b> ${collapsed ? '▶' : '▼'} <b>${esc(pod)}</b>
</span> </span>
</td> </td>
@@ -626,7 +508,7 @@
if (cname) containersC2P.set(cname, pod); if (cname) containersC2P.set(cname, pod);
const row = renderContainerRow(c).replace( const row = renderContainerRow(c).replace(
'<tr', '<tr',
`<tr class="pod-item-row" data-pod="${pod}"${collapsed ? ' style="display:none;"' : ''}` `<tr class="pod-item-row" data-pod="${encodeURIComponent(pod)}"${collapsed ? ' style="display:none;"' : ''}`
); );
html += row; html += row;
} }
@@ -638,17 +520,18 @@
const t = ev.target.closest('.pod-toggle'); const t = ev.target.closest('.pod-toggle');
if (!t) return; if (!t) return;
const pod = t.getAttribute('data-pod'); const podEnc = t.getAttribute('data-pod') || '';
const pod = decodeURIComponent(podEnc);
const isNowCollapsed = !_isCollapsed(pod); const isNowCollapsed = !_isCollapsed(pod);
_setCollapsed(pod, isNowCollapsed); _setCollapsed(pod, isNowCollapsed);
tbody.querySelectorAll(`.pod-item-row[data-pod="${CSS.escape(pod)}"]`).forEach(r => { tbody.querySelectorAll(`.pod-item-row[data-pod="${CSS.escape(podEnc)}"]`).forEach(r => {
r.style.display = isNowCollapsed ? 'none' : ''; r.style.display = isNowCollapsed ? 'none' : '';
}); });
t.innerHTML = t.innerHTML =
`${isNowCollapsed ? '▶' : '▼'} <b>${pod}</b> ` + `${isNowCollapsed ? '▶' : '▼'} <b>${pod}</b> ` +
`<span style="opacity:.7;">(${tbody.querySelectorAll(`.pod-item-row[data-pod="${CSS.escape(pod)}"]`).length})</span>`; `<span style="opacity:.7;">(${tbody.querySelectorAll(`.pod-item-row[data-pod="${CSS.escape(podEnc)}"]`).length})</span>`;
}; };
} }
@@ -670,11 +553,12 @@
} }
const tbody = document.getElementById('containersTbody'); const tbody = document.getElementById('containersTbody');
if (!tbody) return;
renderContainersGrouped(list, tbody, podStatus); renderContainersGrouped(list, tbody, podStatus);
} }
let containersStatsES = null; let containersStatsES = null;
let containersC2P = new Map();
function startContainersStatsStream() { function startContainersStatsStream() {
if (containersStatsES) return; if (containersStatsES) return;
@@ -682,7 +566,13 @@
containersStatsES = new EventSource("/api/containers/stats/stream?interval=1"); containersStatsES = new EventSource("/api/containers/stats/stream?interval=1");
containersStatsES.addEventListener("stats", (ev) => { containersStatsES.addEventListener("stats", (ev) => {
const payload = JSON.parse(ev.data); let payload;
try {
payload = JSON.parse(ev.data);
} catch (e) {
// Slechte SSE chunk -> negeren zodat de stream niet "stilvalt"
return;
}
const statsList = payload?.data?.Stats || []; const statsList = payload?.data?.Stats || [];
// totals per pod voor deze SSE tick // totals per pod voor deze SSE tick
const podCpu = new Map(); // podName -> cpuPct sum const podCpu = new Map(); // podName -> cpuPct sum
@@ -802,49 +692,11 @@
} }
} }
// ---- Systemd UI storage ----
const LS_KEY = 'mvp_systemd_units_v1';
function loadDefaultUnits() {
const defaults = ["demo1.service","demo2.service","sonarr.service"];
document.getElementById('systemdUnits').value = defaults.join("\n");
saveSystemdUnits();
}
function saveSystemdUnits() {
const raw = document.getElementById('systemdUnits').value || '';
const units = raw.split('\n').map(x => x.trim()).filter(Boolean);
localStorage.setItem(LS_KEY, JSON.stringify(units));
systemdRenderRows(units);
refreshActive();
}
async function getSystemdUnitsFromServer() {
const data = await api('/systemd/allowlist', 'GET');
const units = Array.isArray(data.units) ? data.units : [];
// vul textarea ook
const ta = document.getElementById('systemdUnits');
if (ta) ta.value = units.join("\n");
return units;
}
function systemdRenderRows(units) {
const tbody = document.getElementById('systemdTbody');
tbody.innerHTML = units.map(u => `
<tr>
<td><strong class="mono">${esc(u)}</strong></td>
<td class="muted mono" id="sys-out-${cssSafeId(u)}">-</td>
<td>
<div class="flex">
<button class="btn small" onclick="systemdAction('status','${esc(u)}')">Status</button>
<button class="btn small ok" onclick="systemdAction('start','${esc(u)}')">Start</button>
<button class="btn small warn" onclick="systemdAction('restart','${esc(u)}')">Restart</button>
<button class="btn small bad" onclick="systemdAction('stop','${esc(u)}')">Stop</button>
</div>
</td>
</tr>
`).join('');
}
function cssSafeId(s){ function cssSafeId(s){
// simpele safe id: base64-ish const bytes = new TextEncoder().encode(String(s));
return btoa(unescape(encodeURIComponent(s))).replaceAll('=','').replaceAll('+','-').replaceAll('/','_'); let bin = '';
bytes.forEach(b => bin += String.fromCharCode(b));
return btoa(bin).replaceAll('=','').replaceAll('+','-').replaceAll('/','_');
} }
function normalizeContainerName(n) { function normalizeContainerName(n) {
@@ -861,11 +713,6 @@
return n + "B"; return n + "B";
} }
function encodeUnit(unit) {
// encodeURIComponent is genoeg voor @ en .
return encodeURIComponent(unit);
}
async function daemonReload() { async function daemonReload() {
try { try {
const res = await api('/daemon-reload','POST'); const res = await api('/daemon-reload','POST');
@@ -875,67 +722,6 @@
} }
} }
async function systemdAction(action, unit) {
try {
const res = await api(`/${encodeURIComponent(action)}/${encodeUnit(unit)}`, 'POST');
// res.output kan lang zijn
showModal(`systemctl ${action} ${unit}`, (res.output ?? JSON.stringify(res, null, 2)));
// update inline status cell
const cell = document.getElementById('sys-out-' + cssSafeId(unit));
if (cell) {
const summary = (res.output || '').split('\n').slice(0,3).join(' / ') || '(geen output)';
cell.textContent = summary;
}
} catch (e) {
showModal(`systemctl ${action} fout`, e.stack || e.message);
}
}
async function systemdActionSingle(action) {
const unit = (document.getElementById('systemdOne').value || '').trim();
if (!unit) return showModal('Systemd', 'Vul eerst een unit in (bijv. sonarr.service).');
await systemdAction(action, unit);
}
async function systemdRefresh() {
const units = await getSystemdUnitsFromServer();
systemdRenderRows(units);
document.getElementById('countSystemd').textContent = units.length;
for (const u of units) {
try {
const res = await api(`/status/${encodeUnit(u)}`, 'POST');
const cell = document.getElementById('sys-out-' + cssSafeId(u));
if (cell) {
const first = (res.output || '').split('\n')[0] || '';
const activeLine = (res.output || '').split('\n').find(x => x.trim().startsWith('Active:')) || '';
cell.textContent = (first + ' | ' + activeLine).trim();
}
} catch (e) {
const cell = document.getElementById('sys-out-' + cssSafeId(u));
if (cell) cell.textContent = 'ERROR: ' + e.message;
}
}
}
async function systemdMiniRefresh() {
const units = await getSystemdUnitsFromServer();
const mini = document.getElementById('systemdMini');
if (!mini) return;
const lines = [];
for (const u of units.slice(0, 6)) {
try {
const res = await api(`/status/${encodeUnit(u)}`, 'POST');
const activeLine = (res.output || '').split('\n').find(x => x.trim().startsWith('Active:')) || '';
lines.push(`${u}: ${activeLine.replace('Active:','').trim() || 'unknown'}`);
} catch (e) {
lines.push(`${u}: ERROR (${e.message})`);
}
}
mini.innerHTML = `<pre>${esc(lines.join('\n'))}</pre>`;
}
// ========================= // =========================
// Files tab (systemd subtree) // Files tab (systemd subtree)
// ========================= // =========================
@@ -1002,8 +788,8 @@
<span>📂 ${esc(folderLabel)}</span> <span>📂 ${esc(folderLabel)}</span>
</span> </span>
<span class="flex" onclick="event.stopPropagation();"> <span class="flex" onclick="event.stopPropagation();">
<button class="btn small ok" title="Nieuw bestand in ${esc(folderLabel)}" onclick="filesNewFileInFolder('${esc(uiFolderPath)}')">+</button> <button class="btn small ok" title="Nieuw bestand in ${esc(folderLabel)}" onclick="filesNewFileInFolder(decodeURIComponent('${encodeURIComponent(uiFolderPath)}'))">+</button>
<button class="btn small bad" title="Verwijder map (alleen als leeg)" onclick="filesDeleteFolder('${esc(uiFolderPath)}')">🗑️</button> <button class="btn small bad" title="Verwijder map (alleen als leeg)" onclick="filesDeleteFolder(decodeURIComponent('${encodeURIComponent(uiFolderPath)}'))">🗑️</button>
</span> </span>
</div> </div>
`); `);
@@ -1020,7 +806,7 @@
const fullUi = uiFolderPath ? `${uiFolderPath}/${f}` : f; const fullUi = uiFolderPath ? `${uiFolderPath}/${f}` : f;
parts.push(` parts.push(`
<div style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px dashed rgba(36,52,95,.35)"> <div style="display:flex;align-items:center;justify-content:space-between;gap:10px;padding:4px 0;border-bottom:1px dashed rgba(36,52,95,.35)">
<span class="mono" style="cursor:pointer" onclick="filesOpen('${esc(fullUi)}')">📄 ${esc(f)}</span> <span class="mono" style="cursor:pointer" onclick="filesOpen(decodeURIComponent('${encodeURIComponent(fullUi)}'))">📄 ${esc(f)}</span>
</div> </div>
`); `);
} }
@@ -1164,10 +950,6 @@
applySidebarState(); applySidebarState();
const t = document.getElementById('sidebarToggle'); const t = document.getElementById('sidebarToggle');
if (t) t.onclick = toggleSidebar; if (t) t.onclick = toggleSidebar;
// preload systemd units UI
const units = await getSystemdUnitsFromServer();
systemdRenderRows(units);
document.getElementById('countSystemd').textContent = units.length;
// Files editor: CodeMirror init (alleen als textarea bestaat) // Files editor: CodeMirror init (alleen als textarea bestaat)
const taFiles = document.getElementById('filesEditor'); const taFiles = document.getElementById('filesEditor');