|
|
@@ -39,7 +39,9 @@ let state = {
|
|
|
addressLists: [],
|
|
|
connections: [],
|
|
|
policyRules: [],
|
|
|
- routingTables: []
|
|
|
+ routingTables: [],
|
|
|
+ routingFilters: [],
|
|
|
+ filterChainSelected: 'all'
|
|
|
};
|
|
|
|
|
|
const API_BASE = '/api';
|
|
|
@@ -98,6 +100,7 @@ function navigate(tabName) {
|
|
|
routes: { title: 'Routing & Policy', sub: 'Routing tables, static routes (DAS/DAC), and policy routing rules' },
|
|
|
bgp: { title: 'BGP Routing (/routing bgp)', sub: 'FRRouting BGP peering sessions, advertised networks, and BGP routing table (RIB)' },
|
|
|
ospf: { title: 'OSPF Routing (/routing ospf)', sub: 'FRRouting OSPFv2 link-state routing, dynamic neighbor adjacencies, and multi-area topology' },
|
|
|
+ 'routing-filters': { title: 'Routing Filters (/routing filter)', sub: 'MikroTik-style route filter chains, prefix matching, BGP attributes & policy maps' },
|
|
|
'dynamic-routing': { title: 'BGP Routing (/routing bgp)', sub: 'FRRouting dynamic 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)' },
|
|
|
@@ -135,6 +138,7 @@ function navigate(tabName) {
|
|
|
loadDynamicRouting();
|
|
|
switchOspfSubTab(state.activeOspfSubTab || 'ospf-ifaces');
|
|
|
}
|
|
|
+ if (tabName === 'routing-filters') loadDynamicRouting();
|
|
|
if (tabName === 'dynamic-routing') loadDynamicRouting();
|
|
|
if (tabName === 'ppp') loadPPP();
|
|
|
if (tabName === 'eoip') loadEoIP();
|
|
|
@@ -330,6 +334,8 @@ function updateBadges() {
|
|
|
if (bBgp) bBgp.innerText = (state.bgpPeers || []).length;
|
|
|
const bOspf = document.getElementById('badge-ospf-count');
|
|
|
if (bOspf) bOspf.innerText = (state.ospfInterfaces || []).length;
|
|
|
+ const bFilters = document.getElementById('badge-filters-count');
|
|
|
+ if (bFilters) bFilters.innerText = (state.routingFilters || []).length;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -5128,7 +5134,7 @@ function switchDynSubTab(subTab) {
|
|
|
|
|
|
async function loadDynamicRouting(showToastMsg = false) {
|
|
|
try {
|
|
|
- const [sumRes, bgpInstRes, bgpPeersRes, bgpNetsRes, bgpRoutesRes, ospfInstRes, ospfIfacesRes, ospfAreasRes, ospfNbrsRes, ospfRoutesRes] = await Promise.all([
|
|
|
+ const [sumRes, bgpInstRes, bgpPeersRes, bgpNetsRes, bgpRoutesRes, ospfInstRes, ospfIfacesRes, ospfAreasRes, ospfNbrsRes, ospfRoutesRes, filtersRes] = await Promise.all([
|
|
|
fetch(`${API_BASE}/routing/dynamic/summary`),
|
|
|
fetch(`${API_BASE}/routing/bgp/instance`),
|
|
|
fetch(`${API_BASE}/routing/bgp/peers`),
|
|
|
@@ -5138,7 +5144,8 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
fetch(`${API_BASE}/routing/ospf/interfaces`),
|
|
|
fetch(`${API_BASE}/routing/ospf/areas`),
|
|
|
fetch(`${API_BASE}/routing/ospf/neighbors`),
|
|
|
- fetch(`${API_BASE}/routing/ospf/routes`)
|
|
|
+ fetch(`${API_BASE}/routing/ospf/routes`),
|
|
|
+ fetch(`${API_BASE}/routing/filters`)
|
|
|
]);
|
|
|
|
|
|
if (sumRes.ok) state.dynSummary = await sumRes.json();
|
|
|
@@ -5151,6 +5158,7 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
if (ospfAreasRes.ok) state.ospfAreas = await ospfAreasRes.json();
|
|
|
if (ospfNbrsRes.ok) state.ospfNeighbors = await ospfNbrsRes.json();
|
|
|
if (ospfRoutesRes.ok) state.ospfRoutes = await ospfRoutesRes.json();
|
|
|
+ if (filtersRes && filtersRes.ok) state.routingFilters = await filtersRes.json();
|
|
|
|
|
|
try { renderBgpStatusPill(); } catch(e) { console.error('renderBgpStatusPill error:', e); }
|
|
|
try { renderBgpPeers(); } catch(e) { console.error('renderBgpPeers error:', e); }
|
|
|
@@ -5161,6 +5169,7 @@ async function loadDynamicRouting(showToastMsg = false) {
|
|
|
try { renderOspfAreas(); } catch(e) { console.error('renderOspfAreas error:', e); }
|
|
|
try { renderOspfNeighbors(); } catch(e) { console.error('renderOspfNeighbors error:', e); }
|
|
|
try { renderOspfRoutes(); } catch(e) { console.error('renderOspfRoutes 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');
|
|
|
@@ -5232,6 +5241,11 @@ function renderBgpPeers() {
|
|
|
<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 font-mono text-[11px] whitespace-nowrap">
|
|
|
+ ${p.in_filter ? `<span class="px-1.5 py-0.5 rounded bg-blue-950 text-blue-300 border border-blue-800/50" title="In Filter">IN: ${escapeHtml(p.in_filter)}</span> ` : ''}
|
|
|
+ ${p.out_filter ? `<span class="px-1.5 py-0.5 rounded bg-amber-950 text-amber-300 border border-amber-800/50" title="Out Filter">OUT: ${escapeHtml(p.out_filter)}</span>` : ''}
|
|
|
+ ${!p.in_filter && !p.out_filter ? '<span class="text-slate-600">-</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'}">
|
|
|
@@ -5529,6 +5543,10 @@ function openAddBgpPeerModal() {
|
|
|
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;
|
|
|
+ const inFilterEl = document.getElementById('modal-bgp-peer-in-filter');
|
|
|
+ if (inFilterEl) inFilterEl.value = '';
|
|
|
+ const outFilterEl = document.getElementById('modal-bgp-peer-out-filter');
|
|
|
+ if (outFilterEl) outFilterEl.value = '';
|
|
|
document.getElementById('bgp-peer-modal').classList.remove('hidden');
|
|
|
}
|
|
|
|
|
|
@@ -5546,6 +5564,10 @@ function openEditBgpPeerModal(id) {
|
|
|
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 || '';
|
|
|
+ const inFilterEl = document.getElementById('modal-bgp-peer-in-filter');
|
|
|
+ if (inFilterEl) inFilterEl.value = p.in_filter || '';
|
|
|
+ const outFilterEl = document.getElementById('modal-bgp-peer-out-filter');
|
|
|
+ if (outFilterEl) outFilterEl.value = p.out_filter || '';
|
|
|
document.getElementById('bgp-peer-modal-title').innerText = 'Edit BGP Peer Connection';
|
|
|
document.getElementById('bgp-peer-modal').classList.remove('hidden');
|
|
|
}
|
|
|
@@ -5567,6 +5589,8 @@ async function submitBgpPeer(e) {
|
|
|
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,
|
|
|
+ in_filter: (document.getElementById('modal-bgp-peer-in-filter')?.value || '').trim(),
|
|
|
+ out_filter: (document.getElementById('modal-bgp-peer-out-filter')?.value || '').trim(),
|
|
|
comment: document.getElementById('modal-bgp-peer-comment').value.trim()
|
|
|
};
|
|
|
|
|
|
@@ -6729,3 +6753,283 @@ function populateBridgePortCandidateSelect(selectId, currentVal = '') {
|
|
|
if (grpEoip.children.length > 0) sel.appendChild(grpEoip);
|
|
|
}
|
|
|
|
|
|
+// -------------------------------------------------------------
|
|
|
+// Routing Filters Management (/routing filter)
|
|
|
+// -------------------------------------------------------------
|
|
|
+
|
|
|
+function renderRoutingFilters() {
|
|
|
+ const tbody = document.getElementById('routing-filters-table-body');
|
|
|
+ if (!tbody) return;
|
|
|
+ const empty = document.getElementById('routing-filters-empty');
|
|
|
+ const countBadge = document.getElementById('routing-filters-count-badge');
|
|
|
+ const navBadge = document.getElementById('badge-filters-count');
|
|
|
+ const chainSelect = document.getElementById('routing-filter-chain-select');
|
|
|
+
|
|
|
+ const allRules = state.routingFilters || [];
|
|
|
+ if (navBadge) navBadge.innerText = allRules.length;
|
|
|
+
|
|
|
+ // Populate chain filter select options dynamically
|
|
|
+ if (chainSelect) {
|
|
|
+ const currentVal = state.filterChainSelected || 'all';
|
|
|
+ const uniqueChains = Array.from(new Set(allRules.map(r => r.chain).filter(Boolean))).sort();
|
|
|
+ chainSelect.innerHTML = `<option value="all">All Chains (${allRules.length})</option>` +
|
|
|
+ uniqueChains.map(ch => {
|
|
|
+ const count = allRules.filter(r => r.chain === ch).length;
|
|
|
+ return `<option value="${escapeHtml(ch)}"${currentVal === ch ? ' selected' : ''}>${escapeHtml(ch)} (${count})</option>`;
|
|
|
+ }).join('');
|
|
|
+ chainSelect.value = currentVal;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Filter rules by selected chain
|
|
|
+ const filteredRules = (state.filterChainSelected && state.filterChainSelected !== 'all')
|
|
|
+ ? allRules.filter(r => r.chain === state.filterChainSelected)
|
|
|
+ : allRules.slice();
|
|
|
+
|
|
|
+ // Sort by chain then rule_order
|
|
|
+ filteredRules.sort((a, b) => {
|
|
|
+ if (a.chain !== b.chain) return (a.chain || '').localeCompare(b.chain || '');
|
|
|
+ return (a.rule_order || 0) - (b.rule_order || 0);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (countBadge) countBadge.innerText = `${filteredRules.length} Rules`;
|
|
|
+
|
|
|
+ if (filteredRules.length === 0) {
|
|
|
+ tbody.innerHTML = '';
|
|
|
+ if (empty) empty.classList.remove('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (empty) empty.classList.add('hidden');
|
|
|
+
|
|
|
+ tbody.innerHTML = filteredRules.map(r => {
|
|
|
+ const flagA = !r.disabled ? '<span class="text-cyan-400 font-bold" title="Active">A</span>' : '';
|
|
|
+ const flagX = r.disabled ? '<span class="text-rose-400 font-bold ml-1" title="Disabled">X</span>' : '';
|
|
|
+
|
|
|
+ let actionBadge = '';
|
|
|
+ if (r.action === 'accept') {
|
|
|
+ actionBadge = '<span class="px-2 py-0.5 rounded text-[10px] font-sans font-semibold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">accept</span>';
|
|
|
+ } else if (r.action === 'discard') {
|
|
|
+ actionBadge = '<span class="px-2 py-0.5 rounded text-[10px] font-sans font-semibold bg-rose-500/10 text-rose-400 border border-rose-500/20">discard</span>';
|
|
|
+ } else if (r.action === 'reject') {
|
|
|
+ actionBadge = '<span class="px-2 py-0.5 rounded text-[10px] font-sans font-semibold bg-red-950 text-red-300 border border-red-800/40">reject</span>';
|
|
|
+ } else {
|
|
|
+ actionBadge = `<span class="px-2 py-0.5 rounded text-[10px] font-sans font-semibold bg-blue-500/10 text-blue-400 border border-blue-500/20">${escapeHtml(r.action || 'accept')}</span>`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Prefix match
|
|
|
+ let matchPfx = '';
|
|
|
+ if (r.prefix) {
|
|
|
+ matchPfx = `<span class="font-bold text-white">${escapeHtml(r.prefix)}</span>`;
|
|
|
+ if (r.prefix_len) {
|
|
|
+ matchPfx += ` <span class="text-slate-400 text-[11px] font-normal">[${escapeHtml(r.prefix_len)}]</span>`;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ matchPfx = '<span class="text-slate-500 italic">any prefix</span>';
|
|
|
+ }
|
|
|
+
|
|
|
+ // Other matches
|
|
|
+ const matchExtra = [];
|
|
|
+ if (r.match_protocol && r.match_protocol !== 'any') {
|
|
|
+ matchExtra.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 text-slate-300 border border-slate-700">proto:${escapeHtml(r.match_protocol)}</span>`);
|
|
|
+ }
|
|
|
+ if (r.match_as_path) {
|
|
|
+ matchExtra.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-purple-950 text-purple-300 border border-purple-800/40 font-mono">as-path:${escapeHtml(r.match_as_path)}</span>`);
|
|
|
+ }
|
|
|
+ if (r.match_community) {
|
|
|
+ matchExtra.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-indigo-950 text-indigo-300 border border-indigo-800/40 font-mono">comm:${escapeHtml(r.match_community)}</span>`);
|
|
|
+ }
|
|
|
+ const matchExtraHtml = matchExtra.length > 0 ? matchExtra.join(' ') : '<span class="text-slate-600">-</span>';
|
|
|
+
|
|
|
+ // Set modifications
|
|
|
+ const setBadges = [];
|
|
|
+ if (r.set_local_pref > 0) {
|
|
|
+ setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-cyan-950 text-cyan-300 border border-cyan-800/40">loc-pref: ${r.set_local_pref}</span>`);
|
|
|
+ }
|
|
|
+ if (r.set_metric > 0) {
|
|
|
+ setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-amber-950 text-amber-300 border border-amber-800/40">med: ${r.set_metric}</span>`);
|
|
|
+ }
|
|
|
+ if (r.set_as_prepend > 0) {
|
|
|
+ setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-purple-950 text-purple-300 border border-purple-800/40">prepend: +${r.set_as_prepend}</span>`);
|
|
|
+ }
|
|
|
+ if (r.set_community) {
|
|
|
+ setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-indigo-950 text-indigo-300 border border-indigo-800/40">comm: ${escapeHtml(r.set_community)}</span>`);
|
|
|
+ }
|
|
|
+ if (r.set_next_hop) {
|
|
|
+ setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-blue-950 text-blue-300 border border-blue-800/40">nh: ${escapeHtml(r.set_next_hop)}</span>`);
|
|
|
+ }
|
|
|
+ const setHtml = setBadges.length > 0 ? setBadges.join(' ') : '<span class="text-slate-600">-</span>';
|
|
|
+
|
|
|
+ return `
|
|
|
+ <tr class="hover:bg-slate-800/40 transition">
|
|
|
+ <td class="py-2.5 px-3 text-center tracking-wider">${flagA}${flagX}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-slate-400 font-bold">${r.rule_order || 10}</td>
|
|
|
+ <td class="py-2.5 px-3">
|
|
|
+ <span class="px-2 py-0.5 rounded text-[11px] font-mono bg-slate-800 text-cyan-300 border border-slate-700 font-semibold">${escapeHtml(r.chain)}</span>
|
|
|
+ </td>
|
|
|
+ <td class="py-2.5 px-3">${actionBadge}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono">${matchPfx}</td>
|
|
|
+ <td class="py-2.5 px-3 space-x-1">${matchExtraHtml}</td>
|
|
|
+ <td class="py-2.5 px-3 space-x-1 font-mono">${setHtml}</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 font-sans truncate max-w-xs">${escapeHtml(r.comment || '')}</td>
|
|
|
+ <td class="py-2.5 px-3 text-right space-x-1.5 whitespace-nowrap font-sans">
|
|
|
+ <button onclick="toggleRoutingFilterState('${r.id}', ${!r.disabled})" class="px-2 py-1 rounded text-[11px] ${r.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'}">
|
|
|
+ ${r.disabled ? 'Enable' : 'Disable'}
|
|
|
+ </button>
|
|
|
+ <button onclick="openEditRoutingFilterModal('${r.id}')" class="px-2 py-1 rounded text-[11px] bg-slate-800 hover:bg-slate-700 text-emerald-300 border border-slate-700">
|
|
|
+ Edit
|
|
|
+ </button>
|
|
|
+ <button onclick="deleteRoutingFilter('${r.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 filterRoutingFiltersByChain(chain) {
|
|
|
+ state.filterChainSelected = chain;
|
|
|
+ renderRoutingFilters();
|
|
|
+}
|
|
|
+
|
|
|
+function openAddRoutingFilterModal(defaultChain = '') {
|
|
|
+ const form = document.getElementById('routing-filter-form');
|
|
|
+ if (form) form.reset();
|
|
|
+ const idEl = document.getElementById('modal-rf-id');
|
|
|
+ if (idEl) idEl.value = '';
|
|
|
+ const titleEl = document.getElementById('modal-rf-title');
|
|
|
+ if (titleEl) titleEl.innerText = 'Add Routing Filter Rule';
|
|
|
+ const chainEl = document.getElementById('modal-rf-chain');
|
|
|
+ if (chainEl) chainEl.value = defaultChain || (state.filterChainSelected && state.filterChainSelected !== 'all' ? state.filterChainSelected : 'bgp-in');
|
|
|
+ const orderEl = document.getElementById('modal-rf-order');
|
|
|
+ if (orderEl) orderEl.value = ((state.routingFilters || []).length + 1) * 10;
|
|
|
+ const actEl = document.getElementById('modal-rf-action');
|
|
|
+ if (actEl) actEl.value = 'accept';
|
|
|
+ const protoEl = document.getElementById('modal-rf-protocol');
|
|
|
+ if (protoEl) protoEl.value = 'any';
|
|
|
+ const disEl = document.getElementById('modal-rf-disabled');
|
|
|
+ if (disEl) disEl.checked = false;
|
|
|
+ const modal = document.getElementById('modal-routing-filter');
|
|
|
+ if (modal) modal.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditRoutingFilterModal(id) {
|
|
|
+ const r = (state.routingFilters || []).find(item => item.id === id);
|
|
|
+ if (!r) return;
|
|
|
+ const idEl = document.getElementById('modal-rf-id');
|
|
|
+ if (idEl) idEl.value = r.id;
|
|
|
+ const titleEl = document.getElementById('modal-rf-title');
|
|
|
+ if (titleEl) titleEl.innerText = 'Edit Routing Filter Rule';
|
|
|
+ const chainEl = document.getElementById('modal-rf-chain');
|
|
|
+ if (chainEl) chainEl.value = r.chain || '';
|
|
|
+ const orderEl = document.getElementById('modal-rf-order');
|
|
|
+ if (orderEl) orderEl.value = r.rule_order || 10;
|
|
|
+ const actEl = document.getElementById('modal-rf-action');
|
|
|
+ if (actEl) actEl.value = r.action || 'accept';
|
|
|
+ const pfxEl = document.getElementById('modal-rf-prefix');
|
|
|
+ if (pfxEl) pfxEl.value = r.prefix || '';
|
|
|
+ const pfxLenEl = document.getElementById('modal-rf-prefix-len');
|
|
|
+ if (pfxLenEl) pfxLenEl.value = r.prefix_len || '';
|
|
|
+ const protoEl = document.getElementById('modal-rf-protocol');
|
|
|
+ if (protoEl) protoEl.value = r.match_protocol || 'any';
|
|
|
+ const aspEl = document.getElementById('modal-rf-as-path');
|
|
|
+ if (aspEl) aspEl.value = r.match_as_path || '';
|
|
|
+ const commEl = document.getElementById('modal-rf-community');
|
|
|
+ if (commEl) commEl.value = r.match_community || '';
|
|
|
+ const lpEl = document.getElementById('modal-rf-set-local-pref');
|
|
|
+ if (lpEl) lpEl.value = r.set_local_pref || '';
|
|
|
+ const metEl = document.getElementById('modal-rf-set-metric');
|
|
|
+ if (metEl) metEl.value = r.set_metric || '';
|
|
|
+ const prepEl = document.getElementById('modal-rf-set-as-prepend');
|
|
|
+ if (prepEl) prepEl.value = r.set_as_prepend || '';
|
|
|
+ const setCommEl = document.getElementById('modal-rf-set-community');
|
|
|
+ if (setCommEl) setCommEl.value = r.set_community || '';
|
|
|
+ const nhEl = document.getElementById('modal-rf-set-next-hop');
|
|
|
+ if (nhEl) nhEl.value = r.set_next_hop || '';
|
|
|
+ const comEl = document.getElementById('modal-rf-comment');
|
|
|
+ if (comEl) comEl.value = r.comment || '';
|
|
|
+ const disEl = document.getElementById('modal-rf-disabled');
|
|
|
+ if (disEl) disEl.checked = !!r.disabled;
|
|
|
+ const modal = document.getElementById('modal-routing-filter');
|
|
|
+ if (modal) modal.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeRoutingFilterModal() {
|
|
|
+ const modal = document.getElementById('modal-routing-filter');
|
|
|
+ if (modal) modal.classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitRoutingFilter(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const payload = {
|
|
|
+ id: document.getElementById('modal-rf-id').value,
|
|
|
+ chain: document.getElementById('modal-rf-chain').value.trim(),
|
|
|
+ rule_order: parseInt(document.getElementById('modal-rf-order').value, 10) || 10,
|
|
|
+ action: document.getElementById('modal-rf-action').value,
|
|
|
+ prefix: document.getElementById('modal-rf-prefix').value.trim(),
|
|
|
+ prefix_len: document.getElementById('modal-rf-prefix-len').value.trim(),
|
|
|
+ match_protocol: document.getElementById('modal-rf-protocol').value,
|
|
|
+ match_as_path: document.getElementById('modal-rf-as-path').value.trim(),
|
|
|
+ match_community: document.getElementById('modal-rf-community').value.trim(),
|
|
|
+ set_local_pref: parseInt(document.getElementById('modal-rf-set-local-pref').value, 10) || 0,
|
|
|
+ set_metric: parseInt(document.getElementById('modal-rf-set-metric').value, 10) || 0,
|
|
|
+ set_as_prepend: parseInt(document.getElementById('modal-rf-set-as-prepend').value, 10) || 0,
|
|
|
+ set_community: document.getElementById('modal-rf-set-community').value.trim(),
|
|
|
+ set_next_hop: document.getElementById('modal-rf-set-next-hop').value.trim(),
|
|
|
+ comment: document.getElementById('modal-rf-comment').value.trim(),
|
|
|
+ disabled: document.getElementById('modal-rf-disabled').checked
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/filters`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify(payload)
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ closeRoutingFilterModal();
|
|
|
+ showToast('Success', 'Routing filter rule saved', 'success');
|
|
|
+ loadDynamicRouting();
|
|
|
+ } else {
|
|
|
+ const err = await res.json();
|
|
|
+ showToast('Error', err.error || 'Failed to save filter rule', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteRoutingFilter(id) {
|
|
|
+ if (!confirm('Are you sure you want to delete this routing filter rule?')) return;
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/filters/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Deleted', 'Routing filter rule removed', 'info');
|
|
|
+ loadDynamicRouting();
|
|
|
+ } else {
|
|
|
+ const err = await res.json();
|
|
|
+ showToast('Error', err.error || 'Failed to delete rule', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function toggleRoutingFilterState(id, disabled) {
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/routing/filters/${encodeURIComponent(id)}/state`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ disabled })
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Status Updated', `Rule ${disabled ? 'disabled' : 'enabled'}`, 'info');
|
|
|
+ loadDynamicRouting();
|
|
|
+ } else {
|
|
|
+ const err = await res.json();
|
|
|
+ showToast('Error', err.error || 'Failed to toggle rule state', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|