process-watchdog 1.0.0 → 1.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.
- package/BUILDING.md +64 -0
- package/README.md +115 -0
- package/dashboard/watchdog.html +406 -0
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +45 -0
- package/dist/api/routes.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +31 -7
- package/dist/config.js.map +1 -1
- package/dist/index.js +21 -5
- package/dist/index.js.map +1 -1
- package/dist/platform/index.d.ts.map +1 -1
- package/dist/platform/index.js +7 -1
- package/dist/platform/index.js.map +1 -1
- package/dist/platform/linux.d.ts +14 -0
- package/dist/platform/linux.d.ts.map +1 -0
- package/dist/platform/linux.js +235 -0
- package/dist/platform/linux.js.map +1 -0
- package/dist/platform/macos.d.ts +14 -0
- package/dist/platform/macos.d.ts.map +1 -0
- package/dist/platform/macos.js +255 -0
- package/dist/platform/macos.js.map +1 -0
- package/dist/plugins/plugin-loader.d.ts +1 -1
- package/dist/plugins/plugin-loader.d.ts.map +1 -1
- package/dist/plugins/plugin-loader.js +46 -1
- package/dist/plugins/plugin-loader.js.map +1 -1
- package/dist/plugins/process-guard.d.ts.map +1 -1
- package/dist/plugins/process-guard.js +0 -15
- package/dist/plugins/process-guard.js.map +1 -1
- package/dist/tray/index.d.ts +2 -0
- package/dist/tray/index.d.ts.map +1 -0
- package/dist/tray/index.js +73 -0
- package/dist/tray/index.js.map +1 -0
- package/package.json +21 -2
- package/scripts/bundle.js +38 -0
- package/src/api/routes.ts +56 -0
- package/src/config.ts +31 -7
- package/src/index.ts +21 -5
- package/src/platform/index.ts +7 -1
- package/src/platform/linux.ts +255 -0
- package/src/platform/macos.ts +259 -0
- package/src/plugins/plugin-loader.ts +66 -1
- package/src/plugins/process-guard.ts +0 -15
- package/src/tray/index.ts +86 -0
- package/src/tray/tray.ps1 +156 -0
- package/tests/plugins/cpu-monitor.test.ts +5 -5
- package/tests/plugins/disk-health.test.ts +1 -1
- package/tests/plugins/fixtures/broken-plugin.js +5 -0
- package/tests/plugins/fixtures/custom-plugin.js +36 -0
- package/tests/plugins/fixtures/named-plugin.js +31 -0
- package/tests/plugins/plugin-loader.test.ts +124 -7
- package/tests/plugins/process-guard.test.ts +1 -1
package/BUILDING.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Building Standalone Installers
|
|
2
|
+
|
|
3
|
+
Process Watchdog can be packaged into self-contained executables that do not require Node.js to be installed on the target machine. The build pipeline uses **esbuild** to bundle the TypeScript output into a single CommonJS file, then **@yao-pkg/pkg** to wrap it with a Node.js runtime.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
All dependencies are already in `devDependencies` — run `npm install` once before building.
|
|
8
|
+
|
|
9
|
+
## Build commands
|
|
10
|
+
|
|
11
|
+
| Command | Output |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `npm run build:standalone:win` | `standalone/watchdog-win.exe` (Windows x64) |
|
|
14
|
+
| `npm run build:standalone:mac` | `standalone/watchdog-macos` (macOS x64) |
|
|
15
|
+
| `npm run build:standalone:linux` | `standalone/watchdog-linux` (Linux x64) |
|
|
16
|
+
| `npm run build:standalone` | All three targets |
|
|
17
|
+
|
|
18
|
+
Each command runs three steps automatically:
|
|
19
|
+
|
|
20
|
+
1. **`npm run build`** — TypeScript compiler (`tsc`) compiles `src/` → `dist/`
|
|
21
|
+
2. **`npm run bundle`** — esbuild bundles `dist/index.js` → `bundle/watchdog.cjs`
|
|
22
|
+
3. **`pkg`** — wraps `bundle/watchdog.cjs` with a Node.js runtime into a single executable
|
|
23
|
+
|
|
24
|
+
## Why the esbuild step?
|
|
25
|
+
|
|
26
|
+
`better-sqlite3` uses a native `.node` addon (`better_sqlite3.node`) that cannot be inlined by pkg. The esbuild step bundles everything else while leaving `better-sqlite3` and `node-windows` as external `require()` calls. pkg then carries the `.node` file as an **asset** (see `pkg.assets` in `package.json`) and patches the path at runtime so the executable finds it.
|
|
27
|
+
|
|
28
|
+
## Native addon details
|
|
29
|
+
|
|
30
|
+
The `.node` file is embedded in the executable at:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
node_modules/better-sqlite3/build/Release/better_sqlite3.node
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
pkg extracts it to a temporary directory at startup. The `pkg.assets` field in `package.json` controls which files are embedded:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
"assets": [
|
|
40
|
+
"config/**/*",
|
|
41
|
+
"node_modules/better-sqlite3/build/Release/better_sqlite3.node"
|
|
42
|
+
]
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Verifying the build
|
|
46
|
+
|
|
47
|
+
After a successful Windows build:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
./standalone/watchdog-win.exe status
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The executable should print the current watchdog status without needing Node.js installed.
|
|
54
|
+
|
|
55
|
+
## Output locations
|
|
56
|
+
|
|
57
|
+
- `bundle/` — intermediate esbuild output (gitignored)
|
|
58
|
+
- `standalone/` — final executables (gitignored)
|
|
59
|
+
|
|
60
|
+
Neither directory is committed to source control.
|
|
61
|
+
|
|
62
|
+
## Cross-compilation notes
|
|
63
|
+
|
|
64
|
+
pkg downloads pre-built Node.js binaries for the target platform automatically on first use. Building a Linux or macOS binary on Windows is supported — pkg handles the download. However, native addons (`.node` files) are platform-specific: a `better_sqlite3.node` compiled on Windows will not work on Linux. For Linux/macOS targets, the addon must be compiled on that platform. Consider using CI (e.g., GitHub Actions) with platform-specific runners for production cross-platform builds.
|
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Process Watchdog
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/process-watchdog)
|
|
4
|
+
|
|
5
|
+
Modular PC health agent for the aidev.com.au ecosystem.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 5 built-in plugins: `process-guard`, `memory-monitor`, `disk-health`, `startup-optimizer`, `cpu-monitor`
|
|
12
|
+
- REST API on port 3400 (`/api/v1`)
|
|
13
|
+
- `watchdog` CLI tool
|
|
14
|
+
- Windows service support via `node-windows`
|
|
15
|
+
- aidev.com.au dashboard integration (totalRecall, mah)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install -g process-watchdog
|
|
23
|
+
|
|
24
|
+
watchdog status # one-shot health check
|
|
25
|
+
watchdog start # run continuously with scheduler
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## CLI Commands
|
|
31
|
+
|
|
32
|
+
| Command | Description |
|
|
33
|
+
|----------------------------|----------------------------------------------------|
|
|
34
|
+
| `watchdog start` | Load config, start scheduler, watch continuously |
|
|
35
|
+
| `watchdog stop` | Stop the running watchdog process |
|
|
36
|
+
| `watchdog status` | Run check() on each plugin and print a summary |
|
|
37
|
+
| `watchdog check [plugin]` | Detailed metrics for all or a specific plugin |
|
|
38
|
+
| `watchdog fix [plugin]` | Run fix() for all or a specific plugin |
|
|
39
|
+
| `watchdog api start` | Start the HTTP REST API server |
|
|
40
|
+
| `watchdog install-service` | Install watchdog as a Windows system service |
|
|
41
|
+
| `watchdog uninstall-service` | Uninstall the Windows system service |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Plugins
|
|
46
|
+
|
|
47
|
+
| Plugin | Description | Default Interval | Auto-Fix |
|
|
48
|
+
|---------------------|----------------------------------------------|-----------------|----------|
|
|
49
|
+
| `process-guard` | Kills runaway node/cmd/bash processes | 5 min | Yes |
|
|
50
|
+
| `memory-monitor` | Alerts on high RAM usage | 2 min | No |
|
|
51
|
+
| `disk-health` | Checks disk usage and cleans temp files | 30 min | Yes |
|
|
52
|
+
| `startup-optimizer` | Audits startup programs | On demand | No |
|
|
53
|
+
| `cpu-monitor` | Alerts on sustained high CPU usage | 2 min | No |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## API Endpoints
|
|
58
|
+
|
|
59
|
+
All routes are prefixed with `/api/v1`.
|
|
60
|
+
|
|
61
|
+
| Method | Endpoint | Description |
|
|
62
|
+
|--------|------------------------|------------------------------------------------------|
|
|
63
|
+
| GET | `/health` | Overall service health and per-plugin status |
|
|
64
|
+
| GET | `/plugins` | List all plugins with config and last check time |
|
|
65
|
+
| GET | `/plugins/:name` | Plugin details and last 50 history entries |
|
|
66
|
+
| POST | `/plugins/:name/run` | Run `check` or `fix` on a plugin (`{ action }` body) |
|
|
67
|
+
| GET | `/history` | Check/fix history (query: `plugin`, `limit`) |
|
|
68
|
+
| GET | `/config` | Current active configuration |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
**Default config** — `config/default.json` in the package.
|
|
75
|
+
|
|
76
|
+
**User override** — `~/.aidev/watchdog.json` (merged over defaults at startup).
|
|
77
|
+
|
|
78
|
+
Key options:
|
|
79
|
+
|
|
80
|
+
```jsonc
|
|
81
|
+
{
|
|
82
|
+
"port": 3400,
|
|
83
|
+
"logLevel": "info",
|
|
84
|
+
"historyRetentionDays": 30,
|
|
85
|
+
"plugins": {
|
|
86
|
+
"process-guard": { "enabled": true, "interval": 300000, "autoFix": true },
|
|
87
|
+
"memory-monitor": { "enabled": true, "interval": 120000, "autoFix": false }
|
|
88
|
+
// ...
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
History and the SQLite database are stored at `~/.aidev/watchdog.db`.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Dashboard
|
|
98
|
+
|
|
99
|
+
Open `dashboard/watchdog.html` in a browser while the API server is running to view a live health overview compatible with aidev.com.au.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm run build # compile TypeScript → dist/
|
|
107
|
+
npm test # run tests with vitest
|
|
108
|
+
npm run dev # run from source with tsx (no build step)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
package/dashboard/watchdog.html
CHANGED
|
@@ -372,6 +372,176 @@
|
|
|
372
372
|
font-size: 13px;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
/* ── Config editor ── */
|
|
376
|
+
.config-plugin-block {
|
|
377
|
+
border: 1px solid var(--card-border);
|
|
378
|
+
border-radius: 8px;
|
|
379
|
+
margin-bottom: 14px;
|
|
380
|
+
overflow: hidden;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.config-plugin-block:last-of-type { margin-bottom: 0; }
|
|
384
|
+
|
|
385
|
+
.config-plugin-header {
|
|
386
|
+
background: rgba(79,195,247,0.06);
|
|
387
|
+
border-bottom: 1px solid var(--card-border);
|
|
388
|
+
padding: 10px 16px;
|
|
389
|
+
display: flex;
|
|
390
|
+
align-items: center;
|
|
391
|
+
gap: 12px;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.config-plugin-name {
|
|
395
|
+
font-weight: 700;
|
|
396
|
+
font-size: 13px;
|
|
397
|
+
flex: 1;
|
|
398
|
+
color: var(--accent);
|
|
399
|
+
font-family: 'Consolas', 'SF Mono', monospace;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.config-plugin-body {
|
|
403
|
+
padding: 12px 16px;
|
|
404
|
+
display: grid;
|
|
405
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
406
|
+
gap: 10px 20px;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.config-field {
|
|
410
|
+
display: flex;
|
|
411
|
+
flex-direction: column;
|
|
412
|
+
gap: 4px;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
.config-label {
|
|
416
|
+
font-size: 11px;
|
|
417
|
+
font-weight: 600;
|
|
418
|
+
color: var(--text-muted);
|
|
419
|
+
text-transform: uppercase;
|
|
420
|
+
letter-spacing: 0.06em;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.config-input {
|
|
424
|
+
background: var(--bg);
|
|
425
|
+
border: 1px solid var(--card-border);
|
|
426
|
+
border-radius: 5px;
|
|
427
|
+
color: var(--text);
|
|
428
|
+
font-size: 13px;
|
|
429
|
+
padding: 5px 9px;
|
|
430
|
+
width: 100%;
|
|
431
|
+
transition: border-color 0.15s;
|
|
432
|
+
font-family: inherit;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.config-input:focus {
|
|
436
|
+
outline: none;
|
|
437
|
+
border-color: var(--accent);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.config-thresholds {
|
|
441
|
+
grid-column: 1 / -1;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.config-threshold-row {
|
|
445
|
+
display: flex;
|
|
446
|
+
align-items: center;
|
|
447
|
+
gap: 6px;
|
|
448
|
+
margin-bottom: 6px;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.config-threshold-row:last-child { margin-bottom: 0; }
|
|
452
|
+
|
|
453
|
+
.config-threshold-key {
|
|
454
|
+
flex: 1;
|
|
455
|
+
min-width: 0;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.config-threshold-val {
|
|
459
|
+
width: 100px;
|
|
460
|
+
flex-shrink: 0;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/* Toggle switch */
|
|
464
|
+
.toggle-wrap {
|
|
465
|
+
display: flex;
|
|
466
|
+
align-items: center;
|
|
467
|
+
gap: 8px;
|
|
468
|
+
padding-top: 20px;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
.toggle {
|
|
472
|
+
position: relative;
|
|
473
|
+
width: 36px;
|
|
474
|
+
height: 20px;
|
|
475
|
+
flex-shrink: 0;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.toggle input { opacity: 0; width: 0; height: 0; position: absolute; }
|
|
479
|
+
|
|
480
|
+
.toggle-slider {
|
|
481
|
+
position: absolute;
|
|
482
|
+
inset: 0;
|
|
483
|
+
background: var(--card-border);
|
|
484
|
+
border-radius: 20px;
|
|
485
|
+
cursor: pointer;
|
|
486
|
+
transition: background 0.2s;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.toggle-slider::before {
|
|
490
|
+
content: '';
|
|
491
|
+
position: absolute;
|
|
492
|
+
width: 14px;
|
|
493
|
+
height: 14px;
|
|
494
|
+
left: 3px;
|
|
495
|
+
top: 3px;
|
|
496
|
+
background: var(--text-muted);
|
|
497
|
+
border-radius: 50%;
|
|
498
|
+
transition: transform 0.2s, background 0.2s;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.toggle input:checked + .toggle-slider { background: rgba(79,195,247,0.25); }
|
|
502
|
+
.toggle input:checked + .toggle-slider::before {
|
|
503
|
+
transform: translateX(16px);
|
|
504
|
+
background: var(--accent);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/* Config action bar */
|
|
508
|
+
.config-actions {
|
|
509
|
+
display: flex;
|
|
510
|
+
align-items: center;
|
|
511
|
+
gap: 12px;
|
|
512
|
+
margin-top: 16px;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.btn-save {
|
|
516
|
+
background: rgba(79,195,247,0.15);
|
|
517
|
+
border-color: var(--accent);
|
|
518
|
+
color: var(--accent);
|
|
519
|
+
padding: 8px 20px;
|
|
520
|
+
font-size: 13px;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
.config-feedback {
|
|
524
|
+
font-size: 12px;
|
|
525
|
+
font-weight: 600;
|
|
526
|
+
padding: 4px 10px;
|
|
527
|
+
border-radius: 5px;
|
|
528
|
+
display: none;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.config-feedback.success {
|
|
532
|
+
display: inline-block;
|
|
533
|
+
background: rgba(102,187,106,0.15);
|
|
534
|
+
color: var(--healthy);
|
|
535
|
+
border: 1px solid var(--healthy);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.config-feedback.error {
|
|
539
|
+
display: inline-block;
|
|
540
|
+
background: rgba(239,83,80,0.15);
|
|
541
|
+
color: var(--critical);
|
|
542
|
+
border: 1px solid var(--critical);
|
|
543
|
+
}
|
|
544
|
+
|
|
375
545
|
/* ── Footer ── */
|
|
376
546
|
footer {
|
|
377
547
|
text-align: center;
|
|
@@ -440,6 +610,18 @@
|
|
|
440
610
|
</div>
|
|
441
611
|
</section>
|
|
442
612
|
|
|
613
|
+
<section id="config-section">
|
|
614
|
+
<h2>Configuration</h2>
|
|
615
|
+
<div class="card" id="config-card">
|
|
616
|
+
<div class="history-empty" id="config-loading">Loading configuration…</div>
|
|
617
|
+
<div id="config-plugins" style="display:none;"></div>
|
|
618
|
+
<div class="config-actions" id="config-actions" style="display:none;">
|
|
619
|
+
<button class="btn btn-save" id="config-save-btn" type="button">Save Changes</button>
|
|
620
|
+
<span class="config-feedback" id="config-feedback"></span>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
</section>
|
|
624
|
+
|
|
443
625
|
</main>
|
|
444
626
|
|
|
445
627
|
<footer>
|
|
@@ -843,11 +1025,235 @@
|
|
|
843
1025
|
}
|
|
844
1026
|
}
|
|
845
1027
|
|
|
1028
|
+
// ── Config editor ─────────────────────────────────────────────────────────
|
|
1029
|
+
|
|
1030
|
+
// Store the raw config so Save knows what to build from
|
|
1031
|
+
var _loadedConfig = null;
|
|
1032
|
+
|
|
1033
|
+
function buildConfigEditor(config) {
|
|
1034
|
+
_loadedConfig = config;
|
|
1035
|
+
var container = document.getElementById('config-plugins');
|
|
1036
|
+
while (container.firstChild) container.removeChild(container.firstChild);
|
|
1037
|
+
|
|
1038
|
+
var plugins = config.plugins || {};
|
|
1039
|
+
|
|
1040
|
+
Object.keys(plugins).forEach(function (pluginName) {
|
|
1041
|
+
var pcfg = plugins[pluginName];
|
|
1042
|
+
|
|
1043
|
+
var block = el('div', 'config-plugin-block');
|
|
1044
|
+
|
|
1045
|
+
// ── Header: plugin name + enabled toggle ──
|
|
1046
|
+
var header = el('div', 'config-plugin-header');
|
|
1047
|
+
|
|
1048
|
+
var nameEl = el('span', 'config-plugin-name');
|
|
1049
|
+
setText(nameEl, pluginName);
|
|
1050
|
+
header.appendChild(nameEl);
|
|
1051
|
+
|
|
1052
|
+
var enabledLabel = el('span', 'config-label');
|
|
1053
|
+
setText(enabledLabel, 'Enabled');
|
|
1054
|
+
enabledLabel.style.marginRight = '6px';
|
|
1055
|
+
|
|
1056
|
+
var toggleWrap = el('label', 'toggle');
|
|
1057
|
+
var toggleInput = el('input');
|
|
1058
|
+
toggleInput.type = 'checkbox';
|
|
1059
|
+
toggleInput.id = 'cfg-enabled-' + pluginName;
|
|
1060
|
+
toggleInput.checked = !!pcfg.enabled;
|
|
1061
|
+
var toggleSlider = el('span', 'toggle-slider');
|
|
1062
|
+
toggleWrap.appendChild(toggleInput);
|
|
1063
|
+
toggleWrap.appendChild(toggleSlider);
|
|
1064
|
+
|
|
1065
|
+
header.appendChild(enabledLabel);
|
|
1066
|
+
header.appendChild(toggleWrap);
|
|
1067
|
+
block.appendChild(header);
|
|
1068
|
+
|
|
1069
|
+
// ── Body: interval + autoFix + thresholds ──
|
|
1070
|
+
var body = el('div', 'config-plugin-body');
|
|
1071
|
+
|
|
1072
|
+
// Interval (ms → seconds)
|
|
1073
|
+
if (pcfg.interval !== undefined) {
|
|
1074
|
+
var intervalField = el('div', 'config-field');
|
|
1075
|
+
var intervalLabel = el('label', 'config-label');
|
|
1076
|
+
setText(intervalLabel, 'Interval (seconds)');
|
|
1077
|
+
intervalLabel.htmlFor = 'cfg-interval-' + pluginName;
|
|
1078
|
+
var intervalInput = el('input', 'config-input');
|
|
1079
|
+
intervalInput.type = 'number';
|
|
1080
|
+
intervalInput.id = 'cfg-interval-' + pluginName;
|
|
1081
|
+
intervalInput.min = '0';
|
|
1082
|
+
intervalInput.value = String(Math.round(pcfg.interval / 1000));
|
|
1083
|
+
intervalField.appendChild(intervalLabel);
|
|
1084
|
+
intervalField.appendChild(intervalInput);
|
|
1085
|
+
body.appendChild(intervalField);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// AutoFix toggle
|
|
1089
|
+
if (pcfg.autoFix !== undefined) {
|
|
1090
|
+
var autoFixField = el('div', 'config-field');
|
|
1091
|
+
var autoFixLabel = el('label', 'config-label');
|
|
1092
|
+
setText(autoFixLabel, 'Auto Fix');
|
|
1093
|
+
autoFixLabel.htmlFor = 'cfg-autofix-' + pluginName;
|
|
1094
|
+
var autoFixToggleWrap = el('div', 'toggle-wrap');
|
|
1095
|
+
var autoFixToggle = el('label', 'toggle');
|
|
1096
|
+
var autoFixInput = el('input');
|
|
1097
|
+
autoFixInput.type = 'checkbox';
|
|
1098
|
+
autoFixInput.id = 'cfg-autofix-' + pluginName;
|
|
1099
|
+
autoFixInput.checked = !!pcfg.autoFix;
|
|
1100
|
+
var autoFixSlider = el('span', 'toggle-slider');
|
|
1101
|
+
autoFixToggle.appendChild(autoFixInput);
|
|
1102
|
+
autoFixToggle.appendChild(autoFixSlider);
|
|
1103
|
+
autoFixToggleWrap.appendChild(autoFixToggle);
|
|
1104
|
+
autoFixField.appendChild(autoFixLabel);
|
|
1105
|
+
autoFixField.appendChild(autoFixToggleWrap);
|
|
1106
|
+
body.appendChild(autoFixField);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Thresholds
|
|
1110
|
+
var thresholds = pcfg.thresholds || {};
|
|
1111
|
+
var threshKeys = Object.keys(thresholds);
|
|
1112
|
+
if (threshKeys.length > 0) {
|
|
1113
|
+
var threshField = el('div', 'config-field config-thresholds');
|
|
1114
|
+
var threshHeading = el('span', 'config-label');
|
|
1115
|
+
setText(threshHeading, 'Thresholds');
|
|
1116
|
+
threshField.appendChild(threshHeading);
|
|
1117
|
+
|
|
1118
|
+
threshKeys.forEach(function (key) {
|
|
1119
|
+
var row = el('div', 'config-threshold-row');
|
|
1120
|
+
|
|
1121
|
+
var keyInput = el('input', 'config-input config-threshold-key');
|
|
1122
|
+
keyInput.type = 'text';
|
|
1123
|
+
keyInput.value = key;
|
|
1124
|
+
keyInput.readOnly = true;
|
|
1125
|
+
keyInput.style.color = 'var(--text-muted)';
|
|
1126
|
+
|
|
1127
|
+
var valInput = el('input', 'config-input config-threshold-val');
|
|
1128
|
+
valInput.type = 'number';
|
|
1129
|
+
valInput.id = 'cfg-thresh-' + pluginName + '-' + key;
|
|
1130
|
+
valInput.value = String(thresholds[key]);
|
|
1131
|
+
valInput.dataset.plugin = pluginName;
|
|
1132
|
+
valInput.dataset.key = key;
|
|
1133
|
+
|
|
1134
|
+
row.appendChild(keyInput);
|
|
1135
|
+
row.appendChild(valInput);
|
|
1136
|
+
threshField.appendChild(row);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
body.appendChild(threshField);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
block.appendChild(body);
|
|
1143
|
+
container.appendChild(block);
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
document.getElementById('config-loading').style.display = 'none';
|
|
1147
|
+
container.style.display = 'block';
|
|
1148
|
+
document.getElementById('config-actions').style.display = 'flex';
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function collectConfigPayload() {
|
|
1152
|
+
if (!_loadedConfig) return null;
|
|
1153
|
+
var plugins = _loadedConfig.plugins || {};
|
|
1154
|
+
var payload = { plugins: {} };
|
|
1155
|
+
|
|
1156
|
+
Object.keys(plugins).forEach(function (pluginName) {
|
|
1157
|
+
var pcfg = plugins[pluginName];
|
|
1158
|
+
var out = {};
|
|
1159
|
+
|
|
1160
|
+
// enabled
|
|
1161
|
+
var enabledEl = document.getElementById('cfg-enabled-' + pluginName);
|
|
1162
|
+
if (enabledEl) out.enabled = enabledEl.checked;
|
|
1163
|
+
|
|
1164
|
+
// interval (seconds → ms)
|
|
1165
|
+
var intervalEl = document.getElementById('cfg-interval-' + pluginName);
|
|
1166
|
+
if (intervalEl) {
|
|
1167
|
+
var secs = parseFloat(intervalEl.value);
|
|
1168
|
+
out.interval = isNaN(secs) ? pcfg.interval : Math.round(secs * 1000);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// autoFix
|
|
1172
|
+
var autoFixEl = document.getElementById('cfg-autofix-' + pluginName);
|
|
1173
|
+
if (autoFixEl) out.autoFix = autoFixEl.checked;
|
|
1174
|
+
|
|
1175
|
+
// thresholds
|
|
1176
|
+
var thresholds = pcfg.thresholds || {};
|
|
1177
|
+
if (Object.keys(thresholds).length > 0) {
|
|
1178
|
+
out.thresholds = {};
|
|
1179
|
+
Object.keys(thresholds).forEach(function (key) {
|
|
1180
|
+
var valEl = document.getElementById('cfg-thresh-' + pluginName + '-' + key);
|
|
1181
|
+
if (valEl) {
|
|
1182
|
+
var num = parseFloat(valEl.value);
|
|
1183
|
+
out.thresholds[key] = isNaN(num) ? thresholds[key] : num;
|
|
1184
|
+
} else {
|
|
1185
|
+
out.thresholds[key] = thresholds[key];
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
payload.plugins[pluginName] = out;
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
return payload;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function showConfigFeedback(type, message) {
|
|
1197
|
+
var fb = document.getElementById('config-feedback');
|
|
1198
|
+
fb.className = 'config-feedback ' + type;
|
|
1199
|
+
setText(fb, message);
|
|
1200
|
+
clearTimeout(fb._hideTimer);
|
|
1201
|
+
fb._hideTimer = setTimeout(function () {
|
|
1202
|
+
fb.className = 'config-feedback';
|
|
1203
|
+
setText(fb, '');
|
|
1204
|
+
}, 4000);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
async function fetchConfig() {
|
|
1208
|
+
try {
|
|
1209
|
+
var res = await fetch(API_BASE + '/config', { cache: 'no-store' });
|
|
1210
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
1211
|
+
var data = await res.json();
|
|
1212
|
+
buildConfigEditor(data);
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
console.warn('[watchdog] fetchConfig error:', err.message);
|
|
1215
|
+
var loadingEl = document.getElementById('config-loading');
|
|
1216
|
+
setText(loadingEl, 'Unable to load configuration.');
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
async function saveConfig() {
|
|
1221
|
+
var saveBtn = document.getElementById('config-save-btn');
|
|
1222
|
+
saveBtn.disabled = true;
|
|
1223
|
+
var payload = collectConfigPayload();
|
|
1224
|
+
if (!payload) {
|
|
1225
|
+
showConfigFeedback('error', 'Nothing to save.');
|
|
1226
|
+
saveBtn.disabled = false;
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
try {
|
|
1230
|
+
var res = await fetch(API_BASE + '/config', {
|
|
1231
|
+
method: 'PUT',
|
|
1232
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1233
|
+
body: JSON.stringify(payload),
|
|
1234
|
+
});
|
|
1235
|
+
var data = await res.json();
|
|
1236
|
+
if (res.ok) {
|
|
1237
|
+
showConfigFeedback('success', 'Configuration saved.');
|
|
1238
|
+
buildConfigEditor(data);
|
|
1239
|
+
} else {
|
|
1240
|
+
showConfigFeedback('error', data.error || 'Save failed (HTTP ' + res.status + ').');
|
|
1241
|
+
}
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
showConfigFeedback('error', 'Save error: ' + err.message);
|
|
1244
|
+
} finally {
|
|
1245
|
+
saveBtn.disabled = false;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
document.getElementById('config-save-btn').addEventListener('click', saveConfig);
|
|
1250
|
+
|
|
846
1251
|
// ── Boot ─────────────────────────────────────────────────────────────────
|
|
847
1252
|
|
|
848
1253
|
buildGaugeGrid();
|
|
849
1254
|
fetchHealth();
|
|
850
1255
|
fetchHistory();
|
|
1256
|
+
fetchConfig();
|
|
851
1257
|
setInterval(function () { fetchHealth(); fetchHistory(); }, 15000);
|
|
852
1258
|
|
|
853
1259
|
}());
|
package/dist/api/routes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/api/routes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAIjC,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAgB,MAAM,gCAAgC,CAAC;AAC9E,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAc9C,wBAAgB,YAAY,CAC1B,KAAK,EAAE,YAAY,EACnB,OAAO,EAAE,cAAc,EAAE,EACzB,MAAM,EAAE,cAAc,GACrB,MAAM,CA0JR"}
|
package/dist/api/routes.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import os from 'os';
|
|
2
5
|
const STATUS_ORDER = ['healthy', 'warning', 'critical'];
|
|
3
6
|
function worstStatus(statuses) {
|
|
4
7
|
let worst = 'healthy';
|
|
@@ -100,6 +103,48 @@ export function createRoutes(store, plugins, config) {
|
|
|
100
103
|
router.get('/config', (_req, res) => {
|
|
101
104
|
res.json(config);
|
|
102
105
|
});
|
|
106
|
+
// PUT /config — deep-merge partial config and persist to ~/.aidev/watchdog.json
|
|
107
|
+
router.put('/config', async (req, res) => {
|
|
108
|
+
const body = req.body;
|
|
109
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
110
|
+
res.status(400).json({ error: 'Request body must be a JSON object' });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
// Deep-merge the incoming partial config into the live config object
|
|
115
|
+
function isPlainObject(v) {
|
|
116
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
117
|
+
}
|
|
118
|
+
function deepMerge(target, source) {
|
|
119
|
+
const result = { ...target };
|
|
120
|
+
for (const key of Object.keys(source)) {
|
|
121
|
+
const src = source[key];
|
|
122
|
+
const tgt = result[key];
|
|
123
|
+
if (isPlainObject(src) && isPlainObject(tgt)) {
|
|
124
|
+
result[key] = deepMerge(tgt, src);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
result[key] = src;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
const merged = deepMerge(config, body);
|
|
133
|
+
// Apply the merged values back onto the live config object so GET /config
|
|
134
|
+
// reflects the change immediately (without restart)
|
|
135
|
+
Object.assign(config, merged);
|
|
136
|
+
// Persist user overrides to ~/.aidev/watchdog.json
|
|
137
|
+
const aidevDir = join(os.homedir(), '.aidev');
|
|
138
|
+
await mkdir(aidevDir, { recursive: true });
|
|
139
|
+
const userConfigPath = join(aidevDir, 'watchdog.json');
|
|
140
|
+
await writeFile(userConfigPath, JSON.stringify(body, null, 2), 'utf-8');
|
|
141
|
+
res.json(config);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
145
|
+
res.status(500).json({ error: message });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
103
148
|
return router;
|
|
104
149
|
}
|
|
105
150
|
//# sourceMappingURL=routes.js.map
|