// logs.js - 查询日志页面功能 // 全局变量 let currentPage = 1; let totalPages = 1; let logsPerPage = 30; // 默认显示30条记录 let currentFilter = ''; let currentSearch = ''; let logsChart = null; let currentSortField = ''; let currentSortDirection = 'desc'; // 默认降序 // IP地理位置缓存 let ipGeolocationCache = {}; const GEOLOCATION_CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 缓存有效期24小时 // 跟踪器数据库缓存 let trackersDatabase = null; let trackersLoaded = false; let trackersLoading = false; // WebSocket连接和重连计时器 let logsWsConnection = null; let logsWsReconnectTimer = null; // 加载跟踪器数据库 async function loadTrackersDatabase() { if (trackersLoaded) return trackersDatabase; if (trackersLoading) { // 等待正在进行的加载完成 while (trackersLoading) { await new Promise(resolve => setTimeout(resolve, 100)); } return trackersDatabase; } trackersLoading = true; try { const response = await fetch('/tracker/trackers.json'); if (!response.ok) { console.error('加载跟踪器数据库失败:', response.statusText); trackersDatabase = { trackers: {} }; return trackersDatabase; } trackersDatabase = await response.json(); trackersLoaded = true; return trackersDatabase; } catch (error) { console.error('加载跟踪器数据库失败:', error); trackersDatabase = { trackers: {} }; return trackersDatabase; } finally { trackersLoading = false; } } // 检查域名是否在跟踪器数据库中,并返回跟踪器信息 async function isDomainInTrackerDatabase(domain) { if (!trackersDatabase || !trackersLoaded) { await loadTrackersDatabase(); } if (!trackersDatabase || !trackersDatabase.trackers) { return null; } // 检查域名是否直接作为跟踪器键存在 if (trackersDatabase.trackers.hasOwnProperty(domain)) { return trackersDatabase.trackers[domain]; } // 检查域名是否在跟踪器URL中 for (const trackerKey in trackersDatabase.trackers) { if (trackersDatabase.trackers.hasOwnProperty(trackerKey)) { const tracker = trackersDatabase.trackers[trackerKey]; if (tracker && tracker.url) { try { const trackerUrl = new URL(tracker.url); if (trackerUrl.hostname === domain || trackerUrl.hostname.includes(domain)) { return tracker; } } catch (e) { // 忽略无效URL } } } } return null; } // 初始化查询日志页面 function initLogsPage() { console.log('初始化查询日志页面'); // 加载日志统计数据 loadLogsStats(); // 加载日志详情 loadLogs(); // 初始化图表 initLogsChart(); // 绑定事件 bindLogsEvents(); // 建立WebSocket连接,用于实时更新统计数据和图表 connectLogsWebSocket(); // 在页面卸载时清理资源 window.addEventListener('beforeunload', cleanupLogsResources); } // 清理资源 function cleanupLogsResources() { // 清除WebSocket连接 if (logsWsConnection) { logsWsConnection.close(); logsWsConnection = null; } // 清除重连计时器 if (logsWsReconnectTimer) { clearTimeout(logsWsReconnectTimer); logsWsReconnectTimer = null; } } // 绑定事件 function bindLogsEvents() { // 搜索按钮 const searchBtn = document.getElementById('logs-search-btn'); if (searchBtn) { searchBtn.addEventListener('click', () => { currentSearch = document.getElementById('logs-search').value; currentPage = 1; loadLogs(); }); } // 搜索框回车事件 const searchInput = document.getElementById('logs-search'); if (searchInput) { searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { currentSearch = searchInput.value; currentPage = 1; loadLogs(); } }); } // 结果过滤 const resultFilter = document.getElementById('logs-result-filter'); if (resultFilter) { resultFilter.addEventListener('change', () => { currentFilter = resultFilter.value; currentPage = 1; loadLogs(); }); } // 自定义记录数量 const perPageSelect = document.getElementById('logs-per-page'); if (perPageSelect) { perPageSelect.addEventListener('change', () => { logsPerPage = parseInt(perPageSelect.value); currentPage = 1; loadLogs(); }); } // 分页按钮 const prevBtn = document.getElementById('logs-prev-page'); const nextBtn = document.getElementById('logs-next-page'); if (prevBtn) { prevBtn.addEventListener('click', () => { if (currentPage > 1) { currentPage--; loadLogs(); } }); } if (nextBtn) { nextBtn.addEventListener('click', () => { if (currentPage < totalPages) { currentPage++; loadLogs(); } }); } // 页码跳转 const pageInput = document.getElementById('logs-page-input'); const goBtn = document.getElementById('logs-go-page'); if (pageInput) { // 页码输入框回车事件 pageInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { const page = parseInt(pageInput.value); if (page >= 1 && page <= totalPages) { currentPage = page; loadLogs(); } } }); } if (goBtn) { // 前往按钮点击事件 goBtn.addEventListener('click', () => { const page = parseInt(pageInput.value); if (page >= 1 && page <= totalPages) { currentPage = page; loadLogs(); } }); } // 时间范围切换 const timeRangeBtns = document.querySelectorAll('.time-range-btn'); timeRangeBtns.forEach(btn => { btn.addEventListener('click', () => { // 更新按钮样式 timeRangeBtns.forEach(b => { b.classList.remove('bg-primary', 'text-white'); b.classList.add('bg-gray-200', 'text-gray-700'); }); btn.classList.remove('bg-gray-200', 'text-gray-700'); btn.classList.add('bg-primary', 'text-white'); // 更新图表 const range = btn.getAttribute('data-range'); updateLogsChart(range); }); }); // 刷新按钮事件 const refreshBtn = document.getElementById('logs-refresh-btn'); if (refreshBtn) { refreshBtn.addEventListener('click', () => { // 重新加载日志 currentPage = 1; loadLogs(); }); } // 排序按钮事件 const sortHeaders = document.querySelectorAll('th[data-sort]'); sortHeaders.forEach(header => { header.addEventListener('click', () => { const sortField = header.getAttribute('data-sort'); // 如果点击的是当前排序字段,则切换排序方向 if (sortField === currentSortField) { currentSortDirection = currentSortDirection === 'asc' ? 'desc' : 'asc'; } else { // 否则,设置新的排序字段,默认降序 currentSortField = sortField; currentSortDirection = 'desc'; } // 更新排序图标 updateSortIcons(); // 重新加载日志 currentPage = 1; loadLogs(); }); }); } // 更新排序图标 function updateSortIcons() { const sortHeaders = document.querySelectorAll('th[data-sort]'); sortHeaders.forEach(header => { const sortField = header.getAttribute('data-sort'); const icon = header.querySelector('i'); // 重置所有图标 icon.className = 'fa fa-sort ml-1 text-xs'; // 设置当前排序字段的图标 if (sortField === currentSortField) { if (currentSortDirection === 'asc') { icon.className = 'fa fa-sort-asc ml-1 text-xs'; } else { icon.className = 'fa fa-sort-desc ml-1 text-xs'; } } }); } // 加载日志统计数据 function loadLogsStats() { // 使用封装的apiRequest函数进行API调用 apiRequest('/logs/stats') .then(data => { if (data && data.error) { console.error('加载日志统计数据失败:', data.error); return; } // 更新统计卡片 document.getElementById('logs-total-queries').textContent = data.totalQueries; document.getElementById('logs-avg-response-time').textContent = data.avgResponseTime.toFixed(2) + 'ms'; document.getElementById('logs-active-ips').textContent = data.activeIPs; // 计算屏蔽率 const blockRate = data.totalQueries > 0 ? (data.blockedQueries / data.totalQueries * 100).toFixed(1) : '0'; document.getElementById('logs-block-rate').textContent = blockRate + '%'; }) .catch(error => { console.error('加载日志统计数据失败:', error); }); } // 加载日志详情 function loadLogs() { // 显示加载状态 const loadingEl = document.getElementById('logs-loading'); if (loadingEl) { loadingEl.classList.remove('hidden'); } // 构建请求URL let endpoint = `/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * logsPerPage}`; // 添加过滤条件 if (currentFilter) { endpoint += `&result=${currentFilter}`; } // 添加搜索条件 if (currentSearch) { endpoint += `&search=${encodeURIComponent(currentSearch)}`; } // 添加排序条件 if (currentSortField) { endpoint += `&sort=${currentSortField}&direction=${currentSortDirection}`; } // 使用封装的apiRequest函数进行API调用 apiRequest(endpoint) .then(data => { if (data && data.error) { console.error('加载日志详情失败:', data.error); // 隐藏加载状态 if (loadingEl) { loadingEl.classList.add('hidden'); } return; } // 加载日志总数 return apiRequest('/logs/count').then(countData => { return { logs: data, count: countData.count }; }); }) .then(result => { if (!result || !result.logs) { console.error('加载日志详情失败: 无效的响应数据'); // 隐藏加载状态 if (loadingEl) { loadingEl.classList.add('hidden'); } return; } const logs = result.logs; const totalLogs = result.count; // 计算总页数 totalPages = Math.ceil(totalLogs / logsPerPage); // 更新日志表格 updateLogsTable(logs); // 绑定操作按钮事件 bindActionButtonsEvents(); // 更新分页信息 updateLogsPagination(); // 隐藏加载状态 if (loadingEl) { loadingEl.classList.add('hidden'); } }) .catch(error => { console.error('加载日志详情失败:', error); // 隐藏加载状态 if (loadingEl) { loadingEl.classList.add('hidden'); } }); } // 更新日志表格 async function updateLogsTable(logs) { const tableBody = document.getElementById('logs-table-body'); if (!tableBody) return; // 清空表格 tableBody.innerHTML = ''; if (logs.length === 0) { // 显示空状态 const emptyRow = document.createElement('tr'); emptyRow.innerHTML = `
暂无查询日志
`; tableBody.appendChild(emptyRow); return; } // 填充表格 for (const log of logs) { const row = document.createElement('tr'); row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors'; // 格式化时间 - 两行显示,第一行显示时间,第二行显示日期 const time = new Date(log.Timestamp); const formattedDate = time.toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }); const formattedTime = time.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); // 根据结果添加不同的背景色 let rowClass = ''; switch (log.Result) { case 'blocked': rowClass = 'bg-red-50'; // 淡红色填充 break; case 'allowed': // 检查是否是规则允许项目 if (log.BlockRule && log.BlockRule.includes('allow')) { rowClass = 'bg-green-50'; // 规则允许项目用淡绿色填充 } else { rowClass = ''; // 允许的不填充 } break; default: rowClass = ''; } // 添加行背景色 if (rowClass) { row.classList.add(rowClass); } // 添加被屏蔽或允许显示,并增加颜色 let statusText = ''; let statusClass = ''; switch (log.Result) { case 'blocked': statusText = '被屏蔽'; statusClass = 'text-danger'; break; case 'allowed': statusText = '允许'; statusClass = 'text-success'; break; case 'error': statusText = '错误'; statusClass = 'text-warning'; break; default: statusText = ''; statusClass = ''; } // 检查域名是否在跟踪器数据库中 const trackerInfo = await isDomainInTrackerDatabase(log.Domain); const isTracker = trackerInfo !== null; // 构建行内容 - 两行显示,时间列显示时间和日期,请求列显示域名和类型状态 // 添加缓存状态显示 const cacheStatusClass = log.FromCache ? 'text-primary' : 'text-gray-500'; const cacheStatusText = log.FromCache ? '缓存' : '非缓存'; // 检查域名是否被拦截 const isBlocked = log.Result === 'blocked'; // 构建跟踪器浮窗内容 const trackerTooltip = isTracker ? `
已知跟踪器
名称: ${trackerInfo.name}
类别: ${trackersDatabase.categories[trackerInfo.categoryId] || '未知'}
${trackerInfo.url ? `
URL: ${trackerInfo.url}
` : ''} ${trackerInfo.source ? `
源: ${trackerInfo.source}
` : ''}
` : ''; row.innerHTML = `
${formattedTime}
${formattedDate}
${log.ClientIP}
${log.Location || '未知 未知'}
${log.DNSSEC ? '' : ''}
${isTracker ? '' : ''} ${trackerTooltip}
${log.Domain}
类型: ${log.QueryType}, ${statusText}, ${log.FromCache ? '缓存' : '实时'}${log.DNSSEC ? ', DNSSEC' : ''}${log.EDNS ? ', EDNS' : ''}
DNS 服务器: ${log.DNSServer || '无'}, DNSSEC专用: ${log.DNSSECServer || '无'}
${log.ResponseTime}ms ${log.BlockRule || '-'} ${isBlocked ? `` : `` } `; // 添加跟踪器图标悬停事件 if (isTracker) { const iconContainer = row.querySelector('.tracker-icon-container'); const tooltip = iconContainer.querySelector('.tracker-tooltip'); if (iconContainer && tooltip) { tooltip.style.display = 'none'; iconContainer.addEventListener('mouseenter', () => { tooltip.style.display = 'block'; }); iconContainer.addEventListener('mouseleave', () => { tooltip.style.display = 'none'; }); } } // 绑定按钮事件 const blockBtn = row.querySelector('.block-btn'); if (blockBtn) { blockBtn.addEventListener('click', (e) => { e.preventDefault(); const domain = e.currentTarget.dataset.domain; blockDomain(domain); }); } const unblockBtn = row.querySelector('.unblock-btn'); if (unblockBtn) { unblockBtn.addEventListener('click', (e) => { e.preventDefault(); const domain = e.currentTarget.dataset.domain; unblockDomain(domain); }); } tableBody.appendChild(row); } } // 更新分页信息 function updateLogsPagination() { // 更新页码显示 document.getElementById('logs-current-page').textContent = currentPage; document.getElementById('logs-total-pages').textContent = totalPages; // 更新页码输入框 const pageInput = document.getElementById('logs-page-input'); if (pageInput) { pageInput.max = totalPages; pageInput.value = currentPage; } // 更新按钮状态 const prevBtn = document.getElementById('logs-prev-page'); const nextBtn = document.getElementById('logs-next-page'); if (prevBtn) { prevBtn.disabled = currentPage === 1; } if (nextBtn) { nextBtn.disabled = currentPage === totalPages; } } // 初始化日志图表 function initLogsChart() { const ctx = document.getElementById('logs-trend-chart'); if (!ctx) return; // 获取24小时统计数据 apiRequest('/hourly-stats') .then(data => { if (data && data.error) { console.error('初始化日志图表失败:', data.error); return; } // 创建图表 logsChart = new Chart(ctx, { type: 'line', data: { labels: data.labels, datasets: [{ label: '查询数', data: data.data, borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)', tension: 0.4, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'top' }, tooltip: { mode: 'index', intersect: false } }, scales: { y: { beginAtZero: true, ticks: { precision: 0 } } } } }); }) .catch(error => { console.error('初始化日志图表失败:', error); }); } // 更新日志图表 function updateLogsChart(range) { if (!logsChart) return; let endpoint = ''; switch (range) { case '24h': endpoint = '/hourly-stats'; break; case '7d': endpoint = '/daily-stats'; break; case '30d': endpoint = '/monthly-stats'; break; default: endpoint = '/hourly-stats'; } // 使用封装的apiRequest函数进行API调用 apiRequest(endpoint) .then(data => { if (data && data.error) { console.error('更新日志图表失败:', data.error); return; } // 更新图表数据 logsChart.data.labels = data.labels; logsChart.data.datasets[0].data = data.data; logsChart.update(); }) .catch(error => { console.error('更新日志图表失败:', error); }); } // 建立WebSocket连接 function connectLogsWebSocket() { try { // 构建WebSocket URL const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.host}/ws/stats`; console.log('正在连接WebSocket:', wsUrl); // 创建WebSocket连接 logsWsConnection = new WebSocket(wsUrl); // 连接打开事件 logsWsConnection.onopen = function() { console.log('WebSocket连接已建立'); }; // 接收消息事件 logsWsConnection.onmessage = function(event) { try { const data = JSON.parse(event.data); if (data.type === 'initial_data' || data.type === 'stats_update') { console.log('收到实时数据更新'); // 只更新统计数据,不更新日志详情 updateLogsStatsFromWebSocket(data.data); } } catch (error) { console.error('处理WebSocket消息失败:', error); } }; // 连接关闭事件 logsWsConnection.onclose = function(event) { console.warn('WebSocket连接已关闭,代码:', event.code); logsWsConnection = null; // 设置重连 setupLogsReconnect(); }; // 连接错误事件 logsWsConnection.onerror = function(error) { console.error('WebSocket连接错误:', error); }; } catch (error) { console.error('创建WebSocket连接失败:', error); } } // 设置重连逻辑 function setupLogsReconnect() { if (logsWsReconnectTimer) { return; // 已经有重连计时器在运行 } const reconnectDelay = 5000; // 5秒后重连 console.log(`将在${reconnectDelay}ms后尝试重新连接WebSocket`); logsWsReconnectTimer = setTimeout(() => { connectLogsWebSocket(); }, reconnectDelay); } // 从WebSocket更新日志统计数据 function updateLogsStatsFromWebSocket(stats) { try { // 更新统计卡片 if (stats.dns) { // 适配不同的数据结构 const totalQueries = stats.dns.Queries || 0; const blockedQueries = stats.dns.Blocked || 0; const allowedQueries = stats.dns.Allowed || 0; const errorQueries = stats.dns.Errors || 0; const avgResponseTime = stats.dns.AvgResponseTime || 0; const activeIPs = stats.activeIPs || Object.keys(stats.dns.SourceIPs || {}).length; // 更新统计卡片 document.getElementById('logs-total-queries').textContent = totalQueries; document.getElementById('logs-avg-response-time').textContent = avgResponseTime.toFixed(2) + 'ms'; document.getElementById('logs-active-ips').textContent = activeIPs; // 计算屏蔽率 const blockRate = totalQueries > 0 ? (blockedQueries / totalQueries * 100).toFixed(1) : '0'; document.getElementById('logs-block-rate').textContent = blockRate + '%'; } } catch (error) { console.error('从WebSocket更新日志统计数据失败:', error); } } // 拦截域名 async function blockDomain(domain) { try { console.log(`开始拦截域名: ${domain}`); // 创建拦截规则,使用AdBlock Plus格式 const blockRule = `||${domain}^`; console.log(`创建的拦截规则: ${blockRule}`); // 调用API添加拦截规则 console.log(`调用API添加拦截规则,路径: /shield, 方法: POST`); const response = await apiRequest('/shield', 'POST', { rule: blockRule }); console.log(`API响应:`, response); // 处理不同的响应格式 if (response && (response.success || response.status === 'success')) { // 重新加载日志,显示更新后的状态 loadLogs(); // 刷新规则列表 refreshRulesList(); // 显示成功通知 if (typeof window.showNotification === 'function') { window.showNotification(`已成功拦截域名: ${domain}`, 'success'); } } else { const errorMsg = response ? (response.message || '添加拦截规则失败') : '添加拦截规则失败: 无效的API响应'; console.error(`拦截域名失败: ${errorMsg}`); throw new Error(errorMsg); } } catch (error) { console.error('拦截域名失败:', error); // 显示错误通知 if (typeof window.showNotification === 'function') { window.showNotification(`拦截域名失败: ${error.message}`, 'danger'); } } } // 绑定操作按钮事件 function bindActionButtonsEvents() { // 绑定拦截按钮事件 const blockBtns = document.querySelectorAll('.block-btn'); blockBtns.forEach(btn => { btn.addEventListener('click', async (e) => { e.preventDefault(); const domain = e.currentTarget.dataset.domain; await blockDomain(domain); }); }); // 绑定放行按钮事件 const unblockBtns = document.querySelectorAll('.unblock-btn'); unblockBtns.forEach(btn => { btn.addEventListener('click', async (e) => { e.preventDefault(); const domain = e.currentTarget.dataset.domain; await unblockDomain(domain); }); }); } // 刷新规则列表 async function refreshRulesList() { try { // 调用API重新加载规则 const response = await apiRequest('/shield', 'GET'); if (response) { // 处理规则列表响应 let allRules = []; if (response && typeof response === 'object') { // 合并所有类型的规则到一个数组 if (Array.isArray(response.domainRules)) allRules = allRules.concat(response.domainRules); if (Array.isArray(response.domainExceptions)) allRules = allRules.concat(response.domainExceptions); if (Array.isArray(response.regexRules)) allRules = allRules.concat(response.regexRules); if (Array.isArray(response.regexExceptions)) allRules = allRules.concat(response.regexExceptions); } // 更新规则列表 if (window.rules) { rules = allRules; filteredRules = [...rules]; // 更新规则数量统计 if (window.updateRulesCount && typeof window.updateRulesCount === 'function') { window.updateRulesCount(rules.length); } } } } catch (error) { console.error('刷新规则列表失败:', error); } } // 放行域名 async function unblockDomain(domain) { try { console.log(`开始放行域名: ${domain}`); // 创建放行规则,使用AdBlock Plus格式 const allowRule = `@@||${domain}^`; console.log(`创建的放行规则: ${allowRule}`); // 调用API添加放行规则 console.log(`调用API添加放行规则,路径: /shield, 方法: POST`); const response = await apiRequest('/shield', 'POST', { rule: allowRule }); console.log(`API响应:`, response); // 处理不同的响应格式 if (response && (response.success || response.status === 'success')) { // 重新加载日志,显示更新后的状态 loadLogs(); // 刷新规则列表 refreshRulesList(); // 显示成功通知 if (typeof window.showNotification === 'function') { window.showNotification(`已成功放行域名: ${domain}`, 'success'); } } else { const errorMsg = response ? (response.message || '添加放行规则失败') : '添加放行规则失败: 无效的API响应'; console.error(`放行域名失败: ${errorMsg}`); throw new Error(errorMsg); } } catch (error) { console.error('放行域名失败:', error); // 显示错误通知 if (typeof window.showNotification === 'function') { window.showNotification(`放行域名失败: ${error.message}`, 'danger'); } } } // 定期更新日志统计数据(备用方案) setInterval(() => { // 只有在查询日志页面时才更新 if (window.location.hash === '#logs') { loadLogsStats(); // 不自动更新日志详情,只更新统计数据 } }, 30000); // 每30秒更新一次