bgrun 3.10.1 → 3.11.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/README.md +76 -2
- package/dashboard/app/api/deps/route.ts +49 -0
- package/dashboard/app/api/guard/route.ts +50 -0
- package/dashboard/app/api/guard-all/route.ts +50 -0
- package/dashboard/app/api/logs/rotate/route.ts +45 -0
- package/dashboard/app/api/processes/route.ts +67 -10
- package/dashboard/app/globals.css +386 -6
- package/dashboard/app/page.client.tsx +257 -8
- package/dashboard/app/page.tsx +20 -1
- package/dist/index.js +462 -30
- package/package.json +61 -60
- package/src/api.ts +3 -3
- package/src/commands/list.ts +3 -3
- package/src/commands/run.ts +17 -0
- package/src/db.ts +8 -0
- package/src/deps.ts +126 -0
- package/src/guard.ts +157 -0
- package/src/index.ts +108 -3
- package/src/log-rotation.ts +93 -0
- package/src/logger.ts +4 -3
- package/src/platform.ts +39 -23
- package/src/server.ts +55 -11
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** @jsxImportSource
|
|
1
|
+
/** @jsxImportSource melina/client */
|
|
2
2
|
/**
|
|
3
3
|
* bgrun Dashboard — Page Client Interactivity
|
|
4
4
|
*
|
|
@@ -26,6 +26,10 @@ interface ProcessData {
|
|
|
26
26
|
configPath: string;
|
|
27
27
|
stdoutPath: string;
|
|
28
28
|
stderrPath: string;
|
|
29
|
+
guardRestarts: number;
|
|
30
|
+
cpu?: number;
|
|
31
|
+
cpuHistory?: number[];
|
|
32
|
+
memoryHistory?: number[];
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
// ─── SVG Icon Helpers ───
|
|
@@ -94,6 +98,51 @@ function DeployIcon() {
|
|
|
94
98
|
);
|
|
95
99
|
}
|
|
96
100
|
|
|
101
|
+
function ShieldIcon() {
|
|
102
|
+
return (
|
|
103
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
104
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
105
|
+
</svg>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Sparkline Component ───
|
|
110
|
+
function MiniSparkline({ data, height = 24, stroke = "var(--text-accent)" }: { data: number[], height?: number, stroke?: string }) {
|
|
111
|
+
if (!data || data.length < 2) return <svg className="sparkline" viewBox="0 0 100 24" height={height} width="60" />;
|
|
112
|
+
|
|
113
|
+
const max = Math.max(...data, 1);
|
|
114
|
+
const min = Math.min(...data, 0); // anchor to 0 minimum
|
|
115
|
+
const range = max - min || 1;
|
|
116
|
+
const padding = 2; // top/bottom pixel padding
|
|
117
|
+
|
|
118
|
+
// Normalize path across 100 viewBox width
|
|
119
|
+
const path = data.map((val, i) => {
|
|
120
|
+
const x = (i / (data.length - 1)) * 100;
|
|
121
|
+
const normalizedY = ((val - min) / range);
|
|
122
|
+
const y = padding + (1 - normalizedY) * (24 - padding * 2);
|
|
123
|
+
return `${i === 0 ? 'M' : 'L'} ${x} ${y}`;
|
|
124
|
+
}).join(' ');
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<svg className="sparkline" viewBox="0 0 100 24" height={height} width="60" fill="none" stroke={stroke} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'inline-block', verticalAlign: 'middle', marginLeft: '6px', opacity: 0.8 }}>
|
|
128
|
+
<path d={path} />
|
|
129
|
+
</svg>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Guard Helper ───
|
|
134
|
+
|
|
135
|
+
function isGuarded(p: ProcessData): boolean {
|
|
136
|
+
if (!p.env) return false;
|
|
137
|
+
// env is comma-separated "KEY=VAL,KEY2=VAL2" or JSON string
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(p.env);
|
|
140
|
+
return parsed.BGR_KEEP_ALIVE === 'true';
|
|
141
|
+
} catch {
|
|
142
|
+
return p.env.includes('BGR_KEEP_ALIVE=true');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
97
146
|
// ─── Utility: Format Runtime ───
|
|
98
147
|
|
|
99
148
|
function formatRuntime(raw: string): string {
|
|
@@ -148,11 +197,22 @@ function shortenPath(dir: string): string {
|
|
|
148
197
|
// ─── JSX Components ───
|
|
149
198
|
|
|
150
199
|
function ProcessRow({ p, animate }: { p: ProcessData; animate?: boolean }) {
|
|
200
|
+
const guarded = isGuarded(p);
|
|
151
201
|
return (
|
|
152
202
|
<tr data-process-name={p.name} className={animate ? 'animate-in' : ''} style={animate ? { opacity: '0' } : undefined}>
|
|
153
203
|
<td>
|
|
154
204
|
<div className="process-name">
|
|
155
205
|
<span>{p.name}</span>
|
|
206
|
+
<button
|
|
207
|
+
className={`guard-toggle ${guarded ? 'guarded' : ''}`}
|
|
208
|
+
data-action="guard"
|
|
209
|
+
data-name={p.name}
|
|
210
|
+
data-guarded={guarded ? 'true' : 'false'}
|
|
211
|
+
title={guarded ? 'Process is guarded — click to disable auto-restart' : 'Click to enable auto-restart guard'}
|
|
212
|
+
onClick={(e: Event) => e.stopPropagation()}
|
|
213
|
+
>
|
|
214
|
+
<ShieldIcon />
|
|
215
|
+
</button>
|
|
156
216
|
</div>
|
|
157
217
|
</td>
|
|
158
218
|
<td>
|
|
@@ -168,14 +228,26 @@ function ProcessRow({ p, animate }: { p: ProcessData; animate?: boolean }) {
|
|
|
168
228
|
: <span style={{ color: 'var(--text-muted)' }}>–</span>
|
|
169
229
|
}
|
|
170
230
|
</td>
|
|
231
|
+
<td className="cpu">
|
|
232
|
+
{p.running && (p.cpu !== undefined)
|
|
233
|
+
? <div className="metrics-cell">
|
|
234
|
+
<span>{p.cpu > 0 ? `${p.cpu.toFixed(1)}%` : '<0.1%'}</span>
|
|
235
|
+
<MiniSparkline data={p.cpuHistory || []} stroke="#7ee787" />
|
|
236
|
+
</div>
|
|
237
|
+
: <span style={{ color: 'var(--text-muted)' }}>–</span>
|
|
238
|
+
}
|
|
239
|
+
</td>
|
|
171
240
|
<td className="memory">
|
|
172
|
-
{p.memory > 0
|
|
173
|
-
? <
|
|
241
|
+
{p.running && p.memory > 0
|
|
242
|
+
? <div className="metrics-cell">
|
|
243
|
+
<span className="memory-badge">{formatMemory(p.memory)}</span>
|
|
244
|
+
<MiniSparkline data={p.memoryHistory || []} stroke="#a5d6ff" />
|
|
245
|
+
</div>
|
|
174
246
|
: <span style={{ color: 'var(--text-muted)' }}>–</span>
|
|
175
247
|
}
|
|
176
248
|
</td>
|
|
177
249
|
<td className="command" title={p.command}>{p.command}</td>
|
|
178
|
-
<td className="runtime">{formatRuntime(p.runtime)}</td>
|
|
250
|
+
<td className="runtime">{formatRuntime(String(p.runtime))}</td>
|
|
179
251
|
</tr>
|
|
180
252
|
);
|
|
181
253
|
}
|
|
@@ -215,11 +287,13 @@ function EmptyState() {
|
|
|
215
287
|
}
|
|
216
288
|
|
|
217
289
|
function ProcessCard({ p }: { p: ProcessData }) {
|
|
290
|
+
const guarded = isGuarded(p);
|
|
218
291
|
return (
|
|
219
292
|
<div className="process-card" data-process-name={p.name}>
|
|
220
293
|
<div className="card-header">
|
|
221
294
|
<div className="process-name">
|
|
222
295
|
<span>{p.name}</span>
|
|
296
|
+
{guarded && <span className="guard-badge" title="Auto-restart enabled">🛡️</span>}
|
|
223
297
|
</div>
|
|
224
298
|
<span className={`status-badge ${p.running ? 'running' : 'stopped'}`}>
|
|
225
299
|
<span className="status-dot"></span>
|
|
@@ -229,11 +303,15 @@ function ProcessCard({ p }: { p: ProcessData }) {
|
|
|
229
303
|
<div className="card-details">
|
|
230
304
|
<div className="card-detail"><span className="card-label">PID</span><span>{p.pid}</span></div>
|
|
231
305
|
<div className="card-detail"><span className="card-label">Port</span>{p.port ? <a className="port-link" href={`http://localhost:${p.port}`} target="_blank" rel="noopener" onClick={(e: Event) => e.stopPropagation()}>:{p.port}</a> : <span>–</span>}</div>
|
|
232
|
-
<div className="card-detail"><span className="card-label">
|
|
233
|
-
<div className="card-detail"><span className="card-label">
|
|
306
|
+
<div className="card-detail"><span className="card-label">CPU</span>{p.running && (p.cpu !== undefined) ? <div style={{ display: 'flex', alignItems: 'center' }}><span>{p.cpu > 0 ? `${p.cpu.toFixed(1)}%` : '<0.1%'}</span><MiniSparkline data={p.cpuHistory || []} stroke="#7ee787" /></div> : <span>–</span>}</div>
|
|
307
|
+
<div className="card-detail"><span className="card-label">Memory</span>{p.running && p.memory > 0 ? <div style={{ display: 'flex', alignItems: 'center' }}><span>{formatMemory(p.memory)}</span><MiniSparkline data={p.memoryHistory || []} stroke="#a5d6ff" /></div> : <span>–</span>}</div>
|
|
308
|
+
<div className="card-detail"><span className="card-label">Runtime</span><span>{formatRuntime(String(p.runtime))}</span></div>
|
|
234
309
|
</div>
|
|
235
310
|
<div className="card-command" title={p.command}>{p.command}</div>
|
|
236
311
|
<div className="card-actions">
|
|
312
|
+
<button className={`action-btn guard ${guarded ? 'active' : ''}`} data-action="guard" data-name={p.name} data-guarded={guarded ? 'true' : 'false'} title={guarded ? 'Disable auto-restart' : 'Enable auto-restart'}>
|
|
313
|
+
<ShieldIcon /> {guarded ? 'Unguard' : 'Guard'}
|
|
314
|
+
</button>
|
|
237
315
|
<button className="action-btn info" data-action="logs" data-name={p.name} title="View Logs">
|
|
238
316
|
<LogsIcon /> Logs
|
|
239
317
|
</button>
|
|
@@ -401,16 +479,51 @@ export default function mount(): () => void {
|
|
|
401
479
|
const total = processes.length;
|
|
402
480
|
const running = processes.filter(p => p.running).length;
|
|
403
481
|
const stopped = total - running;
|
|
482
|
+
const guarded = processes.filter(p => isGuarded(p)).length;
|
|
483
|
+
const guardable = processes.filter(p => p.name !== 'bgr-dashboard').length;
|
|
404
484
|
const totalMemory = processes.reduce((sum, p) => sum + (p.memory || 0), 0);
|
|
405
485
|
|
|
406
486
|
const tc = $('total-count');
|
|
407
487
|
const rc = $('running-count');
|
|
408
488
|
const sc = $('stopped-count');
|
|
489
|
+
const gc = $('guarded-count');
|
|
409
490
|
const mc = $('memory-count');
|
|
491
|
+
const rrc = $('restarts-count');
|
|
410
492
|
if (tc) tc.textContent = String(total);
|
|
411
493
|
if (rc) rc.textContent = String(running);
|
|
412
494
|
if (sc) sc.textContent = String(stopped);
|
|
495
|
+
if (gc) gc.textContent = String(guarded);
|
|
413
496
|
if (mc) mc.textContent = formatMemory(totalMemory) || '0 MB';
|
|
497
|
+
const totalRestarts = processes.reduce((sum, p) => sum + (p.guardRestarts || 0), 0);
|
|
498
|
+
if (rrc) rrc.textContent = String(totalRestarts);
|
|
499
|
+
|
|
500
|
+
// Update Guard All button state
|
|
501
|
+
const guardAllBtn = $('guard-all-btn');
|
|
502
|
+
const guardAllLabel = $('guard-all-label');
|
|
503
|
+
if (guardAllBtn && guardAllLabel) {
|
|
504
|
+
const allGuarded = guardable > 0 && guarded >= guardable;
|
|
505
|
+
guardAllBtn.classList.toggle('all-guarded', allGuarded);
|
|
506
|
+
guardAllLabel.textContent = allGuarded ? 'Unguard All' : 'Guard All';
|
|
507
|
+
guardAllBtn.title = allGuarded ? 'Remove guard from all processes' : 'Guard all processes (auto-restart on crash)';
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Update guard sentinel pill
|
|
511
|
+
const guardPill = $('guard-sentinel-pill');
|
|
512
|
+
const guardLabel = $('guard-sentinel-label');
|
|
513
|
+
if (guardPill && guardLabel) {
|
|
514
|
+
const guardProc = processes.find(p => p.name === 'bgr-guard');
|
|
515
|
+
guardPill.classList.remove('active', 'stopped');
|
|
516
|
+
if (guardProc && guardProc.running) {
|
|
517
|
+
guardPill.classList.add('active');
|
|
518
|
+
const restarts = guardProc.guardRestarts || 0;
|
|
519
|
+
guardLabel.textContent = restarts > 0 ? `Guard: ON (${restarts}↻)` : 'Guard: ON';
|
|
520
|
+
} else if (guardProc) {
|
|
521
|
+
guardPill.classList.add('stopped');
|
|
522
|
+
guardLabel.textContent = 'Guard: OFF';
|
|
523
|
+
} else {
|
|
524
|
+
guardLabel.textContent = 'Guard: –';
|
|
525
|
+
}
|
|
526
|
+
}
|
|
414
527
|
}
|
|
415
528
|
|
|
416
529
|
function renderProcesses(processes: ProcessData[]) {
|
|
@@ -608,6 +721,29 @@ export default function mount(): () => void {
|
|
|
608
721
|
break;
|
|
609
722
|
}
|
|
610
723
|
|
|
724
|
+
case 'guard': {
|
|
725
|
+
const currentlyGuarded = btn.dataset.guarded === 'true';
|
|
726
|
+
const newState = !currentlyGuarded;
|
|
727
|
+
try {
|
|
728
|
+
const res = await fetch('/api/guard', {
|
|
729
|
+
method: 'POST',
|
|
730
|
+
headers: { 'Content-Type': 'application/json' },
|
|
731
|
+
body: JSON.stringify({ name, enabled: newState }),
|
|
732
|
+
});
|
|
733
|
+
if (res.ok) {
|
|
734
|
+
showToast(`${newState ? 'Guarded' : 'Unguarded'} "${name}"`, 'success');
|
|
735
|
+
} else {
|
|
736
|
+
const data = await res.json();
|
|
737
|
+
showToast(data.error || `Failed to toggle guard for "${name}"`, 'error');
|
|
738
|
+
}
|
|
739
|
+
} catch {
|
|
740
|
+
showToast(`Failed to toggle guard for "${name}"`, 'error');
|
|
741
|
+
}
|
|
742
|
+
await loadProcessesFresh();
|
|
743
|
+
mutationUntil = Date.now() + 3000;
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
|
|
611
747
|
case 'logs':
|
|
612
748
|
openDrawer(name);
|
|
613
749
|
break;
|
|
@@ -631,12 +767,17 @@ export default function mount(): () => void {
|
|
|
631
767
|
const proc = allProcesses.find(p => p.name === name);
|
|
632
768
|
if (!proc) return;
|
|
633
769
|
|
|
770
|
+
const guarded = isGuarded(proc);
|
|
634
771
|
const menu = (
|
|
635
772
|
<div className="context-menu" style={{ left: `${x}px`, top: `${y}px` }}>
|
|
636
773
|
<button className="context-item" data-action="logs" data-name={name}>
|
|
637
774
|
<LogsIcon /> View Logs
|
|
638
775
|
</button>
|
|
639
776
|
<div className="context-divider"></div>
|
|
777
|
+
<button className={`context-item ${guarded ? 'guard-active' : 'guard'}`} data-action="guard" data-name={name} data-guarded={guarded ? 'true' : 'false'}>
|
|
778
|
+
<ShieldIcon /> {guarded ? 'Disable Guard' : 'Enable Guard'}
|
|
779
|
+
</button>
|
|
780
|
+
<div className="context-divider"></div>
|
|
640
781
|
{proc.running
|
|
641
782
|
? <button className="context-item danger" data-action="stop" data-name={name}>
|
|
642
783
|
<StopIcon /> Stop
|
|
@@ -842,11 +983,12 @@ export default function mount(): () => void {
|
|
|
842
983
|
const proc = allProcesses.find(p => p.name === name);
|
|
843
984
|
const meta = $('drawer-meta');
|
|
844
985
|
if (meta && proc) {
|
|
986
|
+
const guarded = isGuarded(proc);
|
|
845
987
|
const metaItems = [
|
|
846
988
|
{ label: 'Status', value: proc.running ? '● Running' : '○ Stopped' },
|
|
847
989
|
{ label: 'PID', value: String(proc.pid) },
|
|
848
990
|
{ label: 'Port', value: proc.port ? `:${proc.port}` : '–', href: proc.port ? `http://localhost:${proc.port}` : undefined },
|
|
849
|
-
{ label: 'Runtime', value: formatRuntime(proc.runtime) },
|
|
991
|
+
{ label: 'Runtime', value: formatRuntime(String(proc.runtime)) },
|
|
850
992
|
{ label: 'Command', value: proc.command },
|
|
851
993
|
{ label: 'Directory', value: proc.directory || '–' },
|
|
852
994
|
{ label: 'Memory', value: formatMemory(proc.memory) },
|
|
@@ -862,7 +1004,74 @@ export default function mount(): () => void {
|
|
|
862
1004
|
}
|
|
863
1005
|
</div>
|
|
864
1006
|
) as unknown as Node);
|
|
865
|
-
|
|
1007
|
+
|
|
1008
|
+
// Guard toggle row with inline switch
|
|
1009
|
+
const guardRow = (
|
|
1010
|
+
<div className={`meta-item meta-guard ${guarded ? 'guarded' : ''}`}>
|
|
1011
|
+
<span className="meta-label">
|
|
1012
|
+
<ShieldIcon /> Guard
|
|
1013
|
+
</span>
|
|
1014
|
+
<label className="guard-toggle" title={guarded ? 'Auto-restart is ON — click to disable' : 'Auto-restart is OFF — click to enable'}>
|
|
1015
|
+
<input type="checkbox" checked={guarded} className="guard-toggle-input" />
|
|
1016
|
+
<span className="guard-toggle-track">
|
|
1017
|
+
<span className="guard-toggle-thumb"></span>
|
|
1018
|
+
</span>
|
|
1019
|
+
<span className="guard-toggle-label">{guarded ? 'Protected' : 'Off'}</span>
|
|
1020
|
+
</label>
|
|
1021
|
+
</div>
|
|
1022
|
+
) as unknown as HTMLElement;
|
|
1023
|
+
|
|
1024
|
+
// Wire toggle click
|
|
1025
|
+
const checkbox = guardRow.querySelector('.guard-toggle-input') as HTMLInputElement;
|
|
1026
|
+
checkbox?.addEventListener('change', async () => {
|
|
1027
|
+
const newState = checkbox.checked;
|
|
1028
|
+
const labelEl = guardRow.querySelector('.guard-toggle-label');
|
|
1029
|
+
const trackEl = guardRow.querySelector('.guard-toggle-track');
|
|
1030
|
+
// Optimistic UI update
|
|
1031
|
+
if (labelEl) labelEl.textContent = newState ? 'Protected' : 'Off';
|
|
1032
|
+
guardRow.classList.toggle('guarded', newState);
|
|
1033
|
+
try {
|
|
1034
|
+
const res = await fetch('/api/guard', {
|
|
1035
|
+
method: 'POST',
|
|
1036
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1037
|
+
body: JSON.stringify({ name, enabled: newState }),
|
|
1038
|
+
});
|
|
1039
|
+
if (res.ok) {
|
|
1040
|
+
showToast(`${newState ? 'Guard enabled' : 'Guard disabled'} for "${name}"`, 'success');
|
|
1041
|
+
} else {
|
|
1042
|
+
// Rollback
|
|
1043
|
+
checkbox.checked = !newState;
|
|
1044
|
+
if (labelEl) labelEl.textContent = !newState ? 'Protected' : 'Off';
|
|
1045
|
+
guardRow.classList.toggle('guarded', !newState);
|
|
1046
|
+
showToast('Failed to toggle guard', 'error');
|
|
1047
|
+
}
|
|
1048
|
+
} catch {
|
|
1049
|
+
checkbox.checked = !newState;
|
|
1050
|
+
if (labelEl) labelEl.textContent = !newState ? 'Protected' : 'Off';
|
|
1051
|
+
guardRow.classList.toggle('guarded', !newState);
|
|
1052
|
+
showToast('Failed to toggle guard', 'error');
|
|
1053
|
+
}
|
|
1054
|
+
await loadProcessesFresh();
|
|
1055
|
+
mutationUntil = Date.now() + 3000;
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
// Guard restart counter (only shown when > 0)
|
|
1059
|
+
const extraRows: Node[] = [];
|
|
1060
|
+
if (proc.guardRestarts > 0) {
|
|
1061
|
+
extraRows.push((
|
|
1062
|
+
<div className="meta-item meta-restarts">
|
|
1063
|
+
<span className="meta-label">Guard Restarts</span>
|
|
1064
|
+
<span className="meta-value">
|
|
1065
|
+
<span className="restart-count-badge">{proc.guardRestarts}</span>
|
|
1066
|
+
<span className="restart-count-text">
|
|
1067
|
+
{proc.guardRestarts === 1 ? 'auto-restart this session' : 'auto-restarts this session'}
|
|
1068
|
+
</span>
|
|
1069
|
+
</span>
|
|
1070
|
+
</div>
|
|
1071
|
+
) as unknown as Node);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
meta.replaceChildren(...items, guardRow, ...extraRows);
|
|
866
1075
|
}
|
|
867
1076
|
|
|
868
1077
|
// Reset log subtab to stdout (skip auto-refresh, we call it once below)
|
|
@@ -1261,6 +1470,46 @@ export default function mount(): () => void {
|
|
|
1261
1470
|
if (drawerProcess) refreshDrawerLogs();
|
|
1262
1471
|
});
|
|
1263
1472
|
|
|
1473
|
+
// ─── Guard All Button ───
|
|
1474
|
+
$('guard-all-btn')?.addEventListener('click', async () => {
|
|
1475
|
+
const guardAllBtn = $('guard-all-btn') as HTMLButtonElement;
|
|
1476
|
+
if (!guardAllBtn) return;
|
|
1477
|
+
|
|
1478
|
+
const guardable = allProcesses.filter(p => p.name !== 'bgr-dashboard');
|
|
1479
|
+
const guarded = guardable.filter(p => isGuarded(p)).length;
|
|
1480
|
+
const allGuarded = guardable.length > 0 && guarded >= guardable.length;
|
|
1481
|
+
const newState = !allGuarded;
|
|
1482
|
+
|
|
1483
|
+
// Disable button during operation
|
|
1484
|
+
guardAllBtn.disabled = true;
|
|
1485
|
+
guardAllBtn.style.opacity = '0.5';
|
|
1486
|
+
|
|
1487
|
+
try {
|
|
1488
|
+
const res = await fetch('/api/guard-all', {
|
|
1489
|
+
method: 'POST',
|
|
1490
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1491
|
+
body: JSON.stringify({ enabled: newState }),
|
|
1492
|
+
});
|
|
1493
|
+
if (res.ok) {
|
|
1494
|
+
const data = await res.json();
|
|
1495
|
+
showToast(
|
|
1496
|
+
`${newState ? 'Guarded' : 'Unguarded'} ${data.count} process${data.count !== 1 ? 'es' : ''}`,
|
|
1497
|
+
'success'
|
|
1498
|
+
);
|
|
1499
|
+
} else {
|
|
1500
|
+
const data = await res.json();
|
|
1501
|
+
showToast(data.error || 'Failed to toggle guard for all processes', 'error');
|
|
1502
|
+
}
|
|
1503
|
+
} catch {
|
|
1504
|
+
showToast('Failed to toggle guard for all processes', 'error');
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
guardAllBtn.disabled = false;
|
|
1508
|
+
guardAllBtn.style.opacity = '';
|
|
1509
|
+
await loadProcessesFresh();
|
|
1510
|
+
mutationUntil = Date.now() + 3000;
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1264
1513
|
// Group toggle removed — always-on directory grouping
|
|
1265
1514
|
|
|
1266
1515
|
// ─── Keyboard Shortcuts ───
|
package/dashboard/app/page.tsx
CHANGED
|
@@ -25,10 +25,18 @@ export default function DashboardPage() {
|
|
|
25
25
|
<div className="stat-label">Stopped</div>
|
|
26
26
|
<div className="stat-value" id="stopped-count">–</div>
|
|
27
27
|
</div>
|
|
28
|
+
<div className="stat-card guarded">
|
|
29
|
+
<div className="stat-label">Guarded</div>
|
|
30
|
+
<div className="stat-value" id="guarded-count">–</div>
|
|
31
|
+
</div>
|
|
28
32
|
<div className="stat-card memory">
|
|
29
33
|
<div className="stat-label">Total Memory</div>
|
|
30
34
|
<div className="stat-value" id="memory-count">–</div>
|
|
31
35
|
</div>
|
|
36
|
+
<div className="stat-card restarts">
|
|
37
|
+
<div className="stat-label">Guard Restarts</div>
|
|
38
|
+
<div className="stat-value" id="restarts-count">0</div>
|
|
39
|
+
</div>
|
|
32
40
|
</div>
|
|
33
41
|
|
|
34
42
|
{/* Toolbar */}
|
|
@@ -55,6 +63,16 @@ export default function DashboardPage() {
|
|
|
55
63
|
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
|
|
56
64
|
</svg>
|
|
57
65
|
</button>
|
|
66
|
+
<button className="btn btn-ghost btn-guard-all" id="guard-all-btn" title="Guard All Processes">
|
|
67
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
68
|
+
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
|
|
69
|
+
</svg>
|
|
70
|
+
<span id="guard-all-label">Guard All</span>
|
|
71
|
+
</button>
|
|
72
|
+
<span className="guard-sentinel-pill" id="guard-sentinel-pill" title="Standalone guard process status">
|
|
73
|
+
<span className="guard-sentinel-dot" id="guard-sentinel-dot" />
|
|
74
|
+
<span id="guard-sentinel-label">Guard: –</span>
|
|
75
|
+
</span>
|
|
58
76
|
<button className="btn btn-primary" id="new-process-btn">
|
|
59
77
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
60
78
|
<line x1="12" y1="5" x2="12" y2="19" />
|
|
@@ -74,7 +92,8 @@ export default function DashboardPage() {
|
|
|
74
92
|
<th style={{ width: '90px' }}>Status</th>
|
|
75
93
|
<th style={{ width: '70px' }}>PID</th>
|
|
76
94
|
<th style={{ width: '70px' }}>Port</th>
|
|
77
|
-
<th style={{ width: '
|
|
95
|
+
<th style={{ width: '80px' }}>CPU</th>
|
|
96
|
+
<th style={{ width: '120px' }}>Memory</th>
|
|
78
97
|
<th>Command</th>
|
|
79
98
|
<th style={{ width: '100px' }}>Runtime</th>
|
|
80
99
|
</tr>
|