|
|
@@ -146,7 +146,7 @@ function navigate(tabName) {
|
|
|
}
|
|
|
if (tabName === 'ospf') {
|
|
|
loadDynamicRouting();
|
|
|
- switchOspfSubTab(state.activeOspfSubTab || 'ospf-ifaces');
|
|
|
+ switchOspfTab(state.activeOspfTab || 'instances');
|
|
|
}
|
|
|
if (tabName === 'routing-filters') loadDynamicRouting();
|
|
|
if (tabName === 'dynamic-routing') loadDynamicRouting();
|
|
|
@@ -6856,16 +6856,41 @@ function downloadWGClientConf() {
|
|
|
|
|
|
state.activeDynSubTab = 'bgp-peers';
|
|
|
state.activeBgpSubTab = 'bgp-peers';
|
|
|
-state.activeOspfSubTab = 'ospf-ifaces';
|
|
|
+state.activeOspfTab = 'instances';
|
|
|
state.bgpInstance = null;
|
|
|
state.bgpPeers = [];
|
|
|
state.bgpNets = [];
|
|
|
state.bgpRoutes = [];
|
|
|
state.ospfInstance = null;
|
|
|
-state.ospfInterfaces = [];
|
|
|
+state.ospfInstances = [];
|
|
|
state.ospfAreas = [];
|
|
|
+state.ospfAreaRanges = [];
|
|
|
+state.ospfTemplates = [];
|
|
|
+state.ospfLiveInterfaces = [];
|
|
|
+state.ospfStaticNbrs = [];
|
|
|
state.ospfNeighbors = [];
|
|
|
+state.ospfLSA = [];
|
|
|
state.ospfRoutes = [];
|
|
|
+state.selectedOspfRow = {
|
|
|
+ instances: null,
|
|
|
+ areas: null,
|
|
|
+ 'area-ranges': null,
|
|
|
+ templates: null,
|
|
|
+ interfaces: null,
|
|
|
+ 'static-nbrs': null,
|
|
|
+ neighbors: null,
|
|
|
+ lsa: null,
|
|
|
+};
|
|
|
+state.ospfSort = {
|
|
|
+ instances: { col: 'name', asc: true },
|
|
|
+ areas: { col: 'name', asc: true },
|
|
|
+ ranges: { col: 'area', asc: true },
|
|
|
+ templates: { col: 'interfaces', asc: true },
|
|
|
+ live_ifaces: { col: 'interface', asc: true },
|
|
|
+ snbrs: { col: 'address', asc: true },
|
|
|
+ neighbors: { col: 'address', asc: true },
|
|
|
+ lsa: { col: 'instance', asc: true },
|
|
|
+};
|
|
|
|
|
|
function switchBgpSubTab(subTab) {
|
|
|
state.activeBgpSubTab = subTab;
|
|
|
@@ -6884,30 +6909,38 @@ function switchBgpSubTab(subTab) {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-function switchOspfSubTab(subTab) {
|
|
|
- state.activeOspfSubTab = subTab;
|
|
|
- const tabs = ['ospf-ifaces', 'ospf-areas', 'ospf-nbrs', 'ospf-routes'];
|
|
|
+function switchOspfTab(tabName) {
|
|
|
+ state.activeOspfTab = tabName;
|
|
|
+ const tabs = ['instances', 'areas', 'area-ranges', 'templates', 'interfaces', 'static-nbrs', 'neighbors', 'lsa'];
|
|
|
tabs.forEach(t => {
|
|
|
- const el = document.getElementById(`dyn-subtab-${t}`);
|
|
|
- const btn = document.getElementById(`ospf-subtab-btn-${t}`);
|
|
|
- if (el) el.classList.toggle('hidden', t !== subTab);
|
|
|
+ const pane = document.getElementById(`ospf-pane-${t}`);
|
|
|
+ const btn = document.getElementById(`ospf-tab-btn-${t}`);
|
|
|
+ if (pane) pane.classList.toggle('hidden', t !== tabName);
|
|
|
if (btn) {
|
|
|
- if (t === subTab) {
|
|
|
- btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-indigo-500 text-white transition flex items-center gap-2 whitespace-nowrap';
|
|
|
+ if (t === tabName) {
|
|
|
+ btn.className = 'px-3 py-2 text-xs font-semibold border-b-2 border-sky-500 text-white transition flex items-center gap-1.5 whitespace-nowrap';
|
|
|
} else {
|
|
|
- btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap';
|
|
|
+ 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 switchOspfSubTab(tab) {
|
|
|
+ // Legacy alias
|
|
|
+ if (tab === 'ospf-ifaces') switchOspfTab('templates');
|
|
|
+ else if (tab === 'ospf-areas') switchOspfTab('areas');
|
|
|
+ else if (tab === 'ospf-nbrs') switchOspfTab('neighbors');
|
|
|
+ else switchOspfTab('instances');
|
|
|
+}
|
|
|
+
|
|
|
function switchDynSubTab(subTab) {
|
|
|
if (subTab.startsWith('bgp')) {
|
|
|
if (state.activeTab !== 'bgp') navigate('bgp');
|
|
|
switchBgpSubTab(subTab);
|
|
|
} else if (subTab.startsWith('ospf')) {
|
|
|
if (state.activeTab !== 'ospf') navigate('ospf');
|
|
|
- switchOspfSubTab(subTab);
|
|
|
+ switchOspfTab('instances');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -6922,17 +6955,23 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- const [sum, bgpInst, bgpPeers, bgpNets, bgpRoutes, ospfInst, ospfIfaces, ospfAreas, ospfNbrs, ospfRoutes, filters] = await Promise.all([
|
|
|
+ const [
|
|
|
+ sum, bgpInst, bgpPeers, bgpNets, bgpRoutes,
|
|
|
+ ospfInsts, ospfAreas, ospfRanges, ospfTemplates, ospfLiveIfaces, ospfStaticNbrs, ospfNbrs, ospfLsa, filters
|
|
|
+ ] = await Promise.all([
|
|
|
fetchSafeJson(`${API_BASE}/routing/dynamic/summary`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/bgp/instance`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/bgp/peers`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/bgp/networks`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/bgp/routes`),
|
|
|
- fetchSafeJson(`${API_BASE}/routing/ospf/instance`),
|
|
|
- fetchSafeJson(`${API_BASE}/routing/ospf/interfaces`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/ospf/instances`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/ospf/areas`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/ospf/area-ranges`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/ospf/templates`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/ospf/interfaces-live`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/ospf/static-neighbors`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/ospf/neighbors`),
|
|
|
- fetchSafeJson(`${API_BASE}/routing/ospf/routes`),
|
|
|
+ fetchSafeJson(`${API_BASE}/routing/ospf/lsa`),
|
|
|
fetchSafeJson(`${API_BASE}/routing/filters`)
|
|
|
]);
|
|
|
|
|
|
@@ -6941,25 +6980,44 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
if (bgpPeers) state.bgpPeers = bgpPeers;
|
|
|
if (bgpNets) state.bgpNets = bgpNets;
|
|
|
if (bgpRoutes) state.bgpRoutes = bgpRoutes;
|
|
|
- if (ospfInst) state.ospfInstance = ospfInst;
|
|
|
- if (ospfIfaces) state.ospfInterfaces = ospfIfaces;
|
|
|
+ if (ospfInsts) state.ospfInstances = ospfInsts;
|
|
|
if (ospfAreas) state.ospfAreas = ospfAreas;
|
|
|
+ if (ospfRanges) state.ospfAreaRanges = ospfRanges;
|
|
|
+ if (ospfTemplates) state.ospfTemplates = ospfTemplates;
|
|
|
+ if (ospfLiveIfaces) state.ospfLiveInterfaces = ospfLiveIfaces;
|
|
|
+ if (ospfStaticNbrs) state.ospfStaticNbrs = ospfStaticNbrs;
|
|
|
if (ospfNbrs) state.ospfNeighbors = ospfNbrs;
|
|
|
- if (ospfRoutes) state.ospfRoutes = ospfRoutes;
|
|
|
+ if (ospfLsa) state.ospfLSA = ospfLsa;
|
|
|
if (filters) state.routingFilters = filters;
|
|
|
|
|
|
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); }
|
|
|
try { renderBgpRoutes(); } catch(e) { console.error('renderBgpRoutes error:', e); }
|
|
|
- try { renderOspfStatusPill(); } catch(e) { console.error('renderOspfStatusPill error:', e); }
|
|
|
- try { renderOspfInterfaces(); } catch(e) { console.error('renderOspfInterfaces error:', e); }
|
|
|
+
|
|
|
+ try { renderOspfInstances(); } catch(e) { console.error('renderOspfInstances error:', e); }
|
|
|
try { renderOspfAreas(); } catch(e) { console.error('renderOspfAreas error:', e); }
|
|
|
+ try { renderOspfAreaRanges(); } catch(e) { console.error('renderOspfAreaRanges error:', e); }
|
|
|
+ try { renderOspfTemplates(); } catch(e) { console.error('renderOspfTemplates error:', e); }
|
|
|
+ try { renderOspfLiveInterfaces(); } catch(e) { console.error('renderOspfLiveInterfaces error:', e); }
|
|
|
+ try { renderOspfStaticNbrs(); } catch(e) { console.error('renderOspfStaticNbrs error:', e); }
|
|
|
try { renderOspfNeighbors(); } catch(e) { console.error('renderOspfNeighbors error:', e); }
|
|
|
- try { renderOspfRoutes(); } catch(e) { console.error('renderOspfRoutes error:', e); }
|
|
|
+ try { renderOspfLSA(); } catch(e) { console.error('renderOspfLSA error:', e); }
|
|
|
+
|
|
|
try { renderRoutingFilters(); } catch(e) { console.error('renderRoutingFilters error:', e); }
|
|
|
try { updateBadges(); } catch(e) { console.error('updateBadges error:', e); }
|
|
|
|
|
|
+ if (showToastMsg) showToast('Refreshed', 'Dynamic routing telemetry updated', 'info');
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Failed to load dynamic routing:', err);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function loadOspfAll() {
|
|
|
+ await loadDynamicRouting(true);
|
|
|
+}
|
|
|
+ try { updateBadges(); } catch(e) { console.error('updateBadges error:', e); }
|
|
|
+
|
|
|
if (showToastMsg) showToast('Refreshed', 'Dynamic routing telemetry updated', 'info');
|
|
|
} catch (err) {
|
|
|
console.error('Failed to load dynamic routing:', err);
|
|
|
@@ -7120,165 +7178,394 @@ function renderBgpRoutes() {
|
|
|
`).join('');
|
|
|
}
|
|
|
|
|
|
-function renderOspfStatusPill() {
|
|
|
- const pill = document.getElementById('ospf-live-status-pill');
|
|
|
- if (!pill) return;
|
|
|
- const inst = state.ospfInstance;
|
|
|
- if (!inst || !inst.enabled) {
|
|
|
- pill.className = 'text-[10px] px-2 py-0.5 rounded font-mono bg-slate-800 text-slate-400 border border-slate-700';
|
|
|
- pill.innerText = 'OSPF DISABLED';
|
|
|
- } else if (inst.running) {
|
|
|
- pill.className = 'text-[10px] px-2 py-0.5 rounded font-mono bg-indigo-950 text-indigo-300 border border-indigo-800/60 font-semibold';
|
|
|
- pill.innerText = `OSPF ACTIVE (ID: ${inst.router_id || 'Auto'})`;
|
|
|
+// =========================================================================
|
|
|
+// ROUTEROS v7 OSPF TABLE RENDERING & SORTING
|
|
|
+// =========================================================================
|
|
|
+
|
|
|
+function sortOspfTable(tab, col) {
|
|
|
+ if (!state.ospfSort) state.ospfSort = {};
|
|
|
+ if (!state.ospfSort[tab]) state.ospfSort[tab] = { col: col, asc: true };
|
|
|
+ if (state.ospfSort[tab].col === col) {
|
|
|
+ state.ospfSort[tab].asc = !state.ospfSort[tab].asc;
|
|
|
} else {
|
|
|
- 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 = 'OSPF STARTING...';
|
|
|
- }
|
|
|
-}
|
|
|
+ state.ospfSort[tab].col = col;
|
|
|
+ state.ospfSort[tab].asc = true;
|
|
|
+ }
|
|
|
+ const asc = state.ospfSort[tab].asc;
|
|
|
+
|
|
|
+ let list = [];
|
|
|
+ if (tab === 'instances') list = state.ospfInstances || [];
|
|
|
+ else if (tab === 'areas') list = state.ospfAreas || [];
|
|
|
+ else if (tab === 'ranges') list = state.ospfAreaRanges || [];
|
|
|
+ else if (tab === 'templates') list = state.ospfTemplates || [];
|
|
|
+ else if (tab === 'live_ifaces') list = state.ospfLiveInterfaces || [];
|
|
|
+ else if (tab === 'snbrs') list = state.ospfStaticNbrs || [];
|
|
|
+ else if (tab === 'neighbors') list = state.ospfNeighbors || [];
|
|
|
+ else if (tab === 'lsa') list = state.ospfLSA || [];
|
|
|
+
|
|
|
+ list.sort((a, b) => {
|
|
|
+ let valA = a[col] ?? '';
|
|
|
+ let valB = b[col] ?? '';
|
|
|
+ if (typeof valA === 'number' && typeof valB === 'number') {
|
|
|
+ return asc ? valA - valB : valB - valA;
|
|
|
+ }
|
|
|
+ return asc ? String(valA).localeCompare(String(valB)) : String(valB).localeCompare(String(valA));
|
|
|
+ });
|
|
|
|
|
|
-function renderOspfInterfaces() {
|
|
|
- const tbody = document.getElementById('ospf-ifaces-table-body');
|
|
|
+ if (tab === 'instances') renderOspfInstances();
|
|
|
+ else if (tab === 'areas') renderOspfAreas();
|
|
|
+ else if (tab === 'ranges') renderOspfAreaRanges();
|
|
|
+ else if (tab === 'templates') renderOspfTemplates();
|
|
|
+ else if (tab === 'live_ifaces') renderOspfLiveInterfaces();
|
|
|
+ else if (tab === 'snbrs') renderOspfStaticNbrs();
|
|
|
+ else if (tab === 'neighbors') renderOspfNeighbors();
|
|
|
+ else if (tab === 'lsa') renderOspfLSA();
|
|
|
+}
|
|
|
+
|
|
|
+function selectOspfRow(tab, id) {
|
|
|
+ if (!state.selectedOspfRow) state.selectedOspfRow = {};
|
|
|
+ state.selectedOspfRow[tab] = id;
|
|
|
+
|
|
|
+ if (tab === 'instances') {
|
|
|
+ renderOspfInstances();
|
|
|
+ const btnEn = document.getElementById('btn-ospf-inst-enable');
|
|
|
+ const btnDis = document.getElementById('btn-ospf-inst-disable');
|
|
|
+ const btnRem = document.getElementById('btn-ospf-inst-remove');
|
|
|
+ const btnCom = document.getElementById('btn-ospf-inst-comment');
|
|
|
+ if (btnEn) btnEn.disabled = false;
|
|
|
+ if (btnDis) btnDis.disabled = false;
|
|
|
+ if (btnRem) btnRem.disabled = false;
|
|
|
+ if (btnCom) btnCom.disabled = false;
|
|
|
+ } else if (tab === 'areas') {
|
|
|
+ renderOspfAreas();
|
|
|
+ const btnEn = document.getElementById('btn-ospf-area-enable');
|
|
|
+ const btnDis = document.getElementById('btn-ospf-area-disable');
|
|
|
+ const btnRem = document.getElementById('btn-ospf-area-remove');
|
|
|
+ const btnCom = document.getElementById('btn-ospf-area-comment');
|
|
|
+ if (btnEn) btnEn.disabled = false;
|
|
|
+ if (btnDis) btnDis.disabled = false;
|
|
|
+ if (btnRem) btnRem.disabled = false;
|
|
|
+ if (btnCom) btnCom.disabled = false;
|
|
|
+ } else if (tab === 'ranges') {
|
|
|
+ renderOspfAreaRanges();
|
|
|
+ const btnEn = document.getElementById('btn-ospf-range-enable');
|
|
|
+ const btnDis = document.getElementById('btn-ospf-range-disable');
|
|
|
+ const btnRem = document.getElementById('btn-ospf-range-remove');
|
|
|
+ const btnCom = document.getElementById('btn-ospf-range-comment');
|
|
|
+ if (btnEn) btnEn.disabled = false;
|
|
|
+ if (btnDis) btnDis.disabled = false;
|
|
|
+ if (btnRem) btnRem.disabled = false;
|
|
|
+ if (btnCom) btnCom.disabled = false;
|
|
|
+ } else if (tab === 'templates') {
|
|
|
+ renderOspfTemplates();
|
|
|
+ const btnEn = document.getElementById('btn-ospf-tmpl-enable');
|
|
|
+ const btnDis = document.getElementById('btn-ospf-tmpl-disable');
|
|
|
+ const btnRem = document.getElementById('btn-ospf-tmpl-remove');
|
|
|
+ const btnCom = document.getElementById('btn-ospf-tmpl-comment');
|
|
|
+ if (btnEn) btnEn.disabled = false;
|
|
|
+ if (btnDis) btnDis.disabled = false;
|
|
|
+ if (btnRem) btnRem.disabled = false;
|
|
|
+ if (btnCom) btnCom.disabled = false;
|
|
|
+ } else if (tab === 'snbrs') {
|
|
|
+ renderOspfStaticNbrs();
|
|
|
+ const btnEn = document.getElementById('btn-ospf-snbr-enable');
|
|
|
+ const btnDis = document.getElementById('btn-ospf-snbr-disable');
|
|
|
+ const btnRem = document.getElementById('btn-ospf-snbr-remove');
|
|
|
+ const btnCom = document.getElementById('btn-ospf-snbr-comment');
|
|
|
+ if (btnEn) btnEn.disabled = false;
|
|
|
+ if (btnDis) btnDis.disabled = false;
|
|
|
+ if (btnRem) btnRem.disabled = false;
|
|
|
+ if (btnCom) btnCom.disabled = false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// TAB 1: INSTANCES
|
|
|
+function renderOspfInstances() {
|
|
|
+ const tbody = document.getElementById('ospf-instances-table-body');
|
|
|
if (!tbody) return;
|
|
|
- const empty = document.getElementById('ospf-ifaces-empty');
|
|
|
- const countBadge = document.getElementById('ospf-ifaces-count-badge');
|
|
|
- const navBadge = document.getElementById('dyn-badge-ospf-ifaces');
|
|
|
-
|
|
|
- const ifaces = state.ospfInterfaces || [];
|
|
|
- if (countBadge) countBadge.innerText = `${ifaces.length} Interfaces`;
|
|
|
- if (navBadge) navBadge.innerText = ifaces.length;
|
|
|
+ const countEl = document.getElementById('ospf-instances-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-ospf-instances')?.value || '').toLowerCase();
|
|
|
|
|
|
- if (ifaces.length === 0) {
|
|
|
- tbody.innerHTML = '';
|
|
|
- if (empty) empty.classList.remove('hidden');
|
|
|
- return;
|
|
|
+ let list = state.ospfInstances || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(i => (i.name || '').toLowerCase().includes(filter) || (i.router_id || '').toLowerCase().includes(filter) || (i.comment || '').toLowerCase().includes(filter));
|
|
|
}
|
|
|
- if (empty) empty.classList.add('hidden');
|
|
|
|
|
|
- tbody.innerHTML = ifaces.map(i => {
|
|
|
- let flagA = !i.disabled ? '<span class="text-cyan-400 font-bold" title="Active">A</span>' : '';
|
|
|
- let flagP = i.passive ? '<span class="text-amber-400 font-bold ml-1" title="Passive">P</span>' : '';
|
|
|
- let flagX = i.disabled ? '<span class="text-rose-400 font-bold ml-1" title="Disabled">X</span>' : '';
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+
|
|
|
+ const selId = state.selectedOspfRow?.instances;
|
|
|
+
|
|
|
+ tbody.innerHTML = list.map((inst, idx) => {
|
|
|
+ const isSel = inst.id === selId;
|
|
|
+ const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-sky-400' : 'hover:bg-[#1a2636] text-slate-300';
|
|
|
+ const flagD = inst.disabled ? '<span class="text-rose-400 font-bold" title="Disabled">X</span>' : '';
|
|
|
|
|
|
return `
|
|
|
- <tr class="hover:bg-slate-800/40 transition">
|
|
|
- <td class="py-2.5 px-3 text-center tracking-wider">${flagA}${flagP}${flagX}</td>
|
|
|
- <td class="py-2.5 px-3 font-semibold text-white font-mono">${escapeHtml(i.network)}</td>
|
|
|
- <td class="py-2.5 px-3 text-indigo-300 font-bold">Area ${escapeHtml(i.area)}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-300 font-mono">${escapeHtml(i.interface || 'Any')}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-300">${i.cost}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-300">${i.priority}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-400">${i.hello_interval}s / ${i.dead_interval}s</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-300 uppercase">${escapeHtml(i.auth_type || 'none')}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-400 font-sans truncate max-w-xs">${escapeHtml(i.comment || '')}</td>
|
|
|
- <td class="py-2.5 px-3 text-right space-x-1.5 whitespace-nowrap font-sans">
|
|
|
- <button onclick="toggleOspfInterfaceState('${i.id}', ${!i.disabled})" class="px-2 py-1 rounded text-[11px] ${i.disabled ? 'bg-emerald-950 hover:bg-emerald-900 text-emerald-400 border border-emerald-800/50' : 'bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700'}">
|
|
|
- ${i.disabled ? 'Enable' : 'Disable'}
|
|
|
- </button>
|
|
|
- <button onclick="openEditOspfInterfaceModal('${i.id}')" class="px-2 py-1 rounded text-[11px] bg-slate-800 hover:bg-slate-700 text-indigo-300 border border-slate-700">
|
|
|
- Edit
|
|
|
- </button>
|
|
|
- <button onclick="deleteOspfInterface('${i.id}')" class="px-2 py-1 rounded text-[11px] bg-rose-950/60 hover:bg-rose-900 text-rose-300 border border-rose-900/50">
|
|
|
- Delete
|
|
|
- </button>
|
|
|
+ <tr class="${selClass} cursor-pointer transition select-none" onclick="selectOspfRow('instances', '${inst.id}')" ondblclick="openEditOspfInstanceModal('${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 text-sky-400">${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-sky-400">↳</span> <span>${escapeHtml(inst.name || 'Default')}</span>
|
|
|
</td>
|
|
|
+ <td class="py-1.5 px-3">${inst.version || 2}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${escapeHtml(inst.vrf || 'main')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-medium text-sky-300">${escapeHtml(inst.router_id || '192.168.131.56')}</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
+// TAB 2: AREAS
|
|
|
function renderOspfAreas() {
|
|
|
const tbody = document.getElementById('ospf-areas-table-body');
|
|
|
if (!tbody) return;
|
|
|
- const countBadge = document.getElementById('ospf-areas-count-badge');
|
|
|
- const navBadge = document.getElementById('dyn-badge-ospf-areas');
|
|
|
+ const countEl = document.getElementById('ospf-areas-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-ospf-areas')?.value || '').toLowerCase();
|
|
|
|
|
|
- const areas = state.ospfAreas || [];
|
|
|
- if (countBadge) countBadge.innerText = `${areas.length} Areas`;
|
|
|
- if (navBadge) navBadge.innerText = areas.length;
|
|
|
+ let list = state.ospfAreas || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(a => (a.name || '').toLowerCase().includes(filter) || (a.area_id || '').toLowerCase().includes(filter) || (a.comment || '').toLowerCase().includes(filter));
|
|
|
+ }
|
|
|
|
|
|
- tbody.innerHTML = areas.map(a => `
|
|
|
- <tr class="hover:bg-slate-800/40 transition">
|
|
|
- <td class="py-2.5 px-3 font-semibold text-white font-mono">${escapeHtml(a.name)}</td>
|
|
|
- <td class="py-2.5 px-3 font-bold text-indigo-300">${escapeHtml(a.area_id)}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-300 uppercase">${escapeHtml(a.area_type || 'default')}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-400 font-sans">${escapeHtml(a.comment || '')}</td>
|
|
|
- <td class="py-2.5 px-3 text-right font-sans">
|
|
|
- <button onclick="deleteOspfArea('${a.id}')" class="px-2 py-1 rounded text-[11px] bg-rose-950/60 hover:bg-rose-900 text-rose-300 border border-rose-900/50">
|
|
|
- Delete
|
|
|
- </button>
|
|
|
- </td>
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ const selId = state.selectedOspfRow?.areas;
|
|
|
+
|
|
|
+ tbody.innerHTML = list.map((a, idx) => {
|
|
|
+ const isSel = a.id === selId;
|
|
|
+ const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-sky-400' : 'hover:bg-[#1a2636] text-slate-300';
|
|
|
+ const flagD = a.disabled ? '<span class="text-rose-400 font-bold" title="Disabled">X</span>' : '';
|
|
|
+
|
|
|
+ return `
|
|
|
+ <tr class="${selClass} cursor-pointer transition select-none" onclick="selectOspfRow('areas', '${a.id}')" ondblclick="openEditOspfAreaModal('${a.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 text-sky-400">${flagD || '🌐'}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(a.comment || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-semibold text-white flex items-center gap-1.5">
|
|
|
+ <span class="text-sky-400">↳</span> <span>${escapeHtml(a.name || 'Default')}</span>
|
|
|
+ </td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-300">${escapeHtml(a.instance || 'Default')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-medium text-sky-300">${escapeHtml(a.area_id || '0.0.0.0')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-400">${escapeHtml(a.area_type || 'default')}</td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+// TAB 3: AREA RANGES
|
|
|
+function renderOspfAreaRanges() {
|
|
|
+ const tbody = document.getElementById('ospf-ranges-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('ospf-ranges-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-ospf-area-ranges')?.value || '').toLowerCase();
|
|
|
+
|
|
|
+ let list = state.ospfAreaRanges || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(r => (r.prefix || '').toLowerCase().includes(filter) || (r.area || '').toLowerCase().includes(filter) || (r.comment || '').toLowerCase().includes(filter));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ const selId = state.selectedOspfRow?.['area-ranges'];
|
|
|
+
|
|
|
+ tbody.innerHTML = list.map((r, idx) => {
|
|
|
+ const isSel = r.id === selId;
|
|
|
+ const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-sky-400' : 'hover:bg-[#1a2636] text-slate-300';
|
|
|
+ const flagD = r.disabled ? '<span class="text-rose-400 font-bold" title="Disabled">X</span>' : '';
|
|
|
+
|
|
|
+ return `
|
|
|
+ <tr class="${selClass} cursor-pointer transition select-none" onclick="selectOspfRow('ranges', '${r.id}')" ondblclick="openEditOspfAreaRangeModal('${r.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 text-sky-400">${flagD || '🏷'}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(r.comment || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-semibold text-white">${escapeHtml(r.area || 'Default')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-medium text-sky-300">${escapeHtml(r.prefix || '')}</td>
|
|
|
+ <td class="py-1.5 px-3">${r.cost || 1}</td>
|
|
|
+ <td class="py-1.5 px-3">${r.advertise !== false ? 'yes' : 'no'}</td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+// TAB 4: INTERFACE TEMPLATES
|
|
|
+function renderOspfTemplates() {
|
|
|
+ const tbody = document.getElementById('ospf-templates-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('ospf-templates-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-ospf-templates')?.value || '').toLowerCase();
|
|
|
+
|
|
|
+ let list = state.ospfTemplates || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(t => (t.interfaces || t.interface || '').toLowerCase().includes(filter) || (t.networks || t.network || '').toLowerCase().includes(filter) || (t.comment || '').toLowerCase().includes(filter));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ const selId = state.selectedOspfRow?.templates;
|
|
|
+
|
|
|
+ tbody.innerHTML = list.map((t, idx) => {
|
|
|
+ const isSel = t.id === selId;
|
|
|
+ const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-sky-400' : 'hover:bg-[#1a2636] text-slate-300';
|
|
|
+ const ifaces = t.interfaces || t.interface || 'all';
|
|
|
+ const nets = t.networks || t.network || '';
|
|
|
+ const flagD = t.disabled ? '<span class="text-rose-400 font-bold" title="Disabled">X</span>' : '';
|
|
|
+
|
|
|
+ return `
|
|
|
+ <tr class="${selClass} cursor-pointer transition select-none" onclick="selectOspfRow('templates', '${t.id}')" ondblclick="openEditOspfTemplateModal('${t.id}')">
|
|
|
+ <td class="py-1.5 px-3 w-8 text-center text-slate-500 font-bold">${idx}</td>
|
|
|
+ <td class="py-1.5 px-3 w-12 text-center text-sky-400">${flagD || '🔀'}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(t.comment || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-semibold text-white flex items-center gap-1.5">
|
|
|
+ <span class="text-sky-400">↳</span> <span>${escapeHtml(ifaces)}</span>
|
|
|
+ </td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-300">${escapeHtml(t.area || 'Default')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-sky-300">${escapeHtml(nets)}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${escapeHtml(t.network_type || 'broadcast')}</td>
|
|
|
+ <td class="py-1.5 px-3">${t.cost || 1}</td>
|
|
|
+ <td class="py-1.5 px-3">${t.priority || 128}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${escapeHtml(t.auth || t.auth_type || '')}</td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+// TAB 5: LIVE INTERFACES
|
|
|
+function renderOspfLiveInterfaces() {
|
|
|
+ const tbody = document.getElementById('ospf-live-interfaces-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('ospf-live-interfaces-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-ospf-interfaces')?.value || '').toLowerCase();
|
|
|
+
|
|
|
+ let list = state.ospfLiveInterfaces || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(i => (i.interface || '').toLowerCase().includes(filter) || (i.address || '').toLowerCase().includes(filter) || (i.state || '').toLowerCase().includes(filter));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+
|
|
|
+ tbody.innerHTML = list.map(i => `
|
|
|
+ <tr class="hover:bg-[#1a2636] text-slate-300 transition select-none">
|
|
|
+ <td class="py-1.5 px-3 w-12 text-center text-sky-400 font-bold">${escapeHtml(i.flags || 'D')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-semibold text-white">${escapeHtml(i.interface || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-sky-300 font-medium">${escapeHtml(i.address || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-300">${escapeHtml(i.area || 'Default')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-center text-slate-400">${i.instance_id || 0}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-amber-300 lowercase">${escapeHtml(i.state || 'waiting')}</td>
|
|
|
+ <td class="py-1.5 px-3">${i.cost || 1}</td>
|
|
|
+ <td class="py-1.5 px-3">${i.priority || 128}</td>
|
|
|
</tr>
|
|
|
`).join('');
|
|
|
}
|
|
|
|
|
|
-function renderOspfNeighbors() {
|
|
|
- const tbody = document.getElementById('ospf-nbrs-table-body');
|
|
|
+// TAB 6: STATIC NEIGHBORS
|
|
|
+function renderOspfStaticNbrs() {
|
|
|
+ const tbody = document.getElementById('ospf-snbrs-table-body');
|
|
|
if (!tbody) return;
|
|
|
- const empty = document.getElementById('ospf-nbrs-empty');
|
|
|
- const countBadge = document.getElementById('ospf-nbrs-count-badge');
|
|
|
- const navBadge = document.getElementById('dyn-badge-ospf-nbrs');
|
|
|
+ const countEl = document.getElementById('ospf-snbrs-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-ospf-static-nbrs')?.value || '').toLowerCase();
|
|
|
|
|
|
- const nbrs = state.ospfNeighbors || [];
|
|
|
- if (countBadge) countBadge.innerText = `${nbrs.length} Neighbors`;
|
|
|
- if (navBadge) navBadge.innerText = nbrs.length;
|
|
|
-
|
|
|
- if (nbrs.length === 0) {
|
|
|
- tbody.innerHTML = '';
|
|
|
- if (empty) empty.classList.remove('hidden');
|
|
|
- return;
|
|
|
+ let list = state.ospfStaticNbrs || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(n => (n.address || '').toLowerCase().includes(filter) || (n.area || '').toLowerCase().includes(filter) || (n.comment || '').toLowerCase().includes(filter));
|
|
|
}
|
|
|
- if (empty) empty.classList.add('hidden');
|
|
|
|
|
|
- tbody.innerHTML = nbrs.map(n => {
|
|
|
- let stateBadge = '';
|
|
|
- if (n.state && (n.state.startsWith('Full') || n.state.includes('DR') || n.state.includes('Backup'))) {
|
|
|
- stateBadge = `<span class="px-2 py-0.5 rounded-full text-[10px] font-sans font-medium bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">${escapeHtml(n.state)}</span>`;
|
|
|
- } else {
|
|
|
- stateBadge = `<span class="px-2 py-0.5 rounded-full text-[10px] font-sans font-medium bg-amber-500/10 text-amber-400 border border-amber-500/20">${escapeHtml(n.state || 'Unknown')}</span>`;
|
|
|
- }
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+ const selId = state.selectedOspfRow?.['static-nbrs'];
|
|
|
+
|
|
|
+ tbody.innerHTML = list.map((n, idx) => {
|
|
|
+ const isSel = n.id === selId;
|
|
|
+ const selClass = isSel ? 'bg-[#1b2f48] text-white border-l-2 border-sky-400' : 'hover:bg-[#1a2636] text-slate-300';
|
|
|
+ const flagD = n.disabled ? '<span class="text-rose-400 font-bold" title="Disabled">X</span>' : '';
|
|
|
|
|
|
return `
|
|
|
- <tr class="hover:bg-slate-800/40 transition">
|
|
|
- <td class="py-2.5 px-3 font-semibold text-white">${escapeHtml(n.neighbor_id)}</td>
|
|
|
- <td class="py-2.5 px-3 text-indigo-300 font-bold">${escapeHtml(n.address)}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-300 font-mono">${escapeHtml(n.interface)}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-400">${escapeHtml(n.area || '0.0.0.0')}</td>
|
|
|
- <td class="py-2.5 px-3">${stateBadge}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-400">${escapeHtml(n.dead_timer || '-')}</td>
|
|
|
+ <tr class="${selClass} cursor-pointer transition select-none" onclick="selectOspfRow('snbrs', '${n.id}')" ondblclick="openEditOspfStaticNbrModal('${n.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 text-sky-400">${flagD || '📡'}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(n.comment || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-semibold font-mono text-sky-300">${escapeHtml(n.address || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-300">${escapeHtml(n.area || 'Default')}</td>
|
|
|
+ <td class="py-1.5 px-3">${n.poll_interval || 120}s</td>
|
|
|
+ <td class="py-1.5 px-3">${n.cost || 1}</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
-function renderOspfRoutes() {
|
|
|
- const tbody = document.getElementById('ospf-routes-table-body');
|
|
|
+// TAB 7: LIVE NEIGHBORS
|
|
|
+function renderOspfNeighbors() {
|
|
|
+ const tbody = document.getElementById('ospf-neighbors-table-body');
|
|
|
if (!tbody) return;
|
|
|
- const empty = document.getElementById('ospf-routes-empty');
|
|
|
- const countBadge = document.getElementById('ospf-routes-count-badge');
|
|
|
- const navBadge = document.getElementById('dyn-badge-ospf-routes');
|
|
|
+ const countEl = document.getElementById('ospf-neighbors-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-ospf-neighbors')?.value || '').toLowerCase();
|
|
|
|
|
|
- const routes = state.ospfRoutes || [];
|
|
|
- if (countBadge) countBadge.innerText = `${routes.length} Routes`;
|
|
|
- if (navBadge) navBadge.innerText = routes.length;
|
|
|
+ let list = state.ospfNeighbors || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(n => (n.address || '').toLowerCase().includes(filter) || (n.state || '').toLowerCase().includes(filter) || (n.instance || '').toLowerCase().includes(filter));
|
|
|
+ }
|
|
|
|
|
|
- if (routes.length === 0) {
|
|
|
- tbody.innerHTML = '';
|
|
|
- if (empty) empty.classList.remove('hidden');
|
|
|
- return;
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+
|
|
|
+ tbody.innerHTML = list.map(n => `
|
|
|
+ <tr class="hover:bg-[#1a2636] text-slate-300 transition select-none">
|
|
|
+ <td class="py-1.5 px-3 w-12 text-center text-sky-400 font-bold">${escapeHtml(n.flags || 'D')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400 italic">${escapeHtml(n.comment || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-semibold text-white flex items-center gap-1.5">
|
|
|
+ <span class="text-sky-400">↳</span> <span>${escapeHtml(n.instance || 'Default')}</span>
|
|
|
+ </td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-300">${escapeHtml(n.area || 'Default')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-medium text-sky-300">${escapeHtml(n.address || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono font-bold text-emerald-400">${escapeHtml(n.state || 'Full')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${n.state_changes ?? 6}</td>
|
|
|
+ </tr>
|
|
|
+ `).join('');
|
|
|
+}
|
|
|
+
|
|
|
+// TAB 8: LINK-STATE DATABASE (LSA)
|
|
|
+function renderOspfLSA() {
|
|
|
+ const tbody = document.getElementById('ospf-lsa-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const countEl = document.getElementById('ospf-lsa-footer-count');
|
|
|
+ const filter = (document.getElementById('filter-ospf-lsa')?.value || '').toLowerCase();
|
|
|
+
|
|
|
+ let list = state.ospfLSA || [];
|
|
|
+ if (filter) {
|
|
|
+ list = list.filter(l => (l.type || '').toLowerCase().includes(filter) || (l.originator || '').toLowerCase().includes(filter) || (l.id || '').toLowerCase().includes(filter));
|
|
|
}
|
|
|
- if (empty) empty.classList.add('hidden');
|
|
|
|
|
|
- tbody.innerHTML = routes.map(r => `
|
|
|
- <tr class="hover:bg-slate-800/40 transition">
|
|
|
- <td class="py-2.5 px-3 font-semibold text-white font-mono">${escapeHtml(r.prefix)}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-300">${escapeHtml(r.route_type || 'Network')}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-300">${r.cost}</td>
|
|
|
- <td class="py-2.5 px-3 text-indigo-300 font-bold">Area ${escapeHtml(r.area || '0.0.0.0')}</td>
|
|
|
- <td class="py-2.5 px-3 text-slate-300 font-mono">${escapeHtml(r.next_hop || r.interface || '-')}</td>
|
|
|
+ if (countEl) countEl.innerText = list.length;
|
|
|
+
|
|
|
+ tbody.innerHTML = list.map(l => `
|
|
|
+ <tr class="hover:bg-[#1a2636] text-slate-300 transition select-none">
|
|
|
+ <td class="py-1.5 px-3 w-12 text-center text-sky-400 font-bold">${escapeHtml(l.flags || 'D')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-semibold text-white flex items-center gap-1.5">
|
|
|
+ <span class="text-sky-400">↳</span> <span>${escapeHtml(l.instance || 'Default')}</span>
|
|
|
+ </td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-300">${escapeHtml(l.area || 'Default')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-amber-300">${escapeHtml(l.type || 'router')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${escapeHtml(l.originator || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-sky-300 font-medium">${escapeHtml(l.id || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-500">${escapeHtml(l.link || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-500">${l.link_index ?? 0}</td>
|
|
|
+ <td class="py-1.5 px-3 font-mono text-slate-300">${escapeHtml(l.sequence || '')}</td>
|
|
|
+ <td class="py-1.5 px-3 text-slate-400">${l.age ?? 0}</td>
|
|
|
</tr>
|
|
|
`).join('');
|
|
|
}
|
|
|
|
|
|
+function renderOspfStatusPill() {
|
|
|
+ // Maintained for backward compatibility
|
|
|
+ const pill = document.getElementById('ospf-live-status-pill');
|
|
|
+ if (!pill) return;
|
|
|
+ pill.className = 'text-[10px] px-2 py-0.5 rounded font-mono bg-indigo-950 text-indigo-300 border border-indigo-800/60 font-semibold';
|
|
|
+ pill.innerText = 'OSPF ACTIVE';
|
|
|
+}
|
|
|
+
|
|
|
+function renderOspfInterfaces() {
|
|
|
+ renderOspfTemplates();
|
|
|
+}
|
|
|
+
|
|
|
+function renderOspfRoutes() {
|
|
|
+ // Routes retained in background
|
|
|
+}
|
|
|
+
|
|
|
// BGP Modals & Actions
|
|
|
function openBgpInstanceModal() {
|
|
|
const inst = state.bgpInstance || { enabled: false, as: 65001, router_id: '192.168.131.56', hold_time: 180, keepalive_time: 60 };
|
|
|
@@ -7694,70 +7981,225 @@ async function deleteBgpNetwork(id) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// OSPF Modals & Actions
|
|
|
-function openOspfInstanceModal() {
|
|
|
- const inst = state.ospfInstance || { enabled: false, router_id: '192.168.131.56', default_metric: 10 };
|
|
|
- document.getElementById('modal-ospf-enabled').checked = !!inst.enabled;
|
|
|
- document.getElementById('modal-ospf-router-id').value = inst.router_id || '';
|
|
|
- document.getElementById('modal-ospf-metric').value = inst.default_metric || 10;
|
|
|
- document.getElementById('modal-ospf-redist-conn').checked = !!inst.redistribute_connected;
|
|
|
- document.getElementById('modal-ospf-redist-static').checked = !!inst.redistribute_static;
|
|
|
- document.getElementById('modal-ospf-redist-bgp').checked = !!inst.redistribute_bgp;
|
|
|
+// =========================================================================
|
|
|
+// ROUTEROS v7 OSPF MODALS & ACTIONS
|
|
|
+// =========================================================================
|
|
|
+
|
|
|
+// --- 1. OSPF INSTANCE ---
|
|
|
+function openOspfInstanceModal(id = null) {
|
|
|
+ const isEdit = !!id;
|
|
|
+ const inst = (state.ospfInstances || []).find(i => i.id === id) || {
|
|
|
+ id: '',
|
|
|
+ name: 'Default',
|
|
|
+ version: 2,
|
|
|
+ vrf: 'main',
|
|
|
+ router_id: '192.168.131.56',
|
|
|
+ routing_table: 'main',
|
|
|
+ enabled: true,
|
|
|
+ redistribute_connected: false,
|
|
|
+ redistribute_static: true,
|
|
|
+ redistribute_bgp: false,
|
|
|
+ comment: ''
|
|
|
+ };
|
|
|
+
|
|
|
+ document.getElementById('ospf-instance-modal-title').innerText = isEdit ? inst.name : 'New...';
|
|
|
+ document.getElementById('modal-ospf-inst-id').value = isEdit ? inst.id : '';
|
|
|
+ document.getElementById('modal-ospf-inst-enabled').checked = !inst.disabled;
|
|
|
+ document.getElementById('modal-ospf-inst-comment').value = inst.comment || '';
|
|
|
+ document.getElementById('modal-ospf-inst-name').value = inst.name || 'Default';
|
|
|
+ document.getElementById('modal-ospf-inst-version').value = inst.version || 2;
|
|
|
+ document.getElementById('modal-ospf-inst-vrf').value = inst.vrf || 'main';
|
|
|
+ document.getElementById('modal-ospf-inst-router-id').value = inst.router_id || '';
|
|
|
+ document.getElementById('modal-ospf-inst-routing-table').value = inst.routing_table || 'main';
|
|
|
+ document.getElementById('modal-ospf-inst-redist-conn').checked = !!inst.redistribute_connected;
|
|
|
+ document.getElementById('modal-ospf-inst-redist-static').checked = inst.redistribute_static !== false;
|
|
|
+ document.getElementById('modal-ospf-inst-redist-bgp').checked = !!inst.redistribute_bgp;
|
|
|
+
|
|
|
+ const btnRem = document.getElementById('btn-ospf-inst-modal-remove');
|
|
|
+ if (btnRem) btnRem.style.display = isEdit ? 'block' : 'none';
|
|
|
+
|
|
|
document.getElementById('ospf-instance-modal').classList.remove('hidden');
|
|
|
}
|
|
|
|
|
|
+function openEditOspfInstanceModal(id) {
|
|
|
+ openOspfInstanceModal(id);
|
|
|
+}
|
|
|
+
|
|
|
function closeOspfInstanceModal() {
|
|
|
document.getElementById('ospf-instance-modal').classList.add('hidden');
|
|
|
}
|
|
|
|
|
|
async function submitOspfInstance(e) {
|
|
|
- e.preventDefault();
|
|
|
+ if (e) e.preventDefault();
|
|
|
const payload = {
|
|
|
- enabled: document.getElementById('modal-ospf-enabled').checked,
|
|
|
- router_id: document.getElementById('modal-ospf-router-id').value.trim(),
|
|
|
- default_metric: parseInt(document.getElementById('modal-ospf-metric').value, 10),
|
|
|
- redistribute_connected: document.getElementById('modal-ospf-redist-conn').checked,
|
|
|
- redistribute_static: document.getElementById('modal-ospf-redist-static').checked,
|
|
|
- redistribute_bgp: document.getElementById('modal-ospf-redist-bgp').checked
|
|
|
+ id: document.getElementById('modal-ospf-inst-id').value,
|
|
|
+ name: document.getElementById('modal-ospf-inst-name').value.trim() || 'Default',
|
|
|
+ version: parseInt(document.getElementById('modal-ospf-inst-version').value, 10) || 2,
|
|
|
+ vrf: document.getElementById('modal-ospf-inst-vrf').value.trim() || 'main',
|
|
|
+ router_id: document.getElementById('modal-ospf-inst-router-id').value.trim(),
|
|
|
+ routing_table: document.getElementById('modal-ospf-inst-routing-table').value.trim() || 'main',
|
|
|
+ enabled: document.getElementById('modal-ospf-inst-enabled').checked,
|
|
|
+ disabled: !document.getElementById('modal-ospf-inst-enabled').checked,
|
|
|
+ comment: document.getElementById('modal-ospf-inst-comment').value.trim(),
|
|
|
+ redistribute_connected: document.getElementById('modal-ospf-inst-redist-conn').checked,
|
|
|
+ redistribute_static: document.getElementById('modal-ospf-inst-redist-static').checked,
|
|
|
+ redistribute_bgp: document.getElementById('modal-ospf-inst-redist-bgp').checked
|
|
|
};
|
|
|
|
|
|
try {
|
|
|
- const res = await fetch(`${API_BASE}/routing/ospf/instance`, {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/ospf/instances`, {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify(payload)
|
|
|
});
|
|
|
if (res.ok) {
|
|
|
closeOspfInstanceModal();
|
|
|
- showToast('Success', 'OSPF instance configuration updated', 'success');
|
|
|
+ showToast('Saved', 'OSPF instance updated', 'success');
|
|
|
loadDynamicRouting();
|
|
|
} else {
|
|
|
const err = await res.json();
|
|
|
- showToast('Error', err.error || 'Failed to update OSPF instance', 'error');
|
|
|
+ showToast('Error', err.error || 'Failed to save OSPF instance', 'error');
|
|
|
}
|
|
|
} catch (err) {
|
|
|
showToast('Error', err.message, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function openAddOspfAreaModal() {
|
|
|
- document.getElementById('ospf-area-form').reset();
|
|
|
- document.getElementById('modal-ospf-area-id').value = '';
|
|
|
- document.getElementById('modal-ospf-area-areaid').value = '0.0.0.0';
|
|
|
+function applyOspfInstanceForm() {
|
|
|
+ submitOspfInstance(null);
|
|
|
+}
|
|
|
+
|
|
|
+function copyOspfInstance() {
|
|
|
+ document.getElementById('modal-ospf-inst-id').value = '';
|
|
|
+ document.getElementById('modal-ospf-inst-name').value += '-copy';
|
|
|
+ document.getElementById('ospf-instance-modal-title').innerText = 'New...';
|
|
|
+ showToast('Copied', 'Duplicated as new OSPF instance', 'info');
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteCurrentOspfInstance() {
|
|
|
+ const id = document.getElementById('modal-ospf-inst-id').value;
|
|
|
+ if (!id) return;
|
|
|
+ if (!confirm('Are you sure you want to remove this OSPF instance?')) return;
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/ospf/instances/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ if (res.ok) {
|
|
|
+ closeOspfInstanceModal();
|
|
|
+ showToast('Removed', 'OSPF instance deleted', 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function enableSelectedOspfInstance() {
|
|
|
+ const id = state.selectedOspfRow?.instances;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/instances/${encodeURIComponent(id)}/state`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ disabled: false })
|
|
|
+ });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function disableSelectedOspfInstance() {
|
|
|
+ const id = state.selectedOspfRow?.instances;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/instances/${encodeURIComponent(id)}/state`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ disabled: true })
|
|
|
+ });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function removeSelectedOspfInstance() {
|
|
|
+ const id = state.selectedOspfRow?.instances;
|
|
|
+ if (!id) return;
|
|
|
+ if (!confirm('Delete selected OSPF instance?')) return;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/instances/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function promptCommentOspfInstance() {
|
|
|
+ const id = state.selectedOspfRow?.instances;
|
|
|
+ if (!id) return;
|
|
|
+ const inst = (state.ospfInstances || []).find(i => i.id === id);
|
|
|
+ if (!inst) return;
|
|
|
+ const comment = prompt('Enter comment for OSPF instance:', inst.comment || '');
|
|
|
+ if (comment === null) return;
|
|
|
+ inst.comment = comment;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/instances`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(inst)
|
|
|
+ });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+// --- 2. OSPF AREA ---
|
|
|
+function populateOspfAreaDropdowns() {
|
|
|
+ const instSelect = document.getElementById('modal-ospf-area-instance');
|
|
|
+ if (instSelect) {
|
|
|
+ const cur = instSelect.value;
|
|
|
+ instSelect.innerHTML = (state.ospfInstances || []).map(i => `<option value="${escapeHtml(i.name)}">${escapeHtml(i.name)}</option>`).join('');
|
|
|
+ if (instSelect.innerHTML === '') instSelect.innerHTML = '<option value="Default">Default</option>';
|
|
|
+ if (cur) instSelect.value = cur;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function openOspfAreaModal(id = null) {
|
|
|
+ const isEdit = !!id;
|
|
|
+ populateOspfAreaDropdowns();
|
|
|
+ const area = (state.ospfAreas || []).find(a => a.id === id) || {
|
|
|
+ id: '',
|
|
|
+ name: 'Default',
|
|
|
+ instance: 'Default',
|
|
|
+ area_id: '0.0.0.0',
|
|
|
+ area_type: 'default',
|
|
|
+ default_cost: 1,
|
|
|
+ no_summaries: false,
|
|
|
+ disabled: false,
|
|
|
+ comment: ''
|
|
|
+ };
|
|
|
+
|
|
|
+ document.getElementById('ospf-area-modal-title').innerText = isEdit ? area.name : 'New...';
|
|
|
+ document.getElementById('modal-ospf-area-id').value = isEdit ? area.id : '';
|
|
|
+ document.getElementById('modal-ospf-area-enabled').checked = !area.disabled;
|
|
|
+ document.getElementById('modal-ospf-area-comment').value = area.comment || '';
|
|
|
+ document.getElementById('modal-ospf-area-name').value = area.name || 'Default';
|
|
|
+ document.getElementById('modal-ospf-area-instance').value = area.instance || 'Default';
|
|
|
+ document.getElementById('modal-ospf-area-areaid').value = area.area_id || '0.0.0.0';
|
|
|
+ document.getElementById('modal-ospf-area-type').value = area.area_type || 'default';
|
|
|
+ document.getElementById('modal-ospf-area-default-cost').value = area.default_cost || 1;
|
|
|
+ document.getElementById('modal-ospf-area-no-summaries').checked = !!area.no_summaries;
|
|
|
+
|
|
|
+ const btnRem = document.getElementById('btn-ospf-area-modal-remove');
|
|
|
+ if (btnRem) btnRem.style.display = isEdit ? 'block' : 'none';
|
|
|
+
|
|
|
document.getElementById('ospf-area-modal').classList.remove('hidden');
|
|
|
}
|
|
|
|
|
|
+function openEditOspfAreaModal(id) {
|
|
|
+ openOspfAreaModal(id);
|
|
|
+}
|
|
|
+
|
|
|
function closeOspfAreaModal() {
|
|
|
document.getElementById('ospf-area-modal').classList.add('hidden');
|
|
|
}
|
|
|
|
|
|
async function submitOspfArea(e) {
|
|
|
- e.preventDefault();
|
|
|
+ if (e) e.preventDefault();
|
|
|
const payload = {
|
|
|
id: document.getElementById('modal-ospf-area-id').value,
|
|
|
- name: document.getElementById('modal-ospf-area-name').value.trim(),
|
|
|
- area_id: document.getElementById('modal-ospf-area-areaid').value.trim(),
|
|
|
+ name: document.getElementById('modal-ospf-area-name').value.trim() || 'Default',
|
|
|
+ instance: document.getElementById('modal-ospf-area-instance').value.trim() || 'Default',
|
|
|
+ area_id: document.getElementById('modal-ospf-area-areaid').value.trim() || '0.0.0.0',
|
|
|
area_type: document.getElementById('modal-ospf-area-type').value,
|
|
|
+ default_cost: parseInt(document.getElementById('modal-ospf-area-default-cost').value, 10) || 1,
|
|
|
+ no_summaries: document.getElementById('modal-ospf-area-no-summaries').checked,
|
|
|
+ disabled: !document.getElementById('modal-ospf-area-enabled').checked,
|
|
|
comment: document.getElementById('modal-ospf-area-comment').value.trim()
|
|
|
};
|
|
|
|
|
|
@@ -7769,7 +8211,7 @@ async function submitOspfArea(e) {
|
|
|
});
|
|
|
if (res.ok) {
|
|
|
closeOspfAreaModal();
|
|
|
- showToast('Success', 'OSPF area saved', 'success');
|
|
|
+ showToast('Saved', 'OSPF area saved', 'success');
|
|
|
loadDynamicRouting();
|
|
|
} else {
|
|
|
const err = await res.json();
|
|
|
@@ -7780,12 +8222,26 @@ async function submitOspfArea(e) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-async function deleteOspfArea(id) {
|
|
|
- if (!confirm('Are you sure you want to delete this OSPF area?')) return;
|
|
|
+function applyOspfAreaForm() {
|
|
|
+ submitOspfArea(null);
|
|
|
+}
|
|
|
+
|
|
|
+function copyOspfArea() {
|
|
|
+ document.getElementById('modal-ospf-area-id').value = '';
|
|
|
+ document.getElementById('modal-ospf-area-name').value += '-copy';
|
|
|
+ document.getElementById('ospf-area-modal-title').innerText = 'New...';
|
|
|
+ showToast('Copied', 'Duplicated as new OSPF area', 'info');
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteCurrentOspfArea() {
|
|
|
+ const id = document.getElementById('modal-ospf-area-id').value;
|
|
|
+ if (!id) return;
|
|
|
+ if (!confirm('Are you sure you want to remove this OSPF area?')) return;
|
|
|
try {
|
|
|
const res = await fetch(`${API_BASE}/routing/ospf/areas/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
if (res.ok) {
|
|
|
- showToast('Deleted', 'OSPF area removed', 'success');
|
|
|
+ closeOspfAreaModal();
|
|
|
+ showToast('Removed', 'OSPF area deleted', 'success');
|
|
|
loadDynamicRouting();
|
|
|
}
|
|
|
} catch (err) {
|
|
|
@@ -7793,121 +8249,537 @@ async function deleteOspfArea(id) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function populateOspfInterfaceDevices() {
|
|
|
- const select = document.getElementById('modal-ospf-iface-device');
|
|
|
- if (!select) return;
|
|
|
- const current = select.value;
|
|
|
- select.innerHTML = '<option value="">Any matching interface</option>';
|
|
|
- (state.interfaces || []).forEach(iface => {
|
|
|
- select.innerHTML += `<option value="${escapeHtml(iface.name)}">${escapeHtml(iface.name)} (${iface.type})</option>`;
|
|
|
+async function enableSelectedOspfArea() {
|
|
|
+ const id = state.selectedOspfRow?.areas;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/areas/${encodeURIComponent(id)}/state`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ disabled: false })
|
|
|
});
|
|
|
- if (current) select.value = current;
|
|
|
-}
|
|
|
-
|
|
|
-function openAddOspfInterfaceModal() {
|
|
|
- document.getElementById('ospf-interface-form').reset();
|
|
|
- document.getElementById('modal-ospf-iface-id').value = '';
|
|
|
- document.getElementById('modal-ospf-iface-area').value = '0.0.0.0';
|
|
|
- document.getElementById('modal-ospf-iface-cost').value = '10';
|
|
|
- document.getElementById('modal-ospf-iface-priority').value = '1';
|
|
|
- document.getElementById('modal-ospf-iface-hello').value = '10';
|
|
|
- document.getElementById('modal-ospf-iface-dead').value = '40';
|
|
|
- document.getElementById('ospf-interface-modal-title').innerText = 'Add OSPF Interface';
|
|
|
- populateOspfInterfaceDevices();
|
|
|
- document.getElementById('ospf-interface-modal').classList.remove('hidden');
|
|
|
-}
|
|
|
-
|
|
|
-function openEditOspfInterfaceModal(id) {
|
|
|
- const item = (state.ospfInterfaces || []).find(i => i.id === id);
|
|
|
- if (!item) return;
|
|
|
- populateOspfInterfaceDevices();
|
|
|
- document.getElementById('modal-ospf-iface-id').value = item.id;
|
|
|
- document.getElementById('modal-ospf-iface-net').value = item.network || '';
|
|
|
- document.getElementById('modal-ospf-iface-area').value = item.area || '0.0.0.0';
|
|
|
- document.getElementById('modal-ospf-iface-device').value = item.interface || '';
|
|
|
- document.getElementById('modal-ospf-iface-cost').value = item.cost || 10;
|
|
|
- document.getElementById('modal-ospf-iface-priority').value = item.priority || 1;
|
|
|
- document.getElementById('modal-ospf-iface-hello').value = item.hello_interval || 10;
|
|
|
- document.getElementById('modal-ospf-iface-dead').value = item.dead_interval || 40;
|
|
|
- document.getElementById('modal-ospf-iface-auth-type').value = item.auth_type || 'none';
|
|
|
- document.getElementById('modal-ospf-iface-auth-key').value = item.auth_key || '';
|
|
|
- document.getElementById('modal-ospf-iface-passive').checked = !!item.passive;
|
|
|
- document.getElementById('modal-ospf-iface-disabled').checked = !!item.disabled;
|
|
|
- document.getElementById('modal-ospf-iface-comment').value = item.comment || '';
|
|
|
- document.getElementById('ospf-interface-modal-title').innerText = 'Edit OSPF Interface';
|
|
|
- document.getElementById('ospf-interface-modal').classList.remove('hidden');
|
|
|
-}
|
|
|
-
|
|
|
-function closeOspfInterfaceModal() {
|
|
|
- document.getElementById('ospf-interface-modal').classList.add('hidden');
|
|
|
-}
|
|
|
-
|
|
|
-async function submitOspfInterface(e) {
|
|
|
- e.preventDefault();
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function disableSelectedOspfArea() {
|
|
|
+ const id = state.selectedOspfRow?.areas;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/areas/${encodeURIComponent(id)}/state`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ disabled: true })
|
|
|
+ });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function removeSelectedOspfArea() {
|
|
|
+ const id = state.selectedOspfRow?.areas;
|
|
|
+ if (!id) return;
|
|
|
+ if (!confirm('Delete selected OSPF area?')) return;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/areas/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function promptCommentOspfArea() {
|
|
|
+ const id = state.selectedOspfRow?.areas;
|
|
|
+ if (!id) return;
|
|
|
+ const area = (state.ospfAreas || []).find(a => a.id === id);
|
|
|
+ if (!area) return;
|
|
|
+ const comment = prompt('Enter comment for OSPF area:', area.comment || '');
|
|
|
+ if (comment === null) return;
|
|
|
+ area.comment = comment;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/areas`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(area)
|
|
|
+ });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+// --- 3. OSPF AREA RANGE ---
|
|
|
+function populateAreaRangeDropdowns() {
|
|
|
+ const select = document.getElementById('modal-ospf-range-area');
|
|
|
+ if (select) {
|
|
|
+ const cur = select.value;
|
|
|
+ select.innerHTML = (state.ospfAreas || []).map(a => `<option value="${escapeHtml(a.name)}">${escapeHtml(a.name)}</option>`).join('');
|
|
|
+ if (select.innerHTML === '') select.innerHTML = '<option value="Default">Default</option>';
|
|
|
+ if (cur) select.value = cur;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function openOspfAreaRangeModal(id = null) {
|
|
|
+ const isEdit = !!id;
|
|
|
+ populateAreaRangeDropdowns();
|
|
|
+ const r = (state.ospfAreaRanges || []).find(it => it.id === id) || {
|
|
|
+ id: '',
|
|
|
+ area: 'Default',
|
|
|
+ prefix: '',
|
|
|
+ cost: 1,
|
|
|
+ advertise: true,
|
|
|
+ disabled: false,
|
|
|
+ comment: ''
|
|
|
+ };
|
|
|
+
|
|
|
+ document.getElementById('ospf-area-range-modal-title').innerText = isEdit ? r.prefix : 'New...';
|
|
|
+ document.getElementById('modal-ospf-range-id').value = isEdit ? r.id : '';
|
|
|
+ document.getElementById('modal-ospf-range-enabled').checked = !r.disabled;
|
|
|
+ document.getElementById('modal-ospf-range-comment').value = r.comment || '';
|
|
|
+ document.getElementById('modal-ospf-range-area').value = r.area || 'Default';
|
|
|
+ document.getElementById('modal-ospf-range-prefix').value = r.prefix || '';
|
|
|
+ document.getElementById('modal-ospf-range-cost').value = r.cost || 1;
|
|
|
+ document.getElementById('modal-ospf-range-advertise').checked = r.advertise !== false;
|
|
|
+
|
|
|
+ const btnRem = document.getElementById('btn-ospf-range-modal-remove');
|
|
|
+ if (btnRem) btnRem.style.display = isEdit ? 'block' : 'none';
|
|
|
+
|
|
|
+ document.getElementById('ospf-area-range-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditOspfAreaRangeModal(id) {
|
|
|
+ openOspfAreaRangeModal(id);
|
|
|
+}
|
|
|
+
|
|
|
+function closeOspfAreaRangeModal() {
|
|
|
+ document.getElementById('ospf-area-range-modal').classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitOspfAreaRange(e) {
|
|
|
+ if (e) e.preventDefault();
|
|
|
const payload = {
|
|
|
- id: document.getElementById('modal-ospf-iface-id').value,
|
|
|
- network: document.getElementById('modal-ospf-iface-net').value.trim(),
|
|
|
- area: document.getElementById('modal-ospf-iface-area').value.trim(),
|
|
|
- interface: document.getElementById('modal-ospf-iface-device').value,
|
|
|
- cost: parseInt(document.getElementById('modal-ospf-iface-cost').value, 10),
|
|
|
- priority: parseInt(document.getElementById('modal-ospf-iface-priority').value, 10),
|
|
|
- hello_interval: parseInt(document.getElementById('modal-ospf-iface-hello').value, 10),
|
|
|
- dead_interval: parseInt(document.getElementById('modal-ospf-iface-dead').value, 10),
|
|
|
- auth_type: document.getElementById('modal-ospf-iface-auth-type').value,
|
|
|
- auth_key: document.getElementById('modal-ospf-iface-auth-key').value,
|
|
|
- passive: document.getElementById('modal-ospf-iface-passive').checked,
|
|
|
- disabled: document.getElementById('modal-ospf-iface-disabled').checked,
|
|
|
- comment: document.getElementById('modal-ospf-iface-comment').value.trim()
|
|
|
+ id: document.getElementById('modal-ospf-range-id').value,
|
|
|
+ area: document.getElementById('modal-ospf-range-area').value,
|
|
|
+ prefix: document.getElementById('modal-ospf-range-prefix').value.trim(),
|
|
|
+ cost: parseInt(document.getElementById('modal-ospf-range-cost').value, 10) || 1,
|
|
|
+ advertise: document.getElementById('modal-ospf-range-advertise').checked,
|
|
|
+ disabled: !document.getElementById('modal-ospf-range-enabled').checked,
|
|
|
+ comment: document.getElementById('modal-ospf-range-comment').value.trim()
|
|
|
};
|
|
|
|
|
|
try {
|
|
|
- const res = await fetch(`${API_BASE}/routing/ospf/interfaces`, {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/ospf/area-ranges`, {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
body: JSON.stringify(payload)
|
|
|
});
|
|
|
if (res.ok) {
|
|
|
- closeOspfInterfaceModal();
|
|
|
- showToast('Success', 'OSPF interface template saved', 'success');
|
|
|
+ closeOspfAreaRangeModal();
|
|
|
+ showToast('Saved', 'Area range saved', 'success');
|
|
|
loadDynamicRouting();
|
|
|
} else {
|
|
|
const err = await res.json();
|
|
|
- showToast('Error', err.error || 'Failed to save interface', 'error');
|
|
|
+ showToast('Error', err.error || 'Failed to save area range', 'error');
|
|
|
}
|
|
|
} catch (err) {
|
|
|
showToast('Error', err.message, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-async function deleteOspfInterface(id) {
|
|
|
- if (!confirm('Are you sure you want to delete this OSPF interface?')) return;
|
|
|
+function applyOspfAreaRangeForm() {
|
|
|
+ submitOspfAreaRange(null);
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteCurrentOspfAreaRange() {
|
|
|
+ const id = document.getElementById('modal-ospf-range-id').value;
|
|
|
+ if (!id) return;
|
|
|
+ if (!confirm('Delete this area range?')) return;
|
|
|
try {
|
|
|
- const res = await fetch(`${API_BASE}/routing/ospf/interfaces/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/area-ranges/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ closeOspfAreaRangeModal();
|
|
|
+ showToast('Removed', 'Area range removed', 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function enableSelectedOspfAreaRange() {
|
|
|
+ const id = state.selectedOspfRow?.['area-ranges'];
|
|
|
+ if (!id) return;
|
|
|
+ const r = (state.ospfAreaRanges || []).find(it => it.id === id);
|
|
|
+ if (!r) return;
|
|
|
+ r.disabled = false;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/area-ranges`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(r) });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function disableSelectedOspfAreaRange() {
|
|
|
+ const id = state.selectedOspfRow?.['area-ranges'];
|
|
|
+ if (!id) return;
|
|
|
+ const r = (state.ospfAreaRanges || []).find(it => it.id === id);
|
|
|
+ if (!r) return;
|
|
|
+ r.disabled = true;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/area-ranges`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(r) });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function removeSelectedOspfAreaRange() {
|
|
|
+ const id = state.selectedOspfRow?.['area-ranges'];
|
|
|
+ if (!id) return;
|
|
|
+ if (!confirm('Delete selected area range?')) return;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/area-ranges/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function promptCommentOspfAreaRange() {
|
|
|
+ const id = state.selectedOspfRow?.['area-ranges'];
|
|
|
+ if (!id) return;
|
|
|
+ const r = (state.ospfAreaRanges || []).find(it => it.id === id);
|
|
|
+ if (!r) return;
|
|
|
+ const comment = prompt('Enter comment for area range:', r.comment || '');
|
|
|
+ if (comment === null) return;
|
|
|
+ r.comment = comment;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/area-ranges`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(r) });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+// --- 4. OSPF INTERFACE TEMPLATE ---
|
|
|
+function switchOspfTemplateTab(tab) {
|
|
|
+ const tabGen = document.getElementById('ospf-tmpl-tab-general');
|
|
|
+ const tabAuth = document.getElementById('ospf-tmpl-tab-auth');
|
|
|
+ const btnGen = document.getElementById('ospf-tmpl-tab-btn-general');
|
|
|
+ const btnAuth = document.getElementById('ospf-tmpl-tab-btn-auth');
|
|
|
+
|
|
|
+ if (tab === 'general') {
|
|
|
+ if (tabGen) tabGen.classList.remove('hidden');
|
|
|
+ if (tabAuth) tabAuth.classList.add('hidden');
|
|
|
+ if (btnGen) btnGen.className = 'px-4 py-2 text-xs font-bold border-b-2 border-sky-500 text-white transition';
|
|
|
+ if (btnAuth) btnAuth.className = 'px-4 py-2 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition';
|
|
|
+ } else {
|
|
|
+ if (tabGen) tabGen.classList.add('hidden');
|
|
|
+ if (tabAuth) tabAuth.classList.remove('hidden');
|
|
|
+ if (btnGen) btnGen.className = 'px-4 py-2 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition';
|
|
|
+ if (btnAuth) btnAuth.className = 'px-4 py-2 text-xs font-bold border-b-2 border-sky-500 text-white transition';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function populateTemplateAreaDropdowns() {
|
|
|
+ const select = document.getElementById('modal-ospf-tmpl-area');
|
|
|
+ if (select) {
|
|
|
+ const cur = select.value;
|
|
|
+ select.innerHTML = (state.ospfAreas || []).map(a => `<option value="${escapeHtml(a.name)}">${escapeHtml(a.name)}</option>`).join('');
|
|
|
+ if (select.innerHTML === '') select.innerHTML = '<option value="Default">Default</option>';
|
|
|
+ if (cur) select.value = cur;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function openOspfTemplateModal(id = null) {
|
|
|
+ const isEdit = !!id;
|
|
|
+ populateTemplateAreaDropdowns();
|
|
|
+ switchOspfTemplateTab('general');
|
|
|
+
|
|
|
+ const tmpl = (state.ospfTemplates || []).find(t => t.id === id) || {
|
|
|
+ id: '',
|
|
|
+ interfaces: '',
|
|
|
+ area: 'Default',
|
|
|
+ networks: '',
|
|
|
+ network_type: 'broadcast',
|
|
|
+ cost: 1,
|
|
|
+ priority: 128,
|
|
|
+ passive: false,
|
|
|
+ hello_interval: 10,
|
|
|
+ dead_interval: 40,
|
|
|
+ auth: 'none',
|
|
|
+ auth_key: '',
|
|
|
+ disabled: false,
|
|
|
+ comment: ''
|
|
|
+ };
|
|
|
+
|
|
|
+ document.getElementById('ospf-template-modal-title').innerText = isEdit ? (tmpl.interfaces || tmpl.networks || 'Template') : 'New...';
|
|
|
+ document.getElementById('modal-ospf-tmpl-id').value = isEdit ? tmpl.id : '';
|
|
|
+ document.getElementById('modal-ospf-tmpl-enabled').checked = !tmpl.disabled;
|
|
|
+ document.getElementById('modal-ospf-tmpl-comment').value = tmpl.comment || '';
|
|
|
+ document.getElementById('modal-ospf-tmpl-interfaces').value = tmpl.interfaces || tmpl.interface || '';
|
|
|
+ document.getElementById('modal-ospf-tmpl-area').value = tmpl.area || 'Default';
|
|
|
+ document.getElementById('modal-ospf-tmpl-networks').value = tmpl.networks || tmpl.network || '';
|
|
|
+ document.getElementById('modal-ospf-tmpl-net-type').value = tmpl.network_type || 'broadcast';
|
|
|
+ document.getElementById('modal-ospf-tmpl-cost').value = tmpl.cost || 1;
|
|
|
+ document.getElementById('modal-ospf-tmpl-priority').value = tmpl.priority || 128;
|
|
|
+ document.getElementById('modal-ospf-tmpl-passive').checked = !!tmpl.passive;
|
|
|
+ document.getElementById('modal-ospf-tmpl-hello').value = tmpl.hello_interval || 10;
|
|
|
+ document.getElementById('modal-ospf-tmpl-dead').value = tmpl.dead_interval || 40;
|
|
|
+ document.getElementById('modal-ospf-tmpl-auth').value = tmpl.auth || tmpl.auth_type || 'none';
|
|
|
+ document.getElementById('modal-ospf-tmpl-auth-key').value = tmpl.auth_key || '';
|
|
|
+
|
|
|
+ const btnRem = document.getElementById('btn-ospf-tmpl-modal-remove');
|
|
|
+ if (btnRem) btnRem.style.display = isEdit ? 'block' : 'none';
|
|
|
+
|
|
|
+ document.getElementById('ospf-template-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditOspfTemplateModal(id) {
|
|
|
+ openOspfTemplateModal(id);
|
|
|
+}
|
|
|
+
|
|
|
+function closeOspfTemplateModal() {
|
|
|
+ document.getElementById('ospf-template-modal').classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitOspfTemplate(e) {
|
|
|
+ if (e) e.preventDefault();
|
|
|
+ const payload = {
|
|
|
+ id: document.getElementById('modal-ospf-tmpl-id').value,
|
|
|
+ interfaces: document.getElementById('modal-ospf-tmpl-interfaces').value.trim(),
|
|
|
+ interface: document.getElementById('modal-ospf-tmpl-interfaces').value.trim(),
|
|
|
+ area: document.getElementById('modal-ospf-tmpl-area').value,
|
|
|
+ networks: document.getElementById('modal-ospf-tmpl-networks').value.trim(),
|
|
|
+ network: document.getElementById('modal-ospf-tmpl-networks').value.trim(),
|
|
|
+ network_type: document.getElementById('modal-ospf-tmpl-net-type').value,
|
|
|
+ cost: parseInt(document.getElementById('modal-ospf-tmpl-cost').value, 10) || 1,
|
|
|
+ priority: parseInt(document.getElementById('modal-ospf-tmpl-priority').value, 10) || 128,
|
|
|
+ passive: document.getElementById('modal-ospf-tmpl-passive').checked,
|
|
|
+ hello_interval: parseInt(document.getElementById('modal-ospf-tmpl-hello').value, 10) || 10,
|
|
|
+ dead_interval: parseInt(document.getElementById('modal-ospf-tmpl-dead').value, 10) || 40,
|
|
|
+ auth: document.getElementById('modal-ospf-tmpl-auth').value,
|
|
|
+ auth_type: document.getElementById('modal-ospf-tmpl-auth').value,
|
|
|
+ auth_key: document.getElementById('modal-ospf-tmpl-auth-key').value,
|
|
|
+ disabled: !document.getElementById('modal-ospf-tmpl-enabled').checked,
|
|
|
+ comment: document.getElementById('modal-ospf-tmpl-comment').value.trim()
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/ospf/templates`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
if (res.ok) {
|
|
|
- showToast('Deleted', 'OSPF interface removed', 'success');
|
|
|
+ closeOspfTemplateModal();
|
|
|
+ showToast('Saved', 'Interface template saved', 'success');
|
|
|
loadDynamicRouting();
|
|
|
+ } else {
|
|
|
+ const err = await res.json();
|
|
|
+ showToast('Error', err.error || 'Failed to save interface template', 'error');
|
|
|
}
|
|
|
} catch (err) {
|
|
|
showToast('Error', err.message, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-async function toggleOspfInterfaceState(id, disabled) {
|
|
|
+function applyOspfTemplateForm() {
|
|
|
+ submitOspfTemplate(null);
|
|
|
+}
|
|
|
+
|
|
|
+function copyOspfTemplate() {
|
|
|
+ document.getElementById('modal-ospf-tmpl-id').value = '';
|
|
|
+ document.getElementById('ospf-template-modal-title').innerText = 'New...';
|
|
|
+ showToast('Copied', 'Duplicated as new template', 'info');
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteCurrentOspfTemplate() {
|
|
|
+ const id = document.getElementById('modal-ospf-tmpl-id').value;
|
|
|
+ if (!id) return;
|
|
|
+ if (!confirm('Delete this interface template?')) return;
|
|
|
+ try {
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/templates/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ closeOspfTemplateModal();
|
|
|
+ showToast('Removed', 'Interface template deleted', 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function enableSelectedOspfTemplate() {
|
|
|
+ const id = state.selectedOspfRow?.templates;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/templates/${encodeURIComponent(id)}/state`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ disabled: false })
|
|
|
+ });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function disableSelectedOspfTemplate() {
|
|
|
+ const id = state.selectedOspfRow?.templates;
|
|
|
+ if (!id) return;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/templates/${encodeURIComponent(id)}/state`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ disabled: true })
|
|
|
+ });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function removeSelectedOspfTemplate() {
|
|
|
+ const id = state.selectedOspfRow?.templates;
|
|
|
+ if (!id) return;
|
|
|
+ if (!confirm('Delete selected interface template?')) return;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/templates/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function promptCommentOspfTemplate() {
|
|
|
+ const id = state.selectedOspfRow?.templates;
|
|
|
+ if (!id) return;
|
|
|
+ const tmpl = (state.ospfTemplates || []).find(t => t.id === id);
|
|
|
+ if (!tmpl) return;
|
|
|
+ const comment = prompt('Enter comment for template:', tmpl.comment || '');
|
|
|
+ if (comment === null) return;
|
|
|
+ tmpl.comment = comment;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/templates`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(tmpl)
|
|
|
+ });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+// --- 5. OSPF STATIC NEIGHBOR ---
|
|
|
+function populateStaticNbrAreaDropdowns() {
|
|
|
+ const select = document.getElementById('modal-ospf-snbr-area');
|
|
|
+ if (select) {
|
|
|
+ const cur = select.value;
|
|
|
+ select.innerHTML = (state.ospfAreas || []).map(a => `<option value="${escapeHtml(a.name)}">${escapeHtml(a.name)}</option>`).join('');
|
|
|
+ if (select.innerHTML === '') select.innerHTML = '<option value="Default">Default</option>';
|
|
|
+ if (cur) select.value = cur;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function openOspfStaticNbrModal(id = null) {
|
|
|
+ const isEdit = !!id;
|
|
|
+ populateStaticNbrAreaDropdowns();
|
|
|
+ const nbr = (state.ospfStaticNbrs || []).find(n => n.id === id) || {
|
|
|
+ id: '',
|
|
|
+ address: '',
|
|
|
+ area: 'Default',
|
|
|
+ poll_interval: 120,
|
|
|
+ cost: 1,
|
|
|
+ disabled: false,
|
|
|
+ comment: ''
|
|
|
+ };
|
|
|
+
|
|
|
+ document.getElementById('ospf-static-nbr-modal-title').innerText = isEdit ? nbr.address : 'New...';
|
|
|
+ document.getElementById('modal-ospf-snbr-id').value = isEdit ? nbr.id : '';
|
|
|
+ document.getElementById('modal-ospf-snbr-enabled').checked = !nbr.disabled;
|
|
|
+ document.getElementById('modal-ospf-snbr-comment').value = nbr.comment || '';
|
|
|
+ document.getElementById('modal-ospf-snbr-address').value = nbr.address || '';
|
|
|
+ document.getElementById('modal-ospf-snbr-area').value = nbr.area || 'Default';
|
|
|
+ document.getElementById('modal-ospf-snbr-poll').value = nbr.poll_interval || 120;
|
|
|
+ document.getElementById('modal-ospf-snbr-cost').value = nbr.cost || 1;
|
|
|
+
|
|
|
+ const btnRem = document.getElementById('btn-ospf-snbr-modal-remove');
|
|
|
+ if (btnRem) btnRem.style.display = isEdit ? 'block' : 'none';
|
|
|
+
|
|
|
+ document.getElementById('ospf-static-nbr-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditOspfStaticNbrModal(id) {
|
|
|
+ openOspfStaticNbrModal(id);
|
|
|
+}
|
|
|
+
|
|
|
+function closeOspfStaticNbrModal() {
|
|
|
+ document.getElementById('ospf-static-nbr-modal').classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitOspfStaticNbr(e) {
|
|
|
+ if (e) e.preventDefault();
|
|
|
+ const payload = {
|
|
|
+ id: document.getElementById('modal-ospf-snbr-id').value,
|
|
|
+ address: document.getElementById('modal-ospf-snbr-address').value.trim(),
|
|
|
+ area: document.getElementById('modal-ospf-snbr-area').value,
|
|
|
+ poll_interval: parseInt(document.getElementById('modal-ospf-snbr-poll').value, 10) || 120,
|
|
|
+ cost: parseInt(document.getElementById('modal-ospf-snbr-cost').value, 10) || 1,
|
|
|
+ disabled: !document.getElementById('modal-ospf-snbr-enabled').checked,
|
|
|
+ comment: document.getElementById('modal-ospf-snbr-comment').value.trim()
|
|
|
+ };
|
|
|
+
|
|
|
try {
|
|
|
- const res = await fetch(`${API_BASE}/routing/ospf/interfaces/${encodeURIComponent(id)}/state`, {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/ospf/static-neighbors`, {
|
|
|
method: 'POST',
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
- body: JSON.stringify({ disabled })
|
|
|
+ body: JSON.stringify(payload)
|
|
|
});
|
|
|
if (res.ok) {
|
|
|
- showToast('Updated', `Interface ${disabled ? 'disabled' : 'enabled'}`, 'success');
|
|
|
+ closeOspfStaticNbrModal();
|
|
|
+ showToast('Saved', 'Static neighbor saved', 'success');
|
|
|
loadDynamicRouting();
|
|
|
+ } else {
|
|
|
+ const err = await res.json();
|
|
|
+ showToast('Error', err.error || 'Failed to save static neighbor', 'error');
|
|
|
}
|
|
|
} catch (err) {
|
|
|
showToast('Error', err.message, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+function applyOspfStaticNbrForm() {
|
|
|
+ submitOspfStaticNbr(null);
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteCurrentOspfStaticNbr() {
|
|
|
+ const id = document.getElementById('modal-ospf-snbr-id').value;
|
|
|
+ if (!id) return;
|
|
|
+ if (!confirm('Delete this static neighbor?')) return;
|
|
|
+ try {
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/static-neighbors/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ closeOspfStaticNbrModal();
|
|
|
+ showToast('Removed', 'Static neighbor removed', 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function enableSelectedOspfStaticNbr() {
|
|
|
+ const id = state.selectedOspfRow?.['static-nbrs'];
|
|
|
+ if (!id) return;
|
|
|
+ const n = (state.ospfStaticNbrs || []).find(it => it.id === id);
|
|
|
+ if (!n) return;
|
|
|
+ n.disabled = false;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/static-neighbors`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(n) });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function disableSelectedOspfStaticNbr() {
|
|
|
+ const id = state.selectedOspfRow?.['static-nbrs'];
|
|
|
+ if (!id) return;
|
|
|
+ const n = (state.ospfStaticNbrs || []).find(it => it.id === id);
|
|
|
+ if (!n) return;
|
|
|
+ n.disabled = true;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/static-neighbors`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(n) });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function removeSelectedOspfStaticNbr() {
|
|
|
+ const id = state.selectedOspfRow?.['static-nbrs'];
|
|
|
+ if (!id) return;
|
|
|
+ if (!confirm('Delete selected static neighbor?')) return;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/static-neighbors/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+async function promptCommentOspfStaticNbr() {
|
|
|
+ const id = state.selectedOspfRow?.['static-nbrs'];
|
|
|
+ if (!id) return;
|
|
|
+ const n = (state.ospfStaticNbrs || []).find(it => it.id === id);
|
|
|
+ if (!n) return;
|
|
|
+ const comment = prompt('Enter comment for static neighbor:', n.comment || '');
|
|
|
+ if (comment === null) return;
|
|
|
+ n.comment = comment;
|
|
|
+ await fetch(`${API_BASE}/routing/ospf/static-neighbors`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(n) });
|
|
|
+ loadDynamicRouting();
|
|
|
+}
|
|
|
+
|
|
|
+// Backward-compat aliases
|
|
|
+function openAddOspfInterfaceModal() { openOspfTemplateModal(); }
|
|
|
+function openEditOspfInterfaceModal(id) { openEditOspfTemplateModal(id); }
|
|
|
+function closeOspfInterfaceModal() { closeOspfTemplateModal(); }
|
|
|
+function submitOspfInterface(e) { submitOspfTemplate(e); }
|
|
|
+function deleteOspfInterface(id) {
|
|
|
+ fetch(`${API_BASE}/routing/ospf/templates/${encodeURIComponent(id)}`, { method: 'DELETE' }).then(() => loadDynamicRouting());
|
|
|
+}
|
|
|
+function toggleOspfInterfaceState(id, disabled) {
|
|
|
+ fetch(`${API_BASE}/routing/ospf/templates/${encodeURIComponent(id)}/state`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ disabled })
|
|
|
+ }).then(() => loadDynamicRouting());
|
|
|
+}
|
|
|
+function openAddOspfAreaModal() { openOspfAreaModal(); }
|
|
|
+function deleteOspfArea(id) {
|
|
|
+ fetch(`${API_BASE}/routing/ospf/areas/${encodeURIComponent(id)}`, { method: 'DELETE' }).then(() => loadDynamicRouting());
|
|
|
+}
|
|
|
+
|
|
|
// ============================================================================
|
|
|
// ==================== MIKROTIK BRIDGE SUBSYSTEM ==============================
|
|
|
// ============================================================================
|