@@ -94,8 +94,12 @@ function osLabel(os){
9494 return String ( os ) . charAt ( 0 ) . toUpperCase ( ) + String ( os ) . slice ( 1 ) ;
9595}
9696
97+ function lossValues ( s ) {
98+ return [ s . ping_10010 , s . ping_189 , s . ping_10086 ] . map ( p => clamp ( num ( p ) , 0 , 100 ) ) ;
99+ }
100+
97101function isBlocked ( s ) {
98- return [ s . ping_10010 , s . ping_189 , s . ping_10086 ] . every ( p => clamp ( num ( p ) , 0 , 100 ) >= 100 ) ;
102+ return lossValues ( s ) . every ( p => p >= 100 ) ;
99103}
100104
101105function metrics ( s ) {
@@ -105,11 +109,20 @@ function metrics(s){
105109 const monthIn = Math . max ( 0 , num ( s . network_in ) - num ( s . last_network_in ) ) ;
106110 const monthOut = Math . max ( 0 , num ( s . network_out ) - num ( s . last_network_out ) ) ;
107111 const traffic = monthIn + monthOut ;
108- const loss = Math . max ( num ( s . ping_10010 ) , num ( s . ping_189 ) , num ( s . ping_10086 ) ) ;
109- const blocked = online && isBlocked ( s ) ;
110- const critical = online && ( num ( s . cpu ) >= 90 || memPct >= 90 || hddPct >= 90 || loss >= 20 ) ;
111- const warning = online && ( traffic >= 500 * 1000 * 1000 * 1000 || loss >= 10 || num ( s . cpu ) >= 75 || memPct >= 80 || hddPct >= 85 ) ;
112- return { online, memPct, hddPct, monthIn, monthOut, traffic, loss, blocked, critical, warning, alert : critical || warning , highlight : ! online || critical || warning } ;
112+ const losses = lossValues ( s ) ;
113+ const loss = Math . max ( ...losses ) ;
114+ const lossBadCount = losses . filter ( p => p >= 40 ) . length ;
115+ const lossWarnCount = losses . filter ( p => p >= 30 ) . length ;
116+ const blocked = online && losses . every ( p => p >= 100 ) ;
117+ const resourceCritical = online && ( num ( s . cpu ) >= 90 || memPct >= 90 || hddPct >= 90 ) ;
118+ const resourceWarning = online && ( num ( s . cpu ) >= 75 || memPct >= 80 || hddPct >= 85 ) ;
119+ const trafficWarning = online && traffic >= 500 * 1000 * 1000 * 1000 ;
120+ const lossCritical = online && loss >= 40 ;
121+ const lossWarning = online && ! lossCritical && loss >= 30 ;
122+ const critical = online && ( resourceCritical || blocked || lossCritical ) ;
123+ const warning = online && ( resourceWarning || lossWarning ) ;
124+ const rowLevel = ! online || critical ? 'critical' : ( resourceWarning || lossWarning ? 'warning' : '' ) ;
125+ return { online, memPct, hddPct, monthIn, monthOut, traffic, loss, lossBadCount, lossWarnCount, blocked, resourceCritical, resourceWarning, trafficWarning, lossCritical, lossWarning, critical, warning, alert : critical || warning , highlight : ! ! rowLevel , rowLevel } ;
113126}
114127
115128async function fetchData ( ) {
@@ -255,7 +268,12 @@ function trafficCaps(s, small){
255268}
256269function gaugeHTML ( type , value ) {
257270 const pct = clamp ( num ( value ) , 0 , 100 ) ;
258- const warnAttr = pct >= 90 ? 'data-bad' : ( pct >= 50 ? 'data-warn' : '' ) ;
271+ const thresholds = {
272+ cpu : { warn : 75 , bad : 90 } ,
273+ mem : { warn : 80 , bad : 90 } ,
274+ hdd : { warn : 85 , bad : 90 }
275+ } [ type ] || { warn : 75 , bad : 90 } ;
276+ const warnAttr = pct >= thresholds . bad ? 'data-bad' : ( pct >= thresholds . warn ? 'data-warn' : '' ) ;
259277 const label = type === 'cpu' ? 'CPU' : type === 'mem' ? '内存' : '硬盘' ;
260278 return `<div class="gauge-half" data-type="${ type } " ${ warnAttr } style="--p:${ ( pct / 100 ) . toFixed ( 3 ) } " title="${ label } ${ pct . toFixed ( 0 ) } %">
261279 <svg viewBox="0 0 100 50" preserveAspectRatio="xMidYMid meet" aria-hidden="true"><path class="track" d="M10 50 A40 40 0 0 1 90 50" /><path class="arc" d="M10 50 A40 40 0 0 1 90 50" /></svg>
@@ -264,7 +282,13 @@ function gaugeHTML(type, value){
264282}
265283function resourceMeter ( label , value , pct , kind ) {
266284 const safePct = clamp ( num ( pct ) , 0 , 100 ) ;
267- const level = safePct >= 90 ? 'bad' : ( safePct >= 75 ? 'warn' : 'ok' ) ;
285+ const thresholds = {
286+ cpu : { warn : 75 , bad : 90 } ,
287+ mem : { warn : 80 , bad : 90 } ,
288+ swap : { warn : 80 , bad : 90 } ,
289+ hdd : { warn : 85 , bad : 90 }
290+ } [ kind ] || { warn : 75 , bad : 90 } ;
291+ const level = safePct >= thresholds . bad ? 'bad' : ( safePct >= thresholds . warn ? 'warn' : 'ok' ) ;
268292 return `<div class="resource-meter" data-kind="${ esc ( kind ) } " data-level="${ level } " style="--p:${ safePct . toFixed ( 1 ) } %">
269293 <div class="resource-meter-head"><span>${ esc ( label ) } </span><strong>${ esc ( value ) } </strong></div>
270294 <div class="resource-track"><i></i></div>
@@ -288,7 +312,7 @@ function chartLegend(series){
288312function buckets ( s ) {
289313 return `<div class="buckets" title="联通 / 电信 / 移动">${ [ s . ping_10010 , s . ping_189 , s . ping_10086 ] . map ( p => {
290314 const v = clamp ( num ( p ) , 0 , 100 ) ;
291- const level = v >= 20 ? 'bad' : ( v >= 10 ? 'warn' : 'ok' ) ;
315+ const level = v >= 40 ? 'bad' : ( v >= 30 ? 'warn' : 'ok' ) ;
292316 return `<div class="bucket" data-lv="${ level } "><span style="--h:${ v } %"></span><label>${ v . toFixed ( 0 ) } %</label></div>` ;
293317 } ) . join ( '' ) } </div>`;
294318}
@@ -303,7 +327,8 @@ function renderServers(){
303327 const m = metrics ( s ) ;
304328 const netNow = `${ humanMinKBFromB ( s . network_rx ) } | ${ humanMinKBFromB ( s . network_tx ) } ` ;
305329 const netTotal = `${ humanMinMBFromB ( s . network_in ) } | ${ humanMinMBFromB ( s . network_out ) } ` ;
306- return `<tr data-key="${ esc ( s . _key ) } " data-online="${ m . online ? 1 : 0 } " class="row-server${ m . highlight ? ' high-load' : '' } ${ osClass ( s . os ) } " style="cursor:${ m . online ? 'pointer' : 'default' } ;">
330+ const alertClass = m . rowLevel ? ` alert-${ m . rowLevel } ` : '' ;
331+ return `<tr data-key="${ esc ( s . _key ) } " data-online="${ m . online ? 1 : 0 } " class="row-server${ alertClass } ${ osClass ( s . os ) } " style="cursor:${ m . online ? 'pointer' : 'default' } ;">
307332 <td>${ protoPill ( s ) } </td>
308333 <td>${ trafficCaps ( s ) } </td>
309334 <td>${ esc ( s . name || '-' ) } </td>
@@ -330,7 +355,8 @@ function renderServersCards(){
330355 if ( window . innerWidth > 700 ) { wrap . innerHTML = '' ; return ; }
331356 wrap . innerHTML = visibleServers ( ) . map ( s => {
332357 const m = metrics ( s ) ;
333- return `<div class="card${ m . online ? '' : ' offline' } ${ m . highlight ? ' high-load' : '' } ${ osClass ( s . os ) } " data-key="${ esc ( s . _key ) } " data-online="${ m . online ? 1 : 0 } ">
358+ const alertClass = m . rowLevel ? ` alert-${ m . rowLevel } ` : '' ;
359+ return `<div class="card${ m . online ? '' : ' offline' } ${ alertClass } ${ osClass ( s . os ) } " data-key="${ esc ( s . _key ) } " data-online="${ m . online ? 1 : 0 } ">
334360 <div class="card-header"><div class="card-title">${ esc ( s . name || '-' ) } <span class="tag">${ esc ( s . location || '-' ) } </span></div>${ protoPill ( s ) } </div>
335361 <div class="kvlist">
336362 <div><span class="key">负载</span><span>${ s . load_1 === - 1 ? '–' : num ( s . load_1 ) . toFixed ( 2 ) } </span></div>
@@ -379,7 +405,8 @@ function renderSSL(){
379405 const cls = c . mismatch || c . expire_days <= 0 ? 'err' : c . expire_days <= 7 ? 'warn' : 'ok' ;
380406 const status = c . mismatch ? '域名不匹配' : c . expire_days <= 0 ? '已过期' : c . expire_days <= 7 ? '将到期' : '正常' ;
381407 const dt = c . expire_ts ? new Date ( c . expire_ts * 1000 ) . toISOString ( ) . replace ( 'T' , ' ' ) . replace ( / \. \d + Z / , '' ) : '-' ;
382- return `<tr class="${ cls !== 'ok' ? 'high-load' : '' } "><td>${ esc ( c . name || '-' ) } </td><td>${ esc ( String ( c . domain || '' ) . replace ( / ^ h t t p s ? : \/ \/ / , '' ) ) } </td><td>${ esc ( c . port || 443 ) } </td><td><span class="badge ${ cls } ">${ esc ( c . expire_days ?? '-' ) } </span></td><td>${ dt } </td><td><span class="badge ${ cls } ">${ status } </span></td></tr>` ;
408+ const alertClass = cls === 'err' ? 'alert-critical' : cls === 'warn' ? 'alert-warning' : '' ;
409+ return `<tr class="${ alertClass } "><td>${ esc ( c . name || '-' ) } </td><td>${ esc ( String ( c . domain || '' ) . replace ( / ^ h t t p s ? : \/ \/ / , '' ) ) } </td><td>${ esc ( c . port || 443 ) } </td><td><span class="badge ${ cls } ">${ esc ( c . expire_days ?? '-' ) } </span></td><td>${ dt } </td><td><span class="badge ${ cls } ">${ status } </span></td></tr>` ;
383410 } ) . join ( '' ) || `<tr><td colspan="6" class="muted" style="text-align:center;padding:1rem;">无证书数据</td></tr>` ;
384411}
385412function renderSSLCards ( ) {
@@ -389,7 +416,8 @@ function renderSSLCards(){
389416 const cls = c . mismatch || c . expire_days <= 0 ? 'err' : c . expire_days <= 7 ? 'warn' : 'ok' ;
390417 const status = c . mismatch ? '域名不匹配' : c . expire_days <= 0 ? '已过期' : c . expire_days <= 7 ? '将到期' : '正常' ;
391418 const dt = c . expire_ts ? new Date ( c . expire_ts * 1000 ) . toISOString ( ) . slice ( 0 , 10 ) : '-' ;
392- return `<div class="card${ cls !== 'ok' ? ' high-load' : '' } "><div class="card-header"><div class="card-title">${ esc ( c . name || '-' ) } </div><span class="status-pill ${ cls === 'err' ? 'off' : 'on' } ">${ status } </span></div><div class="kvlist"><div><span class="key">域名</span><span>${ esc ( String ( c . domain || '' ) . replace ( / ^ h t t p s ? : \/ \/ / , '' ) ) } </span></div><div><span class="key">端口</span><span>${ esc ( c . port || 443 ) } </span></div><div><span class="key">剩余</span><span>${ esc ( c . expire_days ?? '-' ) } 天</span></div><div><span class="key">到期</span><span>${ dt } </span></div></div></div>` ;
419+ const alertClass = cls === 'err' ? ' alert-critical' : cls === 'warn' ? ' alert-warning' : '' ;
420+ return `<div class="card${ alertClass } "><div class="card-header"><div class="card-title">${ esc ( c . name || '-' ) } </div><span class="status-pill ${ cls === 'err' ? 'off' : 'on' } ">${ status } </span></div><div class="kvlist"><div><span class="key">域名</span><span>${ esc ( String ( c . domain || '' ) . replace ( / ^ h t t p s ? : \/ \/ / , '' ) ) } </span></div><div><span class="key">端口</span><span>${ esc ( c . port || 443 ) } </span></div><div><span class="key">剩余</span><span>${ esc ( c . expire_days ?? '-' ) } 天</span></div><div><span class="key">到期</span><span>${ dt } </span></div></div></div>` ;
393421 } ) . join ( '' ) || '<div class="empty-state">无证书数据</div>' ;
394422}
395423
@@ -407,7 +435,11 @@ function refreshDetail(){
407435 const title = $ ( 'detailTitle' ) ;
408436 title . innerHTML = `${ esc ( s . name || '-' ) } 详情${ s . os ? `<span class="os-chip${ osClass ( s . os ) } ">${ esc ( osLabel ( s . os ) ) } </span>` : '' } ` ;
409437 const modalBox = document . querySelector ( '#detailModal .modal-box' ) ;
410- if ( modalBox ) modalBox . classList . toggle ( 'high-load' , m . highlight ) ;
438+ if ( modalBox ) {
439+ modalBox . classList . remove ( 'high-load' ) ;
440+ modalBox . classList . toggle ( 'alert-critical' , m . rowLevel === 'critical' ) ;
441+ modalBox . classList . toggle ( 'alert-warning' , m . rowLevel === 'warning' ) ;
442+ }
411443 const loadSeries = [
412444 { data : S . loadHist [ s . _key ] ?. l1 || [ ] , color :'#8b5cf6' , label :'load1' } ,
413445 { data : S . loadHist [ s . _key ] ?. l5 || [ ] , color :'#10b981' , label :'load5' } ,
0 commit comments