From dcec9598c10b2efd9972203acb24112d99015cc1 Mon Sep 17 00:00:00 2001 From: cppla Date: Tue, 2 Sep 2025 17:43:43 +0800 Subject: [PATCH] 1.1.7 update --- README.md | 25 +++--- clients/client-linux.py | 159 +++++++++++++++++-------------------- clients/client-psutil.py | 165 +++++++++++++++++---------------------- server/config.json | 14 ++-- web/css/app.css | 24 +++++- web/js/app.js | 90 ++++++++++++++------- 6 files changed, 246 insertions(+), 231 deletions(-) diff --git a/README.md b/README.md index 22450a9..50b537d 100644 --- a/README.md +++ b/README.md @@ -92,16 +92,16 @@ cd ServerStatus/server && make ], "monitors": [ { - "name": "监测网站,默认为一天在线率", - "host": "https://www.baidu.com", - "interval": 1200, + "name": "抖音", + "host": "https://www.douyin.com", + "interval": 600, "type": "https" }, { - "name": "监测tcp服务端口", - "host": "1.1.1.1:80", - "interval": 1200, - "type": "tcp" + "name": "京东", + "host": "https://www.jd.com", + "interval": 600, + "type": "https" } ], "sslcerts": [ @@ -183,21 +183,24 @@ web-dir参数为上一步设置的网站根目录,务必修改成自己网站 客户端有两个版本,client-linux为普通linux,client-psutil为跨平台版,普通版不成功,换成跨平台版即可。 -#### 一、client-linux版配置: +## 4.1、client-linux版配置: 1、vim client-linux.py, 修改SERVER地址,username帐号, password密码 2、python3 client-linux.py 运行即可。 -#### 二、client-psutil版配置: +## 4.2、client-psutil版配置: 1、安装psutil跨平台依赖库 ``` -`Debian/Ubuntu`: apt -y install python3-pip && pip3 install psutil +`Debian/Ubuntu`: apt -y install python3-psutil `Centos/Redhat`: yum -y install python3-pip gcc python3-devel && pip3 install psutil `Windows`: https://pypi.org/project/psutil/ ``` 2、vim client-psutil.py, 修改SERVER地址,username帐号, password密码 3、python3 client-psutil.py 运行即可。 -服务器和客户端自行加入开机启动,或进程守护,或后台方式运行。 例如: nohup python3 client-linux.py & +## 4.3 服务器和客户端自行加入开机启动,或后台方式运行。 +1、后台运行: nohup python3 client-linux.py & + +2、开机启动(crontab -e): @reboot /usr/bin/python3 /root/client-linux.py `extra scene (run web/ssview.py)` ![Shell View](https://dl.cpp.la/Archive/serverstatus-shell.png?version=2023) diff --git a/clients/client-linux.py b/clients/client-linux.py index 4005869..2f86f7e 100755 --- a/clients/client-linux.py +++ b/clients/client-linux.py @@ -1,9 +1,8 @@ #!/usr/bin/env python3 # coding: utf-8 -# Update by : https://github.com/cppla/ServerStatus, Update date: 20220530 -# 版本:1.0.3, 支持Python版本:2.7 to 3.10 +# Update by : https://github.com/cppla/ServerStatus, Update date: 20250902 +# 版本:1.1.0, 支持Python版本:3.6+ # 支持操作系统: Linux, OSX, FreeBSD, OpenBSD and NetBSD, both 32-bit and 64-bit architectures -# ONLINE_PACKET_HISTORY_LEN, 探测间隔1200s,记录24小时在线率(72);探测时间300s,记录24小时(288);探测间隔60s,记录7天(10080) # 说明: 默认情况下修改server和user就可以了。丢包率监测方向可以自定义,例如:CU = "www.facebook.com"。 SERVER = "127.0.0.1" @@ -18,11 +17,9 @@ CM = "cm.tz.cloudcpp.com" PROBEPORT = 80 PROBE_PROTOCOL_PREFER = "ipv4" # ipv4, ipv6 PING_PACKET_HISTORY_LEN = 100 -ONLINE_PACKET_HISTORY_LEN = 72 INTERVAL = 1 import socket -import ssl import time import timeit import re @@ -33,10 +30,7 @@ import errno import subprocess import threading import platform -if sys.version_info.major == 3: - from queue import Queue -elif sys.version_info.major == 2: - from Queue import Queue +from queue import Queue def get_uptime(): with open('/proc/uptime', 'r') as f: @@ -321,87 +315,66 @@ def get_realtime_data(): def _monitor_thread(name, host, interval, type): - lostPacket = 0 - packet_queue = Queue(maxsize=ONLINE_PACKET_HISTORY_LEN) while True: if name not in monitorServer.keys(): break - if packet_queue.full(): - if packet_queue.get() == 0: - lostPacket -= 1 try: - if type == "http": - address = host.replace("http://", "") - m = timeit.default_timer() - if PROBE_PROTOCOL_PREFER == 'ipv4': - IP = socket.getaddrinfo(address, None, socket.AF_INET)[0][4][0] + # 1) 解析目标 host 与端口 + if type == 'http': + addr = str(host).replace('http://','') + addr = addr.split('/',1)[0] + port = 80 + if ':' in addr and not addr.startswith('['): + a, p = addr.rsplit(':',1) + if p.isdigit(): + addr, port = a, int(p) + elif type == 'https': + addr = str(host).replace('https://','') + addr = addr.split('/',1)[0] + port = 443 + if ':' in addr and not addr.startswith('['): + a, p = addr.rsplit(':',1) + if p.isdigit(): + addr, port = a, int(p) + elif type == 'tcp': + addr = str(host) + if addr.startswith('[') and ']' in addr: + a = addr[1:addr.index(']')] + rest = addr[addr.index(']')+1:] + if rest.startswith(':') and rest[1:].isdigit(): + addr, port = a, int(rest[1:]) + else: + raise Exception('bad tcp target') else: - IP = socket.getaddrinfo(address, None, socket.AF_INET6)[0][4][0] - monitorServer[name]["dns_time"] = int((timeit.default_timer() - m) * 1000) - m = timeit.default_timer() - k = socket.create_connection((IP, 80), timeout=6) - monitorServer[name]["connect_time"] = int((timeit.default_timer() - m) * 1000) - m = timeit.default_timer() - k.sendall("GET / HTTP/1.2\r\nHost:{}\r\nUser-Agent:ServerStatus/cppla\r\nConnection:close\r\n\r\n".format(address).encode('utf-8')) - response = b"" - while True: - data = k.recv(4096) - if not data: - break - response += data - http_code = response.decode('utf-8').split('\r\n')[0].split()[1] - monitorServer[name]["download_time"] = int((timeit.default_timer() - m) * 1000) - k.close() - if http_code not in ['200', '204', '301', '302', '401']: - raise Exception("http code not in 200, 204, 301, 302, 401") - elif type == "https": - context = ssl._create_unverified_context() - address = host.replace("https://", "") - m = timeit.default_timer() - if PROBE_PROTOCOL_PREFER == 'ipv4': - IP = socket.getaddrinfo(address, None, socket.AF_INET)[0][4][0] + a, p = addr.rsplit(':',1) + addr, port = a, int(p) + else: + time.sleep(interval) + continue + + # 2) 解析 IP(按偏好族) + IP = addr + if addr.count(':') < 1: # 非纯 IPv6 + try: + if PROBE_PROTOCOL_PREFER == 'ipv4': + IP = socket.getaddrinfo(addr, None, socket.AF_INET)[0][4][0] + else: + IP = socket.getaddrinfo(addr, None, socket.AF_INET6)[0][4][0] + except Exception: + pass + + # 3) 建连耗时(timeout=1s),ECONNREFUSED 也计入 + try: + b = timeit.default_timer() + socket.create_connection((IP, port), timeout=1).close() + monitorServer[name]["latency"] = int((timeit.default_timer() - b) * 1000) + except socket.error as error: + if getattr(error, 'errno', None) == errno.ECONNREFUSED: + monitorServer[name]["latency"] = int((timeit.default_timer() - b) * 1000) else: - IP = socket.getaddrinfo(address, None, socket.AF_INET6)[0][4][0] - monitorServer[name]["dns_time"] = int((timeit.default_timer() - m) * 1000) - m = timeit.default_timer() - k = socket.create_connection((IP, 443), timeout=6) - monitorServer[name]["connect_time"] = int((timeit.default_timer() - m) * 1000) - m = timeit.default_timer() - kk = context.wrap_socket(k, server_hostname=address) - kk.sendall("GET / HTTP/1.2\r\nHost:{}\r\nUser-Agent:ServerStatus/cppla\r\nConnection:close\r\n\r\n".format(address).encode('utf-8')) - response = b"" - while True: - data = kk.recv(4096) - if not data: - break - response += data - http_code = response.decode('utf-8').split('\r\n')[0].split()[1] - monitorServer[name]["download_time"] = int((timeit.default_timer() - m) * 1000) - kk.close() - k.close() - if http_code not in ['200', '204', '301', '302', '401']: - raise Exception("http code not in 200, 204, 301, 302, 401") - elif type == "tcp": - m = timeit.default_timer() - if PROBE_PROTOCOL_PREFER == 'ipv4': - IP = socket.getaddrinfo(host.split(":")[0], None, socket.AF_INET)[0][4][0] - else: - IP = socket.getaddrinfo(host.split(":")[0], None, socket.AF_INET6)[0][4][0] - monitorServer[name]["dns_time"] = int((timeit.default_timer() - m) * 1000) - m = timeit.default_timer() - k = socket.create_connection((IP, int(host.split(":")[1])), timeout=6) - monitorServer[name]["connect_time"] = int((timeit.default_timer() - m) * 1000) - m = timeit.default_timer() - k.send(b"GET / HTTP/1.2\r\n\r\n") - k.recv(1024) - monitorServer[name]["download_time"] = int((timeit.default_timer() - m) * 1000) - k.close() - packet_queue.put(1) - except Exception as e: - lostPacket += 1 - packet_queue.put(0) - if packet_queue.qsize() > 5: - monitorServer[name]["online_rate"] = 1 - float(lostPacket) / packet_queue.qsize() + monitorServer[name]["latency"] = 0 + except Exception: + monitorServer[name]["latency"] = 0 time.sleep(interval) def byte_str(object): @@ -456,10 +429,8 @@ if __name__ == '__main__': jdata = json.loads(i[i.find("{"):i.find("}")+1]) monitorServer[jdata.get("name")] = { "type": jdata.get("type"), - "dns_time": 0, - "connect_time": 0, - "download_time": 0, - "online_rate": 1 + "host": jdata.get("host"), + "latency": 0 } t = threading.Thread( target=_monitor_thread, @@ -549,7 +520,17 @@ if __name__ == '__main__': 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()) + items = [] + for _n, st in monitorServer.items(): + key = str(_n) + try: + ms = int(st.get('latency') or 0) + except Exception: + ms = 0 + items.append((key, max(0, ms))) + # 稳定顺序:按 key 排序 + items.sort(key=lambda x: x[0]) + array['custom'] = ';'.join(f"{k}={v}" for k,v in items) s.send(byte_str("update " + json.dumps(array) + "\n")) except KeyboardInterrupt: raise diff --git a/clients/client-psutil.py b/clients/client-psutil.py index 2b99e8c..9f3eb67 100755 --- a/clients/client-psutil.py +++ b/clients/client-psutil.py @@ -1,10 +1,9 @@ #!/usr/bin/env python3 # coding: utf-8 -# Update by : https://github.com/cppla/ServerStatus, Update date: 20220530 +# Update by : https://github.com/cppla/ServerStatus, Update date: 20250902 # 依赖于psutil跨平台库 -# 版本:1.0.3, 支持Python版本:2.7 to 3.10 +# 版本:1.1.0, 支持Python版本:3.6+ # 支持操作系统: Linux, Windows, OSX, Sun Solaris, FreeBSD, OpenBSD and NetBSD, both 32-bit and 64-bit architectures -# ONLINE_PACKET_HISTORY_LEN, 探测间隔1200s,记录24小时在线率(72);探测时间300s,记录24小时(288);探测间隔60s,记录7天(10080) # 说明: 默认情况下修改server和user就可以了。丢包率监测方向可以自定义,例如:CU = "www.facebook.com"。 SERVER = "127.0.0.1" @@ -19,11 +18,9 @@ CM = "cm.tz.cloudcpp.com" PROBEPORT = 80 PROBE_PROTOCOL_PREFER = "ipv4" # ipv4, ipv6 PING_PACKET_HISTORY_LEN = 100 -ONLINE_PACKET_HISTORY_LEN = 72 INTERVAL = 1 import socket -import ssl import time import timeit import os @@ -33,10 +30,7 @@ import errno import psutil import threading import platform -if sys.version_info.major == 3: - from queue import Queue -elif sys.version_info.major == 2: - from Queue import Queue +from queue import Queue def get_uptime(): return int(time.time() - psutil.boot_time()) @@ -309,87 +303,68 @@ def get_realtime_data(): ti.start() def _monitor_thread(name, host, interval, type): - lostPacket = 0 - packet_queue = Queue(maxsize=ONLINE_PACKET_HISTORY_LEN) + # 参考 _ping_thread 风格:每轮解析一次目标,按协议族偏好解析 IP,测 TCP 建连耗时 while True: - if name not in monitorServer.keys(): + if name not in monitorServer: break - if packet_queue.full(): - if packet_queue.get() == 0: - lostPacket -= 1 try: - if type == "http": - address = host.replace("http://", "") - m = timeit.default_timer() - if PROBE_PROTOCOL_PREFER == 'ipv4': - IP = socket.getaddrinfo(address, None, socket.AF_INET)[0][4][0] + # 1) 解析目标 host 与端口 + if type == 'http': + addr = str(host).replace('http://','') + addr = addr.split('/',1)[0] + port = 80 + if ':' in addr and not addr.startswith('['): + a, p = addr.rsplit(':',1) + if p.isdigit(): + addr, port = a, int(p) + elif type == 'https': + addr = str(host).replace('https://','') + addr = addr.split('/',1)[0] + port = 443 + if ':' in addr and not addr.startswith('['): + a, p = addr.rsplit(':',1) + if p.isdigit(): + addr, port = a, int(p) + elif type == 'tcp': + addr = str(host) + if addr.startswith('[') and ']' in addr: + # [v6]:port + a = addr[1:addr.index(']')] + rest = addr[addr.index(']')+1:] + if rest.startswith(':') and rest[1:].isdigit(): + addr, port = a, int(rest[1:]) + else: + raise Exception('bad tcp target') else: - IP = socket.getaddrinfo(address, None, socket.AF_INET6)[0][4][0] - monitorServer[name]["dns_time"] = int((timeit.default_timer() - m) * 1000) - m = timeit.default_timer() - k = socket.create_connection((IP, 80), timeout=6) - monitorServer[name]["connect_time"] = int((timeit.default_timer() - m) * 1000) - m = timeit.default_timer() - k.sendall("GET / HTTP/1.2\r\nHost:{}\r\nUser-Agent:ServerStatus/cppla\r\nConnection:close\r\n\r\n".format(address).encode('utf-8')) - response = b"" - while True: - data = k.recv(4096) - if not data: - break - response += data - http_code = response.decode('utf-8').split('\r\n')[0].split()[1] - monitorServer[name]["download_time"] = int((timeit.default_timer() - m) * 1000) - k.close() - if http_code not in ['200', '204', '301', '302', '401']: - raise Exception("http code not in 200, 204, 301, 302, 401") - elif type == "https": - context = ssl._create_unverified_context() - address = host.replace("https://", "") - m = timeit.default_timer() - if PROBE_PROTOCOL_PREFER == 'ipv4': - IP = socket.getaddrinfo(address, None, socket.AF_INET)[0][4][0] + a, p = addr.rsplit(':',1) + addr, port = a, int(p) + else: + time.sleep(interval) + continue + + # 2) 解析 IP(按偏好族),与 _ping_thread 保持一致的判定 + IP = addr + if addr.count(':') < 1: # 非纯 IPv6,可能是 IPv4 或域名 + try: + if PROBE_PROTOCOL_PREFER == 'ipv4': + IP = socket.getaddrinfo(addr, None, socket.AF_INET)[0][4][0] + else: + IP = socket.getaddrinfo(addr, None, socket.AF_INET6)[0][4][0] + except Exception: + pass + + # 3) 测 TCP 建连耗时(timeout=1s);ECONNREFUSED 也记为耗时 + try: + b = timeit.default_timer() + socket.create_connection((IP, port), timeout=1).close() + monitorServer[name]['latency'] = int((timeit.default_timer() - b) * 1000) + except socket.error as error: + if getattr(error, 'errno', None) == errno.ECONNREFUSED: + monitorServer[name]['latency'] = int((timeit.default_timer() - b) * 1000) else: - IP = socket.getaddrinfo(address, None, socket.AF_INET6)[0][4][0] - monitorServer[name]["dns_time"] = int((timeit.default_timer() - m) * 1000) - m = timeit.default_timer() - k = socket.create_connection((IP, 443), timeout=6) - monitorServer[name]["connect_time"] = int((timeit.default_timer() - m) * 1000) - m = timeit.default_timer() - kk = context.wrap_socket(k, server_hostname=address) - kk.sendall("GET / HTTP/1.2\r\nHost:{}\r\nUser-Agent:ServerStatus/cppla\r\nConnection:close\r\n\r\n".format(address).encode('utf-8')) - response = b"" - while True: - data = kk.recv(4096) - if not data: - break - response += data - http_code = response.decode('utf-8').split('\r\n')[0].split()[1] - monitorServer[name]["download_time"] = int((timeit.default_timer() - m) * 1000) - kk.close() - k.close() - if http_code not in ['200', '204', '301', '302', '401']: - raise Exception("http code not in 200, 204, 301, 302, 401") - elif type == "tcp": - m = timeit.default_timer() - if PROBE_PROTOCOL_PREFER == 'ipv4': - IP = socket.getaddrinfo(host.split(":")[0], None, socket.AF_INET)[0][4][0] - else: - IP = socket.getaddrinfo(host.split(":")[0], None, socket.AF_INET6)[0][4][0] - monitorServer[name]["dns_time"] = int((timeit.default_timer() - m) * 1000) - m = timeit.default_timer() - k = socket.create_connection((IP, int(host.split(":")[1])), timeout=6) - monitorServer[name]["connect_time"] = int((timeit.default_timer() - m) * 1000) - m = timeit.default_timer() - k.send(b"GET / HTTP/1.2\r\n\r\n") - k.recv(1024) - monitorServer[name]["download_time"] = int((timeit.default_timer() - m) * 1000) - k.close() - packet_queue.put(1) - except Exception as e: - lostPacket += 1 - packet_queue.put(0) - if packet_queue.qsize() > 5: - monitorServer[name]["online_rate"] = 1 - float(lostPacket) / packet_queue.qsize() + monitorServer[name]['latency'] = 0 + except Exception: + monitorServer[name]['latency'] = 0 time.sleep(interval) @@ -444,10 +419,8 @@ if __name__ == '__main__': jdata = json.loads(i[i.find("{"):i.find("}")+1]) monitorServer[jdata.get("name")] = { "type": jdata.get("type"), - "dns_time": 0, - "connect_time": 0, - "download_time": 0, - "online_rate": 1 + "host": jdata.get("host"), + "latency": 0 } t = threading.Thread( target=_monitor_thread, @@ -532,9 +505,17 @@ if __name__ == '__main__': 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()) + items = [] + for _n, st in monitorServer.items(): + key = str(_n) + try: + ms = int(st.get('latency') or 0) + except Exception: + ms = 0 + items.append((key, max(0, ms))) + # 稳定顺序:按 key 排序 + items.sort(key=lambda x: x[0]) + array['custom'] = ';'.join(f"{k}={v}" for k,v in items) s.send(byte_str("update " + json.dumps(array) + "\n")) except KeyboardInterrupt: raise diff --git a/server/config.json b/server/config.json index 3685163..be2e417 100644 --- a/server/config.json +++ b/server/config.json @@ -42,25 +42,25 @@ { "name": "抖音", "host": "https://www.douyin.com", - "interval": 1200, + "interval": 600, "type": "https" }, { "name": "京东", "host": "https://www.jd.com", - "interval": 1200, + "interval": 600, "type": "https" }, { "name": "百度", "host": "https://www.baidu.com", - "interval": 1200, + "interval": 600, "type": "https" }, { "name": "淘宝", "host": "https://www.taobao.com", - "interval": 1200, + "interval": 600, "type": "https" } ], @@ -69,21 +69,21 @@ "name": "my.cloudcpp.com", "domain": "https://my.cloudcpp.com", "port": 443, - "interval": 3600, + "interval": 7200, "callback": "https://yourSMSurl" }, { "name": "tz.cloudcpp.com", "domain": "https://tz.cloudcpp.com", "port": 443, - "interval": 3600, + "interval": 7200, "callback": "https://yourSMSurl" }, { "name": "3.0.2.1", "domain": "https://3.0.2.1", "port": 443, - "interval": 3600, + "interval": 7200, "callback": "https://yourSMSurl" } ], diff --git a/web/css/app.css b/web/css/app.css index 700c1aa..6d0cbb6 100644 --- a/web/css/app.css +++ b/web/css/app.css @@ -94,8 +94,7 @@ table.data tbody tr:hover{background:rgba(255,255,255,.04)} .footer a{color:var(--text-dim)} .footer a:hover{color:var(--accent)} .muted{color:var(--text-dim)} -.status-off{color:var(--danger);font-weight:600} -.status-on{color:var(--ok);font-weight:600} +/* 旧状态文字样式已不再使用(采用 pill) */ @media (max-width:1100px){.nav{flex-wrap:wrap}.table-wrap{border-radius:8px}} @keyframes fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}} @@ -284,6 +283,27 @@ table.data tbody tr[class*="os-"]:hover{background:linear-gradient(180deg, color .cards .card.expanded .expand-area{display:block;} /* 旧移动端 latency spark 样式移除 */ +/* 简易信号格,用于服务连通性延迟展示 */ +.sig{display:inline-flex;gap:2px;vertical-align:baseline;margin:0 4px 0 6px;align-items:flex-end;line-height:1} +.sig .b{width:3px;background:color-mix(in srgb,var(--text-dim) 35%,transparent);border-radius:2px;display:inline-block} +.sig .b:nth-child(1){height:7px} +.sig .b:nth-child(2){height:9px} +.sig .b:nth-child(3){height:11px} +.sig .b:nth-child(4){height:13px} +.sig .b:nth-child(5){height:15px} +.sig .b.on{background:var(--ok)} +.sig .b.off{opacity:.35} + +/* 服务监测项:横向排列的“名称 信号格 Nms”,自动换行 */ +.mon-items{display:flex;flex-wrap:wrap;gap:10px 16px;align-items:center} +.mon-item{display:inline-flex;align-items:center;white-space:nowrap;line-height:1} +.mon-item .name{margin-right:4px} +.mon-item .ms{margin-left:4px;font-variant-numeric:tabular-nums} +.mon-item .sig{margin:0 4px;transform:translateY(-1px)} + +/* 允许服务表第 4 列换行,便于横向 chip 自动折行 */ +#monitorsTable tbody td:nth-child(4){white-space:normal} + /* 新 Logo 样式 */ .brand{display:flex;align-items:center;gap:.55rem;font-weight:600;letter-spacing:.5px;font-size:16px;position:relative} .brand .logo-mark{display:inline-flex;width:34px;height:34px;border-radius:10px;background:linear-gradient(145deg,var(--logo-start) 0%,var(--logo-end) 90%);color:#fff;align-items:center;justify-content:center;box-shadow:0 4px 12px -2px rgba(0,0,0,.45),0 0 0 1px rgba(255,255,255,.08);transition:var(--trans)} diff --git a/web/js/app.js b/web/js/app.js index e2f48aa..e36242c 100644 --- a/web/js/app.js +++ b/web/js/app.js @@ -1,5 +1,5 @@ // 简洁现代前端 - 仅使用原生 JS -const S = { updated:0, servers:[], ssl:[], error:false, hist:{}, metricHist:{}, loadHist:{} };// hist latency; metricHist: {key:{cpu:[],mem:[],hdd:[]}}; loadHist: {key:[]} +const S = { updated:0, servers:[], ssl:[], error:false, hist:{}, loadHist:{} };// hist latency; loadHist: {key:{l1:[],l5:[],l15:[]}} const els = { notice: ()=>document.getElementById('notice'), last: ()=>document.getElementById('lastUpdate'), @@ -8,7 +8,7 @@ const els = { sslBody: ()=>document.getElementById('sslBody') }; -// (清理) 已移除 bytes / humanAuto 等未使用的通用进位函数 +// (清理) 精简进位函数,仅保留最小所需 // 最小单位 MB: function humanMinMBFromKB(kb){ if(kb==null||isNaN(kb)) return '-'; // 输入单位: KB let mb = kb/1000; const units=['MB','GB','TB','PB']; let i=0; while(mb>=1000 && i{ if(H[k].length>MAX) H[k].splice(0,H[k].length-MAX); }); // 指标历史 (仅在线时记录) - if(!S.metricHist[key]) S.metricHist[key] = {cpu:[],mem:[],hdd:[]}; - const MH = S.metricHist[key]; - if(s.online4||s.online6){ - const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0; - const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0; - MH.cpu.push(s.cpu||0); - MH.mem.push(memPct||0); - MH.hdd.push(hddPct||0); - const MAXM=120; ['cpu','mem','hdd'].forEach(k=>{ if(MH[k].length>MAXM) MH[k].splice(0,MH[k].length-MAXM); }); - } + // 移除 CPU/内存/硬盘历史累积(不再使用) // 负载历史 (记录 load_1 / load_5 / load_15) if(!S.loadHist[key]) S.loadHist[key] = {l1:[],l5:[],l15:[]}; const LH = S.loadHist[key]; @@ -138,8 +129,8 @@ function renderServers(){ const tbody = els.serversBody(); let html=''; S.servers.forEach((s,idx)=>{ - const online = s.online4||s.online6; - const proto = online ? (s.online4 && s.online6? '双栈': s.online4? 'IPv4':'IPv6') : '离线'; + const online = (s.online4 || s.online6); + const proto = online ? (s.online4 && s.online6 ? '双栈' : (s.online4 ? 'IPv4' : 'IPv6')) : '离线'; const statusPill = online ? `${proto}` : `${proto}`; const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0; const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0; @@ -244,12 +235,35 @@ function renderServersCards(){ function renderMonitors(){ const tbody = els.monitorsBody(); let html=''; + function parseCustom(str){ + const items = []; + if(typeof str !== 'string' || !str.trim()) return {items:[]}; + str.split(';').forEach(seg=>{ + if(!seg) return; + const [rawK,rawV] = seg.split('='); + if(!rawK) return; + const k = String(rawK).trim(); + const v = parseInt((rawV||'').trim(),10); + if(!isNaN(v)) items.push({key:k, label:k, ms:Math.max(0,v)}); + }); + return {items}; + } + function bars(ms){ + const levels = [50,100,150,220]; + let on = 0; if(typeof ms==='number'){ if(ms<=levels[0]) on=5; else if(ms<=levels[1]) on=4; else if(ms<=levels[2]) on=3; else if(ms<=levels[3]) on=2; else on=1; } + return ''+[0,1,2,3,4].map(i=>``).join('')+''; + } S.servers.forEach(s=>{ + const isOnline = (s.online4||s.online6); + const proto = isOnline ? (s.online4 && s.online6 ? '双栈' : (s.online4 ? 'IPv4' : 'IPv6')) : '离线'; + const pill = isOnline ? `${proto}` : `${proto}`; + const parsed = parseCustom(s.custom||''); + const row = parsed.items.map(it=> `${it.label}${bars(it.ms)}${it.ms}ms`).join(''); html += ` - ${(s.online4||s.online6)?'在线':'离线'} + ${pill} ${s.name||'-'} ${s.location||'-'} - ${s.custom||'-'} +
${row||'-'}
`; }); tbody.innerHTML = html || `无数据`; @@ -260,14 +274,34 @@ function renderMonitorsCards(){ const wrap = document.getElementById('monitorsCards'); if(!wrap) return; if(window.innerWidth>700){ wrap.innerHTML=''; return; } let html=''; + function parseCustom(str){ + const items = []; + if(typeof str !== 'string' || !str.trim()) return {items:[]}; + str.split(';').forEach(seg=>{ + if(!seg) return; + const [rawK,rawV] = seg.split('='); + if(!rawK) return; + const k = String(rawK).trim(); + const v = parseInt((rawV||'').trim(),10); + if(!isNaN(v)) items.push({key:k, label:k, ms:Math.max(0,v)}); + }); + return {items}; + } + function bars(ms){ + const levels = [50,100,150,220]; + let on = 0; if(typeof ms==='number'){ if(ms<=levels[0]) on=5; else if(ms<=levels[1]) on=4; else if(ms<=levels[2]) on=3; else if(ms<=levels[3]) on=2; else on=1; } + return ''+[0,1,2,3,4].map(i=>``).join('')+''; + } S.servers.forEach(s=>{ - const online = (s.online4||s.online6)?'在线':'离线'; - const pill = `${online}`; + const isOnline = (s.online4||s.online6); + const proto = isOnline ? (s.online4 && s.online6 ? '双栈' : (s.online4 ? 'IPv4' : 'IPv6')) : '离线'; + const pill = `${proto}`; + const parsed = parseCustom(s.custom||''); + const row = parsed.items.map(it=> `${it.label}${bars(it.ms)}${it.ms}ms`).join(''); html += `
${s.name||'-'} ${s.location||'-'}
${pill}
-
监测内容${s.custom||'-'}
-
协议${online}
+
监测内容${row||'-'}
`; }); @@ -307,7 +341,7 @@ function renderSSLCards(){
域名${(c.domain||'').replace(/^https?:\/\//,'')}
端口${c.port||443}
-
剩余(天)${c.expire_days??'-'}
+
剩余(天)${c.expire_days??'-'}
到期${dt.split(' ')[0]||dt}
`; @@ -378,8 +412,7 @@ function openDetail(i){ const ioRead = (typeof s.io_read==='number')? s.io_read:0; const ioWrite = (typeof s.io_write==='number')? s.io_write:0; const procLine = `${num(s.tcp_count)} / ${num(s.udp_count)} / ${num(s.process_count)} / ${num(s.thread_count)}`; - // 保留延迟数据用于图表,但不再展示当前延迟文字行 - const latText = offline ? '离线' : `CU/CT/CM: ${num(s.time_10010)}ms (${(s.ping_10010||0).toFixed(0)}%) / ${num(s.time_189)}ms (${(s.ping_189||0).toFixed(0)}%) / ${num(s.time_10086)}ms (${(s.ping_10086||0).toFixed(0)}%)`; + // 保留延迟数据用于图表 const key = s._key || [s.name||'-', s.location||'-', s.type||'-'].join('|')+'#1'; let latencyBlock = ''; @@ -557,7 +590,7 @@ function drawLoadChart(key){ }); } -//# sourceMappingURL=app.js.map +// source map 注释移除,避免 404 请求 // ====== 详情动态刷新 ====== function findServerByKey(key){ return S.servers.find(x=> (x._key)===key); } @@ -565,13 +598,10 @@ function updateDetailMetrics(key){ const s = findServerByKey(key); if(!s) return; if(!(s.online4||s.online6)) return; // 离线不更新 const procLine = `${num(s.tcp_count)} / ${num(s.udp_count)} / ${num(s.process_count)} / ${num(s.thread_count)}`; const procEl = document.getElementById('detail-proc'); if(procEl) procEl.textContent = procLine; - const cuEl=document.getElementById('lat-cu'); if(cuEl) cuEl.textContent = num(s.time_10010)+'ms'; - const ctEl=document.getElementById('lat-ct'); if(ctEl) ctEl.textContent = num(s.time_189)+'ms'; - const cmEl=document.getElementById('lat-cm'); if(cmEl) cmEl.textContent = num(s.time_10086)+'ms'; // 延迟动态刷新 (若存在) - const cuE1=document.getElementById('lat-cu'); if(cuE1) cuE1.textContent = num(s.time_10010)+'ms'; - const ctE1=document.getElementById('lat-ct'); if(ctE1) ctE1.textContent = num(s.time_189)+'ms'; - const cmE1=document.getElementById('lat-cm'); if(cmE1) cmE1.textContent = num(s.time_10086)+'ms'; + const cuEl=document.getElementById('lat-cu'); if(cuEl) cuEl.textContent = num(s.time_10010)+'ms'; + const ctEl=document.getElementById('lat-ct'); if(ctEl) ctEl.textContent = num(s.time_189)+'ms'; + const cmEl=document.getElementById('lat-cm'); if(cmEl) cmEl.textContent = num(s.time_10086)+'ms'; // 刷新联通/电信/移动历史计数(取三者最大长度) const latCntEl = document.getElementById('lat-count'); if(latCntEl){