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 += `