diff --git a/templates_full.txt b/templates_full.txt index b3eed2b..e5b4aad 100644 --- a/templates_full.txt +++ b/templates_full.txt @@ -698,17 +698,32 @@ 磁盘使用率 +

勾选多项可随机组合监控,任一资源超标即触发通知

+ + +
% / °C
-

对不同资源类型使用统一阈值(如CPU用%则填百分比,温度用°C则填温度值)

+

对不同资源类型使用统一阈值(如CPU用%则填百分比,温度用°C则填温度值)

@@ -1245,6 +1260,8 @@ fetch('/api/system/stats').then(r => r.json()).then(sysData => { if (sysData.available) checkThresholds(sysData); }).catch(() => {}); + // 同时检查服务下线邮件规则 + checkEmailRules(null); } catch (e) { console.error('加载失败:', e); @@ -1561,7 +1578,7 @@ return; } - const resourceNames = { 'cpu': 'CPU', 'cpu_temp': 'CPU温度', 'memory': '内存', 'disk': '磁盘' }; + const resourceNames = { 'cpu': 'CPU', 'cpu_temp': 'CPU温度', 'memory': '内存', 'disk': '磁盘', 'service_down': '服务下线' }; const resourceUnits = { 'cpu': '%', 'cpu_temp': '°C', 'memory': '%', 'disk': '%' }; list.innerHTML = rules.map(rule => { @@ -1575,14 +1592,19 @@ // 显示资源列表(支持多资源和旧版单资源兼容) const resources = rule.resources || (rule.resource ? [rule.resource] : []); - const resourceDisplay = resources.map(r => resourceNames[r] || r).join('+'); + let resourceDisplay; + if (resources.includes('service_down') && rule.serviceName) { + resourceDisplay = `🔴 ${rule.serviceName} 下线 ≥ ${rule.threshold}分钟`; + } else { + resourceDisplay = resources.map(r => resourceNames[r] || r).join('+') + ' ≥ ' + rule.threshold; + } return `
${rule.enabled ? '启用' : '禁用'} ${rule.name} - ${resourceDisplay} ≥ ${rule.threshold} + ${resourceDisplay} ${triggerDisplay} → ${rule.email} ${silentInfo ? ` ${silentInfo}` : ''} @@ -1602,9 +1624,13 @@ document.getElementById('emailRuleName').value = ''; // 重置资源复选框 document.querySelectorAll('.email-rule-resource').forEach(cb => cb.checked = false); + document.getElementById('serviceDownSelector').style.display = 'none'; + document.getElementById('emailRuleServiceId').value = ''; document.getElementById('emailRuleThreshold').value = 80; document.getElementById('emailRuleThresholdUnit').textContent = '% / °C'; + document.getElementById('emailRuleThresholdHint').textContent = '对不同资源类型使用统一阈值'; document.getElementById('emailRuleTriggerType').value = 'single'; + document.getElementById('emailRuleTriggerType').disabled = false; document.getElementById('emailRuleTriggerParam').value = 3; document.getElementById('triggerParamsDiv').style.display = 'none'; document.getElementById('emailRuleAddress').value = ''; @@ -1629,24 +1655,73 @@ const selected = getSelectedResources(); const unitEl = document.getElementById('emailRuleThresholdUnit'); const thresholdEl = document.getElementById('emailRuleThreshold'); + const hintEl = document.getElementById('emailRuleThresholdHint'); + const serviceSelector = document.getElementById('serviceDownSelector'); + const triggerTypeEl = document.getElementById('emailRuleTriggerType'); + const triggerParamsDiv = document.getElementById('triggerParamsDiv'); - if (selected.length === 0) { - unitEl.textContent = '% / °C'; - } else if (selected.length === 1 && selected[0] === 'cpu_temp') { - unitEl.textContent = '°C'; - if (parseInt(thresholdEl.value) > 120) thresholdEl.value = 80; - thresholdEl.max = 120; - thresholdEl.min = 30; - } else { - unitEl.textContent = '%'; - thresholdEl.max = 100; + const hasServiceDown = selected.includes('service_down'); + + // 显示/隐藏服务选择器 + if (hasServiceDown) { + serviceSelector.style.display = 'block'; + populateServiceSelector(); + // 服务下线使用分钟作为阈值单位 + unitEl.textContent = '分钟'; + thresholdEl.value = 5; + thresholdEl.max = 1440; thresholdEl.min = 1; - if (selected.includes('cpu_temp')) { + hintEl.textContent = '服务连续离线超过此分钟数即触发通知'; + // 服务下线固定为duration类型 + if (triggerTypeEl) { + triggerTypeEl.value = 'duration'; + triggerTypeEl.disabled = true; + } + if (triggerParamsDiv) triggerParamsDiv.style.display = 'none'; + } else { + serviceSelector.style.display = 'none'; + if (triggerTypeEl) triggerTypeEl.disabled = false; + updateTriggerParams(); + + if (selected.length === 0) { unitEl.textContent = '% / °C'; + hintEl.textContent = '对不同资源类型使用统一阈值'; + } else if (selected.length === 1 && selected[0] === 'cpu_temp') { + unitEl.textContent = '°C'; + if (parseInt(thresholdEl.value) > 120) thresholdEl.value = 80; + thresholdEl.max = 120; + thresholdEl.min = 30; + hintEl.textContent = 'CPU温度超过此值即触发通知'; + } else { + unitEl.textContent = '%'; + thresholdEl.max = 100; + thresholdEl.min = 1; + hintEl.textContent = '对不同资源类型使用统一阈值'; + if (selected.includes('cpu_temp')) { + unitEl.textContent = '% / °C'; + } } } } + function populateServiceSelector() { + const sel = document.getElementById('emailRuleServiceId'); + const currentVal = sel.value; + // 收集所有web类型服务 + const webServices = projects.filter(p => p.type === 'web'); + let options = ''; + webServices.forEach(p => { + const portsStr = p.ports ? p.ports.join(',') : ''; + const selected = p.id === currentVal ? ' selected' : ''; + options += ``; + }); + sel.innerHTML = options; + } + + function onServiceDownChange() { + // 将来可以在这里做联动 + } + function updateTriggerParams() { const triggerType = document.getElementById('emailRuleTriggerType').value; const paramsDiv = document.getElementById('triggerParamsDiv'); @@ -1704,6 +1779,10 @@ if (cb) cb.checked = true; }); onResourceChange(); + // 服务下线相关 + if (rule.serviceId) { + document.getElementById('emailRuleServiceId').value = rule.serviceId; + } document.getElementById('emailRuleThreshold').value = rule.threshold; document.getElementById('emailRuleTriggerType').value = rule.triggerType || 'single'; document.getElementById('emailRuleTriggerParam').value = rule.triggerParam || 3; @@ -1732,6 +1811,7 @@ const silentEnd = document.getElementById('emailRuleSilentEnd').value; const silentEnabled = document.getElementById('emailRuleSilentEnabled').checked; const enabled = document.getElementById('emailRuleEnabled').checked; + const serviceId = document.getElementById('emailRuleServiceId').value; if (!name || !email) { alert('请填写完整信息'); @@ -1743,23 +1823,32 @@ return; } + // 服务下线必须选择具体服务 + if (resources.includes('service_down') && !serviceId) { + alert('请选择要监控的服务'); + return; + } + const rules = getEmailRules(); if (ruleId) { // 编辑 const idx = rules.findIndex(r => r.id === ruleId); if (idx >= 0) { - rules[idx] = { ...rules[idx], name, resources, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled }; + rules[idx] = { ...rules[idx], name, resources, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled, serviceId }; } } else { // 新增 + const svc = projects.find(p => p.id === serviceId); rules.push({ id: 'rule_' + Date.now(), name, resources, threshold, triggerType, triggerParam, email, interval, silentStart, silentEnd, silentEnabled, enabled, + serviceId, serviceName: svc ? svc.name : '', + offlineSince: null, // 服务下线开始时间 lastSent: 0, - exceedCount: 0, // 连续超过次数 - exceedDuration: 0, // 累计超过时间 - recentValues: [] // 最近采样值 + exceedCount: 0, + exceedDuration: 0, + recentValues: [] }); } @@ -1818,6 +1907,33 @@ for (const resource of resources) { let value; + + // 服务下线监控 - 特殊处理(不需要data参数) + if (resource === 'service_down') { + const svcId = rule.serviceId; + if (!svcId) continue; + const svc = projects.find(p => p.id === svcId); + if (!svc) continue; + const isOnline = svc.status?.status === 'running'; + + if (isOnline) { + rule.offlineSince = null; + } else { + if (!rule.offlineSince) { + rule.offlineSince = now; + } + const offlineMinutes = (now - rule.offlineSince) / 60000; + if (offlineMinutes >= rule.threshold) { + const svcName = svc.name || rule.serviceName || svcId; + exceededResources.push(`🔴 ${svcName} 已离线 ${Math.round(offlineMinutes)} 分钟 (阈值: ${rule.threshold} 分钟)`); + } + } + continue; + } + + // 系统资源监控需要 data 参数 + if (!data) continue; + if (resource === 'cpu_temp') { // CPU温度取两个Package中较高的那个 const t0 = data.cpu?.temp_package_0;