mirror of https://github.com/cppla/ServerStatus
				
				
				
			1.1.7 update
							parent
							
								
									f8527cc297
								
							
						
					
					
						commit
						dcec9598c1
					
				
							
								
								
									
										25
									
								
								README.md
								
								
								
								
							
							
						
						
									
										25
									
								
								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)`
 | 
			
		||||

 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
                    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]
 | 
			
		||||
                        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, 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]
 | 
			
		||||
                    a, p = addr.rsplit(':',1)
 | 
			
		||||
                    addr, port = a, int(p)
 | 
			
		||||
            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()
 | 
			
		||||
                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:
 | 
			
		||||
                    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'] = "<br>".join(f"{k}\\t解析: {v['dns_time']}\\t连接: {v['connect_time']}\\t下载: {v['download_time']}\\t在线率: <code>{v['online_rate']*100:.1f}%</code>" 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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
                    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]
 | 
			
		||||
                        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, 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]
 | 
			
		||||
                    a, p = addr.rsplit(':',1)
 | 
			
		||||
                    addr, port = a, int(p)
 | 
			
		||||
            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()
 | 
			
		||||
                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:
 | 
			
		||||
                    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'] = "<br>".join("{}\t解析: {}\t连接: {}\t下载: {}\t在线率: <code>{:.2f}%</code>".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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
		}
 | 
			
		||||
	],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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<units.length-1){ mb/=1000;i++; }
 | 
			
		||||
| 
						 | 
				
			
			@ -102,16 +102,7 @@ async function fetchData(){
 | 
			
		|||
  const MAX=120; // 保留最多 120 条
 | 
			
		||||
      ['cu','ct','cm'].forEach(k=>{ 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 ? `<span class="pill on">${proto}</span>` : `<span class="pill off">${proto}</span>`;
 | 
			
		||||
  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 '<span class="sig">'+[0,1,2,3,4].map(i=>`<i class="b ${i<on?'on':'off'}"></i>`).join('')+'</span>';
 | 
			
		||||
  }
 | 
			
		||||
  S.servers.forEach(s=>{
 | 
			
		||||
    const isOnline = (s.online4||s.online6);
 | 
			
		||||
    const proto = isOnline ? (s.online4 && s.online6 ? '双栈' : (s.online4 ? 'IPv4' : 'IPv6')) : '离线';
 | 
			
		||||
    const pill = isOnline ? `<span class="pill on">${proto}</span>` : `<span class="pill off">${proto}</span>`;
 | 
			
		||||
    const parsed = parseCustom(s.custom||'');
 | 
			
		||||
    const row = parsed.items.map(it=> `<span class="mon-item"><span class="name">${it.label}</span>${bars(it.ms)}<span class="ms">${it.ms}ms</span></span>`).join('');
 | 
			
		||||
    html += `<tr>
 | 
			
		||||
      <td>${(s.online4||s.online6)?'在线':'离线'}</td>
 | 
			
		||||
      <td>${pill}</td>
 | 
			
		||||
      <td>${s.name||'-'}</td>
 | 
			
		||||
      <td>${s.location||'-'}</td>
 | 
			
		||||
      <td>${s.custom||'-'}</td>
 | 
			
		||||
  <td><div class="mon-items">${row||'-'}</div></td>
 | 
			
		||||
    </tr>`;
 | 
			
		||||
  });
 | 
			
		||||
  tbody.innerHTML = html || `<tr><td colspan="4" class="muted" style="text-align:center;padding:1rem;">无数据</td></tr>`;
 | 
			
		||||
| 
						 | 
				
			
			@ -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 '<span class="sig">'+[0,1,2,3,4].map(i=>`<i class="b ${i<on?'on':'off'}"></i>`).join('')+'</span>';
 | 
			
		||||
  }
 | 
			
		||||
  S.servers.forEach(s=>{
 | 
			
		||||
    const online = (s.online4||s.online6)?'在线':'离线';
 | 
			
		||||
    const pill = `<span class="status-pill ${online==='在线'?'on':'off'}">${online}</span>`;
 | 
			
		||||
    const isOnline = (s.online4||s.online6);
 | 
			
		||||
    const proto = isOnline ? (s.online4 && s.online6 ? '双栈' : (s.online4 ? 'IPv4' : 'IPv6')) : '离线';
 | 
			
		||||
    const pill = `<span class="status-pill ${isOnline?'on':'off'}">${proto}</span>`;
 | 
			
		||||
  const parsed = parseCustom(s.custom||'');
 | 
			
		||||
  const row = parsed.items.map(it=> `<span class=\"mon-item\"><span class=\"name\">${it.label}</span>${bars(it.ms)}<span class=\"ms\">${it.ms}ms</span></span>`).join('');
 | 
			
		||||
    html += `<div class="card">
 | 
			
		||||
      <div class="card-header"><div class="card-title">${s.name||'-'} <span class="tag">${s.location||'-'}</span></div>${pill}</div>
 | 
			
		||||
      <div class="kvlist" style="grid-template-columns:repeat(2,minmax(0,1fr));">
 | 
			
		||||
        <div><span class="key">监测内容</span><span>${s.custom||'-'}</span></div>
 | 
			
		||||
        <div><span class="key">协议</span><span>${online}</span></div>
 | 
			
		||||
  <div><span class="key">监测内容</span><span class="mon-items">${row||'-'}</span></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>`;
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			@ -307,7 +341,7 @@ function renderSSLCards(){
 | 
			
		|||
      <div class="kvlist" style="grid-template-columns:repeat(2,minmax(0,1fr));">
 | 
			
		||||
        <div><span class="key">域名</span><span>${(c.domain||'').replace(/^https?:\/\//,'')}</span></div>
 | 
			
		||||
        <div><span class="key">端口</span><span>${c.port||443}</span></div>
 | 
			
		||||
        <div><span class="key">剩余(天)</span><span>${c.expire_days??'-'}</span></div>
 | 
			
		||||
  <div><span class="key">剩余(天)</span><span><span class="badge ${cls}">${c.expire_days??'-'}</span></span></div>
 | 
			
		||||
        <div><span class="key">到期</span><span>${dt.split(' ')[0]||dt}</span></div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>`;
 | 
			
		||||
| 
						 | 
				
			
			@ -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 latCntEl = document.getElementById('lat-count');
 | 
			
		||||
  if(latCntEl){
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue