@irsyadulibad/servermon 1.2.5 → 1.2.6
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/.claude/settings.local.json +5 -0
- package/package.json +1 -1
- package/src/cli/dashboard.ts +262 -83
- package/src/cli/index.ts +28 -36
- package/src/cli/service.ts +18 -26
- package/src/config/index.ts +2 -5
- package/src/daemon/index.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@irsyadulibad/servermon",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"description": "Lightweight server monitoring daemon — collects system metrics and sends structured reports to Telegram. Built with Bun + TypeScript.",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"type": "module",
|
package/src/cli/dashboard.ts
CHANGED
|
@@ -52,7 +52,6 @@ tailwindcss.config = {
|
|
|
52
52
|
const STYLES = /* css */ `
|
|
53
53
|
body { font-family: ui-monospace, "SF Mono", "Cascadia Code", monospace; }
|
|
54
54
|
.progress-bar { transition: width 0.6s ease; }
|
|
55
|
-
.htmx-request { opacity: 0.5; pointer-events: none; }
|
|
56
55
|
.card-fade { transition: opacity 0.3s ease; }
|
|
57
56
|
.glow-green { box-shadow: 0 0 12px rgba(52,211,153,0.15); }
|
|
58
57
|
.glow-blue { box-shadow: 0 0 12px rgba(96,165,250,0.15); }
|
|
@@ -64,14 +63,26 @@ body { font-family: ui-monospace, "SF Mono", "Cascadia Code", monospace; }
|
|
|
64
63
|
/* ------------------------------------------------------------------ */
|
|
65
64
|
|
|
66
65
|
function esc(s: string): string {
|
|
67
|
-
return s
|
|
66
|
+
return s
|
|
67
|
+
.replace(/&/g, "&")
|
|
68
|
+
.replace(/</g, "<")
|
|
69
|
+
.replace(/>/g, ">")
|
|
70
|
+
.replace(/"/g, """);
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
/** Semantic bar colour based on utilisation percentage. */
|
|
71
74
|
function barColor(pct: number, severity: "cpu" | "mem" | "disk" = "disk"): string {
|
|
72
|
-
if (severity === "cpu")
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
if (severity === "cpu")
|
|
76
|
+
return pct > 80 ? "bg-red-500" : pct > 50 ? "bg-yellow-500" : "bg-emerald-500";
|
|
77
|
+
if (severity === "mem")
|
|
78
|
+
return pct > 80 ? "bg-red-500" : pct > 50 ? "bg-yellow-500" : "bg-blue-500";
|
|
79
|
+
return pct > 90
|
|
80
|
+
? "bg-red-500"
|
|
81
|
+
: pct > 75
|
|
82
|
+
? "bg-orange-500"
|
|
83
|
+
: pct > 50
|
|
84
|
+
? "bg-yellow-500"
|
|
85
|
+
: "bg-blue-500";
|
|
75
86
|
}
|
|
76
87
|
|
|
77
88
|
/** Text colour for percentage label. */
|
|
@@ -83,14 +94,22 @@ function pctColor(pct: number): string {
|
|
|
83
94
|
/* Template functions (pure, testable, no side-effects) */
|
|
84
95
|
/* ------------------------------------------------------------------ */
|
|
85
96
|
|
|
86
|
-
function headerSection(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
function headerSection(
|
|
98
|
+
hostname: string,
|
|
99
|
+
platform: string,
|
|
100
|
+
arch: string,
|
|
101
|
+
uptime: number,
|
|
102
|
+
temperature: number | null,
|
|
103
|
+
port: number,
|
|
104
|
+
version: string
|
|
105
|
+
): string {
|
|
106
|
+
const tempHtml =
|
|
107
|
+
temperature != null
|
|
108
|
+
? `<span class="text-sm font-medium ${temperature > 75 ? "text-red-400" : temperature > 50 ? "text-yellow-400" : "text-green-400"}">${temperature.toFixed(1)}°C</span>`
|
|
109
|
+
: '<span class="text-sm text-slate-500">—°C</span>';
|
|
110
|
+
|
|
111
|
+
const tempIcon =
|
|
112
|
+
temperature != null ? (temperature > 75 ? "🔴" : temperature > 50 ? "🟡" : "🟢") : "⚪";
|
|
94
113
|
|
|
95
114
|
return /* html */ `
|
|
96
115
|
<header class="max-w-6xl mx-auto mb-6">
|
|
@@ -134,7 +153,7 @@ function cpuCard(cpu: SystemMetrics["cpu"]): string {
|
|
|
134
153
|
const color = barColor(pct, "cpu");
|
|
135
154
|
const glow = pct > 80 ? "glow-red" : pct > 50 ? "" : "glow-green";
|
|
136
155
|
return /* html */ `
|
|
137
|
-
<div class="bg-gradient-to-br from-slate-900 to-slate-900/80 border border-slate-800 rounded-xl p-4 ${glow} card-fade">
|
|
156
|
+
<div class="bg-gradient-to-br from-slate-900 to-slate-900/80 border border-slate-800 rounded-xl p-4 lg:col-span-2 ${glow} card-fade">
|
|
138
157
|
<div class="flex items-center justify-between mb-3">
|
|
139
158
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-400">CPU</h3>
|
|
140
159
|
<span class="text-[10px] font-mono text-slate-500">${cpu.cores} cores</span>
|
|
@@ -158,7 +177,7 @@ function memCard(mem: SystemMetrics["memory"]): string {
|
|
|
158
177
|
const color = barColor(pct, "mem");
|
|
159
178
|
const glow = pct > 80 ? "glow-red" : "";
|
|
160
179
|
return /* html */ `
|
|
161
|
-
<div class="bg-gradient-to-br from-slate-900 to-slate-900/80 border border-slate-800 rounded-xl p-4 ${glow} card-fade">
|
|
180
|
+
<div class="bg-gradient-to-br from-slate-900 to-slate-900/80 border border-slate-800 rounded-xl p-4 lg:col-span-2 ${glow} card-fade">
|
|
162
181
|
<div class="flex items-center justify-between mb-3">
|
|
163
182
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-400">Memory</h3>
|
|
164
183
|
<span class="text-[10px] font-mono text-slate-500">${formatBytes(mem.total)}</span>
|
|
@@ -172,9 +191,11 @@ function memCard(mem: SystemMetrics["memory"]): string {
|
|
|
172
191
|
</div>
|
|
173
192
|
<div class="text-xs text-slate-500 space-y-0.5">
|
|
174
193
|
<p><span class="text-slate-300">used</span> ${formatBytes(mem.used)} <span class="text-slate-600">·</span> <span class="text-slate-300">free</span> ${formatBytes(mem.free)}</p>
|
|
175
|
-
${
|
|
176
|
-
|
|
177
|
-
|
|
194
|
+
${
|
|
195
|
+
mem.swapTotal > 0
|
|
196
|
+
? `<p>swap ${formatBytes(mem.swapUsed)} / ${formatBytes(mem.swapTotal)} <span class="${pctColor(mem.swapUsagePercent)} tabular-nums">(${mem.swapUsagePercent.toFixed(1)}%)</span></p>`
|
|
197
|
+
: '<p class="text-slate-600">swap n/a</p>'
|
|
198
|
+
}
|
|
178
199
|
</div>
|
|
179
200
|
</div>`;
|
|
180
201
|
}
|
|
@@ -182,10 +203,11 @@ function memCard(mem: SystemMetrics["memory"]): string {
|
|
|
182
203
|
function diskSection(disks: DiskInfo[]): string {
|
|
183
204
|
if (disks.length === 0) return "";
|
|
184
205
|
|
|
185
|
-
const rows = disks
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
206
|
+
const rows = disks
|
|
207
|
+
.map((d) => {
|
|
208
|
+
const bar = barColor(d.usagePercent, "disk");
|
|
209
|
+
const txt = pctColor(d.usagePercent);
|
|
210
|
+
return /* html */ `
|
|
189
211
|
<tr class="hover:bg-slate-800/30 transition-colors">
|
|
190
212
|
<td class="py-1.5 pr-3 text-slate-300 font-medium">${esc(d.mount)}</td>
|
|
191
213
|
<td class="py-1.5 pr-3 text-right text-slate-400 text-xs tabular-nums">${formatBytes(d.used)} / ${formatBytes(d.total)}</td>
|
|
@@ -196,10 +218,11 @@ function diskSection(disks: DiskInfo[]): string {
|
|
|
196
218
|
</td>
|
|
197
219
|
<td class="py-1.5 pl-2 text-right text-xs font-semibold tabular-nums ${txt}">${d.usagePercent}%</td>
|
|
198
220
|
</tr>`;
|
|
199
|
-
|
|
221
|
+
})
|
|
222
|
+
.join("");
|
|
200
223
|
|
|
201
224
|
return /* html */ `
|
|
202
|
-
<div class="bg-gradient-to-br from-slate-900 to-slate-900/80 border border-slate-800 rounded-xl p-4 md:col-span-2 card-fade">
|
|
225
|
+
<div class="bg-gradient-to-br from-slate-900 to-slate-900/80 border border-slate-800 rounded-xl p-4 md:col-span-2 lg:col-span-3 card-fade">
|
|
203
226
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-400 mb-3">Disks</h3>
|
|
204
227
|
<div class="overflow-x-auto">
|
|
205
228
|
<table class="w-full text-xs">
|
|
@@ -222,7 +245,9 @@ function netCard(net: SystemMetrics["network"]): string {
|
|
|
222
245
|
const txWidth = Math.min((net.txRate / NET_BAR_SCALE) * 100, 100);
|
|
223
246
|
|
|
224
247
|
return /* html */ `
|
|
225
|
-
<div class="bg-gradient-to-br from-slate-900 to-slate-900/80 border border-slate-800 rounded-xl p-4 glow-blue card-fade">
|
|
248
|
+
<div class="bg-gradient-to-br from-slate-900 to-slate-900/80 border border-slate-800 rounded-xl p-4 md:col-span-2 lg:col-span-2 glow-blue card-fade">
|
|
249
|
+
<span id="_rx_val" data-val="${net.rxRate}" class="hidden"></span>
|
|
250
|
+
<span id="_tx_val" data-val="${net.txRate}" class="hidden"></span>
|
|
226
251
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-400 mb-3">Network</h3>
|
|
227
252
|
<div class="space-y-4">
|
|
228
253
|
<div>
|
|
@@ -254,19 +279,21 @@ function netCard(net: SystemMetrics["network"]): string {
|
|
|
254
279
|
function procSection(procs: SystemMetrics["topProcs"]): string {
|
|
255
280
|
if (procs.length === 0) return "";
|
|
256
281
|
|
|
257
|
-
const rows = procs
|
|
258
|
-
|
|
259
|
-
|
|
282
|
+
const rows = procs
|
|
283
|
+
.map((p, i) => {
|
|
284
|
+
const highlight = i === 0 ? "text-yellow-400" : "text-slate-200";
|
|
285
|
+
return /* html */ `
|
|
260
286
|
<tr class="hover:bg-slate-800/30 transition-colors">
|
|
261
287
|
<td class="py-1.5 pr-3 text-slate-500 tabular-nums">${p.pid}</td>
|
|
262
288
|
<td class="py-1.5 pr-3 ${highlight} truncate max-w-[200px]" title="${esc(p.name)}">${esc(p.name)}</td>
|
|
263
289
|
<td class="py-1.5 pr-3 text-right text-slate-300 tabular-nums">${p.cpuPercent.toFixed(1)}%</td>
|
|
264
290
|
<td class="py-1.5 text-right text-slate-300 tabular-nums">${p.memPercent.toFixed(1)}%</td>
|
|
265
291
|
</tr>`;
|
|
266
|
-
|
|
292
|
+
})
|
|
293
|
+
.join("");
|
|
267
294
|
|
|
268
295
|
return /* html */ `
|
|
269
|
-
<div class="bg-gradient-to-br from-slate-900 to-slate-900/80 border border-slate-800 rounded-xl p-4 md:col-span-2 card-fade">
|
|
296
|
+
<div class="bg-gradient-to-br from-slate-900 to-slate-900/80 border border-slate-800 rounded-xl p-4 md:col-span-2 lg:col-span-3 card-fade">
|
|
270
297
|
<div class="flex items-center justify-between mb-3">
|
|
271
298
|
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-400">Top Processes</h3>
|
|
272
299
|
<span class="text-[10px] text-slate-500">by CPU</span>
|
|
@@ -289,14 +316,34 @@ function procSection(procs: SystemMetrics["topProcs"]): string {
|
|
|
289
316
|
|
|
290
317
|
function chartSection(): string {
|
|
291
318
|
return /* html */ `
|
|
292
|
-
<div class="bg-gradient-to-br from-slate-900 to-slate-900/80 border border-slate-800 rounded-xl p-4 md:col-span-2 card-fade" id="_chart_wrapper">
|
|
293
|
-
<div class="
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
319
|
+
<div class="bg-gradient-to-br from-slate-900 to-slate-900/80 border border-slate-800 rounded-xl p-4 md:col-span-2 lg:col-span-6 card-fade" id="_chart_wrapper">
|
|
320
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-x-6 gap-y-4">
|
|
321
|
+
<div>
|
|
322
|
+
<div class="flex items-center justify-between mb-3">
|
|
323
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-400">CPU & RAM</h3>
|
|
324
|
+
<div class="flex items-center gap-3 text-[10px] text-slate-500">
|
|
325
|
+
<span class="flex items-center gap-1"><span class="inline-block w-2 h-2 rounded-full bg-emerald-400"></span>CPU</span>
|
|
326
|
+
<span class="flex items-center gap-1"><span class="inline-block w-2 h-2 rounded-full bg-blue-400"></span>RAM</span>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="h-48">
|
|
330
|
+
<canvas id="historyChart"></canvas>
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
<div>
|
|
334
|
+
<div class="flex items-center justify-between mb-3">
|
|
335
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-400">Network I/O</h3>
|
|
336
|
+
<div class="flex items-center gap-3 text-[10px] text-slate-500">
|
|
337
|
+
<span class="flex items-center gap-1"><span class="inline-block w-2 h-2 rounded-full bg-emerald-400"></span>RX</span>
|
|
338
|
+
<span class="flex items-center gap-1"><span class="inline-block w-2 h-2 rounded-full bg-violet-400"></span>TX</span>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
<div class="h-48">
|
|
342
|
+
<canvas id="netChart"></canvas>
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
299
345
|
</div>
|
|
346
|
+
<p class="text-[10px] text-slate-600 text-right mt-2">60 samples · ≈5 min rolling window</p>
|
|
300
347
|
</div>`;
|
|
301
348
|
}
|
|
302
349
|
|
|
@@ -311,14 +358,14 @@ function footerSection(hostname: string): string {
|
|
|
311
358
|
/** HTMX-swappable dashboard body (replaces #dashboard container). */
|
|
312
359
|
function metricsFragment(metrics: SystemMetrics): string {
|
|
313
360
|
return /* html */ `
|
|
314
|
-
<div id="dashboard" class="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-4"
|
|
361
|
+
<div id="dashboard" class="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4"
|
|
315
362
|
hx-get="/_dashboard/metrics" hx-trigger="every 5s" hx-swap="outerHTML" hx-target="#dashboard">
|
|
316
363
|
${cpuCard(metrics.cpu)}
|
|
317
364
|
${memCard(metrics.memory)}
|
|
318
|
-
${diskSection(metrics.disks)}
|
|
319
365
|
${netCard(metrics.network)}
|
|
320
|
-
${procSection(metrics.topProcs)}
|
|
321
366
|
${chartSection()}
|
|
367
|
+
${diskSection(metrics.disks)}
|
|
368
|
+
${procSection(metrics.topProcs)}
|
|
322
369
|
</div>`;
|
|
323
370
|
}
|
|
324
371
|
|
|
@@ -345,76 +392,208 @@ function layout(metrics: SystemMetrics, port: number, version: string): string {
|
|
|
345
392
|
|
|
346
393
|
<script>
|
|
347
394
|
(function() {
|
|
348
|
-
|
|
349
|
-
|
|
395
|
+
const MAX = 60;
|
|
396
|
+
|
|
397
|
+
function makeGradient(ctx, colorTop, colorBot) {
|
|
398
|
+
const g = ctx.createLinearGradient(0, 0, 0, ctx.canvas.clientHeight || 160);
|
|
399
|
+
g.addColorStop(0, colorTop);
|
|
400
|
+
g.addColorStop(1, colorBot);
|
|
401
|
+
return g;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function baseOptions(yLabel, yMax) {
|
|
405
|
+
return {
|
|
406
|
+
responsive: true,
|
|
407
|
+
maintainAspectRatio: false,
|
|
408
|
+
animation: { duration: 400, easing: "easeOutQuart" },
|
|
409
|
+
interaction: { mode: "index", intersect: false },
|
|
410
|
+
scales: {
|
|
411
|
+
x: {
|
|
412
|
+
display: true,
|
|
413
|
+
grid: { display: false },
|
|
414
|
+
ticks: {
|
|
415
|
+
color: "#475569",
|
|
416
|
+
maxTicksLimit: 7,
|
|
417
|
+
maxRotation: 0,
|
|
418
|
+
callback: function(val, i) {
|
|
419
|
+
const lbl = this.chart.data.labels[i];
|
|
420
|
+
return lbl || "";
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
border: { display: false },
|
|
424
|
+
},
|
|
425
|
+
y: {
|
|
426
|
+
min: 0, max: yMax,
|
|
427
|
+
grid: { color: "rgba(148,163,184,0.05)", drawBorder: false },
|
|
428
|
+
ticks: {
|
|
429
|
+
color: "#475569",
|
|
430
|
+
maxTicksLimit: 5,
|
|
431
|
+
callback: (v) => yLabel === "%" ? v + "%" : v,
|
|
432
|
+
},
|
|
433
|
+
border: { display: false },
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
plugins: {
|
|
437
|
+
legend: { display: false },
|
|
438
|
+
tooltip: {
|
|
439
|
+
backgroundColor: "rgba(15,23,42,0.95)",
|
|
440
|
+
borderColor: "rgba(148,163,184,0.12)",
|
|
441
|
+
borderWidth: 1,
|
|
442
|
+
titleColor: "#94a3b8",
|
|
443
|
+
bodyColor: "#e2e8f0",
|
|
444
|
+
padding: 10,
|
|
445
|
+
callbacks: {
|
|
446
|
+
label: function(ctx) {
|
|
447
|
+
const val = ctx.parsed.y;
|
|
448
|
+
if (yLabel === "%") return " " + ctx.dataset.label + ": " + val.toFixed(1) + "%";
|
|
449
|
+
return " " + ctx.dataset.label + ": " + formatBytesRate(val);
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function formatBytesRate(bps) {
|
|
458
|
+
if (bps >= 1048576) return (bps / 1048576).toFixed(1) + " MB/s";
|
|
459
|
+
if (bps >= 1024) return (bps / 1024).toFixed(1) + " KB/s";
|
|
460
|
+
return bps.toFixed(0) + " B/s";
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function nowLabel() {
|
|
464
|
+
const d = new Date();
|
|
465
|
+
return d.getHours().toString().padStart(2,"0") + ":" + d.getMinutes().toString().padStart(2,"0") + ":" + d.getSeconds().toString().padStart(2,"0");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function initCharts() {
|
|
350
469
|
const canvas = document.getElementById("historyChart");
|
|
470
|
+
const netCanvas = document.getElementById("netChart");
|
|
351
471
|
if (!canvas || window._chartInited) return;
|
|
352
472
|
window._chartInited = true;
|
|
353
473
|
|
|
474
|
+
/* Destroy stale chart instances bound to the now-replaced canvases. */
|
|
475
|
+
if (window.__chart) { window.__chart.destroy(); window.__chart = null; }
|
|
476
|
+
if (window.__netChart) { window.__netChart.destroy(); window.__netChart = null; }
|
|
477
|
+
|
|
478
|
+
/* -- history data store (preserve across HTMX swaps) -- */
|
|
479
|
+
const history = window.__history || {
|
|
480
|
+
labels: new Array(MAX).fill(""),
|
|
481
|
+
cpu: new Array(MAX).fill(null),
|
|
482
|
+
mem: new Array(MAX).fill(null),
|
|
483
|
+
rx: new Array(MAX).fill(null),
|
|
484
|
+
tx: new Array(MAX).fill(null),
|
|
485
|
+
};
|
|
486
|
+
window.__history = history;
|
|
487
|
+
|
|
488
|
+
/* -- CPU/RAM chart -- */
|
|
354
489
|
const ctx = canvas.getContext("2d");
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
const labels = new Array(MAX).fill("");
|
|
490
|
+
const cpuGrad = makeGradient(ctx, "rgba(52,211,153,0.25)", "rgba(52,211,153,0.01)");
|
|
491
|
+
const memGrad = makeGradient(ctx, "rgba(96,165,250,0.25)", "rgba(96,165,250,0.01)");
|
|
358
492
|
|
|
359
493
|
window.__chart = new Chart(ctx, {
|
|
360
494
|
type: "line",
|
|
361
495
|
data: {
|
|
362
|
-
labels: labels,
|
|
496
|
+
labels: history.labels,
|
|
363
497
|
datasets: [
|
|
364
|
-
{
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
tension: 0.
|
|
498
|
+
{
|
|
499
|
+
label: "CPU",
|
|
500
|
+
data: history.cpu,
|
|
501
|
+
borderColor: "#34d399",
|
|
502
|
+
backgroundColor: cpuGrad,
|
|
503
|
+
tension: 0.4, fill: true, pointRadius: 0,
|
|
504
|
+
borderWidth: 2,
|
|
505
|
+
spanGaps: false,
|
|
506
|
+
},
|
|
507
|
+
{
|
|
508
|
+
label: "RAM",
|
|
509
|
+
data: history.mem,
|
|
510
|
+
borderColor: "#60a5fa",
|
|
511
|
+
backgroundColor: memGrad,
|
|
512
|
+
tension: 0.4, fill: true, pointRadius: 0,
|
|
513
|
+
borderWidth: 2,
|
|
514
|
+
spanGaps: false,
|
|
515
|
+
},
|
|
370
516
|
],
|
|
371
517
|
},
|
|
372
|
-
options:
|
|
373
|
-
responsive: true, maintainAspectRatio: false, animation: false,
|
|
374
|
-
scales: {
|
|
375
|
-
x: { display: false },
|
|
376
|
-
y: { min: 0, max: 100,
|
|
377
|
-
grid: { color: "rgba(148,163,184,0.06)" },
|
|
378
|
-
ticks: { color: "#64748b", maxTicksLimit: 5 } },
|
|
379
|
-
},
|
|
380
|
-
plugins: {
|
|
381
|
-
legend: { labels: { color: "#94a3b8", boxWidth: 12, padding: 12 } },
|
|
382
|
-
},
|
|
383
|
-
},
|
|
518
|
+
options: baseOptions("%", 100),
|
|
384
519
|
});
|
|
385
520
|
|
|
386
|
-
|
|
521
|
+
/* -- Network chart -- */
|
|
522
|
+
if (netCanvas) {
|
|
523
|
+
const nctx = netCanvas.getContext("2d");
|
|
524
|
+
const rxGrad = makeGradient(nctx, "rgba(52,211,153,0.20)", "rgba(52,211,153,0.01)");
|
|
525
|
+
const txGrad = makeGradient(nctx, "rgba(167,139,250,0.20)", "rgba(167,139,250,0.01)");
|
|
526
|
+
|
|
527
|
+
window.__netChart = new Chart(nctx, {
|
|
528
|
+
type: "line",
|
|
529
|
+
data: {
|
|
530
|
+
labels: history.labels,
|
|
531
|
+
datasets: [
|
|
532
|
+
{
|
|
533
|
+
label: "RX",
|
|
534
|
+
data: history.rx,
|
|
535
|
+
borderColor: "#34d399",
|
|
536
|
+
backgroundColor: rxGrad,
|
|
537
|
+
tension: 0.4, fill: true, pointRadius: 0,
|
|
538
|
+
borderWidth: 1.5,
|
|
539
|
+
spanGaps: false,
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
label: "TX",
|
|
543
|
+
data: history.tx,
|
|
544
|
+
borderColor: "#a78bfa",
|
|
545
|
+
backgroundColor: txGrad,
|
|
546
|
+
tension: 0.4, fill: true, pointRadius: 0,
|
|
547
|
+
borderWidth: 1.5,
|
|
548
|
+
spanGaps: false,
|
|
549
|
+
},
|
|
550
|
+
],
|
|
551
|
+
},
|
|
552
|
+
options: (function() {
|
|
553
|
+
const o = baseOptions("bytes", undefined);
|
|
554
|
+
o.scales.y.ticks.callback = (v) => formatBytesRate(v);
|
|
555
|
+
return o;
|
|
556
|
+
})(),
|
|
557
|
+
});
|
|
558
|
+
}
|
|
387
559
|
}
|
|
388
560
|
|
|
389
|
-
/* Push latest values from DOM every 5s (after HTMX swap) */
|
|
390
561
|
function pushMetrics() {
|
|
391
562
|
const cpuEl = document.getElementById("_cpu_val");
|
|
392
563
|
const memEl = document.getElementById("_mem_val");
|
|
393
|
-
|
|
564
|
+
const rxEl = document.getElementById("_rx_val");
|
|
565
|
+
const txEl = document.getElementById("_tx_val");
|
|
566
|
+
if (!cpuEl || !memEl || !window.__history) return;
|
|
394
567
|
|
|
395
|
-
const
|
|
396
|
-
const
|
|
568
|
+
const h = window.__history;
|
|
569
|
+
const label = nowLabel();
|
|
397
570
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
571
|
+
h.labels.push(label);
|
|
572
|
+
h.cpu.push(parseFloat(cpuEl.dataset.val) || 0);
|
|
573
|
+
h.mem.push(parseFloat(memEl.dataset.val) || 0);
|
|
574
|
+
h.rx.push(rxEl ? (parseFloat(rxEl.dataset.val) || 0) : null);
|
|
575
|
+
h.tx.push(txEl ? (parseFloat(txEl.dataset.val) || 0) : null);
|
|
576
|
+
|
|
577
|
+
if (h.labels.length > MAX) { h.labels.shift(); h.cpu.shift(); h.mem.shift(); h.rx.shift(); h.tx.shift(); }
|
|
578
|
+
|
|
579
|
+
if (window.__chart) window.__chart.update();
|
|
580
|
+
if (window.__netChart) window.__netChart.update();
|
|
405
581
|
}
|
|
406
582
|
|
|
407
|
-
/*
|
|
408
|
-
document.addEventListener("DOMContentLoaded",
|
|
409
|
-
|
|
583
|
+
/* First load: build charts and seed the first sample. */
|
|
584
|
+
document.addEventListener("DOMContentLoaded", function() {
|
|
585
|
+
initCharts();
|
|
586
|
+
setTimeout(pushMetrics, 200);
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
/* Each HTMX refresh replaces the canvas — rebuild on the preserved
|
|
590
|
+
history and append exactly one fresh sample. This is the only push
|
|
591
|
+
path, so samples map 1:1 to the 5s refresh (no stale/duplicate data). */
|
|
410
592
|
document.body.addEventListener("htmx:afterSwap", function() {
|
|
411
593
|
window._chartInited = false;
|
|
412
|
-
|
|
413
|
-
/* Push a value immediately after swap so chart isn't all zeros */
|
|
594
|
+
initCharts();
|
|
414
595
|
setTimeout(pushMetrics, 100);
|
|
415
596
|
});
|
|
416
|
-
/* Periodic push */
|
|
417
|
-
setInterval(pushMetrics, 5000);
|
|
418
597
|
})();
|
|
419
598
|
</script>
|
|
420
599
|
</body>
|
package/src/cli/index.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { Crust } from "@crustjs/core";
|
|
2
2
|
import { helpPlugin, versionPlugin, didYouMeanPlugin } from "@crustjs/plugins";
|
|
3
|
-
import {
|
|
4
|
-
loadConfig,
|
|
5
|
-
saveConfig,
|
|
6
|
-
configPath,
|
|
7
|
-
listServers,
|
|
8
|
-
deleteConfig,
|
|
9
|
-
} from "../config";
|
|
3
|
+
import { loadConfig, saveConfig, configPath, listServers, deleteConfig } from "../config";
|
|
10
4
|
import { printBanner } from "./banner";
|
|
11
5
|
import { serviceCmd } from "./service";
|
|
12
6
|
import pkg from "../../package.json";
|
|
@@ -103,38 +97,36 @@ export function createApp(): Crust {
|
|
|
103
97
|
)
|
|
104
98
|
/* ---- start ---- */
|
|
105
99
|
.command("start", (cmd) =>
|
|
106
|
-
cmd
|
|
107
|
-
|
|
108
|
-
.run(async ({ flags }) => {
|
|
109
|
-
const name = flags.name;
|
|
100
|
+
cmd.meta({ description: "Start the monitoring daemon" }).run(async ({ flags }) => {
|
|
101
|
+
const name = flags.name;
|
|
110
102
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
103
|
+
if (name) {
|
|
104
|
+
const config = await loadConfig(name);
|
|
105
|
+
if (!config) {
|
|
106
|
+
console.error(
|
|
107
|
+
`❌ No config found for server "${name}". Run \`servermon setup --name ${name}\` first.`
|
|
108
|
+
);
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
process.env["TELEGRAM_BOT_TOKEN"] = config.token;
|
|
112
|
+
process.env["MONITOR_INTERVAL"] = String(config.interval);
|
|
113
|
+
if (config.chatId) process.env["TELEGRAM_CHAT_ID"] = config.chatId;
|
|
114
|
+
if (name) process.env["SERVER_NAME"] = name;
|
|
123
115
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
116
|
+
console.log(`📁 Config: ${configPath()}`);
|
|
117
|
+
console.log(`📡 Bot: ...${config.token.slice(-8)}`);
|
|
118
|
+
if (config.chatId) console.log(`💬 Chat: ${config.chatId}`);
|
|
119
|
+
if (name) console.log(`🏷 Name: ${name}`);
|
|
120
|
+
console.log();
|
|
129
121
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
122
|
+
const { start } = await import("../daemon");
|
|
123
|
+
await start();
|
|
124
|
+
} else {
|
|
125
|
+
console.log(" 🌐 Multi-server mode — monitoring all configured servers\n");
|
|
126
|
+
const { startAll } = await import("../daemon");
|
|
127
|
+
await startAll();
|
|
128
|
+
}
|
|
129
|
+
})
|
|
138
130
|
)
|
|
139
131
|
|
|
140
132
|
/* ---- report ---- */
|
package/src/cli/service.ts
CHANGED
|
@@ -56,7 +56,9 @@ function getServiceStatus(): string {
|
|
|
56
56
|
async function cmdInstall(): Promise<void> {
|
|
57
57
|
const servers = await listServers();
|
|
58
58
|
if (servers.length === 0) {
|
|
59
|
-
console.error(
|
|
59
|
+
console.error(
|
|
60
|
+
"❌ No servers configured. Run `servermon setup` or `servermon setup --name <name>` first."
|
|
61
|
+
);
|
|
60
62
|
process.exit(1);
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -196,39 +198,29 @@ export const serviceCmd: Parameters<Crust["command"]>[1] = (cmd) =>
|
|
|
196
198
|
cmd
|
|
197
199
|
.meta({ description: "Manage systemd service" })
|
|
198
200
|
.command("install", (sub) =>
|
|
199
|
-
sub
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
await cmdInstall();
|
|
203
|
-
})
|
|
201
|
+
sub.meta({ description: "Install systemd service & start" }).run(async () => {
|
|
202
|
+
await cmdInstall();
|
|
203
|
+
})
|
|
204
204
|
)
|
|
205
205
|
.command("status", (sub) =>
|
|
206
|
-
sub
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
cmdStatus();
|
|
210
|
-
})
|
|
206
|
+
sub.meta({ description: "Check service health" }).run(() => {
|
|
207
|
+
cmdStatus();
|
|
208
|
+
})
|
|
211
209
|
)
|
|
212
210
|
.command("stop", (sub) =>
|
|
213
|
-
sub
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
cmdStop();
|
|
217
|
-
})
|
|
211
|
+
sub.meta({ description: "Stop the service" }).run(() => {
|
|
212
|
+
cmdStop();
|
|
213
|
+
})
|
|
218
214
|
)
|
|
219
215
|
.command("restart", (sub) =>
|
|
220
|
-
sub
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
cmdRestart();
|
|
224
|
-
})
|
|
216
|
+
sub.meta({ description: "Restart the service" }).run(() => {
|
|
217
|
+
cmdRestart();
|
|
218
|
+
})
|
|
225
219
|
)
|
|
226
220
|
.command("logs", (sub) =>
|
|
227
|
-
sub
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
cmdLogs();
|
|
231
|
-
})
|
|
221
|
+
sub.meta({ description: "Follow real-time logs" }).run(() => {
|
|
222
|
+
cmdLogs();
|
|
223
|
+
})
|
|
232
224
|
)
|
|
233
225
|
.command("uninstall", (sub) =>
|
|
234
226
|
sub
|
package/src/config/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { homedir } from "os";
|
|
2
2
|
import { join } from "path";
|
|
3
|
-
import { mkdir
|
|
4
|
-
import { existsSync } from "fs";
|
|
3
|
+
import { mkdir } from "fs/promises";
|
|
5
4
|
import type { ServerMonConfig, ServerEntry } from "../types";
|
|
6
5
|
|
|
7
6
|
const CONFIG_DIR = join(homedir(), ".irsyadulibad", "servermon");
|
|
@@ -61,9 +60,7 @@ export async function loadConfig(name?: string): Promise<ServerEntry | null> {
|
|
|
61
60
|
};
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
export async function saveConfig(
|
|
65
|
-
entry: ServerEntry & { name?: string },
|
|
66
|
-
): Promise<void> {
|
|
63
|
+
export async function saveConfig(entry: ServerEntry & { name?: string }): Promise<void> {
|
|
67
64
|
const cfg = (await readConfig()) ?? { servers: {} };
|
|
68
65
|
const key = entry.name ?? "default";
|
|
69
66
|
cfg.servers[key] = {
|
package/src/daemon/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { sendReport } from "../reporter";
|
|
2
2
|
import { saveConfig, loadConfig, listServers } from "../config";
|
|
3
|
-
import type {
|
|
3
|
+
import type { NamedConfig } from "../types";
|
|
4
4
|
|
|
5
5
|
/* ------------------------------------------------------------------ */
|
|
6
6
|
/* Environment variables (set by cli/) */
|