buddy-workbench 0.1.10 โ†’ 0.1.12

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buddy-workbench",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,8 @@
10
10
  "server.js",
11
11
  "server/",
12
12
  "ui/dist/",
13
- "plugins/"
13
+ "plugins/",
14
+ "pages/"
14
15
  ],
15
16
  "scripts": {
16
17
  "start": "exec node server.js",
package/pages/.gitkeep ADDED
@@ -0,0 +1 @@
1
+ # Keep pages directory
@@ -0,0 +1,72 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Welcome - DevBuddy Static Pages</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0f172a;
10
+ --card-bg: #1e293b;
11
+ --text: #f8fafc;
12
+ --text-muted: #94a3b8;
13
+ --accent: #38bdf8;
14
+ --border: #334155;
15
+ }
16
+ body {
17
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
18
+ background: var(--bg);
19
+ color: var(--text);
20
+ display: flex;
21
+ justify-content: center;
22
+ align-items: center;
23
+ min-height: 100vh;
24
+ margin: 0;
25
+ padding: 24px;
26
+ box-sizing: border-box;
27
+ }
28
+ .card {
29
+ background: var(--card-bg);
30
+ padding: 32px;
31
+ border-radius: 16px;
32
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
33
+ border: 1px solid var(--border);
34
+ text-align: center;
35
+ max-width: 520px;
36
+ width: 100%;
37
+ }
38
+ .icon {
39
+ font-size: 48px;
40
+ margin-bottom: 16px;
41
+ }
42
+ h1 {
43
+ color: var(--accent);
44
+ margin-top: 0;
45
+ margin-bottom: 12px;
46
+ font-size: 24px;
47
+ }
48
+ p {
49
+ color: var(--text-muted);
50
+ line-height: 1.6;
51
+ margin-bottom: 20px;
52
+ }
53
+ code {
54
+ background: rgba(15, 23, 42, 0.8);
55
+ padding: 4px 8px;
56
+ border-radius: 6px;
57
+ color: #f43f5e;
58
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
59
+ font-size: 0.9em;
60
+ border: 1px solid var(--border);
61
+ }
62
+ </style>
63
+ </head>
64
+ <body>
65
+ <div class="card">
66
+ <div class="icon">๐Ÿš€</div>
67
+ <h1>Static Server Active</h1>
68
+ <p>Place your HTML, CSS, and JS files inside the <code>pages/</code> directory in your workspace.</p>
69
+ <p>Configure this page in Settings &gt; Static Pages with URL <code>/pages/welcome/index.html</code></p>
70
+ </div>
71
+ </body>
72
+ </html>
package/server/config.js CHANGED
@@ -10,8 +10,11 @@ export const paths = {
10
10
  jiraFilters: join(root, 'data', 'jira-filters.json'),
11
11
  todos: join(root, 'data', 'todos.json'),
12
12
  staticPages: join(root, 'data', 'static-pages.json'),
13
+ errors: join(root, 'data', 'errors.json'),
13
14
  shutdownLog: join(root, 'data', 'shutdown.log'),
14
15
  clipboardDir: join(root, 'data', 'clipboard'),
15
16
  plugins: join(root, 'plugins'),
17
+ pages: join(root, 'pages'),
18
+ dataPages: join(root, 'data', 'pages'),
16
19
  ui: join(root, 'ui', 'dist')
17
20
  };
