Files
mek-tech-consulting/views/racks-view.ejs
T

752 lines
46 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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,'&quot;') + ' · ' + (dev.model||dev.manufacturer||dev.device_type).replace(/"/g,'&quot;') + ' · ' + (dev.mgmt_ip||'').replace(/"/g,'&quot;') + '">';
_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>