saymon-syswatch-linux 1.0.2 โ 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +229 -59
- package/package.json +2 -2
- package/public/index.html +127 -20
- package/src/routes.js +7 -0
- package/src/services.js +19 -1
package/README.md
CHANGED
|
@@ -1,32 +1,61 @@
|
|
|
1
|
-
# SysWatch
|
|
1
|
+
# SysWatch
|
|
2
2
|
|
|
3
3
|
**Linux system monitor + learning dashboard** โ install it, run it, done.
|
|
4
4
|
|
|
5
|
-
Monitor CPU, memory, disk, services, journal logs and processes from a web UI.
|
|
5
|
+
Monitor CPU, memory, disk, services, journal logs and processes from a clean web UI.
|
|
6
6
|
Every panel shows the exact Linux commands being used โ so you **learn while you monitor**.
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
| Requirement | Version |
|
|
13
|
+
|---|---|
|
|
14
|
+
| OS | Linux only (Ubuntu 20.04+, Debian 11+, RHEL 8+, Arch, etc.) |
|
|
15
|
+
| Node.js | 18 or higher |
|
|
16
|
+
| systemd | Required for service control and journalctl |
|
|
17
|
+
| Permissions | Must run as `root` / `sudo` |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Step 1 โ Install Node.js 18+ (if not already installed)
|
|
11
22
|
|
|
12
23
|
```bash
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
node --version
|
|
25
|
+
```
|
|
15
26
|
|
|
16
|
-
|
|
17
|
-
sudo syswatch
|
|
27
|
+
If missing or below v18:
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
```bash
|
|
30
|
+
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
|
31
|
+
sudo apt install -y nodejs
|
|
32
|
+
node --version # should print v20.x.x
|
|
21
33
|
```
|
|
22
34
|
|
|
23
|
-
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Step 2 โ Install SysWatch globally
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
sudo npm install -g saymon-syswatch-linux
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Verify installation:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
which syswatch # shows path e.g. /usr/local/bin/syswatch
|
|
47
|
+
syswatch --help
|
|
48
|
+
```
|
|
24
49
|
|
|
25
50
|
---
|
|
26
51
|
|
|
27
|
-
##
|
|
52
|
+
## Step 3 โ Run the first-time setup wizard
|
|
28
53
|
|
|
29
|
-
|
|
54
|
+
```bash
|
|
55
|
+
sudo syswatch
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The wizard runs automatically when no config is found. It asks 4 questions:
|
|
30
59
|
|
|
31
60
|
```
|
|
32
61
|
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
|
|
@@ -40,68 +69,61 @@ Step 1/4 โ Set your login password
|
|
|
40
69
|
โ Password set!
|
|
41
70
|
|
|
42
71
|
Step 2/4 โ HTTP Port
|
|
43
|
-
Port [8080]:
|
|
44
|
-
โ HTTP port:
|
|
72
|
+
Port [8080]: 8585 โ pick any free port
|
|
73
|
+
โ HTTP port: 8585
|
|
45
74
|
|
|
46
75
|
Step 3/4 โ Who can access SysWatch?
|
|
47
|
-
1) Only this computer (
|
|
48
|
-
2) Anyone on my network (0.0.0.0)
|
|
49
|
-
Choose [1]:
|
|
50
|
-
โ Access:
|
|
76
|
+
1) Only this computer (127.0.0.1) โ safer
|
|
77
|
+
2) Anyone on my network (0.0.0.0) โ for remote/LAN access
|
|
78
|
+
Choose [1]: 2
|
|
79
|
+
โ Access: Network (0.0.0.0)
|
|
51
80
|
|
|
52
|
-
Step 4/4 โ Enable HTTPS?
|
|
53
|
-
Enable HTTPS? [y/N]:
|
|
81
|
+
Step 4/4 โ Enable HTTPS? (optional)
|
|
82
|
+
Enable HTTPS? [y/N]: N
|
|
54
83
|
โ HTTPS: Disabled
|
|
55
84
|
|
|
56
|
-
โ Setup complete!
|
|
57
|
-
|
|
85
|
+
โ Setup complete!
|
|
86
|
+
Config saved โ /etc/syswatch/syswatch.config.json
|
|
58
87
|
```
|
|
59
88
|
|
|
60
|
-
|
|
89
|
+
Config is saved to a **fixed path** โ never in your current directory:
|
|
90
|
+
|
|
91
|
+
| Run as | Config location |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `sudo` / root | `/etc/syswatch/syswatch.config.json` |
|
|
94
|
+
| Normal user | `~/.config/syswatch/syswatch.config.json` |
|
|
61
95
|
|
|
62
96
|
---
|
|
63
97
|
|
|
64
|
-
##
|
|
98
|
+
## Step 4 โ Open your browser
|
|
65
99
|
|
|
66
|
-
```
|
|
67
|
-
|
|
100
|
+
```
|
|
101
|
+
http://YOUR_SERVER_IP:PORT
|
|
68
102
|
```
|
|
69
103
|
|
|
70
|
-
|
|
104
|
+
Example: `http://192.168.1.10:8585`
|
|
71
105
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
sudo syswatch # first run = setup wizard, then start
|
|
76
|
-
sudo syswatch --setup # re-run setup wizard anytime
|
|
77
|
-
sudo syswatch --no-auth # skip password (local testing only)
|
|
78
|
-
sudo syswatch --port 3000 # override port
|
|
79
|
-
sudo syswatch --host 0.0.0.0 # allow LAN access
|
|
80
|
-
sudo syswatch --https # enable HTTPS too
|
|
81
|
-
sudo syswatch --help # show all options
|
|
82
|
-
```
|
|
106
|
+
> **On AWS EC2 / cloud servers:** You must also open the port in your **Security Group inbound rules** (Custom TCP โ your port โ 0.0.0.0/0). UFW alone is not enough on cloud VMs.
|
|
83
107
|
|
|
84
108
|
---
|
|
85
109
|
|
|
86
|
-
##
|
|
110
|
+
## Step 5 โ Set up auto-start on boot (recommended)
|
|
87
111
|
|
|
88
|
-
|
|
112
|
+
### Find the correct binary path first
|
|
89
113
|
|
|
90
|
-
**Let's Encrypt (real domain):**
|
|
91
114
|
```bash
|
|
92
|
-
|
|
93
|
-
#
|
|
94
|
-
# "certPath": "/etc/letsencrypt/live/yourdomain.com/fullchain.pem"
|
|
95
|
-
# "keyPath": "/etc/letsencrypt/live/yourdomain.com/privkey.pem"
|
|
115
|
+
which syswatch
|
|
116
|
+
# Usually: /usr/local/bin/syswatch
|
|
96
117
|
```
|
|
97
118
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
## ๐ Auto-start on boot (systemd)
|
|
119
|
+
### Create the systemd service
|
|
101
120
|
|
|
102
121
|
```bash
|
|
103
122
|
sudo nano /etc/systemd/system/syswatch.service
|
|
104
123
|
```
|
|
124
|
+
|
|
125
|
+
Paste (replace the path if `which syswatch` gave a different result):
|
|
126
|
+
|
|
105
127
|
```ini
|
|
106
128
|
[Unit]
|
|
107
129
|
Description=SysWatch Linux Monitor
|
|
@@ -110,28 +132,103 @@ After=network.target
|
|
|
110
132
|
[Service]
|
|
111
133
|
Type=simple
|
|
112
134
|
User=root
|
|
113
|
-
ExecStart=/usr/bin/syswatch
|
|
135
|
+
ExecStart=/usr/local/bin/syswatch
|
|
114
136
|
Restart=on-failure
|
|
137
|
+
RestartSec=5
|
|
115
138
|
|
|
116
139
|
[Install]
|
|
117
140
|
WantedBy=multi-user.target
|
|
118
141
|
```
|
|
142
|
+
|
|
143
|
+
### Enable and start
|
|
144
|
+
|
|
119
145
|
```bash
|
|
120
146
|
sudo systemctl daemon-reload
|
|
121
147
|
sudo systemctl enable --now syswatch
|
|
148
|
+
sudo systemctl status syswatch
|
|
122
149
|
```
|
|
123
150
|
|
|
151
|
+
You should see `Active: active (running)`.
|
|
152
|
+
|
|
124
153
|
---
|
|
125
154
|
|
|
126
|
-
##
|
|
155
|
+
## CLI Options
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
sudo syswatch # first run = setup wizard, then start
|
|
159
|
+
sudo syswatch --setup # re-run setup wizard anytime
|
|
160
|
+
sudo syswatch --no-auth # skip password (local testing only)
|
|
161
|
+
sudo syswatch --port 3000 # override port
|
|
162
|
+
sudo syswatch --host 0.0.0.0 # allow LAN/remote access
|
|
163
|
+
sudo syswatch --https # enable HTTPS
|
|
164
|
+
sudo syswatch --help # show all options
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Running alongside other services (Jitsi, nginx, etc.)
|
|
170
|
+
|
|
171
|
+
If your server already runs other software, **check which ports are in use first**:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
sudo ss -tlnp | grep LISTEN
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Then pick a free port for SysWatch. Common ports to avoid:
|
|
178
|
+
|
|
179
|
+
| Port | Used by |
|
|
180
|
+
|---|---|
|
|
181
|
+
| 80, 443 | nginx / Apache / Jitsi web |
|
|
182
|
+
| 4443 | Jitsi Videobridge TCP fallback |
|
|
183
|
+
| 8080, 8888 | Jitsi internal APIs |
|
|
184
|
+
| 9090 | Jitsi Jicofo REST API |
|
|
185
|
+
| 5222, 5269 | Prosody XMPP |
|
|
186
|
+
| 22 | SSH |
|
|
187
|
+
|
|
188
|
+
**Safe choices:** `8585`, `7070`, `3001`, `9191`, `9999`
|
|
189
|
+
|
|
190
|
+
Re-run setup to change port anytime:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
sudo syswatch --setup
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## HTTPS
|
|
199
|
+
|
|
200
|
+
### Self-signed certificate (quick)
|
|
127
201
|
|
|
128
|
-
|
|
129
|
-
|
|
202
|
+
Answer `y` to the HTTPS question during setup. A certificate is auto-generated using `openssl`. Your browser will show a "Not Secure" warning โ click **Advanced โ Proceed**. This is normal for self-signed certs.
|
|
203
|
+
|
|
204
|
+
### Let's Encrypt (real domain)
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
sudo certbot certonly --standalone -d yourdomain.com
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Then edit `/etc/syswatch/syswatch.config.json`:
|
|
211
|
+
|
|
212
|
+
```json
|
|
213
|
+
{
|
|
214
|
+
"certPath": "/etc/letsencrypt/live/yourdomain.com/fullchain.pem",
|
|
215
|
+
"keyPath": "/etc/letsencrypt/live/yourdomain.com/privkey.pem"
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Restart: `sudo systemctl restart syswatch`
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Learn Mode
|
|
224
|
+
|
|
225
|
+
Toggle **Learn Mode** in the dashboard top-right corner. Every panel reveals:
|
|
226
|
+
- The exact Linux command being executed
|
|
130
227
|
- What each flag does
|
|
131
228
|
- The Node.js code that calls it
|
|
132
229
|
|
|
133
230
|
| Panel | Commands you learn |
|
|
134
|
-
|
|
231
|
+
|---|---|
|
|
135
232
|
| Overview | `/proc/stat` ยท `/proc/meminfo` ยท `/proc/loadavg` ยท `/proc/net/dev` |
|
|
136
233
|
| Services | `systemctl list-units` ยท `systemctl show` ยท `systemctl restart` |
|
|
137
234
|
| Logs | `journalctl -f -u nginx -p err -o json --since "1h ago"` |
|
|
@@ -140,15 +237,88 @@ Toggle **๐ Learn Mode** in the dashboard top-right. Every panel shows:
|
|
|
140
237
|
|
|
141
238
|
---
|
|
142
239
|
|
|
143
|
-
##
|
|
240
|
+
## Troubleshooting
|
|
241
|
+
|
|
242
|
+
### `status=203/EXEC` โ binary not found by systemd
|
|
243
|
+
|
|
244
|
+
Systemd uses a minimal PATH. Find the real binary path and update the service:
|
|
144
245
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
246
|
+
```bash
|
|
247
|
+
which syswatch
|
|
248
|
+
# e.g. /usr/local/bin/syswatch
|
|
249
|
+
|
|
250
|
+
sudo sed -i "s|ExecStart=.*|ExecStart=$(which syswatch)|" /etc/systemd/system/syswatch.service
|
|
251
|
+
sudo systemctl daemon-reload
|
|
252
|
+
sudo systemctl restart syswatch
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
### `EADDRINUSE` โ port already in use
|
|
258
|
+
|
|
259
|
+
You started syswatch manually and then also started it via systemd (two instances fighting for the same port). Kill the manual one:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
sudo pkill -f syswatch
|
|
263
|
+
sleep 2
|
|
264
|
+
sudo systemctl restart syswatch
|
|
265
|
+
sudo systemctl status syswatch
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Always use systemd to manage the process โ never run `syswatch &` in the background manually on a server.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### Browser shows "This site can't be reached" on a cloud server
|
|
273
|
+
|
|
274
|
+
Two things to check:
|
|
275
|
+
|
|
276
|
+
**1. UFW firewall:**
|
|
277
|
+
```bash
|
|
278
|
+
sudo ufw allow YOUR_PORT/tcp
|
|
279
|
+
sudo ufw status
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**2. AWS / cloud Security Group:**
|
|
283
|
+
Go to EC2 โ Instances โ your instance โ Security tab โ Edit inbound rules โ Add rule:
|
|
284
|
+
- Type: Custom TCP
|
|
285
|
+
- Port: your chosen port (e.g. 8585)
|
|
286
|
+
- Source: 0.0.0.0/0
|
|
287
|
+
|
|
288
|
+
UFW alone is not enough on AWS EC2, GCP, Azure, etc. โ the cloud-level firewall must also allow the port.
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
### View live logs
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
sudo journalctl -u syswatch -f
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
### Reset / change password or port
|
|
301
|
+
|
|
302
|
+
```bash
|
|
303
|
+
sudo syswatch --setup
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
This re-runs the full wizard and overwrites the config.
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
### Uninstall
|
|
311
|
+
|
|
312
|
+
```bash
|
|
313
|
+
sudo systemctl disable --now syswatch
|
|
314
|
+
sudo rm /etc/systemd/system/syswatch.service
|
|
315
|
+
sudo systemctl daemon-reload
|
|
316
|
+
sudo npm uninstall -g saymon-syswatch-linux
|
|
317
|
+
sudo rm -rf /etc/syswatch
|
|
318
|
+
```
|
|
149
319
|
|
|
150
320
|
---
|
|
151
321
|
|
|
152
|
-
##
|
|
322
|
+
## License
|
|
153
323
|
|
|
154
324
|
MIT
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saymon-syswatch-linux",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Linux system monitor & learning dashboard โ HTTP/HTTPS, journal logs, services, processes",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"syswatch": "
|
|
7
|
+
"syswatch": "bin/syswatch.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/server.js",
|
package/public/index.html
CHANGED
|
@@ -190,10 +190,7 @@ body{background:var(--bg);color:var(--text);font-family:var(--mono);font-size:13
|
|
|
190
190
|
</div>
|
|
191
191
|
<div class="nav-group">
|
|
192
192
|
<div class="nav-group-label">Quick Units</div>
|
|
193
|
-
<div
|
|
194
|
-
<div class="nav-item" onclick="gotoLogs('sshd')"><div class="sdot dg"></div>sshd</div>
|
|
195
|
-
<div class="nav-item" onclick="gotoLogs('docker')"><div class="sdot dy"></div>docker</div>
|
|
196
|
-
<div class="nav-item" onclick="gotoLogs('postgresql')"><div class="sdot dy"></div>postgresql</div>
|
|
193
|
+
<div id="quickUnitsNav"><div style="padding:8px 16px;font-size:11px;color:var(--muted)">loading...</div></div>
|
|
197
194
|
</div>
|
|
198
195
|
<div style="padding:16px;margin-top:auto;border-top:1px solid var(--border)">
|
|
199
196
|
<div style="font-size:10px;color:var(--muted);line-height:1.8">
|
|
@@ -326,10 +323,6 @@ body{background:var(--bg);color:var(--text);font-family:var(--mono);font-size:13
|
|
|
326
323
|
<input class="inp inp-grow" type="text" id="logSearch" placeholder="grep pattern..." oninput="applyLogFilter()">
|
|
327
324
|
<select class="inp" id="logUnit" onchange="reconnectSSE()">
|
|
328
325
|
<option value="">All units</option>
|
|
329
|
-
<option value="nginx">nginx</option>
|
|
330
|
-
<option value="sshd">sshd</option>
|
|
331
|
-
<option value="docker">docker</option>
|
|
332
|
-
<option value="postgresql">postgresql</option>
|
|
333
326
|
<option value="kernel">kernel</option>
|
|
334
327
|
</select>
|
|
335
328
|
<select class="inp" id="logPrio" onchange="reconnectSSE()">
|
|
@@ -350,9 +343,12 @@ body{background:var(--bg);color:var(--text);font-family:var(--mono);font-size:13
|
|
|
350
343
|
</div>
|
|
351
344
|
<div class="learn-panel" id="lp-logs">
|
|
352
345
|
<div class="lp-inner">
|
|
353
|
-
<div class="ls"
|
|
346
|
+
<div class="ls">
|
|
347
|
+
<div class="ls-title">โก journalctl Commands</div>
|
|
348
|
+
<!-- Dynamic: updates when unit/priority filter changes -->
|
|
349
|
+
<div id="liveCmdBlock"></div>
|
|
354
350
|
<div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">Follow live</span><span class="cmd-copy" onclick="cc(this,'journalctl -f')">copy</span></div><div class="cmd-code">journalctl -f</div><div class="cmd-expl">Like <code class="ic">tail -f</code> for systemd journal. Streams new entries in real time. Ctrl+C to stop.</div><div class="flags"><div class="flag"><span class="fn">-f</span><span class="fd">Follow โ keep streaming new entries</span></div><div class="flag"><span class="fn">-u nginx</span><span class="fd">Filter to only nginx.service</span></div><div class="flag"><span class="fn">-p err</span><span class="fd">Priority: emerg/alert/crit/err/warning/notice/info/debug</span></div><div class="flag"><span class="fn">-n 100</span><span class="fd">Show last N lines only</span></div><div class="flag"><span class="fn">--since "1h ago"</span><span class="fd">Time filter โ relative or absolute</span></div><div class="flag"><span class="fn">-o json</span><span class="fd">JSON output โ one JSON object per line, perfect for Node.js</span></div><div class="flag"><span class="fn">-b</span><span class="fd">Only this boot's logs</span></div><div class="flag"><span class="fn">-b -1</span><span class="fd">Previous boot's logs</span></div></div></div>
|
|
355
|
-
<div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">Search logs</span><span class="cmd-copy" onclick="cc(this
|
|
351
|
+
<div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">Search logs</span><span class="cmd-copy" id="learnSearchCopy" onclick="cc(this, learnCurrentUnit() ? 'journalctl -u '+learnCurrentUnit()+' --since today -o json | grep ERROR' : 'journalctl --since today -o json | grep ERROR')">copy</span></div><div class="cmd-code" id="learnSearchCmd">journalctl --since today -o json | grep ERROR</div><div class="cmd-expl">With <code class="ic">-o json</code> each entry is one line โ grep produces clean JSON objects. Pipe to <code class="ic">jq</code> for structured filtering.</div></div>
|
|
356
352
|
<div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">Disk usage</span><span class="cmd-copy" onclick="cc(this,'journalctl --disk-usage')">copy</span></div><div class="cmd-code">journalctl --disk-usage</div><div class="cmd-expl">See how much disk the journal uses. Rotate/trim: <code class="ic">journalctl --vacuum-size=500M</code> or <code class="ic">--vacuum-time=7d</code></div></div>
|
|
357
353
|
<div class="node-block"><div class="node-hdr">โฌก Node.js SSE stream</div><div class="node-code"><span class="cm">// Server-Sent Events: real-time log stream</span>
|
|
358
354
|
app.<span class="fn2">get</span>(<span class="str">'/api/logs/stream'</span>, (req, res) => {
|
|
@@ -486,17 +482,129 @@ function goto(id) {
|
|
|
486
482
|
if (id === 'services') loadSvcs();
|
|
487
483
|
if (id === 'processes') loadProcs();
|
|
488
484
|
if (id === 'boot') loadBoot();
|
|
489
|
-
if (id === 'logs') connectSSE();
|
|
485
|
+
if (id === 'logs') { loadQuickUnits(); connectSSE(); }
|
|
490
486
|
}
|
|
491
487
|
|
|
492
488
|
function gotoLogs(unit) {
|
|
493
489
|
goto('logs');
|
|
494
490
|
setTimeout(() => {
|
|
495
491
|
const sel = document.getElementById('logUnit');
|
|
496
|
-
if (sel) {
|
|
492
|
+
if (sel) {
|
|
493
|
+
// ensure option exists (may have been added dynamically)
|
|
494
|
+
if (unit && ![...sel.options].some(o => o.value === unit)) {
|
|
495
|
+
const opt = document.createElement('option');
|
|
496
|
+
opt.value = opt.textContent = unit;
|
|
497
|
+
sel.appendChild(opt);
|
|
498
|
+
}
|
|
499
|
+
sel.value = unit;
|
|
500
|
+
reconnectSSE();
|
|
501
|
+
}
|
|
497
502
|
}, 100);
|
|
498
503
|
}
|
|
499
504
|
|
|
505
|
+
// โโ load real service names from API โโโโโโโโโ
|
|
506
|
+
let _serviceNames = [];
|
|
507
|
+
|
|
508
|
+
async function loadQuickUnits() {
|
|
509
|
+
try {
|
|
510
|
+
const r = await fetch('/api/services/names', { credentials: 'include' });
|
|
511
|
+
if (!r.ok) return;
|
|
512
|
+
const units = await r.json();
|
|
513
|
+
_serviceNames = units;
|
|
514
|
+
|
|
515
|
+
// โโ populate sidebar Quick Units โโโโโโโโโโ
|
|
516
|
+
const nav = document.getElementById('quickUnitsNav');
|
|
517
|
+
if (nav) {
|
|
518
|
+
const active = units.filter(u => u.state === 'active' || u.sub === 'running');
|
|
519
|
+
nav.innerHTML = active.length === 0
|
|
520
|
+
? '<div style="padding:8px 16px;font-size:11px;color:var(--muted)">no active units</div>'
|
|
521
|
+
: active.map(u => {
|
|
522
|
+
const dotCls = u.sub === 'running' ? 'dg'
|
|
523
|
+
: u.state === 'failed' ? 'dr' : 'dy';
|
|
524
|
+
return `<div class="nav-item" onclick="gotoLogs('${u.name}')">
|
|
525
|
+
<div class="sdot ${dotCls}"></div>${u.name}
|
|
526
|
+
</div>`;
|
|
527
|
+
}).join('');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// โโ populate logUnit dropdown โโโโโโโโโโโโโ
|
|
531
|
+
const sel = document.getElementById('logUnit');
|
|
532
|
+
if (sel) {
|
|
533
|
+
// keep "All units" + "kernel", remove old dynamic options
|
|
534
|
+
[...sel.options].forEach(o => {
|
|
535
|
+
if (o.value !== '' && o.value !== 'kernel') o.remove();
|
|
536
|
+
});
|
|
537
|
+
// insert active units alphabetically after "All units"
|
|
538
|
+
const kernelOpt = sel.querySelector('option[value="kernel"]');
|
|
539
|
+
units.forEach(u => {
|
|
540
|
+
const opt = document.createElement('option');
|
|
541
|
+
opt.value = opt.textContent = u.name;
|
|
542
|
+
sel.insertBefore(opt, kernelOpt);
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
} catch (e) {
|
|
546
|
+
console.warn('loadQuickUnits failed', e);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// โโ helper: current selected unit โโโโโโโโโโโโ
|
|
551
|
+
function learnCurrentUnit() {
|
|
552
|
+
return document.getElementById('logUnit')?.value || '';
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// โโ update learn panel live command block โโโโโ
|
|
556
|
+
function updateLearnCmd() {
|
|
557
|
+
const unit = learnCurrentUnit();
|
|
558
|
+
const prio = document.getElementById('logPrio')?.value || '';
|
|
559
|
+
|
|
560
|
+
// Build the exact command parts
|
|
561
|
+
const parts = ['journalctl', '-f', '-o', 'json'];
|
|
562
|
+
const flags = [];
|
|
563
|
+
|
|
564
|
+
if (unit) {
|
|
565
|
+
parts.splice(1, 0, '-u', unit);
|
|
566
|
+
flags.push([`-u ${unit}`, `Filter journal to <strong>${unit}.service</strong> only`]);
|
|
567
|
+
}
|
|
568
|
+
if (prio) {
|
|
569
|
+
parts.splice(unit ? 3 : 1, 0, '-p', prio);
|
|
570
|
+
flags.push([`-p ${prio}`, `Show only <em>${prio}</em> priority and more severe (lower number = more severe)`]);
|
|
571
|
+
}
|
|
572
|
+
flags.push(['-f', 'Follow โ keep streaming new entries in real time']);
|
|
573
|
+
flags.push(['-o json', 'Output as JSON โ one object per line, easy to parse with Node.js or <code class="ic">jq</code>']);
|
|
574
|
+
|
|
575
|
+
const cmd = parts.join(' ');
|
|
576
|
+
|
|
577
|
+
// Update footer status bar
|
|
578
|
+
const cmdEl = document.getElementById('logCmd');
|
|
579
|
+
if (cmdEl) cmdEl.textContent = cmd;
|
|
580
|
+
|
|
581
|
+
// Update learn panel live block
|
|
582
|
+
const block = document.getElementById('liveCmdBlock');
|
|
583
|
+
if (!block) return;
|
|
584
|
+
|
|
585
|
+
const flagsHtml = flags.map(([f, d]) =>
|
|
586
|
+
`<div class="flag"><span class="fn">${f}</span><span class="fd">${d}</span></div>`
|
|
587
|
+
).join('');
|
|
588
|
+
|
|
589
|
+
block.innerHTML = `
|
|
590
|
+
<div class="cmd-block" style="border-color:rgba(63,185,80,.4);margin-bottom:14px">
|
|
591
|
+
<div class="cmd-hdr" style="background:rgba(63,185,80,.08);border-bottom-color:rgba(63,185,80,.25)">
|
|
592
|
+
<span class="cmd-lbl" style="color:var(--green)">โถ RUNNING NOW</span>
|
|
593
|
+
<span class="cmd-copy" onclick="cc(this,'${cmd}')">copy</span>
|
|
594
|
+
</div>
|
|
595
|
+
<div class="cmd-code" style="color:var(--green);font-size:13px">${cmd}</div>
|
|
596
|
+
<div class="flags">${flagsHtml}</div>
|
|
597
|
+
</div>`;
|
|
598
|
+
|
|
599
|
+
// Update the "Search logs" example to use current unit
|
|
600
|
+
const searchCmd = document.getElementById('learnSearchCmd');
|
|
601
|
+
if (searchCmd) {
|
|
602
|
+
searchCmd.textContent = unit
|
|
603
|
+
? `journalctl -u ${unit} --since today -o json | grep ERROR`
|
|
604
|
+
: `journalctl --since today -o json | grep ERROR`;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
500
608
|
// โโ learn panel โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
501
609
|
function toggleLearn() {
|
|
502
610
|
learnOn = !learnOn;
|
|
@@ -519,10 +627,13 @@ async function logout() {
|
|
|
519
627
|
window.location.href = '/login';
|
|
520
628
|
}
|
|
521
629
|
|
|
522
|
-
// Check auth on load
|
|
630
|
+
// Check auth on load, then kick off unit list
|
|
523
631
|
fetch('/auth/status', { credentials: 'include' })
|
|
524
632
|
.then(r => r.json())
|
|
525
|
-
.then(d => {
|
|
633
|
+
.then(d => {
|
|
634
|
+
if (d.authEnabled && !d.authenticated) { window.location.href = '/login'; return; }
|
|
635
|
+
loadQuickUnits();
|
|
636
|
+
});
|
|
526
637
|
|
|
527
638
|
// โโ METRICS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
528
639
|
async function loadMetrics() {
|
|
@@ -703,12 +814,8 @@ function connectSSE() {
|
|
|
703
814
|
if (unit) url += '&unit=' + unit;
|
|
704
815
|
if (prio) url += '&priority=' + prio;
|
|
705
816
|
|
|
706
|
-
// update command
|
|
707
|
-
|
|
708
|
-
if (unit) cmd += ' -u ' + unit;
|
|
709
|
-
if (prio) cmd += ' -p ' + prio;
|
|
710
|
-
const cmdEl = document.getElementById('logCmd');
|
|
711
|
-
if (cmdEl) cmdEl.textContent = cmd;
|
|
817
|
+
// update command display + learn panel
|
|
818
|
+
updateLearnCmd();
|
|
712
819
|
|
|
713
820
|
sseConn = new EventSource(url);
|
|
714
821
|
|
package/src/routes.js
CHANGED
|
@@ -51,6 +51,13 @@ function registerRoutes(app, { requireAuth }) {
|
|
|
51
51
|
res.json(list);
|
|
52
52
|
}));
|
|
53
53
|
|
|
54
|
+
// GET /api/services/names โ lightweight: name + activeState only (for sidebar/dropdowns)
|
|
55
|
+
// Must be defined BEFORE /:name to avoid Express matching "names" as a param
|
|
56
|
+
app.get('/api/services/names', requireAuth, wrap(async (req, res) => {
|
|
57
|
+
const list = await services.listServiceNames();
|
|
58
|
+
res.json(list);
|
|
59
|
+
}));
|
|
60
|
+
|
|
54
61
|
// GET /api/services/:name โ single service info
|
|
55
62
|
app.get('/api/services/:name', requireAuth, wrap(async (req, res) => {
|
|
56
63
|
const info = await services.getServiceInfo(req.params.name);
|
package/src/services.js
CHANGED
|
@@ -81,4 +81,22 @@ async function getServiceLogs(name, lines = 100) {
|
|
|
81
81
|
.filter(Boolean);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
84
|
+
// โโ lightweight list: name + activeState only โโโโโ
|
|
85
|
+
// Equivalent: systemctl list-units --type=service --all --output=json
|
|
86
|
+
async function listServiceNames() {
|
|
87
|
+
try {
|
|
88
|
+
const { stdout } = await execP(
|
|
89
|
+
'systemctl list-units --type=service --all --output=json --no-pager 2>/dev/null'
|
|
90
|
+
);
|
|
91
|
+
const units = JSON.parse(stdout);
|
|
92
|
+
return units.map(u => ({
|
|
93
|
+
name: u.unit.replace(/\.service$/, ''),
|
|
94
|
+
state: u.active, // 'active' | 'inactive' | 'failed' | 'activating'
|
|
95
|
+
sub: u.sub, // 'running' | 'dead' | 'exited' | ...
|
|
96
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
97
|
+
} catch {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { listServices, listServiceNames, getServiceInfo, controlService, getServiceLogs };
|