refract(ui): WebUI: netwerklogica uit index.html gehaald naar networks.js
This commit is contained in:
@@ -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 `<div class="muted">Geen containers gevonden op dit netwerk.</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || {};
|
||||||
|
|
||||||
|
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 += `<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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = `<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;
|
||||||
|
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 = `
|
||||||
|
<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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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();
|
||||||
|
})();
|
||||||
+3
-212
@@ -367,19 +367,6 @@
|
|||||||
return r.json();
|
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>`;
|
||||||
@@ -428,7 +415,8 @@
|
|||||||
filesRefresh();
|
filesRefresh();
|
||||||
}
|
}
|
||||||
if (tab === 'networks') {
|
if (tab === 'networks') {
|
||||||
networksRefresh();
|
window.mvpNetworks?.bindUiOnce?.();
|
||||||
|
window.mvpNetworks?.refresh?.();
|
||||||
}
|
}
|
||||||
if (tab === "images") {
|
if (tab === "images") {
|
||||||
loadImages();
|
loadImages();
|
||||||
@@ -910,201 +898,6 @@
|
|||||||
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)
|
||||||
@@ -1335,9 +1128,6 @@
|
|||||||
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) {
|
||||||
@@ -1357,6 +1147,7 @@
|
|||||||
setInterval(() => { pingApi(); }, 20000);
|
setInterval(() => { pingApi(); }, 20000);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
<script src="assets/js/tabs/networks.js"></script>
|
||||||
<script src="assets/js/tabs/images.js"></script>
|
<script src="assets/js/tabs/images.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user