pyre-cli 10.1.0 → 10.2.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 (46) hide show
  1. package/README.md +195 -494
  2. package/dist/{checkCmd-RKW6QGDR.js → checkCmd-WQAHAUWO.js} +1 -1
  3. package/dist/{chunk-GHRDW646.js → chunk-23YWCI3O.js} +375 -135
  4. package/dist/{service-Q74BBILO.js → chunk-KFPTD266.js} +43 -3
  5. package/dist/{doctor-GYHFTNZQ.js → chunk-KVUMOA6C.js} +7 -6
  6. package/dist/{chunk-53O2RGSL.js → chunk-QM5L43EK.js} +19 -2
  7. package/dist/{chunk-6UHZP4QP.js → chunk-XTOOEKLM.js} +24 -12
  8. package/dist/{collectors-H43PN4PS.js → collectors-JDN5Y7EV.js} +1 -1
  9. package/dist/{completions-JKYEJNGR.js → completions-ZVTNE52B.js} +15 -11
  10. package/dist/{config-GYGJXU2Q.js → config-J4YMADEF.js} +3 -1
  11. package/dist/doctor-LHFW4UJP.js +10 -0
  12. package/dist/firebaseSync-ON3RZ6KX.js +91 -0
  13. package/dist/index.js +4243 -735
  14. package/dist/monitors/platform/windows-ui.ps1 +42 -0
  15. package/dist/{netUsageCmd-WKBTW2YG.js → netUsageCmd-67IBZZYN.js} +2 -2
  16. package/dist/{pipeCmd-7REDBLBU.js → pipeCmd-2N4JPKYE.js} +1 -1
  17. package/dist/{prometheusCmd-PL64BLN5.js → prometheusCmd-LEUM5WCZ.js} +1 -1
  18. package/dist/serveCmd-L7LVJOZN.js +353 -0
  19. package/dist/service-7GIX3YNC.js +16 -0
  20. package/dist/syncCmd-42IRF633.js +246 -0
  21. package/dist/{topoCmd-DU2HHZNH.js → topoCmd-DZ3YS5IL.js} +1 -1
  22. package/dist/{watchdogCmd-UF53JPZB.js → watchdogCmd-6GR4UN6B.js} +7 -7
  23. package/dist/{wizard-TD7BGX6O.js → wizard-JQMQWFAW.js} +1 -1
  24. package/package.json +4 -3
  25. package/parity.md +2 -0
  26. package/src/completions.ts +15 -11
  27. package/src/easy_features.test.ts +149 -0
  28. package/src/firebaseSync.ts +157 -0
  29. package/src/formatters/html_template.ts +3086 -598
  30. package/src/formatters/render.ts +25 -12
  31. package/src/index.ts +516 -23
  32. package/src/live/input.ts +501 -148
  33. package/src/live/render.ts +163 -18
  34. package/src/live/state.ts +52 -2
  35. package/src/live/types.ts +1 -1
  36. package/src/monitors/collectors.ts +3 -10
  37. package/src/monitors/platform/windows-ui.ps1 +42 -0
  38. package/src/monitors/platform/windows.ts +442 -147
  39. package/src/monitors/types.ts +8 -0
  40. package/src/serveCmd.ts +17 -2
  41. package/src/service.ts +49 -0
  42. package/src/state/config.ts +48 -0
  43. package/src/syncCmd.ts +159 -11
  44. package/src/web/dashboard.ts +205 -0
  45. package/dist/serveCmd-2H5VLF3P.js +0 -153
  46. package/dist/syncCmd-A2W3QB3S.js +0 -126
