@irsyadulibad/servermon 1.2.4 → 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 +4 -2
- package/src/cli/dashboard.ts +681 -0
- package/src/cli/index.ts +46 -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",
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@crustjs/core": "^0.0.19",
|
|
36
|
-
"@crustjs/plugins": "^0.1.2"
|
|
36
|
+
"@crustjs/plugins": "^0.1.2",
|
|
37
|
+
"@elysiajs/html": "^1.4.2",
|
|
38
|
+
"elysia": "^1.4.29"
|
|
37
39
|
}
|
|
38
40
|
}
|
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* servermon dashboard — lightweight web UI for monitoring system metrics.
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* startDashboard(port)
|
|
6
|
+
* └─ createDashboardApp(port) ← Elysia routes
|
|
7
|
+
* ├─ GET / ← full page layout
|
|
8
|
+
* ├─ GET /_dashboard/metrics ← HTMX fragment (auto-refresh)
|
|
9
|
+
* └─ GET /_dashboard/health ← JSON health check
|
|
10
|
+
*
|
|
11
|
+
* Templates layer (pure functions, zero Elysia imports):
|
|
12
|
+
* layout() → full HTML page shell
|
|
13
|
+
* metricsFragment → HTMX-swappable dashboard grid
|
|
14
|
+
* cpuCard() → CPU usage card
|
|
15
|
+
* memCard() → RAM usage card
|
|
16
|
+
* diskSection() → disk usage table
|
|
17
|
+
* netCard() → network RX/TX card
|
|
18
|
+
* procSection() → top processes table
|
|
19
|
+
* chartSection() → Chart.js canvas shell
|
|
20
|
+
* headerSection() → host info + temperature row
|
|
21
|
+
*
|
|
22
|
+
* Dependencies: elysia, @elysiajs/html, tailwindcss (CDN), htmx (CDN), chart.js (CDN)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { Elysia } from "elysia";
|
|
26
|
+
import { html } from "@elysiajs/html";
|
|
27
|
+
import { collectMetrics, formatBytes, formatRate, formatUptime } from "../monitor";
|
|
28
|
+
import type { DiskInfo, SystemMetrics } from "../types";
|
|
29
|
+
import pkg from "../../package.json";
|
|
30
|
+
|
|
31
|
+
/* ------------------------------------------------------------------ */
|
|
32
|
+
/* Constants */
|
|
33
|
+
/* ------------------------------------------------------------------ */
|
|
34
|
+
|
|
35
|
+
const APP_VERSION = pkg.version;
|
|
36
|
+
const NET_BAR_SCALE = 10 * 1024 * 1024; // 10 MB/s = 100% bar width
|
|
37
|
+
|
|
38
|
+
/* ------------------------------------------------------------------ */
|
|
39
|
+
/* CSS / Theme */
|
|
40
|
+
/* ------------------------------------------------------------------ */
|
|
41
|
+
|
|
42
|
+
const THEME_SCRIPT = /* js */ `
|
|
43
|
+
tailwindcss.config = {
|
|
44
|
+
darkMode: "class",
|
|
45
|
+
theme: {
|
|
46
|
+
extend: {
|
|
47
|
+
fontFamily: { mono: ['ui-monospace', 'SF Mono', 'Cascadia Code', 'monospace'] },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
};`;
|
|
51
|
+
|
|
52
|
+
const STYLES = /* css */ `
|
|
53
|
+
body { font-family: ui-monospace, "SF Mono", "Cascadia Code", monospace; }
|
|
54
|
+
.progress-bar { transition: width 0.6s ease; }
|
|
55
|
+
.card-fade { transition: opacity 0.3s ease; }
|
|
56
|
+
.glow-green { box-shadow: 0 0 12px rgba(52,211,153,0.15); }
|
|
57
|
+
.glow-blue { box-shadow: 0 0 12px rgba(96,165,250,0.15); }
|
|
58
|
+
.glow-red { box-shadow: 0 0 12px rgba(248,113,113,0.15); }
|
|
59
|
+
@media (prefers-color-scheme: dark) { body { background: #020617; } }`;
|
|
60
|
+
|
|
61
|
+
/* ------------------------------------------------------------------ */
|
|
62
|
+
/* Utility helpers (shared by all template functions) */
|
|
63
|
+
/* ------------------------------------------------------------------ */
|
|
64
|
+
|
|
65
|
+
function esc(s: string): string {
|
|
66
|
+
return s
|
|
67
|
+
.replace(/&/g, "&")
|
|
68
|
+
.replace(/</g, "<")
|
|
69
|
+
.replace(/>/g, ">")
|
|
70
|
+
.replace(/"/g, """);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Semantic bar colour based on utilisation percentage. */
|
|
74
|
+
function barColor(pct: number, severity: "cpu" | "mem" | "disk" = "disk"): string {
|
|
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";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Text colour for percentage label. */
|
|
89
|
+
function pctColor(pct: number): string {
|
|
90
|
+
return pct > 80 ? "text-red-400" : pct > 50 ? "text-yellow-400" : "text-slate-400";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* ------------------------------------------------------------------ */
|
|
94
|
+
/* Template functions (pure, testable, no side-effects) */
|
|
95
|
+
/* ------------------------------------------------------------------ */
|
|
96
|
+
|
|
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 ? "🟡" : "🟢") : "⚪";
|
|
113
|
+
|
|
114
|
+
return /* html */ `
|
|
115
|
+
<header class="max-w-6xl mx-auto mb-6">
|
|
116
|
+
<div class="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 bg-slate-900/50 border border-slate-800 rounded-xl p-4">
|
|
117
|
+
<div class="flex items-center gap-3">
|
|
118
|
+
<div class="w-10 h-10 rounded-lg bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-lg font-bold text-emerald-400 glow-green">◉</div>
|
|
119
|
+
<div>
|
|
120
|
+
<h1 class="text-lg font-bold tracking-tight text-slate-100">
|
|
121
|
+
ServerMon
|
|
122
|
+
<span class="text-xs font-normal text-slate-500 ml-1.5">v${version}</span>
|
|
123
|
+
</h1>
|
|
124
|
+
<p class="text-xs text-slate-400 mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5">
|
|
125
|
+
<span class="font-semibold text-slate-300">${esc(hostname)}</span>
|
|
126
|
+
<span class="text-slate-600">·</span>
|
|
127
|
+
<span>${platform}/${arch}</span>
|
|
128
|
+
<span class="text-slate-600">·</span>
|
|
129
|
+
<span>up ${formatUptime(uptime)}</span>
|
|
130
|
+
<span class="text-slate-600">·</span>
|
|
131
|
+
<span>${tempIcon} ${tempHtml}</span>
|
|
132
|
+
</p>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="flex items-center gap-4 text-xs text-slate-500">
|
|
136
|
+
<span class="flex items-center gap-1.5">
|
|
137
|
+
<span class="relative flex h-2.5 w-2.5">
|
|
138
|
+
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
|
139
|
+
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
|
|
140
|
+
</span>
|
|
141
|
+
live
|
|
142
|
+
</span>
|
|
143
|
+
<span class="hidden sm:inline-flex items-center gap-1.5 bg-slate-800 rounded-md px-2.5 py-1 text-slate-400 font-mono">
|
|
144
|
+
:${port}
|
|
145
|
+
</span>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</header>`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function cpuCard(cpu: SystemMetrics["cpu"]): string {
|
|
152
|
+
const pct = cpu.usagePercent;
|
|
153
|
+
const color = barColor(pct, "cpu");
|
|
154
|
+
const glow = pct > 80 ? "glow-red" : pct > 50 ? "" : "glow-green";
|
|
155
|
+
return /* html */ `
|
|
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">
|
|
157
|
+
<div class="flex items-center justify-between mb-3">
|
|
158
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-400">CPU</h3>
|
|
159
|
+
<span class="text-[10px] font-mono text-slate-500">${cpu.cores} cores</span>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="flex items-end gap-2 mb-3">
|
|
162
|
+
<span class="text-4xl font-bold tracking-tight tabular-nums text-slate-100" id="_cpu_val" data-val="${pct}">${pct.toFixed(1)}</span>
|
|
163
|
+
<span class="text-sm text-slate-400 mb-1">%</span>
|
|
164
|
+
</div>
|
|
165
|
+
<div class="w-full h-2.5 bg-slate-700/80 rounded-full mb-3 overflow-hidden">
|
|
166
|
+
<div class="${color} progress-bar h-full rounded-full" style="width:${Math.min(pct, 100)}%"></div>
|
|
167
|
+
</div>
|
|
168
|
+
<div class="text-xs text-slate-500 space-y-0.5">
|
|
169
|
+
<p class="truncate" title="${esc(cpu.model)}">${esc(cpu.model)}</p>
|
|
170
|
+
<p>load: <span class="tabular-nums">${cpu.loadAvg["1min"].toFixed(2)}</span> / <span class="tabular-nums">${cpu.loadAvg["5min"].toFixed(2)}</span> / <span class="tabular-nums">${cpu.loadAvg["15min"].toFixed(2)}</span></p>
|
|
171
|
+
</div>
|
|
172
|
+
</div>`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function memCard(mem: SystemMetrics["memory"]): string {
|
|
176
|
+
const pct = mem.usagePercent;
|
|
177
|
+
const color = barColor(pct, "mem");
|
|
178
|
+
const glow = pct > 80 ? "glow-red" : "";
|
|
179
|
+
return /* html */ `
|
|
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">
|
|
181
|
+
<div class="flex items-center justify-between mb-3">
|
|
182
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-400">Memory</h3>
|
|
183
|
+
<span class="text-[10px] font-mono text-slate-500">${formatBytes(mem.total)}</span>
|
|
184
|
+
</div>
|
|
185
|
+
<div class="flex items-end gap-2 mb-3">
|
|
186
|
+
<span class="text-4xl font-bold tracking-tight tabular-nums text-slate-100" id="_mem_val" data-val="${pct}">${pct.toFixed(1)}</span>
|
|
187
|
+
<span class="text-sm text-slate-400 mb-1">%</span>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="w-full h-2.5 bg-slate-700/80 rounded-full mb-3 overflow-hidden">
|
|
190
|
+
<div class="${color} progress-bar h-full rounded-full" style="width:${Math.min(pct, 100)}%"></div>
|
|
191
|
+
</div>
|
|
192
|
+
<div class="text-xs text-slate-500 space-y-0.5">
|
|
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>
|
|
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
|
+
}
|
|
199
|
+
</div>
|
|
200
|
+
</div>`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function diskSection(disks: DiskInfo[]): string {
|
|
204
|
+
if (disks.length === 0) return "";
|
|
205
|
+
|
|
206
|
+
const rows = disks
|
|
207
|
+
.map((d) => {
|
|
208
|
+
const bar = barColor(d.usagePercent, "disk");
|
|
209
|
+
const txt = pctColor(d.usagePercent);
|
|
210
|
+
return /* html */ `
|
|
211
|
+
<tr class="hover:bg-slate-800/30 transition-colors">
|
|
212
|
+
<td class="py-1.5 pr-3 text-slate-300 font-medium">${esc(d.mount)}</td>
|
|
213
|
+
<td class="py-1.5 pr-3 text-right text-slate-400 text-xs tabular-nums">${formatBytes(d.used)} / ${formatBytes(d.total)}</td>
|
|
214
|
+
<td class="py-1.5 w-36">
|
|
215
|
+
<div class="w-full bg-slate-700/60 rounded-full h-2 overflow-hidden">
|
|
216
|
+
<div class="${bar} progress-bar h-full rounded-full" style="width:${Math.min(d.usagePercent, 100)}%"></div>
|
|
217
|
+
</div>
|
|
218
|
+
</td>
|
|
219
|
+
<td class="py-1.5 pl-2 text-right text-xs font-semibold tabular-nums ${txt}">${d.usagePercent}%</td>
|
|
220
|
+
</tr>`;
|
|
221
|
+
})
|
|
222
|
+
.join("");
|
|
223
|
+
|
|
224
|
+
return /* html */ `
|
|
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">
|
|
226
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-400 mb-3">Disks</h3>
|
|
227
|
+
<div class="overflow-x-auto">
|
|
228
|
+
<table class="w-full text-xs">
|
|
229
|
+
<thead>
|
|
230
|
+
<tr class="text-slate-500 text-left border-b border-slate-800">
|
|
231
|
+
<th class="pb-2 pr-3 font-medium uppercase tracking-wider text-[10px]">Mount</th>
|
|
232
|
+
<th class="pb-2 pr-3 text-right font-medium uppercase tracking-wider text-[10px]">Usage</th>
|
|
233
|
+
<th class="pb-2 w-36"></th>
|
|
234
|
+
<th class="pb-2 pl-2 text-right font-medium uppercase tracking-wider text-[10px]">%</th>
|
|
235
|
+
</tr>
|
|
236
|
+
</thead>
|
|
237
|
+
<tbody>${rows}</tbody>
|
|
238
|
+
</table>
|
|
239
|
+
</div>
|
|
240
|
+
</div>`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function netCard(net: SystemMetrics["network"]): string {
|
|
244
|
+
const rxWidth = Math.min((net.rxRate / NET_BAR_SCALE) * 100, 100);
|
|
245
|
+
const txWidth = Math.min((net.txRate / NET_BAR_SCALE) * 100, 100);
|
|
246
|
+
|
|
247
|
+
return /* html */ `
|
|
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>
|
|
251
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-400 mb-3">Network</h3>
|
|
252
|
+
<div class="space-y-4">
|
|
253
|
+
<div>
|
|
254
|
+
<div class="flex justify-between text-xs mb-1.5">
|
|
255
|
+
<span class="text-slate-400 flex items-center gap-1.5"><span class="text-emerald-400">↓</span> RX</span>
|
|
256
|
+
<span class="text-emerald-400 font-semibold tabular-nums">${formatRate(net.rxRate)}</span>
|
|
257
|
+
</div>
|
|
258
|
+
<div class="w-full bg-slate-700/60 rounded-full h-1.5 overflow-hidden">
|
|
259
|
+
<div class="bg-emerald-500 h-full rounded-full transition-all duration-500" style="width:${rxWidth}%"></div>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<div>
|
|
263
|
+
<div class="flex justify-between text-xs mb-1.5">
|
|
264
|
+
<span class="text-slate-400 flex items-center gap-1.5"><span class="text-blue-400">↑</span> TX</span>
|
|
265
|
+
<span class="text-blue-400 font-semibold tabular-nums">${formatRate(net.txRate)}</span>
|
|
266
|
+
</div>
|
|
267
|
+
<div class="w-full bg-slate-700/60 rounded-full h-1.5 overflow-hidden">
|
|
268
|
+
<div class="bg-blue-500 h-full rounded-full transition-all duration-500" style="width:${txWidth}%"></div>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
<div class="text-xs text-slate-500 pt-2 border-t border-slate-800 flex justify-between">
|
|
272
|
+
<span>total ↓ ${formatBytes(net.rxTotal)}</span>
|
|
273
|
+
<span>total ↑ ${formatBytes(net.txTotal)}</span>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
</div>`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function procSection(procs: SystemMetrics["topProcs"]): string {
|
|
280
|
+
if (procs.length === 0) return "";
|
|
281
|
+
|
|
282
|
+
const rows = procs
|
|
283
|
+
.map((p, i) => {
|
|
284
|
+
const highlight = i === 0 ? "text-yellow-400" : "text-slate-200";
|
|
285
|
+
return /* html */ `
|
|
286
|
+
<tr class="hover:bg-slate-800/30 transition-colors">
|
|
287
|
+
<td class="py-1.5 pr-3 text-slate-500 tabular-nums">${p.pid}</td>
|
|
288
|
+
<td class="py-1.5 pr-3 ${highlight} truncate max-w-[200px]" title="${esc(p.name)}">${esc(p.name)}</td>
|
|
289
|
+
<td class="py-1.5 pr-3 text-right text-slate-300 tabular-nums">${p.cpuPercent.toFixed(1)}%</td>
|
|
290
|
+
<td class="py-1.5 text-right text-slate-300 tabular-nums">${p.memPercent.toFixed(1)}%</td>
|
|
291
|
+
</tr>`;
|
|
292
|
+
})
|
|
293
|
+
.join("");
|
|
294
|
+
|
|
295
|
+
return /* html */ `
|
|
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">
|
|
297
|
+
<div class="flex items-center justify-between mb-3">
|
|
298
|
+
<h3 class="text-xs font-semibold uppercase tracking-wider text-slate-400">Top Processes</h3>
|
|
299
|
+
<span class="text-[10px] text-slate-500">by CPU</span>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="overflow-x-auto">
|
|
302
|
+
<table class="w-full text-xs">
|
|
303
|
+
<thead>
|
|
304
|
+
<tr class="text-slate-500 text-left border-b border-slate-800">
|
|
305
|
+
<th class="pb-2 pr-3 font-medium uppercase tracking-wider text-[10px]">PID</th>
|
|
306
|
+
<th class="pb-2 pr-3 font-medium uppercase tracking-wider text-[10px]">Name</th>
|
|
307
|
+
<th class="pb-2 pr-3 text-right font-medium uppercase tracking-wider text-[10px]">CPU</th>
|
|
308
|
+
<th class="pb-2 text-right font-medium uppercase tracking-wider text-[10px]">MEM</th>
|
|
309
|
+
</tr>
|
|
310
|
+
</thead>
|
|
311
|
+
<tbody>${rows}</tbody>
|
|
312
|
+
</table>
|
|
313
|
+
</div>
|
|
314
|
+
</div>`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function chartSection(): string {
|
|
318
|
+
return /* html */ `
|
|
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>
|
|
345
|
+
</div>
|
|
346
|
+
<p class="text-[10px] text-slate-600 text-right mt-2">60 samples · ≈5 min rolling window</p>
|
|
347
|
+
</div>`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function footerSection(hostname: string): string {
|
|
351
|
+
return /* html */ `
|
|
352
|
+
<footer class="max-w-6xl mx-auto mt-6 pb-6 text-center text-[10px] text-slate-600 space-y-0.5">
|
|
353
|
+
<p>ServerMon · ${esc(hostname)}</p>
|
|
354
|
+
<p>pressing <kbd class="bg-slate-800 px-1 rounded text-slate-400">Ctrl+C</kbd> in the terminal stops this dashboard</p>
|
|
355
|
+
</footer>`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** HTMX-swappable dashboard body (replaces #dashboard container). */
|
|
359
|
+
function metricsFragment(metrics: SystemMetrics): string {
|
|
360
|
+
return /* html */ `
|
|
361
|
+
<div id="dashboard" class="max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-4"
|
|
362
|
+
hx-get="/_dashboard/metrics" hx-trigger="every 5s" hx-swap="outerHTML" hx-target="#dashboard">
|
|
363
|
+
${cpuCard(metrics.cpu)}
|
|
364
|
+
${memCard(metrics.memory)}
|
|
365
|
+
${netCard(metrics.network)}
|
|
366
|
+
${chartSection()}
|
|
367
|
+
${diskSection(metrics.disks)}
|
|
368
|
+
${procSection(metrics.topProcs)}
|
|
369
|
+
</div>`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Full HTML page (served on first load). */
|
|
373
|
+
function layout(metrics: SystemMetrics, port: number, version: string): string {
|
|
374
|
+
const { hostname, platform, arch, uptime, temperature } = metrics;
|
|
375
|
+
|
|
376
|
+
return /* html */ `<!DOCTYPE html>
|
|
377
|
+
<html lang="en" class="dark">
|
|
378
|
+
<head>
|
|
379
|
+
<meta charset="UTF-8" />
|
|
380
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
381
|
+
<title>ServerMon — ${esc(hostname)}</title>
|
|
382
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
383
|
+
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
|
384
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
|
|
385
|
+
<script>${THEME_SCRIPT}</script>
|
|
386
|
+
<style>${STYLES}</style>
|
|
387
|
+
</head>
|
|
388
|
+
<body class="bg-slate-950 text-slate-100 min-h-screen p-4 md:p-6">
|
|
389
|
+
${headerSection(hostname, platform, arch, uptime, temperature, port, version)}
|
|
390
|
+
${metricsFragment(metrics)}
|
|
391
|
+
${footerSection(hostname)}
|
|
392
|
+
|
|
393
|
+
<script>
|
|
394
|
+
(function() {
|
|
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() {
|
|
469
|
+
const canvas = document.getElementById("historyChart");
|
|
470
|
+
const netCanvas = document.getElementById("netChart");
|
|
471
|
+
if (!canvas || window._chartInited) return;
|
|
472
|
+
window._chartInited = true;
|
|
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 -- */
|
|
489
|
+
const ctx = canvas.getContext("2d");
|
|
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)");
|
|
492
|
+
|
|
493
|
+
window.__chart = new Chart(ctx, {
|
|
494
|
+
type: "line",
|
|
495
|
+
data: {
|
|
496
|
+
labels: history.labels,
|
|
497
|
+
datasets: [
|
|
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
|
+
},
|
|
516
|
+
],
|
|
517
|
+
},
|
|
518
|
+
options: baseOptions("%", 100),
|
|
519
|
+
});
|
|
520
|
+
|
|
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
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function pushMetrics() {
|
|
562
|
+
const cpuEl = document.getElementById("_cpu_val");
|
|
563
|
+
const memEl = document.getElementById("_mem_val");
|
|
564
|
+
const rxEl = document.getElementById("_rx_val");
|
|
565
|
+
const txEl = document.getElementById("_tx_val");
|
|
566
|
+
if (!cpuEl || !memEl || !window.__history) return;
|
|
567
|
+
|
|
568
|
+
const h = window.__history;
|
|
569
|
+
const label = nowLabel();
|
|
570
|
+
|
|
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();
|
|
581
|
+
}
|
|
582
|
+
|
|
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). */
|
|
592
|
+
document.body.addEventListener("htmx:afterSwap", function() {
|
|
593
|
+
window._chartInited = false;
|
|
594
|
+
initCharts();
|
|
595
|
+
setTimeout(pushMetrics, 100);
|
|
596
|
+
});
|
|
597
|
+
})();
|
|
598
|
+
</script>
|
|
599
|
+
</body>
|
|
600
|
+
</html>`;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/* ------------------------------------------------------------------ */
|
|
604
|
+
/* Application layer */
|
|
605
|
+
/* ------------------------------------------------------------------ */
|
|
606
|
+
|
|
607
|
+
async function getMetrics(): Promise<SystemMetrics> {
|
|
608
|
+
const m = await collectMetrics();
|
|
609
|
+
if (m.cpu.usagePercent > 100) m.cpu.usagePercent = 100;
|
|
610
|
+
return m;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function createDashboardApp(port: number): Elysia {
|
|
614
|
+
return new Elysia()
|
|
615
|
+
.use(html())
|
|
616
|
+
.get("/", async () => {
|
|
617
|
+
try {
|
|
618
|
+
const metrics = await getMetrics();
|
|
619
|
+
return layout(metrics, port, APP_VERSION);
|
|
620
|
+
} catch (err) {
|
|
621
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
622
|
+
return layoutError(`Failed to collect metrics: ${esc(msg)}`);
|
|
623
|
+
}
|
|
624
|
+
})
|
|
625
|
+
.get("/_dashboard/metrics", async () => {
|
|
626
|
+
try {
|
|
627
|
+
const metrics = await getMetrics();
|
|
628
|
+
return metricsFragment(metrics);
|
|
629
|
+
} catch {
|
|
630
|
+
return `<div id="dashboard" class="max-w-6xl mx-auto text-center py-12">
|
|
631
|
+
<p class="text-red-400">⚠️ Failed to refresh metrics. Retrying in 5s…</p>
|
|
632
|
+
</div>`;
|
|
633
|
+
}
|
|
634
|
+
})
|
|
635
|
+
.get("/_dashboard/health", () => ({
|
|
636
|
+
status: "ok",
|
|
637
|
+
uptime: Math.floor(process.uptime()),
|
|
638
|
+
version: APP_VERSION,
|
|
639
|
+
}));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function layoutError(message: string): string {
|
|
643
|
+
return /* html */ `<!DOCTYPE html>
|
|
644
|
+
<html lang="en" class="dark">
|
|
645
|
+
<head><meta charset="UTF-8"><title>ServerMon — Error</title>
|
|
646
|
+
<script src="https://cdn.tailwindcss.com"></script></head>
|
|
647
|
+
<body class="bg-slate-950 text-slate-100 min-h-screen flex items-center justify-center p-8">
|
|
648
|
+
<div class="bg-slate-900 border border-red-800/50 rounded-xl p-8 max-w-lg text-center">
|
|
649
|
+
<div class="text-4xl mb-4">⚠️</div>
|
|
650
|
+
<h1 class="text-lg font-bold mb-2">Dashboard Error</h1>
|
|
651
|
+
<p class="text-sm text-slate-400 mb-4">${message}</p>
|
|
652
|
+
<a href="/" class="text-sm text-emerald-400 hover:underline">Retry</a>
|
|
653
|
+
</div>
|
|
654
|
+
</body></html>`;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/* ------------------------------------------------------------------ */
|
|
658
|
+
/* Public entry point */
|
|
659
|
+
/* ------------------------------------------------------------------ */
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Start the embedded web dashboard.
|
|
663
|
+
*
|
|
664
|
+
* Usage: `servermon dashboard --port 3456`
|
|
665
|
+
*
|
|
666
|
+
* Opens an HTMX + Elysia dashboard showing real-time system metrics.
|
|
667
|
+
*/
|
|
668
|
+
export async function startDashboard(port: number): Promise<void> {
|
|
669
|
+
const app = createDashboardApp(port);
|
|
670
|
+
|
|
671
|
+
console.log();
|
|
672
|
+
console.log(` 🌐 Dashboard starting at http://localhost:${port}`);
|
|
673
|
+
console.log(` ⏱ Auto-refresh every 5s (HTMX + Chart.js)`);
|
|
674
|
+
console.log(` ⌨ Press Ctrl+C to stop`);
|
|
675
|
+
console.log();
|
|
676
|
+
|
|
677
|
+
app.listen(port, () => {
|
|
678
|
+
console.log(` ✅ ServerMon Dashboard — http://localhost:${port}`);
|
|
679
|
+
console.log();
|
|
680
|
+
});
|
|
681
|
+
}
|
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 ---- */
|
|
@@ -237,6 +229,24 @@ export function createApp(): Crust {
|
|
|
237
229
|
})
|
|
238
230
|
)
|
|
239
231
|
|
|
232
|
+
/* ---- dashboard ---- */
|
|
233
|
+
.command("dashboard", (cmd) =>
|
|
234
|
+
cmd
|
|
235
|
+
.meta({ description: "Start embedded web dashboard (HTMX + Elysia)" })
|
|
236
|
+
.flags({
|
|
237
|
+
port: {
|
|
238
|
+
type: "number",
|
|
239
|
+
description: "HTTP port (default: 3456)",
|
|
240
|
+
short: "p",
|
|
241
|
+
default: 3456,
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
.run(async ({ flags }) => {
|
|
245
|
+
const { startDashboard } = await import("./dashboard");
|
|
246
|
+
await startDashboard(flags.port ?? 3456);
|
|
247
|
+
})
|
|
248
|
+
)
|
|
249
|
+
|
|
240
250
|
/* ---- service ---- */
|
|
241
251
|
.command("service", (cmd) => serviceCmd(cmd))
|
|
242
252
|
|
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/) */
|