@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.
Files changed (64) hide show
  1. package/dist/api/events.js +45 -0
  2. package/dist/api/server.js +195 -12
  3. package/package.json +2 -2
  4. package/src/ui-dist/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
  5. package/src/ui-dist/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
  6. package/src/ui-dist/assets/ibm-plex-mono-cyrillic-500-normal-Ael50iVv.woff +0 -0
  7. package/src/ui-dist/assets/ibm-plex-mono-cyrillic-500-normal-Bq9vWWag.woff2 +0 -0
  8. package/src/ui-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
  9. package/src/ui-dist/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
  10. package/src/ui-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BIfNGwUT.woff +0 -0
  11. package/src/ui-dist/assets/ibm-plex-mono-cyrillic-ext-500-normal-BqneJy0T.woff2 +0 -0
  12. package/src/ui-dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  13. package/src/ui-dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  14. package/src/ui-dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  15. package/src/ui-dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  16. package/src/ui-dist/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
  17. package/src/ui-dist/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
  18. package/src/ui-dist/assets/ibm-plex-mono-latin-ext-500-normal-CAhNIIs5.woff2 +0 -0
  19. package/src/ui-dist/assets/ibm-plex-mono-latin-ext-500-normal-CZ70TYgx.woff +0 -0
  20. package/src/ui-dist/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
  21. package/src/ui-dist/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
  22. package/src/ui-dist/assets/ibm-plex-mono-vietnamese-500-normal-C8zxqsMH.woff +0 -0
  23. package/src/ui-dist/assets/ibm-plex-mono-vietnamese-500-normal-DZ4AoWbu.woff2 +0 -0
  24. package/src/ui-dist/assets/ibm-plex-sans-cyrillic-400-normal-BTotfTJu.woff +0 -0
  25. package/src/ui-dist/assets/ibm-plex-sans-cyrillic-400-normal-DZqxrq2p.woff2 +0 -0
  26. package/src/ui-dist/assets/ibm-plex-sans-cyrillic-500-normal-ByOcLdNv.woff +0 -0
  27. package/src/ui-dist/assets/ibm-plex-sans-cyrillic-500-normal-CocWQlwt.woff2 +0 -0
  28. package/src/ui-dist/assets/ibm-plex-sans-cyrillic-600-normal-71GNu3SW.woff2 +0 -0
  29. package/src/ui-dist/assets/ibm-plex-sans-cyrillic-600-normal-BGq0mW3O.woff +0 -0
  30. package/src/ui-dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-Dsrv2Tcn.woff +0 -0
  31. package/src/ui-dist/assets/ibm-plex-sans-cyrillic-ext-400-normal-g30qAdWV.woff2 +0 -0
  32. package/src/ui-dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-Cs5J6C77.woff2 +0 -0
  33. package/src/ui-dist/assets/ibm-plex-sans-cyrillic-ext-500-normal-DB5PtV2g.woff +0 -0
  34. package/src/ui-dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-Bz0x94Yp.woff +0 -0
  35. package/src/ui-dist/assets/ibm-plex-sans-cyrillic-ext-600-normal-DUMzJB7m.woff2 +0 -0
  36. package/src/ui-dist/assets/ibm-plex-sans-greek-400-normal-D9ESIMu3.woff +0 -0
  37. package/src/ui-dist/assets/ibm-plex-sans-greek-400-normal-_efipK4i.woff2 +0 -0
  38. package/src/ui-dist/assets/ibm-plex-sans-greek-500-normal-CuWXN6rf.woff +0 -0
  39. package/src/ui-dist/assets/ibm-plex-sans-greek-500-normal-JMMifIXV.woff2 +0 -0
  40. package/src/ui-dist/assets/ibm-plex-sans-greek-600-normal-D-CqTdkO.woff +0 -0
  41. package/src/ui-dist/assets/ibm-plex-sans-greek-600-normal-DzTrcv_p.woff2 +0 -0
  42. package/src/ui-dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  43. package/src/ui-dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  44. package/src/ui-dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  45. package/src/ui-dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  46. package/src/ui-dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  47. package/src/ui-dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  48. package/src/ui-dist/assets/ibm-plex-sans-latin-ext-400-normal-C5H60-Va.woff2 +0 -0
  49. package/src/ui-dist/assets/ibm-plex-sans-latin-ext-400-normal-RBey6euL.woff +0 -0
  50. package/src/ui-dist/assets/ibm-plex-sans-latin-ext-500-normal-D0aIdm-b.woff +0 -0
  51. package/src/ui-dist/assets/ibm-plex-sans-latin-ext-500-normal-DakdToA3.woff2 +0 -0
  52. package/src/ui-dist/assets/ibm-plex-sans-latin-ext-600-normal-DIrixKbi.woff +0 -0
  53. package/src/ui-dist/assets/ibm-plex-sans-latin-ext-600-normal-DOrvGEcy.woff2 +0 -0
  54. package/src/ui-dist/assets/ibm-plex-sans-vietnamese-400-normal-DG4YqDda.woff2 +0 -0
  55. package/src/ui-dist/assets/ibm-plex-sans-vietnamese-400-normal-fK1oJ5dG.woff +0 -0
  56. package/src/ui-dist/assets/ibm-plex-sans-vietnamese-500-normal-BEb3_waV.woff +0 -0
  57. package/src/ui-dist/assets/ibm-plex-sans-vietnamese-500-normal-e4dixQRQ.woff2 +0 -0
  58. package/src/ui-dist/assets/ibm-plex-sans-vietnamese-600-normal-DgdngZtN.woff +0 -0
  59. package/src/ui-dist/assets/ibm-plex-sans-vietnamese-600-normal-DpPYBSTl.woff2 +0 -0
  60. package/src/ui-dist/assets/index-BEo79vjN.js +87 -0
  61. package/src/ui-dist/assets/index-DNdosrlQ.css +1 -0
  62. package/src/ui-dist/index.html +14 -0
  63. package/src/ui/index.html +0 -1452
  64. 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?.();
@@ -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
- const PORT = parseInt(process.env.WAYMARK_PORT || '3001', 10);
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
- // Serve UI path works for both ts-node (src/api/) and compiled (dist/api/)
100
- const UI_DIR = path.resolve(__dirname, '../../src/ui');
101
- app.use(express_1.default.static(UI_DIR));
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 from .waymark/config.json
431
- app.get('/api/project', (req, res) => {
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 configPath = path.join(process.env.WAYMARK_PROJECT_ROOT || process.cwd(), '.waymark', 'config.json');
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({ projectName: cfg.projectName || null, port: cfg.port || PORT });
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
- app.get('*', (req, res) => {
963
- res.sendFile(path.join(UI_DIR, 'index.html'));
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": "2.0.3",
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",