This commit is contained in:
cppla
2026-07-09 10:10:08 +08:00
parent 6ad3a8c294
commit 66e6179332
7 changed files with 376 additions and 58 deletions
+52
View File
@@ -133,6 +133,54 @@ def get_cpu():
result = 100-(t[len(t)-1]*100.00/st) result = 100-(t[len(t)-1]*100.00/st)
return round(result, 1) 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(): def liuliang():
NET_IN = 0 NET_IN = 0
NET_OUT = 0 NET_OUT = 0
@@ -505,6 +553,8 @@ if __name__ == '__main__':
print(data) print(data)
raise socket.error raise socket.error
CPUCores = get_cpu_cores()
CPUModel = get_cpu_model()
while True: while True:
CPU = get_cpu() CPU = get_cpu()
NET_IN, NET_OUT = liuliang() NET_IN, NET_OUT = liuliang()
@@ -530,6 +580,8 @@ if __name__ == '__main__':
array['hdd_total'] = HDDTotal array['hdd_total'] = HDDTotal
array['hdd_used'] = HDDUsed array['hdd_used'] = HDDUsed
array['cpu'] = CPU array['cpu'] = CPU
array['cpu_cores'] = CPUCores
array['cpu_model'] = CPUModel
array['network_rx'] = netSpeed.get("netrx") array['network_rx'] = netSpeed.get("netrx")
array['network_tx'] = netSpeed.get("nettx") array['network_tx'] = netSpeed.get("nettx")
array['network_in'] = NET_IN array['network_in'] = NET_IN
+46
View File
@@ -28,6 +28,7 @@ import sys
import json import json
import errno import errno
import psutil import psutil
import subprocess
import threading import threading
import platform import platform
from queue import Queue from queue import Queue
@@ -92,6 +93,47 @@ def get_hdd():
def get_cpu(): def get_cpu():
return psutil.cpu_percent(interval=INTERVAL) 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(): def liuliang():
NET_IN = 0 NET_IN = 0
NET_OUT = 0 NET_OUT = 0
@@ -472,6 +514,8 @@ if __name__ == '__main__':
print(data) print(data)
raise socket.error raise socket.error
CPUCores = get_cpu_cores()
CPUModel = get_cpu_model()
while 1: while 1:
CPU = get_cpu() CPU = get_cpu()
NET_IN, NET_OUT = liuliang() NET_IN, NET_OUT = liuliang()
@@ -498,6 +542,8 @@ if __name__ == '__main__':
array['hdd_total'] = HDDTotal array['hdd_total'] = HDDTotal
array['hdd_used'] = HDDUsed array['hdd_used'] = HDDUsed
array['cpu'] = CPU array['cpu'] = CPU
array['cpu_cores'] = CPUCores
array['cpu_model'] = CPUModel
array['network_rx'] = netSpeed.get("netrx") array['network_rx'] = netSpeed.get("netrx")
array['network_tx'] = netSpeed.get("nettx") array['network_tx'] = netSpeed.get("nettx")
array['network_in'] = NET_IN array['network_in'] = NET_IN
+62 -7
View File
@@ -17,6 +17,48 @@
static volatile int gs_Running = 1; static volatile int gs_Running = 1;
static volatile int gs_ReloadConfig = 0; 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) static int64_t ParseOpenSSLEnddate(const char *line)
{ {
// line format: notAfter=Aug 12 23:59:59 2025 GMT // 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; pClient->m_Stats.m_IOWrite = rStart["io_write"].u.integer;
if(rStart["cpu"].type) if(rStart["cpu"].type)
pClient->m_Stats.m_CPU = rStart["cpu"].u.dbl; 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) if(rStart["online4"].type && pClient->m_ClientNetType == NETTYPE_IPV6)
pClient->m_Stats.m_Online4 = rStart["online4"].u.boolean; pClient->m_Stats.m_Online4 = rStart["online4"].u.boolean;
if(rStart["online6"].type && pClient->m_ClientNetType == NETTYPE_IPV4) 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; 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), 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_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", 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, 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_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_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_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_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_IORead, pClients[i].m_Stats.m_IOWrite,
pClients[i].m_Stats.m_aCustom, aCustomEsc,
pClients[i].m_Stats.m_aOS[0] ? pClients[i].m_Stats.m_aOS : ""); aOSEsc);
pBuf += strlen(pBuf); pBuf += strlen(pBuf);
} }
else else
{ {
// sava network traffic record to json when close client // sava network traffic record to json when close client
// last_network_in == last network in record, last_network_out == last network out record // 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_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); pBuf += strlen(pBuf);
} }
} }
@@ -1118,4 +1174,3 @@ int main(int argc, const char *argv[])
return RetVal; return RetVal;
} }
+2
View File
@@ -76,8 +76,10 @@ class CMain
int64_t m_threadCount; int64_t m_threadCount;
int64_t m_IORead; int64_t m_IORead;
int64_t m_IOWrite; int64_t m_IOWrite;
int64_t m_CPUCores;
double m_CPU; double m_CPU;
char m_aCustom[1024]; char m_aCustom[1024];
char m_aCPUModel[192];
// OS name reported by client (e.g. linux/windows/darwin/freebsd) // OS name reported by client (e.g. linux/windows/darwin/freebsd)
char m_aOS[64]; char m_aOS[64];
// Options // Options
+13 -2
View File
@@ -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)} .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[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)} .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 CU/CT/CM (simple version) */
.buckets{display:flex;align-items:flex-end;gap:8px;min-width:140px} .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 着色:左侧彩条 + 渐变与卡片一致 */
/* 取消弹窗背景着色,改为仅在标题展示系统胶囊 */ /* 取消弹窗背景着色,改为仅在标题展示系统胶囊 */
.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;} .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,保持不变) */ .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-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 .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{font-size:.6rem;padding:.2rem .45rem;border-radius:999px;font-weight:500;}
.cards .status-pill.on{background:var(--ok);color:#fff;} .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{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-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)} .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-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{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} .detail-section h4{margin:0;font-size:13px;color:var(--text-dim);font-weight:700;letter-spacing:.4px}
+2 -2
View File
@@ -6,7 +6,7 @@
<meta name="description" content="云监控,ServerStatus中文版,ServerStatus,ServerStatus cppla" /> <meta name="description" content="云监控,ServerStatus中文版,ServerStatus,ServerStatus cppla" />
<title>云监控</title> <title>云监控</title>
<link rel="icon" type="image/svg+xml" href="favicon.svg" /> <link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="stylesheet" href="css/app.css?v=20260707-5" /> <link rel="stylesheet" href="css/app.css?v=20260709-1" />
</head> </head>
<body> <body>
<header class="topbar"> <header class="topbar">
@@ -200,6 +200,6 @@
<a href="https://github.com/cppla/ServerStatus" target="_blank" rel="noopener">ServerStatus中文版</a> <a href="https://github.com/cppla/ServerStatus" target="_blank" rel="noopener">ServerStatus中文版</a>
</footer> </footer>
<script src="js/app.js?v=20260707-5" defer></script> <script src="js/app.js?v=20260709-1" defer></script>
</body> </body>
</html> </html>
+199 -47
View File
@@ -6,6 +6,7 @@ const S = {
loadHist: {}, loadHist: {},
openDetailKey: null, openDetailKey: null,
activeTab: 'servers', activeTab: 'servers',
layoutCompact: null,
osOptionsSignature: '', osOptionsSignature: '',
suppressStatsReloadUntil: 0, suppressStatsReloadUntil: 0,
filters: { query: '', status: 'all', os: 'all', sort: 'name', dir: 'desc' }, filters: { query: '', status: 'all', os: 'all', sort: 'name', dir: 'desc' },
@@ -25,6 +26,32 @@ const esc = (v) => String(v ?? '').replace(/[&<>"']/g, ch => ({'&':'&amp;','<':'
const clamp = (v, min, max) => Math.max(min, Math.min(max, v)); const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : 0; 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 humanMinMBFromKB(kb){ return humanBytes(num(kb) * 1000, 1000 * 1000); }
function humanMinMBFromMB(mb){ return humanBytes(num(mb) * 1000 * 1000, 1000 * 1000); } function humanMinMBFromMB(mb){ return humanBytes(num(mb) * 1000 * 1000, 1000 * 1000); }
function humanMinMBFromB(bytes){ return humanBytes(num(bytes), 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); const out = value >= 100 ? value.toFixed(0) : value.toFixed(1);
return out + units[index]; 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){ function humanAgo(ts){
if(!ts) return '-'; if(!ts) return '-';
const sec = Math.max(0, Math.floor(Date.now() / 1000 - Number(ts))); const sec = Math.max(0, Math.floor(Date.now() / 1000 - Number(ts)));
@@ -201,19 +249,65 @@ function visibleServers(){
function render(){ function render(){
$('notice').style.display = 'none'; $('notice').style.display = 'none';
normalizeServersToolbarState();
renderOverview(); renderOverview();
renderOsOptions(); renderActivePanel();
renderServers();
renderServersCards();
renderMonitors();
renderMonitorsCards();
renderSSL();
renderSSLCards();
updateTime(); updateTime();
if(S.openDetailKey) refreshDetail(); 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(){ function renderOverview(){
const total = S.servers.length; const total = S.servers.length;
const online = S.servers.filter(s => metrics(s).online).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; const heavy = m.traffic >= 1000 * 1000 * 1000 * 1000;
return `<span class="caps-traffic duo ${heavy ? 'heavy' : 'normal'}${small ? ' sm' : ''}" title="本月下行 | 上行"><span class="half in">${humanMinMBFromB(m.monthIn)}</span><span class="half out">${humanMinMBFromB(m.monthOut)}</span></span>`; return `<span class="caps-traffic duo ${heavy ? 'heavy' : 'normal'}${small ? ' sm' : ''}" title="本月下行 | 上行"><span class="half in">${humanMinMBFromB(m.monthIn)}</span><span class="half out">${humanMinMBFromB(m.monthOut)}</span></span>`;
} }
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 `<span class="load-with-cores" title="负载 ${esc(load)} / CPU ${esc(cores)}"><span class="load-value">${esc(load)}</span><span class="load-core-bubble">${esc(cores)}</span></span>`;
}
function gaugeHTML(type, value){ function gaugeHTML(type, value){
const pct = clamp(num(value), 0, 100); const pct = clamp(num(value), 0, 100);
const thresholds = { const thresholds = {
@@ -342,47 +442,79 @@ function buckets(s){
}).join('')}</div>`; }).join('')}</div>`;
} }
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 `<tr data-key="${esc(s._key)}" data-online="${m.online ? 1 : 0}" data-sig="${esc(signature)}" class="row-server${alertClass}${osClass(s.os)}" style="cursor:${m.online ? 'pointer' : 'default'};">
<td>${protoPill(s)}</td>
<td>${trafficCaps(s)}</td>
<td><span class="node-name" title="${esc(s.name || '-')}">${esc(s.name || '-')}</span></td>
<td>${virtPill(s.type)}</td>
<td>${esc(s.location || '-')}</td>
<td>${esc(s.uptime || '-')}</td>
<td>${loadCellHTML(s)}</td>
<td>${netNow}</td>
<td>${netTotal}</td>
<td>${m.online ? gaugeHTML('cpu', s.cpu) : '-'}</td>
<td>${m.online ? gaugeHTML('mem', m.memPct) : '-'}</td>
<td>${m.online ? gaugeHTML('hdd', m.hddPct) : '-'}</td>
<td>${buckets(s)}</td>
</tr>`;
}
function renderServers(){ function renderServers(){
const rows = visibleServers(); const rows = visibleServers();
const tbody = $('serversBody');
document.querySelectorAll('#serversTable th[data-sort]').forEach(th => { 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-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'); 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 = `<tr class="empty-row"><td colspan="13" class="muted" style="text-align:center;padding:1rem;">无数据</td></tr>`;
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 m = metrics(s);
const netNow = `${humanMinKBFromB(s.network_rx)} | ${humanMinKBFromB(s.network_tx)}`; const signature = serverRowSignature(s, m);
const netTotal = `${humanMinMBFromB(s.network_in)} | ${humanMinMBFromB(s.network_out)}`; const current = existing.get(key);
const alertClass = m.rowLevel ? ` alert-${m.rowLevel}` : ''; let row = current;
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'};"> if(!row || row.dataset.sig !== signature){
<td>${protoPill(s)}</td> row = htmlElement(serverRowHTML(s, m, signature));
<td>${trafficCaps(s)}</td> if(current) current.replaceWith(row);
<td><span class="node-name" title="${esc(s.name || '-')}">${esc(s.name || '-')}</span></td> }
<td>${virtPill(s.type)}</td> tbody.appendChild(row);
<td>${esc(s.location || '-')}</td> });
<td>${esc(s.uptime || '-')}</td>
<td>${s.load_1 === -1 ? '' : num(s.load_1).toFixed(2)}</td>
<td>${netNow}</td>
<td>${netTotal}</td>
<td>${m.online ? gaugeHTML('cpu', s.cpu) : '-'}</td>
<td>${m.online ? gaugeHTML('mem', m.memPct) : '-'}</td>
<td>${m.online ? gaugeHTML('hdd', m.hddPct) : '-'}</td>
<td>${buckets(s)}</td>
</tr>`;
}).join('') || `<tr><td colspan="13" class="muted" style="text-align:center;padding:1rem;">无数据</td></tr>`;
document.querySelectorAll('#serversBody .row-server').forEach(row => row.addEventListener('click', () => {
if(row.dataset.online !== '1') return;
openDetail(row.dataset.key);
}));
} }
function renderServersCards(){ function renderServersCards(){
const wrap = $('serversCards'); const wrap = $('serversCards');
if(window.innerWidth > 700){ wrap.innerHTML = ''; return; }
wrap.innerHTML = visibleServers().map(s => { wrap.innerHTML = visibleServers().map(s => {
const m = metrics(s); const m = metrics(s);
const alertClass = m.rowLevel ? ` alert-${m.rowLevel}` : ''; const alertClass = m.rowLevel ? ` alert-${m.rowLevel}` : '';
const spec = serverSpecLabel(s);
return `<div class="card${m.online ? '' : ' offline'}${alertClass}${osClass(s.os)}" data-key="${esc(s._key)}" data-online="${m.online ? 1 : 0}"> return `<div class="card${m.online ? '' : ' offline'}${alertClass}${osClass(s.os)}" data-key="${esc(s._key)}" data-online="${m.online ? 1 : 0}">
<div class="card-header"><div class="card-title">${esc(s.name || '-')} <span class="tag">${esc(s.location || '-')}</span></div>${protoPill(s)}</div> <div class="card-header"><div class="card-title">${esc(s.name || '-')}${spec ? `<span class="card-spec-chip" title="CPU 核心 / 总内存">${esc(spec)}</span>` : ''} <span class="tag">${esc(s.location || '-')}</span></div>${protoPill(s)}</div>
<div class="kvlist"> <div class="kvlist">
<div><span class="key">负载</span><span>${s.load_1 === -1 ? '' : num(s.load_1).toFixed(2)}</span></div> <div><span class="key">负载</span><span>${s.load_1 === -1 ? '' : num(s.load_1).toFixed(2)}</span></div>
<div><span class="key">在线</span><span>${esc(s.uptime || '-')}</span></div> <div><span class="key">在线</span><span>${esc(s.uptime || '-')}</span></div>
@@ -394,10 +526,6 @@ function renderServersCards(){
${buckets(s)} ${buckets(s)}
</div>`; </div>`;
}).join('') || '<div class="empty-state">无数据</div>'; }).join('') || '<div class="empty-state">无数据</div>';
wrap.querySelectorAll('.card').forEach(card => card.addEventListener('click', () => {
if(card.dataset.online !== '1') return;
openDetail(card.dataset.key);
}));
} }
function parseCustom(str){ function parseCustom(str){
@@ -458,7 +586,9 @@ function refreshDetail(){
if(!s){ closeDetail(); return; } if(!s){ closeDetail(); return; }
const m = metrics(s); const m = metrics(s);
const title = $('detailTitle'); const title = $('detailTitle');
title.innerHTML = `${esc(s.name || '-')} 详情${s.os ? `<span class="os-chip${osClass(s.os)}">${esc(osLabel(s.os))}</span>` : ''}`; const spec = serverSpecLabel(s);
const cpuModel = cpuModelLabel(s);
title.innerHTML = `${esc(s.name || '-')} 详情${s.os ? `<span class="os-chip${osClass(s.os)}">${esc(osLabel(s.os))}</span>` : ''}${spec ? `<span class="spec-chip" title="CPU 核心 / 总内存">${esc(spec)}</span>` : ''}${cpuModel ? `<span class="cpu-model-chip" title="${esc(cpuModel)}">${esc(cpuModel)}</span>` : ''}`;
const modalBox = document.querySelector('#detailModal .modal-box'); const modalBox = document.querySelector('#detailModal .modal-box');
if(modalBox){ if(modalBox){
modalBox.classList.remove('high-load'); modalBox.classList.remove('high-load');
@@ -563,9 +693,10 @@ function bindTabs(){
if(e.target.tagName !== 'BUTTON') return; if(e.target.tagName !== 'BUTTON') return;
const tab = e.target.dataset.tab; const tab = e.target.dataset.tab;
S.activeTab = tab; S.activeTab = tab;
scheduleServersRender.cancel();
document.querySelectorAll('.nav button').forEach(btn => btn.classList.toggle('active', btn === e.target)); 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)); document.querySelectorAll('.panel').forEach(panel => panel.classList.toggle('active', panel.id === 'panel-' + tab));
normalizeServersToolbarState(); renderActivePanel();
if(tab === 'config') ensureAdminChecked(); if(tab === 'config') ensureAdminChecked();
}); });
} }
@@ -584,30 +715,43 @@ function bindTheme(){
}); });
} }
function bindFilters(){ 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 => { $('statusFilter').addEventListener('click', e => {
if(e.target.tagName !== 'BUTTON') return; if(e.target.tagName !== 'BUTTON') return;
S.filters.status = e.target.dataset.filter; S.filters.status = e.target.dataset.filter;
document.querySelectorAll('#statusFilter button').forEach(btn => btn.classList.toggle('active', btn === e.target)); 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); $('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', () => { $('sortDirection').addEventListener('click', () => {
S.filters.dir = S.filters.dir === 'desc' ? 'asc' : 'desc'; S.filters.dir = S.filters.dir === 'desc' ? 'asc' : 'desc';
$('sortDirection').textContent = S.filters.dir === 'desc' ? '降序' : '升序'; $('sortDirection').textContent = S.filters.dir === 'desc' ? '降序' : '升序';
renderServers(); renderServersCards(); renderServersViewNow();
}); });
document.querySelectorAll('#serversTable th[data-sort]').forEach(th => th.addEventListener('click', () => { 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'; if(S.filters.sort === th.dataset.sort) S.filters.dir = S.filters.dir === 'desc' ? 'asc' : 'desc';
else S.filters.sort = th.dataset.sort; else S.filters.sort = th.dataset.sort;
$('sortSelect').value = S.filters.sort; $('sortSelect').value = S.filters.sort;
$('sortDirection').textContent = S.filters.dir === 'desc' ? '降序' : '升序'; $('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(){ function adminHeaders(){
return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${S.admin.token}` }; return { 'Content-Type': 'application/json', 'Authorization': `Bearer ${S.admin.token}` };
} }
@@ -916,11 +1060,19 @@ function bindAdmin(){
$('detailClose').addEventListener('click', closeDetail); $('detailClose').addEventListener('click', closeDetail);
$('detailModal').addEventListener('click', e => { if(e.target.id === 'detailModal') 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(); bindTabs();
bindTheme(); bindTheme();
bindFilters(); bindFilters();
bindServerInteractions();
bindAdmin(); bindAdmin();
fetchData(); fetchData();
setInterval(fetchData, 1000); setInterval(fetchData, 1000);