bgrun 3.11.0 → 3.12.1

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 CHANGED
@@ -5,6 +5,7 @@
5
5
  **Production-ready process manager with dashboard and programmatic API, designed for running your containers, services, and AI agents.**
6
6
 
7
7
  [![npm](https://img.shields.io/npm/v/bgrun?color=F7A41D&label=npm&logo=npm)](https://www.npmjs.com/package/bgrun)
8
+ [![CI](https://github.com/Mements/bgr/actions/workflows/ci.yml/badge.svg)](https://github.com/Mements/bgr/actions/workflows/ci.yml)
8
9
  [![bun](https://img.shields.io/badge/runtime-bun-F7A41D?logo=bun)](https://bun.sh/)
9
10
  [![license](https://img.shields.io/npm/l/bgrun)](./LICENSE)
10
11
 
@@ -74,11 +75,16 @@ bgrun.yourdomain.com {
74
75
  Features:
75
76
  - Real-time process status via SSE (no polling)
76
77
  - Start, stop, restart, and delete processes from the UI
77
- - Live stdout/stderr log viewer with search
78
+ - Live stdout/stderr log viewer with search and virtual scrolling
78
79
  - Memory, PID, port, runtime, and guard restarts at a glance
79
80
  - Guard toggle per-process (auto-restart on crash)
80
- - Responsive mobile layout
81
+ - Keyboard shortcuts — `↑/↓` or `j/k` navigate, `Enter` open, `R` restart, `S` stop, `G` guard, `D` delete, `N` new, `?` help
82
+ - Search with debounce, result count badge, and persistence across SSE updates
83
+ - Auto-calibrated virtual scroll for large log files (10K+ lines)
84
+ - Dark / light theme toggle
85
+ - Responsive mobile layout with cards view
81
86
  - Collapsible directory groups
87
+ - Right-click context menu on process rows
82
88
 
83
89
  ---
84
90
 
@@ -26,7 +26,12 @@ export async function GET(req: Request) {
26
26
  controller.enqueue(encoder.encode(`data: []\n\n`));
27
27
  }
28
28
 
29
- controller.enqueue(encoder.encode(`: keepalive\n\n`));
29
+ // Periodic keepalive to prevent proxy/browser timeouts
30
+ const keepaliveInterval = setInterval(() => {
31
+ try {
32
+ controller.enqueue(encoder.encode(`: keepalive\n\n`));
33
+ } catch { /* stream closed */ }
34
+ }, 15_000);
30
35
 
31
36
  // Then send updates every INTERVAL_MS
32
37
  const interval = setInterval(async () => {
@@ -40,7 +45,10 @@ export async function GET(req: Request) {
40
45
  }, INTERVAL_MS);
41
46
 
42
47
  // Store cleanup for when the stream is cancelled
43
- (stream as any).__cleanup = () => clearInterval(interval);
48
+ (stream as any).__cleanup = () => {
49
+ clearInterval(interval);
50
+ clearInterval(keepaliveInterval);
51
+ };
44
52
  },
45
53
  cancel() {
46
54
  if ((stream as any).__cleanup) {
@@ -5,7 +5,7 @@
5
5
  * When enabled=true, the built-in guard will auto-restart this process if it dies.
6
6
  * When enabled=false, the process is left alone.
7
7
  */
8
- import { getProcess, updateProcessEnv } from '../../../../src/db';
8
+ import { getProcess, updateProcessEnv, addHistoryEntry } from '../../../../src/db';
9
9
 
10
10
  export async function POST(req: Request) {
11
11
  try {
@@ -35,6 +35,9 @@ export async function POST(req: Request) {
35
35
  // Save back
36
36
  updateProcessEnv(body.name, JSON.stringify(env));
37
37
 
38
+ // Record history
39
+ addHistoryEntry(body.name, body.enabled ? 'guard_on' : 'guard_off');
40
+
38
41
  return Response.json({
39
42
  ok: true,
40
43
  name: body.name,
@@ -0,0 +1,5 @@
1
+ import { guardEvents } from '../../../../src/server';
2
+
3
+ export async function GET() {
4
+ return Response.json(guardEvents);
5
+ }
@@ -0,0 +1,39 @@
1
+ import { getProcessHistory, getRecentHistory, addHistoryEntry } from '../../../../src/db';
2
+
3
+ export async function GET(req: Request) {
4
+ const url = new URL(req.url);
5
+ const name = url.searchParams.get('name');
6
+ const limit = parseInt(url.searchParams.get('limit') || '50');
7
+
8
+ let history;
9
+ if (name) {
10
+ history = getProcessHistory(name, limit);
11
+ } else {
12
+ history = getRecentHistory(limit);
13
+ }
14
+
15
+ return Response.json(history.map((h: any) => ({
16
+ process_name: h.process_name,
17
+ event: h.event,
18
+ pid: h.pid,
19
+ timestamp: h.timestamp,
20
+ metadata: h.metadata ? JSON.parse(h.metadata) : {},
21
+ })));
22
+ }
23
+
24
+ export async function POST(req: Request) {
25
+ try {
26
+ const body = await req.json();
27
+ const { process_name, event, pid, metadata } = body;
28
+
29
+ if (!process_name || !event) {
30
+ return Response.json({ error: 'process_name and event are required' }, { status: 400 });
31
+ }
32
+
33
+ addHistoryEntry(process_name, event, pid, metadata);
34
+ return Response.json({ success: true });
35
+ } catch (err) {
36
+ console.error('[api/history] Error adding history:', err);
37
+ return Response.json({ error: 'Failed to add history' }, { status: 500 });
38
+ }
39
+ }
@@ -237,10 +237,11 @@ async function fetchProcesses(): Promise<any[]> {
237
237
  export async function GET(req: Request) {
238
238
  const url = new URL(req.url);
239
239
  const bustCache = url.searchParams.has('t');
240
+ const portFilter = url.searchParams.get('port');
240
241
  const now = Date.now();
241
242
 
242
- // Return cached data if still fresh and no bust param
243
- if (!bustCache && cache.data && (now - cache.timestamp) < CACHE_TTL_MS) {
243
+ // Return cached data if still fresh and no bust param and no port filter
244
+ if (!bustCache && !portFilter && cache.data && (now - cache.timestamp) < CACHE_TTL_MS) {
244
245
  return Response.json(cache.data);
245
246
  }
246
247
 
@@ -258,7 +259,14 @@ export async function GET(req: Request) {
258
259
  }
259
260
 
260
261
  try {
261
- const result = await cache.inflight;
262
+ let result = await cache.inflight;
263
+ // Filter by port if specified (also ensures fresh data by bypassing cache above)
264
+ if (portFilter) {
265
+ const portNum = parseInt(portFilter);
266
+ if (!isNaN(portNum)) {
267
+ result = result.filter((p: any) => p.ports?.includes(portNum));
268
+ }
269
+ }
262
270
  return Response.json(result);
263
271
  } catch (err) {
264
272
  console.error('[api/processes] Error fetching processes:', err);
@@ -2,10 +2,13 @@
2
2
  * POST /api/restart/:name — Force-restart a process
3
3
  */
4
4
  import { handleRun } from '../../../../../src/commands/run';
5
+ import { addHistoryEntry, getProcess } from '../../../../../src/db';
5
6
  import { measure } from 'measure-fn';
6
7
 
7
8
  export async function POST(req: Request, { params }: { params: { name: string } }) {
8
9
  const name = decodeURIComponent(params.name);
10
+ const proc = getProcess(name);
11
+ const oldPid = proc?.pid;
9
12
 
10
13
  try {
11
14
  await measure(`Restart "${name}"`, () => handleRun({
@@ -14,6 +17,10 @@ export async function POST(req: Request, { params }: { params: { name: string }
14
17
  force: true,
15
18
  remoteName: '',
16
19
  }));
20
+
21
+ // Record history
22
+ addHistoryEntry(name, 'restart', oldPid);
23
+
17
24
  return Response.json({ success: true });
18
25
  } catch (e: any) {
19
26
  return Response.json({ error: e.message }, { status: 500 });
@@ -2,6 +2,7 @@
2
2
  * POST /api/start — Create or start a process
3
3
  */
4
4
  import { handleRun } from '../../../../src/commands/run';
5
+ import { addHistoryEntry } from '../../../../src/db';
5
6
  import { measure } from 'measure-fn';
6
7
 
7
8
  export async function POST(req: Request) {
@@ -16,6 +17,10 @@ export async function POST(req: Request) {
16
17
  force: body.force || false,
17
18
  remoteName: '',
18
19
  }));
20
+
21
+ // Record history
22
+ addHistoryEntry(body.name, 'start');
23
+
19
24
  return Response.json({ success: true });
20
25
  } catch (e: any) {
21
26
  return Response.json({ error: e.message }, { status: 500 });
@@ -4,7 +4,7 @@
4
4
  * Kills the registered PID, then kills anything remaining on the port.
5
5
  * Sets PID to 0 to prevent reconciliation from hijacking unrelated processes.
6
6
  */
7
- import { getProcess, updateProcessPid } from '../../../../../src/db';
7
+ import { getProcess, updateProcessPid, addHistoryEntry } from '../../../../../src/db';
8
8
  import { isProcessRunning, terminateProcess, getProcessPorts, killProcessOnPort } from '../../../../../src/platform';
9
9
  import { measure } from 'measure-fn';
10
10
 
@@ -37,5 +37,8 @@ export async function POST(req: Request, { params }: { params: { name: string }
37
37
  // a random matching process as this one
38
38
  updateProcessPid(name, 0);
39
39
 
40
+ // Record history
41
+ addHistoryEntry(name, 'stop', proc.pid);
42
+
40
43
  return Response.json({ success: true });
41
44
  }
@@ -0,0 +1,47 @@
1
+ import { getAllTemplates, saveTemplate, deleteTemplate } from '../../../../src/db';
2
+
3
+ export async function GET() {
4
+ const templates = getAllTemplates();
5
+ return Response.json(templates.map((t: any) => ({
6
+ name: t.name,
7
+ command: t.command,
8
+ workdir: t.workdir,
9
+ env: t.env,
10
+ group: t.group,
11
+ created_at: t.created_at,
12
+ })));
13
+ }
14
+
15
+ export async function POST(req: Request) {
16
+ try {
17
+ const body = await req.json();
18
+ const { name, command, workdir, env, group } = body;
19
+
20
+ if (!name || !command) {
21
+ return Response.json({ error: 'name and command are required' }, { status: 400 });
22
+ }
23
+
24
+ saveTemplate({ name, command, workdir, env, group });
25
+ return Response.json({ success: true, name });
26
+ } catch (err) {
27
+ console.error('[api/templates] Error saving template:', err);
28
+ return Response.json({ error: 'Failed to save template' }, { status: 500 });
29
+ }
30
+ }
31
+
32
+ export async function DELETE(req: Request) {
33
+ try {
34
+ const url = new URL(req.url);
35
+ const name = url.searchParams.get('name');
36
+
37
+ if (!name) {
38
+ return Response.json({ error: 'name is required' }, { status: 400 });
39
+ }
40
+
41
+ deleteTemplate(name);
42
+ return Response.json({ success: true });
43
+ } catch (err) {
44
+ console.error('[api/templates] Error deleting template:', err);
45
+ return Response.json({ error: 'Failed to delete template' }, { status: 500 });
46
+ }
47
+ }