|
@@ -0,0 +1,804 @@
|
|
|
|
|
+let sessionId = localStorage.getItem('session_id') || null;
|
|
|
|
|
+let autoRefreshInterval = null;
|
|
|
|
|
+let autoRefreshEnabled = false;
|
|
|
|
|
+
|
|
|
|
|
+// Restore session on page load
|
|
|
|
|
+if (sessionId) {
|
|
|
|
|
+ document.getElementById('loginSection').style.display = 'none';
|
|
|
|
|
+ document.getElementById('dashboard').style.display = 'block';
|
|
|
|
|
+ document.getElementById('logoutBtn').style.display = 'block';
|
|
|
|
|
+ loadDashboard();
|
|
|
|
|
+ loadDHCPConfig();
|
|
|
|
|
+ loadDNSConfig();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Auto Refresh
|
|
|
|
|
+function toggleAutoRefresh() {
|
|
|
|
|
+ autoRefreshEnabled = !autoRefreshEnabled;
|
|
|
|
|
+ const btn = document.getElementById('autoRefreshBtn');
|
|
|
|
|
+
|
|
|
|
|
+ if (autoRefreshEnabled) {
|
|
|
|
|
+ btn.textContent = '▶️ 自动刷新: 开';
|
|
|
|
|
+ autoRefreshInterval = setInterval(loadClients, 10000); // 每10秒刷新
|
|
|
|
|
+ } else {
|
|
|
|
|
+ btn.textContent = '⏸️ 自动刷新: 关';
|
|
|
|
|
+ clearInterval(autoRefreshInterval);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Login
|
|
|
|
|
+document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+
|
|
|
|
|
+ const username = document.getElementById('username').value;
|
|
|
|
|
+ const password = document.getElementById('password').value;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/login', {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
+ body: JSON.stringify({ username, password })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ sessionId = data.session_id;
|
|
|
|
|
+ localStorage.setItem('session_id', sessionId);
|
|
|
|
|
+ document.getElementById('loginSection').style.display = 'none';
|
|
|
|
|
+ document.getElementById('dashboard').style.display = 'block';
|
|
|
|
|
+ document.getElementById('logoutBtn').style.display = 'block';
|
|
|
|
|
+ loadDashboard();
|
|
|
|
|
+ loadDHCPConfig();
|
|
|
|
|
+ loadDNSConfig();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ alert(data.error || '登录失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ alert('登录失败:' + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// Logout
|
|
|
|
|
+document.getElementById('logoutBtn').addEventListener('click', () => {
|
|
|
|
|
+ sessionId = null;
|
|
|
|
|
+ location.reload();
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// Navigation
|
|
|
|
|
+document.querySelectorAll('nav a').forEach(link => {
|
|
|
|
|
+ link.addEventListener('click', (e) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ const target = e.target.getAttribute('href').substring(1);
|
|
|
|
|
+
|
|
|
|
|
+ document.querySelectorAll('section').forEach(section => {
|
|
|
|
|
+ if (section.id !== 'loginSection') {
|
|
|
|
|
+ section.style.display = 'none';
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById(target).style.display = 'block';
|
|
|
|
|
+
|
|
|
|
|
+ if (target === 'dashboard') loadDashboard();
|
|
|
|
|
+ if (target === 'clients') loadClients();
|
|
|
|
|
+ if (target === 'dhcp') loadDHCPConfig();
|
|
|
|
|
+ if (target === 'dns') loadDNSConfig();
|
|
|
|
|
+ if (target === 'settings') loadSystemInfo();
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// Load Dashboard
|
|
|
|
|
+async function loadDashboard() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dashboard', {
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('activeLeases').textContent = data.active_leases || 0;
|
|
|
|
|
+ document.getElementById('staticBindings').textContent = data.static_bindings || 0;
|
|
|
|
|
+ document.getElementById('dnsRecords').textContent = data.dns_records || 0;
|
|
|
|
|
+ document.getElementById('onlineDevices').textContent = data.online_devices || 0;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to load dashboard:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Load DHCP Clients
|
|
|
|
|
+async function loadClients() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dhcp/leases', {
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ const tbody = document.querySelector('#clientsTable tbody');
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ if (!data.leases || data.leases.length === 0) {
|
|
|
|
|
+ tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#999;">暂无客户端</td></tr>';
|
|
|
|
|
+ updatePoolStats(0, 0);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const now = Math.floor(Date.now() / 1000);
|
|
|
|
|
+ let activeCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+ data.leases.forEach(lease => {
|
|
|
|
|
+ const expiresAt = lease.ExpiresAt || 0;
|
|
|
|
|
+ const remaining = expiresAt - now;
|
|
|
|
|
+ const isActive = remaining > 0;
|
|
|
|
|
+ if (isActive) activeCount++;
|
|
|
|
|
+
|
|
|
|
|
+ const row = document.createElement('tr');
|
|
|
|
|
+
|
|
|
|
|
+ // MAC
|
|
|
|
|
+ const macCell = document.createElement('td');
|
|
|
|
|
+ macCell.textContent = lease.MAC || '-';
|
|
|
|
|
+ row.appendChild(macCell);
|
|
|
|
|
+
|
|
|
|
|
+ // IP
|
|
|
|
|
+ const ipCell = document.createElement('td');
|
|
|
|
|
+ ipCell.textContent = lease.IP || '-';
|
|
|
|
|
+ row.appendChild(ipCell);
|
|
|
|
|
+
|
|
|
|
|
+ // Hostname
|
|
|
|
|
+ const hostCell = document.createElement('td');
|
|
|
|
|
+ hostCell.textContent = lease.Hostname || '-';
|
|
|
|
|
+ row.appendChild(hostCell);
|
|
|
|
|
+
|
|
|
|
|
+ // Remaining time
|
|
|
|
|
+ const remainCell = document.createElement('td');
|
|
|
|
|
+ remainCell.textContent = isActive ? formatTimeRemaining(remaining) : '已过期';
|
|
|
|
|
+ remainCell.style.color = isActive ? '#27ae60' : '#e74c3c';
|
|
|
|
|
+ row.appendChild(remainCell);
|
|
|
|
|
+
|
|
|
|
|
+ // Expiry time
|
|
|
|
|
+ const expireCell = document.createElement('td');
|
|
|
|
|
+ expireCell.textContent = expiresAt > 0 ? new Date(expiresAt * 1000).toLocaleString() : '-';
|
|
|
|
|
+ row.appendChild(expireCell);
|
|
|
|
|
+
|
|
|
|
|
+ // Status
|
|
|
|
|
+ const statusCell = document.createElement('td');
|
|
|
|
|
+ statusCell.innerHTML = isActive ? '<span class="status-active">● 在线</span>' : '<span class="status-expired">● 已过期</span>';
|
|
|
|
|
+ row.appendChild(statusCell);
|
|
|
|
|
+
|
|
|
|
|
+ tbody.appendChild(row);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ updatePoolStats(activeCount, data.leases.length);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to load clients:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Update Pool Stats
|
|
|
|
|
+async function updatePoolStats(activeCount, totalCount) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dhcp/config', {
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ const cfg = data.config;
|
|
|
|
|
+
|
|
|
|
|
+ if (cfg) {
|
|
|
|
|
+ const startIP = cfg.ip_pool_start || '192.168.1.100';
|
|
|
|
|
+ const endIP = cfg.ip_pool_end || '192.168.1.200';
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('poolRange').textContent = `${startIP} - ${endIP}`;
|
|
|
|
|
+ document.getElementById('poolUsed').textContent = activeCount;
|
|
|
|
|
+
|
|
|
|
|
+ // Calculate pool size
|
|
|
|
|
+ const startBytes = ipToBytes(startIP);
|
|
|
|
|
+ const endBytes = ipToBytes(endIP);
|
|
|
|
|
+ const poolSize = bytesToIP(endBytes) - bytesToIP(startBytes) + 1;
|
|
|
|
|
+ const available = poolSize - activeCount;
|
|
|
|
|
+ const usage = poolSize > 0 ? Math.round((activeCount / poolSize) * 100) : 0;
|
|
|
|
|
+
|
|
|
|
|
+ document.getElementById('poolAvailable').textContent = available;
|
|
|
|
|
+ document.getElementById('poolUsage').textContent = usage + '%';
|
|
|
|
|
+
|
|
|
|
|
+ const barFill = document.getElementById('poolBarFill');
|
|
|
|
|
+ barFill.style.width = usage + '%';
|
|
|
|
|
+ barFill.textContent = usage + '%';
|
|
|
|
|
+
|
|
|
|
|
+ // Color based on usage
|
|
|
|
|
+ if (usage > 90) {
|
|
|
|
|
+ barFill.style.background = 'linear-gradient(90deg, #e74c3c, #c0392b)';
|
|
|
|
|
+ } else if (usage > 70) {
|
|
|
|
|
+ barFill.style.background = 'linear-gradient(90deg, #f39c12, #e67e22)';
|
|
|
|
|
+ } else {
|
|
|
|
|
+ barFill.style.background = 'linear-gradient(90deg, #27ae60, #2ecc71)';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to update pool stats:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Helper functions
|
|
|
|
|
+function ipToBytes(ip) {
|
|
|
|
|
+ return ip.split('.').map(Number);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function bytesToIP(bytes) {
|
|
|
|
|
+ return (bytes[0] << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function formatTimeRemaining(seconds) {
|
|
|
|
|
+ if (seconds <= 0) return '已过期';
|
|
|
|
|
+
|
|
|
|
|
+ const days = Math.floor(seconds / 86400);
|
|
|
|
|
+ const hours = Math.floor((seconds % 86400) / 3600);
|
|
|
|
|
+ const minutes = Math.floor((seconds % 3600) / 60);
|
|
|
|
|
+
|
|
|
|
|
+ if (days > 0) return `${days}天${hours}小时`;
|
|
|
|
|
+ if (hours > 0) return `${hours}小时${minutes}分钟`;
|
|
|
|
|
+ return `${minutes}分钟`;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Load DHCP Config
|
|
|
|
|
+async function loadDHCPConfig() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dhcp/config', {
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ const cfg = data.config;
|
|
|
|
|
+
|
|
|
|
|
+ if (cfg) {
|
|
|
|
|
+ document.getElementById('dhcpEnabled').checked = cfg.enabled;
|
|
|
|
|
+ document.getElementById('dhcpInterface').value = cfg.interface || '';
|
|
|
|
|
+ document.getElementById('dhcpNetwork').value = cfg.network || '';
|
|
|
|
|
+ document.getElementById('dhcpNetmask').value = cfg.netmask || '';
|
|
|
|
|
+ document.getElementById('dhcpGateway').value = cfg.gateway || '';
|
|
|
|
|
+ document.getElementById('dhcpDomain').value = cfg.domain_name || '';
|
|
|
|
|
+ document.getElementById('dhcpPoolStart').value = cfg.ip_pool_start || '';
|
|
|
|
|
+ document.getElementById('dhcpPoolEnd').value = cfg.ip_pool_end || '';
|
|
|
|
|
+ document.getElementById('dhcpLeaseTime').value = cfg.lease_time || 86400;
|
|
|
|
|
+ document.getElementById('dhcpDnsServers').value = (cfg.dns_servers || []).join(',');
|
|
|
|
|
+ document.getElementById('dhcpExcludedIps').value = (cfg.excluded_ips || []).join(',');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ loadBindings();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to load DHCP config:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Save DHCP Basic Config
|
|
|
|
|
+document.getElementById('dhcpBasicForm').addEventListener('submit', async (e) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+
|
|
|
|
|
+ const config = {
|
|
|
|
|
+ enabled: document.getElementById('dhcpEnabled').checked,
|
|
|
|
|
+ interface: document.getElementById('dhcpInterface').value,
|
|
|
|
|
+ network: document.getElementById('dhcpNetwork').value,
|
|
|
|
|
+ netmask: document.getElementById('dhcpNetmask').value,
|
|
|
|
|
+ gateway: document.getElementById('dhcpGateway').value,
|
|
|
|
|
+ domain_name: document.getElementById('dhcpDomain').value
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dhcp/config', {
|
|
|
|
|
+ method: 'PUT',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'X-Session-ID': sessionId,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify(config)
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const contentType = response.headers.get('content-type');
|
|
|
|
|
+ if (!contentType || !contentType.includes('application/json')) {
|
|
|
|
|
+ const text = await response.text();
|
|
|
|
|
+ console.error('Non-JSON response:', text);
|
|
|
|
|
+ alert('服务器返回了非 JSON 格式响应,请查看控制台');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ alert('基础配置已保存');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ alert('保存失败:' + (data.error || '未知错误'));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Save error:', error);
|
|
|
|
|
+ alert('保存失败:' + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// Save DHCP Pool Config
|
|
|
|
|
+document.getElementById('dhcpPoolForm').addEventListener('submit', async (e) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+
|
|
|
|
|
+ const config = {
|
|
|
|
|
+ ip_pool_start: document.getElementById('dhcpPoolStart').value,
|
|
|
|
|
+ ip_pool_end: document.getElementById('dhcpPoolEnd').value,
|
|
|
|
|
+ lease_time: parseInt(document.getElementById('dhcpLeaseTime').value)
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dhcp/config', {
|
|
|
|
|
+ method: 'PUT',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'X-Session-ID': sessionId,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify(config)
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const contentType = response.headers.get('content-type');
|
|
|
|
|
+ if (!contentType || !contentType.includes('application/json')) {
|
|
|
|
|
+ const text = await response.text();
|
|
|
|
|
+ console.error('Non-JSON response:', text);
|
|
|
|
|
+ alert('服务器返回了非 JSON 格式响应,请查看控制台');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ alert('地址池配置已保存');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ alert('保存失败:' + (data.error || '未知错误'));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Save error:', error);
|
|
|
|
|
+ alert('保存失败:' + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// Save DHCP DNS Config
|
|
|
|
|
+document.getElementById('dhcpDnsForm').addEventListener('submit', async (e) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+
|
|
|
|
|
+ const dnsServers = document.getElementById('dhcpDnsServers').value.split(',').map(s => s.trim()).filter(s => s);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dhcp/config', {
|
|
|
|
|
+ method: 'PUT',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'X-Session-ID': sessionId,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({ dns_servers: dnsServers })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const contentType = response.headers.get('content-type');
|
|
|
|
|
+ if (!contentType || !contentType.includes('application/json')) {
|
|
|
|
|
+ const text = await response.text();
|
|
|
|
|
+ console.error('Non-JSON response:', text);
|
|
|
|
|
+ alert('服务器返回了非 JSON 格式响应,请查看控制台');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ alert('DNS 配置已保存');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ alert('保存失败:' + (data.error || '未知错误'));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Save error:', error);
|
|
|
|
|
+ alert('保存失败:' + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// Save DHCP Excluded IPs
|
|
|
|
|
+document.getElementById('dhcpExcludedForm').addEventListener('submit', async (e) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+
|
|
|
|
|
+ const excludedIps = document.getElementById('dhcpExcludedIps').value.split(',').map(s => s.trim()).filter(s => s);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dhcp/config', {
|
|
|
|
|
+ method: 'PUT',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'X-Session-ID': sessionId,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({ excluded_ips: excludedIps })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const contentType = response.headers.get('content-type');
|
|
|
|
|
+ if (!contentType || !contentType.includes('application/json')) {
|
|
|
|
|
+ const text = await response.text();
|
|
|
|
|
+ console.error('Non-JSON response:', text);
|
|
|
|
|
+ alert('服务器返回了非 JSON 格式响应,请查看控制台');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ alert('排除列表已保存');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ alert('保存失败:' + (data.error || '未知错误'));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Save error:', error);
|
|
|
|
|
+ alert('保存失败:' + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// Load Bindings
|
|
|
|
|
+async function loadBindings() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dhcp/bindings', {
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ const tbody = document.querySelector('#bindingsTable tbody');
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ data.bindings.forEach(binding => {
|
|
|
|
|
+ const row = tbody.insertRow();
|
|
|
|
|
+ row.insertCell(0).textContent = binding.MAC;
|
|
|
|
|
+ row.insertCell(1).textContent = binding.IP;
|
|
|
|
|
+ row.insertCell(2).textContent = binding.Hostname || '-';
|
|
|
|
|
+ row.insertCell(3).textContent = binding.Description || '-';
|
|
|
|
|
+
|
|
|
|
|
+ const actionCell = row.insertCell(4);
|
|
|
|
|
+ const deleteBtn = document.createElement('button');
|
|
|
|
|
+ deleteBtn.textContent = '删除';
|
|
|
|
|
+ deleteBtn.onclick = () => deleteBinding(binding.ID);
|
|
|
|
|
+ actionCell.appendChild(deleteBtn);
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to load bindings:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function showAddBindingForm() {
|
|
|
|
|
+ // TODO: Implement add binding form
|
|
|
|
|
+ alert('添加绑定功能开发中...');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function deleteBinding(id) {
|
|
|
|
|
+ if (!confirm('确定要删除这个绑定吗?')) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch(`/api/dhcp/bindings/${id}`, {
|
|
|
|
|
+ method: 'DELETE',
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ loadBindings();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ alert('删除失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ alert('删除失败:' + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Load DNS Config
|
|
|
|
|
+async function loadDNSConfig() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dns/config', {
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ const cfg = data.config;
|
|
|
|
|
+
|
|
|
|
|
+ if (cfg) {
|
|
|
|
|
+ document.getElementById('dnsEnabled').checked = cfg.enabled;
|
|
|
|
|
+ document.getElementById('dnsListenAddr').value = cfg.listen_addr || '0.0.0.0';
|
|
|
|
|
+ document.getElementById('dnsListenPort').value = cfg.listen_port || 53;
|
|
|
|
|
+ document.getElementById('dnsRecursion').checked = cfg.recursion !== false;
|
|
|
|
|
+ document.getElementById('dnsUpstream').value = (cfg.upstream || []).join(',');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ loadDNSRecords();
|
|
|
|
|
+ loadZones();
|
|
|
|
|
+ loadLogs();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to load DNS config:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Save DNS Basic Config
|
|
|
|
|
+document.getElementById('dnsBasicForm').addEventListener('submit', async (e) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+
|
|
|
|
|
+ const config = {
|
|
|
|
|
+ enabled: document.getElementById('dnsEnabled').checked,
|
|
|
|
|
+ listen_addr: document.getElementById('dnsListenAddr').value,
|
|
|
|
|
+ listen_port: parseInt(document.getElementById('dnsListenPort').value),
|
|
|
|
|
+ recursion: document.getElementById('dnsRecursion').checked
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dns/config', {
|
|
|
|
|
+ method: 'PUT',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'X-Session-ID': sessionId,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify(config)
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const contentType = response.headers.get('content-type');
|
|
|
|
|
+ if (!contentType || !contentType.includes('application/json')) {
|
|
|
|
|
+ const text = await response.text();
|
|
|
|
|
+ console.error('Non-JSON response:', text);
|
|
|
|
|
+ alert('服务器返回了非 JSON 格式响应,请查看控制台');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ alert('DNS 基础配置已保存');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ alert('保存失败:' + (data.error || '未知错误'));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Save error:', error);
|
|
|
|
|
+ alert('保存失败:' + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// Save DNS Upstream
|
|
|
|
|
+document.getElementById('dnsUpstreamForm').addEventListener('submit', async (e) => {
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+
|
|
|
|
|
+ const upstream = document.getElementById('dnsUpstream').value.split(',').map(s => s.trim()).filter(s => s);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dns/config', {
|
|
|
|
|
+ method: 'PUT',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'X-Session-ID': sessionId,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({ upstream })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const contentType = response.headers.get('content-type');
|
|
|
|
|
+ if (!contentType || !contentType.includes('application/json')) {
|
|
|
|
|
+ const text = await response.text();
|
|
|
|
|
+ console.error('Non-JSON response:', text);
|
|
|
|
|
+ alert('服务器返回了非 JSON 格式响应,请查看控制台');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ alert('上游 DNS 已保存');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ alert('保存失败:' + (data.error || '未知错误'));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Save error:', error);
|
|
|
|
|
+ alert('保存失败:' + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+// Load DNS Records
|
|
|
|
|
+async function loadDNSRecords() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dns/records', {
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ const tbody = document.querySelector('#recordsTable tbody');
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ data.records.forEach(record => {
|
|
|
|
|
+ const row = tbody.insertRow();
|
|
|
|
|
+ row.insertCell(0).textContent = record.Name;
|
|
|
|
|
+ row.insertCell(1).textContent = record.Type;
|
|
|
|
|
+ row.insertCell(2).textContent = record.Value;
|
|
|
|
|
+ row.insertCell(3).textContent = record.TTL;
|
|
|
|
|
+
|
|
|
|
|
+ const actionCell = row.insertCell(4);
|
|
|
|
|
+ const deleteBtn = document.createElement('button');
|
|
|
|
|
+ deleteBtn.textContent = '删除';
|
|
|
|
|
+ deleteBtn.onclick = () => deleteRecord(record.ID);
|
|
|
|
|
+ actionCell.appendChild(deleteBtn);
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to load records:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function showAddRecordForm() {
|
|
|
|
|
+ // TODO: Implement add record form
|
|
|
|
|
+ alert('添加记录功能开发中...');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function deleteRecord(id) {
|
|
|
|
|
+ if (!confirm('确定要删除这条记录吗?')) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch(`/api/dns/records/${id}`, {
|
|
|
|
|
+ method: 'DELETE',
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ loadDNSRecords();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ alert('删除失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ alert('删除失败:' + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Load Zones
|
|
|
|
|
+async function loadZones() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dns/zones', {
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ const tbody = document.querySelector('#zonesTable tbody');
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ data.zones.forEach(zone => {
|
|
|
|
|
+ const row = tbody.insertRow();
|
|
|
|
|
+ row.insertCell(0).textContent = zone.name;
|
|
|
|
|
+ row.insertCell(1).textContent = zone.type;
|
|
|
|
|
+ row.insertCell(2).textContent = zone.record_count;
|
|
|
|
|
+
|
|
|
|
|
+ const actionCell = row.insertCell(3);
|
|
|
|
|
+ const deleteBtn = document.createElement('button');
|
|
|
|
|
+ deleteBtn.textContent = '删除';
|
|
|
|
|
+ deleteBtn.onclick = () => deleteZone(zone.ID);
|
|
|
|
|
+ actionCell.appendChild(deleteBtn);
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to load zones:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function showAddZoneForm() {
|
|
|
|
|
+ // TODO: Implement add zone form
|
|
|
|
|
+ alert('添加区域功能开发中...');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function deleteZone(id) {
|
|
|
|
|
+ if (!confirm('确定要删除这个区域吗?')) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch(`/api/dns/zones/${id}`, {
|
|
|
|
|
+ method: 'DELETE',
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ loadZones();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ alert('删除失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ alert('删除失败:' + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Load Logs
|
|
|
|
|
+async function loadLogs() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/dns/logs', {
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ const tbody = document.querySelector('#logsTable tbody');
|
|
|
|
|
+ tbody.innerHTML = '';
|
|
|
|
|
+
|
|
|
|
|
+ data.logs.forEach(log => {
|
|
|
|
|
+ const row = tbody.insertRow();
|
|
|
|
|
+ row.insertCell(0).textContent = new Date(log.Timestamp * 1000).toLocaleString();
|
|
|
|
|
+ row.insertCell(1).textContent = log.ClientIP;
|
|
|
|
|
+ row.insertCell(2).textContent = log.QueryName;
|
|
|
|
|
+ row.insertCell(3).textContent = log.QueryType;
|
|
|
|
|
+ row.insertCell(4).textContent = log.Response || '-';
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to load logs:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Load System Info
|
|
|
|
|
+async function loadSystemInfo() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/config', {
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ // TODO: Update system info display
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Failed to load system info:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Export Config
|
|
|
|
|
+async function exportConfig() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/config/export', {
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const blob = await response.blob();
|
|
|
|
|
+ const url = window.URL.createObjectURL(blob);
|
|
|
|
|
+ const a = document.createElement('a');
|
|
|
|
|
+ a.href = url;
|
|
|
|
|
+ a.download = 'dhcp-dns-config.json';
|
|
|
|
|
+ a.click();
|
|
|
|
|
+ window.URL.revokeObjectURL(url);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ alert('导出失败:' + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Import Config
|
|
|
|
|
+async function importConfig() {
|
|
|
|
|
+ const input = document.createElement('input');
|
|
|
|
|
+ input.type = 'file';
|
|
|
|
|
+ input.accept = '.json';
|
|
|
|
|
+
|
|
|
|
|
+ input.onchange = async (e) => {
|
|
|
|
|
+ const file = e.target.files[0];
|
|
|
|
|
+ if (!file) return;
|
|
|
|
|
+
|
|
|
|
|
+ const formData = new FormData();
|
|
|
|
|
+ formData.append('config', file);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/config/import', {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId },
|
|
|
|
|
+ body: formData
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ alert('配置已导入');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ alert('导入失败:' + data.error);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ alert('导入失败:' + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ input.click();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Restart Service
|
|
|
|
|
+async function restartService() {
|
|
|
|
|
+ if (!confirm('确定要重启服务吗?服务将短暂中断。')) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('/api/service/restart', {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: { 'X-Session-ID': sessionId }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ alert('服务重启请求已发送');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const data = await response.json();
|
|
|
|
|
+ alert('重启失败:' + data.error);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ alert('重启失败:' + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|