pyre-cli 7.1.0 → 8.1.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,655 @@
1
+ export function getDashboardHtml(): string {
2
+ return `<!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8"/>
6
+ <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
7
+ <title>Pyre</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com"/>
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"/>
11
+ <style>
12
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
13
+ :root{
14
+ --bg: #000000;
15
+ --bg1: #0a0a0a;
16
+ --bg2: #141414;
17
+ --bg3: rgba(255,255,255,0.05);
18
+ --border: #333333;
19
+ --border2: #444444;
20
+ --text: #ffffff;
21
+ --text2: #aaaaaa;
22
+ --text3: #666666;
23
+ --accent: #ffffff;
24
+ --accent2: #dddddd;
25
+ --cyan: #ffffff;
26
+ --green: #ffffff;
27
+ --yellow: #bbbbbb;
28
+ --red: #dddddd;
29
+ --orange: #cccccc;
30
+ --purple: #eeeeee;
31
+ --glow: rgba(255,255,255,0.15);
32
+ --font: 'Inter', -apple-system, sans-serif;
33
+ --mono: 'JetBrains Mono', ui-monospace, monospace;
34
+ }
35
+ html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--text);font-family:var(--font);font-size:12px;}
36
+ body {
37
+ background-image: radial-gradient(var(--bg3) 1px, transparent 1px);
38
+ background-size: 20px 20px;
39
+ display:flex;flex-direction:column;height:100vh;
40
+ }
41
+
42
+ #titlebar{
43
+ height:42px;min-height:42px;-webkit-app-region:drag;
44
+ display:flex;align-items:center;padding:0 16px;
45
+ border-bottom:1px solid var(--border);
46
+ background:rgba(5,5,8,0.8);backdrop-filter:blur(10px);
47
+ z-index:100;
48
+ }
49
+ #titlebar-logo{
50
+ font-family:var(--mono);font-size:14px;font-weight:700;
51
+ color:var(--cyan);margin-right:24px;text-transform:uppercase;
52
+ letter-spacing:0.1em;
53
+ }
54
+ #titlebar-host{font-family:var(--mono);font-size:11px;color:var(--text2);flex:1;}
55
+ #conn-badge{display:flex;align-items:center;gap:6px;font-size:11px;font-family:var(--mono);color:var(--text2);-webkit-app-region:no-drag;}
56
+ .conn-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);animation:pulse 2s infinite;}
57
+ .conn-dot.offline{background:var(--red);box-shadow:0 0 8px var(--red);animation:none;}
58
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
59
+
60
+ #tabbar{
61
+ display:flex;align-items:center;gap:4px;padding:6px 14px 0;
62
+ border-bottom:1px solid var(--border);
63
+ background:rgba(10,12,22,0.8);overflow-x:auto;-webkit-app-region:no-drag;
64
+ }
65
+ #tabbar::-webkit-scrollbar{display:none}
66
+ .tab{
67
+ padding:8px 16px;font-size:11px;font-family:var(--mono);color:var(--text3);
68
+ cursor:pointer;white-space:nowrap;user-select:none;
69
+ border-bottom:2px solid transparent;transition:all 0.2s;
70
+ text-transform:uppercase;font-weight:600;
71
+ }
72
+ .tab:hover{color:var(--text2);background:var(--bg3);}
73
+ .tab.active{color:var(--accent);border-bottom:2px solid var(--accent);background:linear-gradient(0deg, rgba(255,255,255,0.1), transparent);}
74
+ .tab-key{opacity:0.5;margin-right:6px;}
75
+
76
+ #main{flex:1;overflow-y:auto;overflow-x:hidden;padding:16px;display:flex;flex-direction:column;}
77
+ #main::-webkit-scrollbar{width:8px}
78
+ #main::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
79
+
80
+ #statusbar{
81
+ height:28px;min-height:28px;display:flex;align-items:center;padding:0 16px;gap:20px;
82
+ font-size:10px;color:var(--text3);border-top:1px solid var(--border);
83
+ background:var(--bg);font-family:var(--mono);
84
+ }
85
+ .sb-item{display:flex;align-items:center;gap:6px;}
86
+ .sb-sep{color:var(--border);}
87
+ .up-val{color:var(--text);}
88
+
89
+ /* btop cards */
90
+ .card{
91
+ background:var(--bg1);
92
+ border:1px solid var(--border);
93
+ border-radius:6px;
94
+ padding:16px;
95
+ position:relative;
96
+ margin-top:12px;
97
+ transition:border-color 0.2s, box-shadow 0.2s;
98
+ display:flex;flex-direction:column;
99
+ }
100
+ .card:hover{border-color:var(--cyan);box-shadow:0 0 10px rgba(255,255,255,0.05);}
101
+ .card-title{
102
+ position:absolute;top:-9px;left:12px;
103
+ background:var(--bg);
104
+ padding:0 8px;font-size:11px;font-family:var(--mono);font-weight:700;
105
+ color:var(--accent);text-transform:uppercase;
106
+ display:flex;align-items:center;gap:6px;
107
+ }
108
+ .card-title::before{content:"";display:block;width:6px;height:6px;border-radius:50%;background:var(--accent);box-shadow:0 0 5px var(--accent);}
109
+
110
+ /* Grid System */
111
+ .grid-2x2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; flex:1; }
112
+ .panel-row { display:flex; gap:12px; margin-bottom:12px; }
113
+ .panel-col { display:flex; flex-direction:column; gap:12px; flex:1; }
114
+ .detail-split { display:flex; gap:16px; height:100%; }
115
+ .detail-left { flex:6; display:flex; flex-direction:column; gap:12px; }
116
+ .detail-right { flex:4; display:flex; flex-direction:column; gap:12px; }
117
+ .detail-full { display:flex; flex-direction:column; gap:12px; height:100%; }
118
+
119
+ /* Stats */
120
+ .stat{display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px dashed var(--border);}
121
+ .stat:last-child{border-bottom:none;}
122
+ .stat-label{font-size:11px;color:var(--text2);}
123
+ .stat-val{font-family:var(--mono);font-size:12px;color:var(--text);font-weight:500;}
124
+ .big-metric{text-align:center;padding:20px 0;}
125
+ .big-val{font-family:var(--mono);font-size:48px;font-weight:700;color:var(--cyan);text-shadow:0 0 15px rgba(255,255,255,0.3);}
126
+ .big-unit{font-family:var(--mono);font-size:14px;color:var(--text2);margin-top:8px;}
127
+
128
+ /* Gauges */
129
+ .gauge-wrap{margin:8px 0;}
130
+ .gauge-header{display:flex;justify-content:space-between;margin-bottom:6px;font-size:11px;font-family:var(--mono);}
131
+ .gauge-pct{font-weight:600;}
132
+ .gauge-track{height:20px;background:var(--bg2);border-radius:4px;overflow:hidden;position:relative;border:1px solid var(--border);}
133
+ .gauge-fill{height:100%;transition:width 0.4s ease;display:flex;align-items:center;justify-content:flex-end;padding-right:6px;font-family:var(--mono);font-size:10px;font-weight:700;color:#000;}
134
+ .gauge-fill.ok{background:linear-gradient(90deg,var(--text),#999);}
135
+ .gauge-fill.warn{background:linear-gradient(90deg,var(--text2),#666);}
136
+ .gauge-fill.crit{background:linear-gradient(90deg,var(--text3),#333);}
137
+
138
+ /* SVG Rings */
139
+ .ring-container { display:flex; justify-content:space-around; padding:20px 0; background:var(--bg1); border:1px solid var(--border); border-radius:6px; margin-bottom:12px; }
140
+ .ring-box { display:flex; flex-direction:column; align-items:center; gap:10px; }
141
+ .ring-title { font-family:var(--mono); font-size:11px; color:var(--text2); font-weight:700; text-transform:uppercase; }
142
+ .svg-ring { position:relative; width:100px; height:100px; }
143
+ .svg-ring.large { width:200px; height:200px; }
144
+ .svg-ring svg { transform:rotate(-90deg); width:100%; height:100%; }
145
+ .ring-center { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; }
146
+ .ring-val { font-family:var(--mono); font-size:22px; font-weight:700; }
147
+ .svg-ring.large .ring-val { font-size:42px; }
148
+
149
+ /* Sparklines */
150
+ .spark-wrap { flex:1; min-height:80px; display:flex; flex-direction:column; margin-top:8px; }
151
+ .spark-label { font-family:var(--mono); font-size:10px; color:var(--text3); margin-bottom:4px; }
152
+ canvas.spark { width:100%; height:80px; display:block; border-radius:4px; background:var(--bg2); border:1px solid var(--border); }
153
+ canvas.spark.large { height:200px; }
154
+
155
+ /* Core Grid */
156
+ .core-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(30px, 1fr)); gap:6px; margin-top:12px; }
157
+ .core-col { display:flex; flex-direction:column; gap:4px; align-items:center; }
158
+ .core-bar { width:100%; height:100px; background:var(--bg2); border-radius:4px; display:flex; align-items:flex-end; overflow:hidden; border:1px solid var(--border); }
159
+ .core-fill { width:100%; transition:height 0.3s; }
160
+ .core-fill.ok { background:linear-gradient(0deg,var(--text),#999); }
161
+ .core-fill.warn { background:linear-gradient(0deg,var(--text2),#666); }
162
+ .core-fill.crit { background:linear-gradient(0deg,var(--text3),#333); }
163
+ .core-lbl { font-family:var(--mono); font-size:9px; color:var(--text2); }
164
+
165
+ /* Table */
166
+ .table-wrap { flex:1; overflow-y:auto; border:1px solid var(--border); border-radius:6px; background:var(--bg1); }
167
+ table.proc { width:100%; border-collapse:collapse; font-family:var(--mono); font-size:11px; text-align:left; }
168
+ table.proc thead th { position:sticky; top:0; background:var(--bg2); padding:8px 12px; color:var(--accent); font-weight:700; text-transform:uppercase; border-bottom:1px solid var(--border); cursor:pointer; user-select:none; z-index:10; }
169
+ table.proc thead th:hover { color:var(--cyan); background:var(--border); }
170
+ table.proc tbody tr { border-bottom:1px solid var(--border); cursor:default; }
171
+ table.proc tbody tr:nth-child(even) { background:rgba(255,255,255,0.01); }
172
+ table.proc tbody tr:hover { background:var(--bg2); }
173
+ table.proc td { padding:6px 12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:250px; }
174
+ .proc-cmd { color:var(--text); }
175
+ .table-wrap::-webkit-scrollbar { width:8px; }
176
+ .table-wrap::-webkit-scrollbar-thumb { background:var(--border); }
177
+
178
+ /* Helpers */
179
+ .flex-row { display:flex; gap:12px; }
180
+ .flex-1 { flex:1; }
181
+ .mb-12 { margin-bottom:12px; }
182
+ .txt-green { color:var(--green); }
183
+ .txt-yellow { color:var(--yellow); }
184
+ .txt-red { color:var(--red); }
185
+ .txt-cyan { color:var(--cyan); }
186
+ .txt-purple { color:var(--purple); }
187
+
188
+ /* Chips */
189
+ .chips { display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }
190
+ .chip { padding:6px 12px; border-radius:4px; font-family:var(--mono); font-size:11px; border:1px solid var(--border); background:var(--bg2); }
191
+ .chip.ok { border-color:var(--green); color:var(--green); }
192
+ .chip.warn { border-color:var(--yellow); color:var(--yellow); }
193
+ .chip.crit { border-color:var(--red); color:var(--red); }
194
+
195
+ .anomaly { display:flex; gap:12px; padding:12px; border:1px solid var(--red); background:rgba(255,255,255,0.1); border-radius:6px; margin-bottom:12px; }
196
+ .anomaly-icon { font-size:20px; }
197
+ .anomaly-title { font-family:var(--mono); font-weight:700; color:var(--red); font-size:12px; }
198
+ .anomaly-desc { font-family:var(--mono); font-size:11px; color:var(--text); margin-top:4px; }
199
+
200
+ .view { display:none; height:100%; flex-direction:column; }
201
+ .view.active { display:flex; }
202
+ #view-content { flex:1; display:flex; flex-direction:column; animation:fadeIn 0.2s ease; height:100%; }
203
+ @keyframes fadeIn{from{opacity:0;}to{opacity:1;}}
204
+
205
+ </style>
206
+ </head>
207
+ <body>
208
+ <div id="titlebar">
209
+ <span id="titlebar-logo">Pyre</span>
210
+ <span id="titlebar-host">Loading...</span>
211
+ <div id="conn-badge"><div class="conn-dot" id="conn-dot"></div><span id="conn-text">Connecting</span></div>
212
+ </div>
213
+ <div id="tabbar">
214
+ <div class="tab active" data-view="overview" onclick="switchView('overview')"><span class="tab-key">1</span>Overview</div>
215
+ <div class="tab" data-view="cpu" onclick="switchView('cpu')"><span class="tab-key">2</span>CPU</div>
216
+ <div class="tab" data-view="memory" onclick="switchView('memory')"><span class="tab-key">3</span>Memory</div>
217
+ <div class="tab" data-view="gpu" onclick="switchView('gpu')"><span class="tab-key">4</span>GPU</div>
218
+ <div class="tab" data-view="power" onclick="switchView('power')"><span class="tab-key">5</span>Power</div>
219
+ <div class="tab" data-view="battery" onclick="switchView('battery')"><span class="tab-key">6</span>Battery</div>
220
+ <div class="tab" data-view="thermal" onclick="switchView('thermal')"><span class="tab-key">7</span>Thermal</div>
221
+ <div class="tab" data-view="network" onclick="switchView('network')"><span class="tab-key">8</span>Network</div>
222
+ <div class="tab" data-view="disk" onclick="switchView('disk')"><span class="tab-key">9</span>Disk</div>
223
+ <div class="tab" data-view="processes" onclick="switchView('processes')"><span class="tab-key">P</span>Processes</div>
224
+ <div class="tab" data-view="anomalies" onclick="switchView('anomalies')"><span class="tab-key">A</span>Anomalies</div>
225
+ </div>
226
+ <div id="main">
227
+ <div id="view-content"></div>
228
+ </div>
229
+ <div id="statusbar">
230
+ <div class="sb-item">⟳ <span id="sb-refresh" class="up-val">–</span></div><div class="sb-sep">│</div>
231
+ <div class="sb-item">↑ <span id="sb-uptime" class="up-val">–</span></div><div class="sb-sep">│</div>
232
+ <div class="sb-item">CPU <span id="sb-cpu" class="up-val">–</span></div><div class="sb-sep">│</div>
233
+ <div class="sb-item">MEM <span id="sb-mem" class="up-val">–</span></div><div class="sb-sep">│</div>
234
+ <div class="sb-item">BATT <span id="sb-batt" class="up-val">–</span></div><div class="sb-sep">│</div>
235
+ <div class="sb-item">PROCS <span id="sb-procs" class="up-val">–</span></div>
236
+ </div>
237
+ <script>
238
+ let currentView = 'overview';
239
+ let latestData = null;
240
+ const history = { cpu:[], mem:[], temp:[], rxRate:[], txRate:[], power:[] };
241
+ const MAX_HIST = 100;
242
+ let procSort = { col:'cpu', dir:'desc' };
243
+
244
+ function pushHist(arr, val) { arr.push(val||0); if(arr.length>MAX_HIST)arr.shift(); }
245
+ function fmtBytes(b, dec=2) {
246
+ if(!b) return '0 B';
247
+ const k=1024, s=['B','KB','MB','GB','TB'];
248
+ const i=Math.floor(Math.log(Math.abs(b))/Math.log(k));
249
+ return parseFloat((b/Math.pow(k,i)).toFixed(dec))+' '+s[i];
250
+ }
251
+ function statusCls(v) { return v>85?'crit':v>65?'warn':'ok'; }
252
+ function escapeHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
253
+
254
+ function svgRing(val, max, label, unit='', size=100) {
255
+ const r = size*0.4, circ = 2*Math.PI*r;
256
+ const pct = Math.min(100, Math.max(0, (val/max)*100));
257
+ const off = circ*(1 - pct/100);
258
+ const cls = statusCls(pct);
259
+ const col = cls==='crit'?'var(--text3)':cls==='warn'?'var(--text2)':'var(--text)';
260
+ return \`<div class="ring-box">
261
+ <div class="ring-title">\${label}</div>
262
+ <div class="svg-ring \${size>150?'large':''}">
263
+ <svg viewBox="0 0 \${size} \${size}">
264
+ <circle cx="\${size/2}" cy="\${size/2}" r="\${r}" fill="none" stroke="var(--border)" stroke-width="\${size*0.08}"/>
265
+ <circle cx="\${size/2}" cy="\${size/2}" r="\${r}" fill="none" stroke="\${col}" stroke-width="\${size*0.08}"
266
+ stroke-dasharray="\${circ}" stroke-dashoffset="\${off}" stroke-linecap="round"
267
+ style="transition:stroke-dashoffset 0.5s ease" />
268
+ </svg>
269
+ <div class="ring-center">
270
+ <div class="ring-val" style="color:\${col}">\${val.toFixed(1)}\${unit}</div>
271
+ </div>
272
+ </div>
273
+ </div>\`;
274
+ }
275
+
276
+ function drawSpark(id, arr, col1, col2, mx) {
277
+ const c = document.getElementById(id);
278
+ if(!c || !arr.length) return;
279
+ const dpr = window.devicePixelRatio || 1;
280
+ const rect = c.getBoundingClientRect();
281
+ c.width = rect.width * dpr;
282
+ c.height = rect.height * dpr;
283
+ const W = c.width, H = c.height;
284
+ const ctx = c.getContext('2d');
285
+ ctx.clearRect(0,0,W,H);
286
+
287
+ // Grid lines
288
+ ctx.strokeStyle = 'rgba(255,255,255,0.05)';
289
+ ctx.lineWidth = 1 * dpr;
290
+ [0.25, 0.5, 0.75].forEach(y => {
291
+ ctx.beginPath(); ctx.moveTo(0, H*y); ctx.lineTo(W, H*y); ctx.stroke();
292
+ });
293
+
294
+ const m = mx || Math.max(...arr, 1);
295
+ const pts = arr.map((v,i) => [i/(arr.length-1||1)*W, H - (v/m)*H]);
296
+
297
+ const grad = ctx.createLinearGradient(0,0,W,0);
298
+ grad.addColorStop(0, col1); grad.addColorStop(1, col2);
299
+ const aGrad = ctx.createLinearGradient(0,0,0,H);
300
+ aGrad.addColorStop(0, col2+'66'); aGrad.addColorStop(1, 'transparent');
301
+
302
+ ctx.beginPath(); ctx.moveTo(pts[0][0], H);
303
+ pts.forEach(p=>ctx.lineTo(p[0],p[1])); ctx.lineTo(pts[pts.length-1][0], H);
304
+ ctx.fillStyle = aGrad; ctx.fill();
305
+
306
+ ctx.beginPath(); pts.forEach((p,i)=>i?ctx.lineTo(p[0],p[1]):ctx.moveTo(p[0],p[1]));
307
+ ctx.strokeStyle = grad; ctx.lineWidth = 2 * dpr; ctx.stroke();
308
+ }
309
+
310
+ function gaugeHtml(lbl, pct, valStr='') {
311
+ const cls = statusCls(pct);
312
+ return \`<div class="gauge-wrap">
313
+ <div class="gauge-header"><span>\${lbl}</span><span class="txt-\${cls==='crit'?'red':cls==='warn'?'yellow':'green'}">\${pct.toFixed(1)}%</span></div>
314
+ <div class="gauge-track"><div class="gauge-fill \${cls}" style="width:\${pct}%">\${valStr}</div></div>
315
+ </div>\`;
316
+ }
317
+
318
+ function statRow(k,v) { return \`<div class="stat"><div class="stat-label">\${k}</div><div class="stat-val">\${escapeHtml(v)}</div></div>\`; }
319
+ function card(title, content, cls='') { return \`<div class="card \${cls}"><div class="card-title">\${title}</div>\${content}</div>\`; }
320
+
321
+ function renderOverview(d) {
322
+ const cpu = d.cpu.usage;
323
+ const mem = d.memory.usagePercent;
324
+ const temp = d.cpu.temperature ?? d.thermal.temperatures?.cpu_die ?? 0;
325
+ const batt = d.battery?.level ?? 0;
326
+
327
+ const rings = \`<div class="ring-container">
328
+ \${svgRing(cpu, 100, 'CPU', '%')}
329
+ \${svgRing(mem, 100, 'MEM', '%')}
330
+ \${svgRing(temp, 100, 'TEMP', '°')}
331
+ \${d.battery ? svgRing(batt, 100, 'BATT', '%') : ''}
332
+ </div>\`;
333
+
334
+ const topProcs = (d.processes||[]).slice(0,8).map(p=>\`<tr>
335
+ <td>\${p.pid}</td>
336
+ <td class="proc-cmd" title="\${escapeHtml(p.command)}">\${escapeHtml(p.command)}</td>
337
+ <td>\${escapeHtml(p.user)}</td>
338
+ <td class="txt-\${statusCls(p.cpu)}">\${p.cpu.toFixed(1)}</td>
339
+ <td>\${p.mem.toFixed(1)}</td>
340
+ </tr>\`).join('');
341
+
342
+ return \`<div class="detail-full">
343
+ \${rings}
344
+ <div class="grid-2x2">
345
+ \${card('CPU', \`
346
+ \${gaugeHtml('Usage', cpu, d.cpu.frequency+' MHz')}
347
+ <div class="spark-wrap"><div class="spark-label">CPU History</div><canvas class="spark" id="sp-ov-cpu"></canvas></div>
348
+ \`)}
349
+ \${card('Memory', \`
350
+ \${gaugeHtml('Usage', mem, fmtBytes(d.memory.used))}
351
+ \${statRow('Total', fmtBytes(d.memory.total))}
352
+ \${statRow('Free', fmtBytes(d.memory.free))}
353
+ \${statRow('Swap', fmtBytes(d.memory.swapUsed)+' / '+fmtBytes(d.memory.swapTotal))}
354
+ \`)}
355
+ \${card('Network', \`
356
+ <div class="flex-row mb-12">
357
+ <div class="flex-1 txt-cyan" style="font-family:var(--mono);font-size:14px;font-weight:700">⬇ \${fmtBytes(d.network.rxBytes)}</div>
358
+ <div class="flex-1 txt-purple" style="font-family:var(--mono);font-size:14px;font-weight:700;text-align:right">⬆ \${fmtBytes(d.network.txBytes)}</div>
359
+ </div>
360
+ <div class="flex-row" style="flex:1">
361
+ <div class="spark-wrap" style="margin:0"><canvas class="spark" id="sp-ov-rx"></canvas></div>
362
+ <div class="spark-wrap" style="margin:0"><canvas class="spark" id="sp-ov-tx"></canvas></div>
363
+ </div>
364
+ \`)}
365
+ \${card('Top Processes', \`
366
+ <div class="table-wrap" style="border:none;background:transparent;overflow:hidden;">
367
+ <table class="proc">
368
+ <thead><tr><th>PID</th><th>Command</th><th>User</th><th>CPU%</th><th>MEM%</th></tr></thead>
369
+ <tbody>\${topProcs}</tbody>
370
+ </table>
371
+ </div>
372
+ \`, 'mb-0')}
373
+ </div>
374
+ </div>\`;
375
+ }
376
+
377
+ function renderCpu(d) {
378
+ let cores = '';
379
+ if(d.cpu.coreUsage) {
380
+ cores = '<div class="core-grid">' + d.cpu.coreUsage.map((u,i) => {
381
+ const p = Math.min(100,u);
382
+ return \`<div class="core-col">
383
+ <div class="core-bar"><div class="core-fill \${statusCls(p)}" style="height:\${p}%"></div></div>
384
+ <div class="core-lbl">C\${i}</div>
385
+ </div>\`;
386
+ }).join('') + '</div>';
387
+ }
388
+ return \`<div class="detail-split">
389
+ <div class="detail-left">
390
+ \${card('CPU Usage', \`
391
+ <div class="spark-wrap" style="flex:none"><canvas class="spark large" id="sp-cpu-l"></canvas></div>
392
+ \${cores}
393
+ \`, 'flex-1')}
394
+ </div>
395
+ <div class="detail-right">
396
+ \${card('Processor Info', \`
397
+ <div class="big-metric"><div class="big-val">\${d.cpu.usage.toFixed(1)}%</div><div class="big-unit">Overall Utilisation</div></div>
398
+ \${statRow('Model', d.cpu.brand)}
399
+ \${statRow('Cores', d.cpu.physicalCores + ' Physical / ' + d.cpu.cores + ' Logical')}
400
+ \${statRow('Clock', d.cpu.frequency + ' MHz')}
401
+ \${statRow('Load 1m', d.cpu.loadAvg[0].toFixed(2))}
402
+ \${statRow('Load 5m', d.cpu.loadAvg[1].toFixed(2))}
403
+ \${statRow('Load 15m', d.cpu.loadAvg[2].toFixed(2))}
404
+ \`, 'flex-1')}
405
+ </div>
406
+ </div>\`;
407
+ }
408
+
409
+ function renderMemory(d) {
410
+ return \`<div class="detail-full">
411
+ \${card('Memory', \`
412
+ <div class="big-metric"><div class="big-val">\${d.memory.usagePercent.toFixed(1)}%</div><div class="big-unit">Memory Used</div></div>
413
+ \${gaugeHtml('RAM', d.memory.usagePercent, fmtBytes(d.memory.used))}
414
+ \${gaugeHtml('Swap', d.memory.swapTotal? (d.memory.swapUsed/d.memory.swapTotal)*100 : 0, fmtBytes(d.memory.swapUsed))}
415
+ <div class="grid-2x2" style="margin-top:20px;flex:none">
416
+ <div>\${statRow('Total', fmtBytes(d.memory.total))}\${statRow('Used', fmtBytes(d.memory.used))}</div>
417
+ <div>\${statRow('Free', fmtBytes(d.memory.free))}\${statRow('Cached', fmtBytes(d.memory.total - d.memory.free - d.memory.used))}</div>
418
+ </div>
419
+ <div class="spark-wrap" style="margin-top:20px"><div class="spark-label">Memory History</div><canvas class="spark large" id="sp-mem-l"></canvas></div>
420
+ \`, 'flex-1')}
421
+ </div>\`;
422
+ }
423
+
424
+ function renderGpu(d) {
425
+ if(!d.gpu) return '<div class="big-metric">No GPU Data</div>';
426
+ return \`<div class="detail-full">
427
+ \${card('Graphics Processor', \`
428
+ <div class="big-metric"><div class="big-val">\${d.gpu.utilization.toFixed(1)}%</div><div class="big-unit">\${d.gpu.model}</div></div>
429
+ \${gaugeHtml('Utilisation', d.gpu.utilization)}
430
+ \${statRow('VRAM Used', fmtBytes(d.gpu.memory))}
431
+ \${statRow('Active Processes', d.gpu.processes)}
432
+ \${d.gpu.temperature? statRow('Temperature', d.gpu.temperature+'°C') : ''}
433
+ \`, 'flex-1')}
434
+ </div>\`;
435
+ }
436
+
437
+ function renderPower(d) {
438
+ const tw = d.power?.combinedWatts ?? (d.battery?.powerWatts || 0);
439
+ return \`<div class="detail-full">
440
+ \${card('Power Consumption', \`
441
+ <div class="big-metric"><div class="big-val">\${tw.toFixed(2)} W</div><div class="big-unit">Total Draw</div></div>
442
+ <div class="flex-row">
443
+ \${d.power?.cpuWatts!=null ? \`<div class="flex-1">\${gaugeHtml('CPU Package', Math.min(100,d.power.cpuWatts/100*100), d.power.cpuWatts.toFixed(2)+'W')}</div>\` : ''}
444
+ \${d.power?.gpuWatts!=null ? \`<div class="flex-1">\${gaugeHtml('GPU Package', Math.min(100,d.power.gpuWatts/200*100), d.power.gpuWatts.toFixed(2)+'W')}</div>\` : ''}
445
+ </div>
446
+ \`, 'flex-1')}
447
+ </div>\`;
448
+ }
449
+
450
+ function renderBattery(d) {
451
+ if(!d.battery) return '<div class="big-metric">No Battery</div>';
452
+ return \`<div class="detail-full">
453
+ \${card('Battery', \`
454
+ <div class="ring-container" style="border:none;background:transparent;">
455
+ \${svgRing(d.battery.level, 100, 'LEVEL', '%', 200)}
456
+ </div>
457
+ <div class="grid-2x2">
458
+ <div>
459
+ \${statRow('State', d.battery.state)}
460
+ \${statRow('Power Source', d.battery.powerSource)}
461
+ \${statRow('Condition', d.battery.condition)}
462
+ \${d.battery.cycles ? statRow('Cycles', d.battery.cycles) : ''}
463
+ </div>
464
+ <div>
465
+ \${d.battery.timeRemaining ? statRow('Time Remaining', d.battery.timeRemaining) : ''}
466
+ \${d.battery.powerWatts ? statRow('Power Draw', d.battery.powerWatts.toFixed(2)+' W') : ''}
467
+ \${d.battery.maxCapacityPercent ? statRow('Max Capacity', d.battery.maxCapacityPercent+'%') : ''}
468
+ </div>
469
+ </div>
470
+ \`, 'flex-1')}
471
+ </div>\`;
472
+ }
473
+
474
+ function renderThermal(d) {
475
+ const chips = d.thermal.temperatures ? Object.entries(d.thermal.temperatures).map(([k,v]) => \`<div class="chip \${statusCls(v)}">\${k}: \${v.toFixed(1)}°C</div>\`).join('') : '';
476
+ return \`<div class="detail-full">
477
+ \${card('Thermal Status', \`
478
+ \${statRow('State', d.thermal.state)}
479
+ \${statRow('Pressure', d.thermal.pressureLevel ?? 'Normal')}
480
+ <div class="chips mb-12">\${chips}</div>
481
+ <div class="spark-wrap"><div class="spark-label">CPU Temp History</div><canvas class="spark large" id="sp-therm-l"></canvas></div>
482
+ \`, 'flex-1')}
483
+ </div>\`;
484
+ }
485
+
486
+ function renderNetwork(d) {
487
+ return \`<div class="detail-split">
488
+ <div class="detail-left">
489
+ \${card('Traffic', \`
490
+ <div class="flex-row">
491
+ <div class="flex-1 spark-wrap"><div class="spark-label txt-cyan">⬇ RX Rate</div><canvas class="spark large" id="sp-net-rx-l"></canvas></div>
492
+ <div class="flex-1 spark-wrap"><div class="spark-label txt-purple">⬆ TX Rate</div><canvas class="spark large" id="sp-net-tx-l"></canvas></div>
493
+ </div>
494
+ \`, 'flex-1')}
495
+ </div>
496
+ <div class="detail-right">
497
+ \${card('Interface Info', \`
498
+ <div class="big-metric" style="padding:10px 0"><div class="big-val txt-cyan" style="font-size:24px">\${d.network.interface}</div><div class="big-unit">\${d.network.ip}</div></div>
499
+ \${statRow('RX Total', fmtBytes(d.network.rxBytes))}
500
+ \${statRow('TX Total', fmtBytes(d.network.txBytes))}
501
+ \${statRow('RX Packets', (d.network.rxPackets||0).toLocaleString())}
502
+ \${statRow('TX Packets', (d.network.txPackets||0).toLocaleString())}
503
+ \${d.network.connections ? statRow('TCP Conns', d.network.connections) : ''}
504
+ \`, 'flex-1')}
505
+ </div>
506
+ </div>\`;
507
+ }
508
+
509
+ function renderDisk(d) {
510
+ const rows = (d.disk||[]).map(ds => \`
511
+ <div class="mb-12">
512
+ <div class="flex-row" style="justify-content:space-between;font-family:var(--mono);font-size:12px;margin-bottom:6px;">
513
+ <span class="txt-cyan" style="font-weight:700">\${ds.mountpoint}</span>
514
+ <span>\${ds.used} / \${ds.size}</span>
515
+ </div>
516
+ \${gaugeHtml('', parseFloat(ds.capacity)||0, ds.capacity)}
517
+ </div>\`).join('');
518
+ return \`<div class="detail-full">\${card('Storage Volumes', rows, 'flex-1')}</div>\`;
519
+ }
520
+
521
+ function sortProcs(procs) {
522
+ const col = procSort.col;
523
+ const dir = procSort.dir;
524
+ return [...procs].sort((a,b)=>{
525
+ let va=a[col], vb=b[col];
526
+ if(typeof va==='string'){va=va.toLowerCase();vb=vb.toLowerCase();}
527
+ return (va<vb ? -1 : va>vb ? 1 : 0) * (dir==='asc'?1:-1);
528
+ });
529
+ }
530
+ function sortBy(col) {
531
+ if(procSort.col===col) procSort.dir = procSort.dir==='asc'?'desc':'asc';
532
+ else {procSort.col=col; procSort.dir='desc';}
533
+ if(latestData) updateView();
534
+ }
535
+ function sortHdr(col) { return procSort.col===col ? (procSort.dir==='asc'?' ↑':' ↓') : ''; }
536
+ function fmtTime(s) { if(!s)return'–'; const h=Math.floor(s/3600), m=Math.floor((s%3600)/60), sc=Math.floor(s%60); return h?h+'h'+m+'m':m?m+'m'+sc+'s':sc+'s'; }
537
+
538
+ function renderProcesses(d) {
539
+ const procs = sortProcs(d.processes||[]);
540
+ const rows = procs.map(p=>\`<tr>
541
+ <td>\${p.pid}</td>
542
+ <td class="proc-cmd" title="\${escapeHtml(p.command)}">\${escapeHtml(p.command)}</td>
543
+ <td>\${escapeHtml(p.user)}</td>
544
+ <td class="txt-\${statusCls(p.cpu)}">\${p.cpu.toFixed(1)}</td>
545
+ <td>\${p.mem.toFixed(1)}</td>
546
+ <td>\${p.threads||0}</td>
547
+ <td>\${p.state}</td>
548
+ <td>\${fmtTime(p.runtime)}</td>
549
+ </tr>\`).join('');
550
+ return \`<div class="detail-full">
551
+ \${card('Process List ('+procs.length+')', \`
552
+ <div class="table-wrap">
553
+ <table class="proc">
554
+ <thead><tr>
555
+ <th onclick="sortBy('pid')">PID\${sortHdr('pid')}</th>
556
+ <th onclick="sortBy('command')">Command\${sortHdr('command')}</th>
557
+ <th onclick="sortBy('user')">User\${sortHdr('user')}</th>
558
+ <th onclick="sortBy('cpu')">CPU%\${sortHdr('cpu')}</th>
559
+ <th onclick="sortBy('mem')">MEM%\${sortHdr('mem')}</th>
560
+ <th onclick="sortBy('threads')">Thr\${sortHdr('threads')}</th>
561
+ <th onclick="sortBy('state')">State\${sortHdr('state')}</th>
562
+ <th onclick="sortBy('runtime')">Time\${sortHdr('runtime')}</th>
563
+ </tr></thead>
564
+ <tbody>\${rows}</tbody>
565
+ </table>
566
+ </div>
567
+ \`, 'flex-1')}
568
+ </div>\`;
569
+ }
570
+
571
+ function renderAnomalies(d) {
572
+ const arr = d.anomalies||[];
573
+ if(!arr.length) return '<div class="big-metric"><div class="big-val txt-green" style="font-size:32px">System Healthy</div></div>';
574
+ const rows = arr.map(a=>\`<div class="anomaly"><div class="anomaly-icon">⚠️</div><div><div class="anomaly-title">\${escapeHtml(a.type||'Alert')}</div><div class="anomaly-desc">\${escapeHtml(a.message||a.detail||JSON.stringify(a))}</div></div></div>\`).join('');
575
+ return \`<div class="detail-full">\${card('Anomalies', rows, 'flex-1')}</div>\`;
576
+ }
577
+
578
+ function drawSparks() {
579
+ drawSpark('sp-ov-cpu', history.cpu, '#ffffff', '#999999');
580
+ drawSpark('sp-ov-rx', history.rxRate, '#ffffff', '#999999');
581
+ drawSpark('sp-ov-tx', history.txRate, '#bbbbbb', '#666666');
582
+ drawSpark('sp-cpu-l', history.cpu, '#ffffff', '#999999');
583
+ drawSpark('sp-mem-l', history.mem, '#bbbbbb', '#666666');
584
+ drawSpark('sp-therm-l', history.temp, '#dddddd', '#888888');
585
+ drawSpark('sp-net-rx-l', history.rxRate, '#ffffff', '#999999');
586
+ drawSpark('sp-net-tx-l', history.txRate, '#bbbbbb', '#666666');
587
+ }
588
+
589
+ function switchView(v) {
590
+ currentView = v;
591
+ document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('active', t.dataset.view===v));
592
+ if(latestData) updateView();
593
+ }
594
+
595
+ function updateView() {
596
+ const el = document.getElementById('view-content');
597
+ if(!el || !latestData) return;
598
+ const d = latestData;
599
+ const map = {overview:renderOverview, cpu:renderCpu, memory:renderMemory, gpu:renderGpu, power:renderPower, battery:renderBattery, thermal:renderThermal, network:renderNetwork, disk:renderDisk, processes:renderProcesses, anomalies:renderAnomalies};
600
+ if(map[currentView]) {
601
+ el.innerHTML = map[currentView](d);
602
+ requestAnimationFrame(drawSparks);
603
+ }
604
+ }
605
+
606
+ let prx=null, ptx=null;
607
+ function ingestData(d) {
608
+ latestData = d;
609
+ pushHist(history.cpu, d.cpu.usage);
610
+ pushHist(history.mem, d.memory.usagePercent);
611
+ pushHist(history.temp, d.cpu.temperature ?? d.thermal.temperatures?.cpu_die ?? 0);
612
+ if(prx!==null) {
613
+ pushHist(history.rxRate, Math.max(0, d.network.rxBytes-prx));
614
+ pushHist(history.txRate, Math.max(0, d.network.txBytes-ptx));
615
+ }
616
+ prx=d.network.rxBytes; ptx=d.network.txBytes;
617
+ if(d.power?.combinedWatts) pushHist(history.power, d.power.combinedWatts);
618
+
619
+ document.getElementById('titlebar-host').textContent = d.header.hostname+' · '+d.header.os;
620
+ document.getElementById('sb-uptime').textContent = d.header.uptime;
621
+ document.getElementById('sb-cpu').textContent = d.cpu.usage.toFixed(1)+'%';
622
+ document.getElementById('sb-mem').textContent = d.memory.usagePercent.toFixed(1)+'%';
623
+ document.getElementById('sb-batt').textContent = d.battery ? d.battery.level+'%' : '–';
624
+ document.getElementById('sb-procs').textContent = (d.processes||[]).length;
625
+ document.getElementById('sb-refresh').textContent = new Date().toLocaleTimeString();
626
+
627
+ updateView();
628
+ }
629
+
630
+ window.__pyreUpdate = function(jsStr) {
631
+ try {
632
+ const d = JSON.parse(jsStr);
633
+ document.getElementById('conn-dot').className='conn-dot';
634
+ document.getElementById('conn-text').textContent='Live';
635
+ ingestData(d);
636
+ } catch(e) { document.getElementById('conn-text').textContent='Err: '+e.message; }
637
+ };
638
+
639
+ let pTimer=null;
640
+ async function pollOnce() {
641
+ try {
642
+ const r=await fetch('/api/stats',{cache:'no-store'});
643
+ if(!r.ok){ document.getElementById('conn-dot').className='conn-dot offline'; return; }
644
+ document.getElementById('conn-dot').className='conn-dot';
645
+ ingestData(await r.json());
646
+ } catch(e) { document.getElementById('conn-dot').className='conn-dot offline'; }
647
+ }
648
+ setTimeout(()=>{ if(!latestData && !pTimer) { pollOnce(); pTimer=setInterval(pollOnce, 2000); } }, 4000);
649
+
650
+ const keyMap = {'1':'overview','2':'cpu','3':'memory','4':'gpu','5':'power','6':'battery','7':'thermal','8':'network','9':'disk','p':'processes','a':'anomalies'};
651
+ document.addEventListener('keydown', e => { if(keyMap[e.key.toLowerCase()]) switchView(keyMap[e.key.toLowerCase()]); });
652
+ </script>
653
+ </body>
654
+ </html>`;
655
+ }
@@ -13,5 +13,6 @@
13
13
  */
14
14
  export { formatTable, clampWidth, gridColumns, thermalColor, capacityColor, getTabHitboxes, TAB_BAR_ROW, TAB_DEFS, panel, formatBytes, padVisible, visLen, fitVisible } from './render.js';
15
15
  export { formatJson, formatCsv, formatTsv, formatGraphs, formatHtml, formatMarkdown } from './output.js';
16
+ export { getDashboardHtml } from './html_template.js';
16
17
  export type { ThemeName, ThemeColors, VisibleItems, TableOptions, AnomalyAlert } from './types.js';
17
18
  export { THEMES } from './themes.js';