diff --git a/webui/html/assets/js/tabs/networks.js b/webui/html/assets/js/tabs/networks.js
new file mode 100644
index 0000000..6a9dae3
--- /dev/null
+++ b/webui/html/assets/js/tabs/networks.js
@@ -0,0 +1,233 @@
+// webui/html/assets/js/tabs/networks.js
+(function () {
+ // Gebruik gedeelde helpers uit index.html (staan op window in non-module scripts)
+ const apiGet = window.apiGet || (async (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();
+ });
+
+ const esc = window.esc || ((s) => String(s)
+ .replaceAll('&', '&')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('"', '"')
+ .replaceAll("'", ''')
+ );
+
+ function cssId(s) {
+ return String(s).replaceAll(/[^a-zA-Z0-9_-]/g, '_');
+ }
+
+ async function fetchNetworksUsage() {
+ return apiGet('/networks/usage');
+ }
+
+ async function fetchNetworksList() {
+ // returns { networks: [...] }
+ return apiGet('/networks');
+ }
+
+ async function fetchNetworkInspect(name) {
+ return apiGet('/networks/' + encodeURIComponent(name));
+ }
+
+ const state = {
+ expanded: new Set(),
+ usage: null,
+ list: null,
+ inspectCache: new Map(),
+ };
+
+ function toggleNetworkRow(name) {
+ if (state.expanded.has(name)) state.expanded.delete(name);
+ else state.expanded.add(name);
+ renderNetworks();
+ }
+
+ async function refresh() {
+ const statusEl = document.getElementById('networksStatus');
+ if (statusEl) statusEl.textContent = 'Bezig met laden...';
+
+ try {
+ const [usage, list] = await Promise.all([
+ fetchNetworksUsage(),
+ fetchNetworksList(),
+ ]);
+ state.usage = usage;
+ state.list = list;
+ if (statusEl) statusEl.textContent = `Laatst geladen: ${new Date().toLocaleString()}`;
+ renderNetworks();
+ } catch (e) {
+ console.error(e);
+ if (statusEl) statusEl.textContent = 'Fout: ' + (e?.message || e);
+ }
+ }
+
+ function renderNetworkUsersListHTML(netName, containers) {
+ if (!containers.length) {
+ return `
Geen containers gevonden op dit netwerk.
`;
+ }
+
+ 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(`pod: ${esc(pod)}`);
+ if (shared) badges.push(`shared netns`);
+ if (via) badges.push(`${esc(via)}`);
+
+ return `
+ ${esc(cname)}
+ ${badges.length ? `${badges.join('')}
` : ``}
+ `;
+ }).join('');
+
+ return `
+
Containers op ${esc(netName)}:
+
+
`;
+ }
+
+ function renderNetworksRelationsHTML(usage) {
+ const byContainer = usage.byContainer || {};
+ const byMeta = usage.byContainerMeta || {};
+
+ const multi = Object.entries(byContainer)
+ .filter(([_, nets]) => Array.isArray(nets) && nets.length > 1)
+ .sort((a, b) => a[0].localeCompare(b[0]));
+
+ 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 += `Containers met meerdere netwerken
`;
+ if (!multi.length) {
+ html += `Geen.
`;
+ } else {
+ html += `` + multi.map(([name, nets]) =>
+ `- ${esc(name)} → ${esc(nets.join(', '))}
`
+ ).join('') + `
`;
+ }
+
+ html += `Shared network namespace
`;
+ if (!shared.length) {
+ html += `Geen.
`;
+ } else {
+ html += `` + shared.map(([name, meta]) => {
+ const owner = meta.networkOwnerName || meta.networkOwnerId || '?';
+ return `- ${esc(name)} deelt netwerkstack met ${esc(owner)}
`;
+ }).join('') + `
`;
+ }
+
+ return html;
+ }
+
+ async function onInspectNetwork(name) {
+ const box = document.getElementById('inspectBox-' + cssId(name));
+ const status = document.getElementById('inspectStatus-' + cssId(name));
+ if (status) status.textContent = 'Inspect laden...';
+
+ try {
+ let data = state.inspectCache.get(name);
+ if (!data) {
+ data = await fetchNetworkInspect(name);
+ state.inspectCache.set(name, data);
+ }
+ if (box) {
+ box.style.display = '';
+ box.textContent = JSON.stringify(data, null, 2);
+ }
+ if (status) status.textContent = '';
+ } catch (e) {
+ console.error(e);
+ if (status) status.textContent = 'Fout: ' + (e?.message || e);
+ }
+ }
+
+ function renderNetworks() {
+ const tbody = document.getElementById('networksTbody');
+ const rel = document.getElementById('networksRelations');
+ if (!tbody || !rel) return;
+
+ tbody.innerHTML = '';
+ rel.innerHTML = '';
+
+ const usage = state.usage;
+ if (!usage || !usage.byNetwork) {
+ tbody.innerHTML = `| Geen data. Klik op Vernieuwen. |
`;
+ rel.innerHTML = `Geen data.
`;
+ return;
+ }
+
+ const byNetwork = usage.byNetwork;
+ const names = Object.keys(byNetwork).sort((a, b) => a.localeCompare(b));
+
+ for (const netName of names) {
+ const slot = byNetwork[netName] || {};
+ const containers = slot.containers || [];
+ const isOpen = state.expanded.has(netName);
+ const arrow = isOpen ? '▾' : '▸';
+
+ const tr = document.createElement('tr');
+ tr.innerHTML = `
+ |
+ ${esc(netName)} |
+ ${containers.length} |
+ `;
+ tr.querySelector('button').addEventListener('click', () => toggleNetworkRow(netName));
+ tbody.appendChild(tr);
+
+ if (isOpen) {
+ const tr2 = document.createElement('tr');
+ tr2.innerHTML = `
+ |
+
+
+ ${renderNetworkUsersListHTML(netName, containers)}
+
+
+
+
+
+
+ |
+ `;
+ tbody.appendChild(tr2);
+
+ const btn = tr2.querySelector('button[data-inspect]');
+ btn.addEventListener('click', async () => {
+ await onInspectNetwork(netName);
+ });
+ }
+ }
+
+ rel.innerHTML = renderNetworksRelationsHTML(usage);
+ }
+
+ function bindUiOnce() {
+ const btn = document.getElementById('networksRefreshBtn');
+ if (btn && !btn.dataset.bound) {
+ btn.dataset.bound = '1';
+ btn.addEventListener('click', refresh);
+ }
+ }
+
+ // Expose minimal API
+ window.mvpNetworks = {
+ refresh,
+ state,
+ bindUiOnce,
+ };
+
+ // Bind when script loads (DOM is already mostly there because script is at end of body)
+ bindUiOnce();
+})();
\ No newline at end of file
diff --git a/webui/html/index.html b/webui/html/index.html
index 92e4f93..33efb52 100644
--- a/webui/html/index.html
+++ b/webui/html/index.html
@@ -367,19 +367,6 @@
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) {
const t = (s || '').toLowerCase();
if (t.includes('running') || t === 'running' || t === 'active') return `${esc(s)}`;
@@ -428,7 +415,8 @@
filesRefresh();
}
if (tab === 'networks') {
- networksRefresh();
+ window.mvpNetworks?.bindUiOnce?.();
+ window.mvpNetworks?.refresh?.();
}
if (tab === "images") {
loadImages();
@@ -910,201 +898,6 @@
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 = `| Geen data. Klik op Vernieuwen. |
`;
- rel.innerHTML = `Geen data.
`;
- 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 = `
- |
- ${esc(netName)} |
- ${containers.length} |
- `;
- tr.querySelector('button').addEventListener('click', () => toggleNetworkRow(netName));
- tbody.appendChild(tr);
-
- // detail-rij (uitklap)
- if (isOpen) {
- const tr2 = document.createElement('tr');
- tr2.innerHTML = `
- |
-
-
- ${renderNetworkUsersListHTML(netName, containers)}
-
-
-
-
-
-
- |
- `;
- 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 `Geen containers gevonden op dit netwerk.
`;
- }
-
- // 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(`pod: ${esc(pod)}`);
- if (shared) badges.push(`shared netns`);
- if (via) badges.push(`${esc(via)}`);
-
- return `
- ${esc(cname)}
- ${badges.length ? `${badges.join('')}
` : ``}
- `;
- }).join('');
-
- return `
-
Containers op ${esc(netName)}:
-
-
`;
- }
-
- 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 += `Containers met meerdere netwerken
`;
- if (!multi.length) {
- html += `Geen.
`;
- } else {
- html += `` + multi.map(([name, nets]) =>
- `- ${esc(name)} → ${esc(nets.join(', '))}
`
- ).join('') + `
`;
- }
-
- html += `Shared network namespace
`;
- if (!shared.length) {
- html += `Geen.
`;
- } else {
- html += `` + shared.map(([name, meta]) => {
- const owner = meta.networkOwnerName || meta.networkOwnerId || '?';
- return `- ${esc(name)} deelt netwerkstack met ${esc(owner)}
`;
- }).join('') + `
`;
- }
-
- 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)
@@ -1335,9 +1128,6 @@
const t = document.getElementById('sidebarToggle');
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)
const taFiles = document.getElementById('filesEditor');
if (taFiles && window.CodeMirror) {
@@ -1357,6 +1147,7 @@
setInterval(() => { pingApi(); }, 20000);
})();
+