bgrun 3.12.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.
@@ -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
+ }
@@ -515,6 +515,33 @@ body::after {
515
515
  margin-left: 0.25rem;
516
516
  }
517
517
 
518
+ /* Group badge for process cards */
519
+ .group-badge {
520
+ font-size: 0.65rem;
521
+ margin-left: 0.35rem;
522
+ padding: 0.1rem 0.4rem;
523
+ border-radius: 999px;
524
+ background: rgba(139, 148, 158, 0.2);
525
+ color: var(--text-muted);
526
+ font-weight: 500;
527
+ }
528
+
529
+ /* Group filter dropdown */
530
+ .group-filter {
531
+ padding: 0.4rem 0.6rem;
532
+ border-radius: 6px;
533
+ border: 1px solid var(--border);
534
+ background: var(--bg-secondary);
535
+ color: var(--text);
536
+ font-size: 0.85rem;
537
+ cursor: pointer;
538
+ margin-left: 0.5rem;
539
+ }
540
+
541
+ .group-filter:hover {
542
+ border-color: var(--text-muted);
543
+ }
544
+
518
545
  /* Guard action button styling */
519
546
  .action-btn.guard {
520
547
  color: var(--text-muted);
@@ -2000,19 +2027,33 @@ a.port-link:hover {
2000
2027
  text-align: center;
2001
2028
  }
2002
2029
 
2003
- /* ─── Config Editor Panel ─── */
2004
- .drawer-config {
2005
- flex: 1;
2006
- display: flex;
2007
- flex-direction: column;
2008
- min-height: 0;
2009
- }
2010
-
2011
- .config-toolbar {
2012
- display: flex;
2013
- align-items: center;
2014
- justify-content: space-between;
2015
- padding: 0.5rem 1rem;
2030
+ /* ─── Config Editor Panel ─── */
2031
+ .drawer-config {
2032
+ flex: 1;
2033
+ display: flex;
2034
+ flex-direction: column;
2035
+ min-height: 0;
2036
+ }
2037
+
2038
+ #config-panel-toml {
2039
+ flex: 1;
2040
+ display: flex;
2041
+ flex-direction: column;
2042
+ min-height: 0;
2043
+ }
2044
+
2045
+ #config-panel-env {
2046
+ flex: 1;
2047
+ display: flex;
2048
+ flex-direction: column;
2049
+ min-height: 0;
2050
+ }
2051
+
2052
+ .config-toolbar {
2053
+ display: flex;
2054
+ align-items: center;
2055
+ justify-content: space-between;
2056
+ padding: 0.5rem 1rem;
2016
2057
  border-bottom: 1px solid var(--border-glass);
2017
2058
  background: rgba(0, 0, 0, 0.12);
2018
2059
  flex-shrink: 0;
@@ -2030,21 +2071,21 @@ a.port-link:hover {
2030
2071
  min-width: 0;
2031
2072
  }
2032
2073
 
2033
- .config-editor {
2034
- flex: 1;
2035
- width: 100%;
2036
- resize: none;
2037
- background: rgba(0, 0, 0, 0.25);
2038
- color: var(--text-primary);
2039
- font-family: var(--font-mono);
2040
- font-size: 0.78rem;
2041
- line-height: 1.7;
2042
- padding: 1rem 1.5rem;
2043
- border: none;
2044
- outline: none;
2045
- min-height: 0;
2046
- tab-size: 4;
2047
- }
2074
+ .config-editor {
2075
+ flex: 1;
2076
+ width: 100%;
2077
+ resize: vertical;
2078
+ background: rgba(0, 0, 0, 0.25);
2079
+ color: var(--text-primary);
2080
+ font-family: var(--font-mono);
2081
+ font-size: 0.78rem;
2082
+ line-height: 1.7;
2083
+ padding: 1rem 1.5rem;
2084
+ border: none;
2085
+ outline: none;
2086
+ min-height: 220px;
2087
+ tab-size: 4;
2088
+ }
2048
2089
 
2049
2090
  .config-editor:focus {
2050
2091
  background: rgba(0, 0, 0, 0.3);
@@ -2607,4 +2648,318 @@ tr.keyboard-focus td:first-child .process-name span {
2607
2648
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.06);
2608
2649
  line-height: 1;
2609
2650
  flex-shrink: 0;
2610
- }
2651
+ }
2652
+
2653
+ /* ─── Guard Activity Feed ─── */
2654
+ .guard-activity {
2655
+ margin: 16px 0 0 0;
2656
+ padding: 12px 16px;
2657
+ background: var(--bg-surface);
2658
+ border: 1px solid var(--border-glass);
2659
+ border-radius: var(--radius-md);
2660
+ }
2661
+
2662
+ .guard-activity-header {
2663
+ display: flex;
2664
+ align-items: center;
2665
+ justify-content: space-between;
2666
+ margin-bottom: 8px;
2667
+ }
2668
+
2669
+ .guard-activity-title {
2670
+ font-size: 0.8rem;
2671
+ font-weight: 600;
2672
+ color: var(--text-secondary);
2673
+ text-transform: uppercase;
2674
+ letter-spacing: 0.5px;
2675
+ }
2676
+
2677
+ .guard-activity-empty {
2678
+ font-size: 0.75rem;
2679
+ color: var(--text-muted);
2680
+ font-style: italic;
2681
+ }
2682
+
2683
+ .guard-activity-list {
2684
+ display: flex;
2685
+ flex-direction: column;
2686
+ gap: 6px;
2687
+ max-height: 120px;
2688
+ overflow-y: auto;
2689
+ }
2690
+
2691
+ .guard-event {
2692
+ display: flex;
2693
+ align-items: center;
2694
+ gap: 8px;
2695
+ padding: 6px 10px;
2696
+ background: var(--bg-glass);
2697
+ border-radius: var(--radius-sm);
2698
+ font-size: 0.8rem;
2699
+ }
2700
+
2701
+ .guard-event-time {
2702
+ font-size: 0.7rem;
2703
+ color: var(--text-muted);
2704
+ font-family: var(--font-mono);
2705
+ flex-shrink: 0;
2706
+ }
2707
+
2708
+ .guard-event-icon {
2709
+ font-size: 0.85rem;
2710
+ flex-shrink: 0;
2711
+ }
2712
+
2713
+ .guard-event.success .guard-event-icon { color: var(--success); }
2714
+ .guard-event.failed .guard-event-icon { color: var(--danger); }
2715
+
2716
+ .guard-event-name {
2717
+ font-weight: 500;
2718
+ color: var(--text-primary);
2719
+ }
2720
+
2721
+ .guard-event-action {
2722
+ color: var(--text-secondary);
2723
+ }
2724
+
2725
+ .guard-event.success { border-left: 2px solid var(--success); }
2726
+ .guard-event.failed { border-left: 2px solid var(--danger); }
2727
+
2728
+ /* Templates Modal */
2729
+ .modal-wide {
2730
+ max-width: 700px;
2731
+ }
2732
+
2733
+ .templates-form {
2734
+ margin-bottom: 1.5rem;
2735
+ padding-bottom: 1rem;
2736
+ border-bottom: 1px solid var(--border-subtle);
2737
+ }
2738
+
2739
+ .templates-form .form-row {
2740
+ display: flex;
2741
+ gap: 1rem;
2742
+ }
2743
+
2744
+ .templates-form .form-row .form-group {
2745
+ flex: 1;
2746
+ }
2747
+
2748
+ .templates-form .form-group {
2749
+ margin-bottom: 0.75rem;
2750
+ }
2751
+
2752
+ .templates-form label {
2753
+ display: block;
2754
+ font-size: 0.75rem;
2755
+ font-weight: 600;
2756
+ color: var(--text-secondary);
2757
+ margin-bottom: 0.3rem;
2758
+ text-transform: uppercase;
2759
+ letter-spacing: 0.04em;
2760
+ }
2761
+
2762
+ .templates-form input {
2763
+ width: 100%;
2764
+ padding: 0.5rem 0.65rem;
2765
+ border: 1px solid var(--border);
2766
+ border-radius: var(--radius-sm);
2767
+ background: var(--bg-secondary);
2768
+ color: var(--text);
2769
+ font-size: 0.85rem;
2770
+ outline: none;
2771
+ transition: border-color 0.2s;
2772
+ box-sizing: border-box;
2773
+ }
2774
+
2775
+ .templates-form input:focus {
2776
+ border-color: var(--accent);
2777
+ }
2778
+
2779
+ .templates-actions {
2780
+ margin-top: 0.75rem;
2781
+ }
2782
+
2783
+ .templates-list {
2784
+ max-height: 300px;
2785
+ overflow-y: auto;
2786
+ }
2787
+
2788
+ .templates-empty {
2789
+ text-align: center;
2790
+ color: var(--text-muted);
2791
+ padding: 2rem;
2792
+ font-size: 0.9rem;
2793
+ }
2794
+
2795
+ .template-item {
2796
+ display: flex;
2797
+ align-items: center;
2798
+ padding: 0.75rem;
2799
+ border-radius: var(--radius-sm);
2800
+ background: var(--bg-secondary);
2801
+ margin-bottom: 0.5rem;
2802
+ gap: 0.75rem;
2803
+ }
2804
+
2805
+ .template-item-info {
2806
+ flex: 1;
2807
+ min-width: 0;
2808
+ }
2809
+
2810
+ .template-item-name {
2811
+ font-weight: 600;
2812
+ color: var(--text-primary);
2813
+ margin-bottom: 0.2rem;
2814
+ }
2815
+
2816
+ .template-item-command {
2817
+ font-size: 0.8rem;
2818
+ color: var(--text-muted);
2819
+ font-family: var(--font-mono);
2820
+ white-space: nowrap;
2821
+ overflow: hidden;
2822
+ text-overflow: ellipsis;
2823
+ }
2824
+
2825
+ .template-item-group {
2826
+ font-size: 0.7rem;
2827
+ padding: 0.15rem 0.4rem;
2828
+ border-radius: 999px;
2829
+ background: rgba(139, 148, 158, 0.2);
2830
+ color: var(--text-muted);
2831
+ }
2832
+
2833
+ .template-item-actions {
2834
+ display: flex;
2835
+ gap: 0.35rem;
2836
+ }
2837
+
2838
+ .template-item-actions button {
2839
+ padding: 0.35rem 0.5rem;
2840
+ font-size: 0.7rem;
2841
+ border-radius: var(--radius-sm);
2842
+ border: 1px solid var(--border);
2843
+ background: var(--bg-primary);
2844
+ color: var(--text-secondary);
2845
+ cursor: pointer;
2846
+ transition: all 0.2s;
2847
+ }
2848
+
2849
+ .template-item-actions button:hover {
2850
+ background: var(--bg-secondary);
2851
+ color: var(--text);
2852
+ }
2853
+
2854
+ .template-item-actions button.use-btn {
2855
+ background: var(--accent);
2856
+ color: white;
2857
+ border-color: var(--accent);
2858
+ }
2859
+
2860
+ .template-item-actions button.use-btn:hover {
2861
+ background: var(--accent-hover, #3b82f6);
2862
+ }
2863
+
2864
+ .template-item-actions button.delete-btn:hover {
2865
+ border-color: var(--danger);
2866
+ color: var(--danger);
2867
+ }
2868
+
2869
+ /* History Modal */
2870
+ .history-filters {
2871
+ display: flex;
2872
+ gap: 0.75rem;
2873
+ margin-bottom: 1rem;
2874
+ }
2875
+
2876
+ .history-select {
2877
+ padding: 0.4rem 0.6rem;
2878
+ border-radius: 6px;
2879
+ border: 1px solid var(--border);
2880
+ background: var(--bg-secondary);
2881
+ color: var(--text);
2882
+ font-size: 0.85rem;
2883
+ cursor: pointer;
2884
+ }
2885
+
2886
+ .history-list {
2887
+ max-height: 400px;
2888
+ overflow-y: auto;
2889
+ }
2890
+
2891
+ .history-empty {
2892
+ text-align: center;
2893
+ color: var(--text-muted);
2894
+ padding: 2rem;
2895
+ font-size: 0.9rem;
2896
+ }
2897
+
2898
+ .history-item {
2899
+ display: flex;
2900
+ align-items: center;
2901
+ padding: 0.6rem 0.75rem;
2902
+ border-radius: var(--radius-sm);
2903
+ background: var(--bg-secondary);
2904
+ margin-bottom: 0.4rem;
2905
+ gap: 0.75rem;
2906
+ }
2907
+
2908
+ .history-item-time {
2909
+ font-size: 0.75rem;
2910
+ color: var(--text-muted);
2911
+ font-family: var(--font-mono);
2912
+ flex-shrink: 0;
2913
+ width: 85px;
2914
+ }
2915
+
2916
+ .history-item-process {
2917
+ font-weight: 600;
2918
+ color: var(--text-primary);
2919
+ flex: 1;
2920
+ min-width: 0;
2921
+ white-space: nowrap;
2922
+ overflow: hidden;
2923
+ text-overflow: ellipsis;
2924
+ }
2925
+
2926
+ .history-item-event {
2927
+ font-size: 0.75rem;
2928
+ padding: 0.2rem 0.5rem;
2929
+ border-radius: 999px;
2930
+ text-transform: uppercase;
2931
+ font-weight: 600;
2932
+ flex-shrink: 0;
2933
+ }
2934
+
2935
+ .history-item-event.start {
2936
+ background: rgba(34, 197, 94, 0.15);
2937
+ color: #22c55e;
2938
+ }
2939
+
2940
+ .history-item-event.stop {
2941
+ background: rgba(239, 68, 68, 0.15);
2942
+ color: #ef4444;
2943
+ }
2944
+
2945
+ .history-item-event.restart {
2946
+ background: rgba(251, 191, 36, 0.15);
2947
+ color: #fbbf24;
2948
+ }
2949
+
2950
+ .history-item-event.guard_on {
2951
+ background: rgba(59, 130, 246, 0.15);
2952
+ color: #3b82f6;
2953
+ }
2954
+
2955
+ .history-item-event.guard_off {
2956
+ background: rgba(139, 148, 158, 0.15);
2957
+ color: #8b949e;
2958
+ }
2959
+
2960
+ .history-item-pid {
2961
+ font-size: 0.7rem;
2962
+ color: var(--text-muted);
2963
+ font-family: var(--font-mono);
2964
+ flex-shrink: 0;
2965
+ }