238 lines
19 KiB
Plaintext
238 lines
19 KiB
Plaintext
<!DOCTYPE html><html lang="nl"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Infrastructuur — Mek-Tech</title><link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"><link rel="stylesheet" href="/css/style.css"><link rel="stylesheet" href="/css/dynamic.css"><link rel="icon" href="/img/favicon.svg"><script src="https://unpkg.com/lucide@latest"></script>
|
|
<style>
|
|
:root{--infra-bg:var(--bg);--card-glow:0 0 0 1px var(--border)}
|
|
body{background:var(--bg)}
|
|
.infra-wrap{max-width:1200px;margin:0 auto;padding:1.25rem 1.5rem 3rem}
|
|
|
|
/* Header */
|
|
.infra-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem;flex-wrap:wrap;gap:.5rem}
|
|
.infra-hdr h1{font-size:1.4rem;color:var(--text);font-weight:700;display:flex;align-items:center;gap:.5rem}
|
|
.infra-hdr h1 svg{width:24px;height:24px;color:var(--accent)}
|
|
.infra-hdr .sub{font-size:.8rem;color:var(--text-muted);margin-top:.1rem}
|
|
|
|
/* Tabs */
|
|
.infra-tabs{display:flex;gap:0;margin-bottom:1.5rem;background:var(--bg-card);border-radius:var(--radius);border:1px solid var(--border);overflow:hidden}
|
|
.infra-tab{padding:.55rem 1.1rem;font-size:.8rem;font-weight:500;color:var(--text-muted);cursor:pointer;transition:all .12s;border:none;background:none;font-family:inherit;display:flex;align-items:center;gap:.4rem;white-space:nowrap;border-right:1px solid var(--border)}
|
|
.infra-tab:last-child{border-right:none}
|
|
.infra-tab:hover{color:var(--text);background:var(--border-light)}
|
|
.infra-tab.active{color:var(--accent);background:rgba(88,166,255,.08);font-weight:600;box-shadow:inset 0 -2px 0 var(--accent)}
|
|
.infra-tab svg{width:14px;height:14px}
|
|
|
|
/* KPI row */
|
|
.kpi-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:.6rem;margin-bottom:1.25rem}
|
|
.kpi-box{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:.8rem 1rem;display:flex;align-items:center;gap:.75rem;transition:all .15s;position:relative;overflow:hidden}
|
|
.kpi-box:hover{border-color:var(--text-dim)}
|
|
.kpi-box .kb-icon{width:36px;height:36px;border-radius:var(--radius);display:flex;align-items:center;justify-content:center;font-size:1rem;flex-shrink:0}
|
|
.kpi-box .kb-num{font-size:1.4rem;font-weight:700;color:var(--text);line-height:1}
|
|
.kpi-box .kb-label{font-size:.65rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.3px;margin-top:2px}
|
|
|
|
/* Panel content */
|
|
.panel{display:none}.panel.active{display:block}
|
|
|
|
/* Section header */
|
|
.sec-hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:.75rem}
|
|
.sec-hdr h2{font-size:.95rem;color:var(--text);font-weight:600;display:flex;align-items:center;gap:.4rem}
|
|
.sec-hdr h2 svg{width:16px;height:16px;color:var(--accent)}
|
|
|
|
/* Grid layouts */
|
|
.grid-3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:.6rem}
|
|
@media(max-width:900px){.grid-3{grid-template-columns:1fr 1fr}}@media(max-width:600px){.grid-3{grid-template-columns:1fr}}
|
|
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:.6rem}
|
|
@media(max-width:600px){.grid-2{grid-template-columns:1fr}}
|
|
|
|
/* Infra cards */
|
|
.infra-card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:1rem;transition:all .12s}
|
|
.infra-card:hover{border-color:var(--text-dim)}
|
|
|
|
/* Rack mini visualization */
|
|
.rm-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.5rem}
|
|
.rm-card{display:flex;gap:.6rem;padding:.6rem;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);text-decoration:none;color:inherit;transition:all .12s}
|
|
.rm-card:hover{border-color:var(--text-dim)}
|
|
.rm-vis{width:16px;display:flex;flex-direction:column;gap:1px;flex-shrink:0}
|
|
.rm-u{width:100%;height:4px;border-radius:1px;background:var(--border)}.rm-u.filled{background:var(--accent)}
|
|
.rm-info{flex:1;min-width:0}
|
|
.rm-info h4{font-size:.8rem;color:var(--text);font-weight:600}
|
|
.rm-info p{font-size:.68rem;color:var(--text-muted)}
|
|
|
|
/* Network device grid */
|
|
.nd-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:.5rem}
|
|
.nd-card{display:flex;align-items:center;gap:.5rem;padding:.55rem .6rem;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);text-decoration:none;color:inherit;transition:all .12s}
|
|
.nd-card:hover{border-color:var(--text-dim)}
|
|
.nd-icon{width:28px;height:28px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:.65rem;font-weight:700;flex-shrink:0}
|
|
.nd-info{flex:1;min-width:0}
|
|
.nd-info h4{font-size:.75rem;color:var(--text);font-weight:500}
|
|
.nd-info p{font-size:.65rem;color:var(--text-muted)}
|
|
|
|
/* Diagram grid */
|
|
.dg-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:.5rem}
|
|
.dg-card{padding:.65rem;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);text-decoration:none;color:inherit;transition:all .12s;display:flex;flex-direction:column;gap:.2rem}
|
|
.dg-card:hover{border-color:var(--text-dim)}
|
|
.dg-card h4{font-size:.78rem;color:var(--text);font-weight:500}
|
|
.dg-card p{font-size:.65rem;color:var(--text-muted)}
|
|
|
|
/* Conn list */
|
|
.cl-list{display:flex;flex-direction:column;gap:2px}
|
|
.cl-item{display:flex;align-items:center;gap:.5rem;padding:.35rem .5rem;background:var(--bg);border-radius:3px;font-size:.72rem;color:var(--text-muted)}
|
|
|
|
/* Empty state */
|
|
.empty-i{padding:2rem 1rem;text-align:center;color:var(--text-dim);font-size:.8rem}
|
|
|
|
/* Quick actions */
|
|
.qa-strip{display:flex;gap:.35rem;flex-wrap:wrap}
|
|
.qa-strip a{padding:.35rem .65rem;background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);font-size:.72rem;color:var(--text-muted);text-decoration:none;transition:all .1s;display:flex;align-items:center;gap:.3rem}
|
|
.qa-strip a:hover{border-color:var(--accent);color:var(--accent)}
|
|
|
|
/* Data table compact */
|
|
.dt-compact{width:100%;border-collapse:collapse}
|
|
.dt-compact th{text-align:left;padding:.35rem .5rem;font-size:.65rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.3px;border-bottom:1px solid var(--border);background:var(--bg)}
|
|
.dt-compact td{padding:.35rem .5rem;font-size:.75rem;border-bottom:1px solid var(--border-light);color:var(--text)}
|
|
.dt-compact tr:hover td{background:var(--border-light)}
|
|
|
|
/* Form inline */
|
|
.form-inline{display:flex;gap:.35rem;flex-wrap:wrap;align-items:flex-end}
|
|
.form-inline input,.form-inline select{padding:.3rem .5rem;font-size:.75rem;width:auto;min-width:100px}
|
|
.form-inline .btn{padding:.3rem .6rem;font-size:.72rem}
|
|
|
|
/* Modal */
|
|
.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.6);z-index:1000;display:none;align-items:center;justify-content:center}
|
|
.modal-overlay.open{display:flex}
|
|
.modal-box{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius-lg);padding:1.25rem;width:90%;max-width:600px;max-height:80vh;overflow-y:auto}
|
|
.modal-box h3{font-size:.9rem;color:var(--text);margin-bottom:.75rem;display:flex;justify-content:space-between;align-items:center}
|
|
.modal-close{background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:1.1rem}
|
|
</style></head><body>
|
|
<nav class="navbar"><div class="nav-inner"><a href="/" class="nav-brand"><img src="/img/logo.svg" class="nav-logo" alt="Mek-Tech"></a><div style="display:flex;align-items:center;gap:.4rem"><a href="/" class="nav-icon-btn" title="Dashboard"><i data-lucide="layout-dashboard" style="width:15px;height:15px"></i> Dashboard</a><button class="theme-toggle" id="themeToggle" onclick="toggleTheme()"></button><a href="/auth/logout" class="nav-icon-btn" style="color:var(--red)"><i data-lucide="log-out" style="width:15px;height:15px"></i> Uitloggen</a></div></div></nav>
|
|
|
|
<div class="infra-wrap">
|
|
<div class="infra-hdr">
|
|
<div><h1><i data-lucide="server"></i> Infrastructuur Beheer</h1><p class="sub">Racks, netwerkapparatuur en architectuurdiagrammen op één plek</p></div>
|
|
<div class="qa-strip">
|
|
<a href="#" onclick="showModal('rackModal')"><i data-lucide="plus" style="width:12px;height:12px"></i> Rack</a>
|
|
<a href="/networking/devices/new"><i data-lucide="plus" style="width:12px;height:12px"></i> Netwerk</a>
|
|
<a href="/architecture/new"><i data-lucide="plus" style="width:12px;height:12px"></i> Diagram</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="infra-tabs">
|
|
<button class="infra-tab <%= tab === 'overview' ? 'active' : '' %>" data-tab="overview"><i data-lucide="layout-dashboard"></i> Overzicht</button>
|
|
<button class="infra-tab <%= tab === 'racks' ? 'active' : '' %>" data-tab="racks"><i data-lucide="server"></i> Racks <span class="sb-badge" style="font-size:.6rem;margin-left:.2rem"><%= totalRacks %></span></button>
|
|
<button class="infra-tab <%= tab === 'network' ? 'active' : '' %>" data-tab="network"><i data-lucide="wifi"></i> Netwerk <span class="sb-badge" style="font-size:.6rem;margin-left:.2rem"><%= totalNetworkDevices %></span></button>
|
|
<button class="infra-tab <%= tab === 'architecture' ? 'active' : '' %>" data-tab="architecture"><i data-lucide="layers"></i> Architectuur <span class="sb-badge" style="font-size:.6rem;margin-left:.2rem"><%= totalDiagrams %></span></button>
|
|
</div>
|
|
|
|
<!-- ===== OVERVIEW TAB ===== -->
|
|
<div class="panel <%= tab === 'overview' ? 'active' : '' %>" id="panel-overview">
|
|
<div class="kpi-grid">
|
|
<a href="/infrastructure?tab=racks" class="kpi-box" style="text-decoration:none;color:inherit"><div class="kb-icon" style="background:rgba(88,166,255,.1);color:var(--accent)"><i data-lucide="server" style="width:18px;height:18px"></i></div><div><div class="kb-num"><%= totalRacks %></div><div class="kb-label">Racks</div><div class="kb-label" style="color:var(--text-dim)"><%= totalRackDevices %> devices</div></div></a>
|
|
<a href="/infrastructure?tab=network" class="kpi-box" style="text-decoration:none;color:inherit"><div class="kb-icon" style="background:rgba(188,140,255,.1);color:#bc8cff"><i data-lucide="wifi" style="width:18px;height:18px"></i></div><div><div class="kb-num"><%= totalNetworkDevices %></div><div class="kb-label">Netwerk Devices</div><div class="kb-label" style="color:var(--text-dim)"><%= totalConnections %> connecties</div></div></a>
|
|
<a href="/infrastructure?tab=architecture" class="kpi-box" style="text-decoration:none;color:inherit"><div class="kb-icon" style="background:rgba(63,185,80,.1);color:var(--green)"><i data-lucide="layers" style="width:18px;height:18px"></i></div><div><div class="kb-num"><%= totalDiagrams %></div><div class="kb-label">Diagrammen</div><div class="kb-label" style="color:var(--text-dim)">architectuur</div></div></a>
|
|
</div>
|
|
|
|
<div class="grid-3">
|
|
<div class="infra-card"><div class="sec-hdr"><h2><i data-lucide="server"></i> Recente Racks</h2></div>
|
|
<% if (recentRacks.length === 0) { %><div class="empty-i">Nog geen racks</div><% } else { %><div class="rm-grid" style="grid-template-columns:1fr">
|
|
<% recentRacks.forEach(function(r){ %><a href="/racks/<%= r.id %>" class="rm-card" style="padding:.5rem">
|
|
<div class="rm-vis"><% for(var i=0;i<Math.min(r.total_units||42,10);i++){%><div class="rm-u <%= i < (r.device_count||0) ? 'filled' : '' %>"></div><% } %></div>
|
|
<div class="rm-info"><h4><%= r.name %></h4><p><%= r.client_name %> · <%= r.device_count %> devices</p></div>
|
|
</a><% }) %></div><% } %></div>
|
|
|
|
<div class="infra-card"><div class="sec-hdr"><h2><i data-lucide="wifi"></i> Netwerk Types</h2></div>
|
|
<% if (netDeviceTypes.length === 0) { %><div class="empty-i">Geen netwerk devices</div><% } else { %>
|
|
<% var maxNd = Math.max(...netDeviceTypes.map(function(e){return e.cnt}), 1); %>
|
|
<% netDeviceTypes.forEach(function(d){ %><div style="display:flex;justify-content:space-between;align-items:center;padding:.3rem 0;font-size:.75rem"><span style="color:var(--text-muted);text-transform:capitalize"><%= d.device_type %></span><span style="color:var(--text);font-weight:600"><%= d.cnt %></span></div>
|
|
<div style="height:3px;background:var(--border-light);border-radius:2px;margin-bottom:.2rem;overflow:hidden"><div style="height:100%;width:<%= (d.cnt/maxNd*100) %>%background:var(--accent);border-radius:2px"></div></div>
|
|
<% }) %><% } %></div>
|
|
|
|
<div class="infra-card"><div class="sec-hdr"><h2><i data-lucide="layers"></i> Diagram Types</h2></div>
|
|
<% if (diagramTypes.length === 0) { %><div class="empty-i">Geen diagrammen</div><% } else { %>
|
|
<% var maxDg = Math.max(...diagramTypes.map(function(e){return e.cnt}), 1); %>
|
|
<% diagramTypes.forEach(function(d){ %><div style="display:flex;justify-content:space-between;align-items:center;padding:.3rem 0;font-size:.75rem"><span style="color:var(--text-muted);text-transform:capitalize"><%= d.diagram_type.replace(/_/g,' ') %></span><span style="color:var(--text);font-weight:600"><%= d.cnt %></span></div>
|
|
<div style="height:3px;background:var(--border-light);border-radius:2px;margin-bottom:.2rem;overflow:hidden"><div style="height:100%;width:<%= (d.cnt/maxDg*100) %>%background:var(--green);border-radius:2px"></div></div>
|
|
<% }) %><% } %></div>
|
|
</div>
|
|
|
|
<div class="grid-2" style="margin-top:.6rem">
|
|
<div class="infra-card"><div class="sec-hdr"><h2><i data-lucide="activity"></i> Device Types</h2></div>
|
|
<% if (deviceTypes.length === 0) { %><div class="empty-i">Geen rack devices</div><% } else { %><div class="cl-list"><% deviceTypes.forEach(function(d){ %><div class="cl-item"><span style="text-transform:capitalize;flex:1"><%= d.device_type %></span><span style="font-weight:600;color:var(--text)"><%= d.cnt %></span></div><% }) %></div><% } %></div>
|
|
<div class="infra-card"><div class="sec-hdr"><h2><i data-lucide="trending-up"></i> Per Cliënt</h2></div>
|
|
<% if (racksByClient.length === 0) { %><div class="empty-i">Geen data</div><% } else { %><div class="cl-list"><% racksByClient.forEach(function(r){ %><div class="cl-item"><span style="flex:1"><%= r.client_name %></span><span style="font-weight:600;color:var(--text)"><%= r.cnt %> racks</span></div><% }) %></div><% } %></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== RACKS TAB ===== -->
|
|
<div class="panel <%= tab === 'racks' ? 'active' : '' %>" id="panel-racks">
|
|
<div class="sec-hdr"><h2><i data-lucide="server"></i> Server Racks</h2><a href="/racks" class="btn btn-primary btn-small">Alle racks</a></div>
|
|
<% if (racks.length === 0) { %><div class="empty-i">Nog geen racks. <a href="#" class="link" onclick="showModal('rackModal')">Voeg je eerste rack toe</a>.</div><% } else { %>
|
|
<div class="rm-grid">
|
|
<% racks.forEach(function(r){ %><a href="/racks/<%= r.id %>" class="rm-card">
|
|
<div class="rm-vis"><% var maxU = Math.min(r.total_units||42,20); for(var i=0;i<maxU;i++){%><div class="rm-u <%= i < (r.device_count||0) ? 'filled' : '' %>"></div><% } %></div>
|
|
<div class="rm-info"><h4><%= r.name %></h4><p><%= r.client_name %> · <%= r.device_count || 0 %>/<%= r.total_units || 42 %>U</p><p style="font-size:.6rem"><%= r.location || r.datacenter || '' %></p></div>
|
|
</a><% }) %>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
|
|
<!-- ===== NETWORK TAB ===== -->
|
|
<div class="panel <%= tab === 'network' ? 'active' : '' %>" id="panel-network">
|
|
<div class="sec-hdr"><h2><i data-lucide="wifi"></i> Netwerkapparatuur</h2><a href="/networking" class="btn btn-primary btn-small">Alle netwerk</a></div>
|
|
<% if (netDevices.length === 0) { %><div class="empty-i">Geen netwerk devices. <a href="/networking/devices/new" class="link">Voeg device toe</a>.</div><% } else { %>
|
|
<div class="nd-grid">
|
|
<% netDevices.forEach(function(d){ %><a href="/networking/devices/<%= d.id %>" class="nd-card">
|
|
<div class="nd-icon" style="background:<%= d.device_type==='switch'?'rgba(188,140,255,.15)':d.device_type==='router'?'rgba(88,166,255,.15)':d.device_type==='firewall'?'rgba(248,81,73,.15)':'rgba(63,185,80,.15)' %>;color:<%= d.device_type==='switch'?'#bc8cff':d.device_type==='router'?'#58a6ff':d.device_type==='firewall'?'#f85149':'#3fb950' %>"><%= d.device_type.charAt(0).toUpperCase() %></div>
|
|
<div class="nd-info"><h4><%= d.name %></h4><p><%= d.model || d.device_type %> · <%= d.client_name %></p></div>
|
|
</a><% }) %>
|
|
</div>
|
|
<% } %>
|
|
|
|
<% if (connections.length > 0) { %><div class="infra-card" style="margin-top:.75rem">
|
|
<div class="sec-hdr"><h2><i data-lucide="link"></i> Verbindingen</h2></div>
|
|
<div class="cl-list"><% connections.slice(0,10).forEach(function(c){ %><div class="cl-item"><span style="flex:1"><%= c.name || 'Onbekend' %> · <%= c.from_type %> → <%= c.to_type %></span><span style="font-size:.65rem;color:var(--accent)"><%= c.speed %> · <%= c.media_type %></span></div><% }) %></div>
|
|
</div><% } %>
|
|
</div>
|
|
|
|
<!-- ===== ARCHITECTURE TAB ===== -->
|
|
<div class="panel <%= tab === 'architecture' ? 'active' : '' %>" id="panel-architecture">
|
|
<div class="sec-hdr"><h2><i data-lucide="layers"></i> Architectuur Diagrammen</h2><a href="/architecture" class="btn btn-primary btn-small">Alle diagrammen</a></div>
|
|
<% if (diagrams.length === 0) { %><div class="empty-i">Geen diagrammen. <a href="/architecture/new" class="link">Maak je eerste diagram</a>.</div><% } else { %>
|
|
<div class="dg-grid">
|
|
<% diagrams.forEach(function(d){ %><a href="/architecture/<%= d.id %>" class="dg-card">
|
|
<div style="display:flex;gap:.35rem;align-items:center;margin-bottom:.2rem"><span style="font-size:.6rem;padding:.05rem .35rem;border-radius:3px;background:<%= d.diagram_type==='data_architecture'?'rgba(88,166,255,.1)':'rgba(188,140,255,.1)' %>;color:<%= d.diagram_type==='data_architecture'?'#58a6ff':'#bc8cff' %>"><%= d.diagram_type.replace(/_/g,' ') %></span><span style="font-size:.6rem;color:var(--text-dim)"><%= d.node_count || 0 %> nodes</span></div>
|
|
<h4><%= d.name %></h4>
|
|
<p><%= d.client_name %> · <%= new Date(d.updated_at).toLocaleDateString('nl-NL') %></p>
|
|
</a><% }) %>
|
|
</div>
|
|
<% } %>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ===== RACK MODAL ===== -->
|
|
<div class="modal-overlay" id="rackModal"><div class="modal-box">
|
|
<h3>Nieuw Rack <button class="modal-close" onclick="hideModal('rackModal')">✕</button></h3>
|
|
<form method="POST" action="/racks/new">
|
|
<select name="client_id" required style="margin-bottom:.5rem"><option value="">Cliënt...</option><% clients.forEach(function(c){ %><option value="<%= c.id %>"><%= c.name %></option><% }) %></select>
|
|
<div class="form-inline"><input type="text" name="name" placeholder="Rack naam" required><input type="text" name="location" placeholder="Locatie">
|
|
<select name="total_units"><option value="42">42U</option><option value="48">48U</option><option value="24">24U</option><option value="12">12U</option></select>
|
|
<button class="btn btn-primary" type="submit">Aanmaken</button></div>
|
|
</form></div></div>
|
|
|
|
<script>
|
|
document.querySelectorAll('.infra-tab').forEach(function(tab){
|
|
tab.addEventListener('click',function(){
|
|
var t = this.dataset.tab;
|
|
document.querySelectorAll('.infra-tab').forEach(function(b){b.classList.remove('active')});
|
|
document.querySelectorAll('.panel').forEach(function(p){p.classList.remove('active')});
|
|
this.classList.add('active');
|
|
document.getElementById('panel-'+t).classList.add('active');
|
|
var url = new URL(window.location);
|
|
url.searchParams.set('tab', t);
|
|
window.history.replaceState({}, '', url);
|
|
});
|
|
});
|
|
var t = new URL(window.location).searchParams.get('tab');
|
|
if(t){var btn=document.querySelector('.infra-tab[data-tab="'+t+'"]');if(btn)btn.click();}
|
|
function showModal(id){document.getElementById(id).classList.add('open')}
|
|
function hideModal(id){document.getElementById(id).classList.remove('open')}
|
|
document.querySelectorAll('.modal-overlay').forEach(function(m){m.addEventListener('click',function(e){if(e.target===this)this.classList.remove('open')})});
|
|
lucide.createIcons();
|
|
</script>
|
|
<script src="/js/theme.js"></script></body></html>
|