mirror of
https://github.com//cppla/ServerStatus
synced 2025-08-21 01:52:21 +08:00
Compare commits
141 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
15fe753a0f | ||
|
0d185f628b | ||
|
83977b55bf | ||
|
49decdfed1 | ||
|
227e15fe48 | ||
|
73b3d16ff6 | ||
|
c04256535d | ||
|
3c8ebcf710 | ||
|
9a19d2f4bf | ||
|
d3c5e4a02a | ||
|
a03cda0ef0 | ||
|
d25e7871ba | ||
|
fd6c3532d9 | ||
|
89745f3c3a | ||
|
266230f5f0 | ||
|
4c17623dff | ||
|
34e30d9873 | ||
|
1e190878dd | ||
|
874c122a62 | ||
|
fa984cfae6 | ||
|
a544f72db0 | ||
|
eea993c6cc | ||
|
a4a3734c30 | ||
|
a10e39b6f2 | ||
|
360d8d008d | ||
|
830938eac9 | ||
|
1a764ed104 | ||
|
b0c543b44b | ||
|
f85c0a87f9 | ||
|
67d7c5ea0f | ||
|
8576f81404 | ||
|
fa0ee2bf57 | ||
|
e9776b0e69 | ||
|
e01cc118dc | ||
|
784c40b153 | ||
|
6068a2ba17 | ||
|
ced526824f | ||
|
7d2a73bf2e | ||
|
51e2f664cd | ||
|
937b1ea90f | ||
|
b95d1945bf | ||
|
e8446cd52f | ||
|
6098f0fb1a | ||
|
fc89a76cc2 | ||
|
f290947b2c | ||
|
b3adc6b782 | ||
|
705a957606 | ||
|
a307ad7d19 | ||
|
ad95e00723 | ||
|
31330168f3 | ||
|
d9e8f8a7c1 | ||
|
08f15ebdc5 | ||
|
adb05818b9 | ||
|
4e73e8185e | ||
|
0fe01064a4 | ||
|
510567eaec | ||
|
503037c7e2 | ||
|
d75d5438a3 | ||
|
91f11dad76 | ||
|
fdc5abacfc | ||
|
388938e02b | ||
|
f912794068 | ||
|
6331d7d45b | ||
|
25f878a38a | ||
|
a8b9b2d00d | ||
|
dc3868998a | ||
|
75d06c8666 | ||
|
360252182d | ||
|
aa0ccd254c | ||
|
f7b2e7db42 | ||
|
052c75ef23 | ||
|
1f7827cc21 | ||
|
c236aee5dc | ||
|
fb3ecd1796 | ||
|
810f6a0d8a | ||
|
eddf37c413 | ||
|
8dfc6d4718 | ||
|
03446b8f29 | ||
|
6ab046be51 | ||
|
94708c588c | ||
|
144987bcf1 | ||
|
7e46b1062e | ||
|
71cadcfebe | ||
|
d96e711a34 | ||
|
5ebf076330 | ||
|
4a9f5adb6b | ||
|
59fa2d8eab | ||
|
bd4c22edff | ||
|
88229b2f4a | ||
|
d70a56a767 | ||
|
a177345858 | ||
|
11a03a020c | ||
|
ed330bd785 | ||
|
77cb62ae80 | ||
|
7bbca5e881 | ||
|
2e0d1f7fb9 | ||
|
da83c54e5b | ||
|
3ef6cb52c1 | ||
|
67877bacfc | ||
|
a8383efe93 | ||
|
03682772a1 | ||
|
fdca4fd353 | ||
|
a5330dcf2f | ||
|
be78a7cc88 | ||
|
46d43c247f | ||
|
6ae278857a | ||
|
d5e59453f1 | ||
|
d242c6cea1 | ||
|
d7614ab168 | ||
|
ff06df8263 | ||
|
9ee1516f72 | ||
|
7603ce37af | ||
|
5cfd533daf | ||
|
9077f1058a | ||
|
e34ed05901 | ||
|
4ab545f646 | ||
|
a6b4248967 | ||
|
8ad927d88d | ||
|
4b1ec57c6d | ||
|
979a9b5d87 | ||
|
52d62afa59 | ||
|
806b60bd6b | ||
|
d5af5445df | ||
|
c77e00476c | ||
|
b551998eb8 | ||
|
1c606cc79e | ||
|
d602791b11 | ||
|
a695ef8b3a | ||
|
9f60b7962b | ||
|
34b1bca5b1 | ||
|
25917f0883 | ||
|
333bc29c88 | ||
|
c116067c39 | ||
|
14db7ec943 | ||
|
af8244f1d7 | ||
|
1004c9a6bd | ||
|
7f78af03f0 | ||
|
02bbdb18de | ||
|
6d813f932d | ||
|
19d6a5ea8b | ||
|
35de279e89 |
12
Dockerfile
12
Dockerfile
@ -1,7 +1,7 @@
|
||||
# The Dockerfile for build localhost source, not git repo
|
||||
FROM debian:buster as builder
|
||||
FROM debian:bookworm AS builder
|
||||
|
||||
MAINTAINER cppla https://cpp.la
|
||||
LABEL maintainer="cppla <https://cpp.la>"
|
||||
|
||||
RUN apt-get update -y && apt-get -y install gcc g++ make libcurl4-openssl-dev
|
||||
|
||||
@ -9,13 +9,13 @@ COPY . .
|
||||
|
||||
WORKDIR /server
|
||||
|
||||
RUN make
|
||||
RUN make -j
|
||||
RUN pwd && ls -a
|
||||
|
||||
# glibc env run
|
||||
FROM nginx:latest
|
||||
|
||||
RUN mkdir -p /ServerStatus/server/
|
||||
RUN mkdir -p /ServerStatus/server/ && ln -sf /dev/null /var/log/nginx/access.log && ln -sf /dev/null /var/log/nginx/error.log
|
||||
|
||||
COPY --from=builder server /ServerStatus/server/
|
||||
COPY --from=builder web /usr/share/nginx/html/
|
||||
@ -25,5 +25,5 @@ ENV TZ=Asia/Shanghai
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
EXPOSE 80 35601
|
||||
|
||||
CMD nohup sh -c '/etc/init.d/nginx start && /ServerStatus/server/sergate --config=/ServerStatus/server/config.json --web-dir=/usr/share/nginx/html'
|
||||
HEALTHCHECK --interval=5s --timeout=3s --retries=3 CMD curl --fail http://localhost:80 || bash -c 'kill -s 15 -1 && (sleep 10; kill -s 9 -1)'
|
||||
CMD ["sh", "-c", "/etc/init.d/nginx start && /ServerStatus/server/sergate --config=/ServerStatus/server/config.json --web-dir=/usr/share/nginx/html"]
|
||||
|
108
README.md
108
README.md
@ -1,27 +1,26 @@
|
||||
# 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只是为了防止频繁收到报警信息造成骚扰,并不是探测间隔。`
|
||||
`Watchdog触发式告警,interval只是为了防止频繁收到报警信息造成的骚扰,并不是探测间隔。值得注意的是,Exprtk库默认使用窄字符类型,中文等Unicode字符无法解析计算,等待修复。 `
|
||||
|
||||
# 目录介绍:
|
||||
# 目录:
|
||||
|
||||
* clients 客户端文件
|
||||
* server 服务端文件
|
||||
* web 网站文件
|
||||
|
||||
* server/config.json 探针配置文件
|
||||
* server/config.json 探针配置文件
|
||||
* web/json 探针月流量
|
||||
|
||||
# 自动部署:
|
||||
# 部署:
|
||||
|
||||
【服务端】:
|
||||
```bash
|
||||
@ -31,7 +30,7 @@
|
||||
wget --no-check-certificate -qO ~/serverstatus-config.json https://raw.githubusercontent.com/cppla/ServerStatus/master/server/config.json && mkdir ~/serverstatus-monthtraffic
|
||||
docker run -d --restart=always --name=serverstatus -v ~/serverstatus-config.json:/ServerStatus/server/config.json -v ~/serverstatus-monthtraffic:/usr/share/nginx/html/json -p 80:80 -p 35601:35601 cppla/serverstatus:latest
|
||||
|
||||
`Docker-compose`: docker-compose up -d
|
||||
`Docker-compose(推荐)`: docker-compose up -d
|
||||
```
|
||||
|
||||
【客户端】:
|
||||
@ -42,6 +41,15 @@ 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 />
|
||||
|
||||
|
||||
# 手动安装教程:
|
||||
|
||||
**【服务端配置】**
|
||||
@ -58,13 +66,14 @@ cd ServerStatus/server && make
|
||||
|
||||
#### 二、修改配置文件
|
||||
```diff
|
||||
! watchdog rule 可以为任何已知字段的表达式。
|
||||
! watchdog interval 最小通知间隔。
|
||||
! watchdog callback 可自定义为Get方法的URL,告警内容将拼接其后并发起回调。
|
||||
! watchdog rule 可以为任何已知字段的表达式。注意Exprtk库默认使用窄字符类型,中文等Unicode字符无法解析计算,等待修复
|
||||
! watchdog interval 最小通知间隔
|
||||
! watchdog callback 可自定义为Post方法的URL,告警内容将拼接其后并发起回调
|
||||
|
||||
! watchdog callback Telegram:https://api.telegram.org/bot你自己的密钥/sendMessage?parse_mode=HTML&disable_web_page_preview=true&chat_id=你自己的标识&text=
|
||||
! watchdog callback Server酱: https://sctapi.ftqq.com/你自己的密钥.send?title=ServerStatus&desp=
|
||||
! watchdog callback PushDeer: https://api2.pushdeer.com/message/push?pushkey=你自己的密钥&text=
|
||||
! Telegram: https://api.telegram.org/bot你自己的密钥/sendMessage?parse_mode=HTML&disable_web_page_preview=true&chat_id=你自己的标识&text=
|
||||
! Server酱: https://sctapi.ftqq.com/你自己的密钥.send?title=ServerStatus&desp=
|
||||
! PushDeer: https://api2.pushdeer.com/message/push?pushkey=你自己的密钥&text=
|
||||
! HttpBasicAuth: https://用户名:密码@你自己的域名/api/push?message=
|
||||
```
|
||||
|
||||
```
|
||||
@ -79,13 +88,72 @@ cd ServerStatus/server && make
|
||||
"location": "🇨🇳",
|
||||
"password": "USER_DEFAULT_PASSWORD",
|
||||
"monthstart": 1
|
||||
}
|
||||
],
|
||||
"monitors": [
|
||||
{
|
||||
"name": "监测网站,默认为一天在线率",
|
||||
"host": "https://www.baidu.com",
|
||||
"interval": 1200,
|
||||
"type": "https"
|
||||
},
|
||||
{
|
||||
"name": "监测tcp服务端口",
|
||||
"host": "1.1.1.1:80",
|
||||
"interval": 1200,
|
||||
"type": "tcp"
|
||||
}
|
||||
],
|
||||
"sslcerts": [
|
||||
{
|
||||
"name": "demo域名",
|
||||
"domain": "https://demo.example.com",
|
||||
"port": 443,
|
||||
"interval": 600,
|
||||
"callback": "https://yourSMSurl"
|
||||
}
|
||||
],
|
||||
"watchdog":
|
||||
[
|
||||
{
|
||||
"name": "服务器负载高监控",
|
||||
"rule": "cpu>90&load_5>3",
|
||||
"name": "服务器负载高监控,排除内存大于32G物理机,同时排除node1机器",
|
||||
"rule": "cpu>90&load_1>4&memory_total<33554432&name!='node1'",
|
||||
"interval": 600,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "服务器内存使用率过高监控,排除小于1G的机器",
|
||||
"rule": "(memory_used/memory_total)*100>90&memory_total>1048576",
|
||||
"interval": 600,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "服务器宕机告警",
|
||||
"rule": "online4=0&online6=0",
|
||||
"interval": 600,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "DDOS和CC攻击监控,限制甲骨文机器",
|
||||
"rule": "tcp_count>600&type='Oracle'",
|
||||
"interval": 300,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "服务器月出口流量999GB告警",
|
||||
"rule": "(network_out-last_network_out)/1024/1024/1024>999",
|
||||
"interval": 3600,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "阿里云服务器流量18GB告警,限制username为乌兰察布",
|
||||
"rule": "(network_out-last_network_out)/1024/1024/1024>18&(username='wlcb1'|username='wlcb2'|username='wlcb3'|username='wlcb4')",
|
||||
"interval": 3600,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "重要线路丢包率过高检查",
|
||||
"rule": "(ping_10010>10|ping_189>10|ping_10086>10)&(host='sgp'|host='qqhk'|host='hk-21-x'|host='hk-31-x')",
|
||||
"interval": 600,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
@ -132,7 +200,7 @@ web-dir参数为上一步设置的网站根目录,务必修改成自己网站
|
||||
服务器和客户端自行加入开机启动,或进程守护,或后台方式运行。 例如: nohup python3 client-linux.py &
|
||||
|
||||
`extra scene (run web/ssview.py)`
|
||||

|
||||

|
||||
|
||||
|
||||
# Make Better
|
||||
@ -141,7 +209,3 @@ web-dir参数为上一步设置的网站根目录,务必修改成自己网站
|
||||
* mojeda: https://github.com/mojeda
|
||||
* mojeda's ServerStatus: https://github.com/mojeda/ServerStatus
|
||||
* BlueVM's project: http://www.lowendtalk.com/discussion/comment/169690#Comment_169690
|
||||
|
||||
# Jetbrains
|
||||
|
||||
<a href="https://www.jetbrains.com/?from=ServerStatus"><img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_square.png" width="100px"></a>
|
||||
|
@ -3,6 +3,7 @@
|
||||
# Update by : https://github.com/cppla/ServerStatus, Update date: 20220530
|
||||
# 版本:1.0.3, 支持Python版本:2.7 to 3.10
|
||||
# 支持操作系统: 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"
|
||||
@ -17,9 +18,11 @@ 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
|
||||
@ -29,10 +32,10 @@ import json
|
||||
import errno
|
||||
import subprocess
|
||||
import threading
|
||||
try:
|
||||
from queue import Queue # python3
|
||||
except ImportError:
|
||||
from Queue import Queue # python2
|
||||
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:
|
||||
@ -150,11 +153,14 @@ diskIO = {
|
||||
'read': 0,
|
||||
'write': 0
|
||||
}
|
||||
monitorServer = {}
|
||||
|
||||
def _ping_thread(host, mark, port):
|
||||
lostPacket = 0
|
||||
packet_queue = Queue(maxsize=PING_PACKET_HISTORY_LEN)
|
||||
|
||||
while True:
|
||||
# flush dns , every time.
|
||||
IP = host
|
||||
if host.count(':') < 1: # if not plain ipv6 address, means ipv4 address or hostname
|
||||
try:
|
||||
@ -165,7 +171,6 @@ def _ping_thread(host, mark, port):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
while True:
|
||||
if packet_queue.full():
|
||||
if packet_queue.get() == 0:
|
||||
lostPacket -= 1
|
||||
@ -313,6 +318,91 @@ def get_realtime_data():
|
||||
ti.daemon = True
|
||||
ti.start()
|
||||
|
||||
|
||||
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]
|
||||
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]
|
||||
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()
|
||||
time.sleep(interval)
|
||||
|
||||
def byte_str(object):
|
||||
'''
|
||||
bytes to str, str to bytes
|
||||
@ -359,6 +449,28 @@ if __name__ == '__main__':
|
||||
if data.find("You are connecting via") < 0:
|
||||
data = byte_str(s.recv(1024))
|
||||
print(data)
|
||||
monitorServer.clear()
|
||||
for i in data.split('\n'):
|
||||
if "monitor" in i and "type" in i and "{" in i and "}" in i:
|
||||
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
|
||||
}
|
||||
t = threading.Thread(
|
||||
target=_monitor_thread,
|
||||
kwargs={
|
||||
'name': jdata.get("name"),
|
||||
'host': jdata.get("host"),
|
||||
'interval': jdata.get("interval"),
|
||||
'type': jdata.get("type")
|
||||
}
|
||||
)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
timer = 0
|
||||
check_ip = 0
|
||||
@ -377,7 +489,6 @@ if __name__ == '__main__':
|
||||
Load_1, Load_5, Load_15 = os.getloadavg()
|
||||
MemoryTotal, MemoryUsed, SwapTotal, SwapFree = get_memory()
|
||||
HDDTotal, HDDUsed = get_hdd()
|
||||
|
||||
array = {}
|
||||
if not timer:
|
||||
array['online' + str(check_ip)] = get_network(check_ip)
|
||||
@ -400,8 +511,6 @@ if __name__ == '__main__':
|
||||
array['network_tx'] = netSpeed.get("nettx")
|
||||
array['network_in'] = NET_IN
|
||||
array['network_out'] = NET_OUT
|
||||
# todo:兼容旧版本,下个版本删除ip_status
|
||||
array['ip_status'] = True
|
||||
array['ping_10010'] = lostRate.get('10010') * 100
|
||||
array['ping_189'] = lostRate.get('189') * 100
|
||||
array['ping_10086'] = lostRate.get('10086') * 100
|
||||
@ -411,16 +520,18 @@ 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())
|
||||
s.send(byte_str("update " + json.dumps(array) + "\n"))
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except socket.error:
|
||||
monitorServer.clear()
|
||||
print("Disconnected...")
|
||||
if 's' in locals().keys():
|
||||
del s
|
||||
time.sleep(3)
|
||||
except Exception as e:
|
||||
monitorServer.clear()
|
||||
print("Caught Exception:", e)
|
||||
if 's' in locals().keys():
|
||||
del s
|
||||
|
@ -4,6 +4,7 @@
|
||||
# 依赖于psutil跨平台库
|
||||
# 版本:1.0.3, 支持Python版本:2.7 to 3.10
|
||||
# 支持操作系统: 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"
|
||||
@ -18,9 +19,11 @@ 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
|
||||
@ -29,10 +32,10 @@ import json
|
||||
import errno
|
||||
import psutil
|
||||
import threading
|
||||
try:
|
||||
from queue import Queue # python3
|
||||
except ImportError:
|
||||
from Queue import Queue # python2
|
||||
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())
|
||||
@ -46,7 +49,11 @@ def get_swap():
|
||||
return int(Mem.total/1024.0), int(Mem.used/1024.0)
|
||||
|
||||
def get_hdd():
|
||||
valid_fs = [ "ext4", "ext3", "ext2", "reiserfs", "jfs", "btrfs", "fuseblk", "zfs", "simfs", "ntfs", "fat32", "exfat", "xfs" ]
|
||||
if "darwin" in sys.platform:
|
||||
return int(psutil.disk_usage("/").total/1024.0/1024.0), int((psutil.disk_usage("/").total-psutil.disk_usage("/").free)/1024.0/1024.0)
|
||||
else:
|
||||
valid_fs = ["ext4", "ext3", "ext2", "reiserfs", "jfs", "btrfs", "fuseblk", "zfs", "simfs", "ntfs", "fat32",
|
||||
"exfat", "xfs"]
|
||||
disks = dict()
|
||||
size = 0
|
||||
used = 0
|
||||
@ -88,13 +95,23 @@ def tupd():
|
||||
u = int(os.popen('ss -u|wc -l').read()[:-1])-1
|
||||
p = int(os.popen('ps -ef|wc -l').read()[:-1])-2
|
||||
d = int(os.popen('ps -eLf|wc -l').read()[:-1])-2
|
||||
elif sys.platform.startswith("darwin") is True:
|
||||
t = int(os.popen('lsof -nP -iTCP | wc -l').read()[:-1]) - 1
|
||||
u = int(os.popen('lsof -nP -iUDP | wc -l').read()[:-1]) - 1
|
||||
p = len(psutil.pids())
|
||||
d = 0
|
||||
for k in psutil.pids():
|
||||
try:
|
||||
d += psutil.Process(k).num_threads()
|
||||
except:
|
||||
pass
|
||||
|
||||
elif sys.platform.startswith("win") is True:
|
||||
t = int(os.popen('netstat -an|find "TCP" /c').read()[:-1])-1
|
||||
u = int(os.popen('netstat -an|find "UDP" /c').read()[:-1])-1
|
||||
p = len(psutil.pids())
|
||||
d = 0
|
||||
# cpu is high, default: 0
|
||||
# d = sum([psutil.Process(k).num_threads() for k in [x for x in psutil.pids()]])
|
||||
# if you find cpu is high, please set d=0
|
||||
d = sum([psutil.Process(k).num_threads() for k in psutil.pids()])
|
||||
else:
|
||||
t,u,p,d = 0,0,0,0
|
||||
return t,u,p,d
|
||||
@ -134,11 +151,14 @@ diskIO = {
|
||||
'read': 0,
|
||||
'write': 0
|
||||
}
|
||||
monitorServer = {}
|
||||
|
||||
def _ping_thread(host, mark, port):
|
||||
lostPacket = 0
|
||||
packet_queue = Queue(maxsize=PING_PACKET_HISTORY_LEN)
|
||||
|
||||
while True:
|
||||
# flush dns, every time.
|
||||
IP = host
|
||||
if host.count(':') < 1: # if not plain ipv6 address, means ipv4 address or hostname
|
||||
try:
|
||||
@ -149,7 +169,6 @@ def _ping_thread(host, mark, port):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
while True:
|
||||
if packet_queue.full():
|
||||
if packet_queue.get() == 0:
|
||||
lostPacket -= 1
|
||||
@ -205,7 +224,12 @@ def _disk_io():
|
||||
比如我这里是机械硬盘,大量做随机小文件读写,那么很低的读写也就能造成硬盘长时间的等待。
|
||||
如果这里做连续性IO,那么普通机械硬盘写入到100Mb/s,那么也能造成硬盘长时间的等待。
|
||||
磁盘读写有误差:4k,8k ,https://stackoverflow.com/questions/34413926/psutil-vs-dd-monitoring-disk-i-o
|
||||
macos/win,暂不处理。
|
||||
"""
|
||||
if "darwin" in sys.platform or "win" in sys.platform:
|
||||
diskIO["read"] = 0
|
||||
diskIO["write"] = 0
|
||||
else:
|
||||
while True:
|
||||
# first get a list of all processes and disk io counters
|
||||
procs = [p for p in psutil.process_iter()]
|
||||
@ -283,6 +307,91 @@ def get_realtime_data():
|
||||
ti.daemon = True
|
||||
ti.start()
|
||||
|
||||
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]
|
||||
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]
|
||||
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()
|
||||
time.sleep(interval)
|
||||
|
||||
|
||||
def byte_str(object):
|
||||
'''
|
||||
bytes to str, str to bytes
|
||||
@ -329,6 +438,27 @@ if __name__ == '__main__':
|
||||
if data.find("You are connecting via") < 0:
|
||||
data = byte_str(s.recv(1024))
|
||||
print(data)
|
||||
for i in data.split('\n'):
|
||||
if "monitor" in i and "type" in i and "{" in i and "}" in i:
|
||||
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
|
||||
}
|
||||
t = threading.Thread(
|
||||
target=_monitor_thread,
|
||||
kwargs={
|
||||
'name': jdata.get("name"),
|
||||
'host': jdata.get("host"),
|
||||
'interval': jdata.get("interval"),
|
||||
'type': jdata.get("type")
|
||||
}
|
||||
)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
timer = 0
|
||||
check_ip = 0
|
||||
@ -344,11 +474,10 @@ if __name__ == '__main__':
|
||||
CPU = get_cpu()
|
||||
NET_IN, NET_OUT = liuliang()
|
||||
Uptime = get_uptime()
|
||||
Load_1, Load_5, Load_15 = os.getloadavg() if 'linux' in sys.platform else (0.0, 0.0, 0.0)
|
||||
Load_1, Load_5, Load_15 = os.getloadavg() if 'linux' in sys.platform or 'darwin' in sys.platform else (0.0, 0.0, 0.0)
|
||||
MemoryTotal, MemoryUsed = get_memory()
|
||||
SwapTotal, SwapUsed = get_swap()
|
||||
HDDTotal, HDDUsed = get_hdd()
|
||||
|
||||
array = {}
|
||||
if not timer:
|
||||
array['online' + str(check_ip)] = get_network(check_ip)
|
||||
@ -371,8 +500,6 @@ if __name__ == '__main__':
|
||||
array['network_tx'] = netSpeed.get("nettx")
|
||||
array['network_in'] = NET_IN
|
||||
array['network_out'] = NET_OUT
|
||||
# todo:兼容旧版本,下个版本删除ip_status
|
||||
array['ip_status'] = True
|
||||
array['ping_10010'] = lostRate.get('10010') * 100
|
||||
array['ping_189'] = lostRate.get('189') * 100
|
||||
array['ping_10086'] = lostRate.get('10086') * 100
|
||||
@ -382,16 +509,18 @@ 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())
|
||||
s.send(byte_str("update " + json.dumps(array) + "\n"))
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except socket.error:
|
||||
monitorServer.clear()
|
||||
print("Disconnected...")
|
||||
if 's' in locals().keys():
|
||||
del s
|
||||
time.sleep(3)
|
||||
except Exception as e:
|
||||
monitorServer.clear()
|
||||
print("Caught Exception:", e)
|
||||
if 's' in locals().keys():
|
||||
del s
|
||||
|
@ -4,7 +4,12 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: serverstatus_server
|
||||
image: cppla/serverstatus:latest
|
||||
healthcheck:
|
||||
test: curl --fail http://localhost:80 || bash -c 'kill -s 15 -1 && (sleep 10; kill -s 9 -1)'
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
container_name: serverstatus
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
@ -19,7 +24,6 @@ services:
|
||||
|
||||
networks:
|
||||
serverstatus-network:
|
||||
name: serverstatus-network
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.23.0.0/24
|
||||
|
@ -2,7 +2,7 @@ version: "3"
|
||||
services:
|
||||
serverstatus:
|
||||
build:
|
||||
context: .
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
image: serverstatus_server
|
||||
container_name: serverstatus
|
||||
@ -11,14 +11,14 @@ services:
|
||||
serverstatus-network:
|
||||
ipv4_address: 172.23.0.2
|
||||
volumes:
|
||||
- ./server/config.json:/ServerStatus/server/config.json
|
||||
- ./web/json:/usr/share/nginx/html/json
|
||||
- ../server/config.json:/ServerStatus/server/config.json
|
||||
- ../web/json:/usr/share/nginx/html/json
|
||||
ports:
|
||||
- 35601:35601
|
||||
- 8080:80
|
||||
bot:
|
||||
build:
|
||||
context: ./plugin
|
||||
context: .
|
||||
dockerfile: Dockerfile-telegram
|
||||
image: serverstatus_bot
|
||||
container_name: bot4sss
|
@ -22,7 +22,7 @@
|
||||
"disabled": true,
|
||||
"username": "s03",
|
||||
"name": "node3",
|
||||
"type": "Nothing",
|
||||
"type": "hyper",
|
||||
"host": "host3",
|
||||
"location": "🇫🇷",
|
||||
"password": "USER_DEFAULT_PASSWORD",
|
||||
@ -38,29 +38,90 @@
|
||||
"monthstart": 1
|
||||
}
|
||||
],
|
||||
"monitors": [
|
||||
{
|
||||
"name": "baidu",
|
||||
"host": "https://www.baidu.com",
|
||||
"interval": 1200,
|
||||
"type": "https"
|
||||
},
|
||||
{
|
||||
"name": "1111",
|
||||
"host": "1.1.1.1:80",
|
||||
"interval": 1200,
|
||||
"type": "tcp"
|
||||
}
|
||||
],
|
||||
"sslcerts": [
|
||||
{
|
||||
"name": "my.cloudcpp.com",
|
||||
"domain": "https://my.cloudcpp.com",
|
||||
"port": 443,
|
||||
"interval": 3600,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "tz.cloudcpp.com",
|
||||
"domain": "https://tz.cloudcpp.com",
|
||||
"port": 443,
|
||||
"interval": 3600,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "3.0.2.1",
|
||||
"domain": "https://3.0.2.1",
|
||||
"port": 443,
|
||||
"interval": 3600,
|
||||
"callback": "https://yourSMSurl"
|
||||
}
|
||||
],
|
||||
"watchdog": [
|
||||
{
|
||||
"name": "cpu high warning",
|
||||
"rule": "cpu>98",
|
||||
"name": "cpu high warning,exclude username s01",
|
||||
"rule": "cpu>90&load_1>5&username!='s01'",
|
||||
"interval": 600,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "memory high warning",
|
||||
"rule": "(memory_used/memory_total)*100>90",
|
||||
"interval": 600,
|
||||
"name": "memory high warning, exclude less than 1GB vps",
|
||||
"rule": "(memory_used/memory_total)*100>90&memory_total>1048576",
|
||||
"interval": 300,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "offline warning",
|
||||
"rule": "online4=0&online6=0",
|
||||
"interval": 600,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "ddcc attack,limit type Oracle",
|
||||
"rule": "tcp_count>600&type='Oracle'",
|
||||
"interval": 300,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "month 999GB traffic warning",
|
||||
"rule": "(network_out-last_network_out)/1024/1024/1024>999",
|
||||
"interval": 3600,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "aliyun china free 18GB traffic warning",
|
||||
"rule": "(network_out-last_network_out)/1024/1024/1024>18&(username='aliyun1'|username='aliyun2')",
|
||||
"interval": 3600,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "packet loss rate warning",
|
||||
"rule": "(ping_10010>10|ping_189>10|ping_10086>10)&(host='sgp'|host='qqhk'|host='hk-21-x'|host='hk-31-x')",
|
||||
"interval": 3600,
|
||||
"callback": "https://yourSMSurl"
|
||||
},
|
||||
{
|
||||
"name": "you can parse an expression combining any known field",
|
||||
"rule": "load_5>10",
|
||||
"interval": 1800,
|
||||
"rule": "load_5>3",
|
||||
"interval": 900,
|
||||
"callback": "https://yourSMSurl"
|
||||
}
|
||||
]
|
||||
|
10956
server/src/exprtk.hpp
10956
server/src/exprtk.hpp
File diff suppressed because it is too large
Load Diff
@ -131,13 +131,15 @@ static int new_value
|
||||
|
||||
values_size = sizeof (*value->u.object.values) * value->u.object.length;
|
||||
|
||||
if (! ((*(void **) &value->u.object.values) = json_alloc
|
||||
(state, values_size + ((unsigned long) value->u.object.values), 0)) )
|
||||
void *tmp_alloc = json_alloc(state, values_size + ((unsigned long) value->u.object.values), 0);
|
||||
if (!tmp_alloc)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
value->_reserved.object_mem = (*(char **) &value->u.object.values) + values_size;
|
||||
/* 避免违反严格别名:通过中间变量复制 */
|
||||
memcpy(&value->u.object.values, &tmp_alloc, sizeof(void*));
|
||||
char *obj_mem = (char*)value->u.object.values + values_size;
|
||||
memcpy(&value->_reserved.object_mem, &obj_mem, sizeof(char*));
|
||||
|
||||
value->u.object.length = 0;
|
||||
break;
|
||||
@ -361,16 +363,18 @@ json_value * json_parse_ex (json_settings * settings,
|
||||
case json_object:
|
||||
|
||||
if (state.first_pass)
|
||||
(*(json_char **) &top->u.object.values) += string_length + 1;
|
||||
{
|
||||
json_char *adv = (json_char*)top->u.object.values;
|
||||
adv += string_length + 1;
|
||||
memcpy(&top->u.object.values, &adv, sizeof(json_char*));
|
||||
}
|
||||
else
|
||||
{
|
||||
top->u.object.values [top->u.object.length].name
|
||||
= (json_char *) top->_reserved.object_mem;
|
||||
|
||||
top->u.object.values [top->u.object.length].name_length
|
||||
= string_length;
|
||||
|
||||
(*(json_char **) &top->_reserved.object_mem) += string_length + 1;
|
||||
top->u.object.values[top->u.object.length].name = (json_char *)top->_reserved.object_mem;
|
||||
top->u.object.values[top->u.object.length].name_length = string_length;
|
||||
json_char *adv2 = (json_char*)top->_reserved.object_mem;
|
||||
adv2 += string_length + 1;
|
||||
memcpy(&top->_reserved.object_mem, &adv2, sizeof(json_char*));
|
||||
}
|
||||
|
||||
flags |= flag_seek_value | flag_need_colon;
|
||||
|
@ -9,6 +9,184 @@
|
||||
#include "main.h"
|
||||
#include "exprtk.hpp"
|
||||
#include "curl/curl.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string>
|
||||
|
||||
// 全局运行标志(需在 SSLCheckThread 定义前初始化)
|
||||
static volatile int gs_Running = 1;
|
||||
static volatile int gs_ReloadConfig = 0;
|
||||
|
||||
static int64_t ParseOpenSSLEnddate(const char *line)
|
||||
{
|
||||
// line format: notAfter=Aug 12 23:59:59 2025 GMT
|
||||
const char *p = strstr(line, "notAfter=");
|
||||
if(!p) return 0;
|
||||
p += 9;
|
||||
struct tm tmv; memset(&tmv,0,sizeof(tmv));
|
||||
char month[4]={0};
|
||||
int day, hour, min, sec, year;
|
||||
if(sscanf(p, "%3s %d %d:%d:%d %d GMT", month, &day, &hour, &min, &sec, &year)!=6) return 0;
|
||||
const char *months="JanFebMarAprMayJunJulAugSepOctNovDec";
|
||||
const char *mpos = strstr(months, month);
|
||||
if(!mpos) return 0;
|
||||
int mon = (int)((mpos - months)/3);
|
||||
tmv.tm_year = year - 1900;
|
||||
tmv.tm_mon = mon;
|
||||
tmv.tm_mday = day;
|
||||
tmv.tm_hour = hour; tmv.tm_min = min; tmv.tm_sec = sec;
|
||||
time_t t = timegm(&tmv);
|
||||
return (int64_t)t;
|
||||
}
|
||||
|
||||
struct SSLCheckThreadData { CMain *pMain; };
|
||||
static void SSLCheckThread(void *pUser)
|
||||
{
|
||||
SSLCheckThreadData *pData = (SSLCheckThreadData*)pUser;
|
||||
while(gs_Running){
|
||||
for(int i=0;i<NET_MAX_CLIENTS;i++){
|
||||
if(!pData->pMain->SSLCert(i) || !strcmp(pData->pMain->SSLCert(i)->m_aName, "NULL")) break;
|
||||
CMain::CSSLCerts *cert = pData->pMain->SSLCert(i);
|
||||
time_t nowt = time(0);
|
||||
if(cert->m_aLastCheck !=0 && (nowt - cert->m_aLastCheck) < cert->m_aInterval) continue;
|
||||
cert->m_aLastCheck = nowt;
|
||||
char cmd[1024];
|
||||
// 说明: 通过 s_client 获取证书,再用 x509 解析到期时间;统一屏蔽 stderr 以防握手失败/非 TLS 端口时刷屏。
|
||||
// 若配置中写成 https://domain/path 则需要清洗。
|
||||
char cleanHost[256];
|
||||
str_copy(cleanHost, cert->m_aDomain, sizeof(cleanHost));
|
||||
// 去协议
|
||||
if(!strncasecmp(cleanHost, "https://", 8)) memmove(cleanHost, cleanHost+8, strlen(cleanHost+8)+1);
|
||||
else if(!strncasecmp(cleanHost, "http://", 7)) memmove(cleanHost, cleanHost+7, strlen(cleanHost+7)+1);
|
||||
// 去路径
|
||||
char *slash = strchr(cleanHost, '/'); if(slash) *slash='\0';
|
||||
// 若含 :port 再截取主机部分(端口由配置提供)
|
||||
char *colon = strchr(cleanHost, ':'); if(colon) *colon='\0';
|
||||
int n = snprintf(cmd,sizeof(cmd),"echo | openssl s_client -servername %s -connect %s:%d </dev/null 2>/dev/null | openssl x509 -noout -enddate -text 2>/dev/null", cleanHost, cleanHost, cert->m_aPort);
|
||||
if(n <= 0 || n >= (int)sizeof(cmd)) continue; // 避免截断执行
|
||||
FILE *fp = popen(cmd, "r");
|
||||
if(!fp) continue;
|
||||
char line[1024]={0};
|
||||
int foundEnddate=0;
|
||||
int mismatch = 1; // 默认视为不匹配,发现任一匹配域名再置0
|
||||
int haveNames = 0;
|
||||
// 将目标域名转为小写
|
||||
char target[256]; str_copy(target, cleanHost, sizeof(target));
|
||||
for(char *p=target; *p; ++p) *p=tolower(*p);
|
||||
while(fgets(line,sizeof(line),fp)){
|
||||
if(!foundEnddate){
|
||||
int64_t expire = ParseOpenSSLEnddate(line);
|
||||
if(expire>0){ cert->m_aExpireTS = expire; foundEnddate=1; }
|
||||
}
|
||||
// 解析 subjectAltName
|
||||
// 解析 Subject 中的 CN(备用)
|
||||
char *subj = strstr(line, "Subject:");
|
||||
if(subj){
|
||||
char *cn = strstr(subj, " CN=");
|
||||
if(cn){
|
||||
cn += 4; // 跳过 ' CN='
|
||||
char name[256]={0}; int ni=0;
|
||||
while(*cn && *cn!='/' && *cn!=',' && *cn!='\n' && ni<(int)sizeof(name)-1){ name[ni++]=*cn++; }
|
||||
name[ni]='\0';
|
||||
while(ni>0 && (name[ni-1]==' '||name[ni-1]=='\r'||name[ni-1]=='\t')){ name[--ni]='\0'; }
|
||||
for(char *q=name; *q; ++q) *q=tolower(*q);
|
||||
if(ni>0){
|
||||
haveNames=1;
|
||||
int match=0;
|
||||
if(name[0]=='*' && name[1]=='.'){
|
||||
const char *sub = strchr(target,'.');
|
||||
if(sub && !strcmp(sub+1, name+2)) match=1;
|
||||
}else if(!strcmp(name,target)) match=1;
|
||||
if(match){ mismatch=0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
if(strstr(line, "DNS:")){
|
||||
char *p = line;
|
||||
while((p = strstr(p, "DNS:"))){
|
||||
p += 4; while(*p==' '){p++;}
|
||||
char name[256]={0}; int ni=0;
|
||||
while(*p && *p!=',' && *p!='\n' && ni<(int)sizeof(name)-1){ name[ni++]=*p++; }
|
||||
name[ni]='\0';
|
||||
// 去空白
|
||||
while(ni>0 && (name[ni-1]==' '||name[ni-1]=='\r'||name[ni-1]=='\t')){ name[--ni]='\0'; }
|
||||
for(char *q=name; *q; ++q) *q=tolower(*q);
|
||||
haveNames=1;
|
||||
// 通配符匹配 *.example.com
|
||||
int match=0;
|
||||
if(name[0]=='*' && name[1]=='.'){
|
||||
const char *sub = strchr(target,'.');
|
||||
if(sub && !strcmp(sub+1, name+2)) match=1;
|
||||
}else if(!strcmp(name,target)) match=1;
|
||||
if(match){ mismatch=0; goto names_done; }
|
||||
}
|
||||
}
|
||||
}
|
||||
names_done:
|
||||
pclose(fp);
|
||||
if(haveNames){ cert->m_aHostnameMismatch = mismatch ? 1 : 0; }
|
||||
else { /* 未能提取任何域名,保留原状态,不触发误报 */ }
|
||||
// 告警: 仅在不匹配且 24h 冷却
|
||||
if(cert->m_aHostnameMismatch==1){
|
||||
if(cert->m_aLastAlarmMismatch==0 || nowt - cert->m_aLastAlarmMismatch > 24*3600){
|
||||
if(strlen(cert->m_aCallback)>0){
|
||||
CURL *curl = curl_easy_init();
|
||||
if(curl){
|
||||
char msg[1024];
|
||||
snprintf(msg,sizeof(msg),"【SSL证书域名不匹配】%s(%s) 证书域名与配置不一致", cert->m_aName, cert->m_aDomain);
|
||||
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);
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "signature=ServerStatusSSL");
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 6L);
|
||||
curl_easy_perform(curl);
|
||||
if(enc) curl_free(enc);
|
||||
curl_easy_cleanup(curl);
|
||||
}
|
||||
}
|
||||
cert->m_aLastAlarmMismatch = nowt;
|
||||
}
|
||||
}
|
||||
// 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; }
|
||||
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();
|
||||
if(curl){
|
||||
char msg[1024];
|
||||
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);
|
||||
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);
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "signature=ServerStatusSSL");
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 6L);
|
||||
curl_easy_perform(curl);
|
||||
if(enc) curl_free(enc);
|
||||
curl_easy_cleanup(curl);
|
||||
}
|
||||
*lastAlarm = nowt;
|
||||
}
|
||||
}
|
||||
}
|
||||
thread_sleep(5000);
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(CONF_FAMILY_UNIX)
|
||||
#include <signal.h>
|
||||
@ -18,9 +196,6 @@
|
||||
#define PRId64 "I64d"
|
||||
#endif
|
||||
|
||||
static volatile int gs_Running = 1;
|
||||
static volatile int gs_ReloadConfig = 0;
|
||||
|
||||
static void ExitFunc(int Signal)
|
||||
{
|
||||
printf("[EXIT] Caught signal %d\n", Signal);
|
||||
@ -92,19 +267,24 @@ void CMain::OnNewClient(int ClientNetID, int ClientID)
|
||||
Client(ClientID)->m_Stats.m_Online4 = true;
|
||||
else if(Client(ClientID)->m_ClientNetType == NETTYPE_IPV6)
|
||||
Client(ClientID)->m_Stats.m_Online6 = true;
|
||||
|
||||
// Send monitor to client
|
||||
// support by cpp.la
|
||||
int ID = 0;
|
||||
char monitorBuffer[2048];
|
||||
while (strcmp(Monitors(ID)->m_aName, "NULL"))
|
||||
{
|
||||
memset(monitorBuffer, 0, sizeof(monitorBuffer));
|
||||
sprintf(monitorBuffer, "{\"name\":\"%s\",\"host\":\"%s\",\"interval\":%d,\"type\":\"%s\",\"monitor\":%d}", Monitors(ID)->m_aName, Monitors(ID)->m_aHost, Monitors(ID)->m_aInterval, Monitors(ID)->m_aType, ID);
|
||||
m_Server.Network()->Send(ClientNetID, monitorBuffer);
|
||||
ID++;
|
||||
}
|
||||
}
|
||||
|
||||
void CMain::OnDelClient(int ClientNetID)
|
||||
{
|
||||
int ClientID = ClientNetToClient(ClientNetID);
|
||||
dbg_msg("main", "OnDelClient(ncid=%d, cid=%d)", ClientNetID, ClientID);
|
||||
//copy offline message for watchdog
|
||||
WatchdogMessage(ClientNetID,
|
||||
0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0,0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0,0, 0, 0,
|
||||
0, 0, 0, 0);
|
||||
if(ClientID >= 0 && ClientID < NET_MAX_CLIENTS)
|
||||
{
|
||||
Client(ClientID)->m_Connected = false;
|
||||
@ -112,6 +292,10 @@ void CMain::OnDelClient(int ClientNetID)
|
||||
Client(ClientID)->m_ClientNetType = NETTYPE_INVALID;
|
||||
mem_zero(&Client(ClientID)->m_Stats, sizeof(CClient::CStats));
|
||||
}
|
||||
m_OfflineAlarmThreadData.pClients = m_aClients;
|
||||
m_OfflineAlarmThreadData.pWatchDogs = m_aCWatchDogs;
|
||||
m_OfflineAlarmThreadData.m_ReloadRequired = ClientID;
|
||||
thread_create(offlineAlarmThread, &m_OfflineAlarmThreadData);
|
||||
}
|
||||
|
||||
int CMain::HandleMessage(int ClientNetID, char *pMessage)
|
||||
@ -207,7 +391,8 @@ int CMain::HandleMessage(int ClientNetID, char *pMessage)
|
||||
pClient->m_Stats.m_time_10010, pClient->m_Stats.m_time_189, pClient->m_Stats.m_time_10086,
|
||||
pClient->m_Stats.m_tcpCount, pClient->m_Stats.m_udpCount, pClient->m_Stats.m_processCount,
|
||||
pClient->m_Stats.m_threadCount, pClient->m_Stats.m_NetworkRx, pClient->m_Stats.m_NetworkTx,
|
||||
pClient->m_Stats.m_NetworkIN, pClient->m_Stats.m_NetworkOUT,pClient->m_Stats.m_MemTotal,
|
||||
pClient->m_Stats.m_NetworkIN, pClient->m_Stats.m_NetworkOUT,
|
||||
pClient->m_LastNetworkIN, pClient->m_LastNetworkOUT, pClient->m_Stats.m_MemTotal,
|
||||
pClient->m_Stats.m_MemUsed, pClient->m_Stats.m_SwapTotal, pClient->m_Stats.m_SwapUsed,
|
||||
pClient->m_Stats.m_HDDTotal, pClient->m_Stats.m_HDDUsed, pClient->m_Stats.m_IORead,
|
||||
pClient->m_Stats.m_IOWrite, pClient->m_Stats.m_CPU, pClient->m_Stats.m_Online4,
|
||||
@ -258,20 +443,52 @@ int CMain::HandleMessage(int ClientNetID, char *pMessage)
|
||||
|
||||
|
||||
void CMain::WatchdogMessage(int ClientNetID, double load_1, double load_5, double load_15, double ping_10010, double ping_189, double ping_10086,
|
||||
double time_10010, double time_189, double time_10086, double tcp, double udp, double process, double thread,
|
||||
double network_rx, double network_tx, double network_in, double network_out, double memory_total, double memory_used,
|
||||
double time_10010, double time_189, double time_10086, double tcp_count, double udp_count, double process_count, double thread_count,
|
||||
double network_rx, double network_tx, double network_in, double network_out, double last_network_in, double last_network_out, double memory_total, double memory_used,
|
||||
double swap_total, double swap_used, double hdd_total, double hdd_used, double io_read, double io_write, double cpu,
|
||||
double online4, double online6)
|
||||
{
|
||||
int ID = 0;
|
||||
while (strcmp(Watchdog(ID)->m_aName, "NULL"))
|
||||
{
|
||||
// Exprtk库默认使用窄字符类型,但可能会出现中文等Unicode字符无法正确解析的问题。
|
||||
// todo: 为解决此问题,可以使用宽字符类型替换Exprtk库中默认的窄字符类型。
|
||||
// #include <string>
|
||||
// #include <vector>
|
||||
// #include <exprtk.hpp>
|
||||
// typedef exprtk::expression<wchar_t> expression_type;
|
||||
// typedef exprtk::parser<wchar_t> parser_type;
|
||||
// int main()
|
||||
// {
|
||||
// std::wstring expression_string = L"sin(x)";
|
||||
// expression_type expression;
|
||||
// parser_type parser;
|
||||
// parser.compile(expression_string, expression);
|
||||
// double x = 3.14;
|
||||
// double result = expression.value();
|
||||
// return 0;
|
||||
// }
|
||||
typedef exprtk::symbol_table<double> symbol_table_t;
|
||||
typedef exprtk::expression<double> expression_t;
|
||||
typedef exprtk::parser<double> parser_t;
|
||||
const std::string expression_string = Watchdog(ID)->m_aRule;
|
||||
int ClientID = ClientNetToClient(ClientNetID);
|
||||
if(ClientID < 0 || ClientID >= NET_MAX_CLIENTS) {
|
||||
ID++;
|
||||
continue; // 无效客户端,跳过当前 watchdog 规则
|
||||
}
|
||||
std::string username = Client(ClientID)->m_aUsername;
|
||||
std::string name = Client(ClientID)->m_aName;
|
||||
std::string type = Client(ClientID)->m_aType;
|
||||
std::string host = Client(ClientID)->m_aHost;
|
||||
std::string location = Client(ClientID)->m_aLocation;
|
||||
|
||||
symbol_table_t symbol_table;
|
||||
symbol_table.add_stringvar("username", username);
|
||||
symbol_table.add_stringvar("name", name);
|
||||
symbol_table.add_stringvar("type", type);
|
||||
symbol_table.add_stringvar("host", host);
|
||||
symbol_table.add_stringvar("location", location);
|
||||
symbol_table.add_variable("load_1",load_1);
|
||||
symbol_table.add_variable("load_5",load_5);
|
||||
symbol_table.add_variable("load_15",load_15);
|
||||
@ -281,14 +498,16 @@ void CMain::WatchdogMessage(int ClientNetID, double load_1, double load_5, doubl
|
||||
symbol_table.add_variable("time_10010",time_10010);
|
||||
symbol_table.add_variable("time_189",time_189);
|
||||
symbol_table.add_variable("time_10086",time_10086);
|
||||
symbol_table.add_variable("tcp",tcp);
|
||||
symbol_table.add_variable("udp",udp);
|
||||
symbol_table.add_variable("process",process);
|
||||
symbol_table.add_variable("thread",thread);
|
||||
symbol_table.add_variable("tcp_count",tcp_count);
|
||||
symbol_table.add_variable("udp_count",udp_count);
|
||||
symbol_table.add_variable("process_count",process_count);
|
||||
symbol_table.add_variable("thread_count",thread_count);
|
||||
symbol_table.add_variable("network_rx",network_rx);
|
||||
symbol_table.add_variable("network_tx",network_tx);
|
||||
symbol_table.add_variable("network_in",network_in);
|
||||
symbol_table.add_variable("network_out",network_out);
|
||||
symbol_table.add_variable("last_network_in",last_network_in);
|
||||
symbol_table.add_variable("last_network_out",last_network_out);
|
||||
symbol_table.add_variable("memory_total",memory_total);
|
||||
symbol_table.add_variable("memory_used",memory_used);
|
||||
symbol_table.add_variable("swap_total",swap_total);
|
||||
@ -310,11 +529,14 @@ void CMain::WatchdogMessage(int ClientNetID, double load_1, double load_5, doubl
|
||||
|
||||
if (expression.value() > 0)
|
||||
{
|
||||
int ClientID = ClientNetToClient(ClientNetID);
|
||||
time_t currentStamp = (long long)time(/*ago*/0);
|
||||
if ((currentStamp-Client(ClientID)->m_AlarmLastTime) > Watchdog(ID)->m_aInterval)
|
||||
{
|
||||
//todo 这里需要换成线程
|
||||
if (!Client(ClientID)->m_Stats.m_Online4 && !Client(ClientID)->m_Stats.m_Online6)
|
||||
{
|
||||
//休眠5分钟如果5分钟后状态发生了变更,消息不发出。
|
||||
printf("download\n");
|
||||
}
|
||||
Client(ClientID)->m_AlarmLastTime = currentStamp;
|
||||
CURL *curl;
|
||||
CURLcode res;
|
||||
@ -326,17 +548,16 @@ void CMain::WatchdogMessage(int ClientNetID, double load_1, double load_5, doubl
|
||||
char standardTime[32]= { 0 };
|
||||
strftime(standardTime, sizeof(standardTime), "%Y-%m-%d %H:%M:%S",localtime(¤tStamp));
|
||||
|
||||
//url encode
|
||||
//url encode, Rules conflict with url special characters,eg:&, del rules, by https://cpp.la, 2023-10-09
|
||||
char encodeBuffer[2048] = { 0 };
|
||||
sprintf(encodeBuffer, " \n\n【告警名称】 %s \n\n【告警规则】 %s \n\n【告警时间】 %s \n\n ---------------- \n\n【用户名】 %s \n\n【节点名】 %s \n\n【虚拟化】 %s \n\n【主机名】 %s \n\n【位 置】 %s",
|
||||
sprintf(encodeBuffer, "【告警名称】 %s \n\n【告警时间】 %s \n\n【用户名】 %s \n\n【节点名】 %s \n\n【虚拟化】 %s \n\n【主机名】 %s \n\n【位 置】 %s",
|
||||
Watchdog(ID)->m_aName,
|
||||
Watchdog(ID)->m_aRule,
|
||||
standardTime,
|
||||
Client(ClientID)->m_aUsername,
|
||||
Client(ClientID)->m_aName,
|
||||
Client(ClientID)->m_aType,
|
||||
Client(ClientID)->m_aHost,
|
||||
Client(ClientID)->m_aLocation);
|
||||
username.c_str(),
|
||||
name.c_str(),
|
||||
type.c_str(),
|
||||
host.c_str(),
|
||||
location.c_str());
|
||||
char *encodeUrl = curl_easy_escape(curl, encodeBuffer, strlen(encodeBuffer));
|
||||
|
||||
//standard url
|
||||
@ -344,7 +565,11 @@ void CMain::WatchdogMessage(int ClientNetID, double load_1, double load_5, doubl
|
||||
sprintf(urlBuffer, "%s%s",Watchdog(ID)->m_aCallback, encodeUrl);
|
||||
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_URL, urlBuffer);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS,"signature=ServerStatus");
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 6L);
|
||||
res = curl_easy_perform(curl);
|
||||
@ -427,13 +652,23 @@ void CMain::JSONUpdateThread(void *pUser)
|
||||
pBuf += strlen(pBuf);
|
||||
}
|
||||
}
|
||||
if(!m_pJSONUpdateThreadData->m_ReloadRequired)
|
||||
str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"updated\": \"%lld\"\n}", (long long)time(/*ago*/0));
|
||||
else
|
||||
// append ssl certs data
|
||||
str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"sslcerts\": [\n");
|
||||
pBuf += strlen(pBuf);
|
||||
for(int si = 0; si < NET_MAX_CLIENTS; si++)
|
||||
{
|
||||
str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"updated\": \"%lld\",\n\"reload\": true\n}", (long long)time(/*ago*/0));
|
||||
m_pJSONUpdateThreadData->m_ReloadRequired--;
|
||||
if(!m_pJSONUpdateThreadData->pMain->SSLCert(si) || !strcmp(m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aName, "NULL")) break;
|
||||
int64_t expire_ts = m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aExpireTS;
|
||||
int expire_days = 0;
|
||||
if(expire_ts>0){
|
||||
int64_t nowts = (long long)time(/*ago*/0);
|
||||
expire_days = (int)((expire_ts - nowts)/86400);
|
||||
}
|
||||
str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf), "{ \"name\": \"%s\", \"domain\": \"%s\", \"port\": %d, \"expire_ts\": %lld, \"expire_days\": %d, \"mismatch\": %s },\n", m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aName, m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aDomain, m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aPort, (long long)expire_ts, expire_days, m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aHostnameMismatch?"true":"false");
|
||||
pBuf += strlen(pBuf);
|
||||
}
|
||||
if(pBuf - aFileBuf >= 2) str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"updated\": \"%lld\"%s\n}", (long long)time(/*ago*/0), m_pJSONUpdateThreadData->m_ReloadRequired?",\n\"reload\": true":"");
|
||||
if(m_pJSONUpdateThreadData->m_ReloadRequired) m_pJSONUpdateThreadData->m_ReloadRequired--;
|
||||
pBuf += strlen(pBuf);
|
||||
|
||||
char aJSONFileTmp[1024];
|
||||
@ -457,6 +692,109 @@ void CMain::JSONUpdateThread(void *pUser)
|
||||
fs_rename(pConfig->m_aJSONFile, aJSONFileTmp);
|
||||
}
|
||||
|
||||
void CMain::offlineAlarmThread(void *pUser)
|
||||
{
|
||||
CJSONUpdateThreadData *m_OfflineAlarmThreadData = (CJSONUpdateThreadData *)pUser;
|
||||
CClient *pClients = m_OfflineAlarmThreadData->pClients;
|
||||
CWatchDog *pWatchDogs = m_OfflineAlarmThreadData->pWatchDogs;
|
||||
volatile short ClientID = m_OfflineAlarmThreadData->m_ReloadRequired;
|
||||
thread_sleep(25000);
|
||||
if(!pClients[ClientID].m_Connected)
|
||||
{
|
||||
int ID = 0;
|
||||
while (strcmp(pWatchDogs[ID].m_aName, "NULL"))
|
||||
{
|
||||
typedef exprtk::symbol_table<double> symbol_table_t;
|
||||
typedef exprtk::expression<double> expression_t;
|
||||
typedef exprtk::parser<double> parser_t;
|
||||
const std::string expression_string = pWatchDogs[ID].m_aRule;
|
||||
std::string username = pClients[ClientID].m_aUsername;
|
||||
std::string name = pClients[ClientID].m_aName;
|
||||
std::string type = pClients[ClientID].m_aType;
|
||||
std::string host = pClients[ClientID].m_aHost;
|
||||
std::string location = pClients[ClientID].m_aLocation;
|
||||
std::double_t online4 = pClients[ClientID].m_Stats.m_Online4;
|
||||
std::double_t online6 = pClients[ClientID].m_Stats.m_Online6;
|
||||
|
||||
symbol_table_t symbol_table;
|
||||
symbol_table.add_stringvar("username", username);
|
||||
symbol_table.add_stringvar("name", name);
|
||||
symbol_table.add_stringvar("type", type);
|
||||
symbol_table.add_stringvar("host", host);
|
||||
symbol_table.add_stringvar("location", location);
|
||||
symbol_table.add_variable("online4",online4);
|
||||
symbol_table.add_variable("online6",online6);
|
||||
symbol_table.add_constants();
|
||||
|
||||
expression_t expression;
|
||||
expression.register_symbol_table(symbol_table);
|
||||
|
||||
parser_t parser;
|
||||
parser.compile(expression_string,expression);
|
||||
|
||||
if (expression.value() > 0)
|
||||
{
|
||||
time_t currentStamp = (long long)time(/*ago*/0);
|
||||
if ((currentStamp-pClients[ClientID].m_AlarmLastTime) > pWatchDogs[ID].m_aInterval)
|
||||
{
|
||||
printf("客户端下线且超过阈值, Client disconnects and sends alert information\n");
|
||||
pClients[ClientID].m_AlarmLastTime = currentStamp;
|
||||
CURL *curl;
|
||||
CURLcode res;
|
||||
curl_global_init(CURL_GLOBAL_ALL);
|
||||
|
||||
curl = curl_easy_init();
|
||||
if(curl) {
|
||||
//standard time
|
||||
char standardTime[32]= { 0 };
|
||||
strftime(standardTime, sizeof(standardTime), "%Y-%m-%d %H:%M:%S",localtime(¤tStamp));
|
||||
|
||||
//url encode, Rules conflict with url special characters,eg:&, del rules, by https://cpp.la, 2023-10-09
|
||||
char encodeBuffer[2048] = { 0 };
|
||||
sprintf(encodeBuffer, "【告警名称】 %s \n\n【告警时间】 %s \n\n【用户名】 %s \n\n【节点名】 %s \n\n【虚拟化】 %s \n\n【主机名】 %s \n\n【位 置】 %s",
|
||||
pWatchDogs[ID].m_aName,
|
||||
standardTime,
|
||||
pClients[ClientID].m_aUsername,
|
||||
pClients[ClientID].m_aName,
|
||||
pClients[ClientID].m_aType,
|
||||
pClients[ClientID].m_aHost,
|
||||
pClients[ClientID].m_aLocation);
|
||||
char *encodeUrl = curl_easy_escape(curl, encodeBuffer, strlen(encodeBuffer));
|
||||
|
||||
//standard url
|
||||
char urlBuffer[2048] = { 0 };
|
||||
sprintf(urlBuffer, "%s%s",pWatchDogs[ID].m_aCallback, encodeUrl);
|
||||
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_POST, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_URL, urlBuffer);
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS,"signature=ServerStatus");
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 6L);
|
||||
res = curl_easy_perform(curl);
|
||||
if(res != CURLE_OK)
|
||||
fprintf(stderr, "watchdog failed: %s\n", curl_easy_strerror(res));
|
||||
if(encodeUrl)
|
||||
curl_free(encodeUrl);
|
||||
curl_easy_cleanup(curl);
|
||||
}
|
||||
curl_global_cleanup();
|
||||
}
|
||||
else
|
||||
printf("客户端下线但未超过阈值,No alarm if the threshold is not exceeded\n");
|
||||
}
|
||||
ID++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("网络波动,No alarm information is sent due to network fluctuations\n");
|
||||
}
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
int CMain::ReadConfig()
|
||||
{
|
||||
// read and parse config
|
||||
@ -557,8 +895,61 @@ int CMain::ReadConfig()
|
||||
ID++;
|
||||
}
|
||||
str_copy(Watchdog(ID)->m_aName, "NULL", sizeof(Watchdog(ID)->m_aName));
|
||||
} else
|
||||
}
|
||||
else
|
||||
{
|
||||
str_copy(Watchdog(ID)->m_aName, "NULL", sizeof(Watchdog(ID)->m_aName));
|
||||
}
|
||||
|
||||
// monitor
|
||||
// support by: https://cpp.la
|
||||
ID = 0;
|
||||
const json_value &mStart = (*pJsonData)["monitors"];
|
||||
if(mStart.type == json_array)
|
||||
{
|
||||
for(unsigned i = 0; i < mStart.u.array.length; i++)
|
||||
{
|
||||
if(ID < 0 || ID >= NET_MAX_CLIENTS)
|
||||
continue;
|
||||
|
||||
str_copy(Monitors(ID)->m_aName, mStart[i]["name"].u.string.ptr, sizeof(Monitors(ID)->m_aName));
|
||||
str_copy(Monitors(ID)->m_aHost, mStart[i]["host"].u.string.ptr, sizeof(Monitors(ID)->m_aHost));
|
||||
Monitors(ID)->m_aInterval = mStart[i]["interval"].u.integer;
|
||||
str_copy(Monitors(ID)->m_aType, mStart[i]["type"].u.string.ptr, sizeof(Monitors(ID)->m_aType));
|
||||
|
||||
ID++;
|
||||
}
|
||||
str_copy(Monitors(ID)->m_aName, "NULL", sizeof(Monitors(ID)->m_aName));
|
||||
}
|
||||
else
|
||||
{
|
||||
str_copy(Monitors(ID)->m_aName, "NULL", sizeof(Monitors(ID)->m_aName));
|
||||
}
|
||||
|
||||
// sslcerts
|
||||
ID = 0;
|
||||
const json_value &sStart = (*pJsonData)["sslcerts"];
|
||||
if(sStart.type == json_array)
|
||||
{
|
||||
for(unsigned i = 0; i < sStart.u.array.length; i++)
|
||||
{
|
||||
if(ID < 0 || ID >= NET_MAX_CLIENTS)
|
||||
continue;
|
||||
str_copy(SSLCert(ID)->m_aName, sStart[i]["name"].u.string.ptr, sizeof(SSLCert(ID)->m_aName));
|
||||
str_copy(SSLCert(ID)->m_aDomain, sStart[i]["domain"].u.string.ptr, sizeof(SSLCert(ID)->m_aDomain));
|
||||
SSLCert(ID)->m_aPort = sStart[i]["port"].u.integer;
|
||||
SSLCert(ID)->m_aInterval = sStart[i]["interval"].u.integer;
|
||||
str_copy(SSLCert(ID)->m_aCallback, sStart[i]["callback"].u.string.ptr, sizeof(SSLCert(ID)->m_aCallback));
|
||||
SSLCert(ID)->m_aExpireTS = 0; // reset
|
||||
SSLCert(ID)->m_aLastCheck = 0;
|
||||
SSLCert(ID)->m_aLastAlarm7 = 0;
|
||||
SSLCert(ID)->m_aLastAlarm3 = 0;
|
||||
SSLCert(ID)->m_aLastAlarm1 = 0;
|
||||
ID++;
|
||||
}
|
||||
str_copy(SSLCert(ID)->m_aName, "NULL", sizeof(SSLCert(ID)->m_aName));
|
||||
}else
|
||||
str_copy(SSLCert(ID)->m_aName, "NULL", sizeof(SSLCert(ID)->m_aName));
|
||||
|
||||
// if file exists, read last network traffic record,reset m_LastNetworkIN and m_LastNetworkOUT
|
||||
// support by: https://cpp.la
|
||||
@ -633,7 +1024,11 @@ int CMain::Run()
|
||||
m_JSONUpdateThreadData.m_ReloadRequired = 2;
|
||||
m_JSONUpdateThreadData.pClients = m_aClients;
|
||||
m_JSONUpdateThreadData.pConfig = &m_Config;
|
||||
m_JSONUpdateThreadData.pWatchDogs = m_aCWatchDogs;
|
||||
m_JSONUpdateThreadData.pMain = this;
|
||||
void *LoadThread = thread_create(JSONUpdateThread, &m_JSONUpdateThreadData);
|
||||
// Start SSL check thread
|
||||
static SSLCheckThreadData sslData; sslData.pMain = this; thread_create(SSLCheckThread, &sslData);
|
||||
//thread_detach(LoadThread);
|
||||
|
||||
while(gs_Running)
|
||||
|
@ -77,7 +77,7 @@ class CMain
|
||||
int64_t m_IORead;
|
||||
int64_t m_IOWrite;
|
||||
double m_CPU;
|
||||
char m_aCustom[512];
|
||||
char m_aCustom[1024];
|
||||
// Options
|
||||
bool m_Pong;
|
||||
} m_Stats;
|
||||
@ -90,14 +90,39 @@ class CMain
|
||||
char m_aCallback[1024];
|
||||
} m_aCWatchDogs[NET_MAX_CLIENTS];
|
||||
|
||||
struct CMonitors{
|
||||
char m_aName[128];
|
||||
char m_aHost[128];
|
||||
int m_aInterval;
|
||||
char m_aType[128];
|
||||
} m_aCMonitors[NET_MAX_CLIENTS];
|
||||
public:
|
||||
struct CSSLCerts{
|
||||
char m_aName[128];
|
||||
char m_aDomain[256];
|
||||
int m_aPort;
|
||||
int m_aInterval; // seconds
|
||||
char m_aCallback[1024];
|
||||
int64_t m_aExpireTS; // epoch seconds cache
|
||||
int64_t m_aLastCheck; // last check time
|
||||
int64_t m_aLastAlarm7;
|
||||
int64_t m_aLastAlarm3;
|
||||
int64_t m_aLastAlarm1;
|
||||
int m_aHostnameMismatch; // 1: 域名与证书不匹配
|
||||
int64_t m_aLastAlarmMismatch; // 上次不匹配告警时间
|
||||
} m_aCSSLCerts[NET_MAX_CLIENTS];
|
||||
|
||||
struct CJSONUpdateThreadData
|
||||
{
|
||||
CClient *pClients;
|
||||
CConfig *pConfig;
|
||||
CWatchDog *pWatchDogs;
|
||||
CMain *pMain;
|
||||
volatile short m_ReloadRequired;
|
||||
} m_JSONUpdateThreadData;
|
||||
} m_JSONUpdateThreadData, m_OfflineAlarmThreadData;
|
||||
|
||||
static void JSONUpdateThread(void *pUser);
|
||||
static void offlineAlarmThread(void *pUser);
|
||||
public:
|
||||
CMain(CConfig Config);
|
||||
|
||||
@ -108,11 +133,14 @@ public:
|
||||
int Run();
|
||||
|
||||
CWatchDog *Watchdog(int ruleID) { return &m_aCWatchDogs[ruleID]; }
|
||||
CMonitors *Monitors(int ruleID) { return &m_aCMonitors[ruleID]; }
|
||||
CSSLCerts *SSLCert(int ruleID) { return &m_aCSSLCerts[ruleID]; }
|
||||
|
||||
void WatchdogMessage(int ClientNetID,
|
||||
double load_1, double load_5, double load_15, double ping_10010, double ping_189, double ping_10086,
|
||||
double time_10010, double time_189, double time_10086, double tcp, double udp, double process, double thread,
|
||||
double network_rx, double network_tx, double network_in, double network_out,double memory_total,
|
||||
double memory_used,double swap_total, double swap_used, double hdd_total,
|
||||
double time_10010, double time_189, double time_10086, double tcp_count, double udp_count, double process_count, double thread_count,
|
||||
double network_rx, double network_tx, double network_in, double network_out, double last_network_in, double last_network_out,
|
||||
double memory_total, double memory_used,double swap_total, double swap_used, double hdd_total,
|
||||
double hdd_used, double io_read, double io_write, double cpu,double online4, double online6);
|
||||
|
||||
CClient *Client(int ClientID) { return &m_aClients[ClientID]; }
|
||||
|
@ -10,7 +10,7 @@ enum
|
||||
NET_CONNSTATE_ERROR=4,
|
||||
|
||||
NET_MAX_PACKETSIZE = 1400,
|
||||
NET_MAX_CLIENTS = 256
|
||||
NET_MAX_CLIENTS = 512
|
||||
};
|
||||
|
||||
typedef int (*NETFUNC_DELCLIENT)(int ClientID, const char* pReason, void *pUser);
|
||||
|
262
web/css/app.css
Normal file
262
web/css/app.css
Normal file
@ -0,0 +1,262 @@
|
||||
:root{--bg:#0f1115;--bg-alt:#171a21;--border:#262a33;--text:#e2e8f0;--text-dim:#7a899d;--accent:#3b82f6;--accent-glow:#60a5fa;--danger:#ef4444;--warn:#f59e0b;--ok:#10b981;--radius:10px;--radius-sm:4px;--shadow:0 4px 12px -2px rgba(0,0,0,.4);--font:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,'Noto Sans SC',sans-serif;--trans:.25s cubic-bezier(.4,0,.2,1);--logo-start:#3b82f6;--logo-end:#2563eb;--logo-accent-grad-start:#5fa8ff;--logo-accent-grad-end:#93c5fd}
|
||||
body.light{--bg:#f6f7f9;--bg-alt:#ffffff;--border:#e2e8f0;--text:#1e293b;--text-dim:#64748b;--accent:#2563eb;--accent-glow:#3b82f6;--shadow:0 4px 20px -4px rgba(0,0,0,.08);--logo-start:#2563eb;--logo-end:#1d4ed8;--logo-accent-grad-start:#1d4ed8;--logo-accent-grad-end:#60a5fa}
|
||||
*{box-sizing:border-box}
|
||||
html,body{height:100%;margin:0;padding:0;font-family:var(--font);background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased}
|
||||
html.light{background:var(--bg)}
|
||||
body,button{font-size:14px;line-height:1.35}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{color:var(--accent-glow)}
|
||||
.topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:1rem;padding:.75rem 1.25rem;background:var(--bg-alt);border-bottom:1px solid var(--border)}
|
||||
.brand{font-weight:600;letter-spacing:.5px;font-size:15px}
|
||||
.nav{display:flex;gap:.5rem}
|
||||
.nav button{background:transparent;border:1px solid var(--border);color:var(--text-dim);padding:.45rem .9rem;border-radius:var(--radius-sm);cursor:pointer;display:flex;align-items:center;gap:.35rem;transition:var(--trans);font-weight:500}
|
||||
.nav button.active,.nav button:hover{color:var(--text);background:var(--accent);border-color:var(--accent);box-shadow:0 0 0 1px var(--accent-glow),0 4px 10px -2px rgba(0,0,0,.5)}
|
||||
.actions{margin-left:auto;display:flex;align-items:center;gap:.75rem}
|
||||
.actions button{background:var(--bg);border:1px solid var(--border);color:var(--text-dim);height:32px;width:38px;border-radius:8px;cursor:pointer;display:grid;place-items:center;transition:var(--trans)}
|
||||
.actions button:hover{color:var(--text);border-color:var(--accent);background:var(--accent)}
|
||||
.wrapper{max-width:1680px;margin:1.2rem auto;padding:0 1.2rem;display:flex;flex-direction:column;gap:1.25rem}
|
||||
.notice{padding:.9rem 1rem;border:1px solid var(--border);background:linear-gradient(145deg,var(--bg-alt),var(--bg));border-radius:var(--radius);display:flex;align-items:center;gap:.75rem;font-size:13px}
|
||||
.notice.info:before{content:"";width:8px;height:8px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 20%,transparent)}
|
||||
.panel{display:none;flex-direction:column;gap:.75rem;animation:fade .4s ease}
|
||||
.panel.active{display:flex}
|
||||
.table-wrap{overflow:auto;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-alt);box-shadow:var(--shadow)}
|
||||
table.data{width:100%;border-collapse:separate;border-spacing:0;min-width:960px}
|
||||
table.data thead th{position:sticky;top:0;background:var(--bg-alt);font-weight:500;text-align:left;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-dim);padding:.7rem .75rem;border-bottom:1px solid var(--border);white-space:nowrap}
|
||||
table.data tbody td{padding:.55rem .75rem;border-bottom:1px solid var(--border);font-size:13px;vertical-align:middle;white-space:nowrap}
|
||||
/* 防止数值变化导致列抖动:为月流量(7)/当前网络(8)/总流量(9)设置固定宽度并使用等宽数字 */
|
||||
table.data th,table.data td{font-variant-numeric:tabular-nums}
|
||||
/* 月流量(2) 左对齐;当前网络(8)/总流量(9) 居中 */
|
||||
#serversTable thead th:nth-child(2),#serversTable tbody td:nth-child(2){
|
||||
/* 月流量列:向左贴近协议(减小左 padding),同时加大右 padding 拉开与节点距离 */
|
||||
width:128px;min-width:128px;max-width:128px;font-variant-numeric:tabular-nums;letter-spacing:.3px;text-align:center;padding:0 1.05rem 0 .15rem;
|
||||
}
|
||||
/* 节点列加宽,避免被月流量胶囊视觉挤压 */
|
||||
#serversTable thead th:nth-child(3),#serversTable tbody td:nth-child(3){
|
||||
width:160px;min-width:160px;max-width:160px;
|
||||
}
|
||||
/* 协议列继续收紧右侧 padding 与固定宽度 */
|
||||
#serversTable thead th:nth-child(1),#serversTable tbody td:nth-child(1){
|
||||
padding-right:.14rem;width:78px;min-width:78px;max-width:78px; /* 扩大协议列并恢复适度间距 */
|
||||
}
|
||||
/* 让双色胶囊更靠近协议列 */
|
||||
#serversTable tbody td:nth-child(2) .caps-traffic.duo{margin-left:-6px;} /* 向协议方向微移,视觉更靠近;右侧 padding 增大避免靠近节点 */
|
||||
#serversTable thead th:nth-child(8),#serversTable tbody td:nth-child(8),
|
||||
#serversTable thead th:nth-child(9),#serversTable tbody td:nth-child(9){
|
||||
width:132px;min-width:132px;max-width:132px;font-variant-numeric:tabular-nums;letter-spacing:.3px;text-align:center;
|
||||
/* 进一步拉开与 CPU/内存/硬盘 组的视觉距离 */
|
||||
padding-right:1.95rem;
|
||||
}
|
||||
/* CPU / 内存 / 硬盘 列:居中 + 固定宽度 与仪表盘一致 */
|
||||
#serversTable thead th:nth-child(10),#serversTable tbody td:nth-child(10),
|
||||
#serversTable thead th:nth-child(11),#serversTable tbody td:nth-child(11),
|
||||
#serversTable thead th:nth-child(12),#serversTable tbody td:nth-child(12){
|
||||
width:70px;min-width:70px;max-width:70px;text-align:center;padding-left:1.1rem;padding-right:0; /* 继续右移并贴近右侧列 */
|
||||
}
|
||||
/* 月流量胶囊 */
|
||||
.caps-traffic{display:inline-flex;align-items:center;gap:6px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));border:1px solid var(--border);padding:3px 12px 3px 10px;border-radius:999px;font-size:12px;line-height:1;font-weight:500;position:relative;box-shadow:0 2px 4px -2px rgba(0,0,0,.35),0 0 0 1px rgba(255,255,255,.03);}
|
||||
.caps-traffic:before{content:"";position:absolute;inset:0;border-radius:inherit;background:radial-gradient(circle at 20% 20%,rgba(255,255,255,.06),transparent 70%);pointer-events:none;}
|
||||
.caps-traffic .io{display:inline-flex;align-items:center;gap:2px;font-variant-numeric:tabular-nums;letter-spacing:.3px;}
|
||||
.caps-traffic .io.in{color:var(--ok);}
|
||||
.caps-traffic .io.out{color:var(--accent);}
|
||||
.caps-traffic .sep{opacity:.4;font-size:11px;display:none;}
|
||||
.caps-traffic.sm{padding:2px 8px 2px 7px;font-size:11px;gap:5px;}
|
||||
.caps-traffic.sm .io{font-size:11px;}
|
||||
/* 双色胶囊:左红右黄 */
|
||||
.caps-traffic.duo{background:none;border:0;gap:0;padding:0;box-shadow:none;position:relative;border-radius:999px;overflow:hidden;font-size:12px;}
|
||||
/* 宽度按内容自适应(不再拉伸占满列),每半边仅为其文本 + padding,可容纳最大 111.1MB */
|
||||
.caps-traffic.duo .half{flex:0 0 auto;display:flex;align-items:center;justify-content:center;padding:2px 4px;font-variant-numeric:tabular-nums;font-weight:600;line-height:1.25; /* 与 .pill 保持一致高度 */ letter-spacing:.25px;color:#fff;font-size:12px;white-space:nowrap;}
|
||||
/* 双色胶囊配色:
|
||||
normal (默认): 左白(#fff) 右蓝(accent)
|
||||
heavy (>=500GB 任一方向): 左黄(warn) 右红(danger)
|
||||
*/
|
||||
/* normal 初始:淡绿色(入) + 淡蓝色(出) */
|
||||
.caps-traffic.duo.normal .half.in{background:#d1fae5;color:#065f46;} /* emerald-100 / text-emerald-800 */
|
||||
body.light .caps-traffic.duo.normal .half.in{background:#d1fae5;color:#065f46;}
|
||||
.caps-traffic.duo.normal .half.out{background:#bfdbfe;color:#1e3a8a;} /* blue-200 / text-blue-900 */
|
||||
body.light .caps-traffic.duo.normal .half.out{background:#bfdbfe;color:#1e3a8a;}
|
||||
|
||||
.caps-traffic.duo.heavy .half.in{background:var(--warn);color:#111;}
|
||||
body.light .caps-traffic.duo.heavy .half.in{background:var(--warn);color:#111;}
|
||||
.caps-traffic.duo.heavy .half.out{background:var(--danger);color:#fff;}
|
||||
body.light .caps-traffic.duo.heavy .half.out{color:#fff;}
|
||||
|
||||
/* 半之间分隔线 */
|
||||
.caps-traffic.duo .half + .half{border-left:1px solid rgba(0,0,0,.18);}
|
||||
body.light .caps-traffic.duo .half + .half{border-left:1px solid rgba(0,0,0,.08);}
|
||||
.caps-traffic.duo.sm .half{padding:1px 4px;font-size:10px;min-width:0;}
|
||||
table.data tbody tr:last-child td{border-bottom:none}
|
||||
table.data tbody tr:hover{background:rgba(255,255,255,.04)}
|
||||
.badge{display:inline-block;padding:2px 6px;font-size:11px;border-radius:12px;font-weight:500;line-height:1.2;background:var(--bg);border:1px solid var(--border);color:var(--text-dim)}
|
||||
.badge.ok{background:rgba(16,185,129,.15);color:var(--ok);border-color:rgba(16,185,129,.3)}
|
||||
.badge.warn{background:rgba(245,158,11,.15);color:var(--warn);border-color:rgba(245,158,11,.4)}
|
||||
.badge.err{background:rgba(239,68,68,.15);color:var(--danger);border-color:rgba(239,68,68,.4)}
|
||||
.footer{margin:2rem 0 2.5rem;display:flex;align-items:center;justify-content:center;gap:.5rem;font-size:12px;color:var(--text-dim)}
|
||||
.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}
|
||||
@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-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)}
|
||||
.modal-content{font-size:13px;line-height:1.5;display:grid;gap:.6rem}
|
||||
.kv{display:flex;justify-content:space-between;gap:1rem;padding:.5rem .75rem;background:linear-gradient(145deg,var(--bg),var(--bg-alt));border:1px solid var(--border);border-radius:10px}
|
||||
.kv span{white-space:nowrap}
|
||||
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}
|
||||
/* 资源使用百分比色彩标签 */
|
||||
/* 回退:移除资源使用百分比彩色标签样式 */
|
||||
|
||||
/* 详情弹窗三列信息行 */
|
||||
|
||||
/* 旧 spark 样式已移除,现使用半圆仪表盘 */
|
||||
/* 全圆旧样式(保留以便回退) */
|
||||
.gauge{--p:0;--col:var(--accent);width:74px;height:74px;position:relative;display:grid;place-items:center;font-size:11px;font-family:ui-monospace,monospace;font-weight:600;color:var(--text);}
|
||||
.gauge:before{content:"";position:absolute;inset:0;border-radius:50%;background:conic-gradient(var(--col) calc(var(--p)*1turn),rgba(255,255,255,0.06) 0);mask:radial-gradient(circle at 50% 50%,transparent 58%,#000 59%);-webkit-mask:radial-gradient(circle at 50% 50%,transparent 58%,#000 59%);border:1px solid var(--border);box-shadow:0 2px 6px -2px rgba(0,0,0,.4),0 0 0 1px rgba(255,255,255,.05);}
|
||||
.gauge span{position:relative;z-index:1}
|
||||
|
||||
/* 半圆仪表盘 */
|
||||
.gauge-half{--p:0;width:60px;height:34px;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;font-family:ui-monospace,monospace;font-size:10px;font-weight:600;gap:0;color:var(--text);}
|
||||
.gauge-half svg{width:100%;height:28px;overflow:visible;}
|
||||
.gauge-half path{fill:none;stroke-linecap:round;}
|
||||
/* 基础颜色 (暗色/亮色自动过渡) */
|
||||
.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}
|
||||
/* 阈值颜色:>=50% 警告黄,>=90% 危险红 */
|
||||
.gauge-half[data-warn] path.arc{stroke:var(--warn)}
|
||||
.gauge-half[data-bad] path.arc{stroke:var(--danger)}
|
||||
/* 指针:以中心(50,50)为原点旋转;半圆角度范围 180deg -> 从 180deg (左) 到 0deg(右) */
|
||||
.gauge-half span{line-height:1;position:relative;top:-6px;font-size:12px;}
|
||||
/* 亮色模式细化对比度 */
|
||||
body.light .gauge-half path.track{stroke:color-mix(in srgb,var(--text-dim) 28%,transparent)}
|
||||
body.light .gauge-half path.arc{filter:none}
|
||||
body.light .gauge-half .needle{background:linear-gradient(var(--text),var(--text-dim))}
|
||||
|
||||
/* status pill */
|
||||
.pill{display:inline-block;padding:2px 8px;font-size:12px;font-weight:600;border-radius:999px;letter-spacing:.45px;min-width:48px;text-align:center;line-height:1.25;border:0;box-shadow:0 2px 4px -1px rgba(0,0,0,.4),0 0 0 1px rgba(255,255,255,.04);transition:var(--trans);color:#fff}
|
||||
.pill.on{background:var(--ok)}
|
||||
.pill.off{background:var(--danger)}
|
||||
.pill.on:hover{filter:brightness(1.1)}
|
||||
.pill.off:hover{filter:brightness(1.1)}
|
||||
|
||||
/* buckets CU/CT/CM (simple version) */
|
||||
.buckets{display:flex;align-items:flex-end;gap:8px;min-width:140px}
|
||||
.bucket{position:relative;width:26px;height:34px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));border:1px solid var(--border);border-radius:8px;padding:4px 4px 16px;box-sizing:border-box;display:flex;justify-content:flex-end}
|
||||
.bucket span{display:block;width:100%;background:var(--accent);border-radius:4px 4px 6px 6px;height:var(--h);align-self:flex-end;transition:height .8s cubic-bezier(.4,0,.2,1),background .3s}
|
||||
.bucket[data-lv=warn] span{background:var(--warn)}
|
||||
.bucket[data-lv=bad] span{background:var(--danger)}
|
||||
.bucket label{position:absolute;left:0;right:0;bottom:2px;font-size:10px;text-align:center;color:var(--text-dim);pointer-events:none}
|
||||
.bucket:hover label{color:var(--text)}
|
||||
|
||||
/* 居中联通电信移动列 */
|
||||
#serversTable thead th:last-child, #serversTable tbody td:last-child { text-align:center; }
|
||||
/* 放大第13列宽度以容纳更宽水桶 */
|
||||
#serversTable thead th:nth-child(13),#serversTable tbody td:nth-child(13){
|
||||
width:150px;min-width:150px;max-width:150px;padding-left:0;padding-right:.55rem; /* 去除左 padding 进一步贴近 */
|
||||
}
|
||||
/* 调整“总流量”表头(第9列)padding 使标题文字居中,不受正文额外右 padding 影响 */
|
||||
#serversTable thead th:nth-child(9){padding-left:.75rem;padding-right:.75rem;}
|
||||
.buckets{justify-content:center}
|
||||
|
||||
/* 响应式隐藏非关键列,保持可读性 */
|
||||
@media (max-width:1100px){
|
||||
#serversTable{min-width:100%;}
|
||||
#serversTable thead th:nth-child(3),
|
||||
#serversTable tbody td:nth-child(3), /* 节点 */
|
||||
#serversTable thead th:nth-child(4),
|
||||
#serversTable tbody td:nth-child(4), /* 虚拟化 */
|
||||
#serversTable thead th:nth-child(8),
|
||||
#serversTable tbody td:nth-child(8) /* 当前网络 */ {display:none}
|
||||
}
|
||||
@media (max-width:820px){
|
||||
#serversTable thead th:nth-child(5),
|
||||
#serversTable tbody td:nth-child(5), /* 在线 */
|
||||
#serversTable thead th:nth-child(6),
|
||||
#serversTable tbody td:nth-child(6) /* 负载 */ {display:none}
|
||||
.buckets{gap:6px;min-width:100px}
|
||||
}
|
||||
@media (max-width:640px){
|
||||
#serversTable thead th:nth-child(4),
|
||||
#serversTable tbody td:nth-child(4) /* 位置 */ {display:none}
|
||||
.topbar{flex-wrap:wrap;padding:.6rem .8rem}
|
||||
.actions{width:100%;justify-content:flex-end;margin-top:.4rem}
|
||||
.wrapper{padding:0 .7rem}
|
||||
#panel-servers .table-wrap{display:none;}
|
||||
#serversCards{display:grid!important;grid-template-columns:1fr;gap:.75rem;margin-top:.5rem;}
|
||||
/* 服务与证书移动端卡片 */
|
||||
#panel-monitors .table-wrap, #panel-ssl .table-wrap{display:none;}
|
||||
#monitorsCards,#sslCards{display:grid!important;grid-template-columns:1fr;gap:.75rem;margin-top:.5rem;}
|
||||
.modal-box{max-width:100%;border-radius:14px;padding:1rem .95rem;}
|
||||
.modal-content{max-height:65vh;overflow:auto;}
|
||||
}
|
||||
|
||||
/* SSL 表(证书)列宽与换行修正,避免复用服务器列宽导致名称与域名重叠 */
|
||||
#sslTable th,#sslTable td{white-space:nowrap;padding:.55rem .75rem;}
|
||||
#sslTable th:nth-child(1),#sslTable td:nth-child(1){width:140px;min-width:120px;}
|
||||
/* 域名允许换行以防过长挤压 */
|
||||
#sslTable th:nth-child(2),#sslTable td:nth-child(2){white-space:normal;max-width:320px;overflow-wrap:anywhere;}
|
||||
|
||||
/* === 覆盖:要求三个表 (servers / monitors / ssl) 全部改为自动宽度 === */
|
||||
/* 1. 取消全局 table.data 的 min-width 对这三个表的影响 */
|
||||
#serversTable,#monitorsTable,#sslTable{min-width:0;}
|
||||
/* 2. 统一去除之前为 serversTable 设定的列固定宽度,允许浏览器自动分配 */
|
||||
#serversTable thead th,#serversTable tbody td,
|
||||
#monitorsTable thead th,#monitorsTable tbody td,
|
||||
#sslTable thead th,#sslTable tbody td{
|
||||
width:auto!important;min-width:0!important;max-width:none!important;
|
||||
padding:.55rem .7rem;
|
||||
}
|
||||
/* 3. 允许证书域名不受 max-width 限制(如仍需换行可保留 overflow-wrap) */
|
||||
#sslTable th:nth-child(2),#sslTable td:nth-child(2){max-width:none;white-space:normal;overflow-wrap:anywhere;}
|
||||
/* 4. 取消“联通|电信|移动”列固定宽度 */
|
||||
#serversTable thead th:nth-child(13),#serversTable tbody td:nth-child(13){width:auto!important;min-width:0!important;}
|
||||
/* 5. 如需稍微限制仪表盘相关列最小可读宽度,可设定一个较小下限 (可选) -- 暂不设置,完全交由自动布局 */
|
||||
|
||||
/* === 新增:为“月流量 / 当前网络 / 总流量”三列设置最小宽度,防止内容被压缩换行或挤压 === */
|
||||
/* 目标最小宽度按示例 "111.1GB|111.1GB" 设计(13 个字符左右),取 16ch 留余量 */
|
||||
#serversTable thead th:nth-child(2),#serversTable tbody td:nth-child(2),
|
||||
#serversTable thead th:nth-child(8),#serversTable tbody td:nth-child(8),
|
||||
#serversTable thead th:nth-child(9),#serversTable tbody td:nth-child(9){
|
||||
min-width:22ch !important; /* 保留自动宽度,但不小于此值 */
|
||||
text-align:center;
|
||||
font-variant-numeric:tabular-nums;
|
||||
}
|
||||
.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-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem;}
|
||||
.cards .card-title{font-weight:600;font-size:.95rem;}
|
||||
.cards .tag{font-size:.65rem;padding:.15rem .4rem;border-radius:4px;background:var(--border);letter-spacing:.5px;}
|
||||
.cards .status-pill{font-size:.6rem;padding:.2rem .45rem;border-radius:999px;font-weight:500;}
|
||||
.cards .status-pill.on{background:var(--ok);color:#fff;}
|
||||
.cards .status-pill.off{background:var(--danger);color:#fff;}
|
||||
.cards .kvlist{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.35rem .75rem;font-size:.7rem;line-height:1.25;}
|
||||
.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;}
|
||||
/* 旧移动端 latency spark 样式移除 */
|
||||
|
||||
/* 新 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)}
|
||||
.brand .logo-mark svg{display:block}
|
||||
.brand .logo-text{font-size:17px;font-weight:700;line-height:1;display:flex;align-items:baseline;gap:2px;letter-spacing:1px}
|
||||
.brand .logo-text .logo-accent{background:linear-gradient(90deg,var(--logo-accent-grad-start),var(--logo-accent-grad-end));-webkit-background-clip:text;background-clip:text;color:transparent;filter:drop-shadow(0 2px 4px rgba(0,0,0,.3))}
|
||||
.brand:hover .logo-mark{transform:translateY(-2px) scale(1.05)}
|
||||
.brand:hover .logo-text{color:var(--text)}
|
||||
@media (max-width:640px){.brand .logo-text{font-size:15px}.brand .logo-mark{width:30px;height:30px}}
|
6
web/css/bootstrap-theme.min.css
vendored
6
web/css/bootstrap-theme.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6
web/css/bootstrap.min.css
vendored
6
web/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,70 +0,0 @@
|
||||
body { background: #212e36 url('../img/dark.png'); color: #dcdcdc; }
|
||||
.navbar { min-height: 40px; }
|
||||
.navbar-brand { color: #FFF !important; padding: 10px; font-size: 20px; }
|
||||
.dropdown .dropdown-toggle { padding-bottom: 10px; padding-top: 10px; }
|
||||
.dropdown-menu > li > a { color: #FFF !important; background-color: #222222 !important; }
|
||||
.dropdown-menu > li > a:hover { color: #FFF !important; background: #000 !important; }
|
||||
.dropdown-menu { background: #222 !important; background-color: #222222 !important; }
|
||||
.navbar-inverse .navbar-inner { background-color:#1B1B1B; background-image:-moz-linear-gradient(top, #222222, #111111); background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111)); background-image:-webkit-linear-gradient(top, #222222, #111111); background-image:-o-linear-gradient(top, #222222, #111111); background-image:linear-gradient(to bottom, #212e36, #212e36); background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); border-color: #252525; }
|
||||
.content { background: #212e36; padding: 20px; border-radius: 5px; border: 1px #212e36 solid; -webkit-box-shadow: 0 0px 0px rgba(0, 0, 0, 0); -moz-box-shadow: 0 0px 0px rgba(0, 0, 0, 0); box-shadow: 0 0px 0px rgba(0, 0, 0, 0); margin-bottom: 20px; }
|
||||
.table { background: #363b40; margin-bottom: 0; border-collapse: collapse; border-radius: 3px; }
|
||||
.table th { text-align: center; }
|
||||
.table-striped tbody > tr.even > td, .table-striped tbody > tr.even > th { background-color: #212e36; }
|
||||
.table-striped tbody > tr.odd > td, .table-striped tbody > tr.odd > th { background-color: #212e36; }
|
||||
.table td { text-align: center; border-color: #2F2F2F; }
|
||||
.progress { margin-bottom: 0; background: #363b40; }
|
||||
.table-hover > tbody > tr:hover > td { background: #414141; }
|
||||
tr.even.expandRow > :hover { background: #212e36 !important; }
|
||||
tr.odd.expandRow > :hover { background: #212e36 !important; }
|
||||
.expandRow > td { padding: 0 !important; border-top: 0px !important; }
|
||||
#month_traffic { min-width: 85px; max-width: 95px;}
|
||||
#network { min-width: 115px; }
|
||||
#cpu, #ram, #hdd { min-width: 45px; max-width: 90px; }
|
||||
#ping { max-width: 95px; }
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
|
||||
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
|
||||
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
|
||||
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
|
||||
}
|
||||
@media only screen and (max-width: 720px) {
|
||||
body { font-size: 10px; }
|
||||
.content { padding: 0; }
|
||||
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
|
||||
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
|
||||
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
|
||||
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
|
||||
}
|
||||
@media only screen and (max-width: 620px) {
|
||||
body { font-size: 10px; }
|
||||
.content { padding: 0; }
|
||||
#month_traffic, tr td:nth-child(2) { display:none; visibility:hidden; }
|
||||
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
|
||||
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
|
||||
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
|
||||
#traffic, tr td:nth-child(9) { display:none; visibility:hidden; }
|
||||
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
|
||||
}
|
||||
@media only screen and (max-width: 533px) {
|
||||
body { font-size: 10px; }
|
||||
.content { padding: 0; }
|
||||
#month_traffic, tr td:nth-child(2) { display:none; visibility:hidden; }
|
||||
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
|
||||
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
|
||||
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
|
||||
#traffic, tr td:nth-child(9) { display:none; visibility:hidden; }
|
||||
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
|
||||
}
|
||||
@media only screen and (max-width: 450px) {
|
||||
body { font-size: 10px; }
|
||||
.content { padding: 0; }
|
||||
#month_traffic, tr td:nth-child(2) { display:none; visibility:hidden; }
|
||||
#name, tr td:nth-child(3) { min-width: 55px; max-width: 85px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
|
||||
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
|
||||
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
|
||||
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
|
||||
#traffic, tr td:nth-child(9) { display:none; visibility:hidden; }
|
||||
#cpu, #ram, #hdd { min-width: 25px; max-width: 50px; }
|
||||
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
body { background: #ebebeb url('../img/light.png'); }
|
||||
.navbar { min-height: 40px; }
|
||||
.navbar-brand { color: #fff; padding: 10px; font-size: 20px; }
|
||||
.dropdown .dropdown-toggle { padding-bottom: 10px; padding-top: 10px; }
|
||||
.navbar-inverse .navbar-brand { color: #fff; padding: 10px; font-size: 20px; }
|
||||
.content { background: #ffffff; padding: 20px; border-radius: 5px; border: 1px #cecece solid; -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, .1); -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, .1); box-shadow: 0 1px 10px rgba(0, 0, 0, .1); margin-bottom: 20px; }
|
||||
.table { background: #ffffff; margin-bottom: 0; border-collapse: collapse; border-radius: 3px; }
|
||||
.table th, .table td { text-align: center; }
|
||||
.table-striped tbody > tr.even > td, .table-striped tbody > tr.even > th { background-color: #F9F9F9; }
|
||||
.table-striped tbody > tr.odd > td, .table-striped tbody > tr.odd > th { background-color: #FFF; }
|
||||
.progress { margin-bottom: 0; }
|
||||
.progress-bar { color: #000; }
|
||||
.table-hover > tbody > tr:hover > td { background: #E6E6E6; }
|
||||
tr.even.expandRow > :hover { background: #F9F9F9 !important; }
|
||||
tr.odd.expandRow > :hover { background: #FFF !important; }
|
||||
.expandRow > td { padding: 0 !important; border-top: 0px !important; }
|
||||
#month_traffic { min-width: 85px; max-width: 95px;}
|
||||
#network { min-width: 115px; }
|
||||
#cpu, #ram, #hdd { min-width: 45px; max-width: 90px; }
|
||||
#ping { max-width: 95px; }
|
||||
|
||||
@media only screen and (max-width: 1200px) {
|
||||
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
|
||||
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
|
||||
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
|
||||
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
|
||||
}
|
||||
@media only screen and (max-width: 720px) {
|
||||
body { font-size: 10px; }
|
||||
.content { padding: 0; }
|
||||
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
|
||||
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
|
||||
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
|
||||
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
|
||||
}
|
||||
@media only screen and (max-width: 620px) {
|
||||
body { font-size: 10px; }
|
||||
.content { padding: 0; }
|
||||
#month_traffic, tr td:nth-child(2) { display:none; visibility:hidden; }
|
||||
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
|
||||
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
|
||||
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
|
||||
#traffic, tr td:nth-child(9) { display:none; visibility:hidden; }
|
||||
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
|
||||
}
|
||||
@media only screen and (max-width: 533px) {
|
||||
body { font-size: 10px; }
|
||||
.content { padding: 0; }
|
||||
#month_traffic, tr td:nth-child(2) { display:none; visibility:hidden; }
|
||||
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
|
||||
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
|
||||
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
|
||||
#traffic, tr td:nth-child(9) { display:none; visibility:hidden; }
|
||||
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
|
||||
}
|
||||
@media only screen and (max-width: 450px) {
|
||||
body { font-size: 10px; }
|
||||
.content { padding: 0; }
|
||||
#month_traffic, tr td:nth-child(2) { display:none; visibility:hidden; }
|
||||
#name, tr td:nth-child(3) { min-width: 55px; max-width: 85px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
|
||||
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
|
||||
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
|
||||
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
|
||||
#traffic, tr td:nth-child(9) { display:none; visibility:hidden; }
|
||||
#cpu, #ram, #hdd { min-width: 25px; max-width: 50px; }
|
||||
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
|
||||
}
|
BIN
web/favicon.ico
BIN
web/favicon.ico
Binary file not shown.
Before Width: 128px | Height: 86px | Size: 44 KiB |
11
web/favicon.svg
Normal file
11
web/favicon.svg
Normal file
@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="8" y1="8" x2="56" y2="56" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#3b82f6" />
|
||||
<stop offset="1" stop-color="#2563eb" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="8" y="12" width="48" height="40" rx="12" fill="url(#g)" />
|
||||
<path stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" d="M22 38v-8l6 4 8-10 6 6v8" />
|
||||
<circle cx="22" cy="22" r="4" fill="#fff" />
|
||||
</svg>
|
After (image error) Size: 539 B |
BIN
web/img/dark.png
BIN
web/img/dark.png
Binary file not shown.
Before ![]() (image error) Size: 1.9 KiB |
Binary file not shown.
Before ![]() (image error) Size: 19 KiB |
195
web/index.html
195
web/index.html
@ -1,110 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
json字段保持完整, 自行自定义前端展示
|
||||
ლ(•̀ _ •́ ლ)
|
||||
ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ)
|
||||
ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ)
|
||||
ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ)
|
||||
ლ(•̀ _ •́ ლ)
|
||||
follow me, better solution for you. by:https://cpp.la
|
||||
-->
|
||||
<html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta name="description" content="云监控,ServerStatus中文版,ServerStatus,ServerStatus cppla" />
|
||||
<title>云监控</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="云监控">
|
||||
<meta name="author" content="BotoX">
|
||||
<link rel="stylesheet" href="css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="css/bootstrap-theme.min.css">
|
||||
<link rel="stylesheet" href="css/light.css" title="light">
|
||||
<link rel="stylesheet" href="css/dark.css" title="dark">
|
||||
<style>
|
||||
body {
|
||||
padding-top: 70px;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
</style>
|
||||
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="js/html5shiv.js"></script>
|
||||
<script src="js/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="alternate icon" href="favicon.ico" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div role="navigation" class="navbar navbar-inverse navbar-fixed-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button data-target=".navbar-collapse" data-toggle="collapse" class="navbar-toggle" type="button">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a href="#" class="navbar-brand">云监控</a>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
<ul class="nav navbar-nav">
|
||||
<li class="dropdown">
|
||||
<a data-toggle="dropdown" class="dropdown-toggle" href="#">风格<b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="#" onclick="setActiveStyleSheet('dark', true)">黑夜</a></li>
|
||||
<li><a href="#" onclick="setActiveStyleSheet('light', true)">白天</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div><!--/.nav-collapse -->
|
||||
</div>
|
||||
<header class="topbar">
|
||||
<div class="brand" title="云监控">
|
||||
<span class="logo-mark" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M7.5 17a4.5 4.5 0 0 1-.9-8.92A6 6 0 0 1 18.4 9.6 4 4 0 0 1 18 17H7.5Z" />
|
||||
<rect x="9" y="11" width="6" height="4" rx="1" />
|
||||
<path d="M11 15v2.5a.5.5 0 0 0 .5.5h1" />
|
||||
</svg>
|
||||
</span>
|
||||
<span class="logo-text"><span class="logo-accent">云</span>监控</span>
|
||||
</div>
|
||||
<nav class="nav" id="navTabs">
|
||||
<button data-tab="servers" class="active">主机</button>
|
||||
<button data-tab="monitors">服务</button>
|
||||
<button data-tab="ssl">证书</button>
|
||||
</nav>
|
||||
<div class="actions">
|
||||
<button id="themeToggle" title="切换主题 (当前: 自动或手动)" aria-label="切换主题">🌓</button>
|
||||
<span id="lastUpdate" class="muted">更新中...</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container content">
|
||||
<div id="loading-notice">
|
||||
<noscript>
|
||||
<div class="alert alert-danger" style="text-align: center;">
|
||||
<strong>Enable JavaScript</strong> , please do it.
|
||||
</div>
|
||||
</noscript>
|
||||
<div style="text-align: center;">
|
||||
警告:如果出现此消息,请确保您已启用Javascript! <br />否则云监控主服务没启动或已关闭.
|
||||
</div>
|
||||
<p></p>
|
||||
</div>
|
||||
<table class="table table-striped table-condensed table-hover">
|
||||
<main class="wrapper">
|
||||
<div id="notice" class="notice info">初始化中...</div>
|
||||
|
||||
<section id="panel-servers" class="panel active" aria-labelledby="主机">
|
||||
<div class="table-wrap">
|
||||
<table class="data" id="serversTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="online_status" style="text-align: center;">🔗协议</th>
|
||||
<th id="month_traffic" style="text-align: center;">📊月流量↓|↑</th>
|
||||
<th id="name">📌节点</th>
|
||||
<th id="type">🗂️虚拟化</th>
|
||||
<th id="location">🌍位置</th>
|
||||
<th id="uptime">⏱️在线</th>
|
||||
<th id="load">负载</th>
|
||||
<th id="network">🚦网络↓|↑</th>
|
||||
<th id="traffic">📋总流量↓|↑</th>
|
||||
<th id="cpu">🎯核芯</th>
|
||||
<th id="ram">⚡️内存</th>
|
||||
<th id="hdd">💾硬盘</th>
|
||||
<th id="ping">🌐CU|CT|CM</th>
|
||||
<th>协议</th>
|
||||
<th>月流量 ↓|↑</th>
|
||||
<th>节点</th>
|
||||
<th>虚拟化</th>
|
||||
<th>位置</th>
|
||||
<th>在线</th>
|
||||
<th>负载</th>
|
||||
<th>当前网络 ↓|↑</th>
|
||||
<th>总流量 ↓|↑</th>
|
||||
<th>CPU</th>
|
||||
<th>内存</th>
|
||||
<th>硬盘</th>
|
||||
<th style="text-align:center;">联通|电信|移动</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="servers">
|
||||
<!-- Servers here \o/ -->
|
||||
</tbody>
|
||||
<tbody id="serversBody"></tbody>
|
||||
</table>
|
||||
<br />
|
||||
<div id="updated">Updating...</div>
|
||||
</div>
|
||||
<!-- 移动端卡片布局 -->
|
||||
<div id="serversCards" class="cards" style="display:none;"></div>
|
||||
</section>
|
||||
|
||||
<section id="panel-monitors" class="panel" aria-labelledby="服务">
|
||||
<div class="table-wrap">
|
||||
<table class="data" id="monitorsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>协议</th>
|
||||
<th>监测节点</th>
|
||||
<th>监测位置</th>
|
||||
<th>监测内容</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="monitorsBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- 移动端卡片布局 (服务) -->
|
||||
<div id="monitorsCards" class="cards" style="display:none;"></div>
|
||||
</section>
|
||||
|
||||
<section id="panel-ssl" class="panel" aria-labelledby="证书">
|
||||
<div class="table-wrap">
|
||||
<table class="data" id="sslTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>名称</th>
|
||||
<th>域名</th>
|
||||
<th>端口</th>
|
||||
<th>剩余(天)</th>
|
||||
<th>到期(UTC)</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="sslBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- 移动端卡片布局 (证书) -->
|
||||
<div id="sslCards" class="cards" style="display:none;"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<div id="detailModal" class="modal-backdrop" style="display:none;">
|
||||
<div class="modal-box" role="dialog" aria-modal="true" aria-labelledby="detailTitle">
|
||||
<button class="modal-close" id="detailClose" aria-label="关闭">×</button>
|
||||
<h3 id="detailTitle" class="modal-title">节点详情</h3>
|
||||
<div id="detailContent" class="modal-content">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<p style="text-align: center; font-size: 10px;">
|
||||
<a href="https://github.com/cppla/ServerStatus">ServerStatus中文版</a>
|
||||
</p>
|
||||
</div>
|
||||
<script src="js/jquery.min.js"></script>
|
||||
<script src="js/bootstrap.min.js"></script>
|
||||
<script src="js/serverstatus.js"></script>
|
||||
<footer class="footer">
|
||||
<a href="https://github.com/cppla/ServerStatus" target="_blank" rel="noopener">ServerStatus中文版</a>
|
||||
</footer>
|
||||
|
||||
<script src="js/app.js" defer></script>
|
||||
</body>
|
||||
</html>
|
537
web/js/app.js
Normal file
537
web/js/app.js
Normal file
@ -0,0 +1,537 @@
|
||||
// 简洁现代前端 - 仅使用原生 JS
|
||||
const S = { updated:0, servers:[], ssl:[], error:false, hist:{}, metricHist:{}, loadHist:{} };// hist latency; metricHist: {key:{cpu:[],mem:[],hdd:[]}}; loadHist: {key:[]}
|
||||
const els = {
|
||||
notice: ()=>document.getElementById('notice'),
|
||||
last: ()=>document.getElementById('lastUpdate'),
|
||||
serversBody: ()=>document.getElementById('serversBody'),
|
||||
monitorsBody: ()=>document.getElementById('monitorsBody'),
|
||||
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++; }
|
||||
const out = mb>=100? mb.toFixed(0): mb.toFixed(1); return out+units[i]; }
|
||||
function humanMinMBFromMB(mbVal){ if(mbVal==null||isNaN(mbVal)) return '-'; // 输入单位: MB
|
||||
let v=mbVal; const units=['MB','GB','TB','PB']; let i=0; while(v>=1000 && i<units.length-1){ v/=1000;i++; }
|
||||
const out = v>=100? v.toFixed(0): v.toFixed(1); return out+units[i]; }
|
||||
function humanMinMBFromB(bytes){ if(bytes==null||isNaN(bytes)) return '-'; // 输入单位: B
|
||||
let mb = bytes/1000/1000; const units=['MB','GB','TB','PB']; let i=0; while(mb>=1000 && i<units.length-1){ mb/=1000;i++; }
|
||||
const out = mb>=100? mb.toFixed(0): mb.toFixed(1); return out+units[i]; }
|
||||
function humanRateMinMBFromB(bytes){ if(bytes==null||isNaN(bytes)) return '-'; if(bytes<=0) return '0.0MB'; return humanMinMBFromB(bytes); }
|
||||
function humanMinKBFromB(bytes){ if(bytes==null||isNaN(bytes)) return '-'; // 输入单位: B; 最小单位 KB
|
||||
let kb = bytes/1000; const units=['KB','MB','GB','TB','PB']; let i=0; while(kb>=1000 && i<units.length-1){ kb/=1000; i++; }
|
||||
const out = kb>=100? kb.toFixed(0): kb.toFixed(1); return out+units[i]; }
|
||||
// (清理) pct / clsBy 已不再使用
|
||||
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 : '-'; }
|
||||
|
||||
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';
|
||||
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 分钟
|
||||
['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); });
|
||||
}
|
||||
// 负载历史 (记录 load_1 / load_5 / load_15)
|
||||
if(!S.loadHist[key]) S.loadHist[key] = {l1:[],l5:[],l15:[]};
|
||||
const LH = S.loadHist[key];
|
||||
const pushLoad = (arr,val)=>{ if(typeof val === 'number' && val >= 0){ arr.push(val); if(arr.length>120) arr.splice(0,arr.length-120); } };
|
||||
pushLoad(LH.l1, s.load_1);
|
||||
pushLoad(LH.l5, s.load_5);
|
||||
pushLoad(LH.l15, s.load_15);
|
||||
});
|
||||
render();
|
||||
}catch(e){ S.error=true; els.notice().textContent = '数据获取失败'; console.error(e); }
|
||||
}
|
||||
|
||||
function render(){
|
||||
els.notice().style.display='none';
|
||||
renderServers();
|
||||
renderServersCards();
|
||||
renderMonitors();
|
||||
renderMonitorsCards();
|
||||
renderSSL();
|
||||
renderSSLCards();
|
||||
updateTime();
|
||||
}
|
||||
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 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;
|
||||
const monthInBytes = (s.network_in - s.last_network_in) || 0; // 原始: B
|
||||
const monthOutBytes = (s.network_out - s.last_network_out) || 0;
|
||||
const monthIn = humanMinMBFromB(monthInBytes); // 最小单位 MB
|
||||
const monthOut = humanMinMBFromB(monthOutBytes);
|
||||
const HEAVY_THRESHOLD = 500 * 1000 * 1000 * 1000; // 500GB
|
||||
const heavy = monthInBytes >= HEAVY_THRESHOLD || monthOutBytes >= HEAVY_THRESHOLD;
|
||||
const trafficCls = heavy ? 'caps-traffic duo heavy' : 'caps-traffic duo normal';
|
||||
const netNow = humanMinKBFromB(s.network_rx) + ' | ' + humanMinKBFromB(s.network_tx); // 最小单位 KB
|
||||
const netTotal = humanMinMBFromB(s.network_in)+' | '+humanMinMBFromB(s.network_out); // 最小单位 MB
|
||||
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';
|
||||
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;'}">
|
||||
<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>
|
||||
<td>${s.type||'-'}</td>
|
||||
<td>${s.location||'-'}</td>
|
||||
<td>${s.uptime||'-'}</td>
|
||||
<td>${s.load_1==-1?'–':Math.max(0,(s.load_1||0)).toFixed(2)}</td>
|
||||
<td>${netNow}</td>
|
||||
<td>${netTotal}</td>
|
||||
<td>${online?gaugeHTML('cpu', s.cpu||0):'-'}</td>
|
||||
<td>${online?gaugeHTML('mem', memPct):'-'}</td>
|
||||
<td>${online?gaugeHTML('hdd', hddPct):'-'}</td>
|
||||
<td>${pingBuckets}</td>
|
||||
</tr>`;
|
||||
});
|
||||
tbody.innerHTML = html || `<tr><td colspan="13" class="muted" style="text-align:center;padding:1rem;">无数据</td></tr>`;
|
||||
|
||||
// 绑定行点击
|
||||
tbody.querySelectorAll('tr.row-server').forEach(tr=>{
|
||||
tr.addEventListener('click',()=>{
|
||||
if(tr.getAttribute('data-online')!=='1') return; // 离线不弹出
|
||||
const i = parseInt(tr.getAttribute('data-idx'));
|
||||
openDetail(i);
|
||||
});
|
||||
});
|
||||
|
||||
// 仪表盘无需历史 spark 小图
|
||||
}
|
||||
// 生成仪表盘 (圆形 conic-gradient)
|
||||
function gaugeHTML(type,val){
|
||||
const pct = Math.max(0,Math.min(100,val));
|
||||
const p = (pct/100).toFixed(3);
|
||||
const warnAttr = pct>=90? 'data-bad' : (pct>=50? 'data-warn' : '');
|
||||
return `<div class="gauge-half" data-type="${type}" ${warnAttr} style="--p:${p}" title="${labelOf(type)} ${pct.toFixed(0)}%">
|
||||
<svg viewBox="0 0 100 50" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
|
||||
<path class="track" d="M10 50 A40 40 0 0 1 90 50" />
|
||||
<path class="arc" d="M10 50 A40 40 0 0 1 90 50" />
|
||||
</svg>
|
||||
<span>${pct.toFixed(0)}%</span>
|
||||
</div>`;
|
||||
}
|
||||
function labelOf(t){ return t==='cpu'?'CPU': t==='mem'?'内存':'硬盘'; }
|
||||
function renderServersCards(){
|
||||
const wrap = document.getElementById('serversCards');
|
||||
if(!wrap) return;
|
||||
// 仅在窄屏时显示 (和 CSS 一致判断, 可稍放宽避免闪烁)
|
||||
if(window.innerWidth>700){ wrap.innerHTML=''; return; }
|
||||
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 pill = `<span class="status-pill ${online?'on':'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;
|
||||
// 月流量(移动端)并应用 500GB 阈值配色逻辑
|
||||
const monthInBytes = (s.network_in - s.last_network_in) || 0; // B
|
||||
const monthOutBytes = (s.network_out - s.last_network_out) || 0;
|
||||
const monthIn = humanMinMBFromB(monthInBytes);
|
||||
const monthOut = humanMinMBFromB(monthOutBytes);
|
||||
const HEAVY_THRESHOLD = 500 * 1000 * 1000 * 1000; // 500GB
|
||||
const heavy = monthInBytes >= HEAVY_THRESHOLD || monthOutBytes >= HEAVY_THRESHOLD;
|
||||
const trafficCls = heavy ? 'caps-traffic duo heavy sm' : 'caps-traffic duo normal sm';
|
||||
const netNow = humanMinKBFromB(s.network_rx)+' | '+humanMinKBFromB(s.network_tx);
|
||||
const netTotal = humanMinMBFromB(s.network_in)+' | '+humanMinMBFromB(s.network_out);
|
||||
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';
|
||||
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>`;
|
||||
});
|
||||
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;}
|
||||
if(card.getAttribute('data-online')!=='1') return; // 离线不弹
|
||||
openDetail(idx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderMonitors(){
|
||||
const tbody = els.monitorsBody();
|
||||
let html='';
|
||||
S.servers.forEach(s=>{
|
||||
html += `<tr>
|
||||
<td>${(s.online4||s.online6)?'在线':'离线'}</td>
|
||||
<td>${s.name||'-'}</td>
|
||||
<td>${s.location||'-'}</td>
|
||||
<td>${s.custom||'-'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
tbody.innerHTML = html || `<tr><td colspan="4" class="muted" style="text-align:center;padding:1rem;">无数据</td></tr>`;
|
||||
}
|
||||
|
||||
// 服务卡片 (移动端)
|
||||
function renderMonitorsCards(){
|
||||
const wrap = document.getElementById('monitorsCards');
|
||||
if(!wrap) return; if(window.innerWidth>700){ wrap.innerHTML=''; return; }
|
||||
let html='';
|
||||
S.servers.forEach(s=>{
|
||||
const online = (s.online4||s.online6)?'在线':'离线';
|
||||
const pill = `<span class="status-pill ${online==='在线'?'on':'off'}">${online}</span>`;
|
||||
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>
|
||||
</div>`;
|
||||
});
|
||||
wrap.innerHTML = html || '<div class="muted" style="font-size:.75rem;text-align:center;padding:1rem;">无数据</div>';
|
||||
}
|
||||
|
||||
function renderSSL(){
|
||||
const tbody = els.sslBody();
|
||||
let html='';
|
||||
S.ssl.forEach(c=>{
|
||||
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>
|
||||
<td>${c.name||'-'}</td>
|
||||
<td>${(c.domain||'').replace(/^https?:\/\//,'')}</td>
|
||||
<td>${c.port||443}</td>
|
||||
<td><span class="badge ${cls}">${c.expire_days??'-'}</span></td>
|
||||
<td>${dt}</td>
|
||||
<td><span class="badge ${cls}">${status}</span></td>
|
||||
</tr>`;
|
||||
});
|
||||
tbody.innerHTML = html || `<tr><td colspan="6" class="muted" style="text-align:center;padding:1rem;">无证书数据</td></tr>`;
|
||||
}
|
||||
|
||||
// 证书卡片 (移动端)
|
||||
function renderSSLCards(){
|
||||
const wrap = document.getElementById('sslCards');
|
||||
if(!wrap) return; if(window.innerWidth>700){ wrap.innerHTML=''; return; }
|
||||
let html='';
|
||||
S.ssl.forEach(c=>{
|
||||
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">
|
||||
<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>${dt.split(' ')[0]||dt}</span></div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
wrap.innerHTML = html || '<div class="muted" style="font-size:.75rem;text-align:center;padding:1rem;">无证书数据</div>';
|
||||
}
|
||||
|
||||
function updateTime(){
|
||||
const el = els.last();
|
||||
if(S.updated){ el.textContent = '最后更新: '+ humanAgo(S.updated); }
|
||||
}
|
||||
|
||||
function bindTabs(){
|
||||
document.getElementById('navTabs').addEventListener('click',e=>{
|
||||
if(e.target.tagName!=='BUTTON') return; const tab=e.target.dataset.tab;
|
||||
document.querySelectorAll('.nav button').forEach(b=>b.classList.toggle('active',b===e.target));
|
||||
document.querySelectorAll('.panel').forEach(p=>p.classList.toggle('active', p.id==='panel-'+tab));
|
||||
});
|
||||
}
|
||||
function bindTheme(){
|
||||
const btn = document.getElementById('themeToggle');
|
||||
const mql = window.matchMedia('(prefers-color-scheme: light)');
|
||||
const saved = localStorage.getItem('theme'); // 'light' | 'dark' | null (auto)
|
||||
|
||||
const apply = (isLight)=>{ document.body.classList.toggle('light', isLight); document.documentElement.classList.toggle('light', isLight); };
|
||||
|
||||
if(!saved){
|
||||
// 自动跟随系统
|
||||
apply(mql.matches);
|
||||
// 监听系统偏好变化(仅在未手动选择时)
|
||||
mql.addEventListener('change', e=>{ if(!localStorage.getItem('theme')) apply(e.matches); });
|
||||
} else {
|
||||
apply(saved==='light');
|
||||
}
|
||||
|
||||
btn.addEventListener('click',()=>{
|
||||
// 用户手动切换后即固定,不再自动
|
||||
const toLight = !document.body.classList.contains('light');
|
||||
apply(toLight);
|
||||
localStorage.setItem('theme', toLight?'light':'dark');
|
||||
});
|
||||
}
|
||||
|
||||
bindTabs();
|
||||
bindTheme();
|
||||
fetchData();
|
||||
setInterval(fetchData, 1000);
|
||||
setInterval(updateTime, 60000);
|
||||
|
||||
// 详情弹窗逻辑
|
||||
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 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;
|
||||
const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0;
|
||||
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';
|
||||
|
||||
let latencyBlock = '';
|
||||
if(!offline){
|
||||
latencyBlock = `
|
||||
<div style="display:flex;flex-direction:column;gap:.4rem;">
|
||||
<canvas id="latChart" height="150" 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:1rem;flex-wrap:wrap;">
|
||||
<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>
|
||||
</div>
|
||||
</div>`;
|
||||
} else {
|
||||
latencyBlock = `
|
||||
<div style="display:flex;flex-direction:column;gap:.4rem;">
|
||||
<canvas id="latChart" height="150" 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;opacity:.6;">离线,无联通/电信/移动延迟数据</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// 旧进度条函数 barHTML/ioBar 已弃用
|
||||
// 资源行(移除百分比显示,仅显示 已用 / 总量)
|
||||
// 资源行(单独拆分 span 便于后续动态刷新)
|
||||
// 单位来源:memory/swap: KB; hdd: MB; io: B (速率) -> 统一最小单位显示为 MB,并向上进位 (MB/GB/TB)
|
||||
const memUsed = s.memory_total!=null? humanMinMBFromKB(s.memory_used||0):'-';
|
||||
const memTotal = s.memory_total!=null? humanMinMBFromKB(s.memory_total):'-';
|
||||
const swapUsed = s.swap_total!=null? humanMinMBFromKB(s.swap_used||0):'-';
|
||||
const swapTotal = s.swap_total!=null? humanMinMBFromKB(s.swap_total):'-';
|
||||
const hddUsed = s.hdd_total!=null? humanMinMBFromMB(s.hdd_used||0):'-';
|
||||
const hddTotal = s.hdd_total!=null? humanMinMBFromMB(s.hdd_total):'-';
|
||||
const ioReadLine = (ioRead!=null)? humanRateMinMBFromB(ioRead):'-';
|
||||
const ioWriteLine = (ioWrite!=null)? humanRateMinMBFromB(ioWrite):'-';
|
||||
const memColor = memPct>80? ' style="color:var(--danger)"':''; // 已用/总量显示为单一块,所以对已用着色
|
||||
const swapColor = swapPct>80? ' style="color:var(--danger)"':'';
|
||||
const hddColor = hddPct>80? ' style="color:var(--danger)"':'';
|
||||
// IO 阈值:>100MB (原始单位 B) -> >100*1000*1000 B
|
||||
const readColor = ioRead>100*1000*1000? ' style="color:var(--danger)"':'';
|
||||
const writeColor = ioWrite>100*1000*1000? ' style="color:var(--danger)"':'';
|
||||
box.innerHTML = `
|
||||
<div class="kv"><span>TCP/UDP/进/线</span><span class="mono" id="detail-proc">${procLine}</span></div>
|
||||
<div class="kv"><span>内存 / 虚存</span><span class="mono">
|
||||
<span id="mem-line"${memColor}><span id="mem-used">${memUsed}</span> / <span id="mem-total">${memTotal}</span></span>
|
||||
| <span id="swap-line"${swapColor}><span id="swap-used">${swapUsed}</span> / <span id="swap-total">${swapTotal}</span></span>
|
||||
</span></div>
|
||||
<div class="kv"><span>硬盘 / 读写</span><span class="mono">
|
||||
<span id="disk-line"${hddColor}><span id="hdd-used">${hddUsed}</span> / <span id="hdd-total">${hddTotal}</span></span>
|
||||
| <span id="io-read"${readColor}>${ioReadLine}</span> / <span id="io-write"${writeColor}>${ioWriteLine}</span>
|
||||
</span></div>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 进度条移除:读/写/虚存以文本形式显示于上方合并行 -->
|
||||
${latencyBlock}
|
||||
`;
|
||||
modal.style.display='flex';
|
||||
document.addEventListener('keydown', escCloseOnce);
|
||||
if(!offline){
|
||||
drawLatencyChart(key);
|
||||
drawLoadChart(key);
|
||||
S._openDetailKey = key; // 记录当前弹窗对应节点
|
||||
startDetailAutoUpdate();
|
||||
} else {
|
||||
S._openDetailKey = null;
|
||||
stopDetailAutoUpdate();
|
||||
}
|
||||
}
|
||||
function escCloseOnce(e){ if(e.key==='Escape'){ closeDetail(); } }
|
||||
function closeDetail(){ const m=document.getElementById('detailModal'); m.style.display='none'; document.removeEventListener('keydown', escCloseOnce); stopDetailAutoUpdate(); }
|
||||
document.getElementById('detailClose').addEventListener('click', closeDetail);
|
||||
document.getElementById('detailModal').addEventListener('click', e=>{ if(e.target.id==='detailModal') closeDetail(); });
|
||||
|
||||
// 绘制三网延迟折线图 (简易实现)
|
||||
function drawLatencyChart(key){
|
||||
const data = S.hist[key];
|
||||
const canvas = document.getElementById('latChart');
|
||||
if(!canvas || !data) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const W = canvas.clientWidth; const H = canvas.height; canvas.width = W; // 适配宽度
|
||||
ctx.clearRect(0,0,W,H);
|
||||
const padL=40, padR=10, padT=10, padB=18;
|
||||
const isLight = document.body.classList.contains('light');
|
||||
const axisColor = isLight? 'rgba(0,0,0,0.22)' : 'rgba(255,255,255,0.18)';
|
||||
const gridColor = isLight? 'rgba(0,0,0,0.08)' : 'rgba(255,255,255,0.10)';
|
||||
const textColor = isLight? 'var(--text-dim)' : 'rgba(226,232,240,0.85)';
|
||||
const series = [ {arr:data.cu,color:'#3b82f6'}, {arr:data.ct,color:'#10b981'}, {arr:data.cm,color:'#f59e0b'} ];
|
||||
const allVals = series.flatMap(s=>s.arr);
|
||||
if(!allVals.length){ ctx.fillStyle='var(--text-dim)'; ctx.font='12px system-ui'; ctx.fillText('暂无数据', W/2-30, H/2); return; }
|
||||
const max = Math.max(...allVals);
|
||||
const min = Math.min(...allVals);
|
||||
const range = Math.max(1, max-min);
|
||||
const n = Math.max(...series.map(s=>s.arr.length));
|
||||
const xStep = (W - padL - padR) / Math.max(1,n-1);
|
||||
// 网格与轴 (增强暗色对比)
|
||||
ctx.strokeStyle=axisColor; ctx.lineWidth=1.1;
|
||||
ctx.beginPath(); ctx.moveTo(padL,padT); ctx.lineTo(padL,H-padB); ctx.lineTo(W-padR,H-padB); ctx.stroke();
|
||||
ctx.fillStyle=textColor; ctx.font='10px system-ui';
|
||||
const yMarks=4; for(let i=0;i<=yMarks;i++){ const y = padT + (H-padT-padB)*i/yMarks; const val = (max - range*i/yMarks).toFixed(0)+'ms'; ctx.fillText(val,4,y+3); ctx.strokeStyle=gridColor; ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(W-padR,y); ctx.stroke(); }
|
||||
// 绘制线
|
||||
series.forEach(s=>{
|
||||
if(s.arr.length<2) return;
|
||||
ctx.strokeStyle = s.color; ctx.lineWidth=1.6; ctx.beginPath();
|
||||
s.arr.forEach((v,i)=>{ const x = padL + xStep*i; const y = padT + (H-padT-padB)*(1-(v-min)/range); if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); });
|
||||
ctx.stroke();
|
||||
});
|
||||
}
|
||||
|
||||
// 在每次 render 后若弹窗打开则重绘最新图
|
||||
const _oldRender = render;
|
||||
render = function(){ _oldRender(); if(S._openDetailKey){ drawLatencyChart(S._openDetailKey); drawLoadChart(S._openDetailKey); } };
|
||||
window.addEventListener('resize', ()=>{
|
||||
if(S._openDetailKey){ drawLatencyChart(S._openDetailKey); drawLoadChart(S._openDetailKey); }
|
||||
renderServersCards();
|
||||
renderMonitorsCards();
|
||||
renderSSLCards();
|
||||
});
|
||||
|
||||
// (清理) drawSparks 已移除
|
||||
|
||||
// 负载折线图 (load1 历史)
|
||||
function drawLoadChart(key){
|
||||
const L = S.loadHist[key];
|
||||
const canvas = document.getElementById('loadChart');
|
||||
if(!canvas) return; const ctx = canvas.getContext('2d');
|
||||
if(!L){ ctx.clearRect(0,0,canvas.width,canvas.height); return; }
|
||||
const l1=L.l1||[], l5=L.l5||[], l15=L.l15||[];
|
||||
const canvasW = canvas.clientWidth; const H = canvas.height; canvas.width = canvasW; const W=canvasW;
|
||||
ctx.clearRect(0,0,W,H);
|
||||
if(l1.length<2){ ctx.fillStyle='var(--text-dim)'; ctx.font='12px system-ui'; ctx.fillText('暂无负载数据', W/2-42, H/2); return; }
|
||||
const all = [...l1,...l5,...l15];
|
||||
const padL=38,padR=8,padT=8,padB=16;
|
||||
// 修正:纵轴下限不小于 0,且当真实 range <0.5 时向上扩展 max 而不是向下产生负刻度
|
||||
const rawMax = all.length? Math.max(...all):0;
|
||||
const rawMin = all.length? Math.min(...all):0;
|
||||
const min = 0; // 我们只显示 >=0
|
||||
let max = Math.max(rawMax,0);
|
||||
let range = max - min;
|
||||
const MIN_RANGE = 0.5;
|
||||
if(range < MIN_RANGE){ max = MIN_RANGE; range = MIN_RANGE; }
|
||||
const n = Math.max(l1.length,l5.length,l15.length); const xStep=(W-padL-padR)/Math.max(1,n-1);
|
||||
const isLight = document.body.classList.contains('light');
|
||||
const axisColor = isLight? 'rgba(0,0,0,0.22)' : 'rgba(255,255,255,0.18)';
|
||||
const gridColor = isLight? 'rgba(0,0,0,0.08)' : 'rgba(255,255,255,0.10)';
|
||||
const textColor = isLight? 'var(--text-dim)' : 'rgba(226,232,240,0.85)';
|
||||
// 轴 & 网格 (增强暗色对比)
|
||||
ctx.strokeStyle=axisColor; ctx.lineWidth=1.1; ctx.beginPath(); ctx.moveTo(padL,padT); ctx.lineTo(padL,H-padB); ctx.lineTo(W-padR,H-padB); ctx.stroke();
|
||||
ctx.fillStyle=textColor; ctx.font='10px system-ui';
|
||||
const yMarks=4; for(let i=0;i<=yMarks;i++){
|
||||
const y=padT+(H-padT-padB)*i/yMarks;
|
||||
const val=(max - range*i/yMarks); // top -> bottom
|
||||
const labelVal = (Math.abs(val) < 0.005 ? 0 : val).toFixed(2);
|
||||
ctx.fillText(labelVal,4,y+3);
|
||||
ctx.strokeStyle=gridColor; ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(W-padR,y); ctx.stroke();
|
||||
}
|
||||
const series=[{arr:l1,color:'#8b5cf6',fill:true},{arr:l5,color:'#10b981'},{arr:l15,color:'#f59e0b'}];
|
||||
// 面积先画 load1
|
||||
series.forEach(s=>{
|
||||
if(s.arr.length<2) return;
|
||||
ctx.beginPath(); ctx.lineWidth=1.5; ctx.strokeStyle=s.color;
|
||||
s.arr.forEach((v,i)=>{ const vClamped = Math.max(0, v); const x=padL+xStep*i; const y=padT+(H-padT-padB)*(1-(vClamped-min)/range); if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); });
|
||||
ctx.stroke();
|
||||
if(s.fill){
|
||||
const lastX = padL + xStep*(s.arr.length-1);
|
||||
ctx.lineTo(lastX,H-padB); ctx.lineTo(padL,H-padB); ctx.closePath();
|
||||
const grd = ctx.createLinearGradient(0,padT,0,H-padB); grd.addColorStop(0,'rgba(139,92,246,0.25)'); grd.addColorStop(1,'rgba(139,92,246,0)');
|
||||
ctx.fillStyle=grd; ctx.fill();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//# sourceMappingURL=app.js.map
|
||||
|
||||
// ====== 详情动态刷新 ======
|
||||
function findServerByKey(key){ return S.servers.find(x=> (x.name||x.location||'node')===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 memLineEl = document.getElementById('mem-line');
|
||||
if(memLineEl){
|
||||
const pct = s.memory_total? (s.memory_used/s.memory_total*100):0;
|
||||
document.getElementById('mem-used').textContent = s.memory_total!=null? humanMinMBFromKB(s.memory_used||0):'-';
|
||||
document.getElementById('mem-total').textContent = s.memory_total!=null? humanMinMBFromKB(s.memory_total):'-';
|
||||
if(pct>80) memLineEl.style.color='var(--danger)'; else memLineEl.style.color='';
|
||||
}
|
||||
const swapLineEl = document.getElementById('swap-line');
|
||||
if(swapLineEl){
|
||||
const pct = s.swap_total? (s.swap_used/s.swap_total*100):0;
|
||||
document.getElementById('swap-used').textContent = s.swap_total!=null? humanMinMBFromKB(s.swap_used||0):'-';
|
||||
document.getElementById('swap-total').textContent = s.swap_total!=null? humanMinMBFromKB(s.swap_total):'-';
|
||||
if(pct>80) swapLineEl.style.color='var(--danger)'; else swapLineEl.style.color='';
|
||||
}
|
||||
const diskLineEl = document.getElementById('disk-line');
|
||||
if(diskLineEl){
|
||||
const pct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0;
|
||||
document.getElementById('hdd-used').textContent = s.hdd_total!=null? humanMinMBFromMB(s.hdd_used||0):'-';
|
||||
document.getElementById('hdd-total').textContent = s.hdd_total!=null? humanMinMBFromMB(s.hdd_total):'-';
|
||||
if(pct>80) diskLineEl.style.color='var(--danger)'; else diskLineEl.style.color='';
|
||||
}
|
||||
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)':''; }
|
||||
}
|
||||
function startDetailAutoUpdate(){ stopDetailAutoUpdate(); S._detailTimer = setInterval(()=>{ if(S._openDetailKey) updateDetailMetrics(S._openDetailKey); }, 1000); }
|
||||
function stopDetailAutoUpdate(){ if(S._detailTimer){ clearInterval(S._detailTimer); S._detailTimer=null; } }
|
7
web/js/bootstrap.min.js
vendored
7
web/js/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
326
web/js/html5shiv.js
vendored
326
web/js/html5shiv.js
vendored
@ -1,326 +0,0 @@
|
||||
/**
|
||||
* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
|
||||
*/
|
||||
;(function(window, document) {
|
||||
/*jshint evil:true */
|
||||
/** version */
|
||||
var version = '3.7.3';
|
||||
|
||||
/** Preset options */
|
||||
var options = window.html5 || {};
|
||||
|
||||
/** Used to skip problem elements */
|
||||
var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i;
|
||||
|
||||
/** Not all elements can be cloned in IE **/
|
||||
var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i;
|
||||
|
||||
/** Detect whether the browser supports default html5 styles */
|
||||
var supportsHtml5Styles;
|
||||
|
||||
/** Name of the expando, to work with multiple documents or to re-shiv one document */
|
||||
var expando = '_html5shiv';
|
||||
|
||||
/** The id for the the documents expando */
|
||||
var expanID = 0;
|
||||
|
||||
/** Cached data for each document */
|
||||
var expandoData = {};
|
||||
|
||||
/** Detect whether the browser supports unknown elements */
|
||||
var supportsUnknownElements;
|
||||
|
||||
(function() {
|
||||
try {
|
||||
var a = document.createElement('a');
|
||||
a.innerHTML = '<xyz></xyz>';
|
||||
//if the hidden property is implemented we can assume, that the browser supports basic HTML5 Styles
|
||||
supportsHtml5Styles = ('hidden' in a);
|
||||
|
||||
supportsUnknownElements = a.childNodes.length == 1 || (function() {
|
||||
// assign a false positive if unable to shiv
|
||||
(document.createElement)('a');
|
||||
var frag = document.createDocumentFragment();
|
||||
return (
|
||||
typeof frag.cloneNode == 'undefined' ||
|
||||
typeof frag.createDocumentFragment == 'undefined' ||
|
||||
typeof frag.createElement == 'undefined'
|
||||
);
|
||||
}());
|
||||
} catch(e) {
|
||||
// assign a false positive if detection fails => unable to shiv
|
||||
supportsHtml5Styles = true;
|
||||
supportsUnknownElements = true;
|
||||
}
|
||||
|
||||
}());
|
||||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Creates a style sheet with the given CSS text and adds it to the document.
|
||||
* @private
|
||||
* @param {Document} ownerDocument The document.
|
||||
* @param {String} cssText The CSS text.
|
||||
* @returns {StyleSheet} The style element.
|
||||
*/
|
||||
function addStyleSheet(ownerDocument, cssText) {
|
||||
var p = ownerDocument.createElement('p'),
|
||||
parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement;
|
||||
|
||||
p.innerHTML = 'x<style>' + cssText + '</style>';
|
||||
return parent.insertBefore(p.lastChild, parent.firstChild);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of `html5.elements` as an array.
|
||||
* @private
|
||||
* @returns {Array} An array of shived element node names.
|
||||
*/
|
||||
function getElements() {
|
||||
var elements = html5.elements;
|
||||
return typeof elements == 'string' ? elements.split(' ') : elements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the built-in list of html5 elements
|
||||
* @memberOf html5
|
||||
* @param {String|Array} newElements whitespace separated list or array of new element names to shiv
|
||||
* @param {Document} ownerDocument The context document.
|
||||
*/
|
||||
function addElements(newElements, ownerDocument) {
|
||||
var elements = html5.elements;
|
||||
if(typeof elements != 'string'){
|
||||
elements = elements.join(' ');
|
||||
}
|
||||
if(typeof newElements != 'string'){
|
||||
newElements = newElements.join(' ');
|
||||
}
|
||||
html5.elements = elements +' '+ newElements;
|
||||
shivDocument(ownerDocument);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the data associated to the given document
|
||||
* @private
|
||||
* @param {Document} ownerDocument The document.
|
||||
* @returns {Object} An object of data.
|
||||
*/
|
||||
function getExpandoData(ownerDocument) {
|
||||
var data = expandoData[ownerDocument[expando]];
|
||||
if (!data) {
|
||||
data = {};
|
||||
expanID++;
|
||||
ownerDocument[expando] = expanID;
|
||||
expandoData[expanID] = data;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a shived element for the given nodeName and document
|
||||
* @memberOf html5
|
||||
* @param {String} nodeName name of the element
|
||||
* @param {Document|DocumentFragment} ownerDocument The context document.
|
||||
* @returns {Object} The shived element.
|
||||
*/
|
||||
function createElement(nodeName, ownerDocument, data){
|
||||
if (!ownerDocument) {
|
||||
ownerDocument = document;
|
||||
}
|
||||
if(supportsUnknownElements){
|
||||
return ownerDocument.createElement(nodeName);
|
||||
}
|
||||
if (!data) {
|
||||
data = getExpandoData(ownerDocument);
|
||||
}
|
||||
var node;
|
||||
|
||||
if (data.cache[nodeName]) {
|
||||
node = data.cache[nodeName].cloneNode();
|
||||
} else if (saveClones.test(nodeName)) {
|
||||
node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode();
|
||||
} else {
|
||||
node = data.createElem(nodeName);
|
||||
}
|
||||
|
||||
// Avoid adding some elements to fragments in IE < 9 because
|
||||
// * Attributes like `name` or `type` cannot be set/changed once an element
|
||||
// is inserted into a document/fragment
|
||||
// * Link elements with `src` attributes that are inaccessible, as with
|
||||
// a 403 response, will cause the tab/window to crash
|
||||
// * Script elements appended to fragments will execute when their `src`
|
||||
// or `text` property is set
|
||||
return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a shived DocumentFragment for the given document
|
||||
* @memberOf html5
|
||||
* @param {Document} ownerDocument The context document.
|
||||
* @returns {Object} The shived DocumentFragment.
|
||||
*/
|
||||
function createDocumentFragment(ownerDocument, data){
|
||||
if (!ownerDocument) {
|
||||
ownerDocument = document;
|
||||
}
|
||||
if(supportsUnknownElements){
|
||||
return ownerDocument.createDocumentFragment();
|
||||
}
|
||||
data = data || getExpandoData(ownerDocument);
|
||||
var clone = data.frag.cloneNode(),
|
||||
i = 0,
|
||||
elems = getElements(),
|
||||
l = elems.length;
|
||||
for(;i<l;i++){
|
||||
clone.createElement(elems[i]);
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shivs the `createElement` and `createDocumentFragment` methods of the document.
|
||||
* @private
|
||||
* @param {Document|DocumentFragment} ownerDocument The document.
|
||||
* @param {Object} data of the document.
|
||||
*/
|
||||
function shivMethods(ownerDocument, data) {
|
||||
if (!data.cache) {
|
||||
data.cache = {};
|
||||
data.createElem = ownerDocument.createElement;
|
||||
data.createFrag = ownerDocument.createDocumentFragment;
|
||||
data.frag = data.createFrag();
|
||||
}
|
||||
|
||||
|
||||
ownerDocument.createElement = function(nodeName) {
|
||||
//abort shiv
|
||||
if (!html5.shivMethods) {
|
||||
return data.createElem(nodeName);
|
||||
}
|
||||
return createElement(nodeName, ownerDocument, data);
|
||||
};
|
||||
|
||||
ownerDocument.createDocumentFragment = Function('h,f', 'return function(){' +
|
||||
'var n=f.cloneNode(),c=n.createElement;' +
|
||||
'h.shivMethods&&(' +
|
||||
// unroll the `createElement` calls
|
||||
getElements().join().replace(/[\w\-:]+/g, function(nodeName) {
|
||||
data.createElem(nodeName);
|
||||
data.frag.createElement(nodeName);
|
||||
return 'c("' + nodeName + '")';
|
||||
}) +
|
||||
');return n}'
|
||||
)(html5, data.frag);
|
||||
}
|
||||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Shivs the given document.
|
||||
* @memberOf html5
|
||||
* @param {Document} ownerDocument The document to shiv.
|
||||
* @returns {Document} The shived document.
|
||||
*/
|
||||
function shivDocument(ownerDocument) {
|
||||
if (!ownerDocument) {
|
||||
ownerDocument = document;
|
||||
}
|
||||
var data = getExpandoData(ownerDocument);
|
||||
|
||||
if (html5.shivCSS && !supportsHtml5Styles && !data.hasCSS) {
|
||||
data.hasCSS = !!addStyleSheet(ownerDocument,
|
||||
// corrects block display not defined in IE6/7/8/9
|
||||
'article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}' +
|
||||
// adds styling not present in IE6/7/8/9
|
||||
'mark{background:#FF0;color:#000}' +
|
||||
// hides non-rendered elements
|
||||
'template{display:none}'
|
||||
);
|
||||
}
|
||||
if (!supportsUnknownElements) {
|
||||
shivMethods(ownerDocument, data);
|
||||
}
|
||||
return ownerDocument;
|
||||
}
|
||||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* The `html5` object is exposed so that more elements can be shived and
|
||||
* existing shiving can be detected on iframes.
|
||||
* @type Object
|
||||
* @example
|
||||
*
|
||||
* // options can be changed before the script is included
|
||||
* html5 = { 'elements': 'mark section', 'shivCSS': false, 'shivMethods': false };
|
||||
*/
|
||||
var html5 = {
|
||||
|
||||
/**
|
||||
* An array or space separated string of node names of the elements to shiv.
|
||||
* @memberOf html5
|
||||
* @type Array|String
|
||||
*/
|
||||
'elements': options.elements || 'abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video',
|
||||
|
||||
/**
|
||||
* current version of html5shiv
|
||||
*/
|
||||
'version': version,
|
||||
|
||||
/**
|
||||
* A flag to indicate that the HTML5 style sheet should be inserted.
|
||||
* @memberOf html5
|
||||
* @type Boolean
|
||||
*/
|
||||
'shivCSS': (options.shivCSS !== false),
|
||||
|
||||
/**
|
||||
* Is equal to true if a browser supports creating unknown/HTML5 elements
|
||||
* @memberOf html5
|
||||
* @type boolean
|
||||
*/
|
||||
'supportsUnknownElements': supportsUnknownElements,
|
||||
|
||||
/**
|
||||
* A flag to indicate that the document's `createElement` and `createDocumentFragment`
|
||||
* methods should be overwritten.
|
||||
* @memberOf html5
|
||||
* @type Boolean
|
||||
*/
|
||||
'shivMethods': (options.shivMethods !== false),
|
||||
|
||||
/**
|
||||
* A string to describe the type of `html5` object ("default" or "default print").
|
||||
* @memberOf html5
|
||||
* @type String
|
||||
*/
|
||||
'type': 'default',
|
||||
|
||||
// shivs the document according to the specified `html5` object options
|
||||
'shivDocument': shivDocument,
|
||||
|
||||
//creates a shived element
|
||||
createElement: createElement,
|
||||
|
||||
//creates a shived documentFragment
|
||||
createDocumentFragment: createDocumentFragment,
|
||||
|
||||
//extends list of elements
|
||||
addElements: addElements
|
||||
};
|
||||
|
||||
/*--------------------------------------------------------------------------*/
|
||||
|
||||
// expose html5
|
||||
window.html5 = html5;
|
||||
|
||||
// shiv the document
|
||||
shivDocument(document);
|
||||
|
||||
if(typeof module == 'object' && module.exports){
|
||||
module.exports = html5;
|
||||
}
|
||||
|
||||
}(typeof window !== "undefined" ? window : this, document));
|
2
web/js/jquery.min.js
vendored
2
web/js/jquery.min.js
vendored
File diff suppressed because one or more lines are too long
5
web/js/respond.min.js
vendored
5
web/js/respond.min.js
vendored
@ -1,5 +0,0 @@
|
||||
/*! Respond.js v1.4.2: min/max-width media query polyfill * Copyright 2013 Scott Jehl
|
||||
* Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT
|
||||
* */
|
||||
|
||||
!function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='­<style media="'+a+'"> #mq-test-1 { width: 42px; }</style>',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;b<s.length;b++){var c=s[b],e=c.href,f=c.media,g=c.rel&&"stylesheet"===c.rel.toLowerCase();e&&g&&!o[e]&&(c.styleSheet&&c.styleSheet.rawCssText?(v(c.styleSheet.rawCssText,e,f),o[e]=!0):(!/^([a-zA-Z:]*\/\/)/.test(e)&&!r||e.replace(RegExp.$1,"").split("/")[0]===a.location.host)&&("//"===e.substring(0,2)&&(e=a.location.protocol+e),d.push({href:e,media:f})))}w()};x(),c.update=x,c.getEmValue=t,a.addEventListener?a.addEventListener("resize",b,!1):a.attachEvent&&a.attachEvent("onresize",b)}}(this);
|
@ -1,396 +0,0 @@
|
||||
// serverstatus.js. big data boom today.
|
||||
var error = 0;
|
||||
var d = 0;
|
||||
var server_status = new Array();
|
||||
|
||||
function timeSince(date) {
|
||||
if(date == 0)
|
||||
return "从未.";
|
||||
|
||||
var seconds = Math.floor((new Date() - date) / 1000);
|
||||
var interval = Math.floor(seconds / 60);
|
||||
if (interval > 1)
|
||||
return interval + " 分钟前.";
|
||||
else
|
||||
return "几秒前.";
|
||||
}
|
||||
|
||||
function bytesToSize(bytes, precision, si)
|
||||
{
|
||||
var ret;
|
||||
si = typeof si !== 'undefined' ? si : 0;
|
||||
if(si != 0) {
|
||||
var megabyte = 1000 * 1000;
|
||||
var gigabyte = megabyte * 1000;
|
||||
var terabyte = gigabyte * 1000;
|
||||
} else {
|
||||
var megabyte = 1024 * 1024;
|
||||
var gigabyte = megabyte * 1024;
|
||||
var terabyte = gigabyte * 1024;
|
||||
}
|
||||
|
||||
if ((bytes >= megabyte) && (bytes < gigabyte)) {
|
||||
ret = (bytes / megabyte).toFixed(precision) + ' M';
|
||||
|
||||
} else if ((bytes >= gigabyte) && (bytes < terabyte)) {
|
||||
ret = (bytes / gigabyte).toFixed(precision) + ' G';
|
||||
|
||||
} else if (bytes >= terabyte) {
|
||||
ret = (bytes / terabyte).toFixed(precision) + ' T';
|
||||
|
||||
} else {
|
||||
return bytes + ' B';
|
||||
}
|
||||
return ret;
|
||||
/*if(si != 0) {
|
||||
return ret + 'B';
|
||||
} else {
|
||||
return ret + 'iB';
|
||||
}*/
|
||||
}
|
||||
|
||||
function uptime() {
|
||||
$.getJSON("json/stats.json", function(result) {
|
||||
$("#loading-notice").remove();
|
||||
if(result.reload)
|
||||
setTimeout(function() { location.reload() }, 1000);
|
||||
|
||||
for (var i = 0, rlen=result.servers.length; i < rlen; i++) {
|
||||
var TableRow = $("#servers tr#r" + i);
|
||||
var ExpandRow = $("#servers #rt" + i);
|
||||
var hack; // fuck CSS for making me do this
|
||||
if(i%2) hack="odd"; else hack="even";
|
||||
if (!TableRow.length) {
|
||||
$("#servers").append(
|
||||
"<tr id=\"r" + i + "\" data-toggle=\"collapse\" data-target=\"#rt" + i + "\" class=\"accordion-toggle " + hack + "\">" +
|
||||
"<td id=\"online_status\"><div class=\"progress\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
|
||||
"<td id=\"month_traffic\"><div class=\"progress\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
|
||||
"<td id=\"name\">加载中</td>" +
|
||||
"<td id=\"type\">加载中</td>" +
|
||||
"<td id=\"location\">加载中</td>" +
|
||||
"<td id=\"uptime\">加载中</td>" +
|
||||
"<td id=\"load\">加载中</td>" +
|
||||
"<td id=\"network\">加载中</td>" +
|
||||
"<td id=\"traffic\">加载中</td>" +
|
||||
"<td id=\"cpu\"><div class=\"progress\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
|
||||
"<td id=\"memory\"><div class=\"progress\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
|
||||
"<td id=\"hdd\"><div class=\"progress\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
|
||||
"<td id=\"ping\"><div class=\"progress\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
|
||||
"</tr>" +
|
||||
"<tr class=\"expandRow " + hack + "\"><td colspan=\"16\"><div class=\"accordian-body collapse\" id=\"rt" + i + "\">" +
|
||||
"<div id=\"expand_mem\">加载中</div>" +
|
||||
"<div id=\"expand_swap\">加载中</div>" +
|
||||
"<div id=\"expand_hdd\">加载中</div>" +
|
||||
"<div id=\"expand_tupd\">加载中</div>" +
|
||||
"<div id=\"expand_ping\">加载中</div>" +
|
||||
"<div id=\"expand_custom\">加载中</div>" +
|
||||
"</div></td></tr>"
|
||||
);
|
||||
TableRow = $("#servers tr#r" + i);
|
||||
ExpandRow = $("#servers #rt" + i);
|
||||
server_status[i] = true;
|
||||
}
|
||||
TableRow = TableRow[0];
|
||||
if(error) {
|
||||
TableRow.setAttribute("data-target", "#rt" + i);
|
||||
server_status[i] = true;
|
||||
}
|
||||
|
||||
// online_status
|
||||
if (result.servers[i].online4 && !result.servers[i].online6) {
|
||||
TableRow.children["online_status"].children[0].children[0].className = "progress-bar progress-bar-success";
|
||||
TableRow.children["online_status"].children[0].children[0].innerHTML = "<small>IPv4</small>";
|
||||
} else if (result.servers[i].online4 && result.servers[i].online6) {
|
||||
TableRow.children["online_status"].children[0].children[0].className = "progress-bar progress-bar-success";
|
||||
TableRow.children["online_status"].children[0].children[0].innerHTML = "<small>双栈</small>";
|
||||
} else if (!result.servers[i].online4 && result.servers[i].online6) {
|
||||
TableRow.children["online_status"].children[0].children[0].className = "progress-bar progress-bar-success";
|
||||
TableRow.children["online_status"].children[0].children[0].innerHTML = "<small>IPv6</small>";
|
||||
} else {
|
||||
TableRow.children["online_status"].children[0].children[0].className = "progress-bar progress-bar-danger";
|
||||
TableRow.children["online_status"].children[0].children[0].innerHTML = "<small>关闭</small>";
|
||||
}
|
||||
|
||||
// Name
|
||||
TableRow.children["name"].innerHTML = result.servers[i].name;
|
||||
|
||||
// Type
|
||||
TableRow.children["type"].innerHTML = result.servers[i].type;
|
||||
|
||||
// Location
|
||||
TableRow.children["location"].innerHTML = result.servers[i].location;
|
||||
if (!result.servers[i].online4 && !result.servers[i].online6) {
|
||||
if (server_status[i]) {
|
||||
TableRow.children["uptime"].innerHTML = "–";
|
||||
TableRow.children["load"].innerHTML = "–";
|
||||
TableRow.children["network"].innerHTML = "–";
|
||||
TableRow.children["traffic"].innerHTML = "–";
|
||||
TableRow.children["month_traffic"].children[0].children[0].className = "progress-bar progress-bar-warning";
|
||||
TableRow.children["month_traffic"].children[0].children[0].innerHTML = "<small>关闭</small>";
|
||||
TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-danger";
|
||||
TableRow.children["cpu"].children[0].children[0].style.width = "100%";
|
||||
TableRow.children["cpu"].children[0].children[0].innerHTML = "<small>关闭</small>";
|
||||
TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-danger";
|
||||
TableRow.children["memory"].children[0].children[0].style.width = "100%";
|
||||
TableRow.children["memory"].children[0].children[0].innerHTML = "<small>关闭</small>";
|
||||
TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-danger";
|
||||
TableRow.children["hdd"].children[0].children[0].style.width = "100%";
|
||||
TableRow.children["hdd"].children[0].children[0].innerHTML = "<small>关闭</small>";
|
||||
TableRow.children["ping"].children[0].children[0].className = "progress-bar progress-bar-danger";
|
||||
TableRow.children["ping"].children[0].children[0].style.width = "100%";
|
||||
TableRow.children["ping"].children[0].children[0].innerHTML = "<small>关闭</small>";
|
||||
if(ExpandRow.hasClass("in")) {
|
||||
ExpandRow.collapse("hide");
|
||||
}
|
||||
TableRow.setAttribute("data-target", "");
|
||||
server_status[i] = false;
|
||||
}
|
||||
} else {
|
||||
if (!server_status[i]) {
|
||||
TableRow.setAttribute("data-target", "#rt" + i);
|
||||
server_status[i] = true;
|
||||
}
|
||||
|
||||
// month traffic
|
||||
var monthtraffic = "";
|
||||
var trafficdiff_in = result.servers[i].network_in - result.servers[i].last_network_in;
|
||||
var trafficdiff_out = result.servers[i].network_out - result.servers[i].last_network_out;
|
||||
if(trafficdiff_in < 1024*1024*1024*1024)
|
||||
monthtraffic += (trafficdiff_in/1024/1024/1024).toFixed(1) + "G";
|
||||
else
|
||||
monthtraffic += (trafficdiff_in/1024/1024/1024/1024).toFixed(1) + "T";
|
||||
monthtraffic += " | "
|
||||
if(trafficdiff_out < 1024*1024*1024*1024)
|
||||
monthtraffic += (trafficdiff_out/1024/1024/1024).toFixed(1) + "G";
|
||||
else
|
||||
monthtraffic += (trafficdiff_out/1024/1024/1024/1024).toFixed(1) + "T";
|
||||
TableRow.children["month_traffic"].children[0].children[0].className = "progress-bar progress-bar-success";
|
||||
TableRow.children["month_traffic"].children[0].children[0].innerHTML = "<small>"+monthtraffic+"</small>";
|
||||
|
||||
// Uptime
|
||||
TableRow.children["uptime"].innerHTML = result.servers[i].uptime;
|
||||
|
||||
// Load: default load_1, you can change show: load_1, load_5, load_15
|
||||
if(result.servers[i].load == -1) {
|
||||
TableRow.children["load"].innerHTML = "–";
|
||||
} else {
|
||||
TableRow.children["load"].innerHTML = result.servers[i].load_1.toFixed(2);
|
||||
}
|
||||
|
||||
// Network
|
||||
var netstr = "";
|
||||
if(result.servers[i].network_rx < 1024*1024)
|
||||
netstr += (result.servers[i].network_rx/1024).toFixed(1) + "K";
|
||||
else
|
||||
netstr += (result.servers[i].network_rx/1024/1024).toFixed(1) + "M";
|
||||
netstr += " | "
|
||||
if(result.servers[i].network_tx < 1024*1024)
|
||||
netstr += (result.servers[i].network_tx/1024).toFixed(1) + "K";
|
||||
else
|
||||
netstr += (result.servers[i].network_tx/1024/1024).toFixed(1) + "M";
|
||||
TableRow.children["network"].innerHTML = netstr;
|
||||
|
||||
//Traffic
|
||||
var trafficstr = "";
|
||||
if(result.servers[i].network_in < 1024*1024*1024*1024)
|
||||
trafficstr += (result.servers[i].network_in/1024/1024/1024).toFixed(1) + "G";
|
||||
else
|
||||
trafficstr += (result.servers[i].network_in/1024/1024/1024/1024).toFixed(1) + "T";
|
||||
trafficstr += " | "
|
||||
if(result.servers[i].network_out < 1024*1024*1024*1024)
|
||||
trafficstr += (result.servers[i].network_out/1024/1024/1024).toFixed(1) + "G";
|
||||
else
|
||||
trafficstr += (result.servers[i].network_out/1024/1024/1024/1024).toFixed(1) + "T";
|
||||
TableRow.children["traffic"].innerHTML = trafficstr;
|
||||
|
||||
// CPU
|
||||
if (result.servers[i].cpu >= 90)
|
||||
TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-danger";
|
||||
else if (result.servers[i].cpu >= 80)
|
||||
TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-warning";
|
||||
else
|
||||
TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-success";
|
||||
TableRow.children["cpu"].children[0].children[0].style.width = result.servers[i].cpu + "%";
|
||||
TableRow.children["cpu"].children[0].children[0].innerHTML = result.servers[i].cpu + "%";
|
||||
|
||||
// Memory
|
||||
var Mem = ((result.servers[i].memory_used/result.servers[i].memory_total)*100.0).toFixed(0);
|
||||
if (Mem >= 90)
|
||||
TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-danger";
|
||||
else if (Mem >= 80)
|
||||
TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-warning";
|
||||
else
|
||||
TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-success";
|
||||
TableRow.children["memory"].children[0].children[0].style.width = Mem + "%";
|
||||
TableRow.children["memory"].children[0].children[0].innerHTML = Mem + "%";
|
||||
ExpandRow[0].children["expand_mem"].innerHTML = "内存: " + bytesToSize(result.servers[i].memory_used*1024, 2) + " / " + bytesToSize(result.servers[i].memory_total*1024, 2);
|
||||
// Swap
|
||||
ExpandRow[0].children["expand_swap"].innerHTML = "交换分区: " + bytesToSize(result.servers[i].swap_used*1024, 2) + " / " + bytesToSize(result.servers[i].swap_total*1024, 2);
|
||||
|
||||
// HDD
|
||||
var HDD = ((result.servers[i].hdd_used/result.servers[i].hdd_total)*100.0).toFixed(0);
|
||||
if (HDD >= 90)
|
||||
TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-danger";
|
||||
else if (HDD >= 80)
|
||||
TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-warning";
|
||||
else
|
||||
TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-success";
|
||||
TableRow.children["hdd"].children[0].children[0].style.width = HDD + "%";
|
||||
TableRow.children["hdd"].children[0].children[0].innerHTML = HDD + "%";
|
||||
// IO Speed for HDD.
|
||||
// IO, 过小的B字节单位没有意义
|
||||
var io = "";
|
||||
if(result.servers[i].io_read < 1024*1024)
|
||||
io += parseInt(result.servers[i].io_read/1024) + "K";
|
||||
else
|
||||
io += parseInt(result.servers[i].io_read/1024/1024) + "M";
|
||||
io += " / "
|
||||
if(result.servers[i].io_write < 1024*1024)
|
||||
io += parseInt(result.servers[i].io_write/1024) + "K";
|
||||
else
|
||||
io += parseInt(result.servers[i].io_write/1024/1024) + "M";
|
||||
// Expand for HDD.
|
||||
ExpandRow[0].children["expand_hdd"].innerHTML = "硬盘|读写: " + bytesToSize(result.servers[i].hdd_used*1024*1024, 2) + " / " + bytesToSize(result.servers[i].hdd_total*1024*1024, 2) + " | " + io;
|
||||
|
||||
// delay time
|
||||
|
||||
// tcp, udp, process, thread count
|
||||
ExpandRow[0].children["expand_tupd"].innerHTML = "TCP/UDP/进/线: " + result.servers[i].tcp_count + " / " + result.servers[i].udp_count + " / " + result.servers[i].process_count+ " / " + result.servers[i].thread_count;
|
||||
ExpandRow[0].children["expand_ping"].innerHTML = "联通/电信/移动: " + result.servers[i].time_10010 + "ms / " + result.servers[i].time_189 + "ms / " + result.servers[i].time_10086 + "ms"
|
||||
|
||||
// ping
|
||||
var PING_10010 = result.servers[i].ping_10010.toFixed(0);
|
||||
var PING_189 = result.servers[i].ping_189.toFixed(0);
|
||||
var PING_10086 = result.servers[i].ping_10086.toFixed(0);
|
||||
if (PING_10010 >= 20 || PING_189 >= 20 || PING_10086 >= 20)
|
||||
TableRow.children["ping"].children[0].children[0].className = "progress-bar progress-bar-warning";
|
||||
else
|
||||
TableRow.children["ping"].children[0].children[0].className = "progress-bar progress-bar-success";
|
||||
TableRow.children["ping"].children[0].children[0].innerHTML = PING_10010 + "%💻" + PING_189 + "%💻" + PING_10086 + "%";
|
||||
|
||||
// Custom
|
||||
if (result.servers[i].custom) {
|
||||
ExpandRow[0].children["expand_custom"].innerHTML = result.servers[i].custom
|
||||
} else {
|
||||
ExpandRow[0].children["expand_custom"].innerHTML = ""
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
d = new Date(result.updated*1000);
|
||||
error = 0;
|
||||
}).fail(function(update_error) {
|
||||
if (!error) {
|
||||
$("#servers > tr.accordion-toggle").each(function(i) {
|
||||
var TableRow = $("#servers tr#r" + i)[0];
|
||||
var ExpandRow = $("#servers #rt" + i);
|
||||
TableRow.children["online_status"].children[0].children[0].className = "progress-bar progress-bar-error";
|
||||
TableRow.children["online_status"].children[0].children[0].innerHTML = "<small>错误</small>";
|
||||
TableRow.children["month_traffic"].children[0].children[0].className = "progress-bar progress-bar-error";
|
||||
TableRow.children["month_traffic"].children[0].children[0].innerHTML = "<small>错误</small>";
|
||||
TableRow.children["uptime"].children[0].children[0].className = "progress-bar progress-bar-error";
|
||||
TableRow.children["uptime"].children[0].children[0].innerHTML = "<small>错误</small>";
|
||||
TableRow.children["load"].children[0].children[0].className = "progress-bar progress-bar-error";
|
||||
TableRow.children["load"].children[0].children[0].innerHTML = "<small>错误</small>";
|
||||
TableRow.children["network"].children[0].children[0].className = "progress-bar progress-bar-error";
|
||||
TableRow.children["network"].children[0].children[0].innerHTML = "<small>错误</small>";
|
||||
TableRow.children["traffic"].children[0].children[0].className = "progress-bar progress-bar-error";
|
||||
TableRow.children["traffic"].children[0].children[0].innerHTML = "<small>错误</small>";
|
||||
TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-error";
|
||||
TableRow.children["cpu"].children[0].children[0].style.width = "100%";
|
||||
TableRow.children["cpu"].children[0].children[0].innerHTML = "<small>错误</small>";
|
||||
TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-error";
|
||||
TableRow.children["memory"].children[0].children[0].style.width = "100%";
|
||||
TableRow.children["memory"].children[0].children[0].innerHTML = "<small>错误</small>";
|
||||
TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-error";
|
||||
TableRow.children["hdd"].children[0].children[0].style.width = "100%";
|
||||
TableRow.children["hdd"].children[0].children[0].innerHTML = "<small>错误</small>";
|
||||
TableRow.children["ping"].children[0].children[0].className = "progress-bar progress-bar-error";
|
||||
TableRow.children["ping"].children[0].children[0].style.width = "100%";
|
||||
TableRow.children["ping"].children[0].children[0].innerHTML = "<small>错误</small>";
|
||||
if(ExpandRow.hasClass("in")) {
|
||||
ExpandRow.collapse("hide");
|
||||
}
|
||||
TableRow.setAttribute("data-target", "");
|
||||
server_status[i] = false;
|
||||
});
|
||||
}
|
||||
error = 1;
|
||||
$("#updated").html("更新错误.");
|
||||
});
|
||||
}
|
||||
|
||||
function updateTime() {
|
||||
if (!error)
|
||||
$("#updated").html("最后更新: " + timeSince(d));
|
||||
}
|
||||
|
||||
uptime();
|
||||
updateTime();
|
||||
setInterval(uptime, 2000);
|
||||
setInterval(updateTime, 2000);
|
||||
|
||||
|
||||
// styleswitcher.js
|
||||
function setActiveStyleSheet(title, cookie=false) {
|
||||
var i, a, main;
|
||||
for(i=0; (a = document.getElementsByTagName("link")[i]); i++) {
|
||||
if(a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title")) {
|
||||
a.disabled = true;
|
||||
if(a.getAttribute("title") == title) a.disabled = false;
|
||||
}
|
||||
}
|
||||
if (true==cookie) {
|
||||
createCookie("style", title, 365);
|
||||
}
|
||||
}
|
||||
|
||||
function getActiveStyleSheet() {
|
||||
var i, a;
|
||||
for(i=0; (a = document.getElementsByTagName("link")[i]); i++) {
|
||||
if(a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title") && !a.disabled)
|
||||
return a.getAttribute("title");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function createCookie(name,value,days) {
|
||||
if (days) {
|
||||
var date = new Date();
|
||||
date.setTime(date.getTime()+(days*24*60*60*1000));
|
||||
var expires = "; expires="+date.toGMTString();
|
||||
}
|
||||
else expires = "";
|
||||
document.cookie = name+"="+value+expires+"; path=/";
|
||||
}
|
||||
|
||||
function readCookie(name) {
|
||||
var nameEQ = name + "=";
|
||||
var ca = document.cookie.split(';');
|
||||
for(var i=0;i < ca.length;i++) {
|
||||
var c = ca[i];
|
||||
while (c.charAt(0)==' ')
|
||||
c = c.substring(1,c.length);
|
||||
if (c.indexOf(nameEQ) == 0)
|
||||
return c.substring(nameEQ.length,c.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
window.onload = function(e) {
|
||||
var cookie = readCookie("style");
|
||||
if (cookie && cookie != 'null' ) {
|
||||
setActiveStyleSheet(cookie);
|
||||
} else {
|
||||
function handleChange (mediaQueryListEvent) {
|
||||
if (mediaQueryListEvent.matches) {
|
||||
setActiveStyleSheet('dark');
|
||||
} else {
|
||||
setActiveStyleSheet('light');
|
||||
}
|
||||
}
|
||||
const mediaQueryListDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
setActiveStyleSheet(mediaQueryListDark.matches ? 'dark' : 'light');
|
||||
mediaQueryListDark.addEventListener("change",handleChange);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user