|
|
@@ -138,7 +138,8 @@ function navigate(tabName) {
|
|
|
vrrp: { title: 'VRRP High Availability', sub: 'Virtual Router Redundancy Protocol, active-backup gateway failover & Virtual IP (VIP) routing' },
|
|
|
bwtest: { title: 'Bandwidth Test Benchmark', sub: 'High-speed line-rate throughput benchmark, token-bucket rate limiting, loss & jitter telemetry' },
|
|
|
tools: { title: 'Diagnostic & Troubleshooting Tools', sub: 'Network latency analysis, ICMP/MTR telemetry, path tracing, DNS resolution and TCP probing' },
|
|
|
- persistence: { title: 'Debian Configuration', sub: 'Native /etc/network/interfaces.d/ persistence manager' }
|
|
|
+ persistence: { title: 'Debian Configuration', sub: 'Native /etc/network/interfaces.d/ persistence manager' },
|
|
|
+ settings: { title: 'Router Settings & DNS', sub: 'System DNS forwarder, upstream resolvers, static host mappings, and appliance identity' }
|
|
|
};
|
|
|
|
|
|
const header = titles[tabName] || titles.dashboard;
|
|
|
@@ -183,6 +184,7 @@ function navigate(tabName) {
|
|
|
if (tabName === 'dhcp-server') loadDHCPServer();
|
|
|
if (tabName === 'vrrp') loadVRRP();
|
|
|
if (tabName === 'bwtest') loadBwTestView();
|
|
|
+ if (tabName === 'settings') loadSettingsData();
|
|
|
setTimeout(initTableSorting, 50);
|
|
|
}
|
|
|
|
|
|
@@ -270,6 +272,7 @@ async function fetchData(isManual = false) {
|
|
|
if (state.activeTab === 'dhcp-client') loadDHCPClients(false);
|
|
|
if (state.activeTab === 'dhcp-server') loadDHCPServer(false);
|
|
|
if (state.activeTab === 'vrrp') loadVRRP(false);
|
|
|
+ if (state.activeTab === 'settings') loadSettingsData(false);
|
|
|
fetchBwTestServerStatus();
|
|
|
|
|
|
} catch (err) {
|
|
|
@@ -14764,5 +14767,367 @@ function renderBwTestHistory(list) {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+// ======================== SETTINGS & ROUTER DNS ========================
|
|
|
+state.settingsSubTab = 'dns';
|
|
|
+state.dnsSettings = null;
|
|
|
+state.dnsStaticRecords = [];
|
|
|
+state.dnsStatus = null;
|
|
|
+
|
|
|
+function switchSettingsSubTab(sub) {
|
|
|
+ state.settingsSubTab = sub;
|
|
|
+ const btnDns = document.getElementById('btn-settings-subtab-dns');
|
|
|
+ const btnSys = document.getElementById('btn-settings-subtab-system');
|
|
|
+ const tabDns = document.getElementById('settings-subtab-dns');
|
|
|
+ const tabSys = document.getElementById('settings-subtab-system');
|
|
|
+
|
|
|
+ if (sub === 'dns') {
|
|
|
+ if (btnDns) {
|
|
|
+ btnDns.className = 'px-3.5 py-1.5 rounded-lg text-xs font-semibold bg-cyan-600 text-white transition flex items-center gap-1.5';
|
|
|
+ }
|
|
|
+ if (btnSys) {
|
|
|
+ btnSys.className = 'px-3.5 py-1.5 rounded-lg text-xs font-semibold bg-slate-800 text-slate-300 hover:text-white transition flex items-center gap-1.5';
|
|
|
+ }
|
|
|
+ tabDns?.classList.remove('hidden');
|
|
|
+ tabSys?.classList.add('hidden');
|
|
|
+ loadDNSSettings();
|
|
|
+ } else {
|
|
|
+ if (btnSys) {
|
|
|
+ btnSys.className = 'px-3.5 py-1.5 rounded-lg text-xs font-semibold bg-cyan-600 text-white transition flex items-center gap-1.5';
|
|
|
+ }
|
|
|
+ if (btnDns) {
|
|
|
+ btnDns.className = 'px-3.5 py-1.5 rounded-lg text-xs font-semibold bg-slate-800 text-slate-300 hover:text-white transition flex items-center gap-1.5';
|
|
|
+ }
|
|
|
+ tabSys?.classList.remove('hidden');
|
|
|
+ tabDns?.classList.add('hidden');
|
|
|
+ loadSystemSettings();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function loadSettingsData(showLoading = false) {
|
|
|
+ if (state.settingsSubTab === 'system') {
|
|
|
+ await loadSystemSettings();
|
|
|
+ } else {
|
|
|
+ await loadDNSSettings();
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function loadDNSSettings() {
|
|
|
+ try {
|
|
|
+ const [settingsRes, staticRes, statusRes] = await Promise.all([
|
|
|
+ fetch(`${API_BASE}/dns/settings`),
|
|
|
+ fetch(`${API_BASE}/dns/static`),
|
|
|
+ fetch(`${API_BASE}/dns/status`)
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if (settingsRes.ok) {
|
|
|
+ state.dnsSettings = await settingsRes.json();
|
|
|
+ populateDNSSettingsForm(state.dnsSettings);
|
|
|
+ }
|
|
|
+ if (staticRes.ok) {
|
|
|
+ state.dnsStaticRecords = await staticRes.json();
|
|
|
+ renderStaticDNSList();
|
|
|
+ }
|
|
|
+ if (statusRes.ok) {
|
|
|
+ state.dnsStatus = await statusRes.json();
|
|
|
+ updateDNSStatusView(state.dnsStatus);
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Failed to load DNS settings:', err);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function populateDNSSettingsForm(s) {
|
|
|
+ if (!s) return;
|
|
|
+ const serversInput = document.getElementById('dns-servers-input');
|
|
|
+ if (serversInput) serversInput.value = (s.servers || []).join(', ');
|
|
|
+
|
|
|
+ const cacheSizeInput = document.getElementById('dns-cache-size-input');
|
|
|
+ if (cacheSizeInput) cacheSizeInput.value = s.cache_size || 2048;
|
|
|
+
|
|
|
+ const domainInput = document.getElementById('dns-domain-input');
|
|
|
+ if (domainInput) domainInput.value = s.domain || '';
|
|
|
+
|
|
|
+ const allowRemoteInput = document.getElementById('dns-allow-remote-input');
|
|
|
+ if (allowRemoteInput) allowRemoteInput.checked = s.allow_remote_requests !== false;
|
|
|
+
|
|
|
+ const dynamicEl = document.getElementById('dns-dynamic-servers-list');
|
|
|
+ if (dynamicEl) {
|
|
|
+ if (Array.isArray(s.dynamic_servers) && s.dynamic_servers.length > 0) {
|
|
|
+ dynamicEl.innerHTML = s.dynamic_servers.map(d => `<span class="px-2 py-0.5 rounded bg-cyan-950 text-cyan-300 border border-cyan-800 font-mono">${escapeHtml(d)}</span>`).join(' ');
|
|
|
+ } else {
|
|
|
+ dynamicEl.innerText = 'None';
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function updateDNSStatusView(st) {
|
|
|
+ if (!st) return;
|
|
|
+ const preview = document.getElementById('dns-resolv-preview');
|
|
|
+ if (preview) preview.innerText = st.resolv_conf_content || '(empty)';
|
|
|
+
|
|
|
+ const pillText = document.getElementById('dns-live-status-text');
|
|
|
+ const pill = document.getElementById('dns-live-status-pill');
|
|
|
+ if (pill && pillText) {
|
|
|
+ if (st.service_running) {
|
|
|
+ pill.className = 'px-2.5 py-1 rounded-full text-[11px] font-mono bg-emerald-500/10 text-emerald-400 border border-emerald-500/30 flex items-center gap-1.5';
|
|
|
+ pillText.innerText = 'DNS Service Active';
|
|
|
+ } else {
|
|
|
+ pill.className = 'px-2.5 py-1 rounded-full text-[11px] font-mono bg-rose-500/10 text-rose-400 border border-rose-500/30 flex items-center gap-1.5';
|
|
|
+ pillText.innerText = 'DNS Service Inactive';
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function appendDNSPreset(preset) {
|
|
|
+ const input = document.getElementById('dns-servers-input');
|
|
|
+ if (!input) return;
|
|
|
+ const current = input.value.trim();
|
|
|
+ if (!current) {
|
|
|
+ input.value = preset;
|
|
|
+ } else {
|
|
|
+ const existing = current.split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
|
|
|
+ const toAdd = preset.split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
|
|
|
+ toAdd.forEach(ip => {
|
|
|
+ if (!existing.includes(ip)) existing.push(ip);
|
|
|
+ });
|
|
|
+ input.value = existing.join(', ');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function saveDNSSettings(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const rawServers = document.getElementById('dns-servers-input')?.value || '';
|
|
|
+ const servers = rawServers.split(/[,\s]+/).map(s => s.trim()).filter(Boolean);
|
|
|
+
|
|
|
+ if (servers.length === 0) {
|
|
|
+ showToast('Validation Error', 'Please enter at least one upstream DNS server IP', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const cacheSize = parseInt(document.getElementById('dns-cache-size-input')?.value || '2048', 10);
|
|
|
+ const domain = (document.getElementById('dns-domain-input')?.value || '').trim();
|
|
|
+ const allowRemote = document.getElementById('dns-allow-remote-input')?.checked ?? true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dns/settings`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({
|
|
|
+ servers: servers,
|
|
|
+ cache_size: cacheSize,
|
|
|
+ domain: domain,
|
|
|
+ allow_remote_requests: allowRemote
|
|
|
+ })
|
|
|
+ });
|
|
|
+ const data = await res.json();
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('DNS Applied', 'Router DNS forwarders and resolver settings updated successfully', 'success');
|
|
|
+ loadDNSSettings();
|
|
|
+ } else {
|
|
|
+ showToast('Error', data.error || data.message || 'Failed to save DNS settings', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function flushDNSCache() {
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dns/flush`, { method: 'POST' });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Cache Flushed', 'Router DNS cache cleared and static hosts reloaded', 'success');
|
|
|
+ loadDNSSettings();
|
|
|
+ } else {
|
|
|
+ const err = await res.json();
|
|
|
+ showToast('Error', err.error || 'Failed to flush cache', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function renderStaticDNSList() {
|
|
|
+ const tbody = document.getElementById('dns-static-tbody');
|
|
|
+ const empty = document.getElementById('dns-static-empty');
|
|
|
+ const badge = document.getElementById('dns-static-count-badge');
|
|
|
+ if (!tbody) return;
|
|
|
+ tbody.innerHTML = '';
|
|
|
+
|
|
|
+ const records = state.dnsStaticRecords || [];
|
|
|
+ if (badge) badge.innerText = `${records.length} Records`;
|
|
|
+
|
|
|
+ if (records.length === 0) {
|
|
|
+ empty?.classList.remove('hidden');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ empty?.classList.add('hidden');
|
|
|
+
|
|
|
+ records.forEach(rec => {
|
|
|
+ const tr = document.createElement('tr');
|
|
|
+ tr.className = `hover:bg-slate-800/40 transition text-xs ${rec.disabled ? 'opacity-50' : ''}`;
|
|
|
+
|
|
|
+ tr.innerHTML = `
|
|
|
+ <td class="py-2.5 px-4 font-bold text-white">${escapeHtml(rec.name)}</td>
|
|
|
+ <td class="py-2.5 px-4">
|
|
|
+ <span class="px-2 py-0.5 rounded text-[10px] font-bold ${
|
|
|
+ rec.type === 'CNAME' ? 'bg-purple-950 text-purple-300 border border-purple-800' :
|
|
|
+ (rec.type === 'AAAA' ? 'bg-blue-950 text-blue-300 border border-blue-800' : 'bg-cyan-950 text-cyan-300 border border-cyan-800')
|
|
|
+ }">
|
|
|
+ ${rec.type || 'A'}
|
|
|
+ </span>
|
|
|
+ </td>
|
|
|
+ <td class="py-2.5 px-4 text-emerald-400 font-mono">${escapeHtml(rec.address)}</td>
|
|
|
+ <td class="py-2.5 px-4 text-slate-400 font-mono">${rec.ttl || 3600}s</td>
|
|
|
+ <td class="py-2.5 px-4 text-slate-400 italic truncate max-w-[140px]">${escapeHtml(rec.comment || '-')}</td>
|
|
|
+ <td class="py-2.5 px-4">
|
|
|
+ <span class="px-1.5 py-0.5 rounded text-[10px] font-semibold ${rec.disabled ? 'bg-slate-800 text-slate-500' : 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/30'}">
|
|
|
+ ${rec.disabled ? 'Disabled' : 'Active'}
|
|
|
+ </span>
|
|
|
+ </td>
|
|
|
+ <td class="py-2.5 px-4 text-right space-x-2 font-sans whitespace-nowrap">
|
|
|
+ <button onclick="toggleStaticDNS('${rec.id}', ${!rec.disabled})" class="text-xs ${rec.disabled ? 'text-emerald-400' : 'text-slate-400'} hover:underline">${rec.disabled ? 'Enable' : 'Disable'}</button>
|
|
|
+ <button onclick="openStaticDNSModal('${rec.id}')" class="text-xs text-cyan-400 hover:text-cyan-300 font-semibold">Edit</button>
|
|
|
+ <button onclick="deleteStaticDNS('${rec.id}')" class="text-xs text-slate-500 hover:text-rose-400">Delete</button>
|
|
|
+ </td>
|
|
|
+ `;
|
|
|
+ tbody.appendChild(tr);
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function openStaticDNSModal(id = null) {
|
|
|
+ const modal = document.getElementById('modal-static-dns');
|
|
|
+ const title = document.getElementById('modal-static-dns-title');
|
|
|
+ const form = document.getElementById('form-static-dns');
|
|
|
+ if (!modal || !form) return;
|
|
|
+
|
|
|
+ form.reset();
|
|
|
+ document.getElementById('modal-static-dns-id').value = '';
|
|
|
+
|
|
|
+ if (id) {
|
|
|
+ const rec = (state.dnsStaticRecords || []).find(r => r.id === id);
|
|
|
+ if (rec) {
|
|
|
+ if (title) title.innerText = 'Edit Static DNS Record';
|
|
|
+ document.getElementById('modal-static-dns-id').value = rec.id;
|
|
|
+ document.getElementById('modal-static-dns-name').value = rec.name;
|
|
|
+ document.getElementById('modal-static-dns-type').value = rec.type || 'A';
|
|
|
+ document.getElementById('modal-static-dns-addr').value = rec.address;
|
|
|
+ document.getElementById('modal-static-dns-ttl').value = rec.ttl || 3600;
|
|
|
+ document.getElementById('modal-static-dns-comment').value = rec.comment || '';
|
|
|
+ document.getElementById('modal-static-dns-disabled').checked = !!rec.disabled;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (title) title.innerText = 'Add Static DNS Record';
|
|
|
+ }
|
|
|
+
|
|
|
+ modal.classList.remove('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+function closeStaticDNSModal() {
|
|
|
+ document.getElementById('modal-static-dns')?.classList.add('hidden');
|
|
|
+}
|
|
|
+
|
|
|
+async function submitSaveStaticDNS(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const id = document.getElementById('modal-static-dns-id')?.value || '';
|
|
|
+ const name = (document.getElementById('modal-static-dns-name')?.value || '').trim();
|
|
|
+ const type = document.getElementById('modal-static-dns-type')?.value || 'A';
|
|
|
+ const address = (document.getElementById('modal-static-dns-addr')?.value || '').trim();
|
|
|
+ const ttl = parseInt(document.getElementById('modal-static-dns-ttl')?.value || '3600', 10);
|
|
|
+ const comment = (document.getElementById('modal-static-dns-comment')?.value || '').trim();
|
|
|
+ const disabled = document.getElementById('modal-static-dns-disabled')?.checked ?? false;
|
|
|
+
|
|
|
+ if (!name || !address) {
|
|
|
+ showToast('Validation Error', 'Name and Target Address are required', 'error');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dns/static`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ id, name, type, address, ttl, comment, disabled })
|
|
|
+ });
|
|
|
+ const data = await res.json();
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Static DNS Saved', `Saved record ${name} -> ${address}`, 'success');
|
|
|
+ closeStaticDNSModal();
|
|
|
+ loadDNSSettings();
|
|
|
+ } else {
|
|
|
+ showToast('Error', data.error || data.message || 'Failed to save record', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function deleteStaticDNS(id) {
|
|
|
+ if (!confirm('Are you sure you want to delete this static DNS entry?')) return;
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dns/static/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Deleted', 'Static DNS record deleted', 'success');
|
|
|
+ loadDNSSettings();
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function toggleStaticDNS(id, disabled) {
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/dns/static/${encodeURIComponent(id)}/state`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ disabled })
|
|
|
+ });
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('Updated', `Record ${disabled ? 'disabled' : 'enabled'}`, 'success');
|
|
|
+ loadDNSSettings();
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function loadSystemSettings() {
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/system/settings`);
|
|
|
+ if (res.ok) {
|
|
|
+ const data = await res.json();
|
|
|
+ const hostInput = document.getElementById('system-hostname-input');
|
|
|
+ if (hostInput) hostInput.value = data.hostname || '';
|
|
|
+
|
|
|
+ const tzInput = document.getElementById('system-timezone-input');
|
|
|
+ if (tzInput && data.timezone) tzInput.value = data.timezone;
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.error('Failed to load system settings:', err);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function saveSystemSettings(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const hostname = (document.getElementById('system-hostname-input')?.value || '').trim();
|
|
|
+ const timezone = document.getElementById('system-timezone-input')?.value || 'UTC';
|
|
|
+
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${API_BASE}/system/settings`, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ hostname, timezone })
|
|
|
+ });
|
|
|
+ const data = await res.json();
|
|
|
+ if (res.ok) {
|
|
|
+ showToast('System Updated', 'Hostname and timezone saved successfully', 'success');
|
|
|
+ const sideH = document.getElementById('side-hostname');
|
|
|
+ if (sideH) sideH.innerText = data.hostname;
|
|
|
+ } else {
|
|
|
+ showToast('Error', data.error || 'Failed to save system settings', 'error');
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ showToast('Error', err.message, 'error');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
|
|
|
|