pyre-cli 10.2.0 → 10.4.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.
Files changed (88) hide show
  1. package/README.md +75 -18
  2. package/dist/{alertChannels-TPSQIJ7S.js → alertChannels-MNX4XH6J.js} +1 -0
  3. package/dist/{anomaliesCmd-25OFKYPK.js → anomaliesCmd-HHTY5LYL.js} +2 -0
  4. package/dist/benchGitCmd-HIXUC5EK.js +87 -0
  5. package/dist/{benchmarkCmd-A3ETK6VY.js → benchmarkCmd-B7DLP2VC.js} +2 -0
  6. package/dist/{blender-FGH3ASEQ.js → blender-6ZX5SPYC.js} +3 -1
  7. package/dist/{brewHealth-P2GW5Y5C.js → brewHealth-VFW33QB3.js} +1 -0
  8. package/dist/budgetCmd-TGO3RYLN.js +11 -0
  9. package/dist/{buildCmd-CDPZ2ZFA.js → buildCmd-E7KUCFYG.js} +3 -1
  10. package/dist/{checkCmd-WQAHAUWO.js → checkCmd-QLYTP77Y.js} +5 -3
  11. package/dist/chunk-3RG5ZIWI.js +10 -0
  12. package/dist/{chunk-VIACN2RU.js → chunk-FYCBGUTW.js} +41 -19
  13. package/dist/chunk-JH7P6KLN.js +143 -0
  14. package/dist/{chunk-QM5L43EK.js → chunk-LDJB5V4N.js} +5 -1
  15. package/dist/chunk-NMGQVRCA.js +111 -0
  16. package/dist/chunk-OCL7P7N7.js +936 -0
  17. package/dist/{chunk-23YWCI3O.js → chunk-QEH4LR6P.js} +28 -686
  18. package/dist/{chunk-KVUMOA6C.js → chunk-RKPGPVTO.js} +77 -4
  19. package/dist/chunk-UF3XQ3R3.js +3800 -0
  20. package/dist/{collectors-JDN5Y7EV.js → collectors-V7SCIIDH.js} +5 -3
  21. package/dist/{completions-ZVTNE52B.js → completions-FTPXDSR2.js} +44 -3
  22. package/dist/{config-J4YMADEF.js → config-5WW5M63G.js} +2 -1
  23. package/dist/containersCmd-FSMYOBG2.js +84 -0
  24. package/dist/{doctor-LHFW4UJP.js → doctor-O4DIAMHK.js} +3 -2
  25. package/dist/{explainCmd-MF6PIMLW.js → explainCmd-7PBPA7O5.js} +1 -0
  26. package/dist/{extensions-XJGHG7X3.js → extensions-QQEV7U3D.js} +1 -0
  27. package/dist/{firebaseSync-ON3RZ6KX.js → firebaseSync-LRN67UTV.js} +2 -0
  28. package/dist/flightRecorder-GJIAQOXC.js +162 -0
  29. package/dist/index.js +737 -4125
  30. package/dist/inspectCmd-5FXJBJTK.js +137 -0
  31. package/dist/menu-AJFGDLBP.js +1233 -0
  32. package/dist/monitors/platform/windows-ui.ps1 +31 -29
  33. package/dist/{netUsageCmd-67IBZZYN.js → netUsageCmd-JP6YKBPF.js} +7 -5
  34. package/dist/optimizeCmd-WZKLFYZU.js +1110 -0
  35. package/dist/{pipeCmd-2N4JPKYE.js → pipeCmd-CPK6FVKB.js} +5 -3
  36. package/dist/playbookCmd-FRROIEXR.js +145 -0
  37. package/dist/{profileProcCmd-N4H6MCBT.js → profileProcCmd-5Y3733E7.js} +64 -13
  38. package/dist/{prometheus-5BL5PVLC.js → prometheus-CYNUR3QT.js} +1 -0
  39. package/dist/{prometheusCmd-LEUM5WCZ.js → prometheusCmd-G2CSEZRQ.js} +5 -3
  40. package/dist/{replayCmd-Z3PYXY7V.js → replayCmd-ODB5SPOQ.js} +25 -6
  41. package/dist/{run-HOGQDDTA.js → run-UJQYT2TO.js} +1 -0
  42. package/dist/{serveCmd-L7LVJOZN.js → serveCmd-KDPWLVQI.js} +5 -3
  43. package/dist/{service-7GIX3YNC.js → service-LIYYR3UI.js} +1 -0
  44. package/dist/{smartCmd-ODWKSFNG.js → smartCmd-2BJJ37GV.js} +130 -5
  45. package/dist/statuslineCmd-EUKYRR2G.js +75 -0
  46. package/dist/{stressCmd-MFXMFXYX.js → stressCmd-GKHGY26X.js} +2 -0
  47. package/dist/{syncCmd-42IRF633.js → syncCmd-MIBFKX24.js} +16 -5
  48. package/dist/topoCmd-RTXQJ6XA.js +189 -0
  49. package/dist/{updateCheck-52X6LJG5.js → updateCheck-LJHEMG3G.js} +2 -0
  50. package/dist/{watchdogCmd-6GR4UN6B.js → watchdogCmd-XM6OPTKM.js} +41 -6
  51. package/dist/{wizard-JQMQWFAW.js → wizard-PRDOJROQ.js} +2 -1
  52. package/package.json +2 -2
  53. package/src/benchGitCmd.ts +110 -0
  54. package/src/budgetCmd.ts +199 -0
  55. package/src/completions.ts +42 -3
  56. package/src/containersCmd.ts +106 -0
  57. package/src/doctor.ts +86 -6
  58. package/src/easy_features.test.ts +83 -0
  59. package/src/flightRecorder.ts +214 -0
  60. package/src/index.ts +183 -9
  61. package/src/inspectCmd.ts +165 -0
  62. package/src/live/input.ts +70 -3
  63. package/src/live/render.ts +1 -1
  64. package/src/live/state.ts +2 -1
  65. package/src/menu/commands.ts +754 -0
  66. package/src/menu/index.ts +358 -0
  67. package/src/menu/render.ts +272 -0
  68. package/src/menu/types.ts +63 -0
  69. package/src/menu.test.ts +110 -0
  70. package/src/monitors/blender.ts +49 -19
  71. package/src/monitors/buildTracker.ts +72 -54
  72. package/src/monitors/platform/windows-ui.ps1 +31 -29
  73. package/src/monitors/platform/windows.ts +613 -280
  74. package/src/optimizeCmd.ts +1248 -0
  75. package/src/platform.test.ts +135 -0
  76. package/src/playbookCmd.ts +156 -0
  77. package/src/power_features.test.ts +251 -0
  78. package/src/profileProcCmd.ts +68 -21
  79. package/src/replayCmd.ts +15 -6
  80. package/src/smartCmd.ts +149 -10
  81. package/src/state/config.ts +13 -0
  82. package/src/statuslineCmd.ts +87 -0
  83. package/src/syncCmd.ts +12 -0
  84. package/src/topoCmd.ts +83 -50
  85. package/src/watchdogCmd.ts +40 -3
  86. package/src/xbar.ts +195 -17
  87. package/dist/chunk-M73HCQD2.js +0 -95
  88. package/dist/topoCmd-DZ3YS5IL.js +0 -148
