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.
Files changed (84) hide show
  1. package/README.md +272 -142
  2. package/dist/configure.d.ts.map +1 -1
  3. package/dist/src/controller/debug_controller.d.ts +2 -2
  4. package/dist/src/controller/debug_controller.d.ts.map +1 -1
  5. package/dist/src/controller/server_stats_controller.d.ts +1 -1
  6. package/dist/src/controller/server_stats_controller.d.ts.map +1 -1
  7. package/dist/src/dashboard/chart_aggregator.d.ts.map +1 -1
  8. package/dist/src/dashboard/chart_aggregator.js +8 -8
  9. package/dist/src/dashboard/dashboard_controller.d.ts +12 -97
  10. package/dist/src/dashboard/dashboard_controller.d.ts.map +1 -1
  11. package/dist/src/dashboard/dashboard_controller.js +244 -522
  12. package/dist/src/dashboard/dashboard_routes.d.ts.map +1 -1
  13. package/dist/src/dashboard/dashboard_routes.js +7 -2
  14. package/dist/src/dashboard/dashboard_store.d.ts +6 -3
  15. package/dist/src/dashboard/dashboard_store.d.ts.map +1 -1
  16. package/dist/src/dashboard/dashboard_store.js +54 -78
  17. package/dist/src/dashboard/integrations/cache_inspector.d.ts.map +1 -1
  18. package/dist/src/dashboard/integrations/queue_inspector.d.ts.map +1 -1
  19. package/dist/src/dashboard/migrator.d.ts.map +1 -1
  20. package/dist/src/dashboard/migrator.js +3 -1
  21. package/dist/src/dashboard/models/stats_event.d.ts +1 -1
  22. package/dist/src/dashboard/models/stats_event.d.ts.map +1 -1
  23. package/dist/src/dashboard/models/stats_query.d.ts +1 -1
  24. package/dist/src/dashboard/models/stats_query.d.ts.map +1 -1
  25. package/dist/src/dashboard/models/stats_request.d.ts +2 -2
  26. package/dist/src/dashboard/models/stats_request.d.ts.map +1 -1
  27. package/dist/src/dashboard/models/stats_request.js +1 -1
  28. package/dist/src/dashboard/models/stats_trace.d.ts +1 -1
  29. package/dist/src/dashboard/models/stats_trace.d.ts.map +1 -1
  30. package/dist/src/debug/debug_store.d.ts +6 -6
  31. package/dist/src/debug/debug_store.d.ts.map +1 -1
  32. package/dist/src/debug/debug_store.js +10 -10
  33. package/dist/src/debug/email_collector.d.ts +0 -9
  34. package/dist/src/debug/email_collector.d.ts.map +1 -1
  35. package/dist/src/debug/email_collector.js +6 -28
  36. package/dist/src/debug/event_collector.d.ts +1 -1
  37. package/dist/src/debug/event_collector.d.ts.map +1 -1
  38. package/dist/src/debug/event_collector.js +17 -17
  39. package/dist/src/debug/query_collector.d.ts +1 -1
  40. package/dist/src/debug/query_collector.d.ts.map +1 -1
  41. package/dist/src/debug/query_collector.js +13 -14
  42. package/dist/src/debug/ring_buffer.d.ts.map +1 -1
  43. package/dist/src/debug/route_inspector.d.ts +1 -1
  44. package/dist/src/debug/route_inspector.d.ts.map +1 -1
  45. package/dist/src/debug/route_inspector.js +12 -12
  46. package/dist/src/debug/trace_collector.d.ts.map +1 -1
  47. package/dist/src/debug/trace_collector.js +6 -5
  48. package/dist/src/edge/client/dashboard.css +516 -171
  49. package/dist/src/edge/client/dashboard.js +2756 -1662
  50. package/dist/src/edge/client/debug-panel.css +476 -133
  51. package/dist/src/edge/client/debug-panel.js +1496 -1043
  52. package/dist/src/edge/client/stats-bar.css +64 -30
  53. package/dist/src/edge/client/stats-bar.js +598 -319
  54. package/dist/src/edge/plugin.d.ts +1 -1
  55. package/dist/src/edge/plugin.d.ts.map +1 -1
  56. package/dist/src/edge/plugin.js +41 -59
  57. package/dist/src/edge/views/stats-bar.edge +1 -1
  58. package/dist/src/index.d.ts +1 -1
  59. package/dist/src/index.d.ts.map +1 -1
  60. package/dist/src/middleware/request_tracking_middleware.d.ts +4 -4
  61. package/dist/src/middleware/request_tracking_middleware.d.ts.map +1 -1
  62. package/dist/src/middleware/request_tracking_middleware.js +7 -6
  63. package/dist/src/prometheus/prometheus_collector.d.ts +1 -1
  64. package/dist/src/prometheus/prometheus_collector.d.ts.map +1 -1
  65. package/dist/src/provider/server_stats_provider.d.ts +1 -1
  66. package/dist/src/provider/server_stats_provider.d.ts.map +1 -1
  67. package/dist/src/provider/server_stats_provider.js +33 -32
  68. package/dist/src/types.d.ts +2 -2
  69. package/dist/src/utils/json_helpers.d.ts +8 -0
  70. package/dist/src/utils/json_helpers.d.ts.map +1 -0
  71. package/dist/src/utils/json_helpers.js +21 -0
  72. package/dist/src/utils/mail_helpers.d.ts +13 -0
  73. package/dist/src/utils/mail_helpers.d.ts.map +1 -0
  74. package/dist/src/utils/mail_helpers.js +26 -0
  75. package/dist/src/utils/math_helpers.d.ts +8 -0
  76. package/dist/src/utils/math_helpers.d.ts.map +1 -0
  77. package/dist/src/utils/math_helpers.js +11 -0
  78. package/dist/src/utils/time_helpers.d.ts +12 -0
  79. package/dist/src/utils/time_helpers.d.ts.map +1 -0
  80. package/dist/src/utils/time_helpers.js +32 -0
  81. package/dist/src/utils/transmit_client.d.ts +9 -0
  82. package/dist/src/utils/transmit_client.d.ts.map +1 -0
  83. package/dist/src/utils/transmit_client.js +20 -0
  84. 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) => v < crit ? 'ss-red' : v < warn ? 'ss-amber' : 'ss-green';
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 = { 'ss-red': '#f87171', 'ss-amber': '#fbbf24', 'ss-green': '#34d399', 'ss-muted': '#737373' };
94
- const HEX_VAR = { 'ss-red': '--ss-red-fg', 'ss-amber': '--ss-amber-fg', 'ss-green': '--ss-accent', 'ss-muted': '--ss-muted' };
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, h = 32, pad = 2;
116
- const svg = document.createElementNS(SVG_NS, 'svg');
117
- svg.setAttribute('width', '' + w);
118
- svg.setAttribute('height', '' + h);
119
- svg.style.display = 'block';
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', '' + (w / 2));
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, ih = h - pad * 2;
135
- const mn = Math.min(...data), mx = Math.max(...data);
136
- const range = mx - mn || 1;
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'); grad.setAttribute('y1', '0');
148
- grad.setAttribute('x2', '0'); grad.setAttribute('y2', '1');
149
- const s1 = document.createElementNS(SVG_NS, 'stop');
150
- s1.setAttribute('offset', '0%'); s1.setAttribute('stop-color', color); s1.setAttribute('stop-opacity', '0.25');
151
- const s2 = document.createElementNS(SVG_NS, 'stop');
152
- s2.setAttribute('offset', '100%'); s2.setAttribute('stop-color', color); s2.setAttribute('stop-opacity', '0.02');
153
- grad.appendChild(s1); grad.appendChild(s2);
154
- defs.appendChild(grad);
155
- svg.appendChild(defs);
156
-
157
- const lastX = (pad + iw).toFixed(1);
158
- const lastY = (pad + ih).toFixed(1);
159
- const firstX = pad.toFixed(1);
160
- const areaD = 'M' + pts[0] + ' ' + pts.slice(1).map((p) => 'L' + p).join(' ')
161
- + ' L' + lastX + ',' + lastY + ' L' + firstX + ',' + lastY + ' Z';
162
- const area = document.createElementNS(SVG_NS, 'path');
163
- area.setAttribute('d', areaD);
164
- area.setAttribute('fill', 'url(#' + gradId + ')');
165
- svg.appendChild(area);
166
-
167
- const line = document.createElementNS(SVG_NS, 'polyline');
168
- line.setAttribute('points', pts.join(' '));
169
- line.setAttribute('fill', 'none');
170
- line.setAttribute('stroke', color);
171
- line.setAttribute('stroke-width', '1.5');
172
- line.setAttribute('stroke-linejoin', 'round');
173
- line.setAttribute('stroke-linecap', 'round');
174
- svg.appendChild(line);
175
-
176
- container.appendChild(svg);
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.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
192
- };
227
+ if (typeof s !== 'string') s = '' + s
228
+ return s
229
+ .replace(/&/g, '&amp;')
230
+ .replace(/</g, '&lt;')
231
+ .replace(/>/g, '&gt;')
232
+ .replace(/"/g, '&quot;')
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 += '<div class="ss-tooltip-header"><span class="ss-tooltip-title">' + esc(title) + '</span>';
240
- if (unit) html += '<span class="ss-tooltip-unit">' + esc(unit) + '</span>';
241
- html += '</div>';
242
- html += '<div class="ss-tooltip-current"><span class="ss-tooltip-current-label">Current: </span>';
243
- html += '<span class="ss-tooltip-current-value">' + esc(currentValue) + '</span></div>';
244
-
245
- const st = computeStats(historyData);
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 += '<div class="ss-tooltip-samples">Last ' + Math.min(historyData.length, MAX_HISTORY) + ' samples (~' + Math.round(Math.min(historyData.length, MAX_HISTORY) * 3 / 60) + ' min)</div>';
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 ? (history[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) : (b.detail || '');
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
- { id: 'node', label: 'NODE', val: (s) => s.nodeVersion, title: 'Node.js Runtime', detail: 'Node.js version running the server process' },
315
- { id: 'up', label: 'UP', val: (s) => formatUptime(s.uptime), title: 'Process Uptime', detail: (s) => 'Process uptime: ' + formatUptime(s.uptime) + ' (' + Math.floor(s.uptime) + 's)' },
316
- { id: 'cpu', label: 'CPU', val: (s) => s.cpuPercent.toFixed(1) + '%', color: (s) => thresh(s.cpuPercent, 50, 80), unit: '%', title: 'CPU Usage', detail: 'Percentage of one CPU core. >50% amber, >80% red.', hist: 'cpuPercent' },
317
- { id: 'evt', label: 'EVT', val: (s) => s.eventLoopLag.toFixed(1) + 'ms', color: (s) => thresh(s.eventLoopLag, 20, 50), unit: 'ms', title: 'Event Loop Latency', detail: 'Delay between scheduled and actual timer execution. >20ms amber, >50ms red.', hist: 'eventLoopLag' },
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
- { id: 'mem', label: 'MEM', val: (s) => formatBytes(s.memHeapUsed), unit: 'bytes', title: 'V8 Heap Usage', detail: (s) => 'Heap: ' + formatBytes(s.memHeapUsed) + ' used of ' + formatBytes(s.memHeapTotal) + ' allocated', hist: 'memHeapUsed' },
320
- { id: 'rss', label: 'RSS', val: (s) => formatBytes(s.memRss), unit: 'bytes', title: 'Resident Set Size', detail: 'Total OS memory footprint including heap, stack, and native allocations', hist: 'memRss' },
321
- { id: 'sys', label: 'SYS', val: (s) => formatMb(s.systemMemoryTotalMb - s.systemMemoryFreeMb) + '/' + formatMb(s.systemMemoryTotalMb), unit: 'MB', title: 'System Memory', detail: (s) => formatMb(s.systemMemoryFreeMb) + ' free of ' + formatMb(s.systemMemoryTotalMb) + ' total', hist: '_sysMemUsed' },
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
- { id: 'rps', label: 'REQ/s', val: (s) => s.requestsPerSecond.toFixed(1), unit: '/s', title: 'Requests per Second', detail: 'HTTP requests per second over a 60-second rolling window', hist: 'requestsPerSecond' },
324
- { id: 'avg', label: 'AVG', val: (s) => s.avgResponseTimeMs.toFixed(0) + 'ms', color: (s) => thresh(s.avgResponseTimeMs, 200, 500), unit: 'ms', title: 'Avg Response Time', detail: 'Average HTTP response time (60s window). >200ms amber, >500ms red.', hist: 'avgResponseTimeMs' },
325
- { id: 'err', label: 'ERR', val: (s) => s.errorRate.toFixed(1) + '%', color: (s) => thresh(s.errorRate, 1, 5), unit: '%', title: 'Error Rate', detail: '5xx error rate (60s window). >1% amber, >5% red.', hist: 'errorRate' },
326
- { id: 'conn', label: 'CONN', val: (s) => '' + s.activeHttpConnections, title: 'Active Connections', detail: 'Currently open HTTP connections', hist: 'activeHttpConnections' },
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
- { id: 'db', label: 'DB', val: (s) => s.dbPoolUsed + '/' + s.dbPoolFree + '/' + s.dbPoolMax, color: (s) => ratioColor(s.dbPoolUsed, s.dbPoolMax), title: 'Database Pool', detail: (s) => 'Used: ' + s.dbPoolUsed + ', Free: ' + s.dbPoolFree + ', Pending: ' + s.dbPoolPending + ', Max: ' + s.dbPoolMax, hist: 'dbPoolUsed' },
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
- { id: 'redis', label: 'REDIS', val: (s) => s.redisOk ? '\u2713' : '\u2717', color: (s) => s.redisOk ? 'ss-green' : 'ss-red', title: 'Redis Status', detail: (s) => s.redisOk ? 'Redis is connected and responding' : 'Redis is not responding!' },
331
- { id: 'rmem', label: 'MEM', val: (s) => s.redisMemoryUsedMb.toFixed(1) + 'M', unit: 'MB', title: 'Redis Memory', detail: (s) => 'Redis server memory usage: ' + s.redisMemoryUsedMb.toFixed(1) + ' MB', hist: 'redisMemoryUsedMb', show: (s) => s.redisOk },
332
- { id: 'rkeys', label: 'KEYS', val: (s) => formatCount(s.redisKeysCount), title: 'Redis Keys', detail: (s) => 'Total keys in Redis: ' + s.redisKeysCount, hist: 'redisKeysCount', show: (s) => s.redisOk },
333
- { id: 'rhit', label: 'HIT', val: (s) => s.redisHitRate.toFixed(0) + '%', color: (s) => threshInverse(s.redisHitRate, 90, 70), unit: '%', title: 'Redis Hit Rate', detail: 'Cache hit rate. <90% amber, <70% red.', hist: 'redisHitRate', show: (s) => s.redisOk },
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
- { id: 'q', label: 'Q', val: (s) => s.queueActive + '/' + s.queueWaiting + '/' + s.queueDelayed, color: (s) => s.queueFailed > 0 ? 'ss-amber' : 'ss-green', title: 'Job Queue', detail: (s) => 'Active: ' + s.queueActive + ', Waiting: ' + s.queueWaiting + ', Delayed: ' + s.queueDelayed + ', Failed: ' + s.queueFailed, hist: 'queueActive' },
336
- { id: 'workers', label: 'WORKERS', val: (s) => '' + s.queueWorkerCount, title: 'Queue Workers', detail: (s) => 'Connected queue worker processes: ' + s.queueWorkerCount },
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
- { id: 'users', label: 'USERS', val: (s) => '' + s.onlineUsers, title: 'Online Users', detail: 'Active user sessions (via Transmit)', hist: 'onlineUsers' },
339
- { id: 'hooks', label: 'HOOKS', val: (s) => '' + s.pendingWebhooks, color: (s) => s.pendingWebhooks > 100 ? 'ss-amber' : 'ss-green', title: 'Pending Webhooks', detail: 'Webhook events awaiting delivery. >100 amber.', hist: 'pendingWebhooks' },
340
- { id: 'mail', label: 'MAIL', val: (s) => '' + s.pendingEmails, color: (s) => s.pendingEmails > 100 ? 'ss-amber' : 'ss-green', title: 'Pending Emails', detail: 'Scheduled emails awaiting send. >100 amber.', hist: 'pendingEmails' },
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
- { id: 'logerr', label: 'LOG ERR', val: (s) => '' + s.logErrorsLast5m, color: (s) => s.logErrorsLast5m > 0 ? 'ss-red' : s.logWarningsLast5m > 0 ? 'ss-amber' : 'ss-green', title: 'Log Errors (5m)', detail: (s) => s.logErrorsLast5m + ' error/fatal entries and ' + s.logWarningsLast5m + ' warnings in the last 5 minutes', hist: 'logErrorsLast5m', href: '/admin/logs?hasError=true' },
343
- { id: 'lograte', label: 'LOG/m', val: (s) => '' + s.logEntriesPerMinute, unit: '/m', title: 'Log Rate', detail: (s) => s.logEntriesLast5m + ' total entries in the last 5 minutes', hist: 'logEntriesPerMinute', href: '/admin/logs' },
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) { cpuEl.textContent = stats.cpuPercent.toFixed(0) + '%'; cpuEl.className = 'ss-value ' + thresh(stats.cpuPercent, 50, 80); }
421
- if (memEl) { memEl.textContent = formatBytes(stats.memHeapUsed); memEl.className = 'ss-value ss-green'; }
422
- if (redisEl) { redisEl.textContent = stats.redisOk ? '\u2713' : '\u2717'; redisEl.className = 'ss-value ' + (stats.redisOk ? 'ss-green' : 'ss-red'); }
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 ? (history[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 = typeof b.detail === 'function'
466
- ? (window.__ssLatest ? b.detail(window.__ssLatest) : b.detail({}))
467
- : b.detail;
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('wheel', (e) => {
510
- if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return;
511
- e.preventDefault();
512
- scrollEl.scrollLeft += e.deltaY;
513
- }, { passive: false });
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
+ })()