feat(ui): netwerk overzicht toegevoegd

This commit is contained in:
kodi
2026-02-21 10:28:20 +01:00
parent 5d5fdab122
commit acbf150e28
3 changed files with 263 additions and 5 deletions
-4
View File
@@ -1,4 +0,0 @@
demo1.service
demo2.service
sonarr.service
mediaserver.service
+5
View File
@@ -153,6 +153,11 @@ tr:hover td{background: rgba(96,165,250,.06)}
font-size:12px; font-size:12px;
color: var(--muted); color: var(--muted);
} }
pre.code{
padding:10px;
border-radius:10px;
border:1px solid rgba(255,255,255,.15);
}
.badge.ok{border-color: rgba(45,212,191,.6); color: var(--ok)} .badge.ok{border-color: rgba(45,212,191,.6); color: var(--ok)}
.badge.bad{border-color: rgba(251,113,133,.6); color: var(--bad)} .badge.bad{border-color: rgba(251,113,133,.6); color: var(--bad)}
.badge.warn{border-color: rgba(251,191,36,.6); color: var(--warn)} .badge.warn{border-color: rgba(251,191,36,.6); color: var(--warn)}
+258 -1
View File
@@ -41,7 +41,6 @@
<div class="sidebarTop"> <div class="sidebarTop">
<button class="btn small ghost sidebarToggle" id="sidebarToggle" title="Sidebar in/uitklappen"></button> <button class="btn small ghost sidebarToggle" id="sidebarToggle" title="Sidebar in/uitklappen"></button>
</div> </div>
<div class="tabs"> <div class="tabs">
<div class="tab active" id="tab-dashboard" onclick="setTab('dashboard')" title="Dashboard"> <div class="tab active" id="tab-dashboard" onclick="setTab('dashboard')" title="Dashboard">
<span class="navIcon">🏠</span><span class="navLabel">Dashboard</span> <span class="navIcon">🏠</span><span class="navLabel">Dashboard</span>
@@ -49,6 +48,9 @@
<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-networks" onclick="setTab('networks')" title="Netwerk">
<span class="navIcon">🌐</span><span class="navLabel">Netwerk</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>
@@ -108,6 +110,38 @@
</div> </div>
</div> </div>
<div id="view-networks" style="display:none;">
<div class="card">
<div class="cardHeader">
<h2>Netwerken</h2>
<div class="row gap">
<button class="btn small" id="networksRefreshBtn">Vernieuwen</button>
</div>
</div>
<div id="networksStatus" class="muted" style="margin:8px 0;"></div>
<table class="table" id="networksTable">
<thead>
<tr>
<th style="width:40px;"></th>
<th>Netwerk</th>
<th># containers</th>
</tr>
</thead>
<tbody id="networksTbody"></tbody>
</table>
</div>
<div class="card" style="margin-top:12px;">
<div class="cardHeader">
<h2>Relaties</h2>
</div>
<div id="networksRelations"></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">
@@ -196,6 +230,28 @@
.replaceAll("'","&#039;"); .replaceAll("'","&#039;");
} }
async function apiGet(path) {
const r = await fetch('/api' + path, { headers: { 'Accept': 'application/json' } });
if (!r.ok) {
const t = await r.text().catch(()=> '');
throw new Error(`HTTP ${r.status} ${path}: ${t}`);
}
return r.json();
}
async function fetchNetworksUsage() {
return apiGet('/networks/usage');
}
async function fetchNetworksList() {
// returns { networks: [...] }
return apiGet('/networks');
}
async function fetchNetworkInspect(name) {
return apiGet('/networks/' + encodeURIComponent(name));
}
function badgeFromStatus(s) { function badgeFromStatus(s) {
const t = (s || '').toLowerCase(); const t = (s || '').toLowerCase();
if (t.includes('running') || t === 'running' || t === 'active') return `<span class="badge ok">${esc(s)}</span>`; if (t.includes('running') || t === 'running' || t === 'active') return `<span class="badge ok">${esc(s)}</span>`;
@@ -243,6 +299,9 @@
if (tab === 'files') { if (tab === 'files') {
filesRefresh(); filesRefresh();
} }
if (tab === 'networks') {
networksRefresh();
}
// Start/stop live stats alleen in Containers tab // Start/stop live stats alleen in Containers tab
if (tab === 'containers') startContainersStatsStream(); if (tab === 'containers') startContainersStatsStream();
@@ -721,6 +780,201 @@
showModal('daemon-reload fout', e.stack || e.message); showModal('daemon-reload fout', e.stack || e.message);
} }
} }
// =========================
// Files tab (systemd subtree)
// =========================
let networksState = {
expanded: new Set(), // welke networks staan open
usage: null,
list: null,
inspectCache: new Map(), // name -> inspect json
};
function toggleNetworkRow(name) {
if (networksState.expanded.has(name)) networksState.expanded.delete(name);
else networksState.expanded.add(name);
renderNetworks(); // re-render
}
async function networksRefresh() {
const statusEl = document.getElementById('networksStatus');
statusEl.textContent = 'Bezig met laden...';
try {
const [usage, list] = await Promise.all([
fetchNetworksUsage(),
fetchNetworksList(),
]);
networksState.usage = usage;
networksState.list = list;
statusEl.textContent = `Laatst geladen: ${new Date().toLocaleString()}`;
renderNetworks();
} catch (e) {
console.error(e);
statusEl.textContent = 'Fout: ' + (e?.message || e);
}
}
function renderNetworks() {
const tbody = document.getElementById('networksTbody');
const rel = document.getElementById('networksRelations');
tbody.innerHTML = '';
rel.innerHTML = '';
const usage = networksState.usage;
if (!usage || !usage.byNetwork) {
tbody.innerHTML = `<tr><td colspan="3" class="muted">Geen data. Klik op Vernieuwen.</td></tr>`;
rel.innerHTML = `<div class="muted">Geen data.</div>`;
return;
}
const byNetwork = usage.byNetwork;
// Sorteer op naam
const names = Object.keys(byNetwork).sort((a,b) => a.localeCompare(b));
// Build rows
for (const netName of names) {
const slot = byNetwork[netName] || {};
const containers = slot.containers || [];
const isOpen = networksState.expanded.has(netName);
const arrow = isOpen ? '▾' : '▸';
// hoofd-rij
const tr = document.createElement('tr');
tr.innerHTML = `
<td><button class="btn small ghost" title="Open/dicht">${arrow}</button></td>
<td>${esc(netName)}</td>
<td>${containers.length}</td>
`;
tr.querySelector('button').addEventListener('click', () => toggleNetworkRow(netName));
tbody.appendChild(tr);
// detail-rij (uitklap)
if (isOpen) {
const tr2 = document.createElement('tr');
tr2.innerHTML = `
<td></td>
<td colspan="2">
<div style="padding:8px 0;">
${renderNetworkUsersListHTML(netName, containers)}
<div class="row gap" style="margin-top:8px;">
<button class="btn small" data-inspect="${esc(netName)}">Inspect details</button>
<span class="muted" id="inspectStatus-${cssId(netName)}"></span>
</div>
<pre class="code" id="inspectBox-${cssId(netName)}" style="display:none; margin-top:8px; max-height:260px; overflow:auto;"></pre>
</div>
</td>
`;
tbody.appendChild(tr2);
// inspect button handler
const btn = tr2.querySelector('button[data-inspect]');
btn.addEventListener('click', async () => {
await onInspectNetwork(netName);
});
}
}
// Relaties sectie
rel.innerHTML = renderNetworksRelationsHTML(usage);
}
function renderNetworkUsersListHTML(netName, containers) {
if (!containers.length) {
return `<div class="muted">Geen containers gevonden op dit netwerk.</div>`;
}
// lijstje
const rows = containers.map(c => {
const cname = c.name || '?';
const pod = c.pod || '';
const shared = c.networkMode && String(c.networkMode).startsWith('container:');
const via = c.networkOwnerName ? `via ${c.networkOwnerName}` : (c.networkOwnerId ? `via ${c.networkOwnerId}` : '');
const badges = [];
if (pod) badges.push(`<span class="badge">pod: ${esc(pod)}</span>`);
if (shared) badges.push(`<span class="badge">shared netns</span>`);
if (via) badges.push(`<span class="badge">${esc(via)}</span>`);
return `<li>
<b>${esc(cname)}</b>
${badges.length ? `<div class="row gap" style="margin-top:4px; flex-wrap:wrap;">${badges.join('')}</div>` : ``}
</li>`;
}).join('');
return `<div>
<div class="muted" style="margin-bottom:6px;">Containers op <b>${esc(netName)}</b>:</div>
<ul style="margin:0; padding-left:18px;">${rows}</ul>
</div>`;
}
function renderNetworksRelationsHTML(usage) {
const byContainer = usage.byContainer || {};
const byMeta = usage.byContainerMeta || {};
// A) containers met meerdere netwerken
const multi = Object.entries(byContainer)
.filter(([_, nets]) => Array.isArray(nets) && nets.length > 1)
.sort((a,b) => a[0].localeCompare(b[0]));
// B) shared netns containers
const shared = Object.entries(byMeta)
.filter(([_, meta]) => meta && meta.networkMode && String(meta.networkMode).startsWith('container:'))
.sort((a,b) => a[0].localeCompare(b[0]));
let html = '';
html += `<h3 style="margin:0 0 6px 0;">Containers met meerdere netwerken</h3>`;
if (!multi.length) {
html += `<div class="muted" style="margin-bottom:12px;">Geen.</div>`;
} else {
html += `<ul style="margin:0 0 12px 0; padding-left:18px;">` + multi.map(([name, nets]) =>
`<li><b>${esc(name)}</b> → ${esc(nets.join(', '))}</li>`
).join('') + `</ul>`;
}
html += `<h3 style="margin:0 0 6px 0;">Shared network namespace</h3>`;
if (!shared.length) {
html += `<div class="muted">Geen.</div>`;
} else {
html += `<ul style="margin:0; padding-left:18px;">` + shared.map(([name, meta]) => {
const owner = meta.networkOwnerName || meta.networkOwnerId || '?';
return `<li><b>${esc(name)}</b> deelt netwerkstack met <b>${esc(owner)}</b></li>`;
}).join('') + `</ul>`;
}
return html;
}
// helper: maak safe id voor DOM
function cssId(s) {
return String(s).replaceAll(/[^a-zA-Z0-9_-]/g, '_');
}
async function onInspectNetwork(name) {
const key = name;
const box = document.getElementById('inspectBox-' + cssId(name));
const status = document.getElementById('inspectStatus-' + cssId(name));
status.textContent = 'Inspect laden...';
try {
let data = networksState.inspectCache.get(key);
if (!data) {
data = await fetchNetworkInspect(name);
networksState.inspectCache.set(key, data);
}
box.style.display = '';
box.textContent = JSON.stringify(data, null, 2);
status.textContent = '';
} catch (e) {
console.error(e);
status.textContent = 'Fout: ' + (e?.message || e);
}
}
// ========================= // =========================
// Files tab (systemd subtree) // Files tab (systemd subtree)
@@ -951,6 +1205,9 @@
const t = document.getElementById('sidebarToggle'); const t = document.getElementById('sidebarToggle');
if (t) t.onclick = toggleSidebar; if (t) t.onclick = toggleSidebar;
// Networks refresh button: 1x listener (niet in setTab!)
document.getElementById('networksRefreshBtn')?.addEventListener('click', networksRefresh);
// 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');
if (taFiles && window.CodeMirror) { if (taFiles && window.CodeMirror) {