@@ -0,0 +1,3800 @@
1
+ import {
2
+ THEMES,
3
+ celsiusToFahrenheit,
4
+ fitVisible,
5
+ formatBytes,
6
+ panel,
7
+ sparkline
8
+ } from "./chunk-XTOOEKLM.js";
9
+
10
+ // src/formatters/output.ts
11
+ import chalk from "chalk";
12
+ function formatJson(data) {
13
+ return JSON.stringify(data, null, 2);
14
+ }
15
+ function formatHtml(data) {
16
+ const rows = (items) => items.map((i) => `<tr><td class="label">${escapeHtml(i.label)}</td><td class="value">${escapeHtml(i.value)}</td></tr>`).join("\n");
17
+ const sections = [];
18
+ sections.push(`<h2>System</h2>
19
+ <table>
20
+ ${rows([
21
+ { label: "Hostname", value: data.header.hostname },
22
+ { label: "OS", value: data.header.os },
23
+ { label: "Uptime", value: data.header.uptime },
24
+ { label: "Timestamp", value: data.timestamp }
25
+ ]).trim()}
26
+ </table>`);
27
+ sections.push(`<h2>CPU</h2>
28
+ <table>
29
+ ${rows([
30
+ { label: "Brand", value: data.cpu.brand },
31
+ { label: "Cores", value: `${data.cpu.physicalCores}/${data.cpu.cores}` },
32
+ { label: "Frequency", value: `${data.cpu.frequency} MHz` },
33
+ { label: "Usage", value: `${data.cpu.usage}%` },
34
+ { label: "Load Avg", value: data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" ") },
35
+ ...data.cpu.temperature ? [{ label: "Temperature", value: `${data.cpu.temperature.toFixed(1)}\xB0C` }] : []
36
+ ]).trim()}
37
+ </table>`);
38
+ sections.push(`<h2>Memory</h2>
39
+ <table>
40
+ ${rows([
41
+ { label: "Total", value: formatBytes(data.memory.total) },
42
+ { label: "Used", value: formatBytes(data.memory.used) },
43
+ { label: "Free", value: formatBytes(data.memory.free) },
44
+ { label: "Usage", value: `${data.memory.usagePercent}%` },
45
+ { label: "Swap", value: `${formatBytes(data.memory.swapUsed)} / ${formatBytes(data.memory.swapTotal)}` }
46
+ ]).trim()}
47
+ </table>`);
48
+ if (data.gpu) {
49
+ sections.push(`<h2>GPU</h2>
50
+ <table>
51
+ ${rows([
52
+ { label: "Model", value: data.gpu.model },
53
+ { label: "Memory", value: formatBytes(data.gpu.memory) },
54
+ { label: "Utilization", value: `${data.gpu.utilization}%` },
55
+ ...data.gpu.temperature ? [{ label: "Temperature", value: `${data.gpu.temperature.toFixed(1)}\xB0C` }] : [],
56
+ { label: "Processes", value: String(data.gpu.processes) }
57
+ ]).trim()}
58
+ </table>`);
59
+ }
60
+ if (data.power) {
61
+ const powerRows = [];
62
+ if (data.power.cpuWatts !== void 0) powerRows.push({ label: "CPU", value: `${data.power.cpuWatts.toFixed(2)} W` });
63
+ if (data.power.gpuWatts !== void 0) powerRows.push({ label: "GPU", value: `${data.power.gpuWatts.toFixed(2)} W` });
64
+ if (data.power.combinedWatts !== void 0) powerRows.push({ label: "Combined", value: `${data.power.combinedWatts.toFixed(2)} W` });
65
+ if (powerRows.length) {
66
+ sections.push(`<h2>Power</h2>
67
+ <table>
68
+ ${rows(powerRows).trim()}
69
+ </table>`);
70
+ }
71
+ }
72
+ if (data.battery) {
73
+ const battRows = [
74
+ { label: "Level", value: `${data.battery.level}%` },
75
+ { label: "State", value: data.battery.state },
76
+ { label: "Time Remaining", value: data.battery.timeRemaining },
77
+ { label: "Health", value: data.battery.health },
78
+ { label: "Power Source", value: data.battery.powerSource }
79
+ ];
80
+ if (data.battery.condition) battRows.push({ label: "Condition", value: data.battery.condition });
81
+ if (data.battery.maxCapacityPercent !== void 0) battRows.push({ label: "Max Capacity", value: `${data.battery.maxCapacityPercent}%` });
82
+ if (data.battery.cycles !== void 0) battRows.push({ label: "Cycles", value: String(data.battery.cycles) });
83
+ if (data.battery.estimatedTimeToEmpty) battRows.push({ label: "Est. Empty", value: data.battery.estimatedTimeToEmpty });
84
+ if (data.battery.dischargeRatePerHour !== void 0) battRows.push({ label: "Discharge Rate", value: `${data.battery.dischargeRatePerHour}%/h` });
85
+ sections.push(`<h2>Battery</h2>
86
+ <table>
87
+ ${rows(battRows).trim()}
88
+ </table>`);
89
+ }
90
+ sections.push(`<h2>Thermal</h2>
91
+ <table>
92
+ ${rows([
93
+ { label: "State", value: data.thermal.state },
94
+ { label: "Pressure", value: ["Nominal", "Fair", "Serious", "Critical"][data.thermal.pressureLevel] || "Unknown" },
95
+ ...data.thermal.temperatures ? Object.entries(data.thermal.temperatures).flatMap(
96
+ ([k, v]) => v !== null && v !== void 0 ? [{ label: k.replace(/_/g, " "), value: `${v.toFixed(1)}\xB0C` }] : []
97
+ ) : []
98
+ ]).trim()}
99
+ </table>`);
100
+ sections.push(`<h2>Network</h2>
101
+ <table>
102
+ ${rows([
103
+ { label: "Interface", value: `${data.network.interface} (${data.network.ip})` },
104
+ { label: "RX", value: `${formatBytes(data.network.rxBytes)} \xB7 ${formatBytes(data.network.rxRate)}/s` },
105
+ { label: "TX", value: `${formatBytes(data.network.txBytes)} \xB7 ${formatBytes(data.network.txRate)}/s` },
106
+ ...data.network.protocols ? [
107
+ { label: "TCP", value: `${data.network.protocols.tcp.connections} conns` },
108
+ { label: "UDP", value: `${data.network.protocols.udp.connections} conns` }
109
+ ] : [],
110
+ ...data.network.connectionStates ? [
111
+ { label: "Active", value: `${data.network.connectionStates.established} ESTABLISHED` },
112
+ { label: "Listening", value: `${data.network.connectionStates.listening}` }
113
+ ] : [],
114
+ ...data.network.listeningPorts && data.network.listeningPorts.length ? [
115
+ { label: "Ports", value: data.network.listeningPorts.slice(0, 8).join(", ") }
116
+ ] : []
117
+ ]).trim()}
118
+ </table>`);
119
+ if (data.packets) {
120
+ const packetRows = [
121
+ { label: "Total", value: `${data.packets.totalPackets} pkt` },
122
+ { label: "RX", value: `${data.packets.rxPackets} pkt` },
123
+ { label: "TX", value: `${data.packets.txPackets} pkt` },
124
+ { label: "Connections", value: `${data.packets.connections} TCP` }
125
+ ];
126
+ if (data.packets.protocolStats) {
127
+ packetRows.push({ label: "TCP", value: `${data.packets.protocolStats.tcp.connections} conns` });
128
+ packetRows.push({ label: "UDP", value: `${data.packets.protocolStats.udp.connections} conns` });
129
+ }
130
+ if (data.packets.connectionStates) {
131
+ packetRows.push({ label: "ESTABLISHED", value: `${data.packets.connectionStates.established}` });
132
+ packetRows.push({ label: "LISTENING", value: `${data.packets.connectionStates.listening}` });
133
+ }
134
+ sections.push(`<h2>Packets</h2>
135
+ <table>
136
+ ${rows(packetRows).trim()}
137
+ </table>`);
138
+ }
139
+ if (data.tasks.length) {
140
+ const taskRows = data.tasks.slice(0, 20).map((t) => ({
141
+ label: `${t.pid}`,
142
+ value: `${t.user.padEnd(10)} ${t.cpu.toFixed(1).padStart(6)}% ${t.mem.toFixed(1).padStart(6)}% ${t.state} ${t.command}`
143
+ }));
144
+ sections.push(`<h2>Tasks</h2>
145
+ <table>
146
+ ${rows(taskRows).trim()}
147
+ </table>`);
148
+ }
149
+ if (data.disk.length) {
150
+ const diskRows = data.disk.map((d) => ({
151
+ label: d.mountpoint,
152
+ value: `${d.used} / ${d.size} (${d.capacity})`
153
+ }));
154
+ sections.push(`<h2>Disk</h2>
155
+ <table>
156
+ ${rows(diskRows).trim()}
157
+ </table>`);
158
+ }
159
+ if (data.processes.length) {
160
+ const procRows = data.processes.slice(0, 50).map((p) => ({
161
+ label: String(p.pid),
162
+ value: `${p.user.padEnd(10)} ${p.cpu.toFixed(1).padStart(6)}% ${p.mem.toFixed(1).padStart(6)}% ${p.state} ${p.command}`
163
+ }));
164
+ sections.push(`<h2>Processes</h2>
165
+ <table>
166
+ ${rows(procRows).trim()}
167
+ </table>`);
168
+ }
169
+ if (data.blenderRenders && data.blenderRenders.length) {
170
+ const blenderRows = data.blenderRenders.map((r) => ({
171
+ label: `PID ${r.pid}`,
172
+ value: `${r.blendFile} \xB7 ${r.renderEngine} \xB7 ${r.currentFrame}/${r.totalFrames} frames \xB7 ${r.completionPercent}% \xB7 ${r.status} \xB7 ${r.elapsedSec}s`
173
+ }));
174
+ sections.push(`<h2>Blender Renders</h2>
175
+ <table>
176
+ ${rows(blenderRows).trim()}
177
+ </table>`);
178
+ }
179
+ return `<!DOCTYPE html>
180
+ <html lang="en">
181
+ <head>
182
+ <meta charset="UTF-8">
183
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
184
+ <title>pyre \u2014 ${data.header.hostname}</title>
185
+ <style>
186
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0a0908; color: #efe9dc; margin: 0; padding: 24px; }
187
+ h1 { color: #ff6a39; margin-bottom: 4px; font-weight: 800; letter-spacing: -0.02em; }
188
+ .meta { color: #9c9384; margin-bottom: 24px; font-size: 0.9rem; }
189
+ h2 { color: #ffb130; margin-top: 28px; border-bottom: 1px solid #211d18; padding-bottom: 6px; font-size: 1.1rem; }
190
+ table { width: 100%; border-collapse: collapse; margin-bottom: 16px; font-family: monospace; }
191
+ td { padding: 8px 12px; border-bottom: 1px solid #1a1712; vertical-align: top; font-size: 0.9rem; }
192
+ td.label { color: #9c9384; width: 22%; font-weight: 600; }
193
+ td.value { color: #efe9dc; }
194
+ tr:last-child td { border-bottom: none; }
195
+ </style>
196
+ </head>
197
+ <body>
198
+ <h1>PYRE <span style="font-size:0.5em; color:#82c774; font-weight:normal;">\u25CF LIVE PORTAL</span></h1>
199
+ <div class="meta" id="header-meta">${escapeHtml(data.header.hostname)} \xB7 ${escapeHtml(data.header.os)} \xB7 up ${escapeHtml(data.header.uptime)} \xB7 <span id="timestamp-val">${escapeHtml(data.timestamp)}</span></div>
200
+ <div id="portal-content">
201
+ ${sections.join("\n ")}
202
+ </div>
203
+
204
+ <script>
205
+ function renderStats(data) {
206
+ if (!data) return;
207
+ const tsEl = document.getElementById('timestamp-val');
208
+ if (tsEl && data.timestamp) tsEl.textContent = data.timestamp;
209
+
210
+ function formatBytes(bytes) {
211
+ if (!bytes || bytes < 0) return '0 B';
212
+ if (bytes < 1024) return bytes + ' B';
213
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
214
+ if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
215
+ return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB';
216
+ }
217
+
218
+ function row(label, val) {
219
+ return '<tr><td class="label">' + label + '</td><td class="value">' + val + '</td></tr>';
220
+ }
221
+
222
+ let html = '';
223
+
224
+ // CPU & Memory
225
+ html += '<h2>CPU & Memory</h2><table>';
226
+ if (data.cpu) {
227
+ html += row('Brand', data.cpu.brand || 'Unknown');
228
+ html += row('Cores', (data.cpu.physicalCores || '') + '/' + (data.cpu.cores || '') + ' (phys/log)');
229
+ html += row('Usage', (data.cpu.usage || 0) + '%');
230
+ if (data.cpu.loadAvg) html += row('Load Avg', data.cpu.loadAvg.map(l => l.toFixed(2)).join(' '));
231
+ if (data.cpu.temperature) html += row('Temperature', data.cpu.temperature.toFixed(1) + '\xB0C');
232
+ }
233
+ if (data.memory) {
234
+ html += row('Memory Usage', data.memory.usagePercent + '% (' + formatBytes(data.memory.used) + ' / ' + formatBytes(data.memory.total) + ')');
235
+ html += row('Swap Used', formatBytes(data.memory.swapUsed) + ' / ' + formatBytes(data.memory.swapTotal));
236
+ }
237
+ html += '</table>';
238
+
239
+ // GPU
240
+ if (data.gpu) {
241
+ html += '<h2>GPU</h2><table>';
242
+ html += row('Model', data.gpu.model || '');
243
+ html += row('Utilization', (data.gpu.utilization || 0) + '%');
244
+ html += row('Memory', formatBytes(data.gpu.memory));
245
+ if (data.gpu.temperature) html += row('Temperature', data.gpu.temperature.toFixed(1) + '\xB0C');
246
+ html += '</table>';
247
+ }
248
+
249
+ // Network
250
+ if (data.network) {
251
+ html += '<h2>Network</h2><table>';
252
+ html += row('Interface', (data.network.interface || '') + ' (' + (data.network.ip || '') + ')');
253
+ html += row('RX', formatBytes(data.network.rxBytes) + ' / ' + (data.network.rxPackets || 0) + ' pkt');
254
+ html += row('TX', formatBytes(data.network.txBytes) + ' / ' + (data.network.txPackets || 0) + ' pkt');
255
+ html += '</table>';
256
+ }
257
+
258
+ // Disk
259
+ if (data.disk && data.disk.length) {
260
+ html += '<h2>Disk</h2><table>';
261
+ data.disk.forEach(d => {
262
+ let io = '';
263
+ if (d.readBytesSec !== undefined || d.writeBytesSec !== undefined) {
264
+ io = ' | R: ' + formatBytes(d.readBytesSec || 0) + '/s W: ' + formatBytes(d.writeBytesSec || 0) + '/s';
265
+ }
266
+ html += row(d.mountpoint, d.used + ' / ' + d.size + ' (' + d.capacity + ')' + io);
267
+ });
268
+ html += '</table>';
269
+ }
270
+
271
+ // Processes
272
+ if (data.processes && data.processes.length) {
273
+ html += '<h2>Processes (Top ' + Math.min(50, data.processes.length) + ')</h2><table>';
274
+ data.processes.slice(0, 50).forEach(p => {
275
+ html += row(String(p.pid), p.user + ' &nbsp; CPU: ' + p.cpu.toFixed(1) + '% &nbsp; MEM: ' + p.mem.toFixed(1) + '% &nbsp; ' + p.command);
276
+ });
277
+ html += '</table>';
278
+ }
279
+
280
+ const contentEl = document.getElementById('portal-content');
281
+ if (contentEl) contentEl.innerHTML = html;
282
+ }
283
+
284
+ if (window.EventSource) {
285
+ const evtSource = new EventSource('/api/stream');
286
+ evtSource.onmessage = function(event) {
287
+ try {
288
+ const data = JSON.parse(event.data);
289
+ renderStats(data);
290
+ } catch(e) {}
291
+ };
292
+ } else {
293
+ setInterval(function() {
294
+ fetch('/api').then(r => r.json()).then(renderStats).catch(function(){});
295
+ }, 2000);
296
+ }
297
+ </script>
298
+ </body>
299
+ </html>`;
300
+ }
301
+ function formatMarkdown(data) {
302
+ const lines = [];
303
+ lines.push(`# pyre \u2014 ${data.header.hostname}`);
304
+ lines.push("");
305
+ lines.push(`**${data.header.os}** \xB7 up ${data.header.uptime} \xB7 ${data.timestamp}`);
306
+ lines.push("");
307
+ lines.push("## System");
308
+ lines.push("");
309
+ lines.push("| Property | Value |");
310
+ lines.push("|---|---|");
311
+ lines.push(`| Hostname | ${mdCell(data.header.hostname)} |`);
312
+ lines.push(`| OS | ${mdCell(data.header.os)} |`);
313
+ lines.push(`| Uptime | ${mdCell(data.header.uptime)} |`);
314
+ lines.push("");
315
+ lines.push("## CPU");
316
+ lines.push("");
317
+ lines.push("| Property | Value |");
318
+ lines.push("|---|---|");
319
+ lines.push(`| Brand | ${mdCell(data.cpu.brand)} |`);
320
+ lines.push(`| Cores | ${mdCell(`${data.cpu.physicalCores}/${data.cpu.cores}`)} |`);
321
+ lines.push(`| Frequency | ${mdCell(`${data.cpu.frequency} MHz`)} |`);
322
+ lines.push(`| Usage | ${mdCell(`${data.cpu.usage}%`)} |`);
323
+ lines.push(`| Load Avg | ${mdCell(data.cpu.loadAvg.map((l) => l.toFixed(2)).join(" "))} |`);
324
+ if (data.cpu.temperature) lines.push(`| Temperature | ${mdCell(`${data.cpu.temperature.toFixed(1)}\xB0C`)} |`);
325
+ lines.push("");
326
+ lines.push("## Memory");
327
+ lines.push("");
328
+ lines.push("| Property | Value |");
329
+ lines.push("|---|---|");
330
+ lines.push(`| Total | ${mdCell(formatBytes(data.memory.total))} |`);
331
+ lines.push(`| Used | ${mdCell(formatBytes(data.memory.used))} |`);
332
+ lines.push(`| Free | ${mdCell(formatBytes(data.memory.free))} |`);
333
+ lines.push(`| Usage | ${mdCell(`${data.memory.usagePercent}%`)} |`);
334
+ lines.push(`| Swap | ${mdCell(`${formatBytes(data.memory.swapUsed)} / ${formatBytes(data.memory.swapTotal)}`)} |`);
335
+ lines.push("");
336
+ if (data.gpu) {
337
+ lines.push("## GPU");
338
+ lines.push("");
339
+ lines.push("| Property | Value |");
340
+ lines.push("|---|---|");
341
+ lines.push(`| Model | ${mdCell(data.gpu.model)} |`);
342
+ lines.push(`| Memory | ${mdCell(formatBytes(data.gpu.memory))} |`);
343
+ lines.push(`| Utilization | ${mdCell(`${data.gpu.utilization}%`)} |`);
344
+ if (data.gpu.temperature) lines.push(`| Temperature | ${mdCell(`${data.gpu.temperature.toFixed(1)}\xB0C`)} |`);
345
+ lines.push(`| Processes | ${mdCell(String(data.gpu.processes))} |`);
346
+ lines.push("");
347
+ }
348
+ if (data.power) {
349
+ const pw = [];
350
+ if (data.power.cpuWatts !== void 0) pw.push(`CPU ${data.power.cpuWatts.toFixed(2)} W`);
351
+ if (data.power.gpuWatts !== void 0) pw.push(`GPU ${data.power.gpuWatts.toFixed(2)} W`);
352
+ if (data.power.combinedWatts !== void 0) pw.push(`Combined ${data.power.combinedWatts.toFixed(2)} W`);
353
+ if (pw.length) {
354
+ lines.push("## Power");
355
+ lines.push("");
356
+ lines.push("| Metric | Value |");
357
+ lines.push("|---|---|");
358
+ if (data.power.cpuWatts !== void 0) lines.push(`| CPU | ${mdCell(`${data.power.cpuWatts.toFixed(2)} W`)} |`);
359
+ if (data.power.gpuWatts !== void 0) lines.push(`| GPU | ${mdCell(`${data.power.gpuWatts.toFixed(2)} W`)} |`);
360
+ if (data.power.combinedWatts !== void 0) lines.push(`| Combined | ${mdCell(`${data.power.combinedWatts.toFixed(2)} W`)} |`);
361
+ lines.push("");
362
+ }
363
+ }
364
+ if (data.battery) {
365
+ lines.push("## Battery");
366
+ lines.push("");
367
+ lines.push("| Property | Value |");
368
+ lines.push("|---|---|");
369
+ lines.push(`| Level | ${mdCell(`${data.battery.level}%`)} |`);
370
+ lines.push(`| State | ${mdCell(data.battery.state)} |`);
371
+ lines.push(`| Time Remaining | ${mdCell(data.battery.timeRemaining)} |`);
372
+ lines.push(`| Health | ${mdCell(data.battery.health)} |`);
373
+ lines.push(`| Power Source | ${mdCell(data.battery.powerSource)} |`);
374
+ if (data.battery.condition) lines.push(`| Condition | ${mdCell(data.battery.condition)} |`);
375
+ if (data.battery.maxCapacityPercent !== void 0) lines.push(`| Max Capacity | ${mdCell(`${data.battery.maxCapacityPercent}%`)} |`);
376
+ if (data.battery.cycles !== void 0) lines.push(`| Cycles | ${mdCell(String(data.battery.cycles))} |`);
377
+ if (data.battery.estimatedTimeToEmpty) lines.push(`| Est. Empty | ${mdCell(data.battery.estimatedTimeToEmpty)} |`);
378
+ if (data.battery.dischargeRatePerHour !== void 0) lines.push(`| Discharge Rate | ${mdCell(`${data.battery.dischargeRatePerHour}%/h`)} |`);
379
+ lines.push("");
380
+ }
381
+ lines.push("## Thermal");
382
+ lines.push("");
383
+ lines.push("| Property | Value |");
384
+ lines.push("|---|---|");
385
+ lines.push(`| State | ${mdCell(data.thermal.state)} |`);
386
+ lines.push(`| Pressure | ${mdCell(["Nominal", "Fair", "Serious", "Critical"][data.thermal.pressureLevel] || "Unknown")} |`);
387
+ if (data.thermal.temperatures) {
388
+ for (const [k, v] of Object.entries(data.thermal.temperatures)) {
389
+ if (v !== null && v !== void 0) lines.push(`| ${mdCell(k.replace(/_/g, " "))} | ${mdCell(`${v.toFixed(1)}\xB0C`)} |`);
390
+ }
391
+ }
392
+ lines.push("");
393
+ lines.push("## Network");
394
+ lines.push("");
395
+ lines.push("| Property | Value |");
396
+ lines.push("|---|---|");
397
+ lines.push(`| Interface | ${mdCell(`${data.network.interface} (${data.network.ip})`)} |`);
398
+ lines.push(`| RX | ${mdCell(`${formatBytes(data.network.rxBytes)} \xB7 ${formatBytes(data.network.rxRate)}/s`)} |`);
399
+ lines.push(`| TX | ${mdCell(`${formatBytes(data.network.txBytes)} \xB7 ${formatBytes(data.network.txRate)}/s`)} |`);
400
+ if (data.network.protocols) {
401
+ lines.push(`| TCP | ${mdCell(`${data.network.protocols.tcp.connections} conns`)} |`);
402
+ lines.push(`| UDP | ${mdCell(`${data.network.protocols.udp.connections} conns`)} |`);
403
+ }
404
+ if (data.network.connectionStates) {
405
+ lines.push(`| Active | ${mdCell(`${data.network.connectionStates.established} ESTABLISHED`)} |`);
406
+ lines.push(`| Listening | ${mdCell(`${data.network.connectionStates.listening}`)} |`);
407
+ }
408
+ if (data.network.listeningPorts && data.network.listeningPorts.length) {
409
+ lines.push(`| Ports | ${mdCell(data.network.listeningPorts.slice(0, 8).join(", "))} |`);
410
+ }
411
+ lines.push("");
412
+ if (data.packets) {
413
+ lines.push("## Packets");
414
+ lines.push("");
415
+ lines.push("| Property | Value |");
416
+ lines.push("|---|---|");
417
+ lines.push(`| Total | ${mdCell(`${data.packets.totalPackets} pkt`)} |`);
418
+ lines.push(`| RX | ${mdCell(`${data.packets.rxPackets} pkt`)} |`);
419
+ lines.push(`| TX | ${mdCell(`${data.packets.txPackets} pkt`)} |`);
420
+ lines.push(`| Connections | ${mdCell(`${data.packets.connections} TCP`)} |`);
421
+ if (data.packets.protocolStats) {
422
+ lines.push(`| TCP | ${mdCell(`${data.packets.protocolStats.tcp.connections} conns`)} |`);
423
+ lines.push(`| UDP | ${mdCell(`${data.packets.protocolStats.udp.connections} conns`)} |`);
424
+ }
425
+ if (data.packets.connectionStates) {
426
+ lines.push(`| ESTABLISHED | ${mdCell(`${data.packets.connectionStates.established}`)} |`);
427
+ lines.push(`| LISTENING | ${mdCell(`${data.packets.connectionStates.listening}`)} |`);
428
+ }
429
+ lines.push("");
430
+ }
431
+ if (data.tasks.length) {
432
+ lines.push("## Tasks");
433
+ lines.push("");
434
+ lines.push("| PID | User | CPU% | MEM% | State | Command |");
435
+ lines.push("|---|---|---|---|---|---|");
436
+ for (const t of data.tasks.slice(0, 20)) {
437
+ lines.push(`| ${t.pid} | ${mdCell(t.user)} | ${t.cpu.toFixed(1)} | ${t.mem.toFixed(1)} | ${t.state} | ${mdCell(t.command)} |`);
438
+ }
439
+ lines.push("");
440
+ }
441
+ if (data.disk.length) {
442
+ lines.push("## Disk");
443
+ lines.push("");
444
+ lines.push("| Mountpoint | Used / Size | Capacity |");
445
+ lines.push("|---|---|---|");
446
+ for (const d of data.disk) {
447
+ lines.push(`| ${mdCell(d.mountpoint)} | ${mdCell(`${d.used} / ${d.size}`)} | ${mdCell(d.capacity)} |`);
448
+ }
449
+ lines.push("");
450
+ }
451
+ if (data.processes.length) {
452
+ lines.push("## Processes");
453
+ lines.push("");
454
+ lines.push("| PID | User | CPU% | MEM% | State | Command |");
455
+ lines.push("|---|---|---|---|---|---|");
456
+ for (const p of data.processes.slice(0, 50)) {
457
+ lines.push(`| ${p.pid} | ${mdCell(p.user)} | ${p.cpu.toFixed(1)} | ${p.mem.toFixed(1)} | ${p.state} | ${mdCell(p.command)} |`);
458
+ }
459
+ lines.push("");
460
+ }
461
+ if (data.blenderRenders && data.blenderRenders.length) {
462
+ lines.push("## Blender Renders");
463
+ lines.push("");
464
+ lines.push("| PID | Blend File | Engine | Frames | Completion | Status | Elapsed |");
465
+ lines.push("|---|---|---|---|---|---|---|");
466
+ for (const r of data.blenderRenders) {
467
+ lines.push(`| ${r.pid} | ${mdCell(r.blendFile)} | ${mdCell(r.renderEngine)} | ${r.currentFrame}/${r.totalFrames} | ${r.completionPercent}% | ${r.status} | ${r.elapsedSec}s |`);
468
+ }
469
+ lines.push("");
470
+ }
471
+ return lines.join("\n");
472
+ }
473
+ function escapeHtml(s) {
474
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
475
+ }
476
+ function mdCell(s) {
477
+ return String(s).replace(/\|/g, "\\|");
478
+ }
479
+ function formatCsv(data) {
480
+ const rows = [
481
+ ["property", "key", "value"],
482
+ ["cpu", "brand", data.cpu.brand],
483
+ ["cpu", "usage", String(data.cpu.usage)],
484
+ ["cpu", "loadAvg_1m", String(data.cpu.loadAvg[0])],
485
+ ["cpu", "loadAvg_5m", String(data.cpu.loadAvg[1])],
486
+ ["cpu", "loadAvg_15m", String(data.cpu.loadAvg[2])],
487
+ ["cpu", "cores", String(data.cpu.cores)],
488
+ ["cpu", "physicalCores", String(data.cpu.physicalCores)],
489
+ ["cpu", "frequency", String(data.cpu.frequency)],
490
+ ["memory", "total", String(data.memory.total)],
491
+ ["memory", "used", String(data.memory.used)],
492
+ ["memory", "usagePercent", String(data.memory.usagePercent)],
493
+ ["memory", "swapUsed", String(data.memory.swapUsed)],
494
+ ["memory", "swapTotal", String(data.memory.swapTotal)],
495
+ ["thermal", "state", data.thermal.state],
496
+ ["thermal", "cpuTempC", data.thermal.temperatures?.cpu_die ? String(data.thermal.temperatures.cpu_die) : ""],
497
+ ["thermal", "cpuTempF", data.thermal.temperatures?.cpu_die != null ? String(celsiusToFahrenheit(data.thermal.temperatures.cpu_die)) : ""],
498
+ ["network", "rxBytes", String(data.network.rxBytes)],
499
+ ["network", "txBytes", String(data.network.txBytes)],
500
+ ["network", "rxRate", String(data.network.rxRate)],
501
+ ["network", "txRate", String(data.network.txRate)],
502
+ ["network", "rxPackets", String(data.network.rxPackets)],
503
+ ["network", "txPackets", String(data.network.txPackets)],
504
+ ["network", "connections", data.network.connections ? String(data.network.connections) : ""],
505
+ ["network", "tcpConns", data.network.protocols ? String(data.network.protocols.tcp.connections) : ""],
506
+ ["network", "udpConns", data.network.protocols ? String(data.network.protocols.udp.connections) : ""],
507
+ ["network", "established", data.network.connectionStates ? String(data.network.connectionStates.established) : ""],
508
+ ["network", "listening", data.network.connectionStates ? String(data.network.connectionStates.listening) : ""],
509
+ ["network", "listeningPorts", data.network.listeningPorts ? data.network.listeningPorts.slice(0, 10).join(";") : ""],
510
+ ["battery", "level", data.battery ? String(data.battery.level) : ""],
511
+ ["battery", "powerSource", data.battery ? data.battery.powerSource : ""],
512
+ ["battery", "condition", data.battery?.condition ?? ""],
513
+ ["battery", "maxCapacityPercent", data.battery?.maxCapacityPercent !== void 0 ? String(data.battery.maxCapacityPercent) : ""],
514
+ ["battery", "cycles", data.battery?.cycles !== void 0 ? String(data.battery.cycles) : ""],
515
+ ["battery", "estimatedTimeToEmpty", data.battery?.estimatedTimeToEmpty ?? ""],
516
+ ["battery", "dischargeRatePerHour", data.battery?.dischargeRatePerHour !== void 0 ? String(data.battery.dischargeRatePerHour) : ""],
517
+ ["battery", "powerWatts", data.battery?.powerWatts !== void 0 ? String(data.battery.powerWatts) : ""],
518
+ ["power", "cpuWatts", data.power?.cpuWatts !== void 0 ? String(data.power.cpuWatts) : ""],
519
+ ["power", "gpuWatts", data.power?.gpuWatts !== void 0 ? String(data.power.gpuWatts) : ""],
520
+ ["power", "combinedWatts", data.power?.combinedWatts !== void 0 ? String(data.power.combinedWatts) : ""],
521
+ ["system", "hostname", data.header.hostname],
522
+ ["system", "uptime", data.header.uptime],
523
+ ["system", "timestamp", data.timestamp]
524
+ ];
525
+ if (data.cpu.coreUsage && data.cpu.coreUsage.length) {
526
+ data.cpu.coreUsage.forEach((u, i) => rows.push(["cpu", `coreUsage_${i}`, String(u)]));
527
+ }
528
+ if (data.packets) {
529
+ rows.push(["packets", "totalPackets", String(data.packets.totalPackets)]);
530
+ rows.push(["packets", "rxPackets", String(data.packets.rxPackets)]);
531
+ rows.push(["packets", "txPackets", String(data.packets.txPackets)]);
532
+ rows.push(["packets", "connections", String(data.packets.connections)]);
533
+ if (data.packets.protocolStats) {
534
+ rows.push(["packets", "tcpConns", String(data.packets.protocolStats.tcp.connections)]);
535
+ rows.push(["packets", "udpConns", String(data.packets.protocolStats.udp.connections)]);
536
+ }
537
+ if (data.packets.connectionStates) {
538
+ rows.push(["packets", "established", String(data.packets.connectionStates.established)]);
539
+ rows.push(["packets", "listening", String(data.packets.connectionStates.listening)]);
540
+ rows.push(["packets", "timeWait", String(data.packets.connectionStates.timeWait)]);
541
+ rows.push(["packets", "closeWait", String(data.packets.connectionStates.closeWait)]);
542
+ }
543
+ for (const iface of data.packets.interfaces || []) {
544
+ rows.push(["packets_iface", iface.iface, `${iface.rxPackets},${iface.txPackets},${iface.rxBytes},${iface.txBytes}`]);
545
+ }
546
+ for (const proc of data.packets.allProcesses || []) {
547
+ rows.push(["packets_proc", `pid=${proc.pid}`, `${proc.command},${proc.rxBytes},${proc.txBytes}`]);
548
+ }
549
+ }
550
+ if (data.gpu) {
551
+ rows.push(["gpu", "model", data.gpu.model]);
552
+ rows.push(["gpu", "memory", String(data.gpu.memory)]);
553
+ rows.push(["gpu", "utilization", String(data.gpu.utilization)]);
554
+ if (data.gpu.temperature) rows.push(["gpu", "tempC", String(data.gpu.temperature)]);
555
+ rows.push(["gpu", "processes", String(data.gpu.processes)]);
556
+ }
557
+ for (const p of data.processes) {
558
+ rows.push(["process", `pid=${p.pid}`, `${p.pid}`]);
559
+ rows.push(["process", `ppid=${p.pid}`, String(p.ppid)]);
560
+ rows.push(["process", `user=${p.pid}`, p.user]);
561
+ rows.push(["process", `cpu=${p.pid}`, String(p.cpu)]);
562
+ rows.push(["process", `mem=${p.pid}`, String(p.mem)]);
563
+ rows.push(["process", `state=${p.pid}`, p.state]);
564
+ rows.push(["process", `threads=${p.pid}`, String(p.threads)]);
565
+ rows.push(["process", `runtime=${p.pid}`, String(p.runtime)]);
566
+ rows.push(["process", `command=${p.pid}`, p.command]);
567
+ }
568
+ for (const r of data.blenderRenders || []) {
569
+ rows.push(["blender", `pid=${r.pid}`, `${r.pid}`]);
570
+ rows.push(["blender", `blend_file=${r.pid}`, r.blendFile]);
571
+ rows.push(["blender", `engine=${r.pid}`, r.renderEngine]);
572
+ rows.push(["blender", `frame_start=${r.pid}`, String(r.frameStart)]);
573
+ rows.push(["blender", `frame_end=${r.pid}`, String(r.frameEnd)]);
574
+ rows.push(["blender", `current_frame=${r.pid}`, String(r.currentFrame)]);
575
+ rows.push(["blender", `total_frames=${r.pid}`, String(r.totalFrames)]);
576
+ rows.push(["blender", `completion_pct=${r.pid}`, String(r.completionPercent)]);
577
+ rows.push(["blender", `status=${r.pid}`, r.status]);
578
+ rows.push(["blender", `elapsed_sec=${r.pid}`, String(r.elapsedSec)]);
579
+ rows.push(["blender", `output_path=${r.pid}`, r.outputPath]);
580
+ if (r.sampleCount !== void 0) rows.push(["blender", `samples=${r.pid}`, String(r.sampleCount)]);
581
+ }
582
+ return rows.map((r) => r.map((item) => `"${String(item).replace(/"/g, '""')}"`).join(",")).join("\n");
583
+ }
584
+ function formatTsv(data) {
585
+ return formatCsv(data).split("\n").map((r) => r.split(",").join(" ")).join("\n");
586
+ }
587
+ function formatGraphs(history, width = 80, themeName = "default", graphMode = "spark") {
588
+ const theme = THEMES[themeName] || THEMES.default;
589
+ const contentWidth = width - 4;
590
+ const sampleCount = history.cpuUsage.length;
591
+ const sparkWidth = Math.max(10, contentWidth - 24);
592
+ const lines = [];
593
+ lines.push(graphRow("CPU %", history.cpuUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode, contentWidth));
594
+ lines.push(graphRow("Mem %", history.memUsage, { min: 0, max: 100 }, (v) => `${v.toFixed(0)}%`, sparkWidth, "collecting data...", graphMode, contentWidth));
595
+ lines.push(
596
+ graphRow("Temp", history.temp, {}, (v) => `${v.toFixed(1)}\xB0C`, sparkWidth, "no sensor access", graphMode, contentWidth)
597
+ );
598
+ lines.push(graphRow("Net RX/s", history.netRxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
599
+ lines.push(graphRow("Net TX/s", history.netTxRate, {}, (v) => `${formatBytes(v)}/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
600
+ if (history.powerWatts.length) {
601
+ lines.push(graphRow("Power W", history.powerWatts, {}, (v) => `${v.toFixed(1)} W`, sparkWidth, "collecting data...", graphMode, contentWidth));
602
+ }
603
+ if (history.rxPacketRate.length) {
604
+ lines.push(graphRow("RX pkt/s", history.rxPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
605
+ }
606
+ if (history.txPacketRate.length) {
607
+ lines.push(graphRow("TX pkt/s", history.txPacketRate, {}, (v) => `${v.toFixed(0)} pkt/s`, sparkWidth, "collecting data...", graphMode, contentWidth));
608
+ }
609
+ const title = `Graphs \xB7 mode:${graphMode} \xB7 last ${sampleCount} sample${sampleCount === 1 ? "" : "s"}`;
610
+ return panel(title, lines, width, theme.graphs, theme.border).join("\n");
611
+ }
612
+ function graphRow(label, values, bounds, fmt, sparkWidth = 40, emptyMessage = "collecting data...", mode = "spark", contentWidth = 76) {
613
+ if (!values.length) {
614
+ return `${chalk.dim(label.padEnd(10))} ${chalk.dim(emptyMessage)}`;
615
+ }
616
+ const visible = values.slice(-sparkWidth);
617
+ const spark = mode === "spark" ? sparkline(visible, bounds) : barGraph(visible, bounds, sparkWidth);
618
+ const current = values[values.length - 1];
619
+ let color = chalk.cyan;
620
+ if (bounds.min !== void 0 && bounds.max !== void 0) {
621
+ const pct = (current - bounds.min) / (bounds.max - bounds.min || 1) * 100;
622
+ color = pct > 90 ? chalk.red : pct > 70 ? chalk.yellow : chalk.green;
623
+ }
624
+ const raw = `${chalk.dim(label.padEnd(10))}${color(spark)} ${chalk.bold(fmt(current))}`;
625
+ return fitVisible(raw, contentWidth);
626
+ }
627
+ function barGraph(values, bounds, width) {
628
+ let min = bounds.min ?? Infinity;
629
+ let max = bounds.max ?? -Infinity;
630
+ if (bounds.min === void 0 || bounds.max === void 0) {
631
+ for (const v of values) {
632
+ if (v < min) min = v;
633
+ if (v > max) max = v;
634
+ }
635
+ }
636
+ const range = max - min || 1;
637
+ return values.map((v) => {
638
+ const pct = (v - min) / range;
639
+ if (pct > 0.8) return "\u2588";
640
+ if (pct > 0.6) return "\u2587";
641
+ if (pct > 0.4) return "\u2586";
642
+ if (pct > 0.2) return "\u2584";
643
+ return " ";
644
+ }).join("");
645
+ }
646
+
647
+ // src/formatters/html_template.ts
648
+ function getDashboardHtml() {
649
+ return `<!DOCTYPE html>
650
+ <html lang="en">
651
+ <head>
652
+ <meta charset="UTF-8"/>
653
+ <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
654
+ <title>Activity Monitor</title>
655
+ <style>
656
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
657
+ :root {
658
+ --bg-window: #1e1e1e;
659
+ --bg-toolbar: #2c2c2e;
660
+ --bg-surface: #252528;
661
+ --bg-surface-hover: rgba(255, 255, 255, 0.05);
662
+ --bg-table-header: #28282a;
663
+ --bg-table-alt: rgba(255, 255, 255, 0.02);
664
+ --bg-selection: #007aff;
665
+ --text-selection: #ffffff;
666
+ --border-color: #38383a;
667
+ --border-light: rgba(255, 255, 255, 0.1);
668
+ --text-primary: #ffffff;
669
+ --text-secondary: #a1a1a6;
670
+ --text-tertiary: #636366;
671
+ --mac-blue: #007aff;
672
+ --mac-green: #34c759;
673
+ --mac-yellow: #ff9f0a;
674
+ --mac-red: #ff453a;
675
+ --mac-purple: #af52de;
676
+ --mac-teal: #64d2ff;
677
+ --font-system: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Helvetica Neue", Helvetica, Arial, sans-serif;
678
+ --font-mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
679
+ }
680
+
681
+ html, body {
682
+ height: 100%;
683
+ width: 100%;
684
+ overflow: hidden;
685
+ background: var(--bg-window);
686
+ color: var(--text-primary);
687
+ font-family: var(--font-system);
688
+ font-size: 12px;
689
+ user-select: none;
690
+ -webkit-user-select: none;
691
+ -webkit-font-smoothing: antialiased;
692
+ }
693
+
694
+ body {
695
+ display: flex;
696
+ flex-direction: column;
697
+ }
698
+
699
+ /* Unified macOS Toolbar */
700
+ #toolbar {
701
+ height: 52px;
702
+ min-height: 52px;
703
+ background: var(--bg-toolbar);
704
+ border-bottom: 1px solid var(--border-color);
705
+ display: flex;
706
+ align-items: center;
707
+ justify-content: space-between;
708
+ padding: 0 14px;
709
+ gap: 12px;
710
+ z-index: 50;
711
+ -webkit-app-region: drag;
712
+ }
713
+
714
+ .toolbar-left, .toolbar-center, .toolbar-right {
715
+ display: flex;
716
+ align-items: center;
717
+ gap: 8px;
718
+ -webkit-app-region: no-drag;
719
+ }
720
+
721
+ .toolbar-center {
722
+ flex: 1;
723
+ justify-content: center;
724
+ }
725
+
726
+ /* macOS Toolbar Push Buttons */
727
+ .mac-btn {
728
+ display: inline-flex;
729
+ align-items: center;
730
+ justify-content: center;
731
+ gap: 6px;
732
+ height: 28px;
733
+ padding: 0 10px;
734
+ border-radius: 6px;
735
+ border: 1px solid var(--border-color);
736
+ background: #3a3a3c;
737
+ color: var(--text-primary);
738
+ font-family: var(--font-system);
739
+ font-size: 12px;
740
+ font-weight: 500;
741
+ cursor: pointer;
742
+ outline: none;
743
+ transition: background 0.15s, border-color 0.15s, opacity 0.15s;
744
+ }
745
+ .mac-btn:hover:not(:disabled) {
746
+ background: #48484a;
747
+ border-color: #545458;
748
+ }
749
+ .mac-btn:active:not(:disabled) {
750
+ background: #323234;
751
+ }
752
+ .mac-btn:disabled {
753
+ opacity: 0.35;
754
+ cursor: default;
755
+ }
756
+ .mac-btn-icon {
757
+ width: 28px;
758
+ padding: 0;
759
+ }
760
+ .mac-btn-danger {
761
+ background: rgba(255, 69, 58, 0.2);
762
+ border-color: rgba(255, 69, 58, 0.4);
763
+ color: #ff6961;
764
+ }
765
+ .mac-btn-danger:hover:not(:disabled) {
766
+ background: rgba(255, 69, 58, 0.35);
767
+ border-color: var(--mac-red);
768
+ color: #fff;
769
+ }
770
+ .mac-btn-primary {
771
+ background: var(--mac-blue);
772
+ border-color: #0062cc;
773
+ color: #fff;
774
+ }
775
+ .mac-btn-primary:hover:not(:disabled) {
776
+ background: #006ee6;
777
+ }
778
+
779
+ /* macOS Segmented Control Tabs */
780
+ .mac-segmented {
781
+ display: inline-flex;
782
+ background: #1c1c1e;
783
+ border: 1px solid var(--border-color);
784
+ border-radius: 8px;
785
+ padding: 2px;
786
+ gap: 2px;
787
+ }
788
+ .mac-seg-item {
789
+ padding: 4px 14px;
790
+ border-radius: 6px;
791
+ font-size: 12px;
792
+ font-weight: 500;
793
+ color: var(--text-secondary);
794
+ cursor: pointer;
795
+ transition: all 0.18s ease;
796
+ display: flex;
797
+ align-items: center;
798
+ gap: 5px;
799
+ }
800
+ .mac-seg-item:hover:not(.active) {
801
+ color: var(--text-primary);
802
+ background: rgba(255, 255, 255, 0.05);
803
+ }
804
+ .mac-seg-item.active {
805
+ background: #636366;
806
+ color: #ffffff;
807
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
808
+ }
809
+
810
+ /* Filter Dropdown */
811
+ .mac-select {
812
+ height: 28px;
813
+ border-radius: 6px;
814
+ background: #3a3a3c;
815
+ border: 1px solid var(--border-color);
816
+ color: var(--text-primary);
817
+ font-family: var(--font-system);
818
+ font-size: 11px;
819
+ padding: 0 8px;
820
+ outline: none;
821
+ cursor: pointer;
822
+ }
823
+ .mac-select:hover {
824
+ background: #48484a;
825
+ }
826
+
827
+ /* Search Field */
828
+ .mac-search-wrap {
829
+ position: relative;
830
+ display: flex;
831
+ align-items: center;
832
+ }
833
+ .mac-search-icon {
834
+ position: absolute;
835
+ left: 8px;
836
+ width: 13px;
837
+ height: 13px;
838
+ fill: var(--text-secondary);
839
+ pointer-events: none;
840
+ }
841
+ .mac-search-input {
842
+ height: 28px;
843
+ width: 170px;
844
+ background: #1c1c1e;
845
+ border: 1px solid var(--border-color);
846
+ border-radius: 6px;
847
+ padding: 0 24px 0 26px;
848
+ color: var(--text-primary);
849
+ font-family: var(--font-system);
850
+ font-size: 12px;
851
+ outline: none;
852
+ transition: width 0.2s, border-color 0.2s;
853
+ }
854
+ .mac-search-input:focus {
855
+ width: 210px;
856
+ border-color: var(--mac-blue);
857
+ box-shadow: 0 0 0 1px var(--mac-blue);
858
+ }
859
+ .mac-search-clear {
860
+ position: absolute;
861
+ right: 6px;
862
+ width: 14px;
863
+ height: 14px;
864
+ border-radius: 50%;
865
+ background: var(--text-tertiary);
866
+ color: #000;
867
+ display: none;
868
+ align-items: center;
869
+ justify-content: center;
870
+ font-size: 10px;
871
+ cursor: pointer;
872
+ line-height: 1;
873
+ }
874
+
875
+ /* Session Recorder Button & Pulsing Dot */
876
+ .mac-btn-record {
877
+ font-family: var(--font-mono);
878
+ font-size: 11px;
879
+ gap: 7px;
880
+ padding: 0 10px;
881
+ border-color: var(--border-color);
882
+ background: #323235;
883
+ }
884
+ .mac-btn-record:hover:not(:disabled) {
885
+ background: #3e3e42;
886
+ }
887
+ .mac-btn-record.recording {
888
+ background: rgba(255, 69, 58, 0.22);
889
+ border-color: var(--mac-red);
890
+ color: #ff6961;
891
+ }
892
+ .rec-dot {
893
+ width: 8px;
894
+ height: 8px;
895
+ border-radius: 50%;
896
+ background: #777;
897
+ display: inline-block;
898
+ transition: background 0.2s;
899
+ }
900
+ .mac-btn-record.recording .rec-dot {
901
+ background: #ff453a;
902
+ box-shadow: 0 0 8px #ff453a;
903
+ animation: rec-pulse 1.2s infinite alternate ease-in-out;
904
+ }
905
+ @keyframes rec-pulse {
906
+ from { opacity: 0.45; transform: scale(0.9); }
907
+ to { opacity: 1; transform: scale(1.2); }
908
+ }
909
+
910
+ /* Export Snapshot Dropdown Menu */
911
+ .mac-dropdown-wrap {
912
+ position: relative;
913
+ display: inline-flex;
914
+ }
915
+ .mac-dropdown-menu {
916
+ position: absolute;
917
+ top: 32px;
918
+ left: 0;
919
+ min-width: 165px;
920
+ background: #2c2c2e;
921
+ border: 1px solid var(--border-color);
922
+ border-radius: 8px;
923
+ box-shadow: 0 8px 24px rgba(0,0,0,0.65);
924
+ padding: 5px;
925
+ display: none;
926
+ flex-direction: column;
927
+ z-index: 200;
928
+ }
929
+ .mac-dropdown-menu.show {
930
+ display: flex;
931
+ }
932
+ .mac-menu-item {
933
+ padding: 6px 12px;
934
+ font-size: 11.5px;
935
+ color: var(--text-primary);
936
+ border-radius: 5px;
937
+ cursor: pointer;
938
+ transition: background 0.15s;
939
+ display: flex;
940
+ align-items: center;
941
+ gap: 8px;
942
+ }
943
+ .mac-menu-item:hover {
944
+ background: var(--mac-blue);
945
+ color: #fff;
946
+ }
947
+
948
+ /* Logs View & Stream Table */
949
+ #logs-view {
950
+ flex: 1;
951
+ display: none;
952
+ flex-direction: column;
953
+ overflow: hidden;
954
+ height: 100%;
955
+ }
956
+ .logs-sub-toolbar {
957
+ height: 38px;
958
+ min-height: 38px;
959
+ background: var(--bg-table-header);
960
+ border-bottom: 1px solid var(--border-color);
961
+ display: flex;
962
+ align-items: center;
963
+ justify-content: space-between;
964
+ padding: 0 12px;
965
+ gap: 10px;
966
+ }
967
+ .logs-sub-left, .logs-sub-right {
968
+ display: flex;
969
+ align-items: center;
970
+ gap: 8px;
971
+ }
972
+ .logs-scroll-area {
973
+ flex: 1;
974
+ overflow-y: auto;
975
+ background: var(--bg-window);
976
+ }
977
+ .log-table {
978
+ width: 100%;
979
+ border-collapse: collapse;
980
+ font-size: 11px;
981
+ }
982
+ .log-table th {
983
+ position: sticky;
984
+ top: 0;
985
+ background: #252528;
986
+ color: var(--text-secondary);
987
+ font-weight: 600;
988
+ text-align: left;
989
+ padding: 6px 10px;
990
+ border-bottom: 1px solid var(--border-color);
991
+ white-space: nowrap;
992
+ z-index: 5;
993
+ }
994
+ .log-table td {
995
+ padding: 6px 10px;
996
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
997
+ vertical-align: middle;
998
+ font-family: var(--font-mono);
999
+ }
1000
+ .log-table tr:hover {
1001
+ background: var(--bg-surface-hover);
1002
+ }
1003
+ .log-badge {
1004
+ display: inline-flex;
1005
+ align-items: center;
1006
+ justify-content: center;
1007
+ padding: 1px 7px;
1008
+ border-radius: 4px;
1009
+ font-size: 10px;
1010
+ font-weight: 700;
1011
+ letter-spacing: 0.03em;
1012
+ text-transform: uppercase;
1013
+ }
1014
+ .log-badge.info { background: rgba(0, 122, 255, 0.18); color: #64b5f6; border: 1px solid rgba(0, 122, 255, 0.3); }
1015
+ .log-badge.warn { background: rgba(255, 159, 10, 0.18); color: #ffb74d; border: 1px solid rgba(255, 159, 10, 0.35); }
1016
+ .log-badge.crit { background: rgba(255, 69, 58, 0.22); color: #ff6e6e; border: 1px solid rgba(255, 69, 58, 0.45); }
1017
+ .log-badge.action { background: rgba(175, 82, 222, 0.2); color: #d084f7; border: 1px solid rgba(175, 82, 222, 0.4); }
1018
+ .log-badge.watchdog { background: rgba(52, 199, 89, 0.2); color: #81c784; border: 1px solid rgba(52, 199, 89, 0.4); }
1019
+
1020
+ .log-subsys {
1021
+ font-weight: 600;
1022
+ color: var(--text-secondary);
1023
+ }
1024
+ .log-msg {
1025
+ color: var(--text-primary);
1026
+ font-family: var(--font-system);
1027
+ font-size: 11.5px;
1028
+ }
1029
+ .log-ctx {
1030
+ color: var(--text-tertiary);
1031
+ font-size: 10.5px;
1032
+ }
1033
+
1034
+ /* History Explorer Table */
1035
+ .history-table {
1036
+ width: 100%;
1037
+ border-collapse: collapse;
1038
+ font-size: 11px;
1039
+ }
1040
+ .history-table th {
1041
+ color: var(--text-secondary);
1042
+ font-weight: 600;
1043
+ text-align: left;
1044
+ padding: 4px 8px;
1045
+ border-bottom: 1px solid var(--border-color);
1046
+ }
1047
+ .history-table td {
1048
+ padding: 5px 8px;
1049
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
1050
+ }
1051
+ .history-link {
1052
+ color: var(--mac-blue);
1053
+ cursor: pointer;
1054
+ text-decoration: none;
1055
+ }
1056
+ .history-link:hover {
1057
+ text-decoration: underline;
1058
+ }
1059
+
1060
+ /* Main Split View */
1061
+ #split-view {
1062
+ flex: 1;
1063
+ display: flex;
1064
+ flex-direction: column;
1065
+ overflow: hidden;
1066
+ position: relative;
1067
+ }
1068
+
1069
+ /* Top Process Table Area */
1070
+ #table-container {
1071
+ flex: 62;
1072
+ min-height: 160px;
1073
+ overflow-y: auto;
1074
+ overflow-x: auto;
1075
+ background: var(--bg-window);
1076
+ position: relative;
1077
+ }
1078
+ #table-container::-webkit-scrollbar {
1079
+ width: 10px;
1080
+ height: 10px;
1081
+ }
1082
+ #table-container::-webkit-scrollbar-track {
1083
+ background: var(--bg-window);
1084
+ }
1085
+ #table-container::-webkit-scrollbar-thumb {
1086
+ background: #424245;
1087
+ border-radius: 5px;
1088
+ border: 2px solid var(--bg-window);
1089
+ }
1090
+
1091
+ table.mac-table {
1092
+ width: 100%;
1093
+ border-collapse: collapse;
1094
+ font-size: 11px;
1095
+ table-layout: fixed;
1096
+ }
1097
+ table.mac-table thead {
1098
+ position: sticky;
1099
+ top: 0;
1100
+ z-index: 10;
1101
+ background: var(--bg-table-header);
1102
+ }
1103
+ table.mac-table th {
1104
+ padding: 6px 10px;
1105
+ text-align: right;
1106
+ font-weight: 600;
1107
+ color: var(--text-secondary);
1108
+ border-bottom: 1px solid var(--border-color);
1109
+ border-right: 1px solid rgba(255, 255, 255, 0.05);
1110
+ cursor: pointer;
1111
+ white-space: nowrap;
1112
+ overflow: hidden;
1113
+ text-overflow: ellipsis;
1114
+ }
1115
+ table.mac-table th:first-child {
1116
+ text-align: left;
1117
+ }
1118
+ table.mac-table th:hover {
1119
+ background: #323235;
1120
+ color: var(--text-primary);
1121
+ }
1122
+ table.mac-table th .sort-caret {
1123
+ display: inline-block;
1124
+ margin-left: 4px;
1125
+ font-size: 8px;
1126
+ color: var(--mac-blue);
1127
+ }
1128
+
1129
+ table.mac-table tbody tr {
1130
+ height: 22px;
1131
+ border-bottom: 1px solid rgba(255, 255, 255, 0.03);
1132
+ cursor: default;
1133
+ }
1134
+ table.mac-table tbody tr:nth-child(even) {
1135
+ background: var(--bg-table-alt);
1136
+ }
1137
+ table.mac-table tbody tr:hover {
1138
+ background: var(--bg-surface-hover);
1139
+ }
1140
+ table.mac-table tbody tr.selected {
1141
+ background: var(--bg-selection) !important;
1142
+ color: var(--text-selection) !important;
1143
+ }
1144
+ table.mac-table tbody tr.selected td {
1145
+ color: var(--text-selection) !important;
1146
+ }
1147
+
1148
+ table.mac-table td {
1149
+ padding: 3px 10px;
1150
+ white-space: nowrap;
1151
+ overflow: hidden;
1152
+ text-overflow: ellipsis;
1153
+ text-align: right;
1154
+ font-family: var(--font-mono);
1155
+ font-variant-numeric: tabular-nums;
1156
+ color: var(--text-primary);
1157
+ }
1158
+ table.mac-table.density-compact td {
1159
+ padding: 1px 7px;
1160
+ font-size: 10px;
1161
+ }
1162
+ table.mac-table.density-comfortable td {
1163
+ padding: 6px 12px;
1164
+ font-size: 12px;
1165
+ }
1166
+ table.mac-table td.col-name {
1167
+ text-align: left;
1168
+ font-family: var(--font-system);
1169
+ display: flex;
1170
+ align-items: center;
1171
+ gap: 7px;
1172
+ }
1173
+ .proc-icon {
1174
+ width: 14px;
1175
+ height: 14px;
1176
+ flex-shrink: 0;
1177
+ display: inline-flex;
1178
+ align-items: center;
1179
+ justify-content: center;
1180
+ border-radius: 3px;
1181
+ background: #3a3a3c;
1182
+ color: #fff;
1183
+ font-size: 9px;
1184
+ font-weight: 700;
1185
+ }
1186
+ .proc-icon.system { background: #5856d6; }
1187
+ .proc-icon.user { background: #007aff; }
1188
+
1189
+ /* Horizontal Split Divider */
1190
+ #split-divider {
1191
+ height: 4px;
1192
+ min-height: 4px;
1193
+ background: #2a2a2d;
1194
+ border-top: 1px solid var(--border-color);
1195
+ border-bottom: 1px solid var(--border-color);
1196
+ cursor: row-resize;
1197
+ position: relative;
1198
+ z-index: 20;
1199
+ }
1200
+ #split-divider::after {
1201
+ content: "";
1202
+ position: absolute;
1203
+ left: 50%;
1204
+ top: 1px;
1205
+ transform: translateX(-50%);
1206
+ width: 28px;
1207
+ height: 2px;
1208
+ background: #4a4a4d;
1209
+ border-radius: 1px;
1210
+ }
1211
+
1212
+ /* Bottom Hardware Dashboard Panels */
1213
+ #bottom-panel {
1214
+ flex: 38;
1215
+ min-height: 160px;
1216
+ background: var(--bg-surface);
1217
+ display: flex;
1218
+ flex-direction: column;
1219
+ overflow: hidden;
1220
+ padding: 12px 16px;
1221
+ position: relative;
1222
+ }
1223
+
1224
+ .panel-content {
1225
+ display: none;
1226
+ height: 100%;
1227
+ width: 100%;
1228
+ gap: 20px;
1229
+ }
1230
+ .panel-content.active {
1231
+ display: flex;
1232
+ }
1233
+
1234
+ /* Left / Right Pane Layout */
1235
+ .pane-col {
1236
+ display: flex;
1237
+ flex-direction: column;
1238
+ justify-content: space-between;
1239
+ }
1240
+ .pane-stats {
1241
+ width: 270px;
1242
+ flex-shrink: 0;
1243
+ display: flex;
1244
+ flex-direction: column;
1245
+ gap: 6px;
1246
+ justify-content: center;
1247
+ }
1248
+ .pane-graph {
1249
+ flex: 1;
1250
+ display: flex;
1251
+ flex-direction: column;
1252
+ position: relative;
1253
+ border: 1px solid var(--border-color);
1254
+ border-radius: 6px;
1255
+ background: #19191b;
1256
+ padding: 8px;
1257
+ overflow: hidden;
1258
+ }
1259
+
1260
+ /* Stat Rows */
1261
+ .stat-item {
1262
+ display: flex;
1263
+ justify-content: space-between;
1264
+ align-items: center;
1265
+ font-size: 11px;
1266
+ }
1267
+ .stat-k {
1268
+ color: var(--text-secondary);
1269
+ font-weight: 500;
1270
+ }
1271
+ .stat-v {
1272
+ font-family: var(--font-mono);
1273
+ font-weight: 600;
1274
+ color: var(--text-primary);
1275
+ font-variant-numeric: tabular-nums;
1276
+ }
1277
+
1278
+ /* Canvas Graph Styling */
1279
+ .graph-header {
1280
+ display: flex;
1281
+ justify-content: space-between;
1282
+ align-items: center;
1283
+ margin-bottom: 4px;
1284
+ }
1285
+ .graph-title {
1286
+ font-size: 11px;
1287
+ font-weight: 600;
1288
+ color: var(--text-secondary);
1289
+ text-transform: uppercase;
1290
+ letter-spacing: 0.04em;
1291
+ }
1292
+ .graph-legend {
1293
+ display: flex;
1294
+ align-items: center;
1295
+ gap: 12px;
1296
+ font-size: 10px;
1297
+ color: var(--text-secondary);
1298
+ }
1299
+ .legend-pill {
1300
+ display: inline-flex;
1301
+ align-items: center;
1302
+ gap: 4px;
1303
+ }
1304
+ .legend-dot {
1305
+ width: 7px;
1306
+ height: 7px;
1307
+ border-radius: 50%;
1308
+ }
1309
+ canvas.mac-canvas {
1310
+ width: 100%;
1311
+ height: 100%;
1312
+ flex: 1;
1313
+ display: block;
1314
+ }
1315
+
1316
+ /* Memory Pressure Badge */
1317
+ .pressure-meter {
1318
+ display: flex;
1319
+ align-items: center;
1320
+ gap: 8px;
1321
+ margin-top: 4px;
1322
+ padding: 6px 10px;
1323
+ border-radius: 6px;
1324
+ background: #1c1c1e;
1325
+ border: 1px solid var(--border-color);
1326
+ }
1327
+ .pressure-dot {
1328
+ width: 10px;
1329
+ height: 10px;
1330
+ border-radius: 50%;
1331
+ background: var(--mac-green);
1332
+ box-shadow: 0 0 8px var(--mac-green);
1333
+ }
1334
+ .pressure-dot.warn { background: var(--mac-yellow); box-shadow: 0 0 8px var(--mac-yellow); }
1335
+ .pressure-dot.crit { background: var(--mac-red); box-shadow: 0 0 8px var(--mac-red); }
1336
+ .pressure-text {
1337
+ font-weight: 600;
1338
+ font-size: 11px;
1339
+ }
1340
+
1341
+ /* Status Bar */
1342
+ #statusbar {
1343
+ height: 24px;
1344
+ min-height: 24px;
1345
+ background: #1a1a1c;
1346
+ border-top: 1px solid var(--border-color);
1347
+ display: flex;
1348
+ align-items: center;
1349
+ justify-content: space-between;
1350
+ padding: 0 14px;
1351
+ font-size: 11px;
1352
+ color: var(--text-secondary);
1353
+ z-index: 40;
1354
+ }
1355
+ .sb-left, .sb-right {
1356
+ display: flex;
1357
+ align-items: center;
1358
+ gap: 12px;
1359
+ }
1360
+ .sb-live-indicator {
1361
+ display: inline-flex;
1362
+ align-items: center;
1363
+ gap: 5px;
1364
+ }
1365
+ .sb-live-dot {
1366
+ width: 6px;
1367
+ height: 6px;
1368
+ border-radius: 50%;
1369
+ background: var(--mac-green);
1370
+ }
1371
+ .sb-live-dot.offline {
1372
+ background: var(--mac-red);
1373
+ }
1374
+
1375
+ /* Modal Sheets */
1376
+ .modal-backdrop {
1377
+ position: fixed;
1378
+ inset: 0;
1379
+ background: rgba(0, 0, 0, 0.55);
1380
+ backdrop-filter: blur(6px);
1381
+ -webkit-backdrop-filter: blur(6px);
1382
+ display: none;
1383
+ align-items: center;
1384
+ justify-content: center;
1385
+ z-index: 1000;
1386
+ }
1387
+ .modal-backdrop.open {
1388
+ display: flex;
1389
+ animation: fadeIn 0.15s ease-out;
1390
+ }
1391
+ .mac-sheet {
1392
+ background: #252528;
1393
+ border: 1px solid var(--border-color);
1394
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7);
1395
+ border-radius: 10px;
1396
+ overflow: hidden;
1397
+ max-width: 90vw;
1398
+ animation: slideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1);
1399
+ }
1400
+ .modal-quit-box { width: 380px; padding: 20px; text-align: center; }
1401
+ .modal-inspect-box { width: 520px; }
1402
+ .modal-settings-box {
1403
+ width: 680px;
1404
+ height: 540px;
1405
+ max-height: 90vh;
1406
+ display: flex;
1407
+ flex-direction: column;
1408
+ }
1409
+ .settings-tabs {
1410
+ display: flex;
1411
+ background: #1c1c1e;
1412
+ border-bottom: 1px solid var(--border-color);
1413
+ padding: 0 10px;
1414
+ gap: 4px;
1415
+ }
1416
+ .settings-tab-btn {
1417
+ padding: 9px 13px;
1418
+ font-size: 11.5px;
1419
+ font-weight: 500;
1420
+ color: var(--text-secondary);
1421
+ background: transparent;
1422
+ border: none;
1423
+ border-bottom: 2px solid transparent;
1424
+ cursor: pointer;
1425
+ display: inline-flex;
1426
+ align-items: center;
1427
+ gap: 6px;
1428
+ transition: all 0.15s;
1429
+ }
1430
+ .settings-tab-btn:hover {
1431
+ color: var(--text-primary);
1432
+ }
1433
+ .settings-tab-btn.active {
1434
+ color: #fff;
1435
+ border-bottom-color: var(--mac-blue);
1436
+ font-weight: 600;
1437
+ }
1438
+ .settings-content-wrap {
1439
+ flex: 1;
1440
+ overflow-y: auto;
1441
+ padding: 16px 20px;
1442
+ background: var(--bg-window);
1443
+ }
1444
+ .settings-pane {
1445
+ display: none;
1446
+ flex-direction: column;
1447
+ gap: 14px;
1448
+ }
1449
+ .settings-pane.active {
1450
+ display: flex;
1451
+ }
1452
+ .settings-card {
1453
+ background: #252528;
1454
+ border: 1px solid var(--border-color);
1455
+ border-radius: 8px;
1456
+ padding: 12px 14px;
1457
+ display: flex;
1458
+ flex-direction: column;
1459
+ gap: 10px;
1460
+ }
1461
+ .settings-card-title {
1462
+ font-size: 12px;
1463
+ font-weight: 600;
1464
+ color: var(--text-primary);
1465
+ margin-bottom: 2px;
1466
+ display: flex;
1467
+ align-items: center;
1468
+ gap: 6px;
1469
+ }
1470
+ .settings-item {
1471
+ display: flex;
1472
+ align-items: center;
1473
+ justify-content: space-between;
1474
+ gap: 12px;
1475
+ }
1476
+ .settings-label-wrap {
1477
+ display: flex;
1478
+ flex-direction: column;
1479
+ gap: 2px;
1480
+ }
1481
+ .settings-label {
1482
+ font-size: 11.5px;
1483
+ font-weight: 500;
1484
+ color: var(--text-primary);
1485
+ }
1486
+ .settings-sublabel {
1487
+ font-size: 10px;
1488
+ color: var(--text-tertiary);
1489
+ }
1490
+ .settings-control {
1491
+ display: flex;
1492
+ align-items: center;
1493
+ gap: 8px;
1494
+ }
1495
+ /* macOS style toggle switch */
1496
+ .mac-switch {
1497
+ position: relative;
1498
+ display: inline-block;
1499
+ width: 38px;
1500
+ height: 22px;
1501
+ }
1502
+ .mac-switch input {
1503
+ opacity: 0;
1504
+ width: 0;
1505
+ height: 0;
1506
+ }
1507
+ .mac-slider {
1508
+ position: absolute;
1509
+ cursor: pointer;
1510
+ inset: 0;
1511
+ background-color: #3e3e42;
1512
+ transition: 0.2s;
1513
+ border-radius: 22px;
1514
+ border: 1px solid var(--border-color);
1515
+ }
1516
+ .mac-slider:before {
1517
+ position: absolute;
1518
+ content: "";
1519
+ height: 16px;
1520
+ width: 16px;
1521
+ left: 2px;
1522
+ bottom: 2px;
1523
+ background-color: #fff;
1524
+ transition: 0.2s;
1525
+ border-radius: 50%;
1526
+ box-shadow: 0 1px 3px rgba(0,0,0,0.4);
1527
+ }
1528
+ input:checked + .mac-slider {
1529
+ background-color: var(--mac-blue);
1530
+ border-color: var(--mac-blue);
1531
+ }
1532
+ input:checked + .mac-slider:before {
1533
+ transform: translateX(16px);
1534
+ }
1535
+ .settings-input {
1536
+ background: #1e1e20;
1537
+ border: 1px solid var(--border-color);
1538
+ border-radius: 5px;
1539
+ color: var(--text-primary);
1540
+ padding: 4px 8px;
1541
+ font-size: 11.5px;
1542
+ outline: none;
1543
+ }
1544
+ .settings-input:focus {
1545
+ border-color: var(--mac-blue);
1546
+ }
1547
+ .status-pill {
1548
+ display: inline-flex;
1549
+ align-items: center;
1550
+ gap: 5px;
1551
+ padding: 2px 8px;
1552
+ border-radius: 12px;
1553
+ font-size: 10.5px;
1554
+ font-weight: 600;
1555
+ }
1556
+ .status-pill.active {
1557
+ background: rgba(52, 199, 89, 0.2);
1558
+ color: #34c759;
1559
+ border: 1px solid rgba(52, 199, 89, 0.35);
1560
+ }
1561
+ .status-pill.inactive {
1562
+ background: rgba(255, 159, 10, 0.2);
1563
+ color: #ff9f0a;
1564
+ border: 1px solid rgba(255, 159, 10, 0.35);
1565
+ }
1566
+
1567
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
1568
+ @keyframes slideDown { from { transform: translateY(-16px) scale(0.98); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } }
1569
+
1570
+ .sheet-header {
1571
+ height: 40px;
1572
+ background: #2c2c2e;
1573
+ border-bottom: 1px solid var(--border-color);
1574
+ display: flex;
1575
+ align-items: center;
1576
+ justify-content: space-between;
1577
+ padding: 0 14px;
1578
+ }
1579
+ .sheet-title {
1580
+ font-weight: 600;
1581
+ font-size: 13px;
1582
+ color: var(--text-primary);
1583
+ }
1584
+ .sheet-close-btn {
1585
+ background: transparent;
1586
+ border: none;
1587
+ color: var(--text-secondary);
1588
+ font-size: 16px;
1589
+ cursor: pointer;
1590
+ padding: 4px;
1591
+ }
1592
+ .sheet-close-btn:hover { color: #fff; }
1593
+ .sheet-body {
1594
+ padding: 16px;
1595
+ max-height: 65vh;
1596
+ overflow-y: auto;
1597
+ }
1598
+ .sheet-footer {
1599
+ padding: 12px 16px;
1600
+ background: #202022;
1601
+ border-top: 1px solid var(--border-color);
1602
+ display: flex;
1603
+ justify-content: flex-end;
1604
+ gap: 10px;
1605
+ }
1606
+
1607
+ /* Quit Sheet Specific */
1608
+ .quit-icon-wrap {
1609
+ margin: 0 auto 12px;
1610
+ width: 54px;
1611
+ height: 54px;
1612
+ }
1613
+ .quit-title {
1614
+ font-size: 14px;
1615
+ font-weight: 700;
1616
+ margin-bottom: 6px;
1617
+ }
1618
+ .quit-desc {
1619
+ font-size: 12px;
1620
+ color: var(--text-secondary);
1621
+ line-height: 1.4;
1622
+ margin-bottom: 20px;
1623
+ }
1624
+
1625
+ /* Toast */
1626
+ #toast {
1627
+ position: fixed;
1628
+ bottom: 34px;
1629
+ left: 50%;
1630
+ transform: translateX(-50%) translateY(20px);
1631
+ background: rgba(40, 40, 44, 0.95);
1632
+ border: 1px solid var(--border-color);
1633
+ color: #fff;
1634
+ padding: 8px 16px;
1635
+ border-radius: 8px;
1636
+ font-size: 12px;
1637
+ box-shadow: 0 8px 24px rgba(0,0,0,0.5);
1638
+ opacity: 0;
1639
+ transition: all 0.25s ease;
1640
+ pointer-events: none;
1641
+ z-index: 2000;
1642
+ }
1643
+ #toast.show {
1644
+ transform: translateX(-50%) translateY(0);
1645
+ opacity: 1;
1646
+ }
1647
+
1648
+ /* Disk Mount Meter */
1649
+ .disk-bar {
1650
+ height: 8px;
1651
+ border-radius: 4px;
1652
+ background: #1c1c1e;
1653
+ overflow: hidden;
1654
+ margin-top: 3px;
1655
+ border: 1px solid var(--border-color);
1656
+ }
1657
+ .disk-bar-fill {
1658
+ height: 100%;
1659
+ background: linear-gradient(90deg, var(--mac-blue), #5ac8fa);
1660
+ }
1661
+ </style>
1662
+ </head>
1663
+ <body>
1664
+
1665
+ <!-- Unified macOS Toolbar -->
1666
+ <header id="toolbar">
1667
+ <div class="toolbar-left">
1668
+ <!-- Stop process button (octagon with \u2715) -->
1669
+ <button id="btn-stop" class="mac-btn mac-btn-icon mac-btn-danger" title="Force a process to quit" disabled onclick="confirmQuitSelected()">
1670
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor">
1671
+ <path d="M7.86 2h8.28L22 7.86v8.28L16.14 22H7.86L2 16.14V7.86L7.86 2zm1.41 2L4 8.27v7.46L8.27 20h7.46L20 15.73V8.27L15.73 4H9.27zM12 10.59l3.3-3.3 1.41 1.42L13.41 12l3.3 3.29-1.41 1.42L12 13.41l-3.29 3.3-1.42-1.42L10.59 12l-3.3-3.29 1.42-1.42L12 10.59z"/>
1672
+ </svg>
1673
+ </button>
1674
+
1675
+ <!-- Inspect process button (\u24D8) -->
1676
+ <button id="btn-inspect" class="mac-btn mac-btn-icon" title="Inspect a process" disabled onclick="openInspectorSelected()">
1677
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor">
1678
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
1679
+ </svg>
1680
+ </button>
1681
+
1682
+ <!-- Continuous CSV Session Recorder button -->
1683
+ <button id="btn-record" class="mac-btn mac-btn-record" title="Start continuous CSV logging session to ./pyre-exports (Shortcut: R)" onclick="toggleWebRecording()">
1684
+ <span class="rec-dot" id="rec-dot"></span>
1685
+ <span id="rec-label">Record</span>
1686
+ </button>
1687
+
1688
+ <!-- Quick Snapshot Exporter -->
1689
+ <div class="mac-dropdown-wrap">
1690
+ <button id="btn-export" class="mac-btn" title="Export current telemetry snapshot" onclick="toggleExportMenu(event)">
1691
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor">
1692
+ <path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z"/>
1693
+ </svg>
1694
+ <span>Export \u25BE</span>
1695
+ </button>
1696
+ <div class="mac-dropdown-menu" id="export-menu">
1697
+ <div class="mac-menu-item" onclick="downloadSnapshot('json')">\u{1F4C4} Snapshot JSON</div>
1698
+ <div class="mac-menu-item" onclick="downloadSnapshot('csv')">\u{1F4CA} Snapshot CSV</div>
1699
+ <div class="mac-menu-item" onclick="downloadSnapshot('tsv')">\u{1F4CB} Snapshot TSV</div>
1700
+ <div class="mac-menu-item" onclick="downloadSnapshot('md')">\u{1F4DD} Snapshot Markdown</div>
1701
+ </div>
1702
+ </div>
1703
+ </div>
1704
+
1705
+ <!-- Segmented Control Tabs (Center) -->
1706
+ <div class="toolbar-center">
1707
+ <nav class="mac-segmented" role="tablist">
1708
+ <div class="mac-seg-item active" data-tab="cpu" onclick="switchTab('cpu')">CPU</div>
1709
+ <div class="mac-seg-item" data-tab="memory" onclick="switchTab('memory')">Memory</div>
1710
+ <div class="mac-seg-item" data-tab="energy" onclick="switchTab('energy')">Energy</div>
1711
+ <div class="mac-seg-item" data-tab="disk" onclick="switchTab('disk')">Disk</div>
1712
+ <div class="mac-seg-item" data-tab="network" onclick="switchTab('network')">Network</div>
1713
+ <div class="mac-seg-item" data-tab="gpu" onclick="switchTab('gpu')">GPU</div>
1714
+ <div class="mac-seg-item" data-tab="logs" onclick="switchTab('logs')">Logs</div>
1715
+ </nav>
1716
+ </div>
1717
+
1718
+ <div class="toolbar-right">
1719
+ <!-- View Filter Menu -->
1720
+ <select id="proc-filter" class="mac-select" onchange="onFilterChange(this.value)" title="Filter processes">
1721
+ <option value="all">All Processes</option>
1722
+ <option value="my">My Processes</option>
1723
+ <option value="active">Active Processes</option>
1724
+ <option value="system">System Processes</option>
1725
+ </select>
1726
+
1727
+ <!-- Search Input Field -->
1728
+ <div class="mac-search-wrap">
1729
+ <svg class="mac-search-icon" viewBox="0 0 24 24">
1730
+ <path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
1731
+ </svg>
1732
+ <input id="search-input" class="mac-search-input" type="text" placeholder="Search" oninput="onSearchInput(this.value)" autocomplete="off" spellcheck="false"/>
1733
+ <div id="search-clear" class="mac-search-clear" onclick="clearSearch()">\u2715</div>
1734
+ </div>
1735
+
1736
+ <!-- Settings / Preferences Button -->
1737
+ <button id="btn-settings" class="mac-btn mac-btn-icon" title="Preferences (Cmd+,)" onclick="openSettingsModal()">
1738
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor">
1739
+ <path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/>
1740
+ </svg>
1741
+ </button>
1742
+ </div>
1743
+ </header>
1744
+
1745
+ <!-- Main Split View -->
1746
+ <div id="split-view">
1747
+ <!-- Top: Dynamic Process Table & Logs View -->
1748
+ <main id="table-container" tabindex="0">
1749
+ <table class="mac-table" id="proc-table">
1750
+ <thead id="proc-thead">
1751
+ <!-- Columns will be injected based on active tab -->
1752
+ </thead>
1753
+ <tbody id="proc-tbody">
1754
+ <!-- Process rows injected here -->
1755
+ </tbody>
1756
+ </table>
1757
+
1758
+ <!-- Dedicated Logs & Audit Stream Container -->
1759
+ <div id="logs-view">
1760
+ <div class="logs-sub-toolbar">
1761
+ <div class="logs-sub-left">
1762
+ <select id="log-filter-level" class="mac-select" onchange="onLogLevelFilterChange(this.value)" title="Filter log severity">
1763
+ <option value="all">All Severities</option>
1764
+ <option value="INFO">Info</option>
1765
+ <option value="WARN">Warnings</option>
1766
+ <option value="CRIT">Critical</option>
1767
+ <option value="ACTION">User Actions</option>
1768
+ <option value="WATCHDOG">Watchdog</option>
1769
+ </select>
1770
+ <select id="log-filter-subsys" class="mac-select" onchange="onLogSubsysFilterChange(this.value)" title="Filter subsystem">
1771
+ <option value="all">All Subsystems</option>
1772
+ <option value="SYSTEM">System</option>
1773
+ <option value="CPU">CPU</option>
1774
+ <option value="MEM">Memory</option>
1775
+ <option value="DISK">Disk</option>
1776
+ <option value="NET">Network</option>
1777
+ <option value="THERMAL">Thermal</option>
1778
+ <option value="PROCESS">Process</option>
1779
+ <option value="LOGGER">Recorder</option>
1780
+ </select>
1781
+ <div class="mac-search-wrap">
1782
+ <svg class="mac-search-icon" viewBox="0 0 24 24">
1783
+ <path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
1784
+ </svg>
1785
+ <input id="log-search-input" class="mac-search-input" style="width:180px;" type="text" placeholder="Search event logs (L)" oninput="onLogSearchInput(this.value)" autocomplete="off" spellcheck="false"/>
1786
+ </div>
1787
+ </div>
1788
+ <div class="logs-sub-right">
1789
+ <button id="btn-autoscroll" class="mac-btn" onclick="toggleAutoScroll()" style="font-size:11px;color:var(--mac-green);">Auto-scroll: ON</button>
1790
+ <button class="mac-btn" onclick="clearLogs()" style="font-size:11px;">Clear</button>
1791
+ <button class="mac-btn mac-btn-primary" onclick="downloadLogsExport()" style="font-size:11px;">Export Logs JSON</button>
1792
+ </div>
1793
+ </div>
1794
+ <div class="logs-scroll-area" id="logs-scroll-area">
1795
+ <table class="log-table" id="logs-table">
1796
+ <thead>
1797
+ <tr>
1798
+ <th style="width:130px;">Time</th>
1799
+ <th style="width:85px;">Severity</th>
1800
+ <th style="width:90px;">Subsystem</th>
1801
+ <th>Event Message</th>
1802
+ <th style="width:220px;text-align:right;">Context / Telemetry</th>
1803
+ </tr>
1804
+ </thead>
1805
+ <tbody id="logs-tbody">
1806
+ <!-- Dynamic logs inserted here -->
1807
+ </tbody>
1808
+ </table>
1809
+ </div>
1810
+ </div>
1811
+ </main>
1812
+
1813
+ <!-- Horizontal Splitter Divider -->
1814
+ <div id="split-divider"></div>
1815
+
1816
+ <!-- Bottom: Activity Monitor Hardware Panels -->
1817
+ <footer id="bottom-panel">
1818
+ <!-- 1. CPU Panel -->
1819
+ <div class="panel-content active" id="panel-cpu">
1820
+ <div class="pane-stats">
1821
+ <div class="stat-item"><span class="stat-k">% System:</span><span class="stat-v" id="cpu-stat-system">0.0%</span></div>
1822
+ <div class="stat-item"><span class="stat-k">% User:</span><span class="stat-v" id="cpu-stat-user">0.0%</span></div>
1823
+ <div class="stat-item"><span class="stat-k">% Idle:</span><span class="stat-v" id="cpu-stat-idle">100.0%</span></div>
1824
+ <div style="height:4px;border-bottom:1px solid var(--border-color);"></div>
1825
+ <div class="stat-item"><span class="stat-k">Threads:</span><span class="stat-v" id="cpu-stat-threads">0</span></div>
1826
+ <div class="stat-item"><span class="stat-k">Processes:</span><span class="stat-v" id="cpu-stat-procs">0</span></div>
1827
+ <div class="stat-item"><span class="stat-k">Processor:</span><span class="stat-v" id="cpu-stat-brand" style="font-size:10px;">\u2013</span></div>
1828
+ </div>
1829
+ <div class="pane-graph">
1830
+ <div class="graph-header">
1831
+ <span class="graph-title">CPU Load</span>
1832
+ <div class="graph-legend">
1833
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-blue)"></span> User</span>
1834
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-red)"></span> System</span>
1835
+ </div>
1836
+ </div>
1837
+ <canvas class="mac-canvas" id="canvas-cpu"></canvas>
1838
+ </div>
1839
+ </div>
1840
+
1841
+ <!-- 2. Memory Panel -->
1842
+ <div class="panel-content" id="panel-memory">
1843
+ <div class="pane-stats">
1844
+ <div class="stat-item"><span class="stat-k">Physical Memory:</span><span class="stat-v" id="mem-stat-phys">\u2013</span></div>
1845
+ <div class="stat-item"><span class="stat-k">Memory Used:</span><span class="stat-v" id="mem-stat-used">\u2013</span></div>
1846
+ <div class="stat-item"><span class="stat-k">App Memory:</span><span class="stat-v" id="mem-stat-app">\u2013</span></div>
1847
+ <div class="stat-item"><span class="stat-k">Wired Memory:</span><span class="stat-v" id="mem-stat-wired">\u2013</span></div>
1848
+ <div class="stat-item"><span class="stat-k">Compressed:</span><span class="stat-v" id="mem-stat-comp">\u2013</span></div>
1849
+ <div class="stat-item"><span class="stat-k">Cached Files:</span><span class="stat-v" id="mem-stat-cached">\u2013</span></div>
1850
+ <div class="stat-item"><span class="stat-k">Swap Used:</span><span class="stat-v" id="mem-stat-swap">\u2013</span></div>
1851
+ </div>
1852
+ <div class="pane-graph">
1853
+ <div class="graph-header">
1854
+ <span class="graph-title">Memory Pressure</span>
1855
+ <div class="pressure-meter">
1856
+ <div class="pressure-dot" id="pressure-dot"></div>
1857
+ <span class="pressure-text" id="pressure-text">Normal</span>
1858
+ </div>
1859
+ </div>
1860
+ <canvas class="mac-canvas" id="canvas-mem"></canvas>
1861
+ </div>
1862
+ </div>
1863
+
1864
+ <!-- 3. Energy Panel -->
1865
+ <div class="panel-content" id="panel-energy">
1866
+ <div class="pane-stats">
1867
+ <div class="stat-item"><span class="stat-k">Total Power Draw:</span><span class="stat-v" id="energy-stat-total">\u2013</span></div>
1868
+ <div class="stat-item"><span class="stat-k">CPU Package:</span><span class="stat-v" id="energy-stat-cpu">\u2013</span></div>
1869
+ <div class="stat-item"><span class="stat-k">GPU Package:</span><span class="stat-v" id="energy-stat-gpu">\u2013</span></div>
1870
+ <div style="height:4px;border-bottom:1px solid var(--border-color);"></div>
1871
+ <div class="stat-item"><span class="stat-k">Battery Level:</span><span class="stat-v" id="energy-stat-batt">\u2013</span></div>
1872
+ <div class="stat-item"><span class="stat-k">Power Source:</span><span class="stat-v" id="energy-stat-source">\u2013</span></div>
1873
+ <div class="stat-item"><span class="stat-k">Time Remaining:</span><span class="stat-v" id="energy-stat-timerem">\u2013</span></div>
1874
+ <div class="stat-item"><span class="stat-k">Battery Health:</span><span class="stat-v" id="energy-stat-health">\u2013</span></div>
1875
+ </div>
1876
+ <div class="pane-graph">
1877
+ <div class="graph-header">
1878
+ <span class="graph-title">Energy Consumption</span>
1879
+ <div class="graph-legend">
1880
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-yellow)"></span> Watts</span>
1881
+ </div>
1882
+ </div>
1883
+ <canvas class="mac-canvas" id="canvas-energy"></canvas>
1884
+ </div>
1885
+ </div>
1886
+
1887
+ <!-- 4. Disk Panel -->
1888
+ <div class="panel-content" id="panel-disk">
1889
+ <div class="pane-stats">
1890
+ <div class="stat-item"><span class="stat-k">Reads in / sec:</span><span class="stat-v" id="disk-stat-reads">\u2013</span></div>
1891
+ <div class="stat-item"><span class="stat-k">Writes out / sec:</span><span class="stat-v" id="disk-stat-writes">\u2013</span></div>
1892
+ <div class="stat-item"><span class="stat-k">Data read:</span><span class="stat-v" id="disk-stat-total-read">\u2013</span></div>
1893
+ <div class="stat-item"><span class="stat-k">Data written:</span><span class="stat-v" id="disk-stat-total-write">\u2013</span></div>
1894
+ <div style="height:4px;border-bottom:1px solid var(--border-color);"></div>
1895
+ <div id="disk-mounts-box" style="display:flex;flex-direction:column;gap:5px;"></div>
1896
+ </div>
1897
+ <div class="pane-graph">
1898
+ <div class="graph-header">
1899
+ <span class="graph-title">Disk Activity</span>
1900
+ <div class="graph-legend">
1901
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-teal)"></span> Read/sec</span>
1902
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-red)"></span> Write/sec</span>
1903
+ </div>
1904
+ </div>
1905
+ <canvas class="mac-canvas" id="canvas-disk"></canvas>
1906
+ </div>
1907
+ </div>
1908
+
1909
+ <!-- 5. Network Panel -->
1910
+ <div class="panel-content" id="panel-network">
1911
+ <div class="pane-stats">
1912
+ <div class="stat-item"><span class="stat-k">Packets in / sec:</span><span class="stat-v" id="net-stat-pkts-in">\u2013</span></div>
1913
+ <div class="stat-item"><span class="stat-k">Packets out / sec:</span><span class="stat-v" id="net-stat-pkts-out">\u2013</span></div>
1914
+ <div class="stat-item"><span class="stat-k">Data received / sec:</span><span class="stat-v" id="net-stat-rate-in">\u2013</span></div>
1915
+ <div class="stat-item"><span class="stat-k">Data sent / sec:</span><span class="stat-v" id="net-stat-rate-out">\u2013</span></div>
1916
+ <div style="height:4px;border-bottom:1px solid var(--border-color);"></div>
1917
+ <div class="stat-item"><span class="stat-k">Data received:</span><span class="stat-v" id="net-stat-total-in">\u2013</span></div>
1918
+ <div class="stat-item"><span class="stat-k">Data sent:</span><span class="stat-v" id="net-stat-total-out">\u2013</span></div>
1919
+ <div class="stat-item"><span class="stat-k">Interface / IP:</span><span class="stat-v" id="net-stat-iface" style="font-size:10px;">\u2013</span></div>
1920
+ </div>
1921
+ <div class="pane-graph">
1922
+ <div class="graph-header">
1923
+ <span class="graph-title">Network Throughput</span>
1924
+ <div class="graph-legend">
1925
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-blue)"></span> In / sec</span>
1926
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-red)"></span> Out / sec</span>
1927
+ </div>
1928
+ </div>
1929
+ <canvas class="mac-canvas" id="canvas-net"></canvas>
1930
+ </div>
1931
+ </div>
1932
+
1933
+ <!-- 6. GPU Panel -->
1934
+ <div class="panel-content" id="panel-gpu">
1935
+ <div class="pane-stats">
1936
+ <div class="stat-item"><span class="stat-k">GPU Processor:</span><span class="stat-v" id="gpu-stat-model" style="font-size:10px;">\u2013</span></div>
1937
+ <div class="stat-item"><span class="stat-k">Utilization:</span><span class="stat-v" id="gpu-stat-util">\u2013</span></div>
1938
+ <div class="stat-item"><span class="stat-k">VRAM Used:</span><span class="stat-v" id="gpu-stat-vram">\u2013</span></div>
1939
+ <div class="stat-item"><span class="stat-k">Active Processes:</span><span class="stat-v" id="gpu-stat-procs">\u2013</span></div>
1940
+ <div class="stat-item"><span class="stat-k">Temperature:</span><span class="stat-v" id="gpu-stat-temp">\u2013</span></div>
1941
+ </div>
1942
+ <div class="pane-graph">
1943
+ <div class="graph-header">
1944
+ <span class="graph-title">GPU Utilization</span>
1945
+ <div class="graph-legend">
1946
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-purple)"></span> GPU %</span>
1947
+ </div>
1948
+ </div>
1949
+ <canvas class="mac-canvas" id="canvas-gpu"></canvas>
1950
+ </div>
1951
+ </div>
1952
+
1953
+ <!-- 7. Logs Panel (Recording Session & History Explorer) -->
1954
+ <div class="panel-content" id="panel-logs">
1955
+ <div class="pane-stats" style="flex:1.2;">
1956
+ <div style="font-weight:700;margin-bottom:4px;color:var(--text-primary);display:flex;align-items:center;gap:6px;">
1957
+ <span>Session CSV Recorder</span>
1958
+ <span id="logs-rec-badge" class="log-badge info">IDLE</span>
1959
+ </div>
1960
+ <div class="stat-item"><span class="stat-k">Status:</span><span class="stat-v" id="rec-stat-status">Idle</span></div>
1961
+ <div class="stat-item"><span class="stat-k">Active File:</span><span class="stat-v" id="rec-stat-file" style="font-size:10px;word-break:break-all;">\u2013</span></div>
1962
+ <div class="stat-item"><span class="stat-k">Samples:</span><span class="stat-v" id="rec-stat-samples">0</span></div>
1963
+ <div class="stat-item"><span class="stat-k">Duration:</span><span class="stat-v" id="rec-stat-duration">0s</span></div>
1964
+ <div class="stat-item"><span class="stat-k">Export Dir:</span><span class="stat-v" id="rec-stat-dir" style="font-size:10px;">./pyre-exports</span></div>
1965
+ <div style="display:flex;gap:8px;margin-top:6px;">
1966
+ <button class="mac-btn mac-btn-primary" id="btn-panel-rec" onclick="toggleWebRecording()" style="height:24px;font-size:11px;">Start Recording</button>
1967
+ <button class="mac-btn" onclick="fetchLogHistory()" style="height:24px;font-size:11px;">Refresh History</button>
1968
+ </div>
1969
+ </div>
1970
+ <div class="pane-graph" style="flex:2;display:flex;flex-direction:column;padding:8px 12px;overflow-y:auto;">
1971
+ <div class="graph-header" style="margin-bottom:6px;">
1972
+ <span class="graph-title">Saved CSV Logs on Disk</span>
1973
+ <span style="font-size:10px;color:var(--text-tertiary);">Replay in CLI: <code>pyre replay &lt;file&gt;</code></span>
1974
+ </div>
1975
+ <div id="history-list-box" style="flex:1;overflow-y:auto;">
1976
+ <table class="history-table">
1977
+ <thead>
1978
+ <tr><th>Filename</th><th>Size</th><th>Recorded At</th><th>Action</th></tr>
1979
+ </thead>
1980
+ <tbody id="history-tbody">
1981
+ <tr><td colspan="4" style="color:var(--text-tertiary);text-align:center;padding:12px;">Scanning ./pyre-exports...</td></tr>
1982
+ </tbody>
1983
+ </table>
1984
+ </div>
1985
+ </div>
1986
+ </div>
1987
+ </footer>
1988
+ </div>
1989
+
1990
+ <!-- macOS Window Status Bar -->
1991
+ <div id="statusbar">
1992
+ <div class="sb-left">
1993
+ <div class="sb-live-indicator">
1994
+ <span class="sb-live-dot" id="sb-dot"></span>
1995
+ <span id="sb-status">Connecting</span>
1996
+ </div>
1997
+ <span id="sb-host">\u2013</span>
1998
+ </div>
1999
+ <div class="sb-right">
2000
+ <span id="sb-count">0 processes</span>
2001
+ <span>Updated <span id="sb-time">\u2013</span></span>
2002
+ </div>
2003
+ </div>
2004
+
2005
+ <!-- Modal 1: Force Quit Confirmation Sheet -->
2006
+ <div class="modal-backdrop" id="modal-quit" onclick="if(event.target===this)closeModal('modal-quit')">
2007
+ <div class="mac-sheet modal-quit-box">
2008
+ <div class="quit-icon-wrap">
2009
+ <svg width="54" height="54" viewBox="0 0 24 24" fill="var(--mac-red)">
2010
+ <path d="M7.86 2h8.28L22 7.86v8.28L16.14 22H7.86L2 16.14V7.86L7.86 2zm1.41 2L4 8.27v7.46L8.27 20h7.46L20 15.73V8.27L15.73 4H9.27zM12 10.59l3.3-3.3 1.41 1.42L13.41 12l3.3 3.29-1.41 1.42L12 13.41l-3.29 3.3-1.42-1.42L10.59 12l-3.3-3.29 1.42-1.42L12 10.59z"/>
2011
+ </svg>
2012
+ </div>
2013
+ <div class="quit-title">Do you want to quit this process?</div>
2014
+ <div class="quit-desc" id="quit-modal-desc">
2015
+ Quitting process may cause unsaved changes to be lost.
2016
+ </div>
2017
+ <div style="display:flex;justify-content:center;gap:10px;">
2018
+ <button class="mac-btn" onclick="closeModal('modal-quit')">Cancel</button>
2019
+ <button class="mac-btn mac-btn-danger" id="btn-do-force-quit" onclick="executeQuit(true)">Force Quit</button>
2020
+ <button class="mac-btn mac-btn-primary" id="btn-do-quit" onclick="executeQuit(false)">Quit</button>
2021
+ </div>
2022
+ </div>
2023
+ </div>
2024
+
2025
+ <!-- Modal 2: Process Inspector Sheet -->
2026
+ <div class="modal-backdrop" id="modal-inspector" onclick="if(event.target===this)closeModal('modal-inspector')">
2027
+ <div class="mac-sheet modal-inspect-box">
2028
+ <div class="sheet-header">
2029
+ <span class="sheet-title" id="inspect-title">Process Inspector</span>
2030
+ <button class="sheet-close-btn" onclick="closeModal('modal-inspector')">\u2715</button>
2031
+ </div>
2032
+ <div class="sheet-body" id="inspect-body">
2033
+ <!-- Process details injected here -->
2034
+ </div>
2035
+ <div class="sheet-footer">
2036
+ <button class="mac-btn" onclick="closeModal('modal-inspector')">Close</button>
2037
+ <button class="mac-btn mac-btn-danger" onclick="confirmQuitFromInspector()">Quit Process\u2026</button>
2038
+ </div>
2039
+ </div>
2040
+ </div>
2041
+
2042
+ <!-- Modal 3: Settings / Preferences Sheet -->
2043
+ <div class="modal-backdrop" id="modal-settings" onclick="if(event.target===this)closeSettingsModal()">
2044
+ <div class="mac-sheet modal-settings-box">
2045
+ <div class="sheet-header">
2046
+ <span class="sheet-title">Pyre Preferences</span>
2047
+ <button class="sheet-close-btn" onclick="closeSettingsModal()">\u2715</button>
2048
+ </div>
2049
+
2050
+ <!-- Tab navigation -->
2051
+ <div class="settings-tabs">
2052
+ <button class="settings-tab-btn active" data-stab="general" onclick="switchSettingsTab('general')">General</button>
2053
+ <button class="settings-tab-btn" data-stab="autostart" onclick="switchSettingsTab('autostart')">Start on Boot</button>
2054
+ <button class="settings-tab-btn" data-stab="alerts" onclick="switchSettingsTab('alerts')">Alerts & Watchdog</button>
2055
+ <button class="settings-tab-btn" data-stab="appearance" onclick="switchSettingsTab('appearance')">Appearance</button>
2056
+ <button class="settings-tab-btn" data-stab="diagnostics" onclick="switchSettingsTab('diagnostics')">Diagnostics</button>
2057
+ </div>
2058
+
2059
+ <div class="settings-content-wrap">
2060
+ <!-- 1. General Pane -->
2061
+ <div class="settings-pane active" id="spane-general">
2062
+ <div class="settings-card">
2063
+ <div class="settings-card-title">Telemetry Update Frequency</div>
2064
+ <div class="settings-item">
2065
+ <div class="settings-label-wrap">
2066
+ <span class="settings-label">Update Interval</span>
2067
+ <span class="settings-sublabel">How frequently Pyre collects and refreshes hardware & process statistics</span>
2068
+ </div>
2069
+ <div class="settings-control">
2070
+ <select id="set-interval" class="mac-select" onchange="onSettingChange()">
2071
+ <option value="0.5">0.5s (Turbo High Performance)</option>
2072
+ <option value="1">1.0s (Fast Gaming/Dev)</option>
2073
+ <option value="2">2.0s (Default Balanced)</option>
2074
+ <option value="5">5.0s (Low Energy)</option>
2075
+ <option value="10">10.0s (Battery Saver)</option>
2076
+ </select>
2077
+ </div>
2078
+ </div>
2079
+ </div>
2080
+
2081
+ <div class="settings-card">
2082
+ <div class="settings-card-title">Process Table Defaults</div>
2083
+ <div class="settings-item">
2084
+ <div class="settings-label-wrap">
2085
+ <span class="settings-label">Default Process Filter</span>
2086
+ <span class="settings-sublabel">Initial process filter when dashboard opens</span>
2087
+ </div>
2088
+ <div class="settings-control">
2089
+ <select id="set-filter" class="mac-select" onchange="onSettingChange()">
2090
+ <option value="all">All Processes</option>
2091
+ <option value="my">My Processes</option>
2092
+ <option value="active">Active Processes Only</option>
2093
+ <option value="system">System Daemons Only</option>
2094
+ </select>
2095
+ </div>
2096
+ </div>
2097
+ <div class="settings-item">
2098
+ <div class="settings-label-wrap">
2099
+ <span class="settings-label">Default Sort Column</span>
2100
+ <span class="settings-sublabel">Default metric used to sort processes on launch</span>
2101
+ </div>
2102
+ <div class="settings-control">
2103
+ <select id="set-sort" class="mac-select" onchange="onSettingChange()">
2104
+ <option value="cpu">% CPU</option>
2105
+ <option value="mem">Memory Usage</option>
2106
+ <option value="runtime">CPU Runtime</option>
2107
+ <option value="threads">Thread Count</option>
2108
+ </select>
2109
+ </div>
2110
+ </div>
2111
+ </div>
2112
+
2113
+ <div class="settings-card">
2114
+ <div class="settings-card-title">Window & Launch Behavior</div>
2115
+ <div class="settings-item">
2116
+ <div class="settings-label-wrap">
2117
+ <span class="settings-label">Launch in Compact Widget Mode</span>
2118
+ <span class="settings-sublabel">Opens a compact floating HUD instead of the full activity window</span>
2119
+ </div>
2120
+ <div class="settings-control">
2121
+ <label class="mac-switch">
2122
+ <input type="checkbox" id="set-compact" onchange="onSettingChange()">
2123
+ <span class="mac-slider"></span>
2124
+ </label>
2125
+ </div>
2126
+ </div>
2127
+ <div class="settings-item">
2128
+ <div class="settings-label-wrap">
2129
+ <span class="settings-label">Keep Window Always on Top</span>
2130
+ <span class="settings-sublabel">Floats the dashboard window above all other full-screen apps</span>
2131
+ </div>
2132
+ <div class="settings-control">
2133
+ <label class="mac-switch">
2134
+ <input type="checkbox" id="set-ontop" onchange="onSettingChange()">
2135
+ <span class="mac-slider"></span>
2136
+ </label>
2137
+ </div>
2138
+ </div>
2139
+ </div>
2140
+ </div>
2141
+
2142
+ <!-- 2. Start on Boot Pane -->
2143
+ <div class="settings-pane" id="spane-autostart">
2144
+ <div class="settings-card">
2145
+ <div class="settings-card-title">Background Telemetry Service (Start on Boot)</div>
2146
+ <div class="settings-item">
2147
+ <div class="settings-label-wrap">
2148
+ <span class="settings-label">Start Pyre Telemetry on System Startup</span>
2149
+ <span class="settings-sublabel">Installs a native user-level LaunchAgent (macOS) or systemd user service (Linux)</span>
2150
+ </div>
2151
+ <div class="settings-control">
2152
+ <label class="mac-switch">
2153
+ <input type="checkbox" id="set-autostart" onchange="toggleAutostartFromUI(this.checked)">
2154
+ <span class="mac-slider"></span>
2155
+ </label>
2156
+ </div>
2157
+ </div>
2158
+ <div style="font-size:11px;background:#1a1a1d;border:1px solid var(--border-color);border-radius:6px;padding:10px;display:flex;flex-direction:column;gap:6px;">
2159
+ <div style="display:flex;justify-content:space-between;align-items:center;">
2160
+ <span style="color:var(--text-secondary);">Service Status:</span>
2161
+ <span id="svc-status-pill" class="status-pill inactive">Checking\u2026</span>
2162
+ </div>
2163
+ <div style="display:flex;justify-content:space-between;">
2164
+ <span style="color:var(--text-secondary);">Service Identifier:</span>
2165
+ <span id="svc-label" style="font-family:var(--font-mono);font-size:10px;">com.pyre.web</span>
2166
+ </div>
2167
+ <div style="display:flex;justify-content:space-between;word-break:break-all;">
2168
+ <span style="color:var(--text-secondary);">Agent Path:</span>
2169
+ <span id="svc-path" style="font-family:var(--font-mono);font-size:10px;">~/Library/LaunchAgents/com.pyre.web.plist</span>
2170
+ </div>
2171
+ <div style="display:flex;justify-content:space-between;word-break:break-all;">
2172
+ <span style="color:var(--text-secondary);">Log File:</span>
2173
+ <span id="svc-log" style="font-family:var(--font-mono);font-size:10px;">~/.config/pyre/web.log</span>
2174
+ </div>
2175
+ </div>
2176
+ </div>
2177
+
2178
+ <div class="settings-card">
2179
+ <div class="settings-card-title">Network & Remote Access</div>
2180
+ <div class="settings-item">
2181
+ <div class="settings-label-wrap">
2182
+ <span class="settings-label">Expose to Local Network (Wi-Fi / LAN)</span>
2183
+ <span class="settings-sublabel">When enabled, Pyre dashboard is reachable from other devices on your LAN</span>
2184
+ </div>
2185
+ <div class="settings-control">
2186
+ <label class="mac-switch">
2187
+ <input type="checkbox" id="set-lan" onchange="onSettingChange()">
2188
+ <span class="mac-slider"></span>
2189
+ </label>
2190
+ </div>
2191
+ </div>
2192
+ <div class="settings-item">
2193
+ <div class="settings-label-wrap">
2194
+ <span class="settings-label">Default Web Server Port</span>
2195
+ <span class="settings-sublabel">Port used when running pyre web or background service</span>
2196
+ </div>
2197
+ <div class="settings-control">
2198
+ <input id="set-port" type="number" class="settings-input" style="width:75px;" value="3000" min="1024" max="65535" onchange="onSettingChange()">
2199
+ </div>
2200
+ </div>
2201
+ </div>
2202
+ </div>
2203
+
2204
+ <!-- 3. Alerts & Watchdog Pane -->
2205
+ <div class="settings-pane" id="spane-alerts">
2206
+ <div class="settings-card">
2207
+ <div class="settings-card-title">Resource Threshold Triggers</div>
2208
+ <div class="settings-item">
2209
+ <div class="settings-label-wrap">
2210
+ <span class="settings-label">CPU Alert Threshold (%)</span>
2211
+ <span class="settings-sublabel">Trigger alert when system or process CPU exceeds this percentage</span>
2212
+ </div>
2213
+ <div class="settings-control">
2214
+ <input id="set-cpu-alert" type="number" class="settings-input" style="width:70px;" min="50" max="100" value="90" onchange="onSettingChange()">
2215
+ <span style="color:var(--text-secondary);font-size:11px;">%</span>
2216
+ </div>
2217
+ </div>
2218
+ <div class="settings-item">
2219
+ <div class="settings-label-wrap">
2220
+ <span class="settings-label">Temperature Alert Threshold (\xB0C)</span>
2221
+ <span class="settings-sublabel">Trigger warning when SoC/CPU temperature exceeds this limit</span>
2222
+ </div>
2223
+ <div class="settings-control">
2224
+ <input id="set-temp-alert" type="number" class="settings-input" style="width:70px;" min="60" max="110" value="95" onchange="onSettingChange()">
2225
+ <span style="color:var(--text-secondary);font-size:11px;">\xB0C</span>
2226
+ </div>
2227
+ </div>
2228
+ </div>
2229
+
2230
+ <div class="settings-card">
2231
+ <div class="settings-card-title">Autonomous Process Watchdog</div>
2232
+ <div class="settings-item">
2233
+ <div class="settings-label-wrap">
2234
+ <span class="settings-label">Target Process Name Filter</span>
2235
+ <span class="settings-sublabel">Leave blank to monitor all processes, or specify e.g. "node", "chrome", "python"</span>
2236
+ </div>
2237
+ <div class="settings-control">
2238
+ <input id="set-watchdog-proc" type="text" class="settings-input" style="width:140px;" placeholder="All processes" onchange="onSettingChange()">
2239
+ </div>
2240
+ </div>
2241
+ <div class="settings-item">
2242
+ <div class="settings-label-wrap">
2243
+ <span class="settings-label">Runaway Memory Limit (%)</span>
2244
+ <span class="settings-sublabel">Threshold to flag or throttle memory-leaking tasks</span>
2245
+ </div>
2246
+ <div class="settings-control">
2247
+ <input id="set-watchdog-mem" type="number" class="settings-input" style="width:70px;" min="10" max="95" value="80" onchange="onSettingChange()">
2248
+ <span style="color:var(--text-secondary);font-size:11px;">%</span>
2249
+ </div>
2250
+ </div>
2251
+ </div>
2252
+
2253
+ <div class="settings-card">
2254
+ <div class="settings-card-title">Notification Channels & Webhooks</div>
2255
+ <div class="settings-item">
2256
+ <div class="settings-label-wrap">
2257
+ <span class="settings-label">Slack Webhook URL</span>
2258
+ <span class="settings-sublabel">https://hooks.slack.com/services/...</span>
2259
+ </div>
2260
+ <input id="set-slack-url" type="text" class="settings-input" style="width:230px;" placeholder="Optional Slack webhook" onchange="onSettingChange()">
2261
+ </div>
2262
+ <div class="settings-item">
2263
+ <div class="settings-label-wrap">
2264
+ <span class="settings-label">Discord Webhook URL</span>
2265
+ <span class="settings-sublabel">https://discord.com/api/webhooks/...</span>
2266
+ </div>
2267
+ <input id="set-discord-url" type="text" class="settings-input" style="width:230px;" placeholder="Optional Discord webhook" onchange="onSettingChange()">
2268
+ </div>
2269
+ <div class="settings-item">
2270
+ <div class="settings-label-wrap">
2271
+ <span class="settings-label">ntfy.sh Topic URL</span>
2272
+ <span class="settings-sublabel">https://ntfy.sh/my-topic</span>
2273
+ </div>
2274
+ <input id="set-ntfy-url" type="text" class="settings-input" style="width:230px;" placeholder="Optional ntfy URL" onchange="onSettingChange()">
2275
+ </div>
2276
+ <div style="display:flex;justify-content:flex-end;margin-top:4px;">
2277
+ <button class="mac-btn mac-btn-primary" onclick="sendTestAlertFromUI()">Dispatch Test Notification</button>
2278
+ </div>
2279
+ </div>
2280
+ </div>
2281
+
2282
+ <!-- 4. Appearance Pane -->
2283
+ <div class="settings-pane" id="spane-appearance">
2284
+ <div class="settings-card">
2285
+ <div class="settings-card-title">Hardware Telemetry Graphs</div>
2286
+ <div class="settings-item">
2287
+ <div class="settings-label-wrap">
2288
+ <span class="settings-label">Rolling History Window</span>
2289
+ <span class="settings-sublabel">Amount of history preserved in CPU, GPU, Network, and Disk graphs</span>
2290
+ </div>
2291
+ <div class="settings-control">
2292
+ <select id="set-history-len" class="mac-select" onchange="onSettingChange()">
2293
+ <option value="30">30 seconds (Compact)</option>
2294
+ <option value="60">60 seconds (Standard)</option>
2295
+ <option value="120">120 seconds (2 Minutes)</option>
2296
+ <option value="300">300 seconds (5 Minutes Long Run)</option>
2297
+ </select>
2298
+ </div>
2299
+ </div>
2300
+ <div class="settings-item">
2301
+ <div class="settings-label-wrap">
2302
+ <span class="settings-label">Animate Live Waveform Gradients</span>
2303
+ <span class="settings-sublabel">Renders smooth neon area fills under live telemetry curves</span>
2304
+ </div>
2305
+ <div class="settings-control">
2306
+ <label class="mac-switch">
2307
+ <input type="checkbox" id="set-show-graphs" checked onchange="onSettingChange()">
2308
+ <span class="mac-slider"></span>
2309
+ </label>
2310
+ </div>
2311
+ </div>
2312
+ </div>
2313
+
2314
+ <div class="settings-card">
2315
+ <div class="settings-card-title">Process Table Row Density</div>
2316
+ <div class="settings-item">
2317
+ <div class="settings-label-wrap">
2318
+ <span class="settings-label">Table Row Density</span>
2319
+ <span class="settings-sublabel">Adjust row density for higher information density</span>
2320
+ </div>
2321
+ <div class="settings-control">
2322
+ <select id="set-density" class="mac-select" onchange="onDensityChange(this.value)">
2323
+ <option value="compact">Compact (Activity Monitor classic)</option>
2324
+ <option value="standard" selected>Standard (Balanced)</option>
2325
+ <option value="comfortable">Comfortable (Larger text & touch)</option>
2326
+ </select>
2327
+ </div>
2328
+ </div>
2329
+ </div>
2330
+ </div>
2331
+
2332
+ <!-- 5. Diagnostics Pane -->
2333
+ <div class="settings-pane" id="spane-diagnostics">
2334
+ <div class="settings-card">
2335
+ <div class="settings-card-title">System Information</div>
2336
+ <div id="diag-sys-info" style="font-size:11px;color:var(--text-secondary);display:flex;flex-direction:column;gap:5px;">
2337
+ <div>Loading system specifications\u2026</div>
2338
+ </div>
2339
+ </div>
2340
+
2341
+ <div class="settings-card">
2342
+ <div class="settings-card-title">System Diagnostics (pyre doctor)</div>
2343
+ <p style="font-size:11px;color:var(--text-secondary);margin:0;">Run an automated health check covering kernel access, SMC thermal sensors, battery diagnostics, permissions, and daemon connectivity.</p>
2344
+ <div style="display:flex;gap:8px;margin-top:4px;">
2345
+ <button class="mac-btn mac-btn-primary" id="btn-run-doctor" onclick="runDoctorFromUI()">Run Diagnostics</button>
2346
+ </div>
2347
+ <div id="doctor-results" style="display:none;background:#151518;border:1px solid var(--border-color);border-radius:6px;padding:10px;font-family:var(--font-mono);font-size:11px;max-height:160px;overflow-y:auto;white-space:pre-wrap;"></div>
2348
+ </div>
2349
+
2350
+ <div class="settings-card" style="border-color:rgba(255,69,58,0.3);">
2351
+ <div class="settings-card-title" style="color:var(--mac-red);">Reset Configuration</div>
2352
+ <div class="settings-item">
2353
+ <div class="settings-label-wrap">
2354
+ <span class="settings-label">Reset Preferences to Factory Defaults</span>
2355
+ <span class="settings-sublabel">Clears ~/.config/pyre/config.json back to original out-of-the-box defaults</span>
2356
+ </div>
2357
+ <button class="mac-btn mac-btn-danger" onclick="resetConfigFromUI()">Reset All Settings</button>
2358
+ </div>
2359
+ </div>
2360
+ </div>
2361
+ </div>
2362
+
2363
+ <div class="sheet-footer" style="display:flex;justify-content:space-between;align-items:center;background:#252528;border-top:1px solid var(--border-color);padding:10px 16px;">
2364
+ <span id="settings-status-hint" style="font-size:11px;color:var(--text-tertiary);">Preferences saved to ~/.config/pyre/config.json</span>
2365
+ <div style="display:flex;gap:8px;">
2366
+ <button class="mac-btn" onclick="closeSettingsModal()">Done</button>
2367
+ </div>
2368
+ </div>
2369
+ </div>
2370
+ </div>
2371
+
2372
+ <!-- Toast notification -->
2373
+ <div id="toast">Action completed</div>
2374
+
2375
+ <script>
2376
+ // State Management
2377
+ let currentTab = 'cpu';
2378
+ let latestData = null;
2379
+ let selectedPid = null;
2380
+ let currentFilter = 'all';
2381
+ let searchQuery = '';
2382
+
2383
+ // Logs & Audit State
2384
+ let logsBuffer = [];
2385
+ let logFilterLevel = 'all';
2386
+ let logFilterSubsys = 'all';
2387
+ let logSearchQuery = '';
2388
+ let autoScrollLogs = true;
2389
+ let isRecording = false;
2390
+ let hasLoggedInit = false;
2391
+ let prevThermalState = null;
2392
+ let lastCpuSpikeTimes = {};
2393
+ let lastHighMemAlert = 0;
2394
+ let lastNetBurstAlert = 0;
2395
+
2396
+ // Sort State per tab
2397
+ const sortState = {
2398
+ cpu: { col: 'cpu', dir: 'desc' },
2399
+ memory: { col: 'mem', dir: 'desc' },
2400
+ energy: { col: 'cpu', dir: 'desc' },
2401
+ disk: { col: 'cpu', dir: 'desc' },
2402
+ network: { col: 'cpu', dir: 'desc' },
2403
+ gpu: { col: 'cpu', dir: 'desc' }
2404
+ };
2405
+
2406
+ // Rolling Metrics History
2407
+ const MAX_POINTS = 60;
2408
+ const historyData = {
2409
+ cpuUser: [],
2410
+ cpuSystem: [],
2411
+ memPressure: [],
2412
+ energyWatts: [],
2413
+ diskRead: [],
2414
+ diskWrite: [],
2415
+ netIn: [],
2416
+ netOut: [],
2417
+ gpuUtil: []
2418
+ };
2419
+
2420
+ // Utilities
2421
+ function pushHistory(arr, val) {
2422
+ arr.push(typeof val === 'number' && !isNaN(val) ? val : 0);
2423
+ if (arr.length > MAX_POINTS) arr.shift();
2424
+ }
2425
+
2426
+ function fmtBytes(b, dec = 1) {
2427
+ if (b === undefined || b === null || isNaN(b) || b === 0) return '0 B';
2428
+ const k = 1024;
2429
+ const s = ['B', 'KB', 'MB', 'GB', 'TB'];
2430
+ const i = Math.floor(Math.log(Math.abs(b)) / Math.log(k));
2431
+ if (i < 0) return '0 B';
2432
+ const v = b / Math.pow(k, i);
2433
+ return (parseFloat(v.toFixed(dec))) + ' ' + (s[i] || 'TB');
2434
+ }
2435
+
2436
+ function fmtTime(sec) {
2437
+ if (!sec) return '0:00.00';
2438
+ const m = Math.floor(sec / 60);
2439
+ const s = Math.floor(sec % 60);
2440
+ const ms = Math.floor((sec % 1) * 100);
2441
+ return m + ':' + (s < 10 ? '0' : '') + s + '.' + (ms < 10 ? '0' : '') + ms;
2442
+ }
2443
+
2444
+ function escapeHtml(s) {
2445
+ return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
2446
+ }
2447
+
2448
+ function showToast(msg) {
2449
+ const t = document.getElementById('toast');
2450
+ t.textContent = msg;
2451
+ t.classList.add('show');
2452
+ setTimeout(() => t.classList.remove('show'), 2600);
2453
+ }
2454
+
2455
+ // Tab Column Schemas
2456
+ const tabColumns = {
2457
+ cpu: [
2458
+ { id: 'command', name: 'Process Name', width: '38%', align: 'left' },
2459
+ { id: 'cpu', name: '% CPU', width: '12%', align: 'right' },
2460
+ { id: 'runtime', name: 'CPU Time', width: '14%', align: 'right' },
2461
+ { id: 'threads', name: 'Threads', width: '10%', align: 'right' },
2462
+ { id: 'state', name: 'State', width: '8%', align: 'center' },
2463
+ { id: 'pid', name: 'PID', width: '8%', align: 'right' },
2464
+ { id: 'user', name: 'User', width: '10%', align: 'left' }
2465
+ ],
2466
+ memory: [
2467
+ { id: 'command', name: 'Process Name', width: '38%', align: 'left' },
2468
+ { id: 'memBytes', name: 'Memory', width: '15%', align: 'right' },
2469
+ { id: 'mem', name: '% Memory', width: '11%', align: 'right' },
2470
+ { id: 'threads', name: 'Threads', width: '10%', align: 'right' },
2471
+ { id: 'state', name: 'State', width: '8%', align: 'center' },
2472
+ { id: 'pid', name: 'PID', width: '8%', align: 'right' },
2473
+ { id: 'user', name: 'User', width: '10%', align: 'left' }
2474
+ ],
2475
+ energy: [
2476
+ { id: 'command', name: 'Process Name', width: '42%', align: 'left' },
2477
+ { id: 'energyImpact', name: 'Energy Impact', width: '16%', align: 'right' },
2478
+ { id: 'cpu', name: '% CPU', width: '12%', align: 'right' },
2479
+ { id: 'threads', name: 'Threads', width: '10%', align: 'right' },
2480
+ { id: 'pid', name: 'PID', width: '10%', align: 'right' },
2481
+ { id: 'user', name: 'User', width: '10%', align: 'left' }
2482
+ ],
2483
+ disk: [
2484
+ { id: 'command', name: 'Process Name', width: '44%', align: 'left' },
2485
+ { id: 'cpu', name: '% CPU', width: '12%', align: 'right' },
2486
+ { id: 'state', name: 'State', width: '12%', align: 'center' },
2487
+ { id: 'threads', name: 'Threads', width: '10%', align: 'right' },
2488
+ { id: 'pid', name: 'PID', width: '10%', align: 'right' },
2489
+ { id: 'user', name: 'User', width: '12%', align: 'left' }
2490
+ ],
2491
+ network: [
2492
+ { id: 'command', name: 'Process Name', width: '38%', align: 'left' },
2493
+ { id: 'netRx', name: 'Sent Bytes', width: '13%', align: 'right' },
2494
+ { id: 'netTx', name: 'Rcvd Bytes', width: '13%', align: 'right' },
2495
+ { id: 'cpu', name: '% CPU', width: '10%', align: 'right' },
2496
+ { id: 'threads', name: 'Threads', width: '10%', align: 'right' },
2497
+ { id: 'pid', name: 'PID', width: '8%', align: 'right' },
2498
+ { id: 'user', name: 'User', width: '8%', align: 'left' }
2499
+ ],
2500
+ gpu: [
2501
+ { id: 'command', name: 'Process Name', width: '42%', align: 'left' },
2502
+ { id: 'cpu', name: '% GPU/CPU', width: '16%', align: 'right' },
2503
+ { id: 'mem', name: '% Memory', width: '14%', align: 'right' },
2504
+ { id: 'threads', name: 'Threads', width: '10%', align: 'right' },
2505
+ { id: 'pid', name: 'PID', width: '8%', align: 'right' },
2506
+ { id: 'user', name: 'User', width: '10%', align: 'left' }
2507
+ ]
2508
+ };
2509
+
2510
+ // Render Table Header
2511
+ function renderTableHeader() {
2512
+ const thead = document.getElementById('proc-thead');
2513
+ const cols = tabColumns[currentTab] || tabColumns.cpu;
2514
+ const curSort = sortState[currentTab];
2515
+
2516
+ let html = '<tr>';
2517
+ cols.forEach(c => {
2518
+ const isSorted = curSort.col === c.id;
2519
+ const caret = isSorted ? (curSort.dir === 'asc' ? ' \u25B2' : ' \u25BC') : '';
2520
+ html += \`<th style="width:\${c.width};text-align:\${c.align}" onclick="handleSort('\${c.id}')">\${escapeHtml(c.name)}<span class="sort-caret">\${caret}</span></th>\`;
2521
+ });
2522
+ html += '</tr>';
2523
+ thead.innerHTML = html;
2524
+ }
2525
+
2526
+ // Sort Handler
2527
+ function handleSort(colId) {
2528
+ const cur = sortState[currentTab];
2529
+ if (cur.col === colId) {
2530
+ cur.dir = cur.dir === 'asc' ? 'desc' : 'asc';
2531
+ } else {
2532
+ cur.col = colId;
2533
+ cur.dir = (colId === 'command' || colId === 'user') ? 'asc' : 'desc';
2534
+ }
2535
+ renderTableHeader();
2536
+ renderTableRows();
2537
+ }
2538
+
2539
+ // Tab Switching
2540
+ function switchTab(tab) {
2541
+ currentTab = tab;
2542
+ document.querySelectorAll('.mac-seg-item').forEach(el => {
2543
+ el.classList.toggle('active', el.dataset.tab === tab);
2544
+ });
2545
+ document.querySelectorAll('.panel-content').forEach(el => {
2546
+ el.classList.toggle('active', el.id === 'panel-' + tab);
2547
+ });
2548
+
2549
+ // Logs tab: show logs-view, hide proc-table; otherwise reverse
2550
+ const procTable = document.getElementById('proc-table');
2551
+ const logsView = document.getElementById('logs-view');
2552
+ const procFilter = document.getElementById('proc-filter');
2553
+ const searchWrap = document.querySelector('.mac-search-wrap');
2554
+ const btnStop = document.getElementById('btn-stop');
2555
+ const btnInspect = document.getElementById('btn-inspect');
2556
+ if (tab === 'logs') {
2557
+ if (procTable) procTable.style.display = 'none';
2558
+ if (logsView) logsView.style.display = 'flex';
2559
+ if (procFilter) procFilter.style.display = 'none';
2560
+ if (searchWrap) searchWrap.style.display = 'none';
2561
+ if (btnStop) btnStop.style.display = 'none';
2562
+ if (btnInspect) btnInspect.style.display = 'none';
2563
+ renderLogs();
2564
+ fetchLogHistory();
2565
+ syncRecordingStatus();
2566
+ } else {
2567
+ if (procTable) procTable.style.display = '';
2568
+ if (logsView) logsView.style.display = 'none';
2569
+ if (procFilter) procFilter.style.display = '';
2570
+ if (searchWrap) searchWrap.style.display = '';
2571
+ if (btnStop) btnStop.style.display = '';
2572
+ if (btnInspect) btnInspect.style.display = '';
2573
+ renderTableHeader();
2574
+ renderTableRows();
2575
+ updateBottomPanel();
2576
+ requestAnimationFrame(drawAllCanvases);
2577
+ }
2578
+ }
2579
+
2580
+ // Process Filtering and Searching
2581
+ function onFilterChange(val) {
2582
+ currentFilter = val;
2583
+ renderTableRows();
2584
+ }
2585
+
2586
+ function onSearchInput(val) {
2587
+ searchQuery = (val || '').trim().toLowerCase();
2588
+ document.getElementById('search-clear').style.display = searchQuery ? 'flex' : 'none';
2589
+ renderTableRows();
2590
+ }
2591
+
2592
+ function clearSearch() {
2593
+ document.getElementById('search-input').value = '';
2594
+ onSearchInput('');
2595
+ }
2596
+
2597
+ // Process Selection
2598
+ function selectRow(pid) {
2599
+ selectedPid = pid;
2600
+ document.querySelectorAll('#proc-tbody tr').forEach(tr => {
2601
+ tr.classList.toggle('selected', Number(tr.dataset.pid) === pid);
2602
+ });
2603
+ const hasSel = selectedPid !== null;
2604
+ document.getElementById('btn-stop').disabled = !hasSel;
2605
+ document.getElementById('btn-inspect').disabled = !hasSel;
2606
+ }
2607
+
2608
+ // Render Table Rows
2609
+ function renderTableRows() {
2610
+ if (!latestData || !latestData.processes) return;
2611
+ const tbody = document.getElementById('proc-tbody');
2612
+ const cols = tabColumns[currentTab] || tabColumns.cpu;
2613
+ const sort = sortState[currentTab];
2614
+ const totalMem = latestData.memory?.total || 1;
2615
+
2616
+ // Filter processes
2617
+ let procs = latestData.processes.map(p => {
2618
+ return {
2619
+ ...p,
2620
+ memBytes: (p.mem / 100) * totalMem,
2621
+ energyImpact: parseFloat(p.cpu.toFixed(1)),
2622
+ netRx: 0,
2623
+ netTx: 0
2624
+ };
2625
+ });
2626
+
2627
+ if (currentFilter === 'my') {
2628
+ const myUser = latestData.header?.user || 'somalip';
2629
+ procs = procs.filter(p => p.user && p.user.toLowerCase().includes(myUser.toLowerCase()));
2630
+ } else if (currentFilter === 'active') {
2631
+ procs = procs.filter(p => p.cpu > 0.05 || p.mem > 0.2);
2632
+ } else if (currentFilter === 'system') {
2633
+ procs = procs.filter(p => p.user === 'root' || p.pid < 100);
2634
+ }
2635
+
2636
+ if (searchQuery) {
2637
+ procs = procs.filter(p => {
2638
+ return p.command.toLowerCase().includes(searchQuery) ||
2639
+ String(p.pid).includes(searchQuery) ||
2640
+ (p.user && p.user.toLowerCase().includes(searchQuery));
2641
+ });
2642
+ }
2643
+
2644
+ // Sort
2645
+ procs.sort((a, b) => {
2646
+ let va = a[sort.col];
2647
+ let vb = b[sort.col];
2648
+ if (typeof va === 'string') {
2649
+ va = va.toLowerCase();
2650
+ vb = (vb || '').toLowerCase();
2651
+ }
2652
+ if (va < vb) return sort.dir === 'asc' ? -1 : 1;
2653
+ if (va > vb) return sort.dir === 'asc' ? 1 : -1;
2654
+ return 0;
2655
+ });
2656
+
2657
+ // Generate HTML
2658
+ let rowsHtml = '';
2659
+ procs.forEach(p => {
2660
+ const isSelected = p.pid === selectedPid;
2661
+ const isSys = p.user === 'root' || p.pid < 100;
2662
+ const initial = (p.command.replace(/^.*[\\/]/, '')[0] || 'P').toUpperCase();
2663
+
2664
+ rowsHtml += \`<tr class="\${isSelected ? 'selected' : ''}" data-pid="\${p.pid}" onclick="selectRow(\${p.pid})" ondblclick="openInspector(\${p.pid})">\`;
2665
+
2666
+ cols.forEach(c => {
2667
+ if (c.id === 'command') {
2668
+ const shortName = p.command.replace(/^.*[\\/]/, '');
2669
+ rowsHtml += \`<td class="col-name" title="\${escapeHtml(p.command)}">
2670
+ <span class="proc-icon \${isSys ? 'system' : 'user'}">\${initial}</span>
2671
+ <span style="overflow:hidden;text-overflow:ellipsis;">\${escapeHtml(shortName)}</span>
2672
+ </td>\`;
2673
+ } else if (c.id === 'cpu') {
2674
+ rowsHtml += \`<td>\${p.cpu.toFixed(1)}</td>\`;
2675
+ } else if (c.id === 'runtime') {
2676
+ rowsHtml += \`<td>\${fmtTime(p.runtime)}</td>\`;
2677
+ } else if (c.id === 'mem') {
2678
+ rowsHtml += \`<td>\${p.mem.toFixed(1)}</td>\`;
2679
+ } else if (c.id === 'memBytes') {
2680
+ rowsHtml += \`<td>\${fmtBytes(p.memBytes)}</td>\`;
2681
+ } else if (c.id === 'energyImpact') {
2682
+ rowsHtml += \`<td>\${p.energyImpact.toFixed(1)}</td>\`;
2683
+ } else if (c.id === 'netRx') {
2684
+ rowsHtml += \`<td>\${fmtBytes(p.netRx)}</td>\`;
2685
+ } else if (c.id === 'netTx') {
2686
+ rowsHtml += \`<td>\${fmtBytes(p.netTx)}</td>\`;
2687
+ } else if (c.id === 'pid') {
2688
+ rowsHtml += \`<td>\${p.pid}</td>\`;
2689
+ } else if (c.id === 'threads') {
2690
+ rowsHtml += \`<td>\${p.threads || 1}</td>\`;
2691
+ } else if (c.id === 'state') {
2692
+ rowsHtml += \`<td style="text-align:center;">\${escapeHtml(p.state || 'R')}</td>\`;
2693
+ } else if (c.id === 'user') {
2694
+ rowsHtml += \`<td style="text-align:left;">\${escapeHtml(p.user || 'root')}</td>\`;
2695
+ } else {
2696
+ rowsHtml += \`<td>\${escapeHtml(p[c.id] || '\u2013')}</td>\`;
2697
+ }
2698
+ });
2699
+
2700
+ rowsHtml += '</tr>';
2701
+ });
2702
+
2703
+ tbody.innerHTML = rowsHtml;
2704
+ document.getElementById('sb-count').textContent = procs.length + ' processes';
2705
+ }
2706
+
2707
+ // Draw Area Charts on Canvas
2708
+ function drawChart(canvasId, pointsA, colorA, pointsB = null, colorB = null, maxVal = 100) {
2709
+ const c = document.getElementById(canvasId);
2710
+ if (!c) return;
2711
+ const dpr = window.devicePixelRatio || 1;
2712
+ const rect = c.getBoundingClientRect();
2713
+ if (rect.width === 0 || rect.height === 0) return;
2714
+
2715
+ if (c.width !== Math.floor(rect.width * dpr) || c.height !== Math.floor(rect.height * dpr)) {
2716
+ c.width = Math.floor(rect.width * dpr);
2717
+ c.height = Math.floor(rect.height * dpr);
2718
+ }
2719
+
2720
+ const W = c.width;
2721
+ const H = c.height;
2722
+ const ctx = c.getContext('2d');
2723
+ ctx.clearRect(0, 0, W, H);
2724
+
2725
+ // Grid lines
2726
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)';
2727
+ ctx.lineWidth = 1 * dpr;
2728
+ [0.25, 0.5, 0.75].forEach(ratio => {
2729
+ ctx.beginPath();
2730
+ ctx.moveTo(0, H * ratio);
2731
+ ctx.lineTo(W, H * ratio);
2732
+ ctx.stroke();
2733
+ });
2734
+
2735
+ if (!pointsA || !pointsA.length) return;
2736
+
2737
+ const actualMax = maxVal || Math.max(...pointsA, ...(pointsB || []), 1);
2738
+
2739
+ // Helper to render area + stroke
2740
+ function renderPath(pts, strokeCol, fillCol) {
2741
+ if (!pts || !pts.length) return;
2742
+ const step = W / (MAX_POINTS - 1);
2743
+ const startX = W - (pts.length - 1) * step;
2744
+
2745
+ ctx.beginPath();
2746
+ ctx.moveTo(startX, H);
2747
+ pts.forEach((v, i) => {
2748
+ const x = startX + i * step;
2749
+ const y = Math.max(0, H - (v / actualMax) * H);
2750
+ ctx.lineTo(x, y);
2751
+ });
2752
+ ctx.lineTo(W, H);
2753
+ ctx.fillStyle = fillCol;
2754
+ ctx.fill();
2755
+
2756
+ ctx.beginPath();
2757
+ pts.forEach((v, i) => {
2758
+ const x = startX + i * step;
2759
+ const y = Math.max(0, H - (v / actualMax) * H);
2760
+ if (i === 0) ctx.moveTo(x, y);
2761
+ else ctx.lineTo(x, y);
2762
+ });
2763
+ ctx.strokeStyle = strokeCol;
2764
+ ctx.lineWidth = 1.5 * dpr;
2765
+ ctx.stroke();
2766
+ }
2767
+
2768
+ if (pointsB && pointsB.length && colorB) {
2769
+ renderPath(pointsB, colorB, colorB + '33');
2770
+ }
2771
+ renderPath(pointsA, colorA, colorA + '44');
2772
+ }
2773
+
2774
+ function drawAllCanvases() {
2775
+ if (currentTab === 'cpu') {
2776
+ drawChart('canvas-cpu', historyData.cpuUser, '#007aff', historyData.cpuSystem, '#ff453a', 100);
2777
+ } else if (currentTab === 'memory') {
2778
+ const pressurePct = latestData?.memory?.usagePercent || 50;
2779
+ const col = pressurePct > 80 ? '#ff453a' : (pressurePct > 60 ? '#ff9f0a' : '#34c759');
2780
+ drawChart('canvas-mem', historyData.memPressure, col, null, null, 100);
2781
+ } else if (currentTab === 'energy') {
2782
+ const maxW = Math.max(10, ...historyData.energyWatts);
2783
+ drawChart('canvas-energy', historyData.energyWatts, '#ff9f0a', null, null, maxW);
2784
+ } else if (currentTab === 'disk') {
2785
+ const maxIO = Math.max(1, ...historyData.diskRead, ...historyData.diskWrite);
2786
+ drawChart('canvas-disk', historyData.diskRead, '#64d2ff', historyData.diskWrite, '#ff453a', maxIO);
2787
+ } else if (currentTab === 'network') {
2788
+ const maxNet = Math.max(1, ...historyData.netIn, ...historyData.netOut);
2789
+ drawChart('canvas-net', historyData.netIn, '#007aff', historyData.netOut, '#ff453a', maxNet);
2790
+ } else if (currentTab === 'gpu') {
2791
+ drawChart('canvas-gpu', historyData.gpuUtil, '#af52de', null, null, 100);
2792
+ }
2793
+ }
2794
+
2795
+ // Update Bottom Panel Elements
2796
+ function updateBottomPanel() {
2797
+ if (!latestData) return;
2798
+ const d = latestData;
2799
+
2800
+ // CPU
2801
+ const totalCpu = d.cpu.usage || 0;
2802
+ const userCpu = totalCpu * 0.75;
2803
+ const sysCpu = totalCpu * 0.25;
2804
+ const idleCpu = Math.max(0, 100 - totalCpu);
2805
+
2806
+ document.getElementById('cpu-stat-system').textContent = sysCpu.toFixed(1) + '%';
2807
+ document.getElementById('cpu-stat-user').textContent = userCpu.toFixed(1) + '%';
2808
+ document.getElementById('cpu-stat-idle').textContent = idleCpu.toFixed(1) + '%';
2809
+
2810
+ const totalThreads = (d.processes || []).reduce((acc, p) => acc + (p.threads || 1), 0);
2811
+ document.getElementById('cpu-stat-threads').textContent = totalThreads.toLocaleString();
2812
+ document.getElementById('cpu-stat-procs').textContent = (d.processes || []).length;
2813
+ document.getElementById('cpu-stat-brand').textContent = (d.cpu.brand || 'Apple Silicon') + ' (' + (d.cpu.cores || 8) + ' cores)';
2814
+
2815
+ // Memory
2816
+ const m = d.memory;
2817
+ document.getElementById('mem-stat-phys').textContent = fmtBytes(m.total);
2818
+ document.getElementById('mem-stat-used').textContent = fmtBytes(m.used);
2819
+ document.getElementById('mem-stat-wired').textContent = fmtBytes(m.wiredBytes || m.used * 0.22);
2820
+ document.getElementById('mem-stat-comp').textContent = fmtBytes(m.compressedBytes || 0);
2821
+ document.getElementById('mem-stat-cached').textContent = fmtBytes(Math.max(0, m.total - m.used - m.free));
2822
+ document.getElementById('mem-stat-swap').textContent = fmtBytes(m.swapUsed);
2823
+ document.getElementById('mem-stat-app').textContent = fmtBytes(Math.max(0, m.used - (m.wiredBytes || m.used * 0.22) - (m.compressedBytes || 0)));
2824
+
2825
+ const pDot = document.getElementById('pressure-dot');
2826
+ const pTxt = document.getElementById('pressure-text');
2827
+ const pct = m.usagePercent || 0;
2828
+ if (pct > 80) {
2829
+ pDot.className = 'pressure-dot crit';
2830
+ pTxt.textContent = 'Critical (' + pct.toFixed(0) + '%)';
2831
+ pTxt.style.color = 'var(--mac-red)';
2832
+ } else if (pct > 65) {
2833
+ pDot.className = 'pressure-dot warn';
2834
+ pTxt.textContent = 'Warning (' + pct.toFixed(0) + '%)';
2835
+ pTxt.style.color = 'var(--mac-yellow)';
2836
+ } else {
2837
+ pDot.className = 'pressure-dot';
2838
+ pTxt.textContent = 'Normal';
2839
+ pTxt.style.color = 'var(--mac-green)';
2840
+ }
2841
+
2842
+ // Energy
2843
+ const tw = d.power?.combinedWatts ?? (d.battery?.powerWatts || 0);
2844
+ document.getElementById('energy-stat-total').textContent = tw ? tw.toFixed(2) + ' W' : '\u2013';
2845
+ document.getElementById('energy-stat-cpu').textContent = d.power?.cpuWatts != null ? d.power.cpuWatts.toFixed(2) + ' W' : '\u2013';
2846
+ document.getElementById('energy-stat-gpu').textContent = d.power?.gpuWatts != null ? d.power.gpuWatts.toFixed(2) + ' W' : '\u2013';
2847
+
2848
+ if (d.battery) {
2849
+ document.getElementById('energy-stat-batt').textContent = d.battery.level + '% (' + d.battery.state + ')';
2850
+ document.getElementById('energy-stat-source').textContent = d.battery.powerSource || 'Battery';
2851
+ document.getElementById('energy-stat-timerem').textContent = d.battery.timeRemaining || 'Calculating\u2026';
2852
+ document.getElementById('energy-stat-health').textContent = (d.battery.condition || 'Normal') + (d.battery.cycles ? ' \xB7 ' + d.battery.cycles + ' cycles' : '');
2853
+ } else {
2854
+ document.getElementById('energy-stat-batt').textContent = 'AC Power Only';
2855
+ document.getElementById('energy-stat-source').textContent = 'Power Adapter';
2856
+ document.getElementById('energy-stat-timerem').textContent = 'N/A';
2857
+ document.getElementById('energy-stat-health').textContent = 'Good';
2858
+ }
2859
+
2860
+ // Disk
2861
+ const firstDisk = (d.disk && d.disk[0]) ? d.disk[0] : null;
2862
+ const dRead = firstDisk?.readBytesSec || 0;
2863
+ const dWrite = firstDisk?.writeBytesSec || 0;
2864
+ document.getElementById('disk-stat-reads').textContent = fmtBytes(dRead) + '/s';
2865
+ document.getElementById('disk-stat-writes').textContent = fmtBytes(dWrite) + '/s';
2866
+ document.getElementById('disk-stat-total-read').textContent = firstDisk?.used || '\u2013';
2867
+ document.getElementById('disk-stat-total-write').textContent = firstDisk?.available || '\u2013';
2868
+
2869
+ // Disk Mounts bars
2870
+ const mountsBox = document.getElementById('disk-mounts-box');
2871
+ if (d.disk && d.disk.length) {
2872
+ mountsBox.innerHTML = d.disk.map(v => \`
2873
+ <div style="font-size:10px;">
2874
+ <div style="display:flex;justify-content:space-between;color:var(--text-secondary);">
2875
+ <span>\${escapeHtml(v.mountpoint || '/')}</span>
2876
+ <span>\${escapeHtml(v.used)} / \${escapeHtml(v.size)}</span>
2877
+ </div>
2878
+ <div class="disk-bar"><div class="disk-bar-fill" style="width:\${parseFloat(v.capacity)||50}%"></div></div>
2879
+ </div>
2880
+ \`).join('');
2881
+ }
2882
+
2883
+ // Network
2884
+ document.getElementById('net-stat-pkts-in').textContent = (d.network.rxPacketsPerSec || Math.round(d.network.rxPackets / 1000) || 0).toLocaleString();
2885
+ document.getElementById('net-stat-pkts-out').textContent = (d.network.txPacketsPerSec || Math.round(d.network.txPackets / 1000) || 0).toLocaleString();
2886
+ document.getElementById('net-stat-rate-in').textContent = fmtBytes(d.network.rxRate || 0) + '/s';
2887
+ document.getElementById('net-stat-rate-out').textContent = fmtBytes(d.network.txRate || 0) + '/s';
2888
+ document.getElementById('net-stat-total-in').textContent = fmtBytes(d.network.rxBytes);
2889
+ document.getElementById('net-stat-total-out').textContent = fmtBytes(d.network.txBytes);
2890
+ document.getElementById('net-stat-iface').textContent = (d.network.interface || 'en0') + ' \xB7 ' + (d.network.ip || '127.0.0.1');
2891
+
2892
+ // GPU
2893
+ if (d.gpu) {
2894
+ document.getElementById('gpu-stat-model').textContent = d.gpu.model || 'Integrated GPU';
2895
+ document.getElementById('gpu-stat-util').textContent = (d.gpu.utilization || 0).toFixed(1) + '%';
2896
+ document.getElementById('gpu-stat-vram').textContent = fmtBytes(d.gpu.memory || 0);
2897
+ document.getElementById('gpu-stat-procs').textContent = d.gpu.processes || 0;
2898
+ document.getElementById('gpu-stat-temp').textContent = d.gpu.temperature ? d.gpu.temperature + '\xB0C' : '\u2013';
2899
+ }
2900
+ }
2901
+
2902
+ // Ingest Incoming Data Stream
2903
+ let prevRx = null, prevTx = null;
2904
+ function ingestStats(d) {
2905
+ latestData = d;
2906
+
2907
+ const totalCpu = d.cpu.usage || 0;
2908
+ pushHistory(historyData.cpuUser, totalCpu * 0.75);
2909
+ pushHistory(historyData.cpuSystem, totalCpu * 0.25);
2910
+ pushHistory(historyData.memPressure, d.memory.usagePercent || 0);
2911
+
2912
+ const tw = d.power?.combinedWatts ?? (d.battery?.powerWatts || 0);
2913
+ pushHistory(historyData.energyWatts, tw);
2914
+
2915
+ const firstDisk = (d.disk && d.disk[0]) ? d.disk[0] : null;
2916
+ pushHistory(historyData.diskRead, firstDisk?.readBytesSec || 0);
2917
+ pushHistory(historyData.diskWrite, firstDisk?.writeBytesSec || 0);
2918
+
2919
+ let rxRate = d.network.rxRate || 0;
2920
+ let txRate = d.network.txRate || 0;
2921
+ if (!rxRate && prevRx !== null) rxRate = Math.max(0, d.network.rxBytes - prevRx) / 2;
2922
+ if (!txRate && prevTx !== null) txRate = Math.max(0, d.network.txBytes - prevTx) / 2;
2923
+ prevRx = d.network.rxBytes; prevTx = d.network.txBytes;
2924
+
2925
+ pushHistory(historyData.netIn, rxRate);
2926
+ pushHistory(historyData.netOut, txRate);
2927
+ pushHistory(historyData.gpuUtil, d.gpu?.utilization || 0);
2928
+
2929
+ // Status Bar
2930
+ document.getElementById('sb-dot').className = 'sb-live-dot';
2931
+ document.getElementById('sb-status').textContent = 'Live';
2932
+ document.getElementById('sb-host').textContent = (d.header.hostname || 'Mac') + ' (' + (d.header.os || 'macOS') + ')';
2933
+ document.getElementById('sb-time').textContent = new Date().toLocaleTimeString();
2934
+
2935
+ // Analyze telemetry for automatic log events
2936
+ analyzeAndLog(d);
2937
+
2938
+ renderTableRows();
2939
+ updateBottomPanel();
2940
+ drawAllCanvases();
2941
+ }
2942
+
2943
+ // Window Resize Hook
2944
+ window.addEventListener('resize', () => {
2945
+ requestAnimationFrame(drawAllCanvases);
2946
+ });
2947
+
2948
+ // Modal Management
2949
+ function openModal(id) {
2950
+ const m = document.getElementById(id);
2951
+ if (m) m.classList.add('open');
2952
+ }
2953
+ function closeModal(id) {
2954
+ const m = document.getElementById(id);
2955
+ if (m) m.classList.remove('open');
2956
+ }
2957
+
2958
+ // Quit Confirmation
2959
+ function confirmQuitSelected() {
2960
+ if (selectedPid === null) return;
2961
+ const p = (latestData?.processes || []).find(x => x.pid === selectedPid);
2962
+ const name = p ? p.command.replace(/^.*[\\/]/, '') : 'selected process';
2963
+ document.getElementById('quit-modal-desc').innerHTML = \`Are you sure you want to quit <b>\${escapeHtml(name)}</b> (PID \${selectedPid})?<br/><span style="font-size:11px;color:var(--text-tertiary);">Quitting may result in unsaved changes being lost.</span>\`;
2964
+ openModal('modal-quit');
2965
+ }
2966
+
2967
+ function confirmQuitFromInspector() {
2968
+ closeModal('modal-inspector');
2969
+ confirmQuitSelected();
2970
+ }
2971
+
2972
+ async function executeQuit(isForce) {
2973
+ if (selectedPid === null) return;
2974
+ const pidToKill = selectedPid;
2975
+ const sig = isForce ? 'SIGKILL' : 'SIGTERM';
2976
+ closeModal('modal-quit');
2977
+
2978
+ try {
2979
+ const res = await fetch(\`/api/kill?pid=\${pidToKill}&signal=\${sig}\`);
2980
+ const data = await res.json();
2981
+ if (data.success) {
2982
+ showToast(\`Sent \${sig} to PID \${pidToKill}\`);
2983
+ const p = (latestData?.processes || []).find(x => x.pid === pidToKill);
2984
+ const pname = p ? p.command.replace(/^.*[\\\\/]/, '') : 'PID ' + pidToKill;
2985
+ addLogEntry('ACTION', 'PROCESS',
2986
+ \`Sent \${sig} to \${pname} (PID \${pidToKill})\`,
2987
+ isForce ? 'Force kill' : 'Graceful quit');
2988
+ selectedPid = null;
2989
+ document.getElementById('btn-stop').disabled = true;
2990
+ document.getElementById('btn-inspect').disabled = true;
2991
+ } else {
2992
+ addLogEntry('WARN', 'PROCESS',
2993
+ \`Failed to send \${sig} to PID \${pidToKill}\`,
2994
+ data.error || 'Unknown error');
2995
+ showToast('Error: ' + (data.error || 'Failed to kill process'));
2996
+ }
2997
+ } catch (e) {
2998
+ showToast('Network error: ' + e.message);
2999
+ }
3000
+ }
3001
+
3002
+ // Process Inspector
3003
+ function openInspector(pid) {
3004
+ selectRow(pid);
3005
+ const p = (latestData?.processes || []).find(x => x.pid === pid);
3006
+ if (!p) return;
3007
+
3008
+ const shortName = p.command.replace(/^.*[\\/]/, '');
3009
+ document.getElementById('inspect-title').textContent = \`\${shortName} (PID \${p.pid})\`;
3010
+
3011
+ const totalMem = latestData.memory?.total || 1;
3012
+ const memBytes = (p.mem / 100) * totalMem;
3013
+
3014
+ document.getElementById('inspect-body').innerHTML = \`
3015
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
3016
+ <div class="proc-icon user" style="width:36px;height:36px;font-size:16px;border-radius:6px;">\${shortName[0]?.toUpperCase() || 'P'}</div>
3017
+ <div>
3018
+ <div style="font-size:14px;font-weight:700;">\${escapeHtml(shortName)}</div>
3019
+ <div style="font-size:11px;color:var(--text-secondary);word-break:break-all;">\${escapeHtml(p.command)}</div>
3020
+ </div>
3021
+ </div>
3022
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px 16px;font-size:12px;">
3023
+ <div class="stat-item"><span class="stat-k">Process ID:</span><span class="stat-v">\${p.pid}</span></div>
3024
+ <div class="stat-item"><span class="stat-k">Parent Process ID:</span><span class="stat-v">\${p.ppid || '1'}</span></div>
3025
+ <div class="stat-item"><span class="stat-k">User:</span><span class="stat-v">\${escapeHtml(p.user || 'root')}</span></div>
3026
+ <div class="stat-item"><span class="stat-k">State:</span><span class="stat-v">\${escapeHtml(p.state || 'R')}</span></div>
3027
+ <div class="stat-item"><span class="stat-k">% CPU Usage:</span><span class="stat-v">\${p.cpu.toFixed(1)}%</span></div>
3028
+ <div class="stat-item"><span class="stat-k">CPU Runtime:</span><span class="stat-v">\${fmtTime(p.runtime)}</span></div>
3029
+ <div class="stat-item"><span class="stat-k">Memory:</span><span class="stat-v">\${fmtBytes(memBytes)} (\${p.mem.toFixed(1)}%)</span></div>
3030
+ <div class="stat-item"><span class="stat-k">Threads:</span><span class="stat-v">\${p.threads || 1}</span></div>
3031
+ </div>
3032
+ \`;
3033
+
3034
+ openModal('modal-inspector');
3035
+ }
3036
+
3037
+ function openInspectorSelected() {
3038
+ if (selectedPid !== null) openInspector(selectedPid);
3039
+ }
3040
+
3041
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 SETTINGS & PREFERENCES \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
3042
+
3043
+ let currentConfig = {};
3044
+
3045
+ async function loadConfig() {
3046
+ try {
3047
+ const res = await fetch('/api/config');
3048
+ if (res.ok) {
3049
+ currentConfig = await res.json();
3050
+ applyConfigToUI(currentConfig);
3051
+ }
3052
+ } catch (err) {
3053
+ console.error('Failed to load config:', err);
3054
+ }
3055
+ }
3056
+
3057
+ function applyConfigToUI(cfg) {
3058
+ if (!cfg) return;
3059
+ const setVal = (id, val) => {
3060
+ const el = document.getElementById(id);
3061
+ if (el && val !== undefined && val !== null) el.value = val;
3062
+ };
3063
+ const setChecked = (id, val) => {
3064
+ const el = document.getElementById(id);
3065
+ if (el && val !== undefined) el.checked = !!val;
3066
+ };
3067
+
3068
+ setVal('set-interval', cfg.interval || cfg.telemetryInterval || '2');
3069
+ setVal('set-filter', cfg.defaultFilter || 'all');
3070
+ setVal('set-sort', cfg.defaultSort || 'cpu');
3071
+ setChecked('set-compact', cfg.compact);
3072
+ setChecked('set-ontop', cfg.alwaysOnTop);
3073
+ setChecked('set-lan', cfg.exposeLan);
3074
+ setVal('set-port', cfg.port || 3000);
3075
+ setVal('set-history-len', cfg.historyDuration || '60');
3076
+ setChecked('set-show-graphs', cfg.showGraphs !== false);
3077
+ setVal('set-density', cfg.tableDensity || 'standard');
3078
+ if (cfg.tableDensity) onDensityChange(cfg.tableDensity, false);
3079
+
3080
+ setVal('set-cpu-alert', cfg.cpuAlertPct ?? 90);
3081
+ setVal('set-temp-alert', cfg.tempAlertC ?? 95);
3082
+ setVal('set-watchdog-proc', cfg.watchdogProcess || '');
3083
+ setVal('set-watchdog-mem', cfg.watchdogMem ?? 80);
3084
+ setVal('set-slack-url', cfg.slackAlertUrl || '');
3085
+ setVal('set-discord-url', cfg.discordAlertUrl || '');
3086
+ setVal('set-ntfy-url', cfg.ntfyUrl || '');
3087
+
3088
+ // Apply default filter to live state if filter is currently 'all'
3089
+ if (cfg.defaultFilter && currentFilter === 'all') {
3090
+ const filterSelect = document.getElementById('filter-select');
3091
+ if (filterSelect) {
3092
+ filterSelect.value = cfg.defaultFilter;
3093
+ currentFilter = cfg.defaultFilter;
3094
+ }
3095
+ }
3096
+ }
3097
+
3098
+ async function fetchServiceStatus() {
3099
+ try {
3100
+ const pill = document.getElementById('svc-status-pill');
3101
+ if (pill) {
3102
+ pill.className = 'status-pill inactive';
3103
+ pill.textContent = 'Checking\u2026';
3104
+ }
3105
+ const res = await fetch('/api/service/status');
3106
+ if (res.ok) {
3107
+ const s = await res.json();
3108
+ const chk = document.getElementById('set-autostart');
3109
+ if (chk) chk.checked = !!s.installed;
3110
+ if (pill) {
3111
+ if (s.installed) {
3112
+ pill.className = 'status-pill active';
3113
+ pill.textContent = 'Active (' + (s.platform === 'darwin' ? 'LaunchAgent loaded' : 'systemd active') + ')';
3114
+ } else {
3115
+ pill.className = 'status-pill inactive';
3116
+ pill.textContent = 'Inactive (Not installed)';
3117
+ }
3118
+ }
3119
+ const lbl = document.getElementById('svc-label');
3120
+ if (lbl && s.serviceLabel) lbl.textContent = s.serviceLabel;
3121
+ const pth = document.getElementById('svc-path');
3122
+ if (pth && s.path) pth.textContent = s.path;
3123
+ const logEl = document.getElementById('svc-log');
3124
+ if (logEl && s.logPath) logEl.textContent = s.logPath;
3125
+ }
3126
+ } catch (err) {
3127
+ console.error('Failed to get service status:', err);
3128
+ }
3129
+ }
3130
+
3131
+ function updateSysInfoInSettings() {
3132
+ const container = document.getElementById('diag-sys-info');
3133
+ if (!container) return;
3134
+ const sys = latestData?.system;
3135
+ if (!sys) {
3136
+ container.innerHTML = '<div>System information waiting for initial telemetry frame\u2026</div>';
3137
+ return;
3138
+ }
3139
+ container.innerHTML = \`
3140
+ <div style="display:flex;justify-content:space-between;"><span style="color:var(--text-secondary);">Operating System:</span><span>\${escapeHtml(sys.platform || 'macOS')} (\${escapeHtml(sys.osRelease || '')})</span></div>
3141
+ <div style="display:flex;justify-content:space-between;"><span style="color:var(--text-secondary);">Hardware Architecture:</span><span>\${escapeHtml(sys.arch || 'arm64')}</span></div>
3142
+ <div style="display:flex;justify-content:space-between;"><span style="color:var(--text-secondary);">Processor Model:</span><span>\${escapeHtml(latestData.cpu?.model || 'Apple Silicon')}</span></div>
3143
+ <div style="display:flex;justify-content:space-between;"><span style="color:var(--text-secondary);">Physical / Logical Cores:</span><span>\${latestData.cpu?.physicalCores || latestData.cpu?.cores || 8} cores</span></div>
3144
+ <div style="display:flex;justify-content:space-between;"><span style="color:var(--text-secondary);">Installed Memory:</span><span>\${fmtBytes(latestData.memory?.total || 0)}</span></div>
3145
+ <div style="display:flex;justify-content:space-between;"><span style="color:var(--text-secondary);">System Uptime:</span><span>\${fmtUptime(sys.uptime || 0)}</span></div>
3146
+ \`;
3147
+ }
3148
+
3149
+ function openSettingsModal() {
3150
+ openModal('modal-settings');
3151
+ loadConfig();
3152
+ fetchServiceStatus();
3153
+ updateSysInfoInSettings();
3154
+ }
3155
+
3156
+ function closeSettingsModal() {
3157
+ closeModal('modal-settings');
3158
+ }
3159
+
3160
+ function switchSettingsTab(tabName) {
3161
+ document.querySelectorAll('.settings-tab-btn').forEach(btn => {
3162
+ btn.classList.toggle('active', btn.dataset.stab === tabName);
3163
+ });
3164
+ document.querySelectorAll('.settings-pane').forEach(pane => {
3165
+ pane.classList.toggle('active', pane.id === 'spane-' + tabName);
3166
+ });
3167
+ if (tabName === 'diagnostics') {
3168
+ updateSysInfoInSettings();
3169
+ }
3170
+ }
3171
+
3172
+ async function onSettingChange() {
3173
+ const getNum = (id, def) => {
3174
+ const el = document.getElementById(id);
3175
+ const n = parseFloat(el?.value);
3176
+ return isNaN(n) ? def : n;
3177
+ };
3178
+ const getStr = (id, def = '') => {
3179
+ const el = document.getElementById(id);
3180
+ return el ? el.value.trim() : def;
3181
+ };
3182
+ const getBool = (id, def = false) => {
3183
+ const el = document.getElementById(id);
3184
+ return el ? el.checked : def;
3185
+ };
3186
+
3187
+ const patch = {
3188
+ interval: getNum('set-interval', 2),
3189
+ defaultFilter: getStr('set-filter', 'all'),
3190
+ defaultSort: getStr('set-sort', 'cpu'),
3191
+ compact: getBool('set-compact', false),
3192
+ alwaysOnTop: getBool('set-ontop', false),
3193
+ exposeLan: getBool('set-lan', false),
3194
+ port: Math.round(getNum('set-port', 3000)),
3195
+ historyDuration: Math.round(getNum('set-history-len', 60)),
3196
+ showGraphs: getBool('set-show-graphs', true),
3197
+ cpuAlertPct: getNum('set-cpu-alert', 90),
3198
+ tempAlertC: getNum('set-temp-alert', 95),
3199
+ watchdogProcess: getStr('set-watchdog-proc', ''),
3200
+ watchdogMem: getNum('set-watchdog-mem', 80),
3201
+ slackAlertUrl: getStr('set-slack-url', ''),
3202
+ discordAlertUrl: getStr('set-discord-url', ''),
3203
+ ntfyUrl: getStr('set-ntfy-url', '')
3204
+ };
3205
+
3206
+ // Adjust graph buffer size if historyDuration changed
3207
+ if (patch.historyDuration && patch.interval) {
3208
+ MAX_POINTS = Math.max(20, Math.round(patch.historyDuration / patch.interval));
3209
+ }
3210
+
3211
+ try {
3212
+ const res = await fetch('/api/config', {
3213
+ method: 'POST',
3214
+ headers: { 'Content-Type': 'application/json' },
3215
+ body: JSON.stringify(patch)
3216
+ });
3217
+ if (res.ok) {
3218
+ const data = await res.json();
3219
+ currentConfig = data.config || patch;
3220
+ const hint = document.getElementById('settings-status-hint');
3221
+ if (hint) {
3222
+ hint.textContent = 'Saved to ~/.config/pyre/config.json \u2022 ' + new Date().toLocaleTimeString();
3223
+ hint.style.color = 'var(--mac-green)';
3224
+ setTimeout(() => {
3225
+ hint.style.color = 'var(--text-tertiary)';
3226
+ hint.textContent = 'Preferences saved to ~/.config/pyre/config.json';
3227
+ }, 3000);
3228
+ }
3229
+ }
3230
+ } catch (err) {
3231
+ console.error('Failed to save preferences:', err);
3232
+ }
3233
+ }
3234
+
3235
+ function onDensityChange(density, save = true) {
3236
+ const table = document.getElementById('proc-table');
3237
+ if (table) {
3238
+ table.classList.remove('density-compact', 'density-standard', 'density-comfortable');
3239
+ table.classList.add('density-' + density);
3240
+ }
3241
+ if (save) {
3242
+ fetch('/api/config', {
3243
+ method: 'POST',
3244
+ headers: { 'Content-Type': 'application/json' },
3245
+ body: JSON.stringify({ tableDensity: density })
3246
+ }).catch(console.error);
3247
+ showToast('Table density: ' + density.charAt(0).toUpperCase() + density.slice(1));
3248
+ }
3249
+ }
3250
+
3251
+ async function toggleAutostartFromUI(enabled) {
3252
+ const pill = document.getElementById('svc-status-pill');
3253
+ if (pill) {
3254
+ pill.textContent = 'Updating service\u2026';
3255
+ pill.className = 'status-pill';
3256
+ }
3257
+ try {
3258
+ const res = await fetch('/api/service/toggle', {
3259
+ method: 'POST',
3260
+ headers: { 'Content-Type': 'application/json' },
3261
+ body: JSON.stringify({ enabled })
3262
+ });
3263
+ const data = await res.json();
3264
+ if (data.success) {
3265
+ showToast(data.message || (enabled ? 'Service enabled' : 'Service disabled'));
3266
+ addLogEntry('ACTION', 'SYSTEM', 'Background service ' + (enabled ? 'enabled' : 'disabled'), data.message);
3267
+ fetchServiceStatus();
3268
+ } else {
3269
+ showToast('Error: ' + (data.error || 'Failed to modify service'));
3270
+ const chk = document.getElementById('set-autostart');
3271
+ if (chk) chk.checked = !enabled;
3272
+ fetchServiceStatus();
3273
+ }
3274
+ } catch (err) {
3275
+ showToast('Failed to contact Pyre server');
3276
+ const chk = document.getElementById('set-autostart');
3277
+ if (chk) chk.checked = !enabled;
3278
+ fetchServiceStatus();
3279
+ }
3280
+ }
3281
+
3282
+ async function sendTestAlertFromUI() {
3283
+ showToast('Dispatching test notification\u2026');
3284
+ try {
3285
+ const res = await fetch('/api/alerts/test', {
3286
+ method: 'POST',
3287
+ headers: { 'Content-Type': 'application/json' },
3288
+ body: JSON.stringify({ message: 'Pyre Activity Monitor: Test notification triggered from Web Dashboard' })
3289
+ });
3290
+ const data = await res.json();
3291
+ if (data.success) {
3292
+ showToast(data.message || 'Notification dispatched');
3293
+ addLogEntry('WATCHDOG', 'SYSTEM', 'Test notification sent', data.message);
3294
+ } else {
3295
+ showToast('Alert test failed: ' + (data.error || 'Unknown error'));
3296
+ }
3297
+ } catch (err) {
3298
+ showToast('Failed to dispatch test notification');
3299
+ }
3300
+ }
3301
+
3302
+ async function runDoctorFromUI() {
3303
+ const btn = document.getElementById('btn-run-doctor');
3304
+ const resEl = document.getElementById('doctor-results');
3305
+ if (btn) {
3306
+ btn.disabled = true;
3307
+ btn.textContent = 'Running\u2026';
3308
+ }
3309
+ if (resEl) {
3310
+ resEl.style.display = 'block';
3311
+ resEl.textContent = 'Running system diagnostic checks\u2026\\nScanning kernel metrics, thermal sensors, battery, launchd daemons, network interfaces\u2026';
3312
+ }
3313
+
3314
+ try {
3315
+ const res = await fetch('/api/doctor');
3316
+ const data = await res.json();
3317
+ if (btn) {
3318
+ btn.disabled = false;
3319
+ btn.textContent = 'Run Diagnostics';
3320
+ }
3321
+ if (resEl) {
3322
+ if (data.success && data.report) {
3323
+ let out = '\u2713 System Diagnostics Complete\\n';
3324
+ out += 'Timestamp: ' + (data.report.timestamp || new Date().toISOString()) + '\\n';
3325
+ out += 'Overall Status: ' + (data.report.status || 'HEALTHY').toUpperCase() + '\\n\\n';
3326
+ if (Array.isArray(data.report.checks)) {
3327
+ for (const c of data.report.checks) {
3328
+ const sym = c.ok ? ' \u2713 ' : ' \u2717 ';
3329
+ out += sym + c.name + (c.detail ? ': ' + c.detail : '') + '\\n';
3330
+ }
3331
+ }
3332
+ resEl.textContent = out;
3333
+ addLogEntry('INFO', 'SYSTEM', 'Diagnostics completed', data.report.status || 'OK');
3334
+ } else {
3335
+ resEl.textContent = 'Diagnostic run complete.\\n\\n' + JSON.stringify(data, null, 2);
3336
+ }
3337
+ }
3338
+ } catch (err) {
3339
+ if (btn) {
3340
+ btn.disabled = false;
3341
+ btn.textContent = 'Run Diagnostics';
3342
+ }
3343
+ if (resEl) {
3344
+ resEl.textContent = 'Error executing diagnostics: ' + err.message;
3345
+ }
3346
+ }
3347
+ }
3348
+
3349
+ async function resetConfigFromUI() {
3350
+ if (!confirm('Are you sure you want to reset all preferences to factory defaults?')) {
3351
+ return;
3352
+ }
3353
+ try {
3354
+ const res = await fetch('/api/config/reset', { method: 'POST' });
3355
+ const data = await res.json();
3356
+ if (data.success) {
3357
+ showToast('Preferences reset to factory defaults');
3358
+ addLogEntry('ACTION', 'UI', 'Preferences reset to factory defaults');
3359
+ loadConfig();
3360
+ } else {
3361
+ showToast('Error resetting preferences: ' + (data.error || 'Unknown error'));
3362
+ }
3363
+ } catch (err) {
3364
+ showToast('Failed to reset preferences');
3365
+ }
3366
+ }
3367
+
3368
+ // Keyboard Navigation
3369
+ document.addEventListener('keydown', e => {
3370
+ if (e.key === 'Escape') {
3371
+ closeModal('modal-quit');
3372
+ closeModal('modal-inspector');
3373
+ closeSettingsModal();
3374
+ return;
3375
+ }
3376
+
3377
+ // Cmd+, or , (when not in an input/select/textarea) opens preferences
3378
+ if ((e.key === ',' && (e.metaKey || e.ctrlKey)) ||
3379
+ (e.key === ',' && !document.activeElement.matches('input, select, textarea'))) {
3380
+ e.preventDefault();
3381
+ openSettingsModal();
3382
+ return;
3383
+ }
3384
+
3385
+ // If focused in search, don't hijack keys
3386
+ if (document.activeElement === document.getElementById('search-input') ||
3387
+ document.activeElement === document.getElementById('log-search-input')) {
3388
+ return;
3389
+ }
3390
+
3391
+ // Segmented shortcuts 1-7
3392
+ const keyTabs = { '1': 'cpu', '2': 'memory', '3': 'energy', '4': 'disk', '5': 'network', '6': 'gpu', '7': 'logs' };
3393
+ if (keyTabs[e.key]) {
3394
+ switchTab(keyTabs[e.key]);
3395
+ return;
3396
+ }
3397
+
3398
+ // r = toggle recording
3399
+ if (e.key === 'r' && !e.metaKey && !e.ctrlKey) {
3400
+ e.preventDefault();
3401
+ toggleWebRecording();
3402
+ return;
3403
+ }
3404
+
3405
+ // l = focus log search (when on logs tab)
3406
+ if (e.key === 'l' && currentTab === 'logs') {
3407
+ e.preventDefault();
3408
+ const ls = document.getElementById('log-search-input');
3409
+ if (ls) { ls.focus(); ls.select(); }
3410
+ return;
3411
+ }
3412
+
3413
+ // Table arrow navigation
3414
+ if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && latestData?.processes?.length) {
3415
+ e.preventDefault();
3416
+ const rows = Array.from(document.querySelectorAll('#proc-tbody tr'));
3417
+ if (!rows.length) return;
3418
+ const curIdx = rows.findIndex(tr => Number(tr.dataset.pid) === selectedPid);
3419
+ let nextIdx = curIdx;
3420
+ if (e.key === 'ArrowDown') nextIdx = curIdx < rows.length - 1 ? curIdx + 1 : 0;
3421
+ if (e.key === 'ArrowUp') nextIdx = curIdx > 0 ? curIdx - 1 : rows.length - 1;
3422
+ const nextPid = Number(rows[nextIdx].dataset.pid);
3423
+ selectRow(nextPid);
3424
+ rows[nextIdx].scrollIntoView({ block: 'nearest' });
3425
+ return;
3426
+ }
3427
+
3428
+ // Enter or Space opens inspector
3429
+ if ((e.key === 'Enter' || e.key === ' ') && selectedPid !== null) {
3430
+ e.preventDefault();
3431
+ openInspectorSelected();
3432
+ return;
3433
+ }
3434
+
3435
+ // Delete or Backspace prompts quit
3436
+ if ((e.key === 'Backspace' || e.key === 'Delete') && selectedPid !== null) {
3437
+ e.preventDefault();
3438
+ confirmQuitSelected();
3439
+ return;
3440
+ }
3441
+
3442
+ // Cmd+F or / focuses search
3443
+ if ((e.key === 'f' && (e.metaKey || e.ctrlKey)) || e.key === '/') {
3444
+ e.preventDefault();
3445
+ const s = document.getElementById('search-input');
3446
+ s.focus();
3447
+ s.select();
3448
+ }
3449
+ });
3450
+
3451
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 LOGGING & RECORDING \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
3452
+
3453
+ const MAX_LOG_ENTRIES = 500;
3454
+
3455
+ function addLogEntry(level, subsys, msg, ctx) {
3456
+ const entry = {
3457
+ ts: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 1 }),
3458
+ level, // 'INFO','WARN','CRIT','ACTION','WATCHDOG'
3459
+ subsys, // 'CPU','MEM','DISK','NET','THERMAL','PROCESS','SYSTEM','LOGGER','UI'
3460
+ msg,
3461
+ ctx: ctx || '',
3462
+ raw: { level, subsys, msg, ctx, ts: new Date().toISOString() }
3463
+ };
3464
+ logsBuffer.unshift(entry);
3465
+ if (logsBuffer.length > MAX_LOG_ENTRIES) logsBuffer.pop();
3466
+ if (currentTab === 'logs') renderLogs();
3467
+ }
3468
+
3469
+ function fmtSizeB(b) {
3470
+ if (!b) return '0 B';
3471
+ if (b > 1e9) return (b/1e9).toFixed(1) + ' GB';
3472
+ if (b > 1e6) return (b/1e6).toFixed(1) + ' MB';
3473
+ if (b > 1e3) return (b/1e3).toFixed(0) + ' KB';
3474
+ return b + ' B';
3475
+ }
3476
+
3477
+ function renderLogs() {
3478
+ const tbody = document.getElementById('logs-tbody');
3479
+ if (!tbody) return;
3480
+
3481
+ const visible = logsBuffer.filter(e => {
3482
+ if (logFilterLevel !== 'all' && e.level !== logFilterLevel) return false;
3483
+ if (logFilterSubsys !== 'all' && e.subsys !== logFilterSubsys) return false;
3484
+ if (logSearchQuery && !e.msg.toLowerCase().includes(logSearchQuery) && !e.ctx.toLowerCase().includes(logSearchQuery)) return false;
3485
+ return true;
3486
+ });
3487
+
3488
+ tbody.innerHTML = visible.map(e => {
3489
+ const badgeCls = e.level.toLowerCase();
3490
+ return \`<tr>
3491
+ <td class="log-ctx">\${escapeHtml(e.ts)}</td>
3492
+ <td><span class="log-badge \${badgeCls}">\${escapeHtml(e.level)}</span></td>
3493
+ <td class="log-subsys">\${escapeHtml(e.subsys)}</td>
3494
+ <td class="log-msg">\${escapeHtml(e.msg)}</td>
3495
+ <td style="text-align:right;" class="log-ctx">\${escapeHtml(e.ctx)}</td>
3496
+ </tr>\`;
3497
+ }).join('');
3498
+
3499
+ if (autoScrollLogs && visible.length) {
3500
+ const area = document.getElementById('logs-scroll-area');
3501
+ if (area) area.scrollTop = 0;
3502
+ }
3503
+ }
3504
+
3505
+ function onLogLevelFilterChange(val) {
3506
+ logFilterLevel = val;
3507
+ renderLogs();
3508
+ }
3509
+
3510
+ function onLogSubsysFilterChange(val) {
3511
+ logFilterSubsys = val;
3512
+ renderLogs();
3513
+ }
3514
+
3515
+ function onLogSearchInput(val) {
3516
+ logSearchQuery = (val || '').toLowerCase();
3517
+ renderLogs();
3518
+ }
3519
+
3520
+ function toggleAutoScroll() {
3521
+ autoScrollLogs = !autoScrollLogs;
3522
+ const btn = document.getElementById('btn-autoscroll');
3523
+ if (btn) {
3524
+ btn.textContent = 'Auto-scroll: ' + (autoScrollLogs ? 'ON' : 'OFF');
3525
+ btn.style.color = autoScrollLogs ? 'var(--mac-green)' : 'var(--text-secondary)';
3526
+ }
3527
+ }
3528
+
3529
+ function clearLogs() {
3530
+ logsBuffer = [];
3531
+ renderLogs();
3532
+ showToast('Event log cleared');
3533
+ }
3534
+
3535
+ function downloadLogsExport() {
3536
+ const json = JSON.stringify(logsBuffer.map(e => e.raw), null, 2);
3537
+ const blob = new Blob([json], { type: 'application/json' });
3538
+ const url = URL.createObjectURL(blob);
3539
+ const a = document.createElement('a');
3540
+ a.href = url;
3541
+ a.download = 'pyre-ui-events-' + new Date().toISOString().slice(0,19).replace(/:/g,'-') + '.json';
3542
+ a.click();
3543
+ URL.revokeObjectURL(url);
3544
+ }
3545
+
3546
+ // Analyze incoming telemetry and emit structured log events
3547
+ function analyzeAndLog(d) {
3548
+ if (!hasLoggedInit) {
3549
+ hasLoggedInit = true;
3550
+ addLogEntry('INFO', 'SYSTEM', 'Pyre-UI connected \u2014 live telemetry streaming started',
3551
+ (d.header?.hostname || 'unknown') + ' \xB7 ' + (d.header?.os || 'macOS'));
3552
+ }
3553
+
3554
+ const now = Date.now();
3555
+ const cpu = d.cpu?.usage || 0;
3556
+ const mem = d.memory?.usagePercent || 0;
3557
+ const thermalState = d.thermal?.state || 'Nominal';
3558
+
3559
+ // CPU spike detection (>70% WARN, >90% CRIT), per-process
3560
+ if (d.processes) {
3561
+ d.processes.forEach(p => {
3562
+ if (!p || !p.pid) return;
3563
+ const lastSpike = lastCpuSpikeTimes[p.pid] || 0;
3564
+ if (p.cpu > 90 && now - lastSpike > 30000) {
3565
+ lastCpuSpikeTimes[p.pid] = now;
3566
+ addLogEntry('CRIT', 'CPU',
3567
+ \`Critical CPU usage: \${p.command.replace(/^.*[\\\\/]/,'')} (PID \${p.pid}) at \${p.cpu.toFixed(1)}%\`,
3568
+ \`Threads: \${p.threads || 1} \xB7 User: \${p.user || 'root'}\`);
3569
+ } else if (p.cpu > 70 && now - lastSpike > 60000) {
3570
+ lastCpuSpikeTimes[p.pid] = now;
3571
+ addLogEntry('WARN', 'CPU',
3572
+ \`High CPU usage: \${p.command.replace(/^.*[\\\\/]/,'')} (PID \${p.pid}) at \${p.cpu.toFixed(1)}%\`,
3573
+ \`Threads: \${p.threads || 1}\`);
3574
+ }
3575
+ });
3576
+ }
3577
+
3578
+ // System-wide CPU
3579
+ if (cpu > 90) {
3580
+ addLogEntry('CRIT', 'CPU', \`System CPU at \${cpu.toFixed(1)}% \u2014 critical load\`, '');
3581
+ } else if (cpu > 75) {
3582
+ addLogEntry('WARN', 'CPU', \`System CPU elevated at \${cpu.toFixed(1)}%\`, '');
3583
+ }
3584
+
3585
+ // Memory pressure
3586
+ if (mem > 85 && now - lastHighMemAlert > 60000) {
3587
+ lastHighMemAlert = now;
3588
+ addLogEntry('CRIT', 'MEM', \`Memory pressure critical: \${mem.toFixed(0)}% used\`,
3589
+ 'Total: ' + fmtSizeB(d.memory?.total) + ' \xB7 Swap: ' + fmtSizeB(d.memory?.swapUsed));
3590
+ } else if (mem > 70 && now - lastHighMemAlert > 120000) {
3591
+ lastHighMemAlert = now;
3592
+ addLogEntry('WARN', 'MEM', \`Memory usage high: \${mem.toFixed(0)}%\`, 'Swap: ' + fmtSizeB(d.memory?.swapUsed));
3593
+ }
3594
+
3595
+ // Thermal state transitions
3596
+ if (prevThermalState !== null && prevThermalState !== thermalState) {
3597
+ const level = thermalState === 'Critical' ? 'CRIT' : thermalState === 'Nominal' ? 'INFO' : 'WARN';
3598
+ addLogEntry(level, 'THERMAL',
3599
+ \`Thermal state changed: \${prevThermalState} \u2192 \${thermalState}\`,
3600
+ \`CPU temp: \${d.cpu?.temperature ? d.cpu.temperature + '\xB0C' : '\u2013'}\`);
3601
+ }
3602
+ prevThermalState = thermalState;
3603
+
3604
+ // Network burst detection
3605
+ const rxRate = d.network?.rxRate || 0;
3606
+ const txRate = d.network?.txRate || 0;
3607
+ if ((rxRate > 50*1024*1024 || txRate > 50*1024*1024) && now - lastNetBurstAlert > 30000) {
3608
+ lastNetBurstAlert = now;
3609
+ addLogEntry('WARN', 'NET',
3610
+ \`Network burst: \u2193\${fmtSizeB(rxRate)}/s \u2191\${fmtSizeB(txRate)}/s\`,
3611
+ \`Interface: \${d.network?.interface || 'en0'}\`);
3612
+ }
3613
+ }
3614
+
3615
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 RECORDING CONTROLS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
3616
+
3617
+ async function toggleWebRecording() {
3618
+ try {
3619
+ if (isRecording) {
3620
+ const res = await fetch('/api/logging/stop');
3621
+ const data = await res.json();
3622
+ isRecording = false;
3623
+ updateRecordingUI(false);
3624
+ addLogEntry('INFO', 'LOGGER',
3625
+ \`Session recording stopped: \${data.samples} samples in \${data.durationSec}s\`,
3626
+ data.file || '');
3627
+ showToast(\`Recording stopped \u2014 \${data.samples} samples saved\`);
3628
+ } else {
3629
+ const res = await fetch('/api/logging/start');
3630
+ const data = await res.json();
3631
+ isRecording = true;
3632
+ updateRecordingUI(true, data.file);
3633
+ addLogEntry('INFO', 'LOGGER', 'Session recording started', data.file || '');
3634
+ showToast('Recording started \u2192 ' + (data.file || './pyre-exports/'));
3635
+ }
3636
+ if (currentTab === 'logs') fetchLogHistory();
3637
+ } catch (e) {
3638
+ showToast('Recording error: ' + e.message);
3639
+ }
3640
+ }
3641
+
3642
+ function updateRecordingUI(recording, filename) {
3643
+ const btn = document.getElementById('btn-record');
3644
+ const label = document.getElementById('rec-label');
3645
+ const panelBtn = document.getElementById('btn-panel-rec');
3646
+ const badge = document.getElementById('logs-rec-badge');
3647
+
3648
+ if (btn) btn.classList.toggle('recording', recording);
3649
+ if (label) label.textContent = recording ? 'Stop REC' : 'Record';
3650
+ if (panelBtn) panelBtn.textContent = recording ? 'Stop Recording' : 'Start Recording';
3651
+ if (badge) {
3652
+ badge.textContent = recording ? 'REC' : 'IDLE';
3653
+ badge.className = 'log-badge ' + (recording ? 'crit' : 'info');
3654
+ }
3655
+ }
3656
+
3657
+ async function syncRecordingStatus() {
3658
+ try {
3659
+ const res = await fetch('/api/logging/status');
3660
+ const data = await res.json();
3661
+ isRecording = data.recording;
3662
+ updateRecordingUI(data.recording, data.file);
3663
+
3664
+ const statStatus = document.getElementById('rec-stat-status');
3665
+ const statFile = document.getElementById('rec-stat-file');
3666
+ const statSamples = document.getElementById('rec-stat-samples');
3667
+ const statDuration = document.getElementById('rec-stat-duration');
3668
+ const statDir = document.getElementById('rec-stat-dir');
3669
+ if (statStatus) statStatus.textContent = data.recording ? 'Recording\u2026' : 'Idle';
3670
+ if (statFile) statFile.textContent = data.file || '\u2013';
3671
+ if (statSamples) statSamples.textContent = data.samples;
3672
+ if (statDuration) statDuration.textContent = data.durationSec + 's';
3673
+ if (statDir) statDir.textContent = data.exportDir;
3674
+ } catch { /* ignore */ }
3675
+ }
3676
+
3677
+ async function fetchLogHistory() {
3678
+ try {
3679
+ const res = await fetch('/api/logging/history');
3680
+ const data = await res.json();
3681
+ const tbody = document.getElementById('history-tbody');
3682
+ if (!tbody) return;
3683
+ if (!data.files || !data.files.length) {
3684
+ tbody.innerHTML = '<tr><td colspan="4" style="color:var(--text-tertiary);text-align:center;padding:12px;">No CSV logs found in ' + escapeHtml(data.exportDir) + '. Click Record to start a session.</td></tr>';
3685
+ return;
3686
+ }
3687
+ tbody.innerHTML = data.files.map(f => {
3688
+ const kb = (f.sizeBytes / 1024).toFixed(1);
3689
+ const mtime = f.mtime ? new Date(f.mtime).toLocaleString() : '\u2013';
3690
+ return \`<tr>
3691
+ <td style="font-family:var(--font-mono);font-size:10px;">\${escapeHtml(f.name)}</td>
3692
+ <td>\${kb} KB</td>
3693
+ <td>\${mtime}</td>
3694
+ <td><a class="history-link" href="/api/logging/download?file=\${encodeURIComponent(f.name)}" download="\${escapeHtml(f.name)}">\u2193 Download</a></td>
3695
+ </tr>\`;
3696
+ }).join('');
3697
+ } catch { /* ignore */ }
3698
+ }
3699
+
3700
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 EXPORT DROPDOWN \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
3701
+
3702
+ function toggleExportMenu(e) {
3703
+ e.stopPropagation();
3704
+ const menu = document.getElementById('export-menu');
3705
+ if (menu) menu.classList.toggle('show');
3706
+ }
3707
+
3708
+ document.addEventListener('click', () => {
3709
+ const menu = document.getElementById('export-menu');
3710
+ if (menu) menu.classList.remove('show');
3711
+ });
3712
+
3713
+ function downloadSnapshot(fmt) {
3714
+ const menu = document.getElementById('export-menu');
3715
+ if (menu) menu.classList.remove('show');
3716
+ const url = '/api/export?format=' + fmt;
3717
+ const a = document.createElement('a');
3718
+ a.href = url;
3719
+ a.download = 'pyre-snapshot.' + fmt;
3720
+ a.click();
3721
+ showToast('Exporting snapshot as ' + fmt.toUpperCase() + '\u2026');
3722
+ addLogEntry('ACTION', 'UI', 'User exported snapshot', 'format: ' + fmt);
3723
+ }
3724
+
3725
+ // SSE Connection
3726
+ let eventSource = null;
3727
+ function initSSE() {
3728
+ try {
3729
+ eventSource = new EventSource('/api/stream');
3730
+ eventSource.onmessage = (event) => {
3731
+ try {
3732
+ const data = JSON.parse(event.data);
3733
+ ingestStats(data);
3734
+ } catch (err) {
3735
+ console.error('SSE parse error:', err);
3736
+ }
3737
+ };
3738
+ eventSource.onerror = () => {
3739
+ document.getElementById('sb-dot').className = 'sb-live-dot offline';
3740
+ document.getElementById('sb-status').textContent = 'Reconnecting';
3741
+ };
3742
+ } catch (err) {
3743
+ console.error('SSE error:', err);
3744
+ }
3745
+ }
3746
+
3747
+ // Fallback Polling (also handles window.__pyreUpdate from Cocoa WebView)
3748
+ window.__pyreUpdate = function(jsonStr) {
3749
+ try {
3750
+ const d = JSON.parse(jsonStr);
3751
+ ingestStats(d);
3752
+ } catch (e) {
3753
+ console.error('pyreUpdate parse error:', e);
3754
+ }
3755
+ };
3756
+
3757
+ async function pollFallback() {
3758
+ try {
3759
+ const res = await fetch('/api/stats', { cache: 'no-store' });
3760
+ if (res.ok) {
3761
+ const data = await res.json();
3762
+ ingestStats(data);
3763
+ }
3764
+ } catch (err) {
3765
+ document.getElementById('sb-dot').className = 'sb-live-dot offline';
3766
+ document.getElementById('sb-status').textContent = 'Offline';
3767
+ }
3768
+ }
3769
+
3770
+ // Initialize
3771
+ renderTableHeader();
3772
+ initSSE();
3773
+ pollFallback();
3774
+ syncRecordingStatus();
3775
+ loadConfig();
3776
+ setInterval(() => {
3777
+ if (!latestData || document.getElementById('sb-status').textContent === 'Offline') {
3778
+ pollFallback();
3779
+ }
3780
+ }, 3000);
3781
+ // Periodically sync recording status & update panel stats
3782
+ setInterval(() => {
3783
+ if (currentTab === 'logs' && isRecording) {
3784
+ syncRecordingStatus();
3785
+ }
3786
+ }, 5000);
3787
+ </script>
3788
+ </body>
3789
+ </html>`;
3790
+ }
3791
+
3792
+ export {
3793
+ formatJson,
3794
+ formatHtml,
3795
+ formatMarkdown,
3796
+ formatCsv,
3797
+ formatTsv,
3798
+ formatGraphs,
3799
+ getDashboardHtml
3800
+ };