Fix DHCP client unable to get IP and config not persisting

- Fixed verifyAssignment being too strict for new clients
- Fixed parseRequestedIP string conversion bug
- Fixed response sent to 0.0.0.0 instead of broadcast address
- Added SO_BROADCAST support for UDP socket
- Fixed session persistence after page refresh (localStorage)
- Added in-memory session store for auth middleware
- Added config reloader so DHCP server picks up web UI changes dynamically
This commit is contained in:
CNBUGS AI
2026-04-24 16:03:54 +08:00
commit 8ad4c3576d
39 changed files with 7756 additions and 0 deletions
+303
View File
@@ -0,0 +1,303 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background: #2c3e50;
color: white;
padding: 20px;
margin-bottom: 20px;
border-radius: 8px;
}
header h1 {
margin-bottom: 15px;
}
nav {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
nav a {
color: white;
text-decoration: none;
padding: 8px 16px;
border-radius: 4px;
background: rgba(255,255,255,0.1);
}
nav a:hover {
background: rgba(255,255,255,0.2);
}
section {
background: white;
padding: 20px;
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h2 {
margin-bottom: 20px;
color: #2c3e50;
}
h3 {
margin-bottom: 15px;
color: #34495e;
}
/* Login Form */
#loginForm {
max-width: 400px;
display: flex;
flex-direction: column;
gap: 15px;
}
input, select {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
button {
padding: 10px 20px;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #2980b9;
}
/* Stats */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.stat-card {
background: #3498db;
color: white;
padding: 20px;
border-radius: 8px;
text-align: center;
}
.stat-card h3 {
color: white;
margin-bottom: 10px;
font-size: 14px;
}
.stat-card p {
font-size: 36px;
font-weight: bold;
}
/* Status Grid */
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.status-item {
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
}
.status-label {
display: block;
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.status-value {
font-size: 18px;
font-weight: bold;
color: #27ae60;
}
/* Info Grid */
.info-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.info-item {
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
}
.info-label {
display: block;
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.info-value {
font-size: 18px;
font-weight: bold;
color: #2c3e50;
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
th {
background: #f8f9fa;
font-weight: 600;
}
tr:hover {
background: #f8f9fa;
}
/* Panels */
.panel {
margin-bottom: 30px;
}
/* Form Styles */
.form-row {
margin-bottom: 15px;
}
.form-row label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #34495e;
}
.form-row input[type="text"],
.form-row input[type="number"] {
width: 100%;
}
.form-row input[type="checkbox"] {
width: auto;
margin-right: 10px;
}
/* Utility */
.hidden {
display: none;
}
/* Pool Stats */
.pool-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
.stat-item {
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
}
.stat-label {
display: block;
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.stat-value {
font-size: 18px;
font-weight: bold;
color: #2c3e50;
}
/* Pool Bar */
.pool-bar {
height: 24px;
background: #e9ecef;
border-radius: 12px;
overflow: hidden;
margin-top: 10px;
}
.pool-bar-fill {
height: 100%;
background: linear-gradient(90deg, #27ae60, #2ecc71);
border-radius: 12px;
transition: width 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 12px;
font-weight: bold;
min-width: 30px;
}
/* Client Status */
.status-active {
color: #27ae60;
font-weight: bold;
}
.status-expired {
color: #e74c3c;
font-weight: bold;
}
/* Responsive */
@media (max-width: 768px) {
.stats {
grid-template-columns: 1fr;
}
.status-grid,
.info-grid {
grid-template-columns: 1fr;
}
nav {
flex-direction: column;
}
}
+804
View File
@@ -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);
}
}
+358
View File
@@ -0,0 +1,358 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DHCP & DNS 管理器</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<div class="container">
<header>
<h1>🌐 DHCP & DNS 管理器</h1>
<nav>
<a href="#dashboard">仪表盘</a>
<a href="#clients">DHCP 客户端</a>
<a href="#dhcp">DHCP 配置</a>
<a href="#dns">DNS 配置</a>
<a href="#settings">系统设置</a>
<button id="logoutBtn" style="display:none;">退出</button>
</nav>
</header>
<!-- Login Section -->
<section id="loginSection">
<h2>登录</h2>
<form id="loginForm">
<input type="text" id="username" placeholder="用户名" required>
<input type="password" id="password" placeholder="密码" required>
<button type="submit">登录</button>
</form>
</section>
<!-- Dashboard -->
<section id="dashboard" style="display:none;">
<h2>仪表盘</h2>
<div class="stats">
<div class="stat-card" onclick="document.querySelector('a[href=\'#clients\']').click()" style="cursor:pointer;">
<h3>活跃租约</h3>
<p id="activeLeases">0</p>
</div>
<div class="stat-card">
<h3>静态绑定</h3>
<p id="staticBindings">0</p>
</div>
<div class="stat-card">
<h3>DNS 记录</h3>
<p id="dnsRecords">0</p>
</div>
<div class="stat-card">
<h3>在线设备</h3>
<p id="onlineDevices">0</p>
</div>
</div>
<div class="panel">
<h3>系统状态</h3>
<div class="status-grid">
<div class="status-item">
<span class="status-label">DHCP 服务</span>
<span class="status-value" id="dhcpStatus">运行中</span>
</div>
<div class="status-item">
<span class="status-label">DNS 服务</span>
<span class="status-value" id="dnsStatus">运行中</span>
</div>
<div class="status-item">
<span class="status-label">Web 服务</span>
<span class="status-value" id="webStatus">运行中</span>
</div>
</div>
</div>
</section>
<!-- DHCP Configuration -->
<section id="dhcp" style="display:none;">
<h2>DHCP 配置</h2>
<div class="panel">
<h3>基础配置</h3>
<form id="dhcpBasicForm">
<div class="form-row">
<label>启用 DHCP</label>
<input type="checkbox" id="dhcpEnabled" checked>
</div>
<div class="form-row">
<label>网络接口</label>
<input type="text" id="dhcpInterface" placeholder="eth0" required>
</div>
<div class="form-row">
<label>网段地址</label>
<input type="text" id="dhcpNetwork" placeholder="192.168.1.0" required>
</div>
<div class="form-row">
<label>子网掩码</label>
<input type="text" id="dhcpNetmask" placeholder="255.255.255.0" required>
</div>
<div class="form-row">
<label>网关地址</label>
<input type="text" id="dhcpGateway" placeholder="192.168.1.1" required>
</div>
<div class="form-row">
<label>域名</label>
<input type="text" id="dhcpDomain" placeholder="local">
</div>
<button type="submit">保存基础配置</button>
</form>
</div>
<div class="panel">
<h3>IP 地址池</h3>
<form id="dhcpPoolForm">
<div class="form-row">
<label>起始 IP</label>
<input type="text" id="dhcpPoolStart" placeholder="192.168.1.100" required>
</div>
<div class="form-row">
<label>结束 IP</label>
<input type="text" id="dhcpPoolEnd" placeholder="192.168.1.200" required>
</div>
<div class="form-row">
<label>租约时间(秒)</label>
<input type="number" id="dhcpLeaseTime" placeholder="86400" value="86400">
</div>
<button type="submit">保存地址池配置</button>
</form>
</div>
<div class="panel">
<h3>DNS 服务器</h3>
<form id="dhcpDnsForm">
<div class="form-row">
<label>DNS 服务器列表(逗号分隔)</label>
<input type="text" id="dhcpDnsServers" placeholder="192.168.1.1,114.114.114.114,8.8.8.8">
</div>
<button type="submit">保存 DNS 配置</button>
</form>
</div>
<div class="panel">
<h3>排除 IP 列表</h3>
<form id="dhcpExcludedForm">
<div class="form-row">
<label>排除的 IP(逗号分隔)</label>
<input type="text" id="dhcpExcludedIps" placeholder="192.168.1.1,192.168.1.2,192.168.1.3">
</div>
<button type="submit">保存排除列表</button>
</form>
</div>
<div class="panel">
<h3>静态 IP 绑定</h3>
<button onclick="showAddBindingForm()">+ 新增绑定</button>
<table id="bindingsTable">
<thead>
<tr>
<th>MAC 地址</th>
<th>IP 地址</th>
<th>主机名</th>
<th>描述</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</section>
<!-- DHCP Clients -->
<section id="clients" style="display:none;">
<h2>DHCP 客户端列表</h2>
<div class="panel">
<h3>已分配 IP 的客户端</h3>
<button onclick="loadClients()">🔄 刷新</button>
<button onclick="toggleAutoRefresh()" id="autoRefreshBtn">⏸️ 自动刷新: 关</button>
<table id="clientsTable">
<thead>
<tr>
<th>MAC 地址</th>
<th>IP 地址</th>
<th>主机名</th>
<th>租约剩余</th>
<th>过期时间</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="6" style="text-align:center;color:#999;">暂无客户端</td>
</tr>
</tbody>
</table>
</div>
<div class="panel">
<h3>IP 地址池使用情况</h3>
<div class="pool-stats">
<div class="stat-item">
<span class="stat-label">地址池范围</span>
<span class="stat-value" id="poolRange">--</span>
</div>
<div class="stat-item">
<span class="stat-label">已分配</span>
<span class="stat-value" id="poolUsed">0</span>
</div>
<div class="stat-item">
<span class="stat-label">可用</span>
<span class="stat-value" id="poolAvailable">0</span>
</div>
<div class="stat-item">
<span class="stat-label">使用率</span>
<span class="stat-value" id="poolUsage">0%</span>
</div>
</div>
<div class="pool-bar">
<div class="pool-bar-fill" id="poolBarFill" style="width: 0%;"></div>
</div>
</div>
</section>
<!-- DNS Configuration -->
<section id="dns" style="display:none;">
<h2>DNS 配置</h2>
<div class="panel">
<h3>基础配置</h3>
<form id="dnsBasicForm">
<div class="form-row">
<label>启用 DNS</label>
<input type="checkbox" id="dnsEnabled" checked>
</div>
<div class="form-row">
<label>监听地址</label>
<input type="text" id="dnsListenAddr" placeholder="0.0.0.0" value="0.0.0.0">
</div>
<div class="form-row">
<label>监听端口</label>
<input type="number" id="dnsListenPort" placeholder="53" value="53">
</div>
<div class="form-row">
<label>启用递归查询</label>
<input type="checkbox" id="dnsRecursion" checked>
</div>
<button type="submit">保存基础配置</button>
</form>
</div>
<div class="panel">
<h3>上游 DNS 服务器</h3>
<form id="dnsUpstreamForm">
<div class="form-row">
<label>上游 DNS(逗号分隔)</label>
<input type="text" id="dnsUpstream" placeholder="8.8.8.8,1.1.1.1,114.114.114.114">
</div>
<button type="submit">保存上游 DNS</button>
</form>
</div>
<div class="panel">
<h3>DNS 区域 (Zone)</h3>
<button onclick="showAddZoneForm()">+ 新增区域</button>
<table id="zonesTable">
<thead>
<tr>
<th>区域名称</th>
<th>类型</th>
<th>记录数</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="panel">
<h3>DNS 记录</h3>
<button onclick="showAddRecordForm()">+ 新增记录</button>
<table id="recordsTable">
<thead>
<tr>
<th>域名</th>
<th>类型</th>
<th></th>
<th>TTL</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="panel">
<h3>查询日志</h3>
<button onclick="loadLogs()">刷新</button>
<table id="logsTable">
<thead>
<tr>
<th>时间</th>
<th>客户端 IP</th>
<th>查询域名</th>
<th>类型</th>
<th>响应</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</section>
<!-- Settings -->
<section id="settings" style="display:none;">
<h2>系统设置</h2>
<div class="panel">
<h3>Web 设置</h3>
<form id="webSettingsForm">
<div class="form-row">
<label>监听地址</label>
<input type="text" id="webHost" placeholder="0.0.0.0" value="0.0.0.0">
</div>
<div class="form-row">
<label>监听端口</label>
<input type="number" id="webPort" placeholder="8080" value="8080">
</div>
<button type="submit">保存 Web 设置</button>
</form>
</div>
<div class="panel">
<h3>配置管理</h3>
<button onclick="exportConfig()">导出配置</button>
<button onclick="importConfig()">导入配置</button>
<button onclick="restartService()">重启服务</button>
</div>
<div class="panel">
<h3>系统信息</h3>
<div class="info-grid">
<div class="info-item">
<span class="info-label">版本</span>
<span class="info-value">v0.1.1</span>
</div>
<div class="info-item">
<span class="info-label">运行时间</span>
<span class="info-value" id="uptime">--</span>
</div>
<div class="info-item">
<span class="info-label">数据库大小</span>
<span class="info-value" id="dbSize">--</span>
</div>
</div>
</div>
</section>
</div>
<script src="/static/js/app.js"></script>
</body>
</html>