@trentapps/manager-protocol 1.1.2 → 1.2.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/README.md +29 -1
- package/dist/analyzers/CSSAnalyzer.d.ts +188 -8
- package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -1
- package/dist/analyzers/CSSAnalyzer.js +794 -192
- package/dist/analyzers/CSSAnalyzer.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/config/dashboard.d.ts +55 -0
- package/dist/config/dashboard.d.ts.map +1 -0
- package/dist/config/dashboard.js +103 -0
- package/dist/config/dashboard.js.map +1 -0
- package/dist/config/index.d.ts +7 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +7 -0
- package/dist/config/index.js.map +1 -0
- package/dist/dashboard/httpDashboard.d.ts +100 -0
- package/dist/dashboard/httpDashboard.d.ts.map +1 -0
- package/dist/dashboard/httpDashboard.js +1276 -0
- package/dist/dashboard/httpDashboard.js.map +1 -0
- package/dist/dashboard/index.d.ts +6 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/index.js +7 -0
- package/dist/dashboard/index.js.map +1 -0
- package/dist/engine/AuditLogger.d.ts +370 -2
- package/dist/engine/AuditLogger.d.ts.map +1 -1
- package/dist/engine/AuditLogger.js +1067 -24
- package/dist/engine/AuditLogger.js.map +1 -1
- package/dist/engine/GitHubApprovalManager.d.ts +13 -0
- package/dist/engine/GitHubApprovalManager.d.ts.map +1 -1
- package/dist/engine/GitHubApprovalManager.js +72 -46
- package/dist/engine/GitHubApprovalManager.js.map +1 -1
- package/dist/engine/GitHubClient.d.ts +183 -0
- package/dist/engine/GitHubClient.d.ts.map +1 -0
- package/dist/engine/GitHubClient.js +411 -0
- package/dist/engine/GitHubClient.js.map +1 -0
- package/dist/engine/RateLimiter.d.ts +5 -3
- package/dist/engine/RateLimiter.d.ts.map +1 -1
- package/dist/engine/RateLimiter.js +53 -70
- package/dist/engine/RateLimiter.js.map +1 -1
- package/dist/engine/RuleDependencyAnalyzer.d.ts +73 -0
- package/dist/engine/RuleDependencyAnalyzer.d.ts.map +1 -0
- package/dist/engine/RuleDependencyAnalyzer.js +475 -0
- package/dist/engine/RuleDependencyAnalyzer.js.map +1 -0
- package/dist/engine/RulesEngine.d.ts +102 -3
- package/dist/engine/RulesEngine.d.ts.map +1 -1
- package/dist/engine/RulesEngine.js +326 -21
- package/dist/engine/RulesEngine.js.map +1 -1
- package/dist/engine/TaskManager.d.ts +11 -10
- package/dist/engine/TaskManager.d.ts.map +1 -1
- package/dist/engine/TaskManager.js +180 -195
- package/dist/engine/TaskManager.js.map +1 -1
- package/dist/engine/index.d.ts +3 -0
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +5 -0
- package/dist/engine/index.js.map +1 -1
- package/dist/rules/azure.d.ts.map +1 -1
- package/dist/rules/azure.js +12 -14
- package/dist/rules/azure.js.map +1 -1
- package/dist/rules/compliance.d.ts.map +1 -1
- package/dist/rules/compliance.js +23 -41
- package/dist/rules/compliance.js.map +1 -1
- package/dist/rules/condition-optimizer.d.ts +151 -0
- package/dist/rules/condition-optimizer.d.ts.map +1 -0
- package/dist/rules/condition-optimizer.js +479 -0
- package/dist/rules/condition-optimizer.js.map +1 -0
- package/dist/rules/css.d.ts.map +1 -1
- package/dist/rules/css.js +538 -0
- package/dist/rules/css.js.map +1 -1
- package/dist/rules/field-standards.d.ts +1172 -0
- package/dist/rules/field-standards.d.ts.map +1 -0
- package/dist/rules/field-standards.js +908 -0
- package/dist/rules/field-standards.js.map +1 -0
- package/dist/rules/flask.d.ts.map +1 -1
- package/dist/rules/flask.js +18 -31
- package/dist/rules/flask.js.map +1 -1
- package/dist/rules/index.d.ts +220 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +155 -0
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/ml-ai.d.ts.map +1 -1
- package/dist/rules/ml-ai.js +11 -13
- package/dist/rules/ml-ai.js.map +1 -1
- package/dist/rules/patterns.d.ts +568 -0
- package/dist/rules/patterns.d.ts.map +1 -0
- package/dist/rules/patterns.js +1359 -0
- package/dist/rules/patterns.js.map +1 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +580 -19
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/shared-patterns.d.ts +268 -0
- package/dist/rules/shared-patterns.d.ts.map +1 -0
- package/dist/rules/shared-patterns.js +556 -0
- package/dist/rules/shared-patterns.js.map +1 -0
- package/dist/rules/storage.d.ts +8 -2
- package/dist/rules/storage.d.ts.map +1 -1
- package/dist/rules/storage.js +541 -3
- package/dist/rules/storage.js.map +1 -1
- package/dist/rules/stripe.d.ts.map +1 -1
- package/dist/rules/stripe.js +19 -26
- package/dist/rules/stripe.js.map +1 -1
- package/dist/rules/websocket.d.ts.map +1 -1
- package/dist/rules/websocket.js +32 -40
- package/dist/rules/websocket.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +96 -17
- package/dist/server.js.map +1 -1
- package/dist/supervisor/AgentSupervisor.d.ts +52 -0
- package/dist/supervisor/AgentSupervisor.d.ts.map +1 -1
- package/dist/supervisor/AgentSupervisor.js +120 -1
- package/dist/supervisor/AgentSupervisor.js.map +1 -1
- package/dist/supervisor/ManagedServerRegistry.d.ts +139 -2
- package/dist/supervisor/ManagedServerRegistry.d.ts.map +1 -1
- package/dist/supervisor/ManagedServerRegistry.js +590 -6
- package/dist/supervisor/ManagedServerRegistry.js.map +1 -1
- package/dist/supervisor/ProjectTracker.d.ts +24 -2
- package/dist/supervisor/ProjectTracker.d.ts.map +1 -1
- package/dist/supervisor/ProjectTracker.js +151 -59
- package/dist/supervisor/ProjectTracker.js.map +1 -1
- package/dist/testing/index.d.ts +11 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +12 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/rule-tester.d.ts +217 -0
- package/dist/testing/rule-tester.d.ts.map +1 -0
- package/dist/testing/rule-tester.examples.d.ts +57 -0
- package/dist/testing/rule-tester.examples.d.ts.map +1 -0
- package/dist/testing/rule-tester.examples.js +375 -0
- package/dist/testing/rule-tester.examples.js.map +1 -0
- package/dist/testing/rule-tester.js +381 -0
- package/dist/testing/rule-tester.js.map +1 -0
- package/dist/testing/rule-validator.d.ts +141 -0
- package/dist/testing/rule-validator.d.ts.map +1 -0
- package/dist/testing/rule-validator.js +640 -0
- package/dist/testing/rule-validator.js.map +1 -0
- package/dist/types/index.d.ts +265 -4
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +57 -2
- package/dist/types/index.js.map +1 -1
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/rate-limiting.d.ts +268 -0
- package/dist/utils/rate-limiting.d.ts.map +1 -0
- package/dist/utils/rate-limiting.js +403 -0
- package/dist/utils/rate-limiting.js.map +1 -0
- package/dist/utils/shared.d.ts +306 -0
- package/dist/utils/shared.d.ts.map +1 -0
- package/dist/utils/shared.js +464 -0
- package/dist/utils/shared.js.map +1 -0
- package/package.json +2 -1
|
@@ -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
|