{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'] = "{:.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