mirror of
https://github.com//cppla/ServerStatus
synced 2025-12-15 02:02:04 +08:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14ee075853 | ||
|
|
c1955d7ca5 | ||
|
|
41ec81bed3 | ||
|
|
1557673db2 | ||
|
|
565f8c7ce0 | ||
|
|
163c46f4de | ||
|
|
a46ed4ea1a | ||
|
|
4d9372768f | ||
|
|
eed03f641d | ||
|
|
25effc0e2f | ||
|
|
dcec9598c1 | ||
|
|
f8527cc297 | ||
|
|
a4fc285be6 | ||
|
|
195594b8c4 | ||
|
|
405e95bff8 | ||
|
|
72b621e277 | ||
|
|
c089ac7834 | ||
|
|
ae648fbe66 | ||
|
|
68a1d3719f | ||
|
|
9cd86ccaa3 | ||
|
|
956a5ace26 |
81
README.md
81
README.md
@@ -1,24 +1,17 @@
|
||||
# ServerStatus中文版:
|
||||
|
||||
* ServerStatus中文版是一个酷炫高逼格的云探针、云监控、服务器云监控、多服务器探针~。。
|
||||
* ServerStatus中文版是一个酷炫高逼格的云探针、云监控、服务器云监控、多服务器探针~。
|
||||
* 在线演示:https://tz.cloudcpp.com
|
||||
|
||||
[](https://github.com/cppla/ServerStatus)
|
||||
[](https://github.com/cppla/ServerStatus)
|
||||
[](https://github.com/cppla/ServerStatus)
|
||||
[](https://github.com/cppla/ServerStatus)
|
||||
[](https://github.com/cppla/ServerStatus)
|
||||
|
||||

|
||||

|
||||
|
||||
`Watchdog触发式告警,interval只是为了防止频繁收到报警信息造成的骚扰,并不是探测间隔。值得注意的是,Exprtk库默认使用窄字符类型,中文等Unicode字符无法解析计算,等待修复。 `
|
||||
|
||||
# 目录:
|
||||
|
||||
* clients 客户端文件
|
||||
* server 服务端文件
|
||||
* web 网站文件
|
||||
* server/config.json 探针配置文件
|
||||
* web/json 探针月流量
|
||||
`Watchdog触发式告警,interval只是为了防止频繁收到报警,并不是探测间隔。值得注意的是Exprtk使用窄字符类型,中文等Unicode字符无法解析计算。 AI已经能够取代大部分程序员`
|
||||
|
||||
|
||||
# 部署:
|
||||
|
||||
@@ -41,16 +34,8 @@ eg:
|
||||
wget --no-check-certificate -qO client-linux.py 'https://raw.githubusercontent.com/cppla/ServerStatus/master/clients/client-linux.py' && nohup python3 client-linux.py SERVER=45.79.67.132 USER=s04 >/dev/null 2>&1 &
|
||||
```
|
||||
|
||||
# 主题:
|
||||
|
||||
* layui:https://github.com/zeyudada/StatusServerLayui ,预览:https://sslt.8zyw.cn
|
||||
<img src=https://dl.cpp.la/Archive/serverstatus_layui.png width=200 height=100 />
|
||||
|
||||
* light:https://github.com/orilights/ServerStatus-Theme-Light ,预览:https://tz.cloudcpp.com/index3.html
|
||||
<img src=https://dl.cpp.la/Archive/serverstatus_light.png width=200 height=100 />
|
||||
|
||||
|
||||
# 手动安装教程:
|
||||
# 教程:
|
||||
|
||||
**【服务端配置】**
|
||||
|
||||
@@ -92,16 +77,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.baidu.com",
|
||||
"interval": 600,
|
||||
"type": "https"
|
||||
}
|
||||
],
|
||||
"sslcerts": [
|
||||
@@ -180,28 +165,34 @@ web-dir参数为上一步设置的网站根目录,务必修改成自己网站
|
||||
```
|
||||
|
||||
**【客户端配置】**
|
||||
|
||||
#### client-linux.py Linux版
|
||||
```bash
|
||||
# 1、修改 client-linux.py 中的 SERVER、username、password
|
||||
python3 client-linux.py
|
||||
# 2、以传参的方式启动
|
||||
python3 client-linux.py SERVER=127.0.0.1 USER=s01
|
||||
|
||||
客户端有两个版本,client-linux为普通linux,client-psutil为跨平台版,普通版不成功,换成跨平台版即可。
|
||||
|
||||
#### 一、client-linux版配置:
|
||||
1、vim client-linux.py, 修改SERVER地址,username帐号, password密码
|
||||
2、python3 client-linux.py 运行即可。
|
||||
|
||||
#### 二、client-psutil版配置:
|
||||
1、安装psutil跨平台依赖库
|
||||
```
|
||||
`Debian/Ubuntu`: apt -y install python3-pip && pip3 install psutil
|
||||
`Centos/Redhat`: yum -y install python3-pip gcc python3-devel && pip3 install psutil
|
||||
`Windows`: https://pypi.org/project/psutil/
|
||||
|
||||
#### client-psutil.py 跨平台版
|
||||
```bash
|
||||
# 安装依赖
|
||||
# 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 &
|
||||
|
||||
`extra scene (run web/ssview.py)`
|
||||

|
||||
#### 后台运行与开机启动
|
||||
```bash
|
||||
# 后台运行
|
||||
nohup python3 client-linux.py &
|
||||
|
||||
# 开机启动 (crontab -e)
|
||||
@reboot /usr/bin/python3 /path/to/client-linux.py
|
||||
```
|
||||
|
||||
# Make Better
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -32,10 +29,8 @@ import json
|
||||
import errno
|
||||
import subprocess
|
||||
import threading
|
||||
if sys.version_info.major == 3:
|
||||
from queue import Queue
|
||||
elif sys.version_info.major == 2:
|
||||
from Queue import Queue
|
||||
import platform
|
||||
from queue import Queue
|
||||
|
||||
def get_uptime():
|
||||
with open('/proc/uptime', 'r') as f:
|
||||
@@ -92,7 +87,7 @@ def liuliang():
|
||||
NET_OUT = 0
|
||||
with open('/proc/net/dev') as f:
|
||||
for line in f.readlines():
|
||||
netinfo = re.findall('([^\s]+):[\s]{0,}(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)', line)
|
||||
netinfo = re.findall(r'([^\s]+):[\s]{0,}(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)', line)
|
||||
if netinfo:
|
||||
if netinfo[0][0] == 'lo' or 'tun' in netinfo[0][0] \
|
||||
or 'docker' in netinfo[0][0] or 'veth' in netinfo[0][0] \
|
||||
@@ -320,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):
|
||||
@@ -455,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,
|
||||
@@ -520,7 +492,45 @@ if __name__ == '__main__':
|
||||
array['tcp'], array['udp'], array['process'], array['thread'] = tupd()
|
||||
array['io_read'] = diskIO.get("read")
|
||||
array['io_write'] = diskIO.get("write")
|
||||
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())
|
||||
# report OS (normalized)
|
||||
try:
|
||||
sysname = platform.system().lower()
|
||||
if sysname.startswith('linux'):
|
||||
os_name = 'linux'
|
||||
# try distro from os-release
|
||||
try:
|
||||
with open('/etc/os-release') as f:
|
||||
for line in f:
|
||||
if line.startswith('ID='):
|
||||
val = line.strip().split('=',1)[1].strip().strip('"')
|
||||
if val: os_name = val
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
elif sysname.startswith('darwin'):
|
||||
os_name = 'darwin'
|
||||
elif sysname.startswith('freebsd'):
|
||||
os_name = 'freebsd'
|
||||
elif sysname.startswith('openbsd'):
|
||||
os_name = 'openbsd'
|
||||
elif sysname.startswith('netbsd'):
|
||||
os_name = 'netbsd'
|
||||
else:
|
||||
os_name = sysname or 'unknown'
|
||||
except Exception:
|
||||
os_name = 'unknown'
|
||||
array['os'] = os_name
|
||||
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
|
||||
@@ -32,10 +29,8 @@ import json
|
||||
import errno
|
||||
import psutil
|
||||
import threading
|
||||
if sys.version_info.major == 3:
|
||||
from queue import Queue
|
||||
elif sys.version_info.major == 2:
|
||||
from Queue import Queue
|
||||
import platform
|
||||
from queue import Queue
|
||||
|
||||
def get_uptime():
|
||||
return int(time.time() - psutil.boot_time())
|
||||
@@ -308,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)
|
||||
|
||||
|
||||
@@ -443,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,
|
||||
@@ -509,7 +483,39 @@ if __name__ == '__main__':
|
||||
array['tcp'], array['udp'], array['process'], array['thread'] = tupd()
|
||||
array['io_read'] = diskIO.get("read")
|
||||
array['io_write'] = diskIO.get("write")
|
||||
array['custom'] = "<br>".join(f"{k}\\t解析: {v['dns_time']}\\t连接: {v['connect_time']}\\t下载: {v['download_time']}\\t在线率: <code>{v['online_rate']*100:.2f}%</code>" for k, v in monitorServer.items())
|
||||
# report OS (normalized)
|
||||
try:
|
||||
sysname = platform.system().lower()
|
||||
if sysname.startswith('windows'):
|
||||
os_name = 'windows'
|
||||
elif sysname.startswith('darwin') or 'mac' in sysname:
|
||||
os_name = 'darwin'
|
||||
elif 'bsd' in sysname:
|
||||
os_name = 'bsd'
|
||||
elif sysname.startswith('linux'):
|
||||
# try distro if available
|
||||
os_name = 'linux'
|
||||
try:
|
||||
import distro # optional
|
||||
os_name = distro.id() or 'linux'
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
os_name = sysname or 'unknown'
|
||||
except Exception:
|
||||
os_name = 'unknown'
|
||||
array['os'] = os_name
|
||||
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
|
||||
|
||||
@@ -40,16 +40,16 @@
|
||||
],
|
||||
"monitors": [
|
||||
{
|
||||
"name": "baidu",
|
||||
"host": "https://www.baidu.com",
|
||||
"interval": 1200,
|
||||
"name": "抖音",
|
||||
"host": "https://www.douyin.com",
|
||||
"interval": 600,
|
||||
"type": "https"
|
||||
},
|
||||
{
|
||||
"name": "1111",
|
||||
"host": "1.1.1.1:80",
|
||||
"interval": 1200,
|
||||
"type": "tcp"
|
||||
"name": "百度",
|
||||
"host": "https://www.baidu.com",
|
||||
"interval": 600,
|
||||
"type": "https"
|
||||
}
|
||||
],
|
||||
"sslcerts": [
|
||||
@@ -57,21 +57,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"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -153,11 +153,13 @@ names_done:
|
||||
}
|
||||
// alarm logic
|
||||
if(cert->m_aExpireTS>0){
|
||||
int days = (int)((cert->m_aExpireTS - nowt)/86400);
|
||||
int64_t *lastAlarm = NULL; int need=0; int target=0;
|
||||
if(days <=7 && days >3){ lastAlarm=&cert->m_aLastAlarm7; target=7; }
|
||||
else if(days <=3 && days >1){ lastAlarm=&cert->m_aLastAlarm3; target=3; }
|
||||
else if(days <=1){ lastAlarm=&cert->m_aLastAlarm1; target=1; }
|
||||
// 剩余天数: 向下取整 (floor) —— 与 JSON expire_days 保持一致,用于阈值分桶和消息显示
|
||||
int64_t secsLeft = cert->m_aExpireTS - nowt;
|
||||
int days = (int)(secsLeft/86400);
|
||||
int64_t *lastAlarm = NULL; int need=0;
|
||||
if(days <=7 && days >3){ lastAlarm=&cert->m_aLastAlarm7; }
|
||||
else if(days <=3 && days >1){ lastAlarm=&cert->m_aLastAlarm3; }
|
||||
else if(days <=1){ lastAlarm=&cert->m_aLastAlarm1; }
|
||||
if(lastAlarm && (*lastAlarm==0 || nowt - *lastAlarm > 20*3600)) need=1; // avoid spam, 20h
|
||||
if(need && strlen(cert->m_aCallback)>0){
|
||||
CURL *curl = curl_easy_init();
|
||||
@@ -166,7 +168,8 @@ names_done:
|
||||
char timebuf[32];
|
||||
time_t expt = (time_t)cert->m_aExpireTS;
|
||||
strftime(timebuf,sizeof(timebuf),"%Y-%m-%d %H:%M:%S", gmtime(&expt));
|
||||
snprintf(msg,sizeof(msg),"【SSL证书提醒】%s(%s) 将在 %d 天后(%s UTC) 到期", cert->m_aName, cert->m_aDomain, target, timebuf);
|
||||
// 使用 floor(days)
|
||||
snprintf(msg,sizeof(msg),"【SSL证书提醒】%s(%s) 将在 %d 天后(%s UTC) 到期", cert->m_aName, cert->m_aDomain, days, timebuf);
|
||||
char *enc = curl_easy_escape(curl,msg,0);
|
||||
char url[1500]; snprintf(url,sizeof(url),"%s%s", cert->m_aCallback, enc?enc:"");
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
@@ -383,6 +386,9 @@ int CMain::HandleMessage(int ClientNetID, char *pMessage)
|
||||
pClient->m_Stats.m_Online6 = rStart["online6"].u.boolean;
|
||||
if(rStart["custom"].type == json_string)
|
||||
str_copy(pClient->m_Stats.m_aCustom, rStart["custom"].u.string.ptr, sizeof(pClient->m_Stats.m_aCustom));
|
||||
// optional OS field from clients
|
||||
if(rStart["os"].type == json_string)
|
||||
str_copy(pClient->m_Stats.m_aOS, rStart["os"].u.string.ptr, sizeof(pClient->m_Stats.m_aOS));
|
||||
|
||||
//copy message for watchdog to analysis
|
||||
WatchdogMessage(ClientNetID,
|
||||
@@ -630,7 +636,7 @@ void CMain::JSONUpdateThread(void *pUser)
|
||||
}
|
||||
|
||||
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\" },\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, \"memory_total\": %" PRId64 ", \"memory_used\": %" PRId64 ", \"swap_total\": %" PRId64 ", \"swap_used\": %" PRId64 ", \"hdd_total\": %" PRId64 ", \"hdd_used\": %" PRId64 ", \"last_network_in\": %" PRId64 ", \"last_network_out\": %" PRId64 ",\"io_read\": %" PRId64 ", \"io_write\": %" PRId64 ",\"custom\": \"%s\", \"os\": \"%s\" },\n",
|
||||
pClients[i].m_aName,pClients[i].m_aType,pClients[i].m_aHost,pClients[i].m_aLocation,
|
||||
pClients[i].m_Stats.m_Online4 ? "true" : "false",pClients[i].m_Stats.m_Online6 ? "true" : "false",
|
||||
aUptime, pClients[i].m_Stats.m_Load_1, pClients[i].m_Stats.m_Load_5, pClients[i].m_Stats.m_Load_15, pClients[i].m_Stats.m_ping_10010, pClients[i].m_Stats.m_ping_189, pClients[i].m_Stats.m_ping_10086,
|
||||
@@ -640,15 +646,17 @@ void CMain::JSONUpdateThread(void *pUser)
|
||||
pClients[i].m_Stats.m_NetworkIN == 0 || pClients[i].m_LastNetworkIN == 0 ? pClients[i].m_Stats.m_NetworkIN : pClients[i].m_LastNetworkIN,
|
||||
pClients[i].m_Stats.m_NetworkOUT == 0 || pClients[i].m_LastNetworkOUT == 0 ? pClients[i].m_Stats.m_NetworkOUT : pClients[i].m_LastNetworkOUT,
|
||||
pClients[i].m_Stats.m_IORead, pClients[i].m_Stats.m_IOWrite,
|
||||
pClients[i].m_Stats.m_aCustom);
|
||||
pClients[i].m_Stats.m_aCustom,
|
||||
pClients[i].m_Stats.m_aOS[0] ? pClients[i].m_Stats.m_aOS : "");
|
||||
pBuf += strlen(pBuf);
|
||||
}
|
||||
else
|
||||
{
|
||||
// sava network traffic record to json when close client
|
||||
// last_network_in == last network in record, last_network_out == last network out record
|
||||
str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf), "{ \"name\": \"%s\", \"type\": \"%s\", \"host\": \"%s\", \"location\": \"%s\", \"online4\": false, \"online6\": false, \"last_network_in\": %" PRId64 ", \"last_network_out\": %" PRId64 " },\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);
|
||||
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",
|
||||
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 : "");
|
||||
pBuf += strlen(pBuf);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,8 @@ class CMain
|
||||
int64_t m_IOWrite;
|
||||
double m_CPU;
|
||||
char m_aCustom[1024];
|
||||
// OS name reported by client (e.g. linux/windows/darwin/freebsd)
|
||||
char m_aOS[64];
|
||||
// Options
|
||||
bool m_Pong;
|
||||
} m_Stats;
|
||||
|
||||
@@ -94,14 +94,15 @@ 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)}}
|
||||
|
||||
/* modal styles */
|
||||
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:flex;align-items:flex-start;justify-content:center;padding:5vh 1rem;z-index:50;backdrop-filter:blur(4px)}
|
||||
.modal-box{position:relative;width:100%;max-width:560px;background:var(--bg-alt);border:1px solid var(--border);border-radius:16px;box-shadow:0 8px 30px -6px rgba(0,0,0,.6);padding:1.25rem 1.35rem;display:flex;flex-direction:column;gap:.9rem;animation:fade .25s ease}
|
||||
.modal-box.high-load{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)}
|
||||
body:not(.light) .modal-box.high-load{background:linear-gradient(180deg, rgba(239,68,68,.24), rgba(239,68,68,.12)), var(--bg-alt)}
|
||||
.modal-title{margin:0;font-size:16px;font-weight:600;letter-spacing:.5px}
|
||||
.modal-close{position:absolute;top:10px;right:12px;background:transparent;border:0;color:var(--text-dim);font-size:20px;line-height:1;cursor:pointer;padding:4px;border-radius:8px;transition:var(--trans)}
|
||||
.modal-close:hover{color:var(--text);background:var(--bg)}
|
||||
@@ -128,7 +129,7 @@ table.data tbody tr:hover{background:rgba(255,255,255,.04)}
|
||||
.gauge-half path.track{stroke:color-mix(in srgb,var(--text-dim) 18%,transparent);stroke-width:6;}
|
||||
.gauge-half path.arc{stroke:var(--gauge-base,#3b82f6);stroke-width:8;stroke-dasharray:126;stroke-dashoffset:calc(126*(1 - var(--p)));transition:stroke-dashoffset .8s cubic-bezier(.4,0,.2,1),stroke .35s;filter:drop-shadow(0 1px 2px rgba(0,0,0,.45));}
|
||||
.gauge-half[data-type=mem] path.arc{--gauge-base:#10b981}
|
||||
.gauge-half[data-type=hdd] path.arc{--gauge-base:#f59e0b}
|
||||
.gauge-half[data-type=hdd] path.arc{--gauge-base:#06b6d4}
|
||||
/* 阈值颜色:>=50% 警告黄,>=90% 危险红 */
|
||||
.gauge-half[data-warn] path.arc{stroke:var(--warn)}
|
||||
.gauge-half[data-bad] path.arc{stroke:var(--danger)}
|
||||
@@ -230,9 +231,46 @@ body.light .gauge-half .needle{background:linear-gradient(var(--text),var(--text
|
||||
}
|
||||
.cards .card{border:1px solid var(--border);border-radius:12px;padding:.75rem .85rem;background:linear-gradient(145deg,var(--bg),var(--bg-alt));display:flex;flex-direction:column;gap:.45rem;position:relative;}
|
||||
.cards .card.offline{opacity:.6;}
|
||||
.cards .card.high-load{border-color:rgba(239,68,68,.55);box-shadow:0 0 0 1px rgba(239,68,68,.4),0 4px 16px -4px rgba(239,68,68,.3);}
|
||||
table.data tbody tr.high-load{background:rgba(239,68,68,.10);}
|
||||
table.data tbody tr.high-load:hover{background:rgba(239,68,68,.18);}
|
||||
.cards .card.high-load{border-color:rgba(239,68,68,.6);background:linear-gradient(180deg, rgba(239,68,68,.22), rgba(239,68,68,.12)), var(--bg-alt);box-shadow:0 0 0 1px rgba(239,68,68,.48),0 6px 18px -6px rgba(239,68,68,.28);}
|
||||
table.data tbody tr.high-load{background:rgba(239,68,68,.18) !important;}
|
||||
table.data tbody tr.high-load:hover{background:rgba(239,68,68,.26) !important;}
|
||||
|
||||
/* SSL 域名告警底色:与高负载相同 */
|
||||
#sslTable td.alert-domain{background:rgba(239,68,68,.18) !important;}
|
||||
#sslTable tr:hover td.alert-domain{background:rgba(239,68,68,.26) !important;}
|
||||
|
||||
/* OS 着色(更明显):
|
||||
1) 为各 OS 类定义 --os-color 变量
|
||||
2) 行左侧使用 inset box-shadow 画 4px 彩条
|
||||
3) 行背景叠加轻度渐变以提示 OS
|
||||
*/
|
||||
table.data tbody tr[class*="os-"]{box-shadow:inset 4px 0 0 0 var(--os-color, transparent);background:linear-gradient(180deg, color-mix(in srgb, var(--os-color, transparent) 10%, transparent), transparent 60%);}
|
||||
table.data tbody tr[class*="os-"]:hover{background:linear-gradient(180deg, color-mix(in srgb, var(--os-color, transparent) 16%, transparent), transparent 65%);}
|
||||
.cards .card[class*="os-"]{border-color:color-mix(in srgb, var(--os-color, var(--accent)) 60%, transparent);box-shadow:0 0 0 1px color-mix(in srgb, var(--os-color, var(--accent)) 40%, transparent),0 4px 16px -6px color-mix(in srgb, var(--os-color, #000) 35%, transparent);}
|
||||
/* 弹窗 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-linux{--os-color: rgba(16,185,129,.85);} /* 绿色 (通用 Linux,保持不变) */
|
||||
.os-ubuntu{--os-color: rgba(221,72,20,.9);} /* Ubuntu 橙 (#dd4814) */
|
||||
.os-debian{--os-color: rgba(215,10,83,.9);} /* Debian 品红 (#d70a53) */
|
||||
.os-centos{--os-color: rgba(102,0,153,.9);} /* CentOS 紫 (#660099) */
|
||||
.os-rocky{--os-color: rgba(1,122,66,.9);} /* Rocky Linux 绿 (#017a42) */
|
||||
.os-almalinux{--os-color: rgba(0,92,170,.9);} /* AlmaLinux 蓝 (#005caa) */
|
||||
.os-rhel{--os-color: rgba(204,0,0,.9);} /* Red Hat 红 (#cc0000) */
|
||||
.os-arch{--os-color: rgba(23,147,209,.9);} /* Arch Linux 蓝 (#1793d1) */
|
||||
.os-alpine{--os-color: rgba(14,87,123,.9);} /* Alpine Linux 蓝 (#0e577b) */
|
||||
.os-fedora{--os-color: rgba(60,110,180,.9);} /* Fedora 蓝 (#3c6eb4) */
|
||||
.os-amazon{--os-color: rgba(255,153,0,.9);} /* Amazon Linux 橙 (#ff9900) */
|
||||
.os-suse{--os-color: rgba(0,150,0,.9);} /* openSUSE 绿 (#009600) */
|
||||
.os-freebsd{--os-color: rgba(166,31,47,.9);} /* FreeBSD 红 (#a61f2f) */
|
||||
.os-openbsd{--os-color: rgba(255,204,0,.9);} /* OpenBSD 黄 (#ffcc00) */
|
||||
.os-bsd{--os-color: rgba(166,31,47,.9);} /* BSD 系统一般跟 FreeBSD 接近 */
|
||||
.os-darwin{--os-color: rgba(29,29,31,.95);} /* macOS 深空灰 (#1d1d1f) */
|
||||
.os-windows{--os-color: rgba(0,120,215,.95);} /* Windows 蓝 (#0078d7) */
|
||||
|
||||
|
||||
|
||||
/* 旧进度条相关样式已清理 */
|
||||
.cards .card-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem;}
|
||||
@@ -245,12 +283,30 @@ table.data tbody tr.high-load:hover{background:rgba(239,68,68,.18);}
|
||||
.cards .kvlist div{display:flex;flex-direction:column;}
|
||||
.cards .kvlist span.key{opacity:.6;}
|
||||
.cards .buckets{margin-top:.25rem;}
|
||||
.cards .expand-btn{position:absolute;top:.5rem;right:.5rem;background:transparent;border:0;color:var(--text-dim);cursor:pointer;font-size:.9rem;padding:.2rem;}
|
||||
.cards .expand-btn:focus, .cards .expand-btn:hover{color:var(--text);}
|
||||
.cards .expand-area{margin-top:.4rem;display:none;animation:fadeIn .25s ease;}
|
||||
.cards .card.expanded .expand-area{display:block;}
|
||||
/* 证书卡片:域名告警底色(与高负载卡片风格一致) */
|
||||
.cards .kvlist .alert-domain{background:rgba(239,68,68,.18);border:1px solid rgba(239,68,68,.35);border-radius:8px;padding:.4rem .5rem;}
|
||||
.cards .kvlist .alert-domain .key{opacity:.85}
|
||||
/* 移除移动端卡片展开箭头与展开区域(已按需简化交互) */
|
||||
/* 旧移动端 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}
|
||||
|
||||
/* 服务监测项:不同组竖排,同一组横排;不考虑自动换行 */
|
||||
.mon-items{display:flex;flex-direction:column;gap:6px;align-items:flex-start}
|
||||
.mon-item{display:inline-flex;align-items:center;white-space:nowrap;line-height:1}
|
||||
.mon-item .name{margin-right:6px}
|
||||
.mon-item .ms{margin-left:6px;font-variant-numeric:tabular-nums}
|
||||
.mon-item .sig{margin:0 6px;transform:translateY(-1px)}
|
||||
|
||||
/* 新 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)}
|
||||
|
||||
228
web/js/app.js
228
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<units.length-1){ mb/=1000;i++; }
|
||||
@@ -27,35 +27,82 @@ function humanMinKBFromB(bytes){ if(bytes==null||isNaN(bytes)) return '-'; //
|
||||
function humanAgo(ts){ if(!ts) return '-'; const s=Math.floor((Date.now()/1000 - ts)); const m=Math.floor(s/60); return m>0? m+' 分钟前':'几秒前'; }
|
||||
function num(v){ return (typeof v==='number' && !isNaN(v)) ? v : '-'; }
|
||||
|
||||
// 将服务端上报的 os 映射为样式类名(用于为行/卡片着色)
|
||||
function osClass(os){
|
||||
if(!os) return '';
|
||||
const v = String(os).toLowerCase();
|
||||
const pick = (k)=>' os-'+k;
|
||||
if(v.includes('ubuntu')) return pick('ubuntu');
|
||||
if(v.includes('debian')) return pick('debian');
|
||||
if(v.includes('centos')) return pick('centos');
|
||||
if(v.includes('rocky')) return pick('rocky');
|
||||
if(v.includes('alma')) return pick('almalinux');
|
||||
if(v.includes('arch')) return pick('arch');
|
||||
if(v.includes('alpine')) return pick('alpine');
|
||||
if(v.includes('fedora')) return pick('fedora');
|
||||
if(v.includes('rhel') || v.includes('redhat')) return pick('rhel');
|
||||
if(v.includes('suse')) return pick('suse');
|
||||
if(v.includes('amazon')) return pick('amazon');
|
||||
if(v.includes('freebsd')) return pick('freebsd');
|
||||
if(v.includes('openbsd')) return pick('openbsd');
|
||||
if(v.includes('netbsd') || v.includes('bsd')) return pick('bsd');
|
||||
// macOS / Darwin 变体:darwin | macOS | os x | osx | apple
|
||||
if(v.includes('darwin') || v.includes('macos') || v.includes('os x') || v.includes('osx') || v.includes('apple') || v.includes('mac')) return pick('darwin');
|
||||
if(v.includes('win')) return pick('windows');
|
||||
if(v.includes('linux')) return pick('linux');
|
||||
return pick(v.replace(/[^a-z0-9_-]+/g,'-').slice(0,20));
|
||||
}
|
||||
|
||||
// 将服务端 os 字段转为友好的显示名称
|
||||
function osLabel(os){
|
||||
if(!os) return '';
|
||||
const v = String(os).toLowerCase();
|
||||
const is = (k)=>v.includes(k);
|
||||
if(is('ubuntu')) return 'Ubuntu';
|
||||
if(is('debian')) return 'Debian';
|
||||
if(is('centos')) return 'CentOS';
|
||||
if(is('rocky')) return 'Rocky Linux';
|
||||
if(is('alma')) return 'AlmaLinux';
|
||||
if(is('rhel') || is('redhat')) return 'Red Hat Enterprise Linux';
|
||||
if(is('arch')) return 'Arch Linux';
|
||||
if(is('alpine')) return 'Alpine Linux';
|
||||
if(is('fedora')) return 'Fedora';
|
||||
if(is('amazon')) return 'Amazon Linux';
|
||||
if(is('suse')) return 'SUSE Linux';
|
||||
if(is('freebsd')) return 'FreeBSD';
|
||||
if(is('openbsd')) return 'OpenBSD';
|
||||
if(is('netbsd') || is('bsd')) return 'BSD';
|
||||
if(is('darwin') || is('macos') || is('os x') || is('osx') || is('apple') || is('mac')) return 'macOS';
|
||||
if(is('win')) return 'Windows';
|
||||
if(is('linux')) return 'Linux';
|
||||
// 默认:首字母大写
|
||||
return String(os).charAt(0).toUpperCase() + String(os).slice(1);
|
||||
}
|
||||
|
||||
async function fetchData(){
|
||||
try {
|
||||
const r = await fetch('json/stats.json?_='+Date.now());
|
||||
if(!r.ok) throw new Error(r.status);
|
||||
const j = await r.json();
|
||||
if(j.reload) location.reload();
|
||||
S.updated = j.updated; S.servers = j.servers||[]; S.ssl = j.sslcerts||[]; S.error=false;
|
||||
// 更新延迟历史 (按节点名聚合)
|
||||
S.servers.forEach(s=>{
|
||||
const key = s.name || s.location || 'node';
|
||||
S.updated = j.updated; S.servers = j.servers||[]; S.ssl = j.sslcerts||[]; S.error=false;
|
||||
// 为每个服务器生成唯一 key(基于 name|location|type + 顺序号),避免同名节点写入同一历史
|
||||
const keyCount = Object.create(null);
|
||||
S.servers.forEach((s, idx)=>{
|
||||
const base = [s.name||'-', s.location||'-', s.type||'-'].join('|');
|
||||
const seq = (keyCount[base]||0) + 1; keyCount[base] = seq;
|
||||
const key = `${base}#${seq}`;
|
||||
s._key = key; // 挂到对象上,后续查找/弹窗均用它
|
||||
if(!S.hist[key]) S.hist[key] = {cu:[],ct:[],cm:[]};
|
||||
const H = S.hist[key];
|
||||
// 使用 time_ 字段 (ms) 若不存在则跳过
|
||||
if(typeof s.time_10010 === 'number') H.cu.push(s.time_10010);
|
||||
if(typeof s.time_189 === 'number') H.ct.push(s.time_189);
|
||||
if(typeof s.time_10086 === 'number') H.cm.push(s.time_10086);
|
||||
const MAX=120; // 保留约 120*4s ≈ 8 分钟
|
||||
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];
|
||||
@@ -82,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;
|
||||
@@ -99,10 +146,10 @@ function renderServers(){
|
||||
const p1 = (s.ping_10010||0); const p2 = (s.ping_189||0); const p3 = (s.ping_10086||0);
|
||||
function bucket(p){ const v = Math.max(0, Math.min(100, p)); const level = v>=20?'bad':(v>=10?'warn':'ok'); return `<div class=\"bucket\" data-lv=\"${level}\"><span style=\"--h:${v}%\"></span><label>${v.toFixed(0)}%</label></div>`; }
|
||||
const pingBuckets = `<div class=\"buckets\" title=\"CU/CT/CM\">${bucket(p1)}${bucket(p2)}${bucket(p3)}</div>`;
|
||||
const key = s.name || s.location || 'node';
|
||||
// 唯一 key 已附加为 s._key(如需使用)
|
||||
const rowCursor = online? 'pointer':'default';
|
||||
const highLoad = online && ( (s.cpu||0)>=90 || (memPct)>=90 || (hddPct)>=90 );
|
||||
html += `<tr data-idx="${idx}" data-online="${online?1:0}" class="row-server${highLoad?' high-load':''}" style="cursor:${rowCursor};${online?'':'opacity:.65;'}">
|
||||
html += `<tr data-idx="${idx}" data-online="${online?1:0}" class="row-server${highLoad?' high-load':''}${osClass(s.os)}" style="cursor:${rowCursor};${online?'':'opacity:.65;'}">
|
||||
<td>${statusPill}</td>
|
||||
<td><span class="${trafficCls}" title="本月下行 | 上行 (≥500GB 触发红黄)"><span class="half in">${monthIn}</span><span class="half out">${monthOut}</span></span></td>
|
||||
<td>${s.name||'-'}</td>
|
||||
@@ -170,17 +217,16 @@ function renderServersCards(){
|
||||
const p1 = (s.ping_10010||0); const p2=(s.ping_189||0); const p3=(s.ping_10086||0);
|
||||
function bucket(p){ const v=Math.max(0,Math.min(100,p)); const level = v>=20?'bad':(v>=10?'warn':'ok'); return `<div class=\"bucket\" data-lv=\"${level}\"><span style=\"--h:${v}%\"></span><label>${v.toFixed(0)}%</label></div>`; }
|
||||
const buckets = `<div class=\"buckets\">${bucket(p1)}${bucket(p2)}${bucket(p3)}</div>`;
|
||||
const key = s.name || s.location || 'node';
|
||||
// 唯一 key 已附加为 s._key(如需使用)
|
||||
const highLoad = online && ( (s.cpu||0)>=90 || (memPct)>=90 || (hddPct)>=90 );
|
||||
html += `<div class=\"card${online?'':' offline'}${highLoad?' high-load':''}\" data-idx=\"${idx}\" data-online=\"${online?1:0}\">\n <button class=\"expand-btn\" aria-label=\"展开\">▼</button>\n <div class=\"card-header\">\n <div class=\"card-title\">${s.name||'-'} <span class=\"tag\">${s.location||'-'}</span></div>\n ${pill}\n </div>\n <div class=\"kvlist\">\n <div><span class=\"key\">负载</span><span>${s.load_1==-1?'–':s.load_1?.toFixed(2)}</span></div>\n <div><span class=\"key\">在线</span><span>${s.uptime||'-'}</span></div>\n <div><span class=\"key\">月流量</span><span><span class=\"${trafficCls}\" title=\"本月下行 | 上行 (≥500GB 触发红黄)\"><span class=\"half in\">${monthIn}</span><span class=\"half out\">${monthOut}</span></span></span></div>\n <div><span class=\"key\">网络</span><span>${netNow}</span></div>\n <div><span class=\"key\">总流量</span><span>${netTotal}</span></div>\n <div><span class=\"key\">CPU</span><span>${s.cpu||0}%</span></div>\n <div><span class=\"key\">内存</span><span>${memPct.toFixed(0)}%</span></div>\n <div><span class=\"key\">硬盘</span><span>${hddPct.toFixed(0)}%</span></div>\n </div>\n ${buckets}\n <div class=\"expand-area\">\n <div style=\"font-size:.65rem;opacity:.7;margin-top:.3rem\">${online?'点击卡片可查看详情':'离线,不可查看详情'}</div>\n </div>\n </div>`;
|
||||
html += `<div class=\"card${online?'':' offline'}${highLoad?' high-load':''}${osClass(s.os)}\" data-idx=\"${idx}\" data-online=\"${online?1:0}\">\n <div class=\"card-header\">\n <div class=\"card-title\">${s.name||'-'} <span class=\"tag\">${s.location||'-'}</span></div>\n ${pill}\n </div>\n <div class=\"kvlist\">\n <div><span class=\"key\">负载</span><span>${s.load_1==-1?'–':s.load_1?.toFixed(2)}</span></div>\n <div><span class=\"key\">在线</span><span>${s.uptime||'-'}</span></div>\n <div><span class=\"key\">月流量</span><span><span class=\"${trafficCls}\" title=\"本月下行 | 上行 (≥500GB 触发红黄)\"><span class=\"half in\">${monthIn}</span><span class=\"half out\">${monthOut}</span></span></span></div>\n <div><span class=\"key\">网络</span><span>${netNow}</span></div>\n <div><span class=\"key\">总流量</span><span>${netTotal}</span></div>\n <div><span class=\"key\">CPU</span><span>${s.cpu||0}%</span></div>\n <div><span class=\"key\">内存</span><span>${memPct.toFixed(0)}%</span></div>\n <div><span class=\"key\">硬盘</span><span>${hddPct.toFixed(0)}%</span></div>\n </div>\n ${buckets}\n </div>`;
|
||||
});
|
||||
wrap.innerHTML = html || '<div class="muted" style="font-size:.75rem;text-align:center;padding:1rem;">无数据</div>';
|
||||
wrap.querySelectorAll('.card').forEach(card=>{
|
||||
const idx = parseInt(card.getAttribute('data-idx'));
|
||||
card.addEventListener('click', e=>{
|
||||
if(e.target.classList.contains('expand-btn')){ card.classList.toggle('expanded'); e.stopPropagation(); return;}
|
||||
card.addEventListener('click', ()=>{
|
||||
if(card.getAttribute('data-online')!=='1') return; // 离线不弹
|
||||
openDetail(idx);
|
||||
openDetail(idx);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -188,12 +234,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 = [20,50,100,160];
|
||||
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>`;
|
||||
@@ -204,14 +273,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 = [20,50,100,160];
|
||||
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>`;
|
||||
});
|
||||
@@ -225,7 +314,9 @@ function renderSSL(){
|
||||
const cls = c.expire_days<=0? 'err': c.expire_days<=7? 'warn':'ok';
|
||||
const status = c.expire_days<=0? '已过期': c.expire_days<=7? '将到期':'正常';
|
||||
const dt = c.expire_ts? new Date(c.expire_ts*1000).toISOString().replace('T',' ').replace(/\.\d+Z/,''):'-';
|
||||
html += `<tr>
|
||||
// 当证书进入警告/错误状态时,高亮整行底色(复用 high-load 行样式)
|
||||
const rowCls = (cls !== 'ok') ? 'high-load' : '';
|
||||
html += `<tr class="${rowCls}">
|
||||
<td>${c.name||'-'}</td>
|
||||
<td>${(c.domain||'').replace(/^https?:\/\//,'')}</td>
|
||||
<td>${c.port||443}</td>
|
||||
@@ -246,12 +337,13 @@ function renderSSLCards(){
|
||||
const cls = c.expire_days<=0? 'err': c.expire_days<=7? 'warn':'ok';
|
||||
const status = c.expire_days<=0? '已过期': c.expire_days<=7? '将到期':'正常';
|
||||
const dt = c.expire_ts? new Date(c.expire_ts*1000).toISOString().replace('T',' ').replace(/\.\d+Z/,''):'-';
|
||||
html += `<div class="card">
|
||||
const cardHigh = (cls !== 'ok') ? ' high-load' : '';
|
||||
html += `<div class="card${cardHigh}">
|
||||
<div class="card-header"><div class="card-title">${c.name||'-'}</div><span class="status-pill ${cls==='err'?'off':'on'}">${status}</span></div>
|
||||
<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>`;
|
||||
@@ -306,7 +398,16 @@ function openDetail(i){
|
||||
const s = S.servers[i]; if(!s) return;
|
||||
const box = document.getElementById('detailContent');
|
||||
const modal = document.getElementById('detailModal');
|
||||
document.getElementById('detailTitle').textContent = s.name + ' 详情';
|
||||
const modalBox = modal.querySelector('.modal-box');
|
||||
const osText = osLabel(s.os);
|
||||
const titleEl = document.getElementById('detailTitle');
|
||||
titleEl.textContent = s.name + ' 详情';
|
||||
if(osText){
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'os-chip' + osClass(s.os);
|
||||
chip.textContent = osText;
|
||||
titleEl.appendChild(chip);
|
||||
}
|
||||
const offline = !(s.online4||s.online6);
|
||||
const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0;
|
||||
const swapPct = s.swap_total? (s.swap_used/s.swap_total*100):0;
|
||||
@@ -314,9 +415,8 @@ 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.name || s.location || 'node';
|
||||
// 保留延迟数据用于图表
|
||||
const key = s._key || [s.name||'-', s.location||'-', s.type||'-'].join('|')+'#1';
|
||||
|
||||
let latencyBlock = '';
|
||||
if(!offline){
|
||||
@@ -327,7 +427,7 @@ function openDetail(i){
|
||||
<span style="color:#3b82f6">● 联通 (<span id="lat-cu">${num(s.time_10010)}ms</span>)</span>
|
||||
<span style="color:#10b981">● 电信 (<span id="lat-ct">${num(s.time_189)}ms</span>)</span>
|
||||
<span style="color:#f59e0b">● 移动 (<span id="lat-cm">${num(s.time_10086)}ms</span>)</span>
|
||||
<span style="opacity:.6"> (~${S.hist[key]?S.hist[key].cu.length:0} 条)</span>
|
||||
<span style="opacity:.6"> (~<span id="lat-count">${(S.hist[key]?Math.max(S.hist[key].cu.length, S.hist[key].ct.length, S.hist[key].cm.length):0)}</span> 条)</span>
|
||||
</div>
|
||||
</div>`;
|
||||
} else {
|
||||
@@ -369,21 +469,24 @@ function openDetail(i){
|
||||
<div style="display:flex;flex-direction:column;gap:.35rem;">
|
||||
<canvas id="loadChart" height="120" style="width:100%;border:1px solid var(--border);border-radius:10px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));"></canvas>
|
||||
<div class="mono" style="font-size:11px;display:flex;gap:.9rem;flex-wrap:wrap;align-items:center;opacity:.8;">
|
||||
<span style="color:#8b5cf6">● load1</span>
|
||||
<span style="color:#10b981">● load5</span>
|
||||
<span style="color:#f59e0b">● load15</span>
|
||||
<span style="opacity:.6">(~${(S.loadHist[key]?S.loadHist[key].l1.length:0)} 条)</span>
|
||||
<span style="color:#8b5cf6">● load1 (<span id="load1-val">${s.load_1==-1?'–':Math.max(0,(s.load_1||0)).toFixed(2)}</span>)</span>
|
||||
<span style="color:#10b981">● load5 (<span id="load5-val">${s.load_5==-1?'–':Math.max(0,(s.load_5||0)).toFixed(2)}</span>)</span>
|
||||
<span style="color:#f59e0b">● load15 (<span id="load15-val">${s.load_15==-1?'–':Math.max(0,(s.load_15||0)).toFixed(2)}</span>)</span>
|
||||
<span style="opacity:.6">(~<span id="load-count">${(S.loadHist[key]?Math.max(S.loadHist[key].l1.length, S.loadHist[key].l5.length, S.loadHist[key].l15.length):0)}</span> 条)</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 进度条移除:读/写/虚存以文本形式显示于上方合并行 -->
|
||||
${latencyBlock}
|
||||
`;
|
||||
modal.style.display='flex';
|
||||
// 根据高负载阈值(>=90% 任一项)给弹窗加高亮底色
|
||||
const highLoad = (s.cpu||0) >= 90 || memPct >= 90 || hddPct >= 90;
|
||||
if(modalBox){ modalBox.classList.toggle('high-load', highLoad); }
|
||||
document.addEventListener('keydown', escCloseOnce);
|
||||
if(!offline){
|
||||
drawLatencyChart(key);
|
||||
drawLoadChart(key);
|
||||
S._openDetailKey = key; // 记录当前弹窗对应节点
|
||||
S._openDetailKey = key; // 记录当前弹窗对应节点(唯一 key)
|
||||
startDetailAutoUpdate();
|
||||
} else {
|
||||
S._openDetailKey = null;
|
||||
@@ -391,7 +494,14 @@ function openDetail(i){
|
||||
}
|
||||
}
|
||||
function escCloseOnce(e){ if(e.key==='Escape'){ closeDetail(); } }
|
||||
function closeDetail(){ const m=document.getElementById('detailModal'); m.style.display='none'; document.removeEventListener('keydown', escCloseOnce); stopDetailAutoUpdate(); }
|
||||
function closeDetail(){
|
||||
const m=document.getElementById('detailModal');
|
||||
m.style.display='none';
|
||||
const b = m.querySelector('.modal-box');
|
||||
if(b) b.classList.remove('high-load');
|
||||
document.removeEventListener('keydown', escCloseOnce);
|
||||
stopDetailAutoUpdate();
|
||||
}
|
||||
document.getElementById('detailClose').addEventListener('click', closeDetail);
|
||||
document.getElementById('detailModal').addEventListener('click', e=>{ if(e.target.id==='detailModal') closeDetail(); });
|
||||
|
||||
@@ -493,21 +603,25 @@ function drawLoadChart(key){
|
||||
});
|
||||
}
|
||||
|
||||
//# sourceMappingURL=app.js.map
|
||||
// source map 注释移除,避免 404 请求
|
||||
|
||||
// ====== 详情动态刷新 ======
|
||||
function findServerByKey(key){ return S.servers.find(x=> (x.name||x.location||'node')===key); }
|
||||
function findServerByKey(key){ return S.servers.find(x=> (x._key)===key); }
|
||||
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){
|
||||
const H = S.hist[key];
|
||||
const n = H ? Math.max(H.cu.length||0, H.ct.length||0, H.cm.length||0) : 0;
|
||||
latCntEl.textContent = n;
|
||||
}
|
||||
// 资源动态刷新
|
||||
const memLineEl = document.getElementById('mem-line');
|
||||
if(memLineEl){
|
||||
@@ -532,6 +646,12 @@ function updateDetailMetrics(key){
|
||||
}
|
||||
const ioReadEl = document.getElementById('io-read'); if(ioReadEl){ const v = (typeof s.io_read==='number')? s.io_read:0; ioReadEl.textContent = humanRateMinMBFromB(v); ioReadEl.style.color = v>100*1000*1000? 'var(--danger)':''; }
|
||||
const ioWriteEl = document.getElementById('io-write'); if(ioWriteEl){ const v = (typeof s.io_write==='number')? s.io_write:0; ioWriteEl.textContent = humanRateMinMBFromB(v); ioWriteEl.style.color = v>100*1000*1000? 'var(--danger)':''; }
|
||||
// 动态刷新负载标签与条数
|
||||
const l1El = document.getElementById('load1-val'); if(l1El) l1El.textContent = s.load_1==-1?'–':Math.max(0,(s.load_1||0)).toFixed(2);
|
||||
const l5El = document.getElementById('load5-val'); if(l5El) l5El.textContent = s.load_5==-1?'–':Math.max(0,(s.load_5||0)).toFixed(2);
|
||||
const l15El = document.getElementById('load15-val'); if(l15El) l15El.textContent = s.load_15==-1?'–':Math.max(0,(s.load_15||0)).toFixed(2);
|
||||
const cntEl = document.getElementById('load-count');
|
||||
if(cntEl){ const L=S.loadHist[key]; const n = L? Math.max(L.l1.length, L.l5.length, L.l15.length):0; cntEl.textContent = n; }
|
||||
}
|
||||
function startDetailAutoUpdate(){ stopDetailAutoUpdate(); S._detailTimer = setInterval(()=>{ if(S._openDetailKey) updateDetailMetrics(S._openDetailKey); }, 1000); }
|
||||
function stopDetailAutoUpdate(){ if(S._detailTimer){ clearInterval(S._detailTimer); S._detailTimer=null; } }
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# coding: utf-8
|
||||
# Update by : https://github.com/cppla/ServerStatus, Update date: 20211009
|
||||
# 支持Python版本:2.7 to 3.9; requirements.txt: requests, PrettyTable
|
||||
# 主要是为了受到CC attack时候方便查看机器状态
|
||||
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
import time
|
||||
from prettytable import PrettyTable
|
||||
|
||||
scroll = True
|
||||
clear = lambda: os.system('clear' if 'linux' in sys.platform or 'darwin' in sys.platform else 'cls')
|
||||
|
||||
|
||||
def sscmd(address):
|
||||
while True:
|
||||
r = requests.get(
|
||||
url=address,
|
||||
headers={
|
||||
"User-Agent": "ServerStatus/20181203",
|
||||
}
|
||||
)
|
||||
jsonR = r.json()
|
||||
|
||||
ss = PrettyTable(
|
||||
[
|
||||
"月流量 ↓|↑",
|
||||
"节点名",
|
||||
"位置",
|
||||
"在线时间",
|
||||
"负载",
|
||||
"网络 ↓|↑",
|
||||
"总流量 ↓|↑",
|
||||
"处理器",
|
||||
"内存",
|
||||
"硬盘"
|
||||
],
|
||||
)
|
||||
for i in jsonR["servers"]:
|
||||
if i["online4"] is False and i["online6"] is False:
|
||||
ss.add_row(
|
||||
[
|
||||
'0.00G',
|
||||
"%s" % i["name"],
|
||||
"%s" % i["location"],
|
||||
'-',
|
||||
'-',
|
||||
'-',
|
||||
'-',
|
||||
'-',
|
||||
'-',
|
||||
'-',
|
||||
]
|
||||
)
|
||||
else:
|
||||
ss.add_row(
|
||||
[
|
||||
"%.2fG|%.2fG" % (float(i["last_network_in"]) / 1024 / 1024 / 1024, float(i["last_network_out"]) / 1024 / 1024 / 1024),
|
||||
"%s" % i["name"],
|
||||
# "%s" % i["type"],
|
||||
"%s" % i["location"],
|
||||
"%s" % i["uptime"],
|
||||
"%s" % (i["load_1"]),
|
||||
"%.2fM|%.2fM" % (float(i["network_rx"]) / 1000 / 1000, float(i["network_tx"]) / 1000 / 1000),
|
||||
"%.2fG|%.2fG" % (
|
||||
float(i["network_in"]) / 1024 / 1024 / 1024, float(i["network_out"]) / 1024 / 1024 / 1024),
|
||||
"%d%%" % (i["cpu"]),
|
||||
"%d%%" % (float(i["memory_used"]) / i["memory_total"] * 100),
|
||||
"%d%%" % (float(i["hdd_used"]) / i["hdd_total"] * 100),
|
||||
]
|
||||
)
|
||||
if scroll is True:
|
||||
clear()
|
||||
print(ss)
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
default = 'https://tz.cloudcpp.com/json/stats.json'
|
||||
ads = sys.argv[1] if len(sys.argv) == 2 else default
|
||||
sscmd(ads)
|
||||
Reference in New Issue
Block a user