bulltrackers-module 1.0.768 → 1.0.769
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/functions/computation-system-v2/UserPortfolioMetrics.js +50 -0
- package/functions/computation-system-v2/computations/BehavioralAnomaly.js +557 -337
- package/functions/computation-system-v2/computations/GlobalAumPerAsset30D.js +103 -0
- package/functions/computation-system-v2/computations/PIDailyAssetAUM.js +134 -0
- package/functions/computation-system-v2/computations/PiFeatureVectors.js +227 -0
- package/functions/computation-system-v2/computations/PiRecommender.js +359 -0
- package/functions/computation-system-v2/computations/SignedInUserList.js +51 -0
- package/functions/computation-system-v2/computations/SignedInUserMirrorHistory.js +138 -0
- package/functions/computation-system-v2/computations/SignedInUserPIProfileMetrics.js +106 -0
- package/functions/computation-system-v2/computations/SignedInUserProfileMetrics.js +324 -0
- package/functions/computation-system-v2/config/bulltrackers.config.js +30 -128
- package/functions/computation-system-v2/core-api.js +17 -9
- package/functions/computation-system-v2/data_schema_reference.MD +108 -0
- package/functions/computation-system-v2/devtools/builder/builder.js +362 -0
- package/functions/computation-system-v2/devtools/builder/examples/user-metrics.yaml +26 -0
- package/functions/computation-system-v2/devtools/index.js +36 -0
- package/functions/computation-system-v2/devtools/shared/MockDataFactory.js +235 -0
- package/functions/computation-system-v2/devtools/shared/SchemaTemplates.js +475 -0
- package/functions/computation-system-v2/devtools/shared/SystemIntrospector.js +517 -0
- package/functions/computation-system-v2/devtools/shared/index.js +16 -0
- package/functions/computation-system-v2/devtools/simulation/DAGAnalyzer.js +243 -0
- package/functions/computation-system-v2/devtools/simulation/MockDataFetcher.js +306 -0
- package/functions/computation-system-v2/devtools/simulation/MockStorageManager.js +336 -0
- package/functions/computation-system-v2/devtools/simulation/SimulationEngine.js +525 -0
- package/functions/computation-system-v2/devtools/simulation/SimulationServer.js +581 -0
- package/functions/computation-system-v2/devtools/simulation/index.js +17 -0
- package/functions/computation-system-v2/devtools/simulation/simulate.js +324 -0
- package/functions/computation-system-v2/devtools/vscode-computation/package.json +90 -0
- package/functions/computation-system-v2/devtools/vscode-computation/snippets/computation.json +128 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/extension.ts +401 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/codeActions.ts +152 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/completions.ts +207 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/diagnostics.ts +205 -0
- package/functions/computation-system-v2/devtools/vscode-computation/src/providers/hover.ts +205 -0
- package/functions/computation-system-v2/devtools/vscode-computation/tsconfig.json +22 -0
- package/functions/computation-system-v2/docs/HowToCreateComputations.MD +602 -0
- package/functions/computation-system-v2/framework/data/DataFetcher.js +250 -184
- package/functions/computation-system-v2/framework/data/MaterializedViewManager.js +84 -0
- package/functions/computation-system-v2/framework/data/QueryBuilder.js +38 -38
- package/functions/computation-system-v2/framework/execution/Orchestrator.js +215 -129
- package/functions/computation-system-v2/framework/scheduling/ScheduleValidator.js +17 -19
- package/functions/computation-system-v2/framework/storage/StateRepository.js +32 -2
- package/functions/computation-system-v2/framework/storage/StorageManager.js +105 -67
- package/functions/computation-system-v2/framework/testing/ComputationTester.js +12 -6
- package/functions/computation-system-v2/handlers/dispatcher.js +57 -29
- package/functions/computation-system-v2/legacy/PiAssetRecommender.js.old +115 -0
- package/functions/computation-system-v2/legacy/PiSimilarityMatrix.js +104 -0
- package/functions/computation-system-v2/legacy/PiSimilarityVector.js +71 -0
- package/functions/computation-system-v2/scripts/debug_aggregation.js +25 -0
- package/functions/computation-system-v2/scripts/test-invalidation-scenarios.js +234 -0
- package/package.json +1 -1
|
@@ -0,0 +1,581 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Simulation Server
|
|
3
|
+
*
|
|
4
|
+
* HTTP server with WebSocket for live updates.
|
|
5
|
+
* Provides:
|
|
6
|
+
* - REST API for simulation control
|
|
7
|
+
* - File watcher for auto-reload
|
|
8
|
+
* - WebSocket for live result push
|
|
9
|
+
* - Dashboard serving
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const http = require('http');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { promisify } = require('util');
|
|
16
|
+
|
|
17
|
+
// Try to load optional dependencies
|
|
18
|
+
let chokidar;
|
|
19
|
+
try {
|
|
20
|
+
chokidar = require('chokidar');
|
|
21
|
+
} catch (e) {
|
|
22
|
+
console.warn('chokidar not available - file watching disabled');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { SimulationEngine } = require('./SimulationEngine');
|
|
26
|
+
const { DAGAnalyzer } = require('./DAGAnalyzer');
|
|
27
|
+
const { SystemIntrospector } = require('../shared/SystemIntrospector');
|
|
28
|
+
|
|
29
|
+
class SimulationServer {
|
|
30
|
+
/**
|
|
31
|
+
* @param {Object} config - The bulltrackers.config.js exports
|
|
32
|
+
* @param {Object} options
|
|
33
|
+
* @param {number} [options.port=3210] - Server port
|
|
34
|
+
* @param {boolean} [options.watch=true] - Enable file watching
|
|
35
|
+
* @param {string} [options.computationsDir] - Directory to watch
|
|
36
|
+
* @param {number} [options.entityCount=10] - Default entity count
|
|
37
|
+
*/
|
|
38
|
+
constructor(config, options = {}) {
|
|
39
|
+
this.config = config;
|
|
40
|
+
this.port = options.port || 3210;
|
|
41
|
+
this.shouldWatch = options.watch !== false;
|
|
42
|
+
this.computationsDir = options.computationsDir ||
|
|
43
|
+
path.resolve(__dirname, '../../computations');
|
|
44
|
+
this.entityCount = options.entityCount || 10;
|
|
45
|
+
|
|
46
|
+
// Core components
|
|
47
|
+
this.engine = new SimulationEngine(config, {
|
|
48
|
+
entityCount: this.entityCount,
|
|
49
|
+
verbose: true
|
|
50
|
+
});
|
|
51
|
+
this.introspector = new SystemIntrospector(config);
|
|
52
|
+
this.dagAnalyzer = new DAGAnalyzer(this.introspector);
|
|
53
|
+
|
|
54
|
+
// Server state
|
|
55
|
+
this.server = null;
|
|
56
|
+
this.watcher = null;
|
|
57
|
+
this.clients = new Set(); // WebSocket-like clients (SSE)
|
|
58
|
+
|
|
59
|
+
// Current simulation state
|
|
60
|
+
this.lastRun = null;
|
|
61
|
+
this.activeComputation = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Start the server.
|
|
66
|
+
*/
|
|
67
|
+
async start() {
|
|
68
|
+
await this.engine.initialize();
|
|
69
|
+
|
|
70
|
+
this.server = http.createServer((req, res) => this._handleRequest(req, res));
|
|
71
|
+
|
|
72
|
+
this.server.listen(this.port, () => {
|
|
73
|
+
console.log(`\n🚀 Simulation Server running at http://localhost:${this.port}`);
|
|
74
|
+
console.log(` Dashboard: http://localhost:${this.port}/`);
|
|
75
|
+
console.log(` API: http://localhost:${this.port}/api`);
|
|
76
|
+
console.log(`\n Watching: ${this.computationsDir}\n`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Start file watcher
|
|
80
|
+
if (this.shouldWatch && chokidar) {
|
|
81
|
+
this._startWatcher();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Stop the server.
|
|
87
|
+
*/
|
|
88
|
+
stop() {
|
|
89
|
+
if (this.watcher) {
|
|
90
|
+
this.watcher.close();
|
|
91
|
+
}
|
|
92
|
+
if (this.server) {
|
|
93
|
+
this.server.close();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// =========================================================================
|
|
98
|
+
// REQUEST HANDLING
|
|
99
|
+
// =========================================================================
|
|
100
|
+
|
|
101
|
+
_handleRequest(req, res) {
|
|
102
|
+
const url = new URL(req.url, `http://localhost:${this.port}`);
|
|
103
|
+
const pathname = url.pathname;
|
|
104
|
+
|
|
105
|
+
// CORS headers
|
|
106
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
107
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
108
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
109
|
+
|
|
110
|
+
if (req.method === 'OPTIONS') {
|
|
111
|
+
res.writeHead(204);
|
|
112
|
+
res.end();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Route handling
|
|
117
|
+
if (pathname === '/' || pathname === '/index.html') {
|
|
118
|
+
this._serveDashboard(res);
|
|
119
|
+
} else if (pathname === '/dashboard.css') {
|
|
120
|
+
this._serveFile(res, 'dashboard.css', 'text/css');
|
|
121
|
+
} else if (pathname === '/dashboard.js') {
|
|
122
|
+
this._serveFile(res, 'dashboard.js', 'application/javascript');
|
|
123
|
+
} else if (pathname === '/events') {
|
|
124
|
+
this._handleSSE(req, res);
|
|
125
|
+
} else if (pathname.startsWith('/api/')) {
|
|
126
|
+
this._handleAPI(req, res, pathname, url);
|
|
127
|
+
} else {
|
|
128
|
+
res.writeHead(404);
|
|
129
|
+
res.end('Not Found');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async _handleAPI(req, res, pathname, url) {
|
|
134
|
+
res.setHeader('Content-Type', 'application/json');
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
let result;
|
|
138
|
+
|
|
139
|
+
switch (pathname) {
|
|
140
|
+
case '/api/stats':
|
|
141
|
+
result = this.introspector.getStats();
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case '/api/computations':
|
|
145
|
+
result = this.introspector.getComputationNames().map(name => ({
|
|
146
|
+
name,
|
|
147
|
+
passLevel: this.introspector.getPassLevel(name),
|
|
148
|
+
config: this.introspector.getComputationConfig(name)
|
|
149
|
+
}));
|
|
150
|
+
break;
|
|
151
|
+
|
|
152
|
+
case '/api/dag':
|
|
153
|
+
result = this.dagAnalyzer.getVisualizationData();
|
|
154
|
+
break;
|
|
155
|
+
|
|
156
|
+
case '/api/dag/summary':
|
|
157
|
+
result = this.dagAnalyzer.getSummary();
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case '/api/tables':
|
|
161
|
+
result = this.introspector.getAvailableTables().map(name => ({
|
|
162
|
+
name,
|
|
163
|
+
...this.introspector.getTableMetadata(name)
|
|
164
|
+
}));
|
|
165
|
+
break;
|
|
166
|
+
|
|
167
|
+
case '/api/rules':
|
|
168
|
+
result = {};
|
|
169
|
+
for (const mod of this.introspector.getRuleModules()) {
|
|
170
|
+
result[mod] = this.introspector.getRuleFunctions(mod);
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
case '/api/simulate': {
|
|
175
|
+
// POST body contains computation name
|
|
176
|
+
const body = await this._readBody(req);
|
|
177
|
+
const { computation, entityCount, entityIds, targetDate } = JSON.parse(body);
|
|
178
|
+
|
|
179
|
+
this.activeComputation = computation;
|
|
180
|
+
this._broadcast({ type: 'simulationStarted', computation });
|
|
181
|
+
|
|
182
|
+
result = await this.engine.runComputation(computation, {
|
|
183
|
+
entityCount: entityCount || this.entityCount,
|
|
184
|
+
entityIds,
|
|
185
|
+
targetDate
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
this.lastRun = { computation, result, timestamp: new Date().toISOString() };
|
|
189
|
+
this._broadcast({ type: 'simulationComplete', ...this.lastRun });
|
|
190
|
+
this.activeComputation = null;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case '/api/simulate-with-deps': {
|
|
195
|
+
const body = await this._readBody(req);
|
|
196
|
+
const { computation, entityCount, targetDate } = JSON.parse(body);
|
|
197
|
+
|
|
198
|
+
this.activeComputation = computation;
|
|
199
|
+
this._broadcast({ type: 'simulationStarted', computation, withDependencies: true });
|
|
200
|
+
|
|
201
|
+
result = await this.engine.runWithDependencies(computation, {
|
|
202
|
+
entityCount: entityCount || this.entityCount,
|
|
203
|
+
targetDate
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
this.lastRun = { computation, result, timestamp: new Date().toISOString() };
|
|
207
|
+
this._broadcast({ type: 'simulationComplete', ...this.lastRun });
|
|
208
|
+
this.activeComputation = null;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
case '/api/validate': {
|
|
213
|
+
const body = await this._readBody(req);
|
|
214
|
+
const { computation } = JSON.parse(body);
|
|
215
|
+
result = this.engine.validateComputation(computation);
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
case '/api/info': {
|
|
220
|
+
const computation = url.searchParams.get('computation');
|
|
221
|
+
result = this.engine.getComputationInfo(computation);
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case '/api/results': {
|
|
226
|
+
const computation = url.searchParams.get('computation');
|
|
227
|
+
result = this.engine.getResults(computation);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
case '/api/reset':
|
|
232
|
+
this.engine.reset();
|
|
233
|
+
result = { success: true };
|
|
234
|
+
break;
|
|
235
|
+
|
|
236
|
+
case '/api/last-run':
|
|
237
|
+
result = this.lastRun || null;
|
|
238
|
+
break;
|
|
239
|
+
|
|
240
|
+
case '/api/execution-log':
|
|
241
|
+
result = this.engine.getExecutionLog();
|
|
242
|
+
break;
|
|
243
|
+
|
|
244
|
+
default:
|
|
245
|
+
res.writeHead(404);
|
|
246
|
+
result = { error: 'Unknown endpoint' };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
res.writeHead(200);
|
|
250
|
+
res.end(JSON.stringify(result, null, 2));
|
|
251
|
+
|
|
252
|
+
} catch (error) {
|
|
253
|
+
res.writeHead(500);
|
|
254
|
+
res.end(JSON.stringify({ error: error.message, stack: error.stack }));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// =========================================================================
|
|
259
|
+
// SSE (Server-Sent Events)
|
|
260
|
+
// =========================================================================
|
|
261
|
+
|
|
262
|
+
_handleSSE(req, res) {
|
|
263
|
+
res.writeHead(200, {
|
|
264
|
+
'Content-Type': 'text/event-stream',
|
|
265
|
+
'Cache-Control': 'no-cache',
|
|
266
|
+
'Connection': 'keep-alive'
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const client = {
|
|
270
|
+
send: (data) => {
|
|
271
|
+
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
this.clients.add(client);
|
|
276
|
+
|
|
277
|
+
req.on('close', () => {
|
|
278
|
+
this.clients.delete(client);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// Send initial state
|
|
282
|
+
client.send({ type: 'connected', timestamp: new Date().toISOString() });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
_broadcast(data) {
|
|
286
|
+
for (const client of this.clients) {
|
|
287
|
+
try {
|
|
288
|
+
client.send(data);
|
|
289
|
+
} catch (e) {
|
|
290
|
+
this.clients.delete(client);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// =========================================================================
|
|
296
|
+
// FILE WATCHING
|
|
297
|
+
// =========================================================================
|
|
298
|
+
|
|
299
|
+
_startWatcher() {
|
|
300
|
+
this.watcher = chokidar.watch(path.join(this.computationsDir, '**/*.js'), {
|
|
301
|
+
ignoreInitial: true
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
this.watcher.on('change', (filepath) => {
|
|
305
|
+
const filename = path.basename(filepath);
|
|
306
|
+
console.log(`📝 File changed: ${filename}`);
|
|
307
|
+
|
|
308
|
+
// Clear require cache for this file
|
|
309
|
+
delete require.cache[require.resolve(filepath)];
|
|
310
|
+
|
|
311
|
+
this._broadcast({
|
|
312
|
+
type: 'fileChanged',
|
|
313
|
+
file: filename,
|
|
314
|
+
path: filepath,
|
|
315
|
+
timestamp: new Date().toISOString()
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// TODO: Auto-rerun if active computation matches changed file
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// =========================================================================
|
|
323
|
+
// STATIC FILE SERVING
|
|
324
|
+
// =========================================================================
|
|
325
|
+
|
|
326
|
+
_serveDashboard(res) {
|
|
327
|
+
const dashboardPath = path.join(__dirname, 'ui', 'dashboard.html');
|
|
328
|
+
if (fs.existsSync(dashboardPath)) {
|
|
329
|
+
res.setHeader('Content-Type', 'text/html');
|
|
330
|
+
res.writeHead(200);
|
|
331
|
+
res.end(fs.readFileSync(dashboardPath, 'utf8'));
|
|
332
|
+
} else {
|
|
333
|
+
res.setHeader('Content-Type', 'text/html');
|
|
334
|
+
res.writeHead(200);
|
|
335
|
+
res.end(this._getInlineDashboard());
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
_serveFile(res, filename, contentType) {
|
|
340
|
+
const filePath = path.join(__dirname, 'ui', filename);
|
|
341
|
+
if (fs.existsSync(filePath)) {
|
|
342
|
+
res.setHeader('Content-Type', contentType);
|
|
343
|
+
res.writeHead(200);
|
|
344
|
+
res.end(fs.readFileSync(filePath, 'utf8'));
|
|
345
|
+
} else {
|
|
346
|
+
res.writeHead(404);
|
|
347
|
+
res.end('Not Found');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
_getInlineDashboard() {
|
|
352
|
+
return `<!DOCTYPE html>
|
|
353
|
+
<html lang="en">
|
|
354
|
+
<head>
|
|
355
|
+
<meta charset="UTF-8">
|
|
356
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
357
|
+
<title>Computation Simulator</title>
|
|
358
|
+
<style>
|
|
359
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
360
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; min-height: 100vh; }
|
|
361
|
+
.container { max-width: 1400px; margin: 0 auto; padding: 1.5rem; }
|
|
362
|
+
header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid #334155; }
|
|
363
|
+
h1 { font-size: 1.5rem; display: flex; align-items: center; gap: 0.5rem; }
|
|
364
|
+
h1::before { content: '🔄'; }
|
|
365
|
+
.status { padding: 0.5rem 1rem; border-radius: 9999px; font-size: 0.875rem; }
|
|
366
|
+
.status.connected { background: #064e3b; color: #6ee7b7; }
|
|
367
|
+
.status.disconnected { background: #7f1d1d; color: #fca5a5; }
|
|
368
|
+
.grid { display: grid; grid-template-columns: 300px 1fr; gap: 1.5rem; }
|
|
369
|
+
.sidebar { display: flex; flex-direction: column; gap: 1rem; }
|
|
370
|
+
.card { background: #1e293b; border-radius: 0.75rem; padding: 1.25rem; }
|
|
371
|
+
.card h2 { font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.05em; color: #94a3b8; margin-bottom: 1rem; }
|
|
372
|
+
.comp-list { max-height: 400px; overflow-y: auto; }
|
|
373
|
+
.comp-item { padding: 0.75rem; border-radius: 0.5rem; cursor: pointer; margin-bottom: 0.5rem; background: #334155; display: flex; justify-content: space-between; align-items: center; transition: background 0.2s; }
|
|
374
|
+
.comp-item:hover { background: #475569; }
|
|
375
|
+
.comp-item.active { background: #3b82f6; }
|
|
376
|
+
.pass-badge { background: #4338ca; padding: 0.25rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; }
|
|
377
|
+
.main { display: flex; flex-direction: column; gap: 1.5rem; }
|
|
378
|
+
.controls { display: flex; gap: 1rem; }
|
|
379
|
+
.btn { padding: 0.75rem 1.5rem; border-radius: 0.5rem; border: none; cursor: pointer; font-weight: 600; transition: all 0.2s; }
|
|
380
|
+
.btn-primary { background: #3b82f6; color: white; }
|
|
381
|
+
.btn-primary:hover { background: #2563eb; }
|
|
382
|
+
.btn-secondary { background: #475569; color: white; }
|
|
383
|
+
.btn-secondary:hover { background: #64748b; }
|
|
384
|
+
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; }
|
|
385
|
+
.stat { background: #334155; padding: 1rem; border-radius: 0.5rem; text-align: center; }
|
|
386
|
+
.stat-value { font-size: 1.5rem; font-weight: 700; color: #60a5fa; }
|
|
387
|
+
.stat-label { font-size: 0.75rem; color: #94a3b8; margin-top: 0.25rem; }
|
|
388
|
+
.output { background: #0f172a; border-radius: 0.5rem; padding: 1rem; max-height: 400px; overflow-y: auto; font-family: monospace; font-size: 0.875rem; white-space: pre-wrap; }
|
|
389
|
+
.log-entry { padding: 0.5rem; border-left: 3px solid #3b82f6; margin-bottom: 0.5rem; background: #1e293b; }
|
|
390
|
+
.log-entry.success { border-color: #22c55e; }
|
|
391
|
+
.log-entry.error { border-color: #ef4444; }
|
|
392
|
+
.validation { padding: 1rem; border-radius: 0.5rem; }
|
|
393
|
+
.validation.valid { background: #064e3b; }
|
|
394
|
+
.validation.invalid { background: #7f1d1d; }
|
|
395
|
+
#result-output { background: #0f172a; }
|
|
396
|
+
</style>
|
|
397
|
+
</head>
|
|
398
|
+
<body>
|
|
399
|
+
<div class="container">
|
|
400
|
+
<header>
|
|
401
|
+
<h1>Computation Simulator</h1>
|
|
402
|
+
<span id="status" class="status disconnected">Disconnected</span>
|
|
403
|
+
</header>
|
|
404
|
+
<div class="grid">
|
|
405
|
+
<aside class="sidebar">
|
|
406
|
+
<div class="card">
|
|
407
|
+
<h2>Computations</h2>
|
|
408
|
+
<div id="comp-list" class="comp-list"></div>
|
|
409
|
+
</div>
|
|
410
|
+
<div class="card">
|
|
411
|
+
<h2>System Stats</h2>
|
|
412
|
+
<div id="system-stats"></div>
|
|
413
|
+
</div>
|
|
414
|
+
</aside>
|
|
415
|
+
<main class="main">
|
|
416
|
+
<div class="card">
|
|
417
|
+
<h2>Controls</h2>
|
|
418
|
+
<div class="controls">
|
|
419
|
+
<button class="btn btn-primary" id="btn-run">Run Selected</button>
|
|
420
|
+
<button class="btn btn-secondary" id="btn-run-deps">Run with Dependencies</button>
|
|
421
|
+
<button class="btn btn-secondary" id="btn-validate">Validate</button>
|
|
422
|
+
<button class="btn btn-secondary" id="btn-reset">Reset</button>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
<div class="card">
|
|
426
|
+
<h2>Results</h2>
|
|
427
|
+
<div class="stats-grid" id="result-stats"></div>
|
|
428
|
+
<div class="output" id="result-output">Select a computation and click Run</div>
|
|
429
|
+
</div>
|
|
430
|
+
<div class="card">
|
|
431
|
+
<h2>Execution Log</h2>
|
|
432
|
+
<div class="output" id="execution-log"></div>
|
|
433
|
+
</div>
|
|
434
|
+
</main>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
<script>
|
|
438
|
+
let selectedComputation = null;
|
|
439
|
+
let eventSource = null;
|
|
440
|
+
|
|
441
|
+
async function init() {
|
|
442
|
+
await loadComputations();
|
|
443
|
+
await loadSystemStats();
|
|
444
|
+
connectSSE();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function loadComputations() {
|
|
448
|
+
const res = await fetch('/api/computations');
|
|
449
|
+
const comps = await res.json();
|
|
450
|
+
const list = document.getElementById('comp-list');
|
|
451
|
+
list.innerHTML = comps.map(c => \`
|
|
452
|
+
<div class="comp-item" data-name="\${c.name}">
|
|
453
|
+
<span>\${c.name}</span>
|
|
454
|
+
<span class="pass-badge">Pass \${c.passLevel}</span>
|
|
455
|
+
</div>
|
|
456
|
+
\`).join('');
|
|
457
|
+
|
|
458
|
+
list.querySelectorAll('.comp-item').forEach(el => {
|
|
459
|
+
el.addEventListener('click', () => {
|
|
460
|
+
document.querySelectorAll('.comp-item').forEach(e => e.classList.remove('active'));
|
|
461
|
+
el.classList.add('active');
|
|
462
|
+
selectedComputation = el.dataset.name;
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async function loadSystemStats() {
|
|
468
|
+
const res = await fetch('/api/stats');
|
|
469
|
+
const stats = await res.json();
|
|
470
|
+
document.getElementById('system-stats').innerHTML = \`
|
|
471
|
+
<div class="stat"><div class="stat-value">\${stats.computations}</div><div class="stat-label">Computations</div></div>
|
|
472
|
+
<div class="stat"><div class="stat-value">\${stats.tables}</div><div class="stat-label">Tables</div></div>
|
|
473
|
+
<div class="stat"><div class="stat-value">\${stats.totalPasses}</div><div class="stat-label">Passes</div></div>
|
|
474
|
+
\`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function connectSSE() {
|
|
478
|
+
eventSource = new EventSource('/events');
|
|
479
|
+
eventSource.onopen = () => {
|
|
480
|
+
document.getElementById('status').className = 'status connected';
|
|
481
|
+
document.getElementById('status').textContent = 'Connected';
|
|
482
|
+
};
|
|
483
|
+
eventSource.onerror = () => {
|
|
484
|
+
document.getElementById('status').className = 'status disconnected';
|
|
485
|
+
document.getElementById('status').textContent = 'Disconnected';
|
|
486
|
+
};
|
|
487
|
+
eventSource.onmessage = (e) => {
|
|
488
|
+
const data = JSON.parse(e.data);
|
|
489
|
+
handleEvent(data);
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function handleEvent(data) {
|
|
494
|
+
if (data.type === 'fileChanged') {
|
|
495
|
+
addLog(\`File changed: \${data.file}\`, 'info');
|
|
496
|
+
} else if (data.type === 'simulationComplete') {
|
|
497
|
+
displayResults(data.result);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function runSimulation(withDeps = false) {
|
|
502
|
+
if (!selectedComputation) return alert('Select a computation first');
|
|
503
|
+
const endpoint = withDeps ? '/api/simulate-with-deps' : '/api/simulate';
|
|
504
|
+
const res = await fetch(endpoint, {
|
|
505
|
+
method: 'POST',
|
|
506
|
+
headers: { 'Content-Type': 'application/json' },
|
|
507
|
+
body: JSON.stringify({ computation: selectedComputation })
|
|
508
|
+
});
|
|
509
|
+
const result = await res.json();
|
|
510
|
+
displayResults(result);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
async function validate() {
|
|
514
|
+
if (!selectedComputation) return alert('Select a computation first');
|
|
515
|
+
const res = await fetch('/api/validate', {
|
|
516
|
+
method: 'POST',
|
|
517
|
+
headers: { 'Content-Type': 'application/json' },
|
|
518
|
+
body: JSON.stringify({ computation: selectedComputation })
|
|
519
|
+
});
|
|
520
|
+
const result = await res.json();
|
|
521
|
+
const output = document.getElementById('result-output');
|
|
522
|
+
output.innerHTML = result.valid
|
|
523
|
+
? '<div class="validation valid">✅ Valid configuration</div>'
|
|
524
|
+
: \`<div class="validation invalid">❌ Errors: \${result.errors.join(', ')}</div>\`;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function displayResults(result) {
|
|
528
|
+
const statsEl = document.getElementById('result-stats');
|
|
529
|
+
const outputEl = document.getElementById('result-output');
|
|
530
|
+
|
|
531
|
+
if (result.stats) {
|
|
532
|
+
statsEl.innerHTML = \`
|
|
533
|
+
<div class="stat"><div class="stat-value">\${result.stats.executionTimeMs}ms</div><div class="stat-label">Execution</div></div>
|
|
534
|
+
<div class="stat"><div class="stat-value">\${result.stats.entitiesProcessed}</div><div class="stat-label">Entities</div></div>
|
|
535
|
+
<div class="stat"><div class="stat-value">\${Math.round(result.stats.memoryUsedBytes / 1024)}KB</div><div class="stat-label">Memory</div></div>
|
|
536
|
+
<div class="stat"><div class="stat-value">\${result.passLevel || '?'}</div><div class="stat-label">Pass Level</div></div>
|
|
537
|
+
\`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
outputEl.textContent = JSON.stringify(result.results || result, null, 2);
|
|
541
|
+
addLog(\`Simulation complete: \${selectedComputation}\`, result.success ? 'success' : 'error');
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function addLog(message, type = 'info') {
|
|
545
|
+
const log = document.getElementById('execution-log');
|
|
546
|
+
const entry = document.createElement('div');
|
|
547
|
+
entry.className = \`log-entry \${type}\`;
|
|
548
|
+
entry.textContent = \`[\${new Date().toLocaleTimeString()}] \${message}\`;
|
|
549
|
+
log.prepend(entry);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
document.getElementById('btn-run').addEventListener('click', () => runSimulation(false));
|
|
553
|
+
document.getElementById('btn-run-deps').addEventListener('click', () => runSimulation(true));
|
|
554
|
+
document.getElementById('btn-validate').addEventListener('click', validate);
|
|
555
|
+
document.getElementById('btn-reset').addEventListener('click', async () => {
|
|
556
|
+
await fetch('/api/reset');
|
|
557
|
+
document.getElementById('result-output').textContent = 'Reset complete';
|
|
558
|
+
document.getElementById('result-stats').innerHTML = '';
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
init();
|
|
562
|
+
</script>
|
|
563
|
+
</body>
|
|
564
|
+
</html>`;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// =========================================================================
|
|
568
|
+
// HELPERS
|
|
569
|
+
// =========================================================================
|
|
570
|
+
|
|
571
|
+
_readBody(req) {
|
|
572
|
+
return new Promise((resolve, reject) => {
|
|
573
|
+
let body = '';
|
|
574
|
+
req.on('data', chunk => body += chunk);
|
|
575
|
+
req.on('end', () => resolve(body));
|
|
576
|
+
req.on('error', reject);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
module.exports = { SimulationServer };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Simulation Module Index
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { SimulationEngine } = require('./SimulationEngine');
|
|
6
|
+
const { SimulationServer } = require('./SimulationServer');
|
|
7
|
+
const { MockDataFetcher } = require('./MockDataFetcher');
|
|
8
|
+
const { MockStorageManager } = require('./MockStorageManager');
|
|
9
|
+
const { DAGAnalyzer } = require('./DAGAnalyzer');
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
SimulationEngine,
|
|
13
|
+
SimulationServer,
|
|
14
|
+
MockDataFetcher,
|
|
15
|
+
MockStorageManager,
|
|
16
|
+
DAGAnalyzer
|
|
17
|
+
};
|