Files
myaps_api/static/monitor/js/monitor.js
T

4831 lines
181 KiB
JavaScript

/**
* MyAPS 监控面板 JavaScript
*/
const API_BASE = '/monitor/api';
let resourceChart = null;
let refreshInterval = null;
let originalTitle = document.title;
let titleAlertInterval = null;
// 存储实际错误状态
let actualErrorState = {};
// 存储已确认的错误状态(用于比较状态变化)
let badgeConfirmedHasError = {};
// WebSocket 连接
let ws = null;
let wsReconnectInterval = null;
let wsReconnectAttempts = 0;
const MAX_RECONNECT_ATTEMPTS = 5;
const RECONNECT_DELAY = 3000;
// 超时机制
let lastActivityTime = Date.now();
let inactivityTimeout = null;
const INACTIVITY_TIMEOUT_MS = 180 * 60 * 1000; // 当连续不活动 180分钟时,触发超时
let isInactive = false;
// 数据缓存机制
const CACHE_DURATION = {
environment: 60 * 60 * 1000, // 环境信息缓存1小时
health: 30 * 1000, // 健康状态缓存30秒
resource: 5 * 1000, // 资源信息缓存5秒
time: 60 * 1000 // 时间格式化缓存1分钟
};
const CACHE_LIMIT = {
data: 100, // 数据缓存最大条目数
time: 500, // 时间格式化缓存最大条目数
dom: 200 // DOM元素缓存最大条目数
};
const dataCache = {};
// DOM元素缓存
const domCache = {};
// 时间格式化缓存
const timeCache = {};
// 页面加载时设置默认日期
window.addEventListener('DOMContentLoaded', function() {
const today = new Date().toISOString().split('T')[0];
// 设置接收请求日期选择器的默认值
const datePicker = document.getElementById('api-date-picker');
if (datePicker) {
datePicker.value = today;
}
// 设置发送请求日期选择器的默认值
const outboundDatePicker = document.getElementById('outbound-date-picker');
if (outboundDatePicker) {
outboundDatePicker.value = today;
}
});
// 获取DOM元素,优先从缓存中获取
function getElement(id) {
if (!domCache[id]) {
// 检查DOM缓存大小
if (Object.keys(domCache).length >= CACHE_LIMIT.dom) {
// 移除最早的缓存项
const oldestKey = Object.keys(domCache)[0];
delete domCache[oldestKey];
}
domCache[id] = document.getElementById(id);
}
return domCache[id];
}
// 从localStorage加载已确认的错误状态
function loadBadgeConfirmedHasError() {
try {
const stored = localStorage.getItem('badgeConfirmedHasError');
if (stored) {
badgeConfirmedHasError = JSON.parse(stored);
}
} catch (error) {
console.error('加载已确认错误状态失败:', error);
badgeConfirmedHasError = {};
}
}
// 检查缓存是否有效
function isCacheValid(key) {
const cacheItem = dataCache[key];
if (!cacheItem) return false;
const now = Date.now();
return now - cacheItem.timestamp < CACHE_DURATION[key];
}
// 获取缓存数据
function getCachedData(key) {
if (isCacheValid(key)) {
return dataCache[key].data;
}
return null;
}
// 更新缓存数据
function updateCache(key, data) {
// 检查数据缓存大小
if (Object.keys(dataCache).length >= CACHE_LIMIT.data) {
// 移除最早的缓存项
let oldestKey = null;
let oldestTimestamp = Infinity;
Object.entries(dataCache).forEach(([k, v]) => {
if (v.timestamp < oldestTimestamp) {
oldestTimestamp = v.timestamp;
oldestKey = k;
}
});
if (oldestKey) {
delete dataCache[oldestKey];
}
}
dataCache[key] = {
data: data,
timestamp: Date.now()
};
}
// 清理过期缓存
function cleanupCache() {
const now = Date.now();
// 清理数据缓存
Object.entries(dataCache).forEach(([key, item]) => {
if (now - item.timestamp >= CACHE_DURATION[key] || !CACHE_DURATION[key]) {
delete dataCache[key];
}
});
// 清理时间缓存
if (Object.keys(timeCache).length >= CACHE_LIMIT.time) {
// 保留最近使用的缓存项
const sortedKeys = Object.keys(timeCache).sort((a, b) => {
return timeCache[b].timestamp - timeCache[a].timestamp;
});
const keysToRemove = sortedKeys.slice(CACHE_LIMIT.time);
keysToRemove.forEach(key => {
delete timeCache[key];
});
}
}
// 定期清理缓存
setInterval(cleanupCache, 5 * 60 * 1000); // 每5分钟清理一次
// 节流函数
function throttle(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 防抖函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// 保存已确认的错误状态到localStorage
function saveBadgeConfirmedHasError() {
try {
localStorage.setItem('badgeConfirmedHasError', JSON.stringify(badgeConfirmedHasError));
} catch (error) {
console.error('保存已确认错误状态失败:', error);
}
}
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
// 加载已确认的错误状态
loadBadgeConfirmedHasError();
initResourceChart();
initRedisCharts();
fetchEnvironment();
// 无论当前在哪个页面,都获取一次日志数据来检查未读状态
await fetchLogsPage();
await refreshAll();
startAutoRefresh();
initWebSocket();
startInactivityTimer();
// 设置默认日期为今天
const today = new Date().toISOString().split('T')[0];
const datePicker = document.getElementById('api-date-picker');
if (datePicker) {
datePicker.value = today;
}
// 监听用户活动
window.addEventListener('mousemove', resetInactivityTimer);
window.addEventListener('keydown', resetInactivityTimer);
window.addEventListener('scroll', resetInactivityTimer);
window.addEventListener('click', resetInactivityTimer);
});
// 启动不活动计时器
function startInactivityTimer() {
if (inactivityTimeout) {
clearTimeout(inactivityTimeout);
}
inactivityTimeout = setTimeout(handleInactivityTimeout, INACTIVITY_TIMEOUT_MS);
}
// 重置不活动计时器
function resetInactivityTimer() {
lastActivityTime = Date.now();
if (isInactive) {
// 如果之前处于不活动状态,更新状态但不自动重连
isInactive = false;
console.log('用户活动,更新监控状态但不自动重连');
}
startInactivityTimer();
}
// 处理不活动超时
function handleInactivityTimeout() {
isInactive = true;
console.log('用户长时间不活动,断开 WebSocket 连接并停止数据刷新');
// 断开 WebSocket 连接
closeWebSocket();
// 断开实时日志流连接
disconnectLiveLogStream();
// 停止自动刷新
if (refreshInterval) {
clearInterval(refreshInterval);
refreshInterval = null;
}
// 显示提示信息
const statusIndicator = document.getElementById('status-indicator');
if (statusIndicator) {
statusIndicator.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.paused') : '● 监控已暂停(长时间未活动)';
statusIndicator.className = 'status warning';
}
// 显示超时提示模态框
showInactivityModal();
}
// 显示超时提示模态框
function showInactivityModal() {
const modal = document.getElementById('inactivity-modal');
if (modal) {
modal.style.display = 'block';
}
}
// 隐藏超时提示模态框
function hideInactivityModal() {
const modal = document.getElementById('inactivity-modal');
if (modal) {
modal.style.display = 'none';
}
}
// 恢复监控
function resumeMonitoring() {
console.log('用户手动恢复监控');
// 隐藏模态框
hideInactivityModal();
// 重置不活动状态
isInactive = false;
// 重新建立 WebSocket 连接
initWebSocket();
// 重新建立实时日志流连接
connectLiveLogStream();
// 重新启动自动刷新
startAutoRefresh();
// 立即刷新一次数据
refreshAll();
// 重置不活动计时器
resetInactivityTimer();
}
// WebSocket 心跳相关变量
let wsHeartbeatInterval = null;
const WS_HEARTBEAT_INTERVAL = 30000; // 心跳间隔,30秒
const WS_RECONNECT_BASE_DELAY = 1000; // 基础重连延迟
const WS_MAX_RECONNECT_DELAY = 30000; // 最大重连延迟
// 初始化 WebSocket 连接
function initWebSocket() {
// 如果用户不活动,不建立 WebSocket 连接
if (isInactive) {
console.log('用户不活动,跳过 WebSocket 连接初始化');
return;
}
// 构建 WebSocket URL
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/monitor/ws`;
// 关闭现有连接
if (ws) {
try {
ws.close(1000, typeof i18n !== 'undefined' ? i18n.t('monitor.connection.reconnecting') : '重新连接');
} catch (error) {
console.error('关闭 WebSocket 连接失败:', error);
}
ws = null;
}
// 清除心跳定时器
if (wsHeartbeatInterval) {
clearInterval(wsHeartbeatInterval);
wsHeartbeatInterval = null;
}
// 创建新连接
try {
ws = new WebSocket(wsUrl);
// 连接建立
ws.onopen = function() {
console.log('WebSocket 连接已建立');
wsReconnectAttempts = 0;
if (wsReconnectInterval) {
clearInterval(wsReconnectInterval);
wsReconnectInterval = null;
}
// 启动心跳
startWebSocketHeartbeat();
};
// 接收消息
ws.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
if (data.type === 'monitor_data') {
handleWebSocketData(data.data);
} else if (data.type === 'heartbeat_response') {
// 心跳响应处理
}
} catch (error) {
console.error('解析 WebSocket 消息失败:', error);
}
};
// 连接关闭
ws.onclose = function(event) {
console.log(`WebSocket 连接已关闭: ${event.code} - ${event.reason}`);
// 清除心跳定时器
if (wsHeartbeatInterval) {
clearInterval(wsHeartbeatInterval);
wsHeartbeatInterval = null;
}
// 只有在非手动关闭的情况下才尝试重连
if (event.code !== 1000) {
attemptReconnect();
}
};
// 连接错误
ws.onerror = function(error) {
console.error('WebSocket 错误:', error);
};
} catch (error) {
console.error('创建 WebSocket 连接失败:', error);
attemptReconnect();
}
}
// 启动 WebSocket 心跳
function startWebSocketHeartbeat() {
// 清除现有心跳定时器
if (wsHeartbeatInterval) {
clearInterval(wsHeartbeatInterval);
wsHeartbeatInterval = null;
}
// 设置新的心跳定时器
wsHeartbeatInterval = setInterval(function() {
if (ws && ws.readyState === WebSocket.OPEN) {
try {
ws.send(JSON.stringify({ type: 'heartbeat', timestamp: Date.now() }));
} catch (error) {
console.error('发送 WebSocket 心跳失败:', error);
// 发送失败时尝试重连
attemptReconnect();
}
} else if (ws && ws.readyState === WebSocket.CLOSED) {
// 连接已关闭,尝试重连
attemptReconnect();
}
}, WS_HEARTBEAT_INTERVAL);
}
// 尝试重新连接
function attemptReconnect() {
if (isInactive) {
console.log('用户不活动,跳过 WebSocket 重连');
return;
}
if (wsReconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
console.error('WebSocket 重连失败,已达到最大尝试次数');
if (wsReconnectInterval) {
clearInterval(wsReconnectInterval);
wsReconnectInterval = null;
}
return;
}
wsReconnectAttempts++;
console.log(`尝试重新连接 WebSocket (${wsReconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
if (wsReconnectInterval) {
clearInterval(wsReconnectInterval);
}
const delay = Math.min(
RECONNECT_DELAY * Math.pow(2, wsReconnectAttempts - 1),
30000
);
wsReconnectInterval = setTimeout(() => {
initWebSocket();
}, delay);
}
// 处理 WebSocket 数据
function handleWebSocketData(data) {
// 如果用户不活动,不处理 WebSocket 数据
if (isInactive) {
console.log('用户不活动,跳过 WebSocket 数据处理');
return;
}
// 检查系统状态是否为错误
const statusIndicator = getElement('status-indicator');
const isSystemError = statusIndicator && statusIndicator.classList.contains('error');
// 如果系统状态正常,才处理数据并更新最后更新时间
if (!isSystemError) {
// 更新资源指标
if (data.resource) {
updateResourceDisplay(data.resource);
}
// 更新数据库指标
if (data.database) {
updateDatabaseDisplay(data.database);
}
// 更新调度器指标
if (data.scheduler) {
updateSchedulerDisplay(data.scheduler);
}
// 更新 HTTP 指标
if (data.http) {
updateHTTPDisplay(data.http);
}
// 更新对外 HTTP 指标
if (data.outbound_http) {
// 这里可以添加对外 HTTP 指标的更新逻辑
}
// 更新告警
if (data.alerts) {
updateAlertsDisplay(data.alerts);
}
// 更新日志数据
if (data.logs) {
updateLogsPageDisplay(data.logs);
}
// 更新 API 请求数据
if (data.api_requests) {
updateAPIRequestsDisplay(data.api_requests);
}
// 更新发送请求数据
if (data.outbound_requests) {
updateOutboundRequestsDisplay(data.outbound_requests);
}
// 更新事件辅助模块数据
if (data.event_helpers) {
updateEventHelpersDisplay(data.event_helpers);
}
// 更新 Redis 指标
if (data.redis) {
updateRedisDisplay(data.redis);
}
// 更新最后更新时间
updateLastUpdateTime();
}
// 检查告警条件
checkAlertConditions();
}
// 更新发送请求显示
function updateOutboundRequestsDisplay(data) {
let requests = data.recent_requests || [];
// 计算24小时前的时间戳
const twentyFourHoursAgo = Date.now() / 1000 - (24 * 60 * 60);
// 根据开关状态决定是否过滤本服务的请求,同时过滤最近24小时的请求
const filteredRequests = showInternalRequests ?
requests.filter(request => request.timestamp >= twentyFourHoursAgo) :
requests.filter(request => {
// 检查URL是否包含localhost或127.0.0.1,同时检查时间
return !request.url.includes('localhost') && !request.url.includes('127.0.0.1') && request.timestamp >= twentyFourHoursAgo;
});
updateOutboundRequestsTable(filteredRequests);
// 如果需要标记为已读,则将所有400+请求标记为已读
if (shouldMarkOutboundRequestsAsRead) {
const readStatus = getOutboundRequestReadStatus();
filteredRequests.forEach(req => {
if (req.status_code >= 400) {
const requestId = generateOutboundRequestId(req.timestamp, req.method, req.url, req.status_code);
readStatus.add(requestId);
}
});
saveOutboundRequestReadStatus(readStatus);
shouldMarkOutboundRequestsAsRead = false;
}
// 获取已读状态
const readStatus = getOutboundRequestReadStatus();
// 检查是否有未读的400+请求
const hasUnreadErrors = filteredRequests.some(req => {
if (req.status_code >= 400) {
const requestId = generateOutboundRequestId(req.timestamp, req.method, req.url, req.status_code);
return !readStatus.has(requestId);
}
return false;
});
// 更新发送请求页签的红点角标
const outboundRequestsBadge = document.getElementById('outbound-requests-badge');
if (outboundRequestsBadge) {
outboundRequestsBadge.style.display = hasUnreadErrors ? 'inline-block' : 'none';
}
}
// 关闭 WebSocket 连接
function closeWebSocket() {
if (ws) {
ws.close();
ws = null;
}
if (wsReconnectInterval) {
clearInterval(wsReconnectInterval);
wsReconnectInterval = null;
}
}
// 获取环境变量
async function fetchEnvironment() {
// 检查缓存
const cachedData = getCachedData('environment');
if (cachedData) {
updateTitleWithEnvironment(cachedData);
return;
}
try {
const response = await fetch(`${API_BASE}/env`);
const data = await response.json();
updateCache('environment', data);
updateTitleWithEnvironment(data);
} catch (error) {
console.error('获取环境变量失败:', error);
}
}
// 更新title为环境变量
function updateTitleWithEnvironment(env) {
const projectDir = env.project_dir || 'MyAPI';
const projectJson = env.project_json.split('.')[0] || '';
const panelText = typeof i18n !== 'undefined' ? i18n.t('monitor.panel') : '监控面板';
originalTitle = `${projectDir} ${projectJson} ${panelText}`;
document.title = originalTitle;
}
// 自动刷新
function startAutoRefresh() {
if (refreshInterval) {
clearInterval(refreshInterval);
}
// 使用节流函数限制refreshAll的执行频率
const throttledRefreshAll = throttle(refreshAll, 5000); // 最多每5秒执行一次
refreshInterval = setInterval(throttledRefreshAll, 10000);
}
// 获取 Redis 指标
async function fetchRedis() {
try {
const response = await fetch(`${API_BASE}/overview`);
const data = await response.json();
if (data.redis) {
updateRedisDisplay(data.redis);
}
} catch (error) {
console.error('获取 Redis 指标失败:', error);
}
}
// 刷新所有数据
async function refreshAll() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过数据刷新');
return;
}
try {
// 先检查健康状态
await fetchHealth();
// 检查系统状态是否为错误(只有连接失败才算是系统错误)
const statusIndicator = getElement('status-indicator');
const statusText = statusIndicator ? statusIndicator.textContent : '';
const failedText = typeof i18n !== 'undefined' ? i18n.t('monitor.connection.failed') : '连接失败';
const isSystemError = statusIndicator && statusIndicator.classList.contains('error') && statusText.includes(failedText);
// 如果系统状态不是连接失败,继续获取其他数据
if (!isSystemError) {
// 基础数据,无论哪个页面都需要刷新
await Promise.all([
fetchResource(),
fetchDatabase(),
fetchScheduler(),
fetchHTTP(),
fetchAlerts(),
fetchOverviewOutboundRequests(),
fetchRedis(),
// 无论在哪个页面都刷新日志列表、API 请求记录和发送请求记录
fetchLogsPage(),
fetchAPIRequests(),
fetchOutboundRequests()
]);
// 只刷新当前页面的特定数据
switch (currentPage) {
case 'database':
await Promise.all([
fetchDatabaseDetail(),
fetchEventStats()
]);
break;
case 'scheduler':
await fetchSchedulerPage();
break;
case 'event-helpers':
await Promise.all([
fetchEventStats(),
fetchEventHelpers(),
fetchDeadLetterStats(),
fetchBinlogListenerStatus()
]);
break;
}
updateLastUpdateTime();
}
checkAlertConditions();
} catch (error) {
console.error('刷新数据失败:', error);
}
}
// 检查告警条件并更新title
function checkAlertConditions() {
const hasDbError = checkDatabaseError();
const hasDatabaseOffline = checkDatabaseOffline();
const hasUnreadLogs = checkUnreadLogs();
const hasSystemError = checkSystemStatus();
if (hasDbError || hasUnreadLogs || hasSystemError) {
startTitleAlert(hasSystemError, hasDatabaseOffline);
} else {
stopTitleAlert();
}
}
// 检查系统状态(后端失连)
function checkSystemStatus() {
const statusIndicator = document.getElementById('status-indicator');
return statusIndicator && statusIndicator.classList.contains('error');
}
// 检查数据库错误
function checkDatabaseError() {
// 检查概览页面的数据库状态
const dbBadge = document.getElementById('db-badge');
if (dbBadge && (dbBadge.classList.contains('warning') || dbBadge.classList.contains('error'))) {
return true;
}
// 检查数据库详情页面的状态
const dbDetailBadge = document.getElementById('db-detail-badge');
if (dbDetailBadge && (dbDetailBadge.classList.contains('warning') || dbDetailBadge.classList.contains('error'))) {
return true;
}
return false;
}
// 检查数据库离线
function checkDatabaseOffline() {
// 检查概览页面的数据库状态
const dbBadge = document.getElementById('db-badge');
if (dbBadge && dbBadge.classList.contains('error')) {
return true;
}
// 检查数据库详情页面的状态
const dbDetailBadge = document.getElementById('db-detail-badge');
if (dbDetailBadge && dbDetailBadge.classList.contains('error')) {
return true;
}
return false;
}
// 检查未读日志
function checkUnreadLogs() {
// 检查日志页面的未读状态
const unreadLogs = document.querySelectorAll('#logs-tbody tr.unread');
const hasUnread = unreadLogs.length > 0;
// 更新红点角标的显示
const logsBadge = document.getElementById('logs-badge');
if (logsBadge) {
logsBadge.style.display = hasUnread ? 'inline-block' : 'none';
}
return hasUnread;
}
// 开始title闪烁
function startTitleAlert(hasSystemError, hasDatabaseOffline) {
if (titleAlertInterval) {
clearInterval(titleAlertInterval);
}
let blinkState = 0;
let blinkPattern;
if (hasSystemError || hasDatabaseOffline) {
// 后端失连或数据库离线时的闪烁模式:红色禁止图标 + 完整文字
blinkPattern = [
`${originalTitle}`, // 状态1:红色禁止图标 + 完整文字
``, // 状态2:红色禁止图标 + 无文字
`${originalTitle}`, // 状态3:红色禁止图标 + 完整文字
`` // 状态4:红色禁止图标 + 无文字
];
} else {
// 普通告警时的闪烁模式
blinkPattern = [
`🚨 ${originalTitle}`, // 状态1:红色图标 + 完整文字
`🚨`, // 状态2:红色图标 + 无文字
`⚠️ ${originalTitle}`, // 状态3:黄色图标 + 完整文字
`⚠️` // 状态4:黄色图标 + 无文字
];
}
titleAlertInterval = setInterval(() => {
// 循环切换闪烁状态
blinkState = (blinkState + 1) % blinkPattern.length;
document.title = blinkPattern[blinkState];
// 每完成一轮闪烁(4次)后暂停一下,形成节律
if (blinkState === 0) {
clearInterval(titleAlertInterval);
setTimeout(() => {
const hasDbError = checkDatabaseError();
const hasDatabaseOffline = checkDatabaseOffline();
const hasUnreadLogs = checkUnreadLogs();
const hasSystemError = checkSystemStatus();
if (hasDbError || hasUnreadLogs || hasSystemError) {
startTitleAlert(hasSystemError, hasDatabaseOffline);
}
}, 800); // 暂停时间,调整节律
}
}, 300); // 闪烁速度,调整闪烁频率
}
// 停止title闪烁
function stopTitleAlert() {
if (titleAlertInterval) {
clearInterval(titleAlertInterval);
titleAlertInterval = null;
document.title = originalTitle;
}
}
// 获取健康状态
async function fetchHealth() {
try {
const response = await fetch(`${API_BASE}/health`);
const data = await response.json();
updateHealthStatus(data);
} catch (error) {
console.error('获取健康状态失败:', error);
setSystemStatus('error', typeof i18n !== 'undefined' ? i18n.t('monitor.connection.failed') : '连接失败');
}
}
// 更新健康状态显示
function updateHealthStatus(data) {
const indicator = getElement('status-indicator');
const statusMap = {
'healthy': { text: typeof i18n !== 'undefined' ? i18n.t('monitor.status.healthy') : '● 系统正常', class: 'healthy' },
'degraded': { text: typeof i18n !== 'undefined' ? i18n.t('monitor.status.partial_warnings') : '● 部分警告', class: 'warning' },
'unhealthy': { text: typeof i18n !== 'undefined' ? i18n.t('monitor.status.unhealthy') : '● 系统异常', class: 'error' }
};
const status = statusMap[data.status] || statusMap['unhealthy'];
indicator.textContent = status.text;
indicator.className = `status ${status.class}`;
}
// 设置系统状态
function setSystemStatus(status, text) {
const indicator = getElement('status-indicator');
indicator.textContent = `${text}`;
indicator.className = `status ${status}`;
}
// 获取资源指标
async function fetchResource() {
try {
const [resourceResponse, networkResponse] = await Promise.all([
fetch(`${API_BASE}/resource`),
fetch(`${API_BASE}/network/bandwidth`)
]);
const resourceData = await resourceResponse.json();
const networkData = await networkResponse.json();
// 计算网络带宽(转换为 KB/s)
let networkUpload = 0;
let networkDownload = 0;
if (networkData.total && !networkData.message) {
networkUpload = Math.round(networkData.total.bps_sent / 1024 * 100) / 100;
networkDownload = Math.round(networkData.total.bps_recv / 1024 * 100) / 100;
}
updateResourceDisplay(resourceData, networkUpload, networkDownload);
} catch (error) {
console.error('获取资源指标失败:', error);
}
}
// 更新资源显示
function updateResourceDisplay(data, networkUpload = 0, networkDownload = 0) {
if (data.error) {
const resourceBadge = getElement('resource-badge');
resourceBadge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.failed') : '错误';
resourceBadge.className = 'badge error';
return;
}
// CPU
const cpuValue = data.cpu?.system || 0;
getElement('cpu-value').textContent = `${cpuValue}%`;
const cpuBar = getElement('cpu-bar');
cpuBar.style.width = `${Math.min(cpuValue, 100)}%`;
cpuBar.className = `progress-fill ${getProgressClass(cpuValue)}`;
// 内存
const memValue = data.memory?.rss || 0;
const memPercent = data.memory?.percent || 0;
getElement('memory-percent').textContent = `${memPercent}%`;
getElement('memory-usage').textContent = `${memValue} M`;
const memBar = getElement('memory-bar');
memBar.style.width = `${Math.min(memPercent, 100)}%`;
memBar.className = `progress-fill ${getProgressClass(memPercent)}`;
// 线程数
getElement('threads-value').textContent = data.threads || '--';
// 运行时间
getElement('uptime-value').textContent = formatUptime(data.uptime || 0);
// 更新图表(包含网络带宽)
updateResourceChart(cpuValue, memPercent, networkUpload, networkDownload);
// 更新徽章
const badge = getElement('resource-badge');
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.status.running') : '运行中';
badge.className = 'badge healthy';
}
// 获取进度条颜色类
function getProgressClass(value) {
if (value >= 80) return 'error';
if (value >= 60) return 'warning';
return '';
}
// 存储上一次的告警列表
let previousAlerts = [];
// 格式化运行时间
function formatUptime(seconds) {
// 检查缓存
const cacheKey = `uptime_${seconds}`;
if (timeCache[cacheKey] && Date.now() - timeCache[cacheKey].timestamp < CACHE_DURATION.time) {
return timeCache[cacheKey].value;
}
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
let result;
if (days > 0) {
result = `${days}d ${hours}h`;
} else if (hours > 0) {
result = `${hours}h ${minutes}m`;
} else {
result = `${minutes}m`;
}
// 更新缓存
timeCache[cacheKey] = {
value: result,
timestamp: Date.now()
};
return result;
}
// 初始化资源图表
function initResourceChart() {
const ctx = document.getElementById('resource-chart').getContext('2d');
resourceChart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{
label: typeof i18n !== 'undefined' ? i18n.t('monitor.chart.cpu') : 'CPU',
data: [],
borderColor: '#d39102ff',
backgroundColor: 'rgba(184, 134, 11, 0.1)',
fill: true,
tension: 0.4,
yAxisID: 'y'
},
{
label: typeof i18n !== 'undefined' ? i18n.t('monitor.chart.memory') : '内存',
data: [],
borderColor: '#9932cc',
backgroundColor: 'rgba(153, 50, 204, 0.1)',
fill: true,
tension: 0.4,
yAxisID: 'y'
},
{
label: typeof i18n !== 'undefined' ? i18n.t('monitor.chart.upload') : '上传',
data: [],
borderColor: '#87ceeb',
backgroundColor: 'rgba(135, 206, 235, 0.1)',
fill: true,
tension: 0.4,
yAxisID: 'y1'
},
{
label: typeof i18n !== 'undefined' ? i18n.t('monitor.chart.download') : '下载',
data: [],
borderColor: '#98fb98',
backgroundColor: 'rgba(152, 251, 152, 0.1)',
fill: true,
tension: 0.4,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
beginAtZero: true,
max: 100,
title: {
display: true,
text: typeof i18n !== 'undefined' ? i18n.t('monitor.chart.cpu_memory_axis') : 'CPU / 内存 (%)'
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
beginAtZero: true,
title: {
display: true,
text: typeof i18n !== 'undefined' ? i18n.t('monitor.chart.network_axis') : '网络上传 / 下载 (KB/s)'
},
grid: {
drawOnChartArea: false
}
}
},
plugins: {
legend: {
position: 'top'
}
},
animation: {
duration: 300
}
}
});
}
// 更新资源图表
function updateResourceChart(cpu, memory, networkUpload = 0, networkDownload = 0) {
const now = new Date();
const timeLabel = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
resourceChart.data.labels.push(timeLabel);
resourceChart.data.datasets[0].data.push(cpu);
resourceChart.data.datasets[1].data.push(memory);
resourceChart.data.datasets[2].data.push(networkUpload);
resourceChart.data.datasets[3].data.push(networkDownload);
// 保持最多20个数据点
if (resourceChart.data.labels.length > 20) {
resourceChart.data.labels.shift();
resourceChart.data.datasets[0].data.shift();
resourceChart.data.datasets[1].data.shift();
resourceChart.data.datasets[2].data.shift();
resourceChart.data.datasets[3].data.shift();
}
resourceChart.update('none');
}
// 获取数据库指标
async function fetchDatabase() {
try {
const response = await fetch(`${API_BASE}/database`);
const data = await response.json();
updateDatabaseDisplay(data);
} catch (error) {
console.error('获取数据库指标失败:', error);
}
}
// 更新数据库显示
function updateDatabaseDisplay(data) {
const connections = data.connections || {};
const summary = connections.summary || {};
const mainDb = data.main_db;
// 更新汇总
getElement('db-total').textContent = summary.total || 0;
getElement('db-healthy').textContent = summary.healthy || 0;
getElement('db-unhealthy').textContent = summary.unhealthy || 0;
// 更新徽章
const badge = getElement('db-badge');
if (summary.unhealthy === 0) {
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.healthy') : '正常';
badge.className = 'badge healthy';
} else {
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.unhealthy') : '异常';
badge.className = 'badge warning';
}
// 更新数据库页签的红点角标
const databaseBadge = getElement('database-badge');
if (databaseBadge) {
databaseBadge.style.display = summary.unhealthy > 0 ? 'inline-block' : 'none';
}
// 更新连接列表
const listEl = getElement('db-list');
const dbConnections = connections.connections || {};
if (Object.keys(dbConnections).length === 0) {
listEl.innerHTML = '<div class="empty-state">' + (typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_db_connections') : '暂无数据库连接') + '</div>';
checkAlertConditions();
return;
}
// 使用文档片段进行批量DOM操作
const fragment = document.createDocumentFragment();
Object.entries(dbConnections).forEach(([name, status]) => {
const dbItem = document.createElement('div');
dbItem.className = 'db-item';
dbItem.innerHTML = `
<span class="db-name">${name === mainDb ? 'Ⓜ️ ' + name : name}</span>
<span class="db-status">
<span class="status-dot ${status.healthy ? 'healthy' : 'error'}"></span>
${status.healthy ? (typeof i18n !== 'undefined' ? i18n.t('monitor.metric.healthy') : '正常') : (status.error || (typeof i18n !== 'undefined' ? i18n.t('monitor.metric.unhealthy') : '异常'))}
</span>
`;
fragment.appendChild(dbItem);
});
// 清空并添加新内容
listEl.innerHTML = '';
listEl.appendChild(fragment);
checkAlertConditions();
}
// 获取定时任务指标
async function fetchScheduler() {
try {
const response = await fetch(`${API_BASE}/scheduler`);
const data = await response.json();
updateSchedulerDisplay(data);
} catch (error) {
console.error('获取定时任务指标失败:', error);
}
}
// 更新定时任务显示
function updateSchedulerDisplay(data) {
const scheduler = data.scheduler || {};
const jobs = data.jobs || [];
// 更新调度器状态
const statusEl = getElement('scheduler-status');
const badge = getElement('scheduler-badge');
if (scheduler.running) {
statusEl.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.status.running') : '运行中';
statusEl.style.color = '#52c41a';
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.status.running') : '运行中';
badge.className = 'badge healthy';
} else {
statusEl.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.status.stopped') : '已停止';
statusEl.style.color = '#faad14';
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.status.stopped') : '已停止';
badge.className = 'badge warning';
}
// 更新任务数量
getElement('job-count').textContent = jobs.length;
// 更新任务列表(如果元素存在)
const listEl = getElement('job-list');
if (listEl) {
if (jobs.length === 0) {
listEl.innerHTML = '<div class="empty-state">' + (typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_scheduler') : '暂无定时任务') + '</div>';
return;
}
// 使用文档片段进行批量DOM操作
const fragment = document.createDocumentFragment();
jobs.forEach(job => {
const jobItem = document.createElement('div');
jobItem.className = 'job-item';
jobItem.innerHTML = `
<div class="job-info">
<span class="job-name">${job.name || job.id}</span>
<span class="job-trigger">${job.trigger}</span>
</div>
<span class="job-next">${job.next_run_time ? formatDateTime(job.next_run_time) : (typeof i18n !== 'undefined' ? i18n.t('monitor.scheduler.not_scheduled') : '未计划')}</span>
`;
fragment.appendChild(jobItem);
});
// 清空并添加新内容
listEl.innerHTML = '';
listEl.appendChild(fragment);
}
}
// 格式化日期时间
function formatDateTime(dateValue) {
if (!dateValue) return typeof i18n !== 'undefined' ? i18n.t('monitor.scheduler.never_run') : '从未执行';
// 检查缓存
const cacheKey = `datetime_${dateValue}`;
if (timeCache[cacheKey] && Date.now() - timeCache[cacheKey].timestamp < CACHE_DURATION.time) {
return timeCache[cacheKey].value;
}
let date;
if (typeof dateValue === 'number') {
// 处理时间戳(秒)
date = new Date(dateValue * 1000);
} else if (typeof dateValue === 'string') {
// 处理ISO字符串
date = new Date(dateValue);
} else {
// 处理其他格式
date = new Date(dateValue);
}
if (isNaN(date.getTime())) {
return typeof i18n !== 'undefined' ? i18n.t('monitor.scheduler.never_run') : '从未执行';
}
const result = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
// 更新缓存
timeCache[cacheKey] = {
value: result,
timestamp: Date.now()
};
return result;
}
// 获取告警
async function fetchAlerts() {
try {
const response = await fetch(`${API_BASE}/alerts?limit=10`);
const data = await response.json();
updateAlertsDisplay(data.alerts || []);
} catch (error) {
console.error('获取告警失败:', error);
}
}
// 获取 HTTP 指标
async function fetchHTTP() {
try {
const response = await fetch(`${API_BASE}/http`);
const data = await response.json();
updateHTTPDisplay(data);
} catch (error) {
console.error('获取 HTTP 指标失败:', error);
}
}
// 更新 HTTP 显示
function updateHTTPDisplay(data) {
const recentRequests = data.recent_requests || [];
// 计算24小时前的时间戳
const twentyFourHoursAgo = Date.now() / 1000 - (24 * 60 * 60);
// 过滤最近24小时的请求
const filteredRequests = recentRequests.filter(request => request.timestamp >= twentyFourHoursAgo);
// 重新计算24小时内的汇总数据
const totalRequests = filteredRequests.length;
const errorRequests = filteredRequests.filter(req => req.status_code >= 400).length;
const totalResponseTime = filteredRequests.reduce((sum, req) => sum + req.duration, 0);
const errorRate = totalRequests > 0 ? (errorRequests / totalRequests) * 100 : 0;
const avgResponseTime = totalRequests > 0 ? totalResponseTime / totalRequests : 0;
const summary = {
total_requests: totalRequests,
error_rate: errorRate,
avg_response_time: avgResponseTime,
requests_per_minute: 0
};
// 更新汇总数据
getElement('http-total').textContent = summary.total_requests || 0;
getElement('http-error-rate').textContent = (summary.error_rate || 0).toFixed(2) + '%';
getElement('http-avg-time').textContent = ((summary.avg_response_time || 0) * 1000).toFixed(0) + 'ms';
getElement('http-rpm').textContent = summary.requests_per_minute || 0;
// 更新徽章
const badge = getElement('http-badge');
const errorRateValue = summary.error_rate || 0;
if (errorRateValue < 1) {
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.healthy') : '正常';
badge.className = 'badge healthy';
} else if (errorRateValue < 5) {
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.alert.warning') : '警告';
badge.className = 'badge warning';
} else {
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.unhealthy') : '异常';
badge.className = 'badge error';
}
// 更新接收请求页签的红点角标(仅在异常状态发生变化时显示)
const apiRequestsBadge = getElement('api-requests-badge');
if (apiRequestsBadge) {
const hasErrors = summary.error_rate > 0;
actualErrorState['api-requests'] = hasErrors;
const confirmedHasError = badgeConfirmedHasError['api-requests'];
const errorStateChanged = hasErrors !== confirmedHasError;
apiRequestsBadge.style.display = errorStateChanged ? 'inline-block' : 'none';
// 更新已确认的错误状态,确保下次比较正确
badgeConfirmedHasError['api-requests'] = hasErrors;
// 保存已确认的错误状态到localStorage
saveBadgeConfirmedHasError();
}
// 更新状态码分布(如果元素存在)
const statusCodesEl = getElement('http-status-codes');
if (statusCodesEl) {
const statusCodes = data.status_codes || {};
if (Object.keys(statusCodes).length === 0) {
statusCodesEl.innerHTML = '<div class="no-data">' + (typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_http_requests') : '暂无接收请求') + '</div>';
} else {
statusCodesEl.innerHTML = Object.entries(statusCodes).map(([code, count]) => {
const codeClass = code.startsWith('2') ? 'success' : (code.startsWith('3') ? 'redirect' : 'error');
return `
<div class="status-code-item">
<span class="status-code ${codeClass}">${code}</span>
<span>${count}</span>
</div>
`;
}).join('');
}
}
// 更新路径统计(如果元素存在)
const pathsEl = getElement('http-paths');
if (pathsEl) {
const pathStats = data.path_stats || {};
if (Object.keys(pathStats).length === 0) {
pathsEl.innerHTML = '<div class="no-data">&nbsp;</div>';
} else {
const sortedPaths = Object.entries(pathStats)
.sort((a, b) => b[1].count - a[1].count)
.slice(0, 10);
pathsEl.innerHTML = sortedPaths.map(([path, stats]) => `
<div class="http-path-item">
<div class="http-path-info">
<span class="http-path-name">${path}</span>
<div class="http-path-stats">
<span>${typeof i18n !== 'undefined' ? i18n.t('monitor.metric.avg_time') : '平均'}: ${(stats.avg_time * 1000).toFixed(0)}ms</span>
${stats.errors > 0 ? `<span style="color: var(--error-color)">${typeof i18n !== 'undefined' ? i18n.t('monitor.metric.failed') : '错误'}: ${stats.errors}</span>` : ''}
${stats.slow_requests > 0 ? `<span style="color: var(--warning-color)">${typeof i18n !== 'undefined' ? i18n.t('monitor.metric.requests_slow') : '慢请求'}: ${stats.slow_requests}</span>` : ''}
</div>
</div>
<span class="http-path-count">${stats.count}</span>
</div>
`).join('');
}
}
}
// 更新告警显示
function updateAlertsDisplay(alerts) {
const listEl = getElement('alert-list');
if (alerts.length === 0) {
listEl.innerHTML = '<div class="empty-state">' + (typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_alerts') : '暂无告警') + '</div>';
previousAlerts = [];
return;
}
// 翻译后端告警消息
const translateAlertMessage = (message) => {
if (typeof i18n === 'undefined') return message;
// 告警消息映射表
const alertMessageMap = {
'调度器未运行': 'alert.scheduler_not_running',
'数据库连接失败': 'alert.db_connection_failed',
'Redis连接失败': 'alert.redis_connection_failed',
'事件监听器已停止': 'alert.event_listener_stopped',
'Binlog监听器已停止': 'alert.binlog_listener_stopped',
'错误率过高': 'alert.high_error_rate',
'内存使用警告': 'alert.memory_warning',
'CPU使用警告': 'alert.cpu_warning'
};
const key = alertMessageMap[message];
return key ? (i18n.t(key) || message) : message;
};
// 生成告警类型标识
const getAlertType = (alert) => {
return `${alert.source}_${alert.message.split(':')[0]}`;
};
// 生成告警HTML
const alertElements = alerts.map(alert => {
const alertType = getAlertType(alert);
// 检查是否是更新的告警
const isUpdated = previousAlerts.some(prevAlert => {
return getAlertType(prevAlert) === alertType && prevAlert.timestamp !== alert.timestamp;
});
const updatedClass = isUpdated ? 'alert-updated' : '';
const translatedMessage = translateAlertMessage(alert.message);
return `
<div class="alert-item ${alert.level} ${updatedClass}">
<span class="alert-message">${translatedMessage}</span>
<span class="alert-time">${formatDateTime(alert.timestamp, 'date')}</span>
</div>
`;
}).join('');
listEl.innerHTML = alertElements;
// 为更新的告警添加闪烁效果
const updatedAlerts = listEl.querySelectorAll('.alert-updated');
updatedAlerts.forEach(alertEl => {
// 添加闪烁动画类
alertEl.classList.add('alert-flashing');
// 10秒后移除闪烁效果
setTimeout(() => {
alertEl.classList.remove('alert-flashing');
}, 10000);
});
// 更新上一次的告警列表
previousAlerts = [...alerts];
}
// 统一的日期时间格式化函数
function formatDateTime(timestamp, format = 'relative') {
// 检查缓存
const cacheKey = `datetime_${timestamp}_${format}`;
if (timeCache[cacheKey] && Date.now() - timeCache[cacheKey].timestamp < CACHE_DURATION.time) {
return timeCache[cacheKey].value;
}
const now = new Date();
let date;
let timestampSec;
if (typeof timestamp === 'string') {
// 处理ISO字符串格式
date = new Date(timestamp);
timestampSec = date.getTime() / 1000;
} else {
// 处理数字时间戳(秒)
date = new Date(timestamp * 1000);
timestampSec = timestamp;
}
const diff = now.getTime() / 1000 - timestampSec;
let result;
switch (format) {
case 'relative':
// 相对时间格式:刚刚、X分钟前、X小时前、X天前
if (diff < 60) {
result = typeof i18n !== 'undefined' ? i18n.t('monitor.time.just_now') : '刚刚';
} else if (diff < 3600) {
const mins = Math.floor(diff / 60);
result = typeof i18n !== 'undefined' ? i18n.t('monitor.time.minutes_ago').replace('{n}', mins) : mins + '分钟前';
} else if (diff < 86400) {
const hours = Math.floor(diff / 3600);
result = typeof i18n !== 'undefined' ? i18n.t('monitor.time.hours_ago').replace('{n}', hours) : hours + '小时前';
} else {
const days = Math.floor(diff / 86400);
result = typeof i18n !== 'undefined' ? i18n.t('monitor.time.days_ago').replace('{n}', days) : days + '天前';
}
break;
case 'date':
// 日期格式:今天 HH:MM、昨天 HH:MM、前天 HH:MM、MM-DD
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const dayBeforeYesterday = new Date(today);
dayBeforeYesterday.setDate(dayBeforeYesterday.getDate() - 2);
const logDate = new Date(date.getFullYear(), date.getMonth(), date.getDate());
let dayLabel = '';
if (logDate.getTime() === today.getTime()) {
dayLabel = typeof i18n !== 'undefined' ? i18n.t('monitor.date.today') : '今天';
} else if (logDate.getTime() === yesterday.getTime()) {
dayLabel = typeof i18n !== 'undefined' ? i18n.t('monitor.date.yesterday') : '昨天';
} else if (logDate.getTime() === dayBeforeYesterday.getTime()) {
dayLabel = typeof i18n !== 'undefined' ? i18n.t('monitor.time.day_before_yesterday') : '前天';
} else {
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
dayLabel = `${month}-${day}`;
}
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
result = `${dayLabel} ${hours}:${minutes}`;
break;
case 'datetime':
// 完整日期时间格式:今天 HH:MM:SS、昨天 HH:MM:SS、前天 HH:MM:SS、MM-DD HH:MM:SS
const todayFull = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterdayFull = new Date(todayFull);
yesterdayFull.setDate(yesterdayFull.getDate() - 1);
const dayBeforeYesterdayFull = new Date(todayFull);
dayBeforeYesterdayFull.setDate(dayBeforeYesterdayFull.getDate() - 2);
const logDateFull = new Date(date.getFullYear(), date.getMonth(), date.getDate());
let dayLabelFull = '';
if (logDateFull.getTime() === todayFull.getTime()) {
dayLabelFull = typeof i18n !== 'undefined' ? i18n.t('monitor.date.today') : '今天';
} else if (logDateFull.getTime() === yesterdayFull.getTime()) {
dayLabelFull = typeof i18n !== 'undefined' ? i18n.t('monitor.date.yesterday') : '昨天';
} else if (logDateFull.getTime() === dayBeforeYesterdayFull.getTime()) {
dayLabelFull = typeof i18n !== 'undefined' ? i18n.t('monitor.time.day_before_yesterday') : '前天';
} else {
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
dayLabelFull = `${month}-${day}`;
}
const hoursFull = date.getHours().toString().padStart(2, '0');
const minutesFull = date.getMinutes().toString().padStart(2, '0');
const secondsFull = date.getSeconds().toString().padStart(2, '0');
result = `${dayLabelFull} ${hoursFull}:${minutesFull}:${secondsFull}`;
break;
default:
// 默认格式:YYYY-MM-DD HH:MM
const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hoursDefault = date.getHours().toString().padStart(2, '0');
const minutesDefault = date.getMinutes().toString().padStart(2, '0');
result = `${year}-${month}-${day} ${hoursDefault}:${minutesDefault}`;
}
// 更新缓存
timeCache[cacheKey] = {
value: result,
timestamp: Date.now()
};
return result;
}
// 获取状态码描述
function getStatusDescription(statusCode) {
const statusDescriptions = {
200: 'OK',
201: 'Created',
202: 'Accepted',
204: 'No Content',
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable'
};
return statusDescriptions[statusCode] || '';
}
// 清空告警
async function clearAlerts() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过告警清空');
return;
}
try {
await fetch(`${API_BASE}/alerts/clear`, { method: 'POST' });
fetchAlerts();
} catch (error) {
console.error('清空告警失败:', error);
}
}
// Redis 图表实例
let redisConnectionsChart = null;
let redisBufferChart = null;
// 初始化 Redis 图表
function initRedisCharts() {
// 初始化连接池使用情况图表
const connectionsCtx = document.getElementById('redis-connections-chart');
if (connectionsCtx) {
redisConnectionsChart = new Chart(connectionsCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: typeof i18n !== 'undefined' ? i18n.t('monitor.chart.used_connections') : '使用连接数',
data: [],
borderColor: '#1890ff',
backgroundColor: 'rgba(24, 144, 255, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
},
plugins: {
legend: {
position: 'top'
}
}
}
});
}
// 初始化缓冲大小变化图表
const bufferCtx = document.getElementById('redis-buffer-chart');
if (bufferCtx) {
redisBufferChart = new Chart(bufferCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: typeof i18n !== 'undefined' ? i18n.t('monitor.chart.buffer_size_mb') : '缓冲大小 (MB)',
data: [],
borderColor: '#52c41a',
backgroundColor: 'rgba(82, 196, 26, 0.1)',
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
},
plugins: {
legend: {
position: 'top'
}
}
}
});
}
}
// 更新 Redis 监控显示
function updateRedisDisplay(data) {
// 更新状态指示器
const statusIndicator = document.getElementById('redis-status');
const redisBadge = document.getElementById('redis-badge');
if (data.healthy) {
statusIndicator.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.healthy') : '健康';
statusIndicator.className = 'status healthy';
redisBadge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.healthy') : '正常';
redisBadge.className = 'badge healthy';
} else {
statusIndicator.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.unhealthy') : '异常';
statusIndicator.className = 'status error';
redisBadge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.unhealthy') : '异常';
redisBadge.className = 'badge error';
}
// 更新主机信息
document.getElementById('redis-host').textContent = data.host || '-';
document.getElementById('redis-port').textContent = data.port || '-';
document.getElementById('redis-db').textContent = data.db || '-';
// 更新连接池信息
document.getElementById('redis-connections-used').textContent = data.connections_used || 0;
document.getElementById('redis-connections-max').textContent = data.connections_max || 0;
document.getElementById('redis-connection-usage').textContent = (data.connection_usage || 0).toFixed(2) + '%';
// 更新缓冲信息
document.getElementById('redis-buffer-size').textContent = formatBytes(data.buffer_size || 0);
document.getElementById('redis-buffer-threshold').textContent = formatBytes(data.buffer_threshold || 0);
document.getElementById('redis-buffer-usage').textContent = (data.buffer_usage || 0).toFixed(2) + '%';
// 更新图表
updateRedisCharts(data);
}
// 更新 Redis 图表
function updateRedisCharts(data) {
const now = new Date();
const timeLabel = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
// 更新连接池使用情况图表
if (redisConnectionsChart) {
redisConnectionsChart.data.labels.push(timeLabel);
redisConnectionsChart.data.datasets[0].data.push(data.connections_used || 0);
// 保持最多20个数据点
if (redisConnectionsChart.data.labels.length > 20) {
redisConnectionsChart.data.labels.shift();
redisConnectionsChart.data.datasets[0].data.shift();
}
redisConnectionsChart.update('none');
}
// 更新缓冲大小变化图表
if (redisBufferChart) {
// 将字节转换为MB
const bufferSizeMB = (data.buffer_size || 0) / (1024 * 1024);
redisBufferChart.data.labels.push(timeLabel);
redisBufferChart.data.datasets[0].data.push(bufferSizeMB);
// 保持最多20个数据点
if (redisBufferChart.data.labels.length > 20) {
redisBufferChart.data.labels.shift();
redisBufferChart.data.datasets[0].data.shift();
}
redisBufferChart.update('none');
}
}
// 格式化字节大小
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 获取日志
async function fetchLogs() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过日志数据获取');
return;
}
try {
const level = document.getElementById('log-level').value;
const url = `${API_BASE}/logs?limit=20${level ? `&level=${level}` : ''}`;
const response = await fetch(url);
const data = await response.json();
updateLogsDisplay(data.logs);
} catch (error) {
console.error('获取日志失败:', error);
}
}
// 更新日志显示
function updateLogsDisplay(logs) {
const listEl = getElement('log-list');
if (logs.length === 0) {
listEl.innerHTML = '<div class="empty-state">' + (typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_logs') : '暂无日志') + '</div>';
checkAlertConditions();
return;
}
listEl.innerHTML = logs.map(log => `
<div class="log-item ${log.level}">
<div class="log-header">
<div class="log-level ${log.level}">${log.level === 'warning' ? (typeof i18n !== 'undefined' ? i18n.t('monitor.alert.warning') : '警告') : (typeof i18n !== 'undefined' ? i18n.t('monitor.alert.error') : '错误')}</div>
<div class="log-info">
<span class="log-module">${log.module}</span>
<span class="log-time">${formatDateTime(log.timestamp, 'date')}</span>
</div>
</div>
<div class="log-message">${log.message}</div>
${log.traceback ? `<div class="log-traceback">${log.traceback}</div>` : ''}
</div>
`).join('');
checkAlertConditions();
}
// 更新最后更新时间
function updateLastUpdateTime() {
const lastUpdateEl = getElement('last-update');
const now = new Date();
const timeStr = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
// 添加动画效果
lastUpdateEl.classList.add('update-flashing');
// 更新时间
const lastUpdateText = typeof i18n !== 'undefined' ? i18n.t('monitor.last_update') : '最后更新';
lastUpdateEl.textContent = `${lastUpdateText} ${timeStr}`;
// 1秒后移除动画效果
setTimeout(() => {
lastUpdateEl.classList.remove('update-flashing');
}, 1000);
}
// 页面切换逻辑
let currentPage = 'overview';
// 标记是否需要将请求标记为已读
let shouldMarkAPIRequestsAsRead = false;
let shouldMarkOutboundRequestsAsRead = false;
document.addEventListener('DOMContentLoaded', () => {
initNavigation();
});
function initNavigation() {
const navItems = document.querySelectorAll('.nav-item');
navItems.forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
const page = item.getAttribute('data-page');
switchPage(page);
});
});
}
function switchPage(pageName) {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过页面切换时的数据刷新');
return;
}
// 处理接收请求和发送请求的特殊逻辑,即使是当前页面也需要执行
if (pageName === 'api-requests') {
shouldMarkAPIRequestsAsRead = true;
fetchAPIRequests();
} else if (pageName === 'outbound-requests') {
shouldMarkOutboundRequestsAsRead = true;
fetchOutboundRequests();
}
// 如果是当前页面,只处理数据刷新和角标逻辑,不切换页面显示
if (currentPage === pageName) return;
currentPage = pageName;
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
if (item.getAttribute('data-page') === pageName) {
item.classList.add('active');
}
});
document.querySelectorAll('.page-content').forEach(page => {
page.classList.remove('active');
});
const targetPage = document.getElementById(`page-${pageName}`);
if (targetPage) {
targetPage.classList.add('active');
}
// 处理其他页面的逻辑
if (pageName === 'database') {
fetchDatabaseDetail();
} else if (pageName === 'scheduler') {
fetchSchedulerPage();
} else if (pageName === 'event-helpers') {
fetchEventStats();
fetchEventHelpers();
fetchDeadLetterStats();
fetchBinlogListenerStatus();
} else if (pageName === 'logs') {
fetchLogsPage();
}
}
// 获取事件统计数据
async function fetchEventStats() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过事件统计数据获取');
return;
}
try {
const response = await fetch(`${API_BASE}/events`);
const data = await response.json();
updateEventStatsDisplay(data);
} catch (error) {
console.error('获取事件统计失败:', error);
}
}
// 获取binlog listener状态
async function fetchBinlogListenerStatus() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过binlog listener状态获取');
return;
}
try {
const response = await fetch(`${API_BASE}/binlog-listener`);
const data = await response.json();
updateBinlogListenerDisplay(data);
} catch (error) {
console.error('获取binlog listener状态失败:', error);
}
}
// 获取事件辅助模块数据
async function fetchEventHelpers() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过事件辅助模块数据获取');
return;
}
try {
const response = await fetch(`${API_BASE}/event-helpers`);
const data = await response.json();
updateEventHelpersDisplay(data);
} catch (error) {
console.error('获取事件辅助模块数据失败:', error);
}
}
// 更新事件统计显示
function updateEventStatsDisplay(data) {
const eventStats = data.event_stats || {};
const summary = data.summary || {};
document.getElementById('events-total-received').textContent = summary.total_received || 0;
document.getElementById('events-total-processed').textContent = summary.total_processed || 0;
document.getElementById('events-total-failed').textContent = summary.total_failed || 0;
document.getElementById('events-total-pending').textContent = summary.total_pending || 0;
document.getElementById('events-success-rate').textContent = (summary.overall_success_rate || 0).toFixed(1) + '%';
document.getElementById('events-active-types').textContent = `${summary.active_event_types || 0} / ${summary.total_event_types || 0}`;
const tbodyEl = document.getElementById('events-tbody');
if (Object.keys(eventStats).length === 0) {
tbodyEl.innerHTML = '<tr><td colspan="11" class="empty-state">' + (typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_events') : '暂无事件统计') + '</td></tr>';
return;
}
tbodyEl.innerHTML = Object.entries(eventStats).map(([eventType, stats]) => {
const status = getEventStatus(stats);
const normalText = typeof i18n !== 'undefined' ? i18n.t('monitor.alert.normal') : '正常';
const warningText = typeof i18n !== 'undefined' ? i18n.t('monitor.alert.warning') : '警告';
const statusClass = status === normalText ? 'healthy' : status === warningText ? 'warning' : 'error';
let lastActivity = '-';
if (stats.last_activity_time) {
lastActivity = formatDateTime(stats.last_activity_time, 'relative');
}
const successRate = stats.success_rate || 0;
const avgLatency = stats.avg_processing_latency || 0;
return `
<tr>
<td>${stats.description || eventType}</td>
<td>${stats.total_received || 0}</td>
<td>${stats.pending_count || 0}</td>
<td>${stats.total_processed || 0}</td>
<td>${stats.total_failed || 0}</td>
<td>${successRate.toFixed(1)}%</td>
<td>${avgLatency.toFixed(1)}ms</td>
<td>${stats.current_buffer_size || 0} / ${stats.batch_size || 0}</td>
<td>${lastActivity}</td>
<td><span class="badge ${statusClass}">${status}</span></td>
<td>
<button class="btn btn-small" onclick="flushEvent('${eventType}')">${typeof i18n !== 'undefined' ? i18n.t('monitor.btn.refresh') : '刷新'}</button>
</td>
</tr>
`;
}).join('');
}
// 更新binlog listener状态显示
function updateBinlogListenerDisplay(data) {
const listener = data.binlog_listener || {};
// 显示背压监控信息
document.getElementById('backpressure-summary').style.display = 'block';
document.getElementById('backpressure-pending').style.display = 'block';
document.getElementById('backpressure-percent').style.display = 'block';
document.getElementById('event-loop-status').style.display = 'block';
const pendingCount = listener.pending_events || 0;
const threshold = listener.backpressure_threshold || 10000;
const percent = listener.backpressure_percent || 0;
document.getElementById('backpressure-pending-count').textContent = pendingCount;
document.getElementById('backpressure-usage').textContent = percent.toFixed(1) + '%';
// 根据背压状态设置颜色
let statusText = typeof i18n !== 'undefined' ? i18n.t('monitor.alert.normal') : '正常';
let statusClass = 'healthy';
if (percent >= 100) {
statusText = typeof i18n !== 'undefined' ? i18n.t('monitor.alert.critical') : '严重';
statusClass = 'error';
} else if (percent >= 75) {
statusText = typeof i18n !== 'undefined' ? i18n.t('monitor.alert.warning') : '警告';
statusClass = 'warning';
}
const statusEl = document.getElementById('backpressure-status');
statusEl.textContent = statusText;
statusEl.className = 'summary-value ' + statusClass;
// 更新事件循环健康状态
const eventLoopHealthy = listener.event_loop_healthy;
const eventLoopEl = document.getElementById('event-loop-health');
if (eventLoopHealthy === true) {
eventLoopEl.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.healthy') : '正常';
eventLoopEl.className = 'summary-value healthy';
} else if (eventLoopHealthy === false) {
eventLoopEl.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.unhealthy') : '异常';
eventLoopEl.className = 'summary-value error';
} else {
eventLoopEl.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.unknown') : '未知';
eventLoopEl.className = 'summary-value';
}
}
// 获取事件状态
function getEventStatus(stats) {
const pending = stats.pending_count || 0;
const batchSize = stats.batch_size || 10000;
const successRate = stats.success_rate || 100;
if (pending >= batchSize * 5 || successRate < 80) {
return typeof i18n !== 'undefined' ? i18n.t('monitor.metric.unhealthy') : '异常';
} else if (pending >= batchSize * 2 || successRate < 95) {
return typeof i18n !== 'undefined' ? i18n.t('monitor.metric.warning') : '警告';
}
return typeof i18n !== 'undefined' ? i18n.t('monitor.metric.healthy') : '正常';
}
// 刷新事件统计
function refreshEventStats() {
fetchEventStats();
}
// 刷新所有事件
async function flushAllEvents() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过事件刷新');
return;
}
try {
await fetch(`${API_BASE}/events/flush`, { method: 'POST' });
fetchEventStats();
} catch (error) {
console.error('刷新事件失败:', error);
}
}
// 刷新指定事件
async function flushEvent(eventType) {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过指定事件刷新');
return;
}
try {
await fetch(`${API_BASE}/events/flush?event_type=${encodeURIComponent(eventType)}`, { method: 'POST' });
fetchEventStats();
} catch (error) {
console.error('刷新事件失败:', error);
}
}
// 重置事件统计
async function resetEventStats() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过事件统计重置');
return;
}
const confirmMsg = typeof i18n !== 'undefined' ? i18n.t('monitor.reset_stats_confirm') : '确定要重置所有事件统计吗?';
if (!confirm(confirmMsg)) {
return;
}
try {
await fetch(`${API_BASE}/events/reset-stats`, { method: 'POST' });
fetchEventStats();
} catch (error) {
console.error('重置事件统计失败:', error);
}
}
// 数据库详情页面
async function fetchDatabaseDetail() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过数据库详情数据获取');
return;
}
try {
const response = await fetch(`${API_BASE}/database`);
const data = await response.json();
updateDatabaseDetailDisplay(data);
} catch (error) {
console.error('获取数据库详情失败:', error);
}
}
function updateDatabaseDetailDisplay(data) {
const tbodyEl = document.getElementById('db-detail-tbody');
const connections = data.connections || {};
const dbConnections = connections.connections || {};
const pools = data.pool?.pools || {};
const mainDb = data.main_db;
if (Object.keys(dbConnections).length === 0) {
tbodyEl.innerHTML = '<tr><td colspan="11" class="empty-state">' + (typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_db_connections') : '暂无数据库连接') + '</td></tr>';
checkAlertConditions();
return;
}
const badge = document.getElementById('db-detail-badge');
const summary = connections.summary || {};
if (summary.unhealthy === 0) {
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.healthy') : '正常';
badge.className = 'badge healthy';
} else {
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.unhealthy') : '异常';
badge.className = 'badge error';
}
// 更新数据库页签的红点角标
const databaseBadge = document.getElementById('database-badge');
if (databaseBadge) {
databaseBadge.style.display = summary.unhealthy > 0 ? 'inline-block' : 'none';
}
tbodyEl.innerHTML = Object.entries(dbConnections).map(([name, status]) => {
const pool = pools[name] || {};
const stats = pool.stats || {};
let connectionUsagePercent = 0;
if (pool.used_connections && pool.max_size) {
connectionUsagePercent = Math.round((pool.used_connections / pool.max_size) * 100);
}
let lastCheckTime = '-';
if (status.last_check) {
const checkDate = new Date(status.last_check * 1000);
lastCheckTime = `${checkDate.getHours().toString().padStart(2, '0')}:${checkDate.getMinutes().toString().padStart(2, '0')}:${checkDate.getSeconds().toString().padStart(2, '0')}`;
}
return `
<tr>
<td>${name === mainDb ? 'Ⓜ️ ' + name : name}</td>
<td class="status-${status.healthy ? 'healthy' : 'error'}">
${status.healthy ? (typeof i18n !== 'undefined' ? i18n.t('monitor.connection.connected') : '已连接') : (status.error || (typeof i18n !== 'undefined' ? i18n.t('monitor.connection.disconnected') : '断开'))}
</td>
<td>${lastCheckTime}</td>
<td>${pool.current_size !== undefined ? pool.current_size : '-'}</td>
<td>${pool.max_size !== undefined ? pool.max_size : '-'}</td>
<td>${pool.min_size !== undefined ? pool.min_size : '-'}</td>
<td>${pool.idle_connections !== undefined ? pool.idle_connections : '-'}</td>
<td>${pool.used_connections !== undefined ? pool.used_connections : '-'}</td>
<td>
${pool.max_size ? `
<div class="progress-bar">
<div class="progress-fill ${connectionUsagePercent >= 80 ? 'error' : connectionUsagePercent >= 60 ? 'warning' : ''}" style="width: ${connectionUsagePercent}%"></div>
</div>
<span style="display: block; margin-top: 4px; font-size: 12px;">${connectionUsagePercent}%</span>
` : '-'}
</td>
<td>${stats.total_processed || 0}</td>
<td>${stats.batches_executed || 0}</td>
</tr>
`;
}).join('');
checkAlertConditions();
}
// API 请求页面
async function fetchAPIRequests() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过 API 请求数据获取');
return;
}
try {
const response = await fetch(`${API_BASE}/http`);
const data = await response.json();
updateAPIRequestsDisplay(data);
} catch (error) {
console.error('获取 API 请求失败:', error);
}
}
function updateAPIRequestsDisplay(data) {
const tbodyEl = document.getElementById('api-requests-tbody');
let requests = data.recent_requests || [];
// 计算24小时前的时间戳
const twentyFourHoursAgo = Date.now() / 1000 - (24 * 60 * 60);
// 根据开关状态决定是否过滤内部请求,同时过滤最近24小时的请求
const filteredRequests = showAPIIntternalRequests ?
requests.filter(request => request.timestamp >= twentyFourHoursAgo) :
requests.filter(request => {
// 过滤掉客户端IP为127.0.0.1、localhost或::1的请求,同时检查时间
return request.client_ip !== '127.0.0.1' && request.client_ip !== 'localhost' && request.client_ip !== '::1' && request.timestamp >= twentyFourHoursAgo;
});
// 根据时间戳倒序排序
filteredRequests.sort((a, b) => b.timestamp - a.timestamp);
// 如果需要标记为已读,则将所有400+请求标记为已读
if (shouldMarkAPIRequestsAsRead) {
const readStatus = getAPIRequestReadStatus();
filteredRequests.forEach(req => {
if (req.status_code >= 400) {
const requestId = generateAPIRequestId(req.timestamp, req.method, req.path, req.status_code);
readStatus.add(requestId);
}
});
saveAPIRequestReadStatus(readStatus);
shouldMarkAPIRequestsAsRead = false;
}
// 获取已读状态
const readStatus = getAPIRequestReadStatus();
// 检查是否有未读的400+请求
const hasUnreadErrors = filteredRequests.some(req => {
if (req.status_code >= 400) {
const requestId = generateAPIRequestId(req.timestamp, req.method, req.path, req.status_code);
return !readStatus.has(requestId);
}
return false;
});
// 更新接收请求页签的红点角标
const apiRequestsBadge = document.getElementById('api-requests-badge');
if (apiRequestsBadge) {
apiRequestsBadge.style.display = hasUnreadErrors ? 'inline-block' : 'none';
}
if (filteredRequests.length === 0) {
tbodyEl.innerHTML = '<tr><td colspan="9" class="empty-state">' + (typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_api_requests') : '暂无 API 请求记录') + '</td></tr>';
return;
}
// 使用文档片段进行批量DOM操作
const fragment = document.createDocumentFragment();
filteredRequests.forEach((req, index) => {
const timeStr = formatDateTime(req.timestamp, 'datetime');
const methodClass = req.method.toLowerCase();
const isSuccess = req.status_code < 400;
const statusClass = isSuccess ? 'success' : 'error';
const errorMsg = req.error_message || '';
// 转义特殊字符,防止HTML属性值被截断
const escapedPath = req.path.replace(/"/g, '&quot;');
const escapedErrorMsg = errorMsg.replace(/"/g, '&quot;');
// 根据响应时间设置样式
const durationMs = req.duration * 1000;
let durationClass = '';
if (durationMs > 1000) {
durationClass = 'duration-slow';
} else if (durationMs > 500) {
durationClass = 'duration-medium';
}
// 处理查询参数显示
let queryParamsDisplay = '';
if (req.query_params) {
try {
const parsedParams = JSON.parse(req.query_params);
if (Object.keys(parsedParams).length > 0) {
queryParamsDisplay = Object.entries(parsedParams)
.map(([key, value]) => `${key}=${value}`)
.join('&');
}
} catch (e) {
queryParamsDisplay = req.query_params;
}
}
const statusDescription = getStatusDescription(req.status_code);
const statusText = statusDescription ? `${req.status_code} ${statusDescription}` : `${req.status_code}`;
const tr = document.createElement('tr');
tr.onclick = () => showRequestDetail(index);
tr.dataset.requestIndex = index;
tr.innerHTML = `
<td class="font-mono">${index + 1}</td>
<td>${timeStr}</td>
<td><span class="api-method ${methodClass}">${req.method}</span></td>
<td class="font-mono" style="font-size: 12px;">${req.path}</td>
<td class="font-mono" style="font-size: 12px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${queryParamsDisplay}">${queryParamsDisplay || '-'}</td>
<td><span class="api-status ${statusClass}">${statusText}</span></td>
<td class="${durationClass}">${durationMs.toFixed(0)}ms</td>
<td>${req.client_ip}</td>
<td class="error-message-cell" title="${escapedErrorMsg}">${errorMsg}</td>
`;
fragment.appendChild(tr);
});
// 清空并添加新内容
tbodyEl.innerHTML = '';
tbodyEl.appendChild(fragment);
// 存储过滤和排序后的请求数据,以便点击时使用,限制最近100条
const filteredAndSortedRequests = filteredRequests.slice(0, 100);
window.apiRequestsData = {
...data,
recent_requests: filteredAndSortedRequests
};
}
// 显示请求详情
function showRequestDetail(index) {
const data = window.apiRequestsData;
if (!data || !data.recent_requests || !data.recent_requests[index]) {
console.error('请求数据不存在');
return;
}
const req = data.recent_requests[index];
const modalEl = document.getElementById('api-request-modal');
const requestInfoEl = document.getElementById('api-detail-request-info');
const requestBodyEl = document.getElementById('api-detail-request-body');
const responseBodyEl = document.getElementById('api-detail-response-body');
// 格式化时间
const date = new Date(req.timestamp * 1000);
const timeStr = `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,'0')}-${date.getDate().toString().padStart(2,'0')} ${date.getHours().toString().padStart(2,'0')}:${date.getMinutes().toString().padStart(2,'0')}:${date.getSeconds().toString().padStart(2,'0')}`;
// 处理查询参数显示
let queryParamsDisplay = '';
if (req.query_params) {
try {
const parsedParams = JSON.parse(req.query_params);
if (Object.keys(parsedParams).length > 0) {
queryParamsDisplay = Object.entries(parsedParams)
.map(([key, value]) => `${key}=${value}`)
.join('&');
}
} catch (e) {
queryParamsDisplay = req.query_params;
}
}
const statusDescription = getStatusDescription(req.status_code);
const statusText = statusDescription ? `${req.status_code} ${statusDescription}` : `${req.status_code}`;
// 更新请求信息
requestInfoEl.innerHTML = `
<div class="api-detail-info-item">
<span class="api-detail-info-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.col.timestamp') : '时间戳'}</span>
<span class="api-detail-info-value">${timeStr}</span>
</div>
<div class="api-detail-info-item">
<span class="api-detail-info-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.col.method') : '方法'}</span>
<span class="api-detail-info-value">${req.method}</span>
</div>
<div class="api-detail-info-item">
<span class="api-detail-info-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.col.path') : '路径'}</span>
<span class="api-detail-info-value">${req.path}</span>
</div>
<div class="api-detail-info-item">
<span class="api-detail-info-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.col.query_params') : '查询参数'}</span>
<span class="api-detail-info-value font-mono" style="font-size: 12px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${queryParamsDisplay}">${queryParamsDisplay || '-'}</span>
</div>
<div class="api-detail-info-item">
<span class="api-detail-info-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.col.status') : '状态码'}</span>
<span class="api-detail-info-value">${statusText}</span>
</div>
<div class="api-detail-info-item">
<span class="api-detail-info-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.col.duration') : '响应时间'}</span>
<span class="api-detail-info-value">${(req.duration * 1000).toFixed(0)}ms</span>
</div>
<div class="api-detail-info-item">
<span class="api-detail-info-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.col.client_ip') : '客户端IP'}</span>
<span class="api-detail-info-value">${req.client_ip}</span>
</div>
${req.error_message ? `
<div class="api-detail-info-item">
<span class="api-detail-info-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.col.error_message') : '错误信息'}</span>
<span class="api-detail-info-value" style="color: var(--error-color)">${req.error_message}</span>
</div>
` : ''}
`;
// 更新请求体
const requestBodyContent = formatBodyForDisplay(req.request_body);
requestBodyEl.textContent = requestBodyContent.display;
// 只存储请求ID,用于复制时查找原始数据
requestBodyEl.dataset.requestIndex = String(index);
// 更新响应体
const responseBodyContent = formatBodyForDisplay(req.response_body);
responseBodyEl.textContent = responseBodyContent.display;
// 只存储请求ID,用于复制时查找原始数据
responseBodyEl.dataset.requestIndex = String(index);
// 更新数据量警告
updateBodySizeWarning('request', requestBodyContent.length);
updateBodySizeWarning('response', responseBodyContent.length);
// 重置高亮按钮状态
document.querySelectorAll('.section-actions button').forEach(btn => {
const highlightOn = typeof i18n !== 'undefined' ? i18n.t('monitor.highlight.on') : '已高亮';
const highlightOff = typeof i18n !== 'undefined' ? i18n.t('monitor.highlight.off') : '高亮';
if (btn.textContent === highlightOn) {
btn.textContent = highlightOff;
}
});
// 显示模态对话框
modalEl.style.display = 'flex';
// 阻止背景滚动
document.body.style.overflow = 'hidden';
}
// 隐藏请求详情
function hideRequestDetail() {
const modalEl = document.getElementById('api-request-modal');
modalEl.style.display = 'none';
// 恢复背景滚动
document.body.style.overflow = '';
// 清理请求体和响应体内容,避免内存泄漏
const requestBodyEl = document.getElementById('api-detail-request-body');
const responseBodyEl = document.getElementById('api-detail-response-body');
if (requestBodyEl) {
requestBodyEl.textContent = '';
}
if (responseBodyEl) {
responseBodyEl.textContent = '';
}
// 重置高亮按钮状态
document.querySelectorAll('.section-actions button').forEach(btn => {
const highlightOn = typeof i18n !== 'undefined' ? i18n.t('monitor.highlight.on') : '已高亮';
const highlightOff = typeof i18n !== 'undefined' ? i18n.t('monitor.highlight.off') : '高亮';
if (btn.textContent === highlightOn) {
btn.textContent = highlightOff;
}
});
}
// 高亮请求体
function highlightRequestBody() {
const requestBodyEl = document.getElementById('api-detail-request-body');
if (typeof Prism !== 'undefined' && requestBodyEl) {
// 保存原始内容
const originalContent = requestBodyEl.textContent;
// 清理旧的高亮内容
requestBodyEl.innerHTML = originalContent;
// 重新高亮
Prism.highlightElement(requestBodyEl);
// 更新按钮状态
const btn = document.querySelector('.section-actions button:nth-child(1)');
if (btn) {
btn.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.highlight.on') : '已高亮';
}
}
}
// 高亮响应体
function highlightResponseBody() {
const responseBodyEl = document.getElementById('api-detail-response-body');
if (typeof Prism !== 'undefined' && responseBodyEl) {
// 保存原始内容
const originalContent = responseBodyEl.textContent;
// 清理旧的高亮内容
responseBodyEl.innerHTML = originalContent;
// 重新高亮
Prism.highlightElement(responseBodyEl);
// 更新按钮状态
const btn = document.querySelectorAll('.section-actions')[1].querySelector('button:nth-child(1)');
if (btn) {
btn.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.highlight.on') : '已高亮';
}
}
}
// 复制请求体到剪贴板
// 通用复制到剪贴板函数(支持降级方案)
function copyToClipboard(text) {
// 优先使用现代 Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text);
}
// 降级方案:使用 textarea + execCommand
return new Promise((resolve, reject) => {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
// 选择文本
textarea.select();
textarea.setSelectionRange(0, textarea.value.length); // 兼容移动设备
try {
const successful = document.execCommand('copy');
if (successful) {
resolve();
} else {
reject(new Error(typeof i18n !== 'undefined' ? i18n.t('monitor.copy.error') : '复制失败'));
}
} catch (err) {
reject(err);
} finally {
document.body.removeChild(textarea);
}
});
}
// 复制请求体到剪贴板
function copyRequestBody() {
const requestBodyEl = document.getElementById('api-detail-request-body');
if (!requestBodyEl) return;
// 从原始数据源获取完整内容
const requestIndex = parseInt(requestBodyEl.dataset.requestIndex);
let content = '';
if (!isNaN(requestIndex) && window.apiRequestsData && window.apiRequestsData.recent_requests && window.apiRequestsData.recent_requests[requestIndex]) {
// 从原始数据获取完整内容
content = formatBody(window.apiRequestsData.recent_requests[requestIndex].request_body);
}
// 如果无法从原始数据获取,使用显示的内容作为降级方案
if (!content || content.trim() === '') {
content = requestBodyEl.textContent;
}
// 如果高亮后textContent为空,尝试获取innerText
if (!content || content.trim() === '') {
content = requestBodyEl.innerText;
}
copyToClipboard(content)
.then(() => {
// 显示复制成功提示
const sectionActions = requestBodyEl.closest('.api-detail-section').querySelector('.section-actions');
const btn = sectionActions ? sectionActions.querySelector('button:nth-child(2)') : null;
if (btn) {
const originalText = btn.textContent;
btn.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.copy.success') : '已复制';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
}
})
.catch(err => {
console.error('复制失败:', err);
// 显示复制失败提示
alert(typeof i18n !== 'undefined' ? i18n.t('monitor.copy.failed') : '复制失败,请手动复制');
});
}
// 复制响应体到剪贴板
function copyResponseBody() {
const responseBodyEl = document.getElementById('api-detail-response-body');
if (!responseBodyEl) return;
// 从原始数据源获取完整内容
const requestIndex = parseInt(responseBodyEl.dataset.requestIndex);
let content = '';
if (!isNaN(requestIndex) && window.apiRequestsData && window.apiRequestsData.recent_requests && window.apiRequestsData.recent_requests[requestIndex]) {
// 从原始数据获取完整内容
content = formatBody(window.apiRequestsData.recent_requests[requestIndex].response_body);
}
// 如果无法从原始数据获取,使用显示的内容作为降级方案
if (!content || content.trim() === '') {
content = responseBodyEl.textContent;
}
// 如果高亮后textContent为空,尝试获取innerText
if (!content || content.trim() === '') {
content = responseBodyEl.innerText;
}
copyToClipboard(content)
.then(() => {
// 显示复制成功提示
const sectionActions = responseBodyEl.closest('.api-detail-section').querySelector('.section-actions');
const btn = sectionActions ? sectionActions.querySelector('button:nth-child(2)') : null;
if (btn) {
const originalText = btn.textContent;
btn.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.copy.success') : '已复制';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
}
})
.catch(err => {
console.error('复制失败:', err);
// 显示复制失败提示
alert(typeof i18n !== 'undefined' ? i18n.t('monitor.copy.failed') : '复制失败,请手动复制');
});
}
function refreshAPIRequests() {
fetchAPIRequests();
}
async function resetAPIStats() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过 API 统计重置');
return;
}
try {
await fetch(`${API_BASE}/http/reset`, { method: 'POST' });
fetchAPIRequests();
} catch (error) {
console.error('重置 API 统计失败:', error);
}
}
// 按日期获取请求记录
async function fetchRequestsByDate() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过按日期获取请求记录');
return;
}
const datePicker = document.getElementById('api-date-picker');
const date = datePicker.value;
if (!date) {
alert(typeof i18n !== 'undefined' ? i18n.t('monitor.prompt.select_date') : '请选择日期');
return;
}
try {
const response = await fetch(`${API_BASE}/http/requests?date=${date}`);
const data = await response.json();
updateAPIRequestsTable(data.requests);
// 更新页面标题,显示当前查询的日期
const pageTitle = document.querySelector('#page-api-requests h2');
if (pageTitle) {
const titleText = typeof i18n !== 'undefined' ? i18n.t('monitor.page.http_requests_log') : '接收请求记录';
pageTitle.textContent = `${titleText} (${date})`;
}
} catch (error) {
console.error('按日期获取请求记录失败:', error);
alert(typeof i18n !== 'undefined' ? i18n.t('monitor.prompt.fetch_failed') : '获取请求记录失败,请稍后重试');
}
}
function updateAPIRequestsTable(requests) {
const tableBody = document.getElementById('api-requests-tbody');
if (!tableBody) return;
// 限制存储最近100条数据
const limitedRequests = requests.slice(0, 100);
if (limitedRequests.length === 0) {
const emptyText = typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_api_requests') : '暂无 API 请求记录';
tableBody.innerHTML = '<tr><td colspan="9" class="empty-state">' + emptyText + '</td></tr>';
return;
}
// 使用文档片段减少DOM操作次数
const fragment = document.createDocumentFragment();
limitedRequests.forEach((req, index) => {
const timeStr = formatDateTime(req.timestamp, 'datetime');
const methodClass = req.method.toLowerCase();
const isSuccess = req.status_code < 400;
const statusClass = isSuccess ? 'success' : 'error';
const errorMsg = req.error_message || '';
// 转义特殊字符,防止HTML属性值被截断
const escapedPath = req.path.replace(/"/g, '&quot;');
const escapedErrorMsg = errorMsg.replace(/"/g, '&quot;');
// 根据响应时间设置样式
const durationMs = req.response_time;
let durationClass = '';
if (durationMs > 1000) {
durationClass = 'duration-slow';
} else if (durationMs > 500) {
durationClass = 'duration-medium';
}
// 处理查询参数显示
let queryParamsDisplay = '';
if (req.query_params) {
try {
const parsedParams = JSON.parse(req.query_params);
if (Object.keys(parsedParams).length > 0) {
queryParamsDisplay = Object.entries(parsedParams)
.map(([key, value]) => `${key}=${value}`)
.join('&');
}
} catch (e) {
queryParamsDisplay = req.query_params;
}
}
const statusDescription = getStatusDescription(req.status_code);
const statusText = statusDescription ? `${req.status_code} ${statusDescription}` : `${req.status_code}`;
const tr = document.createElement('tr');
tr.onclick = () => {
// 存储当前请求数据,以便点击时使用
window.apiRequestsData = {
recent_requests: limitedRequests
};
showRequestDetail(index);
};
tr.dataset.requestIndex = index;
tr.innerHTML = `
<td class="font-mono">${index + 1}</td>
<td>${timeStr}</td>
<td><span class="api-method ${methodClass}">${req.method}</span></td>
<td class="font-mono" style="font-size: 12px;">${req.path}</td>
<td class="font-mono" style="font-size: 12px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${queryParamsDisplay}">${queryParamsDisplay || '-'}</td>
<td><span class="api-status ${statusClass}">${statusText}</span></td>
<td class="${durationClass}">${durationMs.toFixed(0)}ms</td>
<td>${req.client_ip}</td>
<td class="error-message-cell" title="${escapedErrorMsg}">${errorMsg}</td>
`;
fragment.appendChild(tr);
});
tableBody.innerHTML = '';
tableBody.appendChild(fragment);
}
// 按日期获取对外请求记录
async function fetchOutboundRequestsByDate() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过按日期获取对外请求记录');
return;
}
const datePicker = document.getElementById('outbound-date-picker');
const date = datePicker.value;
if (!date) {
alert(typeof i18n !== 'undefined' ? i18n.t('monitor.prompt.select_date') : '请选择日期');
return;
}
try {
const response = await fetch(`${API_BASE}/outbound-http/requests?date=${date}`);
const data = await response.json();
updateOutboundRequestsTable(data.requests);
// 更新页面标题,显示当前查询的日期
const pageTitle = document.querySelector('#page-outbound-requests h2');
if (pageTitle) {
const titleText = typeof i18n !== 'undefined' ? i18n.t('monitor.page.outbound_requests_log') : '发送请求记录';
pageTitle.textContent = `${titleText} (${date})`;
}
} catch (error) {
console.error('按日期获取对外请求记录失败:', error);
alert(typeof i18n !== 'undefined' ? i18n.t('monitor.prompt.fetch_outbound_failed') : '获取对外请求记录失败,请稍后重试');
}
}
// 日志页面
async function fetchLogsPage() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过日志数据获取');
return;
}
try {
const level = document.getElementById('log-page-level').value;
const url = `${API_BASE}/logs?limit=100${level ? `&level=${level}` : ''}`;
const response = await fetch(url);
const data = await response.json();
// 确保data.logs存在
updateLogsPageDisplay(data.logs || []);
} catch (error) {
console.error('获取日志失败:', error);
// 发生错误时显示空日志
updateLogsPageDisplay([]);
}
}
function updateLogsPageDisplay(logs) {
const tbodyEl = document.getElementById('logs-tbody');
if (logs.length === 0) {
tbodyEl.innerHTML = '<tr><td colspan="5" class="empty-state">' + (typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_logs') : '暂无日志记录') + '</td></tr>';
checkAlertConditions();
return;
}
// 从 localStorage 获取已读状态
const readStatus = getReadStatusFromStorage();
// 过滤掉非 warning 和 error 级别的日志
const warningErrorLogs = logs.filter(log => log.level === 'warning' || log.level === 'error');
// 根据开关状态过滤日志
const filteredLogs = showReadLogs ? warningErrorLogs : warningErrorLogs.filter(log => {
const logId = generateLogId(log.timestamp, log.module, log.message);
return !readStatus.has(logId);
});
if (filteredLogs.length === 0) {
tbodyEl.innerHTML = '<tr><td colspan="5" class="empty-state">' + (typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_logs') : '暂无日志记录') + '</td></tr>';
checkAlertConditions();
return;
}
tbodyEl.innerHTML = filteredLogs.map((log, index) => {
const timeStr = formatDateTime(log.timestamp, 'datetime');
// 确保 title 属性使用完整的消息内容
const fullMessage = log.message || '';
// 生成唯一日志 ID
const logId = generateLogId(log.timestamp, log.module, log.message);
// 检查是否已读
const isRead = readStatus.has(logId);
const readStatusClass = isRead ? 'read' : 'unread';
const readIcon = isRead ? '✓' : '';
// 转义消息中的特殊字符,防止HTML属性值被截断
const escapedFullMessage = fullMessage
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// 移除模块名称中的"smart_"前缀
const moduleName = log.module.replace(/^smart_/, '');
return `
<tr data-log-id="${logId}" data-log-index="${index}" class="${readStatusClass}" onclick="showLogDetail(${index})">
<td class="font-mono">${index + 1}</td>
<td>${timeStr}</td>
<td><span class="log-level-badge ${log.level}">${log.level.toUpperCase()}</span></td>
<td>${moduleName}</td>
<td class="log-message-cell" title="${escapedFullMessage}" data-full-message="${escapedFullMessage}">${escapeHtml(log.message || '')}</td>
<td class="read-status-cell">
<span class="read-checkbox ${readStatusClass}" onclick="toggleLogReadStatus('${logId}'); event.stopPropagation();">${readIcon}</span>
</td>
</tr>
`;
}).join('');
// 保存日志数据,以便点击时使用
window.logsData = filteredLogs;
checkAlertConditions();
checkUnreadLogs();
// 悬停效果由全局事件委托处理
}
// 生成日志唯一 ID
function generateLogId(timestamp, module, message) {
const str = `${timestamp}:${module}:${message}`;
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16);
}
// 从 localStorage 获取已读状态
function getReadStatusFromStorage() {
try {
const stored = localStorage.getItem('monitor_log_read_status');
return stored ? new Set(JSON.parse(stored)) : new Set();
} catch (e) {
console.error('读取已读状态失败:', e);
return new Set();
}
}
// 保存已读状态到 localStorage
function saveReadStatusToStorage(readStatus) {
try {
localStorage.setItem('monitor_log_read_status', JSON.stringify(Array.from(readStatus)));
} catch (e) {
console.error('保存已读状态失败:', e);
}
}
// 生成接收请求唯一 ID
function generateAPIRequestId(timestamp, method, path, statusCode) {
const str = `${timestamp}:${method}:${path}:${statusCode}`;
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16);
}
// 生成发送请求唯一 ID
function generateOutboundRequestId(timestamp, method, url, statusCode) {
const str = `${timestamp}:${method}:${url}:${statusCode}`;
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
return Math.abs(hash).toString(16);
}
// 从 localStorage 获取接收请求已读状态
function getAPIRequestReadStatus() {
try {
const stored = localStorage.getItem('monitor_api_request_read_status');
return stored ? new Set(JSON.parse(stored)) : new Set();
} catch (e) {
console.error('读取接收请求已读状态失败:', e);
return new Set();
}
}
// 保存接收请求已读状态到 localStorage
function saveAPIRequestReadStatus(readStatus) {
try {
localStorage.setItem('monitor_api_request_read_status', JSON.stringify(Array.from(readStatus)));
} catch (e) {
console.error('保存接收请求已读状态失败:', e);
}
}
// 从 localStorage 获取发送请求已读状态
function getOutboundRequestReadStatus() {
try {
const stored = localStorage.getItem('monitor_outbound_request_read_status');
return stored ? new Set(JSON.parse(stored)) : new Set();
} catch (e) {
console.error('读取发送请求已读状态失败:', e);
return new Set();
}
}
// 保存发送请求已读状态到 localStorage
function saveOutboundRequestReadStatus(readStatus) {
try {
localStorage.setItem('monitor_outbound_request_read_status', JSON.stringify(Array.from(readStatus)));
} catch (e) {
console.error('保存发送请求已读状态失败:', e);
}
}
// 切换日志已读状态
function toggleLogReadStatus(logId) {
const readStatus = getReadStatusFromStorage();
if (readStatus.has(logId)) {
readStatus.delete(logId);
} else {
readStatus.add(logId);
}
saveReadStatusToStorage(readStatus);
// 更新显示
const row = document.querySelector(`tr[data-log-id="${logId}"]`);
if (row) {
const checkbox = row.querySelector('.read-checkbox');
const isRead = readStatus.has(logId);
if (isRead) {
row.classList.remove('unread');
row.classList.add('read');
checkbox.classList.remove('unread');
checkbox.classList.add('read');
checkbox.textContent = '✓';
} else {
row.classList.remove('read');
row.classList.add('unread');
checkbox.classList.remove('read');
checkbox.classList.add('unread');
checkbox.textContent = '';
}
}
checkAlertConditions();
checkUnreadLogs();
}
// 初始化悬停效果
function initHoverEffect() {
// 创建全局悬停元素
const tooltip = document.createElement('div');
tooltip.className = 'custom-tooltip';
tooltip.style.position = 'absolute';
tooltip.style.backgroundColor = '#f0f0f0'; // 烟灰底色
tooltip.style.color = '#333';
tooltip.style.padding = '10px 15px';
tooltip.style.borderRadius = '8px'; // 圆角矩形框
tooltip.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
tooltip.style.zIndex = '1000';
tooltip.style.maxWidth = '500px';
tooltip.style.wordBreak = 'break-word';
tooltip.style.whiteSpace = 'pre-wrap'; // 保留空白字符和换行
tooltip.style.display = 'none';
document.body.appendChild(tooltip);
// 保存事件处理函数的引用
const mouseEnterHandler = (e) => {
if (e.target && typeof e.target.closest === 'function') {
const cell = e.target.closest('.log-message-cell');
if (cell) {
// 获取完整的消息内容
const fullMessage = cell.getAttribute('data-full-message');
if (fullMessage !== null && fullMessage !== undefined) {
// 移除默认的 title 行为
cell.removeAttribute('title');
// 显示悬停框
const rect = cell.getBoundingClientRect();
tooltip.style.left = `${rect.left}px`;
tooltip.style.top = `${rect.bottom + 10}px`;
tooltip.textContent = fullMessage || '';
tooltip.style.display = 'block';
}
}
}
};
const mouseLeaveHandler = (e) => {
if (e.target && typeof e.target.closest === 'function') {
const cell = e.target.closest('.log-message-cell');
if (cell) {
tooltip.style.display = 'none';
}
}
};
const mouseMoveHandler = (e) => {
if (e.target && typeof e.target.closest === 'function') {
const cell = e.target.closest('.log-message-cell');
if (!cell) {
tooltip.style.display = 'none';
}
} else {
// 如果 e.target 不支持 closest 方法,直接隐藏悬停框
tooltip.style.display = 'none';
}
};
// 添加事件监听器
document.addEventListener('mouseenter', mouseEnterHandler);
document.addEventListener('mouseleave', mouseLeaveHandler);
document.addEventListener('mousemove', mouseMoveHandler);
// 页面卸载时移除事件监听器
window.addEventListener('beforeunload', function() {
document.removeEventListener('mouseenter', mouseEnterHandler);
document.removeEventListener('mouseleave', mouseLeaveHandler);
document.removeEventListener('mousemove', mouseMoveHandler);
// 移除悬停元素
if (tooltip && tooltip.parentNode) {
tooltip.parentNode.removeChild(tooltip);
}
});
}
// 批量标记已读
function markAllLogsAsRead() {
const logRows = document.querySelectorAll('#logs-tbody tr[data-log-id]');
const logIds = Array.from(logRows).map(row => row.getAttribute('data-log-id'));
if (logIds.length === 0) {
return;
}
const readStatus = getReadStatusFromStorage();
logIds.forEach(id => readStatus.add(id));
saveReadStatusToStorage(readStatus);
// 刷新日志列表
fetchLogsPage();
}
// 清空已读状态
function clearReadStatus() {
try {
localStorage.removeItem('monitor_log_read_status');
// 刷新日志列表
fetchLogsPage();
} catch (e) {
console.error('清空已读状态失败:', e);
}
}
// 页面加载完成后初始化悬停效果
document.addEventListener('DOMContentLoaded', initHoverEffect);
function refreshLogsPage() {
fetchLogsPage();
}
// 定时任务详情页面
async function fetchSchedulerPage() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过定时任务详情数据获取');
return;
}
try {
const response = await fetch(`${API_BASE}/scheduler`);
const data = await response.json();
updateSchedulerDetailDisplay(data);
} catch (error) {
console.error('获取定时任务详情失败:', error);
}
}
function updateSchedulerDetailDisplay(data) {
console.log('定时任务数据:', data);
const gridEl = document.getElementById('scheduler-detail-grid');
const scheduler = data.scheduler || {};
let jobs = data.jobs || [];
// 更新徽章
const badge = document.getElementById('scheduler-detail-badge');
if (scheduler.running) {
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.status.running') : '运行中';
badge.className = 'badge healthy';
} else {
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.status.stopped') : '已停止';
badge.className = 'badge warning';
}
if (jobs.length === 0) {
gridEl.innerHTML = '<div class="empty-state">' + (typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_scheduler') : '暂无定时任务') + '</div>';
return;
}
// 将系统级任务置顶显示
// 项目级任务:PROJECT_DIR 目录下的任务
const envData = getCachedData('environment');
const projectDir = envData ? envData.project_dir : '';
jobs.sort((a, b) => {
const aIsProjectTask = projectDir && (a.name || a.id).includes(`project_files.${projectDir}`);
const bIsProjectTask = projectDir && (b.name || b.id).includes(`project_files.${projectDir}`);
const aIsSystem = !aIsProjectTask;
const bIsSystem = !bIsProjectTask;
if (aIsSystem && !bIsSystem) return -1;
if (!aIsSystem && bIsSystem) return 1;
return 0;
});
gridEl.innerHTML = jobs.map(job => {
const lastRunTime = job.last_run_time ? formatDateTime(job.last_run_time) : (typeof i18n !== 'undefined' ? i18n.t('monitor.scheduler.never_run') : '从未执行');
const nextRunTime = job.next_run_time ? formatDateTime(job.next_run_time) : (typeof i18n !== 'undefined' ? i18n.t('monitor.scheduler.not_scheduled') : '未计划');
const maxExecutionTime = job.max_execution_time ? `${job.max_execution_time.toFixed(2)} ` + (typeof i18n !== 'undefined' ? i18n.t('monitor.other.seconds') : '秒') : (typeof i18n !== 'undefined' ? i18n.t('monitor.scheduler.default') : '默认');
const envData = getCachedData('environment');
const projectDir = envData ? envData.project_dir : '';
const isProjectTask = projectDir && (job.name || job.id).includes(`project_files.${projectDir}`);
const isSystemTask = !isProjectTask;
// 解析下次执行时间为日期和时间部分
let timeStr = typeof i18n !== 'undefined' ? i18n.t('monitor.scheduler.not_scheduled') : '未计划';
let dateStr = '';
if (job.next_run_time) {
let date;
if (typeof job.next_run_time === 'number') {
date = new Date(job.next_run_time * 1000);
} else if (typeof job.next_run_time === 'string') {
date = new Date(job.next_run_time);
} else {
date = new Date(job.next_run_time);
}
if (!isNaN(date.getTime())) {
timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
// 计算日期差值,显示相对时间
const today = new Date();
today.setHours(0, 0, 0, 0);
const jobDate = new Date(date);
jobDate.setHours(0, 0, 0, 0);
const diffTime = jobDate - today;
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
dateStr = typeof i18n !== 'undefined' ? i18n.t('monitor.date.today') : '今天';
} else if (diffDays === 1) {
dateStr = typeof i18n !== 'undefined' ? i18n.t('monitor.time.tomorrow') : '明天';
} else if (diffDays === 2) {
dateStr = typeof i18n !== 'undefined' ? i18n.t('monitor.time.day_after_tomorrow') : '后天';
} else {
dateStr = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
}
}
}
return `
<div class="scheduler-detail-item ${isSystemTask ? 'system-task' : ''}">
<div class="scheduler-time-dot"></div>
<div class="scheduler-time-section">
<span class="scheduler-date">${dateStr}</span>
<span class="scheduler-time">${timeStr}</span>
${job.next_run_time ? `<span class="scheduler-countdown" data-next-run-time="${job.next_run_time}">${formatNextExecutionTime(job.next_run_time)}</span>` : ''}
</div>
<div class="scheduler-content-section">
<div class="scheduler-detail-header">
<a href="#" class="scheduler-detail-name">${job.description || job.name || job.id}</a>
<span class="scheduler-detail-status">
<span class="status-dot healthy"></span>
${(() => {
const taskType = isSystemTask ?
(typeof i18n !== 'undefined' ? i18n.t('monitor.task.system') : '系统') :
(typeof i18n !== 'undefined' ? i18n.t('monitor.task.project') : '项目');
return typeof i18n !== 'undefined' ? taskType : taskType + '级任务';
})()}
</span>
</div>
<table class="scheduler-detail-table">
<tr class="scheduler-detail-row">
<td class="scheduler-detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.scheduler.rule') : '定时规则'}</td>
<td class="scheduler-detail-value">${job.trigger || (typeof i18n !== 'undefined' ? i18n.t('monitor.time.unknown') : '未知')}</td>
</tr>
<tr class="scheduler-detail-row">
<td class="scheduler-detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.scheduler.last_run') : '最近执行'}</td>
<td class="scheduler-detail-value execution-history">
${job.execution_history && job.execution_history.length > 0 ?
job.execution_history.slice().sort((a, b) => new Date(b.time) - new Date(a.time)).map(record => `
<span class="execution-status ${record.error ? 'error' : 'success'}" ${record.error ? `title="${record.error}"` : ''}>
${formatRecentExecutionTime(record.time)}
</span>
`).join('') :
`<span class="execution-status never-executed">${typeof i18n !== 'undefined' ? i18n.t('monitor.scheduler.never_run') : '从未执行'}</span>`
}
</td>
</tr>
<tr class="scheduler-detail-row">
<td class="scheduler-detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.scheduler.max_time') : '最大执行时间'}</td>
<td class="scheduler-detail-value">${maxExecutionTime}</td>
</tr>
${job.execution_time ? `
<tr class="scheduler-detail-row">
<td class="scheduler-detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.task.avg_time') : '平均执行时间'}</td>
<td class="scheduler-detail-value">${(job.execution_time * 1000).toFixed(0)} ms</td>
</tr>
` : ''}
</table>
</div>
</div>
`;
}).join('');
}
function refreshSchedulerPage() {
fetchSchedulerPage();
}
// 格式化最近执行时间(相对时间)
function formatRecentExecutionTime(timestamp) {
if (!timestamp) return typeof i18n !== 'undefined' ? i18n.t('monitor.scheduler.never_run') : '从未执行';
let date;
if (typeof timestamp === 'number') {
date = new Date(timestamp * 1000);
} else if (typeof timestamp === 'string') {
date = new Date(timestamp);
} else {
date = new Date(timestamp);
}
if (isNaN(date.getTime())) {
return typeof i18n !== 'undefined' ? i18n.t('monitor.scheduler.never_run') : '从未执行';
}
// 检查是否在最近24小时内
const now = new Date();
const twentyFourHoursAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
if (date < twentyFourHoursAgo) {
return typeof i18n !== 'undefined' ? i18n.t('monitor.time.over_24h') : '超过24小时';
}
// 计算日期差值
const today = new Date();
today.setHours(0, 0, 0, 0);
const execDate = new Date(date);
execDate.setHours(0, 0, 0, 0);
const diffTime = today - execDate;
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
let dayStr = '';
if (diffDays === 0) {
dayStr = typeof i18n !== 'undefined' ? i18n.t('monitor.date.today') : '今天';
} else if (diffDays === 1) {
dayStr = typeof i18n !== 'undefined' ? i18n.t('monitor.date.yesterday') : '昨天';
} else {
if (typeof i18n !== 'undefined') {
dayStr = i18n.t('monitor.time.month_day').replace('{month}', execDate.getMonth() + 1).replace('{day}', execDate.getDate());
} else {
dayStr = `${execDate.getMonth() + 1}${execDate.getDate()}`;
}
}
const timeStr = `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
return `${dayStr} ${timeStr}`;
}
// 格式化下次执行时间(对于24小时内的任务显示倒计时)
function formatNextExecutionTime(timestamp) {
if (!timestamp) return typeof i18n !== 'undefined' ? i18n.t('monitor.time.unknown') : '未知';
let date;
if (typeof timestamp === 'number') {
date = new Date(timestamp * 1000);
} else if (typeof timestamp === 'string') {
date = new Date(timestamp);
} else {
date = new Date(timestamp);
}
if (isNaN(date.getTime())) {
return typeof i18n !== 'undefined' ? i18n.t('monitor.time.unknown') : '未知';
}
const now = new Date();
const diffMs = date - now;
if (diffMs < 0) {
return typeof i18n !== 'undefined' ? i18n.t('monitor.time.expired') : '已过期';
}
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
if (diffHours < 24) {
// 24小时内,显示实际倒计时
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
return `${diffHours.toString().padStart(2, '0')}:${diffMinutes.toString().padStart(2, '0')}:${diffSeconds.toString().padStart(2, '0')}`;
} else {
// 超过24小时,显示"X天X小时后"
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const remainingHours = diffHours % 24;
if (typeof i18n !== 'undefined') {
return i18n.t('monitor.time.days_hours_later').replace('{days}', diffDays).replace('{hours}', remainingHours);
} else {
return `${diffDays}${remainingHours}小时后`;
}
}
}
// 更新倒计时
function updateCountdowns() {
// 更新时间轴旁边的倒计时
document.querySelectorAll('.scheduler-countdown').forEach(element => {
const nextRunTimeStr = element.getAttribute('data-next-run-time');
if (nextRunTimeStr) {
const nextRunTime = new Date(nextRunTimeStr);
const now = new Date();
const diffMs = nextRunTime - now;
if (diffMs < 0) {
element.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.scheduler.running') : '执行中';
} else {
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
if (diffHours < 24) {
// 24小时内,显示倒计时
const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
const diffSeconds = Math.floor((diffMs % (1000 * 60)) / 1000);
element.textContent = `${diffHours.toString().padStart(2, '0')}:${diffMinutes.toString().padStart(2, '0')}:${diffSeconds.toString().padStart(2, '0')}`;
} else {
// 超过24小时,显示"X天X小时后"
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
const remainingHours = diffHours % 24;
if (typeof i18n !== 'undefined') {
element.textContent = i18n.t('monitor.time.days_hours_later').replace('{days}', diffDays).replace('{hours}', remainingHours);
} else {
element.textContent = `${diffDays}${remainingHours}小时后`;
}
}
}
}
});
}
// 启动倒计时更新
setInterval(updateCountdowns, 1000);
// 开关状态:是否显示内部请求
let showInternalRequests = false;
// 开关状态:是否显示接收请求中的内部请求
let showAPIIntternalRequests = false;
// 开关状态:是否显示已读日志,默认隐藏已读
let showReadLogs = false;
// 存储发送请求数据
let outboundRequestsData = [];
// 获取发送请求数据
async function fetchOutboundRequests() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过发送请求数据获取');
return;
}
try {
const response = await fetch(`${API_BASE}/outbound-http/all`);
const data = await response.json();
// 调试:打印API返回的数据
console.log('API返回的发送请求数据:', data);
// 处理API响应数据,兼容不同的数据结构
const requests = Array.isArray(data) ? data : (data.value || []);
// 计算24小时前的时间戳
const twentyFourHoursAgo = Date.now() / 1000 - (24 * 60 * 60);
// 根据开关状态决定是否过滤本服务的请求,同时过滤最近24小时的请求
const filteredRequests = showInternalRequests ?
requests.filter(request => request.timestamp >= twentyFourHoursAgo) :
requests.filter(request => {
// 检查URL是否包含localhost或127.0.0.1,同时检查时间
return !request.url.includes('localhost') && !request.url.includes('127.0.0.1') && request.timestamp >= twentyFourHoursAgo;
});
// 调试:打印过滤后的数据
console.log('过滤后的发送请求数据:', filteredRequests);
// 按时间戳倒序排序,最新的排在前面
filteredRequests.sort((a, b) => b.timestamp - a.timestamp);
updateOutboundRequestsTable(filteredRequests);
// 如果需要标记为已读,则将所有400+请求标记为已读
if (shouldMarkOutboundRequestsAsRead) {
const readStatus = getOutboundRequestReadStatus();
filteredRequests.forEach(req => {
if (req.status_code >= 400) {
const requestId = generateOutboundRequestId(req.timestamp, req.method, req.url, req.status_code);
readStatus.add(requestId);
}
});
saveOutboundRequestReadStatus(readStatus);
shouldMarkOutboundRequestsAsRead = false;
}
// 获取已读状态
const readStatus = getOutboundRequestReadStatus();
// 检查是否有未读的400+请求
const hasUnreadErrors = filteredRequests.some(req => {
if (req.status_code >= 400) {
const requestId = generateOutboundRequestId(req.timestamp, req.method, req.url, req.status_code);
return !readStatus.has(requestId);
}
return false;
});
// 更新发送请求页签的红点角标
const outboundRequestsBadge = document.getElementById('outbound-requests-badge');
if (outboundRequestsBadge) {
outboundRequestsBadge.style.display = hasUnreadErrors ? 'inline-block' : 'none';
}
} catch (error) {
console.error('获取发送请求数据失败:', error);
}
}
// 切换内部请求显示状态
function toggleInternalRequests() {
const checkbox = document.getElementById('show-internal-requests');
showInternalRequests = checkbox.checked;
fetchOutboundRequests();
}
// 切换接收请求中的内部请求显示状态
function toggleAPIIntternalRequests() {
const checkbox = document.getElementById('show-api-internal-requests');
showAPIIntternalRequests = checkbox.checked;
fetchAPIRequests();
}
// 切换已读日志显示状态
function toggleReadLogs() {
const checkbox = document.getElementById('show-read-logs');
showReadLogs = checkbox.checked;
fetchLogsPage();
}
// 更新发送请求表格
function updateOutboundRequestsTable(requests) {
const tableBody = document.getElementById('outbound-requests-table');
if (!tableBody) return;
// 限制存储最近100条数据
outboundRequestsData = requests.slice(0, 100);
// 调试:打印outboundRequestsData
console.log('outboundRequestsData:', outboundRequestsData);
if (outboundRequestsData.length === 0) {
tableBody.innerHTML = '<tr><td colspan="6" class="empty-state">' + (typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_outbound_requests') : '暂无发送请求记录') + '</td></tr>';
return;
}
// 使用文档片段减少DOM操作次数
const fragment = document.createDocumentFragment();
outboundRequestsData.forEach((request, index) => {
const row = document.createElement('tr');
row.innerHTML = formatOutboundRequestRow(request, index);
row.onclick = () => showOutboundRequestDetail(index);
row.setAttribute('data-request-index', index);
fragment.appendChild(row);
});
tableBody.innerHTML = '';
tableBody.appendChild(fragment);
}
// 获取状态码描述
function getStatusDescription(statusCode) {
const statusDescriptions = {
200: 'OK',
201: 'Created',
202: 'Accepted',
204: 'No Content',
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable'
};
return statusDescriptions[statusCode] || '';
}
// 格式化发送请求行
function formatOutboundRequestRow(request, index) {
const timestamp = formatDateTime(request.timestamp, 'datetime');
const duration = (request.duration * 1000).toFixed(0);
const statusClass = request.status_code >= 400 ? 'error' : 'success';
const durationClass = request.duration > 1 ? 'slow' : '';
// 获取状态码描述
const statusDescription = getStatusDescription(request.status_code);
const statusText = statusDescription ? `${request.status_code} ${statusDescription}` : `${request.status_code}`;
return `
<tr data-request-index="${index}">
<td class="font-mono">${index + 1}</td>
<td>${timestamp}</td>
<td><span class="method ${request.method.toLowerCase()}">${request.method}</span></td>
<td class="url">${request.url}</td>
<td class="status ${statusClass}">${statusText}</td>
<td class="duration ${durationClass}">${duration}ms</td>
<td>${request.module || 'unknown'}</td>
<td class="error-message">${request.error_message || '-'}</td>
</tr>
`;
}
// 显示发送请求详情 - 优化版本
// 常量定义
const MAX_CONTENT_LENGTH = 10000;
const WARNING_THRESHOLD = 5000;
// 单例模态框容器
let outboundRequestModal = null;
let logDetailModal = null;
function showOutboundRequestDetail(index) {
const request = outboundRequestsData[index];
if (!request) {
console.error('请求数据不存在');
return;
}
// 清理之前的模态框(单例模式)
if (outboundRequestModal) {
try {
document.body.removeChild(outboundRequestModal);
} catch (e) {
console.warn('移除旧模态框失败:', e);
}
outboundRequestModal = null;
}
// 创建新模态框
outboundRequestModal = document.createElement('div');
outboundRequestModal.className = 'modal';
outboundRequestModal.id = 'outbound-request-detail-modal';
// 解析可能的JSON字符串字段
let parsedRequestHeaders = request.request_headers;
if (typeof parsedRequestHeaders === 'string') {
try {
parsedRequestHeaders = JSON.parse(parsedRequestHeaders);
} catch (e) {
console.log('解析请求头JSON失败', e);
}
}
let parsedResponseHeaders = request.response_headers;
if (typeof parsedResponseHeaders === 'string') {
try {
parsedResponseHeaders = JSON.parse(parsedResponseHeaders);
} catch (e) {
console.log('解析响应头JSON失败', e);
}
}
// 构建HTML内容
let htmlContent = `
<div class="modal-overlay" onclick="hideOutboundRequestDetail()"></div>
<div class="modal-content">
<div class="modal-header" style="display: flex; justify-content: space-between; align-items: center; padding: 10px 15px;">
<h3 style="margin: 0; font-size: 18px;">${typeof i18n !== 'undefined' ? i18n.t('monitor.modal.outbound_detail') : '发送请求详情'}</h3>
<div class="modal-actions" style="display: flex; align-items: center;">
<button class="export-btn" onclick="exportOutboundRequestDetail(${index})" style="padding: 8px 16px; border: none; border-radius: 8px; background-color: #4CAF50; color: white; font-weight: bold; cursor: pointer; font-size: 14px; transition: all 0.3s ease;">${typeof i18n !== 'undefined' ? i18n.t('monitor.btn.export') : '导出'}</button>
<button class="close-btn" onclick="hideOutboundRequestDetail()" style="padding: 8px 16px; border: none; border-radius: 8px; background-color: #f44336; color: white; cursor: pointer; font-size: 14px; margin-left: 10px;">&times;</button>
</div>
</div>
<div class="modal-body" style="max-height: 70vh; overflow-y: auto;">
<div class="detail-section">
<h4>${typeof i18n !== 'undefined' ? i18n.t('monitor.card.db_status') : '基本信息'}</h4>
<table class="detail-table">
<tbody>
<tr>
<td class="detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.modal.timestamp') : '时间戳:'}</td>
<td class="detail-value">${typeof request.timestamp === 'string' ? request.timestamp : new Date(request.timestamp * 1000).toLocaleString('zh-CN')}</td>
</tr>
<tr>
<td class="detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.modal.method') : '方法:'}</td>
<td class="detail-value">${request.method}</td>
</tr>
<tr>
<td class="detail-label">URL:</td>
<td class="detail-value">${request.url}</td>
</tr>
<tr>
<td class="detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.modal.status_code') : '状态码:'}</td>
<td class="detail-value ${request.status_code >= 400 ? 'error' : 'success'}">${request.status_code}</td>
</tr>
<tr>
<td class="detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.modal.response_time') : '响应时间:'}</td>
<td class="detail-value ${request.duration > 1 ? 'slow' : ''}">${(request.duration * 1000).toFixed(0)}ms</td>
</tr>
<tr>
<td class="detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.modal.module') : '模块:'}</td>
<td class="detail-value">${request.module || 'unknown'}</td>
</tr>`;
// 错误信息部分
if (request.error_message) {
htmlContent += `
<tr>
<td class="detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.modal.error_msg') : '错误信息:'}</td>
<td class="detail-value error">${escapeHtml(request.error_message)}</td>
</tr>`;
}
htmlContent += `
</tbody>
</table>
</div>`;
// 请求头部分
if (parsedRequestHeaders) {
const headersStr = JSON.stringify(parsedRequestHeaders, null, 2);
htmlContent += createSectionWithTruncation(typeof i18n !== 'undefined' ? i18n.t('monitor.section.request_headers') : '请求头', headersStr, headersStr.length);
}
// 请求体部分
if (request.request_body) {
const formattedBody = formatBody(request.request_body);
htmlContent += createSectionWithTruncation(typeof i18n !== 'undefined' ? i18n.t('monitor.section.request_body') : '请求体', formattedBody, formattedBody.length);
}
// 响应头部分
if (parsedResponseHeaders) {
const headersStr = JSON.stringify(parsedResponseHeaders, null, 2);
htmlContent += createSectionWithTruncation(typeof i18n !== 'undefined' ? i18n.t('monitor.section.response_headers') : '响应头', headersStr, headersStr.length);
}
// 响应体部分
let responseBodyContent = typeof i18n !== 'undefined' ? i18n.t('monitor.section.no_response') : '无响应体数据';
let responseBodyLength = 0;
if (request.response_body !== undefined && request.response_body !== null) {
try {
responseBodyContent = formatBody(request.response_body);
responseBodyLength = responseBodyContent.length;
} catch (e) {
const failedPrefix = typeof i18n !== 'undefined' ? i18n.t('monitor.section.response_failed') : '响应体处理失败: ';
responseBodyContent = failedPrefix + e.message;
}
}
htmlContent += createSectionWithTruncation(typeof i18n !== 'undefined' ? i18n.t('monitor.section.response_body') : '响应体', responseBodyContent, responseBodyLength);
htmlContent += `
</div>
</div>`;
outboundRequestModal.innerHTML = htmlContent;
document.body.appendChild(outboundRequestModal);
// 阻止背景滚动
document.body.style.overflow = 'hidden';
// 使用局部语法高亮代替全局高亮
highlightCodeBlocks(outboundRequestModal);
}
// 安全解析JSON
function parseJsonSafely(str) {
if (!str) return null;
if (typeof str !== 'string') return str;
try {
return JSON.parse(str);
} catch (e) {
console.log('解析JSON失败', e);
return str;
}
}
// 格式化时间戳
function formatTimestamp(timestamp) {
if (typeof timestamp === 'string') return timestamp;
return new Date(timestamp * 1000).toLocaleString('zh-CN');
}
// 格式化请求体/响应体
function formatBody(body) {
if (typeof body === 'string') {
try {
const parsed = JSON.parse(body);
return JSON.stringify(parsed, null, 2);
} catch (e) {
return body;
}
}
return JSON.stringify(body, null, 2);
}
// 创建带截断功能的内容区域
function createSectionWithTruncation(title, content, length) {
const isLarge = length > WARNING_THRESHOLD;
const isTruncated = length > MAX_CONTENT_LENGTH;
const truncatedText = typeof i18n !== 'undefined' ? i18n.t('monitor.section.truncated') : '[内容已截断,完整内容请导出查看]';
const displayContent = isTruncated ? content.substring(0, MAX_CONTENT_LENGTH) + '...\n\n' + truncatedText : content;
const warningHtml = isLarge ? `<div style="color: #ff9800; margin-bottom: 8px; font-size: 12px; display: flex; align-items: center;">${typeof i18n !== 'undefined' ? i18n.t('monitor.section.large_data_warning').replace('{count}', length.toLocaleString()) : '⚠️ 数据量较大(' + length.toLocaleString() + '字符),可能影响显示性能'}</div>` : '';
return `
<div class="detail-section" style="margin-top: 15px;">
<h4 style="margin: 0;">${title}</h4>
${warningHtml}
<pre style="max-height: 300px; overflow-y: auto; word-wrap: break-word; white-space: pre-wrap;"><code class="language-json">${escapeHtml(displayContent)}</code></pre>
${isTruncated ? `<div style="text-align: right; margin-top: 4px; font-size: 12px; color: #666;">${typeof i18n !== 'undefined' ? i18n.t('monitor.section.showing_chars').replace('{count}', MAX_CONTENT_LENGTH.toLocaleString()) : '仅显示前' + MAX_CONTENT_LENGTH.toLocaleString() + '字符'}</div>` : ''}
</div>`;
}
// 局部语法高亮
function highlightCodeBlocks(container) {
if (typeof Prism === 'undefined') return;
const codeBlocks = container.querySelectorAll('code.language-json');
codeBlocks.forEach(block => {
try {
Prism.highlightElement(block);
} catch (e) {
console.warn('语法高亮失败:', e);
}
});
}
// HTML转义
function escapeHtml(str) {
if (!str) return '';
return str.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// 隐藏发送请求详情
function hideOutboundRequestDetail() {
if (outboundRequestModal) {
try {
// 清理语法高亮产生的额外元素
const codeBlocks = outboundRequestModal.querySelectorAll('code');
codeBlocks.forEach(block => {
block.innerHTML = block.textContent;
});
document.body.removeChild(outboundRequestModal);
outboundRequestModal = null;
} catch (e) {
console.warn('移除模态框失败:', e);
outboundRequestModal = null;
}
}
// 恢复背景滚动
document.body.style.overflow = '';
}
// 格式化请求体/响应体用于显示(带截断)
function formatBodyForDisplay(body) {
if (!body) {
return { display: typeof i18n !== 'undefined' ? i18n.t('monitor.section.no_data') : '无数据', length: 0 };
}
let formattedBody = body;
if (typeof body === 'string') {
try {
const parsed = JSON.parse(body);
formattedBody = JSON.stringify(parsed, null, 2);
} catch (e) {
// 不是JSON,保持原样
}
} else {
formattedBody = JSON.stringify(body, null, 2);
}
const length = formattedBody.length;
const isTruncated = length > MAX_CONTENT_LENGTH;
const truncatedText = typeof i18n !== 'undefined' ? i18n.t('monitor.section.truncated') : '[内容已截断,完整内容请复制查看]';
const displayContent = isTruncated ? formattedBody.substring(0, MAX_CONTENT_LENGTH) + '...\n\n' + truncatedText : formattedBody;
return { display: displayContent, length: length, truncated: isTruncated };
}
// 更新数据量警告
function updateBodySizeWarning(type, length) {
const warningEl = document.getElementById(`api-detail-${type}-body-warning`);
if (!warningEl) return;
if (length > WARNING_THRESHOLD) {
warningEl.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.section.large_data_warning').replace('{count}', length.toLocaleString()) : '⚠️ 数据量较大(' + length.toLocaleString() + '字符)';
warningEl.style.display = 'block';
} else {
warningEl.style.display = 'none';
}
}
// 导出发送请求详情
function exportOutboundRequestDetail(index) {
const request = outboundRequestsData[index];
// 解析可能的JSON字符串字段
let parsedRequestHeaders = request.request_headers;
if (typeof parsedRequestHeaders === 'string') {
try {
parsedRequestHeaders = JSON.parse(parsedRequestHeaders);
} catch (e) {
console.log('解析请求头JSON失败', e);
}
}
let parsedResponseHeaders = request.response_headers;
if (typeof parsedResponseHeaders === 'string') {
try {
parsedResponseHeaders = JSON.parse(parsedResponseHeaders);
} catch (e) {
console.log('解析响应头JSON失败', e);
}
}
// 格式化请求体
let formattedRequestBody = '';
if (request.request_body) {
formattedRequestBody = request.request_body;
if (typeof request.request_body === 'string') {
try {
formattedRequestBody = JSON.stringify(JSON.parse(request.request_body), null, 2);
} catch (e) {
formattedRequestBody = request.request_body;
}
} else {
formattedRequestBody = JSON.stringify(request.request_body, null, 2);
}
}
// 格式化响应体
let formattedResponseBody = typeof i18n !== 'undefined' ? i18n.t('monitor.section.no_response') : '无响应体数据';
if (request.response_body !== undefined && request.response_body !== null) {
try {
if (typeof request.response_body === 'string') {
try {
// 尝试解析JSON
const parsed = JSON.parse(request.response_body);
formattedResponseBody = JSON.stringify(parsed, null, 2);
} catch (e) {
formattedResponseBody = request.response_body;
}
} else {
formattedResponseBody = JSON.stringify(request.response_body, null, 2);
}
} catch (e) {
const failedPrefix = typeof i18n !== 'undefined' ? i18n.t('monitor.section.response_failed') : '响应体处理失败: ';
formattedResponseBody = failedPrefix + e.message;
}
}
// 处理时间戳
let timestampDisplay = '';
let filenameTimestamp = '';
if (typeof request.timestamp === 'string') {
timestampDisplay = request.timestamp;
filenameTimestamp = request.timestamp.replace(/[: ]/g, '-');
} else {
timestampDisplay = new Date(request.timestamp * 1000).toLocaleString('zh-CN');
filenameTimestamp = new Date(request.timestamp * 1000).toISOString().replace(/[: ]/g, '-');
}
// 构建导出HTML
let html = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>发送请求详情</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.modal-content {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.modal-header h3 {
margin: 0;
color: #333;
}
.detail-section {
margin-bottom: 20px;
}
.detail-section h4 {
margin: 0 0 10px 0;
color: #555;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
}
.detail-table {
width: 100%;
border-collapse: collapse;
}
.detail-table td {
padding: 8px;
border-bottom: 1px solid #eee;
}
.detail-label {
font-weight: bold;
width: 100px;
color: #555;
}
.detail-value {
color: #333;
}
.detail-value.error {
color: #f44336;
}
.detail-value.success {
color: #4CAF50;
}
.detail-value.slow {
color: #ff9800;
}
pre {
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
overflow-x: auto;
margin: 0;
}
code {
font-family: 'Courier New', Courier, monospace;
}
</style>
</head>
<body>
<div class="modal-content">
<div class="modal-header">
<h3>发送请求详情</h3>
</div>
<div class="modal-body">
<div class="detail-section">
<h4>${typeof i18n !== 'undefined' ? i18n.t('monitor.section.basic_info') : '基本信息'}</h4>
<table class="detail-table">
<tbody>
<tr>
<td class="detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.modal.timestamp') : '时间戳:'}</td>
<td class="detail-value">${timestampDisplay}</td>
</tr>
<tr>
<td class="detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.modal.method') : '方法:'}</td>
<td class="detail-value">${request.method}</td>
</tr>
<tr>
<td class="detail-label">URL:</td>
<td class="detail-value">${request.url}</td>
</tr>
<tr>
<td class="detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.modal.status_code') : '状态码:'}</td>
<td class="detail-value ${request.status_code >= 400 ? 'error' : 'success'}">${request.status_code}</td>
</tr>
<tr>
<td class="detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.modal.response_time') : '响应时间:'}</td>
<td class="detail-value ${request.duration > 1 ? 'slow' : ''}">${(request.duration * 1000).toFixed(0)}ms</td>
</tr>`;
if (request.error_message) {
html += `
<tr>
<td class="detail-label">${typeof i18n !== 'undefined' ? i18n.t('monitor.modal.error_msg') : '错误信息:'}</td>
<td class="detail-value error">${request.error_message}</td>
</tr>`;
}
html += `
</tbody>
</table>
</div>`;
if (parsedRequestHeaders) {
html += `
<div class="detail-section">
<h4>请求头</h4>
<pre><code>${JSON.stringify(parsedRequestHeaders, null, 2)}</code></pre>
</div>`;
}
if (formattedRequestBody) {
html += `
<div class="detail-section">
<h4>请求体</h4>
<pre><code>${formattedRequestBody}</code></pre>
</div>`;
}
if (parsedResponseHeaders) {
html += `
<div class="detail-section">
<h4>响应头</h4>
<pre><code>${JSON.stringify(parsedResponseHeaders, null, 2)}</code></pre>
</div>`;
}
if (formattedResponseBody) {
html += `
<div class="detail-section">
<h4>响应体</h4>
<pre><code>${formattedResponseBody}</code></pre>
</div>`;
}
html += `
</div>
</div>
</body>
</html>
`;
// 导出为HTML文件
const blob = new Blob([html], { type: 'text/html' });
const urlBlob = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = urlBlob;
// 获取时间戳作为文件名
a.download = `outbound-request-${filenameTimestamp}.html`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(urlBlob);
}
// 刷新发送请求数据
function refreshOutboundRequests() {
fetchOutboundRequests();
}
// 重置发送请求统计
async function resetOutboundStats() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过发送请求统计重置');
return;
}
try {
const response = await fetch(`${API_BASE}/outbound-http/reset`, { method: 'POST' });
const data = await response.json();
alert(data.message);
fetchOutboundRequests();
} catch (error) {
console.error('重置发送请求统计失败:', error);
alert(typeof i18n !== 'undefined' ? i18n.t('monitor.prompt.reset_failed') : '重置失败,请重试');
}
}
// 添加请求时间戳,用于控制请求频率
const requestTimestamps = new Map();
const REQUEST_INTERVAL = 5000; // 请求间隔,单位毫秒
// 获取并更新overview页面的发送请求数据
async function fetchOverviewOutboundRequests() {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过overview页面发送请求数据获取');
return;
}
// 检查请求频率,避免429错误
const now = Date.now();
const lastRequestTime = requestTimestamps.get('fetchOverviewOutboundRequests');
if (lastRequestTime && now - lastRequestTime < REQUEST_INTERVAL) {
console.log('请求频率过高,跳过overview页面发送请求数据获取');
return;
}
try {
// 记录请求时间
requestTimestamps.set('fetchOverviewOutboundRequests', now);
console.log('开始获取发送请求数据,API路径:', `${API_BASE}/outbound-http/all`);
const response = await fetch(`${API_BASE}/outbound-http/all`);
console.log('获取发送请求数据响应状态:', response.status);
if (!response.ok) {
// 处理429错误
if (response.status === 429) {
console.warn('请求频率过高,服务器返回429错误');
// 使用默认值
const defaultSummary = {
total_requests: 0,
error_rate: 0,
avg_response_time: 0,
requests_per_minute: 0
};
updateOverviewOutboundSummary(defaultSummary);
updateOverviewOutboundList([]);
// 更新状态徽章
const badge = document.getElementById('outbound-badge');
if (badge) {
badge.className = 'badge warning';
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.status.rate_limited') : '限流';
}
return;
}
throw new Error(`HTTP error! status: ${response.status}`);
}
const requests = await response.json();
console.log('获取发送请求数据成功,请求数量:', requests.length);
// 计算24小时前的时间戳
const twentyFourHoursAgo = Date.now() / 1000 - (24 * 60 * 60);
// 过滤内部请求和最近24小时的请求
const baseUrl = 'http://localhost:8000';
const filteredRequests = requests.filter(request => {
return !request.url.startsWith(baseUrl) && request.timestamp >= twentyFourHoursAgo;
});
console.log('过滤后请求数量:', filteredRequests.length);
// 计算统计信息
let summary = {
total_requests: 0,
error_rate: 0,
avg_response_time: 0,
requests_per_minute: 0
};
if (filteredRequests.length > 0) {
const totalRequests = filteredRequests.length;
const errorRequests = filteredRequests.filter(req => req.status_code >= 400).length;
const totalResponseTime = filteredRequests.reduce((sum, req) => sum + req.duration, 0);
summary = {
total_requests: totalRequests,
error_rate: (errorRequests / totalRequests) * 100,
avg_response_time: totalResponseTime / totalRequests,
requests_per_minute: 0
};
}
// 更新overview页面的统计
updateOverviewOutboundSummary(summary);
// 更新overview页面的请求列表(显示最近5条)
const recentRequests = filteredRequests.slice(0, 5);
updateOverviewOutboundList(recentRequests);
// 更新状态徽章
const badge = document.getElementById('outbound-badge');
if (badge) {
if (summary.error_rate > 0) {
badge.className = 'badge error';
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.unhealthy') : '异常';
} else {
badge.className = 'badge healthy';
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.healthy') : '正常';
}
}
// 更新发送请求页签的红点角标(仅在异常状态发生变化时显示)
const outboundRequestsBadge = document.getElementById('outbound-requests-badge');
if (outboundRequestsBadge) {
const hasErrors = summary.error_rate > 0;
actualErrorState['outbound-requests'] = hasErrors;
const confirmedHasError = badgeConfirmedHasError['outbound-requests'];
const errorStateChanged = hasErrors !== confirmedHasError;
outboundRequestsBadge.style.display = errorStateChanged ? 'inline-block' : 'none';
// 更新已确认的错误状态,确保下次比较正确
badgeConfirmedHasError['outbound-requests'] = hasErrors;
// 保存已确认的错误状态到localStorage
saveBadgeConfirmedHasError();
}
} catch (error) {
console.error('获取overview页面发送请求数据失败:', error);
console.error('错误详情:', error.message);
console.error('错误堆栈:', error.stack);
// 发生错误时使用默认值
const defaultSummary = {
total_requests: 0,
error_rate: 0,
avg_response_time: 0,
requests_per_minute: 0
};
updateOverviewOutboundSummary(defaultSummary);
updateOverviewOutboundList([]);
// 更新状态徽章
const badge = document.getElementById('outbound-badge');
if (badge) {
badge.className = 'badge error';
badge.textContent = typeof i18n !== 'undefined' ? i18n.t('monitor.metric.failed') : '错误';
}
}
}
// 更新overview页面的发送请求统计
function updateOverviewOutboundSummary(summary) {
const totalEl = document.getElementById('outbound-total');
const errorRateEl = document.getElementById('outbound-error-rate');
const avgEl = document.getElementById('outbound-avg');
if (totalEl) {
totalEl.textContent = summary.total_requests || 0;
}
if (errorRateEl) {
errorRateEl.textContent = (summary.error_rate || 0).toFixed(2) + '%';
if (summary.error_rate > 0) {
errorRateEl.classList.add('error');
} else {
errorRateEl.classList.remove('error');
}
}
if (avgEl) {
avgEl.textContent = (summary.avg_response_time * 1000).toFixed(0) + 'ms';
}
}
// 更新overview页面的发送请求列表
function updateOverviewOutboundList(requests) {
const listEl = document.getElementById('outbound-list');
if (!listEl) return;
if (requests.length === 0) {
listEl.innerHTML = '<div class="no-data">' + (typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_outbound_requests') : '暂无发送请求') + '</div>';
return;
}
listEl.innerHTML = requests.map(request => {
const timestamp = new Date(request.timestamp * 1000).toLocaleString('zh-CN');
const duration = (request.duration * 1000).toFixed(0);
const statusClass = request.status_code >= 400 ? 'error' : 'success';
return `
<div class="outbound-item">
<div class="outbound-header">
<span class="method ${request.method.toLowerCase()}">${request.method}</span>
<span class="timestamp">${timestamp}</span>
</div>
<div class="outbound-url">${request.url}</div>
<div class="outbound-footer">
<span class="status ${statusClass}">${request.status_code}</span>
<span class="duration">${duration}ms</span>
</div>
</div>
`;
}).join('');
}
// 页面卸载时清理定时器
window.addEventListener('beforeunload', function() {
if (refreshInterval) {
clearInterval(refreshInterval);
}
if (titleAlertInterval) {
clearInterval(titleAlertInterval);
}
});
// 监控内存使用
function monitorMemoryUsage() {
if (performance && performance.memory) {
const memory = performance.memory;
console.log('Memory usage:', {
used: (memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
total: (memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
limit: (memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2) + ' MB'
});
}
}
// 定期监控内存使用
const memoryMonitorInterval = setInterval(monitorMemoryUsage, 60000); // 每分钟检查一次
// 页面卸载时清理内存监控定时器
window.addEventListener('beforeunload', function() {
if (memoryMonitorInterval) {
clearInterval(memoryMonitorInterval);
}
});
// 显示日志详细信息 - 优化版本
async function showLogDetail(index) {
// 如果用户不活动,不发送请求
if (isInactive) {
console.log('用户不活动,跳过日志详情获取');
return;
}
const logs = window.logsData || [];
if (!logs[index]) return;
// 清理之前的模态框(单例模式)
if (logDetailModal) {
try {
document.body.removeChild(logDetailModal);
} catch (e) {
console.warn('移除旧日志详情模态框失败:', e);
}
logDetailModal = null;
}
// 获取完整的日志数据(包含所有级别,包括DEBUG)
let allLogs = [];
try {
const response = await fetch(`${API_BASE}/logs?limit=200`);
const data = await response.json();
// 按时间倒序排列
allLogs = data.logs.sort((a, b) => b.timestamp - a.timestamp);
} catch (error) {
console.error('获取完整日志失败:', error);
allLogs = logs;
}
// 找到当前选中日志在完整数据中的索引
const selectedLog = logs[index];
const selectedLogIndex = allLogs.findIndex(log =>
log.timestamp === selectedLog.timestamp &&
log.module === selectedLog.module &&
log.message === selectedLog.message
);
// 计算前后各10条日志的范围
const startIndex = Math.max(0, selectedLogIndex - 10);
const endIndex = Math.min(allLogs.length - 1, selectedLogIndex + 10);
const surroundingLogs = allLogs.slice(startIndex, endIndex + 1);
// 创建模态框
logDetailModal = document.createElement('div');
logDetailModal.className = 'modal';
logDetailModal.innerHTML = `
<div class="modal-overlay" onclick="hideLogDetail()"></div>
<div class="modal-content">
<div class="modal-header">
<h3>${typeof i18n !== 'undefined' ? i18n.t('monitor.card.scheduler_detail') : '日志详细信息'}</h3>
<div class="modal-header-actions">
<select id="modal-log-level" onchange="filterModalLogs(this.value, ${selectedLogIndex})" style="margin-right: 12px; padding: 4px 8px; border: 1px solid #e8e8e8; border-radius: 4px;">
<option value="">${typeof i18n !== 'undefined' ? i18n.t('monitor.log_level.all_with_debug') : '全部级别 (含DEBUG)'}</option>
<option value="error">${typeof i18n !== 'undefined' ? i18n.t('monitor.log_level.error') : '错误日志'}</option>
<option value="warning">${typeof i18n !== 'undefined' ? i18n.t('monitor.log_level.warning') : '警告日志'}</option>
<option value="info">${typeof i18n !== 'undefined' ? i18n.t('monitor.log_level.info') : '信息日志'}</option>
<option value="debug">${typeof i18n !== 'undefined' ? i18n.t('monitor.log_level.debug') : '调试日志'}</option>
</select>
<button class="close-btn" onclick="hideLogDetail()">&times;</button>
</div>
</div>
<div class="modal-body" style="max-height: 75vh; overflow-y: auto;">
<div class="log-detail-section">
<h4>选中日志</h4>
<div class="log-detail-item selected">
${formatLogDetail(selectedLog, selectedLogIndex, selectedLogIndex)}
</div>
</div>
<div class="log-detail-section">
<h4>前后关联日志</h4>
<div class="surrounding-logs" id="surrounding-logs-container">
${surroundingLogs.map((log, i) => {
const actualIndex = startIndex + i;
const isSelected = actualIndex === selectedLogIndex;
return `
<div class="log-detail-item ${isSelected ? 'selected' : ''}" data-log-level="${log.level}">
${formatLogDetail(log, actualIndex, selectedLogIndex)}
</div>
`;
}).join('')}
</div>
</div>
</div>
</div>
`;
document.body.appendChild(logDetailModal);
// 阻止背景滚动
document.body.style.overflow = 'hidden';
// 使用局部语法高亮
highlightCodeBlocks(logDetailModal);
// 保存完整日志数据到模态框元素上
logDetailModal.dataset.allLogs = JSON.stringify(allLogs);
logDetailModal.dataset.selectedLogIndex = selectedLogIndex;
}
// 筛选模态框中的日志 - 优化版本
function filterModalLogs(level, selectedLogIndex) {
if (!logDetailModal) return;
const allLogs = JSON.parse(logDetailModal.dataset.allLogs);
// 计算前后各10条日志的范围
const startIndex = Math.max(0, selectedLogIndex - 10);
const endIndex = Math.min(allLogs.length - 1, selectedLogIndex + 10);
let surroundingLogs = allLogs.slice(startIndex, endIndex + 1);
// 根据级别筛选
if (level) {
surroundingLogs = surroundingLogs.filter(log => log.level === level);
}
// 更新显示
const container = document.getElementById('surrounding-logs-container');
if (container) {
container.innerHTML = surroundingLogs.map((log, i) => {
const actualIndex = allLogs.findIndex(l =>
l.timestamp === log.timestamp &&
l.module === log.module &&
l.message === log.message
);
const isSelected = actualIndex === selectedLogIndex;
return `
<div class="log-detail-item ${isSelected ? 'selected' : ''}" data-log-level="${log.level}">
${formatLogDetail(log, actualIndex, selectedLogIndex)}
</div>
`;
}).join('');
// 使用局部语法高亮
highlightCodeBlocks(logDetailModal);
}
}
// 隐藏日志详情模态框
function hideLogDetail() {
if (logDetailModal) {
try {
// 清理语法高亮产生的额外元素
const codeBlocks = logDetailModal.querySelectorAll('code');
codeBlocks.forEach(block => {
block.innerHTML = block.textContent;
});
document.body.removeChild(logDetailModal);
logDetailModal = null;
} catch (e) {
console.warn('移除日志详情模态框失败:', e);
logDetailModal = null;
}
}
// 恢复背景滚动
document.body.style.overflow = '';
}
// HTML转义函数,防止XSS攻击
function escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// 格式化日志详细信息
function formatLogDetail(log, index, selectedIndex) {
const date = new Date(log.timestamp * 1000);
const timeStr = `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2,'0')}-${date.getDate().toString().padStart(2,'0')} ${date.getHours().toString().padStart(2,'0')}:${date.getMinutes().toString().padStart(2,'0')}:${date.getSeconds().toString().padStart(2,'0')}.${date.getMilliseconds().toString().padStart(3,'0')}`;
// 计算相对编号:选中的为0,早先为负数,晚为正数
const relativeIndex = selectedIndex !== undefined ? index - selectedIndex : index;
// 转义日志内容,防止HTML/JS注入
const escapedMessage = escapeHtml(log.message || '');
const escapedTraceback = log.traceback ? escapeHtml(log.traceback) : '';
return `
<div class="log-detail-header">
<div class="log-detail-info">
<span class="log-detail-index">#${relativeIndex}</span>
<span class="log-level-badge ${log.level}">${log.level.toUpperCase()}</span>
<span class="log-detail-module">${log.module}</span>
<span class="log-detail-time">${timeStr}</span>
</div>
</div>
<div class="log-detail-message">
<pre><code class="language-text">${escapedMessage}</code></pre>
</div>
${escapedTraceback ? `
<div class="log-detail-traceback">
<h5>堆栈跟踪</h5>
<pre><code class="language-python">${escapedTraceback}</code></pre>
</div>
` : ''}
`;
}
// 刷新DeadLetter队列统计
async function refreshDeadLetterStats() {
try {
const response = await fetch(`${API_BASE}/dead-letter`);
const data = await response.json();
updateDeadLetterDisplay(data);
} catch (error) {
console.error('获取DeadLetter队列数据失败:', error);
}
}
// 清空DeadLetter队列
async function clearDeadLetters() {
try {
const response = await fetch(`${API_BASE}/dead-letter/clear`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
refreshDeadLetterStats();
showNotification(typeof i18n !== 'undefined' ? i18n.t('monitor.success.dl_cleared') : 'DeadLetter队列已清空', 'success');
} else {
showNotification(typeof i18n !== 'undefined' ? i18n.t('monitor.error.clear_dl_failed') : '清空DeadLetter队列失败', 'error');
}
} catch (error) {
console.error('清空DeadLetter队列失败:', error);
showNotification(typeof i18n !== 'undefined' ? i18n.t('monitor.error.clear_dl_failed') : '清空DeadLetter队列失败', 'error');
}
}
// 更新DeadLetter队列显示
function updateDeadLetterDisplay(data) {
const deadLetters = data.dead_letters || [];
const total = deadLetters.length;
const recent = deadLetters.length > 0 ? formatDateTime(deadLetters[0].timestamp) : '--';
const successRate = data.success_rate || 0;
// 更新汇总数据
const totalEl = document.getElementById('dead-letter-total');
if (totalEl) totalEl.textContent = total;
const recentEl = document.getElementById('dead-letter-recent');
if (recentEl) recentEl.textContent = recent;
const rateEl = document.getElementById('dead-letter-success-rate');
if (rateEl) rateEl.textContent = successRate.toFixed(2) + '%';
// 更新DeadLetter列表
const tbody = document.getElementById('dead-letter-tbody');
if (tbody) {
if (deadLetters.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">' + (typeof i18n !== 'undefined' ? i18n.t('monitor.status.no_dead_letters') : '暂无DL') + '</td></tr>';
return;
}
// 使用文档片段进行批量DOM操作
const fragment = document.createDocumentFragment();
deadLetters.forEach(letter => {
const tr = document.createElement('tr');
tr.innerHTML = `
<td>${letter.id}</td>
<td>${letter.event_type}</td>
<td>${formatDateTime(letter.timestamp)}</td>
<td>${letter.event_data.database || 'unknown'}</td>
<td>${letter.event_data.table || 'unknown'}</td>
<td class="error-message">${letter.error_message}</td>
<td>
<button class="btn btn-small" onclick="reprocessDeadLetter('${letter.id}')">${typeof i18n !== 'undefined' ? i18n.t('monitor.dl.reprocess') : '重新处理'}</button>
</td>
`;
fragment.appendChild(tr);
});
// 清空并添加新内容
tbody.innerHTML = '';
tbody.appendChild(fragment);
}
}
// 重新处理DeadLetter
async function reprocessDeadLetter(letterId) {
try {
const response = await fetch(`${API_BASE}/dead-letter/reprocess/${letterId}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
refreshDeadLetterStats();
showNotification(typeof i18n !== 'undefined' ? i18n.t('monitor.dl.reprocess_success') : 'DeadLetter重新处理成功', 'success');
} else {
showNotification(typeof i18n !== 'undefined' ? i18n.t('monitor.dl.reprocess_failed') : 'DeadLetter重新处理失败', 'error');
}
} catch (error) {
console.error('重新处理DeadLetter失败:', error);
showNotification(typeof i18n !== 'undefined' ? i18n.t('monitor.dl.reprocess_failed') : '重新处理DeadLetter失败', 'error');
}
}
// 显示通知
function showNotification(message, type) {
// 创建通知元素
const notification = document.createElement('div');
notification.className = `notification ${type}`;
notification.textContent = message;
// 添加到页面
document.body.appendChild(notification);
// 显示通知
setTimeout(() => {
notification.classList.add('show');
}, 10);
// 3秒后隐藏通知
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
// 获取DeadLetter队列数据
async function fetchDeadLetterStats() {
try {
const response = await fetch(`${API_BASE}/dead-letter`);
const data = await response.json();
updateDeadLetterDisplay(data);
} catch (error) {
console.error('获取DeadLetter队列数据失败:', error);
}
}
// 在数据库页面刷新时获取数据库详细信息
async function refreshDatabasePage() {
await fetchDatabaseDetail();
}
// 更新事件辅助模块显示
function updateEventHelpersDisplay(data) {
if (!data) return;
// 回调跟踪器
if (data.callback_tracker) {
const tracker = data.callback_tracker;
getElement('callback-pending').textContent = tracker.pending_count || 0;
getElement('callback-max-retries').textContent = tracker.max_retries || 3;
getElement('callback-pending-retries').textContent = tracker.total_pending_retries || 0;
}
// DeadLetter队列
if (data.dead_letter_queue) {
const dlq = data.dead_letter_queue;
getElement('dlq-pending').textContent = dlq.pending_count || 0;
getElement('dlq-total').textContent = dlq.total_event_count || 0;
getElement('dlq-file-size').textContent = formatFileSize(dlq.total_file_size || 0);
getElement('dlq-running').textContent = dlq.running ? (typeof i18n !== 'undefined' ? i18n.t('monitor.status.running') : '运行中') : (typeof i18n !== 'undefined' ? i18n.t('monitor.status.stopped') : '已停止');
// 更新DeadLetter队列详细信息区域
const totalEl = document.getElementById('dead-letter-total');
if (totalEl) totalEl.textContent = dlq.total_event_count || 0;
const recentEl = document.getElementById('dead-letter-recent');
if (recentEl) recentEl.textContent = '-';
const rateEl = document.getElementById('dead-letter-success-rate');
if (rateEl) rateEl.textContent = '100%';
}
// 事件去重器
if (data.event_deduplicator) {
const deduplicator = data.event_deduplicator;
getElement('deduplicator-total').textContent = deduplicator.total_entries || 0;
getElement('deduplicator-active').textContent = deduplicator.active_entries || 0;
getElement('deduplicator-ttl').textContent = deduplicator.ttl || 300;
getElement('deduplicator-max').textContent = deduplicator.max_entries || 100000;
}
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
}
// ==================== 日志历史查询已移至 history-logs.html ====================