@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.
@@ -0,0 +1,5 @@
1
+ {
2
+ "permissions": {
3
+ "allow": ["Bash(bunx tsc *)"]
4
+ }
5
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@irsyadulibad/servermon",
3
- "version": "1.2.5",
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",
@@ -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.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
66
+ return s
67
+ .replace(/&/g, "&amp;")
68
+ .replace(/</g, "&lt;")
69
+ .replace(/>/g, "&gt;")
70
+ .replace(/"/g, "&quot;");
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") return pct > 80 ? "bg-red-500" : pct > 50 ? "bg-yellow-500" : "bg-emerald-500";
73
- if (severity === "mem") return pct > 80 ? "bg-red-500" : pct > 50 ? "bg-yellow-500" : "bg-blue-500";
74
- return pct > 90 ? "bg-red-500" : pct > 75 ? "bg-orange-500" : pct > 50 ? "bg-yellow-500" : "bg-blue-500";
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(hostname: string, platform: string, arch: string, uptime: number, temperature: number | null, port: number, version: string): string {
87
- const tempHtml = temperature != null
88
- ? `<span class="text-sm font-medium ${temperature > 75 ? 'text-red-400' : temperature > 50 ? 'text-yellow-400' : 'text-green-400'}">${temperature.toFixed(1)}°C</span>`
89
- : '<span class="text-sm text-slate-500">—°C</span>';
90
-
91
- const tempIcon = temperature != null
92
- ? (temperature > 75 ? '🔴' : temperature > 50 ? '🟡' : '🟢')
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
- ${mem.swapTotal > 0
176
- ? `<p>swap ${formatBytes(mem.swapUsed)} / ${formatBytes(mem.swapTotal)} <span class="${pctColor(mem.swapUsagePercent)} tabular-nums">(${mem.swapUsagePercent.toFixed(1)}%)</span></p>`
177
- : '<p class="text-slate-600">swap n/a</p>'}
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.map((d) => {
186
- const bar = barColor(d.usagePercent, "disk");
187
- const txt = pctColor(d.usagePercent);
188
- return /* html */ `
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
- }).join("");
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.map((p, i) => {
258
- const highlight = i === 0 ? "text-yellow-400" : "text-slate-200";
259
- return /* html */ `
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
- }).join("");
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="flex items-center justify-between mb-3">
294
- <h3 class="text-xs font-semibold uppercase tracking-wider text-slate-400">CPU & RAM History</h3>
295
- <span class="text-[10px] text-slate-500">last 60 samples (≈5 min)</span>
296
- </div>
297
- <div class="h-52">
298
- <canvas id="historyChart"></canvas>
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 &amp; 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
- /* ---- Chart.js history (60-sample rolling window) ---- */
349
- function initChart() {
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 MAX = 60;
356
- const data = { cpu: new Array(MAX).fill(0), mem: new Array(MAX).fill(0) };
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
- { label: "CPU %", data: data.cpu,
365
- borderColor: "#34d399", backgroundColor: "rgba(52,211,153,0.06)",
366
- tension: 0.3, fill: true, pointRadius: 0, borderWidth: 1.5 },
367
- { label: "RAM %", data: data.mem,
368
- borderColor: "#60a5fa", backgroundColor: "rgba(96,165,250,0.06)",
369
- tension: 0.3, fill: true, pointRadius: 0, borderWidth: 1.5 },
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
- window.__history = data;
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
- if (!cpuEl || !memEl || !window.__history || !window.__chart) return;
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 cpu = parseFloat(cpuEl.dataset.val) || 0;
396
- const mem = parseFloat(memEl.dataset.val) || 0;
568
+ const h = window.__history;
569
+ const label = nowLabel();
397
570
 
398
- window.__history.cpu.push(cpu);
399
- window.__history.mem.push(mem);
400
- if (window.__history.cpu.length > 60) {
401
- window.__history.cpu.shift();
402
- window.__history.mem.shift();
403
- }
404
- window.__chart.update("none");
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
- /* Init on first load */
408
- document.addEventListener("DOMContentLoaded", initChart);
409
- /* Re-init after HTMX swap (chart canvas is replaced) */
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
- initChart();
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
- .meta({ description: "Start the monitoring daemon" })
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
- if (name) {
112
- const config = await loadConfig(name);
113
- if (!config) {
114
- console.error(
115
- `❌ No config found for server "${name}". Run \`servermon setup --name ${name}\` first.`
116
- );
117
- process.exit(1);
118
- }
119
- process.env["TELEGRAM_BOT_TOKEN"] = config.token;
120
- process.env["MONITOR_INTERVAL"] = String(config.interval);
121
- if (config.chatId) process.env["TELEGRAM_CHAT_ID"] = config.chatId;
122
- if (name) process.env["SERVER_NAME"] = name;
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
- console.log(`📁 Config: ${configPath()}`);
125
- console.log(`📡 Bot: ...${config.token.slice(-8)}`);
126
- if (config.chatId) console.log(`💬 Chat: ${config.chatId}`);
127
- if (name) console.log(`🏷 Name: ${name}`);
128
- console.log();
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
- const { start } = await import("../daemon");
131
- await start();
132
- } else {
133
- console.log(" 🌐 Multi-server mode — monitoring all configured servers\n");
134
- const { startAll } = await import("../daemon");
135
- await startAll();
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 ---- */
@@ -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("❌ No servers configured. Run `servermon setup` or `servermon setup --name <name>` first.");
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
- .meta({ description: "Install systemd service & start" })
201
- .run(async () => {
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
- .meta({ description: "Check service health" })
208
- .run(() => {
209
- cmdStatus();
210
- })
206
+ sub.meta({ description: "Check service health" }).run(() => {
207
+ cmdStatus();
208
+ })
211
209
  )
212
210
  .command("stop", (sub) =>
213
- sub
214
- .meta({ description: "Stop the service" })
215
- .run(() => {
216
- cmdStop();
217
- })
211
+ sub.meta({ description: "Stop the service" }).run(() => {
212
+ cmdStop();
213
+ })
218
214
  )
219
215
  .command("restart", (sub) =>
220
- sub
221
- .meta({ description: "Restart the service" })
222
- .run(() => {
223
- cmdRestart();
224
- })
216
+ sub.meta({ description: "Restart the service" }).run(() => {
217
+ cmdRestart();
218
+ })
225
219
  )
226
220
  .command("logs", (sub) =>
227
- sub
228
- .meta({ description: "Follow real-time logs" })
229
- .run(() => {
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
@@ -1,7 +1,6 @@
1
1
  import { homedir } from "os";
2
2
  import { join } from "path";
3
- import { mkdir, unlink } from "fs/promises";
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] = {
@@ -1,6 +1,6 @@
1
1
  import { sendReport } from "../reporter";
2
2
  import { saveConfig, loadConfig, listServers } from "../config";
3
- import type { ServerEntry, NamedConfig } from "../types";
3
+ import type { NamedConfig } from "../types";
4
4
 
5
5
  /* ------------------------------------------------------------------ */
6
6
  /* Environment variables (set by cli/) */