|
|
@@ -366,6 +366,10 @@ function navigate(tabName) {
|
|
|
sdwan: { title: 'Software-Defined WAN (SD-WAN)', sub: 'Multi-link aggregation, traffic load balancing & quality SLA health monitoring' },
|
|
|
bgp: { title: 'BGP Dynamic Routing', sub: 'BGP multi-instance peering sessions, advertised networks, and routing table (RIB)' },
|
|
|
ospf: { title: 'OSPF Dynamic Routing', sub: 'OSPFv2 link-state routing, dynamic neighbor adjacencies, and multi-area topology' },
|
|
|
+ rip: { title: 'RIP Dynamic Routing', sub: 'Routing Information Protocol (RIPv2 IPv4 & RIPng IPv6) hop-count distance-vector routing' },
|
|
|
+ bfd: { title: 'BFD Fast Session Detection', sub: 'Bidirectional Forwarding Detection (BFD) sub-second link failure detection for BGP and OSPF' },
|
|
|
+ babel: { title: 'Babel Dynamic Routing', sub: 'Babel loop-avoiding distance-vector routing for wired and wireless mesh networks (RFC 8966)' },
|
|
|
+ radv: { title: 'IPv6 Router Advertisements (RAdv)', sub: 'BIRD 2 ICMPv6 Router Advertisement daemon for IPv6 SLAAC autoconfiguration and RDNSS' },
|
|
|
'routing-filters': { title: 'Routing Policy Filters', sub: 'Route filter chains, prefix matching, BGP community maps & policy-based path control' },
|
|
|
'dynamic-routing': { title: 'BGP Dynamic Routing', sub: 'Dynamic routing engine and peering sessions' },
|
|
|
ppp: { title: 'PPP Servers & Secrets', sub: 'Subscriber authentication, dynamic IP pools and VPN access concentrators' },
|
|
|
@@ -421,6 +425,22 @@ function navigate(tabName) {
|
|
|
loadDynamicRouting();
|
|
|
switchOspfTab(state.activeOspfTab || 'instances');
|
|
|
}
|
|
|
+ if (tabName === 'rip') {
|
|
|
+ loadDynamicRouting();
|
|
|
+ switchRipTab(state.activeRipTab || 'instances');
|
|
|
+ }
|
|
|
+ if (tabName === 'bfd') {
|
|
|
+ loadDynamicRouting();
|
|
|
+ switchBfdTab(state.activeBfdTab || 'config');
|
|
|
+ }
|
|
|
+ if (tabName === 'babel') {
|
|
|
+ loadDynamicRouting();
|
|
|
+ switchBabelTab(state.activeBabelTab || 'instances');
|
|
|
+ }
|
|
|
+ if (tabName === 'radv') {
|
|
|
+ loadDynamicRouting();
|
|
|
+ switchRadvTab(state.activeRadvTab || 'instances');
|
|
|
+ }
|
|
|
if (tabName === 'routing-filters') loadDynamicRouting();
|
|
|
if (tabName === 'dynamic-routing') loadDynamicRouting();
|
|
|
if (tabName === 'ppp') loadPPP();
|
|
|
@@ -539,7 +559,7 @@ async function refreshCoreData(isManual = false) {
|
|
|
if (state.activeTab === 'vlans') renderVlansList();
|
|
|
if (state.activeTab === 'firewall') await loadFirewall(false);
|
|
|
if (state.activeTab === 'routes') await loadRoutes(false);
|
|
|
- if (state.activeTab === 'dynamic-routing' || state.activeTab === 'bgp' || state.activeTab === 'ospf') await loadDynamicRouting(false);
|
|
|
+ if (state.activeTab === 'dynamic-routing' || state.activeTab === 'bgp' || state.activeTab === 'ospf' || state.activeTab === 'rip' || state.activeTab === 'bfd' || state.activeTab === 'babel' || state.activeTab === 'radv') await loadDynamicRouting(false);
|
|
|
if (state.activeTab === 'ppp') await loadPPP(false);
|
|
|
if (state.activeTab === 'eoip') await loadEoIP(false);
|
|
|
if (state.activeTab === 'vxlan') await loadVXLAN(false);
|
|
|
@@ -780,10 +800,23 @@ function updateBadges() {
|
|
|
const d = state.dynSummary;
|
|
|
setDotBadge('badge-bgp-dot', d.bgp_running);
|
|
|
setDotBadge('badge-ospf-dot', d.ospf_running);
|
|
|
+ setDotBadge('badge-rip-dot', d.rip_running);
|
|
|
+ setDotBadge('badge-bfd-dot', d.bfd_running);
|
|
|
+ setDotBadge('badge-babel-dot', d.babel_running);
|
|
|
+ setDotBadge('badge-radv-dot', d.radv_running);
|
|
|
+
|
|
|
const bBgp = document.getElementById('badge-bgp-count');
|
|
|
if (bBgp) bBgp.innerText = (state.bgpPeers || []).length;
|
|
|
const bOspf = document.getElementById('badge-ospf-count');
|
|
|
if (bOspf) bOspf.innerText = (state.ospfInterfaces || []).length;
|
|
|
+ const bRip = document.getElementById('badge-rip-count');
|
|
|
+ if (bRip) bRip.innerText = (state.ripInterfaces || []).length;
|
|
|
+ const bBfd = document.getElementById('badge-bfd-count');
|
|
|
+ if (bBfd) bBfd.innerText = (state.bfdInterfaces || []).length;
|
|
|
+ const bBabel = document.getElementById('badge-babel-count');
|
|
|
+ if (bBabel) bBabel.innerText = (state.babelInterfaces || []).length;
|
|
|
+ const bRadv = document.getElementById('badge-radv-count');
|
|
|
+ if (bRadv) bRadv.innerText = (state.radvInterfaces || []).length;
|
|
|
const bFilters = document.getElementById('badge-filters-count');
|
|
|
if (bFilters) bFilters.innerText = (state.routingFilters || []).length;
|
|
|
}
|
|
|
@@ -9798,7 +9831,11 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
const [
|
|
|
sum, bgpInst, bgpPeers, bgpNets, bgpRoutes,
|
|
|
ospfInsts, ospfAreas, ospfRanges, ospfTemplates, ospfLiveIfaces, ospfStaticNbrs, ospfNbrs, ospfLsa, filters,
|
|
|
- selectRules, commLists, asLists
|
|
|
+ selectRules, commLists, asLists,
|
|
|
+ ripInsts, ripIfaces, ripNbrsLive, ripRoutes,
|
|
|
+ bfdInst, bfdIfaces, bfdPeers, bfdSessions,
|
|
|
+ babelInsts, babelIfaces, babelNbrs, babelRoutes,
|
|
|
+ radvInsts, radvIfaces
|
|
|
] = await Promise.all([
|
|
|
fetchSafeJson(`${API_BASE}/routing/dynamic/summary`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/bgp/instance`),
|
|
|
@@ -9815,7 +9852,21 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
fetchSafeJson(`${API_BASE}/routing/ospf/lsa`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/filters`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/filters/community-lists`),
|
|
|
- fetchSafeJson(`${API_BASE}/routing/filters/as-lists`)
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/filters/as-lists`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/rip/instances`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/rip/interfaces`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/rip/neighbors-live`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/rip/routes`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/bfd/instance`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/bfd/interfaces`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/bfd/peers`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/bfd/sessions`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/babel/instances`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/babel/interfaces`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/babel/neighbors`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/babel/routes`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/radv/instances`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/radv/interfaces`)
|
|
|
]);
|
|
|
|
|
|
if (sum) state.dynSummary = sum;
|
|
|
@@ -9835,6 +9886,24 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
if (commLists) state.routingCommunityLists = commLists;
|
|
|
if (asLists) state.routingASLists = asLists;
|
|
|
|
|
|
+ if (ripInsts) state.ripInstances = ripInsts;
|
|
|
+ if (ripIfaces) state.ripInterfaces = ripIfaces;
|
|
|
+ if (ripNbrsLive) state.ripNeighborsLive = ripNbrsLive;
|
|
|
+ if (ripRoutes) state.ripRoutes = ripRoutes;
|
|
|
+
|
|
|
+ if (bfdInst) state.bfdInstance = bfdInst;
|
|
|
+ if (bfdIfaces) state.bfdInterfaces = bfdIfaces;
|
|
|
+ if (bfdPeers) state.bfdNeighbors = bfdPeers;
|
|
|
+ if (bfdSessions) state.bfdSessions = bfdSessions;
|
|
|
+
|
|
|
+ if (babelInsts) state.babelInstances = babelInsts;
|
|
|
+ if (babelIfaces) state.babelInterfaces = babelIfaces;
|
|
|
+ if (babelNbrs) state.babelNeighbors = babelNbrs;
|
|
|
+ if (babelRoutes) state.babelRoutes = babelRoutes;
|
|
|
+
|
|
|
+ if (radvInsts) state.radvInstances = radvInsts;
|
|
|
+ if (radvIfaces) state.radvInterfaces = radvIfaces;
|
|
|
+
|
|
|
try { renderBgpStatusPill(); } catch(e) { console.error('renderBgpStatusPill error:', e); }
|
|
|
try { renderBgpPeers(); } catch(e) { console.error('renderBgpPeers error:', e); }
|
|
|
try { renderBgpNetworks(); } catch(e) { console.error('renderBgpNetworks error:', e); }
|
|
|
@@ -9849,6 +9918,24 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
try { renderOspfNeighbors(); } catch(e) { console.error('renderOspfNeighbors error:', e); }
|
|
|
try { renderOspfLSA(); } catch(e) { console.error('renderOspfLSA error:', e); }
|
|
|
|
|
|
+ try { renderRipInstances(); } catch(e) { console.error('renderRipInstances error:', e); }
|
|
|
+ try { renderRipInterfaces(); } catch(e) { console.error('renderRipInterfaces error:', e); }
|
|
|
+ try { renderRipNeighbors(); } catch(e) { console.error('renderRipNeighbors error:', e); }
|
|
|
+ try { renderRipRoutes(); } catch(e) { console.error('renderRipRoutes error:', e); }
|
|
|
+
|
|
|
+ try { renderBfdConfig(); } catch(e) { console.error('renderBfdConfig error:', e); }
|
|
|
+ try { renderBfdInterfaces(); } catch(e) { console.error('renderBfdInterfaces error:', e); }
|
|
|
+ try { renderBfdPeers(); } catch(e) { console.error('renderBfdPeers error:', e); }
|
|
|
+ try { renderBfdSessions(); } catch(e) { console.error('renderBfdSessions error:', e); }
|
|
|
+
|
|
|
+ try { renderBabelInstances(); } catch(e) { console.error('renderBabelInstances error:', e); }
|
|
|
+ try { renderBabelInterfaces(); } catch(e) { console.error('renderBabelInterfaces error:', e); }
|
|
|
+ try { renderBabelNeighbors(); } catch(e) { console.error('renderBabelNeighbors error:', e); }
|
|
|
+ try { renderBabelRoutes(); } catch(e) { console.error('renderBabelRoutes error:', e); }
|
|
|
+
|
|
|
+ try { renderRAdvInstances(); } catch(e) { console.error('renderRAdvInstances error:', e); }
|
|
|
+ try { renderRAdvInterfaces(); } catch(e) { console.error('renderRAdvInterfaces error:', e); }
|
|
|
+
|
|
|
try { renderRoutingFilters(); } catch(e) { console.error('renderRoutingFilters error:', e); }
|
|
|
try { updateBadges(); } catch(e) { console.error('updateBadges error:', e); }
|
|
|
|
|
|
@@ -19493,3 +19580,1296 @@ async function deleteSDWANMember(groupId, iface) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// ==============================================================================
|
|
|
+// DYNAMIC ROUTING: RIP MODULE
|
|
|
+// ==============================================================================
|
|
|
+state.ripInstances = [];
|
|
|
+state.ripInterfaces = [];
|
|
|
+state.ripNeighborsLive = [];
|
|
|
+state.ripRoutes = [];
|
|
|
+state.selectedRipRow = {};
|
|
|
+state.activeRipTab = 'instances';
|
|
|
+
|
|
|
+function switchRipTab(tab) {
|
|
|
+ state.activeRipTab = tab;
|
|
|
+ ['instances', 'interfaces', 'neighbors', 'routes'].forEach(t => {
|
|
|
+ const pane = document.getElementById(`rip-pane-${t}`);
|
|
|
+ const btn = document.getElementById(`rip-tab-btn-${t}`);
|
|
|
+ if (pane) {
|
|
|
+ if (t === tab) pane.classList.remove('hidden');
|
|
|
+ else pane.classList.add('hidden');
|
|
|
+ }
|
|
|
+ if (btn) {
|
|
|
+ if (t === tab) {
|
|
|
+ btn.className = 'px-3 py-2 text-xs font-semibold border-b-2 border-rose-500 text-white transition flex items-center gap-1.5 whitespace-nowrap';
|
|
|
+ } else {
|
|
|
+ btn.className = 'px-3 py-2 text-xs font-semibold border-b-2 border-transparent text-slate-400 hover:text-white transition flex items-center gap-1.5 whitespace-nowrap';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function selectRipRow(tab, id) {
|
|
|
+ if (!state.selectedRipRow) state.selectedRipRow = {};
|
|
|
+ state.selectedRipRow[tab] = id;
|
|
|
+ if (tab === 'instances') {
|
|
|
+ renderRipInstances();
|
|
|
+ const btnEn = document.getElementById('btn-rip-inst-enable');
|
|
|
+ const btnDis = document.getElementById('btn-rip-inst-disable');
|
|
|
+ const btnRem = document.getElementById('btn-rip-inst-remove');
|
|
|
+ if (btnEn) btnEn.disabled = false;
|
|
|
+ if (btnDis) btnDis.disabled = false;
|
|
|
+ if (btnRem) btnRem.disabled = false;
|
|
|
+ } else if (tab === 'interfaces') {
|
|
|
+ renderRipInterfaces();
|
|
|
+ const btnEn = document.getElementById('btn-rip-iface-enable');
|
|
|
+ const btnDis = document.getElementById('btn-rip-iface-disable');
|
|
|
+ const btnRem = document.getElementById('btn-rip-iface-remove');
|
|
|
+ if (btnEn) btnEn.disabled = false;
|
|
|
+ if (btnDis) btnDis.disabled = false;
|
|
|
+ if (btnRem) btnRem.disabled = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderRipInstances() {
|
|
|
+ const tbody = document.getElementById('rip-instances-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('rip-instances-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-rip-instances')?.value || '').toLowerCase();
|
|
|
+ let list = state.ripInstances || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(i => (i.name || '').toLowerCase().includes(filter) || (i.comment || '').toLowerCase().includes(filter));
|
|
|
+ }
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ const selId = state.selectedRipRow?.instances;
|
|
|
+ tbody.innerHTML = list.map((inst, idx) => {
|
|
|
+ const isSel = inst.id === selId;
|
|
|
+ const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-rose-400' : 'hover:bg-[#1a2636] text-slate-300';
|
|
|
+ const flagD = inst.disabled ? '<span class="text-rose-400 font-bold" title="Disabled">X</span>' : '<span class="text-emerald-400">R</span>';
|
|
|
+ const redist = [];
|
|
|
+ if (inst.redistribute_connected) redist.push('conn');
|
|
|
+ if (inst.redistribute_static) redist.push('static');
|
|
|
+ if (inst.redistribute_bgp) redist.push('bgp');
|
|
|
+ if (inst.redistribute_ospf) redist.push('ospf');
|
|
|
+ if (inst.redistribute_default) redist.push('default');
|
|
|
+ return `
|
|
|
+ <tr class="${selClass} cursor-pointer transition select-none" onclick="selectRipRow('instances', '${inst.id}')" ondblclick="openEditRipInstanceModal('${inst.id}')">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 w-12 text-center">${flagD}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(inst.comment || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-semibold text-white flex items-center gap-1.5">
|
|
|
+ <span class="text-rose-400">↳</span> <span>${escapeHtml(inst.name || 'default')}</span>
|
|
|
+ </td>
|
|
|
+ <td class="py-1.5 px-3 uppercase text-rose-300 font-bold">${escapeHtml(inst.version || 'rip2')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${inst.update_time || 30}s</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${inst.timeout_time || 180}s</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-cyan-300">${redist.join(', ') || 'none'}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${inst.default_metric || 1}</td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function renderRipInterfaces() {
|
|
|
+ const tbody = document.getElementById('rip-interfaces-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('rip-interfaces-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-rip-interfaces')?.value || '').toLowerCase();
|
|
|
+ let list = state.ripInterfaces || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(i => (i.interface || '').toLowerCase().includes(filter) || (i.comment || '').toLowerCase().includes(filter));
|
|
|
+ }
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ const selId = state.selectedRipRow?.interfaces;
|
|
|
+ tbody.innerHTML = list.map((iface, idx) => {
|
|
|
+ const isSel = iface.id === selId;
|
|
|
+ const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-rose-400' : 'hover:bg-[#1a2636] text-slate-300';
|
|
|
+ const flagD = iface.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '<span class="text-emerald-400">R</span>';
|
|
|
+ return `
|
|
|
+ <tr class="${selClass} cursor-pointer transition select-none" onclick="selectRipRow('interfaces', '${iface.id}')" ondblclick="openEditRipInterfaceModal('${iface.id}')">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 w-12 text-center">${flagD}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(iface.comment || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-semibold text-white">${escapeHtml(iface.interface || '*')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-300">${escapeHtml(iface.mode || 'multicast')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${iface.metric || 1}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${iface.passive ? 'yes' : 'no'}</td>
|
|
|
+ <td class="py-1.5 px-3">${iface.use_bfd ? '<span class="text-amber-400 font-bold">yes</span>' : '<span class="text-slate-500">no</span>'}</td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function renderRipNeighbors() {
|
|
|
+ const tbody = document.getElementById('rip-neighbors-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('rip-neighbors-footer-count');
|
|
|
+ const list = state.ripNeighborsLive || [];
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ tbody.innerHTML = list.map((nbr, idx) => `
|
|
|
+ <tr class="hover:bg-[#1a2636] text-slate-300">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-semibold text-rose-300">${escapeHtml(nbr.address || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${escapeHtml(nbr.interface || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${escapeHtml(nbr.last_seen || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${escapeHtml(nbr.uptime || '')}</td>
|
|
|
+ </tr>
|
|
|
+ `).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function renderRipRoutes() {
|
|
|
+ const tbody = document.getElementById('rip-routes-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('rip-routes-footer-count');
|
|
|
+ const list = state.ripRoutes || [];
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ tbody.innerHTML = list.map((r, idx) => `
|
|
|
+ <tr class="hover:bg-[#1a2636] text-slate-300">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-semibold text-white">${escapeHtml(r.prefix || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-cyan-300">${escapeHtml(r.next_hop || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${r.metric || 1}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${escapeHtml(r.interface || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${escapeHtml(r.uptime || '')}</td>
|
|
|
+ </tr>
|
|
|
+ `).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function openRipInstanceModal() {
|
|
|
+ document.getElementById('rip-instance-modal-title').innerText = 'New RIP Instance';
|
|
|
+ document.getElementById('modal-rip-instance-id').value = '';
|
|
|
+ document.getElementById('modal-rip-instance-name').value = 'rip1';
|
|
|
+ document.getElementById('modal-rip-instance-version').value = 'rip2';
|
|
|
+ document.getElementById('modal-rip-instance-update').value = '30';
|
|
|
+ document.getElementById('modal-rip-instance-timeout').value = '180';
|
|
|
+ document.getElementById('modal-rip-instance-garbage').value = '120';
|
|
|
+ document.getElementById('modal-rip-instance-metric').value = '1';
|
|
|
+ document.getElementById('modal-rip-instance-comment').value = '';
|
|
|
+ document.getElementById('modal-rip-instance-enabled').checked = true;
|
|
|
+ document.getElementById('modal-rip-instance-redist-conn').checked = false;
|
|
|
+ document.getElementById('modal-rip-instance-redist-static').checked = false;
|
|
|
+ document.getElementById('modal-rip-instance-redist-bgp').checked = false;
|
|
|
+ document.getElementById('modal-rip-instance-redist-ospf').checked = false;
|
|
|
+ document.getElementById('modal-rip-instance-redist-default').checked = false;
|
|
|
+ document.getElementById('rip-instance-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditRipInstanceModal(id) {
|
|
|
+ const inst = (state.ripInstances || []).find(i => i.id === id);
|
|
|
+ if (!inst) return;
|
|
|
+ document.getElementById('rip-instance-modal-title').innerText = `Edit RIP Instance: ${inst.name}`;
|
|
|
+ document.getElementById('modal-rip-instance-id').value = inst.id;
|
|
|
+ document.getElementById('modal-rip-instance-name').value = inst.name || '';
|
|
|
+ document.getElementById('modal-rip-instance-version').value = inst.version || 'rip2';
|
|
|
+ document.getElementById('modal-rip-instance-update').value = inst.update_time || 30;
|
|
|
+ document.getElementById('modal-rip-instance-timeout').value = inst.timeout_time || 180;
|
|
|
+ document.getElementById('modal-rip-instance-garbage').value = inst.garbage_time || 120;
|
|
|
+ document.getElementById('modal-rip-instance-metric').value = inst.default_metric || 1;
|
|
|
+ document.getElementById('modal-rip-instance-comment').value = inst.comment || '';
|
|
|
+ document.getElementById('modal-rip-instance-enabled').checked = !inst.disabled;
|
|
|
+ document.getElementById('modal-rip-instance-redist-conn').checked = !!inst.redistribute_connected;
|
|
|
+ document.getElementById('modal-rip-instance-redist-static').checked = !!inst.redistribute_static;
|
|
|
+ document.getElementById('modal-rip-instance-redist-bgp').checked = !!inst.redistribute_bgp;
|
|
|
+ document.getElementById('modal-rip-instance-redist-ospf').checked = !!inst.redistribute_ospf;
|
|
|
+ document.getElementById('modal-rip-instance-redist-default').checked = !!inst.redistribute_default;
|
|
|
+ document.getElementById('rip-instance-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeRipInstanceModal() {
|
|
|
+ document.getElementById('rip-instance-modal')?.classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitRipInstance(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const id = document.getElementById('modal-rip-instance-id').value;
|
|
|
+ const payload = {
|
|
|
+ id: id || undefined,
|
|
|
+ name: document.getElementById('modal-rip-instance-name').value.trim(),
|
|
|
+ version: document.getElementById('modal-rip-instance-version').value,
|
|
|
+ update_time: parseInt(document.getElementById('modal-rip-instance-update').value, 10) || 30,
|
|
|
+ timeout_time: parseInt(document.getElementById('modal-rip-instance-timeout').value, 10) || 180,
|
|
|
+ garbage_time: parseInt(document.getElementById('modal-rip-instance-garbage').value, 10) || 120,
|
|
|
+ default_metric: parseInt(document.getElementById('modal-rip-instance-metric').value, 10) || 1,
|
|
|
+ comment: document.getElementById('modal-rip-instance-comment').value.trim(),
|
|
|
+ disabled: !document.getElementById('modal-rip-instance-enabled').checked,
|
|
|
+ redistribute_connected: document.getElementById('modal-rip-instance-redist-conn').checked,
|
|
|
+ redistribute_static: document.getElementById('modal-rip-instance-redist-static').checked,
|
|
|
+ redistribute_bgp: document.getElementById('modal-rip-instance-redist-bgp').checked,
|
|
|
+ redistribute_ospf: document.getElementById('modal-rip-instance-redist-ospf').checked,
|
|
|
+ redistribute_default: document.getElementById('modal-rip-instance-redist-default').checked
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/rip/instances`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (!res.ok) throw new Error(await res.text());
|
|
|
+ closeRipInstanceModal();
|
|
|
+ showToast('Saved', `RIP instance ${payload.name} saved`, 'success');
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function enableSelectedRipInstance() {
|
|
|
+ const id = state.selectedRipRow?.instances;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/rip/instances/${id}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: false }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function disableSelectedRipInstance() {
|
|
|
+ const id = state.selectedRipRow?.instances;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/rip/instances/${id}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: true }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function removeSelectedRipInstance() {
|
|
|
+ const id = state.selectedRipRow?.instances;
|
|
|
+ if (!id || !confirm('Are you sure you want to delete this RIP instance?')) return;
|
|
|
+ await fetch(`${API_BASE}/routing/rip/instances/${id}`, { method: 'DELETE' });
|
|
|
+ state.selectedRipRow.instances = null;
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+function openRipInterfaceModal() {
|
|
|
+ document.getElementById('rip-interface-modal-title').innerText = 'New RIP Interface';
|
|
|
+ document.getElementById('modal-rip-iface-id').value = '';
|
|
|
+ document.getElementById('modal-rip-iface-pattern').value = '';
|
|
|
+ document.getElementById('modal-rip-iface-instance').value = 'rip1';
|
|
|
+ document.getElementById('modal-rip-iface-mode').value = 'multicast';
|
|
|
+ document.getElementById('modal-rip-iface-metric').value = '1';
|
|
|
+ document.getElementById('modal-rip-iface-comment').value = '';
|
|
|
+ document.getElementById('modal-rip-iface-enabled').checked = true;
|
|
|
+ document.getElementById('modal-rip-iface-passive').checked = false;
|
|
|
+ document.getElementById('modal-rip-iface-bfd').checked = false;
|
|
|
+ document.getElementById('rip-interface-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditRipInterfaceModal(id) {
|
|
|
+ const iface = (state.ripInterfaces || []).find(i => i.id === id);
|
|
|
+ if (!iface) return;
|
|
|
+ document.getElementById('rip-interface-modal-title').innerText = `Edit RIP Interface: ${iface.interface}`;
|
|
|
+ document.getElementById('modal-rip-iface-id').value = iface.id;
|
|
|
+ document.getElementById('modal-rip-iface-pattern').value = iface.interface || '';
|
|
|
+ document.getElementById('modal-rip-iface-instance').value = iface.instance || '';
|
|
|
+ document.getElementById('modal-rip-iface-mode').value = iface.mode || 'multicast';
|
|
|
+ document.getElementById('modal-rip-iface-metric').value = iface.metric || 1;
|
|
|
+ document.getElementById('modal-rip-iface-comment').value = iface.comment || '';
|
|
|
+ document.getElementById('modal-rip-iface-enabled').checked = !iface.disabled;
|
|
|
+ document.getElementById('modal-rip-iface-passive').checked = !!iface.passive;
|
|
|
+ document.getElementById('modal-rip-iface-bfd').checked = !!iface.use_bfd;
|
|
|
+ document.getElementById('rip-interface-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeRipInterfaceModal() {
|
|
|
+ document.getElementById('rip-interface-modal')?.classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitRipInterface(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const id = document.getElementById('modal-rip-iface-id').value;
|
|
|
+ const payload = {
|
|
|
+ id: id || undefined,
|
|
|
+ interface: document.getElementById('modal-rip-iface-pattern').value.trim(),
|
|
|
+ instance: document.getElementById('modal-rip-iface-instance').value.trim(),
|
|
|
+ mode: document.getElementById('modal-rip-iface-mode').value,
|
|
|
+ metric: parseInt(document.getElementById('modal-rip-iface-metric').value, 10) || 1,
|
|
|
+ comment: document.getElementById('modal-rip-iface-comment').value.trim(),
|
|
|
+ disabled: !document.getElementById('modal-rip-iface-enabled').checked,
|
|
|
+ passive: document.getElementById('modal-rip-iface-passive').checked,
|
|
|
+ use_bfd: document.getElementById('modal-rip-iface-bfd').checked
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/rip/interfaces`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (!res.ok) throw new Error(await res.text());
|
|
|
+ closeRipInterfaceModal();
|
|
|
+ showToast('Saved', `RIP interface template saved`, 'success');
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function enableSelectedRipInterface() {
|
|
|
+ const id = state.selectedRipRow?.interfaces;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/rip/interfaces/${id}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: false }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function disableSelectedRipInterface() {
|
|
|
+ const id = state.selectedRipRow?.interfaces;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/rip/interfaces/${id}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: true }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function removeSelectedRipInterface() {
|
|
|
+ const id = state.selectedRipRow?.interfaces;
|
|
|
+ if (!id || !confirm('Are you sure you want to delete this RIP interface template?')) return;
|
|
|
+ await fetch(`${API_BASE}/routing/rip/interfaces/${id}`, { method: 'DELETE' });
|
|
|
+ state.selectedRipRow.interfaces = null;
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+// ==============================================================================
|
|
|
+// DYNAMIC ROUTING: BFD MODULE
|
|
|
+// ==============================================================================
|
|
|
+state.bfdInstance = null;
|
|
|
+state.bfdInterfaces = [];
|
|
|
+state.bfdNeighbors = [];
|
|
|
+state.bfdSessions = [];
|
|
|
+state.selectedBfdRow = {};
|
|
|
+state.activeBfdTab = 'config';
|
|
|
+
|
|
|
+function switchBfdTab(tab) {
|
|
|
+ state.activeBfdTab = tab;
|
|
|
+ ['config', 'interfaces', 'peers', 'sessions'].forEach(t => {
|
|
|
+ const pane = document.getElementById(`bfd-pane-${t}`);
|
|
|
+ const btn = document.getElementById(`bfd-tab-btn-${t}`);
|
|
|
+ if (pane) {
|
|
|
+ if (t === tab) pane.classList.remove('hidden');
|
|
|
+ else pane.classList.add('hidden');
|
|
|
+ }
|
|
|
+ if (btn) {
|
|
|
+ if (t === tab) {
|
|
|
+ btn.className = 'px-3 py-2 text-xs font-semibold border-b-2 border-amber-500 text-white transition flex items-center gap-1.5 whitespace-nowrap';
|
|
|
+ } else {
|
|
|
+ btn.className = 'px-3 py-2 text-xs font-semibold border-b-2 border-transparent text-slate-400 hover:text-white transition flex items-center gap-1.5 whitespace-nowrap';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function selectBfdRow(tab, id) {
|
|
|
+ if (!state.selectedBfdRow) state.selectedBfdRow = {};
|
|
|
+ state.selectedBfdRow[tab] = id;
|
|
|
+ if (tab === 'interfaces') {
|
|
|
+ renderBfdInterfaces();
|
|
|
+ const btnEn = document.getElementById('btn-bfd-iface-enable');
|
|
|
+ const btnDis = document.getElementById('btn-bfd-iface-disable');
|
|
|
+ const btnRem = document.getElementById('btn-bfd-iface-remove');
|
|
|
+ if (btnEn) btnEn.disabled = false;
|
|
|
+ if (btnDis) btnDis.disabled = false;
|
|
|
+ if (btnRem) btnRem.disabled = false;
|
|
|
+ } else if (tab === 'peers') {
|
|
|
+ renderBfdPeers();
|
|
|
+ const btnEn = document.getElementById('btn-bfd-peer-enable');
|
|
|
+ const btnDis = document.getElementById('btn-bfd-peer-disable');
|
|
|
+ const btnRem = document.getElementById('btn-bfd-peer-remove');
|
|
|
+ if (btnEn) btnEn.disabled = false;
|
|
|
+ if (btnDis) btnDis.disabled = false;
|
|
|
+ if (btnRem) btnRem.disabled = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderBfdConfig() {
|
|
|
+ const inst = state.bfdInstance;
|
|
|
+ const pill = document.getElementById('bfd-live-status-pill');
|
|
|
+ const toggleBtn = document.getElementById('bfd-global-toggle-btn');
|
|
|
+ const ifaceStat = document.getElementById('bfd-stat-interfaces');
|
|
|
+ const peerStat = document.getElementById('bfd-stat-peers');
|
|
|
+ const sessStat = document.getElementById('bfd-stat-sessions');
|
|
|
+
|
|
|
+ if (ifaceStat) ifaceStat.innerText = (state.bfdInterfaces || []).length;
|
|
|
+ if (peerStat) peerStat.innerText = (state.bfdNeighbors || []).length;
|
|
|
+ if (sessStat) sessStat.innerText = (state.bfdSessions || []).length;
|
|
|
+
|
|
|
+ if (!inst || inst.disabled) {
|
|
|
+ if (pill) {
|
|
|
+ pill.className = 'text-[10px] px-2 py-0.5 rounded font-mono bg-slate-800 text-slate-400 border border-slate-700';
|
|
|
+ pill.innerText = 'DISABLED';
|
|
|
+ }
|
|
|
+ if (toggleBtn) {
|
|
|
+ toggleBtn.innerText = 'Enable BFD';
|
|
|
+ toggleBtn.className = 'px-4 py-2 rounded-lg text-xs font-semibold bg-amber-600 hover:bg-amber-500 text-white shadow-lg shadow-amber-500/20 transition';
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (pill) {
|
|
|
+ pill.className = 'text-[10px] px-2 py-0.5 rounded font-mono bg-amber-950 text-amber-300 border border-amber-800/60 font-semibold';
|
|
|
+ pill.innerText = inst.running ? 'RUNNING (ACTIVE)' : 'CONFIGURED';
|
|
|
+ }
|
|
|
+ if (toggleBtn) {
|
|
|
+ toggleBtn.innerText = 'Disable BFD';
|
|
|
+ toggleBtn.className = 'px-4 py-2 rounded-lg text-xs font-semibold bg-rose-600/80 hover:bg-rose-600 text-white transition';
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function toggleBfdGlobalState() {
|
|
|
+ const current = state.bfdInstance;
|
|
|
+ const newDisabled = current ? !current.disabled : false;
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/bfd/instance`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ enabled: !newDisabled, disabled: newDisabled })
|
|
|
+ });
|
|
|
+ if (!res.ok) throw new Error(await res.text());
|
|
|
+ showToast('Success', `BFD protocol ${newDisabled ? 'disabled' : 'enabled'}`, 'success');
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderBfdInterfaces() {
|
|
|
+ const tbody = document.getElementById('bfd-interfaces-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('bfd-interfaces-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-bfd-interfaces')?.value || '').toLowerCase();
|
|
|
+ let list = state.bfdInterfaces || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(i => (i.interface || '').toLowerCase().includes(filter) || (i.comment || '').toLowerCase().includes(filter));
|
|
|
+ }
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ const selId = state.selectedBfdRow?.interfaces;
|
|
|
+ tbody.innerHTML = list.map((iface, idx) => {
|
|
|
+ const isSel = iface.id === selId;
|
|
|
+ const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-amber-400' : 'hover:bg-[#1a2636] text-slate-300';
|
|
|
+ const flagD = iface.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '<span class="text-emerald-400">B</span>';
|
|
|
+ return `
|
|
|
+ <tr class="${selClass} cursor-pointer transition select-none" onclick="selectBfdRow('interfaces', '${iface.id}')" ondblclick="openEditBfdInterfaceModal('${iface.id}')">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 w-12 text-center">${flagD}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(iface.comment || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-semibold text-white">${escapeHtml(iface.interface || '*')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${iface.min_rx_interval || 100} ms</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${iface.min_tx_interval || 100} ms</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-400">${iface.idle_tx_interval || 1000} ms</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${iface.multiplier || 3}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${iface.passive ? 'yes' : 'no'}</td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function renderBfdPeers() {
|
|
|
+ const tbody = document.getElementById('bfd-peers-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('bfd-peers-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-bfd-peers')?.value || '').toLowerCase();
|
|
|
+ let list = state.bfdNeighbors || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(p => (p.address || '').toLowerCase().includes(filter) || (p.comment || '').toLowerCase().includes(filter));
|
|
|
+ }
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ const selId = state.selectedBfdRow?.peers;
|
|
|
+ tbody.innerHTML = list.map((p, idx) => {
|
|
|
+ const isSel = p.id === selId;
|
|
|
+ const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-amber-400' : 'hover:bg-[#1a2636] text-slate-300';
|
|
|
+ const flagD = p.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '<span class="text-emerald-400">B</span>';
|
|
|
+ return `
|
|
|
+ <tr class="${selClass} cursor-pointer transition select-none" onclick="selectBfdRow('peers', '${p.id}')" ondblclick="openEditBfdPeerModal('${p.id}')">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 w-12 text-center">${flagD}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(p.comment || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-semibold text-amber-300">${escapeHtml(p.address || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${escapeHtml(p.interface || 'auto')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${p.multihop ? 'yes' : 'no'}</td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function renderBfdSessions() {
|
|
|
+ const tbody = document.getElementById('bfd-sessions-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('bfd-sessions-footer-count');
|
|
|
+ const list = state.bfdSessions || [];
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ tbody.innerHTML = list.map((s, idx) => {
|
|
|
+ const isUp = (s.state || '').toLowerCase() === 'up';
|
|
|
+ const stateBadge = isUp
|
|
|
+ ? '<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30">UP</span>'
|
|
|
+ : `<span class="px-2 py-0.5 rounded text-[10px] font-bold bg-rose-500/20 text-rose-300 border border-rose-500/30">${escapeHtml(s.state || 'DOWN')}</span>`;
|
|
|
+ return `
|
|
|
+ <tr class="hover:bg-[#1a2636] text-slate-300">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-semibold text-white">${escapeHtml(s.ip || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${escapeHtml(s.interface || '')}</td>
|
|
|
+ <td class="py-1.5 px-3">${stateBadge}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-400">${escapeHtml(s.interval || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-400">${escapeHtml(s.timeout || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${escapeHtml(s.since || '')}</td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function openBfdInterfaceModal() {
|
|
|
+ document.getElementById('bfd-interface-modal-title').innerText = 'New BFD Interface Profile';
|
|
|
+ document.getElementById('modal-bfd-iface-id').value = '';
|
|
|
+ document.getElementById('modal-bfd-iface-pattern').value = '*';
|
|
|
+ document.getElementById('modal-bfd-iface-min-rx').value = '100';
|
|
|
+ document.getElementById('modal-bfd-iface-min-tx').value = '100';
|
|
|
+ document.getElementById('modal-bfd-iface-idle-tx').value = '1000';
|
|
|
+ document.getElementById('modal-bfd-iface-multiplier').value = '3';
|
|
|
+ document.getElementById('modal-bfd-iface-comment').value = '';
|
|
|
+ document.getElementById('modal-bfd-iface-enabled').checked = true;
|
|
|
+ document.getElementById('modal-bfd-iface-passive').checked = false;
|
|
|
+ document.getElementById('bfd-interface-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditBfdInterfaceModal(id) {
|
|
|
+ const iface = (state.bfdInterfaces || []).find(i => i.id === id);
|
|
|
+ if (!iface) return;
|
|
|
+ document.getElementById('bfd-interface-modal-title').innerText = `Edit BFD Interface: ${iface.interface}`;
|
|
|
+ document.getElementById('modal-bfd-iface-id').value = iface.id;
|
|
|
+ document.getElementById('modal-bfd-iface-pattern').value = iface.interface || '*';
|
|
|
+ document.getElementById('modal-bfd-iface-min-rx').value = iface.min_rx_interval || 100;
|
|
|
+ document.getElementById('modal-bfd-iface-min-tx').value = iface.min_tx_interval || 100;
|
|
|
+ document.getElementById('modal-bfd-iface-idle-tx').value = iface.idle_tx_interval || 1000;
|
|
|
+ document.getElementById('modal-bfd-iface-multiplier').value = iface.multiplier || 3;
|
|
|
+ document.getElementById('modal-bfd-iface-comment').value = iface.comment || '';
|
|
|
+ document.getElementById('modal-bfd-iface-enabled').checked = !iface.disabled;
|
|
|
+ document.getElementById('modal-bfd-iface-passive').checked = !!iface.passive;
|
|
|
+ document.getElementById('bfd-interface-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeBfdInterfaceModal() {
|
|
|
+ document.getElementById('bfd-interface-modal')?.classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitBfdInterface(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const id = document.getElementById('modal-bfd-iface-id').value;
|
|
|
+ const payload = {
|
|
|
+ id: id || undefined,
|
|
|
+ interface: document.getElementById('modal-bfd-iface-pattern').value.trim(),
|
|
|
+ min_rx_interval: parseInt(document.getElementById('modal-bfd-iface-min-rx').value, 10) || 100,
|
|
|
+ min_tx_interval: parseInt(document.getElementById('modal-bfd-iface-min-tx').value, 10) || 100,
|
|
|
+ idle_tx_interval: parseInt(document.getElementById('modal-bfd-iface-idle-tx').value, 10) || 1000,
|
|
|
+ multiplier: parseInt(document.getElementById('modal-bfd-iface-multiplier').value, 10) || 3,
|
|
|
+ comment: document.getElementById('modal-bfd-iface-comment').value.trim(),
|
|
|
+ disabled: !document.getElementById('modal-bfd-iface-enabled').checked,
|
|
|
+ passive: document.getElementById('modal-bfd-iface-passive').checked
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/bfd/interfaces`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (!res.ok) throw new Error(await res.text());
|
|
|
+ closeBfdInterfaceModal();
|
|
|
+ showToast('Saved', `BFD profile saved`, 'success');
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function enableSelectedBfdInterface() {
|
|
|
+ const id = state.selectedBfdRow?.interfaces;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/bfd/interfaces/${id}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: false }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function disableSelectedBfdInterface() {
|
|
|
+ const id = state.selectedBfdRow?.interfaces;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/bfd/interfaces/${id}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: true }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function removeSelectedBfdInterface() {
|
|
|
+ const id = state.selectedBfdRow?.interfaces;
|
|
|
+ if (!id || !confirm('Are you sure you want to remove this BFD interface template?')) return;
|
|
|
+ await fetch(`${API_BASE}/routing/bfd/interfaces/${id}`, { method: 'DELETE' });
|
|
|
+ state.selectedBfdRow.interfaces = null;
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+function openBfdPeerModal() {
|
|
|
+ document.getElementById('bfd-peer-modal-title').innerText = 'New Static BFD Peer';
|
|
|
+ document.getElementById('modal-bfd-peer-id').value = '';
|
|
|
+ document.getElementById('modal-bfd-peer-address').value = '';
|
|
|
+ document.getElementById('modal-bfd-peer-interface').value = '';
|
|
|
+ document.getElementById('modal-bfd-peer-comment').value = '';
|
|
|
+ document.getElementById('modal-bfd-peer-enabled').checked = true;
|
|
|
+ document.getElementById('modal-bfd-peer-multihop').checked = false;
|
|
|
+ document.getElementById('bfd-peer-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditBfdPeerModal(id) {
|
|
|
+ const p = (state.bfdNeighbors || []).find(n => n.id === id);
|
|
|
+ if (!p) return;
|
|
|
+ document.getElementById('bfd-peer-modal-title').innerText = `Edit BFD Peer: ${p.address}`;
|
|
|
+ document.getElementById('modal-bfd-peer-id').value = p.id;
|
|
|
+ document.getElementById('modal-bfd-peer-address').value = p.address || '';
|
|
|
+ document.getElementById('modal-bfd-peer-interface').value = p.interface || '';
|
|
|
+ document.getElementById('modal-bfd-peer-comment').value = p.comment || '';
|
|
|
+ document.getElementById('modal-bfd-peer-enabled').checked = !p.disabled;
|
|
|
+ document.getElementById('modal-bfd-peer-multihop').checked = !!p.multihop;
|
|
|
+ document.getElementById('bfd-peer-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeBfdPeerModal() {
|
|
|
+ document.getElementById('bfd-peer-modal')?.classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitBfdPeer(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const id = document.getElementById('modal-bfd-peer-id').value;
|
|
|
+ const payload = {
|
|
|
+ id: id || undefined,
|
|
|
+ address: document.getElementById('modal-bfd-peer-address').value.trim(),
|
|
|
+ interface: document.getElementById('modal-bfd-peer-interface').value.trim(),
|
|
|
+ comment: document.getElementById('modal-bfd-peer-comment').value.trim(),
|
|
|
+ disabled: !document.getElementById('modal-bfd-peer-enabled').checked,
|
|
|
+ multihop: document.getElementById('modal-bfd-peer-multihop').checked
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/bfd/peers`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (!res.ok) throw new Error(await res.text());
|
|
|
+ closeBfdPeerModal();
|
|
|
+ showToast('Saved', `BFD peer saved`, 'success');
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function enableSelectedBfdPeer() {
|
|
|
+ const id = state.selectedBfdRow?.peers;
|
|
|
+ if (!id) return;
|
|
|
+ const p = (state.bfdNeighbors || []).find(n => n.id === id);
|
|
|
+ if (!p) return;
|
|
|
+ await fetch(`${API_BASE}/routing/bfd/peers`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...p, disabled: false }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function disableSelectedBfdPeer() {
|
|
|
+ const id = state.selectedBfdRow?.peers;
|
|
|
+ if (!id) return;
|
|
|
+ const p = (state.bfdNeighbors || []).find(n => n.id === id);
|
|
|
+ if (!p) return;
|
|
|
+ await fetch(`${API_BASE}/routing/bfd/peers`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...p, disabled: true }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function removeSelectedBfdPeer() {
|
|
|
+ const id = state.selectedBfdRow?.peers;
|
|
|
+ if (!id || !confirm('Are you sure you want to remove this BFD peer?')) return;
|
|
|
+ await fetch(`${API_BASE}/routing/bfd/peers/${id}`, { method: 'DELETE' });
|
|
|
+ state.selectedBfdRow.peers = null;
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+// ==============================================================================
|
|
|
+// DYNAMIC ROUTING: BABEL MODULE
|
|
|
+// ==============================================================================
|
|
|
+state.babelInstances = [];
|
|
|
+state.babelInterfaces = [];
|
|
|
+state.babelNeighbors = [];
|
|
|
+state.babelRoutes = [];
|
|
|
+state.selectedBabelRow = {};
|
|
|
+state.activeBabelTab = 'instances';
|
|
|
+
|
|
|
+function switchBabelTab(tab) {
|
|
|
+ state.activeBabelTab = tab;
|
|
|
+ ['instances', 'interfaces', 'neighbors', 'routes'].forEach(t => {
|
|
|
+ const pane = document.getElementById(`babel-pane-${t}`);
|
|
|
+ const btn = document.getElementById(`babel-tab-btn-${t}`);
|
|
|
+ if (pane) {
|
|
|
+ if (t === tab) pane.classList.remove('hidden');
|
|
|
+ else pane.classList.add('hidden');
|
|
|
+ }
|
|
|
+ if (btn) {
|
|
|
+ if (t === tab) {
|
|
|
+ btn.className = 'px-3 py-2 text-xs font-semibold border-b-2 border-teal-500 text-white transition flex items-center gap-1.5 whitespace-nowrap';
|
|
|
+ } else {
|
|
|
+ btn.className = 'px-3 py-2 text-xs font-semibold border-b-2 border-transparent text-slate-400 hover:text-white transition flex items-center gap-1.5 whitespace-nowrap';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function selectBabelRow(tab, id) {
|
|
|
+ if (!state.selectedBabelRow) state.selectedBabelRow = {};
|
|
|
+ state.selectedBabelRow[tab] = id;
|
|
|
+ if (tab === 'instances') {
|
|
|
+ renderBabelInstances();
|
|
|
+ const btnEn = document.getElementById('btn-babel-inst-enable');
|
|
|
+ const btnDis = document.getElementById('btn-babel-inst-disable');
|
|
|
+ const btnRem = document.getElementById('btn-babel-inst-remove');
|
|
|
+ if (btnEn) btnEn.disabled = false;
|
|
|
+ if (btnDis) btnDis.disabled = false;
|
|
|
+ if (btnRem) btnRem.disabled = false;
|
|
|
+ } else if (tab === 'interfaces') {
|
|
|
+ renderBabelInterfaces();
|
|
|
+ const btnEn = document.getElementById('btn-babel-iface-enable');
|
|
|
+ const btnDis = document.getElementById('btn-babel-iface-disable');
|
|
|
+ const btnRem = document.getElementById('btn-babel-iface-remove');
|
|
|
+ if (btnEn) btnEn.disabled = false;
|
|
|
+ if (btnDis) btnDis.disabled = false;
|
|
|
+ if (btnRem) btnRem.disabled = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderBabelInstances() {
|
|
|
+ const tbody = document.getElementById('babel-instances-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('babel-instances-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-babel-instances')?.value || '').toLowerCase();
|
|
|
+ let list = state.babelInstances || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(i => (i.name || '').toLowerCase().includes(filter) || (i.comment || '').toLowerCase().includes(filter));
|
|
|
+ }
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ const selId = state.selectedBabelRow?.instances;
|
|
|
+ tbody.innerHTML = list.map((inst, idx) => {
|
|
|
+ const isSel = inst.id === selId;
|
|
|
+ const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-teal-400' : 'hover:bg-[#1a2636] text-slate-300';
|
|
|
+ const flagD = inst.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '<span class="text-emerald-400">B</span>';
|
|
|
+ const redist = [];
|
|
|
+ if (inst.redistribute_connected) redist.push('conn');
|
|
|
+ if (inst.redistribute_static) redist.push('static');
|
|
|
+ if (inst.redistribute_bgp) redist.push('bgp');
|
|
|
+ if (inst.redistribute_ospf) redist.push('ospf');
|
|
|
+ if (inst.redistribute_default) redist.push('default');
|
|
|
+ return `
|
|
|
+ <tr class="${selClass} cursor-pointer transition select-none" onclick="selectBabelRow('instances', '${inst.id}')" ondblclick="openEditBabelInstanceModal('${inst.id}')">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 w-12 text-center">${flagD}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(inst.comment || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-semibold text-white flex items-center gap-1.5">
|
|
|
+ <span class="text-teal-400">↳</span> <span>${escapeHtml(inst.name || 'default')}</span>
|
|
|
+ </td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-cyan-300">${escapeHtml(inst.router_id || 'auto')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-emerald-400 font-bold">IPv4 + IPv6</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-cyan-300">${redist.join(', ') || 'none'}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${inst.default_metric || 10}</td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function renderBabelInterfaces() {
|
|
|
+ const tbody = document.getElementById('babel-interfaces-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('babel-interfaces-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-babel-interfaces')?.value || '').toLowerCase();
|
|
|
+ let list = state.babelInterfaces || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(i => (i.interface || '').toLowerCase().includes(filter) || (i.comment || '').toLowerCase().includes(filter));
|
|
|
+ }
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ const selId = state.selectedBabelRow?.interfaces;
|
|
|
+ tbody.innerHTML = list.map((iface, idx) => {
|
|
|
+ const isSel = iface.id === selId;
|
|
|
+ const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-teal-400' : 'hover:bg-[#1a2636] text-slate-300';
|
|
|
+ const flagD = iface.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '<span class="text-emerald-400">B</span>';
|
|
|
+ return `
|
|
|
+ <tr class="${selClass} cursor-pointer transition select-none" onclick="selectBabelRow('interfaces', '${iface.id}')" ondblclick="openEditBabelInterfaceModal('${iface.id}')">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 w-12 text-center">${flagD}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(iface.comment || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-semibold text-white">${escapeHtml(iface.interface || '*')}</td>
|
|
|
+ <td class="py-1.5 px-3 uppercase text-teal-300 font-bold">${escapeHtml(iface.type || 'wired')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${iface.rxcost || 10}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${iface.hello_interval || 4}s</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${iface.update_interval || 16}s</td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function renderBabelNeighbors() {
|
|
|
+ const tbody = document.getElementById('babel-neighbors-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('babel-neighbors-footer-count');
|
|
|
+ const list = state.babelNeighbors || [];
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ tbody.innerHTML = list.map((nbr, idx) => `
|
|
|
+ <tr class="hover:bg-[#1a2636] text-slate-300">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-semibold text-teal-300">${escapeHtml(nbr.ip || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${escapeHtml(nbr.interface || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-400">${nbr.metric || 0}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-400">${nbr.cost || 0}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${escapeHtml(nbr.uptime || '')}</td>
|
|
|
+ </tr>
|
|
|
+ `).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function renderBabelRoutes() {
|
|
|
+ const tbody = document.getElementById('babel-routes-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('babel-routes-footer-count');
|
|
|
+ const list = state.babelRoutes || [];
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ tbody.innerHTML = list.map((r, idx) => `
|
|
|
+ <tr class="hover:bg-[#1a2636] text-slate-300">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-semibold text-white">${escapeHtml(r.prefix || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-cyan-300">${escapeHtml(r.next_hop || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${r.metric || 10}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${escapeHtml(r.interface || '')}</td>
|
|
|
+ </tr>
|
|
|
+ `).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function openBabelInstanceModal() {
|
|
|
+ document.getElementById('babel-instance-modal-title').innerText = 'New Babel Instance';
|
|
|
+ document.getElementById('modal-babel-instance-id').value = '';
|
|
|
+ document.getElementById('modal-babel-instance-name').value = 'babel1';
|
|
|
+ document.getElementById('modal-babel-instance-routerid').value = '';
|
|
|
+ document.getElementById('modal-babel-instance-metric').value = '10';
|
|
|
+ document.getElementById('modal-babel-instance-comment').value = '';
|
|
|
+ document.getElementById('modal-babel-instance-enabled').checked = true;
|
|
|
+ document.getElementById('modal-babel-instance-redist-conn').checked = false;
|
|
|
+ document.getElementById('modal-babel-instance-redist-static').checked = false;
|
|
|
+ document.getElementById('modal-babel-instance-redist-bgp').checked = false;
|
|
|
+ document.getElementById('modal-babel-instance-redist-ospf').checked = false;
|
|
|
+ document.getElementById('modal-babel-instance-redist-default').checked = false;
|
|
|
+ document.getElementById('babel-instance-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditBabelInstanceModal(id) {
|
|
|
+ const inst = (state.babelInstances || []).find(i => i.id === id);
|
|
|
+ if (!inst) return;
|
|
|
+ document.getElementById('babel-instance-modal-title').innerText = `Edit Babel Instance: ${inst.name}`;
|
|
|
+ document.getElementById('modal-babel-instance-id').value = inst.id;
|
|
|
+ document.getElementById('modal-babel-instance-name').value = inst.name || '';
|
|
|
+ document.getElementById('modal-babel-instance-routerid').value = inst.router_id || '';
|
|
|
+ document.getElementById('modal-babel-instance-metric').value = inst.default_metric || 10;
|
|
|
+ document.getElementById('modal-babel-instance-comment').value = inst.comment || '';
|
|
|
+ document.getElementById('modal-babel-instance-enabled').checked = !inst.disabled;
|
|
|
+ document.getElementById('modal-babel-instance-redist-conn').checked = !!inst.redistribute_connected;
|
|
|
+ document.getElementById('modal-babel-instance-redist-static').checked = !!inst.redistribute_static;
|
|
|
+ document.getElementById('modal-babel-instance-redist-bgp').checked = !!inst.redistribute_bgp;
|
|
|
+ document.getElementById('modal-babel-instance-redist-ospf').checked = !!inst.redistribute_ospf;
|
|
|
+ document.getElementById('modal-babel-instance-redist-default').checked = !!inst.redistribute_default;
|
|
|
+ document.getElementById('babel-instance-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeBabelInstanceModal() {
|
|
|
+ document.getElementById('babel-instance-modal')?.classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitBabelInstance(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const id = document.getElementById('modal-babel-instance-id').value;
|
|
|
+ const payload = {
|
|
|
+ id: id || undefined,
|
|
|
+ name: document.getElementById('modal-babel-instance-name').value.trim(),
|
|
|
+ router_id: document.getElementById('modal-babel-instance-routerid').value.trim(),
|
|
|
+ default_metric: parseInt(document.getElementById('modal-babel-instance-metric').value, 10) || 10,
|
|
|
+ comment: document.getElementById('modal-babel-instance-comment').value.trim(),
|
|
|
+ disabled: !document.getElementById('modal-babel-instance-enabled').checked,
|
|
|
+ redistribute_connected: document.getElementById('modal-babel-instance-redist-conn').checked,
|
|
|
+ redistribute_static: document.getElementById('modal-babel-instance-redist-static').checked,
|
|
|
+ redistribute_bgp: document.getElementById('modal-babel-instance-redist-bgp').checked,
|
|
|
+ redistribute_ospf: document.getElementById('modal-babel-instance-redist-ospf').checked,
|
|
|
+ redistribute_default: document.getElementById('modal-babel-instance-redist-default').checked
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/babel/instances`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (!res.ok) throw new Error(await res.text());
|
|
|
+ closeBabelInstanceModal();
|
|
|
+ showToast('Saved', `Babel instance saved`, 'success');
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function enableSelectedBabelInstance() {
|
|
|
+ const id = state.selectedBabelRow?.instances;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/babel/instances/${id}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: false }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function disableSelectedBabelInstance() {
|
|
|
+ const id = state.selectedBabelRow?.instances;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/babel/instances/${id}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: true }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function removeSelectedBabelInstance() {
|
|
|
+ const id = state.selectedBabelRow?.instances;
|
|
|
+ if (!id || !confirm('Are you sure you want to remove this Babel instance?')) return;
|
|
|
+ await fetch(`${API_BASE}/routing/babel/instances/${id}`, { method: 'DELETE' });
|
|
|
+ state.selectedBabelRow.instances = null;
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+function openBabelInterfaceModal() {
|
|
|
+ document.getElementById('babel-interface-modal-title').innerText = 'New Babel Interface';
|
|
|
+ document.getElementById('modal-babel-iface-id').value = '';
|
|
|
+ document.getElementById('modal-babel-iface-pattern').value = '';
|
|
|
+ document.getElementById('modal-babel-iface-type').value = 'wired';
|
|
|
+ document.getElementById('modal-babel-iface-rxcost').value = '10';
|
|
|
+ document.getElementById('modal-babel-iface-hello').value = '4';
|
|
|
+ document.getElementById('modal-babel-iface-update').value = '16';
|
|
|
+ document.getElementById('modal-babel-iface-comment').value = '';
|
|
|
+ document.getElementById('modal-babel-iface-enabled').checked = true;
|
|
|
+ document.getElementById('babel-interface-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditBabelInterfaceModal(id) {
|
|
|
+ const iface = (state.babelInterfaces || []).find(i => i.id === id);
|
|
|
+ if (!iface) return;
|
|
|
+ document.getElementById('babel-interface-modal-title').innerText = `Edit Babel Interface: ${iface.interface}`;
|
|
|
+ document.getElementById('modal-babel-iface-id').value = iface.id;
|
|
|
+ document.getElementById('modal-babel-iface-pattern').value = iface.interface || '';
|
|
|
+ document.getElementById('modal-babel-iface-type').value = iface.type || 'wired';
|
|
|
+ document.getElementById('modal-babel-iface-rxcost').value = iface.rxcost || 10;
|
|
|
+ document.getElementById('modal-babel-iface-hello').value = iface.hello_interval || 4;
|
|
|
+ document.getElementById('modal-babel-iface-update').value = iface.update_interval || 16;
|
|
|
+ document.getElementById('modal-babel-iface-comment').value = iface.comment || '';
|
|
|
+ document.getElementById('modal-babel-iface-enabled').checked = !iface.disabled;
|
|
|
+ document.getElementById('babel-interface-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeBabelInterfaceModal() {
|
|
|
+ document.getElementById('babel-interface-modal')?.classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitBabelInterface(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const id = document.getElementById('modal-babel-iface-id').value;
|
|
|
+ const payload = {
|
|
|
+ id: id || undefined,
|
|
|
+ interface: document.getElementById('modal-babel-iface-pattern').value.trim(),
|
|
|
+ type: document.getElementById('modal-babel-iface-type').value,
|
|
|
+ rxcost: parseInt(document.getElementById('modal-babel-iface-rxcost').value, 10) || 10,
|
|
|
+ hello_interval: parseInt(document.getElementById('modal-babel-iface-hello').value, 10) || 4,
|
|
|
+ update_interval: parseInt(document.getElementById('modal-babel-iface-update').value, 10) || 16,
|
|
|
+ comment: document.getElementById('modal-babel-iface-comment').value.trim(),
|
|
|
+ disabled: !document.getElementById('modal-babel-iface-enabled').checked
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/babel/interfaces`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (!res.ok) throw new Error(await res.text());
|
|
|
+ closeBabelInterfaceModal();
|
|
|
+ showToast('Saved', `Babel interface template saved`, 'success');
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function enableSelectedBabelInterface() {
|
|
|
+ const id = state.selectedBabelRow?.interfaces;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/babel/interfaces/${id}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: false }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function disableSelectedBabelInterface() {
|
|
|
+ const id = state.selectedBabelRow?.interfaces;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/babel/interfaces/${id}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: true }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function removeSelectedBabelInterface() {
|
|
|
+ const id = state.selectedBabelRow?.interfaces;
|
|
|
+ if (!id || !confirm('Are you sure you want to remove this Babel interface template?')) return;
|
|
|
+ await fetch(`${API_BASE}/routing/babel/interfaces/${id}`, { method: 'DELETE' });
|
|
|
+ state.selectedBabelRow.interfaces = null;
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+// ==============================================================================
|
|
|
+// DYNAMIC ROUTING: RADV MODULE (IPv6 Router Advertisements)
|
|
|
+// ==============================================================================
|
|
|
+state.radvInstances = [];
|
|
|
+state.radvInterfaces = [];
|
|
|
+state.selectedRadvRow = {};
|
|
|
+state.activeRadvTab = 'instances';
|
|
|
+
|
|
|
+function switchRadvTab(tab) {
|
|
|
+ state.activeRadvTab = tab;
|
|
|
+ ['instances', 'interfaces'].forEach(t => {
|
|
|
+ const pane = document.getElementById(`radv-pane-${t}`);
|
|
|
+ const btn = document.getElementById(`radv-tab-btn-${t}`);
|
|
|
+ if (pane) {
|
|
|
+ if (t === tab) pane.classList.remove('hidden');
|
|
|
+ else pane.classList.add('hidden');
|
|
|
+ }
|
|
|
+ if (btn) {
|
|
|
+ if (t === tab) {
|
|
|
+ btn.className = 'px-3 py-2 text-xs font-semibold border-b-2 border-blue-500 text-white transition flex items-center gap-1.5 whitespace-nowrap';
|
|
|
+ } else {
|
|
|
+ btn.className = 'px-3 py-2 text-xs font-semibold border-b-2 border-transparent text-slate-400 hover:text-white transition flex items-center gap-1.5 whitespace-nowrap';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function selectRadvRow(tab, id) {
|
|
|
+ if (!state.selectedRadvRow) state.selectedRadvRow = {};
|
|
|
+ state.selectedRadvRow[tab] = id;
|
|
|
+ if (tab === 'instances') {
|
|
|
+ renderRAdvInstances();
|
|
|
+ const btnEn = document.getElementById('btn-radv-inst-enable');
|
|
|
+ const btnDis = document.getElementById('btn-radv-inst-disable');
|
|
|
+ const btnRem = document.getElementById('btn-radv-inst-remove');
|
|
|
+ if (btnEn) btnEn.disabled = false;
|
|
|
+ if (btnDis) btnDis.disabled = false;
|
|
|
+ if (btnRem) btnRem.disabled = false;
|
|
|
+ } else if (tab === 'interfaces') {
|
|
|
+ renderRAdvInterfaces();
|
|
|
+ const btnEn = document.getElementById('btn-radv-iface-enable');
|
|
|
+ const btnDis = document.getElementById('btn-radv-iface-disable');
|
|
|
+ const btnRem = document.getElementById('btn-radv-iface-remove');
|
|
|
+ if (btnEn) btnEn.disabled = false;
|
|
|
+ if (btnDis) btnDis.disabled = false;
|
|
|
+ if (btnRem) btnRem.disabled = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderRAdvInstances() {
|
|
|
+ const tbody = document.getElementById('radv-instances-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('radv-instances-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-radv-instances')?.value || '').toLowerCase();
|
|
|
+ let list = state.radvInstances || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(i => (i.name || '').toLowerCase().includes(filter) || (i.comment || '').toLowerCase().includes(filter));
|
|
|
+ }
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ const selId = state.selectedRadvRow?.instances;
|
|
|
+ tbody.innerHTML = list.map((inst, idx) => {
|
|
|
+ const isSel = inst.id === selId;
|
|
|
+ const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-blue-400' : 'hover:bg-[#1a2636] text-slate-300';
|
|
|
+ const flagD = inst.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '<span class="text-emerald-400">R</span>';
|
|
|
+ return `
|
|
|
+ <tr class="${selClass} cursor-pointer transition select-none" onclick="selectRadvRow('instances', '${inst.id}')" ondblclick="openEditRAdvInstanceModal('${inst.id}')">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 w-12 text-center">${flagD}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(inst.comment || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-semibold text-white flex items-center gap-1.5">
|
|
|
+ <span class="text-blue-400">↳</span> <span>${escapeHtml(inst.name || 'default')}</span>
|
|
|
+ </td>
|
|
|
+ <td class="py-1.5 px-3">${inst.running ? '<span class="text-emerald-400 font-bold">running</span>' : '<span class="text-slate-500">stopped</span>'}</td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function renderRAdvInterfaces() {
|
|
|
+ const tbody = document.getElementById('radv-interfaces-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('radv-interfaces-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-radv-interfaces')?.value || '').toLowerCase();
|
|
|
+ let list = state.radvInterfaces || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(i => (i.interface || '').toLowerCase().includes(filter) || (i.comment || '').toLowerCase().includes(filter));
|
|
|
+ }
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ const selId = state.selectedRadvRow?.interfaces;
|
|
|
+ tbody.innerHTML = list.map((iface, idx) => {
|
|
|
+ const isSel = iface.id === selId;
|
|
|
+ const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-blue-400' : 'hover:bg-[#1a2636] text-slate-300';
|
|
|
+ const flagD = iface.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '<span class="text-emerald-400">R</span>';
|
|
|
+ const prefixes = (iface.prefixes || []).join(', ') || 'default';
|
|
|
+ const rdnss = (iface.rdnss || []).join(', ') || 'none';
|
|
|
+ return `
|
|
|
+ <tr class="${selClass} cursor-pointer transition select-none" onclick="selectRadvRow('interfaces', '${iface.id}')" ondblclick="openEditRAdvInterfaceModal('${iface.id}')">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 w-12 text-center">${flagD}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(iface.comment || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-semibold text-white">${escapeHtml(iface.interface || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-300">${iface.min_ra_interval || 200}s / ${iface.max_ra_interval || 600}s</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${iface.default_lifetime || 1800}s</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-cyan-300">${escapeHtml(prefixes)}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-purple-300">${escapeHtml(rdnss)}</td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function openRAdvInstanceModal() {
|
|
|
+ document.getElementById('radv-instance-modal-title').innerText = 'New RAdv Instance';
|
|
|
+ document.getElementById('modal-radv-instance-id').value = '';
|
|
|
+ document.getElementById('modal-radv-instance-name').value = 'radv1';
|
|
|
+ document.getElementById('modal-radv-instance-comment').value = '';
|
|
|
+ document.getElementById('modal-radv-instance-enabled').checked = true;
|
|
|
+ document.getElementById('radv-instance-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditRAdvInstanceModal(id) {
|
|
|
+ const inst = (state.radvInstances || []).find(i => i.id === id);
|
|
|
+ if (!inst) return;
|
|
|
+ document.getElementById('radv-instance-modal-title').innerText = `Edit RAdv Instance: ${inst.name}`;
|
|
|
+ document.getElementById('modal-radv-instance-id').value = inst.id;
|
|
|
+ document.getElementById('modal-radv-instance-name').value = inst.name || '';
|
|
|
+ document.getElementById('modal-radv-instance-comment').value = inst.comment || '';
|
|
|
+ document.getElementById('modal-radv-instance-enabled').checked = !inst.disabled;
|
|
|
+ document.getElementById('radv-instance-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeRAdvInstanceModal() {
|
|
|
+ document.getElementById('radv-instance-modal')?.classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitRAdvInstance(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const id = document.getElementById('modal-radv-instance-id').value;
|
|
|
+ const payload = {
|
|
|
+ id: id || undefined,
|
|
|
+ name: document.getElementById('modal-radv-instance-name').value.trim(),
|
|
|
+ comment: document.getElementById('modal-radv-instance-comment').value.trim(),
|
|
|
+ disabled: !document.getElementById('modal-radv-instance-enabled').checked
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/radv/instances`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (!res.ok) throw new Error(await res.text());
|
|
|
+ closeRAdvInstanceModal();
|
|
|
+ showToast('Saved', `RAdv instance saved`, 'success');
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function enableSelectedRadvInstance() {
|
|
|
+ const id = state.selectedRadvRow?.instances;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/radv/instances/${id}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: false }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function disableSelectedRadvInstance() {
|
|
|
+ const id = state.selectedRadvRow?.instances;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/radv/instances/${id}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: true }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function removeSelectedRadvInstance() {
|
|
|
+ const id = state.selectedRadvRow?.instances;
|
|
|
+ if (!id || !confirm('Are you sure you want to remove this RAdv instance?')) return;
|
|
|
+ await fetch(`${API_BASE}/routing/radv/instances/${id}`, { method: 'DELETE' });
|
|
|
+ state.selectedRadvRow.instances = null;
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+function openRAdvInterfaceModal() {
|
|
|
+ document.getElementById('radv-interface-modal-title').innerText = 'New RAdv Interface';
|
|
|
+ document.getElementById('modal-radv-iface-id').value = '';
|
|
|
+ document.getElementById('modal-radv-iface-interface').value = '';
|
|
|
+ document.getElementById('modal-radv-iface-instance').value = 'radv1';
|
|
|
+ document.getElementById('modal-radv-iface-min-ra').value = '200';
|
|
|
+ document.getElementById('modal-radv-iface-max-ra').value = '600';
|
|
|
+ document.getElementById('modal-radv-iface-lifetime').value = '1800';
|
|
|
+ document.getElementById('modal-radv-iface-hoplimit').value = '64';
|
|
|
+ document.getElementById('modal-radv-iface-prefixes').value = '';
|
|
|
+ document.getElementById('modal-radv-iface-rdnss').value = '';
|
|
|
+ document.getElementById('modal-radv-iface-comment').value = '';
|
|
|
+ document.getElementById('modal-radv-iface-enabled').checked = true;
|
|
|
+ document.getElementById('radv-interface-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditRAdvInterfaceModal(id) {
|
|
|
+ const iface = (state.radvInterfaces || []).find(i => i.id === id);
|
|
|
+ if (!iface) return;
|
|
|
+ document.getElementById('radv-interface-modal-title').innerText = `Edit RAdv Interface: ${iface.interface}`;
|
|
|
+ document.getElementById('modal-radv-iface-id').value = iface.id;
|
|
|
+ document.getElementById('modal-radv-iface-interface').value = iface.interface || '';
|
|
|
+ document.getElementById('modal-radv-iface-instance').value = iface.instance || '';
|
|
|
+ document.getElementById('modal-radv-iface-min-ra').value = iface.min_ra_interval || 200;
|
|
|
+ document.getElementById('modal-radv-iface-max-ra').value = iface.max_ra_interval || 600;
|
|
|
+ document.getElementById('modal-radv-iface-lifetime').value = iface.default_lifetime || 1800;
|
|
|
+ document.getElementById('modal-radv-iface-hoplimit').value = iface.hop_limit || 64;
|
|
|
+ document.getElementById('modal-radv-iface-prefixes').value = (iface.prefixes || []).join(', ');
|
|
|
+ document.getElementById('modal-radv-iface-rdnss').value = (iface.rdnss || []).join(', ');
|
|
|
+ document.getElementById('modal-radv-iface-comment').value = iface.comment || '';
|
|
|
+ document.getElementById('modal-radv-iface-enabled').checked = !iface.disabled;
|
|
|
+ document.getElementById('radv-interface-modal')?.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeRAdvInterfaceModal() {
|
|
|
+ document.getElementById('radv-interface-modal')?.classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitRAdvInterface(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const id = document.getElementById('modal-radv-iface-id').value;
|
|
|
+ const pRaw = document.getElementById('modal-radv-iface-prefixes').value;
|
|
|
+ const rRaw = document.getElementById('modal-radv-iface-rdnss').value;
|
|
|
+ const prefixes = pRaw ? pRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
|
+ const rdnss = rRaw ? rRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
|
+ const payload = {
|
|
|
+ id: id || undefined,
|
|
|
+ interface: document.getElementById('modal-radv-iface-interface').value.trim(),
|
|
|
+ instance: document.getElementById('modal-radv-iface-instance').value.trim(),
|
|
|
+ min_ra_interval: parseInt(document.getElementById('modal-radv-iface-min-ra').value, 10) || 200,
|
|
|
+ max_ra_interval: parseInt(document.getElementById('modal-radv-iface-max-ra').value, 10) || 600,
|
|
|
+ default_lifetime: parseInt(document.getElementById('modal-radv-iface-lifetime').value, 10) || 1800,
|
|
|
+ hop_limit: parseInt(document.getElementById('modal-radv-iface-hoplimit').value, 10) || 64,
|
|
|
+ prefixes: prefixes,
|
|
|
+ rdnss: rdnss,
|
|
|
+ comment: document.getElementById('modal-radv-iface-comment').value.trim(),
|
|
|
+ disabled: !document.getElementById('modal-radv-iface-enabled').checked
|
|
|
+ };
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/radv/interfaces`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (!res.ok) throw new Error(await res.text());
|
|
|
+ closeRAdvInterfaceModal();
|
|
|
+ showToast('Saved', `RAdv interface saved`, 'success');
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function enableSelectedRadvInterface() {
|
|
|
+ const id = state.selectedRadvRow?.interfaces;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/radv/interfaces/${id}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: false }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function disableSelectedRadvInterface() {
|
|
|
+ const id = state.selectedRadvRow?.interfaces;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/radv/interfaces/${id}/state`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ disabled: true }) });
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+async function removeSelectedRadvInterface() {
|
|
|
+ const id = state.selectedRadvRow?.interfaces;
|
|
|
+ if (!id || !confirm('Are you sure you want to remove this RAdv interface?')) return;
|
|
|
+ await fetch(`${API_BASE}/routing/radv/interfaces/${id}`, { method: 'DELETE' });
|
|
|
+ state.selectedRadvRow.interfaces = null;
|
|
|
+ await loadDynamicRouting(false);
|
|
|
+}
|
|
|
+
|
|
|
+
|