@yixinshuke/node-dashboard 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +29 -6
- package/src/assets/index.html +309 -0
- package/src/assets/js/agg.js +99 -0
- package/src/assets/js/common.js +594 -0
- package/src/assets/js/instances.js +152 -0
- package/src/assets/js/push.js +178 -0
- package/src/config.ts +27 -0
- package/src/dashboard.ts +171 -0
- package/src/http-logger.ts +65 -0
- package/src/index.ts +10 -0
- package/src/panel-token-inject.ts +53 -0
- package/src/qpm-hist.ts +208 -0
- package/src/shardorm-logger.ts +107 -0
- package/src/sql-trace.ts +291 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@yixinshuke/node-dashboard",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@yixinshuke/node-dashboard",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "服务器监控大屏:指标采集(QPM/SQL 透出/HTTP 日志)+ 聚合 API + 前端 index.html",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.ts",
|
|
7
|
+
"types": "src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.ts",
|
|
10
|
+
"./assets/index.html": "./src/assets/index.html"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
18
|
+
"test": "node --test --import tsx test/*.test.ts",
|
|
19
|
+
"demo": "node --import tsx demo/run.mjs",
|
|
20
|
+
"pub": "npm publish --access public",
|
|
21
|
+
"pub:dry": "npm publish --access public --dry-run"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@yixinshuke/node-http-client": "^0.1.3",
|
|
25
|
+
"@yixinshuke/node-log": "^0.0.2",
|
|
26
|
+
"@yixinshuke/node-shardorm": "^0.0.1"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT"
|
|
29
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>__SYSTEM_NAME__ - 服务状态大屏</title>
|
|
7
|
+
<link rel="icon" href="data:," />
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0f1420; --panel: #1a2030; --panel2: #222a3d; --line: #2c3550;
|
|
12
|
+
--text: #e6ebf5; --muted: #8b97b3; --ok: #36d399; --warn: #fbbd23;
|
|
13
|
+
--down: #f87272; --unknown: #7c8db5; --accent: #5b9dff;
|
|
14
|
+
}
|
|
15
|
+
* { box-sizing: border-box; }
|
|
16
|
+
body { margin: 0; background: var(--bg); color: var(--text); font-family: -apple-system, "Segoe UI", "Microsoft YaHei", sans-serif; }
|
|
17
|
+
header { padding: 16px 24px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 16px; flex-wrap: wrap; }
|
|
18
|
+
header h1 { font-size: 18px; margin: 0; font-weight: 600; }
|
|
19
|
+
.badge { padding: 2px 10px; border-radius: 999px; font-size: 12px; background: var(--panel2); color: var(--muted); }
|
|
20
|
+
.env-badge { padding: 1px 9px; border-radius: 6px; font-size: 12px; font-weight: 600; }
|
|
21
|
+
.env-badge.prod { background: rgba(248,114,114,.16); color: var(--down); }
|
|
22
|
+
.env-badge.test { background: rgba(251,189,35,.16); color: var(--warn); }
|
|
23
|
+
.env-badge.dev { background: rgba(91,157,255,.16); color: var(--accent); }
|
|
24
|
+
.env-badge.unknown { background: var(--panel2); color: var(--muted); }
|
|
25
|
+
/* 连体切换器(segmented control):两组通用 */
|
|
26
|
+
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--panel2); }
|
|
27
|
+
.seg button { background: transparent; color: var(--muted); border: none; border-right: 1px solid var(--line); padding: 6px 14px; cursor: pointer; font-size: 13px; }
|
|
28
|
+
.seg button:last-child { border-right: none; }
|
|
29
|
+
.seg button.active { background: var(--accent); color: #fff; }
|
|
30
|
+
.seg#filters { margin-left: auto; }
|
|
31
|
+
.seg button[data-env="prod"] { color: var(--down); }
|
|
32
|
+
.seg button[data-env="prod"].active { background: var(--down); color: #fff; }
|
|
33
|
+
.seg button[data-env="test"] { color: var(--warn); }
|
|
34
|
+
.seg button[data-env="test"].active { background: var(--warn); color: #1a2030; }
|
|
35
|
+
.seg button[data-env="dev"] { color: var(--accent); }
|
|
36
|
+
.seg button[data-env="dev"].active { background: var(--accent); color: #fff; }
|
|
37
|
+
.refresh-ctl { display: flex; gap: 10px; align-items: center; color: var(--muted); font-size: 13px; }
|
|
38
|
+
.refresh-ctl button { background: var(--panel2); color: var(--text); border: 1px solid var(--line); padding: 6px 14px; border-radius: 8px; cursor: pointer; font-size: 13px; }
|
|
39
|
+
.refresh-ctl button:disabled { opacity: .6; cursor: default; }
|
|
40
|
+
.refresh-ctl button.loading::before {
|
|
41
|
+
content: "";
|
|
42
|
+
display: inline-block;
|
|
43
|
+
width: 11px; height: 11px;
|
|
44
|
+
border: 2px solid var(--muted);
|
|
45
|
+
border-top-color: var(--text);
|
|
46
|
+
border-radius: 50%;
|
|
47
|
+
margin-right: 6px;
|
|
48
|
+
vertical-align: -2px;
|
|
49
|
+
animation: spin .7s linear infinite;
|
|
50
|
+
}
|
|
51
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
52
|
+
.summary b { color: var(--text); }
|
|
53
|
+
.grid { padding: 16px 24px; display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 16px; }
|
|
54
|
+
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; overflow: hidden; }
|
|
55
|
+
.card.err { border-color: var(--down); }
|
|
56
|
+
.card.self { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(91,157,255,.35), 0 0 16px rgba(91,157,255,.22); }
|
|
57
|
+
.self-badge { background: var(--accent); color: #fff; font-size: 11px; padding: 1px 8px; border-radius: 6px; font-weight: 600; }
|
|
58
|
+
.card .chead { padding: 12px 16px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 10px; }
|
|
59
|
+
.card .chead .title { font-weight: 600; }
|
|
60
|
+
.card .chead .env { margin-left: auto; font-size: 12px; color: var(--muted); }
|
|
61
|
+
.card .cbody { padding: 14px 16px; display: grid; gap: 14px; }
|
|
62
|
+
/* 卡体上下两部分:上=主机信息/CPU内存/磁盘 + 组件监控(宽屏两列),下=统计+折线卡片横排换行 */
|
|
63
|
+
.ctop { display: grid; gap: 14px; align-content: start; min-width: 0; }
|
|
64
|
+
.ctop > div { display: grid; gap: 14px; align-content: start; min-width: 0; }
|
|
65
|
+
.cstats { display: flex; flex-wrap: wrap; gap: 14px; }
|
|
66
|
+
.cstats > .router { flex: 1 1 360px; min-width: 340px; margin: 0; }
|
|
67
|
+
.cstats .spark-chart { height: 120px; }
|
|
68
|
+
.cbody.full { grid-template-columns: 1fr; }
|
|
69
|
+
@media (min-width: 1100px) {
|
|
70
|
+
.grid { grid-template-columns: repeat(auto-fill, minmax(660px, 1fr)); }
|
|
71
|
+
/* 宽屏:上半部分左右两列(左=主机信息/CPU内存/磁盘,右=组件健康/推送中心),窄屏回落上下单列 */
|
|
72
|
+
.ctop { grid-template-columns: 1fr 1fr; }
|
|
73
|
+
}
|
|
74
|
+
.kv { display: grid; grid-template-columns: 1fr 1fr; gap: 5px 12px; font-size: 12px; }
|
|
75
|
+
.kv .item { display: flex; gap: 6px; align-items: baseline; }
|
|
76
|
+
.kv .k { color: var(--muted); white-space: nowrap; flex: 0 0 auto; }
|
|
77
|
+
.kv .v { color: var(--text); word-break: break-all; }
|
|
78
|
+
.bars { display: grid; gap: 10px; }
|
|
79
|
+
.bar { }
|
|
80
|
+
.bar .lab { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
|
81
|
+
.track { height: 8px; background: var(--panel2); border-radius: 6px; overflow: hidden; }
|
|
82
|
+
.fill { height: 100%; border-radius: 6px; transition: width .4s; }
|
|
83
|
+
.rings { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
|
|
84
|
+
.ring-wrap { display: flex; flex-direction: column; align-items: center; gap: 4px; background: var(--panel2); border-radius: 12px; padding: 12px 8px; }
|
|
85
|
+
.ring { width: 104px; height: 104px; }
|
|
86
|
+
.ring .track { fill: none; stroke: var(--line); stroke-width: 9; }
|
|
87
|
+
.ring .prog { fill: none; stroke-width: 9; stroke-linecap: round; transform: rotate(-90deg); transform-origin: 50px 50px; transition: stroke-dashoffset .4s; }
|
|
88
|
+
.ring-pct { font-size: 17px; font-weight: 700; fill: var(--text); text-anchor: middle; dominant-baseline: central; }
|
|
89
|
+
.ring-label { font-size: 13px; color: var(--text); }
|
|
90
|
+
.ring-sub { font-size: 11px; color: var(--muted); text-align: center; line-height: 1.45; }
|
|
91
|
+
.disk { background: var(--panel2); border-radius: 10px; padding: 8px 10px; }
|
|
92
|
+
.disk-head { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); margin-bottom: 4px; }
|
|
93
|
+
.disk-meta { font-size: 11px; color: var(--muted); margin-top: 4px; }
|
|
94
|
+
.router { background: var(--panel2); border-radius: 10px; padding: 10px 12px; }
|
|
95
|
+
.router-head { font-size: 13px; color: var(--text); margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
|
|
96
|
+
.router-head .tag { font-size: 11px; color: var(--muted); background: var(--panel); padding: 1px 8px; border-radius: 6px; }
|
|
97
|
+
.router-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px 12px; }
|
|
98
|
+
.router-grid .cell { display: flex; flex-direction: column; }
|
|
99
|
+
.router-grid .cell .lab { font-size: 11px; color: var(--muted); }
|
|
100
|
+
.router-grid .cell .val { font-size: 15px; font-weight: 700; color: var(--text); }
|
|
101
|
+
.router-spark { margin-top: 8px; }
|
|
102
|
+
.router-spark svg { width: 100%; height: 36px; display: block; }
|
|
103
|
+
.spark-chart { width: 100%; height: 88px; }
|
|
104
|
+
.api-chart { width: 100%; height: 120px; }
|
|
105
|
+
/* 聚合总览卡片统一高度:统计数字区/折线区固定高度(分服务器卡片不受影响),
|
|
106
|
+
统计区按最多的 2 行 3 列(72px)对齐,折线区统一 120px,避免图例换行导致参差 */
|
|
107
|
+
#aggView .router-grid, .cstats .router-grid { height: 96px; align-content: start; overflow: hidden; }
|
|
108
|
+
#aggView .spark-chart, #aggView .api-chart { height: 120px; }
|
|
109
|
+
.comps { display: grid; gap: 8px; }
|
|
110
|
+
.comp-head { font-size: 12px; color: var(--muted); padding-left: 2px; }
|
|
111
|
+
/* 缓存/缓冲后端徽标:redis 高亮蓝色、内存绿色、未启用/无锁灰色 */
|
|
112
|
+
.backend-badge { margin-left: auto; font-size: 11px; font-weight: 600; padding: 2px 10px; border-radius: 999px; }
|
|
113
|
+
.backend-badge.redis { background: rgba(91,157,255,.16); color: var(--accent); }
|
|
114
|
+
.backend-badge.memory { background: rgba(54,211,153,.14); color: var(--ok); }
|
|
115
|
+
.backend-badge.off, .backend-badge.none, .backend-badge.self { background: var(--panel); color: var(--muted); }
|
|
116
|
+
/* 模块为主的健康分组 */
|
|
117
|
+
.comps.mods { gap: 6px; }
|
|
118
|
+
.mod-group { display: grid; gap: 6px; }
|
|
119
|
+
.mod-group.root { margin-top: 2px; }
|
|
120
|
+
.comp.sub { background: transparent; padding: 6px 10px 6px 8px; border-left: 2px solid var(--line); margin-left: 4px; }
|
|
121
|
+
.mod-attrs { margin-left: auto; display: flex; gap: 6px; align-items: center; }
|
|
122
|
+
.mod-attrs .backend-badge { margin-left: 0; }
|
|
123
|
+
.mod-attrs .state { margin-left: 4px; }
|
|
124
|
+
.comp { display: flex; align-items: center; gap: 10px; font-size: 13px; background: var(--panel2); padding: 8px 10px; border-radius: 8px; }
|
|
125
|
+
.comp .name { width: 96px; color: var(--muted); }
|
|
126
|
+
.comp .state { margin-left: auto; font-size: 12px; }
|
|
127
|
+
.dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; }
|
|
128
|
+
.dot.ok { background: var(--ok); } .dot.warn { background: var(--warn); } .dot.down { background: var(--down); }
|
|
129
|
+
.dot.unknown { background: var(--unknown); } .dot.skipped { background: var(--unknown); }
|
|
130
|
+
.empty { padding: 60px; text-align: center; color: var(--muted); }
|
|
131
|
+
.kpi { padding: 14px 24px; display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 14px; border-bottom: 1px solid var(--line); background: var(--panel); }
|
|
132
|
+
.kpi .stat { background: var(--panel2); border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; }
|
|
133
|
+
.kpi .stat .lab { font-size: 12px; color: var(--muted); }
|
|
134
|
+
.kpi .stat .val { font-size: 26px; font-weight: 800; margin-top: 4px; }
|
|
135
|
+
.kpi .stat .sub { font-size: 11px; color: var(--muted); margin-top: 2px; }
|
|
136
|
+
.kpi .rows { margin-top: 8px; display: grid; gap: 4px; }
|
|
137
|
+
.kpi .krow { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--muted); }
|
|
138
|
+
.kpi .krow span:last-child { color: var(--text); font-weight: 600; text-align: right; }
|
|
139
|
+
.tabs { padding: 10px 24px 0; display: flex; gap: 8px; }
|
|
140
|
+
.tabs button { background: var(--panel2); color: var(--muted); border: 1px solid var(--line); padding: 7px 16px; border-radius: 8px; cursor: pointer; font-size: 13px; }
|
|
141
|
+
.tabs button.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
142
|
+
.agg { padding: 16px 24px; display: grid; grid-template-columns: repeat(auto-fill, minmax(390px, 1fr)); gap: 16px; }
|
|
143
|
+
.agg .panel { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 16px; }
|
|
144
|
+
.agg .panel.empty { opacity: .65; }
|
|
145
|
+
.agg .panel .ph { font-size: 14px; font-weight: 600; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
|
|
146
|
+
.agg .panel .big { font-size: 34px; font-weight: 800; color: var(--accent); }
|
|
147
|
+
.agg .panel.empty .big { color: var(--muted); }
|
|
148
|
+
.agg .panel .row { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); margin-top: 6px; }
|
|
149
|
+
.hidden { display: none !important; }
|
|
150
|
+
.cmd-table { width:100%; border-collapse:collapse; font-size:12px; }
|
|
151
|
+
.cmd-table th, .cmd-table td { text-align:right; padding:6px 8px; border-bottom:1px solid var(--line); color:var(--text); }
|
|
152
|
+
.cmd-table th:first-child, .cmd-table td:first-child { text-align:left; color:var(--muted); }
|
|
153
|
+
.cmd-table tbody tr:hover { background:var(--panel2); }
|
|
154
|
+
|
|
155
|
+
/* 推送测试弹窗 */
|
|
156
|
+
.modal-mask { position: fixed; inset: 0; background: rgba(6,10,18,.66); display: none; align-items: center; justify-content: center; z-index: 50; }
|
|
157
|
+
.modal-mask.open { display: flex; }
|
|
158
|
+
.modal { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; width: min(440px, 92vw); padding: 18px 20px; box-shadow: 0 18px 50px rgba(0,0,0,.5); }
|
|
159
|
+
.modal h3 { margin: 0 0 4px; font-size: 15px; }
|
|
160
|
+
.modal .hint { font-size: 12px; color: var(--muted); margin-bottom: 12px; }
|
|
161
|
+
.modal label { display: block; font-size: 12px; color: var(--muted); margin: 10px 0 4px; }
|
|
162
|
+
.modal input, .modal textarea, .modal select { width: 100%; background: var(--panel2); color: var(--text); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; font-size: 13px; font-family: inherit; }
|
|
163
|
+
.modal textarea { resize: vertical; min-height: 60px; }
|
|
164
|
+
.modal .mfoot { display: flex; gap: 10px; justify-content: flex-end; margin-top: 16px; }
|
|
165
|
+
.modal .mfoot button { background: var(--panel2); color: var(--text); border: 1px solid var(--line); padding: 8px 16px; border-radius: 8px; cursor: pointer; font-size: 13px; }
|
|
166
|
+
.modal .mfoot button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
167
|
+
.modal .mfoot button:disabled { opacity: .6; cursor: default; }
|
|
168
|
+
.modal .mresult { margin-top: 12px; font-size: 12px; color: var(--muted); white-space: pre-wrap; word-break: break-all; }
|
|
169
|
+
.segmented { display:inline-flex; border:1px solid var(--line); border-radius:8px; overflow:hidden; }
|
|
170
|
+
.segmented button { background:var(--panel2); color:var(--text); border:none; padding:8px 16px; cursor:pointer; font-size:13px; }
|
|
171
|
+
.segmented button + button { border-left:1px solid var(--line); }
|
|
172
|
+
.segmented button.active { background:var(--accent); color:#fff; }
|
|
173
|
+
</style>
|
|
174
|
+
</head>
|
|
175
|
+
<body>
|
|
176
|
+
<header>
|
|
177
|
+
<h1>__SYSTEM_NAME__ - 服务状态大屏</h1>
|
|
178
|
+
<span class="badge" id="serviceName">—</span>
|
|
179
|
+
<span class="badge" id="updated">—</span>
|
|
180
|
+
<div class="seg" id="filters">
|
|
181
|
+
<button data-env="all" class="active">全部</button>
|
|
182
|
+
<button data-env="prod">生产</button>
|
|
183
|
+
<button data-env="test">测试</button>
|
|
184
|
+
<button data-env="dev">开发</button>
|
|
185
|
+
</div>
|
|
186
|
+
<div class="seg" id="winCtl">
|
|
187
|
+
<button data-w="10" class="active">10分钟</button>
|
|
188
|
+
<button data-w="60">1小时</button>
|
|
189
|
+
<button data-w="360">6小时</button>
|
|
190
|
+
<button data-w="720">12小时</button>
|
|
191
|
+
<button data-w="1440">24小时</button>
|
|
192
|
+
</div>
|
|
193
|
+
<div class="refresh-ctl">
|
|
194
|
+
<label><input type="checkbox" id="auto" checked /> 自动刷新 <b id="refreshSec">5</b>s</label>
|
|
195
|
+
<button id="refreshNow">立即刷新</button>
|
|
196
|
+
<button id="pushTestBtn" class="hidden">推送测试</button>
|
|
197
|
+
</div>
|
|
198
|
+
</header>
|
|
199
|
+
<div class="kpi" id="kpi">
|
|
200
|
+
<div class="stat">
|
|
201
|
+
<div class="lab">服务器</div>
|
|
202
|
+
<div class="val" id="kpiServers">—</div>
|
|
203
|
+
<div class="sub">在线实例</div>
|
|
204
|
+
<div class="rows">
|
|
205
|
+
<div class="krow"><span>健康</span><span id="kpiOk" style="color:var(--ok)">—</span></div>
|
|
206
|
+
<div class="krow"><span>异常</span><span id="kpiBad">—</span></div>
|
|
207
|
+
<div class="krow"><span>env</span><span id="kpiEnv">—</span></div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
<div class="stat">
|
|
211
|
+
<div class="lab">API 路由 · apirouter</div>
|
|
212
|
+
<div class="val" id="kpiRouter">—</div>
|
|
213
|
+
<div class="sub">当前QPM</div>
|
|
214
|
+
<div class="rows">
|
|
215
|
+
<div class="krow"><span>总请求 (24h)</span><span id="kpiRouterTotal">—</span></div>
|
|
216
|
+
<div class="krow"><span>成功率</span><span id="kpiRouterRate">—</span></div>
|
|
217
|
+
<div class="krow"><span>平均耗时</span><span id="kpiRouterAvg">—</span></div>
|
|
218
|
+
<div class="krow"><span>失败数</span><span id="kpiRouterFail">—</span></div>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
<div class="stat">
|
|
222
|
+
<div class="lab">分库分表 · shardorm</div>
|
|
223
|
+
<div class="val" id="kpiShardorm">—</div>
|
|
224
|
+
<div class="sub">当前QPM</div>
|
|
225
|
+
<div class="rows">
|
|
226
|
+
<div class="krow"><span>总请求 (24h)</span><span id="kpiShardormTotal">—</span></div>
|
|
227
|
+
<div class="krow"><span>成功率</span><span id="kpiShardormRate">—</span></div>
|
|
228
|
+
<div class="krow"><span>平均耗时</span><span id="kpiShardormAvg">—</span></div>
|
|
229
|
+
<div class="krow"><span>失败数</span><span id="kpiShardormFail">—</span></div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="stat">
|
|
233
|
+
<div class="lab">出口 HTTP · http-client</div>
|
|
234
|
+
<div class="val" id="kpiHttp">—</div>
|
|
235
|
+
<div class="sub">当前QPM</div>
|
|
236
|
+
<div class="rows">
|
|
237
|
+
<div class="krow"><span>总请求 (24h)</span><span id="kpiHttpTotal">—</span></div>
|
|
238
|
+
<div class="krow"><span>成功率</span><span id="kpiHttpRate">—</span></div>
|
|
239
|
+
<div class="krow"><span>平均耗时</span><span id="kpiHttpAvg">—</span></div>
|
|
240
|
+
<div class="krow"><span>失败数</span><span id="kpiHttpFail">—</span></div>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
<div class="stat">
|
|
244
|
+
<div class="lab">推送中心 · node-push</div>
|
|
245
|
+
<div class="val" id="kpiPush">—</div>
|
|
246
|
+
<div class="sub">当前QPM</div>
|
|
247
|
+
<div class="rows">
|
|
248
|
+
<div class="krow"><span>累计推送 (总)</span><span id="kpiPushTotal">—</span></div>
|
|
249
|
+
<div class="krow"><span>成功率</span><span id="kpiPushRate">—</span></div>
|
|
250
|
+
<div class="krow"><span>平均耗时</span><span id="kpiPushAvg">—</span></div>
|
|
251
|
+
<div class="krow"><span>失败数</span><span id="kpiPushFail">—</span></div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="tabs" id="tabs">
|
|
256
|
+
<button data-tab="agg" class="active">聚合总览</button>
|
|
257
|
+
<button data-tab="instances">分服务器</button>
|
|
258
|
+
</div>
|
|
259
|
+
<div class="agg" id="aggView"></div>
|
|
260
|
+
<div class="grid hidden" id="grid"></div>
|
|
261
|
+
|
|
262
|
+
<!-- node-push 测试发送弹窗(按钮仅当 node-push 已加载时显示) -->
|
|
263
|
+
<div class="modal-mask" id="pushModal">
|
|
264
|
+
<div class="modal">
|
|
265
|
+
<h3>推送测试 · node-push</h3>
|
|
266
|
+
<div class="hint">走 <code>/_sys/push/test</code>(需 access token,见下方输入框)。发送模式:<b>立即推送</b>=同步直发、失败即报错;<b>节流推送</b>=按间隔合并、到点才发(间隔内多条合并为一条)。发送后大屏 push 区块的 QPM / 分主题 / 分通道会随之变化。</div>
|
|
267
|
+
<label>目标服务器(分服务器测试:定位哪台推送异常)</label>
|
|
268
|
+
<select id="ptServer"></select>
|
|
269
|
+
<label>推送通道 channelType</label>
|
|
270
|
+
<select id="ptChannel"></select>
|
|
271
|
+
<div id="ptChanCfg"></div>
|
|
272
|
+
<label id="ptToLabel">接收人</label>
|
|
273
|
+
<input id="ptTo" placeholder="多个用逗号分隔" />
|
|
274
|
+
<label>业务主题 topic(必填;决定大屏分组与日志目录 push/{topic}/)</label>
|
|
275
|
+
<input id="ptTopic" value="demo_topic" maxlength="64" />
|
|
276
|
+
<label>标题</label>
|
|
277
|
+
<input id="ptTitle" value="大屏测试推送" maxlength="256" />
|
|
278
|
+
<label>连发条数(便于观察 QPM 上升)</label>
|
|
279
|
+
<select id="ptCount">
|
|
280
|
+
<option value="1">1 条</option>
|
|
281
|
+
<option value="5">5 条</option>
|
|
282
|
+
<option value="10">10 条</option>
|
|
283
|
+
</select>
|
|
284
|
+
<label>发送模式</label>
|
|
285
|
+
<div class="segmented" id="ptModeSeg">
|
|
286
|
+
<button type="button" data-mode="immediate" class="active">立即推送</button>
|
|
287
|
+
<button type="button" data-mode="throttle">节流推送</button>
|
|
288
|
+
</div>
|
|
289
|
+
<input type="hidden" id="ptMode" value="immediate" />
|
|
290
|
+
<label id="ptIntervalLabel" style="display:none">节流间隔(毫秒,默认 60000 = 1 分钟;间隔内多条合并为一条)</label>
|
|
291
|
+
<input id="ptInterval" type="number" value="60000" min="1000" step="1000" style="display:none" />
|
|
292
|
+
<label>access token(必填:/_sys/push/* 会返回渠道真实凭证并触发对外发送,必须带令牌)</label>
|
|
293
|
+
<input id="ptToken" type="password" placeholder="粘贴 accessToken(登录响应里的明文令牌);未填则无法预填渠道配置,发送也会被 401" />
|
|
294
|
+
<div class="mfoot">
|
|
295
|
+
<button id="ptClose">关闭</button>
|
|
296
|
+
<button id="ptSend" class="primary">发送</button>
|
|
297
|
+
</div>
|
|
298
|
+
<div class="mresult" id="ptResult"></div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
<!-- 大屏脚本:common(引擎,须最先)→ agg(聚合总览)→ instances(分服务器)→ push(测试弹窗),最后启动 load() -->
|
|
303
|
+
<script src="js/common.js"></script>
|
|
304
|
+
<script src="js/agg.js"></script>
|
|
305
|
+
<script src="js/instances.js"></script>
|
|
306
|
+
<script src="js/push.js"></script>
|
|
307
|
+
<script>load(); startTimer();</script>
|
|
308
|
+
</body>
|
|
309
|
+
</html>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// ===== node-dashboard 大屏 · 聚合总览(agg.js)=====
|
|
2
|
+
// 聚合 KPI 顶栏(renderKpi)与聚合总览区块(renderAgg)。
|
|
3
|
+
// 依赖 common.js 提供的 $ / qpmCell / SPARK_COLORS / mountSparks 等全局符号。
|
|
4
|
+
|
|
5
|
+
function renderKpi(agg) {
|
|
6
|
+
if (!agg) return;
|
|
7
|
+
const b = agg.byService || {};
|
|
8
|
+
$("kpiServers").textContent = agg.serverCount ?? "—";
|
|
9
|
+
$("kpiEnv").textContent = agg.env ?? "—";
|
|
10
|
+
// 每张服务卡:当前QPM(大数字)+ 24h 总请求/成功率/平均耗时/失败数(失败 >0 红色高亮)
|
|
11
|
+
const svc = (key, p) => {
|
|
12
|
+
const s = b[key] || { qpm: 0, ok: 0, fail: 0, avg: 0 };
|
|
13
|
+
const total = (s.ok ?? 0) + (s.fail ?? 0);
|
|
14
|
+
const rate = total ? (s.ok / total * 100).toFixed(2) + "%" : "—";
|
|
15
|
+
const bad = (s.fail ?? 0) > 0;
|
|
16
|
+
$(`kpi${p}`).textContent = s.qpm ?? 0;
|
|
17
|
+
$(`kpi${p}Total`).textContent = total.toLocaleString();
|
|
18
|
+
const rateEl = $(`kpi${p}Rate`);
|
|
19
|
+
rateEl.textContent = rate;
|
|
20
|
+
rateEl.style.color = bad ? "var(--down)" : "";
|
|
21
|
+
$(`kpi${p}Avg`).textContent = (s.avg ?? 0) + "ms";
|
|
22
|
+
const failEl = $(`kpi${p}Fail`);
|
|
23
|
+
failEl.textContent = s.fail ?? 0;
|
|
24
|
+
failEl.style.color = bad ? "var(--down)" : "";
|
|
25
|
+
};
|
|
26
|
+
svc("apirouter", "Router");
|
|
27
|
+
svc("shardorm", "Shardorm");
|
|
28
|
+
svc("http", "Http");
|
|
29
|
+
// node-push KPI:当前 QPM + 累计推送/成功率(overview 跨实例累加)
|
|
30
|
+
const sp = b.push || { qpm: 0, totalTasks: 0, successTasks: 0, failedTasks: 0 };
|
|
31
|
+
const pok = sp.successTasks ?? 0, pfail = sp.failedTasks ?? 0;
|
|
32
|
+
const ptotal = pok + pfail;
|
|
33
|
+
const prate = ptotal ? (pok / ptotal * 100).toFixed(2) + "%" : "—";
|
|
34
|
+
const pbad = pfail > 0;
|
|
35
|
+
$("kpiPush").textContent = sp.qpm ?? 0;
|
|
36
|
+
$("kpiPushTotal").textContent = (sp.totalTasks ?? 0).toLocaleString();
|
|
37
|
+
const pRateEl = $("kpiPushRate");
|
|
38
|
+
pRateEl.textContent = prate;
|
|
39
|
+
pRateEl.style.color = pbad ? "var(--down)" : "";
|
|
40
|
+
$("kpiPushAvg").textContent = "—";
|
|
41
|
+
const pFailEl = $("kpiPushFail");
|
|
42
|
+
pFailEl.textContent = pfail;
|
|
43
|
+
pFailEl.style.color = pbad ? "var(--down)" : "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function renderAgg(agg) {
|
|
47
|
+
if (!agg) return;
|
|
48
|
+
const services = [
|
|
49
|
+
{ key: "push", name: "推送中心 · node-push", color: "#f0883e" },
|
|
50
|
+
];
|
|
51
|
+
let html = services.map((sv) => {
|
|
52
|
+
const s = agg.byService?.[sv.key] || {};
|
|
53
|
+
if (sv.key === "push") {
|
|
54
|
+
const ok = s.successTasks ?? 0;
|
|
55
|
+
const fail = s.failedTasks ?? 0;
|
|
56
|
+
const empty = !s.qpm && !s.totalTasks;
|
|
57
|
+
const rate = ok + fail ? (ok / (ok + fail) * 100).toFixed(1) + "%" : "—";
|
|
58
|
+
return `<div class="router">
|
|
59
|
+
<div class="router-head"><span class="dot ${empty ? "unknown" : "ok"}"></span>${sv.name} <span class="tag">QPM</span></div>
|
|
60
|
+
<div class="router-grid">
|
|
61
|
+
${qpmCell("当前QPM", s.qpm ?? 0)}
|
|
62
|
+
${qpmCell("总请求", s.totalTasks ?? 0)}
|
|
63
|
+
${qpmCell("成功率", rate)}
|
|
64
|
+
${qpmCell("成功", ok)}
|
|
65
|
+
${qpmCell("失败", fail)}
|
|
66
|
+
${qpmCell("平均耗时", "—")}
|
|
67
|
+
</div>
|
|
68
|
+
<div class="spark-chart" data-kind="push"></div>
|
|
69
|
+
</div>`;
|
|
70
|
+
}
|
|
71
|
+
const ok = s.ok ?? 0;
|
|
72
|
+
const fail = s.fail ?? 0;
|
|
73
|
+
const empty = !s.qpm;
|
|
74
|
+
const rate = ok + fail ? (ok / (ok + fail) * 100).toFixed(1) + "%" : "—";
|
|
75
|
+
return `<div class="router">
|
|
76
|
+
<div class="router-head"><span class="dot ${empty ? "unknown" : "ok"}"></span>${sv.name} <span class="tag">QPM</span></div>
|
|
77
|
+
<div class="router-grid">
|
|
78
|
+
${qpmCell("当前QPM", s.qpm ?? 0)}
|
|
79
|
+
${qpmCell("总请求", ok + fail)}
|
|
80
|
+
${qpmCell("成功率", rate)}
|
|
81
|
+
${qpmCell("成功", ok)}
|
|
82
|
+
${qpmCell("失败", fail)}
|
|
83
|
+
${qpmCell("平均耗时", (s.avg ?? 0) + "ms")}
|
|
84
|
+
</div>
|
|
85
|
+
<div class="spark-chart" data-kind="${sv.key}"></div>
|
|
86
|
+
</div>`;
|
|
87
|
+
}).join("");
|
|
88
|
+
// apirouter 3 通道 ×(次数/时间)= 6 卡片,图表在 loadInner 中异步拉取后绘制
|
|
89
|
+
// 出口 HTTP:HTTP 次数 + HTTP 耗时 2 卡(格式与 apirouter 通道卡一致)
|
|
90
|
+
html += channelPairHtml("agg", "http", "出口 HTTP · http-client", "HTTP 次数 QPM", "HTTP 耗时 ms");
|
|
91
|
+
// 分库分表 shardorm:SQL 执行次数 + SQL 执行时间 2 卡(格式与 apirouter 通道卡一致)
|
|
92
|
+
html += channelPairHtml("agg", "shardorm", "分库分表 · shardorm", "SQL 次数 QPM", "SQL 耗时 ms");
|
|
93
|
+
html += apiChannelCardsHtml("agg");
|
|
94
|
+
$("aggView").innerHTML = html;
|
|
95
|
+
mountSparks($("aggView"), (ds) => ({
|
|
96
|
+
pts: agg.byService?.[ds.kind]?.recent || [],
|
|
97
|
+
color: SPARK_COLORS[ds.kind] || "#5b9dff",
|
|
98
|
+
}));
|
|
99
|
+
}
|