@trentapps/manager-protocol 1.1.3 → 1.2.1

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 (142) hide show
  1. package/README.md +164 -17
  2. package/dist/analyzers/CSSAnalyzer.d.ts +180 -8
  3. package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -1
  4. package/dist/analyzers/CSSAnalyzer.js +561 -105
  5. package/dist/analyzers/CSSAnalyzer.js.map +1 -1
  6. package/dist/config/dashboard.d.ts +55 -0
  7. package/dist/config/dashboard.d.ts.map +1 -0
  8. package/dist/config/dashboard.js +103 -0
  9. package/dist/config/dashboard.js.map +1 -0
  10. package/dist/config/index.d.ts +7 -0
  11. package/dist/config/index.d.ts.map +1 -0
  12. package/dist/config/index.js +7 -0
  13. package/dist/config/index.js.map +1 -0
  14. package/dist/dashboard/httpDashboard.d.ts +100 -0
  15. package/dist/dashboard/httpDashboard.d.ts.map +1 -0
  16. package/dist/dashboard/httpDashboard.js +1276 -0
  17. package/dist/dashboard/httpDashboard.js.map +1 -0
  18. package/dist/dashboard/index.d.ts +6 -0
  19. package/dist/dashboard/index.d.ts.map +1 -0
  20. package/dist/dashboard/index.js +7 -0
  21. package/dist/dashboard/index.js.map +1 -0
  22. package/dist/engine/AuditLogger.d.ts +370 -2
  23. package/dist/engine/AuditLogger.d.ts.map +1 -1
  24. package/dist/engine/AuditLogger.js +1064 -24
  25. package/dist/engine/AuditLogger.js.map +1 -1
  26. package/dist/engine/GitHubClient.d.ts +183 -0
  27. package/dist/engine/GitHubClient.d.ts.map +1 -0
  28. package/dist/engine/GitHubClient.js +411 -0
  29. package/dist/engine/GitHubClient.js.map +1 -0
  30. package/dist/engine/RateLimiter.d.ts +5 -3
  31. package/dist/engine/RateLimiter.d.ts.map +1 -1
  32. package/dist/engine/RateLimiter.js +49 -72
  33. package/dist/engine/RateLimiter.js.map +1 -1
  34. package/dist/engine/RuleDependencyAnalyzer.d.ts +73 -0
  35. package/dist/engine/RuleDependencyAnalyzer.d.ts.map +1 -0
  36. package/dist/engine/RuleDependencyAnalyzer.js +475 -0
  37. package/dist/engine/RuleDependencyAnalyzer.js.map +1 -0
  38. package/dist/engine/RulesEngine.d.ts +102 -3
  39. package/dist/engine/RulesEngine.d.ts.map +1 -1
  40. package/dist/engine/RulesEngine.js +326 -21
  41. package/dist/engine/RulesEngine.js.map +1 -1
  42. package/dist/engine/TaskManager.d.ts +10 -14
  43. package/dist/engine/TaskManager.d.ts.map +1 -1
  44. package/dist/engine/TaskManager.js +169 -197
  45. package/dist/engine/TaskManager.js.map +1 -1
  46. package/dist/engine/index.d.ts +3 -0
  47. package/dist/engine/index.d.ts.map +1 -1
  48. package/dist/engine/index.js +5 -0
  49. package/dist/engine/index.js.map +1 -1
  50. package/dist/rules/azure.d.ts.map +1 -1
  51. package/dist/rules/azure.js +12 -14
  52. package/dist/rules/azure.js.map +1 -1
  53. package/dist/rules/compliance.d.ts.map +1 -1
  54. package/dist/rules/compliance.js +23 -41
  55. package/dist/rules/compliance.js.map +1 -1
  56. package/dist/rules/condition-optimizer.d.ts +151 -0
  57. package/dist/rules/condition-optimizer.d.ts.map +1 -0
  58. package/dist/rules/condition-optimizer.js +479 -0
  59. package/dist/rules/condition-optimizer.js.map +1 -0
  60. package/dist/rules/css.d.ts.map +1 -1
  61. package/dist/rules/css.js +538 -0
  62. package/dist/rules/css.js.map +1 -1
  63. package/dist/rules/field-standards.d.ts +1172 -0
  64. package/dist/rules/field-standards.d.ts.map +1 -0
  65. package/dist/rules/field-standards.js +908 -0
  66. package/dist/rules/field-standards.js.map +1 -0
  67. package/dist/rules/flask.d.ts.map +1 -1
  68. package/dist/rules/flask.js +18 -31
  69. package/dist/rules/flask.js.map +1 -1
  70. package/dist/rules/index.d.ts +220 -0
  71. package/dist/rules/index.d.ts.map +1 -1
  72. package/dist/rules/index.js +155 -0
  73. package/dist/rules/index.js.map +1 -1
  74. package/dist/rules/ml-ai.d.ts.map +1 -1
  75. package/dist/rules/ml-ai.js +11 -13
  76. package/dist/rules/ml-ai.js.map +1 -1
  77. package/dist/rules/patterns.d.ts +568 -0
  78. package/dist/rules/patterns.d.ts.map +1 -0
  79. package/dist/rules/patterns.js +1359 -0
  80. package/dist/rules/patterns.js.map +1 -0
  81. package/dist/rules/security.d.ts.map +1 -1
  82. package/dist/rules/security.js +580 -19
  83. package/dist/rules/security.js.map +1 -1
  84. package/dist/rules/shared-patterns.d.ts +268 -0
  85. package/dist/rules/shared-patterns.d.ts.map +1 -0
  86. package/dist/rules/shared-patterns.js +556 -0
  87. package/dist/rules/shared-patterns.js.map +1 -0
  88. package/dist/rules/storage.d.ts +8 -2
  89. package/dist/rules/storage.d.ts.map +1 -1
  90. package/dist/rules/storage.js +541 -3
  91. package/dist/rules/storage.js.map +1 -1
  92. package/dist/rules/stripe.d.ts.map +1 -1
  93. package/dist/rules/stripe.js +19 -26
  94. package/dist/rules/stripe.js.map +1 -1
  95. package/dist/rules/websocket.d.ts.map +1 -1
  96. package/dist/rules/websocket.js +32 -40
  97. package/dist/rules/websocket.js.map +1 -1
  98. package/dist/supervisor/AgentSupervisor.d.ts +52 -0
  99. package/dist/supervisor/AgentSupervisor.d.ts.map +1 -1
  100. package/dist/supervisor/AgentSupervisor.js +120 -1
  101. package/dist/supervisor/AgentSupervisor.js.map +1 -1
  102. package/dist/supervisor/ManagedServerRegistry.d.ts +139 -2
  103. package/dist/supervisor/ManagedServerRegistry.d.ts.map +1 -1
  104. package/dist/supervisor/ManagedServerRegistry.js +590 -6
  105. package/dist/supervisor/ManagedServerRegistry.js.map +1 -1
  106. package/dist/supervisor/ProjectTracker.d.ts +2 -1
  107. package/dist/supervisor/ProjectTracker.d.ts.map +1 -1
  108. package/dist/supervisor/ProjectTracker.js +5 -9
  109. package/dist/supervisor/ProjectTracker.js.map +1 -1
  110. package/dist/testing/index.d.ts +11 -0
  111. package/dist/testing/index.d.ts.map +1 -0
  112. package/dist/testing/index.js +12 -0
  113. package/dist/testing/index.js.map +1 -0
  114. package/dist/testing/rule-tester.d.ts +217 -0
  115. package/dist/testing/rule-tester.d.ts.map +1 -0
  116. package/dist/testing/rule-tester.examples.d.ts +57 -0
  117. package/dist/testing/rule-tester.examples.d.ts.map +1 -0
  118. package/dist/testing/rule-tester.examples.js +375 -0
  119. package/dist/testing/rule-tester.examples.js.map +1 -0
  120. package/dist/testing/rule-tester.js +381 -0
  121. package/dist/testing/rule-tester.js.map +1 -0
  122. package/dist/testing/rule-validator.d.ts +141 -0
  123. package/dist/testing/rule-validator.d.ts.map +1 -0
  124. package/dist/testing/rule-validator.js +640 -0
  125. package/dist/testing/rule-validator.js.map +1 -0
  126. package/dist/types/index.d.ts +265 -4
  127. package/dist/types/index.d.ts.map +1 -1
  128. package/dist/types/index.js +57 -2
  129. package/dist/types/index.js.map +1 -1
  130. package/dist/utils/index.d.ts +2 -0
  131. package/dist/utils/index.d.ts.map +1 -1
  132. package/dist/utils/index.js +2 -0
  133. package/dist/utils/index.js.map +1 -1
  134. package/dist/utils/rate-limiting.d.ts +268 -0
  135. package/dist/utils/rate-limiting.d.ts.map +1 -0
  136. package/dist/utils/rate-limiting.js +403 -0
  137. package/dist/utils/rate-limiting.js.map +1 -0
  138. package/dist/utils/shared.d.ts +306 -0
  139. package/dist/utils/shared.d.ts.map +1 -0
  140. package/dist/utils/shared.js +464 -0
  141. package/dist/utils/shared.js.map +1 -0
  142. package/package.json +3 -2
