@way_marks/server 2.0.3 → 4.0.0
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/dist/api/events.js +45 -0
- package/dist/api/server.js +195 -12
- package/package.json +2 -2
- package/src/ui-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
- package/src/ui-dist/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
- package/src/ui-dist/assets/index-BEo79vjN.js +87 -0
- package/src/ui-dist/assets/index-DNdosrlQ.css +1 -0
- package/src/ui-dist/index.html +14 -0
- package/src/ui/index.html +0 -1452
- package/src/ui/index.html.bak +0 -429
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachSubscriber = attachSubscriber;
|
|
4
|
+
exports.emit = emit;
|
|
5
|
+
const subscribers = new Set();
|
|
6
|
+
function attachSubscriber(res) {
|
|
7
|
+
// SSE headers
|
|
8
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
9
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
10
|
+
res.setHeader('Connection', 'keep-alive');
|
|
11
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
12
|
+
res.flushHeaders?.();
|
|
13
|
+
const sub = { res, closed: false };
|
|
14
|
+
subscribers.add(sub);
|
|
15
|
+
// Initial hello so the client opens onmessage cleanly
|
|
16
|
+
res.write(`event: hello\ndata: {"ok":true}\n\n`);
|
|
17
|
+
return () => {
|
|
18
|
+
sub.closed = true;
|
|
19
|
+
subscribers.delete(sub);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
function emit(topic, payload = {}) {
|
|
23
|
+
const data = JSON.stringify({ topic, ...payload });
|
|
24
|
+
const frame = `event: ${topic}\ndata: ${data}\n\n`;
|
|
25
|
+
for (const sub of subscribers) {
|
|
26
|
+
if (sub.closed)
|
|
27
|
+
continue;
|
|
28
|
+
try {
|
|
29
|
+
sub.res.write(frame);
|
|
30
|
+
}
|
|
31
|
+
catch { /* client disconnected; cleanup happens via close handler */ }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Heartbeat keeps proxies / browsers from closing the stream.
|
|
35
|
+
const HEARTBEAT_MS = 25000;
|
|
36
|
+
setInterval(() => {
|
|
37
|
+
for (const sub of subscribers) {
|
|
38
|
+
if (sub.closed)
|
|
39
|
+
continue;
|
|
40
|
+
try {
|
|
41
|
+
sub.res.write(`: heartbeat\n\n`);
|
|
42
|
+
}
|
|
43
|
+
catch { /* noop */ }
|
|
44
|
+
}
|
|
45
|
+
}, HEARTBEAT_MS).unref?.();
|
package/dist/api/server.js
CHANGED
|
@@ -46,6 +46,7 @@ const engine_1 = require("../policies/engine");
|
|
|
46
46
|
const handler_1 = require("../approvals/handler");
|
|
47
47
|
const manager_2 = require("../approval/manager");
|
|
48
48
|
const manager_3 = require("../escalation/manager");
|
|
49
|
+
const events_1 = require("./events");
|
|
49
50
|
// Import registry for Phase 2 hub navigation
|
|
50
51
|
const registryPath = path.join(process.env.HOME || process.env.USERPROFILE || '', '.waymark', 'registry.json');
|
|
51
52
|
function getRegistryProjects() {
|
|
@@ -59,6 +60,42 @@ function getRegistryProjects() {
|
|
|
59
60
|
return [];
|
|
60
61
|
}
|
|
61
62
|
}
|
|
63
|
+
function readRegistry() {
|
|
64
|
+
if (!fs.existsSync(registryPath))
|
|
65
|
+
return { projects: {}, releasedPorts: [] };
|
|
66
|
+
try {
|
|
67
|
+
const r = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
68
|
+
return { projects: r.projects || {}, releasedPorts: r.releasedPorts || [], lastUpdated: r.lastUpdated };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return { projects: {}, releasedPorts: [] };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function writeRegistry(reg) {
|
|
75
|
+
reg.lastUpdated = new Date().toISOString();
|
|
76
|
+
fs.writeFileSync(registryPath, JSON.stringify({ version: 1, ...reg }, null, 2) + '\n');
|
|
77
|
+
}
|
|
78
|
+
function mutateRegistryEntry(id, mutator) {
|
|
79
|
+
const reg = readRegistry();
|
|
80
|
+
const entry = reg.projects[id];
|
|
81
|
+
if (!entry)
|
|
82
|
+
return null;
|
|
83
|
+
mutator(entry);
|
|
84
|
+
reg.projects[id] = entry;
|
|
85
|
+
writeRegistry(reg);
|
|
86
|
+
return entry;
|
|
87
|
+
}
|
|
88
|
+
function tryKill(pid) {
|
|
89
|
+
if (!pid)
|
|
90
|
+
return false;
|
|
91
|
+
try {
|
|
92
|
+
process.kill(pid, 'SIGTERM');
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
62
99
|
// Phase 4: Garbage collection for registry
|
|
63
100
|
function garbageCollectRegistryFile() {
|
|
64
101
|
try {
|
|
@@ -93,12 +130,42 @@ function garbageCollectRegistryFile() {
|
|
|
93
130
|
}
|
|
94
131
|
}
|
|
95
132
|
const app = (0, express_1.default)();
|
|
96
|
-
|
|
133
|
+
// Fallback only — `waymark start` always passes WAYMARK_PORT explicitly.
|
|
134
|
+
// 47000 is the new default range (47000-47999); 3001 was the legacy default.
|
|
135
|
+
const PORT = parseInt(process.env.WAYMARK_PORT || '47000', 10);
|
|
97
136
|
app.use(express_1.default.json());
|
|
98
137
|
app.use(express_1.default.urlencoded({ extended: true }));
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
138
|
+
// Same-machine peer CORS for the Hub view: another Waymark dashboard on a
|
|
139
|
+
// different localhost port may probe this server's /api/* (e.g. /api/stats,
|
|
140
|
+
// /api/hub/*). Allow it without opening up arbitrary remote origins.
|
|
141
|
+
app.use((req, res, next) => {
|
|
142
|
+
const origin = req.headers.origin;
|
|
143
|
+
if (typeof origin === 'string' && /^http:\/\/(localhost|127\.0\.0\.1):\d+$/.test(origin)) {
|
|
144
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
145
|
+
res.setHeader('Vary', 'Origin');
|
|
146
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
|
|
147
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
148
|
+
if (req.method === 'OPTIONS')
|
|
149
|
+
return res.sendStatus(204);
|
|
150
|
+
}
|
|
151
|
+
next();
|
|
152
|
+
});
|
|
153
|
+
// Serve UI — path works for both ts-node (src/api/) and compiled (dist/api/).
|
|
154
|
+
const UI_DIR = path.resolve(__dirname, '../../src/ui-dist');
|
|
155
|
+
const UI_INDEX = path.join(UI_DIR, 'index.html');
|
|
156
|
+
const UI_BUILT = fs.existsSync(UI_INDEX);
|
|
157
|
+
if (UI_BUILT) {
|
|
158
|
+
app.use(express_1.default.static(UI_DIR));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
console.warn('[waymark] ui-dist/ not found — dashboard will return a setup banner. ' +
|
|
162
|
+
'Run `npm run build -w @way_marks/web` to build the dashboard.');
|
|
163
|
+
}
|
|
164
|
+
// GET /api/events — Server-Sent Events stream for live UI updates
|
|
165
|
+
app.get('/api/events', (req, res) => {
|
|
166
|
+
const detach = (0, events_1.attachSubscriber)(res);
|
|
167
|
+
req.on('close', detach);
|
|
168
|
+
});
|
|
102
169
|
// GET /api/actions — list all actions (or ?count=true for pending count)
|
|
103
170
|
app.get('/api/actions', (req, res) => {
|
|
104
171
|
try {
|
|
@@ -192,6 +259,7 @@ app.post('/api/actions/:action_id/approve', async (req, res) => {
|
|
|
192
259
|
const status = result.error === 'Action not found' ? 404 : 400;
|
|
193
260
|
return res.status(status).json({ error: result.error });
|
|
194
261
|
}
|
|
262
|
+
(0, events_1.emit)('actions', { action_id: req.params.action_id, kind: 'approved' });
|
|
195
263
|
res.json(result);
|
|
196
264
|
}
|
|
197
265
|
catch (err) {
|
|
@@ -207,6 +275,7 @@ app.post('/api/actions/:action_id/reject', async (req, res) => {
|
|
|
207
275
|
const status = result.error === 'Action not found' ? 404 : 400;
|
|
208
276
|
return res.status(status).json({ error: result.error });
|
|
209
277
|
}
|
|
278
|
+
(0, events_1.emit)('actions', { action_id: req.params.action_id, kind: 'rejected' });
|
|
210
279
|
res.json(result);
|
|
211
280
|
}
|
|
212
281
|
catch (err) {
|
|
@@ -266,12 +335,14 @@ app.post('/api/actions/:action_id/rollback', (req, res) => {
|
|
|
266
335
|
if (!action.before_snapshot) {
|
|
267
336
|
fs.unlinkSync(action.target_path);
|
|
268
337
|
(0, database_1.markRolledBack)(action.action_id);
|
|
338
|
+
(0, events_1.emit)('actions', { action_id: action.action_id, kind: 'rolled_back' });
|
|
269
339
|
return res.json({ success: true, action: 'deleted', message: `Deleted new file: ${action.target_path}` });
|
|
270
340
|
}
|
|
271
341
|
// Restore file to before_snapshot
|
|
272
342
|
fs.mkdirSync(path.dirname(action.target_path), { recursive: true });
|
|
273
343
|
fs.writeFileSync(action.target_path, action.before_snapshot, 'utf8');
|
|
274
344
|
(0, database_1.markRolledBack)(action.action_id);
|
|
345
|
+
(0, events_1.emit)('actions', { action_id: action.action_id, kind: 'rolled_back' });
|
|
275
346
|
res.json({ success: true, action: 'restored', message: `Restored ${action.target_path} to previous state` });
|
|
276
347
|
}
|
|
277
348
|
catch (err) {
|
|
@@ -384,6 +455,8 @@ app.post('/api/sessions/:session_id/rollback', (req, res) => {
|
|
|
384
455
|
message: 'Rollback failed',
|
|
385
456
|
});
|
|
386
457
|
}
|
|
458
|
+
(0, events_1.emit)('sessions', { session_id, kind: 'rolled_back', count: actions.length });
|
|
459
|
+
(0, events_1.emit)('actions', { session_id, kind: 'session_rolled_back' });
|
|
387
460
|
res.json({
|
|
388
461
|
success: true,
|
|
389
462
|
message: `Rolled back session ${session_id}`,
|
|
@@ -427,15 +500,22 @@ app.get('/api/config', (req, res) => {
|
|
|
427
500
|
res.status(500).json({ error: err.message });
|
|
428
501
|
}
|
|
429
502
|
});
|
|
430
|
-
// GET /api/project — returns project metadata
|
|
431
|
-
|
|
503
|
+
// GET /api/project — returns live project metadata for the running server.
|
|
504
|
+
// Source of truth: .waymark/config.json (written by `waymark start`).
|
|
505
|
+
// Falls back to env-only state when running stand-alone (no .waymark/ yet).
|
|
506
|
+
app.get('/api/project', (_req, res) => {
|
|
432
507
|
try {
|
|
433
|
-
const
|
|
508
|
+
const projectRoot = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
|
|
509
|
+
const configPath = path.join(projectRoot, '.waymark', 'config.json');
|
|
434
510
|
if (!fs.existsSync(configPath)) {
|
|
435
|
-
return res.json({ projectName: null, port: PORT });
|
|
511
|
+
return res.json({ projectName: null, port: PORT, projectRoot });
|
|
436
512
|
}
|
|
437
513
|
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
438
|
-
res.json({
|
|
514
|
+
res.json({
|
|
515
|
+
projectName: cfg.projectName || null,
|
|
516
|
+
port: cfg.port || PORT,
|
|
517
|
+
projectRoot: cfg.projectRoot || projectRoot,
|
|
518
|
+
});
|
|
439
519
|
}
|
|
440
520
|
catch (err) {
|
|
441
521
|
res.status(500).json({ error: err.message });
|
|
@@ -462,6 +542,86 @@ app.post('/api/registry/cleanup', (req, res) => {
|
|
|
462
542
|
}
|
|
463
543
|
});
|
|
464
544
|
// ============================================================================
|
|
545
|
+
// Hub: cross-project mutations driven from any peer's dashboard.
|
|
546
|
+
// All write to ~/.waymark/registry.json the same way the CLI does.
|
|
547
|
+
// ============================================================================
|
|
548
|
+
// POST /api/hub/projects/:id/pause — flip status to paused (port retained)
|
|
549
|
+
app.post('/api/hub/projects/:id/pause', (req, res) => {
|
|
550
|
+
try {
|
|
551
|
+
const { id } = req.params;
|
|
552
|
+
const updated = mutateRegistryEntry(id, (e) => {
|
|
553
|
+
e.status = 'paused';
|
|
554
|
+
e.pausedAt = new Date().toISOString();
|
|
555
|
+
});
|
|
556
|
+
if (!updated)
|
|
557
|
+
return res.status(404).json({ error: `Project not found: ${id}` });
|
|
558
|
+
res.json({ success: true, project: updated });
|
|
559
|
+
}
|
|
560
|
+
catch (err) {
|
|
561
|
+
res.status(500).json({ error: err.message });
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
// POST /api/hub/projects/:id/resume — flip status back to running
|
|
565
|
+
app.post('/api/hub/projects/:id/resume', (req, res) => {
|
|
566
|
+
try {
|
|
567
|
+
const { id } = req.params;
|
|
568
|
+
const updated = mutateRegistryEntry(id, (e) => {
|
|
569
|
+
e.status = 'running';
|
|
570
|
+
delete e.pausedAt;
|
|
571
|
+
});
|
|
572
|
+
if (!updated)
|
|
573
|
+
return res.status(404).json({ error: `Project not found: ${id}` });
|
|
574
|
+
res.json({ success: true, project: updated });
|
|
575
|
+
}
|
|
576
|
+
catch (err) {
|
|
577
|
+
res.status(500).json({ error: err.message });
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
// POST /api/hub/projects/:id/stop — SIGTERM the peer's mcp+api, mark stopped,
|
|
581
|
+
// release the port for reuse. Mirrors `waymark stop` behaviour without needing
|
|
582
|
+
// the user to cd into the other project.
|
|
583
|
+
app.post('/api/hub/projects/:id/stop', (req, res) => {
|
|
584
|
+
try {
|
|
585
|
+
const { id } = req.params;
|
|
586
|
+
const reg = readRegistry();
|
|
587
|
+
const entry = reg.projects[id];
|
|
588
|
+
if (!entry)
|
|
589
|
+
return res.status(404).json({ error: `Project not found: ${id}` });
|
|
590
|
+
const killedApi = tryKill(entry.api_pid);
|
|
591
|
+
const killedMcp = tryKill(entry.mcp_pid);
|
|
592
|
+
entry.status = 'stopped';
|
|
593
|
+
entry.stoppedAt = new Date().toISOString();
|
|
594
|
+
reg.projects[id] = entry;
|
|
595
|
+
// Release port for reuse (mirrors registry.releasePort behaviour).
|
|
596
|
+
if (entry.port && Array.isArray(reg.releasedPorts)) {
|
|
597
|
+
reg.releasedPorts.push(entry.port);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
reg.releasedPorts = [entry.port];
|
|
601
|
+
}
|
|
602
|
+
writeRegistry(reg);
|
|
603
|
+
res.json({
|
|
604
|
+
success: true,
|
|
605
|
+
project: entry,
|
|
606
|
+
killed: { api: killedApi, mcp: killedMcp },
|
|
607
|
+
message: killedApi || killedMcp ? `Stopped ${id}.` : `${id} was not running (registry cleaned).`,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
res.status(500).json({ error: err.message });
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
// POST /api/hub/gc — alias of /api/registry/cleanup; convenient from the Hub UI.
|
|
615
|
+
app.post('/api/hub/gc', (_req, res) => {
|
|
616
|
+
try {
|
|
617
|
+
const removed = garbageCollectRegistryFile();
|
|
618
|
+
res.json({ success: true, removed, message: `Garbage collected ${removed} stale entries` });
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
res.status(500).json({ error: err.message });
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
// ============================================================================
|
|
465
625
|
// PHASE 2: Team Approval Routing Endpoints
|
|
466
626
|
// ============================================================================
|
|
467
627
|
// GET /api/team/members — list all team members
|
|
@@ -488,6 +648,7 @@ app.post('/api/team/members', (req, res) => {
|
|
|
488
648
|
return res.status(400).json({ error: `Email ${email} already in use` });
|
|
489
649
|
}
|
|
490
650
|
(0, database_1.addTeamMember)(member_id, name, email, added_by, slack_id);
|
|
651
|
+
(0, events_1.emit)('team', { member_id, kind: 'added' });
|
|
491
652
|
res.json({ success: true, member_id, message: `Added team member: ${name}` });
|
|
492
653
|
}
|
|
493
654
|
catch (err) {
|
|
@@ -503,6 +664,7 @@ app.delete('/api/team/members/:member_id', (req, res) => {
|
|
|
503
664
|
return res.status(404).json({ error: `Team member ${member_id} not found` });
|
|
504
665
|
}
|
|
505
666
|
(0, database_1.removeTeamMember)(member_id);
|
|
667
|
+
(0, events_1.emit)('team', { member_id, kind: 'removed' });
|
|
506
668
|
res.json({ success: true, message: `Removed team member: ${member.name}` });
|
|
507
669
|
}
|
|
508
670
|
catch (err) {
|
|
@@ -528,6 +690,7 @@ app.post('/api/approval-routes', (req, res) => {
|
|
|
528
690
|
return res.status(400).json({ error: 'Missing required fields: route_id, name, approver_ids (array)' });
|
|
529
691
|
}
|
|
530
692
|
(0, database_1.addApprovalRoute)(route_id, name, approver_ids, created_by, description, condition_type, condition_json);
|
|
693
|
+
(0, events_1.emit)('approval-routes', { route_id, kind: 'added' });
|
|
531
694
|
res.json({ success: true, route_id, message: `Created approval route: ${name}` });
|
|
532
695
|
}
|
|
533
696
|
catch (err) {
|
|
@@ -544,6 +707,7 @@ app.put('/api/approval-routes/:route_id', (req, res) => {
|
|
|
544
707
|
return res.status(404).json({ error: `Approval route ${route_id} not found` });
|
|
545
708
|
}
|
|
546
709
|
(0, database_1.updateApprovalRoute)(route_id, { name, description, approver_ids });
|
|
710
|
+
(0, events_1.emit)('approval-routes', { route_id, kind: 'updated' });
|
|
547
711
|
res.json({ success: true, message: `Updated approval route: ${route_id}` });
|
|
548
712
|
}
|
|
549
713
|
catch (err) {
|
|
@@ -559,6 +723,7 @@ app.delete('/api/approval-routes/:route_id', (req, res) => {
|
|
|
559
723
|
return res.status(404).json({ error: `Approval route ${route_id} not found` });
|
|
560
724
|
}
|
|
561
725
|
(0, database_1.deleteApprovalRoute)(route_id);
|
|
726
|
+
(0, events_1.emit)('approval-routes', { route_id, kind: 'deleted' });
|
|
562
727
|
res.json({ success: true, message: `Deleted approval route: ${route_id}` });
|
|
563
728
|
}
|
|
564
729
|
catch (err) {
|
|
@@ -608,6 +773,7 @@ app.post('/api/approvals/:request_id/approve', (req, res) => {
|
|
|
608
773
|
return res.status(400).json({ error: 'Missing required field: approver_id' });
|
|
609
774
|
}
|
|
610
775
|
const status = (0, manager_2.submitApprovalDecision)(request_id, approver_id, 'approve', reason);
|
|
776
|
+
(0, events_1.emit)('approvals', { request_id, kind: 'approve', approver_id });
|
|
611
777
|
res.json({
|
|
612
778
|
success: true,
|
|
613
779
|
message: `Approved by ${approver_id}`,
|
|
@@ -627,6 +793,7 @@ app.post('/api/approvals/:request_id/reject', (req, res) => {
|
|
|
627
793
|
return res.status(400).json({ error: 'Missing required field: approver_id' });
|
|
628
794
|
}
|
|
629
795
|
const status = (0, manager_2.submitApprovalDecision)(request_id, approver_id, 'reject', reason);
|
|
796
|
+
(0, events_1.emit)('approvals', { request_id, kind: 'reject', approver_id });
|
|
630
797
|
res.json({
|
|
631
798
|
success: true,
|
|
632
799
|
message: `Rejected by ${approver_id}`,
|
|
@@ -674,6 +841,7 @@ app.post('/api/escalations/rules', (req, res) => {
|
|
|
674
841
|
return res.status(400).json({ error: 'Missing required fields: rule_id, name, escalation_targets (array)' });
|
|
675
842
|
}
|
|
676
843
|
(0, database_1.addEscalationRule)(rule_id, name, escalation_targets, created_by, description, timeout_hours);
|
|
844
|
+
(0, events_1.emit)('escalation-rules', { rule_id, kind: 'added' });
|
|
677
845
|
res.json({ success: true, rule_id, message: `Created escalation rule: ${name}` });
|
|
678
846
|
}
|
|
679
847
|
catch (err) {
|
|
@@ -690,6 +858,7 @@ app.put('/api/escalations/rules/:rule_id', (req, res) => {
|
|
|
690
858
|
return res.status(404).json({ error: `Escalation rule ${rule_id} not found` });
|
|
691
859
|
}
|
|
692
860
|
(0, database_1.updateEscalationRule)(rule_id, { name, description, escalation_targets, timeout_hours });
|
|
861
|
+
(0, events_1.emit)('escalation-rules', { rule_id, kind: 'updated' });
|
|
693
862
|
res.json({ success: true, message: `Updated escalation rule: ${rule_id}` });
|
|
694
863
|
}
|
|
695
864
|
catch (err) {
|
|
@@ -705,6 +874,7 @@ app.delete('/api/escalations/rules/:rule_id', (req, res) => {
|
|
|
705
874
|
return res.status(404).json({ error: `Escalation rule ${rule_id} not found` });
|
|
706
875
|
}
|
|
707
876
|
(0, database_1.deleteEscalationRule)(rule_id);
|
|
877
|
+
(0, events_1.emit)('escalation-rules', { rule_id, kind: 'deleted' });
|
|
708
878
|
res.json({ success: true, message: `Deleted escalation rule: ${rule_id}` });
|
|
709
879
|
}
|
|
710
880
|
catch (err) {
|
|
@@ -756,6 +926,8 @@ app.post('/api/escalations/:request_id/decide', (req, res) => {
|
|
|
756
926
|
return res.status(400).json({ error: 'Decision must be "proceed" or "block"' });
|
|
757
927
|
}
|
|
758
928
|
const status = (0, manager_3.submitEscalationDecision)(request_id, target_id, decision, reason);
|
|
929
|
+
(0, events_1.emit)('escalations', { request_id, kind: 'decided', decision, target_id });
|
|
930
|
+
(0, events_1.emit)('approvals', { request_id, kind: 'escalation_decided' });
|
|
759
931
|
res.json({
|
|
760
932
|
success: true,
|
|
761
933
|
message: `${target_id} decided to ${decision}`,
|
|
@@ -958,9 +1130,20 @@ app.delete('/api/remediation/policies/:policy_id', (req, res) => {
|
|
|
958
1130
|
res.status(500).json({ error: err.message });
|
|
959
1131
|
}
|
|
960
1132
|
});
|
|
961
|
-
// Fallback: serve UI for any unmatched route
|
|
962
|
-
|
|
963
|
-
|
|
1133
|
+
// Fallback: serve UI for any unmatched route. If the dashboard hasn't been
|
|
1134
|
+
// built yet, emit a friendly setup banner instead of a 404.
|
|
1135
|
+
app.get('*', (_req, res) => {
|
|
1136
|
+
if (UI_BUILT) {
|
|
1137
|
+
return res.sendFile(UI_INDEX);
|
|
1138
|
+
}
|
|
1139
|
+
res.status(503).type('html').send(`<!doctype html>
|
|
1140
|
+
<html><head><meta charset="utf-8"/><title>Waymark — dashboard not built</title>
|
|
1141
|
+
<style>body{font:14px/1.5 -apple-system,system-ui,sans-serif;color:#1d2126;background:#fafaf8;display:grid;place-items:center;min-height:100vh;margin:0}main{max-width:520px;padding:32px}h1{margin:0 0 8px;font-size:18px}code{background:#ebebe8;padding:2px 6px;border-radius:4px;font-family:ui-monospace,Menlo,monospace}</style>
|
|
1142
|
+
</head><body><main>
|
|
1143
|
+
<h1>Dashboard not built</h1>
|
|
1144
|
+
<p>The Waymark API is running, but the React dashboard hasn't been built yet.</p>
|
|
1145
|
+
<p>Run <code>npm run build -w @way_marks/web</code> from the project root, then refresh.</p>
|
|
1146
|
+
</main></body></html>`);
|
|
964
1147
|
});
|
|
965
1148
|
app.listen(PORT, () => {
|
|
966
1149
|
console.log(`Waymark UI + API running at http://localhost:${PORT}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@way_marks/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"description": "Waymark MCP server and dashboard",
|
|
5
5
|
"author": "Waymark <hello@waymarks.dev>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"files": [
|
|
29
29
|
"dist/**",
|
|
30
|
-
"src/ui/**"
|
|
30
|
+
"src/ui-dist/**"
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsc",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|