This commit is contained in:
cppla 2025-08-29 19:32:51 +08:00
parent 9cd86ccaa3
commit 68a1d3719f
6 changed files with 126 additions and 10 deletions

View File

@ -32,6 +32,7 @@ import json
import errno
import subprocess
import threading
import platform
if sys.version_info.major == 3:
from queue import Queue
elif sys.version_info.major == 2:
@ -520,6 +521,34 @@ if __name__ == '__main__':
array['tcp'], array['udp'], array['process'], array['thread'] = tupd()
array['io_read'] = diskIO.get("read")
array['io_write'] = diskIO.get("write")
# report OS (normalized)
try:
sysname = platform.system().lower()
if sysname.startswith('linux'):
os_name = 'linux'
# try distro from os-release
try:
with open('/etc/os-release') as f:
for line in f:
if line.startswith('ID='):
val = line.strip().split('=',1)[1].strip().strip('"')
if val: os_name = val
break
except Exception:
pass
elif sysname.startswith('darwin'):
os_name = 'darwin'
elif sysname.startswith('freebsd'):
os_name = 'freebsd'
elif sysname.startswith('openbsd'):
os_name = 'openbsd'
elif sysname.startswith('netbsd'):
os_name = 'netbsd'
else:
os_name = sysname or 'unknown'
except Exception:
os_name = 'unknown'
array['os'] = os_name
array['custom'] = "<br>".join(f"{k}\\t解析: {v['dns_time']}\\t连接: {v['connect_time']}\\t下载: {v['download_time']}\\t在线率: <code>{v['online_rate']*100:.1f}%</code>" for k, v in monitorServer.items())
s.send(byte_str("update " + json.dumps(array) + "\n"))
except KeyboardInterrupt:

View File

@ -32,6 +32,7 @@ import json
import errno
import psutil
import threading
import platform
if sys.version_info.major == 3:
from queue import Queue
elif sys.version_info.major == 2:
@ -509,7 +510,31 @@ if __name__ == '__main__':
array['tcp'], array['udp'], array['process'], array['thread'] = tupd()
array['io_read'] = diskIO.get("read")
array['io_write'] = diskIO.get("write")
array['custom'] = "<br>".join(f"{k}\\t解析: {v['dns_time']}\\t连接: {v['connect_time']}\\t下载: {v['download_time']}\\t在线率: <code>{v['online_rate']*100:.2f}%</code>" for k, v in monitorServer.items())
# report OS (normalized)
try:
sysname = platform.system().lower()
if sysname.startswith('windows'):
os_name = 'windows'
elif sysname.startswith('darwin') or 'mac' in sysname:
os_name = 'darwin'
elif 'bsd' in sysname:
os_name = 'bsd'
elif sysname.startswith('linux'):
# try distro if available
os_name = 'linux'
try:
import distro # optional
os_name = distro.id() or 'linux'
except Exception:
pass
else:
os_name = sysname or 'unknown'
except Exception:
os_name = 'unknown'
array['os'] = os_name
array['custom'] = "<br>".join("{}\t解析: {}\t连接: {}\t下载: {}\t在线率: <code>{:.2f}%</code>".format(
k, v.get('dns_time'), v.get('connect_time'), v.get('download_time'), (v.get('online_rate') or 0.0)*100
) for k, v in monitorServer.items())
s.send(byte_str("update " + json.dumps(array) + "\n"))
except KeyboardInterrupt:
raise

View File