@@ -0,0 +1,1276 @@
1
+ /**
2
+ * HTTP Dashboard Server for Agent Supervisor
3
+ *
4
+ * Provides a web-based dashboard for monitoring agent activities,
5
+ * managing approvals, viewing tasks, and checking audit logs.
6
+ *
7
+ * Configuration is externalized via environment variables:
8
+ * - DASHBOARD_PORT: Port to listen on (default: 3100)
9
+ * - DASHBOARD_HOST: Host to bind to (default: localhost)
10
+ * - DASHBOARD_AUTH_TOKEN: Optional authentication token
11
+ * - DASHBOARD_CORS_ORIGIN: CORS origin (default: *)
12
+ * - DASHBOARD_CACHE_TTL: Cache TTL in ms (default: 5000)
13
+ * - DASHBOARD_WS_INTERVAL: WebSocket update interval in ms (default: 2000)
14
+ */
15
+ import * as http from 'http';
16
+ import * as crypto from 'crypto';
17
+ import { URL } from 'url';
18
+ import { WebSocketServer, WebSocket } from 'ws';
19
+ import { supervisor } from '../supervisor/AgentSupervisor.js';
20
+ import { projectTracker } from '../supervisor/ProjectTracker.js';
21
+ import { taskManager } from '../engine/TaskManager.js';
22
+ import { loadDashboardConfig, validateDashboardConfig } from '../config/dashboard.js';
23
+ /**
24
+ * Simple in-memory response cache for API endpoints.
25
+ * Caches GET request responses with configurable TTL.
26
+ */
27
+ export class ResponseCache {
28
+ cache = new Map();
29
+ defaultTtlMs;
30
+ cleanupIntervalId = null;
31
+ constructor(defaultTtlMs = 5000) {
32
+ this.defaultTtlMs = defaultTtlMs;
33
+ // Cleanup expired entries every 30 seconds
34
+ this.cleanupIntervalId = setInterval(() => this.cleanup(), 30000);
35
+ }
36
+ /**
37
+ * Generate a cache key from request path and query parameters
38
+ */
39
+ generateKey(path, queryParams) {
40
+ let key = path;
41
+ if (queryParams && queryParams.toString()) {
42
+ // Sort query params for consistent key generation
43
+ const sortedParams = new URLSearchParams([...queryParams.entries()].sort());
44
+ key += '?' + sortedParams.toString();
45
+ }
46
+ return key;
47
+ }
48
+ /**
49
+ * Generate an ETag from response body
50
+ */
51
+ generateEtag(body) {
52
+ return crypto.createHash('md5').update(body).digest('hex');
53
+ }
54
+ /**
55
+ * Get a cached response if available and not expired
56
+ */
57
+ get(key) {
58
+ const entry = this.cache.get(key);
59
+ if (!entry) {
60
+ return null;
61
+ }
62
+ // Check if expired
63
+ if (Date.now() - entry.cachedAt > entry.ttlMs) {
64
+ this.cache.delete(key);
65
+ return null;
66
+ }
67
+ return entry;
68
+ }
69
+ /**
70
+ * Store a response in the cache
71
+ */
72
+ set(key, body, contentType = 'application/json', ttlMs) {
73
+ const entry = {
74
+ body,
75
+ contentType,
76
+ etag: this.generateEtag(body),
77
+ cachedAt: Date.now(),
78
+ ttlMs: ttlMs ?? this.defaultTtlMs
79
+ };
80
+ this.cache.set(key, entry);
81
+ return entry;
82
+ }
83
+ /**
84
+ * Invalidate cache entries matching a pattern
85
+ * Used when mutations occur to ensure fresh data
86
+ */
87
+ invalidate(pattern) {
88
+ let count = 0;
89
+ const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
90
+ for (const key of this.cache.keys()) {
91
+ if (regex.test(key)) {
92
+ this.cache.delete(key);
93
+ count++;
94
+ }
95
+ }
96
+ return count;
97
+ }
98
+ /**
99
+ * Invalidate all cache entries
100
+ */
101
+ invalidateAll() {
102
+ this.cache.clear();
103
+ }
104
+ /**
105
+ * Remove expired entries
106
+ */
107
+ cleanup() {
108
+ const now = Date.now();
109
+ let count = 0;
110
+ for (const [key, entry] of this.cache.entries()) {
111
+ if (now - entry.cachedAt > entry.ttlMs) {
112
+ this.cache.delete(key);
113
+ count++;
114
+ }
115
+ }
116
+ return count;
117
+ }
118
+ /**
119
+ * Get cache statistics
120
+ */
121
+ getStats() {
122
+ return {
123
+ size: this.cache.size,
124
+ defaultTtlMs: this.defaultTtlMs
125
+ };
126
+ }
127
+ /**
128
+ * Stop the cleanup interval
129
+ */
130
+ destroy() {
131
+ if (this.cleanupIntervalId) {
132
+ clearInterval(this.cleanupIntervalId);
133
+ this.cleanupIntervalId = null;
134
+ }
135
+ this.cache.clear();
136
+ }
137
+ }
138
+ // Load configuration from environment
139
+ const dashboardConfig = loadDashboardConfig();
140
+ // Global response cache instance (using config TTL)
141
+ const responseCache = new ResponseCache(dashboardConfig.cacheTtl);
142
+ const routes = [];
143
+ /**
144
+ * Register a route handler
145
+ */
146
+ function route(method, path, handler, options) {
147
+ // Convert path params like :id to named capture groups
148
+ const paramNames = [];
149
+ const patternStr = path.replace(/:(\w+)/g, (_, name) => {
150
+ paramNames.push(name);
151
+ return '([^/]+)';
152
+ });
153
+ routes.push({
154
+ method,
155
+ pattern: new RegExp(`^${patternStr}$`),
156
+ paramNames,
157
+ handler,
158
+ cacheable: options?.cacheable ?? (method === 'GET'),
159
+ cacheInvalidationPatterns: options?.cacheInvalidationPatterns
160
+ });
161
+ }
162
+ /**
163
+ * Send JSON response with caching headers
164
+ */
165
+ function sendJson(res, data, statusCode = 200, cacheEntry) {
166
+ const body = JSON.stringify(data);
167
+ res.setHeader('Content-Type', 'application/json');
168
+ if (cacheEntry) {
169
+ // Add caching headers
170
+ res.setHeader('ETag', `"${cacheEntry.etag}"`);
171
+ res.setHeader('Cache-Control', `private, max-age=${Math.floor(cacheEntry.ttlMs / 1000)}`);
172
+ res.setHeader('X-Cache', 'HIT');
173
+ }
174
+ else {
175
+ // Generate ETag for new responses
176
+ const etag = crypto.createHash('md5').update(body).digest('hex');
177
+ res.setHeader('ETag', `"${etag}"`);
178
+ res.setHeader('Cache-Control', 'private, max-age=5');
179
+ res.setHeader('X-Cache', 'MISS');
180
+ }
181
+ res.statusCode = statusCode;
182
+ res.end(body);
183
+ }
184
+ /**
185
+ * Send 304 Not Modified response
186
+ */
187
+ function sendNotModified(res, etag) {
188
+ res.setHeader('ETag', `"${etag}"`);
189
+ res.setHeader('Cache-Control', 'private, max-age=5');
190
+ res.statusCode = 304;
191
+ res.end();
192
+ }
193
+ /**
194
+ * Send error response
195
+ */
196
+ function sendError(res, message, statusCode = 500) {
197
+ res.setHeader('Content-Type', 'application/json');
198
+ res.setHeader('Cache-Control', 'no-store');
199
+ res.statusCode = statusCode;
200
+ res.end(JSON.stringify({ error: message }));
201
+ }
202
+ /**
203
+ * Parse request body as JSON
204
+ */
205
+ async function parseBody(req) {
206
+ return new Promise((resolve, reject) => {
207
+ const chunks = [];
208
+ req.on('data', chunk => chunks.push(chunk));
209
+ req.on('end', () => {
210
+ const body = Buffer.concat(chunks).toString();
211
+ if (!body) {
212
+ resolve(undefined);
213
+ return;
214
+ }
215
+ try {
216
+ resolve(JSON.parse(body));
217
+ }
218
+ catch {
219
+ reject(new Error('Invalid JSON'));
220
+ }
221
+ });
222
+ req.on('error', reject);
223
+ });
224
+ }
225
+ /**
226
+ * Check If-None-Match header for conditional request
227
+ */
228
+ function checkConditionalRequest(req, etag) {
229
+ const ifNoneMatch = req.headers['if-none-match'];
230
+ if (!ifNoneMatch)
231
+ return false;
232
+ // Handle multiple ETags and weak ETags
233
+ const etags = ifNoneMatch.split(',').map(e => e.trim().replace(/^W\//, '').replace(/"/g, ''));
234
+ return etags.includes(etag) || etags.includes('*');
235
+ }
236
+ // ============================================================================
237
+ // CACHE MIDDLEWARE
238
+ // ============================================================================
239
+ /**
240
+ * Wraps a route handler with caching logic
241
+ */
242
+ function withCache(handler, cacheable = true) {
243
+ return async (req, res, params, body) => {
244
+ // Only cache GET requests
245
+ if (req.method !== 'GET' || !cacheable) {
246
+ return handler(req, res, params, body);
247
+ }
248
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
249
+ const cacheKey = responseCache.generateKey(url.pathname, url.searchParams);
250
+ // Check cache
251
+ const cached = responseCache.get(cacheKey);
252
+ if (cached) {
253
+ // Check conditional request
254
+ if (checkConditionalRequest(req, cached.etag)) {
255
+ sendNotModified(res, cached.etag);
256
+ return;
257
+ }
258
+ // Return cached response
259
+ res.setHeader('Content-Type', cached.contentType);
260
+ res.setHeader('ETag', `"${cached.etag}"`);
261
+ res.setHeader('Cache-Control', `private, max-age=${Math.floor(cached.ttlMs / 1000)}`);
262
+ res.setHeader('X-Cache', 'HIT');
263
+ res.statusCode = 200;
264
+ res.end(cached.body);
265
+ return;
266
+ }
267
+ // Intercept the response to cache it
268
+ const originalEnd = res.end.bind(res);
269
+ let responseBody = '';
270
+ res.end = function (chunk, ...args) {
271
+ if (chunk) {
272
+ responseBody = typeof chunk === 'string' ? chunk : chunk.toString();
273
+ }
274
+ // Only cache successful responses
275
+ if (res.statusCode === 200 && responseBody) {
276
+ const contentType = res.getHeader('Content-Type') || 'application/json';
277
+ const entry = responseCache.set(cacheKey, responseBody, contentType);
278
+ res.setHeader('ETag', `"${entry.etag}"`);
279
+ res.setHeader('X-Cache', 'MISS');
280
+ }
281
+ return originalEnd.apply(res, [chunk, ...args]);
282
+ };
283
+ return handler(req, res, params, body);
284
+ };
285
+ }
286
+ /**
287
+ * Invalidate cache entries after mutation operations
288
+ */
289
+ function invalidateCacheForMutation(patterns) {
290
+ for (const pattern of patterns) {
291
+ responseCache.invalidate(pattern);
292
+ }
293
+ }
294
+ // ============================================================================
295
+ // API ROUTES
296
+ // ============================================================================
297
+ // Health check
298
+ route('GET', '/api/health', async (_req, res) => {
299
+ sendJson(res, {
300
+ status: 'healthy',
301
+ timestamp: new Date().toISOString(),
302
+ cache: responseCache.getStats()
303
+ });
304
+ }, { cacheable: false });
305
+ // Get all tasks
306
+ route('GET', '/api/tasks', async (_req, res) => {
307
+ try {
308
+ const tasks = await taskManager.getTasksByProject();
309
+ sendJson(res, { tasks, count: tasks.length });
310
+ }
311
+ catch (error) {
312
+ sendError(res, error.message);
313
+ }
314
+ }, { cacheable: true, cacheInvalidationPatterns: [/^\/api\/tasks/] });
315
+ // Create task
316
+ route('POST', '/api/tasks', async (_req, res, _params, body) => {
317
+ try {
318
+ const task = await taskManager.createTask(body);
319
+ // Invalidate task-related caches
320
+ invalidateCacheForMutation([/^\/api\/tasks/]);
321
+ sendJson(res, task, 201);
322
+ }
323
+ catch (error) {
324
+ sendError(res, error.message);
325
+ }
326
+ }, { cacheable: false });
327
+ // Get single task
328
+ route('GET', '/api/tasks/:id', async (_req, res, params) => {
329
+ try {
330
+ const task = await taskManager.getTask(undefined, params.id);
331
+ if (!task) {
332
+ sendError(res, 'Task not found', 404);
333
+ return;
334
+ }
335
+ sendJson(res, task);
336
+ }
337
+ catch (error) {
338
+ sendError(res, error.message);
339
+ }
340
+ }, { cacheable: true });
341
+ // Update task
342
+ route('PATCH', '/api/tasks/:id', async (_req, res, params, body) => {
343
+ try {
344
+ const task = await taskManager.updateTask(undefined, params.id, body);
345
+ // Invalidate task-related caches
346
+ invalidateCacheForMutation([/^\/api\/tasks/]);
347
+ sendJson(res, task);
348
+ }
349
+ catch (error) {
350
+ sendError(res, error.message);
351
+ }
352
+ }, { cacheable: false });
353
+ // Delete task
354
+ route('DELETE', '/api/tasks/:id', async (_req, res, params) => {
355
+ try {
356
+ await taskManager.deleteTask(undefined, params.id);
357
+ // Invalidate task-related caches
358
+ invalidateCacheForMutation([/^\/api\/tasks/]);
359
+ sendJson(res, { deleted: true });
360
+ }
361
+ catch (error) {
362
+ sendError(res, error.message);
363
+ }
364
+ }, { cacheable: false });
365
+ // Get agents
366
+ route('GET', '/api/agents', async (_req, res) => {
367
+ try {
368
+ const agents = projectTracker.getAgents();
369
+ sendJson(res, { agents, count: agents.length });
370
+ }
371
+ catch (error) {
372
+ sendError(res, error.message);
373
+ }
374
+ }, { cacheable: true, cacheInvalidationPatterns: [/^\/api\/agents/] });
375
+ // Get pending approvals
376
+ route('GET', '/api/approvals', async (_req, res) => {
377
+ try {
378
+ const approvals = await supervisor.getPendingApprovals();
379
+ sendJson(res, { approvals, count: approvals.length });
380
+ }
381
+ catch (error) {
382
+ sendError(res, error.message);
383
+ }
384
+ }, { cacheable: true, cacheInvalidationPatterns: [/^\/api\/approvals/] });
385
+ // Get audit events
386
+ route('GET', '/api/audit', async (req, res) => {
387
+ try {
388
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
389
+ const limit = parseInt(url.searchParams.get('limit') || '100');
390
+ const events = supervisor.getAuditEvents({ limit });
391
+ sendJson(res, { events, count: events.length });
392
+ }
393
+ catch (error) {
394
+ sendError(res, error.message);
395
+ }
396
+ }, { cacheable: true });
397
+ // Get monitored apps
398
+ route('GET', '/api/apps', async (_req, res) => {
399
+ try {
400
+ const apps = supervisor.getAllMonitoredApps();
401
+ sendJson(res, { apps, count: apps.length });
402
+ }
403
+ catch (error) {
404
+ sendError(res, error.message);
405
+ }
406
+ }, { cacheable: true, cacheInvalidationPatterns: [/^\/api\/apps/] });
407
+ // Get app health
408
+ route('GET', '/api/apps/:id/health', async (_req, res, params) => {
409
+ try {
410
+ const health = await supervisor.checkAppHealth(params.id);
411
+ sendJson(res, health);
412
+ }
413
+ catch (error) {
414
+ sendError(res, error.message);
415
+ }
416
+ }, { cacheable: true });
417
+ // Get cache stats
418
+ route('GET', '/api/cache/stats', async (_req, res) => {
419
+ sendJson(res, responseCache.getStats());
420
+ }, { cacheable: false });
421
+ // Clear cache (for admin)
422
+ route('POST', '/api/cache/clear', async (_req, res) => {
423
+ responseCache.invalidateAll();
424
+ sendJson(res, { cleared: true, timestamp: new Date().toISOString() });
425
+ }, { cacheable: false });
426
+ // ============================================================================
427
+ // REQUEST HANDLER
428
+ // ============================================================================
429
+ /**
430
+ * Check authentication if token is configured
431
+ */
432
+ function checkAuth(req) {
433
+ if (!dashboardConfig.authToken) {
434
+ return true; // No auth required if token not configured
435
+ }
436
+ const authHeader = req.headers.authorization;
437
+ if (!authHeader) {
438
+ return false;
439
+ }
440
+ const [type, token] = authHeader.split(' ');
441
+ return type === 'Bearer' && token === dashboardConfig.authToken;
442
+ }
443
+ async function handleRequest(req, res) {
444
+ // CORS headers (using config)
445
+ res.setHeader('Access-Control-Allow-Origin', dashboardConfig.corsOrigin);
446
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS');
447
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, If-None-Match');
448
+ res.setHeader('Access-Control-Expose-Headers', 'ETag, X-Cache');
449
+ // Handle preflight
450
+ if (req.method === 'OPTIONS') {
451
+ res.statusCode = 204;
452
+ res.end();
453
+ return;
454
+ }
455
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
456
+ // Authentication check (skip for health endpoint and dashboard HTML)
457
+ const isPublicPath = url.pathname === '/api/health' || url.pathname === '/' || url.pathname === '/index.html';
458
+ if (!isPublicPath && !checkAuth(req)) {
459
+ sendError(res, 'Unauthorized', 401);
460
+ return;
461
+ }
462
+ const method = req.method;
463
+ // Find matching route
464
+ for (const r of routes) {
465
+ if (r.method !== method)
466
+ continue;
467
+ const match = url.pathname.match(r.pattern);
468
+ if (!match)
469
+ continue;
470
+ // Extract params
471
+ const params = {};
472
+ r.paramNames.forEach((name, i) => {
473
+ params[name] = match[i + 1];
474
+ });
475
+ // Parse body for mutation methods
476
+ let body;
477
+ if (['POST', 'PATCH', 'PUT'].includes(method)) {
478
+ try {
479
+ body = await parseBody(req);
480
+ }
481
+ catch (error) {
482
+ sendError(res, 'Invalid JSON body', 400);
483
+ return;
484
+ }
485
+ }
486
+ // Execute handler with cache wrapper
487
+ const wrappedHandler = withCache(r.handler, r.cacheable);
488
+ await wrappedHandler(req, res, params, body);
489
+ return;
490
+ }
491
+ // Serve dashboard HTML for root
492
+ if (url.pathname === '/' || url.pathname === '/index.html') {
493
+ serveDashboardHtml(res);
494
+ return;
495
+ }
496
+ // 404 for unknown routes
497
+ sendError(res, 'Not found', 404);
498
+ }
499
+ // ============================================================================
500
+ // DASHBOARD HTML
501
+ // ============================================================================
502
+ function serveDashboardHtml(res) {
503
+ const html = `<!DOCTYPE html>
504
+ <html lang="en">
505
+ <head>
506
+ <meta charset="UTF-8">
507
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
508
+ <title>Agent Supervisor Dashboard</title>
509
+ <style>
510
+ /* ============================================
511
+ CSS VARIABLES FOR THEMING (#21, #75)
512
+ ============================================ */
513
+ :root {
514
+ /* Light theme (default) */
515
+ --bg-color: #f5f5f5;
516
+ --bg-color-secondary: #ffffff;
517
+ --text-color: #333333;
518
+ --text-color-secondary: #666666;
519
+ --text-color-muted: #888888;
520
+ --border-color: #eeeeee;
521
+ --shadow-color: rgba(0, 0, 0, 0.1);
522
+ --focus-ring-color: #3b82f6;
523
+ --link-color: #2563eb;
524
+ --link-hover-color: #1d4ed8;
525
+
526
+ /* Status colors */
527
+ --color-success: #22c55e;
528
+ --color-success-bg: #dcfce7;
529
+ --color-success-text: #166534;
530
+ --color-warning: #f59e0b;
531
+ --color-warning-bg: #fef3c7;
532
+ --color-warning-text: #92400e;
533
+ --color-danger: #ef4444;
534
+ --color-danger-bg: #fee2e2;
535
+ --color-danger-text: #991b1b;
536
+ --color-info: #3b82f6;
537
+ --color-info-bg: #dbeafe;
538
+ --color-info-text: #1e40af;
539
+
540
+ /* Spacing */
541
+ --spacing-xs: 4px;
542
+ --spacing-sm: 8px;
543
+ --spacing-md: 12px;
544
+ --spacing-lg: 20px;
545
+ --spacing-xl: 32px;
546
+
547
+ /* Border radius */
548
+ --radius-sm: 4px;
549
+ --radius-md: 8px;
550
+ --radius-lg: 12px;
551
+
552
+ /* Typography */
553
+ --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
554
+ --font-size-sm: 12px;
555
+ --font-size-base: 14px;
556
+ --font-size-lg: 16px;
557
+ --font-size-xl: 20px;
558
+ --font-size-2xl: 32px;
559
+
560
+ /* Transitions */
561
+ --transition-fast: 150ms ease;
562
+ --transition-normal: 250ms ease;
563
+ }
564
+
565
+ /* Dark theme */
566
+ [data-theme="dark"] {
567
+ --bg-color: #1a1a2e;
568
+ --bg-color-secondary: #16213e;
569
+ --text-color: #eaeaea;
570
+ --text-color-secondary: #b0b0b0;
571
+ --text-color-muted: #888888;
572
+ --border-color: #2d2d44;
573
+ --shadow-color: rgba(0, 0, 0, 0.3);
574
+ --focus-ring-color: #60a5fa;
575
+ --link-color: #60a5fa;
576
+ --link-hover-color: #93c5fd;
577
+
578
+ /* Dark theme status colors with better contrast */
579
+ --color-success-bg: #166534;
580
+ --color-success-text: #dcfce7;
581
+ --color-warning-bg: #92400e;
582
+ --color-warning-text: #fef3c7;
583
+ --color-danger-bg: #991b1b;
584
+ --color-danger-text: #fee2e2;
585
+ --color-info-bg: #1e40af;
586
+ --color-info-text: #dbeafe;
587
+ }
588
+
589
+ /* ============================================
590
+ BASE STYLES
591
+ ============================================ */
592
+ * {
593
+ box-sizing: border-box;
594
+ margin: 0;
595
+ padding: 0;
596
+ }
597
+
598
+ html {
599
+ scroll-behavior: smooth;
600
+ }
601
+
602
+ body {
603
+ font-family: var(--font-family);
604
+ font-size: var(--font-size-base);
605
+ line-height: 1.5;
606
+ background: var(--bg-color);
607
+ color: var(--text-color);
608
+ transition: background-color var(--transition-normal), color var(--transition-normal);
609
+ }
610
+
611
+ /* ============================================
612
+ ACCESSIBILITY: Skip to Content Link (#75)
613
+ ============================================ */
614
+ .skip-to-content {
615
+ position: absolute;
616
+ top: -40px;
617
+ left: 0;
618
+ background: var(--focus-ring-color);
619
+ color: white;
620
+ padding: var(--spacing-sm) var(--spacing-md);
621
+ z-index: 1000;
622
+ text-decoration: none;
623
+ font-weight: 500;
624
+ border-radius: 0 0 var(--radius-sm) 0;
625
+ transition: top var(--transition-fast);
626
+ }
627
+
628
+ .skip-to-content:focus {
629
+ top: 0;
630
+ outline: none;
631
+ }
632
+
633
+ /* ============================================
634
+ ACCESSIBILITY: Focus Indicators (#75)
635
+ ============================================ */
636
+ *:focus {
637
+ outline: 2px solid var(--focus-ring-color);
638
+ outline-offset: 2px;
639
+ }
640
+
641
+ *:focus:not(:focus-visible) {
642
+ outline: none;
643
+ }
644
+
645
+ *:focus-visible {
646
+ outline: 2px solid var(--focus-ring-color);
647
+ outline-offset: 2px;
648
+ }
649
+
650
+ /* ============================================
651
+ LAYOUT
652
+ ============================================ */
653
+ .container {
654
+ max-width: 1200px;
655
+ margin: 0 auto;
656
+ padding: var(--spacing-lg);
657
+ }
658
+
659
+ /* ============================================
660
+ HEADER
661
+ ============================================ */
662
+ .header {
663
+ display: flex;
664
+ justify-content: space-between;
665
+ align-items: center;
666
+ flex-wrap: wrap;
667
+ gap: var(--spacing-md);
668
+ margin-bottom: var(--spacing-lg);
669
+ }
670
+
671
+ h1 {
672
+ color: var(--text-color);
673
+ font-size: var(--font-size-xl);
674
+ font-weight: 600;
675
+ }
676
+
677
+ /* ============================================
678
+ THEME TOGGLE (#21)
679
+ ============================================ */
680
+ .theme-toggle {
681
+ display: flex;
682
+ align-items: center;
683
+ gap: var(--spacing-sm);
684
+ }
685
+
686
+ .theme-toggle-btn {
687
+ background: var(--bg-color-secondary);
688
+ border: 1px solid var(--border-color);
689
+ border-radius: var(--radius-md);
690
+ padding: var(--spacing-sm) var(--spacing-md);
691
+ color: var(--text-color);
692
+ font-family: inherit;
693
+ font-size: var(--font-size-sm);
694
+ cursor: pointer;
695
+ display: flex;
696
+ align-items: center;
697
+ gap: var(--spacing-xs);
698
+ transition: background-color var(--transition-fast), border-color var(--transition-fast);
699
+ }
700
+
701
+ .theme-toggle-btn:hover {
702
+ background: var(--border-color);
703
+ }
704
+
705
+ .theme-icon {
706
+ width: 16px;
707
+ height: 16px;
708
+ }
709
+
710
+ /* ============================================
711
+ CARDS
712
+ ============================================ */
713
+ .card {
714
+ background: var(--bg-color-secondary);
715
+ border-radius: var(--radius-md);
716
+ padding: var(--spacing-lg);
717
+ margin-bottom: var(--spacing-lg);
718
+ box-shadow: 0 2px 4px var(--shadow-color);
719
+ transition: background-color var(--transition-normal), box-shadow var(--transition-normal);
720
+ }
721
+
722
+ .card h2 {
723
+ color: var(--text-color-secondary);
724
+ font-size: var(--font-size-sm);
725
+ text-transform: uppercase;
726
+ letter-spacing: 0.05em;
727
+ margin-bottom: var(--spacing-sm);
728
+ font-weight: 500;
729
+ }
730
+
731
+ .stat {
732
+ font-size: var(--font-size-2xl);
733
+ font-weight: bold;
734
+ color: var(--text-color);
735
+ }
736
+
737
+ /* ============================================
738
+ GRID LAYOUT (Responsive) (#75)
739
+ ============================================ */
740
+ .grid {
741
+ display: grid;
742
+ grid-template-columns: repeat(4, 1fr);
743
+ gap: var(--spacing-lg);
744
+ }
745
+
746
+ /* Tablet breakpoint */
747
+ @media (max-width: 992px) {
748
+ .grid {
749
+ grid-template-columns: repeat(2, 1fr);
750
+ }
751
+ }
752
+
753
+ /* Mobile breakpoint */
754
+ @media (max-width: 576px) {
755
+ .container {
756
+ padding: var(--spacing-md);
757
+ }
758
+
759
+ .grid {
760
+ grid-template-columns: 1fr;
761
+ }
762
+
763
+ h1 {
764
+ font-size: var(--font-size-lg);
765
+ }
766
+
767
+ .header {
768
+ flex-direction: column;
769
+ align-items: flex-start;
770
+ }
771
+
772
+ .stat {
773
+ font-size: 24px;
774
+ }
775
+
776
+ .card {
777
+ padding: var(--spacing-md);
778
+ }
779
+ }
780
+
781
+ /* ============================================
782
+ STATUS COLORS
783
+ ============================================ */
784
+ .status-healthy { color: var(--color-success); }
785
+ .status-warning { color: var(--color-warning); }
786
+ .status-error { color: var(--color-danger); }
787
+
788
+ /* ============================================
789
+ TABLES (Responsive) (#75)
790
+ ============================================ */
791
+ .table-container {
792
+ overflow-x: auto;
793
+ -webkit-overflow-scrolling: touch;
794
+ }
795
+
796
+ table {
797
+ width: 100%;
798
+ border-collapse: collapse;
799
+ min-width: 500px;
800
+ }
801
+
802
+ th, td {
803
+ text-align: left;
804
+ padding: var(--spacing-md);
805
+ border-bottom: 1px solid var(--border-color);
806
+ }
807
+
808
+ th {
809
+ color: var(--text-color-secondary);
810
+ font-weight: 500;
811
+ font-size: var(--font-size-sm);
812
+ }
813
+
814
+ td {
815
+ color: var(--text-color);
816
+ }
817
+
818
+ tr:hover td {
819
+ background: var(--bg-color);
820
+ }
821
+
822
+ /* Mobile table adjustments */
823
+ @media (max-width: 576px) {
824
+ th, td {
825
+ padding: var(--spacing-sm);
826
+ font-size: var(--font-size-sm);
827
+ }
828
+
829
+ table {
830
+ min-width: 400px;
831
+ }
832
+ }
833
+
834
+ /* ============================================
835
+ BADGES
836
+ ============================================ */
837
+ .badge {
838
+ display: inline-block;
839
+ padding: var(--spacing-xs) var(--spacing-sm);
840
+ border-radius: var(--radius-sm);
841
+ font-size: var(--font-size-sm);
842
+ font-weight: 500;
843
+ white-space: nowrap;
844
+ }
845
+
846
+ .badge-success {
847
+ background: var(--color-success-bg);
848
+ color: var(--color-success-text);
849
+ }
850
+
851
+ .badge-warning {
852
+ background: var(--color-warning-bg);
853
+ color: var(--color-warning-text);
854
+ }
855
+
856
+ .badge-danger {
857
+ background: var(--color-danger-bg);
858
+ color: var(--color-danger-text);
859
+ }
860
+
861
+ .badge-info {
862
+ background: var(--color-info-bg);
863
+ color: var(--color-info-text);
864
+ }
865
+
866
+ /* ============================================
867
+ LOADING STATE
868
+ ============================================ */
869
+ .loading {
870
+ color: var(--text-color-muted);
871
+ font-style: italic;
872
+ }
873
+
874
+ /* ============================================
875
+ SCREEN READER ONLY (#75)
876
+ ============================================ */
877
+ .sr-only {
878
+ position: absolute;
879
+ width: 1px;
880
+ height: 1px;
881
+ padding: 0;
882
+ margin: -1px;
883
+ overflow: hidden;
884
+ clip: rect(0, 0, 0, 0);
885
+ white-space: nowrap;
886
+ border: 0;
887
+ }
888
+
889
+ /* ============================================
890
+ REDUCED MOTION (#75)
891
+ ============================================ */
892
+ @media (prefers-reduced-motion: reduce) {
893
+ *,
894
+ *::before,
895
+ *::after {
896
+ animation-duration: 0.01ms !important;
897
+ animation-iteration-count: 1 !important;
898
+ transition-duration: 0.01ms !important;
899
+ }
900
+ }
901
+ </style>
902
+ </head>
903
+ <body>
904
+ <!-- Skip to content link for accessibility (#75) -->
905
+ <a href="#main-content" class="skip-to-content">Skip to main content</a>
906
+
907
+ <div class="container">
908
+ <header class="header" role="banner">
909
+ <h1>Agent Supervisor Dashboard</h1>
910
+ <div class="theme-toggle">
911
+ <button
912
+ type="button"
913
+ class="theme-toggle-btn"
914
+ id="theme-toggle"
915
+ aria-label="Toggle dark mode"
916
+ title="Toggle dark/light theme"
917
+ >
918
+ <svg class="theme-icon" id="theme-icon-light" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
919
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path>
920
+ </svg>
921
+ <svg class="theme-icon" id="theme-icon-dark" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="display: none;">
922
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path>
923
+ </svg>
924
+ <span id="theme-label">Dark</span>
925
+ </button>
926
+ </div>
927
+ </header>
928
+
929
+ <main id="main-content" role="main">
930
+ <section aria-labelledby="stats-heading">
931
+ <h2 id="stats-heading" class="sr-only">Dashboard Statistics</h2>
932
+ <div class="grid" role="list">
933
+ <article class="card" role="listitem" aria-labelledby="agents-label">
934
+ <h2 id="agents-label">Active Agents</h2>
935
+ <div class="stat" id="agent-count" aria-live="polite">-</div>
936
+ </article>
937
+ <article class="card" role="listitem" aria-labelledby="approvals-label">
938
+ <h2 id="approvals-label">Pending Approvals</h2>
939
+ <div class="stat" id="approval-count" aria-live="polite">-</div>
940
+ </article>
941
+ <article class="card" role="listitem" aria-labelledby="tasks-label">
942
+ <h2 id="tasks-label">Open Tasks</h2>
943
+ <div class="stat" id="task-count" aria-live="polite">-</div>
944
+ </article>
945
+ <article class="card" role="listitem" aria-labelledby="apps-label">
946
+ <h2 id="apps-label">Monitored Apps</h2>
947
+ <div class="stat" id="app-count" aria-live="polite">-</div>
948
+ </article>
949
+ </div>
950
+ </section>
951
+
952
+ <section class="card" aria-labelledby="recent-tasks-heading">
953
+ <h2 id="recent-tasks-heading">Recent Tasks</h2>
954
+ <div class="table-container">
955
+ <table role="table" aria-labelledby="recent-tasks-heading">
956
+ <thead>
957
+ <tr>
958
+ <th scope="col">ID</th>
959
+ <th scope="col">Title</th>
960
+ <th scope="col">Status</th>
961
+ <th scope="col">Priority</th>
962
+ </tr>
963
+ </thead>
964
+ <tbody id="tasks-table">
965
+ <tr><td colspan="4" class="loading">Loading...</td></tr>
966
+ </tbody>
967
+ </table>
968
+ </div>
969
+ </section>
970
+
971
+ <section class="card" aria-labelledby="audit-events-heading">
972
+ <h2 id="audit-events-heading">Recent Audit Events</h2>
973
+ <div class="table-container">
974
+ <table role="table" aria-labelledby="audit-events-heading">
975
+ <thead>
976
+ <tr>
977
+ <th scope="col">Time</th>
978
+ <th scope="col">Action</th>
979
+ <th scope="col">Type</th>
980
+ <th scope="col">Outcome</th>
981
+ </tr>
982
+ </thead>
983
+ <tbody id="audit-table">
984
+ <tr><td colspan="4" class="loading">Loading...</td></tr>
985
+ </tbody>
986
+ </table>
987
+ </div>
988
+ </section>
989
+ </main>
990
+ </div>
991
+
992
+ <script>
993
+ // =============================================
994
+ // Theme Management (#21)
995
+ // =============================================
996
+ const ThemeManager = {
997
+ STORAGE_KEY: 'agent-supervisor-theme',
998
+ THEMES: { LIGHT: 'light', DARK: 'dark' },
999
+
1000
+ init() {
1001
+ // Check localStorage first, then system preference
1002
+ const stored = localStorage.getItem(this.STORAGE_KEY);
1003
+ if (stored) {
1004
+ this.setTheme(stored);
1005
+ } else {
1006
+ // Respect prefers-color-scheme
1007
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
1008
+ this.setTheme(prefersDark ? this.THEMES.DARK : this.THEMES.LIGHT);
1009
+ }
1010
+
1011
+ // Listen for system theme changes
1012
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
1013
+ // Only auto-switch if user hasn't set a preference
1014
+ if (!localStorage.getItem(this.STORAGE_KEY)) {
1015
+ this.setTheme(e.matches ? this.THEMES.DARK : this.THEMES.LIGHT);
1016
+ }
1017
+ });
1018
+
1019
+ // Set up toggle button
1020
+ const toggleBtn = document.getElementById('theme-toggle');
1021
+ if (toggleBtn) {
1022
+ toggleBtn.addEventListener('click', () => this.toggle());
1023
+ }
1024
+ },
1025
+
1026
+ setTheme(theme) {
1027
+ document.documentElement.setAttribute('data-theme', theme);
1028
+ this.updateToggleUI(theme);
1029
+ },
1030
+
1031
+ updateToggleUI(theme) {
1032
+ const lightIcon = document.getElementById('theme-icon-light');
1033
+ const darkIcon = document.getElementById('theme-icon-dark');
1034
+ const label = document.getElementById('theme-label');
1035
+ const btn = document.getElementById('theme-toggle');
1036
+
1037
+ if (theme === this.THEMES.DARK) {
1038
+ if (lightIcon) lightIcon.style.display = 'none';
1039
+ if (darkIcon) darkIcon.style.display = 'block';
1040
+ if (label) label.textContent = 'Light';
1041
+ if (btn) btn.setAttribute('aria-pressed', 'true');
1042
+ } else {
1043
+ if (lightIcon) lightIcon.style.display = 'block';
1044
+ if (darkIcon) darkIcon.style.display = 'none';
1045
+ if (label) label.textContent = 'Dark';
1046
+ if (btn) btn.setAttribute('aria-pressed', 'false');
1047
+ }
1048
+ },
1049
+
1050
+ toggle() {
1051
+ const current = document.documentElement.getAttribute('data-theme') || this.THEMES.LIGHT;
1052
+ const next = current === this.THEMES.DARK ? this.THEMES.LIGHT : this.THEMES.DARK;
1053
+ this.setTheme(next);
1054
+ localStorage.setItem(this.STORAGE_KEY, next);
1055
+ },
1056
+
1057
+ getTheme() {
1058
+ return document.documentElement.getAttribute('data-theme') || this.THEMES.LIGHT;
1059
+ }
1060
+ };
1061
+
1062
+ // Initialize theme on page load
1063
+ ThemeManager.init();
1064
+
1065
+ // =============================================
1066
+ // Data Fetching
1067
+ // =============================================
1068
+ async function fetchData() {
1069
+ try {
1070
+ const [agents, approvals, tasks, apps, audit] = await Promise.all([
1071
+ fetch('/api/agents').then(r => r.json()),
1072
+ fetch('/api/approvals').then(r => r.json()),
1073
+ fetch('/api/tasks').then(r => r.json()),
1074
+ fetch('/api/apps').then(r => r.json()),
1075
+ fetch('/api/audit?limit=10').then(r => r.json())
1076
+ ]);
1077
+
1078
+ document.getElementById('agent-count').textContent = agents.count || 0;
1079
+ document.getElementById('approval-count').textContent = approvals.count || 0;
1080
+ document.getElementById('task-count').textContent = tasks.count || 0;
1081
+ document.getElementById('app-count').textContent = apps.count || 0;
1082
+
1083
+ // Render tasks
1084
+ const tasksTbody = document.getElementById('tasks-table');
1085
+ if (tasks.tasks && tasks.tasks.length > 0) {
1086
+ tasksTbody.innerHTML = tasks.tasks.slice(0, 10).map(t => \`
1087
+ <tr>
1088
+ <td>#\${escapeHtml(String(t.id))}</td>
1089
+ <td>\${escapeHtml(t.title)}</td>
1090
+ <td><span class="badge badge-\${getStatusBadge(t.status)}" role="status">\${escapeHtml(t.status)}</span></td>
1091
+ <td><span class="badge badge-\${getPriorityBadge(t.priority)}">\${escapeHtml(t.priority)}</span></td>
1092
+ </tr>
1093
+ \`).join('');
1094
+ } else {
1095
+ tasksTbody.innerHTML = '<tr><td colspan="4">No tasks found</td></tr>';
1096
+ }
1097
+
1098
+ // Render audit events
1099
+ const auditTbody = document.getElementById('audit-table');
1100
+ if (audit.events && audit.events.length > 0) {
1101
+ auditTbody.innerHTML = audit.events.map(e => \`
1102
+ <tr>
1103
+ <td>\${escapeHtml(new Date(e.timestamp).toLocaleString())}</td>
1104
+ <td>\${escapeHtml(e.action)}</td>
1105
+ <td>\${escapeHtml(e.eventType)}</td>
1106
+ <td><span class="badge badge-\${getOutcomeBadge(e.outcome)}" role="status">\${escapeHtml(e.outcome)}</span></td>
1107
+ </tr>
1108
+ \`).join('');
1109
+ } else {
1110
+ auditTbody.innerHTML = '<tr><td colspan="4">No events found</td></tr>';
1111
+ }
1112
+ } catch (error) {
1113
+ console.error('Failed to fetch data:', error);
1114
+ }
1115
+ }
1116
+
1117
+ // =============================================
1118
+ // Utility Functions
1119
+ // =============================================
1120
+ function escapeHtml(text) {
1121
+ const div = document.createElement('div');
1122
+ div.textContent = text;
1123
+ return div.innerHTML;
1124
+ }
1125
+
1126
+ function getStatusBadge(status) {
1127
+ const map = { completed: 'success', pending: 'info', in_progress: 'warning', blocked: 'danger' };
1128
+ return map[status] || 'info';
1129
+ }
1130
+
1131
+ function getPriorityBadge(priority) {
1132
+ const map = { critical: 'danger', high: 'warning', medium: 'info', low: 'success' };
1133
+ return map[priority] || 'info';
1134
+ }
1135
+
1136
+ function getOutcomeBadge(outcome) {
1137
+ const map = { success: 'success', failure: 'danger', pending: 'warning' };
1138
+ return map[outcome] || 'info';
1139
+ }
1140
+
1141
+ // =============================================
1142
+ // Initialization
1143
+ // =============================================
1144
+ // Initial fetch
1145
+ fetchData();
1146
+
1147
+ // Refresh every 10 seconds
1148
+ setInterval(fetchData, 10000);
1149
+ </script>
1150
+ </body>
1151
+ </html>`;
1152
+ res.setHeader('Content-Type', 'text/html');
1153
+ res.setHeader('Cache-Control', 'no-cache');
1154
+ res.statusCode = 200;
1155
+ res.end(html);
1156
+ }
1157
+ // ============================================================================
1158
+ // WEBSOCKET SERVER
1159
+ // ============================================================================
1160
+ let wss = null;
1161
+ function broadcastUpdate(data) {
1162
+ if (!wss)
1163
+ return;
1164
+ const message = JSON.stringify(data);
1165
+ wss.clients.forEach(client => {
1166
+ if (client.readyState === WebSocket.OPEN) {
1167
+ client.send(message);
1168
+ }
1169
+ });
1170
+ }
1171
+ // ============================================================================
1172
+ // SERVER LIFECYCLE
1173
+ // ============================================================================
1174
+ let server = null;
1175
+ let wsUpdateInterval = null;
1176
+ /**
1177
+ * Start the dashboard server with optional port override.
1178
+ *
1179
+ * Uses externalized configuration from environment variables:
1180
+ * - DASHBOARD_PORT: Port (default: 3100)
1181
+ * - DASHBOARD_HOST: Host (default: localhost)
1182
+ * - DASHBOARD_AUTH_TOKEN: Optional auth token
1183
+ * - DASHBOARD_CORS_ORIGIN: CORS origin (default: *)
1184
+ * - DASHBOARD_CACHE_TTL: Cache TTL in ms (default: 5000)
1185
+ * - DASHBOARD_WS_INTERVAL: WebSocket interval in ms (default: 2000)
1186
+ *
1187
+ * @param port Optional port override (takes precedence over env var)
1188
+ */
1189
+ export async function startDashboardServer(port) {
1190
+ // Validate configuration
1191
+ const validationErrors = validateDashboardConfig(dashboardConfig);
1192
+ if (validationErrors.length > 0) {
1193
+ throw new Error(`Invalid dashboard configuration: ${validationErrors.join(', ')}`);
1194
+ }
1195
+ // Use provided port or fall back to config
1196
+ const portNum = port !== undefined
1197
+ ? (typeof port === 'string' ? parseInt(port) : port)
1198
+ : dashboardConfig.port;
1199
+ server = http.createServer(handleRequest);
1200
+ // Setup WebSocket server
1201
+ wss = new WebSocketServer({ server });
1202
+ wss.on('connection', (ws) => {
1203
+ console.log('WebSocket client connected');
1204
+ // Send initial config to client
1205
+ ws.send(JSON.stringify({
1206
+ type: 'config',
1207
+ wsInterval: dashboardConfig.wsInterval
1208
+ }));
1209
+ ws.on('close', () => {
1210
+ console.log('WebSocket client disconnected');
1211
+ });
1212
+ });
1213
+ // Periodic broadcasts (using config interval)
1214
+ wsUpdateInterval = setInterval(async () => {
1215
+ try {
1216
+ const agents = projectTracker.getAgents();
1217
+ const approvals = await supervisor.getPendingApprovals();
1218
+ broadcastUpdate({
1219
+ type: 'update',
1220
+ timestamp: new Date().toISOString(),
1221
+ agents: agents.length,
1222
+ pendingApprovals: approvals.length
1223
+ });
1224
+ }
1225
+ catch {
1226
+ // Ignore broadcast errors
1227
+ }
1228
+ }, dashboardConfig.wsInterval);
1229
+ return new Promise((resolve, reject) => {
1230
+ server.on('error', reject);
1231
+ server.listen(portNum, dashboardConfig.host, () => {
1232
+ console.log(`Dashboard server listening on http://${dashboardConfig.host}:${portNum}`);
1233
+ console.log(`WebSocket endpoint: ws://${dashboardConfig.host}:${portNum}`);
1234
+ console.log(`Configuration: cacheTtl=${dashboardConfig.cacheTtl}ms, wsInterval=${dashboardConfig.wsInterval}ms`);
1235
+ if (dashboardConfig.authToken) {
1236
+ console.log('Authentication: enabled (Bearer token required)');
1237
+ }
1238
+ else {
1239
+ console.log('Authentication: disabled');
1240
+ }
1241
+ resolve(server);
1242
+ });
1243
+ });
1244
+ }
1245
+ export function stopDashboardServer() {
1246
+ return new Promise((resolve) => {
1247
+ // Clear WebSocket update interval
1248
+ if (wsUpdateInterval) {
1249
+ clearInterval(wsUpdateInterval);
1250
+ wsUpdateInterval = null;
1251
+ }
1252
+ responseCache.destroy();
1253
+ if (wss) {
1254
+ wss.close();
1255
+ wss = null;
1256
+ }
1257
+ if (server) {
1258
+ server.close(() => {
1259
+ server = null;
1260
+ resolve();
1261
+ });
1262
+ }
1263
+ else {
1264
+ resolve();
1265
+ }
1266
+ });
1267
+ }
1268
+ /**
1269
+ * Get current dashboard configuration
1270
+ */
1271
+ export function getDashboardConfig() {
1272
+ return { ...dashboardConfig };
1273
+ }
1274
+ // Export cache for testing
1275
+ export { responseCache };
1276
+ //# sourceMappingURL=httpDashboard.js.map