buddy-workbench 0.1.9 → 0.1.11
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/package.json +2 -1
- package/server/config.js +4 -0
- package/server/repositories/errors.js +44 -0
- package/server/repositories/jira-filters.js +16 -0
- package/server/repositories/settings.js +30 -0
- package/server/repositories/static-pages.js +29 -0
- package/server/repositories/todos.js +16 -0
- package/server/routes/clipboard.js +11 -2
- package/server/routes/errors.js +31 -0
- package/server/routes/jira-filters.js +161 -0
- package/server/routes/pr-review.js +41 -36
- package/server/routes/settings.js +23 -1
- package/server/routes/static-pages.js +82 -0
- package/server/routes/todos.js +177 -0
- package/server/services/browser.js +28 -0
- package/server/services/clipboard-history.js +71 -5
- package/server/services/process-manager.js +8 -0
- package/server.js +58 -2
- package/ui/dist/assets/index-BuBK4DeN.css +1 -0
- package/ui/dist/assets/index-CdpFZDzz.js +521 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-BHs_wPig.css +0 -1
- package/ui/dist/assets/index-ntRr7eTI.js +0 -500
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { listStaticPages, saveStaticPages } from '../repositories/static-pages.js';
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
|
|
6
|
+
// List static pages
|
|
7
|
+
router.get('/', (_req, res) => {
|
|
8
|
+
res.json(listStaticPages());
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Save all static pages
|
|
12
|
+
router.put('/', (req, res) => {
|
|
13
|
+
if (!Array.isArray(req.body)) {
|
|
14
|
+
return res.status(400).json({ error: 'Expected an array of static pages.' });
|
|
15
|
+
}
|
|
16
|
+
saveStaticPages(req.body);
|
|
17
|
+
res.json(listStaticPages());
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// Create single static page
|
|
21
|
+
router.post('/', (req, res) => {
|
|
22
|
+
const { name, url } = req.body || {};
|
|
23
|
+
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
24
|
+
return res.status(400).json({ error: 'Page name is required.' });
|
|
25
|
+
}
|
|
26
|
+
if (!url || typeof url !== 'string' || !url.trim()) {
|
|
27
|
+
return res.status(400).json({ error: 'URL is required.' });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const pages = listStaticPages();
|
|
31
|
+
const newPage = {
|
|
32
|
+
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
33
|
+
name: name.trim(),
|
|
34
|
+
url: url.trim(),
|
|
35
|
+
icon: 'GlobalOutlined'
|
|
36
|
+
};
|
|
37
|
+
pages.push(newPage);
|
|
38
|
+
saveStaticPages(pages);
|
|
39
|
+
res.status(201).json(newPage);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Update single static page
|
|
43
|
+
router.put('/:id', (req, res) => {
|
|
44
|
+
const { id } = req.params;
|
|
45
|
+
const { name, url } = req.body || {};
|
|
46
|
+
const pages = listStaticPages();
|
|
47
|
+
const index = pages.findIndex((p) => String(p.id) === String(id));
|
|
48
|
+
if (index === -1) {
|
|
49
|
+
return res.status(404).json({ error: 'Static page not found.' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (name !== undefined) {
|
|
53
|
+
if (typeof name !== 'string' || !name.trim()) {
|
|
54
|
+
return res.status(400).json({ error: 'Page name cannot be empty.' });
|
|
55
|
+
}
|
|
56
|
+
pages[index].name = name.trim();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (url !== undefined) {
|
|
60
|
+
if (typeof url !== 'string' || !url.trim()) {
|
|
61
|
+
return res.status(400).json({ error: 'URL cannot be empty.' });
|
|
62
|
+
}
|
|
63
|
+
pages[index].url = url.trim();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
saveStaticPages(pages);
|
|
67
|
+
res.json(pages[index]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Delete static page
|
|
71
|
+
router.delete('/:id', (req, res) => {
|
|
72
|
+
const { id } = req.params;
|
|
73
|
+
const pages = listStaticPages();
|
|
74
|
+
const filtered = pages.filter((p) => String(p.id) !== String(id));
|
|
75
|
+
if (filtered.length === pages.length) {
|
|
76
|
+
return res.status(404).json({ error: 'Static page not found.' });
|
|
77
|
+
}
|
|
78
|
+
saveStaticPages(filtered);
|
|
79
|
+
res.status(204).end();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export default router;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { listTodos, saveTodos } from '../repositories/todos.js';
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
|
|
6
|
+
function validTodo(body) {
|
|
7
|
+
const title = String(body?.title || '').trim();
|
|
8
|
+
if (!title) return null;
|
|
9
|
+
|
|
10
|
+
const priority = ['high', 'medium', 'low'].includes(body?.priority) ? body.priority : 'medium';
|
|
11
|
+
const category = String(body?.category || 'work').trim() || 'work';
|
|
12
|
+
const description = String(body?.description || '').trim();
|
|
13
|
+
const dueDate = body?.dueDate ? String(body.dueDate).trim() : null;
|
|
14
|
+
const completed = Boolean(body?.completed);
|
|
15
|
+
const archived = Boolean(body?.archived);
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
title,
|
|
19
|
+
description,
|
|
20
|
+
priority,
|
|
21
|
+
category,
|
|
22
|
+
dueDate,
|
|
23
|
+
completed,
|
|
24
|
+
archived
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
router.get('/', (_req, res) => {
|
|
29
|
+
res.json(listTodos());
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
router.post('/', (req, res) => {
|
|
33
|
+
const data = validTodo(req.body);
|
|
34
|
+
if (!data) {
|
|
35
|
+
return res.status(400).json({ error: 'Title is required.' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const now = new Date().toISOString();
|
|
39
|
+
const newTodo = {
|
|
40
|
+
id: crypto.randomUUID(),
|
|
41
|
+
...data,
|
|
42
|
+
completedAt: data.completed ? now : null,
|
|
43
|
+
archivedAt: data.archived ? now : null,
|
|
44
|
+
createdAt: now,
|
|
45
|
+
updatedAt: now
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const todos = listTodos();
|
|
49
|
+
todos.unshift(newTodo);
|
|
50
|
+
saveTodos(todos);
|
|
51
|
+
|
|
52
|
+
res.status(201).json(newTodo);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
router.put('/:id', (req, res) => {
|
|
56
|
+
const { id } = req.params;
|
|
57
|
+
const todos = listTodos();
|
|
58
|
+
const index = todos.findIndex((item) => item.id === id);
|
|
59
|
+
|
|
60
|
+
if (index < 0) {
|
|
61
|
+
return res.status(404).json({ error: 'Todo item not found.' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const data = validTodo(req.body);
|
|
65
|
+
if (!data) {
|
|
66
|
+
return res.status(400).json({ error: 'Title is required.' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const existing = todos[index];
|
|
70
|
+
const now = new Date().toISOString();
|
|
71
|
+
const completedAt = data.completed
|
|
72
|
+
? (existing.completed ? existing.completedAt : now)
|
|
73
|
+
: null;
|
|
74
|
+
const archivedAt = data.archived
|
|
75
|
+
? (existing.archived ? existing.archivedAt : now)
|
|
76
|
+
: null;
|
|
77
|
+
|
|
78
|
+
const updated = {
|
|
79
|
+
...existing,
|
|
80
|
+
...data,
|
|
81
|
+
completedAt,
|
|
82
|
+
archivedAt,
|
|
83
|
+
updatedAt: now
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
todos[index] = updated;
|
|
87
|
+
saveTodos(todos);
|
|
88
|
+
|
|
89
|
+
res.json(updated);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
router.post('/:id/archive', (req, res) => {
|
|
93
|
+
const { id } = req.params;
|
|
94
|
+
const todos = listTodos();
|
|
95
|
+
const index = todos.findIndex((item) => item.id === id);
|
|
96
|
+
|
|
97
|
+
if (index < 0) {
|
|
98
|
+
return res.status(404).json({ error: 'Todo item not found.' });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const now = new Date().toISOString();
|
|
102
|
+
const updated = {
|
|
103
|
+
...todos[index],
|
|
104
|
+
archived: true,
|
|
105
|
+
archivedAt: now,
|
|
106
|
+
updatedAt: now
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
todos[index] = updated;
|
|
110
|
+
saveTodos(todos);
|
|
111
|
+
res.json(updated);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
router.post('/:id/unarchive', (req, res) => {
|
|
115
|
+
const { id } = req.params;
|
|
116
|
+
const todos = listTodos();
|
|
117
|
+
const index = todos.findIndex((item) => item.id === id);
|
|
118
|
+
|
|
119
|
+
if (index < 0) {
|
|
120
|
+
return res.status(404).json({ error: 'Todo item not found.' });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const now = new Date().toISOString();
|
|
124
|
+
const updated = {
|
|
125
|
+
...todos[index],
|
|
126
|
+
archived: false,
|
|
127
|
+
archivedAt: null,
|
|
128
|
+
updatedAt: now
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
todos[index] = updated;
|
|
132
|
+
saveTodos(todos);
|
|
133
|
+
res.json(updated);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
router.delete('/:id', (req, res) => {
|
|
137
|
+
const { id } = req.params;
|
|
138
|
+
const todos = listTodos();
|
|
139
|
+
const next = todos.filter((item) => item.id !== id);
|
|
140
|
+
|
|
141
|
+
if (next.length === todos.length) {
|
|
142
|
+
return res.status(404).json({ error: 'Todo item not found.' });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
saveTodos(next);
|
|
146
|
+
res.status(204).end();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
router.post('/batch', (req, res) => {
|
|
150
|
+
const { action, ids } = req.body || {};
|
|
151
|
+
let todos = listTodos();
|
|
152
|
+
const now = new Date().toISOString();
|
|
153
|
+
const idSet = new Set(Array.isArray(ids) ? ids : []);
|
|
154
|
+
|
|
155
|
+
if (action === 'markCompleted') {
|
|
156
|
+
todos = todos.map((item) => (idSet.has(item.id) ? { ...item, completed: true, completedAt: item.completedAt || now, updatedAt: now } : item));
|
|
157
|
+
} else if (action === 'markPending') {
|
|
158
|
+
todos = todos.map((item) => (idSet.has(item.id) ? { ...item, completed: false, completedAt: null, updatedAt: now } : item));
|
|
159
|
+
} else if (action === 'archive') {
|
|
160
|
+
todos = todos.map((item) => (idSet.has(item.id) ? { ...item, archived: true, archivedAt: now, updatedAt: now } : item));
|
|
161
|
+
} else if (action === 'unarchive') {
|
|
162
|
+
todos = todos.map((item) => (idSet.has(item.id) ? { ...item, archived: false, archivedAt: null, updatedAt: now } : item));
|
|
163
|
+
} else if (action === 'delete') {
|
|
164
|
+
todos = todos.filter((item) => !idSet.has(item.id));
|
|
165
|
+
} else if (action === 'clearCompleted') {
|
|
166
|
+
todos = todos.filter((item) => !item.completed || item.archived);
|
|
167
|
+
} else if (action === 'clearArchived') {
|
|
168
|
+
todos = todos.filter((item) => !item.archived);
|
|
169
|
+
} else {
|
|
170
|
+
return res.status(400).json({ error: 'Invalid batch action.' });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
saveTodos(todos);
|
|
174
|
+
res.json(todos);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export default router;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readSettings } from '../repositories/settings.js';
|
|
3
|
+
|
|
4
|
+
export function openInSystemBrowser(url) {
|
|
5
|
+
if (!url || typeof url !== 'string') return;
|
|
6
|
+
const settings = readSettings();
|
|
7
|
+
const browser = settings.defaultBrowser || 'chrome';
|
|
8
|
+
|
|
9
|
+
if (process.platform === 'darwin') {
|
|
10
|
+
const macAppMap = {
|
|
11
|
+
chrome: 'Google Chrome',
|
|
12
|
+
edge: 'Microsoft Edge',
|
|
13
|
+
safari: 'Safari'
|
|
14
|
+
};
|
|
15
|
+
const appName = macAppMap[browser] || 'Google Chrome';
|
|
16
|
+
execFile('open', ['-a', appName, url]);
|
|
17
|
+
} else if (process.platform === 'win32') {
|
|
18
|
+
const winCmdMap = {
|
|
19
|
+
chrome: 'chrome',
|
|
20
|
+
edge: 'msedge',
|
|
21
|
+
safari: 'msedge'
|
|
22
|
+
};
|
|
23
|
+
const cmd = winCmdMap[browser] || 'chrome';
|
|
24
|
+
execFile('cmd', ['/c', 'start', '', cmd, url]);
|
|
25
|
+
} else {
|
|
26
|
+
execFile('xdg-open', [url]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -19,8 +19,74 @@ export function clipboardDates() {
|
|
|
19
19
|
return readdirSync(paths.clipboardDir, { withFileTypes: true }).filter((year) => year.isDirectory() && /^\d{4}$/.test(year.name)).flatMap((year) => readdirSync(join(paths.clipboardDir, year.name), { withFileTypes: true }).filter((month) => month.isDirectory() && /^\d{2}$/.test(month.name)).flatMap((month) => readdirSync(join(paths.clipboardDir, year.name, month.name), { withFileTypes: true }).filter((day) => day.isFile() && /^\d{2}\.json$/.test(day.name)).map((day) => `${year.name}-${month.name}-${day.name.slice(0, 2)}`))).sort().reverse();
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
function getEditorUrl(editor = 'vscode', path = '') {
|
|
23
|
+
if (editor === 'idea') return `idea://open?file=${encodeURIComponent(path)}`;
|
|
24
|
+
if (editor === 'devin') return `devin://file${encodeURI(path)}`;
|
|
25
|
+
return `vscode://file${encodeURI(path)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
function dayItems(date) { return readJson(dayFile(date), []); }
|
|
23
|
-
export function clipboardItems(date = today()) {
|
|
29
|
+
export function clipboardItems(date = today()) {
|
|
30
|
+
const settings = readSettings();
|
|
31
|
+
const editor = settings.defaultEditor || 'vscode';
|
|
32
|
+
const editorName = editor === 'idea' ? 'IntelliJ IDEA' : editor === 'devin' ? 'Devin' : 'VS Code';
|
|
33
|
+
return dayItems(date).map((item) => item.contentFile ? { ...item, editorUrl: getEditorUrl(editor, join(paths.clipboardDir, 'content', item.contentFile)), editorName } : item);
|
|
34
|
+
}
|
|
35
|
+
export function allTaggedClipboardItems() {
|
|
36
|
+
const settings = readSettings();
|
|
37
|
+
const editor = settings.defaultEditor || 'vscode';
|
|
38
|
+
const editorName = editor === 'idea' ? 'IntelliJ IDEA' : editor === 'devin' ? 'Devin' : 'VS Code';
|
|
39
|
+
const dates = clipboardDates();
|
|
40
|
+
const result = [];
|
|
41
|
+
|
|
42
|
+
for (const date of dates) {
|
|
43
|
+
const items = dayItems(date);
|
|
44
|
+
for (const item of items) {
|
|
45
|
+
if (Array.isArray(item.tags) && item.tags.length > 0) {
|
|
46
|
+
result.push(
|
|
47
|
+
item.contentFile
|
|
48
|
+
? {
|
|
49
|
+
...item,
|
|
50
|
+
date,
|
|
51
|
+
editorUrl: getEditorUrl(editor, join(paths.clipboardDir, 'content', item.contentFile)),
|
|
52
|
+
editorName
|
|
53
|
+
}
|
|
54
|
+
: { ...item, date }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function updateClipboardItemTags(date, id, tags) {
|
|
63
|
+
let targetDate = date;
|
|
64
|
+
let items = dayItems(targetDate);
|
|
65
|
+
let item = items.find((entry) => entry.id === id);
|
|
66
|
+
|
|
67
|
+
if (!item) {
|
|
68
|
+
const dates = clipboardDates();
|
|
69
|
+
for (const d of dates) {
|
|
70
|
+
const dayList = dayItems(d);
|
|
71
|
+
const found = dayList.find((entry) => entry.id === id);
|
|
72
|
+
if (found) {
|
|
73
|
+
targetDate = d;
|
|
74
|
+
items = dayList;
|
|
75
|
+
item = found;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!item) return null;
|
|
82
|
+
|
|
83
|
+
const validTags = Array.isArray(tags) ? tags.map((t) => String(t).trim()).filter(Boolean) : [];
|
|
84
|
+
const updatedItem = { ...item, tags: validTags };
|
|
85
|
+
const nextItems = items.map((entry) => (entry.id === id ? updatedItem : entry));
|
|
86
|
+
writeJson(dayFile(targetDate), nextItems);
|
|
87
|
+
return updatedItem;
|
|
88
|
+
}
|
|
89
|
+
|
|
24
90
|
export function clipboardOriginal(date, id) { const item = dayItems(date).find((entry) => entry.id === id); if (!item?.contentFile) return item?.text || null; try { return readFileSync(join(paths.clipboardDir, 'content', item.contentFile), 'utf8'); } catch { return null; } }
|
|
25
91
|
export function deleteClipboardItem(date, id) {
|
|
26
92
|
const items = dayItems(date); const item = items.find((entry) => entry.id === id);
|
|
@@ -82,18 +148,18 @@ export async function captureClipboard() {
|
|
|
82
148
|
if (alreadyExists) return;
|
|
83
149
|
|
|
84
150
|
const id = crypto.randomUUID(); const isLong = text.length > previewLimit;
|
|
85
|
-
if (isLong) { mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true }); writeFileSync(join(paths.clipboardDir, 'content',
|
|
86
|
-
const item = isLong ? { id, preview: `${text.slice(0, previewLimit)}…`, contentFile:
|
|
151
|
+
if (isLong) { mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true }); writeFileSync(join(paths.clipboardDir, 'content', id), text, 'utf8'); }
|
|
152
|
+
const item = isLong ? { id, preview: `${text.slice(0, previewLimit)}…`, contentFile: id, createdAt: new Date().toISOString() } : { id, text, createdAt: new Date().toISOString() };
|
|
87
153
|
writeJson(dayFile(date), [item, ...items].slice(0, 200));
|
|
88
154
|
} catch {}
|
|
89
155
|
}
|
|
90
156
|
|
|
91
157
|
export function startClipboardCapture() { captureClipboard(); const timer = setInterval(captureClipboard, 2000); timer.unref(); }
|
|
92
158
|
|
|
93
|
-
export function saveClipboardImage(buffer) {
|
|
159
|
+
export function saveClipboardImage(buffer, ext = 'png') {
|
|
94
160
|
const date = today();
|
|
95
161
|
const id = crypto.randomUUID();
|
|
96
|
-
const filename = `${id}
|
|
162
|
+
const filename = `${id}.${ext}`;
|
|
97
163
|
|
|
98
164
|
mkdirSync(join(paths.clipboardDir, 'content'), { recursive: true });
|
|
99
165
|
writeFileSync(join(paths.clipboardDir, 'content', filename), buffer);
|
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
6
|
import { listPortHistory, savePortHistory } from '../repositories/port-history.js';
|
|
7
|
+
import { addErrorRecord } from '../repositories/errors.js';
|
|
7
8
|
|
|
8
9
|
const running = new Map();
|
|
9
10
|
const logs = new Map();
|
|
@@ -217,6 +218,13 @@ export function runScript(launcher, script) {
|
|
|
217
218
|
child.on('exit', (code) => {
|
|
218
219
|
appendLog(key, code === 0 ? 'output' : 'error', `\nProcess exited with code ${code}.\n`);
|
|
219
220
|
const item = logs.get(key);
|
|
221
|
+
if (code !== null && code !== 0) {
|
|
222
|
+
addErrorRecord({
|
|
223
|
+
source: `Launcher: ${launcher.alias}`,
|
|
224
|
+
message: `Script "${script.name}" exited with code ${code}`,
|
|
225
|
+
details: item?.error || item?.output || `Command: ${script.command}`
|
|
226
|
+
});
|
|
227
|
+
}
|
|
220
228
|
if (item) item.errorCount = 0;
|
|
221
229
|
running.delete(key);
|
|
222
230
|
retireGroup(child.pid);
|
package/server.js
CHANGED
|
@@ -13,11 +13,46 @@ import groupTaskRoutes from './server/routes/group-tasks.js';
|
|
|
13
13
|
import portDiagnosticsRoutes from './server/routes/port-diagnostics.js';
|
|
14
14
|
import settingsRoutes from './server/routes/settings.js';
|
|
15
15
|
import prReviewRoutes from './server/routes/pr-review.js';
|
|
16
|
+
import jiraFiltersRoutes from './server/routes/jira-filters.js';
|
|
17
|
+
import todoRoutes from './server/routes/todos.js';
|
|
18
|
+
import staticPagesRoutes from './server/routes/static-pages.js';
|
|
19
|
+
import errorRoutes from './server/routes/errors.js';
|
|
20
|
+
import { addErrorRecord } from './server/repositories/errors.js';
|
|
16
21
|
import { startClipboardCapture } from './server/services/clipboard-history.js';
|
|
17
22
|
|
|
18
23
|
const port = Number(process.env.PORT || 3100);
|
|
19
24
|
const app = express();
|
|
20
25
|
app.use(express.json());
|
|
26
|
+
|
|
27
|
+
// Intercept error responses (HTTP status >= 400) from any API route
|
|
28
|
+
app.use((req, res, next) => {
|
|
29
|
+
const originalJson = res.json;
|
|
30
|
+
res.json = function (body) {
|
|
31
|
+
if (res.statusCode >= 400 && body && body.error && !req.path.startsWith('/api/errors')) {
|
|
32
|
+
const host = req.get('host') || req.headers.host || `localhost:${port}`;
|
|
33
|
+
const fullUrl = `${req.protocol}://${host}${req.originalUrl || req.url}`;
|
|
34
|
+
|
|
35
|
+
const detailsLines = [];
|
|
36
|
+
if (body.targetUrl) {
|
|
37
|
+
detailsLines.push(`Target URL: ${body.targetUrl}`);
|
|
38
|
+
}
|
|
39
|
+
detailsLines.push(`Endpoint: ${req.method} ${fullUrl}`);
|
|
40
|
+
detailsLines.push(`HTTP Status: ${res.statusCode}`);
|
|
41
|
+
if (typeof body.error === 'string' && body.error !== `HTTP ${res.statusCode} Error`) {
|
|
42
|
+
detailsLines.push(`Error: ${body.error}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
addErrorRecord({
|
|
46
|
+
source: `Backend API (${req.method} ${req.path})`,
|
|
47
|
+
message: typeof body.error === 'string' ? body.error : `HTTP ${res.statusCode} Error`,
|
|
48
|
+
details: detailsLines.join('\n')
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return originalJson.call(this, body);
|
|
52
|
+
};
|
|
53
|
+
next();
|
|
54
|
+
});
|
|
55
|
+
|
|
21
56
|
app.use(express.static(paths.ui));
|
|
22
57
|
app.use('/plugins', express.static(paths.plugins));
|
|
23
58
|
app.use('/api/launchers', launcherRoutes);
|
|
@@ -27,6 +62,19 @@ app.use('/api/group-tasks', groupTaskRoutes);
|
|
|
27
62
|
app.use('/api/port-diagnostics', portDiagnosticsRoutes);
|
|
28
63
|
app.use('/api/settings', settingsRoutes);
|
|
29
64
|
app.use('/api/pr-review', prReviewRoutes);
|
|
65
|
+
app.use('/api/jira-filters', jiraFiltersRoutes);
|
|
66
|
+
app.use('/api/todos', todoRoutes);
|
|
67
|
+
app.use('/api/static-pages', staticPagesRoutes);
|
|
68
|
+
app.use('/api/errors', errorRoutes);
|
|
69
|
+
|
|
70
|
+
app.use((err, req, res, _next) => {
|
|
71
|
+
addErrorRecord({
|
|
72
|
+
source: `Backend API (${req.method} ${req.path})`,
|
|
73
|
+
message: err.message || 'Internal Server Error',
|
|
74
|
+
details: err.stack || String(err)
|
|
75
|
+
});
|
|
76
|
+
res.status(500).json({ error: err.message || 'Internal Server Error' });
|
|
77
|
+
});
|
|
30
78
|
|
|
31
79
|
await freePort(port);
|
|
32
80
|
const server = app.listen(port, () => { console.log(`Buddy Workbench: http://localhost:${port} (pid ${process.pid})`); shutdownTrace(`server started (pid=${process.pid})`); });
|
|
@@ -51,6 +99,14 @@ async function shutdown(exitCode = 0) {
|
|
|
51
99
|
process.once('SIGINT', () => { shutdownTrace('received SIGINT'); void shutdown(); });
|
|
52
100
|
process.once('SIGTERM', () => { shutdownTrace('received SIGTERM'); void shutdown(); });
|
|
53
101
|
process.once('SIGHUP', () => { shutdownTrace('received SIGHUP'); void shutdown(); });
|
|
54
|
-
process.once('uncaughtException', (error) => {
|
|
55
|
-
|
|
102
|
+
process.once('uncaughtException', (error) => {
|
|
103
|
+
addErrorRecord({ source: 'Server UncaughtException', message: error.message || 'Uncaught Server Exception', details: error.stack || String(error) });
|
|
104
|
+
console.error(error);
|
|
105
|
+
void shutdown(1);
|
|
106
|
+
});
|
|
107
|
+
process.once('unhandledRejection', (error) => {
|
|
108
|
+
addErrorRecord({ source: 'Server UnhandledRejection', message: error?.message || String(error || 'Unhandled Promise Rejection'), details: error?.stack || String(error) });
|
|
109
|
+
console.error(error);
|
|
110
|
+
void shutdown(1);
|
|
111
|
+
});
|
|
56
112
|
process.once('exit', () => stopAllScripts(true));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
html,body{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}@-ms-viewport{width:device-width}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[title],abbr[data-original-title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=text],input[type=password],input[type=number],textarea{-webkit-appearance:none}ol,ul,dl{margin-top:0;margin-bottom:1em}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}pre,code,kbd,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}a,area,button,[role=button],input:not([type=range]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;text-align:left;caption-side:bottom}input,button,select,optgroup,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{padding:0;border-style:none}input[type=radio],input[type=checkbox]{box-sizing:border-box;padding:0}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}:root{font-family:Inter,ui-sans-serif,system-ui,-apple-system,PingFang SC,sans-serif;color:#172033;background:#f7f8fc}html,body{height:100vh;overflow:hidden}body{margin:0;background:#f7f8fc}.workbench{height:100vh;overflow:hidden;background:#f7f8fc}.sidebar{position:sticky!important;top:0;height:100vh;background:#171c2b!important;padding:28px 14px}.sidebar .ant-menu-title-content{-webkit-user-select:none;user-select:none}.brand{padding:0 12px 30px;color:#fff;font-size:21px;font-weight:750;letter-spacing:-.4px}.brand span{color:#9589ff}.sidebar .ant-menu{background:transparent;border-inline-end:0}.sidebar-bottom{position:absolute;right:14px;bottom:28px;left:14px}.sidebar-footer{padding:14px 12px 0;color:#727b94;font-size:12px}.page{height:100vh;overflow-y:auto;padding:24px clamp(20px,3.5vw,40px)}.page-header{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;margin-bottom:30px}.page-header .ant-typography{margin:0}.launcher-grid{display:grid;gap:7px}.launcher-card{cursor:pointer;border-color:#e3e6ef}.launcher-card .ant-card-body{padding:10px 14px}.service-card{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:46px}.service-card .ant-typography{display:block;margin:0}.service-card h4.ant-typography{margin:0 0 1px;font-size:15px;line-height:20px}.service-info{min-width:0;flex:1}.service-info>.ant-typography{font-size:12px;line-height:16px}.service-action{display:flex;flex-direction:column;align-items:flex-end;gap:3px;white-space:nowrap}.service-action .ant-badge{font-size:11px;line-height:14px}.launcher-table .ant-table-tbody>tr{cursor:pointer}.start-script-icon{color:#8c8c8c;font-size:12px}.script-add{margin-bottom:10px}.service-config{margin-bottom:10px;margin-top:10px}.service-config .ant-collapse-content-box{padding-bottom:0!important}.service-config .ant-form-item{margin-bottom:14px}.clipboard-picker{min-width:190px}.squoosh-page{position:relative;min-height:calc(100vh - 48px);margin:-24px clamp(-20px,-3.5vw,-40px);padding:24px clamp(20px,3.5vw,40px);color:#172033}.squoosh-page.is-file-dragging{box-shadow:inset 0 0 0 3px #7667e8}.squoosh-page.is-file-dragging:after{position:absolute;z-index:20;top:12px;right:12px;bottom:12px;left:12px;display:grid;place-items:center;border:2px dashed #7667e8;border-radius:12px;background:#f8f7ffc7;color:#5b4cc4;content:"Drop image to compress";font-size:20px;font-weight:700;pointer-events:none}.squoosh-header{display:flex;align-items:center;justify-content:space-between;gap:20px;padding-bottom:20px;border-bottom:1px solid #e4e7ef}.squoosh-header .ant-typography{margin:0;color:inherit}.squoosh-header>div>.ant-typography:first-child,.settings-heading>.ant-typography:first-child{color:#6d5ce7;font-size:11px;font-weight:750;letter-spacing:.12em}.squoosh-header h2.ant-typography{margin-top:4px;font-size:25px}.squoosh-studio{display:grid;grid-template-columns:minmax(0,1fr) 380px;margin-top:16px;border:1px solid #e1e5ed;border-radius:10px;overflow:hidden;background:#fff;box-shadow:0 12px 32px #1f2a4412}.squoosh-preview-area{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));min-width:0;background:#fbfcfe}.preview-pane{display:grid;grid-template-rows:auto minmax(0,1fr);min-width:0;min-height:570px}.preview-pane+.preview-pane{border-left:1px solid #e1e5ed}.preview-pane>header{display:flex;justify-content:space-between;align-items:center;gap:12px;min-height:58px;padding:0 16px;border-bottom:1px solid #e1e5ed;background:#fff}.preview-pane>header>div{display:grid;gap:1px;min-width:0}.preview-pane>header .ant-typography{color:#27344c;font-weight:650}.preview-pane>header span{overflow:hidden;color:#7d8799;font-size:12px;text-overflow:ellipsis;white-space:nowrap}.preview-canvas{position:relative;display:grid;min-width:0;min-height:0;place-items:center;padding:22px;background-color:#f3f5f9;background-image:linear-gradient(45deg,#e9edf4 25%,transparent 25%),linear-gradient(-45deg,#e9edf4 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#e9edf4 75%),linear-gradient(-45deg,transparent 75%,#e9edf4 75%);background-position:0 0,0 12px,12px -12px,-12px 0;background-size:24px 24px}.image-dropzone{width:100%;max-width:420px}.image-dropzone.ant-upload-wrapper .ant-upload-drag{border-color:#cfd6e4;background:#ffffffe0}.image-dropzone.ant-upload-wrapper .ant-upload-drag:hover{border-color:#7667e8}.image-dropzone .ant-upload{padding:76px 18px!important}.image-dropzone .ant-upload-drag-icon .anticon{color:#6d5ce7}.image-dropzone .ant-upload-text{color:#27344c!important}.image-dropzone .ant-upload-hint{color:#7d8799!important}.image-panel{display:flex;align-items:center;justify-content:center;width:100%;height:100%;min-width:0;min-height:0}.image-panel img{display:block;width:auto;max-width:100%;max-height:min(65vh,650px);object-fit:contain;border-radius:5px;box-shadow:0 10px 30px #00000052}.image-panel-meta{display:grid;gap:3px;min-width:0;text-align:center}.image-panel-meta .ant-typography{min-width:0;color:#46516a}.squoosh-settings{overflow-y:auto;padding:22px 20px;background:#fff}.settings-heading{margin-bottom:21px;padding-bottom:17px;border-bottom:1px solid #e7eaf0}.settings-heading .ant-typography{margin:0;color:#27344c}.settings-heading h4.ant-typography{margin-top:5px}.settings-heading .ant-typography-secondary{margin-top:4px;color:#7d8799;font-size:13px}.option-row,.option-switch,.option-select{display:flex;justify-content:space-between;align-items:center;gap:16px}.option-row{margin-bottom:4px}.option-row .ant-typography,.option-switch .ant-typography,.option-select .ant-typography{color:#27344c}.option-row .ant-typography-secondary,.option-switch .ant-typography-secondary,.option-select .ant-typography-secondary{color:#7d8799;font-size:12px}.option-switch{padding:15px 0;border-top:1px solid #e7eaf0}.option-select{padding:14px 0;border-top:1px solid #e7eaf0}.option-select .ant-select,.option-select .ant-input-number{flex:0 0 170px;width:170px}.advanced-options{margin:6px -12px 16px}.advanced-options .ant-collapse-header,.advanced-options .ant-collapse-content{color:#46516a!important}.advanced-options .ant-collapse-content{background:transparent}.advanced-options .ant-collapse-content-box{padding-top:0!important}.image-loading{display:grid;min-height:330px;place-items:center}.image-loading .ant-spin-text{color:#46516a}.preview-pane>header button.ant-btn.ant-btn-color-primary{min-width:116px;border-color:#5f50d8!important;background:#6657dc!important;color:#fff!important;font-weight:650;text-shadow:none}.preview-pane>header button.ant-btn.ant-btn-color-primary>span,.preview-pane>header button.ant-btn.ant-btn-color-primary .anticon{color:#fff!important}.preview-pane>header button.ant-btn.ant-btn-color-primary:hover{border-color:#5143c2!important;background:#5143c2!important;color:#fff!important}.compression-saving{position:absolute;bottom:24px;left:50%;z-index:1;margin:0;padding:9px 12px;border-radius:4px;background:#fff7e6;color:#ad6800;font-weight:600;text-align:center;transform:translate(-50%);white-space:nowrap}.compression-saving.is-saving{background:#f6ffed;color:#389e0d}.clipboard-tabs{margin:-12px 0 16px}.clipboard-tabs .ant-tabs-nav{margin-bottom:0}.clipboard-list{display:grid;gap:8px}.clipboard-item .ant-card-body{position:relative;display:block;padding:12px 14px}.clipboard-item .ant-card-body>div{width:100%;min-width:0}.clipboard-actions{position:absolute;top:8px;right:8px;display:flex}.clipboard-item pre{width:100%;max-height:160px;overflow:auto;margin:5px 0 0;white-space:pre-wrap;overflow-wrap:anywhere;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}.clipboard-original{display:flex;gap:12px;margin-top:8px;font-size:12px}.port-manual-query{margin:-12px 0 16px}.port-query-result{margin-top:12px}.group-task-list{display:grid;gap:8px}.group-task-header{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:16px}.group-task-empty{margin:16px 0}.group-task-card{display:flex;align-items:center;justify-content:space-between;gap:16px}.group-task-card>div:first-child{display:grid;gap:2px}.group-task-card .ant-space{flex-wrap:nowrap}.group-task-card .ant-btn{width:72px}.group-task-selector{display:grid;gap:8px;width:100%}.group-task-selector .ant-card-body{padding:10px 14px}.group-task-projects{display:grid;gap:8px}.settings-page{width:100%;max-width:none}.settings-page>.ant-typography{margin:0}.settings-page-header{display:flex;align-items:center;justify-content:space-between;gap:16px}.settings-page-header .ant-typography{margin:0}.settings-page-header>.ant-btn{min-width:96px}.settings-card{width:100%;margin-top:20px;border-radius:14px;border:1px solid #e4e8f1;box-shadow:0 2px 12px #1f2a440d}.settings-card h4.ant-typography{margin:0}.settings-icon-wrap{display:flex;align-items:center;justify-content:center;width:40px;height:40px;flex-shrink:0;border-radius:10px;background:linear-gradient(135deg,#ece9fc,#ddd6fe)}.settings-icon{color:#6d5ce7;font-size:18px}.settings-title{display:flex;align-items:center;gap:14px;margin-bottom:24px;padding-bottom:20px;border-bottom:1px solid #eef0f6}.settings-title .ant-typography{margin:0}.settings-title .ant-typography-secondary{font-size:13px;margin-top:2px}.token-settings-form{display:grid;gap:0}.token-row{display:grid;grid-template-columns:200px 1fr;align-items:center;gap:20px;padding:16px 18px;margin:0 -6px;border-radius:10px;border:1px solid transparent;transition:background .18s,border-color .18s}.token-row:hover{background:#f8f9fc;border-color:#eef0f6}.token-row+.token-row{margin-top:2px}.token-row.is-configured{background:#fafbfe}.token-row-info{display:grid;gap:2px;min-width:0}.token-row-name-line{display:flex;align-items:center;gap:8px}.token-row-name{font-size:14px;white-space:nowrap}.token-row-hint{font-size:12px;line-height:1.4}.token-row-tag.ant-tag{margin:0;font-size:11px;line-height:18px;padding-inline:6px;border-radius:4px}.token-row .ant-form-item{margin:0;min-width:0}.token-row .ant-input,.token-row .ant-input-password{border-radius:8px}.script-header{display:flex;align-items:center;gap:16px}.script-header .ant-divider{flex:1;min-width:0;margin:16px 0}.script-header>.ant-btn{flex-shrink:0}.script-editor{margin-bottom:10px}.script-fields{display:grid;grid-template-columns:1fr 1.5fr;gap:12px}.script-fields .ant-form-item{margin-bottom:12px}.log-output{min-height:360px;max-height:58vh;overflow:auto;margin:0;padding:12px 0;border-radius:4px;background:#111827;color:#d1fae5;font:12px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace}.log-line{min-height:19px}.log-line:hover{background:#ffffff0a}.log-line code{padding:0 15px;overflow-wrap:anywhere;color:inherit;white-space:pre-wrap;font:inherit}.log-link{color:#7dd3fc;text-decoration:underline;text-underline-offset:2px}.log-link:hover{color:#bae6fd}.ansi-black{color:#475569}.ansi-red,.log-error{color:#fb7185}.ansi-green,.log-success{color:#86efac}.ansi-yellow,.log-warning{color:#fcd34d}.ansi-blue{color:#93c5fd}.ansi-magenta{color:#f0abfc}.ansi-cyan{color:#67e8f9}.ansi-gray{color:#94a3b8}.ansi-white{color:#f8fafc}@media(max-width:1100px){.squoosh-studio{grid-template-columns:1fr}.squoosh-settings{border-top:1px solid #e1e5ed}.preview-pane{min-height:440px}}@media(max-width:650px){.sidebar{width:64px!important;min-width:64px!important;padding:20px 8px}.brand{padding:0 10px 25px;font-size:0}.brand span{font-size:20px}.sidebar .ant-menu-title-content,.sidebar-footer{display:none}.sidebar-bottom{right:8px;bottom:20px;left:8px}.page{padding:32px 20px}.service-card,.page-header{align-items:flex-start;flex-direction:column}.script-fields{grid-template-columns:1fr}.token-row{grid-template-columns:1fr;gap:10px;padding:14px 12px}.squoosh-page{min-height:calc(100vh - 64px);margin:-12px -20px -32px;padding:18px 20px 28px}.squoosh-header{align-items:flex-start;flex-direction:column}.squoosh-preview-area{grid-template-columns:1fr}.preview-pane{min-height:360px}.preview-pane+.preview-pane{border-top:1px solid #e1e5ed;border-left:0}.preview-canvas{min-height:300px}}.standalone-log-container{display:flex;flex-direction:column;height:100vh;background:#111827;overflow:hidden}.standalone-log-header{display:flex;justify-content:space-between;align-items:center;padding:12px 24px;background:#1f2937;border-bottom:1px solid #374151;color:#f3f4f6;flex-shrink:0}.standalone-log-body{flex:1;min-height:0;display:flex;flex-direction:column;background:#111827}.standalone-log-body .log-output{flex:1;min-height:0;max-height:none!important;border-radius:0!important;margin:0!important;padding:16px 0}.page.is-static-page{padding:0!important;overflow:hidden}.static-page-container{width:100%;height:100%;overflow:hidden;background:#fff}.static-page-iframe{width:100%;height:100%;border:none;display:block}.sidebar-bottom-inner{display:flex;align-items:center;gap:4px}.sidebar-bottom-inner .ant-menu{flex:1;min-width:0}.sidebar-error-btn{color:#a6adb4!important;border:none!important;background:transparent!important;display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:6px;flex-shrink:0;transition:all .2s}.sidebar-error-btn:hover{color:#ff4d4f!important;background:#ff4d4f26!important}.sidebar-error-btn.is-active{color:#fff!important;background:#ff4d4f!important}
|