@ -383,6 +383,9 @@ int CMain::HandleMessage(int ClientNetID, char *pMessage)
pClient->m_Stats.m_Online6 = rStart["online6"].u.boolean;
if(rStart["custom"].type == json_string)
str_copy(pClient->m_Stats.m_aCustom, rStart["custom"].u.string.ptr, sizeof(pClient->m_Stats.m_aCustom));
// optional OS field from clients
if(rStart["os"].type == json_string)
str_copy(pClient->m_Stats.m_aOS, rStart["os"].u.string.ptr, sizeof(pClient->m_Stats.m_aOS));
//copy message for watchdog to analysis
WatchdogMessage(ClientNetID,
@ -630,7 +633,7 @@ void CMain::JSONUpdateThread(void *pUser)
}
str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf),
"{ \"name\": \"%s\",\"type\": \"%s\",\"host\": \"%s\",\"location\": \"%s\",\"online4\": %s, \"online6\": %s, \"uptime\": \"%s\",\"load_1\": %.2f, \"load_5\": %.2f, \"load_15\": %.2f,\"ping_10010\": %.2f, \"ping_189\": %.2f, \"ping_10086\": %.2f,\"time_10010\": %" PRId64 ", \"time_189\": %" PRId64 ", \"time_10086\": %" PRId64 ", \"tcp_count\": %" PRId64 ", \"udp_count\": %" PRId64 ", \"process_count\": %" PRId64 ", \"thread_count\": %" PRId64 ", \"network_rx\": %" PRId64 ", \"network_tx\": %" PRId64 ", \"network_in\": %" PRId64 ", \"network_out\": %" PRId64 ", \"cpu\": %d, \"memory_total\": %" PRId64 ", \"memory_used\": %" PRId64 ", \"swap_total\": %" PRId64 ", \"swap_used\": %" PRId64 ", \"hdd_total\": %" PRId64 ", \"hdd_used\": %" PRId64 ", \"last_network_in\": %" PRId64 ", \"last_network_out\": %" PRId64 ",\"io_read\": %" PRId64 ", \"io_write\": %" PRId64 ",\"custom\": \"%s\" },\n",
"{ \"name\": \"%s\",\"type\": \"%s\",\"host\": \"%s\",\"location\": \"%s\",\"online4\": %s, \"online6\": %s, \"uptime\": \"%s\",\"load_1\": %.2f, \"load_5\": %.2f, \"load_15\": %.2f,\"ping_10010\": %.2f, \"ping_189\": %.2f, \"ping_10086\": %.2f,\"time_10010\": %" PRId64 ", \"time_189\": %" PRId64 ", \"time_10086\": %" PRId64 ", \"tcp_count\": %" PRId64 ", \"udp_count\": %" PRId64 ", \"process_count\": %" PRId64 ", \"thread_count\": %" PRId64 ", \"network_rx\": %" PRId64 ", \"network_tx\": %" PRId64 ", \"network_in\": %" PRId64 ", \"network_out\": %" PRId64 ", \"cpu\": %d, \"memory_total\": %" PRId64 ", \"memory_used\": %" PRId64 ", \"swap_total\": %" PRId64 ", \"swap_used\": %" PRId64 ", \"hdd_total\": %" PRId64 ", \"hdd_used\": %" PRId64 ", \"last_network_in\": %" PRId64 ", \"last_network_out\": %" PRId64 ",\"io_read\": %" PRId64 ", \"io_write\": %" PRId64 ",\"custom\": \"%s\", \"os\": \"%s\" },\n",
pClients[i].m_aName,pClients[i].m_aType,pClients[i].m_aHost,pClients[i].m_aLocation,
pClients[i].m_Stats.m_Online4 ? "true" : "false",pClients[i].m_Stats.m_Online6 ? "true" : "false",
aUptime, pClients[i].m_Stats.m_Load_1, pClients[i].m_Stats.m_Load_5, pClients[i].m_Stats.m_Load_15, pClients[i].m_Stats.m_ping_10010, pClients[i].m_Stats.m_ping_189, pClients[i].m_Stats.m_ping_10086,
@ -640,15 +643,17 @@ void CMain::JSONUpdateThread(void *pUser)
pClients[i].m_Stats.m_NetworkIN == 0 || pClients[i].m_LastNetworkIN == 0 ? pClients[i].m_Stats.m_NetworkIN : pClients[i].m_LastNetworkIN,
pClients[i].m_Stats.m_NetworkOUT == 0 || pClients[i].m_LastNetworkOUT == 0 ? pClients[i].m_Stats.m_NetworkOUT : pClients[i].m_LastNetworkOUT,
pClients[i].m_Stats.m_IORead, pClients[i].m_Stats.m_IOWrite,
pClients[i].m_Stats.m_aCustom);
pClients[i].m_Stats.m_aCustom,
pClients[i].m_Stats.m_aOS[0] ? pClients[i].m_Stats.m_aOS : "");
pBuf += strlen(pBuf);
}
else
{
// sava network traffic record to json when close client
// last_network_in == last network in record, last_network_out == last network out record
str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf), "{ \"name\": \"%s\", \"type\": \"%s\", \"host\": \"%s\", \"location\": \"%s\", \"online4\": false, \"online6\": false, \"last_network_in\": %" PRId64 ", \"last_network_out\": %" PRId64 " },\n",
pClients[i].m_aName, pClients[i].m_aType, pClients[i].m_aHost, pClients[i].m_aLocation, pClients[i].m_LastNetworkIN, pClients[i].m_LastNetworkOUT);
str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf), "{ \"name\": \"%s\", \"type\": \"%s\", \"host\": \"%s\", \"location\": \"%s\", \"online4\": false, \"online6\": false, \"last_network_in\": %" PRId64 ", \"last_network_out\": %" PRId64 ", \"os\": \"%s\" },\n",
pClients[i].m_aName, pClients[i].m_aType, pClients[i].m_aHost, pClients[i].m_aLocation, pClients[i].m_LastNetworkIN, pClients[i].m_LastNetworkOUT,
pClients[i].m_Stats.m_aOS[0] ? pClients[i].m_Stats.m_aOS : "");
pBuf += strlen(pBuf);
}
}

