752 lines
46 KiB
Plaintext
752 lines
46 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="nl" data-theme="dark">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title><%= rack.name %> — Rack View — 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" type="image/svg+xml">
|
||
<script src="https://unpkg.com/lucide@latest"></script>
|
||
<style>
|
||
/* ── Dracula-inspired Rack Theme ── */
|
||
:root {
|
||
--d-bg: #1e1f2b;
|
||
--d-card: #282a36;
|
||
--d-border: #44475a;
|
||
--d-text: #f8f8f2;
|
||
--d-muted: #6272a4;
|
||
--d-dim: #44475a;
|
||
--d-accent: #ff79c6;
|
||
--d-green: #50fa7b;
|
||
--d-cyan: #8be9fd;
|
||
--d-yellow: #f1fa8c;
|
||
--d-orange: #ffb86c;
|
||
--d-red: #ff5555;
|
||
--rack-w: 280px;
|
||
--u-h: 30px;
|
||
}
|
||
body { background: var(--d-bg); color: var(--d-text); font-family: Inter, sans-serif; min-height: 100vh; }
|
||
::-webkit-scrollbar{width:6px;height:6px}
|
||
::-webkit-scrollbar-track{background:var(--d-bg)}
|
||
::-webkit-scrollbar-thumb{background:var(--d-dim);border-radius:3px}
|
||
|
||
.rack-layout{display:flex;gap:1.5rem;align-items:flex-start;padding:1rem 0}
|
||
@media(max-width:1000px){.rack-layout{flex-direction:column}}
|
||
|
||
/* ── Rack Frame ── */
|
||
.rack-col{flex-shrink:0;position:relative}
|
||
.rack-view{width:var(--rack-w);background:var(--d-card);border:2px solid var(--d-border);border-radius:6px;overflow:hidden;box-shadow:0 4px 24px rgba(0,0,0,.3)}
|
||
.rack-top{background:var(--d-border);padding:.4rem .6rem;font-size:.7rem;color:var(--d-text);text-align:center;border-bottom:2px solid var(--d-dim);font-weight:600;letter-spacing:1px;text-transform:uppercase}
|
||
.rack-body{display:flex;flex-direction:column;position:relative}
|
||
.rack-body::before{content:'';position:absolute;top:0;bottom:0;left:26px;width:2px;background:var(--d-dim);opacity:.3}
|
||
.rack-body::after{content:'';position:absolute;top:0;bottom:0;right:26px;width:2px;background:var(--d-dim);opacity:.3}
|
||
|
||
/* U slot */
|
||
.rack-u{display:flex;align-items:center;height:var(--u-h);border-bottom:1px solid var(--d-dim);position:relative;transition:background .15s}
|
||
.rack-u:hover{background:rgba(68,71,90,.3)}
|
||
.rack-u.empty{background:var(--d-card)}
|
||
.rack-u .u-num{width:24px;font-size:.45rem;color:var(--d-muted);text-align:center;flex-shrink:0;font-weight:500;z-index:1}
|
||
.rack-u .u-mid{position:absolute;left:26px;right:26px;height:1px;background:rgba(68,71,90,.15);top:50%}
|
||
|
||
/* Device block */
|
||
.rack-device{display:flex;align-items:center;gap:.25rem;cursor:grab;position:absolute;left:24px;right:24px;z-index:10;border-radius:3px;overflow:hidden;transition:box-shadow .15s}
|
||
.rack-device:hover{box-shadow:0 0 0 1px rgba(255,255,255,.1),0 4px 12px rgba(0,0,0,.4);z-index:20}
|
||
.rack-device.dragging{opacity:.7;z-index:100;box-shadow:0 8px 32px rgba(0,0,0,.5)}
|
||
.rack-device .d-lu{flex-shrink:0;display:flex;align-items:center;justify-content:center;width:18px;height:18px}
|
||
.rack-device .d-lu svg{width:14px;height:14px;color:rgba(255,255,255,.7)}
|
||
.rack-device .d-label{flex:1;font-weight:600;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:.6rem;text-shadow:0 1px 2px rgba(0,0,0,.3);line-height:1}
|
||
.rack-device .d-spec{font-size:.45rem;color:rgba(255,255,255,.5);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:none}
|
||
.rack-view:hover .rack-device .d-spec{display:block;max-width:60px}
|
||
.rack-device .d-u-badge{font-size:.45rem;color:rgba(255,255,255,.4);background:rgba(0,0,0,.2);border-radius:2px;padding:0 .25rem;flex-shrink:0}
|
||
.rack-device .d-ip{font-size:.42rem;color:rgba(255,255,255,.35);font-family:monospace;flex-shrink:0;display:none}
|
||
.rack-device:hover .d-ip{display:inline}
|
||
|
||
/* Vendor color swatches */
|
||
.v-hpe{background:linear-gradient(135deg,#ff7b00 0%,#e06e00 100%);border-left:3px solid #ffa500}
|
||
.v-dell{background:linear-gradient(135deg,#0076a8 0%,#005a80 100%);border-left:3px solid #00a3e0}
|
||
.v-lenovo{background:linear-gradient(135deg,#e41f33 0%,#b01828 100%);border-left:3px solid #ff2d4a}
|
||
.v-cisco{background:linear-gradient(135deg,#00529a 0%,#003d73 100%);border-left:3px solid #00a0ff}
|
||
.v-juniper{background:linear-gradient(135deg,#44a833 0%,#338026 100%);border-left:3px solid #5dd64a}
|
||
.v-arista{background:linear-gradient(135deg,#c42 0%,#992211 100%);border-left:3px solid #ff4422}
|
||
.v-netapp{background:linear-gradient(135deg,#00a1df 0%,#007aaa 100%);border-left:3px solid #00ccff}
|
||
.v-pure{background:linear-gradient(135deg,#4a90d9 0%,#3670b5 100%);border-left:3px solid #6aafff}
|
||
.v-fortinet{background:linear-gradient(135deg,#ee3124 0%,#c4271c 100%);border-left:3px solid #ff4433}
|
||
.v-supermicro{background:linear-gradient(135deg,#e6a817 0%,#c48d0e 100%);border-left:3px solid #ffcc33}
|
||
.v-apc{background:linear-gradient(135deg,#6e7681 0%,#555d66 100%);border-left:3px solid #8b95a5}
|
||
.v-f5{background:linear-gradient(135deg,#d32f2f 0%,#b71c1c 100%);border-left:3px solid #e53935}
|
||
.v-paloalto{background:linear-gradient(135deg,#ed7b30 0%,#cc6220 100%);border-left:3px solid #ff8a42}
|
||
.v-nvidia{background:linear-gradient(135deg,#76b900 0%,#5c9400 100%);border-left:3px solid #8ce020}
|
||
.v-oracle{background:linear-gradient(135deg,#f80000 0%,#c50000 100%);border-left:3px solid #ff2222}
|
||
.v-ibm{background:linear-gradient(135deg,#0062ff 0%,#004acc 100%);border-left:3px solid #4488ff}
|
||
.v-other{background:linear-gradient(135deg,#555d66 0%,#3d444d 100%);border-left:3px solid #6e7681}
|
||
|
||
/* Device type icon colors */
|
||
.di-server{color:#58a6ff}
|
||
.di-switch{color:#8be9fd}
|
||
.di-storage{color:#50fa7b}
|
||
.di-firewall{color:#ff5555}
|
||
.di-router{color:#ff79c6}
|
||
.di-loadbalancer{color:#f1fa8c}
|
||
.di-san{color:#8be9fd}
|
||
.di-patch_panel{color:#6272a4}
|
||
.di-pdu{color:#ffb86c}
|
||
.di-ups{color:#f1fa8c}
|
||
.di-cooling{color:#8be9fd}
|
||
.di-tape{color:#bd93f9}
|
||
.di-kvm{color:#ff79c6}
|
||
.di-other{color:#6272a4}
|
||
|
||
/* ── Sidebar ── */
|
||
.sidebar{flex:1;min-width:0;max-width:480px}
|
||
.sidebar-card{background:var(--d-card);border:1px solid var(--d-border);border-radius:6px;padding:.75rem;margin-bottom:.75rem}
|
||
.sidebar-card h3{font-size:.75rem;font-weight:600;color:var(--d-cyan);margin-bottom:.5rem;text-transform:uppercase;letter-spacing:1px;display:flex;align-items:center;gap:.35rem}
|
||
|
||
/* Info grid */
|
||
.info-grid{display:grid;grid-template-columns:1fr 1fr;gap:.35rem}
|
||
.info-grid .ig-label{font-size:.6rem;color:var(--d-muted);text-transform:uppercase;letter-spacing:.5px}
|
||
.info-grid .ig-val{font-size:.75rem;color:var(--d-text);font-weight:500}
|
||
|
||
/* Utilization bar */
|
||
.util-bar{display:flex;height:8px;border-radius:4px;overflow:hidden;background:var(--d-dim);margin:.35rem 0}
|
||
.util-bar .ub-fill{background:var(--d-accent);transition:width .3s}
|
||
.util-bar .ub-fill.green{background:var(--d-green)}
|
||
.util-bar .ub-fill.yellow{background:var(--d-yellow)}
|
||
.util-bar .ub-fill.red{background:var(--d-red)}
|
||
|
||
/* Tabs */
|
||
.rack-tabs{display:flex;gap:2px;background:var(--d-card);border:1px solid var(--d-border);border-radius:6px;overflow:hidden;margin-bottom:.75rem}
|
||
.rack-tab{display:flex;align-items:center;justify-content:center;gap:.25rem;padding:.4rem .75rem;background:none;border:none;color:var(--d-muted);font-size:.7rem;font-weight:500;cursor:pointer;font-family:inherit;transition:all .15s;flex:1;text-align:center}
|
||
.rack-tab svg{width:14px;height:14px}
|
||
.rack-tab:hover{color:var(--d-text);background:rgba(68,71,90,.3)}
|
||
.rack-tab.active{color:var(--d-text);background:var(--d-border)}
|
||
|
||
.tab-content{display:none}
|
||
.tab-content.active{display:block}
|
||
|
||
/* Device library grid */
|
||
.lib-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:.35rem}
|
||
.lib-item{display:flex;align-items:center;gap:.35rem;background:var(--d-bg);border:1px solid var(--d-border);border-radius:4px;padding:.35rem;cursor:pointer;transition:all .12s}
|
||
.lib-item svg{width:16px;height:16px;flex-shrink:0}
|
||
.lib-item:hover{border-color:var(--d-accent);transform:translateY(-1px);box-shadow:0 2px 8px rgba(0,0,0,.2)}
|
||
.lib-item .li-name{font-size:.6rem;font-weight:600;color:var(--d-text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
|
||
.lib-item .li-detail{font-size:.5rem;color:var(--d-muted);flex-shrink:0}
|
||
|
||
/* Quick add buttons */
|
||
.qadd{display:flex;flex-wrap:wrap;gap:.25rem}
|
||
.qadd button{display:inline-flex;align-items:center;gap:.25rem;padding:.25rem .5rem;font-size:.6rem;border-radius:3px;border:none;cursor:pointer;color:#fff;font-weight:500;font-family:inherit;transition:all .12s;opacity:.85}
|
||
.qadd button svg{width:12px;height:12px}
|
||
.qadd button:hover{opacity:1;transform:translateY(-1px)}
|
||
|
||
/* Device list */
|
||
.dlist{display:flex;flex-direction:column;gap:.2rem}
|
||
.dl-item{display:flex;align-items:center;gap:.4rem;background:var(--d-bg);border:1px solid var(--d-border);border-radius:4px;padding:.35rem .5rem;font-size:.65rem;cursor:pointer;transition:all .12s}
|
||
.dl-item:hover{border-color:var(--d-muted)}
|
||
.dl-item .dl-color{width:3px;height:24px;border-radius:2px;flex-shrink:0}
|
||
.dl-item svg{width:14px;height:14px;flex-shrink:0}
|
||
.dl-item .dl-name{flex:1;color:var(--d-text);font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.dl-item .dl-model{color:var(--d-muted);font-size:.6rem;white-space:nowrap}
|
||
.dl-item .dl-u{color:var(--d-dim);font-size:.55rem}
|
||
|
||
/* View toggle */
|
||
.view-toggle{display:flex;gap:2px;margin-bottom:.5rem}
|
||
.view-toggle button{display:flex;align-items:center;gap:.25rem;padding:.25rem .5rem;font-size:.55rem;border:1px solid var(--d-border);border-radius:3px;background:var(--d-bg);color:var(--d-muted);cursor:pointer;font-family:inherit;transition:all .12s;text-transform:uppercase;letter-spacing:.5px}
|
||
.view-toggle button svg{width:12px;height:12px}
|
||
.view-toggle button.active{background:var(--d-border);color:var(--d-text);border-color:var(--d-muted)}
|
||
.view-toggle button:hover{border-color:var(--d-muted)}
|
||
|
||
/* Search */
|
||
.search-box{display:flex;gap:.35rem;margin-bottom:.5rem}
|
||
.search-box input{flex:1;padding:.3rem .5rem;font-size:.65rem;background:var(--d-bg);border:1px solid var(--d-border);border-radius:4px;color:var(--d-text);font-family:inherit}
|
||
.search-box input:focus{border-color:var(--d-accent);outline:none}
|
||
.search-box input::placeholder{color:var(--d-dim)}
|
||
.search-box svg{width:14px;height:14px;color:var(--d-muted);flex-shrink:0;margin-top:.3rem}
|
||
|
||
/* Forms in sidebar */
|
||
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:.35rem;margin-bottom:.35rem}
|
||
.form-row.full{grid-template-columns:1fr}
|
||
.form-row label{font-size:.6rem;color:var(--d-muted);display:block;margin-bottom:.15rem;text-transform:uppercase;letter-spacing:.5px}
|
||
.form-row input,.form-row select,.form-row textarea{width:100%;padding:.3rem .4rem;font-size:.65rem;background:var(--d-bg);border:1px solid var(--d-border);border-radius:3px;color:var(--d-text);font-family:inherit;box-sizing:border-box}
|
||
.form-row input:focus,.form-row select:focus,.form-row textarea:focus{border-color:var(--d-accent);outline:none}
|
||
.form-row select option{background:var(--d-bg);color:var(--d-text)}
|
||
|
||
/* Buttons */
|
||
.btn-rack{display:inline-flex;align-items:center;justify-content:center;gap:.3rem;padding:.35rem .65rem;font-size:.65rem;font-weight:600;border-radius:4px;border:none;cursor:pointer;font-family:inherit;transition:all .12s;text-decoration:none}
|
||
.btn-rack svg{width:14px;height:14px}
|
||
.btn-rack.primary{background:var(--d-accent);color:#000}
|
||
.btn-rack.primary:hover{filter:brightness(1.15)}
|
||
.btn-rack.secondary{background:var(--d-border);color:var(--d-text)}
|
||
.btn-rack.secondary:hover{background:var(--d-muted)}
|
||
.btn-rack.danger{background:var(--d-red);color:#fff}
|
||
.btn-rack.danger:hover{filter:brightness(1.15)}
|
||
.btn-rack.ghost{background:transparent;color:var(--d-muted);border:1px solid var(--d-border)}
|
||
.btn-rack.ghost:hover{border-color:var(--d-muted);color:var(--d-text)}
|
||
.btn-rack.small{padding:.2rem .4rem;font-size:.55rem}
|
||
.btn-rack.full{width:100%;justify-content:center}
|
||
.btn-rack:disabled{opacity:.4;cursor:not-allowed}
|
||
|
||
/* Modal */
|
||
.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);z-index:200;display:none;align-items:center;justify-content:center;backdrop-filter:blur(2px)}
|
||
.modal-overlay.open{display:flex}
|
||
.modal-box{background:var(--d-card);border:1px solid var(--d-border);border-radius:8px;width:90%;max-width:520px;max-height:80vh;overflow-y:auto;box-shadow:0 16px 48px rgba(0,0,0,.5)}
|
||
.modal-head{display:flex;align-items:center;justify-content:space-between;padding:.65rem .75rem;border-bottom:1px solid var(--d-border)}
|
||
.modal-head h3{font-size:.8rem;font-weight:600;color:var(--d-text);display:flex;align-items:center;gap:.35rem}
|
||
.modal-head h3 svg{width:18px;height:18px}
|
||
.modal-close{background:none;border:none;color:var(--d-muted);font-size:1.1rem;cursor:pointer;padding:0 .25rem;font-family:inherit}
|
||
.modal-close:hover{color:var(--d-text)}
|
||
.modal-body{padding:.65rem .75rem}
|
||
.modal-foot{display:flex;gap:.35rem;justify-content:flex-end;padding:.5rem .75rem;border-top:1px solid var(--d-border)}
|
||
.modal-body .dev-avatar{width:48px;height:48px;border-radius:8px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
|
||
.modal-body .dev-avatar svg{width:24px;height:24px;color:#fff}
|
||
|
||
/* Empty state */
|
||
.empty-state{text-align:center;padding:1.5rem .5rem;color:var(--d-muted);font-size:.7rem}
|
||
.empty-state svg{width:32px;height:32px;opacity:.3;display:block;margin:0 auto .5rem auto}
|
||
|
||
/* Status badges */
|
||
.badge{display:inline-block;padding:.1rem .35rem;border-radius:3px;font-size:.5rem;font-weight:600;text-transform:uppercase;letter-spacing:.5px}
|
||
.badge.active{background:rgba(80,250,123,.15);color:var(--d-green)}
|
||
.badge.standby{background:rgba(255,184,108,.15);color:var(--d-orange)}
|
||
.badge.decommissioned{background:rgba(255,85,85,.15);color:var(--d-red)}
|
||
|
||
/* Move hint */
|
||
.move-hint{font-size:.5rem;color:var(--d-muted);text-align:center;padding:.15rem 0;border-bottom:1px solid var(--d-dim);display:flex;align-items:center;justify-content:center;gap:.25rem}
|
||
.move-hint svg{width:10px;height:10px}
|
||
</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="container">
|
||
|
||
<!-- Page header -->
|
||
<a href="javascript:history.back()" class="back-link"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg> Terug</a>
|
||
<div class="page-header">
|
||
<div>
|
||
<a href="/racks" class="back-link" style="color:var(--d-cyan);display:flex;align-items:center;gap:.2rem"><i data-lucide="arrow-left" style="width:12px;height:12px"></i> Alle racks</a>
|
||
<h1 style="color:var(--d-text)"><%= rack.name %></h1>
|
||
<p style="color:var(--d-muted);font-size:.8rem"><%= rack.client_name %> — <%= rack.location || rack.datacenter || 'Locatie onbekend' %></p>
|
||
</div>
|
||
<div class="header-actions">
|
||
<a href="/racks/<%= rack.id %>/edit" class="btn-rack secondary"><i data-lucide="settings"></i> Bewerk rack</a>
|
||
<a href="/racks/<%= rack.id %>/flow/new" class="btn-rack primary"><i data-lucide="git-branch"></i> Applicatie Flow</a>
|
||
<a href="/racks/<%= rack.id %>/landscape/new" class="btn-rack primary"><i data-lucide="layers"></i> Landschap</a>
|
||
<a href="/racks/<%= rack.id %>/report" class="btn-rack ghost"><i data-lucide="file-text"></i> Rapport</a>
|
||
<button class="btn-rack ghost" onclick="exportSVG()"><i data-lucide="download"></i> Export</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="rack-layout">
|
||
|
||
<!-- Rack View -->
|
||
<div class="rack-col">
|
||
<div class="rack-view" id="rackView">
|
||
<div class="rack-top"><i data-lucide="server" style="width:12px;height:12px;vertical-align:middle;margin-right:.3rem"></i><%= rack.name %> · <span id="usedCount"><%= devices.reduce((s,d)=>s+d.height_u,0) %></span>/<%= rack.total_units %>U</div>
|
||
<div class="view-toggle" style="justify-content:center;padding:.3rem 0;border-bottom:1px solid var(--d-dim)">
|
||
<button class="active" data-view="front" onclick="switchView('front')"><i data-lucide="monitor"></i> Front</button>
|
||
<button data-view="rear" onclick="switchView('rear')"><i data-lucide="monitor"></i> Rear</button>
|
||
</div>
|
||
<div class="rack-body" id="rackBody">
|
||
<div class="move-hint"><i data-lucide="move"></i> Drag devices to reposition</div>
|
||
<%
|
||
var units = [];
|
||
devices.forEach(function(d) { for(var u = d.position_u; u < d.position_u + d.height_u; u++) units[u] = d; });
|
||
var _html = '';
|
||
for(var u = rack.total_units; u >= 1; u--) {
|
||
var dev = units[u];
|
||
if (dev && u === dev.position_u) {
|
||
var vc = 'v-other', vb = '#555d66';
|
||
var vm = (dev.manufacturer||'').toLowerCase();
|
||
if(vm.includes('hpe')||vm.includes('hewlett')){vc='v-hpe';vb='#ff7b00'}
|
||
else if(vm.includes('dell')){vc='v-dell';vb='#0076a8'}
|
||
else if(vm.includes('lenovo')){vc='v-lenovo';vb='#e41f33'}
|
||
else if(vm.includes('cisco')){vc='v-cisco';vb='#00529a'}
|
||
else if(vm.includes('juniper')){vc='v-juniper';vb='#44a833'}
|
||
else if(vm.includes('arista')){vc='v-arista';vb='#cc3333'}
|
||
else if(vm.includes('netapp')){vc='v-netapp';vb='#00a1df'}
|
||
else if(vm.includes('pure')){vc='v-pure';vb='#4a90d9'}
|
||
else if(vm.includes('fortinet')){vc='v-fortinet';vb='#ee3124'}
|
||
else if(vm.includes('super')||vm.includes('supermicro')){vc='v-supermicro';vb='#e6a817'}
|
||
else if(vm.includes('apc')){vc='v-apc';vb='#6e7681'}
|
||
else if(vm.includes('f5')){vc='v-f5';vb='#d32f2f'}
|
||
else if(vm.includes('palo')){vc='v-paloalto';vb='#ed7b30'}
|
||
else if(vm.includes('nvidia')){vc='v-nvidia';vb='#76b900'}
|
||
else if(vm.includes('oracle')){vc='v-oracle';vb='#f80000'}
|
||
else if(vm.includes('ibm')){vc='v-ibm';vb='#0062ff'}
|
||
else if(dev.device_type==='patch_panel'||dev.device_type==='pdu'||dev.device_type==='ups'){vc='v-apc';vb='#6e7681'}
|
||
var dtIcon='server';
|
||
if(dev.device_type==='switch') dtIcon='git-branch';
|
||
else if(dev.device_type==='storage') dtIcon='database';
|
||
else if(dev.device_type==='firewall') dtIcon='shield';
|
||
else if(dev.device_type==='router') dtIcon='radio';
|
||
else if(dev.device_type==='loadbalancer') dtIcon='scale';
|
||
else if(dev.device_type==='san') dtIcon='hard-drive';
|
||
else if(dev.device_type==='patch_panel') dtIcon='cable';
|
||
else if(dev.device_type==='pdu') dtIcon='zap';
|
||
else if(dev.device_type==='ups') dtIcon='battery-charging';
|
||
else if(dev.device_type==='cooling') dtIcon='fan';
|
||
else if(dev.device_type==='tape') dtIcon='archive';
|
||
else if(dev.device_type==='kvm') dtIcon='monitor';
|
||
else if(dev.device_type==='other') dtIcon='box';
|
||
var topx = (rack.total_units - dev.position_u - dev.height_u + 1) * 30 + 20;
|
||
var hx = dev.height_u * 30 - 2;
|
||
_html += '<div class="rack-device ' + vc + '" id="dev-' + dev.id + '" style="top:' + topx + 'px;height:' + hx + 'px" data-id="' + dev.id + '" data-u="' + dev.position_u + '" data-h="' + dev.height_u + '" onclick="openModal(' + dev.id + ')" draggable="true" title="' + dev.name.replace(/"/g,'"') + ' · ' + (dev.model||dev.manufacturer||dev.device_type).replace(/"/g,'"') + ' · ' + (dev.mgmt_ip||'').replace(/"/g,'"') + '">';
|
||
_html += '<span class="d-lu di-' + dev.device_type + '"><i data-lucide="' + dtIcon + '"></i></span>';
|
||
_html += '<span class="d-label">' + dev.name + '</span>';
|
||
_html += '<span class="d-spec">' + (dev.model||dev.manufacturer||dev.device_type) + '</span>';
|
||
if (dev.mgmt_ip) _html += '<span class="d-ip">' + dev.mgmt_ip + '</span>';
|
||
_html += '<span class="d-u-badge">' + dev.height_u + 'U</span>';
|
||
_html += '</div>';
|
||
} else if (!dev) {
|
||
_html += '<div class="rack-u empty" data-u="' + u + '"><span class="u-num">' + u + '</span><div class="u-mid"></div></div>';
|
||
}
|
||
}
|
||
%>
|
||
<%- _html %>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sidebar -->
|
||
<div class="sidebar">
|
||
|
||
<!-- Rack Info -->
|
||
<div class="sidebar-card">
|
||
<h3><i data-lucide="bar-chart-3"></i> Rack Status</h3>
|
||
<% var used = devices.reduce((s,d)=>s+d.height_u,0); var pct = Math.round(used/rack.total_units*100); %>
|
||
<div class="util-bar">
|
||
<div class="ub-fill <%= pct>80?'red':pct>60?'yellow':'green' %>" style="width:<%= pct %>%"></div>
|
||
</div>
|
||
<div class="info-grid">
|
||
<div><div class="ig-label">Bezetting</div><div class="ig-val"><%= used %> / <%= rack.total_units %>U (<%= pct %>%)</div></div>
|
||
<div><div class="ig-label">Apparaten</div><div class="ig-val"><%= devices.length %></div></div>
|
||
<div><div class="ig-label">Vrij</div><div class="ig-val"><%= rack.total_units - used %>U</div></div>
|
||
<div><div class="ig-label">Datacenter</div><div class="ig-val"><%= rack.datacenter || '-' %></div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Gekoppelde flows -->
|
||
<div class="sidebar-card">
|
||
<h3><i data-lucide="git-branch"></i> Flows & Landschap</h3>
|
||
<% if (typeof linkedDiagrams !== 'undefined' && linkedDiagrams && linkedDiagrams.length) { %>
|
||
<% linkedDiagrams.forEach(function(d){ %>
|
||
<a href="/architecture/<%= d.id %>" style="display:block;padding:.35rem 0;border-bottom:1px solid var(--d-dim);text-decoration:none;color:var(--d-text);font-size:.7rem">
|
||
<strong><%= d.name %></strong><br>
|
||
<span style="color:var(--d-muted);font-size:.62rem"><%= (d.diagram_type||'').replace(/_/g,' ') %></span>
|
||
</a>
|
||
<% }) %>
|
||
<% } else { %>
|
||
<p style="font-size:.65rem;color:var(--d-muted);margin:0 0 .5rem">Nog geen flows gekoppeld.</p>
|
||
<% } %>
|
||
<div style="display:flex;flex-direction:column;gap:.3rem;margin-top:.5rem">
|
||
<a href="/racks/<%= rack.id %>/flow/new" class="btn-rack primary" style="justify-content:center;font-size:.65rem;padding:.35rem"><i data-lucide="plus"></i> Applicatie Flow</a>
|
||
<a href="/racks/<%= rack.id %>/landscape/new" class="btn-rack secondary" style="justify-content:center;font-size:.65rem;padding:.35rem"><i data-lucide="layers"></i> Applicatie Landschap</a>
|
||
<a href="/racks/<%= rack.id %>/report" class="btn-rack ghost" style="justify-content:center;font-size:.65rem;padding:.35rem"><i data-lucide="file-text"></i> Gecombineerd Rapport</a>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tabs -->
|
||
<div class="rack-tabs">
|
||
<button class="rack-tab active" data-tab="add"><i data-lucide="plus-circle"></i> Add</button>
|
||
<button class="rack-tab" data-tab="library"><i data-lucide="book-open"></i> Library</button>
|
||
<button class="rack-tab" data-tab="list"><i data-lucide="list"></i> List (<%= devices.length %>)</button>
|
||
</div>
|
||
|
||
<!-- ── Tab: Add Device ── -->
|
||
<div class="tab-content active" id="tab-add">
|
||
<div class="sidebar-card" style="margin-bottom:0">
|
||
<h3><i data-lucide="zap"></i> Quick Add</h3>
|
||
<div class="qadd">
|
||
<button onclick="quickAdd('HPE','ProLiant DL380 Gen11','server',2)" style="background:#ff7b00"><i data-lucide="server"></i> HPE Server</button>
|
||
<button onclick="quickAdd('Dell','PowerEdge R760','server',2)" style="background:#0076a8"><i data-lucide="server"></i> Dell Server</button>
|
||
<button onclick="quickAdd('Lenovo','ThinkSystem SR650','server',2)" style="background:#e41f33"><i data-lucide="server"></i> Lenovo Server</button>
|
||
<button onclick="quickAdd('Cisco','Catalyst 9300','switch',1)" style="background:#00529a"><i data-lucide="git-branch"></i> Cisco Switch</button>
|
||
<button onclick="quickAdd('Cisco','Nexus 93180YC','switch',1)" style="background:#00529a"><i data-lucide="git-branch"></i> Nexus</button>
|
||
<button onclick="quickAdd('Juniper','QFX5120','switch',1)" style="background:#44a833"><i data-lucide="git-branch"></i> Juniper QFX</button>
|
||
<button onclick="quickAdd('Arista','7050X3','switch',1)" style="background:#cc3333"><i data-lucide="git-branch"></i> Arista 7050</button>
|
||
<button onclick="quickAdd('NetApp','AFF A250','storage',2)" style="background:#00a1df"><i data-lucide="database"></i> NetApp AFF</button>
|
||
<button onclick="quickAdd('Pure','FlashArray//X','storage',3)" style="background:#4a90d9"><i data-lucide="database"></i> Pure FlashArray</button>
|
||
<button onclick="quickAdd('APC','Smart-UPS 1500','ups',2)" style="background:#6e7681"><i data-lucide="battery-charging"></i> APC UPS</button>
|
||
<button onclick="quickAdd('','Patch Panel 48-port','patch_panel',1)" style="background:#555d66"><i data-lucide="cable"></i> Patch Panel</button>
|
||
<button onclick="quickAdd('APC','PDU 5.7kWh','pdu',1)" style="background:#6e7681"><i data-lucide="zap"></i> PDU</button>
|
||
<button onclick="quickAdd('Cisco','UCS C240 M7','server',2)" style="background:#00529a"><i data-lucide="server"></i> Cisco UCS</button>
|
||
<button onclick="quickAdd('Supermicro','SYS-420GP','server',4)" style="background:#e6a817"><i data-lucide="server"></i> SuperMicro</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sidebar-card">
|
||
<h3><i data-lucide="edit-3"></i> Custom Device</h3>
|
||
<form action="/racks/<%= rack.id %>/devices" method="POST" id="addForm">
|
||
<div class="form-row">
|
||
<div><label>Name *</label><input type="text" name="name" id="fName" required placeholder="WEB-01"></div>
|
||
<div class="field"><label>Type</label><select name="device_type" id="fType">
|
||
<option value="server">Server</option><option value="switch">Switch</option><option value="storage">Storage</option>
|
||
<option value="firewall">Firewall</option><option value="router">Router</option><option value="loadbalancer">Load Balancer</option>
|
||
<option value="san">SAN Switch</option><option value="patch_panel">Patch Panel</option>
|
||
<option value="pdu">PDU</option><option value="ups">UPS</option><option value="cooling">Koeling</option>
|
||
<option value="tape">Tape Library</option><option value="kvm">KVM</option><option value="other">Other</option>
|
||
</select></div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div><label>Vendor</label><select name="manufacturer" id="fVendor">
|
||
<option value="">— Vendor —</option>
|
||
<% ['HPE','Dell','Lenovo','Cisco','Juniper','Arista','NetApp','Pure Storage','Fortinet','F5','Palo Alto','Supermicro','NVIDIA','APC','Oracle','IBM','Other'].forEach(function(v){ %>
|
||
<option value="<%= v %>"><%= v %></option>
|
||
<% }) %>
|
||
</select></div>
|
||
<div><label>Model</label><input type="text" name="model" id="fModel" placeholder="ProLiant DL380 Gen11"></div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div><label>Positie (U) *</label><input type="number" name="position_u" id="fPos" required min="1" max="<%= rack.total_units %>" placeholder="1-42"></div>
|
||
<div><label>Hoogte (U)</label><input type="number" name="height_u" id="fHeight" value="1" min="1" max="20"></div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div><label>Mgmt IP</label><input type="text" name="mgmt_ip" id="fIp" placeholder="10.0.0.10"></div>
|
||
<div><label>Serial / Asset</label><input type="text" name="serial" id="fSerial" placeholder="SN1234567"></div>
|
||
</div>
|
||
<div class="form-row full"><label>Specs</label><input type="text" name="specs" id="fSpecs" placeholder="CPU: Xeon 16C, RAM: 256GB, NVMe 4x1.92TB"></div>
|
||
<div class="form-row full"><label>Notities</label><textarea name="notes" id="fNotes" rows="1"></textarea></div>
|
||
<button type="submit" class="btn-rack primary full" style="margin-top:.35rem"><i data-lucide="plus"></i> Device Toevoegen</button>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Tab: Library ── -->
|
||
<div class="tab-content" id="tab-library">
|
||
<div class="sidebar-card">
|
||
<h3><i data-lucide="search"></i> Device Library</h3>
|
||
<div class="search-box">
|
||
<i data-lucide="search"></i>
|
||
<input type="text" id="libSearch" placeholder="Search library..." oninput="filterLib(this.value)">
|
||
</div>
|
||
<div class="lib-filters" style="display:flex;gap:.35rem;margin-bottom:.5rem;flex-wrap:wrap">
|
||
<select id="libVendorFilter" onchange="filterLib(document.getElementById('libSearch').value)" style="flex:1;min-width:80px;background:var(--d-bg);border:1px solid var(--d-border);border-radius:4px;padding:.2rem .3rem;color:var(--d-text);font-size:.7rem">
|
||
<option value="">Alle Vendors</option>
|
||
<% var vendors = [...new Set(hardwareLibrary.map(function(h){return h.vendor}))].sort(); %>
|
||
<% vendors.forEach(function(v){ %>
|
||
<option value="<%= v %>"><%= v %></option>
|
||
<% }) %>
|
||
</select>
|
||
<select id="libTypeFilter" onchange="filterLib(document.getElementById('libSearch').value)" style="flex:1;min-width:70px;background:var(--d-bg);border:1px solid var(--d-border);border-radius:4px;padding:.2rem .3rem;color:var(--d-text);font-size:.7rem">
|
||
<option value="">Alle Types</option>
|
||
<option value="server">Server</option>
|
||
<option value="storage">Storage</option>
|
||
<option value="switch">Switch</option>
|
||
<option value="firewall">Firewall</option>
|
||
<option value="router">Router</option>
|
||
<option value="loadbalancer">Load Balancer</option>
|
||
<option value="san">SAN</option>
|
||
<option value="ups">UPS</option>
|
||
<option value="pdu">PDU</option>
|
||
<option value="tape">Tape</option>
|
||
<option value="patch_panel">Patch Panel</option>
|
||
<option value="cooling">Koeling</option>
|
||
<option value="kvm">KVM</option>
|
||
<option value="other">Other</option>
|
||
</select>
|
||
</div>
|
||
<div class="lib-grid" id="libGrid">
|
||
<% var iconMap = {server:'server',switch:'git-branch',storage:'database',firewall:'shield',router:'radio',loadbalancer:'scale',san:'hard-drive',patch_panel:'cable',pdu:'zap',ups:'battery-charging',cooling:'fan',tape:'archive',kvm:'monitor',other:'box'}; %>
|
||
<% hardwareLibrary.forEach(function(h){ %>
|
||
<% var li = iconMap[h.type]||'box'; %>
|
||
<div class="lib-item" data-vendor="<%= h.vendor %>" data-type="<%= h.type %>" onclick="quickAdd('<%= h.vendor %>','<%= h.model.replace(/'/g,"\\'") %>','<%= h.type %>',<%= h.height %>)">
|
||
<i data-lucide="<%= li %>" class="di-<%= h.type %>"></i>
|
||
<div class="li-name"><%= h.vendor.split(' ')[0] %> <%= h.model %></div>
|
||
<div class="li-detail"><%= h.height > 0 ? h.height + 'U' : '0U' %></div>
|
||
</div>
|
||
<% }) %>
|
||
<div class="lib-item" onclick="quickAdd('','Patch Panel 48-port','patch_panel',1)" data-vendor="" data-type="patch_panel"><i data-lucide="cable" class="di-patch_panel"></i><div class="li-name">Patch Panel 48</div><div class="li-detail">1U</div></div>
|
||
<div class="lib-item" onclick="quickAdd('','Generic Server','server',1)" data-vendor="" data-type="server"><i data-lucide="server" class="di-server"></i><div class="li-name">Generic Server</div><div class="li-detail">1U</div></div>
|
||
<div class="lib-item" onclick="quickAdd('','Generic Switch','switch',1)" data-vendor="" data-type="switch"><i data-lucide="git-branch" class="di-switch"></i><div class="li-name">Generic Switch</div><div class="li-detail">1U</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Tab: Device List ── -->
|
||
<div class="tab-content" id="tab-list">
|
||
<div class="sidebar-card" style="margin-bottom:0">
|
||
<h3><i data-lucide="list"></i> Device List</h3>
|
||
<div class="search-box">
|
||
<i data-lucide="search"></i>
|
||
<input type="text" placeholder="Search devices..." oninput="filterDevices(this.value)">
|
||
</div>
|
||
<% if (devices.length === 0) { %>
|
||
<div class="empty-state"><i data-lucide="package-open"></i>Geen devices in deze rack.</div>
|
||
<% } else { %>
|
||
<div class="dlist" id="deviceList">
|
||
<% devices.sort((a,b)=>b.position_u-a.position_u).forEach(function(d) {
|
||
var vc='#555d66';
|
||
var vm=(d.manufacturer||'').toLowerCase();
|
||
if(vm.includes('hpe')) vc='#ff7b00'; else if(vm.includes('dell')) vc='#0076a8';
|
||
else if(vm.includes('lenovo')) vc='#e41f33'; else if(vm.includes('cisco')) vc='#00529a';
|
||
else if(vm.includes('juniper')) vc='#44a833'; else if(vm.includes('arista')) vc='#cc3333';
|
||
else if(vm.includes('netapp')) vc='#00a1df'; else if(vm.includes('pure')) vc='#4a90d9';
|
||
else if(vm.includes('fortinet')) vc='#ee3124'; else if(vm.includes('apc')) vc='#6e7681';
|
||
else if(vm.includes('f5')) vc='#d32f2f'; else if(vm.includes('palo')) vc='#ed7b30';
|
||
else if(vm.includes('nvidia')) vc='#76b900'; else if(vm.includes('oracle')) vc='#f80000';
|
||
else if(vm.includes('ibm')) vc='#0062ff';
|
||
var di='server';
|
||
if(d.device_type==='switch') di='git-branch'; else if(d.device_type==='storage') di='database';
|
||
else if(d.device_type==='firewall') di='shield'; else if(d.device_type==='router') di='radio';
|
||
else if(d.device_type==='loadbalancer') di='scale'; else if(d.device_type==='san') di='hard-drive';
|
||
else if(d.device_type==='patch_panel') di='cable'; else if(d.device_type==='pdu') di='zap';
|
||
else if(d.device_type==='ups') di='battery-charging'; else if(d.device_type==='cooling') di='fan';
|
||
else if(d.device_type==='tape') di='archive'; else if(d.device_type==='kvm') di='monitor';
|
||
else if(d.device_type==='other') di='box';
|
||
%>
|
||
<div class="dl-item" onclick="openModal(<%= d.id %>)">
|
||
<div class="dl-color" style="background:<%= vc %>"></div>
|
||
<i data-lucide="<%= di %>" class="di-<%= d.device_type %>"></i>
|
||
<div class="dl-name"><%= d.name %></div>
|
||
<div class="dl-model"><%= d.model || d.manufacturer || '' %></div>
|
||
<span class="badge <%= d.status||'active' %>"><%= d.status||'active' %></span>
|
||
<div class="dl-u"><%= d.position_u %>U</div>
|
||
</div>
|
||
<% }) %>
|
||
</div>
|
||
<% } %>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /sidebar -->
|
||
</div><!-- /rack-layout -->
|
||
|
||
</div><!-- /container -->
|
||
|
||
<!-- ── Device Modal ── -->
|
||
<% devices.forEach(function(d) { %>
|
||
<% var vm=(d.manufacturer||'').toLowerCase(); %>
|
||
<% var vb='#555d66'; %>
|
||
<% if(vm.includes('hpe')) vb='#ff7b00'; else if(vm.includes('dell')) vb='#0076a8';
|
||
else if(vm.includes('lenovo')) vb='#e41f33'; else if(vm.includes('cisco')) vb='#00529a';
|
||
else if(vm.includes('juniper')) vb='#44a833'; else if(vm.includes('arista')) vb='#cc3333';
|
||
else if(vm.includes('netapp')) vb='#00a1df'; else if(vm.includes('pure')) vb='#4a90d9';
|
||
else if(vm.includes('fortinet')) vb='#ee3124'; else if(vm.includes('apc')) vb='#6e7681'; %>
|
||
<% var di='server'; %>
|
||
<% if(d.device_type==='switch') di='git-branch'; else if(d.device_type==='storage') di='database';
|
||
else if(d.device_type==='firewall') di='shield'; else if(d.device_type==='router') di='radio';
|
||
else if(d.device_type==='loadbalancer') di='scale'; else if(d.device_type==='san') di='hard-drive';
|
||
else if(d.device_type==='patch_panel') di='cable'; else if(d.device_type==='pdu') di='zap';
|
||
else if(d.device_type==='ups') di='battery-charging'; else if(d.device_type==='cooling') di='fan';
|
||
else if(d.device_type==='tape') di='archive'; else if(d.device_type==='kvm') di='monitor';
|
||
else if(d.device_type==='other') di='box'; %>
|
||
<div class="modal-overlay" id="modal-<%= d.id %>">
|
||
<div class="modal-box">
|
||
<div class="modal-head">
|
||
<h3><i data-lucide="<%= di %>" class="di-<%= d.device_type %>"></i><%= d.name %></h3>
|
||
<button class="modal-close" onclick="closeModal(<%= d.id %>)">×</button>
|
||
</div>
|
||
<form action="/racks/devices/<%= d.id %>/edit" method="POST">
|
||
<div class="modal-body">
|
||
<div style="display:flex;gap:.75rem;margin-bottom:.65rem">
|
||
<div class="dev-avatar" style="background:<%= vb %>"><i data-lucide="<%= di %>"></i></div>
|
||
<div style="flex:1">
|
||
<div style="font-size:.85rem;font-weight:600;color:var(--d-text)"><%= d.name %></div>
|
||
<div style="font-size:.65rem;color:var(--d-muted)"><%= d.manufacturer||'' %> <%= d.model||'' %> · <%= d.device_type %></div>
|
||
<div style="margin-top:.2rem"><span class="badge <%= d.status||'active' %>"><%= d.status||'active' %></span></div>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div><label>Naam</label><input type="text" name="name" value="<%= d.name %>"></div>
|
||
<div><label>Type</label><select name="device_type">
|
||
<option value="server" <%= d.device_type==='server'?'selected':'' %>>Server</option>
|
||
<option value="switch" <%= d.device_type==='switch'?'selected':'' %>>Switch</option>
|
||
<option value="storage" <%= d.device_type==='storage'?'selected':'' %>>Storage</option>
|
||
<option value="firewall" <%= d.device_type==='firewall'?'selected':'' %>>Firewall</option>
|
||
<option value="router" <%= d.device_type==='router'?'selected':'' %>>Router</option>
|
||
<option value="loadbalancer" <%= d.device_type==='loadbalancer'?'selected':'' %>>Load Balancer</option>
|
||
<option value="san" <%= d.device_type==='san'?'selected':'' %>>SAN Switch</option>
|
||
<option value="patch_panel" <%= d.device_type==='patch_panel'?'selected':'' %>>Patch Panel</option>
|
||
<option value="pdu" <%= d.device_type==='pdu'?'selected':'' %>>PDU</option>
|
||
<option value="ups" <%= d.device_type==='ups'?'selected':'' %>>UPS</option>
|
||
<option value="cooling" <%= d.device_type==='cooling'?'selected':'' %>>Koeling</option>
|
||
<option value="tape" <%= d.device_type==='tape'?'selected':'' %>>Tape Library</option>
|
||
<option value="kvm" <%= d.device_type==='kvm'?'selected':'' %>>KVM</option>
|
||
<option value="other" <%= d.device_type==='other'?'selected':'' %>>Other</option>
|
||
</select></div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div><label>Vendor</label><select name="manufacturer">
|
||
<option value="">— Vendor —</option>
|
||
<% ['HPE','Dell','Lenovo','Cisco','Juniper','Arista','NetApp','Pure Storage','Fortinet','F5','Palo Alto','Supermicro','NVIDIA','APC','Oracle','IBM','Other'].forEach(function(v){ %>
|
||
<option value="<%= v %>" <%= d.manufacturer===v?'selected':'' %>><%= v %></option>
|
||
<% }) %>
|
||
</select></div>
|
||
<div><label>Model</label><input type="text" name="model" value="<%= d.model||'' %>"></div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div><label>Positie (U)</label><input type="number" name="position_u" value="<%= d.position_u %>" min="1" max="<%= rack.total_units %>"></div>
|
||
<div><label>Hoogte (U)</label><input type="number" name="height_u" value="<%= d.height_u %>" min="1" max="20"></div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div><label>Mgmt IP</label><input type="text" name="mgmt_ip" value="<%= d.mgmt_ip||'' %>"></div>
|
||
<div><label>Serial / Asset</label><input type="text" name="serial" value="<%= d.serial||'' %>" placeholder="SN..."></div>
|
||
</div>
|
||
<div class="form-row"><div><label>Status</label><select name="status">
|
||
<option value="active" <%= d.status==='active'?'selected':'' %>>Active</option>
|
||
<option value="standby" <%= d.status==='standby'?'selected':'' %>>Standby</option>
|
||
<option value="decommissioned" <%= d.status==='decommissioned'?'selected':'' %>>Decommissioned</option>
|
||
</select></div></div>
|
||
<div class="form-row full"><label>Specs</label><input type="text" name="specs" value="<%= d.specs||'' %>" placeholder="CPU: Xeon, RAM: 256GB..."></div>
|
||
<div class="form-row full"><label>Notities</label><textarea name="notes" rows="2"><%= d.notes||'' %></textarea></div>
|
||
</div>
|
||
<div class="modal-foot">
|
||
<button type="button" class="btn-rack secondary small" onclick="closeModal(<%= d.id %>)">Annuleren</button>
|
||
<button type="submit" class="btn-rack primary small"><i data-lucide="save"></i> Opslaan</button>
|
||
<button type="button" class="btn-rack danger small" onclick="if(confirm('Device verwijderen?')){var f=document.createElement('form');f.method='POST';f.action='/racks/devices/<%= d.id %>/delete';document.body.appendChild(f);f.submit()}"><i data-lucide="trash-2"></i> Verwijder</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
<% }); %>
|
||
|
||
<script>
|
||
// ── Initialize Lucide Icons ──
|
||
lucide.createIcons();
|
||
|
||
// ── Tab switching ──
|
||
document.querySelectorAll('.rack-tab').forEach(function(b){
|
||
b.addEventListener('click',function(){
|
||
document.querySelectorAll('.rack-tab').forEach(function(x){x.classList.remove('active')});
|
||
document.querySelectorAll('.tab-content').forEach(function(x){x.classList.remove('active')});
|
||
this.classList.add('active');
|
||
document.getElementById('tab-'+this.dataset.tab).classList.add('active');
|
||
lucide.createIcons();
|
||
});
|
||
});
|
||
var hash=window.location.hash.replace('#','');
|
||
if(hash&&document.getElementById('tab-'+hash)){
|
||
var tb=document.querySelector('.rack-tab[data-tab="'+hash+'"]');
|
||
if(tb) tb.click();
|
||
}
|
||
|
||
// ── View switch (front/rear) ──
|
||
function switchView(v){
|
||
document.querySelectorAll('.view-toggle button').forEach(function(x){x.classList.remove('active')});
|
||
document.querySelector('.view-toggle button[data-view="'+v+'"]').classList.add('active');
|
||
if(v==='rear'){
|
||
document.querySelectorAll('.rack-device').forEach(function(d){d.style.opacity='.7'});
|
||
} else {
|
||
document.querySelectorAll('.rack-device').forEach(function(d){d.style.opacity='1'});
|
||
}
|
||
}
|
||
|
||
// ── Quick Add ──
|
||
function quickAdd(vendor, model, type, height) {
|
||
document.getElementById('fName').value = vendor ? vendor.split(' ')[0] + '-' + model.split(' ').pop() : model.split(' ')[0];
|
||
document.getElementById('fType').value = type;
|
||
document.getElementById('fVendor').value = vendor;
|
||
document.getElementById('fModel').value = model;
|
||
document.getElementById('fHeight').value = height;
|
||
document.getElementById('fPos').focus();
|
||
var tb=document.querySelector('.rack-tab[data-tab="add"]');
|
||
if(tb) tb.click();
|
||
}
|
||
|
||
// ── Filter devices list ──
|
||
function filterDevices(q){
|
||
q=q.toLowerCase();
|
||
document.querySelectorAll('.dl-item').forEach(function(el){
|
||
el.style.display=el.textContent.toLowerCase().includes(q)?'flex':'none';
|
||
});
|
||
}
|
||
|
||
// ── Filter library ──
|
||
function filterLib(q){
|
||
q=q.toLowerCase();
|
||
var vf=document.getElementById('libVendorFilter').value;
|
||
var tf=document.getElementById('libTypeFilter').value;
|
||
document.querySelectorAll('.lib-item').forEach(function(el){
|
||
var match=true;
|
||
if(q && !el.textContent.toLowerCase().includes(q)) match=false;
|
||
if(vf && el.getAttribute('data-vendor')!==vf) match=false;
|
||
if(tf && el.getAttribute('data-type')!==tf) match=false;
|
||
el.style.display=match?'flex':'none';
|
||
});
|
||
}
|
||
|
||
// ── Modal ──
|
||
function openModal(id){
|
||
var m=document.getElementById('modal-'+id);
|
||
if(m) m.classList.add('open');
|
||
}
|
||
function closeModal(id){
|
||
var m=document.getElementById('modal-'+id);
|
||
if(m) m.classList.remove('open');
|
||
}
|
||
document.querySelectorAll('.modal-overlay').forEach(function(m){
|
||
m.addEventListener('click',function(e){
|
||
if(e.target===this) this.classList.remove('open');
|
||
});
|
||
});
|
||
|
||
// ── Drag & Drop ──
|
||
var dragEl=null, dragOrigU=0;
|
||
document.querySelectorAll('.rack-device').forEach(function(el){
|
||
el.addEventListener('dragstart',function(e){
|
||
dragEl=this; dragOrigU=parseInt(this.dataset.u);
|
||
this.classList.add('dragging');
|
||
e.dataTransfer.effectAllowed='move';
|
||
e.dataTransfer.setData('text/plain',this.dataset.id);
|
||
});
|
||
el.addEventListener('dragend',function(){this.classList.remove('dragging');dragEl=null;});
|
||
});
|
||
document.querySelectorAll('.rack-u.empty').forEach(function(el){
|
||
el.addEventListener('dragover',function(e){e.preventDefault();this.style.background='rgba(255,121,198,.1)'});
|
||
el.addEventListener('dragleave',function(){this.style.background=''});
|
||
el.addEventListener('drop',function(e){
|
||
e.preventDefault(); this.style.background='';
|
||
var targetU=parseInt(this.dataset.u), devId=dragEl.dataset.id, devH=parseInt(dragEl.dataset.h);
|
||
var newPos=targetU;
|
||
if(newPos<1) newPos=1;
|
||
if(newPos+devH-1><%= rack.total_units %>) newPos=<%= rack.total_units %>-devH+1;
|
||
if(newPos===dragOrigU) return;
|
||
var f=document.createElement('form'); f.method='POST'; f.action='/racks/devices/'+devId+'/edit';
|
||
f.innerHTML='<input name="position_u" value="'+newPos+'"><input name="height_u" value="'+devH+'"><input name="_redirect" value="/racks/<%= rack.id %>">';
|
||
document.body.appendChild(f); f.submit();
|
||
});
|
||
});
|
||
|
||
// ── Export SVG ──
|
||
<%
|
||
// Build SVG server-side
|
||
var _svgunits = [];
|
||
devices.forEach(function(d) { for(var u = d.position_u; u < d.position_u + d.height_u; u++) _svgunits[u] = d; });
|
||
var _svg = '<svg xmlns="http://www.w3.org/2000/svg" width="280" height="'+(rack.total_units*30+60)+'" viewBox="0 0 280 '+(rack.total_units*30+60)+'">';
|
||
_svg += '<rect width="280" height="'+(rack.total_units*30+60)+'" fill="#1e1f2b"/>';
|
||
_svg += '<text x="140" y="20" text-anchor="middle" fill="#f8f8f2" font-family="Inter,sans-serif" font-size="12" font-weight="600">'+rack.name+' · '+rack.total_units+'U</text>';
|
||
_svg += '<rect x="10" y="30" width="260" height="'+(rack.total_units*30)+'" fill="#282a36" stroke="#44475a" stroke-width="2" rx="4"/>';
|
||
for(var u = rack.total_units; u >= 1; u--) {
|
||
var dev = _svgunits[u];
|
||
if (dev && u === dev.position_u) {
|
||
var vm=(dev.manufacturer||'').toLowerCase(), c='#555d66';
|
||
if(vm.includes('hpe')) c='#ff7b00'; else if(vm.includes('dell')) c='#0076a8';
|
||
else if(vm.includes('lenovo')) c='#e41f33'; else if(vm.includes('cisco')) c='#00529a';
|
||
else if(vm.includes('juniper')) c='#44a833'; else if(vm.includes('arista')) c='#cc3333';
|
||
else if(vm.includes('netapp')) c='#00a1df'; else if(vm.includes('pure')) c='#4a90d9';
|
||
else if(vm.includes('fortinet')) c='#ee3124'; else if(vm.includes('apc')) c='#6e7681';
|
||
else if(vm.includes('f5')) c='#d32f2f'; else if(vm.includes('palo')) c='#ed7b30';
|
||
else if(vm.includes('nvidia')) c='#76b900';
|
||
var y=30+(rack.total_units-dev.position_u-dev.height_u+1)*30;
|
||
_svg += '<rect x="24" y="'+y+'" width="232" height="'+(dev.height_u*30-4)+'" fill="'+c+'" rx="3"/>';
|
||
_svg += '<text x="32" y="'+(y+dev.height_u*15+1)+'" fill="#fff" font-family="Inter,sans-serif" font-size="10" font-weight="600">'+dev.name+'</text>';
|
||
_svg += '<text x="240" y="'+(y+dev.height_u*15+1)+'" text-anchor="end" fill="rgba(255,255,255,.5)" font-family="Inter,sans-serif" font-size="8">'+dev.height_u+'U</text>';
|
||
} else if (!dev) {
|
||
var yy=30+(rack.total_units-u)*30;
|
||
_svg += '<rect x="12" y="'+yy+'" width="236" height="30" fill="#1e1f2b" stroke="#44475a" stroke-width=".5" opacity=".3"/>';
|
||
_svg += '<text x="20" y="'+(yy+19)+'" fill="#6272a4" font-family="monospace" font-size="8">'+u+'</text>';
|
||
}
|
||
}
|
||
_svg += '</svg>';
|
||
%>
|
||
var _svgContent = <%- JSON.stringify(_svg) %>;
|
||
function exportSVG(){
|
||
var blob=new Blob([_svgContent],{type:'image/svg+xml'}); var url=URL.createObjectURL(blob);
|
||
var a=document.createElement('a');a.href=url;a.download='<%= rack.name.replace(/[^a-zA-Z0-9]/g, '_') %>.svg';
|
||
document.body.appendChild(a);a.click();document.body.removeChild(a); URL.revokeObjectURL(url);
|
||
}
|
||
</script>
|
||
<script src="/js/theme.js"></script></body>
|
||
</html> |