diff --git a/web/css/app.css b/web/css/app.css index 4002102..4c06c04 100644 --- a/web/css/app.css +++ b/web/css/app.css @@ -61,13 +61,13 @@ table.data th,table.data td{font-variant-numeric:tabular-nums} .caps-traffic .sep{opacity:.4;font-size:11px;display:none;} .caps-traffic.sm{padding:2px 8px 2px 7px;font-size:11px;gap:5px;} .caps-traffic.sm .io{font-size:11px;} -/* 双色胶囊:左红右黄 */ +/* 双色胶囊 */ .caps-traffic.duo{background:none;border:0;gap:0;padding:0;box-shadow:none;position:relative;border-radius:999px;overflow:hidden;font-size:12px;} /* 宽度按内容自适应(不再拉伸占满列),每半边仅为其文本 + padding,可容纳最大 111.1MB */ .caps-traffic.duo .half{flex:0 0 auto;display:flex;align-items:center;justify-content:center;padding:2px 4px;font-variant-numeric:tabular-nums;font-weight:600;line-height:1.25; /* 与 .pill 保持一致高度 */ letter-spacing:.25px;color:#fff;font-size:12px;white-space:nowrap;} /* 双色胶囊配色: - normal (默认): 左白(#fff) 右蓝(accent) - heavy (>=500GB 任一方向): 左黄(warn) 右红(danger) + normal (默认): 左绿右蓝 + heavy (>=500GB 总量): 黄色提醒,不再使用故障红 */ /* normal 初始:淡绿色(入) + 淡蓝色(出) */ .caps-traffic.duo.normal .half.in{background:#d1fae5;color:#065f46;} /* emerald-100 / text-emerald-800 */ @@ -75,10 +75,10 @@ body.light .caps-traffic.duo.normal .half.in{background:#d1fae5;color:#065f46;} .caps-traffic.duo.normal .half.out{background:#bfdbfe;color:#1e3a8a;} /* blue-200 / text-blue-900 */ body.light .caps-traffic.duo.normal .half.out{background:#bfdbfe;color:#1e3a8a;} -.caps-traffic.duo.heavy .half.in{background:var(--warn);color:#111;} -body.light .caps-traffic.duo.heavy .half.in{background:var(--warn);color:#111;} -.caps-traffic.duo.heavy .half.out{background:var(--danger);color:#fff;} -body.light .caps-traffic.duo.heavy .half.out{color:#fff;} +.caps-traffic.duo.heavy .half.in{background:#fde68a;color:#78350f;} +body.light .caps-traffic.duo.heavy .half.in{background:#fde68a;color:#78350f;} +.caps-traffic.duo.heavy .half.out{background:#fbbf24;color:#111827;} +body.light .caps-traffic.duo.heavy .half.out{background:#fbbf24;color:#111827;} /* 半之间分隔线 */ .caps-traffic.duo .half + .half{border-left:1px solid rgba(0,0,0,.18);} @@ -130,7 +130,7 @@ body:not(.light) .modal-box.high-load{background:linear-gradient(180deg, rgba(23 .gauge-half path.arc{stroke:var(--gauge-base,#3b82f6);stroke-width:8;stroke-dasharray:126;stroke-dashoffset:calc(126*(1 - var(--p)));transition:stroke-dashoffset .8s cubic-bezier(.4,0,.2,1),stroke .35s;filter:drop-shadow(0 1px 2px rgba(0,0,0,.45));} .gauge-half[data-type=mem] path.arc{--gauge-base:#10b981} .gauge-half[data-type=hdd] path.arc{--gauge-base:#06b6d4} -/* 阈值颜色:>=50% 警告黄,>=90% 危险红 */ +/* 阈值颜色:CPU>=75%、内存>=80%、硬盘>=85% 警告黄,>=90% 危险红 */ .gauge-half[data-warn] path.arc{stroke:var(--warn)} .gauge-half[data-bad] path.arc{stroke:var(--danger)} /* 指针:以中心(50,50)为原点旋转;半圆角度范围 180deg -> 从 180deg (左) 到 0deg(右) */ @@ -231,9 +231,15 @@ body.light .gauge-half .needle{background:linear-gradient(var(--text),var(--text } .cards .card{border:1px solid var(--border);border-radius:12px;padding:.75rem .85rem;background:linear-gradient(145deg,var(--bg),var(--bg-alt));display:flex;flex-direction:column;gap:.45rem;position:relative;} .cards .card.offline{filter:saturate(.92);} -.cards .card.high-load{border-color:rgba(239,68,68,.6);background:linear-gradient(180deg, rgba(239,68,68,.22), rgba(239,68,68,.12)), var(--bg-alt);box-shadow:0 0 0 1px rgba(239,68,68,.48),0 6px 18px -6px rgba(239,68,68,.28);} -table.data tbody tr.high-load{background:rgba(239,68,68,.18) !important;} -table.data tbody tr.high-load:hover{background:rgba(239,68,68,.26) !important;} +.cards .card.high-load, +.cards .card.alert-critical{border-color:rgba(239,68,68,.6);background:linear-gradient(180deg, rgba(239,68,68,.22), rgba(239,68,68,.12)), var(--bg-alt);box-shadow:0 0 0 1px rgba(239,68,68,.48),0 6px 18px -6px rgba(239,68,68,.28);} +.cards .card.alert-warning{border-color:rgba(245,158,11,.55);background:linear-gradient(180deg, rgba(245,158,11,.16), rgba(245,158,11,.08)), var(--bg-alt);box-shadow:0 0 0 1px rgba(245,158,11,.32),0 6px 18px -8px rgba(245,158,11,.22);} +table.data tbody tr.high-load, +table.data tbody tr.alert-critical{background:rgba(239,68,68,.18) !important;} +table.data tbody tr.high-load:hover, +table.data tbody tr.alert-critical:hover{background:rgba(239,68,68,.26) !important;} +table.data tbody tr.alert-warning{background:rgba(245,158,11,.12) !important;} +table.data tbody tr.alert-warning:hover{background:rgba(245,158,11,.18) !important;} /* SSL 域名告警底色:与高负载相同 */ #sslTable td.alert-domain{background:rgba(239,68,68,.18) !important;} @@ -353,7 +359,9 @@ th[data-sort].sorted-desc:after{border-top-color:var(--accent);opacity:1} /* 右侧详情抽屉 */ .drawer-backdrop{align-items:stretch;justify-content:flex-end;padding:0;background:rgba(0,0,0,.48)} -.detail-drawer{height:100vh;max-height:100vh;width:min(620px,100vw);max-width:min(620px,100vw);border-radius:0;border-top:0;border-right:0;border-bottom:0;padding:1.1rem;overflow:auto;animation:slideIn .24s ease} +.detail-drawer{height:100dvh;max-height:100dvh;width:min(620px,100vw);max-width:min(620px,100vw);border-radius:0;border-top:0;border-right:0;border-bottom:0;padding:1.1rem;overflow:auto;animation:slideIn .24s ease} +.detail-drawer.alert-critical{border-color:rgba(239,68,68,.6);background:linear-gradient(180deg, rgba(239,68,68,.16), rgba(239,68,68,.08)), var(--bg-alt);box-shadow:0 0 0 1px rgba(239,68,68,.38),0 10px 28px -10px rgba(239,68,68,.28)} +.detail-drawer.alert-warning{border-color:rgba(245,158,11,.55);background:linear-gradient(180deg, rgba(245,158,11,.14), rgba(245,158,11,.06)), var(--bg-alt);box-shadow:0 0 0 1px rgba(245,158,11,.28),0 10px 28px -10px rgba(245,158,11,.2)} .modal-title{padding-right:2.2rem} .detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.55rem} .detail-section{border:1px solid var(--border);border-radius:8px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));padding:.75rem;display:flex;flex-direction:column;gap:.65rem} @@ -428,5 +436,6 @@ th[data-sort].sorted-desc:after{border-top-color:var(--accent);opacity:1} .section-head{flex-direction:column} .section-actions{width:100%;justify-content:stretch} .detail-grid,.config-fields{grid-template-columns:1fr} - .detail-drawer{width:100vw} + .detail-drawer{width:100vw;height:100dvh;max-height:100dvh;padding:calc(.95rem + env(safe-area-inset-top)) .95rem calc(.95rem + env(safe-area-inset-bottom));} + .detail-drawer .modal-content{max-height:none;overflow:visible;} } diff --git a/web/js/app.js b/web/js/app.js index e4c19ff..7715ecf 100644 --- a/web/js/app.js +++ b/web/js/app.js @@ -94,8 +94,12 @@ function osLabel(os){ return String(os).charAt(0).toUpperCase() + String(os).slice(1); } +function lossValues(s){ + return [s.ping_10010, s.ping_189, s.ping_10086].map(p => clamp(num(p), 0, 100)); +} + function isBlocked(s){ - return [s.ping_10010, s.ping_189, s.ping_10086].every(p => clamp(num(p), 0, 100) >= 100); + return lossValues(s).every(p => p >= 100); } function metrics(s){ @@ -105,11 +109,20 @@ function metrics(s){ const monthIn = Math.max(0, num(s.network_in) - num(s.last_network_in)); const monthOut = Math.max(0, num(s.network_out) - num(s.last_network_out)); const traffic = monthIn + monthOut; - const loss = Math.max(num(s.ping_10010), num(s.ping_189), num(s.ping_10086)); - const blocked = online && isBlocked(s); - const critical = online && (num(s.cpu) >= 90 || memPct >= 90 || hddPct >= 90 || loss >= 20); - const warning = online && (traffic >= 500 * 1000 * 1000 * 1000 || loss >= 10 || num(s.cpu) >= 75 || memPct >= 80 || hddPct >= 85); - return { online, memPct, hddPct, monthIn, monthOut, traffic, loss, blocked, critical, warning, alert: critical || warning, highlight: !online || critical || warning }; + const losses = lossValues(s); + const loss = Math.max(...losses); + const lossBadCount = losses.filter(p => p >= 40).length; + const lossWarnCount = losses.filter(p => p >= 30).length; + const blocked = online && losses.every(p => p >= 100); + const resourceCritical = online && (num(s.cpu) >= 90 || memPct >= 90 || hddPct >= 90); + const resourceWarning = online && (num(s.cpu) >= 75 || memPct >= 80 || hddPct >= 85); + const trafficWarning = online && traffic >= 500 * 1000 * 1000 * 1000; + const lossCritical = online && loss >= 40; + const lossWarning = online && !lossCritical && loss >= 30; + const critical = online && (resourceCritical || blocked || lossCritical); + const warning = online && (resourceWarning || lossWarning); + const rowLevel = !online || critical ? 'critical' : (resourceWarning || lossWarning ? 'warning' : ''); + return { online, memPct, hddPct, monthIn, monthOut, traffic, loss, lossBadCount, lossWarnCount, blocked, resourceCritical, resourceWarning, trafficWarning, lossCritical, lossWarning, critical, warning, alert: critical || warning, highlight: !!rowLevel, rowLevel }; } async function fetchData(){ @@ -255,7 +268,12 @@ function trafficCaps(s, small){ } function gaugeHTML(type, value){ const pct = clamp(num(value), 0, 100); - const warnAttr = pct >= 90 ? 'data-bad' : (pct >= 50 ? 'data-warn' : ''); + const thresholds = { + cpu: { warn: 75, bad: 90 }, + mem: { warn: 80, bad: 90 }, + hdd: { warn: 85, bad: 90 } + }[type] || { warn: 75, bad: 90 }; + const warnAttr = pct >= thresholds.bad ? 'data-bad' : (pct >= thresholds.warn ? 'data-warn' : ''); const label = type === 'cpu' ? 'CPU' : type === 'mem' ? '内存' : '硬盘'; return `
@@ -264,7 +282,13 @@ function gaugeHTML(type, value){ } function resourceMeter(label, value, pct, kind){ const safePct = clamp(num(pct), 0, 100); - const level = safePct >= 90 ? 'bad' : (safePct >= 75 ? 'warn' : 'ok'); + const thresholds = { + cpu: { warn: 75, bad: 90 }, + mem: { warn: 80, bad: 90 }, + swap: { warn: 80, bad: 90 }, + hdd: { warn: 85, bad: 90 } + }[kind] || { warn: 75, bad: 90 }; + const level = safePct >= thresholds.bad ? 'bad' : (safePct >= thresholds.warn ? 'warn' : 'ok'); return `
${esc(label)}${esc(value)}
@@ -288,7 +312,7 @@ function chartLegend(series){ function buckets(s){ return `
${[s.ping_10010, s.ping_189, s.ping_10086].map(p => { const v = clamp(num(p), 0, 100); - const level = v >= 20 ? 'bad' : (v >= 10 ? 'warn' : 'ok'); + const level = v >= 40 ? 'bad' : (v >= 30 ? 'warn' : 'ok'); return `
`; }).join('')}
`; } @@ -303,7 +327,8 @@ function renderServers(){ const m = metrics(s); const netNow = `${humanMinKBFromB(s.network_rx)} | ${humanMinKBFromB(s.network_tx)}`; const netTotal = `${humanMinMBFromB(s.network_in)} | ${humanMinMBFromB(s.network_out)}`; - return ` + const alertClass = m.rowLevel ? ` alert-${m.rowLevel}` : ''; + return ` ${protoPill(s)} ${trafficCaps(s)} ${esc(s.name || '-')} @@ -330,7 +355,8 @@ function renderServersCards(){ if(window.innerWidth > 700){ wrap.innerHTML = ''; return; } wrap.innerHTML = visibleServers().map(s => { const m = metrics(s); - return `
+ const alertClass = m.rowLevel ? ` alert-${m.rowLevel}` : ''; + return `
${esc(s.name || '-')} ${esc(s.location || '-')}
${protoPill(s)}
负载${s.load_1 === -1 ? '–' : num(s.load_1).toFixed(2)}
@@ -379,7 +405,8 @@ function renderSSL(){ const cls = c.mismatch || c.expire_days <= 0 ? 'err' : c.expire_days <= 7 ? 'warn' : 'ok'; const status = c.mismatch ? '域名不匹配' : c.expire_days <= 0 ? '已过期' : c.expire_days <= 7 ? '将到期' : '正常'; const dt = c.expire_ts ? new Date(c.expire_ts * 1000).toISOString().replace('T',' ').replace(/\.\d+Z/,'') : '-'; - return `${esc(c.name || '-')}${esc(String(c.domain || '').replace(/^https?:\/\//,''))}${esc(c.port || 443)}${esc(c.expire_days ?? '-')}${dt}${status}`; + const alertClass = cls === 'err' ? 'alert-critical' : cls === 'warn' ? 'alert-warning' : ''; + return `${esc(c.name || '-')}${esc(String(c.domain || '').replace(/^https?:\/\//,''))}${esc(c.port || 443)}${esc(c.expire_days ?? '-')}${dt}${status}`; }).join('') || `无证书数据`; } function renderSSLCards(){ @@ -389,7 +416,8 @@ function renderSSLCards(){ const cls = c.mismatch || c.expire_days <= 0 ? 'err' : c.expire_days <= 7 ? 'warn' : 'ok'; const status = c.mismatch ? '域名不匹配' : c.expire_days <= 0 ? '已过期' : c.expire_days <= 7 ? '将到期' : '正常'; const dt = c.expire_ts ? new Date(c.expire_ts * 1000).toISOString().slice(0,10) : '-'; - return `
${esc(c.name || '-')}
${status}
域名${esc(String(c.domain || '').replace(/^https?:\/\//,''))}
端口${esc(c.port || 443)}
剩余${esc(c.expire_days ?? '-')} 天
到期${dt}
`; + const alertClass = cls === 'err' ? ' alert-critical' : cls === 'warn' ? ' alert-warning' : ''; + return `
${esc(c.name || '-')}
${status}
域名${esc(String(c.domain || '').replace(/^https?:\/\//,''))}
端口${esc(c.port || 443)}
剩余${esc(c.expire_days ?? '-')} 天
到期${dt}
`; }).join('') || '
无证书数据
'; } @@ -407,7 +435,11 @@ function refreshDetail(){ const title = $('detailTitle'); title.innerHTML = `${esc(s.name || '-')} 详情${s.os ? `${esc(osLabel(s.os))}` : ''}`; const modalBox = document.querySelector('#detailModal .modal-box'); - if(modalBox) modalBox.classList.toggle('high-load', m.highlight); + if(modalBox){ + modalBox.classList.remove('high-load'); + modalBox.classList.toggle('alert-critical', m.rowLevel === 'critical'); + modalBox.classList.toggle('alert-warning', m.rowLevel === 'warning'); + } const loadSeries = [ { data: S.loadHist[s._key]?.l1 || [], color:'#8b5cf6', label:'load1' }, { data: S.loadHist[s._key]?.l5 || [], color:'#10b981', label:'load5' },