fix: cpu/mem container view
This commit is contained in:
@@ -29,7 +29,7 @@ _PODMAN_API_BASE = None
|
|||||||
_STATS_CACHE_BY_NAME = {} # name -> {"cpu": float|None, "mem_usage": float|None, "mem_perc": float|None}
|
_STATS_CACHE_BY_NAME = {} # name -> {"cpu": float|None, "mem_usage": float|None, "mem_perc": float|None}
|
||||||
_STATS_CACHE_TS = None
|
_STATS_CACHE_TS = None
|
||||||
_STATS_POLLER_TASK = None
|
_STATS_POLLER_TASK = None
|
||||||
_STATS_SHOWN_NAMES: set = set() # namen van systemd-managed containers uit laatste dashboard call
|
_STATS_SHOWN_NAMES: set = set() # namen van alle dashboard-containers uit laatste dashboard call
|
||||||
|
|
||||||
# --- EXEC SESSION CACHE (in-memory) ---
|
# --- EXEC SESSION CACHE (in-memory) ---
|
||||||
_EXEC_SESSIONS = {} # session_id -> _ExecSessionState
|
_EXEC_SESSIONS = {} # session_id -> _ExecSessionState
|
||||||
@@ -461,12 +461,11 @@ def init_containers_router(
|
|||||||
row["Status"] = (out or "").strip()
|
row["Status"] = (out or "").strip()
|
||||||
dashboard.append(row)
|
dashboard.append(row)
|
||||||
|
|
||||||
# Bijwerken welke namen systemd-managed zijn (voor /stats filter)
|
# Bijwerken welke containernamen in het dashboard staan (voor /stats filter)
|
||||||
global _STATS_SHOWN_NAMES
|
global _STATS_SHOWN_NAMES
|
||||||
_STATS_SHOWN_NAMES = {
|
_STATS_SHOWN_NAMES = {
|
||||||
_norm_container_name((c.get("Names") or ["?"])[0])
|
_norm_container_name((c.get("Names") or ["?"])[0])
|
||||||
for c in dashboard
|
for c in dashboard
|
||||||
if c.get("_dashboard_source") == "systemd"
|
|
||||||
} - {"?", ""}
|
} - {"?", ""}
|
||||||
|
|
||||||
return dashboard
|
return dashboard
|
||||||
|
|||||||
@@ -620,34 +620,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let graphCtx = null;
|
let graphCtx = null;
|
||||||
|
let modalGraphCtx = null;
|
||||||
|
let _escBound = false;
|
||||||
|
|
||||||
function renderGraph(model, opts = {}) {
|
function _renderGraphInHost(hostEl, ctx, model, opts = {}) {
|
||||||
const host = document.getElementById('networksMapHost');
|
hostEl.innerHTML = '';
|
||||||
if (!host) return;
|
|
||||||
|
|
||||||
// leeg host (placeholder weg)
|
|
||||||
host.innerHTML = '';
|
|
||||||
const tooltip = document.createElement('div');
|
const tooltip = document.createElement('div');
|
||||||
tooltip.className = 'mapTooltip';
|
tooltip.className = 'mapTooltip';
|
||||||
tooltip.style.display = 'none';
|
tooltip.style.display = 'none';
|
||||||
host.appendChild(tooltip);
|
hostEl.appendChild(tooltip);
|
||||||
|
|
||||||
const w = Math.max(600, host.clientWidth || 600);
|
const w = Math.max(600, hostEl.clientWidth || 600);
|
||||||
const h = Math.max(420, host.clientHeight || 420);
|
const h = Math.max(420, hostEl.clientHeight || 420);
|
||||||
|
|
||||||
const svg = d3.select(host).append('svg')
|
|
||||||
.attr('viewBox', `0 0 ${w} ${h}`);
|
|
||||||
|
|
||||||
|
const svg = d3.select(hostEl).append('svg').attr('viewBox', `0 0 ${w} ${h}`);
|
||||||
const g = svg.append('g');
|
const g = svg.append('g');
|
||||||
|
|
||||||
// zoom/pan
|
|
||||||
const zoom = d3.zoom()
|
const zoom = d3.zoom()
|
||||||
.scaleExtent([0.2, 2.5])
|
.scaleExtent([0.2, 2.5])
|
||||||
.on('zoom', (ev) => g.attr('transform', ev.transform));
|
.on('zoom', (ev) => g.attr('transform', ev.transform));
|
||||||
|
|
||||||
svg.call(zoom);
|
svg.call(zoom);
|
||||||
|
|
||||||
// links
|
|
||||||
const link = g.append('g')
|
const link = g.append('g')
|
||||||
.selectAll('line')
|
.selectAll('line')
|
||||||
.data(model.links)
|
.data(model.links)
|
||||||
@@ -655,7 +648,6 @@
|
|||||||
.append('line')
|
.append('line')
|
||||||
.attr('class', d => d.type === 'shared' ? 'graphLink shared' : 'graphLink');
|
.attr('class', d => d.type === 'shared' ? 'graphLink shared' : 'graphLink');
|
||||||
|
|
||||||
// nodes
|
|
||||||
const node = g.append('g')
|
const node = g.append('g')
|
||||||
.selectAll('g')
|
.selectAll('g')
|
||||||
.data(model.nodes)
|
.data(model.nodes)
|
||||||
@@ -663,49 +655,38 @@
|
|||||||
.append('g')
|
.append('g')
|
||||||
.attr('class', d => `graphNode ${d.type}`);
|
.attr('class', d => `graphNode ${d.type}`);
|
||||||
|
|
||||||
node.append('circle')
|
node.append('circle').attr('r', d => d.type === 'network' ? 14 : 9);
|
||||||
.attr('r', d => d.type === 'network' ? 14 : 9);
|
|
||||||
|
|
||||||
node.append('text')
|
node.append('text')
|
||||||
.attr('class', 'graphLabel')
|
.attr('class', 'graphLabel')
|
||||||
.attr('x', d => d.type === 'network' ? 18 : 12)
|
.attr('x', d => d.type === 'network' ? 18 : 12)
|
||||||
.attr('y', 4)
|
.attr('y', 4)
|
||||||
.text(d => d.label || d.key);
|
.text(d => d.label || d.key);
|
||||||
|
|
||||||
// drag
|
|
||||||
const drag = d3.drag()
|
const drag = d3.drag()
|
||||||
.on('start', (ev, d) => {
|
.on('start', (ev, d) => {
|
||||||
if (!ev.active) graphCtx.sim.alphaTarget(0.2).restart();
|
if (!ev.active) ctx.sim.alphaTarget(0.2).restart();
|
||||||
d.fx = d.x; d.fy = d.y;
|
d.fx = d.x; d.fy = d.y;
|
||||||
})
|
})
|
||||||
.on('drag', (ev, d) => {
|
.on('drag', (ev, d) => { d.fx = ev.x; d.fy = ev.y; })
|
||||||
d.fx = ev.x; d.fy = ev.y;
|
|
||||||
})
|
|
||||||
.on('end', (ev, d) => {
|
.on('end', (ev, d) => {
|
||||||
if (!ev.active) graphCtx.sim.alphaTarget(0);
|
if (!ev.active) ctx.sim.alphaTarget(0);
|
||||||
d.fx = null; d.fy = null;
|
d.fx = null; d.fy = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
node.call(drag);
|
node.call(drag);
|
||||||
|
|
||||||
// hover highlight (connected)
|
|
||||||
node.on('mouseenter', (ev, d) => {
|
node.on('mouseenter', (ev, d) => {
|
||||||
node.classed('graphDim', true);
|
node.classed('graphDim', true);
|
||||||
link.classed('graphDim', true);
|
link.classed('graphDim', true);
|
||||||
|
|
||||||
d3.select(ev.currentTarget).classed('graphDim', false);
|
d3.select(ev.currentTarget).classed('graphDim', false);
|
||||||
|
|
||||||
link.each(function(l) {
|
link.each(function(l) {
|
||||||
const sid = l.source?.id || l.source;
|
const sid = l.source?.id || l.source;
|
||||||
const tid = l.target?.id || l.target;
|
const tid = l.target?.id || l.target;
|
||||||
const hit = (sid === d.id || tid === d.id);
|
if (sid === d.id || tid === d.id) {
|
||||||
if (hit) {
|
|
||||||
d3.select(this).classed('graphDim', false).classed('graphActive', true);
|
d3.select(this).classed('graphDim', false).classed('graphActive', true);
|
||||||
} else {
|
} else {
|
||||||
d3.select(this).classed('graphActive', false);
|
d3.select(this).classed('graphActive', false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const typeLabel = d.type === 'network' ? 'Netwerk' : 'Container';
|
const typeLabel = d.type === 'network' ? 'Netwerk' : 'Container';
|
||||||
const extra = d.type === 'network'
|
const extra = d.type === 'network'
|
||||||
? `Driver: ${d?.meta?.driver || 'onbekend'}`
|
? `Driver: ${d?.meta?.driver || 'onbekend'}`
|
||||||
@@ -713,15 +694,11 @@
|
|||||||
tooltip.innerHTML = `<strong>${typeLabel}</strong><br>${d.label || d.key}<br>${extra}`;
|
tooltip.innerHTML = `<strong>${typeLabel}</strong><br>${d.label || d.key}<br>${extra}`;
|
||||||
tooltip.style.display = 'block';
|
tooltip.style.display = 'block';
|
||||||
});
|
});
|
||||||
|
|
||||||
node.on('mousemove', (ev) => {
|
node.on('mousemove', (ev) => {
|
||||||
const rect = host.getBoundingClientRect();
|
const rect = hostEl.getBoundingClientRect();
|
||||||
const x = (ev.clientX - rect.left) + 14;
|
tooltip.style.left = `${(ev.clientX - rect.left) + 14}px`;
|
||||||
const y = (ev.clientY - rect.top) + 14;
|
tooltip.style.top = `${(ev.clientY - rect.top) + 14}px`;
|
||||||
tooltip.style.left = `${x}px`;
|
|
||||||
tooltip.style.top = `${y}px`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
node.on('mouseleave', () => {
|
node.on('mouseleave', () => {
|
||||||
node.classed('graphDim', false);
|
node.classed('graphDim', false);
|
||||||
link.classed('graphDim', false).classed('graphActive', false);
|
link.classed('graphDim', false).classed('graphActive', false);
|
||||||
@@ -730,15 +707,14 @@
|
|||||||
|
|
||||||
node.on('click', (ev, d) => {
|
node.on('click', (ev, d) => {
|
||||||
if (d.type === 'network') {
|
if (d.type === 'network') {
|
||||||
openNetworkDetail(d.key);
|
if (opts.onNetworkClick) opts.onNetworkClick(d.key);
|
||||||
|
else openNetworkDetail(d.key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const statusEl = opts.statusEl || document.getElementById('networksMapStatus');
|
||||||
const s = document.getElementById('networksMapStatus');
|
if (statusEl) statusEl.textContent = `Geselecteerd: ${d.type} ${d.key}`;
|
||||||
if (s) s.textContent = `Geselecteerd: ${d.type} ${d.key}`;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// simulation
|
|
||||||
const sim = d3.forceSimulation(model.nodes)
|
const sim = d3.forceSimulation(model.nodes)
|
||||||
.force('link', d3.forceLink(model.links).id(d => d.id).distance(80))
|
.force('link', d3.forceLink(model.links).id(d => d.id).distance(80))
|
||||||
.force('charge', d3.forceManyBody().strength(-30))
|
.force('charge', d3.forceManyBody().strength(-30))
|
||||||
@@ -746,23 +722,24 @@
|
|||||||
.force('collide', d3.forceCollide().radius(d => d.type === 'network' ? 18 : 16))
|
.force('collide', d3.forceCollide().radius(d => d.type === 'network' ? 18 : 16))
|
||||||
.on('tick', () => {
|
.on('tick', () => {
|
||||||
link
|
link
|
||||||
.attr('x1', d => d.source.x)
|
.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
|
||||||
.attr('y1', d => d.source.y)
|
.attr('x2', d => d.target.x).attr('y2', d => d.target.y);
|
||||||
.attr('x2', d => d.target.x)
|
|
||||||
.attr('y2', d => d.target.y);
|
|
||||||
|
|
||||||
node.attr('transform', d => `translate(${d.x},${d.y})`);
|
node.attr('transform', d => `translate(${d.x},${d.y})`);
|
||||||
});
|
});
|
||||||
// detail-mode: pin netwerk in het midden
|
|
||||||
if (opts.pinNetwork) {
|
if (opts.pinNetwork) {
|
||||||
const pinId = opts.pinNetwork;
|
const pinned = model.nodes.find(n => n.id === opts.pinNetwork);
|
||||||
const pinned = model.nodes.find(n => n.id === pinId);
|
if (pinned) { pinned.fx = w / 2; pinned.fy = h / 2; }
|
||||||
if (pinned) {
|
|
||||||
pinned.fx = w / 2;
|
|
||||||
pinned.fy = h / 2;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.svg = svg; ctx.g = g; ctx.sim = sim; ctx.model = model; ctx.zoom = zoom;
|
||||||
}
|
}
|
||||||
graphCtx = { svg, g, sim, model, zoom };
|
|
||||||
|
function renderGraph(model, opts = {}) {
|
||||||
|
const host = document.getElementById('networksMapHost');
|
||||||
|
if (!host) return;
|
||||||
|
graphCtx = {};
|
||||||
|
_renderGraphInHost(host, graphCtx, model, opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openNetworkDetail(networkName) {
|
function openNetworkDetail(networkName) {
|
||||||
@@ -839,6 +816,79 @@
|
|||||||
if (s) s.textContent = buildMapStatus('Global', model.nodes.length, model.links.length);
|
if (s) s.textContent = buildMapStatus('Global', model.nodes.length, model.links.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Vergroot modal ----
|
||||||
|
|
||||||
|
function openModalNetworkDetail(networkName) {
|
||||||
|
const host = document.getElementById('networksMapModalHost');
|
||||||
|
if (!host) return;
|
||||||
|
if (!modalGraphCtx) modalGraphCtx = {};
|
||||||
|
|
||||||
|
const model = buildNetworkDetailGraphModel(networkName);
|
||||||
|
_renderGraphInHost(host, modalGraphCtx, model, {
|
||||||
|
pinNetwork: `net:${networkName}`,
|
||||||
|
onNetworkClick: openModalNetworkDetail,
|
||||||
|
statusEl: document.getElementById('networksMapModalStatus'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const s = document.getElementById('networksMapModalStatus');
|
||||||
|
if (s) s.textContent = buildMapStatus(`Detail: ${networkName}`, model.nodes.length, model.links.length);
|
||||||
|
|
||||||
|
const list = Array.isArray(state.list?.networks) ? state.list.networks : [];
|
||||||
|
const meta = list.find(n => (n?.name || n?.Name) === networkName) || { name: networkName };
|
||||||
|
const usage = state.usage?.byNetwork?.[networkName];
|
||||||
|
const containers = Array.isArray(usage?.containers) ? usage.containers : [];
|
||||||
|
const driver = String(meta?.driver || meta?.Driver || '');
|
||||||
|
const subnets = fmtSubnets(meta).join(', ');
|
||||||
|
const listHtml = containers.length
|
||||||
|
? `<ul class="mapDetailList">${containers.map(c => `<li>${esc(c.name || c.id.slice(0, 12))}</li>`).join('')}</ul>`
|
||||||
|
: `<div class="muted">Geen containers op dit netwerk.</div>`;
|
||||||
|
|
||||||
|
const title = document.getElementById('networksMapModalDetailTitle');
|
||||||
|
const body = document.getElementById('networksMapModalDetailBody');
|
||||||
|
const side = document.getElementById('networksMapModalSide');
|
||||||
|
if (title) title.textContent = networkName;
|
||||||
|
if (body) body.innerHTML = `
|
||||||
|
<div class="mapDetailGrid">
|
||||||
|
<div class="mapDetailKey">Driver</div><div>${esc(driver || '—')}</div>
|
||||||
|
<div class="mapDetailKey">Subnets</div><div>${esc(subnets || '—')}</div>
|
||||||
|
<div class="mapDetailKey">Containers</div><div>${containers.length}</div>
|
||||||
|
</div>${listHtml}`;
|
||||||
|
if (side) side.style.display = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function _openModalGlobalMap() {
|
||||||
|
const host = document.getElementById('networksMapModalHost');
|
||||||
|
if (!host) return;
|
||||||
|
modalGraphCtx = {};
|
||||||
|
const model = buildGlobalGraphModel();
|
||||||
|
_renderGraphInHost(host, modalGraphCtx, model, {
|
||||||
|
onNetworkClick: openModalNetworkDetail,
|
||||||
|
statusEl: document.getElementById('networksMapModalStatus'),
|
||||||
|
});
|
||||||
|
const s = document.getElementById('networksMapModalStatus');
|
||||||
|
if (s) s.textContent = buildMapStatus('Global', model.nodes.length, model.links.length);
|
||||||
|
const side = document.getElementById('networksMapModalSide');
|
||||||
|
if (side) side.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMapModal() {
|
||||||
|
const modal = document.getElementById('networksMapModal');
|
||||||
|
if (!modal) return;
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
requestAnimationFrame(() => { _openModalGlobalMap(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeMapModal() {
|
||||||
|
const modal = document.getElementById('networksMapModal');
|
||||||
|
if (modal) modal.style.display = 'none';
|
||||||
|
if (modalGraphCtx && modalGraphCtx.sim) modalGraphCtx.sim.stop();
|
||||||
|
modalGraphCtx = {};
|
||||||
|
const host = document.getElementById('networksMapModalHost');
|
||||||
|
if (host) host.innerHTML = '';
|
||||||
|
const side = document.getElementById('networksMapModalSide');
|
||||||
|
if (side) side.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
function renderNetworks() {
|
function renderNetworks() {
|
||||||
const tbody = document.getElementById('networksTbody');
|
const tbody = document.getElementById('networksTbody');
|
||||||
const rel = document.getElementById('networksRelations');
|
const rel = document.getElementById('networksRelations');
|
||||||
@@ -1019,6 +1069,44 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modal: expand knop
|
||||||
|
const expandBtn = document.getElementById('networksMapExpandBtn');
|
||||||
|
if (expandBtn && !expandBtn.dataset.bound) {
|
||||||
|
expandBtn.dataset.bound = '1';
|
||||||
|
expandBtn.addEventListener('click', openMapModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal: sluitknop
|
||||||
|
const modalCloseBtn = document.getElementById('networksMapModalClose');
|
||||||
|
if (modalCloseBtn && !modalCloseBtn.dataset.bound) {
|
||||||
|
modalCloseBtn.dataset.bound = '1';
|
||||||
|
modalCloseBtn.addEventListener('click', closeMapModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal: overlay klik
|
||||||
|
const mapModal = document.getElementById('networksMapModal');
|
||||||
|
if (mapModal && !mapModal.dataset.bound) {
|
||||||
|
mapModal.dataset.bound = '1';
|
||||||
|
mapModal.addEventListener('click', (ev) => { if (ev.target === mapModal) closeMapModal(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal: terug naar global
|
||||||
|
const modalBackBtn = document.getElementById('networksMapModalBackBtn');
|
||||||
|
if (modalBackBtn && !modalBackBtn.dataset.bound) {
|
||||||
|
modalBackBtn.dataset.bound = '1';
|
||||||
|
modalBackBtn.addEventListener('click', _openModalGlobalMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ESC sluit modal (éénmalig binden)
|
||||||
|
if (!_escBound) {
|
||||||
|
_escBound = true;
|
||||||
|
document.addEventListener('keydown', (ev) => {
|
||||||
|
if (ev.key !== 'Escape') return;
|
||||||
|
const m = document.getElementById('networksMapModal');
|
||||||
|
if (m && m.style.display !== 'none') closeMapModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const detailBody = document.getElementById('networksDetailBody');
|
const detailBody = document.getElementById('networksDetailBody');
|
||||||
if (detailBody && !detailBody.dataset.bound) {
|
if (detailBody && !detailBody.dataset.bound) {
|
||||||
detailBody.dataset.bound = '1';
|
detailBody.dataset.bound = '1';
|
||||||
|
|||||||
+25
-6
@@ -29,9 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<button class="btn ghost" onclick="pingApi()">◉ Ping</button>
|
<button class="btn ghost" id="themeToggleBtn" title="Schakel thema">🌙</button>
|
||||||
<button class="btn" onclick="refreshActive()">↻ Ververs</button>
|
|
||||||
<button class="btn ghost" id="themeToggleBtn" title="Schakel light/dark mode">◐ Theme</button>
|
|
||||||
<span class="statusline headerMeta" id="lastRefreshHeader">Laatste refresh: -</span>
|
<span class="statusline headerMeta" id="lastRefreshHeader">Laatste refresh: -</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,6 +204,7 @@
|
|||||||
<span class="muted">Alleen verbonden</span>
|
<span class="muted">Alleen verbonden</span>
|
||||||
</label>
|
</label>
|
||||||
<span class="muted" id="networksMapStatus" style="margin-left:auto;">Kaartweergave (placeholder)</span>
|
<span class="muted" id="networksMapStatus" style="margin-left:auto;">Kaartweergave (placeholder)</span>
|
||||||
|
<button class="btn small ghost" type="button" id="networksMapExpandBtn" title="Vergroot naar volledig scherm">⛶</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -777,9 +776,9 @@
|
|||||||
function updateThemeToggleUi(theme) {
|
function updateThemeToggleUi(theme) {
|
||||||
const btn = document.getElementById('themeToggleBtn');
|
const btn = document.getElementById('themeToggleBtn');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
const next = theme === 'dark' ? 'light' : 'dark';
|
const goingTo = theme === 'dark' ? 'light' : 'dark';
|
||||||
btn.textContent = `Theme: ${theme === 'dark' ? 'Dark' : 'Light'}`;
|
btn.textContent = goingTo === 'light' ? '☀️' : '🌙';
|
||||||
btn.title = `Schakel naar ${next === 'dark' ? 'dark' : 'light'} mode`;
|
btn.title = goingTo === 'light' ? 'Schakel naar licht thema' : 'Schakel naar donker thema';
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyTheme(theme, persist = false) {
|
function applyTheme(theme, persist = false) {
|
||||||
@@ -832,5 +831,25 @@
|
|||||||
<script src="assets/js/tabs/networks.js"></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>
|
||||||
<script src="assets/js/tabs/volumes.js"></script>
|
<script src="assets/js/tabs/volumes.js"></script>
|
||||||
|
|
||||||
|
<!-- Netwerktopologie vergroot modal -->
|
||||||
|
<div id="networksMapModal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,.72); z-index:1000; align-items:center; justify-content:center;">
|
||||||
|
<div style="position:relative; width:90vw; height:90vh; background:var(--card-bg); border:1px solid var(--card-border); border-radius:16px; display:flex; flex-direction:column; overflow:hidden;">
|
||||||
|
<div style="display:flex; align-items:center; justify-content:space-between; padding:10px 16px; border-bottom:1px solid var(--card-border); flex:0 0 auto;">
|
||||||
|
<span class="muted mono" id="networksMapModalStatus">Netwerktopologie</span>
|
||||||
|
<button class="btn ghost" id="networksMapModalClose" title="Sluiten (Esc)">✕</button>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; flex:1; min-height:0;">
|
||||||
|
<div id="networksMapModalHost" style="flex:1; min-width:0; min-height:0; position:relative; overflow:hidden; background:var(--map-bg);"></div>
|
||||||
|
<div id="networksMapModalSide" style="display:none; width:280px; flex:0 0 280px; border-left:1px solid var(--card-border); overflow-y:auto; padding:12px;">
|
||||||
|
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:10px;">
|
||||||
|
<strong id="networksMapModalDetailTitle">Netwerk</strong>
|
||||||
|
<button class="btn small ghost" id="networksMapModalBackBtn">← Terug</button>
|
||||||
|
</div>
|
||||||
|
<div id="networksMapModalDetailBody"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user