adonisjs-server-stats 1.4.0 → 1.5.2
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/README.md +272 -142
- package/dist/configure.d.ts.map +1 -1
- package/dist/src/controller/debug_controller.d.ts +2 -2
- package/dist/src/controller/debug_controller.d.ts.map +1 -1
- package/dist/src/controller/server_stats_controller.d.ts +1 -1
- package/dist/src/controller/server_stats_controller.d.ts.map +1 -1
- package/dist/src/dashboard/chart_aggregator.d.ts.map +1 -1
- package/dist/src/dashboard/chart_aggregator.js +8 -8
- package/dist/src/dashboard/dashboard_controller.d.ts +12 -97
- package/dist/src/dashboard/dashboard_controller.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_controller.js +244 -522
- package/dist/src/dashboard/dashboard_routes.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_routes.js +7 -2
- package/dist/src/dashboard/dashboard_store.d.ts +6 -3
- package/dist/src/dashboard/dashboard_store.d.ts.map +1 -1
- package/dist/src/dashboard/dashboard_store.js +54 -78
- package/dist/src/dashboard/integrations/cache_inspector.d.ts.map +1 -1
- package/dist/src/dashboard/integrations/queue_inspector.d.ts.map +1 -1
- package/dist/src/dashboard/migrator.d.ts.map +1 -1
- package/dist/src/dashboard/migrator.js +3 -1
- package/dist/src/dashboard/models/stats_event.d.ts +1 -1
- package/dist/src/dashboard/models/stats_event.d.ts.map +1 -1
- package/dist/src/dashboard/models/stats_query.d.ts +1 -1
- package/dist/src/dashboard/models/stats_query.d.ts.map +1 -1
- package/dist/src/dashboard/models/stats_request.d.ts +2 -2
- package/dist/src/dashboard/models/stats_request.d.ts.map +1 -1
- package/dist/src/dashboard/models/stats_request.js +1 -1
- package/dist/src/dashboard/models/stats_trace.d.ts +1 -1
- package/dist/src/dashboard/models/stats_trace.d.ts.map +1 -1
- package/dist/src/debug/debug_store.d.ts +6 -6
- package/dist/src/debug/debug_store.d.ts.map +1 -1
- package/dist/src/debug/debug_store.js +10 -10
- package/dist/src/debug/email_collector.d.ts +0 -9
- package/dist/src/debug/email_collector.d.ts.map +1 -1
- package/dist/src/debug/email_collector.js +6 -28
- package/dist/src/debug/event_collector.d.ts +1 -1
- package/dist/src/debug/event_collector.d.ts.map +1 -1
- package/dist/src/debug/event_collector.js +17 -17
- package/dist/src/debug/query_collector.d.ts +1 -1
- package/dist/src/debug/query_collector.d.ts.map +1 -1
- package/dist/src/debug/query_collector.js +13 -14
- package/dist/src/debug/ring_buffer.d.ts.map +1 -1
- package/dist/src/debug/route_inspector.d.ts +1 -1
- package/dist/src/debug/route_inspector.d.ts.map +1 -1
- package/dist/src/debug/route_inspector.js +12 -12
- package/dist/src/debug/trace_collector.d.ts.map +1 -1
- package/dist/src/debug/trace_collector.js +6 -5
- package/dist/src/edge/client/dashboard.css +516 -171
- package/dist/src/edge/client/dashboard.js +2756 -1662
- package/dist/src/edge/client/debug-panel.css +476 -133
- package/dist/src/edge/client/debug-panel.js +1496 -1043
- package/dist/src/edge/client/stats-bar.css +64 -30
- package/dist/src/edge/client/stats-bar.js +598 -319
- package/dist/src/edge/plugin.d.ts +1 -1
- package/dist/src/edge/plugin.d.ts.map +1 -1
- package/dist/src/edge/plugin.js +41 -59
- package/dist/src/edge/views/stats-bar.edge +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/middleware/request_tracking_middleware.d.ts +4 -4
- package/dist/src/middleware/request_tracking_middleware.d.ts.map +1 -1
- package/dist/src/middleware/request_tracking_middleware.js +7 -6
- package/dist/src/prometheus/prometheus_collector.d.ts +1 -1
- package/dist/src/prometheus/prometheus_collector.d.ts.map +1 -1
- package/dist/src/provider/server_stats_provider.d.ts +1 -1
- package/dist/src/provider/server_stats_provider.d.ts.map +1 -1
- package/dist/src/provider/server_stats_provider.js +33 -32
- package/dist/src/types.d.ts +2 -2
- package/dist/src/utils/json_helpers.d.ts +8 -0
- package/dist/src/utils/json_helpers.d.ts.map +1 -0
- package/dist/src/utils/json_helpers.js +21 -0
- package/dist/src/utils/mail_helpers.d.ts +13 -0
- package/dist/src/utils/mail_helpers.d.ts.map +1 -0
- package/dist/src/utils/mail_helpers.js +26 -0
- package/dist/src/utils/math_helpers.d.ts +8 -0
- package/dist/src/utils/math_helpers.d.ts.map +1 -0
- package/dist/src/utils/math_helpers.js +11 -0
- package/dist/src/utils/time_helpers.d.ts +12 -0
- package/dist/src/utils/time_helpers.d.ts.map +1 -0
- package/dist/src/utils/time_helpers.js +32 -0
- package/dist/src/utils/transmit_client.d.ts +9 -0
- package/dist/src/utils/transmit_client.d.ts.map +1 -0
- package/dist/src/utils/transmit_client.js +20 -0
- package/package.json +35 -29
|
@@ -8,515 +8,794 @@
|
|
|
8
8
|
* data-endpoint — polling URL
|
|
9
9
|
* data-interval — poll interval in ms
|
|
10
10
|
*/
|
|
11
|
-
(function () {
|
|
12
|
-
const bar = document.getElementById('ss-bar')
|
|
13
|
-
const toggle = document.getElementById('ss-toggle')
|
|
14
|
-
const dot = document.getElementById('ss-dot')
|
|
15
|
-
const toggleSummary = document.getElementById('ss-toggle-summary')
|
|
11
|
+
;(function () {
|
|
12
|
+
const bar = document.getElementById('ss-bar')
|
|
13
|
+
const toggle = document.getElementById('ss-toggle')
|
|
14
|
+
const dot = document.getElementById('ss-dot')
|
|
15
|
+
const toggleSummary = document.getElementById('ss-toggle-summary')
|
|
16
16
|
|
|
17
|
-
if (!bar || !toggle) return
|
|
17
|
+
if (!bar || !toggle) return
|
|
18
18
|
|
|
19
19
|
// ── Theme detection & application ───────────────────────────────
|
|
20
|
-
let themeOverride = localStorage.getItem('ss-dash-theme')
|
|
20
|
+
let themeOverride = localStorage.getItem('ss-dash-theme')
|
|
21
21
|
|
|
22
22
|
const applyBarTheme = () => {
|
|
23
23
|
if (themeOverride) {
|
|
24
|
-
bar.setAttribute('data-ss-theme', themeOverride)
|
|
25
|
-
toggle.setAttribute('data-ss-theme', themeOverride)
|
|
24
|
+
bar.setAttribute('data-ss-theme', themeOverride)
|
|
25
|
+
toggle.setAttribute('data-ss-theme', themeOverride)
|
|
26
26
|
} else {
|
|
27
|
-
bar.removeAttribute('data-ss-theme')
|
|
28
|
-
toggle.removeAttribute('data-ss-theme')
|
|
27
|
+
bar.removeAttribute('data-ss-theme')
|
|
28
|
+
toggle.removeAttribute('data-ss-theme')
|
|
29
29
|
}
|
|
30
|
-
}
|
|
30
|
+
}
|
|
31
31
|
|
|
32
|
-
applyBarTheme()
|
|
32
|
+
applyBarTheme()
|
|
33
33
|
|
|
34
34
|
// Expose for debug-panel toggle to call directly
|
|
35
|
-
window.__ssApplyBarTheme = applyBarTheme
|
|
35
|
+
window.__ssApplyBarTheme = applyBarTheme
|
|
36
36
|
|
|
37
37
|
// Listen for cross-tab theme changes
|
|
38
38
|
window.addEventListener('storage', function (e) {
|
|
39
39
|
if (e.key === 'ss-dash-theme') {
|
|
40
|
-
themeOverride = e.newValue
|
|
41
|
-
applyBarTheme()
|
|
40
|
+
themeOverride = e.newValue
|
|
41
|
+
applyBarTheme()
|
|
42
42
|
}
|
|
43
|
-
})
|
|
43
|
+
})
|
|
44
44
|
|
|
45
|
-
const ENDPOINT = bar.dataset.endpoint || '/admin/api/server-stats'
|
|
46
|
-
const INTERVAL = Number(bar.dataset.interval) || 3000
|
|
47
|
-
const MAX_HISTORY = 60
|
|
48
|
-
const STALE_MS = 10000
|
|
45
|
+
const ENDPOINT = bar.dataset.endpoint || '/admin/api/server-stats'
|
|
46
|
+
const INTERVAL = Number(bar.dataset.interval) || 3000
|
|
47
|
+
const MAX_HISTORY = 60
|
|
48
|
+
const STALE_MS = 10000
|
|
49
49
|
|
|
50
50
|
// ── Formatting utils ──────────────────────────────────────────────
|
|
51
51
|
|
|
52
52
|
const formatUptime = (s) => {
|
|
53
|
-
const d = Math.floor(s / 86400)
|
|
54
|
-
const h = Math.floor((s % 86400) / 3600)
|
|
55
|
-
const m = Math.floor((s % 3600) / 60)
|
|
56
|
-
if (d > 0) return d + 'd ' + h + 'h'
|
|
57
|
-
if (h > 0) return h + 'h ' + m + 'm'
|
|
58
|
-
return m + 'm'
|
|
59
|
-
}
|
|
53
|
+
const d = Math.floor(s / 86400)
|
|
54
|
+
const h = Math.floor((s % 86400) / 3600)
|
|
55
|
+
const m = Math.floor((s % 3600) / 60)
|
|
56
|
+
if (d > 0) return d + 'd ' + h + 'h'
|
|
57
|
+
if (h > 0) return h + 'h ' + m + 'm'
|
|
58
|
+
return m + 'm'
|
|
59
|
+
}
|
|
60
60
|
|
|
61
61
|
const formatBytes = (b) => {
|
|
62
|
-
const mb = b / (1024 * 1024)
|
|
63
|
-
if (mb >= 1024) return (mb / 1024).toFixed(1) + 'G'
|
|
64
|
-
return mb.toFixed(0) + 'M'
|
|
65
|
-
}
|
|
62
|
+
const mb = b / (1024 * 1024)
|
|
63
|
+
if (mb >= 1024) return (mb / 1024).toFixed(1) + 'G'
|
|
64
|
+
return mb.toFixed(0) + 'M'
|
|
65
|
+
}
|
|
66
66
|
|
|
67
67
|
const formatMb = (mb) => {
|
|
68
|
-
if (mb >= 1024) return (mb / 1024).toFixed(1) + 'G'
|
|
69
|
-
return mb.toFixed(1) + 'M'
|
|
70
|
-
}
|
|
68
|
+
if (mb >= 1024) return (mb / 1024).toFixed(1) + 'G'
|
|
69
|
+
return mb.toFixed(1) + 'M'
|
|
70
|
+
}
|
|
71
71
|
|
|
72
72
|
const formatCount = (n) => {
|
|
73
|
-
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M'
|
|
74
|
-
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'
|
|
75
|
-
return '' + n
|
|
76
|
-
}
|
|
73
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M'
|
|
74
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K'
|
|
75
|
+
return '' + n
|
|
76
|
+
}
|
|
77
77
|
|
|
78
78
|
const formatStatNum = (v, unit) => {
|
|
79
|
-
if (unit === '%') return v.toFixed(1) + '%'
|
|
80
|
-
if (unit === 'ms') return v.toFixed(0) + 'ms'
|
|
81
|
-
if (unit === 'MB') return v.toFixed(1) + 'M'
|
|
82
|
-
if (unit === 'bytes') return formatBytes(v)
|
|
83
|
-
if (unit === '/s' || unit === '/m') return v.toFixed(1)
|
|
84
|
-
return v.toFixed(1)
|
|
85
|
-
}
|
|
79
|
+
if (unit === '%') return v.toFixed(1) + '%'
|
|
80
|
+
if (unit === 'ms') return v.toFixed(0) + 'ms'
|
|
81
|
+
if (unit === 'MB') return v.toFixed(1) + 'M'
|
|
82
|
+
if (unit === 'bytes') return formatBytes(v)
|
|
83
|
+
if (unit === '/s' || unit === '/m') return v.toFixed(1)
|
|
84
|
+
return v.toFixed(1)
|
|
85
|
+
}
|
|
86
86
|
|
|
87
87
|
// ── Color thresholds ──────────────────────────────────────────────
|
|
88
88
|
// Unified threshold helper: returns CSS class based on warn/crit thresholds
|
|
89
|
-
const thresh = (v, warn, crit) => v > crit ? 'ss-red' : v > warn ? 'ss-amber' : 'ss-green'
|
|
90
|
-
const threshInverse = (v, warn, crit) =>
|
|
89
|
+
const thresh = (v, warn, crit) => (v > crit ? 'ss-red' : v > warn ? 'ss-amber' : 'ss-green')
|
|
90
|
+
const threshInverse = (v, warn, crit) =>
|
|
91
|
+
v < crit ? 'ss-red' : v < warn ? 'ss-amber' : 'ss-green'
|
|
91
92
|
|
|
92
93
|
// Map color class → CSS variable for sparklines (reads themed value at render time)
|
|
93
|
-
const HEX_FALLBACK = {
|
|
94
|
-
|
|
94
|
+
const HEX_FALLBACK = {
|
|
95
|
+
'ss-red': '#f87171',
|
|
96
|
+
'ss-amber': '#fbbf24',
|
|
97
|
+
'ss-green': '#34d399',
|
|
98
|
+
'ss-muted': '#737373',
|
|
99
|
+
}
|
|
100
|
+
const HEX_VAR = {
|
|
101
|
+
'ss-red': '--ss-red-fg',
|
|
102
|
+
'ss-amber': '--ss-amber-fg',
|
|
103
|
+
'ss-green': '--ss-accent',
|
|
104
|
+
'ss-muted': '--ss-muted',
|
|
105
|
+
}
|
|
95
106
|
const hexFromClass = (cls) => {
|
|
96
|
-
const varName = HEX_VAR[cls]
|
|
107
|
+
const varName = HEX_VAR[cls]
|
|
97
108
|
if (varName) {
|
|
98
|
-
const val = getComputedStyle(bar).getPropertyValue(varName).trim()
|
|
99
|
-
if (val) return val
|
|
109
|
+
const val = getComputedStyle(bar).getPropertyValue(varName).trim()
|
|
110
|
+
if (val) return val
|
|
100
111
|
}
|
|
101
|
-
return HEX_FALLBACK[cls] || '#34d399'
|
|
102
|
-
}
|
|
112
|
+
return HEX_FALLBACK[cls] || '#34d399'
|
|
113
|
+
}
|
|
103
114
|
|
|
104
115
|
const ratioColor = (used, max) => {
|
|
105
|
-
if (max === 0) return 'ss-muted'
|
|
106
|
-
const p = used / max
|
|
107
|
-
return p > 0.8 ? 'ss-red' : p > 0.5 ? 'ss-amber' : 'ss-green'
|
|
108
|
-
}
|
|
116
|
+
if (max === 0) return 'ss-muted'
|
|
117
|
+
const p = used / max
|
|
118
|
+
return p > 0.8 ? 'ss-red' : p > 0.5 ? 'ss-amber' : 'ss-green'
|
|
119
|
+
}
|
|
109
120
|
|
|
110
121
|
// ── Sparkline rendering ───────────────────────────────────────────
|
|
111
|
-
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
122
|
+
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
112
123
|
|
|
113
124
|
const renderSparkline = (container, data, color) => {
|
|
114
|
-
container.innerHTML = ''
|
|
115
|
-
const w = 120,
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
svg.
|
|
119
|
-
svg.
|
|
125
|
+
container.innerHTML = ''
|
|
126
|
+
const w = 120,
|
|
127
|
+
h = 32,
|
|
128
|
+
pad = 2
|
|
129
|
+
const svg = document.createElementNS(SVG_NS, 'svg')
|
|
130
|
+
svg.setAttribute('width', '' + w)
|
|
131
|
+
svg.setAttribute('height', '' + h)
|
|
132
|
+
svg.style.display = 'block'
|
|
120
133
|
|
|
121
134
|
if (data.length < 2) {
|
|
122
|
-
const txt = document.createElementNS(SVG_NS, 'text')
|
|
123
|
-
txt.setAttribute('x', '' +
|
|
124
|
-
txt.setAttribute('y', '' + (h / 2 + 3))
|
|
125
|
-
txt.setAttribute('text-anchor', 'middle')
|
|
126
|
-
txt.setAttribute('fill', '#737373')
|
|
127
|
-
txt.setAttribute('font-size', '9')
|
|
128
|
-
txt.textContent = 'collecting\u2026'
|
|
129
|
-
svg.appendChild(txt)
|
|
130
|
-
container.appendChild(svg)
|
|
131
|
-
return
|
|
135
|
+
const txt = document.createElementNS(SVG_NS, 'text')
|
|
136
|
+
txt.setAttribute('x', '' + w / 2)
|
|
137
|
+
txt.setAttribute('y', '' + (h / 2 + 3))
|
|
138
|
+
txt.setAttribute('text-anchor', 'middle')
|
|
139
|
+
txt.setAttribute('fill', '#737373')
|
|
140
|
+
txt.setAttribute('font-size', '9')
|
|
141
|
+
txt.textContent = 'collecting\u2026'
|
|
142
|
+
svg.appendChild(txt)
|
|
143
|
+
container.appendChild(svg)
|
|
144
|
+
return
|
|
132
145
|
}
|
|
133
146
|
|
|
134
|
-
const iw = w - pad * 2,
|
|
135
|
-
|
|
136
|
-
const
|
|
147
|
+
const iw = w - pad * 2,
|
|
148
|
+
ih = h - pad * 2
|
|
149
|
+
const mn = Math.min(...data),
|
|
150
|
+
mx = Math.max(...data)
|
|
151
|
+
const range = mx - mn || 1
|
|
137
152
|
const pts = data.map((v, i) => {
|
|
138
|
-
const x = pad + (i / (data.length - 1)) * iw
|
|
139
|
-
const y = pad + ih - ((v - mn) / range) * ih
|
|
140
|
-
return x.toFixed(1) + ',' + y.toFixed(1)
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
const gradId = 'sg-' + color.replace('#', '')
|
|
144
|
-
const defs = document.createElementNS(SVG_NS, 'defs')
|
|
145
|
-
const grad = document.createElementNS(SVG_NS, 'linearGradient')
|
|
146
|
-
grad.setAttribute('id', gradId)
|
|
147
|
-
grad.setAttribute('x1', '0')
|
|
148
|
-
grad.setAttribute('
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
153
|
+
const x = pad + (i / (data.length - 1)) * iw
|
|
154
|
+
const y = pad + ih - ((v - mn) / range) * ih
|
|
155
|
+
return x.toFixed(1) + ',' + y.toFixed(1)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const gradId = 'sg-' + color.replace('#', '')
|
|
159
|
+
const defs = document.createElementNS(SVG_NS, 'defs')
|
|
160
|
+
const grad = document.createElementNS(SVG_NS, 'linearGradient')
|
|
161
|
+
grad.setAttribute('id', gradId)
|
|
162
|
+
grad.setAttribute('x1', '0')
|
|
163
|
+
grad.setAttribute('y1', '0')
|
|
164
|
+
grad.setAttribute('x2', '0')
|
|
165
|
+
grad.setAttribute('y2', '1')
|
|
166
|
+
const s1 = document.createElementNS(SVG_NS, 'stop')
|
|
167
|
+
s1.setAttribute('offset', '0%')
|
|
168
|
+
s1.setAttribute('stop-color', color)
|
|
169
|
+
s1.setAttribute('stop-opacity', '0.25')
|
|
170
|
+
const s2 = document.createElementNS(SVG_NS, 'stop')
|
|
171
|
+
s2.setAttribute('offset', '100%')
|
|
172
|
+
s2.setAttribute('stop-color', color)
|
|
173
|
+
s2.setAttribute('stop-opacity', '0.02')
|
|
174
|
+
grad.appendChild(s1)
|
|
175
|
+
grad.appendChild(s2)
|
|
176
|
+
defs.appendChild(grad)
|
|
177
|
+
svg.appendChild(defs)
|
|
178
|
+
|
|
179
|
+
const lastX = (pad + iw).toFixed(1)
|
|
180
|
+
const lastY = (pad + ih).toFixed(1)
|
|
181
|
+
const firstX = pad.toFixed(1)
|
|
182
|
+
const areaD =
|
|
183
|
+
'M' +
|
|
184
|
+
pts[0] +
|
|
185
|
+
' ' +
|
|
186
|
+
pts
|
|
187
|
+
.slice(1)
|
|
188
|
+
.map((p) => 'L' + p)
|
|
189
|
+
.join(' ') +
|
|
190
|
+
' L' +
|
|
191
|
+
lastX +
|
|
192
|
+
',' +
|
|
193
|
+
lastY +
|
|
194
|
+
' L' +
|
|
195
|
+
firstX +
|
|
196
|
+
',' +
|
|
197
|
+
lastY +
|
|
198
|
+
' Z'
|
|
199
|
+
const area = document.createElementNS(SVG_NS, 'path')
|
|
200
|
+
area.setAttribute('d', areaD)
|
|
201
|
+
area.setAttribute('fill', 'url(#' + gradId + ')')
|
|
202
|
+
svg.appendChild(area)
|
|
203
|
+
|
|
204
|
+
const line = document.createElementNS(SVG_NS, 'polyline')
|
|
205
|
+
line.setAttribute('points', pts.join(' '))
|
|
206
|
+
line.setAttribute('fill', 'none')
|
|
207
|
+
line.setAttribute('stroke', color)
|
|
208
|
+
line.setAttribute('stroke-width', '1.5')
|
|
209
|
+
line.setAttribute('stroke-linejoin', 'round')
|
|
210
|
+
line.setAttribute('stroke-linecap', 'round')
|
|
211
|
+
svg.appendChild(line)
|
|
212
|
+
|
|
213
|
+
container.appendChild(svg)
|
|
214
|
+
}
|
|
178
215
|
|
|
179
216
|
// ── Stats computation ─────────────────────────────────────────────
|
|
180
217
|
const computeStats = (data) => {
|
|
181
|
-
if (!data || data.length === 0) return null
|
|
182
|
-
const mn = Math.min(...data)
|
|
183
|
-
const mx = Math.max(...data)
|
|
184
|
-
const avg = data.reduce((a, b) => a + b, 0) / data.length
|
|
185
|
-
return { min: mn, max: mx, avg }
|
|
186
|
-
}
|
|
218
|
+
if (!data || data.length === 0) return null
|
|
219
|
+
const mn = Math.min(...data)
|
|
220
|
+
const mx = Math.max(...data)
|
|
221
|
+
const avg = data.reduce((a, b) => a + b, 0) / data.length
|
|
222
|
+
return { min: mn, max: mx, avg }
|
|
223
|
+
}
|
|
187
224
|
|
|
188
225
|
// ── HTML escape ───────────────────────────────────────────────────
|
|
189
226
|
const esc = (s) => {
|
|
190
|
-
if (typeof s !== 'string') s = '' + s
|
|
191
|
-
return s
|
|
192
|
-
|
|
227
|
+
if (typeof s !== 'string') s = '' + s
|
|
228
|
+
return s
|
|
229
|
+
.replace(/&/g, '&')
|
|
230
|
+
.replace(/</g, '<')
|
|
231
|
+
.replace(/>/g, '>')
|
|
232
|
+
.replace(/"/g, '"')
|
|
233
|
+
}
|
|
193
234
|
|
|
194
235
|
// ── Tooltip rendering ─────────────────────────────────────────────
|
|
195
|
-
let pinnedBadge = null
|
|
196
|
-
let activeTooltip = null
|
|
236
|
+
let pinnedBadge = null
|
|
237
|
+
let activeTooltip = null
|
|
197
238
|
|
|
198
239
|
const positionTooltip = (tip, badge) => {
|
|
199
|
-
const badgeRect = badge.getBoundingClientRect()
|
|
200
|
-
const barRect = bar.getBoundingClientRect()
|
|
201
|
-
const leftPos = badgeRect.left - barRect.left + badgeRect.width / 2
|
|
202
|
-
tip.style.bottom = '100%'
|
|
203
|
-
tip.style.left = leftPos + 'px'
|
|
204
|
-
tip.style.transform = 'translateX(-50%)'
|
|
205
|
-
tip.style.marginBottom = '10px'
|
|
240
|
+
const badgeRect = badge.getBoundingClientRect()
|
|
241
|
+
const barRect = bar.getBoundingClientRect()
|
|
242
|
+
const leftPos = badgeRect.left - barRect.left + badgeRect.width / 2
|
|
243
|
+
tip.style.bottom = '100%'
|
|
244
|
+
tip.style.left = leftPos + 'px'
|
|
245
|
+
tip.style.transform = 'translateX(-50%)'
|
|
246
|
+
tip.style.marginBottom = '10px'
|
|
206
247
|
requestAnimationFrame(() => {
|
|
207
|
-
const tipRect = tip.getBoundingClientRect()
|
|
208
|
-
let shift = 0
|
|
209
|
-
if (tipRect.left < 8) shift = 8 - tipRect.left
|
|
210
|
-
else if (tipRect.right > window.innerWidth - 8) shift = window.innerWidth - 8 - tipRect.right
|
|
211
|
-
if (shift) tip.style.transform = 'translateX(calc(-50% + ' + shift + 'px))'
|
|
212
|
-
})
|
|
213
|
-
}
|
|
248
|
+
const tipRect = tip.getBoundingClientRect()
|
|
249
|
+
let shift = 0
|
|
250
|
+
if (tipRect.left < 8) shift = 8 - tipRect.left
|
|
251
|
+
else if (tipRect.right > window.innerWidth - 8) shift = window.innerWidth - 8 - tipRect.right
|
|
252
|
+
if (shift) tip.style.transform = 'translateX(calc(-50% + ' + shift + 'px))'
|
|
253
|
+
})
|
|
254
|
+
}
|
|
214
255
|
|
|
215
256
|
const hideCurrentTooltip = () => {
|
|
216
257
|
if (activeTooltip) {
|
|
217
|
-
activeTooltip.remove()
|
|
218
|
-
activeTooltip = null
|
|
258
|
+
activeTooltip.remove()
|
|
259
|
+
activeTooltip = null
|
|
219
260
|
}
|
|
220
|
-
}
|
|
261
|
+
}
|
|
221
262
|
|
|
222
263
|
const unpinTooltip = () => {
|
|
223
264
|
if (pinnedBadge) {
|
|
224
|
-
pinnedBadge.classList.remove('ss-pinned')
|
|
225
|
-
pinnedBadge = null
|
|
265
|
+
pinnedBadge.classList.remove('ss-pinned')
|
|
266
|
+
pinnedBadge = null
|
|
226
267
|
}
|
|
227
|
-
hideCurrentTooltip()
|
|
228
|
-
}
|
|
268
|
+
hideCurrentTooltip()
|
|
269
|
+
}
|
|
229
270
|
|
|
230
271
|
const showTooltip = (badge, historyData, color, title, unit, currentValue, details, pinned) => {
|
|
231
|
-
hideCurrentTooltip()
|
|
232
|
-
const tip = document.createElement('div')
|
|
233
|
-
tip.className = pinned ? 'ss-tooltip ss-pinned' : 'ss-tooltip'
|
|
272
|
+
hideCurrentTooltip()
|
|
273
|
+
const tip = document.createElement('div')
|
|
274
|
+
tip.className = pinned ? 'ss-tooltip ss-pinned' : 'ss-tooltip'
|
|
234
275
|
|
|
235
|
-
let html = '<div class="ss-tooltip-inner" style="position:relative">'
|
|
276
|
+
let html = '<div class="ss-tooltip-inner" style="position:relative">'
|
|
236
277
|
if (pinned) {
|
|
237
|
-
html += '<button class="ss-tooltip-close" data-ss-close>\u00D7</button>'
|
|
278
|
+
html += '<button class="ss-tooltip-close" data-ss-close>\u00D7</button>'
|
|
238
279
|
}
|
|
239
|
-
html +=
|
|
240
|
-
|
|
241
|
-
html += '</
|
|
242
|
-
html += '
|
|
243
|
-
html +=
|
|
244
|
-
|
|
245
|
-
|
|
280
|
+
html +=
|
|
281
|
+
'<div class="ss-tooltip-header"><span class="ss-tooltip-title">' + esc(title) + '</span>'
|
|
282
|
+
if (unit) html += '<span class="ss-tooltip-unit">' + esc(unit) + '</span>'
|
|
283
|
+
html += '</div>'
|
|
284
|
+
html +=
|
|
285
|
+
'<div class="ss-tooltip-current"><span class="ss-tooltip-current-label">Current: </span>'
|
|
286
|
+
html += '<span class="ss-tooltip-current-value">' + esc(currentValue) + '</span></div>'
|
|
287
|
+
|
|
288
|
+
const st = computeStats(historyData)
|
|
246
289
|
if (st) {
|
|
247
|
-
html += '<div class="ss-tooltip-stats">'
|
|
248
|
-
html += '<span>Min: ' + formatStatNum(st.min, unit) + '</span>'
|
|
249
|
-
html += '<span>Max: ' + formatStatNum(st.max, unit) + '</span>'
|
|
250
|
-
html += '<span>Avg: ' + formatStatNum(st.avg, unit) + '</span>'
|
|
251
|
-
html += '</div>'
|
|
290
|
+
html += '<div class="ss-tooltip-stats">'
|
|
291
|
+
html += '<span>Min: ' + formatStatNum(st.min, unit) + '</span>'
|
|
292
|
+
html += '<span>Max: ' + formatStatNum(st.max, unit) + '</span>'
|
|
293
|
+
html += '<span>Avg: ' + formatStatNum(st.avg, unit) + '</span>'
|
|
294
|
+
html += '</div>'
|
|
252
295
|
}
|
|
253
296
|
if (details) {
|
|
254
|
-
html += '<div class="ss-tooltip-details">' + esc(details) + '</div>'
|
|
297
|
+
html += '<div class="ss-tooltip-details">' + esc(details) + '</div>'
|
|
255
298
|
}
|
|
256
299
|
if (historyData && historyData.length > 0) {
|
|
257
|
-
html += '<div class="ss-tooltip-sparkline" data-sparkline></div>'
|
|
258
|
-
html +=
|
|
300
|
+
html += '<div class="ss-tooltip-sparkline" data-sparkline></div>'
|
|
301
|
+
html +=
|
|
302
|
+
'<div class="ss-tooltip-samples">Last ' +
|
|
303
|
+
Math.min(historyData.length, MAX_HISTORY) +
|
|
304
|
+
' samples (~' +
|
|
305
|
+
Math.round((Math.min(historyData.length, MAX_HISTORY) * 3) / 60) +
|
|
306
|
+
' min)</div>'
|
|
259
307
|
}
|
|
260
|
-
html += '</div><div class="ss-tooltip-arrow"></div>'
|
|
261
|
-
tip.innerHTML = html
|
|
308
|
+
html += '</div><div class="ss-tooltip-arrow"></div>'
|
|
309
|
+
tip.innerHTML = html
|
|
262
310
|
|
|
263
|
-
bar.appendChild(tip)
|
|
264
|
-
positionTooltip(tip, badge)
|
|
265
|
-
activeTooltip = tip
|
|
311
|
+
bar.appendChild(tip)
|
|
312
|
+
positionTooltip(tip, badge)
|
|
313
|
+
activeTooltip = tip
|
|
266
314
|
|
|
267
315
|
if (pinned) {
|
|
268
|
-
const closeBtn = tip.querySelector('[data-ss-close]')
|
|
316
|
+
const closeBtn = tip.querySelector('[data-ss-close]')
|
|
269
317
|
if (closeBtn) {
|
|
270
318
|
closeBtn.addEventListener('click', (e) => {
|
|
271
|
-
e.stopPropagation()
|
|
272
|
-
unpinTooltip()
|
|
273
|
-
})
|
|
319
|
+
e.stopPropagation()
|
|
320
|
+
unpinTooltip()
|
|
321
|
+
})
|
|
274
322
|
}
|
|
275
323
|
}
|
|
276
324
|
|
|
277
|
-
const sparkContainer = tip.querySelector('[data-sparkline]')
|
|
325
|
+
const sparkContainer = tip.querySelector('[data-sparkline]')
|
|
278
326
|
if (sparkContainer && historyData && historyData.length > 0) {
|
|
279
|
-
renderSparkline(sparkContainer, historyData, color || '#34d399')
|
|
327
|
+
renderSparkline(sparkContainer, historyData, color || '#34d399')
|
|
280
328
|
}
|
|
281
|
-
}
|
|
329
|
+
}
|
|
282
330
|
|
|
283
331
|
const refreshPinnedTooltip = () => {
|
|
284
|
-
if (!pinnedBadge || !window.__ssLatest) return
|
|
285
|
-
const badgeId = pinnedBadge.id.replace('ss-b-', '')
|
|
286
|
-
const b = BADGES.find((x) => x.id === badgeId)
|
|
287
|
-
if (!b) return
|
|
288
|
-
const valEl = pinnedBadge.querySelector('.ss-value')
|
|
289
|
-
const currentVal = valEl ? valEl.textContent : ''
|
|
290
|
-
const hist = b.hist ?
|
|
291
|
-
const color = b.color ? hexFromClass(b.color(window.__ssLatest)) : '#34d399'
|
|
292
|
-
const title = typeof b.title === 'string' ? b.title : b.label
|
|
293
|
-
const details = typeof b.detail === 'function' ? b.detail(window.__ssLatest) :
|
|
294
|
-
showTooltip(pinnedBadge, hist, color, title, b.unit, currentVal, details, true)
|
|
295
|
-
}
|
|
332
|
+
if (!pinnedBadge || !window.__ssLatest) return
|
|
333
|
+
const badgeId = pinnedBadge.id.replace('ss-b-', '')
|
|
334
|
+
const b = BADGES.find((x) => x.id === badgeId)
|
|
335
|
+
if (!b) return
|
|
336
|
+
const valEl = pinnedBadge.querySelector('.ss-value')
|
|
337
|
+
const currentVal = valEl ? valEl.textContent : ''
|
|
338
|
+
const hist = b.hist ? history[b.hist] || [] : []
|
|
339
|
+
const color = b.color ? hexFromClass(b.color(window.__ssLatest)) : '#34d399'
|
|
340
|
+
const title = typeof b.title === 'string' ? b.title : b.label
|
|
341
|
+
const details = typeof b.detail === 'function' ? b.detail(window.__ssLatest) : b.detail || ''
|
|
342
|
+
showTooltip(pinnedBadge, hist, color, title, b.unit, currentVal, details, true)
|
|
343
|
+
}
|
|
296
344
|
|
|
297
345
|
// Close pinned tooltip on click outside
|
|
298
346
|
document.addEventListener('click', (e) => {
|
|
299
|
-
if (!pinnedBadge) return
|
|
347
|
+
if (!pinnedBadge) return
|
|
300
348
|
if (!pinnedBadge.contains(e.target) && !(activeTooltip && activeTooltip.contains(e.target))) {
|
|
301
|
-
unpinTooltip()
|
|
349
|
+
unpinTooltip()
|
|
302
350
|
}
|
|
303
|
-
})
|
|
351
|
+
})
|
|
304
352
|
|
|
305
353
|
// Close pinned tooltip on Escape
|
|
306
354
|
document.addEventListener('keydown', (e) => {
|
|
307
|
-
if (e.key === 'Escape' && pinnedBadge) unpinTooltip()
|
|
308
|
-
})
|
|
355
|
+
if (e.key === 'Escape' && pinnedBadge) unpinTooltip()
|
|
356
|
+
})
|
|
309
357
|
|
|
310
358
|
// ── Badge definitions ─────────────────────────────────────────────
|
|
311
359
|
// Compact badge defs: val (getValue), color (getColor→class), title, detail, hist (historyKey), unit, show (showIf), href
|
|
312
360
|
const BADGES = [
|
|
313
361
|
// Process
|
|
314
|
-
{
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
362
|
+
{
|
|
363
|
+
id: 'node',
|
|
364
|
+
label: 'NODE',
|
|
365
|
+
val: (s) => s.nodeVersion,
|
|
366
|
+
title: 'Node.js Runtime',
|
|
367
|
+
detail: 'Node.js version running the server process',
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
id: 'up',
|
|
371
|
+
label: 'UP',
|
|
372
|
+
val: (s) => formatUptime(s.uptime),
|
|
373
|
+
title: 'Process Uptime',
|
|
374
|
+
detail: (s) =>
|
|
375
|
+
'Process uptime: ' + formatUptime(s.uptime) + ' (' + Math.floor(s.uptime) + 's)',
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
id: 'cpu',
|
|
379
|
+
label: 'CPU',
|
|
380
|
+
val: (s) => s.cpuPercent.toFixed(1) + '%',
|
|
381
|
+
color: (s) => thresh(s.cpuPercent, 50, 80),
|
|
382
|
+
unit: '%',
|
|
383
|
+
title: 'CPU Usage',
|
|
384
|
+
detail: 'Percentage of one CPU core. >50% amber, >80% red.',
|
|
385
|
+
hist: 'cpuPercent',
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
id: 'evt',
|
|
389
|
+
label: 'EVT',
|
|
390
|
+
val: (s) => s.eventLoopLag.toFixed(1) + 'ms',
|
|
391
|
+
color: (s) => thresh(s.eventLoopLag, 20, 50),
|
|
392
|
+
unit: 'ms',
|
|
393
|
+
title: 'Event Loop Latency',
|
|
394
|
+
detail: 'Delay between scheduled and actual timer execution. >20ms amber, >50ms red.',
|
|
395
|
+
hist: 'eventLoopLag',
|
|
396
|
+
},
|
|
318
397
|
// Memory
|
|
319
|
-
{
|
|
320
|
-
|
|
321
|
-
|
|
398
|
+
{
|
|
399
|
+
id: 'mem',
|
|
400
|
+
label: 'MEM',
|
|
401
|
+
val: (s) => formatBytes(s.memHeapUsed),
|
|
402
|
+
unit: 'bytes',
|
|
403
|
+
title: 'V8 Heap Usage',
|
|
404
|
+
detail: (s) =>
|
|
405
|
+
'Heap: ' +
|
|
406
|
+
formatBytes(s.memHeapUsed) +
|
|
407
|
+
' used of ' +
|
|
408
|
+
formatBytes(s.memHeapTotal) +
|
|
409
|
+
' allocated',
|
|
410
|
+
hist: 'memHeapUsed',
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
id: 'rss',
|
|
414
|
+
label: 'RSS',
|
|
415
|
+
val: (s) => formatBytes(s.memRss),
|
|
416
|
+
unit: 'bytes',
|
|
417
|
+
title: 'Resident Set Size',
|
|
418
|
+
detail: 'Total OS memory footprint including heap, stack, and native allocations',
|
|
419
|
+
hist: 'memRss',
|
|
420
|
+
},
|
|
421
|
+
{
|
|
422
|
+
id: 'sys',
|
|
423
|
+
label: 'SYS',
|
|
424
|
+
val: (s) =>
|
|
425
|
+
formatMb(s.systemMemoryTotalMb - s.systemMemoryFreeMb) +
|
|
426
|
+
'/' +
|
|
427
|
+
formatMb(s.systemMemoryTotalMb),
|
|
428
|
+
unit: 'MB',
|
|
429
|
+
title: 'System Memory',
|
|
430
|
+
detail: (s) =>
|
|
431
|
+
formatMb(s.systemMemoryFreeMb) + ' free of ' + formatMb(s.systemMemoryTotalMb) + ' total',
|
|
432
|
+
hist: '_sysMemUsed',
|
|
433
|
+
},
|
|
322
434
|
// HTTP
|
|
323
|
-
{
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
435
|
+
{
|
|
436
|
+
id: 'rps',
|
|
437
|
+
label: 'REQ/s',
|
|
438
|
+
val: (s) => s.requestsPerSecond.toFixed(1),
|
|
439
|
+
unit: '/s',
|
|
440
|
+
title: 'Requests per Second',
|
|
441
|
+
detail: 'HTTP requests per second over a 60-second rolling window',
|
|
442
|
+
hist: 'requestsPerSecond',
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
id: 'avg',
|
|
446
|
+
label: 'AVG',
|
|
447
|
+
val: (s) => s.avgResponseTimeMs.toFixed(0) + 'ms',
|
|
448
|
+
color: (s) => thresh(s.avgResponseTimeMs, 200, 500),
|
|
449
|
+
unit: 'ms',
|
|
450
|
+
title: 'Avg Response Time',
|
|
451
|
+
detail: 'Average HTTP response time (60s window). >200ms amber, >500ms red.',
|
|
452
|
+
hist: 'avgResponseTimeMs',
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
id: 'err',
|
|
456
|
+
label: 'ERR',
|
|
457
|
+
val: (s) => s.errorRate.toFixed(1) + '%',
|
|
458
|
+
color: (s) => thresh(s.errorRate, 1, 5),
|
|
459
|
+
unit: '%',
|
|
460
|
+
title: 'Error Rate',
|
|
461
|
+
detail: '5xx error rate (60s window). >1% amber, >5% red.',
|
|
462
|
+
hist: 'errorRate',
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
id: 'conn',
|
|
466
|
+
label: 'CONN',
|
|
467
|
+
val: (s) => '' + s.activeHttpConnections,
|
|
468
|
+
title: 'Active Connections',
|
|
469
|
+
detail: 'Currently open HTTP connections',
|
|
470
|
+
hist: 'activeHttpConnections',
|
|
471
|
+
},
|
|
327
472
|
// DB
|
|
328
|
-
{
|
|
473
|
+
{
|
|
474
|
+
id: 'db',
|
|
475
|
+
label: 'DB',
|
|
476
|
+
val: (s) => s.dbPoolUsed + '/' + s.dbPoolFree + '/' + s.dbPoolMax,
|
|
477
|
+
color: (s) => ratioColor(s.dbPoolUsed, s.dbPoolMax),
|
|
478
|
+
title: 'Database Pool',
|
|
479
|
+
detail: (s) =>
|
|
480
|
+
'Used: ' +
|
|
481
|
+
s.dbPoolUsed +
|
|
482
|
+
', Free: ' +
|
|
483
|
+
s.dbPoolFree +
|
|
484
|
+
', Pending: ' +
|
|
485
|
+
s.dbPoolPending +
|
|
486
|
+
', Max: ' +
|
|
487
|
+
s.dbPoolMax,
|
|
488
|
+
hist: 'dbPoolUsed',
|
|
489
|
+
},
|
|
329
490
|
// Redis
|
|
330
|
-
{
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
491
|
+
{
|
|
492
|
+
id: 'redis',
|
|
493
|
+
label: 'REDIS',
|
|
494
|
+
val: (s) => (s.redisOk ? '\u2713' : '\u2717'),
|
|
495
|
+
color: (s) => (s.redisOk ? 'ss-green' : 'ss-red'),
|
|
496
|
+
title: 'Redis Status',
|
|
497
|
+
detail: (s) => (s.redisOk ? 'Redis is connected and responding' : 'Redis is not responding!'),
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
id: 'rmem',
|
|
501
|
+
label: 'MEM',
|
|
502
|
+
val: (s) => s.redisMemoryUsedMb.toFixed(1) + 'M',
|
|
503
|
+
unit: 'MB',
|
|
504
|
+
title: 'Redis Memory',
|
|
505
|
+
detail: (s) => 'Redis server memory usage: ' + s.redisMemoryUsedMb.toFixed(1) + ' MB',
|
|
506
|
+
hist: 'redisMemoryUsedMb',
|
|
507
|
+
show: (s) => s.redisOk,
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
id: 'rkeys',
|
|
511
|
+
label: 'KEYS',
|
|
512
|
+
val: (s) => formatCount(s.redisKeysCount),
|
|
513
|
+
title: 'Redis Keys',
|
|
514
|
+
detail: (s) => 'Total keys in Redis: ' + s.redisKeysCount,
|
|
515
|
+
hist: 'redisKeysCount',
|
|
516
|
+
show: (s) => s.redisOk,
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
id: 'rhit',
|
|
520
|
+
label: 'HIT',
|
|
521
|
+
val: (s) => s.redisHitRate.toFixed(0) + '%',
|
|
522
|
+
color: (s) => threshInverse(s.redisHitRate, 90, 70),
|
|
523
|
+
unit: '%',
|
|
524
|
+
title: 'Redis Hit Rate',
|
|
525
|
+
detail: 'Cache hit rate. <90% amber, <70% red.',
|
|
526
|
+
hist: 'redisHitRate',
|
|
527
|
+
show: (s) => s.redisOk,
|
|
528
|
+
},
|
|
334
529
|
// Queue
|
|
335
|
-
{
|
|
336
|
-
|
|
530
|
+
{
|
|
531
|
+
id: 'q',
|
|
532
|
+
label: 'Q',
|
|
533
|
+
val: (s) => s.queueActive + '/' + s.queueWaiting + '/' + s.queueDelayed,
|
|
534
|
+
color: (s) => (s.queueFailed > 0 ? 'ss-amber' : 'ss-green'),
|
|
535
|
+
title: 'Job Queue',
|
|
536
|
+
detail: (s) =>
|
|
537
|
+
'Active: ' +
|
|
538
|
+
s.queueActive +
|
|
539
|
+
', Waiting: ' +
|
|
540
|
+
s.queueWaiting +
|
|
541
|
+
', Delayed: ' +
|
|
542
|
+
s.queueDelayed +
|
|
543
|
+
', Failed: ' +
|
|
544
|
+
s.queueFailed,
|
|
545
|
+
hist: 'queueActive',
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
id: 'workers',
|
|
549
|
+
label: 'WORKERS',
|
|
550
|
+
val: (s) => '' + s.queueWorkerCount,
|
|
551
|
+
title: 'Queue Workers',
|
|
552
|
+
detail: (s) => 'Connected queue worker processes: ' + s.queueWorkerCount,
|
|
553
|
+
},
|
|
337
554
|
// App
|
|
338
|
-
{
|
|
339
|
-
|
|
340
|
-
|
|
555
|
+
{
|
|
556
|
+
id: 'users',
|
|
557
|
+
label: 'USERS',
|
|
558
|
+
val: (s) => '' + s.onlineUsers,
|
|
559
|
+
title: 'Online Users',
|
|
560
|
+
detail: 'Active user sessions (via Transmit)',
|
|
561
|
+
hist: 'onlineUsers',
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
id: 'hooks',
|
|
565
|
+
label: 'HOOKS',
|
|
566
|
+
val: (s) => '' + s.pendingWebhooks,
|
|
567
|
+
color: (s) => (s.pendingWebhooks > 100 ? 'ss-amber' : 'ss-green'),
|
|
568
|
+
title: 'Pending Webhooks',
|
|
569
|
+
detail: 'Webhook events awaiting delivery. >100 amber.',
|
|
570
|
+
hist: 'pendingWebhooks',
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
id: 'mail',
|
|
574
|
+
label: 'MAIL',
|
|
575
|
+
val: (s) => '' + s.pendingEmails,
|
|
576
|
+
color: (s) => (s.pendingEmails > 100 ? 'ss-amber' : 'ss-green'),
|
|
577
|
+
title: 'Pending Emails',
|
|
578
|
+
detail: 'Scheduled emails awaiting send. >100 amber.',
|
|
579
|
+
hist: 'pendingEmails',
|
|
580
|
+
},
|
|
341
581
|
// Logs
|
|
342
|
-
{
|
|
343
|
-
|
|
344
|
-
|
|
582
|
+
{
|
|
583
|
+
id: 'logerr',
|
|
584
|
+
label: 'LOG ERR',
|
|
585
|
+
val: (s) => '' + s.logErrorsLast5m,
|
|
586
|
+
color: (s) =>
|
|
587
|
+
s.logErrorsLast5m > 0 ? 'ss-red' : s.logWarningsLast5m > 0 ? 'ss-amber' : 'ss-green',
|
|
588
|
+
title: 'Log Errors (5m)',
|
|
589
|
+
detail: (s) =>
|
|
590
|
+
s.logErrorsLast5m +
|
|
591
|
+
' error/fatal entries and ' +
|
|
592
|
+
s.logWarningsLast5m +
|
|
593
|
+
' warnings in the last 5 minutes',
|
|
594
|
+
hist: 'logErrorsLast5m',
|
|
595
|
+
href: '/admin/logs?hasError=true',
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
id: 'lograte',
|
|
599
|
+
label: 'LOG/m',
|
|
600
|
+
val: (s) => '' + s.logEntriesPerMinute,
|
|
601
|
+
unit: '/m',
|
|
602
|
+
title: 'Log Rate',
|
|
603
|
+
detail: (s) => s.logEntriesLast5m + ' total entries in the last 5 minutes',
|
|
604
|
+
hist: 'logEntriesPerMinute',
|
|
605
|
+
href: '/admin/logs',
|
|
606
|
+
},
|
|
607
|
+
]
|
|
345
608
|
|
|
346
609
|
// ── State ─────────────────────────────────────────────────────────
|
|
347
|
-
const history = {}
|
|
348
|
-
let lastSuccess = 0
|
|
349
|
-
let visible = localStorage.getItem('admin:stats-bar') !== 'hidden'
|
|
610
|
+
const history = {}
|
|
611
|
+
let lastSuccess = 0
|
|
612
|
+
let visible = localStorage.getItem('admin:stats-bar') !== 'hidden'
|
|
350
613
|
|
|
351
614
|
// Apply initial visibility
|
|
352
|
-
applyVisibility()
|
|
615
|
+
applyVisibility()
|
|
353
616
|
|
|
354
617
|
function applyVisibility() {
|
|
355
|
-
bar.className = visible ? 'ss-bar' : 'ss-bar ss-hidden'
|
|
356
|
-
toggle.className = visible ? 'ss-toggle ss-visible' : 'ss-toggle ss-collapsed'
|
|
357
|
-
toggle.title = visible ? 'Hide stats bar' : 'Show stats bar'
|
|
358
|
-
const arrow = toggle.querySelector('.ss-toggle-arrow')
|
|
359
|
-
if (arrow) arrow.textContent = visible ? '\u25BC' : '\u25B2'
|
|
360
|
-
const label = toggle.querySelector('.ss-toggle-label')
|
|
361
|
-
if (label) label.textContent = visible ? 'hide stats' : ''
|
|
362
|
-
if (toggleSummary) toggleSummary.style.display = visible ? 'none' : 'flex'
|
|
618
|
+
bar.className = visible ? 'ss-bar' : 'ss-bar ss-hidden'
|
|
619
|
+
toggle.className = visible ? 'ss-toggle ss-visible' : 'ss-toggle ss-collapsed'
|
|
620
|
+
toggle.title = visible ? 'Hide stats bar' : 'Show stats bar'
|
|
621
|
+
const arrow = toggle.querySelector('.ss-toggle-arrow')
|
|
622
|
+
if (arrow) arrow.textContent = visible ? '\u25BC' : '\u25B2'
|
|
623
|
+
const label = toggle.querySelector('.ss-toggle-label')
|
|
624
|
+
if (label) label.textContent = visible ? 'hide stats' : ''
|
|
625
|
+
if (toggleSummary) toggleSummary.style.display = visible ? 'none' : 'flex'
|
|
363
626
|
}
|
|
364
627
|
|
|
365
628
|
toggle.addEventListener('click', () => {
|
|
366
|
-
visible = !visible
|
|
367
|
-
localStorage.setItem('admin:stats-bar', visible ? 'visible' : 'hidden')
|
|
368
|
-
applyVisibility()
|
|
369
|
-
})
|
|
629
|
+
visible = !visible
|
|
630
|
+
localStorage.setItem('admin:stats-bar', visible ? 'visible' : 'hidden')
|
|
631
|
+
applyVisibility()
|
|
632
|
+
})
|
|
370
633
|
|
|
371
634
|
// ── Fetch & Update ────────────────────────────────────────────────
|
|
372
635
|
const updateDom = (stats) => {
|
|
373
|
-
window.__ssLatest = stats
|
|
374
|
-
lastSuccess = Date.now()
|
|
375
|
-
if (dot) dot.className = 'ss-dot'
|
|
636
|
+
window.__ssLatest = stats
|
|
637
|
+
lastSuccess = Date.now()
|
|
638
|
+
if (dot) dot.className = 'ss-dot'
|
|
376
639
|
|
|
377
640
|
BADGES.forEach((b) => {
|
|
378
|
-
const el = document.getElementById('ss-b-' + b.id)
|
|
379
|
-
if (!el) return
|
|
641
|
+
const el = document.getElementById('ss-b-' + b.id)
|
|
642
|
+
if (!el) return
|
|
380
643
|
|
|
381
644
|
// Conditional visibility
|
|
382
645
|
if (b.show) {
|
|
383
|
-
el.style.display = b.show(stats) ? 'flex' : 'none'
|
|
384
|
-
if (!b.show(stats)) return
|
|
646
|
+
el.style.display = b.show(stats) ? 'flex' : 'none'
|
|
647
|
+
if (!b.show(stats)) return
|
|
385
648
|
}
|
|
386
649
|
|
|
387
650
|
// Update value
|
|
388
|
-
const valEl = el.querySelector('.ss-value')
|
|
651
|
+
const valEl = el.querySelector('.ss-value')
|
|
389
652
|
if (valEl) {
|
|
390
|
-
valEl.textContent = b.val(stats)
|
|
653
|
+
valEl.textContent = b.val(stats)
|
|
391
654
|
|
|
392
655
|
// Update color
|
|
393
656
|
if (b.color) {
|
|
394
|
-
valEl.classList.remove('ss-green', 'ss-amber', 'ss-red', 'ss-muted')
|
|
395
|
-
valEl.classList.add(b.color(stats))
|
|
657
|
+
valEl.classList.remove('ss-green', 'ss-amber', 'ss-red', 'ss-muted')
|
|
658
|
+
valEl.classList.add(b.color(stats))
|
|
396
659
|
}
|
|
397
660
|
}
|
|
398
661
|
|
|
399
662
|
// Push history
|
|
400
663
|
if (b.hist) {
|
|
401
|
-
let val
|
|
664
|
+
let val
|
|
402
665
|
if (b.hist === '_sysMemUsed') {
|
|
403
|
-
val = stats.systemMemoryTotalMb - stats.systemMemoryFreeMb
|
|
666
|
+
val = stats.systemMemoryTotalMb - stats.systemMemoryFreeMb
|
|
404
667
|
} else {
|
|
405
|
-
val = stats[b.hist]
|
|
668
|
+
val = stats[b.hist]
|
|
406
669
|
}
|
|
407
670
|
if (typeof val === 'number') {
|
|
408
|
-
if (!history[b.hist]) history[b.hist] = []
|
|
409
|
-
history[b.hist].push(val)
|
|
410
|
-
if (history[b.hist].length > MAX_HISTORY) history[b.hist].shift()
|
|
671
|
+
if (!history[b.hist]) history[b.hist] = []
|
|
672
|
+
history[b.hist].push(val)
|
|
673
|
+
if (history[b.hist].length > MAX_HISTORY) history[b.hist].shift()
|
|
411
674
|
}
|
|
412
675
|
}
|
|
413
|
-
})
|
|
676
|
+
})
|
|
414
677
|
|
|
415
678
|
// Update toggle summary for collapsed state
|
|
416
679
|
if (toggleSummary) {
|
|
417
|
-
const cpuEl = toggleSummary.querySelector('[data-ts=cpu]')
|
|
418
|
-
const memEl = toggleSummary.querySelector('[data-ts=mem]')
|
|
419
|
-
const redisEl = toggleSummary.querySelector('[data-ts=redis]')
|
|
420
|
-
if (cpuEl) {
|
|
421
|
-
|
|
422
|
-
|
|
680
|
+
const cpuEl = toggleSummary.querySelector('[data-ts=cpu]')
|
|
681
|
+
const memEl = toggleSummary.querySelector('[data-ts=mem]')
|
|
682
|
+
const redisEl = toggleSummary.querySelector('[data-ts=redis]')
|
|
683
|
+
if (cpuEl) {
|
|
684
|
+
cpuEl.textContent = stats.cpuPercent.toFixed(0) + '%'
|
|
685
|
+
cpuEl.className = 'ss-value ' + thresh(stats.cpuPercent, 50, 80)
|
|
686
|
+
}
|
|
687
|
+
if (memEl) {
|
|
688
|
+
memEl.textContent = formatBytes(stats.memHeapUsed)
|
|
689
|
+
memEl.className = 'ss-value ss-green'
|
|
690
|
+
}
|
|
691
|
+
if (redisEl) {
|
|
692
|
+
redisEl.textContent = stats.redisOk ? '\u2713' : '\u2717'
|
|
693
|
+
redisEl.className = 'ss-value ' + (stats.redisOk ? 'ss-green' : 'ss-red')
|
|
694
|
+
}
|
|
423
695
|
}
|
|
424
696
|
|
|
425
|
-
refreshPinnedTooltip()
|
|
426
|
-
}
|
|
697
|
+
refreshPinnedTooltip()
|
|
698
|
+
}
|
|
427
699
|
|
|
428
700
|
const doFetch = () => {
|
|
429
701
|
fetch(ENDPOINT, { credentials: 'same-origin' })
|
|
430
702
|
.then((r) => {
|
|
431
703
|
if (r.status === 403 || r.status === 401) {
|
|
432
|
-
bar.className = 'ss-bar ss-hidden'
|
|
433
|
-
toggle.style.display = 'none'
|
|
434
|
-
return null
|
|
704
|
+
bar.className = 'ss-bar ss-hidden'
|
|
705
|
+
toggle.style.display = 'none'
|
|
706
|
+
return null
|
|
435
707
|
}
|
|
436
|
-
return r.json()
|
|
708
|
+
return r.json()
|
|
437
709
|
})
|
|
438
710
|
.then((data) => {
|
|
439
|
-
if (data) updateDom(data)
|
|
711
|
+
if (data) updateDom(data)
|
|
440
712
|
})
|
|
441
713
|
.catch(() => {
|
|
442
714
|
// network error — mark stale
|
|
443
|
-
})
|
|
444
|
-
}
|
|
715
|
+
})
|
|
716
|
+
}
|
|
445
717
|
|
|
446
718
|
// Stale detection
|
|
447
719
|
setInterval(() => {
|
|
448
720
|
if (lastSuccess > 0 && Date.now() - lastSuccess > STALE_MS && dot) {
|
|
449
|
-
dot.className = 'ss-dot ss-stale'
|
|
721
|
+
dot.className = 'ss-dot ss-stale'
|
|
450
722
|
}
|
|
451
|
-
}, 2000)
|
|
723
|
+
}, 2000)
|
|
452
724
|
|
|
453
725
|
// ── Tooltip event binding ─────────────────────────────────────────
|
|
454
726
|
const getBadgeTooltipData = (b, el) => {
|
|
455
|
-
const valEl = el.querySelector('.ss-value')
|
|
456
|
-
const currentVal = valEl ? valEl.textContent : ''
|
|
457
|
-
const hist = b.hist ?
|
|
458
|
-
let color = '#34d399'
|
|
727
|
+
const valEl = el.querySelector('.ss-value')
|
|
728
|
+
const currentVal = valEl ? valEl.textContent : ''
|
|
729
|
+
const hist = b.hist ? history[b.hist] || [] : []
|
|
730
|
+
let color = '#34d399'
|
|
459
731
|
if (b.color && window.__ssLatest) {
|
|
460
|
-
color = hexFromClass(b.color(window.__ssLatest))
|
|
732
|
+
color = hexFromClass(b.color(window.__ssLatest))
|
|
461
733
|
}
|
|
462
|
-
const title = typeof b.title === 'string' ? b.title : b.label
|
|
463
|
-
let details = ''
|
|
734
|
+
const title = typeof b.title === 'string' ? b.title : b.label
|
|
735
|
+
let details = ''
|
|
464
736
|
if (b.detail) {
|
|
465
|
-
details =
|
|
466
|
-
|
|
467
|
-
|
|
737
|
+
details =
|
|
738
|
+
typeof b.detail === 'function'
|
|
739
|
+
? window.__ssLatest
|
|
740
|
+
? b.detail(window.__ssLatest)
|
|
741
|
+
: b.detail({})
|
|
742
|
+
: b.detail
|
|
468
743
|
}
|
|
469
|
-
return { hist, color, title, details, currentVal, unit: b.unit }
|
|
470
|
-
}
|
|
744
|
+
return { hist, color, title, details, currentVal, unit: b.unit }
|
|
745
|
+
}
|
|
471
746
|
|
|
472
747
|
BADGES.forEach((b) => {
|
|
473
|
-
const el = document.getElementById('ss-b-' + b.id)
|
|
474
|
-
if (!el) return
|
|
748
|
+
const el = document.getElementById('ss-b-' + b.id)
|
|
749
|
+
if (!el) return
|
|
475
750
|
|
|
476
751
|
// Hover: show tooltip preview (non-pinned)
|
|
477
752
|
el.addEventListener('mouseenter', () => {
|
|
478
|
-
if (pinnedBadge) return
|
|
479
|
-
const d = getBadgeTooltipData(b, el)
|
|
480
|
-
showTooltip(el, d.hist, d.color, d.title, d.unit, d.currentVal, d.details, false)
|
|
481
|
-
})
|
|
753
|
+
if (pinnedBadge) return
|
|
754
|
+
const d = getBadgeTooltipData(b, el)
|
|
755
|
+
showTooltip(el, d.hist, d.color, d.title, d.unit, d.currentVal, d.details, false)
|
|
756
|
+
})
|
|
482
757
|
el.addEventListener('mouseleave', () => {
|
|
483
|
-
if (pinnedBadge) return
|
|
484
|
-
hideCurrentTooltip()
|
|
485
|
-
})
|
|
758
|
+
if (pinnedBadge) return
|
|
759
|
+
hideCurrentTooltip()
|
|
760
|
+
})
|
|
486
761
|
|
|
487
762
|
// Click: pin/unpin tooltip
|
|
488
763
|
el.addEventListener('click', (e) => {
|
|
489
|
-
e.stopPropagation()
|
|
764
|
+
e.stopPropagation()
|
|
490
765
|
if (b.href && pinnedBadge === el) {
|
|
491
|
-
window.location.href = b.href
|
|
492
|
-
return
|
|
766
|
+
window.location.href = b.href
|
|
767
|
+
return
|
|
493
768
|
}
|
|
494
769
|
if (pinnedBadge === el) {
|
|
495
|
-
unpinTooltip()
|
|
496
|
-
return
|
|
770
|
+
unpinTooltip()
|
|
771
|
+
return
|
|
497
772
|
}
|
|
498
|
-
unpinTooltip()
|
|
499
|
-
pinnedBadge = el
|
|
500
|
-
el.classList.add('ss-pinned')
|
|
501
|
-
const d = getBadgeTooltipData(b, el)
|
|
502
|
-
showTooltip(el, d.hist, d.color, d.title, d.unit, d.currentVal, d.details, true)
|
|
503
|
-
})
|
|
504
|
-
})
|
|
773
|
+
unpinTooltip()
|
|
774
|
+
pinnedBadge = el
|
|
775
|
+
el.classList.add('ss-pinned')
|
|
776
|
+
const d = getBadgeTooltipData(b, el)
|
|
777
|
+
showTooltip(el, d.hist, d.color, d.title, d.unit, d.currentVal, d.details, true)
|
|
778
|
+
})
|
|
779
|
+
})
|
|
505
780
|
|
|
506
781
|
// ── Horizontal wheel scroll on the bar ──────────────────────────────
|
|
507
|
-
const scrollEl = document.getElementById('ss-bar-scroll')
|
|
782
|
+
const scrollEl = document.getElementById('ss-bar-scroll')
|
|
508
783
|
if (scrollEl) {
|
|
509
|
-
scrollEl.addEventListener(
|
|
510
|
-
|
|
511
|
-
e
|
|
512
|
-
|
|
513
|
-
|
|
784
|
+
scrollEl.addEventListener(
|
|
785
|
+
'wheel',
|
|
786
|
+
(e) => {
|
|
787
|
+
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return
|
|
788
|
+
e.preventDefault()
|
|
789
|
+
scrollEl.scrollLeft += e.deltaY
|
|
790
|
+
},
|
|
791
|
+
{ passive: false }
|
|
792
|
+
)
|
|
514
793
|
scrollEl.addEventListener('scroll', () => {
|
|
515
|
-
if (pinnedBadge && activeTooltip) positionTooltip(activeTooltip, pinnedBadge)
|
|
516
|
-
})
|
|
794
|
+
if (pinnedBadge && activeTooltip) positionTooltip(activeTooltip, pinnedBadge)
|
|
795
|
+
})
|
|
517
796
|
}
|
|
518
797
|
|
|
519
798
|
// ── Start polling ─────────────────────────────────────────────────
|
|
520
|
-
doFetch()
|
|
521
|
-
setInterval(doFetch, INTERVAL)
|
|
522
|
-
})()
|
|
799
|
+
doFetch()
|
|
800
|
+
setInterval(doFetch, INTERVAL)
|
|
801
|
+
})()
|