|
|
@@ -44,7 +44,13 @@ let state = {
|
|
|
filterChainSelected: 'all',
|
|
|
activeToolsSubTab: 'ping',
|
|
|
dhcpClients: [],
|
|
|
- dhcpSummary: null
|
|
|
+ dhcpSummary: null,
|
|
|
+ dhcpServerTab: 'servers',
|
|
|
+ dhcpServers: [],
|
|
|
+ dhcpNetworks: [],
|
|
|
+ dhcpLeases: [],
|
|
|
+ dhcpOptions: [],
|
|
|
+ dhcpServerSummary: null
|
|
|
};
|
|
|
|
|
|
const API_BASE = '/api';
|
|
|
@@ -112,6 +118,7 @@ function navigate(tabName) {
|
|
|
dashboard: { title: 'Dashboard Overview', sub: 'Live router performance and network telemetry' },
|
|
|
interfaces: { title: 'Network Interfaces', sub: 'Interface Table View' },
|
|
|
'dhcp-client': { title: 'DHCP Client', sub: 'DHCP Client List' },
|
|
|
+ 'dhcp-server': { title: 'DHCP Server (/ip dhcp-server)', sub: 'DHCP Server instances, subnet networks (NTP, TFTP, DNS), dynamic leases & static reservations' },
|
|
|
bridge: { title: 'Bridge Interfaces', sub: 'IEEE 802.1D / 802.1Q VLAN-aware Layer 2 switching, RSTP & hardware forwarding (FDB)' },
|
|
|
vlans: { title: 'VLAN Virtual Networks', sub: 'IEEE 802.1Q sub-interface segmentation and routing' },
|
|
|
firewall: { title: 'Network Firewall', sub: 'Stateful packet inspection, NAT, traffic mangle, address lists & connection tracking' },
|
|
|
@@ -171,6 +178,7 @@ function navigate(tabName) {
|
|
|
if (tabName === 'pptp') loadPPTP();
|
|
|
if (tabName === 'tools') loadToolsView();
|
|
|
if (tabName === 'dhcp-client') loadDHCPClients();
|
|
|
+ if (tabName === 'dhcp-server') loadDHCPServer();
|
|
|
setTimeout(initTableSorting, 50);
|
|
|
}
|
|
|
|
|
|
@@ -250,6 +258,7 @@ async function fetchData(isManual = false) {
|
|
|
if (state.activeTab === 'l2tp') loadL2TP(false);
|
|
|
if (state.activeTab === 'pptp') loadPPTP(false);
|
|
|
if (state.activeTab === 'dhcp-client') loadDHCPClients(false);
|
|
|
+ if (state.activeTab === 'dhcp-server') loadDHCPServer(false);
|
|
|
|
|
|
} catch (err) {
|
|
|
console.error('Failed to fetch router state:', err);
|
|
|
@@ -475,6 +484,10 @@ function updateBadges() {
|
|
|
const bFilters = document.getElementById('badge-filters-count');
|
|
|
if (bFilters) bFilters.innerText = (state.routingFilters || []).length;
|
|
|
}
|
|
|
+ const bDhcpServer = document.getElementById('badge-dhcp-server-count');
|
|
|
+ if (bDhcpServer) {
|
|
|
+ bDhcpServer.innerText = state.dhcpServerSummary ? (state.dhcpServerSummary.active_leases || state.dhcpServerSummary.total_servers || 0) : (state.dhcpServers || []).length;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// Render Dashboard View
|
|
|
@@ -12523,6 +12536,769 @@ async function deleteDHCPClient(id, ifaceName) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// ==================== DHCP SERVER (/ip dhcp-server) ====================
|
|
|
+
|
|
|
+async function loadDHCPServer(populate = true) {
|
|
|
+ try {
|
|
|
+ const [sumRes, srvRes, netRes, leaseRes, optRes] = await Promise.all([
|
|
|
+ fetch(`${API_BASE}/dhcp-server/summary`),
|
|
|
+ fetch(`${API_BASE}/dhcp-server/servers`),
|
|
|
+ fetch(`${API_BASE}/dhcp-server/networks`),
|
|
|
+ fetch(`${API_BASE}/dhcp-server/leases`),
|
|
|
+ fetch(`${API_BASE}/dhcp-server/options`)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if (sumRes.ok) state.dhcpServerSummary = await sumRes.json();
|
|
|
+ if (srvRes.ok) state.dhcpServers = await srvRes.json();
|
|
|
+ if (netRes.ok) state.dhcpNetworks = await netRes.json();
|
|
|
+ if (leaseRes.ok) state.dhcpLeases = await leaseRes.json();
|
|
|
+ if (optRes.ok) state.dhcpOptions = await optRes.json();
|
|
|
+
|
|
|
+ const sum = state.dhcpServerSummary || {};
|
|
|
+ const srvs = state.dhcpServers || [];
|
|
|
+ const nets = state.dhcpNetworks || [];
|
|
|
+ const leases = state.dhcpLeases || [];
|
|
|
+ const opts = state.dhcpOptions || [];
|
|
|
+
|
|
|
+ // KPI Cards
|
|
|
+ const statActive = document.getElementById('dhcp-server-stat-active');
|
|
|
+ if (statActive) {
|
|
|
+ const activeCount = srvs.filter(s => s.running).length;
|
|
|
+ statActive.innerText = `${activeCount} / ${srvs.length}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ const statLeases = document.getElementById('dhcp-server-stat-leases');
|
|
|
+ if (statLeases) {
|
|
|
+ statLeases.innerText = leases.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ const statLeasesSub = document.getElementById('dhcp-server-stat-leases-sub');
|
|
|
+ if (statLeasesSub) {
|
|
|
+ const dyn = leases.filter(l => l.dynamic).length;
|
|
|
+ const st = leases.filter(l => !l.dynamic).length;
|
|
|
+ statLeasesSub.innerText = `${dyn} dynamic, ${st} static`;
|
|
|
+ }
|
|
|
+
|
|
|
+ const statNetworks = document.getElementById('dhcp-server-stat-networks');
|
|
|
+ if (statNetworks) {
|
|
|
+ statNetworks.innerText = nets.length;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Subtab Badges
|
|
|
+ const bSrv = document.getElementById('dhcp-server-tab-badge-servers');
|
|
|
+ if (bSrv) bSrv.innerText = srvs.length;
|
|
|
+ const bNet = document.getElementById('dhcp-server-tab-badge-networks');
|
|
|
+ if (bNet) bNet.innerText = nets.length;
|
|
|
+ const bLease = document.getElementById('dhcp-server-tab-badge-leases');
|
|
|
+ if (bLease) bLease.innerText = leases.length;
|
|
|
+ const bOpt = document.getElementById('dhcp-server-tab-badge-options');
|
|
|
+ if (bOpt) bOpt.innerText = opts.length;
|
|
|
+
|
|
|
+ // Sidebar Badge
|
|
|
+ const sideBadge = document.getElementById('badge-dhcp-server-count');
|
|
|
+ if (sideBadge) sideBadge.innerText = leases.length || srvs.length;
|
|
|
+
|
|
|
+ renderCurrentDHCPServerTab();
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Failed to load DHCP Server:', err);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function switchDHCPServerTab(tab) {
|
|
|
+ state.dhcpServerTab = tab;
|
|
|
+ const tabs = ['servers', 'networks', 'leases', 'options'];
|
|
|
+ tabs.forEach(t => {
|
|
|
+ const panel = document.getElementById(`dhcp-server-subtab-${t}`);
|
|
|
+ const btn = document.getElementById(`dhcp-server-tab-btn-${t}`);
|
|
|
+ if (panel) {
|
|
|
+ if (t === tab) panel.classList.remove('hidden');
|
|
|
+ else panel.classList.add('hidden');
|
|
|
+ }
|
|
|
+ if (btn) {
|
|
|
+ if (t === tab) {
|
|
|
+ btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-emerald-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';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const addBtn = document.getElementById('dhcp-server-primary-add-btn');
|
|
|
+ if (addBtn) {
|
|
|
+ if (tab === 'servers') addBtn.innerHTML = '<span>+ Add Server</span>';
|
|
|
+ else if (tab === 'networks') addBtn.innerHTML = '<span>+ Add Network</span>';
|
|
|
+ else if (tab === 'leases') addBtn.innerHTML = '<span>+ Add Static Lease</span>';
|
|
|
+ else if (tab === 'options') addBtn.innerHTML = '<span>+ Add Option</span>';
|
|
|
+ }
|
|
|
+
|
|
|
+ renderCurrentDHCPServerTab();
|
|
|
+}
|
|
|
+
|
|
|
+function handleDHCPServerPrimaryAdd() {
|
|
|
+ const tab = state.dhcpServerTab || 'servers';
|
|
|
+ if (tab === 'servers') openAddDHCPServerModal();
|
|
|
+ else if (tab === 'networks') openAddDHCPNetworkModal();
|
|
|
+ else if (tab === 'leases') openAddDHCPStaticLeaseModal();
|
|
|
+ else if (tab === 'options') openAddDHCPOptionModal();
|
|
|
+}
|
|
|
+
|
|
|
+function renderCurrentDHCPServerTab() {
|
|
|
+ const tab = state.dhcpServerTab || 'servers';
|
|
|
+ if (tab === 'servers') renderDHCPServers();
|
|
|
+ else if (tab === 'networks') renderDHCPNetworks();
|
|
|
+ else if (tab === 'leases') renderDHCPLeases();
|
|
|
+ else if (tab === 'options') renderDHCPOptions();
|
|
|
+}
|
|
|
+
|
|
|
+function renderDHCPServers() {
|
|
|
+ const tbody = document.getElementById('dhcp-server-tbody');
|
|
|
+ const empty = document.getElementById('dhcp-server-empty');
|
|
|
+ if (!tbody) return;
|
|
|
+ tbody.innerHTML = '';
|
|
|
+
|
|
|
+ const servers = state.dhcpServers || [];
|
|
|
+ if (servers.length === 0) {
|
|
|
+ empty?.classList.remove('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ empty?.classList.add('hidden');
|
|
|
+
|
|
|
+ servers.forEach(s => {
|
|
|
+ const tr = document.createElement('tr');
|
|
|
+ tr.className = `hover:bg-slate-800/40 transition text-xs ${s.disabled ? 'opacity-50' : ''}`;
|
|
|
+ tr.innerHTML = `
|
|
|
+ <td class="py-2.5 px-3 text-center">${renderMikroTikFlag(s.running, s.disabled)}</td>
|
|
|
+ <td class="py-2.5 px-3 font-bold text-white font-mono">${escapeHtml(s.name)}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-cyan-400 font-semibold">${escapeHtml(s.interface)}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-emerald-400">${escapeHtml(s.address_pool)}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-slate-300">${escapeHtml(s.lease_time || '12h')}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-slate-300">${s.authoritative ? '<span class="text-emerald-400 font-bold">yes</span>' : '<span class="text-slate-500">no</span>'}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-cyan-300"><span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 border border-slate-700">${s.active_leases || 0}</span></td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[140px]" title="${escapeHtml(s.comment || '')}">${escapeHtml(s.comment || '-')}</td>
|
|
|
+ <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
|
|
|
+ <button onclick="toggleDHCPServerState('${s.id}', ${!s.disabled})" class="text-xs ${s.disabled ? 'text-emerald-400' : 'text-slate-400'} hover:underline">${s.disabled ? 'Enable' : 'Disable'}</button>
|
|
|
+ <button onclick="openEditDHCPServerModal('${s.id}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
|
|
|
+ <button onclick="deleteDHCPServer('${s.id}', '${escapeHtml(s.name)}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
|
|
|
+ </td>
|
|
|
+ `;
|
|
|
+ tbody.appendChild(tr);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function renderDHCPNetworks() {
|
|
|
+ const tbody = document.getElementById('dhcp-networks-tbody');
|
|
|
+ const empty = document.getElementById('dhcp-networks-empty');
|
|
|
+ if (!tbody) return;
|
|
|
+ tbody.innerHTML = '';
|
|
|
+
|
|
|
+ const networks = state.dhcpNetworks || [];
|
|
|
+ if (networks.length === 0) {
|
|
|
+ empty?.classList.remove('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ empty?.classList.add('hidden');
|
|
|
+
|
|
|
+ networks.forEach(n => {
|
|
|
+ const tr = document.createElement('tr');
|
|
|
+ tr.className = 'hover:bg-slate-800/40 transition text-xs';
|
|
|
+
|
|
|
+ const dnsStr = (n.dns_servers || []).join(', ') || '-';
|
|
|
+ const ntpStr = (n.ntp_servers || []).join(', ') || '-';
|
|
|
+
|
|
|
+ tr.innerHTML = `
|
|
|
+ <td class="py-2.5 px-3 font-bold text-emerald-400 font-mono">${escapeHtml(n.address)}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-cyan-300 font-semibold">${escapeHtml(n.gateway || '-')}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-slate-300">${escapeHtml(n.netmask || '-')}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-slate-300 truncate max-w-[120px]" title="${escapeHtml(dnsStr)}">${escapeHtml(dnsStr)}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-slate-400">${escapeHtml(n.domain || '-')}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-purple-300 truncate max-w-[120px]" title="${escapeHtml(ntpStr)}">${escapeHtml(ntpStr)}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-amber-300">${escapeHtml(n.next_server || '-')}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-slate-300">${escapeHtml(n.boot_file_name || '-')}</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[120px]" title="${escapeHtml(n.comment || '')}">${escapeHtml(n.comment || '-')}</td>
|
|
|
+ <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
|
|
|
+ <button onclick="openEditDHCPNetworkModal('${n.id}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
|
|
|
+ <button onclick="deleteDHCPNetwork('${n.id}', '${escapeHtml(n.address)}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
|
|
|
+ </td>
|
|
|
+ `;
|
|
|
+ tbody.appendChild(tr);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function renderDHCPLeases() {
|
|
|
+ const tbody = document.getElementById('dhcp-leases-tbody');
|
|
|
+ const empty = document.getElementById('dhcp-leases-empty');
|
|
|
+ if (!tbody) return;
|
|
|
+ tbody.innerHTML = '';
|
|
|
+
|
|
|
+ const leases = state.dhcpLeases || [];
|
|
|
+ if (leases.length === 0) {
|
|
|
+ empty?.classList.remove('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ empty?.classList.add('hidden');
|
|
|
+
|
|
|
+ leases.forEach(l => {
|
|
|
+ const tr = document.createElement('tr');
|
|
|
+ tr.className = `hover:bg-slate-800/40 transition text-xs ${l.disabled ? 'opacity-50' : ''}`;
|
|
|
+
|
|
|
+ const flag = l.dynamic ? '<span class="text-sky-400 font-bold" title="Dynamic">D</span>' : '<span class="text-purple-400 font-bold" title="Static Reservation">S</span>';
|
|
|
+ const statusBadge = l.status === 'bound'
|
|
|
+ ? '<span class="px-1.5 py-0.5 rounded text-[10px] bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 font-semibold font-mono">bound</span>'
|
|
|
+ : l.status === 'static'
|
|
|
+ ? '<span class="px-1.5 py-0.5 rounded text-[10px] bg-purple-500/20 text-purple-300 border border-purple-500/30 font-semibold font-mono">static</span>'
|
|
|
+ : '<span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 text-slate-400 border border-slate-700 font-mono">waiting</span>';
|
|
|
+
|
|
|
+ const makeStaticBtn = l.dynamic
|
|
|
+ ? `<button onclick="makeDHCPLeaseStatic('${escapeHtml(l.mac_address)}', '${escapeHtml(l.address)}', '${escapeHtml(l.hostname || '')}', '${escapeHtml(l.comment || '')}')" class="text-xs px-2 py-0.5 rounded bg-purple-500/20 text-purple-300 border border-purple-500/30 hover:bg-purple-500/30 font-medium">Make Static</button>`
|
|
|
+ : `<button onclick="deleteDHCPStaticLease('${l.id}')" class="text-xs text-rose-400 hover:text-rose-300 font-medium">Remove</button>`;
|
|
|
+
|
|
|
+ tr.innerHTML = `
|
|
|
+ <td class="py-2.5 px-3 text-center">${flag}</td>
|
|
|
+ <td class="py-2.5 px-3 font-bold text-white font-mono">${escapeHtml(l.address)}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-cyan-300">${escapeHtml(l.mac_address)}</td>
|
|
|
+ <td class="py-2.5 px-3 font-sans text-slate-200 font-medium">${escapeHtml(l.hostname || '*')}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-slate-400">${escapeHtml(l.server || 'all')}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-slate-300">${escapeHtml(l.expires_in || 'never')}</td>
|
|
|
+ <td class="py-2.5 px-3">${statusBadge}</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[120px]" title="${escapeHtml(l.comment || '')}">${escapeHtml(l.comment || '-')}</td>
|
|
|
+ <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
|
|
|
+ ${makeStaticBtn}
|
|
|
+ </td>
|
|
|
+ `;
|
|
|
+ tbody.appendChild(tr);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function renderDHCPOptions() {
|
|
|
+ const tbody = document.getElementById('dhcp-options-tbody');
|
|
|
+ const empty = document.getElementById('dhcp-options-empty');
|
|
|
+ if (!tbody) return;
|
|
|
+ tbody.innerHTML = '';
|
|
|
+
|
|
|
+ const options = state.dhcpOptions || [];
|
|
|
+ if (options.length === 0) {
|
|
|
+ empty?.classList.remove('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ empty?.classList.add('hidden');
|
|
|
+
|
|
|
+ options.forEach(o => {
|
|
|
+ const tr = document.createElement('tr');
|
|
|
+ tr.className = 'hover:bg-slate-800/40 transition text-xs';
|
|
|
+ tr.innerHTML = `
|
|
|
+ <td class="py-2.5 px-3 font-bold text-white font-mono">${escapeHtml(o.name)}</td>
|
|
|
+ <td class="py-2.5 px-3 font-bold text-cyan-400 font-mono">${o.code}</td>
|
|
|
+ <td class="py-2.5 px-3 font-mono text-emerald-400 break-all max-w-xs">${escapeHtml(o.value)}</td>
|
|
|
+ <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-xs" title="${escapeHtml(o.comment || '')}">${escapeHtml(o.comment || '-')}</td>
|
|
|
+ <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
|
|
|
+ <button onclick="openEditDHCPOptionModal('${o.id}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
|
|
|
+ <button onclick="deleteDHCPOption('${o.id}', '${escapeHtml(o.name)}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
|
|
|
+ </td>
|
|
|
+ `;
|
|
|
+ tbody.appendChild(tr);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== MODALS & ACTIONS: SERVER ====================
|
|
|
+function openAddDHCPServerModal() {
|
|
|
+ document.getElementById('dhcp-server-modal-title').innerText = 'Add DHCP Server';
|
|
|
+ document.getElementById('dhcp-server-modal-id').value = '';
|
|
|
+ document.getElementById('dhcp-server-modal-name').value = `dhcp${(state.dhcpServers || []).length + 1}`;
|
|
|
+ document.getElementById('dhcp-server-modal-pool').value = '192.168.88.10-192.168.88.254';
|
|
|
+ document.getElementById('dhcp-server-modal-leasetime').value = '12h';
|
|
|
+ document.getElementById('dhcp-server-modal-comment').value = '';
|
|
|
+ document.getElementById('dhcp-server-modal-authoritative').checked = true;
|
|
|
+ document.getElementById('dhcp-server-modal-disabled').checked = false;
|
|
|
+
|
|
|
+ populateDHCPIfaceSelect('dhcp-server-modal-iface', '');
|
|
|
+ document.getElementById('dhcp-server-instance-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditDHCPServerModal(id) {
|
|
|
+ const s = (state.dhcpServers || []).find(item => item.id === id);
|
|
|
+ if (!s) return;
|
|
|
+ document.getElementById('dhcp-server-modal-title').innerText = `Edit DHCP Server (${s.name})`;
|
|
|
+ document.getElementById('dhcp-server-modal-id').value = s.id;
|
|
|
+ document.getElementById('dhcp-server-modal-name').value = s.name;
|
|
|
+ document.getElementById('dhcp-server-modal-pool').value = s.address_pool;
|
|
|
+ document.getElementById('dhcp-server-modal-leasetime').value = s.lease_time || '12h';
|
|
|
+ document.getElementById('dhcp-server-modal-comment').value = s.comment || '';
|
|
|
+ document.getElementById('dhcp-server-modal-authoritative').checked = s.authoritative !== false;
|
|
|
+ document.getElementById('dhcp-server-modal-disabled').checked = !!s.disabled;
|
|
|
+
|
|
|
+ populateDHCPIfaceSelect('dhcp-server-modal-iface', s.interface);
|
|
|
+ document.getElementById('dhcp-server-instance-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeDHCPServerModal() {
|
|
|
+ document.getElementById('dhcp-server-instance-modal').classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitSaveDHCPServer(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const id = document.getElementById('dhcp-server-modal-id').value;
|
|
|
+ const name = document.getElementById('dhcp-server-modal-name').value.trim();
|
|
|
+ const iface = document.getElementById('dhcp-server-modal-iface').value;
|
|
|
+ const pool = document.getElementById('dhcp-server-modal-pool').value.trim();
|
|
|
+ const leaseTime = document.getElementById('dhcp-server-modal-leasetime').value.trim() || '12h';
|
|
|
+ const comment = document.getElementById('dhcp-server-modal-comment').value.trim();
|
|
|
+ const authoritative = document.getElementById('dhcp-server-modal-authoritative').checked;
|
|
|
+ const disabled = document.getElementById('dhcp-server-modal-disabled').checked;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dhcp-server/servers`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({
|
|
|
+ id,
|
|
|
+ name,
|
|
|
+ interface: iface,
|
|
|
+ address_pool: pool,
|
|
|
+ lease_time: leaseTime,
|
|
|
+ authoritative,
|
|
|
+ disabled,
|
|
|
+ comment
|
|
|
+ })
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('DHCP Server Saved', `Server ${name} configured successfully`, 'success');
|
|
|
+ closeDHCPServerModal();
|
|
|
+ loadDHCPServer(false);
|
|
|
+ fetchData(false);
|
|
|
+ } else {
|
|
|
+ const err = await res.text();
|
|
|
+ showToast('Error', err, 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function toggleDHCPServerState(id, disabled) {
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dhcp-server/servers/${encodeURIComponent(id)}/state`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ disabled })
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('State Updated', disabled ? 'Server disabled' : 'Server enabled', 'info');
|
|
|
+ loadDHCPServer(false);
|
|
|
+ fetchData(false);
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteDHCPServer(id, name) {
|
|
|
+ if (!confirm(`Are you sure you want to delete DHCP Server "${name}"?`)) return;
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dhcp-server/servers/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Deleted', `DHCP Server ${name} removed`, 'success');
|
|
|
+ loadDHCPServer(false);
|
|
|
+ fetchData(false);
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== MODALS & ACTIONS: NETWORKS ====================
|
|
|
+function openAddDHCPNetworkModal() {
|
|
|
+ document.getElementById('dhcp-network-modal-title').innerText = 'Add DHCP Network';
|
|
|
+ document.getElementById('dhcp-network-modal-id').value = '';
|
|
|
+ document.getElementById('dhcp-network-modal-address').value = '192.168.88.0/24';
|
|
|
+ document.getElementById('dhcp-network-modal-gateway').value = '192.168.88.1';
|
|
|
+ document.getElementById('dhcp-network-modal-dns').value = '192.168.88.1, 1.1.1.1, 8.8.8.8';
|
|
|
+ document.getElementById('dhcp-network-modal-domain').value = 'lan';
|
|
|
+ document.getElementById('dhcp-network-modal-ntp').value = '192.168.88.1, time.cloudflare.com';
|
|
|
+ document.getElementById('dhcp-network-modal-tftp').value = '';
|
|
|
+ document.getElementById('dhcp-network-modal-bootfile').value = '';
|
|
|
+ document.getElementById('dhcp-network-modal-wins').value = '';
|
|
|
+ document.getElementById('dhcp-network-modal-netmask').value = '';
|
|
|
+ document.getElementById('dhcp-network-modal-comment').value = '';
|
|
|
+
|
|
|
+ renderDHCPNetworkOptionsCheckboxes([]);
|
|
|
+ document.getElementById('dhcp-network-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditDHCPNetworkModal(id) {
|
|
|
+ const n = (state.dhcpNetworks || []).find(item => item.id === id);
|
|
|
+ if (!n) return;
|
|
|
+
|
|
|
+ document.getElementById('dhcp-network-modal-title').innerText = `Edit DHCP Network (${n.address})`;
|
|
|
+ document.getElementById('dhcp-network-modal-id').value = n.id;
|
|
|
+ document.getElementById('dhcp-network-modal-address').value = n.address;
|
|
|
+ document.getElementById('dhcp-network-modal-gateway').value = n.gateway || '';
|
|
|
+ document.getElementById('dhcp-network-modal-dns').value = (n.dns_servers || []).join(', ');
|
|
|
+ document.getElementById('dhcp-network-modal-domain').value = n.domain || '';
|
|
|
+ document.getElementById('dhcp-network-modal-ntp').value = (n.ntp_servers || []).join(', ');
|
|
|
+ document.getElementById('dhcp-network-modal-tftp').value = n.next_server || '';
|
|
|
+ document.getElementById('dhcp-network-modal-bootfile').value = n.boot_file_name || '';
|
|
|
+ document.getElementById('dhcp-network-modal-wins').value = (n.wins_servers || []).join(', ');
|
|
|
+ document.getElementById('dhcp-network-modal-netmask').value = n.netmask || '';
|
|
|
+ document.getElementById('dhcp-network-modal-comment').value = n.comment || '';
|
|
|
+
|
|
|
+ renderDHCPNetworkOptionsCheckboxes(n.dhcp_options || []);
|
|
|
+ document.getElementById('dhcp-network-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function renderDHCPNetworkOptionsCheckboxes(selectedIds) {
|
|
|
+ const container = document.getElementById('dhcp-network-modal-options-list');
|
|
|
+ if (!container) return;
|
|
|
+ container.innerHTML = '';
|
|
|
+
|
|
|
+ const opts = state.dhcpOptions || [];
|
|
|
+ if (opts.length === 0) {
|
|
|
+ container.innerHTML = '<span class="text-slate-500 text-[11px] col-span-2">No custom options configured. Add some in the "Options" tab.</span>';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ opts.forEach(o => {
|
|
|
+ const isChecked = selectedIds.includes(o.id);
|
|
|
+ const label = document.createElement('label');
|
|
|
+ label.className = 'flex items-center space-x-2 text-slate-300 hover:text-white cursor-pointer';
|
|
|
+ label.innerHTML = `
|
|
|
+ <input type="checkbox" name="dhcp-net-opt" value="${o.id}" ${isChecked ? 'checked' : ''} class="rounded bg-slate-800 border-slate-700 text-emerald-600">
|
|
|
+ <span class="truncate" title="${escapeHtml(o.name)} (Code ${o.code})">${escapeHtml(o.name)} <span class="text-cyan-400">#${o.code}</span></span>
|
|
|
+ `;
|
|
|
+ container.appendChild(label);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function closeDHCPNetworkModal() {
|
|
|
+ document.getElementById('dhcp-network-modal').classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitSaveDHCPNetwork(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const id = document.getElementById('dhcp-network-modal-id').value;
|
|
|
+ const address = document.getElementById('dhcp-network-modal-address').value.trim();
|
|
|
+ const gateway = document.getElementById('dhcp-network-modal-gateway').value.trim();
|
|
|
+ const dnsStr = document.getElementById('dhcp-network-modal-dns').value.trim();
|
|
|
+ const domain = document.getElementById('dhcp-network-modal-domain').value.trim();
|
|
|
+ const ntpStr = document.getElementById('dhcp-network-modal-ntp').value.trim();
|
|
|
+ const tftp = document.getElementById('dhcp-network-modal-tftp').value.trim();
|
|
|
+ const bootfile = document.getElementById('dhcp-network-modal-bootfile').value.trim();
|
|
|
+ const winsStr = document.getElementById('dhcp-network-modal-wins').value.trim();
|
|
|
+ const netmask = document.getElementById('dhcp-network-modal-netmask').value.trim();
|
|
|
+ const comment = document.getElementById('dhcp-network-modal-comment').value.trim();
|
|
|
+
|
|
|
+ const selectedOpts = [];
|
|
|
+ document.querySelectorAll('input[name="dhcp-net-opt"]:checked').forEach(cb => selectedOpts.push(cb.value));
|
|
|
+
|
|
|
+ const dnsServers = dnsStr ? dnsStr.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
|
+ const ntpServers = ntpStr ? ntpStr.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
|
+ const winsServers = winsStr ? winsStr.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dhcp-server/networks`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({
|
|
|
+ id,
|
|
|
+ address,
|
|
|
+ gateway,
|
|
|
+ netmask,
|
|
|
+ dns_servers: dnsServers,
|
|
|
+ domain,
|
|
|
+ ntp_servers: ntpServers,
|
|
|
+ next_server: tftp,
|
|
|
+ boot_file_name: bootfile,
|
|
|
+ wins_servers: winsServers,
|
|
|
+ dhcp_options: selectedOpts,
|
|
|
+ comment
|
|
|
+ })
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Network Saved', `Subnet ${address} configured successfully`, 'success');
|
|
|
+ closeDHCPNetworkModal();
|
|
|
+ loadDHCPServer(false);
|
|
|
+ fetchData(false);
|
|
|
+ } else {
|
|
|
+ const err = await res.text();
|
|
|
+ showToast('Error', err, 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteDHCPNetwork(id, addr) {
|
|
|
+ if (!confirm(`Are you sure you want to delete DHCP Network "${addr}"?`)) return;
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dhcp-server/networks/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Deleted', `Network ${addr} removed`, 'success');
|
|
|
+ loadDHCPServer(false);
|
|
|
+ fetchData(false);
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== MODALS & ACTIONS: LEASES ====================
|
|
|
+function openAddDHCPStaticLeaseModal() {
|
|
|
+ document.getElementById('dhcp-static-lease-modal-title').innerText = 'Add Static Lease Reservation';
|
|
|
+ document.getElementById('dhcp-static-lease-modal-id').value = '';
|
|
|
+ document.getElementById('dhcp-static-lease-modal-mac').value = '';
|
|
|
+ document.getElementById('dhcp-static-lease-modal-ip').value = '';
|
|
|
+ document.getElementById('dhcp-static-lease-modal-hostname').value = '';
|
|
|
+ document.getElementById('dhcp-static-lease-modal-server').value = 'all';
|
|
|
+ document.getElementById('dhcp-static-lease-modal-comment').value = '';
|
|
|
+ document.getElementById('dhcp-static-lease-modal-disabled').checked = false;
|
|
|
+ document.getElementById('dhcp-static-lease-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeDHCPStaticLeaseModal() {
|
|
|
+ document.getElementById('dhcp-static-lease-modal').classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitSaveDHCPStaticLease(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const id = document.getElementById('dhcp-static-lease-modal-id').value;
|
|
|
+ const mac = document.getElementById('dhcp-static-lease-modal-mac').value.trim();
|
|
|
+ const ip = document.getElementById('dhcp-static-lease-modal-ip').value.trim();
|
|
|
+ const hostname = document.getElementById('dhcp-static-lease-modal-hostname').value.trim();
|
|
|
+ const server = document.getElementById('dhcp-static-lease-modal-server').value.trim() || 'all';
|
|
|
+ const comment = document.getElementById('dhcp-static-lease-modal-comment').value.trim();
|
|
|
+ const disabled = document.getElementById('dhcp-static-lease-modal-disabled').checked;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dhcp-server/leases/static`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({
|
|
|
+ id,
|
|
|
+ mac_address: mac,
|
|
|
+ address: ip,
|
|
|
+ hostname,
|
|
|
+ server,
|
|
|
+ comment,
|
|
|
+ disabled
|
|
|
+ })
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Static Lease Saved', `IP ${ip} reserved for ${mac}`, 'success');
|
|
|
+ closeDHCPStaticLeaseModal();
|
|
|
+ loadDHCPServer(false);
|
|
|
+ fetchData(false);
|
|
|
+ } else {
|
|
|
+ const err = await res.text();
|
|
|
+ showToast('Error', err, 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteDHCPStaticLease(id) {
|
|
|
+ if (!confirm('Are you sure you want to remove this static reservation?')) return;
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dhcp-server/leases/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Deleted', 'Static lease removed', 'success');
|
|
|
+ loadDHCPServer(false);
|
|
|
+ fetchData(false);
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function makeDHCPLeaseStatic(mac, ip, hostname, comment) {
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dhcp-server/leases/make-static`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({
|
|
|
+ mac_address: mac,
|
|
|
+ address: ip,
|
|
|
+ hostname,
|
|
|
+ comment: comment || 'Converted to static'
|
|
|
+ })
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Lease Made Static', `IP ${ip} permanently reserved for ${mac}`, 'success');
|
|
|
+ loadDHCPServer(false);
|
|
|
+ fetchData(false);
|
|
|
+ } else {
|
|
|
+ const err = await res.text();
|
|
|
+ showToast('Error', err, 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== MODALS & ACTIONS: OPTIONS ====================
|
|
|
+function openAddDHCPOptionModal() {
|
|
|
+ document.getElementById('dhcp-option-modal-title').innerText = 'Add DHCP Option';
|
|
|
+ document.getElementById('dhcp-option-modal-id').value = '';
|
|
|
+ document.getElementById('dhcp-option-modal-name').value = '';
|
|
|
+ document.getElementById('dhcp-option-modal-code').value = '';
|
|
|
+ document.getElementById('dhcp-option-modal-value').value = '';
|
|
|
+ document.getElementById('dhcp-option-modal-comment').value = '';
|
|
|
+ document.getElementById('dhcp-option-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function openEditDHCPOptionModal(id) {
|
|
|
+ const o = (state.dhcpOptions || []).find(item => item.id === id);
|
|
|
+ if (!o) return;
|
|
|
+ document.getElementById('dhcp-option-modal-title').innerText = `Edit DHCP Option (${o.name})`;
|
|
|
+ document.getElementById('dhcp-option-modal-id').value = o.id;
|
|
|
+ document.getElementById('dhcp-option-modal-name').value = o.name;
|
|
|
+ document.getElementById('dhcp-option-modal-code').value = o.code;
|
|
|
+ document.getElementById('dhcp-option-modal-value').value = o.value;
|
|
|
+ document.getElementById('dhcp-option-modal-comment').value = o.comment || '';
|
|
|
+ document.getElementById('dhcp-option-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeDHCPOptionModal() {
|
|
|
+ document.getElementById('dhcp-option-modal').classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitSaveDHCPOption(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const id = document.getElementById('dhcp-option-modal-id').value;
|
|
|
+ const name = document.getElementById('dhcp-option-modal-name').value.trim();
|
|
|
+ const code = parseInt(document.getElementById('dhcp-option-modal-code').value);
|
|
|
+ const value = document.getElementById('dhcp-option-modal-value').value.trim();
|
|
|
+ const comment = document.getElementById('dhcp-option-modal-comment').value.trim();
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dhcp-server/options`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ id, name, code, value, comment })
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Option Saved', `DHCP option ${name} (#${code}) configured`, 'success');
|
|
|
+ closeDHCPOptionModal();
|
|
|
+ loadDHCPServer(false);
|
|
|
+ fetchData(false);
|
|
|
+ } else {
|
|
|
+ const err = await res.text();
|
|
|
+ showToast('Error', err, 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteDHCPOption(id, name) {
|
|
|
+ if (!confirm(`Are you sure you want to delete DHCP Option "${name}"?`)) return;
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dhcp-server/options/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Deleted', `Option ${name} removed`, 'success');
|
|
|
+ loadDHCPServer(false);
|
|
|
+ fetchData(false);
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ==================== WIZARD: DHCP SETUP (MikroTik style) ====================
|
|
|
+function openDHCPServerSetupModal() {
|
|
|
+ populateDHCPIfaceSelect('dhcp-wizard-iface', '');
|
|
|
+ onDHCPWizardIfaceChange();
|
|
|
+ document.getElementById('dhcp-setup-wizard-modal').classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeDHCPServerSetupModal() {
|
|
|
+ document.getElementById('dhcp-setup-wizard-modal').classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function onDHCPWizardIfaceChange() {
|
|
|
+ const ifaceName = document.getElementById('dhcp-wizard-iface').value;
|
|
|
+ const iface = (state.interfaces || []).find(i => i.name === ifaceName);
|
|
|
+
|
|
|
+ let ip = '192.168.88.1';
|
|
|
+ let prefix = '24';
|
|
|
+ if (iface && iface.ip_address) {
|
|
|
+ const parts = iface.ip_address.split('/');
|
|
|
+ ip = parts[0];
|
|
|
+ if (parts.length > 1) prefix = parts[1];
|
|
|
+ }
|
|
|
+
|
|
|
+ // Calculate subnet base (e.g. 192.168.88.0/24)
|
|
|
+ const ipParts = ip.split('.');
|
|
|
+ if (ipParts.length === 4) {
|
|
|
+ const baseSpace = `${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.0/${prefix}`;
|
|
|
+ const pool = `${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.10-${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.254`;
|
|
|
+ document.getElementById('dhcp-wizard-space').value = baseSpace;
|
|
|
+ document.getElementById('dhcp-wizard-gateway').value = ip;
|
|
|
+ document.getElementById('dhcp-wizard-pool').value = pool;
|
|
|
+ document.getElementById('dhcp-wizard-dns').value = `${ip}, 1.1.1.1, 8.8.8.8`;
|
|
|
+ document.getElementById('dhcp-wizard-ntp').value = `${ip}`;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function submitDHCPServerSetupWizard(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const iface = document.getElementById('dhcp-wizard-iface').value;
|
|
|
+ const space = document.getElementById('dhcp-wizard-space').value.trim();
|
|
|
+ const gw = document.getElementById('dhcp-wizard-gateway').value.trim();
|
|
|
+ const pool = document.getElementById('dhcp-wizard-pool').value.trim();
|
|
|
+ const dnsStr = document.getElementById('dhcp-wizard-dns').value.trim();
|
|
|
+ const leaseTime = document.getElementById('dhcp-wizard-leasetime').value.trim() || '12h';
|
|
|
+ const ntpStr = document.getElementById('dhcp-wizard-ntp').value.trim();
|
|
|
+ const tftp = document.getElementById('dhcp-wizard-tftp').value.trim();
|
|
|
+
|
|
|
+ const dnsServers = dnsStr ? dnsStr.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
|
+ const ntpServers = ntpStr ? ntpStr.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dhcp-server/setup`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({
|
|
|
+ interface: iface,
|
|
|
+ address_space: space,
|
|
|
+ gateway: gw,
|
|
|
+ address_pool: pool,
|
|
|
+ dns_servers: dnsServers,
|
|
|
+ lease_time: leaseTime,
|
|
|
+ ntp_servers: ntpServers,
|
|
|
+ next_server: tftp
|
|
|
+ })
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('DHCP Setup Complete', `DHCP Server setup finished on ${iface}`, 'success');
|
|
|
+ closeDHCPServerSetupModal();
|
|
|
+ loadDHCPServer(false);
|
|
|
+ fetchData(false);
|
|
|
+ } else {
|
|
|
+ const err = await res.text();
|
|
|
+ showToast('Error', err, 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function populateDHCPIfaceSelect(elemId, selectedVal) {
|
|
|
+ const sel = document.getElementById(elemId);
|
|
|
+ if (!sel) return;
|
|
|
+ sel.innerHTML = '<option value="">Select interface...</option>';
|
|
|
+
|
|
|
+ const ifaces = state.interfaces || [];
|
|
|
+ ifaces.forEach(i => {
|
|
|
+ if (i.name === 'lo') return;
|
|
|
+ const opt = document.createElement('option');
|
|
|
+ opt.value = i.name;
|
|
|
+ const ipStr = i.ip_address ? ` (${i.ip_address})` : '';
|
|
|
+ opt.textContent = `${i.name}${ipStr} [${i.role || i.type || 'interface'}]`;
|
|
|
+ if (i.name === selectedVal) opt.selected = true;
|
|
|
+ sel.appendChild(opt);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
// ==================== UNIVERSAL TABLE ORDERING / SORTING (WINBOX / WEBFIG STYLE) ====================
|
|
|
|
|
|
function initTableSorting() {
|