@@ -4,651 +4,3139 @@ export function getDashboardHtml(): string {
4
4
  <head>
5
5
  <meta charset="UTF-8"/>
6
6
  <meta name="viewport" content="width=device-width,initial-scale=1.0"/>
7
- <title>Pyre</title>
8
- <link rel="preconnect" href="https://fonts.googleapis.com"/>
9
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
10
- <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet"/>
7
+ <title>Activity Monitor</title>
11
8
  <style>
12
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
13
- :root{
14
- --bg: #000000;
15
- --bg1: #0a0a0a;
16
- --bg2: #141414;
17
- --bg3: rgba(255,255,255,0.05);
18
- --border: #333333;
19
- --border2: #444444;
20
- --text: #ffffff;
21
- --text2: #aaaaaa;
22
- --text3: #666666;
23
- --accent: #ffffff;
24
- --accent2: #dddddd;
25
- --cyan: #ffffff;
26
- --green: #ffffff;
27
- --yellow: #bbbbbb;
28
- --red: #dddddd;
29
- --orange: #cccccc;
30
- --purple: #eeeeee;
31
- --glow: rgba(255,255,255,0.15);
32
- --font: 'Inter', -apple-system, sans-serif;
33
- --mono: 'JetBrains Mono', ui-monospace, monospace;
34
- }
35
- html,body{height:100%;overflow:hidden;background:var(--bg);color:var(--text);font-family:var(--font);font-size:12px;}
9
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
10
+ :root {
11
+ --bg-window: #1e1e1e;
12
+ --bg-toolbar: #2c2c2e;
13
+ --bg-surface: #252528;
14
+ --bg-surface-hover: rgba(255, 255, 255, 0.05);
15
+ --bg-table-header: #28282a;
16
+ --bg-table-alt: rgba(255, 255, 255, 0.02);
17
+ --bg-selection: #007aff;
18
+ --text-selection: #ffffff;
19
+ --border-color: #38383a;
20
+ --border-light: rgba(255, 255, 255, 0.1);
21
+ --text-primary: #ffffff;
22
+ --text-secondary: #a1a1a6;
23
+ --text-tertiary: #636366;
24
+ --mac-blue: #007aff;
25
+ --mac-green: #34c759;
26
+ --mac-yellow: #ff9f0a;
27
+ --mac-red: #ff453a;
28
+ --mac-purple: #af52de;
29
+ --mac-teal: #64d2ff;
30
+ --font-system: -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "Helvetica Neue", Helvetica, Arial, sans-serif;
31
+ --font-mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
32
+ }
33
+
34
+ html, body {
35
+ height: 100%;
36
+ width: 100%;
37
+ overflow: hidden;
38
+ background: var(--bg-window);
39
+ color: var(--text-primary);
40
+ font-family: var(--font-system);
41
+ font-size: 12px;
42
+ user-select: none;
43
+ -webkit-user-select: none;
44
+ -webkit-font-smoothing: antialiased;
45
+ }
46
+
36
47
  body {
37
- background-image: radial-gradient(var(--bg3) 1px, transparent 1px);
38
- background-size: 20px 20px;
39
- display:flex;flex-direction:column;height:100vh;
40
- }
41
-
42
- #titlebar{
43
- height:42px;min-height:42px;-webkit-app-region:drag;
44
- display:flex;align-items:center;padding:0 16px;
45
- border-bottom:1px solid var(--border);
46
- background:rgba(5,5,8,0.8);backdrop-filter:blur(10px);
47
- z-index:100;
48
- }
49
- #titlebar-logo{
50
- font-family:var(--mono);font-size:14px;font-weight:700;
51
- color:var(--cyan);margin-right:24px;text-transform:uppercase;
52
- letter-spacing:0.1em;
53
- }
54
- #titlebar-host{font-family:var(--mono);font-size:11px;color:var(--text2);flex:1;}
55
- #conn-badge{display:flex;align-items:center;gap:6px;font-size:11px;font-family:var(--mono);color:var(--text2);-webkit-app-region:no-drag;}
56
- .conn-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);animation:pulse 2s infinite;}
57
- .conn-dot.offline{background:var(--red);box-shadow:0 0 8px var(--red);animation:none;}
58
- @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
59
-
60
- #tabbar{
61
- display:flex;align-items:center;gap:4px;padding:6px 14px 0;
62
- border-bottom:1px solid var(--border);
63
- background:rgba(10,12,22,0.8);overflow-x:auto;-webkit-app-region:no-drag;
64
- }
65
- #tabbar::-webkit-scrollbar{display:none}
66
- .tab{
67
- padding:8px 16px;font-size:11px;font-family:var(--mono);color:var(--text3);
68
- cursor:pointer;white-space:nowrap;user-select:none;
69
- border-bottom:2px solid transparent;transition:all 0.2s;
70
- text-transform:uppercase;font-weight:600;
71
- }
72
- .tab:hover{color:var(--text2);background:var(--bg3);}
73
- .tab.active{color:var(--accent);border-bottom:2px solid var(--accent);background:linear-gradient(0deg, rgba(255,255,255,0.1), transparent);}
74
- .tab-key{opacity:0.5;margin-right:6px;}
75
-
76
- #main{flex:1;overflow-y:auto;overflow-x:hidden;padding:16px;display:flex;flex-direction:column;}
77
- #main::-webkit-scrollbar{width:8px}
78
- #main::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}
79
-
80
- #statusbar{
81
- height:28px;min-height:28px;display:flex;align-items:center;padding:0 16px;gap:20px;
82
- font-size:10px;color:var(--text3);border-top:1px solid var(--border);
83
- background:var(--bg);font-family:var(--mono);
84
- }
85
- .sb-item{display:flex;align-items:center;gap:6px;}
86
- .sb-sep{color:var(--border);}
87
- .up-val{color:var(--text);}
88
-
89
- /* btop cards */
90
- .card{
91
- background:var(--bg1);
92
- border:1px solid var(--border);
93
- border-radius:6px;
94
- padding:16px;
95
- position:relative;
96
- margin-top:12px;
97
- transition:border-color 0.2s, box-shadow 0.2s;
98
- display:flex;flex-direction:column;
99
- }
100
- .card:hover{border-color:var(--cyan);box-shadow:0 0 10px rgba(255,255,255,0.05);}
101
- .card-title{
102
- position:absolute;top:-9px;left:12px;
103
- background:var(--bg);
104
- padding:0 8px;font-size:11px;font-family:var(--mono);font-weight:700;
105
- color:var(--accent);text-transform:uppercase;
106
- display:flex;align-items:center;gap:6px;
107
- }
108
- .card-title::before{content:"";display:block;width:6px;height:6px;border-radius:50%;background:var(--accent);box-shadow:0 0 5px var(--accent);}
109
-
110
- /* Grid System */
111
- .grid-2x2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; flex:1; }
112
- .panel-row { display:flex; gap:12px; margin-bottom:12px; }
113
- .panel-col { display:flex; flex-direction:column; gap:12px; flex:1; }
114
- .detail-split { display:flex; gap:16px; height:100%; }
115
- .detail-left { flex:6; display:flex; flex-direction:column; gap:12px; }
116
- .detail-right { flex:4; display:flex; flex-direction:column; gap:12px; }
117
- .detail-full { display:flex; flex-direction:column; gap:12px; height:100%; }
118
-
119
- /* Stats */
120
- .stat{display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px dashed var(--border);}
121
- .stat:last-child{border-bottom:none;}
122
- .stat-label{font-size:11px;color:var(--text2);}
123
- .stat-val{font-family:var(--mono);font-size:12px;color:var(--text);font-weight:500;}
124
- .big-metric{text-align:center;padding:20px 0;}
125
- .big-val{font-family:var(--mono);font-size:48px;font-weight:700;color:var(--cyan);text-shadow:0 0 15px rgba(255,255,255,0.3);}
126
- .big-unit{font-family:var(--mono);font-size:14px;color:var(--text2);margin-top:8px;}
127
-
128
- /* Gauges */
129
- .gauge-wrap{margin:8px 0;}
130
- .gauge-header{display:flex;justify-content:space-between;margin-bottom:6px;font-size:11px;font-family:var(--mono);}
131
- .gauge-pct{font-weight:600;}
132
- .gauge-track{height:20px;background:var(--bg2);border-radius:4px;overflow:hidden;position:relative;border:1px solid var(--border);}
133
- .gauge-fill{height:100%;transition:width 0.4s ease;display:flex;align-items:center;justify-content:flex-end;padding-right:6px;font-family:var(--mono);font-size:10px;font-weight:700;color:#000;}
134
- .gauge-fill.ok{background:linear-gradient(90deg,var(--text),#999);}
135
- .gauge-fill.warn{background:linear-gradient(90deg,var(--text2),#666);}
136
- .gauge-fill.crit{background:linear-gradient(90deg,var(--text3),#333);}
137
-
138
- /* SVG Rings */
139
- .ring-container { display:flex; justify-content:space-around; padding:20px 0; background:var(--bg1); border:1px solid var(--border); border-radius:6px; margin-bottom:12px; }
140
- .ring-box { display:flex; flex-direction:column; align-items:center; gap:10px; }
141
- .ring-title { font-family:var(--mono); font-size:11px; color:var(--text2); font-weight:700; text-transform:uppercase; }
142
- .svg-ring { position:relative; width:100px; height:100px; }
143
- .svg-ring.large { width:200px; height:200px; }
144
- .svg-ring svg { transform:rotate(-90deg); width:100%; height:100%; }
145
- .ring-center { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; }
146
- .ring-val { font-family:var(--mono); font-size:22px; font-weight:700; }
147
- .svg-ring.large .ring-val { font-size:42px; }
148
-
149
- /* Sparklines */
150
- .spark-wrap { flex:1; min-height:80px; display:flex; flex-direction:column; margin-top:8px; }
151
- .spark-label { font-family:var(--mono); font-size:10px; color:var(--text3); margin-bottom:4px; }
152
- canvas.spark { width:100%; height:80px; display:block; border-radius:4px; background:var(--bg2); border:1px solid var(--border); }
153
- canvas.spark.large { height:200px; }
154
-
155
- /* Core Grid */
156
- .core-grid { display:grid; grid-template-columns:repeat(auto-fit, minmax(30px, 1fr)); gap:6px; margin-top:12px; }
157
- .core-col { display:flex; flex-direction:column; gap:4px; align-items:center; }
158
- .core-bar { width:100%; height:100px; background:var(--bg2); border-radius:4px; display:flex; align-items:flex-end; overflow:hidden; border:1px solid var(--border); }
159
- .core-fill { width:100%; transition:height 0.3s; }
160
- .core-fill.ok { background:linear-gradient(0deg,var(--text),#999); }
161
- .core-fill.warn { background:linear-gradient(0deg,var(--text2),#666); }
162
- .core-fill.crit { background:linear-gradient(0deg,var(--text3),#333); }
163
- .core-lbl { font-family:var(--mono); font-size:9px; color:var(--text2); }
164
-
165
- /* Table */
166
- .table-wrap { flex:1; overflow-y:auto; border:1px solid var(--border); border-radius:6px; background:var(--bg1); }
167
- table.proc { width:100%; border-collapse:collapse; font-family:var(--mono); font-size:11px; text-align:left; }
168
- table.proc thead th { position:sticky; top:0; background:var(--bg2); padding:8px 12px; color:var(--accent); font-weight:700; text-transform:uppercase; border-bottom:1px solid var(--border); cursor:pointer; user-select:none; z-index:10; }
169
- table.proc thead th:hover { color:var(--cyan); background:var(--border); }
170
- table.proc tbody tr { border-bottom:1px solid var(--border); cursor:default; }
171
- table.proc tbody tr:nth-child(even) { background:rgba(255,255,255,0.01); }
172
- table.proc tbody tr:hover { background:var(--bg2); }
173
- table.proc td { padding:6px 12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:250px; }
174
- .proc-cmd { color:var(--text); }
175
- .table-wrap::-webkit-scrollbar { width:8px; }
176
- .table-wrap::-webkit-scrollbar-thumb { background:var(--border); }
177
-
178
- /* Helpers */
179
- .flex-row { display:flex; gap:12px; }
180
- .flex-1 { flex:1; }
181
- .mb-12 { margin-bottom:12px; }
182
- .txt-green { color:var(--green); }
183
- .txt-yellow { color:var(--yellow); }
184
- .txt-red { color:var(--red); }
185
- .txt-cyan { color:var(--cyan); }
186
- .txt-purple { color:var(--purple); }
187
-
188
- /* Chips */
189
- .chips { display:flex; flex-wrap:wrap; gap:8px; margin-top:8px; }
190
- .chip { padding:6px 12px; border-radius:4px; font-family:var(--mono); font-size:11px; border:1px solid var(--border); background:var(--bg2); }
191
- .chip.ok { border-color:var(--green); color:var(--green); }
192
- .chip.warn { border-color:var(--yellow); color:var(--yellow); }
193
- .chip.crit { border-color:var(--red); color:var(--red); }
194
-
195
- .anomaly { display:flex; gap:12px; padding:12px; border:1px solid var(--red); background:rgba(255,255,255,0.1); border-radius:6px; margin-bottom:12px; }
196
- .anomaly-icon { font-size:20px; }
197
- .anomaly-title { font-family:var(--mono); font-weight:700; color:var(--red); font-size:12px; }
198
- .anomaly-desc { font-family:var(--mono); font-size:11px; color:var(--text); margin-top:4px; }
199
-
200
- .view { display:none; height:100%; flex-direction:column; }
201
- .view.active { display:flex; }
202
- #view-content { flex:1; display:flex; flex-direction:column; animation:fadeIn 0.2s ease; height:100%; }
203
- @keyframes fadeIn{from{opacity:0;}to{opacity:1;}}
48
+ display: flex;
49
+ flex-direction: column;
50
+ }
51
+
52
+ /* Unified macOS Toolbar */
53
+ #toolbar {
54
+ height: 52px;
55
+ min-height: 52px;
56
+ background: var(--bg-toolbar);
57
+ border-bottom: 1px solid var(--border-color);
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: space-between;
61
+ padding: 0 14px;
62
+ gap: 12px;
63
+ z-index: 50;
64
+ -webkit-app-region: drag;
65
+ }
66
+
67
+ .toolbar-left, .toolbar-center, .toolbar-right {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 8px;
71
+ -webkit-app-region: no-drag;
72
+ }
73
+
74
+ .toolbar-center {
75
+ flex: 1;
76
+ justify-content: center;
77
+ }
78
+
79
+ /* macOS Toolbar Push Buttons */
80
+ .mac-btn {
81
+ display: inline-flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ gap: 6px;
85
+ height: 28px;
86
+ padding: 0 10px;
87
+ border-radius: 6px;
88
+ border: 1px solid var(--border-color);
89
+ background: #3a3a3c;
90
+ color: var(--text-primary);
91
+ font-family: var(--font-system);
92
+ font-size: 12px;
93
+ font-weight: 500;
94
+ cursor: pointer;
95
+ outline: none;
96
+ transition: background 0.15s, border-color 0.15s, opacity 0.15s;
97
+ }
98
+ .mac-btn:hover:not(:disabled) {
99
+ background: #48484a;
100
+ border-color: #545458;
101
+ }
102
+ .mac-btn:active:not(:disabled) {
103
+ background: #323234;
104
+ }
105
+ .mac-btn:disabled {
106
+ opacity: 0.35;
107
+ cursor: default;
108
+ }
109
+ .mac-btn-icon {
110
+ width: 28px;
111
+ padding: 0;
112
+ }
113
+ .mac-btn-danger {
114
+ background: rgba(255, 69, 58, 0.2);
115
+ border-color: rgba(255, 69, 58, 0.4);
116
+ color: #ff6961;
117
+ }
118
+ .mac-btn-danger:hover:not(:disabled) {
119
+ background: rgba(255, 69, 58, 0.35);
120
+ border-color: var(--mac-red);
121
+ color: #fff;
122
+ }
123
+ .mac-btn-primary {
124
+ background: var(--mac-blue);
125
+ border-color: #0062cc;
126
+ color: #fff;
127
+ }
128
+ .mac-btn-primary:hover:not(:disabled) {
129
+ background: #006ee6;
130
+ }
131
+
132
+ /* macOS Segmented Control Tabs */
133
+ .mac-segmented {
134
+ display: inline-flex;
135
+ background: #1c1c1e;
136
+ border: 1px solid var(--border-color);
137
+ border-radius: 8px;
138
+ padding: 2px;
139
+ gap: 2px;
140
+ }
141
+ .mac-seg-item {
142
+ padding: 4px 14px;
143
+ border-radius: 6px;
144
+ font-size: 12px;
145
+ font-weight: 500;
146
+ color: var(--text-secondary);
147
+ cursor: pointer;
148
+ transition: all 0.18s ease;
149
+ display: flex;
150
+ align-items: center;
151
+ gap: 5px;
152
+ }
153
+ .mac-seg-item:hover:not(.active) {
154
+ color: var(--text-primary);
155
+ background: rgba(255, 255, 255, 0.05);
156
+ }
157
+ .mac-seg-item.active {
158
+ background: #636366;
159
+ color: #ffffff;
160
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
161
+ }
162
+
163
+ /* Filter Dropdown */
164
+ .mac-select {
165
+ height: 28px;
166
+ border-radius: 6px;
167
+ background: #3a3a3c;
168
+ border: 1px solid var(--border-color);
169
+ color: var(--text-primary);
170
+ font-family: var(--font-system);
171
+ font-size: 11px;
172
+ padding: 0 8px;
173
+ outline: none;
174
+ cursor: pointer;
175
+ }
176
+ .mac-select:hover {
177
+ background: #48484a;
178
+ }
179
+
180
+ /* Search Field */
181
+ .mac-search-wrap {
182
+ position: relative;
183
+ display: flex;
184
+ align-items: center;
185
+ }
186
+ .mac-search-icon {
187
+ position: absolute;
188
+ left: 8px;
189
+ width: 13px;
190
+ height: 13px;
191
+ fill: var(--text-secondary);
192
+ pointer-events: none;
193
+ }
194
+ .mac-search-input {
195
+ height: 28px;
196
+ width: 170px;
197
+ background: #1c1c1e;
198
+ border: 1px solid var(--border-color);
199
+ border-radius: 6px;
200
+ padding: 0 24px 0 26px;
201
+ color: var(--text-primary);
202
+ font-family: var(--font-system);
203
+ font-size: 12px;
204
+ outline: none;
205
+ transition: width 0.2s, border-color 0.2s;
206
+ }
207
+ .mac-search-input:focus {
208
+ width: 210px;
209
+ border-color: var(--mac-blue);
210
+ box-shadow: 0 0 0 1px var(--mac-blue);
211
+ }
212
+ .mac-search-clear {
213
+ position: absolute;
214
+ right: 6px;
215
+ width: 14px;
216
+ height: 14px;
217
+ border-radius: 50%;
218
+ background: var(--text-tertiary);
219
+ color: #000;
220
+ display: none;
221
+ align-items: center;
222
+ justify-content: center;
223
+ font-size: 10px;
224
+ cursor: pointer;
225
+ line-height: 1;
226
+ }
227
+
228
+ /* Session Recorder Button & Pulsing Dot */
229
+ .mac-btn-record {
230
+ font-family: var(--font-mono);
231
+ font-size: 11px;
232
+ gap: 7px;
233
+ padding: 0 10px;
234
+ border-color: var(--border-color);
235
+ background: #323235;
236
+ }
237
+ .mac-btn-record:hover:not(:disabled) {
238
+ background: #3e3e42;
239
+ }
240
+ .mac-btn-record.recording {
241
+ background: rgba(255, 69, 58, 0.22);
242
+ border-color: var(--mac-red);
243
+ color: #ff6961;
244
+ }
245
+ .rec-dot {
246
+ width: 8px;
247
+ height: 8px;
248
+ border-radius: 50%;
249
+ background: #777;
250
+ display: inline-block;
251
+ transition: background 0.2s;
252
+ }
253
+ .mac-btn-record.recording .rec-dot {
254
+ background: #ff453a;
255
+ box-shadow: 0 0 8px #ff453a;
256
+ animation: rec-pulse 1.2s infinite alternate ease-in-out;
257
+ }
258
+ @keyframes rec-pulse {
259
+ from { opacity: 0.45; transform: scale(0.9); }
260
+ to { opacity: 1; transform: scale(1.2); }
261
+ }
262
+
263
+ /* Export Snapshot Dropdown Menu */
264
+ .mac-dropdown-wrap {
265
+ position: relative;
266
+ display: inline-flex;
267
+ }
268
+ .mac-dropdown-menu {
269
+ position: absolute;
270
+ top: 32px;
271
+ left: 0;
272
+ min-width: 165px;
273
+ background: #2c2c2e;
274
+ border: 1px solid var(--border-color);
275
+ border-radius: 8px;
276
+ box-shadow: 0 8px 24px rgba(0,0,0,0.65);
277
+ padding: 5px;
278
+ display: none;
279
+ flex-direction: column;
280
+ z-index: 200;
281
+ }
282
+ .mac-dropdown-menu.show {
283
+ display: flex;
284
+ }
285
+ .mac-menu-item {
286
+ padding: 6px 12px;
287
+ font-size: 11.5px;
288
+ color: var(--text-primary);
289
+ border-radius: 5px;
290
+ cursor: pointer;
291
+ transition: background 0.15s;
292
+ display: flex;
293
+ align-items: center;
294
+ gap: 8px;
295
+ }
296
+ .mac-menu-item:hover {
297
+ background: var(--mac-blue);
298
+ color: #fff;
299
+ }
300
+
301
+ /* Logs View & Stream Table */
302
+ #logs-view {
303
+ flex: 1;
304
+ display: none;
305
+ flex-direction: column;
306
+ overflow: hidden;
307
+ height: 100%;
308
+ }
309
+ .logs-sub-toolbar {
310
+ height: 38px;
311
+ min-height: 38px;
312
+ background: var(--bg-table-header);
313
+ border-bottom: 1px solid var(--border-color);
314
+ display: flex;
315
+ align-items: center;
316
+ justify-content: space-between;
317
+ padding: 0 12px;
318
+ gap: 10px;
319
+ }
320
+ .logs-sub-left, .logs-sub-right {
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 8px;
324
+ }
325
+ .logs-scroll-area {
326
+ flex: 1;
327
+ overflow-y: auto;
328
+ background: var(--bg-window);
329
+ }
330
+ .log-table {
331
+ width: 100%;
332
+ border-collapse: collapse;
333
+ font-size: 11px;
334
+ }
335
+ .log-table th {
336
+ position: sticky;
337
+ top: 0;
338
+ background: #252528;
339
+ color: var(--text-secondary);
340
+ font-weight: 600;
341
+ text-align: left;
342
+ padding: 6px 10px;
343
+ border-bottom: 1px solid var(--border-color);
344
+ white-space: nowrap;
345
+ z-index: 5;
346
+ }
347
+ .log-table td {
348
+ padding: 6px 10px;
349
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
350
+ vertical-align: middle;
351
+ font-family: var(--font-mono);
352
+ }
353
+ .log-table tr:hover {
354
+ background: var(--bg-surface-hover);
355
+ }
356
+ .log-badge {
357
+ display: inline-flex;
358
+ align-items: center;
359
+ justify-content: center;
360
+ padding: 1px 7px;
361
+ border-radius: 4px;
362
+ font-size: 10px;
363
+ font-weight: 700;
364
+ letter-spacing: 0.03em;
365
+ text-transform: uppercase;
366
+ }
367
+ .log-badge.info { background: rgba(0, 122, 255, 0.18); color: #64b5f6; border: 1px solid rgba(0, 122, 255, 0.3); }
368
+ .log-badge.warn { background: rgba(255, 159, 10, 0.18); color: #ffb74d; border: 1px solid rgba(255, 159, 10, 0.35); }
369
+ .log-badge.crit { background: rgba(255, 69, 58, 0.22); color: #ff6e6e; border: 1px solid rgba(255, 69, 58, 0.45); }
370
+ .log-badge.action { background: rgba(175, 82, 222, 0.2); color: #d084f7; border: 1px solid rgba(175, 82, 222, 0.4); }
371
+ .log-badge.watchdog { background: rgba(52, 199, 89, 0.2); color: #81c784; border: 1px solid rgba(52, 199, 89, 0.4); }
372
+
373
+ .log-subsys {
374
+ font-weight: 600;
375
+ color: var(--text-secondary);
376
+ }
377
+ .log-msg {
378
+ color: var(--text-primary);
379
+ font-family: var(--font-system);
380
+ font-size: 11.5px;
381
+ }
382
+ .log-ctx {
383
+ color: var(--text-tertiary);
384
+ font-size: 10.5px;
385
+ }
386
+
387
+ /* History Explorer Table */
388
+ .history-table {
389
+ width: 100%;
390
+ border-collapse: collapse;
391
+ font-size: 11px;
392
+ }
393
+ .history-table th {
394
+ color: var(--text-secondary);
395
+ font-weight: 600;
396
+ text-align: left;
397
+ padding: 4px 8px;
398
+ border-bottom: 1px solid var(--border-color);
399
+ }
400
+ .history-table td {
401
+ padding: 5px 8px;
402
+ border-bottom: 1px solid rgba(255, 255, 255, 0.04);
403
+ }
404
+ .history-link {
405
+ color: var(--mac-blue);
406
+ cursor: pointer;
407
+ text-decoration: none;
408
+ }
409
+ .history-link:hover {
410
+ text-decoration: underline;
411
+ }
412
+
413
+ /* Main Split View */
414
+ #split-view {
415
+ flex: 1;
416
+ display: flex;
417
+ flex-direction: column;
418
+ overflow: hidden;
419
+ position: relative;
420
+ }
421
+
422
+ /* Top Process Table Area */
423
+ #table-container {
424
+ flex: 62;
425
+ min-height: 160px;
426
+ overflow-y: auto;
427
+ overflow-x: auto;
428
+ background: var(--bg-window);
429
+ position: relative;
430
+ }
431
+ #table-container::-webkit-scrollbar {
432
+ width: 10px;
433
+ height: 10px;
434
+ }
435
+ #table-container::-webkit-scrollbar-track {
436
+ background: var(--bg-window);
437
+ }
438
+ #table-container::-webkit-scrollbar-thumb {
439
+ background: #424245;
440
+ border-radius: 5px;
441
+ border: 2px solid var(--bg-window);
442
+ }
443
+
444
+ table.mac-table {
445
+ width: 100%;
446
+ border-collapse: collapse;
447
+ font-size: 11px;
448
+ table-layout: fixed;
449
+ }
450
+ table.mac-table thead {
451
+ position: sticky;
452
+ top: 0;
453
+ z-index: 10;
454
+ background: var(--bg-table-header);
455
+ }
456
+ table.mac-table th {
457
+ padding: 6px 10px;
458
+ text-align: right;
459
+ font-weight: 600;
460
+ color: var(--text-secondary);
461
+ border-bottom: 1px solid var(--border-color);
462
+ border-right: 1px solid rgba(255, 255, 255, 0.05);
463
+ cursor: pointer;
464
+ white-space: nowrap;
465
+ overflow: hidden;
466
+ text-overflow: ellipsis;
467
+ }
468
+ table.mac-table th:first-child {
469
+ text-align: left;
470
+ }
471
+ table.mac-table th:hover {
472
+ background: #323235;
473
+ color: var(--text-primary);
474
+ }
475
+ table.mac-table th .sort-caret {
476
+ display: inline-block;
477
+ margin-left: 4px;
478
+ font-size: 8px;
479
+ color: var(--mac-blue);
480
+ }
481
+
482
+ table.mac-table tbody tr {
483
+ height: 22px;
484
+ border-bottom: 1px solid rgba(255, 255, 255, 0.03);
485
+ cursor: default;
486
+ }
487
+ table.mac-table tbody tr:nth-child(even) {
488
+ background: var(--bg-table-alt);
489
+ }
490
+ table.mac-table tbody tr:hover {
491
+ background: var(--bg-surface-hover);
492
+ }
493
+ table.mac-table tbody tr.selected {
494
+ background: var(--bg-selection) !important;
495
+ color: var(--text-selection) !important;
496
+ }
497
+ table.mac-table tbody tr.selected td {
498
+ color: var(--text-selection) !important;
499
+ }
500
+
501
+ table.mac-table td {
502
+ padding: 3px 10px;
503
+ white-space: nowrap;
504
+ overflow: hidden;
505
+ text-overflow: ellipsis;
506
+ text-align: right;
507
+ font-family: var(--font-mono);
508
+ font-variant-numeric: tabular-nums;
509
+ color: var(--text-primary);
510
+ }
511
+ table.mac-table.density-compact td {
512
+ padding: 1px 7px;
513
+ font-size: 10px;
514
+ }
515
+ table.mac-table.density-comfortable td {
516
+ padding: 6px 12px;
517
+ font-size: 12px;
518
+ }
519
+ table.mac-table td.col-name {
520
+ text-align: left;
521
+ font-family: var(--font-system);
522
+ display: flex;
523
+ align-items: center;
524
+ gap: 7px;
525
+ }
526
+ .proc-icon {
527
+ width: 14px;
528
+ height: 14px;
529
+ flex-shrink: 0;
530
+ display: inline-flex;
531
+ align-items: center;
532
+ justify-content: center;
533
+ border-radius: 3px;
534
+ background: #3a3a3c;
535
+ color: #fff;
536
+ font-size: 9px;
537
+ font-weight: 700;
538
+ }
539
+ .proc-icon.system { background: #5856d6; }
540
+ .proc-icon.user { background: #007aff; }
541
+
542
+ /* Horizontal Split Divider */
543
+ #split-divider {
544
+ height: 4px;
545
+ min-height: 4px;
546
+ background: #2a2a2d;
547
+ border-top: 1px solid var(--border-color);
548
+ border-bottom: 1px solid var(--border-color);
549
+ cursor: row-resize;
550
+ position: relative;
551
+ z-index: 20;
552
+ }
553
+ #split-divider::after {
554
+ content: "";
555
+ position: absolute;
556
+ left: 50%;
557
+ top: 1px;
558
+ transform: translateX(-50%);
559
+ width: 28px;
560
+ height: 2px;
561
+ background: #4a4a4d;
562
+ border-radius: 1px;
563
+ }
564
+
565
+ /* Bottom Hardware Dashboard Panels */
566
+ #bottom-panel {
567
+ flex: 38;
568
+ min-height: 160px;
569
+ background: var(--bg-surface);
570
+ display: flex;
571
+ flex-direction: column;
572
+ overflow: hidden;
573
+ padding: 12px 16px;
574
+ position: relative;
575
+ }
576
+
577
+ .panel-content {
578
+ display: none;
579
+ height: 100%;
580
+ width: 100%;
581
+ gap: 20px;
582
+ }
583
+ .panel-content.active {
584
+ display: flex;
585
+ }
586
+
587
+ /* Left / Right Pane Layout */
588
+ .pane-col {
589
+ display: flex;
590
+ flex-direction: column;
591
+ justify-content: space-between;
592
+ }
593
+ .pane-stats {
594
+ width: 270px;
595
+ flex-shrink: 0;
596
+ display: flex;
597
+ flex-direction: column;
598
+ gap: 6px;
599
+ justify-content: center;
600
+ }
601
+ .pane-graph {
602
+ flex: 1;
603
+ display: flex;
604
+ flex-direction: column;
605
+ position: relative;
606
+ border: 1px solid var(--border-color);
607
+ border-radius: 6px;
608
+ background: #19191b;
609
+ padding: 8px;
610
+ overflow: hidden;
611
+ }
612
+
613
+ /* Stat Rows */
614
+ .stat-item {
615
+ display: flex;
616
+ justify-content: space-between;
617
+ align-items: center;
618
+ font-size: 11px;
619
+ }
620
+ .stat-k {
621
+ color: var(--text-secondary);
622
+ font-weight: 500;
623
+ }
624
+ .stat-v {
625
+ font-family: var(--font-mono);
626
+ font-weight: 600;
627
+ color: var(--text-primary);
628
+ font-variant-numeric: tabular-nums;
629
+ }
630
+
631
+ /* Canvas Graph Styling */
632
+ .graph-header {
633
+ display: flex;
634
+ justify-content: space-between;
635
+ align-items: center;
636
+ margin-bottom: 4px;
637
+ }
638
+ .graph-title {
639
+ font-size: 11px;
640
+ font-weight: 600;
641
+ color: var(--text-secondary);
642
+ text-transform: uppercase;
643
+ letter-spacing: 0.04em;
644
+ }
645
+ .graph-legend {
646
+ display: flex;
647
+ align-items: center;
648
+ gap: 12px;
649
+ font-size: 10px;
650
+ color: var(--text-secondary);
651
+ }
652
+ .legend-pill {
653
+ display: inline-flex;
654
+ align-items: center;
655
+ gap: 4px;
656
+ }
657
+ .legend-dot {
658
+ width: 7px;
659
+ height: 7px;
660
+ border-radius: 50%;
661
+ }
662
+ canvas.mac-canvas {
663
+ width: 100%;
664
+ height: 100%;
665
+ flex: 1;
666
+ display: block;
667
+ }
668
+
669
+ /* Memory Pressure Badge */
670
+ .pressure-meter {
671
+ display: flex;
672
+ align-items: center;
673
+ gap: 8px;
674
+ margin-top: 4px;
675
+ padding: 6px 10px;
676
+ border-radius: 6px;
677
+ background: #1c1c1e;
678
+ border: 1px solid var(--border-color);
679
+ }
680
+ .pressure-dot {
681
+ width: 10px;
682
+ height: 10px;
683
+ border-radius: 50%;
684
+ background: var(--mac-green);
685
+ box-shadow: 0 0 8px var(--mac-green);
686
+ }
687
+ .pressure-dot.warn { background: var(--mac-yellow); box-shadow: 0 0 8px var(--mac-yellow); }
688
+ .pressure-dot.crit { background: var(--mac-red); box-shadow: 0 0 8px var(--mac-red); }
689
+ .pressure-text {
690
+ font-weight: 600;
691
+ font-size: 11px;
692
+ }
693
+
694
+ /* Status Bar */
695
+ #statusbar {
696
+ height: 24px;
697
+ min-height: 24px;
698
+ background: #1a1a1c;
699
+ border-top: 1px solid var(--border-color);
700
+ display: flex;
701
+ align-items: center;
702
+ justify-content: space-between;
703
+ padding: 0 14px;
704
+ font-size: 11px;
705
+ color: var(--text-secondary);
706
+ z-index: 40;
707
+ }
708
+ .sb-left, .sb-right {
709
+ display: flex;
710
+ align-items: center;
711
+ gap: 12px;
712
+ }
713
+ .sb-live-indicator {
714
+ display: inline-flex;
715
+ align-items: center;
716
+ gap: 5px;
717
+ }
718
+ .sb-live-dot {
719
+ width: 6px;
720
+ height: 6px;
721
+ border-radius: 50%;
722
+ background: var(--mac-green);
723
+ }
724
+ .sb-live-dot.offline {
725
+ background: var(--mac-red);
726
+ }
727
+
728
+ /* Modal Sheets */
729
+ .modal-backdrop {
730
+ position: fixed;
731
+ inset: 0;
732
+ background: rgba(0, 0, 0, 0.55);
733
+ backdrop-filter: blur(6px);
734
+ -webkit-backdrop-filter: blur(6px);
735
+ display: none;
736
+ align-items: center;
737
+ justify-content: center;
738
+ z-index: 1000;
739
+ }
740
+ .modal-backdrop.open {
741
+ display: flex;
742
+ animation: fadeIn 0.15s ease-out;
743
+ }
744
+ .mac-sheet {
745
+ background: #252528;
746
+ border: 1px solid var(--border-color);
747
+ box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7);
748
+ border-radius: 10px;
749
+ overflow: hidden;
750
+ max-width: 90vw;
751
+ animation: slideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1);
752
+ }
753
+ .modal-quit-box { width: 380px; padding: 20px; text-align: center; }
754
+ .modal-inspect-box { width: 520px; }
755
+ .modal-settings-box {
756
+ width: 680px;
757
+ height: 540px;
758
+ max-height: 90vh;
759
+ display: flex;
760
+ flex-direction: column;
761
+ }
762
+ .settings-tabs {
763
+ display: flex;
764
+ background: #1c1c1e;
765
+ border-bottom: 1px solid var(--border-color);
766
+ padding: 0 10px;
767
+ gap: 4px;
768
+ }
769
+ .settings-tab-btn {
770
+ padding: 9px 13px;
771
+ font-size: 11.5px;
772
+ font-weight: 500;
773
+ color: var(--text-secondary);
774
+ background: transparent;
775
+ border: none;
776
+ border-bottom: 2px solid transparent;
777
+ cursor: pointer;
778
+ display: inline-flex;
779
+ align-items: center;
780
+ gap: 6px;
781
+ transition: all 0.15s;
782
+ }
783
+ .settings-tab-btn:hover {
784
+ color: var(--text-primary);
785
+ }
786
+ .settings-tab-btn.active {
787
+ color: #fff;
788
+ border-bottom-color: var(--mac-blue);
789
+ font-weight: 600;
790
+ }
791
+ .settings-content-wrap {
792
+ flex: 1;
793
+ overflow-y: auto;
794
+ padding: 16px 20px;
795
+ background: var(--bg-window);
796
+ }
797
+ .settings-pane {
798
+ display: none;
799
+ flex-direction: column;
800
+ gap: 14px;
801
+ }
802
+ .settings-pane.active {
803
+ display: flex;
804
+ }
805
+ .settings-card {
806
+ background: #252528;
807
+ border: 1px solid var(--border-color);
808
+ border-radius: 8px;
809
+ padding: 12px 14px;
810
+ display: flex;
811
+ flex-direction: column;
812
+ gap: 10px;
813
+ }
814
+ .settings-card-title {
815
+ font-size: 12px;
816
+ font-weight: 600;
817
+ color: var(--text-primary);
818
+ margin-bottom: 2px;
819
+ display: flex;
820
+ align-items: center;
821
+ gap: 6px;
822
+ }
823
+ .settings-item {
824
+ display: flex;
825
+ align-items: center;
826
+ justify-content: space-between;
827
+ gap: 12px;
828
+ }
829
+ .settings-label-wrap {
830
+ display: flex;
831
+ flex-direction: column;
832
+ gap: 2px;
833
+ }
834
+ .settings-label {
835
+ font-size: 11.5px;
836
+ font-weight: 500;
837
+ color: var(--text-primary);
838
+ }
839
+ .settings-sublabel {
840
+ font-size: 10px;
841
+ color: var(--text-tertiary);
842
+ }
843
+ .settings-control {
844
+ display: flex;
845
+ align-items: center;
846
+ gap: 8px;
847
+ }
848
+ /* macOS style toggle switch */
849
+ .mac-switch {
850
+ position: relative;
851
+ display: inline-block;
852
+ width: 38px;
853
+ height: 22px;
854
+ }
855
+ .mac-switch input {
856
+ opacity: 0;
857
+ width: 0;
858
+ height: 0;
859
+ }
860
+ .mac-slider {
861
+ position: absolute;
862
+ cursor: pointer;
863
+ inset: 0;
864
+ background-color: #3e3e42;
865
+ transition: 0.2s;
866
+ border-radius: 22px;
867
+ border: 1px solid var(--border-color);
868
+ }
869
+ .mac-slider:before {
870
+ position: absolute;
871
+ content: "";
872
+ height: 16px;
873
+ width: 16px;
874
+ left: 2px;
875
+ bottom: 2px;
876
+ background-color: #fff;
877
+ transition: 0.2s;
878
+ border-radius: 50%;
879
+ box-shadow: 0 1px 3px rgba(0,0,0,0.4);
880
+ }
881
+ input:checked + .mac-slider {
882
+ background-color: var(--mac-blue);
883
+ border-color: var(--mac-blue);
884
+ }
885
+ input:checked + .mac-slider:before {
886
+ transform: translateX(16px);
887
+ }
888
+ .settings-input {
889
+ background: #1e1e20;
890
+ border: 1px solid var(--border-color);
891
+ border-radius: 5px;
892
+ color: var(--text-primary);
893
+ padding: 4px 8px;
894
+ font-size: 11.5px;
895
+ outline: none;
896
+ }
897
+ .settings-input:focus {
898
+ border-color: var(--mac-blue);
899
+ }
900
+ .status-pill {
901
+ display: inline-flex;
902
+ align-items: center;
903
+ gap: 5px;
904
+ padding: 2px 8px;
905
+ border-radius: 12px;
906
+ font-size: 10.5px;
907
+ font-weight: 600;
908
+ }
909
+ .status-pill.active {
910
+ background: rgba(52, 199, 89, 0.2);
911
+ color: #34c759;
912
+ border: 1px solid rgba(52, 199, 89, 0.35);
913
+ }
914
+ .status-pill.inactive {
915
+ background: rgba(255, 159, 10, 0.2);
916
+ color: #ff9f0a;
917
+ border: 1px solid rgba(255, 159, 10, 0.35);
918
+ }
919
+
920
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
921
+ @keyframes slideDown { from { transform: translateY(-16px) scale(0.98); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } }
922
+
923
+ .sheet-header {
924
+ height: 40px;
925
+ background: #2c2c2e;
926
+ border-bottom: 1px solid var(--border-color);
927
+ display: flex;
928
+ align-items: center;
929
+ justify-content: space-between;
930
+ padding: 0 14px;
931
+ }
932
+ .sheet-title {
933
+ font-weight: 600;
934
+ font-size: 13px;
935
+ color: var(--text-primary);
936
+ }
937
+ .sheet-close-btn {
938
+ background: transparent;
939
+ border: none;
940
+ color: var(--text-secondary);
941
+ font-size: 16px;
942
+ cursor: pointer;
943
+ padding: 4px;
944
+ }
945
+ .sheet-close-btn:hover { color: #fff; }
946
+ .sheet-body {
947
+ padding: 16px;
948
+ max-height: 65vh;
949
+ overflow-y: auto;
950
+ }
951
+ .sheet-footer {
952
+ padding: 12px 16px;
953
+ background: #202022;
954
+ border-top: 1px solid var(--border-color);
955
+ display: flex;
956
+ justify-content: flex-end;
957
+ gap: 10px;
958
+ }
959
+
960
+ /* Quit Sheet Specific */
961
+ .quit-icon-wrap {
962
+ margin: 0 auto 12px;
963
+ width: 54px;
964
+ height: 54px;
965
+ }
966
+ .quit-title {
967
+ font-size: 14px;
968
+ font-weight: 700;
969
+ margin-bottom: 6px;
970
+ }
971
+ .quit-desc {
972
+ font-size: 12px;
973
+ color: var(--text-secondary);
974
+ line-height: 1.4;
975
+ margin-bottom: 20px;
976
+ }
977
+
978
+ /* Toast */
979
+ #toast {
980
+ position: fixed;
981
+ bottom: 34px;
982
+ left: 50%;
983
+ transform: translateX(-50%) translateY(20px);
984
+ background: rgba(40, 40, 44, 0.95);
985
+ border: 1px solid var(--border-color);
986
+ color: #fff;
987
+ padding: 8px 16px;
988
+ border-radius: 8px;
989
+ font-size: 12px;
990
+ box-shadow: 0 8px 24px rgba(0,0,0,0.5);
991
+ opacity: 0;
992
+ transition: all 0.25s ease;
993
+ pointer-events: none;
994
+ z-index: 2000;
995
+ }
996
+ #toast.show {
997
+ transform: translateX(-50%) translateY(0);
998
+ opacity: 1;
999
+ }
1000
+
1001
+ /* Disk Mount Meter */
1002
+ .disk-bar {
1003
+ height: 8px;
1004
+ border-radius: 4px;
1005
+ background: #1c1c1e;
1006
+ overflow: hidden;
1007
+ margin-top: 3px;
1008
+ border: 1px solid var(--border-color);
1009
+ }
1010
+ .disk-bar-fill {
1011
+ height: 100%;
1012
+ background: linear-gradient(90deg, var(--mac-blue), #5ac8fa);
1013
+ }
1014
+ </style>
1015
+ </head>
1016
+ <body>
1017
+
1018
+ <!-- Unified macOS Toolbar -->
1019
+ <header id="toolbar">
1020
+ <div class="toolbar-left">
1021
+ <!-- Stop process button (octagon with ✕) -->
1022
+ <button id="btn-stop" class="mac-btn mac-btn-icon mac-btn-danger" title="Force a process to quit" disabled onclick="confirmQuitSelected()">
1023
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor">
1024
+ <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"/>
1025
+ </svg>
1026
+ </button>
1027
+
1028
+ <!-- Inspect process button (ⓘ) -->
1029
+ <button id="btn-inspect" class="mac-btn mac-btn-icon" title="Inspect a process" disabled onclick="openInspectorSelected()">
1030
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor">
1031
+ <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"/>
1032
+ </svg>
1033
+ </button>
1034
+
1035
+ <!-- Continuous CSV Session Recorder button -->
1036
+ <button id="btn-record" class="mac-btn mac-btn-record" title="Start continuous CSV logging session to ./pyre-exports (Shortcut: R)" onclick="toggleWebRecording()">
1037
+ <span class="rec-dot" id="rec-dot"></span>
1038
+ <span id="rec-label">Record</span>
1039
+ </button>
1040
+
1041
+ <!-- Quick Snapshot Exporter -->
1042
+ <div class="mac-dropdown-wrap">
1043
+ <button id="btn-export" class="mac-btn" title="Export current telemetry snapshot" onclick="toggleExportMenu(event)">
1044
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor">
1045
+ <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"/>
1046
+ </svg>
1047
+ <span>Export ▾</span>
1048
+ </button>
1049
+ <div class="mac-dropdown-menu" id="export-menu">
1050
+ <div class="mac-menu-item" onclick="downloadSnapshot('json')">📄 Snapshot JSON</div>
1051
+ <div class="mac-menu-item" onclick="downloadSnapshot('csv')">📊 Snapshot CSV</div>
1052
+ <div class="mac-menu-item" onclick="downloadSnapshot('tsv')">📋 Snapshot TSV</div>
1053
+ <div class="mac-menu-item" onclick="downloadSnapshot('md')">📝 Snapshot Markdown</div>
1054
+ </div>
1055
+ </div>
1056
+ </div>
1057
+
1058
+ <!-- Segmented Control Tabs (Center) -->
1059
+ <div class="toolbar-center">
1060
+ <nav class="mac-segmented" role="tablist">
1061
+ <div class="mac-seg-item active" data-tab="cpu" onclick="switchTab('cpu')">CPU</div>
1062
+ <div class="mac-seg-item" data-tab="memory" onclick="switchTab('memory')">Memory</div>
1063
+ <div class="mac-seg-item" data-tab="energy" onclick="switchTab('energy')">Energy</div>
1064
+ <div class="mac-seg-item" data-tab="disk" onclick="switchTab('disk')">Disk</div>
1065
+ <div class="mac-seg-item" data-tab="network" onclick="switchTab('network')">Network</div>
1066
+ <div class="mac-seg-item" data-tab="gpu" onclick="switchTab('gpu')">GPU</div>
1067
+ <div class="mac-seg-item" data-tab="logs" onclick="switchTab('logs')">Logs</div>
1068
+ </nav>
1069
+ </div>
1070
+
1071
+ <div class="toolbar-right">
1072
+ <!-- View Filter Menu -->
1073
+ <select id="proc-filter" class="mac-select" onchange="onFilterChange(this.value)" title="Filter processes">
1074
+ <option value="all">All Processes</option>
1075
+ <option value="my">My Processes</option>
1076
+ <option value="active">Active Processes</option>
1077
+ <option value="system">System Processes</option>
1078
+ </select>
1079
+
1080
+ <!-- Search Input Field -->
1081
+ <div class="mac-search-wrap">
1082
+ <svg class="mac-search-icon" viewBox="0 0 24 24">
1083
+ <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"/>
1084
+ </svg>
1085
+ <input id="search-input" class="mac-search-input" type="text" placeholder="Search" oninput="onSearchInput(this.value)" autocomplete="off" spellcheck="false"/>
1086
+ <div id="search-clear" class="mac-search-clear" onclick="clearSearch()">✕</div>
1087
+ </div>
1088
+
1089
+ <!-- Settings / Preferences Button -->
1090
+ <button id="btn-settings" class="mac-btn mac-btn-icon" title="Preferences (Cmd+,)" onclick="openSettingsModal()">
1091
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor">
1092
+ <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"/>
1093
+ </svg>
1094
+ </button>
1095
+ </div>
1096
+ </header>
1097
+
1098
+ <!-- Main Split View -->
1099
+ <div id="split-view">
1100
+ <!-- Top: Dynamic Process Table & Logs View -->
1101
+ <main id="table-container" tabindex="0">
1102
+ <table class="mac-table" id="proc-table">
1103
+ <thead id="proc-thead">
1104
+ <!-- Columns will be injected based on active tab -->
1105
+ </thead>
1106
+ <tbody id="proc-tbody">
1107
+ <!-- Process rows injected here -->
1108
+ </tbody>
1109
+ </table>
1110
+
1111
+ <!-- Dedicated Logs & Audit Stream Container -->
1112
+ <div id="logs-view">
1113
+ <div class="logs-sub-toolbar">
1114
+ <div class="logs-sub-left">
1115
+ <select id="log-filter-level" class="mac-select" onchange="onLogLevelFilterChange(this.value)" title="Filter log severity">
1116
+ <option value="all">All Severities</option>
1117
+ <option value="INFO">Info</option>
1118
+ <option value="WARN">Warnings</option>
1119
+ <option value="CRIT">Critical</option>
1120
+ <option value="ACTION">User Actions</option>
1121
+ <option value="WATCHDOG">Watchdog</option>
1122
+ </select>
1123
+ <select id="log-filter-subsys" class="mac-select" onchange="onLogSubsysFilterChange(this.value)" title="Filter subsystem">
1124
+ <option value="all">All Subsystems</option>
1125
+ <option value="SYSTEM">System</option>
1126
+ <option value="CPU">CPU</option>
1127
+ <option value="MEM">Memory</option>
1128
+ <option value="DISK">Disk</option>
1129
+ <option value="NET">Network</option>
1130
+ <option value="THERMAL">Thermal</option>
1131
+ <option value="PROCESS">Process</option>
1132
+ <option value="LOGGER">Recorder</option>
1133
+ </select>
1134
+ <div class="mac-search-wrap">
1135
+ <svg class="mac-search-icon" viewBox="0 0 24 24">
1136
+ <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"/>
1137
+ </svg>
1138
+ <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"/>
1139
+ </div>
1140
+ </div>
1141
+ <div class="logs-sub-right">
1142
+ <button id="btn-autoscroll" class="mac-btn" onclick="toggleAutoScroll()" style="font-size:11px;color:var(--mac-green);">Auto-scroll: ON</button>
1143
+ <button class="mac-btn" onclick="clearLogs()" style="font-size:11px;">Clear</button>
1144
+ <button class="mac-btn mac-btn-primary" onclick="downloadLogsExport()" style="font-size:11px;">Export Logs JSON</button>
1145
+ </div>
1146
+ </div>
1147
+ <div class="logs-scroll-area" id="logs-scroll-area">
1148
+ <table class="log-table" id="logs-table">
1149
+ <thead>
1150
+ <tr>
1151
+ <th style="width:130px;">Time</th>
1152
+ <th style="width:85px;">Severity</th>
1153
+ <th style="width:90px;">Subsystem</th>
1154
+ <th>Event Message</th>
1155
+ <th style="width:220px;text-align:right;">Context / Telemetry</th>
1156
+ </tr>
1157
+ </thead>
1158
+ <tbody id="logs-tbody">
1159
+ <!-- Dynamic logs inserted here -->
1160
+ </tbody>
1161
+ </table>
1162
+ </div>
1163
+ </div>
1164
+ </main>
1165
+
1166
+ <!-- Horizontal Splitter Divider -->
1167
+ <div id="split-divider"></div>
1168
+
1169
+ <!-- Bottom: Activity Monitor Hardware Panels -->
1170
+ <footer id="bottom-panel">
1171
+ <!-- 1. CPU Panel -->
1172
+ <div class="panel-content active" id="panel-cpu">
1173
+ <div class="pane-stats">
1174
+ <div class="stat-item"><span class="stat-k">% System:</span><span class="stat-v" id="cpu-stat-system">0.0%</span></div>
1175
+ <div class="stat-item"><span class="stat-k">% User:</span><span class="stat-v" id="cpu-stat-user">0.0%</span></div>
1176
+ <div class="stat-item"><span class="stat-k">% Idle:</span><span class="stat-v" id="cpu-stat-idle">100.0%</span></div>
1177
+ <div style="height:4px;border-bottom:1px solid var(--border-color);"></div>
1178
+ <div class="stat-item"><span class="stat-k">Threads:</span><span class="stat-v" id="cpu-stat-threads">0</span></div>
1179
+ <div class="stat-item"><span class="stat-k">Processes:</span><span class="stat-v" id="cpu-stat-procs">0</span></div>
1180
+ <div class="stat-item"><span class="stat-k">Processor:</span><span class="stat-v" id="cpu-stat-brand" style="font-size:10px;">–</span></div>
1181
+ </div>
1182
+ <div class="pane-graph">
1183
+ <div class="graph-header">
1184
+ <span class="graph-title">CPU Load</span>
1185
+ <div class="graph-legend">
1186
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-blue)"></span> User</span>
1187
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-red)"></span> System</span>
1188
+ </div>
1189
+ </div>
1190
+ <canvas class="mac-canvas" id="canvas-cpu"></canvas>
1191
+ </div>
1192
+ </div>
1193
+
1194
+ <!-- 2. Memory Panel -->
1195
+ <div class="panel-content" id="panel-memory">
1196
+ <div class="pane-stats">
1197
+ <div class="stat-item"><span class="stat-k">Physical Memory:</span><span class="stat-v" id="mem-stat-phys">–</span></div>
1198
+ <div class="stat-item"><span class="stat-k">Memory Used:</span><span class="stat-v" id="mem-stat-used">–</span></div>
1199
+ <div class="stat-item"><span class="stat-k">App Memory:</span><span class="stat-v" id="mem-stat-app">–</span></div>
1200
+ <div class="stat-item"><span class="stat-k">Wired Memory:</span><span class="stat-v" id="mem-stat-wired">–</span></div>
1201
+ <div class="stat-item"><span class="stat-k">Compressed:</span><span class="stat-v" id="mem-stat-comp">–</span></div>
1202
+ <div class="stat-item"><span class="stat-k">Cached Files:</span><span class="stat-v" id="mem-stat-cached">–</span></div>
1203
+ <div class="stat-item"><span class="stat-k">Swap Used:</span><span class="stat-v" id="mem-stat-swap">–</span></div>
1204
+ </div>
1205
+ <div class="pane-graph">
1206
+ <div class="graph-header">
1207
+ <span class="graph-title">Memory Pressure</span>
1208
+ <div class="pressure-meter">
1209
+ <div class="pressure-dot" id="pressure-dot"></div>
1210
+ <span class="pressure-text" id="pressure-text">Normal</span>
1211
+ </div>
1212
+ </div>
1213
+ <canvas class="mac-canvas" id="canvas-mem"></canvas>
1214
+ </div>
1215
+ </div>
1216
+
1217
+ <!-- 3. Energy Panel -->
1218
+ <div class="panel-content" id="panel-energy">
1219
+ <div class="pane-stats">
1220
+ <div class="stat-item"><span class="stat-k">Total Power Draw:</span><span class="stat-v" id="energy-stat-total">–</span></div>
1221
+ <div class="stat-item"><span class="stat-k">CPU Package:</span><span class="stat-v" id="energy-stat-cpu">–</span></div>
1222
+ <div class="stat-item"><span class="stat-k">GPU Package:</span><span class="stat-v" id="energy-stat-gpu">–</span></div>
1223
+ <div style="height:4px;border-bottom:1px solid var(--border-color);"></div>
1224
+ <div class="stat-item"><span class="stat-k">Battery Level:</span><span class="stat-v" id="energy-stat-batt">–</span></div>
1225
+ <div class="stat-item"><span class="stat-k">Power Source:</span><span class="stat-v" id="energy-stat-source">–</span></div>
1226
+ <div class="stat-item"><span class="stat-k">Time Remaining:</span><span class="stat-v" id="energy-stat-timerem">–</span></div>
1227
+ <div class="stat-item"><span class="stat-k">Battery Health:</span><span class="stat-v" id="energy-stat-health">–</span></div>
1228
+ </div>
1229
+ <div class="pane-graph">
1230
+ <div class="graph-header">
1231
+ <span class="graph-title">Energy Consumption</span>
1232
+ <div class="graph-legend">
1233
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-yellow)"></span> Watts</span>
1234
+ </div>
1235
+ </div>
1236
+ <canvas class="mac-canvas" id="canvas-energy"></canvas>
1237
+ </div>
1238
+ </div>
1239
+
1240
+ <!-- 4. Disk Panel -->
1241
+ <div class="panel-content" id="panel-disk">
1242
+ <div class="pane-stats">
1243
+ <div class="stat-item"><span class="stat-k">Reads in / sec:</span><span class="stat-v" id="disk-stat-reads">–</span></div>
1244
+ <div class="stat-item"><span class="stat-k">Writes out / sec:</span><span class="stat-v" id="disk-stat-writes">–</span></div>
1245
+ <div class="stat-item"><span class="stat-k">Data read:</span><span class="stat-v" id="disk-stat-total-read">–</span></div>
1246
+ <div class="stat-item"><span class="stat-k">Data written:</span><span class="stat-v" id="disk-stat-total-write">–</span></div>
1247
+ <div style="height:4px;border-bottom:1px solid var(--border-color);"></div>
1248
+ <div id="disk-mounts-box" style="display:flex;flex-direction:column;gap:5px;"></div>
1249
+ </div>
1250
+ <div class="pane-graph">
1251
+ <div class="graph-header">
1252
+ <span class="graph-title">Disk Activity</span>
1253
+ <div class="graph-legend">
1254
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-teal)"></span> Read/sec</span>
1255
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-red)"></span> Write/sec</span>
1256
+ </div>
1257
+ </div>
1258
+ <canvas class="mac-canvas" id="canvas-disk"></canvas>
1259
+ </div>
1260
+ </div>
1261
+
1262
+ <!-- 5. Network Panel -->
1263
+ <div class="panel-content" id="panel-network">
1264
+ <div class="pane-stats">
1265
+ <div class="stat-item"><span class="stat-k">Packets in / sec:</span><span class="stat-v" id="net-stat-pkts-in">–</span></div>
1266
+ <div class="stat-item"><span class="stat-k">Packets out / sec:</span><span class="stat-v" id="net-stat-pkts-out">–</span></div>
1267
+ <div class="stat-item"><span class="stat-k">Data received / sec:</span><span class="stat-v" id="net-stat-rate-in">–</span></div>
1268
+ <div class="stat-item"><span class="stat-k">Data sent / sec:</span><span class="stat-v" id="net-stat-rate-out">–</span></div>
1269
+ <div style="height:4px;border-bottom:1px solid var(--border-color);"></div>
1270
+ <div class="stat-item"><span class="stat-k">Data received:</span><span class="stat-v" id="net-stat-total-in">–</span></div>
1271
+ <div class="stat-item"><span class="stat-k">Data sent:</span><span class="stat-v" id="net-stat-total-out">–</span></div>
1272
+ <div class="stat-item"><span class="stat-k">Interface / IP:</span><span class="stat-v" id="net-stat-iface" style="font-size:10px;">–</span></div>
1273
+ </div>
1274
+ <div class="pane-graph">
1275
+ <div class="graph-header">
1276
+ <span class="graph-title">Network Throughput</span>
1277
+ <div class="graph-legend">
1278
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-blue)"></span> In / sec</span>
1279
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-red)"></span> Out / sec</span>
1280
+ </div>
1281
+ </div>
1282
+ <canvas class="mac-canvas" id="canvas-net"></canvas>
1283
+ </div>
1284
+ </div>
1285
+
1286
+ <!-- 6. GPU Panel -->
1287
+ <div class="panel-content" id="panel-gpu">
1288
+ <div class="pane-stats">
1289
+ <div class="stat-item"><span class="stat-k">GPU Processor:</span><span class="stat-v" id="gpu-stat-model" style="font-size:10px;">–</span></div>
1290
+ <div class="stat-item"><span class="stat-k">Utilization:</span><span class="stat-v" id="gpu-stat-util">–</span></div>
1291
+ <div class="stat-item"><span class="stat-k">VRAM Used:</span><span class="stat-v" id="gpu-stat-vram">–</span></div>
1292
+ <div class="stat-item"><span class="stat-k">Active Processes:</span><span class="stat-v" id="gpu-stat-procs">–</span></div>
1293
+ <div class="stat-item"><span class="stat-k">Temperature:</span><span class="stat-v" id="gpu-stat-temp">–</span></div>
1294
+ </div>
1295
+ <div class="pane-graph">
1296
+ <div class="graph-header">
1297
+ <span class="graph-title">GPU Utilization</span>
1298
+ <div class="graph-legend">
1299
+ <span class="legend-pill"><span class="legend-dot" style="background:var(--mac-purple)"></span> GPU %</span>
1300
+ </div>
1301
+ </div>
1302
+ <canvas class="mac-canvas" id="canvas-gpu"></canvas>
1303
+ </div>
1304
+ </div>
1305
+
1306
+ <!-- 7. Logs Panel (Recording Session & History Explorer) -->
1307
+ <div class="panel-content" id="panel-logs">
1308
+ <div class="pane-stats" style="flex:1.2;">
1309
+ <div style="font-weight:700;margin-bottom:4px;color:var(--text-primary);display:flex;align-items:center;gap:6px;">
1310
+ <span>Session CSV Recorder</span>
1311
+ <span id="logs-rec-badge" class="log-badge info">IDLE</span>
1312
+ </div>
1313
+ <div class="stat-item"><span class="stat-k">Status:</span><span class="stat-v" id="rec-stat-status">Idle</span></div>
1314
+ <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;">–</span></div>
1315
+ <div class="stat-item"><span class="stat-k">Samples:</span><span class="stat-v" id="rec-stat-samples">0</span></div>
1316
+ <div class="stat-item"><span class="stat-k">Duration:</span><span class="stat-v" id="rec-stat-duration">0s</span></div>
1317
+ <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>
1318
+ <div style="display:flex;gap:8px;margin-top:6px;">
1319
+ <button class="mac-btn mac-btn-primary" id="btn-panel-rec" onclick="toggleWebRecording()" style="height:24px;font-size:11px;">Start Recording</button>
1320
+ <button class="mac-btn" onclick="fetchLogHistory()" style="height:24px;font-size:11px;">Refresh History</button>
1321
+ </div>
1322
+ </div>
1323
+ <div class="pane-graph" style="flex:2;display:flex;flex-direction:column;padding:8px 12px;overflow-y:auto;">
1324
+ <div class="graph-header" style="margin-bottom:6px;">
1325
+ <span class="graph-title">Saved CSV Logs on Disk</span>
1326
+ <span style="font-size:10px;color:var(--text-tertiary);">Replay in CLI: <code>pyre replay &lt;file&gt;</code></span>
1327
+ </div>
1328
+ <div id="history-list-box" style="flex:1;overflow-y:auto;">
1329
+ <table class="history-table">
1330
+ <thead>
1331
+ <tr><th>Filename</th><th>Size</th><th>Recorded At</th><th>Action</th></tr>
1332
+ </thead>
1333
+ <tbody id="history-tbody">
1334
+ <tr><td colspan="4" style="color:var(--text-tertiary);text-align:center;padding:12px;">Scanning ./pyre-exports...</td></tr>
1335
+ </tbody>
1336
+ </table>
1337
+ </div>
1338
+ </div>
1339
+ </div>
1340
+ </footer>
1341
+ </div>
1342
+
1343
+ <!-- macOS Window Status Bar -->
1344
+ <div id="statusbar">
1345
+ <div class="sb-left">
1346
+ <div class="sb-live-indicator">
1347
+ <span class="sb-live-dot" id="sb-dot"></span>
1348
+ <span id="sb-status">Connecting</span>
1349
+ </div>
1350
+ <span id="sb-host">–</span>
1351
+ </div>
1352
+ <div class="sb-right">
1353
+ <span id="sb-count">0 processes</span>
1354
+ <span>Updated <span id="sb-time">–</span></span>
1355
+ </div>
1356
+ </div>
1357
+
1358
+ <!-- Modal 1: Force Quit Confirmation Sheet -->
1359
+ <div class="modal-backdrop" id="modal-quit" onclick="if(event.target===this)closeModal('modal-quit')">
1360
+ <div class="mac-sheet modal-quit-box">
1361
+ <div class="quit-icon-wrap">
1362
+ <svg width="54" height="54" viewBox="0 0 24 24" fill="var(--mac-red)">
1363
+ <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"/>
1364
+ </svg>
1365
+ </div>
1366
+ <div class="quit-title">Do you want to quit this process?</div>
1367
+ <div class="quit-desc" id="quit-modal-desc">
1368
+ Quitting process may cause unsaved changes to be lost.
1369
+ </div>
1370
+ <div style="display:flex;justify-content:center;gap:10px;">
1371
+ <button class="mac-btn" onclick="closeModal('modal-quit')">Cancel</button>
1372
+ <button class="mac-btn mac-btn-danger" id="btn-do-force-quit" onclick="executeQuit(true)">Force Quit</button>
1373
+ <button class="mac-btn mac-btn-primary" id="btn-do-quit" onclick="executeQuit(false)">Quit</button>
1374
+ </div>
1375
+ </div>
1376
+ </div>
1377
+
1378
+ <!-- Modal 2: Process Inspector Sheet -->
1379
+ <div class="modal-backdrop" id="modal-inspector" onclick="if(event.target===this)closeModal('modal-inspector')">
1380
+ <div class="mac-sheet modal-inspect-box">
1381
+ <div class="sheet-header">
1382
+ <span class="sheet-title" id="inspect-title">Process Inspector</span>
1383
+ <button class="sheet-close-btn" onclick="closeModal('modal-inspector')">✕</button>
1384
+ </div>
1385
+ <div class="sheet-body" id="inspect-body">
1386
+ <!-- Process details injected here -->
1387
+ </div>
1388
+ <div class="sheet-footer">
1389
+ <button class="mac-btn" onclick="closeModal('modal-inspector')">Close</button>
1390
+ <button class="mac-btn mac-btn-danger" onclick="confirmQuitFromInspector()">Quit Process…</button>
1391
+ </div>
1392
+ </div>
1393
+ </div>
1394
+
1395
+ <!-- Modal 3: Settings / Preferences Sheet -->
1396
+ <div class="modal-backdrop" id="modal-settings" onclick="if(event.target===this)closeSettingsModal()">
1397
+ <div class="mac-sheet modal-settings-box">
1398
+ <div class="sheet-header">
1399
+ <span class="sheet-title">Pyre Preferences</span>
1400
+ <button class="sheet-close-btn" onclick="closeSettingsModal()">✕</button>
1401
+ </div>
1402
+
1403
+ <!-- Tab navigation -->
1404
+ <div class="settings-tabs">
1405
+ <button class="settings-tab-btn active" data-stab="general" onclick="switchSettingsTab('general')">General</button>
1406
+ <button class="settings-tab-btn" data-stab="autostart" onclick="switchSettingsTab('autostart')">Start on Boot</button>
1407
+ <button class="settings-tab-btn" data-stab="alerts" onclick="switchSettingsTab('alerts')">Alerts & Watchdog</button>
1408
+ <button class="settings-tab-btn" data-stab="appearance" onclick="switchSettingsTab('appearance')">Appearance</button>
1409
+ <button class="settings-tab-btn" data-stab="diagnostics" onclick="switchSettingsTab('diagnostics')">Diagnostics</button>
1410
+ </div>
1411
+
1412
+ <div class="settings-content-wrap">
1413
+ <!-- 1. General Pane -->
1414
+ <div class="settings-pane active" id="spane-general">
1415
+ <div class="settings-card">
1416
+ <div class="settings-card-title">Telemetry Update Frequency</div>
1417
+ <div class="settings-item">
1418
+ <div class="settings-label-wrap">
1419
+ <span class="settings-label">Update Interval</span>
1420
+ <span class="settings-sublabel">How frequently Pyre collects and refreshes hardware & process statistics</span>
1421
+ </div>
1422
+ <div class="settings-control">
1423
+ <select id="set-interval" class="mac-select" onchange="onSettingChange()">
1424
+ <option value="0.5">0.5s (Turbo High Performance)</option>
1425
+ <option value="1">1.0s (Fast Gaming/Dev)</option>
1426
+ <option value="2">2.0s (Default Balanced)</option>
1427
+ <option value="5">5.0s (Low Energy)</option>
1428
+ <option value="10">10.0s (Battery Saver)</option>
1429
+ </select>
1430
+ </div>
1431
+ </div>
1432
+ </div>
1433
+
1434
+ <div class="settings-card">
1435
+ <div class="settings-card-title">Process Table Defaults</div>
1436
+ <div class="settings-item">
1437
+ <div class="settings-label-wrap">
1438
+ <span class="settings-label">Default Process Filter</span>
1439
+ <span class="settings-sublabel">Initial process filter when dashboard opens</span>
1440
+ </div>
1441
+ <div class="settings-control">
1442
+ <select id="set-filter" class="mac-select" onchange="onSettingChange()">
1443
+ <option value="all">All Processes</option>
1444
+ <option value="my">My Processes</option>
1445
+ <option value="active">Active Processes Only</option>
1446
+ <option value="system">System Daemons Only</option>
1447
+ </select>
1448
+ </div>
1449
+ </div>
1450
+ <div class="settings-item">
1451
+ <div class="settings-label-wrap">
1452
+ <span class="settings-label">Default Sort Column</span>
1453
+ <span class="settings-sublabel">Default metric used to sort processes on launch</span>
1454
+ </div>
1455
+ <div class="settings-control">
1456
+ <select id="set-sort" class="mac-select" onchange="onSettingChange()">
1457
+ <option value="cpu">% CPU</option>
1458
+ <option value="mem">Memory Usage</option>
1459
+ <option value="runtime">CPU Runtime</option>
1460
+ <option value="threads">Thread Count</option>
1461
+ </select>
1462
+ </div>
1463
+ </div>
1464
+ </div>
1465
+
1466
+ <div class="settings-card">
1467
+ <div class="settings-card-title">Window & Launch Behavior</div>
1468
+ <div class="settings-item">
1469
+ <div class="settings-label-wrap">
1470
+ <span class="settings-label">Launch in Compact Widget Mode</span>
1471
+ <span class="settings-sublabel">Opens a compact floating HUD instead of the full activity window</span>
1472
+ </div>
1473
+ <div class="settings-control">
1474
+ <label class="mac-switch">
1475
+ <input type="checkbox" id="set-compact" onchange="onSettingChange()">
1476
+ <span class="mac-slider"></span>
1477
+ </label>
1478
+ </div>
1479
+ </div>
1480
+ <div class="settings-item">
1481
+ <div class="settings-label-wrap">
1482
+ <span class="settings-label">Keep Window Always on Top</span>
1483
+ <span class="settings-sublabel">Floats the dashboard window above all other full-screen apps</span>
1484
+ </div>
1485
+ <div class="settings-control">
1486
+ <label class="mac-switch">
1487
+ <input type="checkbox" id="set-ontop" onchange="onSettingChange()">
1488
+ <span class="mac-slider"></span>
1489
+ </label>
1490
+ </div>
1491
+ </div>
1492
+ </div>
1493
+ </div>
1494
+
1495
+ <!-- 2. Start on Boot Pane -->
1496
+ <div class="settings-pane" id="spane-autostart">
1497
+ <div class="settings-card">
1498
+ <div class="settings-card-title">Background Telemetry Service (Start on Boot)</div>
1499
+ <div class="settings-item">
1500
+ <div class="settings-label-wrap">
1501
+ <span class="settings-label">Start Pyre Telemetry on System Startup</span>
1502
+ <span class="settings-sublabel">Installs a native user-level LaunchAgent (macOS) or systemd user service (Linux)</span>
1503
+ </div>
1504
+ <div class="settings-control">
1505
+ <label class="mac-switch">
1506
+ <input type="checkbox" id="set-autostart" onchange="toggleAutostartFromUI(this.checked)">
1507
+ <span class="mac-slider"></span>
1508
+ </label>
1509
+ </div>
1510
+ </div>
1511
+ <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;">
1512
+ <div style="display:flex;justify-content:space-between;align-items:center;">
1513
+ <span style="color:var(--text-secondary);">Service Status:</span>
1514
+ <span id="svc-status-pill" class="status-pill inactive">Checking…</span>
1515
+ </div>
1516
+ <div style="display:flex;justify-content:space-between;">
1517
+ <span style="color:var(--text-secondary);">Service Identifier:</span>
1518
+ <span id="svc-label" style="font-family:var(--font-mono);font-size:10px;">com.pyre.web</span>
1519
+ </div>
1520
+ <div style="display:flex;justify-content:space-between;word-break:break-all;">
1521
+ <span style="color:var(--text-secondary);">Agent Path:</span>
1522
+ <span id="svc-path" style="font-family:var(--font-mono);font-size:10px;">~/Library/LaunchAgents/com.pyre.web.plist</span>
1523
+ </div>
1524
+ <div style="display:flex;justify-content:space-between;word-break:break-all;">
1525
+ <span style="color:var(--text-secondary);">Log File:</span>
1526
+ <span id="svc-log" style="font-family:var(--font-mono);font-size:10px;">~/.config/pyre/web.log</span>
1527
+ </div>
1528
+ </div>
1529
+ </div>
1530
+
1531
+ <div class="settings-card">
1532
+ <div class="settings-card-title">Network & Remote Access</div>
1533
+ <div class="settings-item">
1534
+ <div class="settings-label-wrap">
1535
+ <span class="settings-label">Expose to Local Network (Wi-Fi / LAN)</span>
1536
+ <span class="settings-sublabel">When enabled, Pyre dashboard is reachable from other devices on your LAN</span>
1537
+ </div>
1538
+ <div class="settings-control">
1539
+ <label class="mac-switch">
1540
+ <input type="checkbox" id="set-lan" onchange="onSettingChange()">
1541
+ <span class="mac-slider"></span>
1542
+ </label>
1543
+ </div>
1544
+ </div>
1545
+ <div class="settings-item">
1546
+ <div class="settings-label-wrap">
1547
+ <span class="settings-label">Default Web Server Port</span>
1548
+ <span class="settings-sublabel">Port used when running pyre web or background service</span>
1549
+ </div>
1550
+ <div class="settings-control">
1551
+ <input id="set-port" type="number" class="settings-input" style="width:75px;" value="3000" min="1024" max="65535" onchange="onSettingChange()">
1552
+ </div>
1553
+ </div>
1554
+ </div>
1555
+ </div>
1556
+
1557
+ <!-- 3. Alerts & Watchdog Pane -->
1558
+ <div class="settings-pane" id="spane-alerts">
1559
+ <div class="settings-card">
1560
+ <div class="settings-card-title">Resource Threshold Triggers</div>
1561
+ <div class="settings-item">
1562
+ <div class="settings-label-wrap">
1563
+ <span class="settings-label">CPU Alert Threshold (%)</span>
1564
+ <span class="settings-sublabel">Trigger alert when system or process CPU exceeds this percentage</span>
1565
+ </div>
1566
+ <div class="settings-control">
1567
+ <input id="set-cpu-alert" type="number" class="settings-input" style="width:70px;" min="50" max="100" value="90" onchange="onSettingChange()">
1568
+ <span style="color:var(--text-secondary);font-size:11px;">%</span>
1569
+ </div>
1570
+ </div>
1571
+ <div class="settings-item">
1572
+ <div class="settings-label-wrap">
1573
+ <span class="settings-label">Temperature Alert Threshold (°C)</span>
1574
+ <span class="settings-sublabel">Trigger warning when SoC/CPU temperature exceeds this limit</span>
1575
+ </div>
1576
+ <div class="settings-control">
1577
+ <input id="set-temp-alert" type="number" class="settings-input" style="width:70px;" min="60" max="110" value="95" onchange="onSettingChange()">
1578
+ <span style="color:var(--text-secondary);font-size:11px;">°C</span>
1579
+ </div>
1580
+ </div>
1581
+ </div>
1582
+
1583
+ <div class="settings-card">
1584
+ <div class="settings-card-title">Autonomous Process Watchdog</div>
1585
+ <div class="settings-item">
1586
+ <div class="settings-label-wrap">
1587
+ <span class="settings-label">Target Process Name Filter</span>
1588
+ <span class="settings-sublabel">Leave blank to monitor all processes, or specify e.g. "node", "chrome", "python"</span>
1589
+ </div>
1590
+ <div class="settings-control">
1591
+ <input id="set-watchdog-proc" type="text" class="settings-input" style="width:140px;" placeholder="All processes" onchange="onSettingChange()">
1592
+ </div>
1593
+ </div>
1594
+ <div class="settings-item">
1595
+ <div class="settings-label-wrap">
1596
+ <span class="settings-label">Runaway Memory Limit (%)</span>
1597
+ <span class="settings-sublabel">Threshold to flag or throttle memory-leaking tasks</span>
1598
+ </div>
1599
+ <div class="settings-control">
1600
+ <input id="set-watchdog-mem" type="number" class="settings-input" style="width:70px;" min="10" max="95" value="80" onchange="onSettingChange()">
1601
+ <span style="color:var(--text-secondary);font-size:11px;">%</span>
1602
+ </div>
1603
+ </div>
1604
+ </div>
1605
+
1606
+ <div class="settings-card">
1607
+ <div class="settings-card-title">Notification Channels & Webhooks</div>
1608
+ <div class="settings-item">
1609
+ <div class="settings-label-wrap">
1610
+ <span class="settings-label">Slack Webhook URL</span>
1611
+ <span class="settings-sublabel">https://hooks.slack.com/services/...</span>
1612
+ </div>
1613
+ <input id="set-slack-url" type="text" class="settings-input" style="width:230px;" placeholder="Optional Slack webhook" onchange="onSettingChange()">
1614
+ </div>
1615
+ <div class="settings-item">
1616
+ <div class="settings-label-wrap">
1617
+ <span class="settings-label">Discord Webhook URL</span>
1618
+ <span class="settings-sublabel">https://discord.com/api/webhooks/...</span>
1619
+ </div>
1620
+ <input id="set-discord-url" type="text" class="settings-input" style="width:230px;" placeholder="Optional Discord webhook" onchange="onSettingChange()">
1621
+ </div>
1622
+ <div class="settings-item">
1623
+ <div class="settings-label-wrap">
1624
+ <span class="settings-label">ntfy.sh Topic URL</span>
1625
+ <span class="settings-sublabel">https://ntfy.sh/my-topic</span>
1626
+ </div>
1627
+ <input id="set-ntfy-url" type="text" class="settings-input" style="width:230px;" placeholder="Optional ntfy URL" onchange="onSettingChange()">
1628
+ </div>
1629
+ <div style="display:flex;justify-content:flex-end;margin-top:4px;">
1630
+ <button class="mac-btn mac-btn-primary" onclick="sendTestAlertFromUI()">Dispatch Test Notification</button>
1631
+ </div>
1632
+ </div>
1633
+ </div>
1634
+
1635
+ <!-- 4. Appearance Pane -->
1636
+ <div class="settings-pane" id="spane-appearance">
1637
+ <div class="settings-card">
1638
+ <div class="settings-card-title">Hardware Telemetry Graphs</div>
1639
+ <div class="settings-item">
1640
+ <div class="settings-label-wrap">
1641
+ <span class="settings-label">Rolling History Window</span>
1642
+ <span class="settings-sublabel">Amount of history preserved in CPU, GPU, Network, and Disk graphs</span>
1643
+ </div>
1644
+ <div class="settings-control">
1645
+ <select id="set-history-len" class="mac-select" onchange="onSettingChange()">
1646
+ <option value="30">30 seconds (Compact)</option>
1647
+ <option value="60">60 seconds (Standard)</option>
1648
+ <option value="120">120 seconds (2 Minutes)</option>
1649
+ <option value="300">300 seconds (5 Minutes Long Run)</option>
1650
+ </select>
1651
+ </div>
1652
+ </div>
1653
+ <div class="settings-item">
1654
+ <div class="settings-label-wrap">
1655
+ <span class="settings-label">Animate Live Waveform Gradients</span>
1656
+ <span class="settings-sublabel">Renders smooth neon area fills under live telemetry curves</span>
1657
+ </div>
1658
+ <div class="settings-control">
1659
+ <label class="mac-switch">
1660
+ <input type="checkbox" id="set-show-graphs" checked onchange="onSettingChange()">
1661
+ <span class="mac-slider"></span>
1662
+ </label>
1663
+ </div>
1664
+ </div>
1665
+ </div>
1666
+
1667
+ <div class="settings-card">
1668
+ <div class="settings-card-title">Process Table Row Density</div>
1669
+ <div class="settings-item">
1670
+ <div class="settings-label-wrap">
1671
+ <span class="settings-label">Table Row Density</span>
1672
+ <span class="settings-sublabel">Adjust row density for higher information density</span>
1673
+ </div>
1674
+ <div class="settings-control">
1675
+ <select id="set-density" class="mac-select" onchange="onDensityChange(this.value)">
1676
+ <option value="compact">Compact (Activity Monitor classic)</option>
1677
+ <option value="standard" selected>Standard (Balanced)</option>
1678
+ <option value="comfortable">Comfortable (Larger text & touch)</option>
1679
+ </select>
1680
+ </div>
1681
+ </div>
1682
+ </div>
1683
+ </div>
1684
+
1685
+ <!-- 5. Diagnostics Pane -->
1686
+ <div class="settings-pane" id="spane-diagnostics">
1687
+ <div class="settings-card">
1688
+ <div class="settings-card-title">System Information</div>
1689
+ <div id="diag-sys-info" style="font-size:11px;color:var(--text-secondary);display:flex;flex-direction:column;gap:5px;">
1690
+ <div>Loading system specifications…</div>
1691
+ </div>
1692
+ </div>
1693
+
1694
+ <div class="settings-card">
1695
+ <div class="settings-card-title">System Diagnostics (pyre doctor)</div>
1696
+ <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>
1697
+ <div style="display:flex;gap:8px;margin-top:4px;">
1698
+ <button class="mac-btn mac-btn-primary" id="btn-run-doctor" onclick="runDoctorFromUI()">Run Diagnostics</button>
1699
+ </div>
1700
+ <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>
1701
+ </div>
1702
+
1703
+ <div class="settings-card" style="border-color:rgba(255,69,58,0.3);">
1704
+ <div class="settings-card-title" style="color:var(--mac-red);">Reset Configuration</div>
1705
+ <div class="settings-item">
1706
+ <div class="settings-label-wrap">
1707
+ <span class="settings-label">Reset Preferences to Factory Defaults</span>
1708
+ <span class="settings-sublabel">Clears ~/.config/pyre/config.json back to original out-of-the-box defaults</span>
1709
+ </div>
1710
+ <button class="mac-btn mac-btn-danger" onclick="resetConfigFromUI()">Reset All Settings</button>
1711
+ </div>
1712
+ </div>
1713
+ </div>
1714
+ </div>
1715
+
1716
+ <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;">
1717
+ <span id="settings-status-hint" style="font-size:11px;color:var(--text-tertiary);">Preferences saved to ~/.config/pyre/config.json</span>
1718
+ <div style="display:flex;gap:8px;">
1719
+ <button class="mac-btn" onclick="closeSettingsModal()">Done</button>
1720
+ </div>
1721
+ </div>
1722
+ </div>
1723
+ </div>
1724
+
1725
+ <!-- Toast notification -->
1726
+ <div id="toast">Action completed</div>
1727
+
1728
+ <script>
1729
+ // State Management
1730
+ let currentTab = 'cpu';
1731
+ let latestData = null;
1732
+ let selectedPid = null;
1733
+ let currentFilter = 'all';
1734
+ let searchQuery = '';
1735
+
1736
+ // Logs & Audit State
1737
+ let logsBuffer = [];
1738
+ let logFilterLevel = 'all';
1739
+ let logFilterSubsys = 'all';
1740
+ let logSearchQuery = '';
1741
+ let autoScrollLogs = true;
1742
+ let isRecording = false;
1743
+ let hasLoggedInit = false;
1744
+ let prevThermalState = null;
1745
+ let lastCpuSpikeTimes = {};
1746
+ let lastHighMemAlert = 0;
1747
+ let lastNetBurstAlert = 0;
1748
+
1749
+ // Sort State per tab
1750
+ const sortState = {
1751
+ cpu: { col: 'cpu', dir: 'desc' },
1752
+ memory: { col: 'mem', dir: 'desc' },
1753
+ energy: { col: 'cpu', dir: 'desc' },
1754
+ disk: { col: 'cpu', dir: 'desc' },
1755
+ network: { col: 'cpu', dir: 'desc' },
1756
+ gpu: { col: 'cpu', dir: 'desc' }
1757
+ };
1758
+
1759
+ // Rolling Metrics History
1760
+ const MAX_POINTS = 60;
1761
+ const historyData = {
1762
+ cpuUser: [],
1763
+ cpuSystem: [],
1764
+ memPressure: [],
1765
+ energyWatts: [],
1766
+ diskRead: [],
1767
+ diskWrite: [],
1768
+ netIn: [],
1769
+ netOut: [],
1770
+ gpuUtil: []
1771
+ };
1772
+
1773
+ // Utilities
1774
+ function pushHistory(arr, val) {
1775
+ arr.push(typeof val === 'number' && !isNaN(val) ? val : 0);
1776
+ if (arr.length > MAX_POINTS) arr.shift();
1777
+ }
1778
+
1779
+ function fmtBytes(b, dec = 1) {
1780
+ if (b === undefined || b === null || isNaN(b) || b === 0) return '0 B';
1781
+ const k = 1024;
1782
+ const s = ['B', 'KB', 'MB', 'GB', 'TB'];
1783
+ const i = Math.floor(Math.log(Math.abs(b)) / Math.log(k));
1784
+ if (i < 0) return '0 B';
1785
+ const v = b / Math.pow(k, i);
1786
+ return (parseFloat(v.toFixed(dec))) + ' ' + (s[i] || 'TB');
1787
+ }
1788
+
1789
+ function fmtTime(sec) {
1790
+ if (!sec) return '0:00.00';
1791
+ const m = Math.floor(sec / 60);
1792
+ const s = Math.floor(sec % 60);
1793
+ const ms = Math.floor((sec % 1) * 100);
1794
+ return m + ':' + (s < 10 ? '0' : '') + s + '.' + (ms < 10 ? '0' : '') + ms;
1795
+ }
1796
+
1797
+ function escapeHtml(s) {
1798
+ return String(s || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
1799
+ }
1800
+
1801
+ function showToast(msg) {
1802
+ const t = document.getElementById('toast');
1803
+ t.textContent = msg;
1804
+ t.classList.add('show');
1805
+ setTimeout(() => t.classList.remove('show'), 2600);
1806
+ }
1807
+
1808
+ // Tab Column Schemas
1809
+ const tabColumns = {
1810
+ cpu: [
1811
+ { id: 'command', name: 'Process Name', width: '38%', align: 'left' },
1812
+ { id: 'cpu', name: '% CPU', width: '12%', align: 'right' },
1813
+ { id: 'runtime', name: 'CPU Time', width: '14%', align: 'right' },
1814
+ { id: 'threads', name: 'Threads', width: '10%', align: 'right' },
1815
+ { id: 'state', name: 'State', width: '8%', align: 'center' },
1816
+ { id: 'pid', name: 'PID', width: '8%', align: 'right' },
1817
+ { id: 'user', name: 'User', width: '10%', align: 'left' }
1818
+ ],
1819
+ memory: [
1820
+ { id: 'command', name: 'Process Name', width: '38%', align: 'left' },
1821
+ { id: 'memBytes', name: 'Memory', width: '15%', align: 'right' },
1822
+ { id: 'mem', name: '% Memory', width: '11%', align: 'right' },
1823
+ { id: 'threads', name: 'Threads', width: '10%', align: 'right' },
1824
+ { id: 'state', name: 'State', width: '8%', align: 'center' },
1825
+ { id: 'pid', name: 'PID', width: '8%', align: 'right' },
1826
+ { id: 'user', name: 'User', width: '10%', align: 'left' }
1827
+ ],
1828
+ energy: [
1829
+ { id: 'command', name: 'Process Name', width: '42%', align: 'left' },
1830
+ { id: 'energyImpact', name: 'Energy Impact', width: '16%', align: 'right' },
1831
+ { id: 'cpu', name: '% CPU', width: '12%', align: 'right' },
1832
+ { id: 'threads', name: 'Threads', width: '10%', align: 'right' },
1833
+ { id: 'pid', name: 'PID', width: '10%', align: 'right' },
1834
+ { id: 'user', name: 'User', width: '10%', align: 'left' }
1835
+ ],
1836
+ disk: [
1837
+ { id: 'command', name: 'Process Name', width: '44%', align: 'left' },
1838
+ { id: 'cpu', name: '% CPU', width: '12%', align: 'right' },
1839
+ { id: 'state', name: 'State', width: '12%', align: 'center' },
1840
+ { id: 'threads', name: 'Threads', width: '10%', align: 'right' },
1841
+ { id: 'pid', name: 'PID', width: '10%', align: 'right' },
1842
+ { id: 'user', name: 'User', width: '12%', align: 'left' }
1843
+ ],
1844
+ network: [
1845
+ { id: 'command', name: 'Process Name', width: '38%', align: 'left' },
1846
+ { id: 'netRx', name: 'Sent Bytes', width: '13%', align: 'right' },
1847
+ { id: 'netTx', name: 'Rcvd Bytes', width: '13%', align: 'right' },
1848
+ { id: 'cpu', name: '% CPU', width: '10%', align: 'right' },
1849
+ { id: 'threads', name: 'Threads', width: '10%', align: 'right' },
1850
+ { id: 'pid', name: 'PID', width: '8%', align: 'right' },
1851
+ { id: 'user', name: 'User', width: '8%', align: 'left' }
1852
+ ],
1853
+ gpu: [
1854
+ { id: 'command', name: 'Process Name', width: '42%', align: 'left' },
1855
+ { id: 'cpu', name: '% GPU/CPU', width: '16%', align: 'right' },
1856
+ { id: 'mem', name: '% Memory', width: '14%', align: 'right' },
1857
+ { id: 'threads', name: 'Threads', width: '10%', align: 'right' },
1858
+ { id: 'pid', name: 'PID', width: '8%', align: 'right' },
1859
+ { id: 'user', name: 'User', width: '10%', align: 'left' }
1860
+ ]
1861
+ };
1862
+
1863
+ // Render Table Header
1864
+ function renderTableHeader() {
1865
+ const thead = document.getElementById('proc-thead');
1866
+ const cols = tabColumns[currentTab] || tabColumns.cpu;
1867
+ const curSort = sortState[currentTab];
1868
+
1869
+ let html = '<tr>';
1870
+ cols.forEach(c => {
1871
+ const isSorted = curSort.col === c.id;
1872
+ const caret = isSorted ? (curSort.dir === 'asc' ? ' ▲' : ' ▼') : '';
1873
+ html += \`<th style="width:\${c.width};text-align:\${c.align}" onclick="handleSort('\${c.id}')">\${escapeHtml(c.name)}<span class="sort-caret">\${caret}</span></th>\`;
1874
+ });
1875
+ html += '</tr>';
1876
+ thead.innerHTML = html;
1877
+ }
1878
+
1879
+ // Sort Handler
1880
+ function handleSort(colId) {
1881
+ const cur = sortState[currentTab];
1882
+ if (cur.col === colId) {
1883
+ cur.dir = cur.dir === 'asc' ? 'desc' : 'asc';
1884
+ } else {
1885
+ cur.col = colId;
1886
+ cur.dir = (colId === 'command' || colId === 'user') ? 'asc' : 'desc';
1887
+ }
1888
+ renderTableHeader();
1889
+ renderTableRows();
1890
+ }
204
1891
 
205
- </style>
206
- </head>
207
- <body>
208
- <div id="titlebar">
209
- <span id="titlebar-logo">Pyre</span>
210
- <span id="titlebar-host">Loading...</span>
211
- <div id="conn-badge"><div class="conn-dot" id="conn-dot"></div><span id="conn-text">Connecting</span></div>
212
- </div>
213
- <div id="tabbar">
214
- <div class="tab active" data-view="overview" onclick="switchView('overview')"><span class="tab-key">1</span>Overview</div>
215
- <div class="tab" data-view="cpu" onclick="switchView('cpu')"><span class="tab-key">2</span>CPU</div>
216
- <div class="tab" data-view="memory" onclick="switchView('memory')"><span class="tab-key">3</span>Memory</div>
217
- <div class="tab" data-view="gpu" onclick="switchView('gpu')"><span class="tab-key">4</span>GPU</div>
218
- <div class="tab" data-view="power" onclick="switchView('power')"><span class="tab-key">5</span>Power</div>
219
- <div class="tab" data-view="battery" onclick="switchView('battery')"><span class="tab-key">6</span>Battery</div>
220
- <div class="tab" data-view="thermal" onclick="switchView('thermal')"><span class="tab-key">7</span>Thermal</div>
221
- <div class="tab" data-view="network" onclick="switchView('network')"><span class="tab-key">8</span>Network</div>
222
- <div class="tab" data-view="disk" onclick="switchView('disk')"><span class="tab-key">9</span>Disk</div>
223
- <div class="tab" data-view="processes" onclick="switchView('processes')"><span class="tab-key">P</span>Processes</div>
224
- <div class="tab" data-view="anomalies" onclick="switchView('anomalies')"><span class="tab-key">A</span>Anomalies</div>
225
- </div>
226
- <div id="main">
227
- <div id="view-content"></div>
228
- </div>
229
- <div id="statusbar">
230
- <div class="sb-item">⟳ <span id="sb-refresh" class="up-val">–</span></div><div class="sb-sep">│</div>
231
- <div class="sb-item">↑ <span id="sb-uptime" class="up-val">–</span></div><div class="sb-sep">│</div>
232
- <div class="sb-item">CPU <span id="sb-cpu" class="up-val">–</span></div><div class="sb-sep">│</div>
233
- <div class="sb-item">MEM <span id="sb-mem" class="up-val">–</span></div><div class="sb-sep">│</div>
234
- <div class="sb-item">BATT <span id="sb-batt" class="up-val">–</span></div><div class="sb-sep">│</div>
235
- <div class="sb-item">PROCS <span id="sb-procs" class="up-val">–</span></div>
236
- </div>
237
- <script>
238
- let currentView = 'overview';
239
- let latestData = null;
240
- const history = { cpu:[], mem:[], temp:[], rxRate:[], txRate:[], power:[] };
241
- const MAX_HIST = 100;
242
- let procSort = { col:'cpu', dir:'desc' };
243
-
244
- function pushHist(arr, val) { arr.push(val||0); if(arr.length>MAX_HIST)arr.shift(); }
245
- function fmtBytes(b, dec=2) {
246
- if(!b) return '0 B';
247
- const k=1024, s=['B','KB','MB','GB','TB'];
248
- const i=Math.floor(Math.log(Math.abs(b))/Math.log(k));
249
- return parseFloat((b/Math.pow(k,i)).toFixed(dec))+' '+s[i];
250
- }
251
- function statusCls(v) { return v>85?'crit':v>65?'warn':'ok'; }
252
- function escapeHtml(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
253
-
254
- function svgRing(val, max, label, unit='', size=100) {
255
- const r = size*0.4, circ = 2*Math.PI*r;
256
- const pct = Math.min(100, Math.max(0, (val/max)*100));
257
- const off = circ*(1 - pct/100);
258
- const cls = statusCls(pct);
259
- const col = cls==='crit'?'var(--text3)':cls==='warn'?'var(--text2)':'var(--text)';
260
- return \`<div class="ring-box">
261
- <div class="ring-title">\${label}</div>
262
- <div class="svg-ring \${size>150?'large':''}">
263
- <svg viewBox="0 0 \${size} \${size}">
264
- <circle cx="\${size/2}" cy="\${size/2}" r="\${r}" fill="none" stroke="var(--border)" stroke-width="\${size*0.08}"/>
265
- <circle cx="\${size/2}" cy="\${size/2}" r="\${r}" fill="none" stroke="\${col}" stroke-width="\${size*0.08}"
266
- stroke-dasharray="\${circ}" stroke-dashoffset="\${off}" stroke-linecap="round"
267
- style="transition:stroke-dashoffset 0.5s ease" />
268
- </svg>
269
- <div class="ring-center">
270
- <div class="ring-val" style="color:\${col}">\${val.toFixed(1)}\${unit}</div>
271
- </div>
272
- </div>
273
- </div>\`;
1892
+ // Tab Switching
1893
+ function switchTab(tab) {
1894
+ currentTab = tab;
1895
+ document.querySelectorAll('.mac-seg-item').forEach(el => {
1896
+ el.classList.toggle('active', el.dataset.tab === tab);
1897
+ });
1898
+ document.querySelectorAll('.panel-content').forEach(el => {
1899
+ el.classList.toggle('active', el.id === 'panel-' + tab);
1900
+ });
1901
+
1902
+ // Logs tab: show logs-view, hide proc-table; otherwise reverse
1903
+ const procTable = document.getElementById('proc-table');
1904
+ const logsView = document.getElementById('logs-view');
1905
+ const procFilter = document.getElementById('proc-filter');
1906
+ const searchWrap = document.querySelector('.mac-search-wrap');
1907
+ const btnStop = document.getElementById('btn-stop');
1908
+ const btnInspect = document.getElementById('btn-inspect');
1909
+ if (tab === 'logs') {
1910
+ if (procTable) procTable.style.display = 'none';
1911
+ if (logsView) logsView.style.display = 'flex';
1912
+ if (procFilter) procFilter.style.display = 'none';
1913
+ if (searchWrap) searchWrap.style.display = 'none';
1914
+ if (btnStop) btnStop.style.display = 'none';
1915
+ if (btnInspect) btnInspect.style.display = 'none';
1916
+ renderLogs();
1917
+ fetchLogHistory();
1918
+ syncRecordingStatus();
1919
+ } else {
1920
+ if (procTable) procTable.style.display = '';
1921
+ if (logsView) logsView.style.display = 'none';
1922
+ if (procFilter) procFilter.style.display = '';
1923
+ if (searchWrap) searchWrap.style.display = '';
1924
+ if (btnStop) btnStop.style.display = '';
1925
+ if (btnInspect) btnInspect.style.display = '';
1926
+ renderTableHeader();
1927
+ renderTableRows();
1928
+ updateBottomPanel();
1929
+ requestAnimationFrame(drawAllCanvases);
1930
+ }
1931
+ }
1932
+
1933
+ // Process Filtering and Searching
1934
+ function onFilterChange(val) {
1935
+ currentFilter = val;
1936
+ renderTableRows();
1937
+ }
1938
+
1939
+ function onSearchInput(val) {
1940
+ searchQuery = (val || '').trim().toLowerCase();
1941
+ document.getElementById('search-clear').style.display = searchQuery ? 'flex' : 'none';
1942
+ renderTableRows();
1943
+ }
1944
+
1945
+ function clearSearch() {
1946
+ document.getElementById('search-input').value = '';
1947
+ onSearchInput('');
1948
+ }
1949
+
1950
+ // Process Selection
1951
+ function selectRow(pid) {
1952
+ selectedPid = pid;
1953
+ document.querySelectorAll('#proc-tbody tr').forEach(tr => {
1954
+ tr.classList.toggle('selected', Number(tr.dataset.pid) === pid);
1955
+ });
1956
+ const hasSel = selectedPid !== null;
1957
+ document.getElementById('btn-stop').disabled = !hasSel;
1958
+ document.getElementById('btn-inspect').disabled = !hasSel;
1959
+ }
1960
+
1961
+ // Render Table Rows
1962
+ function renderTableRows() {
1963
+ if (!latestData || !latestData.processes) return;
1964
+ const tbody = document.getElementById('proc-tbody');
1965
+ const cols = tabColumns[currentTab] || tabColumns.cpu;
1966
+ const sort = sortState[currentTab];
1967
+ const totalMem = latestData.memory?.total || 1;
1968
+
1969
+ // Filter processes
1970
+ let procs = latestData.processes.map(p => {
1971
+ return {
1972
+ ...p,
1973
+ memBytes: (p.mem / 100) * totalMem,
1974
+ energyImpact: parseFloat(p.cpu.toFixed(1)),
1975
+ netRx: 0,
1976
+ netTx: 0
1977
+ };
1978
+ });
1979
+
1980
+ if (currentFilter === 'my') {
1981
+ const myUser = latestData.header?.user || 'somalip';
1982
+ procs = procs.filter(p => p.user && p.user.toLowerCase().includes(myUser.toLowerCase()));
1983
+ } else if (currentFilter === 'active') {
1984
+ procs = procs.filter(p => p.cpu > 0.05 || p.mem > 0.2);
1985
+ } else if (currentFilter === 'system') {
1986
+ procs = procs.filter(p => p.user === 'root' || p.pid < 100);
1987
+ }
1988
+
1989
+ if (searchQuery) {
1990
+ procs = procs.filter(p => {
1991
+ return p.command.toLowerCase().includes(searchQuery) ||
1992
+ String(p.pid).includes(searchQuery) ||
1993
+ (p.user && p.user.toLowerCase().includes(searchQuery));
1994
+ });
1995
+ }
1996
+
1997
+ // Sort
1998
+ procs.sort((a, b) => {
1999
+ let va = a[sort.col];
2000
+ let vb = b[sort.col];
2001
+ if (typeof va === 'string') {
2002
+ va = va.toLowerCase();
2003
+ vb = (vb || '').toLowerCase();
2004
+ }
2005
+ if (va < vb) return sort.dir === 'asc' ? -1 : 1;
2006
+ if (va > vb) return sort.dir === 'asc' ? 1 : -1;
2007
+ return 0;
2008
+ });
2009
+
2010
+ // Generate HTML
2011
+ let rowsHtml = '';
2012
+ procs.forEach(p => {
2013
+ const isSelected = p.pid === selectedPid;
2014
+ const isSys = p.user === 'root' || p.pid < 100;
2015
+ const initial = (p.command.replace(/^.*[\\\/]/, '')[0] || 'P').toUpperCase();
2016
+
2017
+ rowsHtml += \`<tr class="\${isSelected ? 'selected' : ''}" data-pid="\${p.pid}" onclick="selectRow(\${p.pid})" ondblclick="openInspector(\${p.pid})">\`;
2018
+
2019
+ cols.forEach(c => {
2020
+ if (c.id === 'command') {
2021
+ const shortName = p.command.replace(/^.*[\\\/]/, '');
2022
+ rowsHtml += \`<td class="col-name" title="\${escapeHtml(p.command)}">
2023
+ <span class="proc-icon \${isSys ? 'system' : 'user'}">\${initial}</span>
2024
+ <span style="overflow:hidden;text-overflow:ellipsis;">\${escapeHtml(shortName)}</span>
2025
+ </td>\`;
2026
+ } else if (c.id === 'cpu') {
2027
+ rowsHtml += \`<td>\${p.cpu.toFixed(1)}</td>\`;
2028
+ } else if (c.id === 'runtime') {
2029
+ rowsHtml += \`<td>\${fmtTime(p.runtime)}</td>\`;
2030
+ } else if (c.id === 'mem') {
2031
+ rowsHtml += \`<td>\${p.mem.toFixed(1)}</td>\`;
2032
+ } else if (c.id === 'memBytes') {
2033
+ rowsHtml += \`<td>\${fmtBytes(p.memBytes)}</td>\`;
2034
+ } else if (c.id === 'energyImpact') {
2035
+ rowsHtml += \`<td>\${p.energyImpact.toFixed(1)}</td>\`;
2036
+ } else if (c.id === 'netRx') {
2037
+ rowsHtml += \`<td>\${fmtBytes(p.netRx)}</td>\`;
2038
+ } else if (c.id === 'netTx') {
2039
+ rowsHtml += \`<td>\${fmtBytes(p.netTx)}</td>\`;
2040
+ } else if (c.id === 'pid') {
2041
+ rowsHtml += \`<td>\${p.pid}</td>\`;
2042
+ } else if (c.id === 'threads') {
2043
+ rowsHtml += \`<td>\${p.threads || 1}</td>\`;
2044
+ } else if (c.id === 'state') {
2045
+ rowsHtml += \`<td style="text-align:center;">\${escapeHtml(p.state || 'R')}</td>\`;
2046
+ } else if (c.id === 'user') {
2047
+ rowsHtml += \`<td style="text-align:left;">\${escapeHtml(p.user || 'root')}</td>\`;
2048
+ } else {
2049
+ rowsHtml += \`<td>\${escapeHtml(p[c.id] || '–')}</td>\`;
2050
+ }
2051
+ });
2052
+
2053
+ rowsHtml += '</tr>';
2054
+ });
2055
+
2056
+ tbody.innerHTML = rowsHtml;
2057
+ document.getElementById('sb-count').textContent = procs.length + ' processes';
274
2058
  }
275
2059
 
276
- function drawSpark(id, arr, col1, col2, mx) {
277
- const c = document.getElementById(id);
278
- if(!c || !arr.length) return;
2060
+ // Draw Area Charts on Canvas
2061
+ function drawChart(canvasId, pointsA, colorA, pointsB = null, colorB = null, maxVal = 100) {
2062
+ const c = document.getElementById(canvasId);
2063
+ if (!c) return;
279
2064
  const dpr = window.devicePixelRatio || 1;
280
2065
  const rect = c.getBoundingClientRect();
281
- c.width = rect.width * dpr;
282
- c.height = rect.height * dpr;
283
- const W = c.width, H = c.height;
2066
+ if (rect.width === 0 || rect.height === 0) return;
2067
+
2068
+ if (c.width !== Math.floor(rect.width * dpr) || c.height !== Math.floor(rect.height * dpr)) {
2069
+ c.width = Math.floor(rect.width * dpr);
2070
+ c.height = Math.floor(rect.height * dpr);
2071
+ }
2072
+
2073
+ const W = c.width;
2074
+ const H = c.height;
284
2075
  const ctx = c.getContext('2d');
285
- ctx.clearRect(0,0,W,H);
286
-
2076
+ ctx.clearRect(0, 0, W, H);
2077
+
287
2078
  // Grid lines
288
- ctx.strokeStyle = 'rgba(255,255,255,0.05)';
2079
+ ctx.strokeStyle = 'rgba(255, 255, 255, 0.06)';
289
2080
  ctx.lineWidth = 1 * dpr;
290
- [0.25, 0.5, 0.75].forEach(y => {
291
- ctx.beginPath(); ctx.moveTo(0, H*y); ctx.lineTo(W, H*y); ctx.stroke();
2081
+ [0.25, 0.5, 0.75].forEach(ratio => {
2082
+ ctx.beginPath();
2083
+ ctx.moveTo(0, H * ratio);
2084
+ ctx.lineTo(W, H * ratio);
2085
+ ctx.stroke();
292
2086
  });
293
2087
 
294
- const m = mx || Math.max(...arr, 1);
295
- const pts = arr.map((v,i) => [i/(arr.length-1||1)*W, H - (v/m)*H]);
2088
+ if (!pointsA || !pointsA.length) return;
2089
+
2090
+ const actualMax = maxVal || Math.max(...pointsA, ...(pointsB || []), 1);
2091
+
2092
+ // Helper to render area + stroke
2093
+ function renderPath(pts, strokeCol, fillCol) {
2094
+ if (!pts || !pts.length) return;
2095
+ const step = W / (MAX_POINTS - 1);
2096
+ const startX = W - (pts.length - 1) * step;
2097
+
2098
+ ctx.beginPath();
2099
+ ctx.moveTo(startX, H);
2100
+ pts.forEach((v, i) => {
2101
+ const x = startX + i * step;
2102
+ const y = Math.max(0, H - (v / actualMax) * H);
2103
+ ctx.lineTo(x, y);
2104
+ });
2105
+ ctx.lineTo(W, H);
2106
+ ctx.fillStyle = fillCol;
2107
+ ctx.fill();
2108
+
2109
+ ctx.beginPath();
2110
+ pts.forEach((v, i) => {
2111
+ const x = startX + i * step;
2112
+ const y = Math.max(0, H - (v / actualMax) * H);
2113
+ if (i === 0) ctx.moveTo(x, y);
2114
+ else ctx.lineTo(x, y);
2115
+ });
2116
+ ctx.strokeStyle = strokeCol;
2117
+ ctx.lineWidth = 1.5 * dpr;
2118
+ ctx.stroke();
2119
+ }
2120
+
2121
+ if (pointsB && pointsB.length && colorB) {
2122
+ renderPath(pointsB, colorB, colorB + '33');
2123
+ }
2124
+ renderPath(pointsA, colorA, colorA + '44');
2125
+ }
2126
+
2127
+ function drawAllCanvases() {
2128
+ if (currentTab === 'cpu') {
2129
+ drawChart('canvas-cpu', historyData.cpuUser, '#007aff', historyData.cpuSystem, '#ff453a', 100);
2130
+ } else if (currentTab === 'memory') {
2131
+ const pressurePct = latestData?.memory?.usagePercent || 50;
2132
+ const col = pressurePct > 80 ? '#ff453a' : (pressurePct > 60 ? '#ff9f0a' : '#34c759');
2133
+ drawChart('canvas-mem', historyData.memPressure, col, null, null, 100);
2134
+ } else if (currentTab === 'energy') {
2135
+ const maxW = Math.max(10, ...historyData.energyWatts);
2136
+ drawChart('canvas-energy', historyData.energyWatts, '#ff9f0a', null, null, maxW);
2137
+ } else if (currentTab === 'disk') {
2138
+ const maxIO = Math.max(1, ...historyData.diskRead, ...historyData.diskWrite);
2139
+ drawChart('canvas-disk', historyData.diskRead, '#64d2ff', historyData.diskWrite, '#ff453a', maxIO);
2140
+ } else if (currentTab === 'network') {
2141
+ const maxNet = Math.max(1, ...historyData.netIn, ...historyData.netOut);
2142
+ drawChart('canvas-net', historyData.netIn, '#007aff', historyData.netOut, '#ff453a', maxNet);
2143
+ } else if (currentTab === 'gpu') {
2144
+ drawChart('canvas-gpu', historyData.gpuUtil, '#af52de', null, null, 100);
2145
+ }
2146
+ }
2147
+
2148
+ // Update Bottom Panel Elements
2149
+ function updateBottomPanel() {
2150
+ if (!latestData) return;
2151
+ const d = latestData;
2152
+
2153
+ // CPU
2154
+ const totalCpu = d.cpu.usage || 0;
2155
+ const userCpu = totalCpu * 0.75;
2156
+ const sysCpu = totalCpu * 0.25;
2157
+ const idleCpu = Math.max(0, 100 - totalCpu);
2158
+
2159
+ document.getElementById('cpu-stat-system').textContent = sysCpu.toFixed(1) + '%';
2160
+ document.getElementById('cpu-stat-user').textContent = userCpu.toFixed(1) + '%';
2161
+ document.getElementById('cpu-stat-idle').textContent = idleCpu.toFixed(1) + '%';
296
2162
 
297
- const grad = ctx.createLinearGradient(0,0,W,0);
298
- grad.addColorStop(0, col1); grad.addColorStop(1, col2);
299
- const aGrad = ctx.createLinearGradient(0,0,0,H);
300
- aGrad.addColorStop(0, col2+'66'); aGrad.addColorStop(1, 'transparent');
301
-
302
- ctx.beginPath(); ctx.moveTo(pts[0][0], H);
303
- pts.forEach(p=>ctx.lineTo(p[0],p[1])); ctx.lineTo(pts[pts.length-1][0], H);
304
- ctx.fillStyle = aGrad; ctx.fill();
305
-
306
- ctx.beginPath(); pts.forEach((p,i)=>i?ctx.lineTo(p[0],p[1]):ctx.moveTo(p[0],p[1]));
307
- ctx.strokeStyle = grad; ctx.lineWidth = 2 * dpr; ctx.stroke();
308
- }
309
-
310
- function gaugeHtml(lbl, pct, valStr='') {
311
- const cls = statusCls(pct);
312
- return \`<div class="gauge-wrap">
313
- <div class="gauge-header"><span>\${lbl}</span><span class="txt-\${cls==='crit'?'red':cls==='warn'?'yellow':'green'}">\${pct.toFixed(1)}%</span></div>
314
- <div class="gauge-track"><div class="gauge-fill \${cls}" style="width:\${pct}%">\${valStr}</div></div>
315
- </div>\`;
316
- }
317
-
318
- function statRow(k,v) { return \`<div class="stat"><div class="stat-label">\${k}</div><div class="stat-val">\${escapeHtml(v)}</div></div>\`; }
319
- function card(title, content, cls='') { return \`<div class="card \${cls}"><div class="card-title">\${title}</div>\${content}</div>\`; }
320
-
321
- function renderOverview(d) {
322
- const cpu = d.cpu.usage;
323
- const mem = d.memory.usagePercent;
324
- const temp = d.cpu.temperature ?? d.thermal.temperatures?.cpu_die ?? 0;
325
- const batt = d.battery?.level ?? 0;
326
-
327
- const rings = \`<div class="ring-container">
328
- \${svgRing(cpu, 100, 'CPU', '%')}
329
- \${svgRing(mem, 100, 'MEM', '%')}
330
- \${svgRing(temp, 100, 'TEMP', '°')}
331
- \${d.battery ? svgRing(batt, 100, 'BATT', '%') : ''}
332
- </div>\`;
333
-
334
- const topProcs = (d.processes||[]).slice(0,8).map(p=>\`<tr>
335
- <td>\${p.pid}</td>
336
- <td class="proc-cmd" title="\${escapeHtml(p.command)}">\${escapeHtml(p.command)}</td>
337
- <td>\${escapeHtml(p.user)}</td>
338
- <td class="txt-\${statusCls(p.cpu)}">\${p.cpu.toFixed(1)}</td>
339
- <td>\${p.mem.toFixed(1)}</td>
340
- </tr>\`).join('');
341
-
342
- return \`<div class="detail-full">
343
- \${rings}
344
- <div class="grid-2x2">
345
- \${card('CPU', \`
346
- \${gaugeHtml('Usage', cpu, d.cpu.frequency+' MHz')}
347
- <div class="spark-wrap"><div class="spark-label">CPU History</div><canvas class="spark" id="sp-ov-cpu"></canvas></div>
348
- \`)}
349
- \${card('Memory', \`
350
- \${gaugeHtml('Usage', mem, fmtBytes(d.memory.used))}
351
- \${statRow('Total', fmtBytes(d.memory.total))}
352
- \${statRow('Free', fmtBytes(d.memory.free))}
353
- \${statRow('Swap', fmtBytes(d.memory.swapUsed)+' / '+fmtBytes(d.memory.swapTotal))}
354
- \`)}
355
- \${card('Network', \`
356
- <div class="flex-row mb-12">
357
- <div class="flex-1 txt-cyan" style="font-family:var(--mono);font-size:14px;font-weight:700">⬇ \${fmtBytes(d.network.rxBytes)}</div>
358
- <div class="flex-1 txt-purple" style="font-family:var(--mono);font-size:14px;font-weight:700;text-align:right">⬆ \${fmtBytes(d.network.txBytes)}</div>
359
- </div>
360
- <div class="flex-row" style="flex:1">
361
- <div class="spark-wrap" style="margin:0"><canvas class="spark" id="sp-ov-rx"></canvas></div>
362
- <div class="spark-wrap" style="margin:0"><canvas class="spark" id="sp-ov-tx"></canvas></div>
363
- </div>
364
- \`)}
365
- \${card('Top Processes', \`
366
- <div class="table-wrap" style="border:none;background:transparent;overflow:hidden;">
367
- <table class="proc">
368
- <thead><tr><th>PID</th><th>Command</th><th>User</th><th>CPU%</th><th>MEM%</th></tr></thead>
369
- <tbody>\${topProcs}</tbody>
370
- </table>
2163
+ const totalThreads = (d.processes || []).reduce((acc, p) => acc + (p.threads || 1), 0);
2164
+ document.getElementById('cpu-stat-threads').textContent = totalThreads.toLocaleString();
2165
+ document.getElementById('cpu-stat-procs').textContent = (d.processes || []).length;
2166
+ document.getElementById('cpu-stat-brand').textContent = (d.cpu.brand || 'Apple Silicon') + ' (' + (d.cpu.cores || 8) + ' cores)';
2167
+
2168
+ // Memory
2169
+ const m = d.memory;
2170
+ document.getElementById('mem-stat-phys').textContent = fmtBytes(m.total);
2171
+ document.getElementById('mem-stat-used').textContent = fmtBytes(m.used);
2172
+ document.getElementById('mem-stat-wired').textContent = fmtBytes(m.wiredBytes || m.used * 0.22);
2173
+ document.getElementById('mem-stat-comp').textContent = fmtBytes(m.compressedBytes || 0);
2174
+ document.getElementById('mem-stat-cached').textContent = fmtBytes(Math.max(0, m.total - m.used - m.free));
2175
+ document.getElementById('mem-stat-swap').textContent = fmtBytes(m.swapUsed);
2176
+ document.getElementById('mem-stat-app').textContent = fmtBytes(Math.max(0, m.used - (m.wiredBytes || m.used * 0.22) - (m.compressedBytes || 0)));
2177
+
2178
+ const pDot = document.getElementById('pressure-dot');
2179
+ const pTxt = document.getElementById('pressure-text');
2180
+ const pct = m.usagePercent || 0;
2181
+ if (pct > 80) {
2182
+ pDot.className = 'pressure-dot crit';
2183
+ pTxt.textContent = 'Critical (' + pct.toFixed(0) + '%)';
2184
+ pTxt.style.color = 'var(--mac-red)';
2185
+ } else if (pct > 65) {
2186
+ pDot.className = 'pressure-dot warn';
2187
+ pTxt.textContent = 'Warning (' + pct.toFixed(0) + '%)';
2188
+ pTxt.style.color = 'var(--mac-yellow)';
2189
+ } else {
2190
+ pDot.className = 'pressure-dot';
2191
+ pTxt.textContent = 'Normal';
2192
+ pTxt.style.color = 'var(--mac-green)';
2193
+ }
2194
+
2195
+ // Energy
2196
+ const tw = d.power?.combinedWatts ?? (d.battery?.powerWatts || 0);
2197
+ document.getElementById('energy-stat-total').textContent = tw ? tw.toFixed(2) + ' W' : '–';
2198
+ document.getElementById('energy-stat-cpu').textContent = d.power?.cpuWatts != null ? d.power.cpuWatts.toFixed(2) + ' W' : '–';
2199
+ document.getElementById('energy-stat-gpu').textContent = d.power?.gpuWatts != null ? d.power.gpuWatts.toFixed(2) + ' W' : '–';
2200
+
2201
+ if (d.battery) {
2202
+ document.getElementById('energy-stat-batt').textContent = d.battery.level + '% (' + d.battery.state + ')';
2203
+ document.getElementById('energy-stat-source').textContent = d.battery.powerSource || 'Battery';
2204
+ document.getElementById('energy-stat-timerem').textContent = d.battery.timeRemaining || 'Calculating…';
2205
+ document.getElementById('energy-stat-health').textContent = (d.battery.condition || 'Normal') + (d.battery.cycles ? ' · ' + d.battery.cycles + ' cycles' : '');
2206
+ } else {
2207
+ document.getElementById('energy-stat-batt').textContent = 'AC Power Only';
2208
+ document.getElementById('energy-stat-source').textContent = 'Power Adapter';
2209
+ document.getElementById('energy-stat-timerem').textContent = 'N/A';
2210
+ document.getElementById('energy-stat-health').textContent = 'Good';
2211
+ }
2212
+
2213
+ // Disk
2214
+ const firstDisk = (d.disk && d.disk[0]) ? d.disk[0] : null;
2215
+ const dRead = firstDisk?.readBytesSec || 0;
2216
+ const dWrite = firstDisk?.writeBytesSec || 0;
2217
+ document.getElementById('disk-stat-reads').textContent = fmtBytes(dRead) + '/s';
2218
+ document.getElementById('disk-stat-writes').textContent = fmtBytes(dWrite) + '/s';
2219
+ document.getElementById('disk-stat-total-read').textContent = firstDisk?.used || '–';
2220
+ document.getElementById('disk-stat-total-write').textContent = firstDisk?.available || '–';
2221
+
2222
+ // Disk Mounts bars
2223
+ const mountsBox = document.getElementById('disk-mounts-box');
2224
+ if (d.disk && d.disk.length) {
2225
+ mountsBox.innerHTML = d.disk.map(v => \`
2226
+ <div style="font-size:10px;">
2227
+ <div style="display:flex;justify-content:space-between;color:var(--text-secondary);">
2228
+ <span>\${escapeHtml(v.mountpoint || '/')}</span>
2229
+ <span>\${escapeHtml(v.used)} / \${escapeHtml(v.size)}</span>
371
2230
  </div>
372
- \`, 'mb-0')}
373
- </div>
374
- </div>\`;
375
- }
376
-
377
- function renderCpu(d) {
378
- let cores = '';
379
- if(d.cpu.coreUsage) {
380
- cores = '<div class="core-grid">' + d.cpu.coreUsage.map((u,i) => {
381
- const p = Math.min(100,u);
382
- return \`<div class="core-col">
383
- <div class="core-bar"><div class="core-fill \${statusCls(p)}" style="height:\${p}%"></div></div>
384
- <div class="core-lbl">C\${i}</div>
385
- </div>\`;
386
- }).join('') + '</div>';
387
- }
388
- return \`<div class="detail-split">
389
- <div class="detail-left">
390
- \${card('CPU Usage', \`
391
- <div class="spark-wrap" style="flex:none"><canvas class="spark large" id="sp-cpu-l"></canvas></div>
392
- \${cores}
393
- \`, 'flex-1')}
394
- </div>
395
- <div class="detail-right">
396
- \${card('Processor Info', \`
397
- <div class="big-metric"><div class="big-val">\${d.cpu.usage.toFixed(1)}%</div><div class="big-unit">Overall Utilisation</div></div>
398
- \${statRow('Model', d.cpu.brand)}
399
- \${statRow('Cores', d.cpu.physicalCores + ' Physical / ' + d.cpu.cores + ' Logical')}
400
- \${statRow('Clock', d.cpu.frequency + ' MHz')}
401
- \${statRow('Load 1m', d.cpu.loadAvg[0].toFixed(2))}
402
- \${statRow('Load 5m', d.cpu.loadAvg[1].toFixed(2))}
403
- \${statRow('Load 15m', d.cpu.loadAvg[2].toFixed(2))}
404
- \`, 'flex-1')}
405
- </div>
406
- </div>\`;
407
- }
408
-
409
- function renderMemory(d) {
410
- return \`<div class="detail-full">
411
- \${card('Memory', \`
412
- <div class="big-metric"><div class="big-val">\${d.memory.usagePercent.toFixed(1)}%</div><div class="big-unit">Memory Used</div></div>
413
- \${gaugeHtml('RAM', d.memory.usagePercent, fmtBytes(d.memory.used))}
414
- \${gaugeHtml('Swap', d.memory.swapTotal? (d.memory.swapUsed/d.memory.swapTotal)*100 : 0, fmtBytes(d.memory.swapUsed))}
415
- <div class="grid-2x2" style="margin-top:20px;flex:none">
416
- <div>\${statRow('Total', fmtBytes(d.memory.total))}\${statRow('Used', fmtBytes(d.memory.used))}</div>
417
- <div>\${statRow('Free', fmtBytes(d.memory.free))}\${statRow('Cached', fmtBytes(d.memory.total - d.memory.free - d.memory.used))}</div>
2231
+ <div class="disk-bar"><div class="disk-bar-fill" style="width:\${parseFloat(v.capacity)||50}%"></div></div>
418
2232
  </div>
419
- <div class="spark-wrap" style="margin-top:20px"><div class="spark-label">Memory History</div><canvas class="spark large" id="sp-mem-l"></canvas></div>
420
- \`, 'flex-1')}
421
- </div>\`;
422
- }
2233
+ \`).join('');
2234
+ }
2235
+
2236
+ // Network
2237
+ document.getElementById('net-stat-pkts-in').textContent = (d.network.rxPacketsPerSec || Math.round(d.network.rxPackets / 1000) || 0).toLocaleString();
2238
+ document.getElementById('net-stat-pkts-out').textContent = (d.network.txPacketsPerSec || Math.round(d.network.txPackets / 1000) || 0).toLocaleString();
2239
+ document.getElementById('net-stat-rate-in').textContent = fmtBytes(d.network.rxRate || 0) + '/s';
2240
+ document.getElementById('net-stat-rate-out').textContent = fmtBytes(d.network.txRate || 0) + '/s';
2241
+ document.getElementById('net-stat-total-in').textContent = fmtBytes(d.network.rxBytes);
2242
+ document.getElementById('net-stat-total-out').textContent = fmtBytes(d.network.txBytes);
2243
+ document.getElementById('net-stat-iface').textContent = (d.network.interface || 'en0') + ' · ' + (d.network.ip || '127.0.0.1');
423
2244
 
424
- function renderGpu(d) {
425
- if(!d.gpu) return '<div class="big-metric">No GPU Data</div>';
426
- return \`<div class="detail-full">
427
- \${card('Graphics Processor', \`
428
- <div class="big-metric"><div class="big-val">\${d.gpu.utilization.toFixed(1)}%</div><div class="big-unit">\${d.gpu.model}</div></div>
429
- \${gaugeHtml('Utilisation', d.gpu.utilization)}
430
- \${statRow('VRAM Used', fmtBytes(d.gpu.memory))}
431
- \${statRow('Active Processes', d.gpu.processes)}
432
- \${d.gpu.temperature? statRow('Temperature', d.gpu.temperature+'°C') : ''}
433
- \`, 'flex-1')}
434
- </div>\`;
2245
+ // GPU
2246
+ if (d.gpu) {
2247
+ document.getElementById('gpu-stat-model').textContent = d.gpu.model || 'Integrated GPU';
2248
+ document.getElementById('gpu-stat-util').textContent = (d.gpu.utilization || 0).toFixed(1) + '%';
2249
+ document.getElementById('gpu-stat-vram').textContent = fmtBytes(d.gpu.memory || 0);
2250
+ document.getElementById('gpu-stat-procs').textContent = d.gpu.processes || 0;
2251
+ document.getElementById('gpu-stat-temp').textContent = d.gpu.temperature ? d.gpu.temperature + '°C' : '–';
2252
+ }
435
2253
  }
436
2254
 
437
- function renderPower(d) {
2255
+ // Ingest Incoming Data Stream
2256
+ let prevRx = null, prevTx = null;
2257
+ function ingestStats(d) {
2258
+ latestData = d;
2259
+
2260
+ const totalCpu = d.cpu.usage || 0;
2261
+ pushHistory(historyData.cpuUser, totalCpu * 0.75);
2262
+ pushHistory(historyData.cpuSystem, totalCpu * 0.25);
2263
+ pushHistory(historyData.memPressure, d.memory.usagePercent || 0);
2264
+
438
2265
  const tw = d.power?.combinedWatts ?? (d.battery?.powerWatts || 0);
439
- return \`<div class="detail-full">
440
- \${card('Power Consumption', \`
441
- <div class="big-metric"><div class="big-val">\${tw.toFixed(2)} W</div><div class="big-unit">Total Draw</div></div>
442
- <div class="flex-row">
443
- \${d.power?.cpuWatts!=null ? \`<div class="flex-1">\${gaugeHtml('CPU Package', Math.min(100,d.power.cpuWatts/100*100), d.power.cpuWatts.toFixed(2)+'W')}</div>\` : ''}
444
- \${d.power?.gpuWatts!=null ? \`<div class="flex-1">\${gaugeHtml('GPU Package', Math.min(100,d.power.gpuWatts/200*100), d.power.gpuWatts.toFixed(2)+'W')}</div>\` : ''}
445
- </div>
446
- \`, 'flex-1')}
447
- </div>\`;
2266
+ pushHistory(historyData.energyWatts, tw);
2267
+
2268
+ const firstDisk = (d.disk && d.disk[0]) ? d.disk[0] : null;
2269
+ pushHistory(historyData.diskRead, firstDisk?.readBytesSec || 0);
2270
+ pushHistory(historyData.diskWrite, firstDisk?.writeBytesSec || 0);
2271
+
2272
+ let rxRate = d.network.rxRate || 0;
2273
+ let txRate = d.network.txRate || 0;
2274
+ if (!rxRate && prevRx !== null) rxRate = Math.max(0, d.network.rxBytes - prevRx) / 2;
2275
+ if (!txRate && prevTx !== null) txRate = Math.max(0, d.network.txBytes - prevTx) / 2;
2276
+ prevRx = d.network.rxBytes; prevTx = d.network.txBytes;
2277
+
2278
+ pushHistory(historyData.netIn, rxRate);
2279
+ pushHistory(historyData.netOut, txRate);
2280
+ pushHistory(historyData.gpuUtil, d.gpu?.utilization || 0);
2281
+
2282
+ // Status Bar
2283
+ document.getElementById('sb-dot').className = 'sb-live-dot';
2284
+ document.getElementById('sb-status').textContent = 'Live';
2285
+ document.getElementById('sb-host').textContent = (d.header.hostname || 'Mac') + ' (' + (d.header.os || 'macOS') + ')';
2286
+ document.getElementById('sb-time').textContent = new Date().toLocaleTimeString();
2287
+
2288
+ // Analyze telemetry for automatic log events
2289
+ analyzeAndLog(d);
2290
+
2291
+ renderTableRows();
2292
+ updateBottomPanel();
2293
+ drawAllCanvases();
448
2294
  }
449
2295
 
450
- function renderBattery(d) {
451
- if(!d.battery) return '<div class="big-metric">No Battery</div>';
452
- return \`<div class="detail-full">
453
- \${card('Battery', \`
454
- <div class="ring-container" style="border:none;background:transparent;">
455
- \${svgRing(d.battery.level, 100, 'LEVEL', '%', 200)}
456
- </div>
457
- <div class="grid-2x2">
458
- <div>
459
- \${statRow('State', d.battery.state)}
460
- \${statRow('Power Source', d.battery.powerSource)}
461
- \${statRow('Condition', d.battery.condition)}
462
- \${d.battery.cycles ? statRow('Cycles', d.battery.cycles) : ''}
463
- </div>
464
- <div>
465
- \${d.battery.timeRemaining ? statRow('Time Remaining', d.battery.timeRemaining) : ''}
466
- \${d.battery.powerWatts ? statRow('Power Draw', d.battery.powerWatts.toFixed(2)+' W') : ''}
467
- \${d.battery.maxCapacityPercent ? statRow('Max Capacity', d.battery.maxCapacityPercent+'%') : ''}
468
- </div>
2296
+ // Window Resize Hook
2297
+ window.addEventListener('resize', () => {
2298
+ requestAnimationFrame(drawAllCanvases);
2299
+ });
2300
+
2301
+ // Modal Management
2302
+ function openModal(id) {
2303
+ const m = document.getElementById(id);
2304
+ if (m) m.classList.add('open');
2305
+ }
2306
+ function closeModal(id) {
2307
+ const m = document.getElementById(id);
2308
+ if (m) m.classList.remove('open');
2309
+ }
2310
+
2311
+ // Quit Confirmation
2312
+ function confirmQuitSelected() {
2313
+ if (selectedPid === null) return;
2314
+ const p = (latestData?.processes || []).find(x => x.pid === selectedPid);
2315
+ const name = p ? p.command.replace(/^.*[\\\/]/, '') : 'selected process';
2316
+ 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>\`;
2317
+ openModal('modal-quit');
2318
+ }
2319
+
2320
+ function confirmQuitFromInspector() {
2321
+ closeModal('modal-inspector');
2322
+ confirmQuitSelected();
2323
+ }
2324
+
2325
+ async function executeQuit(isForce) {
2326
+ if (selectedPid === null) return;
2327
+ const pidToKill = selectedPid;
2328
+ const sig = isForce ? 'SIGKILL' : 'SIGTERM';
2329
+ closeModal('modal-quit');
2330
+
2331
+ try {
2332
+ const res = await fetch(\`/api/kill?pid=\${pidToKill}&signal=\${sig}\`);
2333
+ const data = await res.json();
2334
+ if (data.success) {
2335
+ showToast(\`Sent \${sig} to PID \${pidToKill}\`);
2336
+ const p = (latestData?.processes || []).find(x => x.pid === pidToKill);
2337
+ const pname = p ? p.command.replace(/^.*[\\\\/]/, '') : 'PID ' + pidToKill;
2338
+ addLogEntry('ACTION', 'PROCESS',
2339
+ \`Sent \${sig} to \${pname} (PID \${pidToKill})\`,
2340
+ isForce ? 'Force kill' : 'Graceful quit');
2341
+ selectedPid = null;
2342
+ document.getElementById('btn-stop').disabled = true;
2343
+ document.getElementById('btn-inspect').disabled = true;
2344
+ } else {
2345
+ addLogEntry('WARN', 'PROCESS',
2346
+ \`Failed to send \${sig} to PID \${pidToKill}\`,
2347
+ data.error || 'Unknown error');
2348
+ showToast('Error: ' + (data.error || 'Failed to kill process'));
2349
+ }
2350
+ } catch (e) {
2351
+ showToast('Network error: ' + e.message);
2352
+ }
2353
+ }
2354
+
2355
+ // Process Inspector
2356
+ function openInspector(pid) {
2357
+ selectRow(pid);
2358
+ const p = (latestData?.processes || []).find(x => x.pid === pid);
2359
+ if (!p) return;
2360
+
2361
+ const shortName = p.command.replace(/^.*[\\\/]/, '');
2362
+ document.getElementById('inspect-title').textContent = \`\${shortName} (PID \${p.pid})\`;
2363
+
2364
+ const totalMem = latestData.memory?.total || 1;
2365
+ const memBytes = (p.mem / 100) * totalMem;
2366
+
2367
+ document.getElementById('inspect-body').innerHTML = \`
2368
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:16px;">
2369
+ <div class="proc-icon user" style="width:36px;height:36px;font-size:16px;border-radius:6px;">\${shortName[0]?.toUpperCase() || 'P'}</div>
2370
+ <div>
2371
+ <div style="font-size:14px;font-weight:700;">\${escapeHtml(shortName)}</div>
2372
+ <div style="font-size:11px;color:var(--text-secondary);word-break:break-all;">\${escapeHtml(p.command)}</div>
469
2373
  </div>
470
- \`, 'flex-1')}
471
- </div>\`;
472
- }
473
-
474
- function renderThermal(d) {
475
- const chips = d.thermal.temperatures ? Object.entries(d.thermal.temperatures).map(([k,v]) => \`<div class="chip \${statusCls(v)}">\${k}: \${v.toFixed(1)}°C</div>\`).join('') : '';
476
- return \`<div class="detail-full">
477
- \${card('Thermal Status', \`
478
- \${statRow('State', d.thermal.state)}
479
- \${statRow('Pressure', d.thermal.pressureLevel ?? 'Normal')}
480
- <div class="chips mb-12">\${chips}</div>
481
- <div class="spark-wrap"><div class="spark-label">CPU Temp History</div><canvas class="spark large" id="sp-therm-l"></canvas></div>
482
- \`, 'flex-1')}
483
- </div>\`;
484
- }
485
-
486
- function renderNetwork(d) {
487
- return \`<div class="detail-split">
488
- <div class="detail-left">
489
- \${card('Traffic', \`
490
- <div class="flex-row">
491
- <div class="flex-1 spark-wrap"><div class="spark-label txt-cyan">⬇ RX Rate</div><canvas class="spark large" id="sp-net-rx-l"></canvas></div>
492
- <div class="flex-1 spark-wrap"><div class="spark-label txt-purple">⬆ TX Rate</div><canvas class="spark large" id="sp-net-tx-l"></canvas></div>
493
- </div>
494
- \`, 'flex-1')}
495
2374
  </div>
496
- <div class="detail-right">
497
- \${card('Interface Info', \`
498
- <div class="big-metric" style="padding:10px 0"><div class="big-val txt-cyan" style="font-size:24px">\${d.network.interface}</div><div class="big-unit">\${d.network.ip}</div></div>
499
- \${statRow('RX Total', fmtBytes(d.network.rxBytes))}
500
- \${statRow('TX Total', fmtBytes(d.network.txBytes))}
501
- \${statRow('RX Packets', (d.network.rxPackets||0).toLocaleString())}
502
- \${statRow('TX Packets', (d.network.txPackets||0).toLocaleString())}
503
- \${d.network.connections ? statRow('TCP Conns', d.network.connections) : ''}
504
- \`, 'flex-1')}
2375
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px 16px;font-size:12px;">
2376
+ <div class="stat-item"><span class="stat-k">Process ID:</span><span class="stat-v">\${p.pid}</span></div>
2377
+ <div class="stat-item"><span class="stat-k">Parent Process ID:</span><span class="stat-v">\${p.ppid || '1'}</span></div>
2378
+ <div class="stat-item"><span class="stat-k">User:</span><span class="stat-v">\${escapeHtml(p.user || 'root')}</span></div>
2379
+ <div class="stat-item"><span class="stat-k">State:</span><span class="stat-v">\${escapeHtml(p.state || 'R')}</span></div>
2380
+ <div class="stat-item"><span class="stat-k">% CPU Usage:</span><span class="stat-v">\${p.cpu.toFixed(1)}%</span></div>
2381
+ <div class="stat-item"><span class="stat-k">CPU Runtime:</span><span class="stat-v">\${fmtTime(p.runtime)}</span></div>
2382
+ <div class="stat-item"><span class="stat-k">Memory:</span><span class="stat-v">\${fmtBytes(memBytes)} (\${p.mem.toFixed(1)}%)</span></div>
2383
+ <div class="stat-item"><span class="stat-k">Threads:</span><span class="stat-v">\${p.threads || 1}</span></div>
505
2384
  </div>
506
- </div>\`;
2385
+ \`;
2386
+
2387
+ openModal('modal-inspector');
507
2388
  }
508
2389
 
509
- function renderDisk(d) {
510
- const rows = (d.disk||[]).map(ds => \`
511
- <div class="mb-12">
512
- <div class="flex-row" style="justify-content:space-between;font-family:var(--mono);font-size:12px;margin-bottom:6px;">
513
- <span class="txt-cyan" style="font-weight:700">\${ds.mountpoint}</span>
514
- <span>\${ds.used} / \${ds.size}</span>
515
- </div>
516
- \${gaugeHtml('', parseFloat(ds.capacity)||0, ds.capacity)}
517
- </div>\`).join('');
518
- return \`<div class="detail-full">\${card('Storage Volumes', rows, 'flex-1')}</div>\`;
519
- }
520
-
521
- function sortProcs(procs) {
522
- const col = procSort.col;
523
- const dir = procSort.dir;
524
- return [...procs].sort((a,b)=>{
525
- let va=a[col], vb=b[col];
526
- if(typeof va==='string'){va=va.toLowerCase();vb=vb.toLowerCase();}
527
- return (va<vb ? -1 : va>vb ? 1 : 0) * (dir==='asc'?1:-1);
2390
+ function openInspectorSelected() {
2391
+ if (selectedPid !== null) openInspector(selectedPid);
2392
+ }
2393
+
2394
+ // ═══════════════════ SETTINGS & PREFERENCES ═══════════════════
2395
+
2396
+ let currentConfig = {};
2397
+
2398
+ async function loadConfig() {
2399
+ try {
2400
+ const res = await fetch('/api/config');
2401
+ if (res.ok) {
2402
+ currentConfig = await res.json();
2403
+ applyConfigToUI(currentConfig);
2404
+ }
2405
+ } catch (err) {
2406
+ console.error('Failed to load config:', err);
2407
+ }
2408
+ }
2409
+
2410
+ function applyConfigToUI(cfg) {
2411
+ if (!cfg) return;
2412
+ const setVal = (id, val) => {
2413
+ const el = document.getElementById(id);
2414
+ if (el && val !== undefined && val !== null) el.value = val;
2415
+ };
2416
+ const setChecked = (id, val) => {
2417
+ const el = document.getElementById(id);
2418
+ if (el && val !== undefined) el.checked = !!val;
2419
+ };
2420
+
2421
+ setVal('set-interval', cfg.interval || cfg.telemetryInterval || '2');
2422
+ setVal('set-filter', cfg.defaultFilter || 'all');
2423
+ setVal('set-sort', cfg.defaultSort || 'cpu');
2424
+ setChecked('set-compact', cfg.compact);
2425
+ setChecked('set-ontop', cfg.alwaysOnTop);
2426
+ setChecked('set-lan', cfg.exposeLan);
2427
+ setVal('set-port', cfg.port || 3000);
2428
+ setVal('set-history-len', cfg.historyDuration || '60');
2429
+ setChecked('set-show-graphs', cfg.showGraphs !== false);
2430
+ setVal('set-density', cfg.tableDensity || 'standard');
2431
+ if (cfg.tableDensity) onDensityChange(cfg.tableDensity, false);
2432
+
2433
+ setVal('set-cpu-alert', cfg.cpuAlertPct ?? 90);
2434
+ setVal('set-temp-alert', cfg.tempAlertC ?? 95);
2435
+ setVal('set-watchdog-proc', cfg.watchdogProcess || '');
2436
+ setVal('set-watchdog-mem', cfg.watchdogMem ?? 80);
2437
+ setVal('set-slack-url', cfg.slackAlertUrl || '');
2438
+ setVal('set-discord-url', cfg.discordAlertUrl || '');
2439
+ setVal('set-ntfy-url', cfg.ntfyUrl || '');
2440
+
2441
+ // Apply default filter to live state if filter is currently 'all'
2442
+ if (cfg.defaultFilter && currentFilter === 'all') {
2443
+ const filterSelect = document.getElementById('filter-select');
2444
+ if (filterSelect) {
2445
+ filterSelect.value = cfg.defaultFilter;
2446
+ currentFilter = cfg.defaultFilter;
2447
+ }
2448
+ }
2449
+ }
2450
+
2451
+ async function fetchServiceStatus() {
2452
+ try {
2453
+ const pill = document.getElementById('svc-status-pill');
2454
+ if (pill) {
2455
+ pill.className = 'status-pill inactive';
2456
+ pill.textContent = 'Checking…';
2457
+ }
2458
+ const res = await fetch('/api/service/status');
2459
+ if (res.ok) {
2460
+ const s = await res.json();
2461
+ const chk = document.getElementById('set-autostart');
2462
+ if (chk) chk.checked = !!s.installed;
2463
+ if (pill) {
2464
+ if (s.installed) {
2465
+ pill.className = 'status-pill active';
2466
+ pill.textContent = 'Active (' + (s.platform === 'darwin' ? 'LaunchAgent loaded' : 'systemd active') + ')';
2467
+ } else {
2468
+ pill.className = 'status-pill inactive';
2469
+ pill.textContent = 'Inactive (Not installed)';
2470
+ }
2471
+ }
2472
+ const lbl = document.getElementById('svc-label');
2473
+ if (lbl && s.serviceLabel) lbl.textContent = s.serviceLabel;
2474
+ const pth = document.getElementById('svc-path');
2475
+ if (pth && s.path) pth.textContent = s.path;
2476
+ const logEl = document.getElementById('svc-log');
2477
+ if (logEl && s.logPath) logEl.textContent = s.logPath;
2478
+ }
2479
+ } catch (err) {
2480
+ console.error('Failed to get service status:', err);
2481
+ }
2482
+ }
2483
+
2484
+ function updateSysInfoInSettings() {
2485
+ const container = document.getElementById('diag-sys-info');
2486
+ if (!container) return;
2487
+ const sys = latestData?.system;
2488
+ if (!sys) {
2489
+ container.innerHTML = '<div>System information waiting for initial telemetry frame…</div>';
2490
+ return;
2491
+ }
2492
+ container.innerHTML = \`
2493
+ <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>
2494
+ <div style="display:flex;justify-content:space-between;"><span style="color:var(--text-secondary);">Hardware Architecture:</span><span>\${escapeHtml(sys.arch || 'arm64')}</span></div>
2495
+ <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>
2496
+ <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>
2497
+ <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>
2498
+ <div style="display:flex;justify-content:space-between;"><span style="color:var(--text-secondary);">System Uptime:</span><span>\${fmtUptime(sys.uptime || 0)}</span></div>
2499
+ \`;
2500
+ }
2501
+
2502
+ function openSettingsModal() {
2503
+ openModal('modal-settings');
2504
+ loadConfig();
2505
+ fetchServiceStatus();
2506
+ updateSysInfoInSettings();
2507
+ }
2508
+
2509
+ function closeSettingsModal() {
2510
+ closeModal('modal-settings');
2511
+ }
2512
+
2513
+ function switchSettingsTab(tabName) {
2514
+ document.querySelectorAll('.settings-tab-btn').forEach(btn => {
2515
+ btn.classList.toggle('active', btn.dataset.stab === tabName);
528
2516
  });
2517
+ document.querySelectorAll('.settings-pane').forEach(pane => {
2518
+ pane.classList.toggle('active', pane.id === 'spane-' + tabName);
2519
+ });
2520
+ if (tabName === 'diagnostics') {
2521
+ updateSysInfoInSettings();
2522
+ }
529
2523
  }
530
- function sortBy(col) {
531
- if(procSort.col===col) procSort.dir = procSort.dir==='asc'?'desc':'asc';
532
- else {procSort.col=col; procSort.dir='desc';}
533
- if(latestData) updateView();
534
- }
535
- function sortHdr(col) { return procSort.col===col ? (procSort.dir==='asc'?' ↑':' ↓') : ''; }
536
- function fmtTime(s) { if(!s)return'–'; const h=Math.floor(s/3600), m=Math.floor((s%3600)/60), sc=Math.floor(s%60); return h?h+'h'+m+'m':m?m+'m'+sc+'s':sc+'s'; }
537
-
538
- function renderProcesses(d) {
539
- const procs = sortProcs(d.processes||[]);
540
- const rows = procs.map(p=>\`<tr>
541
- <td>\${p.pid}</td>
542
- <td class="proc-cmd" title="\${escapeHtml(p.command)}">\${escapeHtml(p.command)}</td>
543
- <td>\${escapeHtml(p.user)}</td>
544
- <td class="txt-\${statusCls(p.cpu)}">\${p.cpu.toFixed(1)}</td>
545
- <td>\${p.mem.toFixed(1)}</td>
546
- <td>\${p.threads||0}</td>
547
- <td>\${p.state}</td>
548
- <td>\${fmtTime(p.runtime)}</td>
549
- </tr>\`).join('');
550
- return \`<div class="detail-full">
551
- \${card('Process List ('+procs.length+')', \`
552
- <div class="table-wrap">
553
- <table class="proc">
554
- <thead><tr>
555
- <th onclick="sortBy('pid')">PID\${sortHdr('pid')}</th>
556
- <th onclick="sortBy('command')">Command\${sortHdr('command')}</th>
557
- <th onclick="sortBy('user')">User\${sortHdr('user')}</th>
558
- <th onclick="sortBy('cpu')">CPU%\${sortHdr('cpu')}</th>
559
- <th onclick="sortBy('mem')">MEM%\${sortHdr('mem')}</th>
560
- <th onclick="sortBy('threads')">Thr\${sortHdr('threads')}</th>
561
- <th onclick="sortBy('state')">State\${sortHdr('state')}</th>
562
- <th onclick="sortBy('runtime')">Time\${sortHdr('runtime')}</th>
563
- </tr></thead>
564
- <tbody>\${rows}</tbody>
565
- </table>
566
- </div>
567
- \`, 'flex-1')}
568
- </div>\`;
2524
+
2525
+ async function onSettingChange() {
2526
+ const getNum = (id, def) => {
2527
+ const el = document.getElementById(id);
2528
+ const n = parseFloat(el?.value);
2529
+ return isNaN(n) ? def : n;
2530
+ };
2531
+ const getStr = (id, def = '') => {
2532
+ const el = document.getElementById(id);
2533
+ return el ? el.value.trim() : def;
2534
+ };
2535
+ const getBool = (id, def = false) => {
2536
+ const el = document.getElementById(id);
2537
+ return el ? el.checked : def;
2538
+ };
2539
+
2540
+ const patch = {
2541
+ interval: getNum('set-interval', 2),
2542
+ defaultFilter: getStr('set-filter', 'all'),
2543
+ defaultSort: getStr('set-sort', 'cpu'),
2544
+ compact: getBool('set-compact', false),
2545
+ alwaysOnTop: getBool('set-ontop', false),
2546
+ exposeLan: getBool('set-lan', false),
2547
+ port: Math.round(getNum('set-port', 3000)),
2548
+ historyDuration: Math.round(getNum('set-history-len', 60)),
2549
+ showGraphs: getBool('set-show-graphs', true),
2550
+ cpuAlertPct: getNum('set-cpu-alert', 90),
2551
+ tempAlertC: getNum('set-temp-alert', 95),
2552
+ watchdogProcess: getStr('set-watchdog-proc', ''),
2553
+ watchdogMem: getNum('set-watchdog-mem', 80),
2554
+ slackAlertUrl: getStr('set-slack-url', ''),
2555
+ discordAlertUrl: getStr('set-discord-url', ''),
2556
+ ntfyUrl: getStr('set-ntfy-url', '')
2557
+ };
2558
+
2559
+ // Adjust graph buffer size if historyDuration changed
2560
+ if (patch.historyDuration && patch.interval) {
2561
+ MAX_POINTS = Math.max(20, Math.round(patch.historyDuration / patch.interval));
2562
+ }
2563
+
2564
+ try {
2565
+ const res = await fetch('/api/config', {
2566
+ method: 'POST',
2567
+ headers: { 'Content-Type': 'application/json' },
2568
+ body: JSON.stringify(patch)
2569
+ });
2570
+ if (res.ok) {
2571
+ const data = await res.json();
2572
+ currentConfig = data.config || patch;
2573
+ const hint = document.getElementById('settings-status-hint');
2574
+ if (hint) {
2575
+ hint.textContent = 'Saved to ~/.config/pyre/config.json • ' + new Date().toLocaleTimeString();
2576
+ hint.style.color = 'var(--mac-green)';
2577
+ setTimeout(() => {
2578
+ hint.style.color = 'var(--text-tertiary)';
2579
+ hint.textContent = 'Preferences saved to ~/.config/pyre/config.json';
2580
+ }, 3000);
2581
+ }
2582
+ }
2583
+ } catch (err) {
2584
+ console.error('Failed to save preferences:', err);
2585
+ }
569
2586
  }
570
2587
 
571
- function renderAnomalies(d) {
572
- const arr = d.anomalies||[];
573
- if(!arr.length) return '<div class="big-metric"><div class="big-val txt-green" style="font-size:32px">System Healthy</div></div>';
574
- const rows = arr.map(a=>\`<div class="anomaly"><div class="anomaly-icon">⚠️</div><div><div class="anomaly-title">\${escapeHtml(a.type||'Alert')}</div><div class="anomaly-desc">\${escapeHtml(a.message||a.detail||JSON.stringify(a))}</div></div></div>\`).join('');
575
- return \`<div class="detail-full">\${card('Anomalies', rows, 'flex-1')}</div>\`;
2588
+ function onDensityChange(density, save = true) {
2589
+ const table = document.getElementById('proc-table');
2590
+ if (table) {
2591
+ table.classList.remove('density-compact', 'density-standard', 'density-comfortable');
2592
+ table.classList.add('density-' + density);
2593
+ }
2594
+ if (save) {
2595
+ fetch('/api/config', {
2596
+ method: 'POST',
2597
+ headers: { 'Content-Type': 'application/json' },
2598
+ body: JSON.stringify({ tableDensity: density })
2599
+ }).catch(console.error);
2600
+ showToast('Table density: ' + density.charAt(0).toUpperCase() + density.slice(1));
2601
+ }
576
2602
  }
577
2603
 
578
- function drawSparks() {
579
- drawSpark('sp-ov-cpu', history.cpu, '#ffffff', '#999999');
580
- drawSpark('sp-ov-rx', history.rxRate, '#ffffff', '#999999');
581
- drawSpark('sp-ov-tx', history.txRate, '#bbbbbb', '#666666');
582
- drawSpark('sp-cpu-l', history.cpu, '#ffffff', '#999999');
583
- drawSpark('sp-mem-l', history.mem, '#bbbbbb', '#666666');
584
- drawSpark('sp-therm-l', history.temp, '#dddddd', '#888888');
585
- drawSpark('sp-net-rx-l', history.rxRate, '#ffffff', '#999999');
586
- drawSpark('sp-net-tx-l', history.txRate, '#bbbbbb', '#666666');
2604
+ async function toggleAutostartFromUI(enabled) {
2605
+ const pill = document.getElementById('svc-status-pill');
2606
+ if (pill) {
2607
+ pill.textContent = 'Updating service…';
2608
+ pill.className = 'status-pill';
2609
+ }
2610
+ try {
2611
+ const res = await fetch('/api/service/toggle', {
2612
+ method: 'POST',
2613
+ headers: { 'Content-Type': 'application/json' },
2614
+ body: JSON.stringify({ enabled })
2615
+ });
2616
+ const data = await res.json();
2617
+ if (data.success) {
2618
+ showToast(data.message || (enabled ? 'Service enabled' : 'Service disabled'));
2619
+ addLogEntry('ACTION', 'SYSTEM', 'Background service ' + (enabled ? 'enabled' : 'disabled'), data.message);
2620
+ fetchServiceStatus();
2621
+ } else {
2622
+ showToast('Error: ' + (data.error || 'Failed to modify service'));
2623
+ const chk = document.getElementById('set-autostart');
2624
+ if (chk) chk.checked = !enabled;
2625
+ fetchServiceStatus();
2626
+ }
2627
+ } catch (err) {
2628
+ showToast('Failed to contact Pyre server');
2629
+ const chk = document.getElementById('set-autostart');
2630
+ if (chk) chk.checked = !enabled;
2631
+ fetchServiceStatus();
2632
+ }
587
2633
  }
588
2634
 
589
- function switchView(v) {
590
- currentView = v;
591
- document.querySelectorAll('.tab').forEach(t=>t.classList.toggle('active', t.dataset.view===v));
592
- if(latestData) updateView();
2635
+ async function sendTestAlertFromUI() {
2636
+ showToast('Dispatching test notification…');
2637
+ try {
2638
+ const res = await fetch('/api/alerts/test', {
2639
+ method: 'POST',
2640
+ headers: { 'Content-Type': 'application/json' },
2641
+ body: JSON.stringify({ message: 'Pyre Activity Monitor: Test notification triggered from Web Dashboard' })
2642
+ });
2643
+ const data = await res.json();
2644
+ if (data.success) {
2645
+ showToast(data.message || 'Notification dispatched');
2646
+ addLogEntry('WATCHDOG', 'SYSTEM', 'Test notification sent', data.message);
2647
+ } else {
2648
+ showToast('Alert test failed: ' + (data.error || 'Unknown error'));
2649
+ }
2650
+ } catch (err) {
2651
+ showToast('Failed to dispatch test notification');
2652
+ }
593
2653
  }
594
2654
 
595
- function updateView() {
596
- const el = document.getElementById('view-content');
597
- if(!el || !latestData) return;
598
- const d = latestData;
599
- const map = {overview:renderOverview, cpu:renderCpu, memory:renderMemory, gpu:renderGpu, power:renderPower, battery:renderBattery, thermal:renderThermal, network:renderNetwork, disk:renderDisk, processes:renderProcesses, anomalies:renderAnomalies};
600
- if(map[currentView]) {
601
- el.innerHTML = map[currentView](d);
602
- requestAnimationFrame(drawSparks);
2655
+ async function runDoctorFromUI() {
2656
+ const btn = document.getElementById('btn-run-doctor');
2657
+ const resEl = document.getElementById('doctor-results');
2658
+ if (btn) {
2659
+ btn.disabled = true;
2660
+ btn.textContent = 'Running…';
2661
+ }
2662
+ if (resEl) {
2663
+ resEl.style.display = 'block';
2664
+ resEl.textContent = 'Running system diagnostic checks…\\nScanning kernel metrics, thermal sensors, battery, launchd daemons, network interfaces…';
2665
+ }
2666
+
2667
+ try {
2668
+ const res = await fetch('/api/doctor');
2669
+ const data = await res.json();
2670
+ if (btn) {
2671
+ btn.disabled = false;
2672
+ btn.textContent = 'Run Diagnostics';
2673
+ }
2674
+ if (resEl) {
2675
+ if (data.success && data.report) {
2676
+ let out = '✓ System Diagnostics Complete\\n';
2677
+ out += 'Timestamp: ' + (data.report.timestamp || new Date().toISOString()) + '\\n';
2678
+ out += 'Overall Status: ' + (data.report.status || 'HEALTHY').toUpperCase() + '\\n\\n';
2679
+ if (Array.isArray(data.report.checks)) {
2680
+ for (const c of data.report.checks) {
2681
+ const sym = c.ok ? ' ✓ ' : ' ✗ ';
2682
+ out += sym + c.name + (c.detail ? ': ' + c.detail : '') + '\\n';
2683
+ }
2684
+ }
2685
+ resEl.textContent = out;
2686
+ addLogEntry('INFO', 'SYSTEM', 'Diagnostics completed', data.report.status || 'OK');
2687
+ } else {
2688
+ resEl.textContent = 'Diagnostic run complete.\\n\\n' + JSON.stringify(data, null, 2);
2689
+ }
2690
+ }
2691
+ } catch (err) {
2692
+ if (btn) {
2693
+ btn.disabled = false;
2694
+ btn.textContent = 'Run Diagnostics';
2695
+ }
2696
+ if (resEl) {
2697
+ resEl.textContent = 'Error executing diagnostics: ' + err.message;
2698
+ }
603
2699
  }
604
2700
  }
605
2701
 
606
- let prx=null, ptx=null;
607
- function ingestData(d) {
608
- latestData = d;
609
- pushHist(history.cpu, d.cpu.usage);
610
- pushHist(history.mem, d.memory.usagePercent);
611
- pushHist(history.temp, d.cpu.temperature ?? d.thermal.temperatures?.cpu_die ?? 0);
612
- if(prx!==null) {
613
- pushHist(history.rxRate, Math.max(0, d.network.rxBytes-prx));
614
- pushHist(history.txRate, Math.max(0, d.network.txBytes-ptx));
615
- }
616
- prx=d.network.rxBytes; ptx=d.network.txBytes;
617
- if(d.power?.combinedWatts) pushHist(history.power, d.power.combinedWatts);
618
-
619
- document.getElementById('titlebar-host').textContent = d.header.hostname+' · '+d.header.os;
620
- document.getElementById('sb-uptime').textContent = d.header.uptime;
621
- document.getElementById('sb-cpu').textContent = d.cpu.usage.toFixed(1)+'%';
622
- document.getElementById('sb-mem').textContent = d.memory.usagePercent.toFixed(1)+'%';
623
- document.getElementById('sb-batt').textContent = d.battery ? d.battery.level+'%' : '–';
624
- document.getElementById('sb-procs').textContent = (d.processes||[]).length;
625
- document.getElementById('sb-refresh').textContent = new Date().toLocaleTimeString();
626
-
627
- updateView();
2702
+ async function resetConfigFromUI() {
2703
+ if (!confirm('Are you sure you want to reset all preferences to factory defaults?')) {
2704
+ return;
2705
+ }
2706
+ try {
2707
+ const res = await fetch('/api/config/reset', { method: 'POST' });
2708
+ const data = await res.json();
2709
+ if (data.success) {
2710
+ showToast('Preferences reset to factory defaults');
2711
+ addLogEntry('ACTION', 'UI', 'Preferences reset to factory defaults');
2712
+ loadConfig();
2713
+ } else {
2714
+ showToast('Error resetting preferences: ' + (data.error || 'Unknown error'));
2715
+ }
2716
+ } catch (err) {
2717
+ showToast('Failed to reset preferences');
2718
+ }
2719
+ }
2720
+
2721
+ // Keyboard Navigation
2722
+ document.addEventListener('keydown', e => {
2723
+ if (e.key === 'Escape') {
2724
+ closeModal('modal-quit');
2725
+ closeModal('modal-inspector');
2726
+ closeSettingsModal();
2727
+ return;
2728
+ }
2729
+
2730
+ // Cmd+, or , (when not in an input/select/textarea) opens preferences
2731
+ if ((e.key === ',' && (e.metaKey || e.ctrlKey)) ||
2732
+ (e.key === ',' && !document.activeElement.matches('input, select, textarea'))) {
2733
+ e.preventDefault();
2734
+ openSettingsModal();
2735
+ return;
2736
+ }
2737
+
2738
+ // If focused in search, don't hijack keys
2739
+ if (document.activeElement === document.getElementById('search-input') ||
2740
+ document.activeElement === document.getElementById('log-search-input')) {
2741
+ return;
2742
+ }
2743
+
2744
+ // Segmented shortcuts 1-7
2745
+ const keyTabs = { '1': 'cpu', '2': 'memory', '3': 'energy', '4': 'disk', '5': 'network', '6': 'gpu', '7': 'logs' };
2746
+ if (keyTabs[e.key]) {
2747
+ switchTab(keyTabs[e.key]);
2748
+ return;
2749
+ }
2750
+
2751
+ // r = toggle recording
2752
+ if (e.key === 'r' && !e.metaKey && !e.ctrlKey) {
2753
+ e.preventDefault();
2754
+ toggleWebRecording();
2755
+ return;
2756
+ }
2757
+
2758
+ // l = focus log search (when on logs tab)
2759
+ if (e.key === 'l' && currentTab === 'logs') {
2760
+ e.preventDefault();
2761
+ const ls = document.getElementById('log-search-input');
2762
+ if (ls) { ls.focus(); ls.select(); }
2763
+ return;
2764
+ }
2765
+
2766
+ // Table arrow navigation
2767
+ if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && latestData?.processes?.length) {
2768
+ e.preventDefault();
2769
+ const rows = Array.from(document.querySelectorAll('#proc-tbody tr'));
2770
+ if (!rows.length) return;
2771
+ const curIdx = rows.findIndex(tr => Number(tr.dataset.pid) === selectedPid);
2772
+ let nextIdx = curIdx;
2773
+ if (e.key === 'ArrowDown') nextIdx = curIdx < rows.length - 1 ? curIdx + 1 : 0;
2774
+ if (e.key === 'ArrowUp') nextIdx = curIdx > 0 ? curIdx - 1 : rows.length - 1;
2775
+ const nextPid = Number(rows[nextIdx].dataset.pid);
2776
+ selectRow(nextPid);
2777
+ rows[nextIdx].scrollIntoView({ block: 'nearest' });
2778
+ return;
2779
+ }
2780
+
2781
+ // Enter or Space opens inspector
2782
+ if ((e.key === 'Enter' || e.key === ' ') && selectedPid !== null) {
2783
+ e.preventDefault();
2784
+ openInspectorSelected();
2785
+ return;
2786
+ }
2787
+
2788
+ // Delete or Backspace prompts quit
2789
+ if ((e.key === 'Backspace' || e.key === 'Delete') && selectedPid !== null) {
2790
+ e.preventDefault();
2791
+ confirmQuitSelected();
2792
+ return;
2793
+ }
2794
+
2795
+ // Cmd+F or / focuses search
2796
+ if ((e.key === 'f' && (e.metaKey || e.ctrlKey)) || e.key === '/') {
2797
+ e.preventDefault();
2798
+ const s = document.getElementById('search-input');
2799
+ s.focus();
2800
+ s.select();
2801
+ }
2802
+ });
2803
+
2804
+ // ═══════════════════ LOGGING & RECORDING ═══════════════════
2805
+
2806
+ const MAX_LOG_ENTRIES = 500;
2807
+
2808
+ function addLogEntry(level, subsys, msg, ctx) {
2809
+ const entry = {
2810
+ ts: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 1 }),
2811
+ level, // 'INFO','WARN','CRIT','ACTION','WATCHDOG'
2812
+ subsys, // 'CPU','MEM','DISK','NET','THERMAL','PROCESS','SYSTEM','LOGGER','UI'
2813
+ msg,
2814
+ ctx: ctx || '',
2815
+ raw: { level, subsys, msg, ctx, ts: new Date().toISOString() }
2816
+ };
2817
+ logsBuffer.unshift(entry);
2818
+ if (logsBuffer.length > MAX_LOG_ENTRIES) logsBuffer.pop();
2819
+ if (currentTab === 'logs') renderLogs();
2820
+ }
2821
+
2822
+ function fmtSizeB(b) {
2823
+ if (!b) return '0 B';
2824
+ if (b > 1e9) return (b/1e9).toFixed(1) + ' GB';
2825
+ if (b > 1e6) return (b/1e6).toFixed(1) + ' MB';
2826
+ if (b > 1e3) return (b/1e3).toFixed(0) + ' KB';
2827
+ return b + ' B';
628
2828
  }
629
2829
 
630
- window.__pyreUpdate = function(jsStr) {
2830
+ function renderLogs() {
2831
+ const tbody = document.getElementById('logs-tbody');
2832
+ if (!tbody) return;
2833
+
2834
+ const visible = logsBuffer.filter(e => {
2835
+ if (logFilterLevel !== 'all' && e.level !== logFilterLevel) return false;
2836
+ if (logFilterSubsys !== 'all' && e.subsys !== logFilterSubsys) return false;
2837
+ if (logSearchQuery && !e.msg.toLowerCase().includes(logSearchQuery) && !e.ctx.toLowerCase().includes(logSearchQuery)) return false;
2838
+ return true;
2839
+ });
2840
+
2841
+ tbody.innerHTML = visible.map(e => {
2842
+ const badgeCls = e.level.toLowerCase();
2843
+ return \`<tr>
2844
+ <td class="log-ctx">\${escapeHtml(e.ts)}</td>
2845
+ <td><span class="log-badge \${badgeCls}">\${escapeHtml(e.level)}</span></td>
2846
+ <td class="log-subsys">\${escapeHtml(e.subsys)}</td>
2847
+ <td class="log-msg">\${escapeHtml(e.msg)}</td>
2848
+ <td style="text-align:right;" class="log-ctx">\${escapeHtml(e.ctx)}</td>
2849
+ </tr>\`;
2850
+ }).join('');
2851
+
2852
+ if (autoScrollLogs && visible.length) {
2853
+ const area = document.getElementById('logs-scroll-area');
2854
+ if (area) area.scrollTop = 0;
2855
+ }
2856
+ }
2857
+
2858
+ function onLogLevelFilterChange(val) {
2859
+ logFilterLevel = val;
2860
+ renderLogs();
2861
+ }
2862
+
2863
+ function onLogSubsysFilterChange(val) {
2864
+ logFilterSubsys = val;
2865
+ renderLogs();
2866
+ }
2867
+
2868
+ function onLogSearchInput(val) {
2869
+ logSearchQuery = (val || '').toLowerCase();
2870
+ renderLogs();
2871
+ }
2872
+
2873
+ function toggleAutoScroll() {
2874
+ autoScrollLogs = !autoScrollLogs;
2875
+ const btn = document.getElementById('btn-autoscroll');
2876
+ if (btn) {
2877
+ btn.textContent = 'Auto-scroll: ' + (autoScrollLogs ? 'ON' : 'OFF');
2878
+ btn.style.color = autoScrollLogs ? 'var(--mac-green)' : 'var(--text-secondary)';
2879
+ }
2880
+ }
2881
+
2882
+ function clearLogs() {
2883
+ logsBuffer = [];
2884
+ renderLogs();
2885
+ showToast('Event log cleared');
2886
+ }
2887
+
2888
+ function downloadLogsExport() {
2889
+ const json = JSON.stringify(logsBuffer.map(e => e.raw), null, 2);
2890
+ const blob = new Blob([json], { type: 'application/json' });
2891
+ const url = URL.createObjectURL(blob);
2892
+ const a = document.createElement('a');
2893
+ a.href = url;
2894
+ a.download = 'pyre-ui-events-' + new Date().toISOString().slice(0,19).replace(/:/g,'-') + '.json';
2895
+ a.click();
2896
+ URL.revokeObjectURL(url);
2897
+ }
2898
+
2899
+ // Analyze incoming telemetry and emit structured log events
2900
+ function analyzeAndLog(d) {
2901
+ if (!hasLoggedInit) {
2902
+ hasLoggedInit = true;
2903
+ addLogEntry('INFO', 'SYSTEM', 'Pyre-UI connected — live telemetry streaming started',
2904
+ (d.header?.hostname || 'unknown') + ' · ' + (d.header?.os || 'macOS'));
2905
+ }
2906
+
2907
+ const now = Date.now();
2908
+ const cpu = d.cpu?.usage || 0;
2909
+ const mem = d.memory?.usagePercent || 0;
2910
+ const thermalState = d.thermal?.state || 'Nominal';
2911
+
2912
+ // CPU spike detection (>70% WARN, >90% CRIT), per-process
2913
+ if (d.processes) {
2914
+ d.processes.forEach(p => {
2915
+ if (!p || !p.pid) return;
2916
+ const lastSpike = lastCpuSpikeTimes[p.pid] || 0;
2917
+ if (p.cpu > 90 && now - lastSpike > 30000) {
2918
+ lastCpuSpikeTimes[p.pid] = now;
2919
+ addLogEntry('CRIT', 'CPU',
2920
+ \`Critical CPU usage: \${p.command.replace(/^.*[\\\\/]/,'')} (PID \${p.pid}) at \${p.cpu.toFixed(1)}%\`,
2921
+ \`Threads: \${p.threads || 1} · User: \${p.user || 'root'}\`);
2922
+ } else if (p.cpu > 70 && now - lastSpike > 60000) {
2923
+ lastCpuSpikeTimes[p.pid] = now;
2924
+ addLogEntry('WARN', 'CPU',
2925
+ \`High CPU usage: \${p.command.replace(/^.*[\\\\/]/,'')} (PID \${p.pid}) at \${p.cpu.toFixed(1)}%\`,
2926
+ \`Threads: \${p.threads || 1}\`);
2927
+ }
2928
+ });
2929
+ }
2930
+
2931
+ // System-wide CPU
2932
+ if (cpu > 90) {
2933
+ addLogEntry('CRIT', 'CPU', \`System CPU at \${cpu.toFixed(1)}% — critical load\`, '');
2934
+ } else if (cpu > 75) {
2935
+ addLogEntry('WARN', 'CPU', \`System CPU elevated at \${cpu.toFixed(1)}%\`, '');
2936
+ }
2937
+
2938
+ // Memory pressure
2939
+ if (mem > 85 && now - lastHighMemAlert > 60000) {
2940
+ lastHighMemAlert = now;
2941
+ addLogEntry('CRIT', 'MEM', \`Memory pressure critical: \${mem.toFixed(0)}% used\`,
2942
+ 'Total: ' + fmtSizeB(d.memory?.total) + ' · Swap: ' + fmtSizeB(d.memory?.swapUsed));
2943
+ } else if (mem > 70 && now - lastHighMemAlert > 120000) {
2944
+ lastHighMemAlert = now;
2945
+ addLogEntry('WARN', 'MEM', \`Memory usage high: \${mem.toFixed(0)}%\`, 'Swap: ' + fmtSizeB(d.memory?.swapUsed));
2946
+ }
2947
+
2948
+ // Thermal state transitions
2949
+ if (prevThermalState !== null && prevThermalState !== thermalState) {
2950
+ const level = thermalState === 'Critical' ? 'CRIT' : thermalState === 'Nominal' ? 'INFO' : 'WARN';
2951
+ addLogEntry(level, 'THERMAL',
2952
+ \`Thermal state changed: \${prevThermalState} → \${thermalState}\`,
2953
+ \`CPU temp: \${d.cpu?.temperature ? d.cpu.temperature + '°C' : '–'}\`);
2954
+ }
2955
+ prevThermalState = thermalState;
2956
+
2957
+ // Network burst detection
2958
+ const rxRate = d.network?.rxRate || 0;
2959
+ const txRate = d.network?.txRate || 0;
2960
+ if ((rxRate > 50*1024*1024 || txRate > 50*1024*1024) && now - lastNetBurstAlert > 30000) {
2961
+ lastNetBurstAlert = now;
2962
+ addLogEntry('WARN', 'NET',
2963
+ \`Network burst: ↓\${fmtSizeB(rxRate)}/s ↑\${fmtSizeB(txRate)}/s\`,
2964
+ \`Interface: \${d.network?.interface || 'en0'}\`);
2965
+ }
2966
+ }
2967
+
2968
+ // ═══════════════════ RECORDING CONTROLS ═══════════════════
2969
+
2970
+ async function toggleWebRecording() {
2971
+ try {
2972
+ if (isRecording) {
2973
+ const res = await fetch('/api/logging/stop');
2974
+ const data = await res.json();
2975
+ isRecording = false;
2976
+ updateRecordingUI(false);
2977
+ addLogEntry('INFO', 'LOGGER',
2978
+ \`Session recording stopped: \${data.samples} samples in \${data.durationSec}s\`,
2979
+ data.file || '');
2980
+ showToast(\`Recording stopped — \${data.samples} samples saved\`);
2981
+ } else {
2982
+ const res = await fetch('/api/logging/start');
2983
+ const data = await res.json();
2984
+ isRecording = true;
2985
+ updateRecordingUI(true, data.file);
2986
+ addLogEntry('INFO', 'LOGGER', 'Session recording started', data.file || '');
2987
+ showToast('Recording started → ' + (data.file || './pyre-exports/'));
2988
+ }
2989
+ if (currentTab === 'logs') fetchLogHistory();
2990
+ } catch (e) {
2991
+ showToast('Recording error: ' + e.message);
2992
+ }
2993
+ }
2994
+
2995
+ function updateRecordingUI(recording, filename) {
2996
+ const btn = document.getElementById('btn-record');
2997
+ const label = document.getElementById('rec-label');
2998
+ const panelBtn = document.getElementById('btn-panel-rec');
2999
+ const badge = document.getElementById('logs-rec-badge');
3000
+
3001
+ if (btn) btn.classList.toggle('recording', recording);
3002
+ if (label) label.textContent = recording ? 'Stop REC' : 'Record';
3003
+ if (panelBtn) panelBtn.textContent = recording ? 'Stop Recording' : 'Start Recording';
3004
+ if (badge) {
3005
+ badge.textContent = recording ? 'REC' : 'IDLE';
3006
+ badge.className = 'log-badge ' + (recording ? 'crit' : 'info');
3007
+ }
3008
+ }
3009
+
3010
+ async function syncRecordingStatus() {
3011
+ try {
3012
+ const res = await fetch('/api/logging/status');
3013
+ const data = await res.json();
3014
+ isRecording = data.recording;
3015
+ updateRecordingUI(data.recording, data.file);
3016
+
3017
+ const statStatus = document.getElementById('rec-stat-status');
3018
+ const statFile = document.getElementById('rec-stat-file');
3019
+ const statSamples = document.getElementById('rec-stat-samples');
3020
+ const statDuration = document.getElementById('rec-stat-duration');
3021
+ const statDir = document.getElementById('rec-stat-dir');
3022
+ if (statStatus) statStatus.textContent = data.recording ? 'Recording…' : 'Idle';
3023
+ if (statFile) statFile.textContent = data.file || '–';
3024
+ if (statSamples) statSamples.textContent = data.samples;
3025
+ if (statDuration) statDuration.textContent = data.durationSec + 's';
3026
+ if (statDir) statDir.textContent = data.exportDir;
3027
+ } catch { /* ignore */ }
3028
+ }
3029
+
3030
+ async function fetchLogHistory() {
3031
+ try {
3032
+ const res = await fetch('/api/logging/history');
3033
+ const data = await res.json();
3034
+ const tbody = document.getElementById('history-tbody');
3035
+ if (!tbody) return;
3036
+ if (!data.files || !data.files.length) {
3037
+ 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>';
3038
+ return;
3039
+ }
3040
+ tbody.innerHTML = data.files.map(f => {
3041
+ const kb = (f.sizeBytes / 1024).toFixed(1);
3042
+ const mtime = f.mtime ? new Date(f.mtime).toLocaleString() : '–';
3043
+ return \`<tr>
3044
+ <td style="font-family:var(--font-mono);font-size:10px;">\${escapeHtml(f.name)}</td>
3045
+ <td>\${kb} KB</td>
3046
+ <td>\${mtime}</td>
3047
+ <td><a class="history-link" href="/api/logging/download?file=\${encodeURIComponent(f.name)}" download="\${escapeHtml(f.name)}">↓ Download</a></td>
3048
+ </tr>\`;
3049
+ }).join('');
3050
+ } catch { /* ignore */ }
3051
+ }
3052
+
3053
+ // ═══════════════════ EXPORT DROPDOWN ═══════════════════
3054
+
3055
+ function toggleExportMenu(e) {
3056
+ e.stopPropagation();
3057
+ const menu = document.getElementById('export-menu');
3058
+ if (menu) menu.classList.toggle('show');
3059
+ }
3060
+
3061
+ document.addEventListener('click', () => {
3062
+ const menu = document.getElementById('export-menu');
3063
+ if (menu) menu.classList.remove('show');
3064
+ });
3065
+
3066
+ function downloadSnapshot(fmt) {
3067
+ const menu = document.getElementById('export-menu');
3068
+ if (menu) menu.classList.remove('show');
3069
+ const url = '/api/export?format=' + fmt;
3070
+ const a = document.createElement('a');
3071
+ a.href = url;
3072
+ a.download = 'pyre-snapshot.' + fmt;
3073
+ a.click();
3074
+ showToast('Exporting snapshot as ' + fmt.toUpperCase() + '…');
3075
+ addLogEntry('ACTION', 'UI', 'User exported snapshot', 'format: ' + fmt);
3076
+ }
3077
+
3078
+ // SSE Connection
3079
+ let eventSource = null;
3080
+ function initSSE() {
631
3081
  try {
632
- const d = JSON.parse(jsStr);
633
- document.getElementById('conn-dot').className='conn-dot';
634
- document.getElementById('conn-text').textContent='Live';
635
- ingestData(d);
636
- } catch(e) { document.getElementById('conn-text').textContent='Err: '+e.message; }
3082
+ eventSource = new EventSource('/api/stream');
3083
+ eventSource.onmessage = (event) => {
3084
+ try {
3085
+ const data = JSON.parse(event.data);
3086
+ ingestStats(data);
3087
+ } catch (err) {
3088
+ console.error('SSE parse error:', err);
3089
+ }
3090
+ };
3091
+ eventSource.onerror = () => {
3092
+ document.getElementById('sb-dot').className = 'sb-live-dot offline';
3093
+ document.getElementById('sb-status').textContent = 'Reconnecting';
3094
+ };
3095
+ } catch (err) {
3096
+ console.error('SSE error:', err);
3097
+ }
3098
+ }
3099
+
3100
+ // Fallback Polling (also handles window.__pyreUpdate from Cocoa WebView)
3101
+ window.__pyreUpdate = function(jsonStr) {
3102
+ try {
3103
+ const d = JSON.parse(jsonStr);
3104
+ ingestStats(d);
3105
+ } catch (e) {
3106
+ console.error('pyreUpdate parse error:', e);
3107
+ }
637
3108
  };
638
3109
 
639
- let pTimer=null;
640
- async function pollOnce() {
3110
+ async function pollFallback() {
641
3111
  try {
642
- const r=await fetch('/api/stats',{cache:'no-store'});
643
- if(!r.ok){ document.getElementById('conn-dot').className='conn-dot offline'; return; }
644
- document.getElementById('conn-dot').className='conn-dot';
645
- ingestData(await r.json());
646
- } catch(e) { document.getElementById('conn-dot').className='conn-dot offline'; }
3112
+ const res = await fetch('/api/stats', { cache: 'no-store' });
3113
+ if (res.ok) {
3114
+ const data = await res.json();
3115
+ ingestStats(data);
3116
+ }
3117
+ } catch (err) {
3118
+ document.getElementById('sb-dot').className = 'sb-live-dot offline';
3119
+ document.getElementById('sb-status').textContent = 'Offline';
3120
+ }
647
3121
  }
648
- setTimeout(()=>{ if(!latestData && !pTimer) { pollOnce(); pTimer=setInterval(pollOnce, 2000); } }, 4000);
649
3122
 
650
- const keyMap = {'1':'overview','2':'cpu','3':'memory','4':'gpu','5':'power','6':'battery','7':'thermal','8':'network','9':'disk','p':'processes','a':'anomalies'};
651
- document.addEventListener('keydown', e => { if(keyMap[e.key.toLowerCase()]) switchView(keyMap[e.key.toLowerCase()]); });
3123
+ // Initialize
3124
+ renderTableHeader();
3125
+ initSSE();
3126
+ pollFallback();
3127
+ syncRecordingStatus();
3128
+ loadConfig();
3129
+ setInterval(() => {
3130
+ if (!latestData || document.getElementById('sb-status').textContent === 'Offline') {
3131
+ pollFallback();
3132
+ }
3133
+ }, 3000);
3134
+ // Periodically sync recording status & update panel stats
3135
+ setInterval(() => {
3136
+ if (currentTab === 'logs' && isRecording) {
3137
+ syncRecordingStatus();
3138
+ }
3139
+ }, 5000);
652
3140
  </script>
653
3141
  </body>
654
3142
  </html>`;