Juniper_GenConfig/juniper-config-portal.html
2026-03-17 04:36:15 +00:00

710 lines
40 KiB
HTML
Raw Permalink 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="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:&nbsp;<span>${s.rack||'—'}</span>&nbsp;|&nbsp;AE:&nbsp;<span>${s.ae||'—'}</span>&nbsp;|&nbsp;Ports:&nbsp;<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:&nbsp;<span class="red">${e.physicals.filter(Boolean).length||'—'}</span>&nbsp;|&nbsp;AE:&nbsp;<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:&nbsp;<span>${e.rack||'—'}</span>&nbsp;|&nbsp;AE:&nbsp;<span>${e.ae||'—'}</span>&nbsp;|&nbsp;SysID:&nbsp;<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>