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 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
- ## โšก Install & Run (3 steps)
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
- # Step 1 โ€” Install
14
- sudo npm install -g syswatch
24
+ node --version
25
+ ```
15
26
 
16
- # Step 2 โ€” Run (first time auto-runs setup wizard)
17
- sudo syswatch
27
+ If missing or below v18:
18
28
 
19
- # Step 3 โ€” Open browser
20
- # http://localhost:8080
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
- **That's it.** On first run, a setup wizard asks you 4 simple questions in the terminal โ€” including your password โ€” and creates the config file automatically.
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
- ## ๐Ÿง™ First-Run Setup Wizard
52
+ ## Step 3 โ€” Run the first-time setup wizard
28
53
 
29
- When you run SysWatch for the first time (no config found), it automatically starts an interactive wizard:
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]: (press Enter for default)
44
- โœ“ HTTP port: 8080
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 (localhost) โ† safer
48
- 2) Anyone on my network (0.0.0.0)
49
- Choose [1]:
50
- โœ“ Access: Localhost only
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! Config saved โ†’ syswatch.config.json
57
- Starting SysWatch โ†’ http://localhost:8080
85
+ โœ“ Setup complete!
86
+ Config saved โ†’ /etc/syswatch/syswatch.config.json
58
87
  ```
59
88
 
60
- The wizard creates `syswatch.config.json` automatically. **No manual editing needed.**
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
- ## ๐Ÿ” Re-run Setup (change password, port, etc.)
98
+ ## Step 4 โ€” Open your browser
65
99
 
66
- ```bash
67
- sudo syswatch --setup
100
+ ```
101
+ http://YOUR_SERVER_IP:PORT
68
102
  ```
69
103
 
70
- ---
104
+ Example: `http://192.168.1.10:8585`
71
105
 
72
- ## โš™๏ธ CLI Options
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
- ## ๐Ÿ”’ HTTPS
110
+ ## Step 5 โ€” Set up auto-start on boot (recommended)
87
111
 
88
- **Self-signed (quick):** Answer "y" to the HTTPS question in setup. A cert is auto-generated. Browser will warn "Not Secure" โ€” click Advanced โ†’ Proceed. Normal for self-signed.
112
+ ### Find the correct binary path first
89
113
 
90
- **Let's Encrypt (real domain):**
91
114
  ```bash
92
- sudo certbot certonly --standalone -d yourdomain.com
93
- # Then edit syswatch.config.json:
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
- ## ๐Ÿ“š Learn Mode
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
- Toggle **๐Ÿ“– Learn Mode** in the dashboard top-right. Every panel shows:
129
- - The exact Linux command being run
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
- ## ๐Ÿง Requirements
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
- - **Linux** โ€” Ubuntu 20.04+, Debian 11+, RHEL 8+, Arch, etc.
146
- - **Node.js 18+** โ€” https://nodejs.org
147
- - **systemd** โ€” for service control and journalctl
148
- - **sudo/root** โ€” needed for journal access and service control
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
- ## ๐Ÿ“„ License
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.2",
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": "./bin/syswatch.js"
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 class="nav-item" onclick="gotoLogs('nginx')"><div class="sdot dg"></div>nginx</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"><div class="ls-title">โ‰ก journalctl Commands</div>
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,\"journalctl -u nginx --since today -o json | grep 'ERROR'\")">copy</span></div><div class="cmd-code">journalctl -u nginx --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>
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) { sel.value = unit; reconnectSSE(); }
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 => { if (d.authEnabled && !d.authenticated) window.location.href = '/login'; });
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 hint
707
- let cmd = 'journalctl -f -o json';
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
- module.exports = { listServices, getServiceInfo, controlService, getServiceLogs };
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 };