saymon-syswatch-linux 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,866 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
6
+ <title>SysWatch</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
8
+ <style>
9
+ :root{
10
+ --bg:#080a0d;--surface:#0d1117;--surface2:#161b22;--surface3:#1c2330;
11
+ --border:#21262d;--border2:#30363d;
12
+ --green:#3fb950;--blue:#58a6ff;--yellow:#e3b341;--red:#f85149;
13
+ --purple:#bc8cff;--cyan:#39d0d8;--orange:#f0883e;
14
+ --text:#c9d1d9;--muted:#6e7681;
15
+ --mono:'IBM Plex Mono',monospace;--sans:'IBM Plex Sans',sans-serif;
16
+ }
17
+ *{margin:0;padding:0;box-sizing:border-box}
18
+ html,body{height:100%;overflow:hidden}
19
+ body{background:var(--bg);color:var(--text);font-family:var(--mono);font-size:13px;display:flex;flex-direction:column}
20
+
21
+ /* TOPBAR */
22
+ .topbar{display:flex;align-items:center;justify-content:space-between;padding:0 20px;height:48px;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0;z-index:50}
23
+ .logo{font-family:var(--sans);font-weight:700;font-size:17px;color:#fff;display:flex;align-items:center;gap:10px}
24
+ .logo-mark{width:26px;height:26px;background:var(--green);border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:13px;color:#000;font-weight:700}
25
+ .topbar-right{display:flex;align-items:center;gap:14px}
26
+ .host-chip{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--muted)}
27
+ .pulse{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);animation:blink 2s infinite}
28
+ @keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
29
+ .topbar-btn{display:flex;align-items:center;gap:6px;padding:5px 12px;border-radius:5px;border:1px solid var(--border);background:transparent;color:var(--muted);cursor:pointer;font-family:var(--mono);font-size:11px;transition:all .2s}
30
+ .topbar-btn:hover{border-color:var(--border2);color:var(--text)}
31
+ .topbar-btn.active{background:rgba(188,140,255,.1);border-color:rgba(188,140,255,.35);color:var(--purple)}
32
+ .topbar-btn.logout{color:var(--muted)}
33
+ .topbar-btn.logout:hover{border-color:var(--red);color:var(--red)}
34
+
35
+ /* LAYOUT */
36
+ .app{display:flex;flex:1;overflow:hidden}
37
+
38
+ /* SIDEBAR */
39
+ .sidebar{width:210px;background:var(--surface);border-right:1px solid var(--border);display:flex;flex-direction:column;flex-shrink:0;overflow-y:auto}
40
+ .nav-group{padding:16px 0 8px}
41
+ .nav-group-label{padding:0 16px 6px;font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
42
+ .nav-item{display:flex;align-items:center;gap:9px;padding:8px 16px;cursor:pointer;color:var(--muted);font-size:12px;border-left:2px solid transparent;transition:all .15s}
43
+ .nav-item:hover{color:var(--text);background:var(--surface2)}
44
+ .nav-item.active{color:var(--green);border-left-color:var(--green);background:rgba(63,185,80,.06)}
45
+ .nav-icon{font-size:13px;width:16px;text-align:center}
46
+ .sdot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
47
+ .dg{background:var(--green)}.dy{background:var(--yellow)}.dr{background:var(--red)}
48
+
49
+ /* CONTENT */
50
+ .content{flex:1;overflow:hidden;display:flex;flex-direction:column}
51
+ .page{display:none;flex:1;overflow:hidden;flex-direction:column}
52
+ .page.active{display:flex}
53
+ .split{display:flex;flex:1;overflow:hidden}
54
+ .main-panel{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:16px}
55
+
56
+ /* LEARN PANEL */
57
+ .learn-panel{width:370px;flex-shrink:0;border-left:1px solid var(--border);background:var(--surface);overflow-y:auto;transition:width .3s,opacity .3s}
58
+ .learn-panel.off{width:0;opacity:0;overflow:hidden}
59
+ .lp-inner{padding:20px;min-width:370px}
60
+ .ls-title{font-family:var(--sans);font-weight:600;font-size:13px;color:var(--purple);margin-bottom:12px;display:flex;align-items:center;gap:8px;padding-bottom:8px;border-bottom:1px solid var(--border)}
61
+ .ls{margin-bottom:22px}
62
+ .cmd-block{background:#0a0d11;border:1px solid var(--border2);border-radius:6px;margin-bottom:11px;overflow:hidden}
63
+ .cmd-hdr{display:flex;align-items:center;justify-content:space-between;padding:7px 13px;background:var(--surface2);border-bottom:1px solid var(--border)}
64
+ .cmd-lbl{font-size:10px;color:var(--muted)}
65
+ .cmd-copy{font-size:10px;color:var(--blue);cursor:pointer;padding:2px 8px;border:1px solid rgba(88,166,255,.3);border-radius:3px;background:rgba(88,166,255,.05);transition:all .15s}
66
+ .cmd-copy:hover{background:rgba(88,166,255,.15)}
67
+ .cmd-code{padding:10px 13px;font-size:12px;color:var(--cyan);line-height:1.6;word-break:break-all}
68
+ .cmd-expl{padding:9px 13px;background:rgba(188,140,255,.04);border-top:1px solid var(--border);font-size:11.5px;color:var(--text);line-height:1.7}
69
+ .flags{padding:9px 13px;border-top:1px solid var(--border)}
70
+ .flag{display:flex;gap:10px;align-items:flex-start;margin-bottom:5px;font-size:11px}
71
+ .fn{color:var(--yellow);flex-shrink:0;min-width:100px}
72
+ .fd{color:var(--muted);line-height:1.5}
73
+ .note{background:rgba(63,185,80,.06);border:1px solid rgba(63,185,80,.2);border-radius:6px;padding:10px 13px;font-size:11.5px;color:var(--text);line-height:1.7;margin-top:10px}
74
+ .note strong{color:var(--green)}
75
+ .node-block{background:#0a1628;border:1px solid rgba(88,166,255,.2);border-radius:6px;overflow:hidden;margin-bottom:11px}
76
+ .node-hdr{display:flex;align-items:center;gap:8px;padding:7px 13px;background:rgba(88,166,255,.06);border-bottom:1px solid rgba(88,166,255,.15);font-size:11px;color:var(--blue)}
77
+ .node-code{padding:11px 13px;font-size:11px;color:#a0c4ff;line-height:1.9;white-space:pre;overflow-x:auto}
78
+ .kw{color:#cc99cd}.fn2{color:#6bc5f8}.str{color:#99cc99}.cm{color:#4a5e4a;font-style:italic}.num{color:#f08d49}
79
+
80
+ /* CARDS */
81
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:8px;overflow:hidden}
82
+ .card-hdr{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--border)}
83
+ .card-title{font-family:var(--sans);font-weight:600;font-size:13px;color:#fff;display:flex;align-items:center;gap:8px}
84
+ .card-body{padding:16px}
85
+
86
+ /* METRICS */
87
+ .metrics-row{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
88
+ .metric{background:var(--surface2);border:1px solid var(--border);border-radius:7px;padding:14px;position:relative;overflow:hidden}
89
+ .metric::after{content:'';position:absolute;bottom:0;left:0;right:0;height:2px}
90
+ .ok::after{background:var(--green)}.warn::after{background:var(--yellow)}.crit::after{background:var(--red)}
91
+ .metric-lbl{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);margin-bottom:6px}
92
+ .metric-val{font-family:var(--sans);font-size:26px;font-weight:700;line-height:1;margin-bottom:3px}
93
+ .ok .metric-val{color:var(--green)}.warn .metric-val{color:var(--yellow)}.crit .metric-val{color:var(--red)}
94
+ .metric-sub{font-size:10px;color:var(--muted)}
95
+ .progress{height:3px;background:var(--border);border-radius:2px;overflow:hidden;margin-top:8px}
96
+ .progress-fill{height:100%;border-radius:2px;transition:width 1s}
97
+ .ok .progress-fill{background:var(--green)}.warn .progress-fill{background:var(--yellow)}.crit .progress-fill{background:var(--red)}
98
+
99
+ /* TABLE */
100
+ .tbl{width:100%;border-collapse:collapse}
101
+ .tbl th{padding:8px 12px;font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:var(--muted);border-bottom:1px solid var(--border);text-align:left}
102
+ .tbl td{padding:9px 12px;border-bottom:1px solid rgba(33,38,45,.5);font-size:12px;vertical-align:middle}
103
+ .tbl tr:hover td{background:rgba(255,255,255,.015)}
104
+ .tbl tr:last-child td{border-bottom:none}
105
+
106
+ /* BADGE */
107
+ .badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:3px;font-size:10px;font-weight:600;text-transform:uppercase}
108
+ .bg{background:rgba(63,185,80,.15);color:var(--green)}
109
+ .br{background:rgba(248,81,73,.15);color:var(--red)}
110
+ .by{background:rgba(227,179,65,.15);color:var(--yellow)}
111
+ .bm{background:rgba(110,118,129,.15);color:var(--muted)}
112
+ .bd{width:5px;height:5px;border-radius:50%;background:currentColor}
113
+
114
+ /* INPUTS */
115
+ .inp{background:var(--surface2);border:1px solid var(--border);color:var(--text);padding:6px 12px;border-radius:5px;font-family:var(--mono);font-size:12px;outline:none;transition:border-color .2s}
116
+ .inp:focus{border-color:var(--blue)}
117
+ .inp option{background:var(--surface2)}
118
+ .inp-grow{flex:1;min-width:180px}
119
+ .live-btn{display:flex;align-items:center;gap:6px;padding:6px 13px;border-radius:5px;border:1px solid var(--green);background:rgba(63,185,80,.08);color:var(--green);cursor:pointer;font-family:var(--mono);font-size:11px;transition:all .2s}
120
+ .live-btn.off{border-color:var(--muted);background:transparent;color:var(--muted)}
121
+ .live-dot{width:6px;height:6px;border-radius:50%;background:currentColor;animation:blink 1s infinite}
122
+ .live-btn.off .live-dot{animation:none}
123
+
124
+ /* LOG */
125
+ .log-out{background:#060809;border:1px solid var(--border);border-radius:6px;padding:13px;height:360px;overflow-y:auto;font-size:11.5px;line-height:1.9;scrollbar-width:thin;scrollbar-color:var(--border) transparent}
126
+ .le{display:flex;gap:8px;align-items:baseline;margin-bottom:1px}
127
+ .le-ts{color:#2a4a3a;flex-shrink:0;font-size:11px}
128
+ .le-unit{color:#2d5a7a;flex-shrink:0}
129
+ .le-msg{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60vw}
130
+ .le-msg.err{color:var(--red)}.le-msg.warning{color:var(--yellow)}.le-msg.info{color:var(--blue)}.le-msg.ok{color:var(--green)}
131
+
132
+ /* BOOT BARS */
133
+ .boot-row{display:grid;grid-template-columns:140px 1fr 65px;align-items:center;gap:10px;margin-bottom:5px;font-size:11px}
134
+ .boot-name{color:var(--muted);text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
135
+ .boot-wrap{position:relative;height:16px}
136
+ .boot-fill{position:absolute;height:100%;border-radius:3px;display:flex;align-items:center;padding-left:6px;font-size:10px;color:rgba(0,0,0,.8);font-weight:500;overflow:hidden;white-space:nowrap}
137
+ .boot-ms{text-align:right}
138
+
139
+ /* BTN */
140
+ .btn{padding:4px 10px;border-radius:4px;border:1px solid var(--border);background:var(--surface2);color:var(--text);cursor:pointer;font-family:var(--mono);font-size:11px;transition:all .15s}
141
+ .btn:hover{border-color:var(--green);color:var(--green);background:rgba(63,185,80,.05)}
142
+ .btn.red:hover{border-color:var(--red);color:var(--red);background:rgba(248,81,73,.05)}
143
+
144
+ /* LOADING */
145
+ .loading{display:flex;align-items:center;justify-content:center;height:80px;color:var(--muted);gap:10px;font-size:12px}
146
+ .spin{width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--green);border-radius:50%;animation:spin .7s linear infinite}
147
+ @keyframes spin{to{transform:rotate(360deg)}}
148
+
149
+ /* INLINE CODE */
150
+ .ic{color:var(--cyan);background:rgba(57,208,216,.08);padding:1px 5px;border-radius:3px;font-size:11px}
151
+
152
+ ::-webkit-scrollbar{width:5px;height:5px}
153
+ ::-webkit-scrollbar-track{background:transparent}
154
+ ::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
155
+ </style>
156
+ </head>
157
+ <body>
158
+
159
+ <!-- TOPBAR -->
160
+ <div class="topbar">
161
+ <div class="logo">
162
+ <div class="logo-mark">$_</div>
163
+ SysWatch
164
+ <span style="font-size:11px;color:var(--muted);font-weight:400;font-family:var(--mono)">linux monitor</span>
165
+ </div>
166
+ <div class="topbar-right">
167
+ <div class="host-chip">
168
+ <div class="pulse"></div>
169
+ <span id="hostnameTxt">loading...</span>
170
+ <span style="color:var(--border)">·</span>
171
+ <span id="kernelTxt">—</span>
172
+ <span style="color:var(--border)">·</span>
173
+ <span id="uptimeTxt">—</span>
174
+ </div>
175
+ <button class="topbar-btn active" id="learnBtn" onclick="toggleLearn()">📖 Learn: ON</button>
176
+ <button class="topbar-btn logout" onclick="logout()">↩ Logout</button>
177
+ </div>
178
+ </div>
179
+
180
+ <div class="app">
181
+ <!-- SIDEBAR -->
182
+ <nav class="sidebar">
183
+ <div class="nav-group">
184
+ <div class="nav-group-label">Monitor</div>
185
+ <div class="nav-item active" id="nav-overview" onclick="goto('overview')"><span class="nav-icon">◈</span>Overview</div>
186
+ <div class="nav-item" id="nav-services" onclick="goto('services')"><span class="nav-icon">⚙</span>Services</div>
187
+ <div class="nav-item" id="nav-logs" onclick="goto('logs')"><span class="nav-icon">≡</span>Journal Logs</div>
188
+ <div class="nav-item" id="nav-processes" onclick="goto('processes')"><span class="nav-icon">◎</span>Processes</div>
189
+ <div class="nav-item" id="nav-boot" onclick="goto('boot')"><span class="nav-icon">▶</span>Boot Analysis</div>
190
+ </div>
191
+ <div class="nav-group">
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>
197
+ </div>
198
+ <div style="padding:16px;margin-top:auto;border-top:1px solid var(--border)">
199
+ <div style="font-size:10px;color:var(--muted);line-height:1.8">
200
+ <div style="color:var(--green);margin-bottom:4px">SysWatch v1.0.0</div>
201
+ <a href="https://github.com/YOUR/syswatch" target="_blank" style="color:var(--muted);text-decoration:none">★ Star on GitHub</a>
202
+ </div>
203
+ </div>
204
+ </nav>
205
+
206
+ <!-- CONTENT -->
207
+ <div class="content">
208
+
209
+ <!-- ══════════ OVERVIEW ══════════ -->
210
+ <div class="page active" id="page-overview">
211
+ <div class="split">
212
+ <div class="main-panel">
213
+ <div class="metrics-row" id="metricsRow">
214
+ <div class="metric ok" id="m-cpu"><div class="metric-lbl">CPU</div><div class="metric-val" id="v-cpu">—</div><div class="metric-sub" id="s-cpu">loading...</div><div class="progress"><div class="progress-fill" id="b-cpu" style="width:0%"></div></div></div>
215
+ <div class="metric ok" id="m-mem"><div class="metric-lbl">Memory</div><div class="metric-val" id="v-mem">—</div><div class="metric-sub" id="s-mem">loading...</div><div class="progress"><div class="progress-fill" id="b-mem" style="width:0%"></div></div></div>
216
+ <div class="metric ok" id="m-disk"><div class="metric-lbl">Disk</div><div class="metric-val" id="v-disk">—</div><div class="metric-sub" id="s-disk">loading...</div><div class="progress"><div class="progress-fill" id="b-disk" style="width:0%"></div></div></div>
217
+ <div class="metric ok" id="m-load"><div class="metric-lbl">Load Avg</div><div class="metric-val" id="v-load">—</div><div class="metric-sub" id="s-load">1m · 5m · 15m</div><div class="progress"><div class="progress-fill" id="b-load" style="width:0%"></div></div></div>
218
+ </div>
219
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
220
+ <div class="card">
221
+ <div class="card-hdr"><div class="card-title">⚙ Service Health</div></div>
222
+ <div class="card-body" id="svcHealth"><div class="loading"><div class="spin"></div>loading services...</div></div>
223
+ </div>
224
+ <div class="card">
225
+ <div class="card-hdr"><div class="card-title">≡ Live Journal Feed</div><span class="badge bg"><span class="bd"></span>live</span></div>
226
+ <div style="background:#060809;padding:10px 14px;max-height:130px;overflow:hidden;font-size:11px;line-height:2" id="liveFeed"></div>
227
+ </div>
228
+ </div>
229
+ <div class="card">
230
+ <div class="card-hdr"><div class="card-title">◉ CPU — 60s History</div></div>
231
+ <div class="card-body" style="padding:10px 14px">
232
+ <canvas id="cpuCanvas" style="width:100%;height:70px;display:block"></canvas>
233
+ </div>
234
+ </div>
235
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
236
+ <div class="card">
237
+ <div class="card-hdr"><div class="card-title">↕ Network I/O</div></div>
238
+ <div class="card-body" style="display:flex;gap:20px">
239
+ <div><div style="font-size:10px;color:var(--muted);margin-bottom:4px">RX (in)</div><div style="font-size:20px;font-weight:700;color:var(--blue)" id="net-rx">— KB/s</div></div>
240
+ <div><div style="font-size:10px;color:var(--muted);margin-bottom:4px">TX (out)</div><div style="font-size:20px;font-weight:700;color:var(--cyan)" id="net-tx">— KB/s</div></div>
241
+ </div>
242
+ </div>
243
+ <div class="card">
244
+ <div class="card-hdr"><div class="card-title">💾 Disk I/O</div></div>
245
+ <div class="card-body" style="display:flex;gap:20px">
246
+ <div><div style="font-size:10px;color:var(--muted);margin-bottom:4px">Read</div><div style="font-size:20px;font-weight:700;color:var(--green)" id="disk-r">— KB/s</div></div>
247
+ <div><div style="font-size:10px;color:var(--muted);margin-bottom:4px">Write</div><div style="font-size:20px;font-weight:700;color:var(--yellow)" id="disk-w">— KB/s</div></div>
248
+ </div>
249
+ </div>
250
+ </div>
251
+ </div>
252
+
253
+ <!-- LEARN: overview -->
254
+ <div class="learn-panel" id="lp-overview">
255
+ <div class="lp-inner">
256
+ <div class="ls"><div class="ls-title">📊 Metrics — /proc files</div>
257
+ <div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">CPU usage</span><span class="cmd-copy" onclick="cc(this,'cat /proc/stat')">copy</span></div><div class="cmd-code">cat /proc/stat</div><div class="cmd-expl">Linux exposes raw CPU tick counters here. Read it twice 500ms apart, compute: <code class="ic">(total−idle)/total×100</code>. This is how <code class="ic">top</code> and <code class="ic">htop</code> work internally.</div><div class="flags"><div class="flag"><span class="fn">Fields</span><span class="fd">user, nice, system, idle, iowait, irq, softirq, steal</span></div><div class="flag"><span class="fn">idle+iowait</span><span class="fd">= "doing nothing" ticks</span></div></div></div>
258
+ <div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">Memory</span><span class="cmd-copy" onclick="cc(this,'cat /proc/meminfo')">copy</span></div><div class="cmd-code">cat /proc/meminfo</div><div class="cmd-expl">All memory stats in kB. Use <code class="ic">MemAvailable</code> (not MemFree) — Linux caches files in unused RAM, so MemFree appears low but memory IS available.</div></div>
259
+ <div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">Load average</span><span class="cmd-copy" onclick="cc(this,'cat /proc/loadavg')">copy</span></div><div class="cmd-code">cat /proc/loadavg</div><div class="cmd-expl">5 values: 1min, 5min, 15min load, running/total procs, last PID. Load of 1.0 on 1 CPU = 100% saturated. On 4 CPUs, saturated = 4.0.</div></div>
260
+ <div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">Network I/O</span><span class="cmd-copy" onclick="cc(this,'cat /proc/net/dev')">copy</span></div><div class="cmd-code">cat /proc/net/dev</div><div class="cmd-expl">Cumulative RX/TX byte counters per interface. Compute KB/s by reading twice, dividing byte-difference by elapsed time.</div></div>
261
+ <div class="node-block"><div class="node-hdr">⬡ Node.js reads /proc directly</div><div class="node-code"><span class="kw">const</span> fs = <span class="fn2">require</span>(<span class="str">'fs'</span>);
262
+ <span class="cm">// CPU: read twice, diff the ticks</span>
263
+ <span class="kw">const</span> line = fs.<span class="fn2">readFileSync</span>(<span class="str">'/proc/stat'</span>)
264
+ .<span class="fn2">toString</span>().<span class="fn2">split</span>(<span class="str">'\n'</span>)[<span class="num">0</span>];
265
+ <span class="cm">// Memory: parse key:value lines</span>
266
+ <span class="kw">const</span> mem = fs.<span class="fn2">readFileSync</span>(<span class="str">'/proc/meminfo'</span>).<span class="fn2">toString</span>();
267
+ <span class="kw">const</span> total = mem.<span class="fn2">match</span>(<span class="str">/MemTotal:\s+(\d+)/</span>)?.[<span class="num">1</span>];</div></div>
268
+ <div class="note"><strong>💡 Why no npm packages?</strong> Reading <code class="ic">/proc</code> is a raw file read — almost zero overhead. The kernel writes these files live in memory, so <code class="ic">fs.readFileSync</code> is perfect and fast.</div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ </div>
273
+ </div>
274
+
275
+ <!-- ══════════ SERVICES ══════════ -->
276
+ <div class="page" id="page-services">
277
+ <div class="split">
278
+ <div class="main-panel">
279
+ <div class="card">
280
+ <div class="card-hdr">
281
+ <div class="card-title">⚙ systemd Services</div>
282
+ <div style="display:flex;gap:8px">
283
+ <select class="inp" id="svcFilter" onchange="renderSvcs()">
284
+ <option value="">All Status</option>
285
+ <option value="active">Running</option>
286
+ <option value="failed">Failed</option>
287
+ <option value="inactive">Inactive</option>
288
+ </select>
289
+ <button class="btn" onclick="loadSvcs()">↻ Refresh</button>
290
+ </div>
291
+ </div>
292
+ <div id="svcTableWrap"><div class="loading"><div class="spin"></div>loading services...</div></div>
293
+ </div>
294
+ </div>
295
+ <div class="learn-panel" id="lp-services">
296
+ <div class="lp-inner">
297
+ <div class="ls"><div class="ls-title">⚙ systemctl Commands</div>
298
+ <div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">List all services</span><span class="cmd-copy" onclick="cc(this,'systemctl list-units --type=service --all --output=json')">copy</span></div><div class="cmd-code">systemctl list-units --type=service --all --output=json</div><div class="cmd-expl">Lists every service unit systemd knows. <code class="ic">--all</code> includes stopped ones. <code class="ic">--output=json</code> makes it machine-readable for Node.js.</div><div class="flags"><div class="flag"><span class="fn">--type=service</span><span class="fd">Only .service units (not timers, sockets)</span></div><div class="flag"><span class="fn">--all</span><span class="fd">Include inactive and failed units too</span></div><div class="flag"><span class="fn">--no-pager</span><span class="fd">Don't pipe through less — needed in scripts</span></div></div></div>
299
+ <div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">Service details</span><span class="cmd-copy" onclick="cc(this,'systemctl show nginx.service --property=MainPID,MemoryCurrent')">copy</span></div><div class="cmd-code">systemctl show nginx.service \
300
+ --property=MainPID,MemoryCurrent,ActiveState</div><div class="cmd-expl">Machine-readable properties — no parsing needed. Returns <code class="ic">KEY=VALUE</code> lines. Much faster than parsing <code class="ic">systemctl status</code> output.</div></div>
301
+ <div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">Control a service</span><span class="cmd-copy" onclick="cc(this,'systemctl restart nginx.service')">copy</span></div><div class="cmd-code">systemctl start|stop|restart nginx.service</div><div class="cmd-expl">Requires root. In Node.js use <code class="ic">execP('systemctl restart nginx')</code>. Secure it with a sudoers rule — never give the web process unrestricted sudo.</div></div>
302
+ <div class="node-block"><div class="node-hdr">⬡ Node.js — systemctl JSON</div><div class="node-code"><span class="kw">const</span> { stdout } = <span class="kw">await</span> <span class="fn2">execP</span>(
303
+ <span class="str">`systemctl list-units --type=service \
304
+ --all --output=json --no-pager`</span>
305
+ );
306
+ <span class="kw">const</span> units = JSON.<span class="fn2">parse</span>(stdout);
307
+ <span class="cm">// each unit: { unit, load, active, sub, description }</span></div></div>
308
+ <div class="note"><strong>💡 sudoers tip:</strong> Add <code class="ic">www-data ALL=(root) NOPASSWD: /bin/systemctl restart nginx</code> to allow only that one command — nothing else.</div>
309
+ </div>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ </div>
314
+
315
+ <!-- ══════════ LOGS ══════════ -->
316
+ <div class="page" id="page-logs">
317
+ <div class="split">
318
+ <div class="main-panel">
319
+ <div class="card">
320
+ <div class="card-hdr">
321
+ <div class="card-title">≡ journalctl Viewer</div>
322
+ <button class="live-btn" id="liveBtn" onclick="toggleLive()"><div class="live-dot"></div>LIVE</button>
323
+ </div>
324
+ <div class="card-body" style="display:flex;flex-direction:column;gap:11px">
325
+ <div style="display:flex;gap:10px;flex-wrap:wrap">
326
+ <input class="inp inp-grow" type="text" id="logSearch" placeholder="grep pattern..." oninput="applyLogFilter()">
327
+ <select class="inp" id="logUnit" onchange="reconnectSSE()">
328
+ <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
+ <option value="kernel">kernel</option>
334
+ </select>
335
+ <select class="inp" id="logPrio" onchange="reconnectSSE()">
336
+ <option value="">All priorities</option>
337
+ <option value="err">Error (3)</option>
338
+ <option value="warning">Warning (4)</option>
339
+ <option value="notice">Notice (5)</option>
340
+ <option value="info">Info (6)</option>
341
+ </select>
342
+ </div>
343
+ <div class="log-out" id="logOut"><div class="loading"><div class="spin"></div>connecting to journal stream...</div></div>
344
+ <div style="display:flex;justify-content:space-between;font-size:11px;color:var(--muted)">
345
+ <span id="logCount">—</span>
346
+ <span>Equivalent: <code class="ic" id="logCmd">journalctl -f -o json</code></span>
347
+ </div>
348
+ </div>
349
+ </div>
350
+ </div>
351
+ <div class="learn-panel" id="lp-logs">
352
+ <div class="lp-inner">
353
+ <div class="ls"><div class="ls-title">≡ journalctl Commands</div>
354
+ <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>
356
+ <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
+ <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
+ app.<span class="fn2">get</span>(<span class="str">'/api/logs/stream'</span>, (req, res) => {
359
+ res.<span class="fn2">writeHead</span>(<span class="num">200</span>, {
360
+ <span class="str">'Content-Type'</span>: <span class="str">'text/event-stream'</span>,
361
+ <span class="str">'Cache-Control'</span>: <span class="str">'no-cache'</span>,
362
+ });
363
+ <span class="kw">const</span> proc = <span class="fn2">spawn</span>(<span class="str">'journalctl'</span>,
364
+ [<span class="str">'-f'</span>, <span class="str">'-o'</span>, <span class="str">'json'</span>]);
365
+ proc.stdout.<span class="fn2">on</span>(<span class="str">'data'</span>, d =>
366
+ res.<span class="fn2">write</span>(<span class="str">`data: ${d}\n\n`</span>));
367
+ req.<span class="fn2">on</span>(<span class="str">'close'</span>, () => proc.<span class="fn2">kill</span>());
368
+ });
369
+
370
+ <span class="cm">// Browser side:</span>
371
+ <span class="kw">const</span> es = <span class="kw">new</span> <span class="fn2">EventSource</span>(<span class="str">'/api/logs/stream'</span>);
372
+ es.<span class="fn2">onmessage</span> = e => {
373
+ <span class="kw">const</span> entry = JSON.<span class="fn2">parse</span>(e.data);
374
+ <span class="cm">// entry.MESSAGE, entry._SYSTEMD_UNIT, etc.</span>
375
+ };</div></div>
376
+ <div class="note"><strong>💡 SSE vs WebSocket:</strong> SSE is one-way (server→browser), auto-reconnects, and works over plain HTTP. Perfect for log streaming. Use WebSockets only if you need bidirectional messaging.</div>
377
+ </div>
378
+ </div>
379
+ </div>
380
+ </div>
381
+ </div>
382
+
383
+ <!-- ══════════ PROCESSES ══════════ -->
384
+ <div class="page" id="page-processes">
385
+ <div class="split">
386
+ <div class="main-panel">
387
+ <div class="card">
388
+ <div class="card-hdr">
389
+ <div class="card-title">◎ Processes <span style="color:var(--muted);font-weight:400;font-size:11px" id="procCnt"></span></div>
390
+ <div style="display:flex;gap:8px">
391
+ <select class="inp" id="procSort" onchange="renderProcs()"><option value="cpu">Sort: CPU%</option><option value="memKb">Sort: Memory</option><option value="pid">Sort: PID</option></select>
392
+ <button class="btn" onclick="loadProcs()">↻ Refresh</button>
393
+ </div>
394
+ </div>
395
+ <div id="procTableWrap"><div class="loading"><div class="spin"></div>loading processes...</div></div>
396
+ </div>
397
+ </div>
398
+ <div class="learn-panel" id="lp-processes">
399
+ <div class="lp-inner">
400
+ <div class="ls"><div class="ls-title">◎ Process Commands</div>
401
+ <div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">List processes</span><span class="cmd-copy" onclick="cc(this,'ps aux --sort=-%cpu')">copy</span></div><div class="cmd-code">ps aux --sort=-%cpu</div><div class="cmd-expl">Snapshot of all processes sorted by CPU. Unlike <code class="ic">top</code> it exits immediately — easy to parse. SysWatch reads <code class="ic">/proc/PID/*</code> directly for better control.</div><div class="flags"><div class="flag"><span class="fn">a</span><span class="fd">All users' processes</span></div><div class="flag"><span class="fn">u</span><span class="fd">Show username, CPU%, MEM%</span></div><div class="flag"><span class="fn">x</span><span class="fd">Include processes without a terminal</span></div><div class="flag"><span class="fn">--sort=-%mem</span><span class="fd">Sort by memory descending</span></div></div></div>
402
+ <div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">/proc/PID files</span><span class="cmd-copy" onclick="cc(this,'cat /proc/1234/status')">copy</span></div><div class="cmd-code">cat /proc/1234/comm # process name
403
+ cat /proc/1234/status # VmRSS, Uid, state
404
+ cat /proc/1234/stat # CPU ticks (field 14,15)
405
+ cat /proc/1234/cmdline # full command line</div><div class="cmd-expl">Every running process has a directory in <code class="ic">/proc/PID/</code>. Reading these files is how SysWatch gets live data without any external tool.</div></div>
406
+ <div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">Kill a process</span><span class="cmd-copy" onclick="cc(this,'kill -15 1234 && kill -9 1234')">copy</span></div><div class="cmd-code">kill -15 &lt;PID&gt; # SIGTERM (graceful)
407
+ kill -9 &lt;PID&gt; # SIGKILL (force)</div><div class="cmd-expl">Always try SIGTERM (15) first — it lets the process clean up. SIGKILL (9) can't be caught or ignored; the kernel terminates it immediately.</div></div>
408
+ <div class="node-block"><div class="node-hdr">⬡ Node.js — scan /proc</div><div class="node-code"><span class="cm">// All numeric dirs in /proc = PIDs</span>
409
+ <span class="kw">const</span> pids = fs.<span class="fn2">readdirSync</span>(<span class="str">'/proc'</span>)
410
+ .<span class="fn2">filter</span>(d => <span class="str">/^\d+$/</span>.<span class="fn2">test</span>(d));
411
+
412
+ <span class="kw">for</span> (<span class="kw">const</span> pid <span class="kw">of</span> pids) {
413
+ <span class="kw">const</span> name = fs.<span class="fn2">readFileSync</span>(
414
+ <span class="str">`/proc/${pid}/comm`</span>, <span class="str">'utf8'</span>).<span class="fn2">trim</span>();
415
+ <span class="cm">// VmRSS in status = resident memory (KB)</span>
416
+ }
417
+ <span class="cm">// Kill from Node.js:</span>
418
+ process.<span class="fn2">kill</span>(+pid, <span class="num">15</span>); <span class="cm">// SIGTERM</span></div></div>
419
+ </div>
420
+ </div>
421
+ </div>
422
+ </div>
423
+ </div>
424
+
425
+ <!-- ══════════ BOOT ══════════ -->
426
+ <div class="page" id="page-boot">
427
+ <div class="split">
428
+ <div class="main-panel">
429
+ <div class="card">
430
+ <div class="card-hdr"><div class="card-title">▶ Boot Analysis</div><button class="btn" onclick="loadBoot()">↻ Refresh</button></div>
431
+ <div class="card-body">
432
+ <div style="font-size:11px;color:var(--muted);margin-bottom:14px">Command: <code class="ic">systemd-analyze blame</code> — sorted by startup time</div>
433
+ <div id="bootBars"><div class="loading"><div class="spin"></div>running systemd-analyze...</div></div>
434
+ </div>
435
+ </div>
436
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
437
+ <div class="card"><div class="card-hdr"><div class="card-title">⌛ Time Breakdown</div></div><div class="card-body" id="bootTimes"><div class="loading"><div class="spin"></div></div></div></div>
438
+ <div class="card"><div class="card-hdr"><div class="card-title">🔗 Critical Chain</div></div><div class="card-body" style="font-size:11px;line-height:2;color:var(--muted)">Run: <code class="ic">systemd-analyze critical-chain</code><br>to see the slowest dependency path.</div></div>
439
+ </div>
440
+ </div>
441
+ <div class="learn-panel" id="lp-boot">
442
+ <div class="lp-inner">
443
+ <div class="ls"><div class="ls-title">▶ systemd-analyze Commands</div>
444
+ <div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">Total boot time</span><span class="cmd-copy" onclick="cc(this,'systemd-analyze')">copy</span></div><div class="cmd-code">systemd-analyze</div><div class="cmd-expl">Shows total time split: firmware + bootloader + kernel + userspace. Quick benchmark for your server's startup speed.</div></div>
445
+ <div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">Per-service blame</span><span class="cmd-copy" onclick="cc(this,'systemd-analyze blame')">copy</span></div><div class="cmd-code">systemd-analyze blame</div><div class="cmd-expl">Lists every service sorted by how long it took to initialize. Find what's slowing your boot. Use <code class="ic">--json=short</code> for Node.js parsing.</div></div>
446
+ <div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">Critical dependency chain</span><span class="cmd-copy" onclick="cc(this,'systemd-analyze critical-chain')">copy</span></div><div class="cmd-code">systemd-analyze critical-chain</div><div class="cmd-expl">The slowest path from kernel to <code class="ic">multi-user.target</code>. Like a flamegraph for boot — shows which dependency chain determined total boot time.</div></div>
447
+ <div class="cmd-block"><div class="cmd-hdr"><span class="cmd-lbl">Generate SVG flamegraph</span><span class="cmd-copy" onclick="cc(this,'systemd-analyze plot > boot.svg')">copy</span></div><div class="cmd-code">systemd-analyze plot > boot.svg</div><div class="cmd-expl">Generates a beautiful SVG timeline of the entire boot sequence. Open in a browser.</div></div>
448
+ <div class="node-block"><div class="node-hdr">⬡ Node.js — parse blame</div><div class="node-code"><span class="kw">const</span> { stdout } = <span class="kw">await</span> <span class="fn2">execP</span>(
449
+ <span class="str">'systemd-analyze blame --json=short'</span>
450
+ );
451
+ <span class="cm">// returns array of { name, time } objects</span>
452
+ <span class="kw">const</span> blame = JSON.<span class="fn2">parse</span>(stdout);
453
+ <span class="cm">// blame[0] = slowest service</span></div></div>
454
+ <div class="note"><strong>💡 Reduce boot time:</strong> Disable services you don't need: <code class="ic">systemctl disable bluetooth.service</code>. Use <code class="ic">systemctl mask</code> to prevent a service from ever starting.</div>
455
+ </div>
456
+ </div>
457
+ </div>
458
+ </div>
459
+ </div>
460
+
461
+ </div>
462
+ </div>
463
+
464
+ <script>
465
+ // ══════════════════════════════════════════════
466
+ // SysWatch Frontend — public/js embedded
467
+ // Connects to real Node.js API endpoints
468
+ // ══════════════════════════════════════════════
469
+
470
+ // ── state ─────────────────────────────────────
471
+ let learnOn = true;
472
+ let liveOn = true;
473
+ let sseConn = null;
474
+ let logBuffer = [];
475
+ let svcData = [];
476
+ let procData = [];
477
+ let cpuHistory = [];
478
+ let metricsInterval = null;
479
+
480
+ // ── nav ───────────────────────────────────────
481
+ function goto(id) {
482
+ document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
483
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
484
+ document.getElementById('page-' + id)?.classList.add('active');
485
+ document.getElementById('nav-' + id)?.classList.add('active');
486
+ if (id === 'services') loadSvcs();
487
+ if (id === 'processes') loadProcs();
488
+ if (id === 'boot') loadBoot();
489
+ if (id === 'logs') connectSSE();
490
+ }
491
+
492
+ function gotoLogs(unit) {
493
+ goto('logs');
494
+ setTimeout(() => {
495
+ const sel = document.getElementById('logUnit');
496
+ if (sel) { sel.value = unit; reconnectSSE(); }
497
+ }, 100);
498
+ }
499
+
500
+ // ── learn panel ───────────────────────────────
501
+ function toggleLearn() {
502
+ learnOn = !learnOn;
503
+ document.querySelectorAll('.learn-panel').forEach(p => p.classList.toggle('off', !learnOn));
504
+ const btn = document.getElementById('learnBtn');
505
+ btn.className = 'topbar-btn' + (learnOn ? ' active' : '');
506
+ btn.textContent = '📖 Learn: ' + (learnOn ? 'ON' : 'OFF');
507
+ }
508
+
509
+ // ── copy command ──────────────────────────────
510
+ function cc(el, text) {
511
+ navigator.clipboard.writeText(text).then(() => {
512
+ el.textContent = '✓ copied'; setTimeout(() => el.textContent = 'copy', 2000);
513
+ }).catch(() => { el.textContent = 'copy'; });
514
+ }
515
+
516
+ // ── auth ──────────────────────────────────────
517
+ async function logout() {
518
+ await fetch('/auth/logout', { method: 'POST', credentials: 'include' });
519
+ window.location.href = '/login';
520
+ }
521
+
522
+ // Check auth on load
523
+ fetch('/auth/status', { credentials: 'include' })
524
+ .then(r => r.json())
525
+ .then(d => { if (d.authEnabled && !d.authenticated) window.location.href = '/login'; });
526
+
527
+ // ── METRICS ───────────────────────────────────
528
+ async function loadMetrics() {
529
+ try {
530
+ const r = await fetch('/api/metrics', { credentials: 'include' });
531
+ if (r.status === 401) { window.location.href = '/login'; return; }
532
+ const d = await r.json();
533
+ updateMetrics(d);
534
+ } catch (e) { console.warn('metrics fetch failed', e); }
535
+ }
536
+
537
+ function updateMetrics(d) {
538
+ // System info bar
539
+ if (d.hostname) document.getElementById('hostnameTxt').textContent = d.hostname;
540
+ if (d.kernel) document.getElementById('kernelTxt').textContent = 'kernel ' + d.kernel;
541
+ if (d.uptime) document.getElementById('uptimeTxt').textContent = '↑ ' + d.uptime.pretty;
542
+
543
+ // CPU card
544
+ if (d.cpu !== undefined) {
545
+ const pct = d.cpu;
546
+ const cls = pct > 80 ? 'crit' : pct > 60 ? 'warn' : 'ok';
547
+ setMetric('cpu', pct + '%', d.cpuInfo ? `${d.cpuInfo.cores} cores · ${d.cpuInfo.model?.slice(0,20)}` : '', pct, cls);
548
+ cpuHistory.push(pct);
549
+ if (cpuHistory.length > 30) cpuHistory.shift();
550
+ drawCpuCanvas();
551
+ }
552
+
553
+ // Memory card
554
+ if (d.mem) {
555
+ const m = d.mem;
556
+ const cls = m.pct > 90 ? 'crit' : m.pct > 70 ? 'warn' : 'ok';
557
+ setMetric('mem', m.pct + '%', `${m.usedMb} MB / ${m.totalMb} MB used`, m.pct, cls);
558
+ }
559
+
560
+ // Disk card
561
+ if (d.disk) {
562
+ const cls = d.disk.pct > 90 ? 'crit' : d.disk.pct > 75 ? 'warn' : 'ok';
563
+ setMetric('disk', d.disk.pct + '%', `${d.disk.usedGb} GB / ${d.disk.totalGb} GB`, d.disk.pct, cls);
564
+ }
565
+
566
+ // Load card
567
+ if (d.load) {
568
+ const loadPct = Math.min(100, d.load.m1 / (d.cpuInfo?.cores || 1) * 100);
569
+ const cls = loadPct > 90 ? 'crit' : loadPct > 70 ? 'warn' : 'ok';
570
+ setMetric('load', d.load.m1.toFixed(2), `${d.load.m1} · ${d.load.m5} · ${d.load.m15}`, loadPct, cls);
571
+ }
572
+
573
+ // Network I/O
574
+ if (d.net) {
575
+ document.getElementById('net-rx').textContent = d.net.rxKBs + ' KB/s';
576
+ document.getElementById('net-tx').textContent = d.net.txKBs + ' KB/s';
577
+ }
578
+
579
+ // Disk I/O
580
+ if (d.diskIO) {
581
+ document.getElementById('disk-r').textContent = d.diskIO.readKBs + ' KB/s';
582
+ document.getElementById('disk-w').textContent = d.diskIO.writeKBs + ' KB/s';
583
+ }
584
+ }
585
+
586
+ function setMetric(id, val, sub, pct, cls) {
587
+ const card = document.getElementById('m-' + id);
588
+ if (card) card.className = 'metric ' + cls;
589
+ const v = document.getElementById('v-' + id);
590
+ if (v) v.textContent = val;
591
+ const s = document.getElementById('s-' + id);
592
+ if (s) s.textContent = sub;
593
+ const b = document.getElementById('b-' + id);
594
+ if (b) b.style.width = Math.min(100, pct) + '%';
595
+ }
596
+
597
+ // ── CPU CANVAS ────────────────────────────────
598
+ function drawCpuCanvas() {
599
+ const canvas = document.getElementById('cpuCanvas');
600
+ if (!canvas) return;
601
+ const W = canvas.offsetWidth || 600, H = 70;
602
+ canvas.width = W; canvas.height = H;
603
+ const ctx = canvas.getContext('2d');
604
+ const vals = cpuHistory.length ? cpuHistory : Array(10).fill(0);
605
+ const step = W / (vals.length - 1 || 1);
606
+ ctx.clearRect(0, 0, W, H);
607
+ const grad = ctx.createLinearGradient(0, 0, 0, H);
608
+ grad.addColorStop(0, 'rgba(63,185,80,.25)');
609
+ grad.addColorStop(1, 'rgba(63,185,80,0)');
610
+ ctx.beginPath(); ctx.moveTo(0, H);
611
+ vals.forEach((v, i) => ctx.lineTo(i * step, H - (v / 100 * H * .9)));
612
+ ctx.lineTo(W, H); ctx.closePath();
613
+ ctx.fillStyle = grad; ctx.fill();
614
+ ctx.beginPath();
615
+ vals.forEach((v, i) => { const x=i*step, y=H-(v/100*H*.9); i===0?ctx.moveTo(x,y):ctx.lineTo(x,y); });
616
+ ctx.strokeStyle = '#3fb950'; ctx.lineWidth = 1.5; ctx.stroke();
617
+ }
618
+
619
+ // ── SERVICES ──────────────────────────────────
620
+ async function loadSvcs() {
621
+ document.getElementById('svcTableWrap').innerHTML = '<div class="loading"><div class="spin"></div>loading services...</div>';
622
+ try {
623
+ const r = await fetch('/api/services', { credentials: 'include' });
624
+ svcData = await r.json();
625
+ updateSvcHealth();
626
+ renderSvcs();
627
+ } catch (e) {
628
+ document.getElementById('svcTableWrap').innerHTML = `<div class="loading" style="color:var(--red)">⚠ ${e.message}</div>`;
629
+ }
630
+ }
631
+
632
+ function renderSvcs() {
633
+ const f = document.getElementById('svcFilter')?.value || '';
634
+ const rows = svcData.filter(s => !f || s.active === f || s.activesub === f);
635
+ document.getElementById('svcTableWrap').innerHTML = `
636
+ <table class="tbl">
637
+ <thead><tr><th>Unit</th><th>Description</th><th>Status</th><th>PID</th><th>Memory</th><th>Actions</th></tr></thead>
638
+ <tbody>${rows.map(s => {
639
+ const st = s.active === 'active' ? 'bg' : s.active === 'failed' ? 'br' : 'bm';
640
+ const lbl = s.sub || s.active;
641
+ return `<tr>
642
+ <td style="color:#fff;font-weight:500">${s.unit}</td>
643
+ <td style="color:var(--muted);font-size:11px">${s.description || '—'}</td>
644
+ <td><span class="badge ${st}"><span class="bd"></span>${lbl}</span></td>
645
+ <td style="color:var(--muted)">${s.pid || '—'}</td>
646
+ <td style="color:var(--muted)">${s.memMb || '—'}</td>
647
+ <td style="display:flex;gap:6px">
648
+ <button class="btn" onclick="viewSvcLogs('${s.unit}')">logs</button>
649
+ ${s.active === 'active'
650
+ ? `<button class="btn red" onclick="svcAction('${s.unit}','restart')">restart</button>`
651
+ : `<button class="btn" onclick="svcAction('${s.unit}','start')">start</button>`}
652
+ </td></tr>`;
653
+ }).join('')}</tbody>
654
+ </table>`;
655
+ }
656
+
657
+ function updateSvcHealth() {
658
+ const running = svcData.filter(s => s.active === 'active').length;
659
+ const failed = svcData.filter(s => s.active === 'failed').length;
660
+ const inactive = svcData.filter(s => s.active === 'inactive').length;
661
+ document.getElementById('svcHealth').innerHTML = `
662
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;text-align:center">
663
+ <div style="padding:10px;background:rgba(63,185,80,.06);border:1px solid rgba(63,185,80,.15);border-radius:6px">
664
+ <div style="font-family:var(--sans);font-size:24px;font-weight:700;color:var(--green)">${running}</div>
665
+ <div style="font-size:10px;color:var(--muted);margin-top:3px">RUNNING</div>
666
+ </div>
667
+ <div style="padding:10px;background:rgba(248,81,73,.06);border:1px solid rgba(248,81,73,.15);border-radius:6px">
668
+ <div style="font-family:var(--sans);font-size:24px;font-weight:700;color:var(--red)">${failed}</div>
669
+ <div style="font-size:10px;color:var(--muted);margin-top:3px">FAILED</div>
670
+ </div>
671
+ <div style="padding:10px;background:rgba(110,118,129,.08);border:1px solid var(--border);border-radius:6px">
672
+ <div style="font-family:var(--sans);font-size:24px;font-weight:700;color:var(--muted)">${inactive}</div>
673
+ <div style="font-size:10px;color:var(--muted);margin-top:3px">INACTIVE</div>
674
+ </div>
675
+ </div>`;
676
+ }
677
+
678
+ async function svcAction(name, action) {
679
+ if (!confirm(`Run: systemctl ${action} ${name} ?`)) return;
680
+ const r = await fetch(`/api/services/${name}/control`, {
681
+ method: 'POST', credentials: 'include',
682
+ headers: { 'Content-Type': 'application/json' },
683
+ body: JSON.stringify({ action }),
684
+ });
685
+ const d = await r.json();
686
+ alert(r.ok ? `✓ ${action} ${name} OK` : '✗ ' + d.error);
687
+ loadSvcs();
688
+ }
689
+
690
+ function viewSvcLogs(unit) {
691
+ gotoLogs(unit.replace('.service', ''));
692
+ }
693
+
694
+ // ── LOGS / SSE ────────────────────────────────
695
+ function connectSSE() {
696
+ if (sseConn) { sseConn.close(); sseConn = null; }
697
+ logBuffer = [];
698
+ renderLogBuffer();
699
+
700
+ const unit = document.getElementById('logUnit')?.value || '';
701
+ const prio = document.getElementById('logPrio')?.value || '';
702
+ let url = '/api/logs/stream?lines=80';
703
+ if (unit) url += '&unit=' + unit;
704
+ if (prio) url += '&priority=' + prio;
705
+
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;
712
+
713
+ sseConn = new EventSource(url);
714
+
715
+ sseConn.onmessage = e => {
716
+ try {
717
+ const entry = JSON.parse(e.data);
718
+ if (!entry.msg) return;
719
+ logBuffer.push(entry);
720
+ if (logBuffer.length > 500) logBuffer.shift();
721
+ if (liveOn) renderLogBuffer();
722
+ } catch {}
723
+ };
724
+
725
+ sseConn.onerror = () => {
726
+ const el = document.getElementById('logOut');
727
+ if (el && !logBuffer.length)
728
+ el.innerHTML = '<div class="loading" style="color:var(--red)">⚠ Cannot connect to journal stream. Make sure server is running as root.</div>';
729
+ };
730
+ }
731
+
732
+ function reconnectSSE() { if (document.getElementById('page-logs').classList.contains('active')) connectSSE(); }
733
+ function toggleLive() {
734
+ liveOn = !liveOn;
735
+ const btn = document.getElementById('liveBtn');
736
+ btn.className = 'live-btn' + (liveOn ? '' : ' off');
737
+ btn.innerHTML = `<div class="live-dot"></div>${liveOn ? 'LIVE' : 'PAUSED'}`;
738
+ if (liveOn) renderLogBuffer();
739
+ }
740
+
741
+ function applyLogFilter() { renderLogBuffer(); }
742
+
743
+ function renderLogBuffer() {
744
+ const search = document.getElementById('logSearch')?.value?.toLowerCase() || '';
745
+ let filtered = logBuffer;
746
+ if (search) filtered = filtered.filter(e =>
747
+ e.msg?.toLowerCase().includes(search) || e.unit?.toLowerCase().includes(search));
748
+
749
+ const html = filtered.map(e => {
750
+ const ts = e.ts ? new Date(e.ts).toLocaleTimeString() : '??:??:??';
751
+ const lvl = e.level || 'info';
752
+ const msgClass = ['emerg','alert','crit','err'].includes(lvl) ? 'err'
753
+ : lvl === 'warning' ? 'warning'
754
+ : lvl === 'info' || lvl === 'notice' ? 'info' : '';
755
+ return `<div class="le"><span class="le-ts">${ts}</span><span class="le-unit">${e.unit || 'kernel'}:</span><span class="le-msg ${msgClass}">${e.msg}</span></div>`;
756
+ }).join('');
757
+
758
+ const out = document.getElementById('logOut');
759
+ if (!out) return;
760
+ out.innerHTML = html || '<div style="color:var(--muted);padding:20px;text-align:center">No entries match filter</div>';
761
+ if (liveOn) out.scrollTop = out.scrollHeight;
762
+
763
+ const cnt = document.getElementById('logCount');
764
+ if (cnt) cnt.textContent = `${filtered.length} of ${logBuffer.length} entries`;
765
+
766
+ // also update live feed on overview
767
+ const feed = document.getElementById('liveFeed');
768
+ if (feed) {
769
+ const last8 = logBuffer.slice(-8);
770
+ feed.innerHTML = last8.map(e => {
771
+ const lvl = e.level || 'info';
772
+ const col = ['emerg','alert','crit','err'].includes(lvl) ? 'var(--red)' : lvl === 'warning' ? 'var(--yellow)' : lvl === 'ok' ? 'var(--green)' : 'var(--text)';
773
+ const ts = e.ts ? new Date(e.ts).toLocaleTimeString() : '';
774
+ return `<div style="display:flex;gap:8px;overflow:hidden"><span style="color:var(--muted);flex-shrink:0;font-size:10px">${ts}</span><span style="color:#2d5a7a;flex-shrink:0">${e.unit||'kernel'}</span><span style="color:${col};overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${e.msg}</span></div>`;
775
+ }).join('');
776
+ }
777
+ }
778
+
779
+ // ── PROCESSES ────────────────────────────────
780
+ async function loadProcs() {
781
+ try {
782
+ const r = await fetch('/api/processes', { credentials: 'include' });
783
+ procData = await r.json();
784
+ renderProcs();
785
+ } catch (e) {
786
+ document.getElementById('procTableWrap').innerHTML = `<div class="loading" style="color:var(--red)">⚠ ${e.message}</div>`;
787
+ }
788
+ }
789
+
790
+ function renderProcs() {
791
+ const by = document.getElementById('procSort')?.value || 'cpu';
792
+ const sorted = [...procData].sort((a, b) => b[by] - a[by]).slice(0, 60);
793
+ document.getElementById('procCnt').textContent = `(${procData.length} total)`;
794
+ document.getElementById('procTableWrap').innerHTML = `
795
+ <table class="tbl">
796
+ <thead><tr><th>PID</th><th>Name</th><th>User</th><th>CPU%</th><th>MEM (RSS)</th><th>VSZ</th><th>State</th><th>Kill</th></tr></thead>
797
+ <tbody>${sorted.map(p => {
798
+ const cpuCol = p.cpu > 20 ? 'var(--red)' : p.cpu > 8 ? 'var(--yellow)' : 'var(--green)';
799
+ const stBadge = p.state === 'R' ? 'bg' : 'bm';
800
+ const stLabel = p.state === 'R' ? 'running' : p.state === 'S' ? 'sleep' : p.state || '?';
801
+ return `<tr>
802
+ <td style="color:var(--muted)">${p.pid}</td>
803
+ <td style="color:#fff;font-weight:500">${p.name}</td>
804
+ <td style="color:var(--muted)">${p.user}</td>
805
+ <td style="color:${cpuCol}">${p.cpu}%</td>
806
+ <td style="color:var(--text)">${p.memMb} MB</td>
807
+ <td style="color:var(--muted)">${p.vszMb} MB</td>
808
+ <td><span class="badge ${stBadge}">${stLabel}</span></td>
809
+ <td><button class="btn red" onclick="killProc(${p.pid}, '${p.name}')">kill</button></td>
810
+ </tr>`;
811
+ }).join('')}</tbody>
812
+ </table>`;
813
+ }
814
+
815
+ async function killProc(pid, name) {
816
+ if (!confirm(`Send SIGTERM to PID ${pid} (${name})?`)) return;
817
+ const r = await fetch(`/api/processes/${pid}?signal=15`, { method: 'DELETE', credentials: 'include' });
818
+ const d = await r.json();
819
+ alert(r.ok ? `✓ ${d.message}` : '✗ ' + d.error);
820
+ loadProcs();
821
+ }
822
+
823
+ // ── BOOT ─────────────────────────────────────
824
+ async function loadBoot() {
825
+ try {
826
+ const r = await fetch('/api/boot', { credentials: 'include' });
827
+ const d = await r.json();
828
+ renderBoot(d);
829
+ } catch (e) {
830
+ document.getElementById('bootBars').innerHTML = `<div class="loading" style="color:var(--red)">⚠ ${e.message}</div>`;
831
+ }
832
+ }
833
+
834
+ function renderBoot(d) {
835
+ // blame bars
836
+ const items = d.blame || [];
837
+ const max = Math.max(...items.map(b => b.time || b.activating || 0));
838
+ document.getElementById('bootBars').innerHTML = items.map(b => {
839
+ const ms = b.time || b.activating || 0;
840
+ const pct = (ms / max * 70 + 5);
841
+ const col = ms > 1000 ? 'var(--red)' : ms > 300 ? 'var(--yellow)' : 'var(--green)';
842
+ const nm = (b.name || b.unit || '').replace('.service', '');
843
+ return `<div class="boot-row">
844
+ <div class="boot-name">${nm}</div>
845
+ <div class="boot-wrap"><div class="boot-fill" style="width:${pct}%;background:${col};left:${(100-pct)/4}%">${ms > 300 ? nm : ''}</div></div>
846
+ <div class="boot-ms" style="color:${col}">${ms}ms</div>
847
+ </div>`;
848
+ }).join('') || '<div class="loading" style="color:var(--muted)">No blame data</div>';
849
+
850
+ // time summary
851
+ const t = d.times || {};
852
+ document.getElementById('bootTimes').innerHTML = Object.entries(t).map(([k,v]) =>
853
+ `<div style="display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--border)"><span style="color:var(--muted)">${k}</span><span style="color:var(--green)">${v}s</span></div>`
854
+ ).join('') || `<div style="color:var(--muted);font-size:11px">${d.summary || ''}</div>`;
855
+ }
856
+
857
+ // ── INIT ─────────────────────────────────────
858
+ // Start metrics polling
859
+ loadMetrics();
860
+ metricsInterval = setInterval(loadMetrics, 3000);
861
+
862
+ // Start SSE when on logs page
863
+ connectSSE(); // pre-connect for overview feed
864
+ </script>
865
+ </body>
866
+ </html>