lsh-framework 0.5.4

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 (90) hide show
  1. package/.env.example +51 -0
  2. package/README.md +399 -0
  3. package/dist/app.js +33 -0
  4. package/dist/cicd/analytics.js +261 -0
  5. package/dist/cicd/auth.js +269 -0
  6. package/dist/cicd/cache-manager.js +172 -0
  7. package/dist/cicd/data-retention.js +305 -0
  8. package/dist/cicd/performance-monitor.js +224 -0
  9. package/dist/cicd/webhook-receiver.js +634 -0
  10. package/dist/cli.js +500 -0
  11. package/dist/commands/api.js +343 -0
  12. package/dist/commands/self.js +318 -0
  13. package/dist/commands/theme.js +257 -0
  14. package/dist/commands/zsh-import.js +240 -0
  15. package/dist/components/App.js +1 -0
  16. package/dist/components/Divider.js +29 -0
  17. package/dist/components/REPL.js +43 -0
  18. package/dist/components/Terminal.js +232 -0
  19. package/dist/components/UserInput.js +30 -0
  20. package/dist/daemon/api-server.js +315 -0
  21. package/dist/daemon/job-registry.js +554 -0
  22. package/dist/daemon/lshd.js +822 -0
  23. package/dist/daemon/monitoring-api.js +220 -0
  24. package/dist/examples/supabase-integration.js +106 -0
  25. package/dist/lib/api-error-handler.js +183 -0
  26. package/dist/lib/associative-arrays.js +285 -0
  27. package/dist/lib/base-api-server.js +290 -0
  28. package/dist/lib/base-command-registrar.js +286 -0
  29. package/dist/lib/base-job-manager.js +293 -0
  30. package/dist/lib/brace-expansion.js +160 -0
  31. package/dist/lib/builtin-commands.js +439 -0
  32. package/dist/lib/cloud-config-manager.js +347 -0
  33. package/dist/lib/command-validator.js +190 -0
  34. package/dist/lib/completion-system.js +344 -0
  35. package/dist/lib/cron-job-manager.js +364 -0
  36. package/dist/lib/daemon-client-helper.js +141 -0
  37. package/dist/lib/daemon-client.js +501 -0
  38. package/dist/lib/database-persistence.js +638 -0
  39. package/dist/lib/database-schema.js +259 -0
  40. package/dist/lib/enhanced-history-system.js +246 -0
  41. package/dist/lib/env-validator.js +265 -0
  42. package/dist/lib/executors/builtin-executor.js +52 -0
  43. package/dist/lib/extended-globbing.js +411 -0
  44. package/dist/lib/extended-parameter-expansion.js +227 -0
  45. package/dist/lib/floating-point-arithmetic.js +256 -0
  46. package/dist/lib/history-system.js +245 -0
  47. package/dist/lib/interactive-shell.js +460 -0
  48. package/dist/lib/job-builtins.js +580 -0
  49. package/dist/lib/job-manager.js +386 -0
  50. package/dist/lib/job-storage-database.js +156 -0
  51. package/dist/lib/job-storage-memory.js +73 -0
  52. package/dist/lib/logger.js +274 -0
  53. package/dist/lib/lshrc-init.js +177 -0
  54. package/dist/lib/pathname-expansion.js +216 -0
  55. package/dist/lib/prompt-system.js +328 -0
  56. package/dist/lib/script-runner.js +226 -0
  57. package/dist/lib/secrets-manager.js +193 -0
  58. package/dist/lib/shell-executor.js +2504 -0
  59. package/dist/lib/shell-parser.js +958 -0
  60. package/dist/lib/shell-types.js +6 -0
  61. package/dist/lib/shell.lib.js +40 -0
  62. package/dist/lib/supabase-client.js +58 -0
  63. package/dist/lib/theme-manager.js +476 -0
  64. package/dist/lib/variable-expansion.js +385 -0
  65. package/dist/lib/zsh-compatibility.js +658 -0
  66. package/dist/lib/zsh-import-manager.js +699 -0
  67. package/dist/lib/zsh-options.js +328 -0
  68. package/dist/pipeline/job-tracker.js +491 -0
  69. package/dist/pipeline/mcli-bridge.js +302 -0
  70. package/dist/pipeline/pipeline-service.js +1116 -0
  71. package/dist/pipeline/workflow-engine.js +867 -0
  72. package/dist/services/api/api.js +58 -0
  73. package/dist/services/api/auth.js +35 -0
  74. package/dist/services/api/config.js +7 -0
  75. package/dist/services/api/file.js +22 -0
  76. package/dist/services/cron/cron-registrar.js +235 -0
  77. package/dist/services/cron/cron.js +9 -0
  78. package/dist/services/daemon/daemon-registrar.js +565 -0
  79. package/dist/services/daemon/daemon.js +9 -0
  80. package/dist/services/lib/lib.js +86 -0
  81. package/dist/services/log-file-extractor.js +170 -0
  82. package/dist/services/secrets/secrets.js +94 -0
  83. package/dist/services/shell/shell.js +28 -0
  84. package/dist/services/supabase/supabase-registrar.js +367 -0
  85. package/dist/services/supabase/supabase.js +9 -0
  86. package/dist/services/zapier.js +16 -0
  87. package/dist/simple-api-server.js +148 -0
  88. package/dist/store/store.js +31 -0
  89. package/dist/util/lib.util.js +11 -0
  90. package/package.json +144 -0
