710 lines
40 KiB
HTML
710 lines
40 KiB
HTML
|
|
<!DOCTYPE html>
|
|||
|
|
<html lang="vi">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|||
|
|
<title>Juniper Config Generator</title>
|
|||
|
|
<style>
|
|||
|
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&family=Syne:wght@400;600;700;800&display=swap');
|
|||
|
|
:root {
|
|||
|
|
--bg:#0a0e17;--surface:#111827;--surface2:#1a2235;--border:#1e2d45;
|
|||
|
|
--accent:#00d4ff;--accent2:#ff6b35;--green:#00ff88;
|
|||
|
|
--text:#e2e8f0;--muted:#64748b;--danger:#ff4757;
|
|||
|
|
}
|
|||
|
|
*{box-sizing:border-box;margin:0;padding:0;}
|
|||
|
|
body{background:var(--bg);color:var(--text);font-family:'Syne',sans-serif;min-height:100vh;
|
|||
|
|
background-image:radial-gradient(ellipse 80% 50% at 50% -20%,rgba(0,212,255,.08),transparent),
|
|||
|
|
radial-gradient(ellipse 60% 40% at 80% 100%,rgba(255,107,53,.05),transparent);}
|
|||
|
|
header{border-bottom:1px solid var(--border);padding:18px 32px;display:flex;align-items:center;gap:16px;
|
|||
|
|
background:rgba(17,24,39,.8);backdrop-filter:blur(10px);position:sticky;top:0;z-index:100;}
|
|||
|
|
.logo{width:36px;height:36px;background:linear-gradient(135deg,var(--accent),var(--accent2));
|
|||
|
|
border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:18px;font-weight:800;color:var(--bg);}
|
|||
|
|
header h1{font-size:18px;font-weight:700;letter-spacing:-.02em;}
|
|||
|
|
header span{color:var(--accent);}
|
|||
|
|
.badge{margin-left:auto;font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted);
|
|||
|
|
background:var(--surface2);border:1px solid var(--border);padding:4px 10px;border-radius:20px;}
|
|||
|
|
.container{max-width:1600px;margin:0 auto;padding:32px 48px;}
|
|||
|
|
|
|||
|
|
/* TABS */
|
|||
|
|
.tabs{display:flex;gap:4px;margin-bottom:28px;background:var(--surface);
|
|||
|
|
border:1px solid var(--border);border-radius:10px;padding:4px;width:fit-content;}
|
|||
|
|
.tab-btn{background:transparent;color:var(--muted);padding:8px 20px;border-radius:7px;font-size:13px;font-weight:700;
|
|||
|
|
border:none;cursor:pointer;transition:all .15s;letter-spacing:.02em;}
|
|||
|
|
.tab-btn:hover{color:var(--text);}
|
|||
|
|
.tab-btn.active{background:var(--surface2);color:var(--text);border:1px solid var(--border);}
|
|||
|
|
.tab-btn.active.tab-legacy{color:var(--accent);}
|
|||
|
|
.tab-btn.active.tab-delete{color:var(--danger);}
|
|||
|
|
.tab-btn.active.tab-esi{color:#a78bfa;}
|
|||
|
|
.esi-card{background:var(--surface);border:1px solid rgba(167,139,250,.25);border-radius:12px;overflow:hidden;transition:border-color .2s;margin-bottom:16px;}
|
|||
|
|
.esi-card:hover{border-color:rgba(167,139,250,.5);}
|
|||
|
|
.esi-card .card-header{border-bottom-color:rgba(167,139,250,.15);}
|
|||
|
|
.card-num.esi{background:linear-gradient(135deg,#a78bfa,#7c3aed);}
|
|||
|
|
.toolbar-title.esi span{color:#a78bfa;}
|
|||
|
|
.esi-body{padding:20px;display:flex;flex-direction:column;gap:16px;} /* form top, configs bottom */
|
|||
|
|
.esi-card.collapsed .esi-body{display:none;}
|
|||
|
|
.esi-config-cols{display:grid;grid-template-columns:1fr 1fr;gap:12px;} /* keep side by side */
|
|||
|
|
.config-header.sw1 span{color:#38bdf8;}
|
|||
|
|
.config-header.sw2 span{color:#f472b6;}
|
|||
|
|
.bulk-section.esi-bulk{border-color:rgba(167,139,250,.2);}
|
|||
|
|
.dot.esi{background:#a78bfa;}
|
|||
|
|
.tab-pane{display:none;}
|
|||
|
|
.tab-pane.active{display:block;}
|
|||
|
|
|
|||
|
|
.toolbar{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:24px;align-items:center;}
|
|||
|
|
.toolbar-title{font-size:22px;font-weight:800;letter-spacing:-.03em;flex:1;}
|
|||
|
|
.toolbar-title span{color:var(--accent);}
|
|||
|
|
.toolbar-title.del span{color:var(--danger);}
|
|||
|
|
|
|||
|
|
button{font-family:'Syne',sans-serif;font-size:13px;font-weight:600;border:none;cursor:pointer;
|
|||
|
|
border-radius:8px;padding:8px 16px;transition:all .15s ease;display:flex;align-items:center;gap:6px;}
|
|||
|
|
.btn-primary{background:var(--accent);color:var(--bg);}
|
|||
|
|
.btn-primary:hover{background:#33dcff;transform:translateY(-1px);}
|
|||
|
|
.btn-red{background:var(--danger);color:#fff;}
|
|||
|
|
.btn-red:hover{background:#ff6670;transform:translateY(-1px);}
|
|||
|
|
.btn-secondary{background:var(--surface2);color:var(--text);border:1px solid var(--border);}
|
|||
|
|
.btn-secondary:hover{border-color:var(--accent);color:var(--accent);}
|
|||
|
|
.btn-danger{background:transparent;color:var(--danger);border:1px solid rgba(255,71,87,.3);}
|
|||
|
|
.btn-danger:hover{background:rgba(255,71,87,.1);}
|
|||
|
|
.btn-add-int{background:transparent;color:var(--green);border:1px dashed rgba(0,255,136,.4);
|
|||
|
|
font-size:12px;padding:6px 12px;width:100%;justify-content:center;margin-top:6px;}
|
|||
|
|
.btn-add-int:hover{background:rgba(0,255,136,.07);border-color:var(--green);}
|
|||
|
|
.btn-add-int.red{color:var(--danger);border-color:rgba(255,71,87,.35);}
|
|||
|
|
.btn-add-int.red:hover{background:rgba(255,71,87,.07);border-color:var(--danger);}
|
|||
|
|
|
|||
|
|
/* LEGACY CARDS */
|
|||
|
|
.servers-grid{display:flex;flex-direction:column;gap:16px;}
|
|||
|
|
.server-card{background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden;transition:border-color .2s;}
|
|||
|
|
.server-card:hover{border-color:rgba(0,212,255,.3);}
|
|||
|
|
.card-header{display:flex;align-items:center;gap:12px;padding:14px 20px;background:var(--surface2);
|
|||
|
|
border-bottom:1px solid var(--border);cursor:pointer;user-select:none;}
|
|||
|
|
.card-num{width:28px;height:28px;background:linear-gradient(135deg,var(--accent),#0088cc);border-radius:6px;
|
|||
|
|
display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;color:var(--bg);flex-shrink:0;}
|
|||
|
|
.card-num.del{background:linear-gradient(135deg,var(--danger),#cc3344);}
|
|||
|
|
.card-name{font-weight:700;font-size:15px;flex:1;}
|
|||
|
|
.card-name.empty{color:var(--muted);font-weight:400;font-style:italic;}
|
|||
|
|
.card-meta{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted);display:flex;gap:12px;flex-wrap:wrap;}
|
|||
|
|
.card-meta span{color:var(--accent);}
|
|||
|
|
.card-meta span.red{color:var(--danger);}
|
|||
|
|
.chevron{color:var(--muted);transition:transform .2s;font-size:12px;}
|
|||
|
|
.server-card.collapsed .chevron{transform:rotate(-90deg);}
|
|||
|
|
.card-body{padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:20px;}
|
|||
|
|
.server-card.collapsed .card-body{display:none;}
|
|||
|
|
.form-group{display:flex;flex-direction:column;gap:6px;}
|
|||
|
|
.form-group label{font-size:11px;font-weight:600;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;}
|
|||
|
|
input,select{font-family:'JetBrains Mono',monospace;font-size:13px;background:var(--bg);border:1px solid var(--border);
|
|||
|
|
color:var(--text);border-radius:8px;padding:9px 12px;outline:none;transition:border-color .15s;width:100%;}
|
|||
|
|
input:focus,select:focus{border-color:var(--accent);}
|
|||
|
|
input::placeholder{color:var(--muted);}
|
|||
|
|
.form-row{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
|
|||
|
|
.divider{height:1px;background:var(--border);margin:12px 0;}
|
|||
|
|
.section-label{font-size:11px;font-weight:600;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;
|
|||
|
|
margin-bottom:8px;display:flex;align-items:center;justify-content:space-between;}
|
|||
|
|
.int-count{background:var(--surface2);border:1px solid var(--border);color:var(--accent);font-size:10px;padding:2px 7px;border-radius:10px;}
|
|||
|
|
.int-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;}
|
|||
|
|
.int-badge{font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:700;background:rgba(0,212,255,.1);
|
|||
|
|
color:var(--accent);border:1px solid rgba(0,212,255,.2);border-radius:4px;padding:2px 6px;flex-shrink:0;min-width:28px;text-align:center;}
|
|||
|
|
.int-badge.ae{background:rgba(255,107,53,.12);color:var(--accent2);border-color:rgba(255,107,53,.25);}
|
|||
|
|
.int-badge.del-p{background:rgba(255,71,87,.1);color:var(--danger);border-color:rgba(255,71,87,.25);}
|
|||
|
|
.int-badge.del-ae{background:rgba(255,71,87,.06);color:#ff9999;border-color:rgba(255,71,87,.2);}
|
|||
|
|
.int-row input{flex:1;margin:0;}
|
|||
|
|
.btn-rm{background:transparent;color:var(--muted);border:none;padding:4px 6px;font-size:14px;border-radius:4px;flex-shrink:0;transition:color .15s;}
|
|||
|
|
.btn-rm:hover{color:var(--danger);}
|
|||
|
|
.config-output{background:var(--bg);border:1px solid var(--border);border-radius:8px;overflow:hidden;}
|
|||
|
|
.config-header{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;
|
|||
|
|
background:var(--surface2);border-bottom:1px solid var(--border);}
|
|||
|
|
.config-header span{font-size:11px;font-weight:600;color:var(--accent);letter-spacing:.08em;text-transform:uppercase;}
|
|||
|
|
pre{font-family:'JetBrains Mono',monospace;font-size:12px;line-height:1.7;padding:16px;color:#a8c7fa;white-space:pre;overflow-x:auto;}
|
|||
|
|
pre .d{color:#ff7b93;}pre .s{color:#89ddff;}pre .k{color:var(--green);}pre .v{color:#ffcb6b;}pre .c{color:var(--muted);font-style:italic;}
|
|||
|
|
|
|||
|
|
/* DELETE TAB - entry cards */
|
|||
|
|
.del-card{background:var(--surface);border:1px solid rgba(255,71,87,.2);border-radius:12px;overflow:hidden;transition:border-color .2s;margin-bottom:16px;}
|
|||
|
|
.del-card:hover{border-color:rgba(255,71,87,.45);}
|
|||
|
|
.del-card .card-header{border-bottom-color:rgba(255,71,87,.15);}
|
|||
|
|
|
|||
|
|
/* two-col layout for delete card body */
|
|||
|
|
.del-body{padding:20px;display:grid;grid-template-columns:1fr 1fr;gap:20px;}
|
|||
|
|
.del-card.collapsed .del-body{display:none;}
|
|||
|
|
|
|||
|
|
/* col layout inside delete body left */
|
|||
|
|
.del-col-inputs{display:flex;flex-direction:column;gap:0;}
|
|||
|
|
|
|||
|
|
.bulk-section{margin-top:32px;background:var(--surface);border:1px solid var(--border);border-radius:12px;overflow:hidden;}
|
|||
|
|
.bulk-section.del-bulk{border-color:rgba(255,71,87,.2);}
|
|||
|
|
.bulk-header{display:flex;align-items:center;gap:12px;padding:16px 20px;background:var(--surface2);border-bottom:1px solid var(--border);}
|
|||
|
|
.bulk-header h2{font-size:16px;font-weight:700;flex:1;}
|
|||
|
|
.dot{width:8px;height:8px;background:var(--green);border-radius:50%;animation:pulse 2s infinite;}
|
|||
|
|
.dot.red{background:var(--danger);}
|
|||
|
|
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
|
|||
|
|
.bulk-pre-wrap{padding:20px;}
|
|||
|
|
.toast{position:fixed;bottom:24px;right:24px;background:var(--green);color:var(--bg);font-weight:700;font-size:13px;
|
|||
|
|
padding:12px 20px;border-radius:10px;transform:translateY(100px);transition:transform .3s cubic-bezier(.34,1.56,.64,1);z-index:999;}
|
|||
|
|
.toast.show{transform:translateY(0);}
|
|||
|
|
@media(max-width:900px){
|
|||
|
|
.card-body,.del-body{grid-template-columns:1fr;}
|
|||
|
|
.container{padding:16px;}
|
|||
|
|
.form-row{grid-template-columns:1fr;}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<header>
|
|||
|
|
<div class="logo">J</div>
|
|||
|
|
<h1>Juniper <span>Config</span> Generator</h1>
|
|||
|
|
<div class="badge">JUNOS / LACP / ae-bundle</div>
|
|||
|
|
</header>
|
|||
|
|
<div class="container">
|
|||
|
|
|
|||
|
|
<!-- TABS -->
|
|||
|
|
<div class="tabs">
|
|||
|
|
<button class="tab-btn tab-legacy active" onclick="switchTab('legacy')">⚡ Legacy</button>
|
|||
|
|
<button class="tab-btn tab-esi" onclick="switchTab('esi')">🔗 ESI</button>
|
|||
|
|
<button class="tab-btn tab-delete" onclick="switchTab('delete')">🗑 Delete Interface</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- ===== TAB: LEGACY ===== -->
|
|||
|
|
<div class="tab-pane active" id="tab-legacy">
|
|||
|
|
<div class="toolbar">
|
|||
|
|
<div class="toolbar-title">Server <span>Entries</span></div>
|
|||
|
|
<button class="btn-secondary" onclick="copyAll()">📋 Copy All</button>
|
|||
|
|
<button class="btn-primary" onclick="addServer()">+ Add Server</button>
|
|||
|
|
</div>
|
|||
|
|
<div class="servers-grid" id="serversGrid"></div>
|
|||
|
|
<div class="bulk-section" id="bulkSection" style="display:none">
|
|||
|
|
<div class="bulk-header" style="cursor:pointer" onclick="toggleBulk('bulkBody','bulkChevron')">
|
|||
|
|
<div class="dot"></div>
|
|||
|
|
<h2>Generated Config — All Servers</h2>
|
|||
|
|
<button class="btn-secondary" style="margin-left:auto" onclick="event.stopPropagation();copyAll()">📋 Copy All</button>
|
|||
|
|
<span id="bulkChevron" style="color:var(--muted);font-size:12px;margin-left:8px">▶</span>
|
|||
|
|
</div>
|
|||
|
|
<div id="bulkBody" style="display:none"><div class="bulk-pre-wrap"><pre id="bulkPre"></pre></div></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- ===== TAB: DELETE INTERFACE ===== -->
|
|||
|
|
<div class="tab-pane" id="tab-delete">
|
|||
|
|
<div class="toolbar">
|
|||
|
|
<div class="toolbar-title del">Delete <span>Interface</span></div>
|
|||
|
|
<button class="btn-secondary" onclick="copyAllDel()">📋 Copy All</button>
|
|||
|
|
<button class="btn-red" onclick="addDelEntry()">+ Add Entry</button>
|
|||
|
|
</div>
|
|||
|
|
<div id="delGrid"></div>
|
|||
|
|
<div class="bulk-section del-bulk" id="delBulkSection" style="display:none">
|
|||
|
|
<div class="bulk-header" style="cursor:pointer" onclick="toggleBulk('delBulkBody','delBulkChevron')">
|
|||
|
|
<div class="dot red"></div>
|
|||
|
|
<h2>Delete Config — All Entries</h2>
|
|||
|
|
<button class="btn-secondary" style="margin-left:auto" onclick="event.stopPropagation();copyAllDel()">📋 Copy All</button>
|
|||
|
|
<span id="delBulkChevron" style="color:var(--muted);font-size:12px;margin-left:8px">▶</span>
|
|||
|
|
</div>
|
|||
|
|
<div id="delBulkBody" style="display:none"><div class="bulk-pre-wrap"><pre id="delBulkPre"></pre></div></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
|
|||
|
|
<!-- ===== TAB: ESI ===== -->
|
|||
|
|
<div class="tab-pane" id="tab-esi">
|
|||
|
|
<div class="toolbar">
|
|||
|
|
<div class="toolbar-title esi">ESI <span>Multi-Homing</span></div>
|
|||
|
|
<button class="btn-secondary" onclick="copyAllEsi()">📋 Copy All</button>
|
|||
|
|
<button class="btn-primary" style="background:#a78bfa" onclick="addEsiEntry()">+ Add Entry</button>
|
|||
|
|
</div>
|
|||
|
|
<div id="esiGrid"></div>
|
|||
|
|
<div class="bulk-section esi-bulk" id="esiBulkSection" style="display:none">
|
|||
|
|
<div class="bulk-header" style="cursor:pointer" onclick="toggleBulk('esiBulkBody','esiBulkChevron')">
|
|||
|
|
<div class="dot esi"></div>
|
|||
|
|
<h2>ESI Config — All Entries</h2>
|
|||
|
|
<button class="btn-secondary" style="margin-left:auto" onclick="event.stopPropagation();copyAllEsi()">📋 Copy All</button>
|
|||
|
|
<span id="esiBulkChevron" style="color:var(--muted);font-size:12px;margin-left:8px">▶</span>
|
|||
|
|
</div>
|
|||
|
|
<div id="esiBulkBody" style="display:none"><div class="bulk-pre-wrap"><pre id="esiBulkPre"></pre></div></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
</div>
|
|||
|
|
<div class="toast" id="toast"></div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
// ==================== TABS ====================
|
|||
|
|
function switchTab(name){
|
|||
|
|
document.querySelectorAll('.tab-pane').forEach(p=>p.classList.remove('active'));
|
|||
|
|
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
|||
|
|
document.getElementById('tab-'+name).classList.add('active');
|
|||
|
|
document.querySelector('.tab-'+name).classList.add('active');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ==================== LEGACY TAB ====================
|
|||
|
|
const DEFAULTS={vlan:'2906',storm:'PROFILE_10G',mode:'access',lacp:'active'};
|
|||
|
|
let servers=[],nextId=1;
|
|||
|
|
|
|||
|
|
function addServer(data={}){
|
|||
|
|
const id=nextId++;
|
|||
|
|
servers.push({id,name:data.name||'',rack:data.rack||'',u:data.u||'',ae:data.ae||'',
|
|||
|
|
vlan:data.vlan||'',storm:data.storm||'',mode:data.mode||'',lacp:data.lacp||'',
|
|||
|
|
interfaces:data.interfaces?[...data.interfaces]:['','']});
|
|||
|
|
renderCards();
|
|||
|
|
setTimeout(()=>{const el=document.querySelector('[data-id="'+id+'"]');if(el)el.scrollIntoView({behavior:'smooth',block:'start'});},50);
|
|||
|
|
}
|
|||
|
|
function removeServer(id){servers=servers.filter(s=>s.id!==id);renderCards();}
|
|||
|
|
|
|||
|
|
function syncServer(id){
|
|||
|
|
const s=servers.find(s=>s.id===id);if(!s)return;
|
|||
|
|
const card=document.querySelector('[data-id="'+id+'"]');if(!card)return;
|
|||
|
|
s.name=card.querySelector('[data-field="name"]')?.value.trim()||'';
|
|||
|
|
s.rack=card.querySelector('[data-field="rack"]')?.value.trim()||'';
|
|||
|
|
s.u=card.querySelector('[data-field="u"]')?.value.trim()||'';
|
|||
|
|
s.ae=card.querySelector('[data-field="ae"]')?.value.trim()||'';
|
|||
|
|
s.vlan=card.querySelector('[data-field="vlan"]')?.value.trim()||'';
|
|||
|
|
s.storm=card.querySelector('[data-field="storm"]')?.value.trim()||'';
|
|||
|
|
s.mode=card.querySelector('[data-field="mode"]')?.value||'';
|
|||
|
|
s.lacp=card.querySelector('[data-field="lacp"]')?.value||'';
|
|||
|
|
card.querySelectorAll('.int-row input').forEach((inp,i)=>{s.interfaces[i]=inp.value.trim();});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function addInterface(id){
|
|||
|
|
syncServer(id);const s=servers.find(s=>s.id===id);if(!s)return;
|
|||
|
|
s.interfaces.push('');renderCards();
|
|||
|
|
setTimeout(()=>{const card=document.querySelector('[data-id="'+id+'"]');if(!card)return;
|
|||
|
|
const inputs=card.querySelectorAll('.int-row input');if(inputs.length)inputs[inputs.length-1].focus();},30);
|
|||
|
|
}
|
|||
|
|
function removeInterface(id,idx){
|
|||
|
|
syncServer(id);const s=servers.find(s=>s.id===id);if(!s||s.interfaces.length<=1)return;
|
|||
|
|
s.interfaces.splice(idx,1);renderCards();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateServer(id){
|
|||
|
|
syncServer(id);const s=servers.find(s=>s.id===id);if(!s)return;
|
|||
|
|
const card=document.querySelector('[data-id="'+id+'"]');
|
|||
|
|
if(card){
|
|||
|
|
const nameEl=card.querySelector('.card-name');
|
|||
|
|
nameEl.textContent=s.name||'(chưa đặt tên)';nameEl.className='card-name'+(s.name?'':' empty');
|
|||
|
|
const metas=card.querySelectorAll('.card-meta span');
|
|||
|
|
if(metas[0])metas[0].textContent=s.rack||'—';
|
|||
|
|
if(metas[1])metas[1].textContent=s.ae||'—';
|
|||
|
|
if(metas[2])metas[2].textContent=s.interfaces.filter(Boolean).length||s.interfaces.length;
|
|||
|
|
const ic=card.querySelector('.int-count');if(ic)ic.textContent=s.interfaces.length+' port'+(s.interfaces.length>1?'s':'');
|
|||
|
|
const pre=card.querySelector('pre');if(pre)pre.innerHTML=hl(gen(s));
|
|||
|
|
}
|
|||
|
|
renderBulk();
|
|||
|
|
}
|
|||
|
|
function toggleCard(id){document.querySelector('[data-id="'+id+'"]')?.classList.toggle('collapsed');}
|
|||
|
|
|
|||
|
|
function gen(s){
|
|||
|
|
const vlan=s.vlan||DEFAULTS.vlan,storm=s.storm||DEFAULTS.storm,mode=s.mode||DEFAULTS.mode,lacp=s.lacp||DEFAULTS.lacp;
|
|||
|
|
const ints=s.interfaces.filter(i=>i.trim());
|
|||
|
|
if(!ints.length||!s.ae)return '# Điền ít nhất 1 interface và AE để sinh config...';
|
|||
|
|
const base=(s.name&&s.rack&&s.u)?`${s.name}_${s.rack}_U${s.u}`:(s.name||'SERVER');
|
|||
|
|
const vlans=(s.vlan||DEFAULTS.vlan).split(/[\s,]+/).map(v=>v.trim()).filter(Boolean);
|
|||
|
|
const L=[];
|
|||
|
|
ints.forEach(i=>L.push('delete interfaces '+i));
|
|||
|
|
L.push('delete interfaces '+s.ae);
|
|||
|
|
ints.forEach((i,n)=>{L.push('set interfaces '+i+' description '+base+'_P'+(n+1));L.push('set interfaces '+i+' ether-options 802.3ad '+s.ae);});
|
|||
|
|
L.push('set interfaces '+s.ae+' description '+base);
|
|||
|
|
L.push('set interfaces '+s.ae+' aggregated-ether-options lacp '+lacp);
|
|||
|
|
L.push('set interfaces '+s.ae+' unit 0 family ethernet-switching interface-mode '+mode);
|
|||
|
|
vlans.forEach(v=>L.push('set interfaces '+s.ae+' unit 0 family ethernet-switching vlan members '+v));
|
|||
|
|
L.push('set interfaces '+s.ae+' unit 0 family ethernet-switching storm-control '+storm);
|
|||
|
|
L.push('set protocols mstp interface '+s.ae+' no-root-port');
|
|||
|
|
ints.forEach(i=>L.push('delete protocols mstp interface '+i));
|
|||
|
|
return L.join('\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function hl(text){
|
|||
|
|
return text.split('\n').map(line=>{
|
|||
|
|
if(line.startsWith('#'))return '<span class="c">'+line+'</span>';
|
|||
|
|
if(line.startsWith('delete'))return '<span class="d">'+line+'</span>';
|
|||
|
|
if(line.startsWith('set')){const t=line.split(' ');return '<span class="s">'+t[0]+'</span> <span class="k">'+t.slice(1,-1).join(' ')+'</span> <span class="v">'+t[t.length-1]+'</span>';}
|
|||
|
|
return line;
|
|||
|
|
}).join('\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function buildInts(s){
|
|||
|
|
return s.interfaces.map((v,i)=>`<div class="int-row"><div class="int-badge">P${i+1}</div><input type="text" value="${v}" placeholder="xe-0/0/${i}" oninput="updateServer(${s.id})"><button class="btn-rm" onclick="removeInterface(${s.id},${i})">✕</button></div>`).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderCards(){
|
|||
|
|
const grid=document.getElementById('serversGrid'),sy=window.scrollY;
|
|||
|
|
grid.innerHTML='';
|
|||
|
|
servers.forEach((s,idx)=>{
|
|||
|
|
const card=document.createElement('div');card.className='server-card';card.dataset.id=s.id;
|
|||
|
|
card.innerHTML=`
|
|||
|
|
<div class="card-header" onclick="toggleCard(${s.id})">
|
|||
|
|
<div class="card-num">${idx+1}</div>
|
|||
|
|
<div class="card-name ${s.name?'':'empty'}">${s.name||'(chưa đặt tên)'}</div>
|
|||
|
|
<div class="card-meta">Rack: <span>${s.rack||'—'}</span> | AE: <span>${s.ae||'—'}</span> | Ports: <span>${s.interfaces.filter(Boolean).length||s.interfaces.length}</span></div>
|
|||
|
|
<button class="btn-danger" style="margin-left:8px;padding:4px 10px;font-size:11px" onclick="event.stopPropagation();removeServer(${s.id})">✕ Xóa</button>
|
|||
|
|
<div class="chevron">▼</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="card-body">
|
|||
|
|
<div>
|
|||
|
|
<div class="form-group" style="margin-bottom:12px">
|
|||
|
|
<label>Server Name / Serial</label>
|
|||
|
|
<input type="text" data-field="name" value="${s.name}" placeholder="210235K0UQ6256V00022" oninput="updateServer(${s.id})">
|
|||
|
|
</div>
|
|||
|
|
<div style="display:grid;grid-template-columns:1fr 0.6fr 0.8fr;gap:10px;margin-bottom:12px">
|
|||
|
|
<div class="form-group"><label>Rack</label><input type="text" data-field="rack" value="${s.rack}" placeholder="2BG10" oninput="updateServer(${s.id})"></div>
|
|||
|
|
<div class="form-group"><label>U Position</label><input type="text" data-field="u" value="${s.u}" placeholder="23" oninput="updateServer(${s.id})"></div>
|
|||
|
|
<div class="form-group"><label>AE Interface</label><input type="text" data-field="ae" value="${s.ae}" placeholder="ae38" oninput="updateServer(${s.id})"></div>
|
|||
|
|
</div>
|
|||
|
|
<div class="divider"></div>
|
|||
|
|
<div class="section-label">Physical Interfaces <span class="int-count">${s.interfaces.length} port${s.interfaces.length>1?'s':''}</span></div>
|
|||
|
|
${buildInts(s)}
|
|||
|
|
<button class="btn-add-int" onclick="addInterface(${s.id})">+ Thêm interface</button>
|
|||
|
|
<div class="divider"></div>
|
|||
|
|
<div class="form-row" style="margin-bottom:10px">
|
|||
|
|
<div class="form-group"><label>VLAN Members</label><input type="text" data-field="vlan" value="${s.vlan}" placeholder="${DEFAULTS.vlan}" oninput="updateServer(${s.id})"></div>
|
|||
|
|
<div class="form-group"><label>Storm Profile</label><input type="text" data-field="storm" value="${s.storm}" placeholder="${DEFAULTS.storm}" oninput="updateServer(${s.id})"></div>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-row">
|
|||
|
|
<div class="form-group"><label>Interface Mode</label>
|
|||
|
|
<select data-field="mode" onchange="updateServer(${s.id})">
|
|||
|
|
<option value="" ${!s.mode?'selected':''}>— default (${DEFAULTS.mode}) —</option>
|
|||
|
|
<option value="access" ${s.mode==='access'?'selected':''}>access</option>
|
|||
|
|
<option value="trunk" ${s.mode==='trunk'?'selected':''}>trunk</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group"><label>LACP Mode</label>
|
|||
|
|
<select data-field="lacp" onchange="updateServer(${s.id})">
|
|||
|
|
<option value="" ${!s.lacp?'selected':''}>— default (${DEFAULTS.lacp}) —</option>
|
|||
|
|
<option value="active" ${s.lacp==='active'?'selected':''}>active</option>
|
|||
|
|
<option value="passive" ${s.lacp==='passive'?'selected':''}>passive</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<div class="config-output">
|
|||
|
|
<div class="config-header"><span>⚡ Generated Config</span>
|
|||
|
|
<button class="btn-secondary" style="padding:4px 12px;font-size:11px" onclick="copySingle(${s.id})">📋 Copy</button>
|
|||
|
|
</div>
|
|||
|
|
<pre>${hl(gen(s))}</pre>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
grid.appendChild(card);
|
|||
|
|
});
|
|||
|
|
window.scrollTo(0,sy);renderBulk();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderBulk(){
|
|||
|
|
const bulk=document.getElementById('bulkSection'),pre=document.getElementById('bulkPre');
|
|||
|
|
if(!servers.length){bulk.style.display='none';return;}
|
|||
|
|
bulk.style.display='block';
|
|||
|
|
pre.innerHTML=hl(servers.map((s,i)=>'# ===== Server '+(i+1)+': '+(s.name||'unnamed')+' =====\n'+gen(s)).join('\n\n'));
|
|||
|
|
}
|
|||
|
|
function copySingle(id){const s=servers.find(s=>s.id===id);if(!s)return;navigator.clipboard.writeText(gen(s)).then(()=>toast('✓ Copied!'));}
|
|||
|
|
function copyAll(){if(!servers.length)return;navigator.clipboard.writeText(servers.map((s,i)=>'# ===== Server '+(i+1)+': '+(s.name||'unnamed')+' =====\n'+gen(s)).join('\n\n')).then(()=>toast('✓ All configs copied!'));}
|
|||
|
|
|
|||
|
|
// ==================== DELETE TAB ====================
|
|||
|
|
let delEntries=[],delNextId=1;
|
|||
|
|
|
|||
|
|
function addDelEntry(data={}){
|
|||
|
|
const id=delNextId++;
|
|||
|
|
delEntries.push({
|
|||
|
|
id,
|
|||
|
|
name: data.name||'',
|
|||
|
|
vlan: data.vlan||'',
|
|||
|
|
physicals: data.physicals?[...data.physicals]:[''],
|
|||
|
|
aes: data.aes?[...data.aes]:[''],
|
|||
|
|
});
|
|||
|
|
renderDelCards();
|
|||
|
|
setTimeout(()=>{const el=document.querySelector('[data-del-id="'+id+'"]');if(el)el.scrollIntoView({behavior:'smooth',block:'start'});},50);
|
|||
|
|
}
|
|||
|
|
function removeDelEntry(id){delEntries=delEntries.filter(e=>e.id!==id);renderDelCards();}
|
|||
|
|
|
|||
|
|
function syncDel(id){
|
|||
|
|
const e=delEntries.find(e=>e.id===id);if(!e)return;
|
|||
|
|
const card=document.querySelector('[data-del-id="'+id+'"]');if(!card)return;
|
|||
|
|
e.name=card.querySelector('[data-field="name"]')?.value.trim()||'';
|
|||
|
|
e.vlan=card.querySelector('[data-field="vlan"]')?.value.trim()||'';
|
|||
|
|
e.physicals=[];card.querySelectorAll('.phys-input').forEach(inp=>e.physicals.push(inp.value.trim()));
|
|||
|
|
e.aes=[];card.querySelectorAll('.ae-input').forEach(inp=>e.aes.push(inp.value.trim()));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function addDelPhys(id){syncDel(id);const e=delEntries.find(e=>e.id===id);if(!e)return;e.physicals.push('');renderDelCards();
|
|||
|
|
setTimeout(()=>{const card=document.querySelector('[data-del-id="'+id+'"]');if(!card)return;const inp=card.querySelectorAll('.phys-input');if(inp.length)inp[inp.length-1].focus();},30);}
|
|||
|
|
function removeDelPhys(id,idx){syncDel(id);const e=delEntries.find(e=>e.id===id);if(!e||e.physicals.length<=1)return;e.physicals.splice(idx,1);renderDelCards();}
|
|||
|
|
function addDelAe(id){syncDel(id);const e=delEntries.find(e=>e.id===id);if(!e)return;e.aes.push('');renderDelCards();
|
|||
|
|
setTimeout(()=>{const card=document.querySelector('[data-del-id="'+id+'"]');if(!card)return;const inp=card.querySelectorAll('.ae-input');if(inp.length)inp[inp.length-1].focus();},30);}
|
|||
|
|
function removeDelAe(id,idx){syncDel(id);const e=delEntries.find(e=>e.id===id);if(!e||e.aes.length<=1)return;e.aes.splice(idx,1);renderDelCards();}
|
|||
|
|
|
|||
|
|
function updateDel(id){
|
|||
|
|
syncDel(id);const e=delEntries.find(e=>e.id===id);if(!e)return;
|
|||
|
|
const card=document.querySelector('[data-del-id="'+id+'"]');
|
|||
|
|
if(card){
|
|||
|
|
const nm=card.querySelector('.card-name');nm.textContent=e.name||'(chưa đặt tên)';nm.className='card-name'+(e.name?'':' empty');
|
|||
|
|
const metas=card.querySelectorAll('.card-meta span');
|
|||
|
|
if(metas[0])metas[0].textContent=e.physicals.filter(Boolean).length||'—';
|
|||
|
|
if(metas[1])metas[1].textContent=e.aes.filter(Boolean).length||'—';
|
|||
|
|
const pre=card.querySelector('pre');if(pre)pre.innerHTML=hl(genDel(e));
|
|||
|
|
}
|
|||
|
|
renderDelBulk();
|
|||
|
|
}
|
|||
|
|
function toggleDelCard(id){document.querySelector('[data-del-id="'+id+'"]')?.classList.toggle('collapsed');}
|
|||
|
|
|
|||
|
|
function genDel(e){
|
|||
|
|
const physicals=e.physicals.filter(i=>i.trim());
|
|||
|
|
const aes=e.aes.filter(i=>i.trim());
|
|||
|
|
if(!physicals.length&&!aes.length)return '# Điền ít nhất 1 interface để gen config xóa...';
|
|||
|
|
const vlan=e.vlan||'11';
|
|||
|
|
const L=[];
|
|||
|
|
// each physical: delete + set access + set vlan + set mstp
|
|||
|
|
physicals.forEach(i=>{
|
|||
|
|
L.push('delete interfaces '+i);
|
|||
|
|
L.push('set interfaces '+i+' unit 0 family ethernet-switching interface-mode access');
|
|||
|
|
L.push('set interfaces '+i+' unit 0 family ethernet-switching vlan members '+vlan);
|
|||
|
|
L.push('set protocols mstp interface '+i+' no-root-port');
|
|||
|
|
});
|
|||
|
|
// each ae: delete + delete mstp
|
|||
|
|
aes.forEach(i=>{
|
|||
|
|
L.push('delete interfaces '+i);
|
|||
|
|
L.push('delete protocols mstp interface '+i);
|
|||
|
|
});
|
|||
|
|
return L.join('\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function buildDelPhys(e){
|
|||
|
|
return e.physicals.map((v,i)=>`<div class="int-row"><div class="int-badge del-p">P${i+1}</div><input class="phys-input" type="text" value="${v}" placeholder="xe-0/0/${i}" oninput="updateDel(${e.id})"><button class="btn-rm" onclick="removeDelPhys(${e.id},${i})">✕</button></div>`).join('');
|
|||
|
|
}
|
|||
|
|
function buildDelAes(e){
|
|||
|
|
return e.aes.map((v,i)=>`<div class="int-row"><div class="int-badge del-ae">AE${i+1}</div><input class="ae-input" type="text" value="${v}" placeholder="ae${i+1}" oninput="updateDel(${e.id})"><button class="btn-rm" onclick="removeDelAe(${e.id},${i})">✕</button></div>`).join('');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderDelCards(){
|
|||
|
|
const grid=document.getElementById('delGrid'),sy=window.scrollY;
|
|||
|
|
grid.innerHTML='';
|
|||
|
|
delEntries.forEach((e,idx)=>{
|
|||
|
|
const card=document.createElement('div');card.className='del-card';card.dataset.delId=e.id;
|
|||
|
|
card.innerHTML=`
|
|||
|
|
<div class="card-header" onclick="toggleDelCard(${e.id})">
|
|||
|
|
<div class="card-num del">${idx+1}</div>
|
|||
|
|
<div class="card-name ${e.name?'':'empty'}">${e.name||'(chưa đặt tên)'}</div>
|
|||
|
|
<div class="card-meta">Phys: <span class="red">${e.physicals.filter(Boolean).length||'—'}</span> | AE: <span class="red">${e.aes.filter(Boolean).length||'—'}</span></div>
|
|||
|
|
<button class="btn-danger" style="margin-left:8px;padding:4px 10px;font-size:11px" onclick="event.stopPropagation();removeDelEntry(${e.id})">✕ Xóa</button>
|
|||
|
|
<div class="chevron">▼</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="del-body">
|
|||
|
|
<div class="del-col-inputs">
|
|||
|
|
<div class="form-row" style="margin-bottom:14px">
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>Label / Ghi chú</label>
|
|||
|
|
<input type="text" data-field="name" value="${e.name}" placeholder="Server cũ / mô tả" oninput="updateDel(${e.id})">
|
|||
|
|
</div>
|
|||
|
|
<div class="form-group">
|
|||
|
|
<label>VLAN</label>
|
|||
|
|
<input type="text" data-field="vlan" value="${e.vlan}" placeholder="11" oninput="updateDel(${e.id})">
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="section-label">Physical Interfaces</div>
|
|||
|
|
${buildDelPhys(e)}
|
|||
|
|
<button class="btn-add-int red" onclick="addDelPhys(${e.id})">+ Thêm physical int</button>
|
|||
|
|
<div class="divider"></div>
|
|||
|
|
<div class="section-label">AE Interfaces</div>
|
|||
|
|
${buildDelAes(e)}
|
|||
|
|
<button class="btn-add-int red" onclick="addDelAe(${e.id})">+ Thêm AE int</button>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<div class="config-output">
|
|||
|
|
<div class="config-header"><span>🗑 Delete Config</span>
|
|||
|
|
<button class="btn-secondary" style="padding:4px 12px;font-size:11px" onclick="copySingleDel(${e.id})">📋 Copy</button>
|
|||
|
|
</div>
|
|||
|
|
<pre>${hl(genDel(e))}</pre>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
grid.appendChild(card);
|
|||
|
|
});
|
|||
|
|
window.scrollTo(0,sy);renderDelBulk();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderDelBulk(){
|
|||
|
|
const bulk=document.getElementById('delBulkSection'),pre=document.getElementById('delBulkPre');
|
|||
|
|
if(!delEntries.length){bulk.style.display='none';return;}
|
|||
|
|
bulk.style.display='block';
|
|||
|
|
pre.innerHTML=hl(delEntries.map((e,i)=>'# ===== Entry '+(i+1)+': '+(e.name||'unnamed')+' =====\n'+genDel(e)).join('\n\n'));
|
|||
|
|
}
|
|||
|
|
function copySingleDel(id){const e=delEntries.find(e=>e.id===id);if(!e)return;navigator.clipboard.writeText(genDel(e)).then(()=>toast('✓ Copied!'));}
|
|||
|
|
function copyAllDel(){if(!delEntries.length)return;navigator.clipboard.writeText(delEntries.map((e,i)=>'# ===== Entry '+(i+1)+': '+(e.name||'unnamed')+' =====\n'+genDel(e)).join('\n\n')).then(()=>toast('✓ All delete configs copied!'));}
|
|||
|
|
|
|||
|
|
function toggleBulk(bodyId, chevronId){
|
|||
|
|
const body=document.getElementById(bodyId);
|
|||
|
|
const chev=document.getElementById(chevronId);
|
|||
|
|
const collapsed=body.style.display==='none';
|
|||
|
|
body.style.display=collapsed?'':'none';
|
|||
|
|
chev.textContent=collapsed?'▼':'▶';
|
|||
|
|
}
|
|||
|
|
function toast(msg){const t=document.getElementById('toast');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2200);}
|
|||
|
|
|
|||
|
|
// ==================== ESI TAB ====================
|
|||
|
|
const ESI_DEFAULTS={vlan:'',storm:'PROFILE_10G',mode:'access',lacp:'active',adminKey:'1'};
|
|||
|
|
let esiEntries=[],esiNextId=1;
|
|||
|
|
|
|||
|
|
function addEsiEntry(data={}){
|
|||
|
|
const id=esiNextId++;
|
|||
|
|
esiEntries.push({
|
|||
|
|
id,
|
|||
|
|
name: data.name ||'',
|
|||
|
|
rack: data.rack ||'',
|
|||
|
|
u: data.u ||'',
|
|||
|
|
ae: data.ae ||'',
|
|||
|
|
sw1int: data.sw1int ||'',
|
|||
|
|
sw2int: data.sw2int ||'',
|
|||
|
|
systemId:data.systemId||'',
|
|||
|
|
vlan: data.vlan ||'',
|
|||
|
|
storm: data.storm ||'',
|
|||
|
|
mode: data.mode ||'',
|
|||
|
|
lacp: data.lacp ||'',
|
|||
|
|
adminKey:data.adminKey||'',
|
|||
|
|
});
|
|||
|
|
renderEsiCards();
|
|||
|
|
setTimeout(()=>{const el=document.querySelector('[data-esi-id="'+id+'"]');if(el)el.scrollIntoView({behavior:'smooth',block:'start'});},50);
|
|||
|
|
}
|
|||
|
|
function removeEsiEntry(id){esiEntries=esiEntries.filter(e=>e.id!==id);renderEsiCards();}
|
|||
|
|
|
|||
|
|
function syncEsi(id){
|
|||
|
|
const e=esiEntries.find(e=>e.id===id);if(!e)return;
|
|||
|
|
const card=document.querySelector('[data-esi-id="'+id+'"]');if(!card)return;
|
|||
|
|
const g=f=>card.querySelector('[data-field="'+f+'"]')?.value.trim()||'';
|
|||
|
|
e.name=g('name');e.rack=g('rack');e.u=g('u');e.ae=g('ae');
|
|||
|
|
e.sw1int=g('sw1int');e.sw2int=g('sw2int');e.systemId=g('systemId');
|
|||
|
|
e.vlan=g('vlan');e.storm=g('storm');e.mode=g('mode')||'';e.lacp=g('lacp')||'';e.adminKey=g('adminKey');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateEsi(id){
|
|||
|
|
syncEsi(id);const e=esiEntries.find(e=>e.id===id);if(!e)return;
|
|||
|
|
const card=document.querySelector('[data-esi-id="'+id+'"]');
|
|||
|
|
if(card){
|
|||
|
|
const nm=card.querySelector('.card-name');nm.textContent=e.name||'(chưa đặt tên)';nm.className='card-name'+(e.name?'':' empty');
|
|||
|
|
const metas=card.querySelectorAll('.card-meta span');
|
|||
|
|
if(metas[0])metas[0].textContent=e.rack||'—';
|
|||
|
|
if(metas[1])metas[1].textContent=e.ae||'—';
|
|||
|
|
const pres=card.querySelectorAll('pre');
|
|||
|
|
if(pres[0])pres[0].innerHTML=hl(genEsi(e,1));
|
|||
|
|
if(pres[1])pres[1].innerHTML=hl(genEsi(e,2));
|
|||
|
|
}
|
|||
|
|
renderEsiBulk();
|
|||
|
|
}
|
|||
|
|
function toggleEsiCard(id){document.querySelector('[data-esi-id="'+id+'"]')?.classList.toggle('collapsed');}
|
|||
|
|
|
|||
|
|
function genEsi(e,sw){
|
|||
|
|
const vlan = e.vlan || ESI_DEFAULTS.vlan;
|
|||
|
|
const storm = e.storm || ESI_DEFAULTS.storm;
|
|||
|
|
const mode = e.mode || ESI_DEFAULTS.mode;
|
|||
|
|
const lacp = e.lacp || ESI_DEFAULTS.lacp;
|
|||
|
|
const akey = e.adminKey|| ESI_DEFAULTS.adminKey;
|
|||
|
|
const phyInt = sw===1 ? e.sw1int : e.sw2int;
|
|||
|
|
if(!phyInt||!e.ae) return '# Điền interface và AE để sinh config...';
|
|||
|
|
const base = (e.name&&e.rack&&e.u)?`${e.name}_${e.rack}_U${e.u}`:(e.name||'SERVER');
|
|||
|
|
const vlans = vlan ? vlan.split(/[\s,]+/).map(v=>v.trim()).filter(Boolean) : [];
|
|||
|
|
const L=[];
|
|||
|
|
L.push('delete interfaces '+phyInt);
|
|||
|
|
L.push('delete protocols mstp interface '+phyInt);
|
|||
|
|
L.push('set interfaces '+phyInt+' description '+base+'_P'+sw);
|
|||
|
|
L.push('set interfaces '+phyInt+' ether-options 802.3ad '+e.ae);
|
|||
|
|
L.push('set protocols mstp interface '+e.ae+' no-root-port');
|
|||
|
|
L.push('set interfaces '+e.ae+' description '+base);
|
|||
|
|
if(e.systemId){
|
|||
|
|
L.push('set interfaces '+e.ae+' esi auto-derive lacp');
|
|||
|
|
L.push('set interfaces '+e.ae+' esi all-active');
|
|||
|
|
}
|
|||
|
|
L.push('set interfaces '+e.ae+' aggregated-ether-options lacp '+lacp);
|
|||
|
|
if(e.systemId){
|
|||
|
|
L.push('set interfaces '+e.ae+' aggregated-ether-options lacp system-id '+e.systemId);
|
|||
|
|
}
|
|||
|
|
L.push('set interfaces '+e.ae+' aggregated-ether-options lacp admin-key '+akey);
|
|||
|
|
L.push('set interfaces '+e.ae+' unit 0 family ethernet-switching interface-mode '+mode);
|
|||
|
|
vlans.forEach(v=>L.push('set interfaces '+e.ae+' unit 0 family ethernet-switching vlan members '+v));
|
|||
|
|
L.push('set interfaces '+e.ae+' unit 0 family ethernet-switching storm-control '+storm);
|
|||
|
|
return L.join('\n');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderEsiCards(){
|
|||
|
|
const grid=document.getElementById('esiGrid'),sy=window.scrollY;
|
|||
|
|
grid.innerHTML='';
|
|||
|
|
esiEntries.forEach((e,idx)=>{
|
|||
|
|
const card=document.createElement('div');card.className='esi-card';card.dataset.esiId=e.id;
|
|||
|
|
card.innerHTML=`
|
|||
|
|
<div class="card-header" onclick="toggleEsiCard(${e.id})">
|
|||
|
|
<div class="card-num esi">${idx+1}</div>
|
|||
|
|
<div class="card-name ${e.name?'':'empty'}">${e.name||'(chưa đặt tên)'}</div>
|
|||
|
|
<div class="card-meta">Rack: <span>${e.rack||'—'}</span> | AE: <span>${e.ae||'—'}</span> | SysID: <span>${e.systemId||'—'}</span></div>
|
|||
|
|
<button class="btn-danger" style="margin-left:8px;padding:4px 10px;font-size:11px" onclick="event.stopPropagation();removeEsiEntry(${e.id})">✕ Xóa</button>
|
|||
|
|
<div class="chevron">▼</div>
|
|||
|
|
</div>
|
|||
|
|
<div class="esi-body">
|
|||
|
|
<div style="display:grid;grid-template-columns:2fr 1fr 0.6fr 0.8fr;gap:8px;margin-bottom:10px">
|
|||
|
|
<div class="form-group"><label>Server Name / Serial</label><input type="text" data-field="name" value="${e.name}" placeholder="GOG3NE812A0001" oninput="updateEsi(${e.id})"></div>
|
|||
|
|
<div class="form-group"><label>Rack</label><input type="text" data-field="rack" value="${e.rack}" placeholder="2BO26" oninput="updateEsi(${e.id})"></div>
|
|||
|
|
<div class="form-group"><label>U</label><input type="text" data-field="u" value="${e.u}" placeholder="7" oninput="updateEsi(${e.id})"></div>
|
|||
|
|
<div class="form-group"><label>AE</label><input type="text" data-field="ae" value="${e.ae}" placeholder="ae31" oninput="updateEsi(${e.id})"></div>
|
|||
|
|
</div>
|
|||
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:12px">
|
|||
|
|
<div style="display:flex;flex-direction:column;gap:10px">
|
|||
|
|
<div class="form-group"><label>SW1 Interface (P1)</label><input type="text" data-field="sw1int" value="${e.sw1int}" placeholder="xe-0/0/37" oninput="updateEsi(${e.id})"></div>
|
|||
|
|
<div class="form-group"><label>SW2 Interface (P2)</label><input type="text" data-field="sw2int" value="${e.sw2int}" placeholder="xe-0/0/37" oninput="updateEsi(${e.id})"></div>
|
|||
|
|
<div class="form-group"><label>Storm Profile</label><input type="text" data-field="storm" value="${e.storm}" placeholder="${ESI_DEFAULTS.storm}" oninput="updateEsi(${e.id})"></div>
|
|||
|
|
<div class="form-group"><label>Interface Mode</label>
|
|||
|
|
<select data-field="mode" onchange="updateEsi(${e.id})">
|
|||
|
|
<option value="" ${!e.mode?'selected':''}>— default (${ESI_DEFAULTS.mode}) —</option>
|
|||
|
|
<option value="access" ${e.mode==='access'?'selected':''}>access</option>
|
|||
|
|
<option value="trunk" ${e.mode==='trunk'?'selected':''}>trunk</option>
|
|||
|
|
</select>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div style="display:flex;flex-direction:column;gap:10px">
|
|||
|
|
<div class="form-group"><label>System ID (LACP)</label><input type="text" data-field="systemId" value="${e.systemId}" placeholder="01:00:00:00:02:15" oninput="updateEsi(${e.id})"></div>
|
|||
|
|
<div class="form-group"><label>VLAN Members</label><input type="text" data-field="vlan" value="${e.vlan}" placeholder="${ESI_DEFAULTS.vlan||'2906'}" oninput="updateEsi(${e.id})"></div>
|
|||
|
|
<div class="form-group"><label>Admin Key</label><input type="text" data-field="adminKey" value="${e.adminKey}" placeholder="1" oninput="updateEsi(${e.id})"></div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px">
|
|||
|
|
<div class="config-output">
|
|||
|
|
<div class="config-header sw1">
|
|||
|
|
<span>🔵 SW1 Config</span>
|
|||
|
|
<button class="btn-secondary" style="padding:4px 12px;font-size:11px" onclick="copySingleEsi(${e.id},1)">📋 Copy</button>
|
|||
|
|
</div>
|
|||
|
|
<pre>${hl(genEsi(e,1))}</pre>
|
|||
|
|
</div>
|
|||
|
|
<div class="config-output">
|
|||
|
|
<div class="config-header sw2">
|
|||
|
|
<span>🔴 SW2 Config</span>
|
|||
|
|
<button class="btn-secondary" style="padding:4px 12px;font-size:11px" onclick="copySingleEsi(${e.id},2)">📋 Copy</button>
|
|||
|
|
</div>
|
|||
|
|
<pre>${hl(genEsi(e,2))}</pre>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>`;
|
|||
|
|
grid.appendChild(card);
|
|||
|
|
});
|
|||
|
|
window.scrollTo(0,sy);renderEsiBulk();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderEsiBulk(){
|
|||
|
|
const bulk=document.getElementById('esiBulkSection'),pre=document.getElementById('esiBulkPre');
|
|||
|
|
if(!esiEntries.length){bulk.style.display='none';return;}
|
|||
|
|
bulk.style.display='block';
|
|||
|
|
pre.innerHTML=hl(esiEntries.map((e,i)=>{
|
|||
|
|
return '# ===== ESI '+(i+1)+': '+(e.name||'unnamed')+' — SW1 =====\n'+genEsi(e,1)+'\n\n'
|
|||
|
|
+ '# ===== ESI '+(i+1)+': '+(e.name||'unnamed')+' — SW2 =====\n'+genEsi(e,2);
|
|||
|
|
}).join('\n\n'));
|
|||
|
|
}
|
|||
|
|
function copySingleEsi(id,sw){
|
|||
|
|
const e=esiEntries.find(e=>e.id===id);if(!e)return;
|
|||
|
|
navigator.clipboard.writeText(genEsi(e,sw)).then(()=>toast('✓ SW'+sw+' copied!'));
|
|||
|
|
}
|
|||
|
|
function copyAllEsi(){
|
|||
|
|
if(!esiEntries.length)return;
|
|||
|
|
const txt=esiEntries.map((e,i)=>{
|
|||
|
|
return '# ===== ESI '+(i+1)+': '+(e.name||'unnamed')+' — SW1 =====\n'+genEsi(e,1)+'\n\n'
|
|||
|
|
+ '# ===== ESI '+(i+1)+': '+(e.name||'unnamed')+' — SW2 =====\n'+genEsi(e,2);
|
|||
|
|
}).join('\n\n');
|
|||
|
|
navigator.clipboard.writeText(txt).then(()=>toast('✓ All ESI configs copied!'));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Sample data
|
|||
|
|
addServer({name:'210235K0UQ6256V00022',rack:'2BG10',u:'23',ae:'ae38',interfaces:['xe-0/0/18','xe-1/0/18']});
|
|||
|
|
addServer({name:'210235K0UQ6256V00021',rack:'2BG26',u:'13',ae:'ae33',interfaces:['xe-0/0/14','xe-1/0/14']});
|
|||
|
|
addDelEntry({name:'Server cũ example',physicals:['xe-0/0/18','xe-1/0/18'],aes:['ae38','ae39']});
|
|||
|
|
addEsiEntry({name:'GOG3NE812A0001',rack:'2BO26',u:'7',ae:'ae31',sw1int:'xe-0/0/37',sw2int:'xe-0/0/37',systemId:'01:00:00:00:02:15',vlan:'100',adminKey:'1'});
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|