@@ -0,0 +1,44 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import crypto from 'node:crypto';
4
+ import { paths } from '../config.js';
5
+
6
+ export function listErrors() {
7
+ try {
8
+ return existsSync(paths.errors) ? JSON.parse(readFileSync(paths.errors, 'utf8')) : [];
9
+ } catch {
10
+ return [];
11
+ }
12
+ }
13
+
14
+ export function saveErrors(errors) {
15
+ mkdirSync(dirname(paths.errors), { recursive: true });
16
+ const trimmed = Array.isArray(errors) ? errors.slice(-100) : [];
17
+ writeFileSync(paths.errors, JSON.stringify(trimmed, null, 2));
18
+ }
19
+
20
+ export function addErrorRecord({ source = 'System', message = '', details = '', level = 'error' }) {
21
+ if (!message && !details) return null;
22
+ const current = listErrors();
23
+ const record = {
24
+ id: crypto.randomUUID(),
25
+ timestamp: new Date().toISOString(),
26
+ source: String(source || 'System').trim(),
27
+ level: ['fatal', 'warning', 'error'].includes(level) ? level : 'error',
28
+ message: String(message || '').trim() || 'Unspecified Error',
29
+ details: String(details || '').trim()
30
+ };
31
+ current.push(record);
32
+ saveErrors(current);
33
+ return record;
34
+ }
35
+
36
+ export function clearAllErrors() {
37
+ saveErrors([]);
38
+ }
39
+
40
+ export function deleteErrorRecord(id) {
41
+ const current = listErrors();
42
+ const filtered = current.filter((item) => item.id !== id);
43
+ saveErrors(filtered);
44
+ }
@@ -0,0 +1,31 @@
1
+ import { Router } from 'express';
2
+ import { addErrorRecord, clearAllErrors, deleteErrorRecord, listErrors } from '../repositories/errors.js';
3
+
4
+ const router = Router();
5
+
6
+ router.get('/', (_req, res) => {
7
+ const errors = listErrors();
8
+ // Return newest first
9
+ res.json([...errors].reverse());
10
+ });
11
+
12
+ router.post('/', (req, res) => {
13
+ const { source, message, details, level } = req.body || {};
14
+ if (!message && !details) {
15
+ return res.status(400).json({ error: 'Message or details is required.' });
16
+ }
17
+ const record = addErrorRecord({ source, message, details, level });
18
+ res.status(201).json(record);
19
+ });
20
+
21
+ router.delete('/', (_req, res) => {
22
+ clearAllErrors();
23
+ res.status(204).end();
24
+ });
25
+
26
+ router.delete('/:id', (req, res) => {
27
+ deleteErrorRecord(req.params.id);
28
+ res.status(204).end();
29
+ });
30
+
31
+ export default router;
@@ -130,7 +130,7 @@ router.get('/:id/issues', async (req, res) => {
130
130
  const response = await httpClient.get(url, { headers });
131
131
  if (response.status !== 200) {
132
132
  const errMsg = response.data?.errorMessages?.[0] || response.data?.message || `Jira API returned status ${response.status}`;
133
- return res.status(response.status).json({ error: errMsg });
133
+ return res.status(response.status).json({ error: errMsg, targetUrl: url });
134
134
  }
135
135
 
136
136
  const rawIssues = response.data?.issues || [];
@@ -153,7 +153,8 @@ router.get('/:id/issues', async (req, res) => {
153
153
 
154
154
  res.json({ filter, issues });
155
155
  } catch (error) {
156
- res.status(500).json({ error: error.message || 'Failed to fetch Jira issues.' });
156
+ const targetUrl = `https://${jiraHost}/rest/api/2/search?jql=filter%3D${encodeURIComponent(filter.filterId)}&maxResults=200&fields=summary,priority,duedate,status`;
157
+ res.status(500).json({ error: error.message || 'Failed to fetch Jira issues.', targetUrl });
157
158
  }
158
159
  });
159
160
 
@@ -213,9 +213,9 @@ router.post('/check', async (req, res) => {
213
213
  const prRes = await httpClient.get(prUrl, { headers });
214
214
  if (prRes.status < 200 || prRes.status >= 300) {
215
215
  if (prRes.status === 401) {
216
- return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Access Token in Settings.' });
216
+ return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Access Token in Settings.', targetUrl: prUrl });
217
217
  }
218
- return res.status(prRes.status).json({ error: `Failed to fetch PR info: ${prRes.statusText || prRes.status}` });
218
+ return res.status(prRes.status).json({ error: `Failed to fetch PR info: ${prRes.statusText || prRes.status}`, targetUrl: prUrl });
219
219
  }
220
220
  const prInfo = prRes.data;
221
221
 
@@ -223,7 +223,7 @@ router.post('/check', async (req, res) => {
223
223
  const changesUrl = `https://${host}/rest/api/1.0/projects/${projectKey}/repos/${repositorySlug}/pull-requests/${pullRequestId}/changes?limit=1000`;
224
224
  const changesRes = await httpClient.get(changesUrl, { headers });
225
225
  if (changesRes.status < 200 || changesRes.status >= 300) {
226
- return res.status(changesRes.status).json({ error: `Failed to fetch PR changes: ${changesRes.statusText || changesRes.status}` });
226
+ return res.status(changesRes.status).json({ error: `Failed to fetch PR changes: ${changesRes.statusText || changesRes.status}`, targetUrl: changesUrl });
227
227
  }
228
228
  const changesData = changesRes.data;
229
229
 
@@ -329,14 +329,14 @@ router.get('/my-prs', async (req, res) => {
329
329
  const headers = { 'Accept': 'application/json' };
330
330
  if (token) headers['Authorization'] = `Bearer ${token}`;
331
331
 
332
+ const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
332
333
  try {
333
- const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=author&state=OPEN&limit=100`;
334
334
  const response = await httpClient.get(url, { headers });
335
335
  if (response.status < 200 || response.status >= 300) {
336
336
  if (response.status === 401) {
337
- return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.' });
337
+ return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.', targetUrl: url });
338
338
  }
339
- return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}` });
339
+ return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}`, targetUrl: url });
340
340
  }
341
341
  const data = response.data;
342
342
  const values = data.values || [];
@@ -350,7 +350,7 @@ router.get('/my-prs', async (req, res) => {
350
350
 
351
351
  res.json({ values: enrichPrList(sorted, host) });
352
352
  } catch (error) {
353
- res.status(500).json({ error: error.message });
353
+ res.status(500).json({ error: error.message, targetUrl: url });
354
354
  }
355
355
  });
356
356
 
@@ -363,14 +363,14 @@ router.get('/review-prs', async (req, res) => {
363
363
  const headers = { 'Accept': 'application/json' };
364
364
  if (token) headers['Authorization'] = `Bearer ${token}`;
365
365
 
366
+ const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=reviewer&state=OPEN&limit=100`;
366
367
  try {
367
- const url = `https://${host}/rest/api/1.0/dashboard/pull-requests?role=reviewer&state=OPEN&limit=100`;
368
368
  const response = await httpClient.get(url, { headers });
369
369
  if (response.status < 200 || response.status >= 300) {
370
370
  if (response.status === 401) {
371
- return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.' });
371
+ return res.status(401).json({ error: 'Unauthorized. Please check your Bitbucket Token.', targetUrl: url });
372
372
  }
373
- return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}` });
373
+ return res.status(response.status).json({ error: `Bitbucket API error: ${response.statusText || response.status}`, targetUrl: url });
374
374
  }
375
375
  const data = response.data;
376
376
  const values = data.values || [];
@@ -379,7 +379,7 @@ router.get('/review-prs', async (req, res) => {
379
379
 
380
380
  res.json({ values: enrichPrList(sorted, host) });
381
381
  } catch (error) {
382
- res.status(500).json({ error: error.message });
382
+ res.status(500).json({ error: error.message, targetUrl: url });
383
383
  }
384
384
  });
385
385
 
@@ -423,14 +423,15 @@ router.post('/comment', async (req, res) => {
423
423
  if (response.status < 200 || response.status >= 300) {
424
424
  const errJson = response.data || {};
425
425
  return res.status(response.status).json({
426
- error: errJson.errors?.[0]?.message || errJson.message || `Bitbucket API error: ${response.statusText || response.status}`
426
+ error: errJson.errors?.[0]?.message || errJson.message || `Bitbucket API error: ${response.statusText || response.status}`,
427
+ targetUrl: url
427
428
  });
428
429
  }
429
430
 
430
431
  const data = response.data;
431
432
  res.json({ success: true, data });
432
433
  } catch (error) {
433
- res.status(500).json({ error: error.message });
434
+ res.status(500).json({ error: error.message, targetUrl: url });
434
435
  }
435
436
  });
436
437
 
@@ -8,8 +8,8 @@ router.get('/', (_req, res) => {
8
8
  res.json(listStaticPages());
9
9
  });
10
10
 
11
- // Update static pages
12
- router.post('/', (req, res) => {
11
+ // Save all static pages
12
+ router.put('/', (req, res) => {
13
13
  if (!Array.isArray(req.body)) {
14
14
  return res.status(400).json({ error: 'Expected an array of static pages.' });
15
15
  }
@@ -17,4 +17,66 @@ router.post('/', (req, res) => {
17
17
  res.json(listStaticPages());
18
18
  });
19
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
+
20
82
  export default router;
@@ -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();
@@ -14,7 +15,7 @@ const savedPortHistory = new Map(listPortHistory().map((record) => [record.port,
14
15
  const supervisorPath = fileURLToPath(new URL('./script-supervisor.js', import.meta.url));
15
16
  const execFileAsync = promisify(execFile);
16
17
  const keyFor = (launcherId, scriptId) => `${launcherId}:${scriptId}`;
17
- function countErrorBlocks(text, isStderr = false) {
18
+ export function countErrorBlocks(text, isStderr = false) {
18
19
  if (!text) return 0;
19
20
  const lines = text.split(/\r?\n/);
20
21
  let count = 0;
@@ -27,14 +28,32 @@ function countErrorBlocks(text, isStderr = false) {
27
28
  continue;
28
29
  }
29
30
 
31
+ const cleanLine = trimmed
32
+ .replace(/[\u001b\x1b]\[[0-9;]*[a-zA-ZKk]/g, '')
33
+ .replace(/[\u001b\x1b](?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
34
+
35
+ const isNonErrorLine =
36
+ /<i>|<s>/i.test(cleanLine) ||
37
+ /^\s*<(?:i|s|w|info|success|warn|warning)>/i.test(cleanLine) ||
38
+ /^\s*\[(?:info|success|warn|warning|note|notice)\]/i.test(cleanLine) ||
39
+ /^(?:npm WARN|yarn warning|pnpm warning)/i.test(cleanLine) ||
40
+ /^\s*\((?:node:\d+|node:|Use\b)/i.test(cleanLine) ||
41
+ /^[โ„นโœ”]/u.test(cleanLine) ||
42
+ /\b(?:compiled successfully|build completed|done in)\b/i.test(cleanLine);
43
+
44
+ if (isNonErrorLine) {
45
+ inErrorBlock = false;
46
+ continue;
47
+ }
48
+
30
49
  const isNewHeader =
31
- /^(?:\[?(?:error|fatal|fail|failed)\]?|error:|uncaught|fatal:)/i.test(trimmed) ||
32
- /^(?:TypeError|ReferenceError|SyntaxError|RangeError|EvalError|URIError|Error|UnhandledPromiseRejectionWarning|UnhandledPromiseRejection):/i.test(trimmed) ||
33
- /^npm ERR! (?:code|syscall|path|errno)/i.test(trimmed) ||
34
- /^\[vite\] Internal server error/i.test(trimmed) ||
35
- /\bTS\d{4,5}\b/i.test(trimmed) ||
36
- /\berror TS\d+/i.test(trimmed) ||
37
- /^(?:\[TypeScript\]|TS ERROR)/i.test(trimmed);
50
+ /^(?:\[?(?:error|fatal|fail|failed)\]?|error:|uncaught|fatal:)/i.test(cleanLine) ||
51
+ /^(?:TypeError|ReferenceError|SyntaxError|RangeError|EvalError|URIError|Error|UnhandledPromiseRejectionWarning|UnhandledPromiseRejection):/i.test(cleanLine) ||
52
+ /^npm ERR! (?:code|syscall|path|errno)/i.test(cleanLine) ||
53
+ /^\[vite\] Internal server error/i.test(cleanLine) ||
54
+ /\bTS\d{4,5}\b/i.test(cleanLine) ||
55
+ /\berror TS\d+/i.test(cleanLine) ||
56
+ /^(?:\[TypeScript\]|TS ERROR)/i.test(cleanLine);
38
57
 
39
58
  if (isNewHeader) {
40
59
  inErrorBlock = false;
@@ -43,9 +62,9 @@ function countErrorBlocks(text, isStderr = false) {
43
62
  const isErrorLine =
44
63
  isStderr ||
45
64
  isNewHeader ||
46
- /\b(error|failed|fatal|exception)\b/i.test(trimmed) ||
47
- /^npm ERR!/i.test(trimmed) ||
48
- /\bTS\d{4,5}\b/i.test(trimmed) ||
65
+ /\b(error|failed|fatal|exception)\b/i.test(cleanLine) ||
66
+ /^npm ERR!/i.test(cleanLine) ||
67
+ /\bTS\d{4,5}\b/i.test(cleanLine) ||
49
68
  /^\s*at\s+[\w\d_$.<>]+\s+\(/i.test(line);
50
69
 
51
70
  if (isErrorLine) {
@@ -217,6 +236,13 @@ export function runScript(launcher, script) {
217
236
  child.on('exit', (code) => {
218
237
  appendLog(key, code === 0 ? 'output' : 'error', `\nProcess exited with code ${code}.\n`);
219
238
  const item = logs.get(key);
239
+ if (code !== null && code !== 0) {
240
+ addErrorRecord({
241
+ source: `Launcher: ${launcher.alias}`,
242
+ message: `Script "${script.name}" exited with code ${code}`,
243
+ details: item?.error || item?.output || `Command: ${script.command}`
244
+ });
245
+ }
220
246
  if (item) item.errorCount = 0;
221
247
  running.delete(key);
222
248
  retireGroup(child.pid);
package/server.js CHANGED
@@ -16,13 +16,48 @@ import prReviewRoutes from './server/routes/pr-review.js';
16
16
  import jiraFiltersRoutes from './server/routes/jira-filters.js';
17
17
  import todoRoutes from './server/routes/todos.js';
18
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';
19
21
  import { startClipboardCapture } from './server/services/clipboard-history.js';
20
22
 
21
23
  const port = Number(process.env.PORT || 3100);
22
24
  const app = express();
23
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
+
24
56
  app.use(express.static(paths.ui));
25
57
  app.use('/plugins', express.static(paths.plugins));
58
+ app.use('/pages', express.static(paths.pages));
59
+ app.use('/pages', express.static(paths.dataPages));
60
+ app.use('/data/pages', express.static(paths.dataPages));
26
61
  app.use('/api/launchers', launcherRoutes);
27
62
  app.use('/api/plugins', pluginRoutes);
28
63
  app.use('/api/clipboard', clipboardRoutes);
@@ -33,6 +68,16 @@ app.use('/api/pr-review', prReviewRoutes);
33
68
  app.use('/api/jira-filters', jiraFiltersRoutes);
34
69
  app.use('/api/todos', todoRoutes);
35
70
  app.use('/api/static-pages', staticPagesRoutes);
71
+ app.use('/api/errors', errorRoutes);
72
+
73
+ app.use((err, req, res, _next) => {
74
+ addErrorRecord({
75
+ source: `Backend API (${req.method} ${req.path})`,
76
+ message: err.message || 'Internal Server Error',
77
+ details: err.stack || String(err)
78
+ });
79
+ res.status(500).json({ error: err.message || 'Internal Server Error' });
80
+ });
36
81
 
37
82
  await freePort(port);
38
83
  const server = app.listen(port, () => { console.log(`Buddy Workbench: http://localhost:${port} (pid ${process.pid})`); shutdownTrace(`server started (pid=${process.pid})`); });
@@ -57,6 +102,14 @@ async function shutdown(exitCode = 0) {
57
102
  process.once('SIGINT', () => { shutdownTrace('received SIGINT'); void shutdown(); });
58
103
  process.once('SIGTERM', () => { shutdownTrace('received SIGTERM'); void shutdown(); });
59
104
  process.once('SIGHUP', () => { shutdownTrace('received SIGHUP'); void shutdown(); });
60
- process.once('uncaughtException', (error) => { console.error(error); void shutdown(1); });
61
- process.once('unhandledRejection', (error) => { console.error(error); void shutdown(1); });
105
+ process.once('uncaughtException', (error) => {
106
+ addErrorRecord({ source: 'Server UncaughtException', message: error.message || 'Uncaught Server Exception', details: error.stack || String(error) });
107
+ console.error(error);
108
+ void shutdown(1);
109
+ });
110
+ process.once('unhandledRejection', (error) => {
111
+ addErrorRecord({ source: 'Server UnhandledRejection', message: error?.message || String(error || 'Unhandled Promise Rejection'), details: error?.stack || String(error) });
112
+ console.error(error);
113
+ void shutdown(1);
114
+ });
62
115
  process.once('exit', () => stopAllScripts(true));