@@ -0,0 +1,1116 @@
1
+ import express, { Router } from 'express';
2
+ import { Pool } from 'pg';
3
+ import { JobTracker, JobStatus } from './job-tracker.js';
4
+ import { MCLIBridge } from './mcli-bridge.js';
5
+ import { WorkflowEngine } from './workflow-engine.js';
6
+ import { Server } from 'socket.io';
7
+ import { createServer } from 'http';
8
+ import * as path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import * as fs from 'fs';
11
+ import { execSync, spawn } from 'child_process';
12
+ import { createProxyMiddleware } from 'http-proxy-middleware';
13
+ // Compatibility function for ES modules and CommonJS
14
+ function getCurrentDirname() {
15
+ // Use eval to avoid TypeScript compilation issues in CommonJS mode
16
+ try {
17
+ const importMeta = eval('import.meta');
18
+ return path.dirname(fileURLToPath(importMeta.url));
19
+ }
20
+ catch {
21
+ // Use src/pipeline directory as fallback for testing
22
+ return path.join(process.cwd(), 'src', 'pipeline');
23
+ }
24
+ }
25
+ const currentDir = getCurrentDirname();
26
+ export class PipelineService {
27
+ app;
28
+ server;
29
+ io;
30
+ pool;
31
+ jobTracker;
32
+ mcliBridge;
33
+ workflowEngine;
34
+ config;
35
+ isDemoMode = false;
36
+ streamlitProcess = null;
37
+ getSystemJobs() {
38
+ const jobs = [];
39
+ const monitoringJobs = [
40
+ { script: 'db-health-monitor', name: 'Database Health Monitor', type: 'monitoring', owner: 'ops-team', schedule: '*/5 * * * *' },
41
+ { script: 'politician-trading-monitor', name: 'Politician Trading Monitor', type: 'data-ingestion', owner: 'data-team', schedule: '*/30 * * * *' },
42
+ { script: 'shell-analytics', name: 'Shell Analytics', type: 'analytics', owner: 'analytics-team', schedule: '0 * * * *' },
43
+ { script: 'data-consistency-check', name: 'Data Consistency Check', type: 'validation', owner: 'data-team', schedule: '0 */6 * * *' },
44
+ { script: 'performance-monitor', name: 'Performance Monitor', type: 'monitoring', owner: 'ops-team', schedule: '*/15 * * * *' },
45
+ { script: 'alert-monitor', name: 'Alert Monitor', type: 'alerting', owner: 'ops-team', schedule: '*/2 * * * *' },
46
+ { script: 'daily-summary', name: 'Daily Summary Report', type: 'reporting', owner: 'management', schedule: '0 9 * * *' },
47
+ { script: 'log-cleanup', name: 'Log Cleanup', type: 'maintenance', owner: 'ops-team', schedule: '0 2 * * *' }
48
+ ];
49
+ monitoringJobs.forEach((job, _index) => {
50
+ const logPath = `/Users/lefv/repos/lsh/logs/${job.script}.log`;
51
+ let status = 'unknown';
52
+ let lastRun = new Date(Date.now() - Math.random() * 86400000).toISOString();
53
+ let progress = 0;
54
+ // Try to read actual log file for status
55
+ try {
56
+ if (fs.existsSync(logPath)) {
57
+ const stats = fs.statSync(logPath);
58
+ lastRun = stats.mtime.toISOString();
59
+ // Check if job is currently running based on schedule
60
+ const _now = new Date();
61
+ const schedulePattern = job.schedule;
62
+ if (schedulePattern.includes('*/2')) {
63
+ status = 'running';
64
+ progress = Math.floor(Math.random() * 100);
65
+ }
66
+ else if (schedulePattern.includes('*/5') || schedulePattern.includes('*/15')) {
67
+ status = Math.random() > 0.5 ? 'running' : 'completed';
68
+ progress = status === 'running' ? Math.floor(Math.random() * 100) : 100;
69
+ }
70
+ else {
71
+ status = 'completed';
72
+ progress = 100;
73
+ }
74
+ }
75
+ }
76
+ catch (_error) {
77
+ // If can't read log, use defaults
78
+ }
79
+ jobs.push({
80
+ id: `job-${job.script}`,
81
+ name: job.name,
82
+ type: job.type,
83
+ owner: job.owner,
84
+ status,
85
+ sourceSystem: 'lsh-cron',
86
+ targetSystem: job.type === 'data-ingestion' ? 'database' : 'monitoring',
87
+ schedule: job.schedule,
88
+ createdAt: new Date(Date.now() - 7 * 86400000).toISOString(),
89
+ updatedAt: lastRun,
90
+ progress
91
+ });
92
+ });
93
+ return jobs;
94
+ }
95
+ constructor(config = {}) {
96
+ const port = config.port || parseInt(process.env.PORT || '3034', 10);
97
+ this.config = {
98
+ port,
99
+ databaseUrl: config.databaseUrl || process.env.DATABASE_URL || 'postgresql://localhost:5432/pipeline',
100
+ mcliUrl: config.mcliUrl || process.env.MCLI_URL || 'http://localhost:8000',
101
+ mcliApiKey: config.mcliApiKey || process.env.MCLI_API_KEY,
102
+ webhookBaseUrl: config.webhookBaseUrl || `http://localhost:${port}`
103
+ };
104
+ // Initialize database pool
105
+ this.pool = new Pool({
106
+ connectionString: this.config.databaseUrl
107
+ });
108
+ // Initialize services
109
+ this.jobTracker = new JobTracker(this.pool);
110
+ this.mcliBridge = new MCLIBridge({
111
+ baseUrl: this.config.mcliUrl,
112
+ apiKey: this.config.mcliApiKey,
113
+ webhookUrl: this.config.webhookBaseUrl
114
+ }, this.jobTracker);
115
+ this.workflowEngine = new WorkflowEngine(this.pool, this.jobTracker);
116
+ // Initialize Express app
117
+ this.app = express();
118
+ this.server = createServer(this.app);
119
+ this.io = new Server(this.server, {
120
+ cors: {
121
+ origin: "*",
122
+ methods: ["GET", "POST"]
123
+ }
124
+ });
125
+ this.setupMiddleware();
126
+ this.setupRoutes();
127
+ this.setupWebSocket();
128
+ this.setupEventListeners();
129
+ this.startStreamlit();
130
+ }
131
+ async startStreamlit() {
132
+ try {
133
+ // Check if Streamlit is already running on port 8501
134
+ const checkCmd = 'lsof -i :8501';
135
+ try {
136
+ execSync(checkCmd, { stdio: 'ignore' });
137
+ console.log('✅ Streamlit ML Dashboard is already running on port 8501');
138
+ return;
139
+ }
140
+ catch {
141
+ // Port is free, continue to start Streamlit
142
+ }
143
+ console.log('🚀 Starting Streamlit ML Dashboard...');
144
+ // Path to MCLI repo and Streamlit app
145
+ const mcliPath = '/Users/lefv/repos/mcli';
146
+ const streamlitAppPath = 'src/mcli/ml/dashboard/app_supabase.py';
147
+ // Start Streamlit process
148
+ this.streamlitProcess = spawn('uv', [
149
+ 'run', 'streamlit', 'run', streamlitAppPath,
150
+ '--server.port', '8501',
151
+ '--server.address', 'localhost',
152
+ '--browser.gatherUsageStats', 'false',
153
+ '--server.headless', 'true'
154
+ ], {
155
+ cwd: mcliPath,
156
+ stdio: ['pipe', 'pipe', 'pipe']
157
+ });
158
+ // Handle process output
159
+ this.streamlitProcess.stdout?.on('data', (data) => {
160
+ const output = data.toString();
161
+ if (output.includes('You can now view your Streamlit app')) {
162
+ console.log('✅ Streamlit ML Dashboard started successfully at http://localhost:8501');
163
+ }
164
+ });
165
+ this.streamlitProcess.stderr?.on('data', (data) => {
166
+ const error = data.toString();
167
+ if (!error.includes('WARNING') && !error.includes('INFO')) {
168
+ console.error('❌ Streamlit Error:', error);
169
+ }
170
+ });
171
+ this.streamlitProcess.on('exit', (code) => {
172
+ if (code !== 0 && code !== null) {
173
+ console.error(`❌ Streamlit process exited with code ${code}`);
174
+ }
175
+ this.streamlitProcess = null;
176
+ });
177
+ // Wait a moment for Streamlit to start
178
+ await new Promise(resolve => setTimeout(resolve, 3000));
179
+ }
180
+ catch (error) {
181
+ console.error('❌ Failed to start Streamlit ML Dashboard:', error);
182
+ }
183
+ }
184
+ setupMiddleware() {
185
+ this.app.use(express.json({ limit: '10mb' }));
186
+ this.app.use(express.urlencoded({ extended: true }));
187
+ // Serve dashboard from src directory
188
+ const dashboardPath = path.join(currentDir, '..', '..', 'src', 'pipeline', 'dashboard');
189
+ console.log(`Serving dashboard from: ${dashboardPath}`);
190
+ this.app.use('/dashboard', express.static(dashboardPath));
191
+ // CORS
192
+ this.app.use((req, res, next) => {
193
+ res.header('Access-Control-Allow-Origin', '*');
194
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
195
+ res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
196
+ if (req.method === 'OPTIONS') {
197
+ return res.sendStatus(200);
198
+ }
199
+ next();
200
+ });
201
+ // Request logging
202
+ this.app.use((req, res, next) => {
203
+ console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
204
+ next();
205
+ });
206
+ }
207
+ setupRoutes() {
208
+ const router = Router();
209
+ // Root route - redirect to dashboard
210
+ router.get('/', (req, res) => {
211
+ res.redirect('/dashboard/');
212
+ });
213
+ // Dashboard routes
214
+ router.get('/dashboard/', (req, res) => {
215
+ const dashboardPath = path.join(currentDir, '..', '..', 'src', 'pipeline', 'dashboard', 'index.html');
216
+ res.sendFile(dashboardPath);
217
+ });
218
+ // Hub route - central dashboard hub
219
+ router.get('/hub', (req, res) => {
220
+ const hubPath = path.join(currentDir, '..', '..', 'src', 'pipeline', 'dashboard', 'hub.html');
221
+ res.sendFile(hubPath);
222
+ });
223
+ // === CONSOLIDATED ENDPOINTS FOR ALL SERVICES ===
224
+ // ML Dashboard endpoints (replaces port 8501 Streamlit)
225
+ router.get('/ml', (req, res) => {
226
+ res.redirect('/ml/dashboard');
227
+ });
228
+ // ML Dashboard proxy to Streamlit
229
+ const mlDashboardProxy = createProxyMiddleware({
230
+ target: 'http://localhost:8501',
231
+ changeOrigin: true,
232
+ ws: true,
233
+ pathRewrite: {
234
+ '^/ml/dashboard': '',
235
+ },
236
+ });
237
+ router.use('/ml/dashboard', mlDashboardProxy);
238
+ // CI/CD Dashboard endpoints (replaces port 3033)
239
+ router.get('/cicd', (req, res) => {
240
+ res.redirect('/cicd/dashboard');
241
+ });
242
+ router.get('/cicd/dashboard', (req, res) => {
243
+ // Serve CI/CD dashboard
244
+ const cicdPath = path.join(currentDir, '..', '..', 'src', 'cicd', 'dashboard', 'index.html');
245
+ if (fs.existsSync(cicdPath)) {
246
+ res.sendFile(cicdPath);
247
+ }
248
+ else {
249
+ // Serve a demo CI/CD dashboard
250
+ res.send(`
251
+ <!DOCTYPE html>
252
+ <html>
253
+ <head>
254
+ <title>CI/CD Dashboard</title>
255
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
256
+ </head>
257
+ <body class="bg-light">
258
+ <div class="container py-5">
259
+ <h1>🚀 CI/CD Dashboard</h1>
260
+ <p class="lead">Continuous Integration & Deployment Pipeline</p>
261
+ <div class="row mt-4">
262
+ <div class="col-md-4">
263
+ <div class="card">
264
+ <div class="card-body">
265
+ <h5 class="card-title">Build Status</h5>
266
+ <span class="badge bg-success">Passing</span>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ <div class="col-md-4">
271
+ <div class="card">
272
+ <div class="card-body">
273
+ <h5 class="card-title">Test Coverage</h5>
274
+ <div class="progress">
275
+ <div class="progress-bar bg-success" style="width: 87%">87%</div>
276
+ </div>
277
+ </div>
278
+ </div>
279
+ </div>
280
+ <div class="col-md-4">
281
+ <div class="card">
282
+ <div class="card-body">
283
+ <h5 class="card-title">Deployments</h5>
284
+ <p class="text-muted">Last: 2 hours ago</p>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ </div>
289
+ </div>
290
+ </body>
291
+ </html>
292
+ `);
293
+ }
294
+ });
295
+ router.get('/cicd/health', (req, res) => {
296
+ res.json({ status: 'healthy', service: 'CI/CD Dashboard', timestamp: new Date().toISOString() });
297
+ });
298
+ // CI/CD API endpoints
299
+ router.get('/api/metrics', (req, res) => {
300
+ // Return demo CI/CD metrics
301
+ const today = new Date();
302
+ const totalBuilds = Math.floor(Math.random() * 50) + 20;
303
+ const successfulBuilds = Math.floor(totalBuilds * (0.8 + Math.random() * 0.15));
304
+ const failedBuilds = totalBuilds - successfulBuilds;
305
+ const avgDurationMs = (5 + Math.random() * 15) * 60 * 1000; // 5-20 minutes
306
+ const activePipelines = Math.floor(Math.random() * 5);
307
+ res.json({
308
+ totalBuilds,
309
+ successfulBuilds,
310
+ failedBuilds,
311
+ successRate: totalBuilds > 0 ? (successfulBuilds / totalBuilds) * 100 : 0,
312
+ avgDurationMs,
313
+ activePipelines,
314
+ lastUpdated: today.toISOString()
315
+ });
316
+ });
317
+ router.get('/api/pipelines', (req, res) => {
318
+ // Return demo CI/CD pipeline data
319
+ const limit = parseInt(req.query.limit) || 20;
320
+ const platforms = ['github', 'gitlab', 'jenkins'];
321
+ const repositories = ['lsh', 'mcli', 'data-pipeline', 'monitoring', 'frontend'];
322
+ const statuses = ['completed', 'in_progress', 'failed', 'queued'];
323
+ const actors = ['alice', 'bob', 'charlie', 'diana', 'eve'];
324
+ const workflows = ['CI', 'Deploy', 'Test', 'Release', 'Hotfix'];
325
+ const pipelines = Array.from({ length: limit }, (_, i) => {
326
+ const platform = platforms[Math.floor(Math.random() * platforms.length)];
327
+ const repository = repositories[Math.floor(Math.random() * repositories.length)];
328
+ const status = statuses[Math.floor(Math.random() * statuses.length)];
329
+ const actor = actors[Math.floor(Math.random() * actors.length)];
330
+ const workflow = workflows[Math.floor(Math.random() * workflows.length)];
331
+ const startedAt = new Date(Date.now() - Math.random() * 24 * 60 * 60 * 1000);
332
+ const duration = status === 'completed' ? Math.random() * 1800000 : null; // up to 30 minutes
333
+ const conclusion = status === 'completed' ? (Math.random() > 0.2 ? 'success' : 'failure') : null;
334
+ return {
335
+ id: `pipeline_${i + 1}`,
336
+ workflow_name: workflow,
337
+ repository,
338
+ branch: Math.random() > 0.3 ? 'main' : 'develop',
339
+ platform,
340
+ status,
341
+ conclusion,
342
+ actor,
343
+ started_at: startedAt.toISOString(),
344
+ duration_ms: duration,
345
+ created_at: startedAt.toISOString(),
346
+ updated_at: new Date(startedAt.getTime() + (duration || 0)).toISOString()
347
+ };
348
+ });
349
+ res.json(pipelines);
350
+ });
351
+ // Monitoring API endpoints (replaces port 3035)
352
+ router.get('/monitoring/api/health', (req, res) => {
353
+ res.json({
354
+ status: 'healthy',
355
+ service: 'Monitoring API',
356
+ uptime: process.uptime(),
357
+ timestamp: new Date().toISOString()
358
+ });
359
+ });
360
+ router.get('/monitoring/api/metrics', async (req, res) => {
361
+ // Return system metrics
362
+ const metrics = {
363
+ jobs_total: this.getSystemJobs().length,
364
+ jobs_running: this.getSystemJobs().filter(j => j.status === 'running').length,
365
+ jobs_failed: this.getSystemJobs().filter(j => j.status === 'failed').length,
366
+ system_uptime: process.uptime(),
367
+ memory_usage: process.memoryUsage(),
368
+ timestamp: new Date().toISOString()
369
+ };
370
+ res.json(metrics);
371
+ });
372
+ router.get('/monitoring/api/jobs', (req, res) => {
373
+ // Return monitoring jobs
374
+ const jobs = this.getSystemJobs();
375
+ res.json({ jobs, total: jobs.length });
376
+ });
377
+ router.get('/monitoring/api/alerts', (req, res) => {
378
+ // Return system alerts
379
+ const alerts = [
380
+ { id: 1, level: 'info', message: 'System operating normally', timestamp: new Date().toISOString() }
381
+ ];
382
+ res.json({ alerts, total: alerts.length });
383
+ });
384
+ // Unified health check endpoint for all services
385
+ router.get('/health/all', (req, res) => {
386
+ res.json({
387
+ status: 'healthy',
388
+ services: {
389
+ pipeline: 'running',
390
+ cicd: 'running',
391
+ monitoring: 'running',
392
+ ml: 'requires separate streamlit instance'
393
+ },
394
+ timestamp: new Date().toISOString()
395
+ });
396
+ });
397
+ // Health check
398
+ router.get('/health', (req, res) => {
399
+ res.json({
400
+ status: 'healthy',
401
+ timestamp: new Date().toISOString(),
402
+ services: {
403
+ database: this.pool ? 'connected' : 'disconnected',
404
+ mcli: 'configured',
405
+ jobTracker: 'active'
406
+ }
407
+ });
408
+ });
409
+ // Job Management Routes
410
+ router.post('/api/pipeline/jobs', async (req, res) => {
411
+ if (this.isDemoMode) {
412
+ const demoJob = {
413
+ id: `job-${Date.now()}`,
414
+ ...req.body,
415
+ status: 'queued',
416
+ createdAt: new Date().toISOString(),
417
+ updatedAt: new Date().toISOString()
418
+ };
419
+ return res.status(201).json(demoJob);
420
+ }
421
+ try {
422
+ const job = req.body;
423
+ const createdJob = await this.jobTracker.createJob(job);
424
+ res.status(201).json(createdJob);
425
+ }
426
+ catch (error) {
427
+ console.error('Error creating job:', error);
428
+ res.status(500).json({ error: 'Failed to create job' });
429
+ }
430
+ });
431
+ router.get('/api/pipeline/jobs', async (req, res) => {
432
+ if (this.isDemoMode) {
433
+ // Return jobs based on actual system monitoring jobs
434
+ const jobs = this.getSystemJobs();
435
+ return res.json({ jobs, total: jobs.length });
436
+ }
437
+ try {
438
+ const filters = {
439
+ status: req.query.status,
440
+ sourceSystem: req.query.sourceSystem,
441
+ targetSystem: req.query.targetSystem,
442
+ owner: req.query.owner,
443
+ team: req.query.team,
444
+ limit: parseInt(req.query.limit) || 50,
445
+ offset: parseInt(req.query.offset) || 0
446
+ };
447
+ const result = await this.jobTracker.listJobs(filters);
448
+ res.json(result);
449
+ }
450
+ catch (error) {
451
+ console.error('Error listing jobs:', error);
452
+ res.status(500).json({ error: 'Failed to list jobs' });
453
+ }
454
+ });
455
+ // Job logs endpoint
456
+ router.get('/api/pipeline/jobs/:id/logs', async (req, res) => {
457
+ if (this.isDemoMode) {
458
+ const jobId = req.params.id;
459
+ const scriptName = jobId.replace('job-', '');
460
+ const logPath = `/Users/lefv/repos/lsh/logs/${scriptName}.log`;
461
+ try {
462
+ if (fs.existsSync(logPath)) {
463
+ const logContent = fs.readFileSync(logPath, 'utf-8');
464
+ const lines = logContent.split('\n').slice(-100); // Last 100 lines
465
+ const logs = lines.filter(line => line.trim()).map(line => {
466
+ let level = 'info';
467
+ if (line.includes('ERROR') || line.includes('error'))
468
+ level = 'error';
469
+ else if (line.includes('WARNING') || line.includes('warning'))
470
+ level = 'warning';
471
+ else if (line.includes('SUCCESS') || line.includes('✅'))
472
+ level = 'success';
473
+ return {
474
+ timestamp: new Date().toISOString(),
475
+ level,
476
+ message: line
477
+ };
478
+ });
479
+ return res.json({ logs });
480
+ }
481
+ }
482
+ catch (error) {
483
+ console.error('Error reading log file:', error);
484
+ }
485
+ // Return demo logs if file doesn't exist
486
+ return res.json({
487
+ logs: [
488
+ { timestamp: new Date().toISOString(), level: 'info', message: 'Job started' },
489
+ { timestamp: new Date().toISOString(), level: 'info', message: 'Processing data...' },
490
+ { timestamp: new Date().toISOString(), level: 'success', message: 'Job completed successfully' }
491
+ ]
492
+ });
493
+ }
494
+ // Real implementation would fetch from database
495
+ res.json({ logs: [] });
496
+ });
497
+ router.get('/api/pipeline/jobs/:id', async (req, res) => {
498
+ if (this.isDemoMode) {
499
+ // Find the actual job from the system jobs
500
+ const allJobs = this.getSystemJobs();
501
+ const job = allJobs.find(j => j.id === req.params.id);
502
+ if (job) {
503
+ return res.json(job);
504
+ }
505
+ // Fallback to demo job if not found
506
+ const demoJob = {
507
+ id: req.params.id,
508
+ name: 'Demo Job',
509
+ type: 'batch',
510
+ owner: 'demo-user',
511
+ status: 'running',
512
+ sourceSystem: 'lsh',
513
+ targetSystem: 'mcli',
514
+ createdAt: new Date(Date.now() - 1800000).toISOString(),
515
+ updatedAt: new Date().toISOString(),
516
+ progress: 75,
517
+ schedule: '*/5 * * * *'
518
+ };
519
+ return res.json(demoJob);
520
+ }
521
+ try {
522
+ const job = await this.jobTracker.getJob(req.params.id);
523
+ if (!job) {
524
+ return res.status(404).json({ error: 'Job not found' });
525
+ }
526
+ res.json(job);
527
+ }
528
+ catch (error) {
529
+ console.error('Error getting job:', error);
530
+ res.status(500).json({ error: 'Failed to get job' });
531
+ }
532
+ });
533
+ router.put('/api/pipeline/jobs/:id/cancel', async (req, res) => {
534
+ try {
535
+ const job = await this.jobTracker.getJob(req.params.id);
536
+ if (!job) {
537
+ return res.status(404).json({ error: 'Job not found' });
538
+ }
539
+ await this.jobTracker.updateJobStatus(req.params.id, JobStatus.CANCELLED);
540
+ // Cancel in MCLI if applicable
541
+ if (job.externalId && job.targetSystem === 'mcli') {
542
+ await this.mcliBridge.cancelJob(job.externalId);
543
+ }
544
+ res.json({ message: 'Job cancelled successfully' });
545
+ }
546
+ catch (error) {
547
+ console.error('Error cancelling job:', error);
548
+ res.status(500).json({ error: 'Failed to cancel job' });
549
+ }
550
+ });
551
+ router.put('/api/pipeline/jobs/:id/retry', async (req, res) => {
552
+ try {
553
+ const job = await this.jobTracker.getJob(req.params.id);
554
+ if (!job) {
555
+ return res.status(404).json({ error: 'Job not found' });
556
+ }
557
+ // Create new execution for retry
558
+ const execution = await this.jobTracker.createExecution(req.params.id);
559
+ // If MCLI job, resubmit
560
+ if (job.targetSystem === 'mcli') {
561
+ await this.mcliBridge.submitJobToMCLI(job);
562
+ }
563
+ res.json({ message: 'Job retry initiated', executionId: execution.id });
564
+ }
565
+ catch (error) {
566
+ console.error('Error retrying job:', error);
567
+ res.status(500).json({ error: 'Failed to retry job' });
568
+ }
569
+ });
570
+ // Job Metrics
571
+ router.get('/api/pipeline/jobs/:id/metrics', async (req, res) => {
572
+ try {
573
+ const metrics = await this.jobTracker.getJobMetrics(req.params.id, req.query.metricName);
574
+ res.json(metrics);
575
+ }
576
+ catch (error) {
577
+ console.error('Error getting job metrics:', error);
578
+ res.status(500).json({ error: 'Failed to get job metrics' });
579
+ }
580
+ });
581
+ // Active Jobs
582
+ router.get('/api/pipeline/jobs/active', async (req, res) => {
583
+ if (this.isDemoMode) {
584
+ const allJobs = this.getSystemJobs();
585
+ const activeJobs = allJobs.filter(job => job.status === 'running');
586
+ return res.json(activeJobs);
587
+ }
588
+ try {
589
+ const jobs = await this.jobTracker.getActiveJobs();
590
+ res.json(jobs);
591
+ }
592
+ catch (error) {
593
+ console.error('Error getting active jobs:', error);
594
+ res.status(500).json({ error: 'Failed to get active jobs' });
595
+ }
596
+ });
597
+ // Success Rates
598
+ router.get('/api/pipeline/metrics/success-rates', async (req, res) => {
599
+ if (this.isDemoMode) {
600
+ const successRates = {
601
+ overall: 0.95,
602
+ bySystem: {
603
+ lsh: 0.97,
604
+ mcli: 0.94,
605
+ monitoring: 0.93
606
+ },
607
+ last24h: 0.96,
608
+ last7d: 0.95
609
+ };
610
+ return res.json(successRates);
611
+ }
612
+ try {
613
+ const rates = await this.jobTracker.getJobSuccessRates();
614
+ res.json(rates);
615
+ }
616
+ catch (error) {
617
+ console.error('Error getting success rates:', error);
618
+ res.status(500).json({ error: 'Failed to get success rates' });
619
+ }
620
+ });
621
+ // MCLI Webhook endpoint
622
+ router.post('/webhook/mcli', async (req, res) => {
623
+ try {
624
+ await this.mcliBridge.handleWebhook(req.body);
625
+ res.json({ success: true });
626
+ }
627
+ catch (error) {
628
+ console.error('Error handling MCLI webhook:', error);
629
+ res.status(500).json({ error: 'Failed to handle webhook' });
630
+ }
631
+ });
632
+ // MCLI Status Sync
633
+ router.post('/api/pipeline/sync/mcli/:jobId', async (req, res) => {
634
+ try {
635
+ await this.mcliBridge.syncJobStatus(req.params.jobId);
636
+ res.json({ message: 'Job synced successfully' });
637
+ }
638
+ catch (error) {
639
+ console.error('Error syncing job:', error);
640
+ res.status(500).json({ error: 'Failed to sync job' });
641
+ }
642
+ });
643
+ // MCLI Health Check
644
+ router.get('/api/pipeline/mcli/health', async (req, res) => {
645
+ try {
646
+ const isHealthy = await this.mcliBridge.healthCheck();
647
+ res.json({ healthy: isHealthy });
648
+ }
649
+ catch (error) {
650
+ console.error('Error checking MCLI health:', error);
651
+ res.status(500).json({ error: 'Failed to check MCLI health' });
652
+ }
653
+ });
654
+ // MCLI Statistics
655
+ router.get('/api/pipeline/mcli/statistics', async (req, res) => {
656
+ try {
657
+ const stats = await this.mcliBridge.getStatistics();
658
+ res.json(stats);
659
+ }
660
+ catch (error) {
661
+ console.error('Error getting MCLI statistics:', error);
662
+ res.status(500).json({ error: 'Failed to get MCLI statistics' });
663
+ }
664
+ });
665
+ // Pipeline Statistics
666
+ router.get('/api/pipeline/statistics', async (req, res) => {
667
+ if (this.isDemoMode) {
668
+ // Return statistics based on actual system jobs
669
+ const jobs = this.getSystemJobs();
670
+ const activeJobs = jobs.filter(j => j.status === 'running').length;
671
+ const completedJobs = jobs.filter(j => j.status === 'completed').length;
672
+ const failedJobs = jobs.filter(j => j.status === 'failed').length;
673
+ const stats = {
674
+ total_jobs: String(jobs.length),
675
+ total_executions: String(jobs.length * 24), // Assuming daily runs
676
+ completed_jobs: String(completedJobs),
677
+ failed_jobs: String(failedJobs),
678
+ active_jobs: String(activeJobs),
679
+ avg_duration_ms: '45000',
680
+ max_duration_ms: '180000',
681
+ min_duration_ms: '5000'
682
+ };
683
+ return res.json(stats);
684
+ }
685
+ try {
686
+ const query = `
687
+ SELECT
688
+ COUNT(DISTINCT j.id) as total_jobs,
689
+ COUNT(DISTINCT e.id) as total_executions,
690
+ COUNT(DISTINCT j.id) FILTER (WHERE j.status = 'completed') as completed_jobs,
691
+ COUNT(DISTINCT j.id) FILTER (WHERE j.status = 'failed') as failed_jobs,
692
+ COUNT(DISTINCT j.id) FILTER (WHERE j.status IN ('running', 'queued')) as active_jobs,
693
+ AVG(e.duration_ms) as avg_duration_ms,
694
+ MAX(e.duration_ms) as max_duration_ms,
695
+ MIN(e.duration_ms) FILTER (WHERE e.duration_ms > 0) as min_duration_ms
696
+ FROM pipeline_jobs j
697
+ LEFT JOIN job_executions e ON j.id = e.job_id
698
+ WHERE j.created_at > CURRENT_TIMESTAMP - INTERVAL '7 days'
699
+ `;
700
+ const result = await this.pool.query(query);
701
+ res.json(result.rows[0]);
702
+ }
703
+ catch (error) {
704
+ console.error('Error getting pipeline statistics:', error);
705
+ res.status(500).json({ error: 'Failed to get pipeline statistics' });
706
+ }
707
+ });
708
+ // Recent Events
709
+ router.get('/api/pipeline/events', async (req, res) => {
710
+ if (this.isDemoMode) {
711
+ // Return demo events
712
+ const demoEvents = [
713
+ {
714
+ id: 'event-1',
715
+ type: 'job_completed',
716
+ message: 'Job "Data Sync - Users" completed successfully',
717
+ occurred_at: new Date(Date.now() - 300000).toISOString()
718
+ },
719
+ {
720
+ id: 'event-2',
721
+ type: 'job_started',
722
+ message: 'Job "ML Model Training" started',
723
+ occurred_at: new Date(Date.now() - 600000).toISOString()
724
+ },
725
+ {
726
+ id: 'event-3',
727
+ type: 'job_queued',
728
+ message: 'Job "Metrics Collection" queued for processing',
729
+ occurred_at: new Date(Date.now() - 900000).toISOString()
730
+ }
731
+ ];
732
+ return res.json(demoEvents);
733
+ }
734
+ try {
735
+ const limit = parseInt(req.query.limit) || 100;
736
+ const query = `
737
+ SELECT * FROM pipeline_events
738
+ ORDER BY occurred_at DESC
739
+ LIMIT $1
740
+ `;
741
+ const result = await this.pool.query(query, [limit]);
742
+ res.json(result.rows);
743
+ }
744
+ catch (error) {
745
+ console.error('Error getting events:', error);
746
+ res.status(500).json({ error: 'Failed to get events' });
747
+ }
748
+ });
749
+ // Workflow Management Routes
750
+ router.post('/api/pipeline/workflows', async (req, res) => {
751
+ if (this.isDemoMode) {
752
+ const demoWorkflow = {
753
+ id: `workflow-${Date.now()}`,
754
+ ...req.body,
755
+ status: 'draft',
756
+ createdAt: new Date().toISOString(),
757
+ updatedAt: new Date().toISOString()
758
+ };
759
+ return res.status(201).json(demoWorkflow);
760
+ }
761
+ try {
762
+ const workflow = await this.workflowEngine.createWorkflow(req.body);
763
+ res.status(201).json(workflow);
764
+ }
765
+ catch (error) {
766
+ console.error('Error creating workflow:', error);
767
+ res.status(500).json({ error: 'Failed to create workflow' });
768
+ }
769
+ });
770
+ router.get('/api/pipeline/workflows', async (req, res) => {
771
+ if (this.isDemoMode) {
772
+ const demoWorkflows = [
773
+ {
774
+ id: 'workflow-1',
775
+ name: 'Daily Data Pipeline',
776
+ description: 'Syncs data from LSH to MCLI daily',
777
+ status: 'active',
778
+ nodes: 3,
779
+ createdAt: new Date(Date.now() - 86400000).toISOString()
780
+ },
781
+ {
782
+ id: 'workflow-2',
783
+ name: 'ML Training Pipeline',
784
+ description: 'Trains and deploys ML models',
785
+ status: 'active',
786
+ nodes: 5,
787
+ createdAt: new Date(Date.now() - 172800000).toISOString()
788
+ }
789
+ ];
790
+ return res.json({ workflows: demoWorkflows, total: demoWorkflows.length });
791
+ }
792
+ try {
793
+ const workflows = await this.workflowEngine.listWorkflows({
794
+ status: req.query.status,
795
+ limit: parseInt(req.query.limit) || 50,
796
+ offset: parseInt(req.query.offset) || 0
797
+ });
798
+ res.json(workflows);
799
+ }
800
+ catch (error) {
801
+ console.error('Error listing workflows:', error);
802
+ res.status(500).json({ error: 'Failed to list workflows' });
803
+ }
804
+ });
805
+ router.get('/api/pipeline/workflows/:id', async (req, res) => {
806
+ if (this.isDemoMode) {
807
+ const demoWorkflow = {
808
+ id: req.params.id,
809
+ name: 'Demo Workflow',
810
+ description: 'A demo workflow for testing',
811
+ status: 'active',
812
+ nodes: [
813
+ { id: 'node1', type: 'trigger', name: 'Start' },
814
+ { id: 'node2', type: 'action', name: 'Process Data' },
815
+ { id: 'node3', type: 'condition', name: 'Check Status' }
816
+ ],
817
+ createdAt: new Date(Date.now() - 86400000).toISOString()
818
+ };
819
+ return res.json(demoWorkflow);
820
+ }
821
+ try {
822
+ const workflow = await this.workflowEngine.getWorkflow(req.params.id);
823
+ if (!workflow) {
824
+ return res.status(404).json({ error: 'Workflow not found' });
825
+ }
826
+ res.json(workflow);
827
+ }
828
+ catch (error) {
829
+ console.error('Error getting workflow:', error);
830
+ res.status(500).json({ error: 'Failed to get workflow' });
831
+ }
832
+ });
833
+ router.post('/api/pipeline/workflows/:id/execute', async (req, res) => {
834
+ try {
835
+ const { triggeredBy = 'api', triggerType = 'manual', parameters = {} } = req.body;
836
+ const execution = await this.workflowEngine.executeWorkflow(req.params.id, triggeredBy, triggerType, parameters);
837
+ res.status(201).json(execution);
838
+ }
839
+ catch (error) {
840
+ console.error('Error executing workflow:', error);
841
+ res.status(500).json({ error: 'Failed to execute workflow' });
842
+ }
843
+ });
844
+ router.get('/api/pipeline/workflows/:id/executions', async (req, res) => {
845
+ if (this.isDemoMode) {
846
+ const demoExecutions = [
847
+ {
848
+ id: 'exec-1',
849
+ workflowId: req.params.id,
850
+ status: 'completed',
851
+ startedAt: new Date(Date.now() - 7200000).toISOString(),
852
+ completedAt: new Date(Date.now() - 6000000).toISOString()
853
+ },
854
+ {
855
+ id: 'exec-2',
856
+ workflowId: req.params.id,
857
+ status: 'running',
858
+ startedAt: new Date(Date.now() - 1800000).toISOString()
859
+ }
860
+ ];
861
+ return res.json({ executions: demoExecutions, total: 2 });
862
+ }
863
+ try {
864
+ const executions = await this.workflowEngine.getWorkflowExecutions(req.params.id, {
865
+ limit: parseInt(req.query.limit) || 50,
866
+ offset: parseInt(req.query.offset) || 0
867
+ });
868
+ res.json(executions);
869
+ }
870
+ catch (error) {
871
+ console.error('Error getting workflow executions:', error);
872
+ res.status(500).json({ error: 'Failed to get workflow executions' });
873
+ }
874
+ });
875
+ router.get('/api/pipeline/executions/:id', async (req, res) => {
876
+ try {
877
+ const execution = await this.workflowEngine.getExecution(req.params.id);
878
+ if (!execution) {
879
+ return res.status(404).json({ error: 'Execution not found' });
880
+ }
881
+ res.json(execution);
882
+ }
883
+ catch (error) {
884
+ console.error('Error getting execution:', error);
885
+ res.status(500).json({ error: 'Failed to get execution' });
886
+ }
887
+ });
888
+ router.post('/api/pipeline/executions/:id/cancel', async (req, res) => {
889
+ try {
890
+ await this.workflowEngine.cancelExecution(req.params.id);
891
+ res.json({ message: 'Execution cancelled successfully' });
892
+ }
893
+ catch (error) {
894
+ console.error('Error cancelling execution:', error);
895
+ res.status(500).json({ error: 'Failed to cancel execution' });
896
+ }
897
+ });
898
+ router.put('/api/pipeline/workflows/:id', async (req, res) => {
899
+ try {
900
+ const workflow = await this.workflowEngine.updateWorkflow(req.params.id, req.body);
901
+ res.json(workflow);
902
+ }
903
+ catch (error) {
904
+ console.error('Error updating workflow:', error);
905
+ res.status(500).json({ error: 'Failed to update workflow' });
906
+ }
907
+ });
908
+ router.delete('/api/pipeline/workflows/:id', async (req, res) => {
909
+ try {
910
+ await this.workflowEngine.deleteWorkflow(req.params.id);
911
+ res.json({ message: 'Workflow deleted successfully' });
912
+ }
913
+ catch (error) {
914
+ console.error('Error deleting workflow:', error);
915
+ res.status(500).json({ error: 'Failed to delete workflow' });
916
+ }
917
+ });
918
+ router.post('/api/pipeline/workflows/:id/validate', async (req, res) => {
919
+ try {
920
+ const validation = await this.workflowEngine.validateWorkflowById(req.params.id);
921
+ res.json(validation);
922
+ }
923
+ catch (error) {
924
+ console.error('Error validating workflow:', error);
925
+ res.status(500).json({ error: 'Failed to validate workflow' });
926
+ }
927
+ });
928
+ router.get('/api/pipeline/workflows/:id/dependencies', async (req, res) => {
929
+ try {
930
+ const dependencies = await this.workflowEngine.getWorkflowDependencies(req.params.id);
931
+ res.json(dependencies);
932
+ }
933
+ catch (error) {
934
+ console.error('Error getting workflow dependencies:', error);
935
+ res.status(500).json({ error: 'Failed to get workflow dependencies' });
936
+ }
937
+ });
938
+ this.app.use('/', router);
939
+ }
940
+ setupWebSocket() {
941
+ this.io.on('connection', (socket) => {
942
+ console.log(`WebSocket client connected: ${socket.id}`);
943
+ socket.on('disconnect', () => {
944
+ console.log(`WebSocket client disconnected: ${socket.id}`);
945
+ });
946
+ socket.on('subscribe:job', (jobId) => {
947
+ socket.join(`job:${jobId}`);
948
+ console.log(`Client ${socket.id} subscribed to job ${jobId}`);
949
+ });
950
+ socket.on('unsubscribe:job', (jobId) => {
951
+ socket.leave(`job:${jobId}`);
952
+ console.log(`Client ${socket.id} unsubscribed from job ${jobId}`);
953
+ });
954
+ socket.on('subscribe:workflow', (workflowId) => {
955
+ socket.join(`workflow:${workflowId}`);
956
+ console.log(`Client ${socket.id} subscribed to workflow ${workflowId}`);
957
+ });
958
+ socket.on('unsubscribe:workflow', (workflowId) => {
959
+ socket.leave(`workflow:${workflowId}`);
960
+ console.log(`Client ${socket.id} unsubscribed from workflow ${workflowId}`);
961
+ });
962
+ });
963
+ }
964
+ setupEventListeners() {
965
+ // Job Tracker events
966
+ this.jobTracker.on('job:created', (event) => {
967
+ this.io.emit('job:created', event);
968
+ this.io.to(`job:${event.jobId}`).emit('job:update', event);
969
+ });
970
+ this.jobTracker.on('job:status_changed', (event) => {
971
+ this.io.emit('job:status_changed', event);
972
+ this.io.to(`job:${event.jobId}`).emit('job:update', event);
973
+ });
974
+ this.jobTracker.on('execution:started', (event) => {
975
+ this.io.to(`job:${event.jobId}`).emit('execution:started', event);
976
+ });
977
+ this.jobTracker.on('execution:completed', (event) => {
978
+ this.io.to(`job:${event.jobId}`).emit('execution:completed', event);
979
+ });
980
+ this.jobTracker.on('execution:failed', (event) => {
981
+ this.io.to(`job:${event.jobId}`).emit('execution:failed', event);
982
+ });
983
+ // MCLI Bridge events
984
+ this.mcliBridge.on('mcli:submitted', (event) => {
985
+ this.io.emit('mcli:submitted', event);
986
+ if (event.pipelineJobId) {
987
+ this.io.to(`job:${event.pipelineJobId}`).emit('mcli:submitted', event);
988
+ }
989
+ });
990
+ this.mcliBridge.on('mcli:webhook', (event) => {
991
+ this.io.emit('mcli:webhook', event);
992
+ if (event.pipelineJobId) {
993
+ this.io.to(`job:${event.pipelineJobId}`).emit('mcli:update', event);
994
+ }
995
+ });
996
+ // Workflow Engine events
997
+ this.workflowEngine.on('workflow:created', (event) => {
998
+ this.io.emit('workflow:created', event);
999
+ });
1000
+ this.workflowEngine.on('execution:started', (event) => {
1001
+ this.io.emit('workflow:execution:started', event);
1002
+ this.io.to(`workflow:${event.workflowId}`).emit('execution:started', event);
1003
+ });
1004
+ this.workflowEngine.on('execution:completed', (event) => {
1005
+ this.io.emit('workflow:execution:completed', event);
1006
+ this.io.to(`workflow:${event.workflowId}`).emit('execution:completed', event);
1007
+ });
1008
+ this.workflowEngine.on('execution:failed', (event) => {
1009
+ this.io.emit('workflow:execution:failed', event);
1010
+ this.io.to(`workflow:${event.workflowId}`).emit('execution:failed', event);
1011
+ });
1012
+ this.workflowEngine.on('node:started', (event) => {
1013
+ this.io.to(`workflow:${event.workflowId}`).emit('node:started', event);
1014
+ });
1015
+ this.workflowEngine.on('node:completed', (event) => {
1016
+ this.io.to(`workflow:${event.workflowId}`).emit('node:completed', event);
1017
+ });
1018
+ this.workflowEngine.on('node:failed', (event) => {
1019
+ this.io.to(`workflow:${event.workflowId}`).emit('node:failed', event);
1020
+ });
1021
+ }
1022
+ getApp() {
1023
+ return this.app;
1024
+ }
1025
+ getServer() {
1026
+ return this.server;
1027
+ }
1028
+ async start() {
1029
+ try {
1030
+ // Test database connection
1031
+ try {
1032
+ await this.pool.query('SELECT 1');
1033
+ console.log('✅ Database connected');
1034
+ this.isDemoMode = false;
1035
+ }
1036
+ catch (_dbError) {
1037
+ console.warn('⚠️ Database not available - running in demo mode');
1038
+ console.log(' To enable full functionality, create a PostgreSQL database named "pipeline"');
1039
+ console.log(' and run: psql -d pipeline -f src/pipeline/schema.sql');
1040
+ this.isDemoMode = true;
1041
+ }
1042
+ // Start job tracker polling
1043
+ this.jobTracker.startPolling();
1044
+ console.log('✅ Job tracker started');
1045
+ // Start MCLI periodic sync
1046
+ this.mcliBridge.startPeriodicSync();
1047
+ console.log('✅ MCLI bridge started');
1048
+ // Start workflow engine
1049
+ await this.workflowEngine.start();
1050
+ console.log('✅ Workflow engine started');
1051
+ // Start server
1052
+ this.server.listen(this.config.port, () => {
1053
+ console.log(`🚀 Pipeline service running on port ${this.config.port}`);
1054
+ console.log(`📊 API available at http://localhost:${this.config.port}/api/pipeline`);
1055
+ console.log(`🔄 WebSocket available at ws://localhost:${this.config.port}`);
1056
+ console.log(`🪝 Webhook endpoint at http://localhost:${this.config.port}/webhook/mcli`);
1057
+ });
1058
+ }
1059
+ catch (error) {
1060
+ console.error('Failed to start pipeline service:', error);
1061
+ throw error;
1062
+ }
1063
+ }
1064
+ async stop() {
1065
+ console.log('Shutting down pipeline service...');
1066
+ // Stop polling
1067
+ this.jobTracker.stopPolling();
1068
+ // Stop Streamlit process
1069
+ if (this.streamlitProcess) {
1070
+ console.log('Stopping Streamlit ML Dashboard...');
1071
+ this.streamlitProcess.kill('SIGTERM');
1072
+ this.streamlitProcess = null;
1073
+ }
1074
+ // Cleanup services
1075
+ await this.jobTracker.cleanup();
1076
+ this.mcliBridge.cleanup();
1077
+ await this.workflowEngine.stop();
1078
+ // Close database pool
1079
+ await this.pool.end();
1080
+ // Close server
1081
+ this.server.close();
1082
+ console.log('Pipeline service stopped');
1083
+ }
1084
+ }
1085
+ // Export for CLI usage
1086
+ export async function startPipelineService(config) {
1087
+ const service = new PipelineService(config);
1088
+ await service.start();
1089
+ return service;
1090
+ }
1091
+ // Handle process termination
1092
+ function isMainModule() {
1093
+ try {
1094
+ const importMeta = eval('import.meta');
1095
+ return importMeta.url === `file://${process.argv[1]}`;
1096
+ }
1097
+ catch {
1098
+ // Fallback: check if this file is being run directly
1099
+ return Boolean(process.argv[1] && process.argv[1].endsWith('pipeline-service.js'));
1100
+ }
1101
+ }
1102
+ if (isMainModule()) {
1103
+ const service = new PipelineService();
1104
+ process.on('SIGINT', async () => {
1105
+ await service.stop();
1106
+ process.exit(0);
1107
+ });
1108
+ process.on('SIGTERM', async () => {
1109
+ await service.stop();
1110
+ process.exit(0);
1111
+ });
1112
+ service.start().catch((error) => {
1113
+ console.error('Failed to start:', error);
1114
+ process.exit(1);
1115
+ });
1116
+ }