refactor(ui)!: verwijderen pods/systemd tab en samenvoegen functionaliteit in containers
This commit is contained in:
+34
-252
@@ -49,9 +49,6 @@
|
||||
<div class="tab" id="tab-containers" onclick="setTab('containers')" title="Containers">
|
||||
<span class="navIcon">📦</span><span class="navLabel">Containers</span>
|
||||
</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">
|
||||
<span class="navIcon">📁</span><span class="navLabel">Files</span>
|
||||
</div>
|
||||
@@ -74,7 +71,6 @@
|
||||
<div class="flex">
|
||||
<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="countSystemd">-</span> units (UI)</span>
|
||||
</div>
|
||||
<div class="hint">
|
||||
Deze UI gebruikt jouw API endpoints onder <span class="mono">/api</span> (same origin).
|
||||
@@ -82,20 +78,7 @@
|
||||
</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 id="view-containers" class="grid" style="display:none">
|
||||
<div class="card" style="grid-column: 1 / -1;">
|
||||
<div class="cardHeader">
|
||||
@@ -125,64 +108,6 @@
|
||||
</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 enforce’t 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 class="card" style="grid-column: 1 / -1;">
|
||||
<div class="cardHeader">
|
||||
@@ -214,7 +139,7 @@
|
||||
</div>
|
||||
<textarea id="filesEditor" class="textarea mono" spellcheck="false" placeholder="Selecteer links een bestand..."></textarea>
|
||||
<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>
|
||||
@@ -354,7 +279,7 @@
|
||||
async function pingApi() {
|
||||
try {
|
||||
// simpele ping: pods ophalen
|
||||
await api('/pods', 'GET');
|
||||
await api('/pods-dashboard', 'GET');
|
||||
setApiState(true, 'API: OK');
|
||||
} catch (e) {
|
||||
setApiState(false, 'API: fout (' + e.message + ')');
|
||||
@@ -372,22 +297,16 @@
|
||||
async function refreshActive() {
|
||||
try {
|
||||
if (currentTab === 'containers') await fetchContainers();
|
||||
else if (currentTab === 'systemd') await systemdRefresh();
|
||||
else {
|
||||
// dashboard: haal in achtergrond counts + mini systemd
|
||||
const [pods, containers] = await Promise.all([
|
||||
api('/pods-dashboard','GET'),
|
||||
api('/containers','GET')
|
||||
api('/containers-dashboard','GET')
|
||||
]);
|
||||
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;
|
||||
|
||||
const units = await getSystemdUnitsFromServer();
|
||||
document.getElementById('countSystemd').textContent = units.length;
|
||||
|
||||
await systemdMiniRefresh();
|
||||
}
|
||||
setApiState(true, 'API: OK');
|
||||
} catch (e) {
|
||||
@@ -396,49 +315,14 @@
|
||||
}
|
||||
|
||||
// ---- 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) {
|
||||
try {
|
||||
const res = await api(`/pods/actions/${encodeURIComponent(action)}/${encodeURIComponent(name)}`, 'POST');
|
||||
showModal(`Pod ${action}: ${name}`, JSON.stringify(res, null, 2));
|
||||
await fetchPods();
|
||||
if (currentTab === 'containers') {
|
||||
await fetchContainers();
|
||||
}
|
||||
|
||||
await fetchContainers(); // refresh pods + containers view in één keer
|
||||
} catch (e) {
|
||||
showModal(`Pod ${action} fout`, e.stack || e.message);
|
||||
}
|
||||
@@ -511,15 +395,15 @@
|
||||
<td>${ports || '-'}</td>
|
||||
<td>
|
||||
<div class="flex">
|
||||
<button class="btn icon" title="Inspect" onclick="containerInspect('${esc(name)}')">🔍</button>
|
||||
<button class="btn icon" title="Logs" onclick="containerLogs('${esc(name)}')">📄</button>
|
||||
<button class="btn icon" title="Inspect" onclick="containerInspect(decodeURIComponent('${encodeURIComponent(name)}'))">🔍</button>
|
||||
<button class="btn icon" title="Logs" onclick="containerLogs(decodeURIComponent('${encodeURIComponent(name)}'))">📄</button>
|
||||
${renderActionsDropdown(menuId, 'containerAction', esc(name))}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
let containersC2P = new Map();
|
||||
function renderContainersGrouped(list, tbody, podStatus) {
|
||||
const groups = new Map();
|
||||
for (const c of (list || [])) {
|
||||
@@ -538,8 +422,6 @@
|
||||
return _cmpStr(a, b);
|
||||
});
|
||||
|
||||
const table = tbody.closest('table');
|
||||
const colCount = table ? table.querySelectorAll('thead th').length : 9;
|
||||
containersC2P = new Map();
|
||||
|
||||
let html = '';
|
||||
@@ -589,7 +471,7 @@
|
||||
html += `
|
||||
<tr class="pod-group-row">
|
||||
<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>
|
||||
</span>
|
||||
</td>
|
||||
@@ -626,7 +508,7 @@
|
||||
if (cname) containersC2P.set(cname, pod);
|
||||
const row = renderContainerRow(c).replace(
|
||||
'<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;
|
||||
}
|
||||
@@ -638,17 +520,18 @@
|
||||
const t = ev.target.closest('.pod-toggle');
|
||||
if (!t) return;
|
||||
|
||||
const pod = t.getAttribute('data-pod');
|
||||
const podEnc = t.getAttribute('data-pod') || '';
|
||||
const pod = decodeURIComponent(podEnc);
|
||||
const isNowCollapsed = !_isCollapsed(pod);
|
||||
_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' : '';
|
||||
});
|
||||
|
||||
t.innerHTML =
|
||||
`${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');
|
||||
if (!tbody) return;
|
||||
renderContainersGrouped(list, tbody, podStatus);
|
||||
}
|
||||
|
||||
let containersStatsES = null;
|
||||
let containersC2P = new Map();
|
||||
|
||||
|
||||
function startContainersStatsStream() {
|
||||
if (containersStatsES) return;
|
||||
@@ -682,7 +566,13 @@
|
||||
containersStatsES = new EventSource("/api/containers/stats/stream?interval=1");
|
||||
|
||||
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 || [];
|
||||
// totals per pod voor deze SSE tick
|
||||
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){
|
||||
// simpele safe id: base64-ish
|
||||
return btoa(unescape(encodeURIComponent(s))).replaceAll('=','').replaceAll('+','-').replaceAll('/','_');
|
||||
const bytes = new TextEncoder().encode(String(s));
|
||||
let bin = '';
|
||||
bytes.forEach(b => bin += String.fromCharCode(b));
|
||||
return btoa(bin).replaceAll('=','').replaceAll('+','-').replaceAll('/','_');
|
||||
}
|
||||
|
||||
function normalizeContainerName(n) {
|
||||
@@ -861,11 +713,6 @@
|
||||
return n + "B";
|
||||
}
|
||||
|
||||
function encodeUnit(unit) {
|
||||
// encodeURIComponent is genoeg voor @ en .
|
||||
return encodeURIComponent(unit);
|
||||
}
|
||||
|
||||
async function daemonReload() {
|
||||
try {
|
||||
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)
|
||||
// =========================
|
||||
@@ -1002,8 +788,8 @@
|
||||
<span>📂 ${esc(folderLabel)}</span>
|
||||
</span>
|
||||
<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 bad" title="Verwijder map (alleen als leeg)" onclick="filesDeleteFolder('${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(decodeURIComponent('${encodeURIComponent(uiFolderPath)}'))">🗑️</button>
|
||||
</span>
|
||||
</div>
|
||||
`);
|
||||
@@ -1020,7 +806,7 @@
|
||||
const fullUi = uiFolderPath ? `${uiFolderPath}/${f}` : f;
|
||||
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)">
|
||||
<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>
|
||||
`);
|
||||
}
|
||||
@@ -1164,10 +950,6 @@
|
||||
applySidebarState();
|
||||
const t = document.getElementById('sidebarToggle');
|
||||
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)
|
||||
const taFiles = document.getElementById('filesEditor');
|
||||
|
||||
Reference in New Issue
Block a user