diff --git a/clients/client-linux.py b/clients/client-linux.py index 5bb246b..db8cfb2 100755 --- a/clients/client-linux.py +++ b/clients/client-linux.py @@ -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'] = "
".join(f"{k}\\t解析: {v['dns_time']}\\t连接: {v['connect_time']}\\t下载: {v['download_time']}\\t在线率: {v['online_rate']*100:.1f}%" for k, v in monitorServer.items()) s.send(byte_str("update " + json.dumps(array) + "\n")) except KeyboardInterrupt: diff --git a/clients/client-psutil.py b/clients/client-psutil.py index 972586f..2b99e8c 100755 --- a/clients/client-psutil.py +++ b/clients/client-psutil.py @@ -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'] = "
".join(f"{k}\\t解析: {v['dns_time']}\\t连接: {v['connect_time']}\\t下载: {v['download_time']}\\t在线率: {v['online_rate']*100:.2f}%" 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'] = "
".join("{}\t解析: {}\t连接: {}\t下载: {}\t在线率: {:.2f}%".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 diff --git a/server/src/main.cpp b/server/src/main.cpp index 0943bed..bcb4c20 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp @@ -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); } } diff --git a/server/src/main.h b/server/src/main.h index 3deffdd..9882857 100644 --- a/server/src/main.h +++ b/server/src/main.h @@ -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; diff --git a/web/css/app.css b/web/css/app.css index 0480d0b..6b7009e 100644 --- a/web/css/app.css +++ b/web/css/app.css @@ -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;} diff --git a/web/js/app.js b/web/js/app.js index 104544a..5f44aef 100644 --- a/web/js/app.js +++ b/web/js/app.js @@ -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 += ` + html += ` ${statusPill} ${monthIn}${monthOut} ${s.name||'-'} @@ -176,7 +201,7 @@ function renderServersCards(){ const buckets = `
${bucket(p1)}${bucket(p2)}${bucket(p3)}
`; // 唯一 key 已附加为 s._key(如需使用) const highLoad = online && ( (s.cpu||0)>=90 || (memPct)>=90 || (hddPct)>=90 ); - html += `
\n \n
\n
${s.name||'-'} ${s.location||'-'}
\n ${pill}\n
\n
\n
负载${s.load_1==-1?'–':s.load_1?.toFixed(2)}
\n
在线${s.uptime||'-'}
\n
月流量${monthIn}${monthOut}
\n
网络${netNow}
\n
总流量${netTotal}
\n
CPU${s.cpu||0}%
\n
内存${memPct.toFixed(0)}%
\n
硬盘${hddPct.toFixed(0)}%
\n
\n ${buckets}\n
\n
${online?'点击卡片可查看详情':'离线,不可查看详情'}
\n
\n
`; + html += `
\n \n
\n
${s.name||'-'} ${s.location||'-'}
\n ${pill}\n
\n
\n
负载${s.load_1==-1?'–':s.load_1?.toFixed(2)}
\n
在线${s.uptime||'-'}
\n
月流量${monthIn}${monthOut}
\n
网络${netNow}
\n
总流量${netTotal}
\n
CPU${s.cpu||0}%
\n
内存${memPct.toFixed(0)}%
\n
硬盘${hddPct.toFixed(0)}%
\n
\n ${buckets}\n
\n
${online?'点击卡片可查看详情':'离线,不可查看详情'}
\n
\n
`; }); wrap.innerHTML = html || '
无数据
'; wrap.querySelectorAll('.card').forEach(card=>{