process-watchdog 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/config/default.json +16 -0
  2. package/dashboard/watchdog.html +856 -0
  3. package/dist/api/routes.d.ts +6 -0
  4. package/dist/api/routes.d.ts.map +1 -0
  5. package/dist/api/routes.js +105 -0
  6. package/dist/api/routes.js.map +1 -0
  7. package/dist/api/server.d.ts +10 -0
  8. package/dist/api/server.d.ts.map +1 -0
  9. package/dist/api/server.js +16 -0
  10. package/dist/api/server.js.map +1 -0
  11. package/dist/config.d.ts +27 -0
  12. package/dist/config.d.ts.map +1 -0
  13. package/dist/config.js +45 -0
  14. package/dist/config.js.map +1 -0
  15. package/dist/daemon/processwatchdog.err.log +973 -0
  16. package/dist/daemon/processwatchdog.exe +0 -0
  17. package/dist/daemon/processwatchdog.exe.config +6 -0
  18. package/dist/daemon/processwatchdog.out.log +2 -0
  19. package/dist/daemon/processwatchdog.wrapper.log +18 -0
  20. package/dist/daemon/processwatchdog.xml +30 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +203 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/installer/service.d.ts +3 -0
  26. package/dist/installer/service.d.ts.map +1 -0
  27. package/dist/installer/service.js +41 -0
  28. package/dist/installer/service.js.map +1 -0
  29. package/dist/integrations/mah.d.ts +3 -0
  30. package/dist/integrations/mah.d.ts.map +1 -0
  31. package/dist/integrations/mah.js +22 -0
  32. package/dist/integrations/mah.js.map +1 -0
  33. package/dist/integrations/total-recall.d.ts +3 -0
  34. package/dist/integrations/total-recall.d.ts.map +1 -0
  35. package/dist/integrations/total-recall.js +22 -0
  36. package/dist/integrations/total-recall.js.map +1 -0
  37. package/dist/platform/index.d.ts +4 -0
  38. package/dist/platform/index.d.ts.map +1 -0
  39. package/dist/platform/index.js +10 -0
  40. package/dist/platform/index.js.map +1 -0
  41. package/dist/platform/platform.interface.d.ts +42 -0
  42. package/dist/platform/platform.interface.d.ts.map +1 -0
  43. package/dist/platform/platform.interface.js +2 -0
  44. package/dist/platform/platform.interface.js.map +1 -0
  45. package/dist/platform/windows.d.ts +14 -0
  46. package/dist/platform/windows.d.ts.map +1 -0
  47. package/dist/platform/windows.js +162 -0
  48. package/dist/platform/windows.js.map +1 -0
  49. package/dist/plugins/cpu-monitor.d.ts +11 -0
  50. package/dist/plugins/cpu-monitor.d.ts.map +1 -0
  51. package/dist/plugins/cpu-monitor.js +57 -0
  52. package/dist/plugins/cpu-monitor.js.map +1 -0
  53. package/dist/plugins/disk-health.d.ts +11 -0
  54. package/dist/plugins/disk-health.d.ts.map +1 -0
  55. package/dist/plugins/disk-health.js +111 -0
  56. package/dist/plugins/disk-health.js.map +1 -0
  57. package/dist/plugins/memory-monitor.d.ts +11 -0
  58. package/dist/plugins/memory-monitor.d.ts.map +1 -0
  59. package/dist/plugins/memory-monitor.js +61 -0
  60. package/dist/plugins/memory-monitor.js.map +1 -0
  61. package/dist/plugins/plugin-loader.d.ts +4 -0
  62. package/dist/plugins/plugin-loader.d.ts.map +1 -0
  63. package/dist/plugins/plugin-loader.js +25 -0
  64. package/dist/plugins/plugin-loader.js.map +1 -0
  65. package/dist/plugins/plugin.interface.d.ts +28 -0
  66. package/dist/plugins/plugin.interface.d.ts.map +1 -0
  67. package/dist/plugins/plugin.interface.js +2 -0
  68. package/dist/plugins/plugin.interface.js.map +1 -0
  69. package/dist/plugins/process-guard.d.ts +11 -0
  70. package/dist/plugins/process-guard.d.ts.map +1 -0
  71. package/dist/plugins/process-guard.js +139 -0
  72. package/dist/plugins/process-guard.js.map +1 -0
  73. package/dist/plugins/startup-optimizer.d.ts +11 -0
  74. package/dist/plugins/startup-optimizer.d.ts.map +1 -0
  75. package/dist/plugins/startup-optimizer.js +78 -0
  76. package/dist/plugins/startup-optimizer.js.map +1 -0
  77. package/dist/scheduler.d.ts +16 -0
  78. package/dist/scheduler.d.ts.map +1 -0
  79. package/dist/scheduler.js +46 -0
  80. package/dist/scheduler.js.map +1 -0
  81. package/dist/store/history.d.ts +20 -0
  82. package/dist/store/history.d.ts.map +1 -0
  83. package/dist/store/history.js +60 -0
  84. package/dist/store/history.js.map +1 -0
  85. package/package.json +35 -0
  86. package/src/api/routes.ts +123 -0
  87. package/src/api/server.ts +20 -0
  88. package/src/config.ts +78 -0
  89. package/src/index.ts +228 -0
  90. package/src/installer/service.ts +50 -0
  91. package/src/integrations/mah.ts +22 -0
  92. package/src/integrations/total-recall.ts +27 -0
  93. package/src/platform/index.ts +13 -0
  94. package/src/platform/platform.interface.ts +46 -0
  95. package/src/platform/windows.ts +242 -0
  96. package/src/plugins/cpu-monitor.ts +67 -0
  97. package/src/plugins/disk-health.ts +128 -0
  98. package/src/plugins/memory-monitor.ts +70 -0
  99. package/src/plugins/plugin-loader.ts +27 -0
  100. package/src/plugins/plugin.interface.ts +31 -0
  101. package/src/plugins/process-guard.ts +165 -0
  102. package/src/plugins/startup-optimizer.ts +103 -0
  103. package/src/scheduler.ts +53 -0
  104. package/src/store/history.ts +90 -0
  105. package/tests/api/routes.test.ts +113 -0
  106. package/tests/config.test.ts +24 -0
  107. package/tests/platform/windows.test.ts +59 -0
  108. package/tests/plugins/cpu-monitor.test.ts +69 -0
  109. package/tests/plugins/disk-health.test.ts +69 -0
  110. package/tests/plugins/memory-monitor.test.ts +57 -0
  111. package/tests/plugins/plugin-loader.test.ts +35 -0
  112. package/tests/plugins/process-guard.test.ts +40 -0
  113. package/tests/plugins/startup-optimizer.test.ts +50 -0
  114. package/tests/scheduler.test.ts +69 -0
  115. package/tests/store/history.test.ts +89 -0
  116. package/tsconfig.json +18 -0
  117. package/vitest.config.ts +10 -0
