|
|
@@ -9834,7 +9834,7 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
selectRules, commLists, asLists,
|
|
|
ripInsts, ripIfaces, ripNbrsLive, ripRoutes,
|
|
|
bfdInst, bfdIfaces, bfdPeers, bfdSessions,
|
|
|
- babelInsts, babelIfaces, babelNbrs, babelRoutes,
|
|
|
+ babelInsts, babelIfaces, babelLiveIfaces, babelNbrs, babelRoutes,
|
|
|
radvInsts, radvIfaces
|
|
|
] = await Promise.all([
|
|
|
fetchSafeJson(`${API_BASE}/routing/dynamic/summary`),
|
|
|
@@ -9863,6 +9863,7 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
fetchSafeJson(`${API_BASE}/routing/bfd/sessions`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/babel/instances`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/babel/interfaces`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/babel/interfaces-live`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/babel/neighbors`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/babel/routes`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/radv/instances`),
|
|
|
@@ -9898,6 +9899,7 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
|
|
|
if (babelInsts) state.babelInstances = babelInsts;
|
|
|
if (babelIfaces) state.babelInterfaces = babelIfaces;
|
|
|
+ if (babelLiveIfaces) state.babelInterfacesLive = babelLiveIfaces;
|
|
|
if (babelNbrs) state.babelNeighbors = babelNbrs;
|
|
|
if (babelRoutes) state.babelRoutes = babelRoutes;
|
|
|
|
|
|
@@ -9930,6 +9932,7 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
|
|
|
try { renderBabelInstances(); } catch(e) { console.error('renderBabelInstances error:', e); }
|
|
|
try { renderBabelInterfaces(); } catch(e) { console.error('renderBabelInterfaces error:', e); }
|
|
|
+ try { renderBabelLiveInterfaces(); } catch(e) { console.error('renderBabelLiveInterfaces error:', e); }
|
|
|
try { renderBabelNeighbors(); } catch(e) { console.error('renderBabelNeighbors error:', e); }
|
|
|
try { renderBabelRoutes(); } catch(e) { console.error('renderBabelRoutes error:', e); }
|
|
|
|
|
|
@@ -20275,6 +20278,7 @@ async function removeSelectedBfdPeer() {
|
|
|
// ==============================================================================
|
|
|
state.babelInstances = [];
|
|
|
state.babelInterfaces = [];
|
|
|
+state.babelInterfacesLive = [];
|
|
|
state.babelNeighbors = [];
|
|
|
state.babelRoutes = [];
|
|
|
state.selectedBabelRow = {};
|
|
|
@@ -20282,7 +20286,7 @@ state.activeBabelTab = 'instances';
|
|
|
|
|
|
function switchBabelTab(tab) {
|
|
|
state.activeBabelTab = tab;
|
|
|
- ['instances', 'interfaces', 'neighbors', 'routes'].forEach(t => {
|
|
|
+ ['instances', 'interfaces', 'interfaces-live', 'neighbors', 'routes'].forEach(t => {
|
|
|
const pane = document.getElementById(`babel-pane-${t}`);
|
|
|
const btn = document.getElementById(`babel-tab-btn-${t}`);
|
|
|
if (pane) {
|
|
|
@@ -20335,7 +20339,11 @@ function renderBabelInstances() {
|
|
|
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 flagD = inst.disabled
|
|
|
+ ? '<span class="text-rose-400 font-bold" title="Disabled">X</span>'
|
|
|
+ : (inst.running
|
|
|
+ ? '<span class="text-emerald-400 font-bold" title="Running">R</span>'
|
|
|
+ : '<span class="text-amber-400 font-bold" title="Inactive">I</span>');
|
|
|
const redist = [];
|
|
|
if (inst.redistribute_connected) redist.push('conn');
|
|
|
if (inst.redistribute_static) redist.push('static');
|
|
|
@@ -20366,7 +20374,7 @@ function renderBabelInterfaces() {
|
|
|
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));
|
|
|
+ list = list.filter(i => (i.interface || '').toLowerCase().includes(filter) || (i.instance || '').toLowerCase().includes(filter) || (i.comment || '').toLowerCase().includes(filter));
|
|
|
}
|
|
|
if (countEl) countEl.innerText = list.length;
|
|
|
const selId = state.selectedBabelRow?.interfaces;
|
|
|
@@ -20379,6 +20387,7 @@ function renderBabelInterfaces() {
|
|
|
<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-semibold text-teal-300 font-mono">${escapeHtml(iface.instance || 'all')}</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>
|
|
|
@@ -20389,6 +20398,39 @@ function renderBabelInterfaces() {
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
+function renderBabelLiveInterfaces() {
|
|
|
+ const tbody = document.getElementById('babel-interfaces-live-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('babel-interfaces-live-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-babel-interfaces-live')?.value || '').toLowerCase();
|
|
|
+ let list = state.babelInterfacesLive || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(i => (i.interface || '').toLowerCase().includes(filter) || (i.instance || '').toLowerCase().includes(filter));
|
|
|
+ }
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ tbody.innerHTML = list.map((iface, idx) => {
|
|
|
+ const isUp = (iface.state || '').toLowerCase() === 'up';
|
|
|
+ const stateBadge = isUp
|
|
|
+ ? '<span class="px-1.5 py-0.5 rounded text-[10px] font-bold bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">UP</span>'
|
|
|
+ : '<span class="px-1.5 py-0.5 rounded text-[10px] font-bold bg-rose-500/20 text-rose-400 border border-rose-500/30">DOWN</span>';
|
|
|
+ return `
|
|
|
+ <tr class="hover:bg-[#1a2636] text-slate-300 font-mono">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500 font-sans">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 w-12 text-center text-cyan-400 font-bold">D</td>
|
|
|
+ <td class="py-1.5 px-3 font-semibold text-white">${escapeHtml(iface.interface || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-semibold text-teal-300">${escapeHtml(iface.instance || 'default')}</td>
|
|
|
+ <td class="py-1.5 px-3">${stateBadge}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${escapeHtml(iface.auth || 'No')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-300">${iface.rxcost || 96}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-300">${iface.neighbors || 0}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${escapeHtml(iface.timer || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-cyan-300">${escapeHtml(iface.next_hop_v4 || '-')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400 text-[11px]">${escapeHtml(iface.next_hop_v6 || '-')}</td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
function renderBabelNeighbors() {
|
|
|
const tbody = document.getElementById('babel-neighbors-table-body');
|
|
|
if (!tbody) return;
|
|
|
@@ -20401,8 +20443,8 @@ function renderBabelNeighbors() {
|
|
|
<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>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-400">${nbr.cost || nbr.routes || 0}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${escapeHtml(nbr.uptime || nbr.expires || '')}</td>
|
|
|
</tr>
|
|
|
`).join('');
|
|
|
}
|
|
|
@@ -20465,13 +20507,15 @@ function closeBabelInstanceModal() {
|
|
|
async function submitBabelInstance(e) {
|
|
|
e.preventDefault();
|
|
|
const id = document.getElementById('modal-babel-instance-id').value;
|
|
|
+ const isEnabled = document.getElementById('modal-babel-instance-enabled').checked;
|
|
|
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,
|
|
|
+ enabled: isEnabled,
|
|
|
+ disabled: !isEnabled,
|
|
|
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,
|
|
|
@@ -20515,11 +20559,38 @@ async function removeSelectedBabelInstance() {
|
|
|
await loadDynamicRouting(false);
|
|
|
}
|
|
|
|
|
|
+function toggleBabelAuthKey() {
|
|
|
+ const authVal = document.getElementById('modal-babel-iface-auth')?.value;
|
|
|
+ const wrap = document.getElementById('modal-babel-iface-auth-key-wrap');
|
|
|
+ if (wrap) {
|
|
|
+ if (authVal === 'mac') wrap.classList.remove('hidden');
|
|
|
+ else wrap.classList.add('hidden');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
function openBabelInterfaceModal() {
|
|
|
- document.getElementById('babel-interface-modal-title').innerText = 'New Babel Interface';
|
|
|
+ document.getElementById('babel-interface-modal-title').innerText = 'New Babel Interface Template';
|
|
|
document.getElementById('modal-babel-iface-id').value = '';
|
|
|
- document.getElementById('modal-babel-iface-pattern').value = '';
|
|
|
+
|
|
|
+ const instSel = document.getElementById('modal-babel-iface-instance');
|
|
|
+ if (instSel) {
|
|
|
+ instSel.innerHTML = '<option value="all">all (Any instance)</option>' +
|
|
|
+ (state.babelInstances || []).map(i => `<option value="${escapeHtml(i.name)}">${escapeHtml(i.name)}</option>`).join('');
|
|
|
+ if (state.babelInstances && state.babelInstances.length > 0) {
|
|
|
+ instSel.value = state.babelInstances[0].name;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const dl = document.getElementById('babel-iface-datalist');
|
|
|
+ if (dl) {
|
|
|
+ dl.innerHTML = '<option value="*">' + (state.interfaces || []).map(i => `<option value="${escapeHtml(i.name)}">`).join('');
|
|
|
+ }
|
|
|
+
|
|
|
+ document.getElementById('modal-babel-iface-pattern').value = '*';
|
|
|
document.getElementById('modal-babel-iface-type').value = 'wired';
|
|
|
+ document.getElementById('modal-babel-iface-auth').value = 'none';
|
|
|
+ document.getElementById('modal-babel-iface-auth-key').value = '';
|
|
|
+ toggleBabelAuthKey();
|
|
|
document.getElementById('modal-babel-iface-rxcost').value = '10';
|
|
|
document.getElementById('modal-babel-iface-hello').value = '4';
|
|
|
document.getElementById('modal-babel-iface-update').value = '16';
|
|
|
@@ -20533,8 +20604,24 @@ function openEditBabelInterfaceModal(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;
|
|
|
+
|
|
|
+ const instSel = document.getElementById('modal-babel-iface-instance');
|
|
|
+ if (instSel) {
|
|
|
+ instSel.innerHTML = '<option value="all">all (Any instance)</option>' +
|
|
|
+ (state.babelInstances || []).map(i => `<option value="${escapeHtml(i.name)}">${escapeHtml(i.name)}</option>`).join('');
|
|
|
+ instSel.value = iface.instance || 'all';
|
|
|
+ }
|
|
|
+
|
|
|
+ const dl = document.getElementById('babel-iface-datalist');
|
|
|
+ if (dl) {
|
|
|
+ dl.innerHTML = '<option value="*">' + (state.interfaces || []).map(i => `<option value="${escapeHtml(i.name)}">`).join('');
|
|
|
+ }
|
|
|
+
|
|
|
document.getElementById('modal-babel-iface-pattern').value = iface.interface || '';
|
|
|
document.getElementById('modal-babel-iface-type').value = iface.type || 'wired';
|
|
|
+ document.getElementById('modal-babel-iface-auth').value = iface.auth || 'none';
|
|
|
+ document.getElementById('modal-babel-iface-auth-key').value = iface.auth_key || '';
|
|
|
+ toggleBabelAuthKey();
|
|
|
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;
|
|
|
@@ -20550,15 +20637,19 @@ function closeBabelInterfaceModal() {
|
|
|
async function submitBabelInterface(e) {
|
|
|
e.preventDefault();
|
|
|
const id = document.getElementById('modal-babel-iface-id').value;
|
|
|
+ const isEnabled = document.getElementById('modal-babel-iface-enabled').checked;
|
|
|
const payload = {
|
|
|
id: id || undefined,
|
|
|
+ instance: document.getElementById('modal-babel-iface-instance').value.trim(),
|
|
|
interface: document.getElementById('modal-babel-iface-pattern').value.trim(),
|
|
|
type: document.getElementById('modal-babel-iface-type').value,
|
|
|
+ auth: document.getElementById('modal-babel-iface-auth').value,
|
|
|
+ auth_key: document.getElementById('modal-babel-iface-auth-key').value.trim(),
|
|
|
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
|
|
|
+ disabled: !isEnabled
|
|
|
};
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/routing/babel/interfaces`, {
|