let sessionId = localStorage.getItem('session_id') || null;
let autoRefreshInterval = null;
let autoRefreshEnabled = false;
// Restore session on page load
if (sessionId) {
// Verify session is still valid
fetch('/api/session/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId })
})
.then(res => res.json())
.then(data => {
if (data.valid) {
document.getElementById('loginSection').style.display = 'none';
document.getElementById('dashboard').style.display = 'block';
document.getElementById('logoutBtn').style.display = 'block';
loadDashboard();
loadDHCPConfig();
loadDNSConfig();
} else {
// Session expired, clear and show login
localStorage.removeItem('session_id');
sessionId = null;
}
})
.catch(err => {
console.error('Session verify error:', err);
// On error, clear and show login
localStorage.removeItem('session_id');
sessionId = null;
});
}
// 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', () => {
localStorage.removeItem('session_id');
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 = '
| 暂无客户端 |
';
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 ? '● 在线' : '● 已过期';
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);
}
}