@@ -0,0 +1,856 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Process Watchdog — aidev.com.au</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ :root {
11
+ --bg: #0a0a0a;
12
+ --card-bg: #1a1a2e;
13
+ --card-border: #2a2a4a;
14
+ --accent: #4fc3f7;
15
+ --healthy: #66bb6a;
16
+ --warning: #ffa726;
17
+ --critical: #ef5350;
18
+ --text: #e0e0e0;
19
+ --text-muted: #8888aa;
20
+ --radius: 12px;
21
+ }
22
+
23
+ body {
24
+ background: var(--bg);
25
+ color: var(--text);
26
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
27
+ font-size: 14px;
28
+ line-height: 1.5;
29
+ min-height: 100vh;
30
+ }
31
+
32
+ /* ── Header ── */
33
+ header {
34
+ background: var(--card-bg);
35
+ border-bottom: 1px solid var(--card-border);
36
+ padding: 12px 24px;
37
+ display: flex;
38
+ align-items: center;
39
+ justify-content: space-between;
40
+ gap: 16px;
41
+ flex-wrap: wrap;
42
+ }
43
+
44
+ .brand {
45
+ display: flex;
46
+ align-items: center;
47
+ gap: 10px;
48
+ font-size: 16px;
49
+ font-weight: 600;
50
+ color: var(--accent);
51
+ letter-spacing: 0.02em;
52
+ }
53
+
54
+ .brand-dot {
55
+ width: 10px;
56
+ height: 10px;
57
+ border-radius: 50%;
58
+ background: var(--healthy);
59
+ box-shadow: 0 0 6px var(--healthy);
60
+ flex-shrink: 0;
61
+ transition: background 0.3s, box-shadow 0.3s;
62
+ }
63
+
64
+ /* ── Status bar ── */
65
+ #status-bar {
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 20px;
69
+ flex-wrap: wrap;
70
+ }
71
+
72
+ .status-pill {
73
+ display: inline-flex;
74
+ align-items: center;
75
+ gap: 6px;
76
+ padding: 4px 12px;
77
+ border-radius: 20px;
78
+ font-size: 12px;
79
+ font-weight: 600;
80
+ border: 1px solid transparent;
81
+ transition: background 0.3s, border-color 0.3s, color 0.3s;
82
+ }
83
+
84
+ .status-pill.healthy { background: rgba(102,187,106,0.15); border-color: var(--healthy); color: var(--healthy); }
85
+ .status-pill.warning { background: rgba(255,167, 38,0.15); border-color: var(--warning); color: var(--warning); }
86
+ .status-pill.critical { background: rgba(239, 83, 80,0.15); border-color: var(--critical); color: var(--critical); }
87
+ .status-pill.offline { background: rgba(136,136,170,0.15); border-color: var(--text-muted); color: var(--text-muted); }
88
+
89
+ .status-dot {
90
+ width: 7px;
91
+ height: 7px;
92
+ border-radius: 50%;
93
+ background: currentColor;
94
+ }
95
+
96
+ .status-meta {
97
+ font-size: 12px;
98
+ color: var(--text-muted);
99
+ }
100
+
101
+ .status-meta span { color: var(--text); }
102
+
103
+ /* ── Offline banner ── */
104
+ #offline-banner {
105
+ display: none;
106
+ background: rgba(239,83,80,0.12);
107
+ border: 1px solid var(--critical);
108
+ color: var(--critical);
109
+ text-align: center;
110
+ padding: 10px;
111
+ font-size: 13px;
112
+ }
113
+
114
+ /* ── Main layout ── */
115
+ main {
116
+ padding: 24px;
117
+ display: flex;
118
+ flex-direction: column;
119
+ gap: 28px;
120
+ }
121
+
122
+ section > h2 {
123
+ font-size: 11px;
124
+ font-weight: 700;
125
+ letter-spacing: 0.1em;
126
+ text-transform: uppercase;
127
+ color: var(--text-muted);
128
+ margin-bottom: 12px;
129
+ }
130
+
131
+ /* ── Cards base ── */
132
+ .card {
133
+ background: var(--card-bg);
134
+ border: 1px solid var(--card-border);
135
+ border-radius: var(--radius);
136
+ padding: 18px 20px;
137
+ }
138
+
139
+ /* ── Metric gauges ── */
140
+ .gauge-grid {
141
+ display: grid;
142
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
143
+ gap: 16px;
144
+ }
145
+
146
+ .gauge-card {
147
+ display: flex;
148
+ flex-direction: column;
149
+ gap: 10px;
150
+ }
151
+
152
+ .gauge-header {
153
+ display: flex;
154
+ justify-content: space-between;
155
+ align-items: baseline;
156
+ }
157
+
158
+ .gauge-label {
159
+ font-size: 12px;
160
+ color: var(--text-muted);
161
+ font-weight: 600;
162
+ text-transform: uppercase;
163
+ letter-spacing: 0.06em;
164
+ }
165
+
166
+ .gauge-value {
167
+ font-size: 26px;
168
+ font-weight: 700;
169
+ color: var(--accent);
170
+ line-height: 1;
171
+ transition: color 0.3s;
172
+ }
173
+
174
+ .gauge-value.healthy { color: var(--healthy); }
175
+ .gauge-value.warning { color: var(--warning); }
176
+ .gauge-value.critical { color: var(--critical); }
177
+
178
+ .gauge-unit {
179
+ font-size: 13px;
180
+ color: var(--text-muted);
181
+ margin-left: 2px;
182
+ }
183
+
184
+ .gauge-bar-track {
185
+ height: 6px;
186
+ background: var(--card-border);
187
+ border-radius: 3px;
188
+ overflow: hidden;
189
+ }
190
+
191
+ .gauge-bar-fill {
192
+ height: 100%;
193
+ border-radius: 3px;
194
+ transition: width 0.5s ease, background 0.5s ease;
195
+ background: var(--accent);
196
+ }
197
+
198
+ .gauge-bar-fill.healthy { background: var(--healthy); }
199
+ .gauge-bar-fill.warning { background: var(--warning); }
200
+ .gauge-bar-fill.critical { background: var(--critical); }
201
+
202
+ /* ── Plugin cards ── */
203
+ .plugin-grid {
204
+ display: grid;
205
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
206
+ gap: 16px;
207
+ }
208
+
209
+ .plugin-card {
210
+ display: flex;
211
+ flex-direction: column;
212
+ gap: 12px;
213
+ }
214
+
215
+ .plugin-top {
216
+ display: flex;
217
+ align-items: center;
218
+ gap: 10px;
219
+ }
220
+
221
+ .plugin-dot {
222
+ width: 10px;
223
+ height: 10px;
224
+ border-radius: 50%;
225
+ flex-shrink: 0;
226
+ transition: background 0.3s, box-shadow 0.3s;
227
+ }
228
+
229
+ .plugin-dot.healthy { background: var(--healthy); box-shadow: 0 0 6px var(--healthy); }
230
+ .plugin-dot.warning { background: var(--warning); box-shadow: 0 0 6px var(--warning); }
231
+ .plugin-dot.critical { background: var(--critical); box-shadow: 0 0 6px var(--critical); }
232
+ .plugin-dot.unknown { background: var(--text-muted); }
233
+
234
+ .plugin-name {
235
+ font-weight: 600;
236
+ font-size: 14px;
237
+ flex: 1;
238
+ }
239
+
240
+ .plugin-status-text {
241
+ font-size: 11px;
242
+ font-weight: 600;
243
+ text-transform: uppercase;
244
+ letter-spacing: 0.06em;
245
+ }
246
+
247
+ .plugin-status-text.healthy { color: var(--healthy); }
248
+ .plugin-status-text.warning { color: var(--warning); }
249
+ .plugin-status-text.critical { color: var(--critical); }
250
+ .plugin-status-text.unknown { color: var(--text-muted); }
251
+
252
+ .plugin-meta {
253
+ font-size: 12px;
254
+ color: var(--text-muted);
255
+ }
256
+
257
+ .plugin-meta span { color: var(--text); }
258
+
259
+ .plugin-message {
260
+ font-size: 12px;
261
+ color: var(--text-muted);
262
+ font-style: italic;
263
+ min-height: 16px;
264
+ }
265
+
266
+ .plugin-actions {
267
+ display: flex;
268
+ gap: 8px;
269
+ }
270
+
271
+ /* ── Buttons ── */
272
+ .btn {
273
+ display: inline-flex;
274
+ align-items: center;
275
+ gap: 5px;
276
+ padding: 6px 14px;
277
+ border-radius: 6px;
278
+ font-size: 12px;
279
+ font-weight: 600;
280
+ cursor: pointer;
281
+ border: 1px solid transparent;
282
+ transition: opacity 0.15s, transform 0.1s;
283
+ letter-spacing: 0.02em;
284
+ }
285
+
286
+ .btn:hover { opacity: 0.85; }
287
+ .btn:active { transform: scale(0.97); }
288
+ .btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; }
289
+
290
+ .btn-check {
291
+ background: rgba(79,195,247,0.12);
292
+ border-color: var(--accent);
293
+ color: var(--accent);
294
+ }
295
+
296
+ .btn-fix {
297
+ background: rgba(102,187,106,0.12);
298
+ border-color: var(--healthy);
299
+ color: var(--healthy);
300
+ }
301
+
302
+ /* ── History ── */
303
+ .history-list {
304
+ display: flex;
305
+ flex-direction: column;
306
+ }
307
+
308
+ .history-item {
309
+ display: grid;
310
+ grid-template-columns: 100px 90px 80px 100px 1fr;
311
+ align-items: center;
312
+ gap: 12px;
313
+ padding: 10px 16px;
314
+ border-bottom: 1px solid var(--card-border);
315
+ font-size: 13px;
316
+ transition: background 0.15s;
317
+ }
318
+
319
+ .history-item:last-child { border-bottom: none; }
320
+ .history-item:hover { background: rgba(79,195,247,0.04); }
321
+
322
+ .history-time {
323
+ color: var(--text-muted);
324
+ font-size: 11px;
325
+ font-variant-numeric: tabular-nums;
326
+ }
327
+
328
+ .history-plugin {
329
+ font-weight: 600;
330
+ color: var(--accent);
331
+ white-space: nowrap;
332
+ overflow: hidden;
333
+ text-overflow: ellipsis;
334
+ }
335
+
336
+ .history-type {
337
+ font-size: 11px;
338
+ font-weight: 700;
339
+ text-transform: uppercase;
340
+ letter-spacing: 0.06em;
341
+ color: var(--text-muted);
342
+ }
343
+
344
+ .history-type.fix { color: var(--healthy); }
345
+
346
+ .history-badge {
347
+ display: inline-block;
348
+ padding: 2px 8px;
349
+ border-radius: 10px;
350
+ font-size: 10px;
351
+ font-weight: 700;
352
+ letter-spacing: 0.05em;
353
+ text-transform: uppercase;
354
+ }
355
+
356
+ .history-badge.healthy { background: rgba(102,187,106,0.15); color: var(--healthy); }
357
+ .history-badge.warning { background: rgba(255,167, 38,0.15); color: var(--warning); }
358
+ .history-badge.critical { background: rgba(239, 83, 80,0.15); color: var(--critical); }
359
+ .history-badge.fixed { background: rgba(79,195,247,0.15); color: var(--accent); }
360
+
361
+ .history-message {
362
+ color: var(--text-muted);
363
+ white-space: nowrap;
364
+ overflow: hidden;
365
+ text-overflow: ellipsis;
366
+ }
367
+
368
+ .history-empty {
369
+ padding: 24px;
370
+ text-align: center;
371
+ color: var(--text-muted);
372
+ font-size: 13px;
373
+ }
374
+
375
+ /* ── Footer ── */
376
+ footer {
377
+ text-align: center;
378
+ padding: 12px 24px 24px;
379
+ font-size: 11px;
380
+ color: var(--text-muted);
381
+ }
382
+
383
+ footer a { color: var(--accent); text-decoration: none; }
384
+
385
+ /* ── Responsive ── */
386
+ @media (max-width: 640px) {
387
+ header { padding: 12px 16px; }
388
+ main { padding: 16px; gap: 20px; }
389
+
390
+ .history-item {
391
+ grid-template-columns: 80px 80px 1fr;
392
+ }
393
+
394
+ .history-type,
395
+ .history-badge { display: none; }
396
+ }
397
+ </style>
398
+ </head>
399
+ <body>
400
+
401
+ <header>
402
+ <div class="brand">
403
+ <div class="brand-dot" id="brand-dot"></div>
404
+ Process Watchdog — aidev.com.au
405
+ </div>
406
+
407
+ <div id="status-bar">
408
+ <div class="status-pill offline" id="status-pill">
409
+ <div class="status-dot"></div>
410
+ <span id="status-text">Connecting…</span>
411
+ </div>
412
+ <div class="status-meta">Uptime: <span id="uptime-val">—</span></div>
413
+ <div class="status-meta">Version: <span id="version-val">—</span></div>
414
+ <div class="status-meta">Last update: <span id="last-update-val">—</span></div>
415
+ </div>
416
+ </header>
417
+
418
+ <div id="offline-banner">
419
+ API unreachable — retrying every 15 seconds…
420
+ </div>
421
+
422
+ <main>
423
+
424
+ <section>
425
+ <h2>System Metrics</h2>
426
+ <div class="gauge-grid" id="gauge-grid"></div>
427
+ </section>
428
+
429
+ <section>
430
+ <h2>Plugins</h2>
431
+ <div class="plugin-grid" id="plugin-grid"></div>
432
+ </section>
433
+
434
+ <section>
435
+ <h2>Action History</h2>
436
+ <div class="card" style="padding:0;overflow:hidden;">
437
+ <div class="history-list" id="history-list">
438
+ <div class="history-empty">Loading history…</div>
439
+ </div>
440
+ </div>
441
+ </section>
442
+
443
+ </main>
444
+
445
+ <footer>
446
+ <a href="https://aidev.com.au" target="_blank" rel="noopener">aidev.com.au</a>
447
+ — Process Watchdog Dashboard — polling every 15s
448
+ </footer>
449
+
450
+ <script>
451
+ (function () {
452
+ 'use strict';
453
+
454
+ const API_BASE = 'http://localhost:3400/api/v1';
455
+
456
+ // ── Helpers ──────────────────────────────────────────────────────────────
457
+
458
+ function statusClass(status) {
459
+ if (!status) return 'unknown';
460
+ const s = String(status).toLowerCase();
461
+ if (s === 'healthy') return 'healthy';
462
+ if (s === 'warning') return 'warning';
463
+ if (s === 'critical') return 'critical';
464
+ if (s === 'fixed') return 'fixed';
465
+ return 'unknown';
466
+ }
467
+
468
+ function formatUptime(seconds) {
469
+ const n = Number(seconds);
470
+ if (!isFinite(n)) return '—';
471
+ const d = Math.floor(n / 86400);
472
+ const h = Math.floor((n % 86400) / 3600);
473
+ const m = Math.floor((n % 3600) / 60);
474
+ const s = Math.floor(n % 60);
475
+ if (d > 0) return d + 'd ' + h + 'h ' + m + 'm';
476
+ if (h > 0) return h + 'h ' + m + 'm ' + s + 's';
477
+ if (m > 0) return m + 'm ' + s + 's';
478
+ return s + 's';
479
+ }
480
+
481
+ function formatRelative(isoString) {
482
+ if (!isoString) return '—';
483
+ const date = new Date(isoString);
484
+ if (isNaN(date.getTime())) return String(isoString);
485
+ const diff = Math.round((Date.now() - date.getTime()) / 1000);
486
+ if (diff < 5) return 'just now';
487
+ if (diff < 60) return diff + 's ago';
488
+ if (diff < 3600) return Math.round(diff / 60) + 'm ago';
489
+ if (diff < 86400) return Math.round(diff / 3600) + 'h ago';
490
+ return Math.round(diff / 86400) + 'd ago';
491
+ }
492
+
493
+ function formatTime(isoString) {
494
+ if (!isoString) return '—';
495
+ const date = new Date(isoString);
496
+ if (isNaN(date.getTime())) return String(isoString);
497
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
498
+ }
499
+
500
+ function gaugeColorClass(label, value) {
501
+ const pct = parseFloat(value);
502
+ if (isNaN(pct)) return '';
503
+ const lbl = String(label).toLowerCase();
504
+ if (lbl.includes('process')) return pct > 500 ? 'warning' : 'healthy';
505
+ if (pct >= 90) return 'critical';
506
+ if (pct >= 75) return 'warning';
507
+ return 'healthy';
508
+ }
509
+
510
+ function setText(el, text) {
511
+ el.textContent = String(text ?? '');
512
+ }
513
+
514
+ function el(tag, className) {
515
+ const node = document.createElement(tag);
516
+ if (className) node.className = className;
517
+ return node;
518
+ }
519
+
520
+ // ── Gauge grid ───────────────────────────────────────────────────────────
521
+
522
+ const GAUGES = [
523
+ { id: 'gauge-ram', label: 'RAM', unit: '%', metricKey: 'ramPercent', maxVal: 100 },
524
+ { id: 'gauge-cpu', label: 'CPU', unit: '%', metricKey: 'cpuPercent', maxVal: 100 },
525
+ { id: 'gauge-disk', label: 'Disk', unit: '%', metricKey: 'diskPercent', maxVal: 100 },
526
+ { id: 'gauge-procs', label: 'Total Processes', unit: '', metricKey: 'totalProcesses', maxVal: null },
527
+ ];
528
+
529
+ function buildGaugeGrid() {
530
+ const grid = document.getElementById('gauge-grid');
531
+ for (const g of GAUGES) {
532
+ const card = el('div', 'card gauge-card');
533
+ card.id = g.id;
534
+
535
+ const header = el('div', 'gauge-header');
536
+ const lbl = el('span', 'gauge-label');
537
+ const valSpan = el('span', 'gauge-value');
538
+ valSpan.id = g.id + '-val';
539
+ setText(lbl, g.label);
540
+ setText(valSpan, '—');
541
+ header.appendChild(lbl);
542
+ header.appendChild(valSpan);
543
+ card.appendChild(header);
544
+
545
+ if (g.maxVal !== null) {
546
+ const track = el('div', 'gauge-bar-track');
547
+ const fill = el('div', 'gauge-bar-fill');
548
+ fill.id = g.id + '-bar';
549
+ fill.style.width = '0%';
550
+ track.appendChild(fill);
551
+ card.appendChild(track);
552
+ }
553
+
554
+ grid.appendChild(card);
555
+ }
556
+ }
557
+
558
+ function updateGauges(data) {
559
+ const combined = {};
560
+ if (data.metrics && typeof data.metrics === 'object') {
561
+ Object.assign(combined, data.metrics);
562
+ }
563
+ if (Array.isArray(data.plugins)) {
564
+ for (const p of data.plugins) {
565
+ if (p.metrics && typeof p.metrics === 'object') {
566
+ Object.assign(combined, p.metrics);
567
+ }
568
+ }
569
+ }
570
+
571
+ for (const g of GAUGES) {
572
+ const raw = combined[g.metricKey];
573
+ const valEl = document.getElementById(g.id + '-val');
574
+ const barEl = document.getElementById(g.id + '-bar');
575
+ if (!valEl) continue;
576
+
577
+ if (raw == null) {
578
+ valEl.className = 'gauge-value';
579
+ setText(valEl, '—');
580
+ continue;
581
+ }
582
+
583
+ const display = typeof raw === 'number'
584
+ ? (g.unit === '%' ? raw.toFixed(1) : String(raw))
585
+ : String(raw);
586
+
587
+ const cls = gaugeColorClass(g.label, display);
588
+ valEl.className = 'gauge-value' + (cls ? ' ' + cls : '');
589
+
590
+ // Build value + unit via DOM to avoid setting HTML
591
+ while (valEl.firstChild) valEl.removeChild(valEl.firstChild);
592
+ valEl.appendChild(document.createTextNode(display));
593
+ if (g.unit) {
594
+ const unitSpan = el('span', 'gauge-unit');
595
+ setText(unitSpan, g.unit);
596
+ valEl.appendChild(unitSpan);
597
+ }
598
+
599
+ if (barEl && g.maxVal !== null) {
600
+ const pct = Math.min(100, Math.max(0, (parseFloat(display) / g.maxVal) * 100));
601
+ barEl.style.width = pct + '%';
602
+ barEl.className = 'gauge-bar-fill' + (cls ? ' ' + cls : '');
603
+ }
604
+ }
605
+ }
606
+
607
+ // ── Plugin cards ─────────────────────────────────────────────────────────
608
+
609
+ let pluginNames = [];
610
+
611
+ function buildPluginCards(plugins) {
612
+ const grid = document.getElementById('plugin-grid');
613
+ const incoming = plugins.map(function (p) { return p.name; });
614
+
615
+ if (JSON.stringify(incoming) !== JSON.stringify(pluginNames)) {
616
+ pluginNames = incoming;
617
+ while (grid.firstChild) grid.removeChild(grid.firstChild);
618
+ for (const p of plugins) {
619
+ grid.appendChild(makePluginCard(p));
620
+ }
621
+ } else {
622
+ for (const p of plugins) {
623
+ updatePluginCard(p);
624
+ }
625
+ }
626
+ }
627
+
628
+ function makePluginCard(p) {
629
+ const card = el('div', 'card plugin-card');
630
+ card.id = 'plugin-' + p.name;
631
+ fillPluginCard(card, p);
632
+ return card;
633
+ }
634
+
635
+ function fillPluginCard(card, p) {
636
+ while (card.firstChild) card.removeChild(card.firstChild);
637
+
638
+ const sc = statusClass(p.status);
639
+ const lrun = p.lastRun || p.lastCheck || null;
640
+
641
+ // Top row: dot + name + status text
642
+ const top = el('div', 'plugin-top');
643
+ const dot = el('div', 'plugin-dot ' + sc);
644
+ const nameSpan = el('span', 'plugin-name');
645
+ const statusSpan = el('span', 'plugin-status-text ' + sc);
646
+ setText(nameSpan, p.name);
647
+ setText(statusSpan, p.status || 'unknown');
648
+ top.appendChild(dot);
649
+ top.appendChild(nameSpan);
650
+ top.appendChild(statusSpan);
651
+ card.appendChild(top);
652
+
653
+ // Meta row
654
+ const meta = el('div', 'plugin-meta');
655
+ const metaVal = el('span');
656
+ meta.appendChild(document.createTextNode('Last run: '));
657
+ setText(metaVal, lrun ? formatRelative(lrun) : '—');
658
+ meta.appendChild(metaVal);
659
+ card.appendChild(meta);
660
+
661
+ // Message
662
+ const msg = el('div', 'plugin-message');
663
+ setText(msg, p.message || '');
664
+ card.appendChild(msg);
665
+
666
+ // Buttons
667
+ const actions = el('div', 'plugin-actions');
668
+
669
+ const checkBtn = el('button', 'btn btn-check');
670
+ setText(checkBtn, 'Check');
671
+ checkBtn.type = 'button';
672
+ checkBtn.title = 'Run health check for ' + p.name;
673
+ checkBtn.addEventListener('click', function () { runPlugin(p.name, 'check'); });
674
+
675
+ const fixBtn = el('button', 'btn btn-fix');
676
+ setText(fixBtn, 'Fix');
677
+ fixBtn.type = 'button';
678
+ fixBtn.title = 'Run auto-fix for ' + p.name;
679
+ fixBtn.addEventListener('click', function () { runPlugin(p.name, 'fix'); });
680
+
681
+ actions.appendChild(checkBtn);
682
+ actions.appendChild(fixBtn);
683
+ card.appendChild(actions);
684
+ }
685
+
686
+ function updatePluginCard(p) {
687
+ const card = document.getElementById('plugin-' + p.name);
688
+ if (card) fillPluginCard(card, p);
689
+ }
690
+
691
+ // ── Status bar ───────────────────────────────────────────────────────────
692
+
693
+ function updateStatusBar(data) {
694
+ const sc = statusClass(data.status);
695
+
696
+ const pill = document.getElementById('status-pill');
697
+ pill.className = 'status-pill ' + sc;
698
+ setText(document.getElementById('status-text'),
699
+ data.status ? data.status.charAt(0).toUpperCase() + data.status.slice(1) : 'Unknown');
700
+
701
+ const brandDot = document.getElementById('brand-dot');
702
+ const colorMap = { healthy: 'var(--healthy)', warning: 'var(--warning)',
703
+ critical: 'var(--critical)' };
704
+ const c = colorMap[sc] || 'var(--text-muted)';
705
+ brandDot.style.background = c;
706
+ brandDot.style.boxShadow = '0 0 6px ' + c;
707
+
708
+ setText(document.getElementById('uptime-val'),
709
+ data.uptime != null ? formatUptime(data.uptime) : '—');
710
+ setText(document.getElementById('version-val'), data.version || '—');
711
+ setText(document.getElementById('last-update-val'),
712
+ new Date().toLocaleTimeString());
713
+ }
714
+
715
+ // ── History ──────────────────────────────────────────────────────────────
716
+
717
+ function renderHistory(rows) {
718
+ const list = document.getElementById('history-list');
719
+ while (list.firstChild) list.removeChild(list.firstChild);
720
+
721
+ if (!rows || rows.length === 0) {
722
+ const empty = el('div', 'history-empty');
723
+ setText(empty, 'No history yet.');
724
+ list.appendChild(empty);
725
+ return;
726
+ }
727
+
728
+ for (const r of rows) {
729
+ const row = el('div', 'history-item');
730
+
731
+ const timeEl = el('span', 'history-time');
732
+ const pluginEl = el('span', 'history-plugin');
733
+ const typeEl = el('span', 'history-type ' + (r.type || ''));
734
+ const badgeEl = el('span', 'history-badge ' + statusClass(r.status));
735
+ const msgEl = el('span', 'history-message');
736
+
737
+ setText(timeEl, formatTime(r.timestamp));
738
+ setText(pluginEl, r.plugin || '');
739
+ pluginEl.title = r.plugin || '';
740
+ setText(typeEl, r.type || '');
741
+ setText(badgeEl, r.status || '');
742
+
743
+ let msgText = r.message || '';
744
+ if (!msgText && r.actions) {
745
+ try { msgText = JSON.parse(r.actions).join(', '); } catch (e) { msgText = r.actions; }
746
+ }
747
+ setText(msgEl, msgText);
748
+ msgEl.title = msgText;
749
+
750
+ row.appendChild(timeEl);
751
+ row.appendChild(pluginEl);
752
+ row.appendChild(typeEl);
753
+ row.appendChild(badgeEl);
754
+ row.appendChild(msgEl);
755
+ list.appendChild(row);
756
+ }
757
+ }
758
+
759
+ // ── API calls ─────────────────────────────────────────────────────────────
760
+
761
+ async function fetchHealth() {
762
+ try {
763
+ const res = await fetch(API_BASE + '/health', { cache: 'no-store' });
764
+ if (!res.ok) throw new Error('HTTP ' + res.status);
765
+ const data = await res.json();
766
+
767
+ setOffline(false);
768
+ updateStatusBar(data);
769
+ updateGauges(data);
770
+
771
+ if (Array.isArray(data.plugins) && data.plugins.length > 0) {
772
+ buildPluginCards(data.plugins);
773
+ }
774
+ } catch (err) {
775
+ console.warn('[watchdog] fetchHealth error:', err.message);
776
+ setOffline(true);
777
+ }
778
+ }
779
+
780
+ async function fetchHistory() {
781
+ try {
782
+ const res = await fetch(API_BASE + '/history?limit=20', { cache: 'no-store' });
783
+ if (!res.ok) throw new Error('HTTP ' + res.status);
784
+ const data = await res.json();
785
+ const rows = Array.isArray(data) ? data : (data.history || data.rows || []);
786
+ renderHistory(rows);
787
+ } catch (err) {
788
+ console.warn('[watchdog] fetchHistory error:', err.message);
789
+ const list = document.getElementById('history-list');
790
+ if (!list.querySelector('.history-item')) {
791
+ while (list.firstChild) list.removeChild(list.firstChild);
792
+ const empty = el('div', 'history-empty');
793
+ setText(empty, 'Unable to load history.');
794
+ list.appendChild(empty);
795
+ }
796
+ }
797
+ }
798
+
799
+ async function runPlugin(name, action) {
800
+ const card = document.getElementById('plugin-' + name);
801
+ const btns = card ? card.querySelectorAll('button') : [];
802
+ btns.forEach(function (b) { b.disabled = true; });
803
+
804
+ try {
805
+ const res = await fetch(
806
+ API_BASE + '/plugins/' + encodeURIComponent(name) + '/run',
807
+ {
808
+ method: 'POST',
809
+ headers: { 'Content-Type': 'application/json' },
810
+ body: JSON.stringify({ action: action }),
811
+ }
812
+ );
813
+
814
+ const data = await res.json();
815
+
816
+ if (res.ok) {
817
+ const msg = data.message || data.result || JSON.stringify(data, null, 2);
818
+ alert('[' + name + '] ' + action.toUpperCase() + ' complete\n\n' + msg);
819
+ } else {
820
+ const msg = data.error || data.message || 'HTTP ' + res.status;
821
+ alert('[' + name + '] ' + action.toUpperCase() + ' failed\n\n' + msg);
822
+ }
823
+
824
+ await fetchHealth();
825
+ await fetchHistory();
826
+ } catch (err) {
827
+ alert('[' + name + '] ' + action.toUpperCase() + ' error\n\n' + err.message);
828
+ } finally {
829
+ btns.forEach(function (b) { b.disabled = false; });
830
+ }
831
+ }
832
+
833
+ // ── Offline handling ──────────────────────────────────────────────────────
834
+
835
+ function setOffline(offline) {
836
+ document.getElementById('offline-banner').style.display = offline ? 'block' : 'none';
837
+ if (offline) {
838
+ document.getElementById('status-pill').className = 'status-pill offline';
839
+ setText(document.getElementById('status-text'), 'Offline');
840
+ const brandDot = document.getElementById('brand-dot');
841
+ brandDot.style.background = 'var(--text-muted)';
842
+ brandDot.style.boxShadow = 'none';
843
+ }
844
+ }
845
+
846
+ // ── Boot ─────────────────────────────────────────────────────────────────
847
+
848
+ buildGaugeGrid();
849
+ fetchHealth();
850
+ fetchHistory();
851
+ setInterval(function () { fetchHealth(); fetchHistory(); }, 15000);
852
+
853
+ }());
854
+ </script>
855
+ </body>
856
+ </html>