View File

@ -78,6 +78,8 @@ class CMain
int64_t m_IOWrite;
double m_CPU;
char m_aCustom[1024];
// OS name reported by client (e.g. linux/windows/darwin/freebsd)
char m_aOS[64];
// Options
bool m_Pong;
} m_Stats;

View File

@ -230,9 +230,39 @@ 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{opacity:.6;}
.cards .card.high-load{border-color:rgba(239,68,68,.55);box-shadow:0 0 0 1px rgba(239,68,68,.4),0 4px 16px -4px rgba(239,68,68,.3);}
table.data tbody tr.high-load{background:rgba(239,68,68,.10);}
table.data tbody tr.high-load:hover{background:rgba(239,68,68,.18);}
.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));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;}
/* OS 着色更明显
1) 为各 OS 类定义 --os-color 变量
2) 行左侧使用 inset box-shadow 4px 彩条
3) 行背景叠加轻度渐变以提示 OS
*/
table.data tbody tr[class*="os-"]{box-shadow:inset 4px 0 0 0 var(--os-color, transparent);background:linear-gradient(180deg, color-mix(in srgb, var(--os-color, transparent) 10%, transparent), transparent 60%);}
table.data tbody tr[class*="os-"]:hover{background:linear-gradient(180deg, color-mix(in srgb, var(--os-color, transparent) 16%, transparent), transparent 65%);}
.cards .card[class*="os-"]{border-color:color-mix(in srgb, var(--os-color, var(--accent)) 60%, transparent);box-shadow:0 0 0 1px color-mix(in srgb, var(--os-color, var(--accent)) 40%, transparent),0 4px 16px -6px color-mix(in srgb, var(--os-color, #000) 35%, transparent);}
/* 为常见系统赋色 */
.os-linux{--os-color: rgba(16,185,129,.85);} /* emerald */
.os-ubuntu{--os-color: rgba(251,146,60,.9);} /* orange */
.os-debian{--os-color: rgba(236,72,153,.9);} /* pink */
.os-centos{--os-color: rgba(59,130,246,.9);} /* blue */
.os-rocky{--os-color: rgba(59,130,246,.9);}
.os-almalinux{--os-color: rgba(59,130,246,.9);}
.os-rhel{--os-color: rgba(59,130,246,.9);}
.os-arch{--os-color: rgba(14,165,233,.9);} /* sky */
.os-alpine{--os-color: rgba(2,132,199,.9);} /* blue-600 */
.os-fedora{--os-color: rgba(59,130,246,.9);}
.os-amazon{--os-color: rgba(245,158,11,.9);} /* amber */
.os-suse{--os-color: rgba(34,197,94,.9);} /* green */
.os-freebsd{--os-color: rgba(244,63,94,.9);} /* rose */
.os-openbsd{--os-color: rgba(244,63,94,.9);}
.os-bsd{--os-color: rgba(244,63,94,.9);}
.os-darwin{--os-color: rgba(148,163,184,.95);} /* slate */
.os-windows{--os-color: rgba(59,130,246,.95);} /* blue */
/* 已移除徽标叠加与伪元素,仅保留色条 + 渐变背景作为 OS 提示 */
/* 旧进度条相关样式已清理 */
.cards .card-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem;}

View File

@ -27,6 +27,31 @@ function humanMinKBFromB(bytes){ if(bytes==null||isNaN(bytes)) return '-'; //
function humanAgo(ts){ if(!ts) return '-'; const s=Math.floor((Date.now()/1000 - ts)); const m=Math.floor(s/60); return m>0? m+' 分钟前':'几秒前'; }
function num(v){ return (typeof v==='number' && !isNaN(v)) ? v : '-'; }
// 将服务端上报的 os 映射为样式类名(用于为行/卡片着色)
function osClass(os){
if(!os) return '';
const v = String(os).toLowerCase();
const pick = (k)=>' os-'+k;
if(v.includes('ubuntu')) return pick('ubuntu');
if(v.includes('debian')) return pick('debian');
if(v.includes('centos')) return pick('centos');
if(v.includes('rocky')) return pick('rocky');
if(v.includes('alma')) return pick('almalinux');
if(v.includes('arch')) return pick('arch');
if(v.includes('alpine')) return pick('alpine');
if(v.includes('fedora')) return pick('fedora');
if(v.includes('rhel') || v.includes('redhat')) return pick('rhel');
if(v.includes('suse')) return pick('suse');
if(v.includes('amazon')) return pick('amazon');
if(v.includes('freebsd')) return pick('freebsd');
if(v.includes('openbsd')) return pick('openbsd');
if(v.includes('netbsd') || v.includes('bsd')) return pick('bsd');
if(v.includes('darwin') || v.includes('mac')) return pick('darwin');
if(v.includes('win')) return pick('windows');
if(v.includes('linux')) return pick('linux');
return pick(v.replace(/[^a-z0-9_-]+/g,'-').slice(0,20));
}
async function fetchData(){
try {
const r = await fetch('json/stats.json?_='+Date.now());
@ -106,7 +131,7 @@ function renderServers(){
// 唯一 key 已附加为 s._key如需使用
const rowCursor = online? 'pointer':'default';
const highLoad = online && ( (s.cpu||0)>=90 || (memPct)>=90 || (hddPct)>=90 );
html += `<tr data-idx="${idx}" data-online="${online?1:0}" class="row-server${highLoad?' high-load':''}" style="cursor:${rowCursor};${online?'':'opacity:.65;'}">
html += `<tr data-idx="${idx}" data-online="${online?1:0}" class="row-server${highLoad?' high-load':''}${osClass(s.os)}" style="cursor:${rowCursor};${online?'':'opacity:.65;'}">
<td>${statusPill}</td>
<td><span class="${trafficCls}" title="本月下行 | 上行 (≥500GB 触发红黄)"><span class="half in">${monthIn}</span><span class="half out">${monthOut}</span></span></td>
<td>${s.name||'-'}</td>
@ -176,7 +201,7 @@ function renderServersCards(){
const buckets = `<div class=\"buckets\">${bucket(p1)}${bucket(p2)}${bucket(p3)}</div>`;
// 唯一 key 已附加为 s._key如需使用
const highLoad = online && ( (s.cpu||0)>=90 || (memPct)>=90 || (hddPct)>=90 );
html += `<div class=\"card${online?'':' offline'}${highLoad?' high-load':''}\" data-idx=\"${idx}\" data-online=\"${online?1:0}\">\n <button class=\"expand-btn\" aria-label=\"展开\">▼</button>\n <div class=\"card-header\">\n <div class=\"card-title\">${s.name||'-'} <span class=\"tag\">${s.location||'-'}</span></div>\n ${pill}\n </div>\n <div class=\"kvlist\">\n <div><span class=\"key\">负载</span><span>${s.load_1==-1?'':s.load_1?.toFixed(2)}</span></div>\n <div><span class=\"key\">在线</span><span>${s.uptime||'-'}</span></div>\n <div><span class=\"key\">月流量</span><span><span class=\"${trafficCls}\" title=\"本月下行 | 上行 (≥500GB 触发红黄)\"><span class=\"half in\">${monthIn}</span><span class=\"half out\">${monthOut}</span></span></span></div>\n <div><span class=\"key\">网络</span><span>${netNow}</span></div>\n <div><span class=\"key\">总流量</span><span>${netTotal}</span></div>\n <div><span class=\"key\">CPU</span><span>${s.cpu||0}%</span></div>\n <div><span class=\"key\">内存</span><span>${memPct.toFixed(0)}%</span></div>\n <div><span class=\"key\">硬盘</span><span>${hddPct.toFixed(0)}%</span></div>\n </div>\n ${buckets}\n <div class=\"expand-area\">\n <div style=\"font-size:.65rem;opacity:.7;margin-top:.3rem\">${online?'点击卡片可查看详情':'离线,不可查看详情'}</div>\n </div>\n </div>`;
html += `<div class=\"card${online?'':' offline'}${highLoad?' high-load':''}${osClass(s.os)}\" data-idx=\"${idx}\" data-online=\"${online?1:0}\">\n <button class=\"expand-btn\" aria-label=\"展开\">▼</button>\n <div class=\"card-header\">\n <div class=\"card-title\">${s.name||'-'} <span class=\"tag\">${s.location||'-'}</span></div>\n ${pill}\n </div>\n <div class=\"kvlist\">\n <div><span class=\"key\">负载</span><span>${s.load_1==-1?'':s.load_1?.toFixed(2)}</span></div>\n <div><span class=\"key\">在线</span><span>${s.uptime||'-'}</span></div>\n <div><span class=\"key\">月流量</span><span><span class=\"${trafficCls}\" title=\"本月下行 | 上行 (≥500GB 触发红黄)\"><span class=\"half in\">${monthIn}</span><span class=\"half out\">${monthOut}</span></span></span></div>\n <div><span class=\"key\">网络</span><span>${netNow}</span></div>\n <div><span class=\"key\">总流量</span><span>${netTotal}</span></div>\n <div><span class=\"key\">CPU</span><span>${s.cpu||0}%</span></div>\n <div><span class=\"key\">内存</span><span>${memPct.toFixed(0)}%</span></div>\n <div><span class=\"key\">硬盘</span><span>${hddPct.toFixed(0)}%</span></div>\n </div>\n ${buckets}\n <div class=\"expand-area\">\n <div style=\"font-size:.65rem;opacity:.7;margin-top:.3rem\">${online?'点击卡片可查看详情':'离线,不可查看详情'}</div>\n </div>\n </div>`;
});
wrap.innerHTML = html || '<div class="muted" style="font-size:.75rem;text-align:center;padding:1rem;">无数据</div>';
wrap.querySelectorAll('.card').forEach(card=>{