From 66e617933277b277599573d4a0ceee01c92f5937 Mon Sep 17 00:00:00 2001 From: cppla Date: Thu, 9 Jul 2026 10:10:08 +0800 Subject: [PATCH] update --- clients/client-linux.py | 52 +++++++++ clients/client-psutil.py | 46 ++++++++ server/src/main.cpp | 69 +++++++++-- server/src/main.h | 2 + web/css/app.css | 15 ++- web/index.html | 4 +- web/js/app.js | 246 +++++++++++++++++++++++++++++++-------- 7 files changed, 376 insertions(+), 58 deletions(-) diff --git a/clients/client-linux.py b/clients/client-linux.py index b79e10b..d96e7b3 100755 --- a/clients/client-linux.py +++ b/clients/client-linux.py @@ -133,6 +133,54 @@ def get_cpu(): result = 100-(t[len(t)-1]*100.00/st) return round(result, 1) +def get_cpu_cores(): + try: + with open('/proc/stat') as f: + cores = sum(1 for line in f if re.match(r'^cpu\d+\s', line)) + if cores > 0: + return cores + except Exception: + pass + return os.cpu_count() or 0 + +def normalize_cpu_model(value): + return re.sub(r'\s+', ' ', str(value or '')).strip()[:160] + +def is_generic_cpu_model(value): + v = normalize_cpu_model(value).lower().replace('-', '').replace('_', '').replace(' ', '') + return v in ('', 'unknown', 'x8664', 'amd64', 'i386', 'i686', 'aarch64', 'arm64') or v.startswith('armv') + +def get_cpu_model(): + try: + fallback = "" + with open('/proc/cpuinfo') as f: + for line in f: + if ':' not in line: + continue + key, value = line.split(':', 1) + key = key.strip().lower() + value = normalize_cpu_model(value) + if not value: + continue + if key == 'model name': + return value + if key in ('hardware', 'processor') and not value.isdigit() and not fallback: + fallback = value + if fallback: + return fallback + except Exception: + pass + for cmd in ('sysctl -n machdep.cpu.brand_string', 'sysctl -n hw.model'): + try: + value = subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, timeout=2).decode().strip() + value = normalize_cpu_model(value) + if value: + return value + except Exception: + pass + value = normalize_cpu_model(platform.processor()) + return "" if is_generic_cpu_model(value) else value + def liuliang(): NET_IN = 0 NET_OUT = 0 @@ -505,6 +553,8 @@ if __name__ == '__main__': print(data) raise socket.error + CPUCores = get_cpu_cores() + CPUModel = get_cpu_model() while True: CPU = get_cpu() NET_IN, NET_OUT = liuliang() @@ -530,6 +580,8 @@ if __name__ == '__main__': array['hdd_total'] = HDDTotal array['hdd_used'] = HDDUsed array['cpu'] = CPU + array['cpu_cores'] = CPUCores + array['cpu_model'] = CPUModel array['network_rx'] = netSpeed.get("netrx") array['network_tx'] = netSpeed.get("nettx") array['network_in'] = NET_IN diff --git a/clients/client-psutil.py b/clients/client-psutil.py index 0e4b31f..3ae856e 100755 --- a/clients/client-psutil.py +++ b/clients/client-psutil.py @@ -28,6 +28,7 @@ import sys import json import errno import psutil +import subprocess import threading import platform from queue import Queue @@ -92,6 +93,47 @@ def get_hdd(): def get_cpu(): return psutil.cpu_percent(interval=INTERVAL) +def get_cpu_cores(): + return psutil.cpu_count(logical=True) or 0 + +def normalize_cpu_model(value): + return " ".join(str(value or "").split())[:160] + +def is_generic_cpu_model(value): + v = normalize_cpu_model(value).lower().replace('-', '').replace('_', '').replace(' ', '') + return v in ('', 'unknown', 'x8664', 'amd64', 'i386', 'i686', 'aarch64', 'arm64') or v.startswith('armv') + +def get_cpu_model(): + try: + fallback = "" + with open('/proc/cpuinfo') as f: + for line in f: + if ':' not in line: + continue + key, value = line.split(':', 1) + key = key.strip().lower() + value = normalize_cpu_model(value) + if not value: + continue + if key == 'model name': + return value + if key in ('hardware', 'processor') and not value.isdigit() and not fallback: + fallback = value + if fallback: + return fallback + except Exception: + pass + for cmd in ('sysctl -n machdep.cpu.brand_string', 'sysctl -n hw.model'): + try: + value = subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, timeout=2).decode().strip() + value = normalize_cpu_model(value) + if value: + return value + except Exception: + pass + value = normalize_cpu_model(platform.processor()) + return "" if is_generic_cpu_model(value) else value + def liuliang(): NET_IN = 0 NET_OUT = 0 @@ -472,6 +514,8 @@ if __name__ == '__main__': print(data) raise socket.error + CPUCores = get_cpu_cores() + CPUModel = get_cpu_model() while 1: CPU = get_cpu() NET_IN, NET_OUT = liuliang() @@ -498,6 +542,8 @@ if __name__ == '__main__': array['hdd_total'] = HDDTotal array['hdd_used'] = HDDUsed array['cpu'] = CPU + array['cpu_cores'] = CPUCores + array['cpu_model'] = CPUModel array['network_rx'] = netSpeed.get("netrx") array['network_tx'] = netSpeed.get("nettx") array['network_in'] = NET_IN diff --git a/server/src/main.cpp b/server/src/main.cpp index 0e26205..7dd1c97 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp @@ -17,6 +17,48 @@ static volatile int gs_Running = 1; static volatile int gs_ReloadConfig = 0; +static void JsonEscape(const char *pSrc, char *pDst, int DstSize) +{ + if(!pDst || DstSize <= 0) + return; + int Out = 0; + if(!pSrc) + { + pDst[0] = 0; + return; + } + for(const unsigned char *p = (const unsigned char *)pSrc; *p && Out < DstSize - 1; ++p) + { + const char *pEsc = 0; + switch(*p) + { + case '"': pEsc = "\\\""; break; + case '\\': pEsc = "\\\\"; break; + case '\b': pEsc = "\\b"; break; + case '\f': pEsc = "\\f"; break; + case '\n': pEsc = "\\n"; break; + case '\r': pEsc = "\\r"; break; + case '\t': pEsc = "\\t"; break; + default: break; + } + if(pEsc) + { + for(const char *q = pEsc; *q && Out < DstSize - 1; ++q) + pDst[Out++] = *q; + } + else if(*p < 0x20) + { + if(Out < DstSize - 6) + Out += snprintf(pDst + Out, DstSize - Out, "\\u%04x", *p); + else + break; + } + else + pDst[Out++] = *p; + } + pDst[Out] = 0; +} + static int64_t ParseOpenSSLEnddate(const char *line) { // line format: notAfter=Aug 12 23:59:59 2025 GMT @@ -380,6 +422,10 @@ int CMain::HandleMessage(int ClientNetID, char *pMessage) pClient->m_Stats.m_IOWrite = rStart["io_write"].u.integer; if(rStart["cpu"].type) pClient->m_Stats.m_CPU = rStart["cpu"].u.dbl; + if(rStart["cpu_cores"].type) + pClient->m_Stats.m_CPUCores = rStart["cpu_cores"].u.integer; + if(rStart["cpu_model"].type == json_string) + str_copy(pClient->m_Stats.m_aCPUModel, rStart["cpu_model"].u.string.ptr, sizeof(pClient->m_Stats.m_aCPUModel)); if(rStart["online4"].type && pClient->m_ClientNetType == NETTYPE_IPV6) pClient->m_Stats.m_Online4 = rStart["online4"].u.boolean; if(rStart["online6"].type && pClient->m_ClientNetType == NETTYPE_IPV4) @@ -635,28 +681,38 @@ void CMain::JSONUpdateThread(void *pUser) pClients[i].m_LastNetworkOUT = pClients[i].m_Stats.m_NetworkOUT; } + char aCustomEsc[2048] = { 0 }; + char aOSEsc[128] = { 0 }; + char aCPUModelEsc[384] = { 0 }; + JsonEscape(pClients[i].m_Stats.m_aCustom, aCustomEsc, sizeof(aCustomEsc)); + JsonEscape(pClients[i].m_Stats.m_aOS[0] ? pClients[i].m_Stats.m_aOS : "", aOSEsc, sizeof(aOSEsc)); + JsonEscape(pClients[i].m_Stats.m_aCPUModel[0] ? pClients[i].m_Stats.m_aCPUModel : "", aCPUModelEsc, sizeof(aCPUModelEsc)); 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\", \"os\": \"%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, \"cpu_cores\": %" PRId64 ", \"cpu_model\": \"%s\", \"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, pClients[i].m_Stats.m_time_10010, pClients[i].m_Stats.m_time_189, pClients[i].m_Stats.m_time_10086,pClients[i].m_Stats.m_tcpCount,pClients[i].m_Stats.m_udpCount,pClients[i].m_Stats.m_processCount,pClients[i].m_Stats.m_threadCount, - pClients[i].m_Stats.m_NetworkRx, pClients[i].m_Stats.m_NetworkTx, pClients[i].m_Stats.m_NetworkIN, pClients[i].m_Stats.m_NetworkOUT, (int)pClients[i].m_Stats.m_CPU, pClients[i].m_Stats.m_MemTotal, pClients[i].m_Stats.m_MemUsed, + pClients[i].m_Stats.m_NetworkRx, pClients[i].m_Stats.m_NetworkTx, pClients[i].m_Stats.m_NetworkIN, pClients[i].m_Stats.m_NetworkOUT, (int)pClients[i].m_Stats.m_CPU, pClients[i].m_Stats.m_CPUCores, aCPUModelEsc, pClients[i].m_Stats.m_MemTotal, pClients[i].m_Stats.m_MemUsed, pClients[i].m_Stats.m_SwapTotal, pClients[i].m_Stats.m_SwapUsed, pClients[i].m_Stats.m_HDDTotal, pClients[i].m_Stats.m_HDDUsed, 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_aOS[0] ? pClients[i].m_Stats.m_aOS : ""); + aCustomEsc, + aOSEsc); 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 ", \"os\": \"%s\" },\n", + char aOSEsc[128] = { 0 }; + char aCPUModelEsc[384] = { 0 }; + JsonEscape(pClients[i].m_Stats.m_aOS[0] ? pClients[i].m_Stats.m_aOS : "", aOSEsc, sizeof(aOSEsc)); + JsonEscape(pClients[i].m_Stats.m_aCPUModel[0] ? pClients[i].m_Stats.m_aCPUModel : "", aCPUModelEsc, sizeof(aCPUModelEsc)); + 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\", \"cpu_model\": \"%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 : ""); + aOSEsc, aCPUModelEsc); pBuf += strlen(pBuf); } } @@ -1118,4 +1174,3 @@ int main(int argc, const char *argv[]) return RetVal; } - diff --git a/server/src/main.h b/server/src/main.h index 9882857..9e0d7be 100644 --- a/server/src/main.h +++ b/server/src/main.h @@ -76,8 +76,10 @@ class CMain int64_t m_threadCount; int64_t m_IORead; int64_t m_IOWrite; + int64_t m_CPUCores; double m_CPU; char m_aCustom[1024]; + char m_aCPUModel[192]; // OS name reported by client (e.g. linux/windows/darwin/freebsd) char m_aOS[64]; // Options diff --git a/web/css/app.css b/web/css/app.css index 9f42b66..8539998 100644 --- a/web/css/app.css +++ b/web/css/app.css @@ -151,6 +151,10 @@ body.light .virt-pill{background:color-mix(in srgb,var(--virt) 12%,#fff);color:c .node-name{display:inline-block;max-width:160px;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;font-weight:750;letter-spacing:.15px;color:var(--text)} .row-server[data-online="0"] .node-name{color:color-mix(in srgb,var(--text) 72%,var(--text-dim));font-weight:650} .row-server:hover .node-name{color:var(--accent)} +.load-with-cores{display:inline-flex;align-items:flex-start;gap:.28rem;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:700;line-height:1;white-space:nowrap} +.load-value{display:inline-block;min-width:2.55rem} +.load-core-bubble{display:inline-flex;align-items:center;justify-content:center;height:14px;min-width:24px;padding:0 5px;margin-top:-6px;border-radius:999px;border:1px solid color-mix(in srgb,var(--accent) 34%,var(--border));background:color-mix(in srgb,var(--accent) 13%,var(--bg-alt));color:color-mix(in srgb,var(--accent) 74%,#fff);font-size:9px;font-weight:800;line-height:1;letter-spacing:0;box-shadow:0 1px 3px rgba(0,0,0,.22)} +body.light .load-core-bubble{background:color-mix(in srgb,var(--accent) 8%,#fff);color:color-mix(in srgb,var(--accent) 78%,#111827);box-shadow:0 1px 2px rgba(15,23,42,.08)} /* buckets CU/CT/CM (simple version) */ .buckets{display:flex;align-items:flex-end;gap:8px;min-width:140px} @@ -261,6 +265,10 @@ table.data tbody tr[class*="os-"]:hover{background:linear-gradient(180deg, color /* 弹窗 OS 着色:左侧彩条 + 渐变与卡片一致 */ /* 取消弹窗背景着色,改为仅在标题展示系统胶囊 */ .os-chip{display:inline-flex;align-items:center;padding:2px 8px;margin-left:.5rem;border-radius:999px;font-size:12px;font-weight:600;line-height:1.2;background:var(--os-color, var(--border));color:#fff;border:0;white-space:nowrap;} +.spec-chip{display:inline-flex;align-items:center;padding:2px 8px;margin-left:.4rem;border-radius:999px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;font-weight:750;line-height:1.2;background:color-mix(in srgb,var(--accent) 16%,var(--bg-alt));color:color-mix(in srgb,var(--accent) 72%,#fff);border:1px solid color-mix(in srgb,var(--accent) 38%,var(--border));white-space:nowrap;} +body.light .spec-chip{background:color-mix(in srgb,var(--accent) 10%,#fff);color:color-mix(in srgb,var(--accent) 78%,#111827)} +.cpu-model-chip{display:inline-block;max-width:min(240px,58vw);overflow:hidden;text-overflow:ellipsis;vertical-align:middle;padding:2px 8px;margin-left:.4rem;border-radius:999px;border:1px solid color-mix(in srgb,var(--text-dim) 34%,var(--border));background:color-mix(in srgb,var(--text-dim) 12%,var(--bg-alt));color:color-mix(in srgb,var(--text) 84%,var(--text-dim));font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;font-weight:700;line-height:1.2;white-space:nowrap;} +body.light .cpu-model-chip{background:color-mix(in srgb,var(--text-dim) 9%,#fff);color:color-mix(in srgb,var(--text) 82%,#111827)} /* 为常见系统赋色 */ .os-linux{--os-color: rgba(16,185,129,.85);} /* 绿色 (通用 Linux,保持不变) */ @@ -285,7 +293,9 @@ table.data tbody tr[class*="os-"]:hover{background:linear-gradient(180deg, color /* 旧进度条相关样式已清理 */ .cards .card-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem;} -.cards .card-title{font-weight:600;font-size:.95rem;} +.cards .card-title{display:flex;align-items:center;flex-wrap:wrap;gap:.24rem .34rem;min-width:0;font-weight:600;font-size:.95rem;} +.cards .card-spec-chip{display:inline-flex;align-items:center;justify-content:center;height:16px;padding:0 6px;border-radius:999px;border:1px solid color-mix(in srgb,var(--accent) 34%,var(--border));background:color-mix(in srgb,var(--accent) 13%,var(--bg-alt));color:color-mix(in srgb,var(--accent) 74%,#fff);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;font-weight:800;line-height:1;letter-spacing:0;white-space:nowrap} +body.light .cards .card-spec-chip{background:color-mix(in srgb,var(--accent) 8%,#fff);color:color-mix(in srgb,var(--accent) 78%,#111827)} .cards .tag{font-size:.65rem;padding:.15rem .4rem;border-radius:4px;background:var(--surface);color:var(--text);letter-spacing:.5px;} .cards .status-pill{font-size:.6rem;padding:.2rem .45rem;border-radius:999px;font-weight:500;} .cards .status-pill.on{background:var(--ok);color:#fff;} @@ -372,7 +382,8 @@ th[data-sort].sorted-desc:after{border-top-color:var(--accent);opacity:1} .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} +.modal-title{padding-right:2.2rem;display:flex;align-items:center;flex-wrap:wrap;gap:.32rem .4rem} +.modal-title .os-chip,.modal-title .spec-chip,.modal-title .cpu-model-chip{margin-left:0} .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} .detail-section h4{margin:0;font-size:13px;color:var(--text-dim);font-weight:700;letter-spacing:.4px} diff --git a/web/index.html b/web/index.html index 57be97a..37be1c2 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@ 云监控 - +
@@ -200,6 +200,6 @@ ServerStatus中文版 - + diff --git a/web/js/app.js b/web/js/app.js index 71f3ccd..e356592 100644 --- a/web/js/app.js +++ b/web/js/app.js @@ -6,6 +6,7 @@ const S = { loadHist: {}, openDetailKey: null, activeTab: 'servers', + layoutCompact: null, osOptionsSignature: '', suppressStatsReloadUntil: 0, filters: { query: '', status: 'all', os: 'all', sort: 'name', dir: 'desc' }, @@ -25,6 +26,32 @@ const esc = (v) => String(v ?? '').replace(/[&<>"']/g, ch => ({'&':'&','<':' const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : 0; +function debounce(fn, wait){ + let timer = 0; + const wrapped = (...args) => { + clearTimeout(timer); + timer = setTimeout(() => { + timer = 0; + fn(...args); + }, wait); + }; + wrapped.cancel = () => { + clearTimeout(timer); + timer = 0; + }; + return wrapped; +} +function isCompactLayout(){ return window.innerWidth <= 700; } +function clearHTML(id){ + const el = $(id); + if(el && el.innerHTML) el.innerHTML = ''; +} +function htmlElement(html){ + const tpl = document.createElement('template'); + tpl.innerHTML = html.trim(); + return tpl.content.firstElementChild; +} + function humanMinMBFromKB(kb){ return humanBytes(num(kb) * 1000, 1000 * 1000); } function humanMinMBFromMB(mb){ return humanBytes(num(mb) * 1000 * 1000, 1000 * 1000); } function humanMinMBFromB(bytes){ return humanBytes(num(bytes), 1000 * 1000); } @@ -39,6 +66,27 @@ function humanBytes(bytes, minUnit){ const out = value >= 100 ? value.toFixed(0) : value.toFixed(1); return out + units[index]; } +function cpuCores(s){ + const value = Number(s?.cpu_cores ?? s?.cpu_core ?? s?.cpu_count ?? s?.cores ?? 0); + return Number.isFinite(value) && value > 0 ? Math.round(value) : 0; +} +function cpuCoreLabel(s){ + const cores = cpuCores(s); + return cores > 0 ? `${cores}C` : ''; +} +function memoryGbLabel(s){ + const kb = Number(s?.memory_total ?? 0); + if(!Number.isFinite(kb) || kb <= 0) return ''; + return `${Math.max(1, Math.round(kb / 1024 / 1024))}G`; +} +function serverSpecLabel(s){ + const cores = cpuCoreLabel(s); + if(!cores) return ''; + return `${cores}${memoryGbLabel(s)}`; +} +function cpuModelLabel(s){ + return String(s?.cpu_model || '').trim(); +} function humanAgo(ts){ if(!ts) return '-'; const sec = Math.max(0, Math.floor(Date.now() / 1000 - Number(ts))); @@ -201,19 +249,65 @@ function visibleServers(){ function render(){ $('notice').style.display = 'none'; - normalizeServersToolbarState(); renderOverview(); - renderOsOptions(); - renderServers(); - renderServersCards(); - renderMonitors(); - renderMonitorsCards(); - renderSSL(); - renderSSLCards(); + renderActivePanel(); updateTime(); if(S.openDetailKey) refreshDetail(); } +function renderActivePanel(){ + normalizeServersToolbarState(); + if(S.activeTab === 'servers') renderServersView(); + else if(S.activeTab === 'monitors') renderMonitorsView(); + else if(S.activeTab === 'ssl') renderSSLView(); +} + +function renderServersView(){ + const compact = isCompactLayout(); + S.layoutCompact = compact; + normalizeServersToolbarState(); + renderOsOptions(); + if(compact){ + clearHTML('serversBody'); + renderServersCards(); + }else{ + clearHTML('serversCards'); + renderServers(); + } +} + +function renderMonitorsView(){ + const compact = isCompactLayout(); + S.layoutCompact = compact; + if(compact){ + clearHTML('monitorsBody'); + renderMonitorsCards(); + }else{ + clearHTML('monitorsCards'); + renderMonitors(); + } +} + +function renderSSLView(){ + const compact = isCompactLayout(); + S.layoutCompact = compact; + if(compact){ + clearHTML('sslBody'); + renderSSLCards(); + }else{ + clearHTML('sslCards'); + renderSSL(); + } +} + +const scheduleServersRender = debounce(() => { + if(S.activeTab === 'servers') renderServersView(); +}, 160); +function renderServersViewNow(){ + scheduleServersRender.cancel(); + if(S.activeTab === 'servers') renderServersView(); +} + function renderOverview(){ const total = S.servers.length; const online = S.servers.filter(s => metrics(s).online).length; @@ -291,6 +385,12 @@ function trafficCaps(s, small){ const heavy = m.traffic >= 1000 * 1000 * 1000 * 1000; return `${humanMinMBFromB(m.monthIn)}${humanMinMBFromB(m.monthOut)}`; } +function loadCellHTML(s){ + const load = s.load_1 === -1 ? '–' : num(s.load_1).toFixed(2); + const cores = cpuCoreLabel(s); + if(!cores) return esc(load); + return `${esc(load)}${esc(cores)}`; +} function gaugeHTML(type, value){ const pct = clamp(num(value), 0, 100); const thresholds = { @@ -342,47 +442,79 @@ function buckets(s){ }).join('')}`; } +function serverRowSignature(s, m){ + const text = [ + m.online ? 1 : 0, m.rowLevel, s.online4, s.online6, s.os, + s.name, s.type, s.location, s.uptime, s.load_1, cpuCores(s), + s.network_rx, s.network_tx, s.network_in, s.network_out, s.last_network_in, s.last_network_out, + s.cpu, s.memory_used, s.memory_total, s.hdd_used, s.hdd_total, + s.ping_10010, s.ping_189, s.ping_10086 + ].map(v => String(v ?? '')).join('\u001f'); + return `${stableHash(text).toString(36)}:${text.length}`; +} + +function serverRowHTML(s, m, signature){ + const netNow = `${humanMinKBFromB(s.network_rx)} | ${humanMinKBFromB(s.network_tx)}`; + const netTotal = `${humanMinMBFromB(s.network_in)} | ${humanMinMBFromB(s.network_out)}`; + const alertClass = m.rowLevel ? ` alert-${m.rowLevel}` : ''; + return ` + ${protoPill(s)} + ${trafficCaps(s)} + ${esc(s.name || '-')} + ${virtPill(s.type)} + ${esc(s.location || '-')} + ${esc(s.uptime || '-')} + ${loadCellHTML(s)} + ${netNow} + ${netTotal} + ${m.online ? gaugeHTML('cpu', s.cpu) : '-'} + ${m.online ? gaugeHTML('mem', m.memPct) : '-'} + ${m.online ? gaugeHTML('hdd', m.hddPct) : '-'} + ${buckets(s)} + `; +} + function renderServers(){ const rows = visibleServers(); + const tbody = $('serversBody'); document.querySelectorAll('#serversTable th[data-sort]').forEach(th => { th.classList.toggle('sorted-asc', th.dataset.sort === S.filters.sort && S.filters.dir === 'asc'); th.classList.toggle('sorted-desc', th.dataset.sort === S.filters.sort && S.filters.dir === 'desc'); }); - $('serversBody').innerHTML = rows.map(s => { + if(!rows.length){ + if(tbody.dataset.empty !== 'servers'){ + tbody.innerHTML = `无数据`; + tbody.dataset.empty = 'servers'; + } + return; + } + delete tbody.dataset.empty; + tbody.querySelector('.empty-row')?.remove(); + const existing = new Map([...tbody.querySelectorAll('tr.row-server')].map(row => [row.dataset.key, row])); + const desiredKeys = new Set(rows.map(s => s._key)); + existing.forEach((row, key) => { if(!desiredKeys.has(key)) row.remove(); }); + rows.forEach(s => { + const key = String(s._key); const m = metrics(s); - const netNow = `${humanMinKBFromB(s.network_rx)} | ${humanMinKBFromB(s.network_tx)}`; - const netTotal = `${humanMinMBFromB(s.network_in)} | ${humanMinMBFromB(s.network_out)}`; - const alertClass = m.rowLevel ? ` alert-${m.rowLevel}` : ''; - return ` - ${protoPill(s)} - ${trafficCaps(s)} - ${esc(s.name || '-')} - ${virtPill(s.type)} - ${esc(s.location || '-')} - ${esc(s.uptime || '-')} - ${s.load_1 === -1 ? '–' : num(s.load_1).toFixed(2)} - ${netNow} - ${netTotal} - ${m.online ? gaugeHTML('cpu', s.cpu) : '-'} - ${m.online ? gaugeHTML('mem', m.memPct) : '-'} - ${m.online ? gaugeHTML('hdd', m.hddPct) : '-'} - ${buckets(s)} - `; - }).join('') || `无数据`; - document.querySelectorAll('#serversBody .row-server').forEach(row => row.addEventListener('click', () => { - if(row.dataset.online !== '1') return; - openDetail(row.dataset.key); - })); + const signature = serverRowSignature(s, m); + const current = existing.get(key); + let row = current; + if(!row || row.dataset.sig !== signature){ + row = htmlElement(serverRowHTML(s, m, signature)); + if(current) current.replaceWith(row); + } + tbody.appendChild(row); + }); } function renderServersCards(){ const wrap = $('serversCards'); - if(window.innerWidth > 700){ wrap.innerHTML = ''; return; } wrap.innerHTML = visibleServers().map(s => { const m = metrics(s); const alertClass = m.rowLevel ? ` alert-${m.rowLevel}` : ''; + const spec = serverSpecLabel(s); return `
-
${esc(s.name || '-')} ${esc(s.location || '-')}
${protoPill(s)}
+
${esc(s.name || '-')}${spec ? `${esc(spec)}` : ''} ${esc(s.location || '-')}
${protoPill(s)}
负载${s.load_1 === -1 ? '–' : num(s.load_1).toFixed(2)}
在线${esc(s.uptime || '-')}
@@ -394,10 +526,6 @@ function renderServersCards(){ ${buckets(s)}
`; }).join('') || '
无数据
'; - wrap.querySelectorAll('.card').forEach(card => card.addEventListener('click', () => { - if(card.dataset.online !== '1') return; - openDetail(card.dataset.key); - })); } function parseCustom(str){ @@ -458,7 +586,9 @@ function refreshDetail(){ if(!s){ closeDetail(); return; } const m = metrics(s); const title = $('detailTitle'); - title.innerHTML = `${esc(s.name || '-')} 详情${s.os ? `${esc(osLabel(s.os))}` : ''}`; + const spec = serverSpecLabel(s); + const cpuModel = cpuModelLabel(s); + title.innerHTML = `${esc(s.name || '-')} 详情${s.os ? `${esc(osLabel(s.os))}` : ''}${spec ? `${esc(spec)}` : ''}${cpuModel ? `${esc(cpuModel)}` : ''}`; const modalBox = document.querySelector('#detailModal .modal-box'); if(modalBox){ modalBox.classList.remove('high-load'); @@ -563,9 +693,10 @@ function bindTabs(){ if(e.target.tagName !== 'BUTTON') return; const tab = e.target.dataset.tab; S.activeTab = tab; + scheduleServersRender.cancel(); document.querySelectorAll('.nav button').forEach(btn => btn.classList.toggle('active', btn === e.target)); document.querySelectorAll('.panel').forEach(panel => panel.classList.toggle('active', panel.id === 'panel-' + tab)); - normalizeServersToolbarState(); + renderActivePanel(); if(tab === 'config') ensureAdminChecked(); }); } @@ -584,30 +715,43 @@ function bindTheme(){ }); } function bindFilters(){ - $('serverSearch').addEventListener('input', e => { S.filters.query = e.target.value; renderServers(); renderServersCards(); }); + $('serverSearch').addEventListener('input', e => { S.filters.query = e.target.value; scheduleServersRender(); }); $('statusFilter').addEventListener('click', e => { if(e.target.tagName !== 'BUTTON') return; S.filters.status = e.target.dataset.filter; document.querySelectorAll('#statusFilter button').forEach(btn => btn.classList.toggle('active', btn === e.target)); - renderServers(); renderServersCards(); + renderServersViewNow(); }); - $('osFilter').addEventListener('change', e => { S.filters.os = e.target.value; renderServers(); renderServersCards(); }); + $('osFilter').addEventListener('change', e => { S.filters.os = e.target.value; renderServersViewNow(); }); $('osFilter').addEventListener('blur', renderOsOptions); - $('sortSelect').addEventListener('change', e => { S.filters.sort = e.target.value; renderServers(); renderServersCards(); }); + $('sortSelect').addEventListener('change', e => { S.filters.sort = e.target.value; renderServersViewNow(); }); $('sortDirection').addEventListener('click', () => { S.filters.dir = S.filters.dir === 'desc' ? 'asc' : 'desc'; $('sortDirection').textContent = S.filters.dir === 'desc' ? '降序' : '升序'; - renderServers(); renderServersCards(); + renderServersViewNow(); }); document.querySelectorAll('#serversTable th[data-sort]').forEach(th => th.addEventListener('click', () => { if(S.filters.sort === th.dataset.sort) S.filters.dir = S.filters.dir === 'desc' ? 'asc' : 'desc'; else S.filters.sort = th.dataset.sort; $('sortSelect').value = S.filters.sort; $('sortDirection').textContent = S.filters.dir === 'desc' ? '降序' : '升序'; - renderServers(); renderServersCards(); + renderServersViewNow(); })); } +function bindServerInteractions(){ + $('serversBody').addEventListener('click', e => { + const row = e.target.closest('.row-server'); + if(!row || row.dataset.online !== '1') return; + openDetail(row.dataset.key); + }); + $('serversCards').addEventListener('click', e => { + const card = e.target.closest('.card[data-key]'); + if(!card || card.dataset.online !== '1') return; + openDetail(card.dataset.key); + }); +} + function adminHeaders(){ return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${S.admin.token}` }; } @@ -916,11 +1060,19 @@ function bindAdmin(){ $('detailClose').addEventListener('click', closeDetail); $('detailModal').addEventListener('click', e => { if(e.target.id === 'detailModal') closeDetail(); }); -window.addEventListener('resize', () => { renderServersCards(); renderMonitorsCards(); renderSSLCards(); if(S.openDetailKey) refreshDetail(); }); +window.addEventListener('resize', debounce(() => { + const compact = isCompactLayout(); + if(S.layoutCompact !== compact){ + S.layoutCompact = compact; + renderActivePanel(); + } + if(S.openDetailKey) refreshDetail(); +}, 120)); bindTabs(); bindTheme(); bindFilters(); +bindServerInteractions(); bindAdmin(); fetchData(); setInterval(fetchData, 1000);