|
|
@@ -74,6 +74,7 @@ function navigate(tabName) {
|
|
|
vlans: { title: 'VLAN Virtual Networks', sub: 'IEEE 802.1Q sub-interface segmentation and routing' },
|
|
|
firewall: { title: 'MikroTik Firewall', sub: 'Filter rules, NAT, Mangle, Address Lists & Active Connections tracking' },
|
|
|
routes: { title: 'Routing & Policy', sub: 'Routing tables, static routes (DAS/DAC), and policy routing rules' },
|
|
|
+ 'dynamic-routing': { title: 'Dynamic Routing (BGP & OSPF)', sub: 'FRRouting BGP peering sessions, advertised networks, and OSPFv2 link-state routing' },
|
|
|
ppp: { title: 'PPP Servers & Secrets', sub: 'MikroTik RouterOS user accounts, IP pools and VPN server concentrators' },
|
|
|
eoip: { title: 'EoIP Tunnels', sub: 'Ethernet over IP Layer 2 tunneling (MikroTik RouterOS style)' },
|
|
|
pppoe: { title: 'PPPoE Client WAN', sub: 'Point-to-Point Protocol over Ethernet WAN access' },
|
|
|
@@ -101,6 +102,7 @@ function navigate(tabName) {
|
|
|
|
|
|
if (tabName === 'firewall') loadFirewall();
|
|
|
if (tabName === 'routes') loadRoutes();
|
|
|
+ if (tabName === 'dynamic-routing') loadDynamicRouting();
|
|
|
if (tabName === 'ppp') loadPPP();
|
|
|
if (tabName === 'eoip') loadEoIP();
|
|
|
if (tabName === 'persistence') loadDebianConfig();
|
|
|
@@ -117,12 +119,13 @@ async function fetchData(isManual = false) {
|
|
|
if (isManual && spinner) spinner.classList.add('animate-spin');
|
|
|
|
|
|
try {
|
|
|
- const [statusRes, ifacesRes, vlansRes, vpnRes, fwRes] = await Promise.all([
|
|
|
+ const [statusRes, ifacesRes, vlansRes, vpnRes, fwRes, dynRes] = await Promise.all([
|
|
|
fetch(`${API_BASE}/system/status`),
|
|
|
fetch(`${API_BASE}/interfaces`),
|
|
|
fetch(`${API_BASE}/vlans`),
|
|
|
fetch(`${API_BASE}/vpn/status`),
|
|
|
- fetch(`${API_BASE}/firewall/summary`)
|
|
|
+ fetch(`${API_BASE}/firewall/summary`),
|
|
|
+ fetch(`${API_BASE}/routing/dynamic/summary`)
|
|
|
]);
|
|
|
|
|
|
if (statusRes.ok) state.systemStatus = await statusRes.json();
|
|
|
@@ -136,6 +139,7 @@ async function fetchData(isManual = false) {
|
|
|
}
|
|
|
if (vpnRes.ok) state.vpnStatus = await vpnRes.json();
|
|
|
if (fwRes && fwRes.ok) state.fwSummary = await fwRes.json();
|
|
|
+ if (dynRes && dynRes.ok) state.dynSummary = await dynRes.json();
|
|
|
|
|
|
updateSystemMetrics();
|
|
|
updateBadges();
|
|
|
@@ -145,6 +149,7 @@ async function fetchData(isManual = false) {
|
|
|
if (state.activeTab === 'vlans') renderVlansList();
|
|
|
if (state.activeTab === 'firewall') loadFirewall(false);
|
|
|
if (state.activeTab === 'routes') loadRoutes(false);
|
|
|
+ if (state.activeTab === 'dynamic-routing') loadDynamicRouting(false);
|
|
|
if (state.activeTab === 'ppp') loadPPP(false);
|
|
|
if (state.activeTab === 'eoip') loadEoIP(false);
|
|
|
if (state.activeTab === 'pppoe') loadPPPoE(false);
|
|
|
@@ -266,6 +271,11 @@ function updateBadges() {
|
|
|
setDotBadge('badge-l2tp-status', v.active_l2tp > 0 || v.l2tp_server_running);
|
|
|
setDotBadge('badge-pptp-status', v.active_pptp > 0 || v.pptp_server_running);
|
|
|
}
|
|
|
+ if (state.dynSummary) {
|
|
|
+ const d = state.dynSummary;
|
|
|
+ setDotBadge('badge-bgp-dot', d.bgp_running);
|
|
|
+ setDotBadge('badge-ospf-dot', d.ospf_running);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// Render Dashboard View
|
|
|
@@ -4286,3 +4296,783 @@ function downloadWGClientConf() {
|
|
|
document.body.removeChild(a);
|
|
|
showToast('Downloaded', 'wireguard-client.conf saved', 'success');
|
|
|
}
|
|
|
+
|
|
|
+// =========================================================================
|
|
|
+// DYNAMIC ROUTING (FRRouting BGP & OSPFv2)
|
|
|
+// =========================================================================
|
|
|
+
|
|
|
+state.activeDynSubTab = 'bgp-peers';
|
|
|
+state.bgpInstance = null;
|
|
|
+state.bgpPeers = [];
|
|
|
+state.bgpNets = [];
|
|
|
+state.bgpRoutes = [];
|
|
|
+state.ospfInstance = null;
|
|
|
+state.ospfInterfaces = [];
|
|
|
+state.ospfAreas = [];
|
|
|
+state.ospfNeighbors = [];
|
|
|
+state.ospfRoutes = [];
|
|
|
+
|
|
|
+function switchDynSubTab(subTab) {
|
|
|
+ state.activeDynSubTab = subTab;
|
|
|
+ const tabs = ['bgp-peers', 'bgp-nets', 'bgp-routes', 'ospf-ifaces', 'ospf-areas', 'ospf-nbrs', 'ospf-routes'];
|
|
|
+ tabs.forEach(t => {
|
|
|
+ const el = document.getElementById(`dyn-subtab-${t}`);
|
|
|
+ const btn = document.getElementById(`dyn-subtab-btn-${t}`);
|
|
|
+ if (el) el.classList.toggle('hidden', t !== subTab);
|
|
|
+ if (btn) {
|
|
|
+ if (t === subTab) {
|
|
|
+ btn.className = `px-4 py-2.5 text-xs font-bold border-b-2 ${t.startsWith('bgp') ? 'border-purple-500' : 'border-indigo-500'} text-white transition flex items-center gap-2 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';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+async function loadDynamicRouting(showToastMsg = true) {
|
|
|
+ try {
|
|
|
+ const [sumRes, bgpInstRes, bgpPeersRes, bgpNetsRes, bgpRoutesRes, ospfInstRes, ospfIfacesRes, ospfAreasRes, ospfNbrsRes, ospfRoutesRes] = await Promise.all([
|
|
|
+ fetch(`${API_BASE}/routing/dynamic/summary`),
|
|
|
+ fetch(`${API_BASE}/routing/bgp/instance`),
|
|
|
+ fetch(`${API_BASE}/routing/bgp/peers`),
|
|
|
+ fetch(`${API_BASE}/routing/bgp/networks`),
|
|
|
+ fetch(`${API_BASE}/routing/bgp/routes`),
|
|
|
+ fetch(`${API_BASE}/routing/ospf/instance`),
|
|
|
+ fetch(`${API_BASE}/routing/ospf/interfaces`),
|
|
|
+ fetch(`${API_BASE}/routing/ospf/areas`),
|
|
|
+ fetch(`${API_BASE}/routing/ospf/neighbors`),
|
|
|
+ fetch(`${API_BASE}/routing/ospf/routes`)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if (sumRes.ok) state.dynSummary = await sumRes.json();
|
|
|
+ if (bgpInstRes.ok) state.bgpInstance = await bgpInstRes.json();
|
|
|
+ if (bgpPeersRes.ok) state.bgpPeers = await bgpPeersRes.json();
|
|
|
+ if (bgpNetsRes.ok) state.bgpNets = await bgpNetsRes.json();
|
|
|
+ if (bgpRoutesRes.ok) state.bgpRoutes = await bgpRoutesRes.json();
|
|
|
+ if (ospfInstRes.ok) state.ospfInstance = await ospfInstRes.json();
|
|
|
+ if (ospfIfacesRes.ok) state.ospfInterfaces = await ospfIfacesRes.json();
|
|
|
+ if (ospfAreasRes.ok) state.ospfAreas = await ospfAreasRes.json();
|
|
|
+ if (ospfNbrsRes.ok) state.ospfNeighbors = await ospfNbrsRes.json();
|
|
|
+ if (ospfRoutesRes.ok) state.ospfRoutes = await ospfRoutesRes.json();
|
|
|
+
|
|
|
+ renderBgpStatusPill();
|
|
|
+ renderBgpPeers();
|
|
|
+ renderBgpNetworks();
|
|
|
+ renderBgpRoutes();
|
|
|
+ renderOspfStatusPill();
|
|
|
+ renderOspfInterfaces();
|
|
|
+ renderOspfAreas();
|
|
|
+ renderOspfNeighbors();
|
|
|
+ renderOspfRoutes();
|
|
|
+ updateBadges();
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Failed to load dynamic routing:', err);
|
|
|
+ if (showToastMsg) showToast('Error', 'Failed to load dynamic routing telemetry', 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderBgpStatusPill() {
|
|
|
+ const pill = document.getElementById('bgp-live-status-pill');
|
|
|
+ if (!pill) return;
|
|
|
+ const inst = state.bgpInstance;
|
|
|
+ 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 = 'BGP DISABLED';
|
|
|
+ } else if (inst.running) {
|
|
|
+ pill.className = 'text-[10px] px-2 py-0.5 rounded font-mono bg-purple-950 text-purple-300 border border-purple-800/60 font-semibold';
|
|
|
+ pill.innerText = `BGP ACTIVE (AS ${inst.as})`;
|
|
|
+ } 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 = 'BGP STARTING...';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderBgpPeers() {
|
|
|
+ const tbody = document.getElementById('bgp-peers-table-body');
|
|
|
+ const empty = document.getElementById('bgp-peers-empty');
|
|
|
+ const countBadge = document.getElementById('bgp-peers-count-badge');
|
|
|
+ const navBadge = document.getElementById('dyn-badge-bgp-peers');
|
|
|
+
|
|
|
+ const peers = state.bgpPeers || [];
|
|
|
+ if (countBadge) countBadge.innerText = `${peers.length} Peers`;
|
|
|
+ if (navBadge) navBadge.innerText = peers.length;
|
|
|
+
|
|
|
+ if (peers.length === 0) {
|
|
|
+ tbody.innerHTML = '';
|
|
|
+ empty.classList.remove('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ empty.classList.add('hidden');
|
|
|
+
|
|
|
+ tbody.innerHTML = peers.map(p => {
|
|
|
+ let flagA = !p.disabled ? '<span class="text-cyan-400 font-bold" title="Active">A</span>' : '';
|
|
|
+ let flagE = p.state === 'Established' ? '<span class="text-emerald-400 font-bold ml-1" title="Established">E</span>' : '';
|
|
|
+ let flagX = p.disabled ? '<span class="text-rose-400 font-bold ml-1" title="Disabled">X</span>' : '';
|
|
|
+
|
|
|
+ let stateBadge = '';
|
|
|
+ if (p.disabled) {
|
|
|
+ stateBadge = '<span class="px-2 py-0.5 rounded-full text-[10px] font-sans font-medium bg-slate-800 text-slate-400 border border-slate-700">Disabled</span>';
|
|
|
+ } else if (p.state === 'Established') {
|
|
|
+ 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">Established</span>';
|
|
|
+ } else if (p.state === 'Connect' || p.state === 'Active') {
|
|
|
+ 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">${p.state}</span>`;
|
|
|
+ } else {
|
|
|
+ stateBadge = `<span class="px-2 py-0.5 rounded-full text-[10px] font-sans font-medium bg-slate-800 text-slate-400 border border-slate-700">${p.state || 'Idle'}</span>`;
|
|
|
+ }
|
|
|
+
|
|
|
+ return `
|
|
|
+ <tr class="hover:bg-slate-800/40 transition">
|
|
|
+ <td class="py-2.5 px-3 text-center tracking-wider">${flagA}${flagE}${flagX}</td>
|
|
|
+ <td class="py-2.5 px-3 font-semibold text-white">${escapeHtml(p.name)}</td>
|
|
|
+ <td class="py-2.5 px-3 text-purple-300 font-bold">${escapeHtml(p.remote_address)}</td>
|
|
|
+ <td class="py-2.5 px-3 font-bold text-slate-200">AS ${p.remote_as}</td>
|
|
|
+ <td class="py-2.5 px-3">${stateBadge}</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-400">${escapeHtml(p.uptime || '-')}</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-300">
|
|
|
+ <span class="text-emerald-400 font-bold">↓ ${p.prefix_received || 0}</span> /
|
|
|
+ <span class="text-blue-400 font-bold">↑ ${p.prefix_sent || 0}</span>
|
|
|
+ </td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 font-sans truncate max-w-xs">${escapeHtml(p.comment || '')}</td>
|
|
|
+ <td class="py-2.5 px-3 text-right space-x-1.5 whitespace-nowrap font-sans">
|
|
|
+ <button onclick="toggleBgpPeerState('${p.id}', ${!p.disabled})" class="px-2 py-1 rounded text-[11px] ${p.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'}">
|
|
|
+ ${p.disabled ? 'Enable' : 'Disable'}
|
|
|
+ </button>
|
|
|
+ <button onclick="openEditBgpPeerModal('${p.id}')" class="px-2 py-1 rounded text-[11px] bg-slate-800 hover:bg-slate-700 text-purple-300 border border-slate-700">
|
|
|
+ Edit
|
|
|
+ </button>
|
|
|
+ <button onclick="deleteBgpPeer('${p.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>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function renderBgpNetworks() {
|
|
|
+ const tbody = document.getElementById('bgp-nets-table-body');
|
|
|
+ const empty = document.getElementById('bgp-nets-empty');
|
|
|
+ const countBadge = document.getElementById('bgp-nets-count-badge');
|
|
|
+ const navBadge = document.getElementById('dyn-badge-bgp-nets');
|
|
|
+
|
|
|
+ const nets = state.bgpNets || [];
|
|
|
+ if (countBadge) countBadge.innerText = `${nets.length} Networks`;
|
|
|
+ if (navBadge) navBadge.innerText = nets.length;
|
|
|
+
|
|
|
+ if (nets.length === 0) {
|
|
|
+ tbody.innerHTML = '';
|
|
|
+ empty.classList.remove('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ empty.classList.add('hidden');
|
|
|
+
|
|
|
+ tbody.innerHTML = nets.map(n => `
|
|
|
+ <tr class="hover:bg-slate-800/40 transition">
|
|
|
+ <td class="py-2.5 px-3 font-semibold text-purple-300 font-mono">${escapeHtml(n.network)}</td>
|
|
|
+ <td class="py-2.5 px-3">
|
|
|
+ ${n.disabled
|
|
|
+ ? '<span class="px-2 py-0.5 rounded-full text-[10px] bg-slate-800 text-slate-400 border border-slate-700">Disabled</span>'
|
|
|
+ : '<span class="px-2 py-0.5 rounded-full text-[10px] bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">Announcing</span>'}
|
|
|
+ </td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 font-sans truncate max-w-xs">${escapeHtml(n.comment || '')}</td>
|
|
|
+ <td class="py-2.5 px-3 text-right space-x-1 font-sans">
|
|
|
+ <button onclick="deleteBgpNetwork('${n.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>
|
|
|
+ </tr>
|
|
|
+ `).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function renderBgpRoutes() {
|
|
|
+ const tbody = document.getElementById('bgp-routes-table-body');
|
|
|
+ const empty = document.getElementById('bgp-routes-empty');
|
|
|
+ const countBadge = document.getElementById('bgp-routes-count-badge');
|
|
|
+ const navBadge = document.getElementById('dyn-badge-bgp-routes');
|
|
|
+
|
|
|
+ const routes = state.bgpRoutes || [];
|
|
|
+ if (countBadge) countBadge.innerText = `${routes.length} Routes`;
|
|
|
+ if (navBadge) navBadge.innerText = routes.length;
|
|
|
+
|
|
|
+ if (routes.length === 0) {
|
|
|
+ tbody.innerHTML = '';
|
|
|
+ empty.classList.remove('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ 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">${escapeHtml(r.prefix)}</td>
|
|
|
+ <td class="py-2.5 px-3 text-purple-300 font-bold">${escapeHtml(r.next_hop || '-')}</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-300">${r.metric}</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-300">${r.weight}</td>
|
|
|
+ <td class="py-2.5 px-3 text-amber-300">${escapeHtml(r.path || 'i')}</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-400">${escapeHtml(r.origin || 'IGP')}</td>
|
|
|
+ </tr>
|
|
|
+ `).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'})`;
|
|
|
+ } 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...';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderOspfInterfaces() {
|
|
|
+ const tbody = document.getElementById('ospf-ifaces-table-body');
|
|
|
+ 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;
|
|
|
+
|
|
|
+ if (ifaces.length === 0) {
|
|
|
+ tbody.innerHTML = '';
|
|
|
+ empty.classList.remove('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ 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>' : '';
|
|
|
+
|
|
|
+ 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>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function renderOspfAreas() {
|
|
|
+ const tbody = document.getElementById('ospf-areas-table-body');
|
|
|
+ const countBadge = document.getElementById('ospf-areas-count-badge');
|
|
|
+ const navBadge = document.getElementById('dyn-badge-ospf-areas');
|
|
|
+
|
|
|
+ const areas = state.ospfAreas || [];
|
|
|
+ if (countBadge) countBadge.innerText = `${areas.length} Areas`;
|
|
|
+ if (navBadge) navBadge.innerText = areas.length;
|
|
|
+
|
|
|
+ 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>
|
|
|
+ </tr>
|
|
|
+ `).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function renderOspfNeighbors() {
|
|
|
+ const tbody = document.getElementById('ospf-nbrs-table-body');
|
|
|
+ const empty = document.getElementById('ospf-nbrs-empty');
|
|
|
+ const countBadge = document.getElementById('ospf-nbrs-count-badge');
|
|
|
+ const navBadge = document.getElementById('dyn-badge-ospf-nbrs');
|
|
|
+
|
|
|
+ const nbrs = state.ospfNeighbors || [];
|
|
|
+ if (countBadge) countBadge.innerText = `${nbrs.length} Neighbors`;
|
|
|
+ if (navBadge) navBadge.innerText = nbrs.length;
|
|
|
+
|
|
|
+ if (nbrs.length === 0) {
|
|
|
+ tbody.innerHTML = '';
|
|
|
+ empty.classList.remove('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ 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>`;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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>
|
|
|
+ `;
|
|
|
+ }).join('');
|
|
|
+}
|
|
|
+
|
|
|
+function renderOspfRoutes() {
|
|
|
+ const tbody = document.getElementById('ospf-routes-table-body');
|
|
|
+ const empty = document.getElementById('ospf-routes-empty');
|
|
|
+ const countBadge = document.getElementById('ospf-routes-count-badge');
|
|
|
+ const navBadge = document.getElementById('dyn-badge-ospf-routes');
|
|
|
+
|
|
|
+ const routes = state.ospfRoutes || [];
|
|
|
+ if (countBadge) countBadge.innerText = `${routes.length} Routes`;
|
|
|
+ if (navBadge) navBadge.innerText = routes.length;
|
|
|
+
|
|
|
+ if (routes.length === 0) {
|
|
|
+ tbody.innerHTML = '';
|
|
|
+ empty.classList.remove('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ 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>
|
|
|
+ </tr>
|
|
|
+ `).join('');
|
|
|
+}
|
|
|
+
|
|
|
+// 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 };
|
|
|
+ document.getElementById('modal-bgp-enabled').checked = !!inst.enabled;
|
|
|
+ document.getElementById('modal-bgp-as').value = inst.as || 65001;
|
|
|
+ document.getElementById('modal-bgp-router-id').value = inst.router_id || '';
|
|
|
+ document.getElementById('modal-bgp-keepalive').value = inst.keepalive_time || 60;
|
|
|
+ document.getElementById('modal-bgp-holdtime').value = inst.hold_time || 180;
|
|
|
+ document.getElementById('modal-bgp-redist-conn').checked = !!inst.redistribute_connected;
|
|
|
+ document.getElementById('modal-bgp-redist-static').checked = !!inst.redistribute_static;
|
|
|
+ document.getElementById('modal-bgp-redist-ospf').checked = !!inst.redistribute_ospf;
|
|
|
+ document.getElementById('bgp-instance-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeBgpInstanceModal() {
|
|
|
+ document.getElementById('bgp-instance-modal').classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitBgpInstance(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const payload = {
|
|
|
+ enabled: document.getElementById('modal-bgp-enabled').checked,
|
|
|
+ as: parseInt(document.getElementById('modal-bgp-as').value, 10),
|
|
|
+ router_id: document.getElementById('modal-bgp-router-id').value.trim(),
|
|
|
+ keepalive_time: parseInt(document.getElementById('modal-bgp-keepalive').value, 10),
|
|
|
+ hold_time: parseInt(document.getElementById('modal-bgp-holdtime').value, 10),
|
|
|
+ redistribute_connected: document.getElementById('modal-bgp-redist-conn').checked,
|
|
|
+ redistribute_static: document.getElementById('modal-bgp-redist-static').checked,
|
|
|
+ redistribute_ospf: document.getElementById('modal-bgp-redist-ospf').checked
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/bgp/instance`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ closeBgpInstanceModal();
|
|
|
+ showToast('Success', 'BGP instance configuration updated', 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ } else {
|
|
|
+ const err = await res.json();
|
|
|
+ showToast('Error', err.error || 'Failed to update BGP instance', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function openAddBgpPeerModal() {
|
|
|
+ document.getElementById('bgp-peer-form').reset();
|
|
|
+ document.getElementById('modal-bgp-peer-id').value = '';
|
|
|
+ document.getElementById('bgp-peer-modal-title').innerText = 'Add BGP Peer Connection';
|
|
|
+ document.getElementById('modal-bgp-peer-nexthop-self').checked = true;
|
|
|
+ document.getElementById('bgp-peer-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditBgpPeerModal(id) {
|
|
|
+ const p = (state.bgpPeers || []).find(item => item.id === id);
|
|
|
+ if (!p) return;
|
|
|
+ document.getElementById('modal-bgp-peer-id').value = p.id;
|
|
|
+ document.getElementById('modal-bgp-peer-name').value = p.name || '';
|
|
|
+ document.getElementById('modal-bgp-peer-remote-addr').value = p.remote_address || '';
|
|
|
+ document.getElementById('modal-bgp-peer-remote-as').value = p.remote_as || '';
|
|
|
+ document.getElementById('modal-bgp-peer-local-addr').value = p.local_address || '';
|
|
|
+ document.getElementById('modal-bgp-peer-password').value = p.password || '';
|
|
|
+ document.getElementById('modal-bgp-peer-multihop').checked = !!p.multihop;
|
|
|
+ document.getElementById('modal-bgp-peer-nexthop-self').checked = !!p.nexthop_self;
|
|
|
+ document.getElementById('modal-bgp-peer-passive').checked = !!p.passive;
|
|
|
+ document.getElementById('modal-bgp-peer-disabled').checked = !!p.disabled;
|
|
|
+ document.getElementById('modal-bgp-peer-comment').value = p.comment || '';
|
|
|
+ document.getElementById('bgp-peer-modal-title').innerText = 'Edit BGP Peer Connection';
|
|
|
+ document.getElementById('bgp-peer-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeBgpPeerModal() {
|
|
|
+ document.getElementById('bgp-peer-modal').classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitBgpPeer(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const payload = {
|
|
|
+ id: document.getElementById('modal-bgp-peer-id').value,
|
|
|
+ name: document.getElementById('modal-bgp-peer-name').value.trim(),
|
|
|
+ remote_address: document.getElementById('modal-bgp-peer-remote-addr').value.trim(),
|
|
|
+ remote_as: parseInt(document.getElementById('modal-bgp-peer-remote-as').value, 10),
|
|
|
+ local_address: document.getElementById('modal-bgp-peer-local-addr').value.trim(),
|
|
|
+ password: document.getElementById('modal-bgp-peer-password').value,
|
|
|
+ multihop: document.getElementById('modal-bgp-peer-multihop').checked,
|
|
|
+ nexthop_self: document.getElementById('modal-bgp-peer-nexthop-self').checked,
|
|
|
+ passive: document.getElementById('modal-bgp-peer-passive').checked,
|
|
|
+ disabled: document.getElementById('modal-bgp-peer-disabled').checked,
|
|
|
+ comment: document.getElementById('modal-bgp-peer-comment').value.trim()
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/bgp/peers`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ closeBgpPeerModal();
|
|
|
+ showToast('Success', 'BGP Peer saved', 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ } else {
|
|
|
+ const err = await res.json();
|
|
|
+ showToast('Error', err.error || 'Failed to save peer', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteBgpPeer(id) {
|
|
|
+ if (!confirm('Are you sure you want to delete this BGP peer connection?')) return;
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/bgp/peers/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Deleted', 'BGP peer removed', 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function toggleBgpPeerState(id, disabled) {
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/bgp/peers/${encodeURIComponent(id)}/state`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ disabled })
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Updated', `Peer ${disabled ? 'disabled' : 'enabled'}`, 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function openAddBgpNetworkModal() {
|
|
|
+ document.getElementById('bgp-network-form').reset();
|
|
|
+ document.getElementById('modal-bgp-net-id').value = '';
|
|
|
+ document.getElementById('bgp-network-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeBgpNetworkModal() {
|
|
|
+ document.getElementById('bgp-network-modal').classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitBgpNetwork(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const payload = {
|
|
|
+ id: document.getElementById('modal-bgp-net-id').value,
|
|
|
+ network: document.getElementById('modal-bgp-net-cidr').value.trim(),
|
|
|
+ comment: document.getElementById('modal-bgp-net-comment').value.trim(),
|
|
|
+ disabled: document.getElementById('modal-bgp-net-disabled').checked
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/bgp/networks`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ closeBgpNetworkModal();
|
|
|
+ showToast('Success', 'BGP network advertisement saved', 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ } else {
|
|
|
+ const err = await res.json();
|
|
|
+ showToast('Error', err.error || 'Failed to save network', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteBgpNetwork(id) {
|
|
|
+ if (!confirm('Are you sure you want to stop announcing this network?')) return;
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/bgp/networks/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Deleted', 'Network advertisement removed', 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 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;
|
|
|
+ document.getElementById('ospf-instance-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeOspfInstanceModal() {
|
|
|
+ document.getElementById('ospf-instance-modal').classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitOspfInstance(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
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/ospf/instance`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ closeOspfInstanceModal();
|
|
|
+ showToast('Success', 'OSPF instance configuration updated', 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ } else {
|
|
|
+ const err = await res.json();
|
|
|
+ showToast('Error', err.error || 'Failed to update 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';
|
|
|
+ document.getElementById('ospf-area-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeOspfAreaModal() {
|
|
|
+ document.getElementById('ospf-area-modal').classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitOspfArea(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(),
|
|
|
+ area_type: document.getElementById('modal-ospf-area-type').value,
|
|
|
+ comment: document.getElementById('modal-ospf-area-comment').value.trim()
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/ospf/areas`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ closeOspfAreaModal();
|
|
|
+ showToast('Success', 'OSPF area saved', 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ } else {
|
|
|
+ const err = await res.json();
|
|
|
+ showToast('Error', err.error || 'Failed to save area', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteOspfArea(id) {
|
|
|
+ if (!confirm('Are you sure you want to delete 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');
|
|
|
+ loadDynamicRouting();
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+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>`;
|
|
|
+ });
|
|
|
+ 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();
|
|
|
+ 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()
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/ospf/interfaces`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ closeOspfInterfaceModal();
|
|
|
+ showToast('Success', 'OSPF interface template saved', 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ } else {
|
|
|
+ const err = await res.json();
|
|
|
+ showToast('Error', err.error || 'Failed to save interface', '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;
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/ospf/interfaces/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Deleted', 'OSPF interface removed', 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function toggleOspfInterfaceState(id, disabled) {
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/ospf/interfaces/${encodeURIComponent(id)}/state`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ disabled })
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Updated', `Interface ${disabled ? 'disabled' : 'enabled'}`, 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|