bgrun 3.12.4 → 3.12.6
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.
|
@@ -1,39 +1,105 @@
|
|
|
1
1
|
import { getProcessHistory, getRecentHistory, addHistoryEntry } from '../../../../src/db';
|
|
2
2
|
|
|
3
|
+
function stringifyMetadata(metadata: unknown) {
|
|
4
|
+
try {
|
|
5
|
+
return JSON.stringify(metadata ?? {});
|
|
6
|
+
} catch {
|
|
7
|
+
return '{}';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function escapeCsv(value: unknown) {
|
|
12
|
+
const text = String(value ?? '');
|
|
13
|
+
if (/[",\n\r]/.test(text)) {
|
|
14
|
+
return `"${text.replace(/"/g, '""')}"`;
|
|
15
|
+
}
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildHistoryCsv(rows: Array<{
|
|
20
|
+
process_name: string;
|
|
21
|
+
event: string;
|
|
22
|
+
pid: number | null;
|
|
23
|
+
timestamp: string;
|
|
24
|
+
metadata: unknown;
|
|
25
|
+
}>) {
|
|
26
|
+
const header = ['process_name', 'event', 'pid', 'timestamp', 'metadata'];
|
|
27
|
+
const lines = rows.map((row) => [
|
|
28
|
+
row.process_name,
|
|
29
|
+
row.event,
|
|
30
|
+
row.pid ?? '',
|
|
31
|
+
row.timestamp,
|
|
32
|
+
stringifyMetadata(row.metadata),
|
|
33
|
+
].map(escapeCsv).join(','));
|
|
34
|
+
return [header.join(','), ...lines].join('\n');
|
|
35
|
+
}
|
|
36
|
+
|
|
3
37
|
export async function GET(req: Request) {
|
|
4
38
|
const url = new URL(req.url);
|
|
5
39
|
const name = url.searchParams.get('name');
|
|
40
|
+
const event = (url.searchParams.get('event') || '').trim().toLowerCase();
|
|
41
|
+
const metadataFilter = (url.searchParams.get('metadata') || '')
|
|
42
|
+
.split(',')
|
|
43
|
+
.map((value) => value.toLowerCase().trim())
|
|
44
|
+
.filter(Boolean);
|
|
6
45
|
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
7
|
-
|
|
46
|
+
const format = (url.searchParams.get('format') || 'json').toLowerCase();
|
|
47
|
+
const download = url.searchParams.get('download') === '1';
|
|
48
|
+
|
|
8
49
|
let history;
|
|
9
50
|
if (name) {
|
|
10
51
|
history = getProcessHistory(name, limit);
|
|
11
52
|
} else {
|
|
12
53
|
history = getRecentHistory(limit);
|
|
13
54
|
}
|
|
14
|
-
|
|
15
|
-
|
|
55
|
+
|
|
56
|
+
let rows = history.map((h: any) => ({
|
|
16
57
|
process_name: h.process_name,
|
|
17
58
|
event: h.event,
|
|
18
59
|
pid: h.pid,
|
|
19
60
|
timestamp: h.timestamp,
|
|
20
61
|
metadata: h.metadata ? JSON.parse(h.metadata) : {},
|
|
21
|
-
}))
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
if (event) {
|
|
65
|
+
rows = rows.filter((row) => row.event.toLowerCase() === event);
|
|
66
|
+
}
|
|
67
|
+
if (metadataFilter.length > 0) {
|
|
68
|
+
rows = rows.filter((row) => {
|
|
69
|
+
const haystack = stringifyMetadata(row.metadata).toLowerCase();
|
|
70
|
+
return metadataFilter.every((term) => haystack.includes(term));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (format === 'csv') {
|
|
75
|
+
return new Response(buildHistoryCsv(rows), {
|
|
76
|
+
headers: {
|
|
77
|
+
'content-type': 'text/csv; charset=utf-8',
|
|
78
|
+
...(download ? { 'content-disposition': `attachment; filename="bgr-history${name ? `-${encodeURIComponent(name)}` : ''}.csv"` } : {}),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return Response.json(rows, {
|
|
84
|
+
headers: download
|
|
85
|
+
? { 'content-disposition': `attachment; filename="bgr-history${name ? `-${encodeURIComponent(name)}` : ''}.json"` }
|
|
86
|
+
: undefined,
|
|
87
|
+
});
|
|
22
88
|
}
|
|
23
89
|
|
|
24
90
|
export async function POST(req: Request) {
|
|
25
91
|
try {
|
|
26
92
|
const body = await req.json();
|
|
27
93
|
const { process_name, event, pid, metadata } = body;
|
|
28
|
-
|
|
94
|
+
|
|
29
95
|
if (!process_name || !event) {
|
|
30
96
|
return Response.json({ error: 'process_name and event are required' }, { status: 400 });
|
|
31
97
|
}
|
|
32
|
-
|
|
98
|
+
|
|
33
99
|
addHistoryEntry(process_name, event, pid, metadata);
|
|
34
100
|
return Response.json({ success: true });
|
|
35
101
|
} catch (err) {
|
|
36
102
|
console.error('[api/history] Error adding history:', err);
|
|
37
103
|
return Response.json({ error: 'Failed to add history' }, { status: 500 });
|
|
38
104
|
}
|
|
39
|
-
}
|
|
105
|
+
}
|
|
@@ -1,67 +1,100 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GET /api/logs/:name — Read process stdout/stderr logs
|
|
3
|
-
*
|
|
4
|
-
* Supports incremental loading via query params:
|
|
5
|
-
* ?tab=stdout|stderr — which log to read (default: stdout)
|
|
6
|
-
* ?offset=N — byte offset to start reading from (default: 0 = full file)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
1
|
+
/**
|
|
2
|
+
* GET /api/logs/:name — Read process stdout/stderr logs
|
|
3
|
+
*
|
|
4
|
+
* Supports incremental loading via query params:
|
|
5
|
+
* ?tab=stdout|stderr — which log to read (default: stdout)
|
|
6
|
+
* ?offset=N — byte offset to start reading from (default: 0 = full file)
|
|
7
|
+
* ?format=json|text|csv — response format (default: json)
|
|
8
|
+
*
|
|
9
|
+
* Returns JSON by default:
|
|
10
|
+
* { text, size, mtime, filePath }
|
|
11
|
+
*/
|
|
12
|
+
import { getProcess } from '../../../../../src/db';
|
|
13
|
+
import { stat, open } from 'fs/promises';
|
|
14
|
+
|
|
15
|
+
interface FileInfo {
|
|
16
|
+
text: string;
|
|
17
|
+
size: number;
|
|
18
|
+
mtime: string | null;
|
|
19
|
+
filePath: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function escapeCsv(value: unknown) {
|
|
23
|
+
const text = String(value ?? '');
|
|
24
|
+
if (/[",\n\r]/.test(text)) {
|
|
25
|
+
return `"${text.replace(/"/g, '""')}"`;
|
|
26
|
+
}
|
|
27
|
+
return text;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildLogCsv(text: string) {
|
|
31
|
+
const header = 'line,text';
|
|
32
|
+
const lines = text.split('\n').map((line, index) => `${index + 1},${escapeCsv(line)}`);
|
|
33
|
+
return [header, ...lines].join('\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readLogFile(path: string, offset: number): Promise<FileInfo> {
|
|
37
|
+
try {
|
|
38
|
+
const s = await stat(path);
|
|
39
|
+
const size = s.size;
|
|
40
|
+
const mtime = s.mtime.toISOString();
|
|
41
|
+
|
|
42
|
+
if (offset >= size) {
|
|
43
|
+
return { text: '', size, mtime, filePath: path };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const handle = await open(path, 'r');
|
|
47
|
+
try {
|
|
48
|
+
const bytesToRead = size - offset;
|
|
49
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
50
|
+
await handle.read(buffer, 0, bytesToRead, offset);
|
|
51
|
+
return { text: buffer.toString('utf-8'), size, mtime, filePath: path };
|
|
52
|
+
} finally {
|
|
53
|
+
await handle.close();
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
return { text: '', size: 0, mtime: null, filePath: path };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function GET(req: Request, { params }: { params: { name: string } }) {
|
|
61
|
+
const name = decodeURIComponent(params.name);
|
|
62
|
+
const proc = getProcess(name);
|
|
63
|
+
|
|
64
|
+
if (!proc) {
|
|
65
|
+
return Response.json({ error: 'Process not found' }, { status: 404 });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const url = new URL(req.url);
|
|
69
|
+
const tab = url.searchParams.get('tab') || 'stdout';
|
|
70
|
+
const offset = parseInt(url.searchParams.get('offset') || '0', 10) || 0;
|
|
71
|
+
const format = (url.searchParams.get('format') || 'json').toLowerCase();
|
|
72
|
+
const download = url.searchParams.get('download') === '1';
|
|
73
|
+
|
|
74
|
+
const path = tab === 'stderr' ? proc.stderr_path : proc.stdout_path;
|
|
75
|
+
const info = await readLogFile(path, offset);
|
|
76
|
+
|
|
77
|
+
if (format === 'text') {
|
|
78
|
+
return new Response(info.text, {
|
|
79
|
+
headers: {
|
|
80
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
81
|
+
...(download ? { 'content-disposition': `attachment; filename="${encodeURIComponent(name)}-${tab}.log"` } : {}),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (format === 'csv') {
|
|
87
|
+
return new Response(buildLogCsv(info.text), {
|
|
88
|
+
headers: {
|
|
89
|
+
'content-type': 'text/csv; charset=utf-8',
|
|
90
|
+
...(download ? { 'content-disposition': `attachment; filename="${encodeURIComponent(name)}-${tab}.csv"` } : {}),
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return Response.json(info, {
|
|
96
|
+
headers: download
|
|
97
|
+
? { 'content-disposition': `attachment; filename="${encodeURIComponent(name)}-${tab}.json"` }
|
|
98
|
+
: undefined,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -2463,6 +2463,14 @@ a.port-link:hover {
|
|
|
2463
2463
|
border-bottom: 1px solid var(--border-subtle);
|
|
2464
2464
|
background: rgba(0, 0, 0, 0.08);
|
|
2465
2465
|
flex-shrink: 0;
|
|
2466
|
+
flex-wrap: wrap;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
.toolbar-export-actions {
|
|
2470
|
+
display: inline-flex;
|
|
2471
|
+
align-items: center;
|
|
2472
|
+
gap: 0.5rem;
|
|
2473
|
+
flex-wrap: wrap;
|
|
2466
2474
|
}
|
|
2467
2475
|
|
|
2468
2476
|
.log-search {
|
|
@@ -4000,171 +4008,171 @@ tr.keyboard-focus td:first-child .process-name span {
|
|
|
4000
4008
|
opacity: 0.6;
|
|
4001
4009
|
cursor: wait;
|
|
4002
4010
|
}
|
|
4003
|
-
|
|
4004
|
-
/* ─── Dependencies Graph ─── */
|
|
4005
|
-
|
|
4006
|
-
.deps-controls {
|
|
4007
|
-
display: flex;
|
|
4008
|
-
align-items: center;
|
|
4009
|
-
gap: 0.75rem;
|
|
4010
|
-
margin-bottom: 1.25rem;
|
|
4011
|
-
flex-wrap: wrap;
|
|
4012
|
-
}
|
|
4013
|
-
|
|
4014
|
-
.deps-arrow {
|
|
4015
|
-
color: var(--text-secondary);
|
|
4016
|
-
font-size: 0.85rem;
|
|
4017
|
-
white-space: nowrap;
|
|
4018
|
-
}
|
|
4019
|
-
|
|
4020
|
-
.deps-graph-container {
|
|
4021
|
-
background: rgba(0, 0, 0, 0.15);
|
|
4022
|
-
border-radius: 12px;
|
|
4023
|
-
border: 1px solid var(--border);
|
|
4024
|
-
margin-bottom: 1.25rem;
|
|
4025
|
-
overflow: hidden;
|
|
4026
|
-
min-height: 300px;
|
|
4027
|
-
position: relative;
|
|
4028
|
-
}
|
|
4029
|
-
|
|
4030
|
-
.deps-graph-container svg {
|
|
4031
|
-
display: block;
|
|
4032
|
-
}
|
|
4033
|
-
|
|
4034
|
-
.deps-node {
|
|
4035
|
-
cursor: pointer;
|
|
4036
|
-
}
|
|
4037
|
-
|
|
4038
|
-
.deps-node rect {
|
|
4039
|
-
rx: 8;
|
|
4040
|
-
ry: 8;
|
|
4041
|
-
stroke-width: 2;
|
|
4042
|
-
transition: filter 0.15s;
|
|
4043
|
-
}
|
|
4044
|
-
|
|
4045
|
-
.deps-node:hover rect {
|
|
4046
|
-
filter: brightness(1.3);
|
|
4047
|
-
}
|
|
4048
|
-
|
|
4049
|
-
.deps-node text {
|
|
4050
|
-
font-family: var(--font-mono);
|
|
4051
|
-
font-size: 12px;
|
|
4052
|
-
fill: var(--text-primary);
|
|
4053
|
-
pointer-events: none;
|
|
4054
|
-
}
|
|
4055
|
-
|
|
4056
|
-
.deps-edge {
|
|
4057
|
-
stroke: var(--text-secondary);
|
|
4058
|
-
stroke-width: 1.5;
|
|
4059
|
-
fill: none;
|
|
4060
|
-
opacity: 0.6;
|
|
4061
|
-
marker-end: url(#deps-arrowhead);
|
|
4062
|
-
}
|
|
4063
|
-
|
|
4064
|
-
.deps-edge-highlight {
|
|
4065
|
-
stroke: var(--accent);
|
|
4066
|
-
opacity: 1;
|
|
4067
|
-
stroke-width: 2;
|
|
4068
|
-
}
|
|
4069
|
-
|
|
4070
|
-
.deps-list {
|
|
4071
|
-
margin-bottom: 1rem;
|
|
4072
|
-
}
|
|
4073
|
-
|
|
4074
|
-
.deps-list-item {
|
|
4075
|
-
display: flex;
|
|
4076
|
-
align-items: center;
|
|
4077
|
-
gap: 0.5rem;
|
|
4078
|
-
padding: 0.5rem 0.75rem;
|
|
4079
|
-
border-bottom: 1px solid var(--border);
|
|
4080
|
-
font-size: 0.85rem;
|
|
4081
|
-
}
|
|
4082
|
-
|
|
4083
|
-
.deps-list-item:last-child {
|
|
4084
|
-
border-bottom: none;
|
|
4085
|
-
}
|
|
4086
|
-
|
|
4087
|
-
.deps-list-item .deps-item-process {
|
|
4088
|
-
font-weight: 600;
|
|
4089
|
-
color: var(--accent);
|
|
4090
|
-
font-family: var(--font-mono);
|
|
4091
|
-
}
|
|
4092
|
-
|
|
4093
|
-
.deps-list-item .deps-item-arrow {
|
|
4094
|
-
color: var(--text-secondary);
|
|
4095
|
-
}
|
|
4096
|
-
|
|
4097
|
-
.deps-list-item .deps-item-target {
|
|
4098
|
-
font-family: var(--font-mono);
|
|
4099
|
-
color: var(--text-primary);
|
|
4100
|
-
}
|
|
4101
|
-
|
|
4102
|
-
.deps-list-item .deps-remove-btn {
|
|
4103
|
-
margin-left: auto;
|
|
4104
|
-
background: none;
|
|
4105
|
-
border: none;
|
|
4106
|
-
color: var(--red);
|
|
4107
|
-
cursor: pointer;
|
|
4108
|
-
font-size: 1rem;
|
|
4109
|
-
padding: 0 0.25rem;
|
|
4110
|
-
opacity: 0.6;
|
|
4111
|
-
transition: opacity 0.15s;
|
|
4112
|
-
}
|
|
4113
|
-
|
|
4114
|
-
.deps-list-item .deps-remove-btn:hover {
|
|
4115
|
-
opacity: 1;
|
|
4116
|
-
}
|
|
4117
|
-
|
|
4118
|
-
.deps-start-order {
|
|
4119
|
-
padding: 0.75rem;
|
|
4120
|
-
background: rgba(0, 0, 0, 0.1);
|
|
4121
|
-
border-radius: 8px;
|
|
4122
|
-
font-size: 0.8rem;
|
|
4123
|
-
color: var(--text-secondary);
|
|
4124
|
-
}
|
|
4125
|
-
|
|
4126
|
-
.deps-start-order .deps-order-title {
|
|
4127
|
-
font-weight: 600;
|
|
4128
|
-
color: var(--text-primary);
|
|
4129
|
-
margin-bottom: 0.35rem;
|
|
4130
|
-
}
|
|
4131
|
-
|
|
4132
|
-
.deps-start-order .deps-order-list {
|
|
4133
|
-
display: flex;
|
|
4134
|
-
flex-wrap: wrap;
|
|
4135
|
-
gap: 0.35rem;
|
|
4136
|
-
}
|
|
4137
|
-
|
|
4138
|
-
.deps-order-badge {
|
|
4139
|
-
display: inline-flex;
|
|
4140
|
-
align-items: center;
|
|
4141
|
-
gap: 0.3rem;
|
|
4142
|
-
padding: 0.2rem 0.5rem;
|
|
4143
|
-
background: rgba(var(--accent-rgb, 99, 102, 241), 0.15);
|
|
4144
|
-
border-radius: 6px;
|
|
4145
|
-
font-family: var(--font-mono);
|
|
4146
|
-
font-size: 0.75rem;
|
|
4147
|
-
}
|
|
4148
|
-
|
|
4149
|
-
.deps-order-badge .deps-order-num {
|
|
4150
|
-
color: var(--accent);
|
|
4151
|
-
font-weight: 700;
|
|
4152
|
-
font-size: 0.7rem;
|
|
4153
|
-
}
|
|
4154
|
-
|
|
4155
|
-
.deps-empty {
|
|
4156
|
-
text-align: center;
|
|
4157
|
-
padding: 2rem;
|
|
4158
|
-
color: var(--text-secondary);
|
|
4159
|
-
font-size: 0.9rem;
|
|
4160
|
-
}
|
|
4161
|
-
|
|
4162
|
-
@media (max-width: 768px) {
|
|
4163
|
-
.deps-controls {
|
|
4164
|
-
flex-direction: column;
|
|
4165
|
-
align-items: stretch;
|
|
4166
|
-
}
|
|
4167
|
-
.deps-arrow {
|
|
4168
|
-
text-align: center;
|
|
4169
|
-
}
|
|
4170
|
-
}
|
|
4011
|
+
|
|
4012
|
+
/* ─── Dependencies Graph ─── */
|
|
4013
|
+
|
|
4014
|
+
.deps-controls {
|
|
4015
|
+
display: flex;
|
|
4016
|
+
align-items: center;
|
|
4017
|
+
gap: 0.75rem;
|
|
4018
|
+
margin-bottom: 1.25rem;
|
|
4019
|
+
flex-wrap: wrap;
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
.deps-arrow {
|
|
4023
|
+
color: var(--text-secondary);
|
|
4024
|
+
font-size: 0.85rem;
|
|
4025
|
+
white-space: nowrap;
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
.deps-graph-container {
|
|
4029
|
+
background: rgba(0, 0, 0, 0.15);
|
|
4030
|
+
border-radius: 12px;
|
|
4031
|
+
border: 1px solid var(--border);
|
|
4032
|
+
margin-bottom: 1.25rem;
|
|
4033
|
+
overflow: hidden;
|
|
4034
|
+
min-height: 300px;
|
|
4035
|
+
position: relative;
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
.deps-graph-container svg {
|
|
4039
|
+
display: block;
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
.deps-node {
|
|
4043
|
+
cursor: pointer;
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
.deps-node rect {
|
|
4047
|
+
rx: 8;
|
|
4048
|
+
ry: 8;
|
|
4049
|
+
stroke-width: 2;
|
|
4050
|
+
transition: filter 0.15s;
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
.deps-node:hover rect {
|
|
4054
|
+
filter: brightness(1.3);
|
|
4055
|
+
}
|
|
4056
|
+
|
|
4057
|
+
.deps-node text {
|
|
4058
|
+
font-family: var(--font-mono);
|
|
4059
|
+
font-size: 12px;
|
|
4060
|
+
fill: var(--text-primary);
|
|
4061
|
+
pointer-events: none;
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4064
|
+
.deps-edge {
|
|
4065
|
+
stroke: var(--text-secondary);
|
|
4066
|
+
stroke-width: 1.5;
|
|
4067
|
+
fill: none;
|
|
4068
|
+
opacity: 0.6;
|
|
4069
|
+
marker-end: url(#deps-arrowhead);
|
|
4070
|
+
}
|
|
4071
|
+
|
|
4072
|
+
.deps-edge-highlight {
|
|
4073
|
+
stroke: var(--accent);
|
|
4074
|
+
opacity: 1;
|
|
4075
|
+
stroke-width: 2;
|
|
4076
|
+
}
|
|
4077
|
+
|
|
4078
|
+
.deps-list {
|
|
4079
|
+
margin-bottom: 1rem;
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
.deps-list-item {
|
|
4083
|
+
display: flex;
|
|
4084
|
+
align-items: center;
|
|
4085
|
+
gap: 0.5rem;
|
|
4086
|
+
padding: 0.5rem 0.75rem;
|
|
4087
|
+
border-bottom: 1px solid var(--border);
|
|
4088
|
+
font-size: 0.85rem;
|
|
4089
|
+
}
|
|
4090
|
+
|
|
4091
|
+
.deps-list-item:last-child {
|
|
4092
|
+
border-bottom: none;
|
|
4093
|
+
}
|
|
4094
|
+
|
|
4095
|
+
.deps-list-item .deps-item-process {
|
|
4096
|
+
font-weight: 600;
|
|
4097
|
+
color: var(--accent);
|
|
4098
|
+
font-family: var(--font-mono);
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
.deps-list-item .deps-item-arrow {
|
|
4102
|
+
color: var(--text-secondary);
|
|
4103
|
+
}
|
|
4104
|
+
|
|
4105
|
+
.deps-list-item .deps-item-target {
|
|
4106
|
+
font-family: var(--font-mono);
|
|
4107
|
+
color: var(--text-primary);
|
|
4108
|
+
}
|
|
4109
|
+
|
|
4110
|
+
.deps-list-item .deps-remove-btn {
|
|
4111
|
+
margin-left: auto;
|
|
4112
|
+
background: none;
|
|
4113
|
+
border: none;
|
|
4114
|
+
color: var(--red);
|
|
4115
|
+
cursor: pointer;
|
|
4116
|
+
font-size: 1rem;
|
|
4117
|
+
padding: 0 0.25rem;
|
|
4118
|
+
opacity: 0.6;
|
|
4119
|
+
transition: opacity 0.15s;
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
.deps-list-item .deps-remove-btn:hover {
|
|
4123
|
+
opacity: 1;
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
.deps-start-order {
|
|
4127
|
+
padding: 0.75rem;
|
|
4128
|
+
background: rgba(0, 0, 0, 0.1);
|
|
4129
|
+
border-radius: 8px;
|
|
4130
|
+
font-size: 0.8rem;
|
|
4131
|
+
color: var(--text-secondary);
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
.deps-start-order .deps-order-title {
|
|
4135
|
+
font-weight: 600;
|
|
4136
|
+
color: var(--text-primary);
|
|
4137
|
+
margin-bottom: 0.35rem;
|
|
4138
|
+
}
|
|
4139
|
+
|
|
4140
|
+
.deps-start-order .deps-order-list {
|
|
4141
|
+
display: flex;
|
|
4142
|
+
flex-wrap: wrap;
|
|
4143
|
+
gap: 0.35rem;
|
|
4144
|
+
}
|
|
4145
|
+
|
|
4146
|
+
.deps-order-badge {
|
|
4147
|
+
display: inline-flex;
|
|
4148
|
+
align-items: center;
|
|
4149
|
+
gap: 0.3rem;
|
|
4150
|
+
padding: 0.2rem 0.5rem;
|
|
4151
|
+
background: rgba(var(--accent-rgb, 99, 102, 241), 0.15);
|
|
4152
|
+
border-radius: 6px;
|
|
4153
|
+
font-family: var(--font-mono);
|
|
4154
|
+
font-size: 0.75rem;
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
.deps-order-badge .deps-order-num {
|
|
4158
|
+
color: var(--accent);
|
|
4159
|
+
font-weight: 700;
|
|
4160
|
+
font-size: 0.7rem;
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
.deps-empty {
|
|
4164
|
+
text-align: center;
|
|
4165
|
+
padding: 2rem;
|
|
4166
|
+
color: var(--text-secondary);
|
|
4167
|
+
font-size: 0.9rem;
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
@media (max-width: 768px) {
|
|
4171
|
+
.deps-controls {
|
|
4172
|
+
flex-direction: column;
|
|
4173
|
+
align-items: stretch;
|
|
4174
|
+
}
|
|
4175
|
+
.deps-arrow {
|
|
4176
|
+
text-align: center;
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
@@ -1292,6 +1292,23 @@ export default function mount(): () => void {
|
|
|
1292
1292
|
else renderEnvPanel();
|
|
1293
1293
|
}
|
|
1294
1294
|
|
|
1295
|
+
function updateLogExportLinks() {
|
|
1296
|
+
const textBtn = $('log-export-text-btn') as HTMLAnchorElement | null;
|
|
1297
|
+
const jsonBtn = $('log-export-json-btn') as HTMLAnchorElement | null;
|
|
1298
|
+
const csvBtn = $('log-export-csv-btn') as HTMLAnchorElement | null;
|
|
1299
|
+
const disabledHref = '#';
|
|
1300
|
+
if (!drawerProcess) {
|
|
1301
|
+
if (textBtn) textBtn.href = disabledHref;
|
|
1302
|
+
if (jsonBtn) jsonBtn.href = disabledHref;
|
|
1303
|
+
if (csvBtn) csvBtn.href = disabledHref;
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
const base = `/api/logs/${encodeURIComponent(drawerProcess)}?tab=${drawerTab}&offset=0&download=1`;
|
|
1307
|
+
if (textBtn) textBtn.href = `${base}&format=text`;
|
|
1308
|
+
if (jsonBtn) jsonBtn.href = `${base}&format=json`;
|
|
1309
|
+
if (csvBtn) csvBtn.href = `${base}&format=csv`;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1295
1312
|
function switchLogSubtab(subtab: string, skipRefresh = false) {
|
|
1296
1313
|
drawerTab = subtab as 'stdout' | 'stderr';
|
|
1297
1314
|
$('log-subtabs')?.querySelectorAll('.accordion-subtab').forEach(btn => {
|
|
@@ -1305,6 +1322,7 @@ export default function mount(): () => void {
|
|
|
1305
1322
|
logNeedsFullRebuild = true;
|
|
1306
1323
|
logVirtualActive = false;
|
|
1307
1324
|
logFilteredIndices = [];
|
|
1325
|
+
updateLogExportLinks();
|
|
1308
1326
|
if (!skipRefresh) refreshDrawerLogs();
|
|
1309
1327
|
}
|
|
1310
1328
|
|
|
@@ -1354,6 +1372,7 @@ export default function mount(): () => void {
|
|
|
1354
1372
|
function openDrawer(name: string) {
|
|
1355
1373
|
drawerProcess = name;
|
|
1356
1374
|
drawerTab = 'stdout';
|
|
1375
|
+
updateLogExportLinks();
|
|
1357
1376
|
|
|
1358
1377
|
// Update header
|
|
1359
1378
|
const nameEl = $('drawer-process-name');
|
|
@@ -1501,6 +1520,7 @@ export default function mount(): () => void {
|
|
|
1501
1520
|
drawer?.classList.remove('open');
|
|
1502
1521
|
backdrop?.classList.remove('active');
|
|
1503
1522
|
drawerProcess = null;
|
|
1523
|
+
updateLogExportLinks();
|
|
1504
1524
|
tbody?.querySelectorAll('tr.selected').forEach(r => r.classList.remove('selected'));
|
|
1505
1525
|
}
|
|
1506
1526
|
|
|
@@ -2559,6 +2579,21 @@ export default function mount(): () => void {
|
|
|
2559
2579
|
}
|
|
2560
2580
|
}
|
|
2561
2581
|
|
|
2582
|
+
function updateHistoryExportLinks() {
|
|
2583
|
+
const processFilter = $('history-process-filter') as HTMLSelectElement | null;
|
|
2584
|
+
const eventFilter = $('history-event-filter') as HTMLSelectElement | null;
|
|
2585
|
+
const metadataFilter = $('history-metadata-filter') as HTMLInputElement | null;
|
|
2586
|
+
const params = new URLSearchParams({ limit: '100', download: '1' });
|
|
2587
|
+
if (processFilter?.value) params.set('name', processFilter.value);
|
|
2588
|
+
if (eventFilter?.value) params.set('event', eventFilter.value);
|
|
2589
|
+
if (metadataFilter?.value.trim()) params.set('metadata', metadataFilter.value.trim());
|
|
2590
|
+
|
|
2591
|
+
const jsonBtn = $('history-export-json-btn') as HTMLAnchorElement | null;
|
|
2592
|
+
const csvBtn = $('history-export-csv-btn') as HTMLAnchorElement | null;
|
|
2593
|
+
if (jsonBtn) jsonBtn.href = `/api/history?${params.toString()}&format=json`;
|
|
2594
|
+
if (csvBtn) csvBtn.href = `/api/history?${params.toString()}&format=csv`;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2562
2597
|
function updateHistoryFilters() {
|
|
2563
2598
|
const processFilter = $('history-process-filter') as HTMLSelectElement;
|
|
2564
2599
|
const eventFilter = $('history-event-filter') as HTMLSelectElement;
|
|
@@ -2827,6 +2862,7 @@ export default function mount(): () => void {
|
|
|
2827
2862
|
}
|
|
2828
2863
|
|
|
2829
2864
|
updateHistoryClearButton();
|
|
2865
|
+
updateHistoryExportLinks();
|
|
2830
2866
|
applyHistoryDensity();
|
|
2831
2867
|
applyHistoryShortcutPreference();
|
|
2832
2868
|
applyHistoryDetailsPreference();
|
package/dashboard/app/page.tsx
CHANGED
|
@@ -266,6 +266,11 @@ export default function DashboardPage() {
|
|
|
266
266
|
<div className="drawer-log-toolbar" id="drawer-log-toolbar">
|
|
267
267
|
<input type="text" id="log-search" className="log-search" placeholder="Filter logs..." />
|
|
268
268
|
<span className="log-line-count" id="log-line-count"></span>
|
|
269
|
+
<div className="toolbar-export-actions">
|
|
270
|
+
<a id="log-export-text-btn" className="btn btn-ghost btn-sm" href="#" target="_blank" rel="noopener">Export .log</a>
|
|
271
|
+
<a id="log-export-json-btn" className="btn btn-ghost btn-sm" href="#" target="_blank" rel="noopener">Export JSON</a>
|
|
272
|
+
<a id="log-export-csv-btn" className="btn btn-ghost btn-sm" href="#" target="_blank" rel="noopener">Export CSV</a>
|
|
273
|
+
</div>
|
|
269
274
|
<button id="log-autoscroll-btn" className="log-autoscroll" title="Auto-scroll: OFF">
|
|
270
275
|
<svg viewBox="0 0 24 24"><path d="M12 5v14M5 12l7 7 7-7" /></svg>
|
|
271
276
|
Follow
|
|
@@ -387,6 +392,10 @@ export default function DashboardPage() {
|
|
|
387
392
|
</select>
|
|
388
393
|
</label>
|
|
389
394
|
<button className="btn btn-ghost btn-sm" id="history-clear-filters-btn" title="Clear all history filters">Clear</button>
|
|
395
|
+
<div className="toolbar-export-actions">
|
|
396
|
+
<a id="history-export-json-btn" className="btn btn-ghost btn-sm" href="#" target="_blank" rel="noopener">Export JSON</a>
|
|
397
|
+
<a id="history-export-csv-btn" className="btn btn-ghost btn-sm" href="#" target="_blank" rel="noopener">Export CSV</a>
|
|
398
|
+
</div>
|
|
390
399
|
</div>
|
|
391
400
|
<div className="history-hints-bar">
|
|
392
401
|
<div className="history-hints-bar-left">
|