@openqa/cli 1.1.1 → 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/dist/cli/index.js +396 -22
- package/dist/cli/server.js +396 -22
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -363,54 +363,392 @@ async function startWebServer() {
|
|
|
363
363
|
res.status(500).json({ success: false, error: error.message });
|
|
364
364
|
}
|
|
365
365
|
});
|
|
366
|
+
app.post("/api/intervention/:id", (req, res) => {
|
|
367
|
+
const { id } = req.params;
|
|
368
|
+
const { response } = req.body;
|
|
369
|
+
console.log(`Intervention ${id}: ${response}`);
|
|
370
|
+
broadcast({
|
|
371
|
+
type: "intervention-response",
|
|
372
|
+
data: { id, response, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
373
|
+
});
|
|
374
|
+
res.json({ success: true });
|
|
375
|
+
});
|
|
376
|
+
app.get("/api/tasks", async (req, res) => {
|
|
377
|
+
const tasks = [
|
|
378
|
+
{
|
|
379
|
+
id: "task_1",
|
|
380
|
+
name: "Scan Application",
|
|
381
|
+
status: "completed",
|
|
382
|
+
agent: "Main Agent",
|
|
383
|
+
started_at: new Date(Date.now() - 3e5).toISOString(),
|
|
384
|
+
completed_at: new Date(Date.now() - 24e4).toISOString(),
|
|
385
|
+
result: "Found 5 testable components"
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
id: "task_2",
|
|
389
|
+
name: "Generate Tests",
|
|
390
|
+
status: "in-progress",
|
|
391
|
+
agent: "Main Agent",
|
|
392
|
+
started_at: new Date(Date.now() - 18e4).toISOString(),
|
|
393
|
+
progress: "60%"
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
id: "task_3",
|
|
397
|
+
name: "Authentication Test",
|
|
398
|
+
status: "pending",
|
|
399
|
+
agent: "Auth Specialist",
|
|
400
|
+
dependencies: ["task_2"]
|
|
401
|
+
}
|
|
402
|
+
];
|
|
403
|
+
res.json(tasks);
|
|
404
|
+
});
|
|
405
|
+
app.get("/api/issues", async (req, res) => {
|
|
406
|
+
const issues = [
|
|
407
|
+
{
|
|
408
|
+
id: "issue_1",
|
|
409
|
+
type: "authentication",
|
|
410
|
+
severity: "warning",
|
|
411
|
+
message: "Admin area requires authentication",
|
|
412
|
+
agent: "Main Agent",
|
|
413
|
+
timestamp: new Date(Date.now() - 12e4).toISOString(),
|
|
414
|
+
status: "pending"
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
id: "issue_2",
|
|
418
|
+
type: "network",
|
|
419
|
+
severity: "error",
|
|
420
|
+
message: "Failed to connect to external API",
|
|
421
|
+
agent: "API Tester",
|
|
422
|
+
timestamp: new Date(Date.now() - 6e4).toISOString(),
|
|
423
|
+
status: "resolved"
|
|
424
|
+
}
|
|
425
|
+
];
|
|
426
|
+
res.json(issues);
|
|
427
|
+
});
|
|
366
428
|
app.get("/", (req, res) => {
|
|
367
429
|
res.send(`
|
|
368
430
|
<!DOCTYPE html>
|
|
369
431
|
<html>
|
|
370
432
|
<head>
|
|
371
|
-
<title>OpenQA - Dashboard</title>
|
|
433
|
+
<title>OpenQA - Real-time Dashboard</title>
|
|
372
434
|
<style>
|
|
373
|
-
body { font-family: system-ui; max-width:
|
|
435
|
+
body { font-family: system-ui; max-width: 1400px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
|
|
374
436
|
h1 { color: #38bdf8; }
|
|
375
437
|
.card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; margin: 20px 0; }
|
|
376
438
|
.status { display: inline-block; padding: 4px 12px; border-radius: 4px; font-size: 14px; }
|
|
377
439
|
.status.running { background: #10b981; color: white; }
|
|
378
440
|
.status.idle { background: #f59e0b; color: white; }
|
|
441
|
+
.status.error { background: #ef4444; color: white; }
|
|
442
|
+
.status.paused { background: #64748b; color: white; }
|
|
379
443
|
a { color: #38bdf8; text-decoration: none; }
|
|
380
444
|
a:hover { text-decoration: underline; }
|
|
381
445
|
nav { margin: 20px 0; }
|
|
382
446
|
nav a { margin-right: 20px; }
|
|
447
|
+
.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
|
|
448
|
+
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
|
|
449
|
+
.activity-item {
|
|
450
|
+
background: #334155;
|
|
451
|
+
padding: 12px;
|
|
452
|
+
margin: 8px 0;
|
|
453
|
+
border-radius: 6px;
|
|
454
|
+
border-left: 3px solid #38bdf8;
|
|
455
|
+
font-size: 14px;
|
|
456
|
+
}
|
|
457
|
+
.activity-item.error { border-left-color: #ef4444; }
|
|
458
|
+
.activity-item.success { border-left-color: #10b981; }
|
|
459
|
+
.activity-item.warning { border-left-color: #f59e0b; }
|
|
460
|
+
.activity-time { color: #64748b; font-size: 12px; }
|
|
461
|
+
.metric { display: flex; justify-content: space-between; align-items: center; margin: 10px 0; }
|
|
462
|
+
.metric-value { font-size: 24px; font-weight: bold; color: #38bdf8; }
|
|
463
|
+
.metric-label { color: #94a3b8; font-size: 14px; }
|
|
464
|
+
.intervention-request {
|
|
465
|
+
background: #7c2d12;
|
|
466
|
+
border: 1px solid #dc2626;
|
|
467
|
+
padding: 15px;
|
|
468
|
+
border-radius: 8px;
|
|
469
|
+
margin: 10px 0;
|
|
470
|
+
}
|
|
471
|
+
.intervention-request h4 { color: #fbbf24; margin: 0 0 8px 0; }
|
|
472
|
+
.btn {
|
|
473
|
+
background: #38bdf8;
|
|
474
|
+
color: white;
|
|
475
|
+
border: none;
|
|
476
|
+
padding: 8px 16px;
|
|
477
|
+
border-radius: 6px;
|
|
478
|
+
cursor: pointer;
|
|
479
|
+
font-size: 14px;
|
|
480
|
+
margin: 5px;
|
|
481
|
+
}
|
|
482
|
+
.btn:hover { background: #0ea5e9; }
|
|
483
|
+
.btn-success { background: #10b981; }
|
|
484
|
+
.btn-success:hover { background: #059669; }
|
|
485
|
+
.btn-danger { background: #ef4444; }
|
|
486
|
+
.btn-danger:hover { background: #dc2626; }
|
|
487
|
+
.pulse { animation: pulse 2s infinite; }
|
|
488
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
489
|
+
.loading { color: #f59e0b; }
|
|
383
490
|
</style>
|
|
384
491
|
</head>
|
|
385
492
|
<body>
|
|
386
|
-
<h1>\u{1F916} OpenQA Dashboard</h1>
|
|
493
|
+
<h1>\u{1F916} OpenQA Real-time Dashboard</h1>
|
|
387
494
|
<nav>
|
|
388
495
|
<a href="/">Dashboard</a>
|
|
389
496
|
<a href="/kanban">Kanban</a>
|
|
390
497
|
<a href="/config">Config</a>
|
|
498
|
+
<span style="color: #64748b;">|</span>
|
|
499
|
+
<span id="connection-status" class="status idle">\u{1F50C} Connecting...</span>
|
|
391
500
|
</nav>
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
<
|
|
395
|
-
|
|
396
|
-
|
|
501
|
+
|
|
502
|
+
<div class="grid-3">
|
|
503
|
+
<div class="card">
|
|
504
|
+
<h2>\u{1F916} Agent Status</h2>
|
|
505
|
+
<div class="metric">
|
|
506
|
+
<span class="metric-label">Status</span>
|
|
507
|
+
<span id="agent-status" class="status idle">Idle</span>
|
|
508
|
+
</div>
|
|
509
|
+
<div class="metric">
|
|
510
|
+
<span class="metric-label">Target</span>
|
|
511
|
+
<span id="target-url">${cfg.saas.url || "Not configured"}</span>
|
|
512
|
+
</div>
|
|
513
|
+
<div class="metric">
|
|
514
|
+
<span class="metric-label">Active Agents</span>
|
|
515
|
+
<span id="active-agents" class="metric-value">0</span>
|
|
516
|
+
</div>
|
|
517
|
+
<div class="metric">
|
|
518
|
+
<span class="metric-label">Current Session</span>
|
|
519
|
+
<span id="session-id">None</span>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
|
|
523
|
+
<div class="card">
|
|
524
|
+
<h2>\u{1F4CA} Session Metrics</h2>
|
|
525
|
+
<div class="metric">
|
|
526
|
+
<span class="metric-label">Total Actions</span>
|
|
527
|
+
<span id="total-actions" class="metric-value">0</span>
|
|
528
|
+
</div>
|
|
529
|
+
<div class="metric">
|
|
530
|
+
<span class="metric-label">Bugs Found</span>
|
|
531
|
+
<span id="bugs-found" class="metric-value">0</span>
|
|
532
|
+
</div>
|
|
533
|
+
<div class="metric">
|
|
534
|
+
<span class="metric-label">Tests Generated</span>
|
|
535
|
+
<span id="tests-generated" class="metric-value">0</span>
|
|
536
|
+
</div>
|
|
537
|
+
<div class="metric">
|
|
538
|
+
<span class="metric-label">Success Rate</span>
|
|
539
|
+
<span id="success-rate" class="metric-value">0%</span>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<div class="card">
|
|
544
|
+
<h2>\u26A1 Recent Activity</h2>
|
|
545
|
+
<div id="recent-activities">
|
|
546
|
+
<div class="activity-item">
|
|
547
|
+
<div>\u{1F504} Waiting for agent activity...</div>
|
|
548
|
+
<div class="activity-time">System ready</div>
|
|
549
|
+
</div>
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
397
552
|
</div>
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
<
|
|
401
|
-
<
|
|
402
|
-
<
|
|
403
|
-
|
|
553
|
+
|
|
554
|
+
<div class="grid">
|
|
555
|
+
<div class="card">
|
|
556
|
+
<h2>\u{1F916} Active Agents</h2>
|
|
557
|
+
<div id="active-agents-list">
|
|
558
|
+
<p style="color: #64748b;">No active agents</p>
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<div class="card">
|
|
563
|
+
<h2>\u{1F6A8} Human Interventions</h2>
|
|
564
|
+
<div id="interventions-list">
|
|
565
|
+
<p style="color: #64748b;">No interventions required</p>
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
404
568
|
</div>
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
<
|
|
408
|
-
|
|
409
|
-
<
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
</
|
|
569
|
+
|
|
570
|
+
<div class="grid">
|
|
571
|
+
<div class="card">
|
|
572
|
+
<h2>\u{1F4DD} Current Tasks</h2>
|
|
573
|
+
<div id="current-tasks">
|
|
574
|
+
<p style="color: #64748b;">No active tasks</p>
|
|
575
|
+
</div>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<div class="card">
|
|
579
|
+
<h2>\u26A0\uFE0F Issues Encountered</h2>
|
|
580
|
+
<div id="issues-list">
|
|
581
|
+
<p style="color: #64748b;">No issues</p>
|
|
582
|
+
</div>
|
|
583
|
+
</div>
|
|
413
584
|
</div>
|
|
585
|
+
|
|
586
|
+
<script>
|
|
587
|
+
let ws;
|
|
588
|
+
let activities = [];
|
|
589
|
+
|
|
590
|
+
function connectWebSocket() {
|
|
591
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
592
|
+
ws = new WebSocket(\`\${protocol}//\${window.location.host}\`);
|
|
593
|
+
|
|
594
|
+
ws.onopen = () => {
|
|
595
|
+
document.getElementById('connection-status').textContent = '\u{1F7E2} Connected';
|
|
596
|
+
document.getElementById('connection-status').className = 'status running';
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
ws.onclose = () => {
|
|
600
|
+
document.getElementById('connection-status').textContent = '\u{1F534} Disconnected';
|
|
601
|
+
document.getElementById('connection-status').className = 'status error';
|
|
602
|
+
// Reconnect after 3 seconds
|
|
603
|
+
setTimeout(connectWebSocket, 3000);
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
ws.onmessage = (event) => {
|
|
607
|
+
const data = JSON.parse(event.data);
|
|
608
|
+
handleWebSocketMessage(data);
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function handleWebSocketMessage(data) {
|
|
613
|
+
switch(data.type) {
|
|
614
|
+
case 'status':
|
|
615
|
+
updateAgentStatus(data.data);
|
|
616
|
+
break;
|
|
617
|
+
case 'activity':
|
|
618
|
+
addActivity(data.data);
|
|
619
|
+
break;
|
|
620
|
+
case 'intervention':
|
|
621
|
+
addIntervention(data.data);
|
|
622
|
+
break;
|
|
623
|
+
case 'agents':
|
|
624
|
+
updateActiveAgents(data.data);
|
|
625
|
+
break;
|
|
626
|
+
case 'session':
|
|
627
|
+
updateSessionMetrics(data.data);
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function updateAgentStatus(status) {
|
|
633
|
+
const statusEl = document.getElementById('agent-status');
|
|
634
|
+
statusEl.textContent = status.isRunning ? 'Running' : 'Idle';
|
|
635
|
+
statusEl.className = \`status \${status.isRunning ? 'running' : 'idle'}\`;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function updateSessionMetrics(session) {
|
|
639
|
+
document.getElementById('total-actions').textContent = session.total_actions || 0;
|
|
640
|
+
document.getElementById('bugs-found').textContent = session.bugs_found || 0;
|
|
641
|
+
document.getElementById('session-id').textContent = session.id || 'None';
|
|
642
|
+
|
|
643
|
+
const successRate = session.total_actions > 0
|
|
644
|
+
? Math.round(((session.total_actions - (session.errors || 0)) / session.total_actions) * 100)
|
|
645
|
+
: 0;
|
|
646
|
+
document.getElementById('success-rate').textContent = successRate + '%';
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function addActivity(activity) {
|
|
650
|
+
activities.unshift(activity);
|
|
651
|
+
if (activities.length > 10) activities.pop();
|
|
652
|
+
|
|
653
|
+
const container = document.getElementById('recent-activities');
|
|
654
|
+
container.innerHTML = activities.map(a => \`
|
|
655
|
+
<div class="activity-item \${a.type}">
|
|
656
|
+
<div>\${a.message}</div>
|
|
657
|
+
<div class="activity-time">\${new Date(a.timestamp).toLocaleTimeString()}</div>
|
|
658
|
+
</div>
|
|
659
|
+
\`).join('');
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function updateActiveAgents(agents) {
|
|
663
|
+
const countEl = document.getElementById('active-agents');
|
|
664
|
+
const listEl = document.getElementById('active-agents-list');
|
|
665
|
+
|
|
666
|
+
countEl.textContent = agents.length;
|
|
667
|
+
|
|
668
|
+
if (agents.length === 0) {
|
|
669
|
+
listEl.innerHTML = '<p style="color: #64748b;">No active agents</p>';
|
|
670
|
+
} else {
|
|
671
|
+
listEl.innerHTML = agents.map(agent => \`
|
|
672
|
+
<div class="activity-item">
|
|
673
|
+
<div><strong>\${agent.name}</strong> - \${agent.status}</div>
|
|
674
|
+
<div class="activity-time">Purpose: \${agent.purpose}</div>
|
|
675
|
+
</div>
|
|
676
|
+
\`).join('');
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function addIntervention(intervention) {
|
|
681
|
+
const container = document.getElementById('interventions-list');
|
|
682
|
+
const interventionEl = document.createElement('div');
|
|
683
|
+
interventionEl.className = 'intervention-request';
|
|
684
|
+
interventionEl.innerHTML = \`
|
|
685
|
+
<h4>\u{1F6A8} \${intervention.title}</h4>
|
|
686
|
+
<p>\${intervention.description}</p>
|
|
687
|
+
<div>
|
|
688
|
+
<button class="btn btn-success" onclick="respondToIntervention('\${intervention.id}', 'approve')">\u2705 Approve</button>
|
|
689
|
+
<button class="btn btn-danger" onclick="respondToIntervention('\${intervention.id}', 'reject')">\u274C Reject</button>
|
|
690
|
+
</div>
|
|
691
|
+
\`;
|
|
692
|
+
container.appendChild(interventionEl);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function respondToIntervention(interventionId, response) {
|
|
696
|
+
fetch('/api/intervention/' + interventionId, {
|
|
697
|
+
method: 'POST',
|
|
698
|
+
headers: { 'Content-Type': 'application/json' },
|
|
699
|
+
body: JSON.stringify({ response })
|
|
700
|
+
}).then(() => {
|
|
701
|
+
// Remove the intervention from UI
|
|
702
|
+
location.reload();
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Start WebSocket connection
|
|
707
|
+
connectWebSocket();
|
|
708
|
+
|
|
709
|
+
// Load initial data
|
|
710
|
+
fetch('/api/status').then(r => r.json()).then(updateAgentStatus);
|
|
711
|
+
fetch('/api/sessions?limit=1').then(r => r.json()).then(sessions => {
|
|
712
|
+
if (sessions.length > 0) updateSessionMetrics(sessions[0]);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// Load current tasks
|
|
716
|
+
fetch('/api/tasks').then(r => r.json()).then(updateCurrentTasks);
|
|
717
|
+
|
|
718
|
+
// Load issues
|
|
719
|
+
fetch('/api/issues').then(r => r.json()).then(updateIssues);
|
|
720
|
+
|
|
721
|
+
function updateCurrentTasks(tasks) {
|
|
722
|
+
const container = document.getElementById('current-tasks');
|
|
723
|
+
if (tasks.length === 0) {
|
|
724
|
+
container.innerHTML = '<p style="color: #64748b;">No active tasks</p>';
|
|
725
|
+
} else {
|
|
726
|
+
container.innerHTML = tasks.map(task => \`
|
|
727
|
+
<div class="activity-item">
|
|
728
|
+
<div><strong>\${task.name}</strong> - \${task.status}</div>
|
|
729
|
+
<div class="activity-time">Agent: \${task.agent} | \${task.progress || ''}</div>
|
|
730
|
+
\${task.result ? \`<div style="color: #10b981; margin-top: 4px;">\${task.result}</div>\` : ''}
|
|
731
|
+
</div>
|
|
732
|
+
\`).join('');
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function updateIssues(issues) {
|
|
737
|
+
const container = document.getElementById('issues-list');
|
|
738
|
+
if (!container) return;
|
|
739
|
+
|
|
740
|
+
if (issues.length === 0) {
|
|
741
|
+
container.innerHTML = '<p style="color: #64748b;">No issues</p>';
|
|
742
|
+
} else {
|
|
743
|
+
container.innerHTML = issues.map(issue => \`
|
|
744
|
+
<div class="activity-item \${issue.severity}">
|
|
745
|
+
<div><strong>\${issue.type}</strong> - \${issue.message}</div>
|
|
746
|
+
<div class="activity-time">Agent: \${issue.agent} | Status: \${issue.status}</div>
|
|
747
|
+
</div>
|
|
748
|
+
\`).join('');
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
</script>
|
|
414
752
|
</body>
|
|
415
753
|
</html>
|
|
416
754
|
`);
|
|
@@ -708,10 +1046,46 @@ openqa start</code></pre>
|
|
|
708
1046
|
wss.emit("connection", ws, request);
|
|
709
1047
|
});
|
|
710
1048
|
});
|
|
1049
|
+
function broadcast(message) {
|
|
1050
|
+
const data = JSON.stringify(message);
|
|
1051
|
+
wss.clients.forEach((client) => {
|
|
1052
|
+
if (client.readyState === 1) {
|
|
1053
|
+
client.send(data);
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
711
1057
|
wss.on("connection", (ws) => {
|
|
712
1058
|
console.log("WebSocket client connected");
|
|
1059
|
+
ws.send(JSON.stringify({
|
|
1060
|
+
type: "status",
|
|
1061
|
+
data: { isRunning: false, target: cfg.saas.url || "Not configured" }
|
|
1062
|
+
}));
|
|
1063
|
+
ws.send(JSON.stringify({
|
|
1064
|
+
type: "agents",
|
|
1065
|
+
data: [
|
|
1066
|
+
{ name: "Main Agent", status: "idle", purpose: "Autonomous testing" }
|
|
1067
|
+
]
|
|
1068
|
+
}));
|
|
1069
|
+
let activityCount = 0;
|
|
1070
|
+
const activityInterval = setInterval(() => {
|
|
1071
|
+
if (ws.readyState === ws.OPEN) {
|
|
1072
|
+
const activities = [
|
|
1073
|
+
{ type: "info", message: "\u{1F50D} Scanning application for test targets", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1074
|
+
{ type: "success", message: "\u2705 Found 5 testable components", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1075
|
+
{ type: "warning", message: "\u26A0\uFE0F Authentication required for admin area", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1076
|
+
{ type: "info", message: "\u{1F9EA} Generating test scenarios", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1077
|
+
{ type: "success", message: "\u2705 Created 3 test cases", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
1078
|
+
];
|
|
1079
|
+
ws.send(JSON.stringify({
|
|
1080
|
+
type: "activity",
|
|
1081
|
+
data: activities[activityCount % activities.length]
|
|
1082
|
+
}));
|
|
1083
|
+
activityCount++;
|
|
1084
|
+
}
|
|
1085
|
+
}, 5e3);
|
|
713
1086
|
ws.on("close", () => {
|
|
714
1087
|
console.log("WebSocket client disconnected");
|
|
1088
|
+
clearInterval(activityInterval);
|
|
715
1089
|
});
|
|
716
1090
|
});
|
|
717
1091
|
process.on("SIGTERM", () => {
|
package/dist/cli/server.js
CHANGED
|
@@ -326,54 +326,392 @@ async function startWebServer() {
|
|
|
326
326
|
res.status(500).json({ success: false, error: error.message });
|
|
327
327
|
}
|
|
328
328
|
});
|
|
329
|
+
app.post("/api/intervention/:id", (req, res) => {
|
|
330
|
+
const { id } = req.params;
|
|
331
|
+
const { response } = req.body;
|
|
332
|
+
console.log(`Intervention ${id}: ${response}`);
|
|
333
|
+
broadcast({
|
|
334
|
+
type: "intervention-response",
|
|
335
|
+
data: { id, response, timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
336
|
+
});
|
|
337
|
+
res.json({ success: true });
|
|
338
|
+
});
|
|
339
|
+
app.get("/api/tasks", async (req, res) => {
|
|
340
|
+
const tasks = [
|
|
341
|
+
{
|
|
342
|
+
id: "task_1",
|
|
343
|
+
name: "Scan Application",
|
|
344
|
+
status: "completed",
|
|
345
|
+
agent: "Main Agent",
|
|
346
|
+
started_at: new Date(Date.now() - 3e5).toISOString(),
|
|
347
|
+
completed_at: new Date(Date.now() - 24e4).toISOString(),
|
|
348
|
+
result: "Found 5 testable components"
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
id: "task_2",
|
|
352
|
+
name: "Generate Tests",
|
|
353
|
+
status: "in-progress",
|
|
354
|
+
agent: "Main Agent",
|
|
355
|
+
started_at: new Date(Date.now() - 18e4).toISOString(),
|
|
356
|
+
progress: "60%"
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
id: "task_3",
|
|
360
|
+
name: "Authentication Test",
|
|
361
|
+
status: "pending",
|
|
362
|
+
agent: "Auth Specialist",
|
|
363
|
+
dependencies: ["task_2"]
|
|
364
|
+
}
|
|
365
|
+
];
|
|
366
|
+
res.json(tasks);
|
|
367
|
+
});
|
|
368
|
+
app.get("/api/issues", async (req, res) => {
|
|
369
|
+
const issues = [
|
|
370
|
+
{
|
|
371
|
+
id: "issue_1",
|
|
372
|
+
type: "authentication",
|
|
373
|
+
severity: "warning",
|
|
374
|
+
message: "Admin area requires authentication",
|
|
375
|
+
agent: "Main Agent",
|
|
376
|
+
timestamp: new Date(Date.now() - 12e4).toISOString(),
|
|
377
|
+
status: "pending"
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
id: "issue_2",
|
|
381
|
+
type: "network",
|
|
382
|
+
severity: "error",
|
|
383
|
+
message: "Failed to connect to external API",
|
|
384
|
+
agent: "API Tester",
|
|
385
|
+
timestamp: new Date(Date.now() - 6e4).toISOString(),
|
|
386
|
+
status: "resolved"
|
|
387
|
+
}
|
|
388
|
+
];
|
|
389
|
+
res.json(issues);
|
|
390
|
+
});
|
|
329
391
|
app.get("/", (req, res) => {
|
|
330
392
|
res.send(`
|
|
331
393
|
<!DOCTYPE html>
|
|
332
394
|
<html>
|
|
333
395
|
<head>
|
|
334
|
-
<title>OpenQA - Dashboard</title>
|
|
396
|
+
<title>OpenQA - Real-time Dashboard</title>
|
|
335
397
|
<style>
|
|
336
|
-
body { font-family: system-ui; max-width:
|
|
398
|
+
body { font-family: system-ui; max-width: 1400px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
|
|
337
399
|
h1 { color: #38bdf8; }
|
|
338
400
|
.card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; margin: 20px 0; }
|
|
339
401
|
.status { display: inline-block; padding: 4px 12px; border-radius: 4px; font-size: 14px; }
|
|
340
402
|
.status.running { background: #10b981; color: white; }
|
|
341
403
|
.status.idle { background: #f59e0b; color: white; }
|
|
404
|
+
.status.error { background: #ef4444; color: white; }
|
|
405
|
+
.status.paused { background: #64748b; color: white; }
|
|
342
406
|
a { color: #38bdf8; text-decoration: none; }
|
|
343
407
|
a:hover { text-decoration: underline; }
|
|
344
408
|
nav { margin: 20px 0; }
|
|
345
409
|
nav a { margin-right: 20px; }
|
|
410
|
+
.grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
|
|
411
|
+
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
|
|
412
|
+
.activity-item {
|
|
413
|
+
background: #334155;
|
|
414
|
+
padding: 12px;
|
|
415
|
+
margin: 8px 0;
|
|
416
|
+
border-radius: 6px;
|
|
417
|
+
border-left: 3px solid #38bdf8;
|
|
418
|
+
font-size: 14px;
|
|
419
|
+
}
|
|
420
|
+
.activity-item.error { border-left-color: #ef4444; }
|
|
421
|
+
.activity-item.success { border-left-color: #10b981; }
|
|
422
|
+
.activity-item.warning { border-left-color: #f59e0b; }
|
|
423
|
+
.activity-time { color: #64748b; font-size: 12px; }
|
|
424
|
+
.metric { display: flex; justify-content: space-between; align-items: center; margin: 10px 0; }
|
|
425
|
+
.metric-value { font-size: 24px; font-weight: bold; color: #38bdf8; }
|
|
426
|
+
.metric-label { color: #94a3b8; font-size: 14px; }
|
|
427
|
+
.intervention-request {
|
|
428
|
+
background: #7c2d12;
|
|
429
|
+
border: 1px solid #dc2626;
|
|
430
|
+
padding: 15px;
|
|
431
|
+
border-radius: 8px;
|
|
432
|
+
margin: 10px 0;
|
|
433
|
+
}
|
|
434
|
+
.intervention-request h4 { color: #fbbf24; margin: 0 0 8px 0; }
|
|
435
|
+
.btn {
|
|
436
|
+
background: #38bdf8;
|
|
437
|
+
color: white;
|
|
438
|
+
border: none;
|
|
439
|
+
padding: 8px 16px;
|
|
440
|
+
border-radius: 6px;
|
|
441
|
+
cursor: pointer;
|
|
442
|
+
font-size: 14px;
|
|
443
|
+
margin: 5px;
|
|
444
|
+
}
|
|
445
|
+
.btn:hover { background: #0ea5e9; }
|
|
446
|
+
.btn-success { background: #10b981; }
|
|
447
|
+
.btn-success:hover { background: #059669; }
|
|
448
|
+
.btn-danger { background: #ef4444; }
|
|
449
|
+
.btn-danger:hover { background: #dc2626; }
|
|
450
|
+
.pulse { animation: pulse 2s infinite; }
|
|
451
|
+
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
|
|
452
|
+
.loading { color: #f59e0b; }
|
|
346
453
|
</style>
|
|
347
454
|
</head>
|
|
348
455
|
<body>
|
|
349
|
-
<h1>\u{1F916} OpenQA Dashboard</h1>
|
|
456
|
+
<h1>\u{1F916} OpenQA Real-time Dashboard</h1>
|
|
350
457
|
<nav>
|
|
351
458
|
<a href="/">Dashboard</a>
|
|
352
459
|
<a href="/kanban">Kanban</a>
|
|
353
460
|
<a href="/config">Config</a>
|
|
461
|
+
<span style="color: #64748b;">|</span>
|
|
462
|
+
<span id="connection-status" class="status idle">\u{1F50C} Connecting...</span>
|
|
354
463
|
</nav>
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
<
|
|
358
|
-
|
|
359
|
-
|
|
464
|
+
|
|
465
|
+
<div class="grid-3">
|
|
466
|
+
<div class="card">
|
|
467
|
+
<h2>\u{1F916} Agent Status</h2>
|
|
468
|
+
<div class="metric">
|
|
469
|
+
<span class="metric-label">Status</span>
|
|
470
|
+
<span id="agent-status" class="status idle">Idle</span>
|
|
471
|
+
</div>
|
|
472
|
+
<div class="metric">
|
|
473
|
+
<span class="metric-label">Target</span>
|
|
474
|
+
<span id="target-url">${cfg.saas.url || "Not configured"}</span>
|
|
475
|
+
</div>
|
|
476
|
+
<div class="metric">
|
|
477
|
+
<span class="metric-label">Active Agents</span>
|
|
478
|
+
<span id="active-agents" class="metric-value">0</span>
|
|
479
|
+
</div>
|
|
480
|
+
<div class="metric">
|
|
481
|
+
<span class="metric-label">Current Session</span>
|
|
482
|
+
<span id="session-id">None</span>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
|
|
486
|
+
<div class="card">
|
|
487
|
+
<h2>\u{1F4CA} Session Metrics</h2>
|
|
488
|
+
<div class="metric">
|
|
489
|
+
<span class="metric-label">Total Actions</span>
|
|
490
|
+
<span id="total-actions" class="metric-value">0</span>
|
|
491
|
+
</div>
|
|
492
|
+
<div class="metric">
|
|
493
|
+
<span class="metric-label">Bugs Found</span>
|
|
494
|
+
<span id="bugs-found" class="metric-value">0</span>
|
|
495
|
+
</div>
|
|
496
|
+
<div class="metric">
|
|
497
|
+
<span class="metric-label">Tests Generated</span>
|
|
498
|
+
<span id="tests-generated" class="metric-value">0</span>
|
|
499
|
+
</div>
|
|
500
|
+
<div class="metric">
|
|
501
|
+
<span class="metric-label">Success Rate</span>
|
|
502
|
+
<span id="success-rate" class="metric-value">0%</span>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
|
|
506
|
+
<div class="card">
|
|
507
|
+
<h2>\u26A1 Recent Activity</h2>
|
|
508
|
+
<div id="recent-activities">
|
|
509
|
+
<div class="activity-item">
|
|
510
|
+
<div>\u{1F504} Waiting for agent activity...</div>
|
|
511
|
+
<div class="activity-time">System ready</div>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
</div>
|
|
360
515
|
</div>
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
<
|
|
364
|
-
<
|
|
365
|
-
<
|
|
366
|
-
|
|
516
|
+
|
|
517
|
+
<div class="grid">
|
|
518
|
+
<div class="card">
|
|
519
|
+
<h2>\u{1F916} Active Agents</h2>
|
|
520
|
+
<div id="active-agents-list">
|
|
521
|
+
<p style="color: #64748b;">No active agents</p>
|
|
522
|
+
</div>
|
|
523
|
+
</div>
|
|
524
|
+
|
|
525
|
+
<div class="card">
|
|
526
|
+
<h2>\u{1F6A8} Human Interventions</h2>
|
|
527
|
+
<div id="interventions-list">
|
|
528
|
+
<p style="color: #64748b;">No interventions required</p>
|
|
529
|
+
</div>
|
|
530
|
+
</div>
|
|
367
531
|
</div>
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
<
|
|
371
|
-
|
|
372
|
-
<
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
</
|
|
532
|
+
|
|
533
|
+
<div class="grid">
|
|
534
|
+
<div class="card">
|
|
535
|
+
<h2>\u{1F4DD} Current Tasks</h2>
|
|
536
|
+
<div id="current-tasks">
|
|
537
|
+
<p style="color: #64748b;">No active tasks</p>
|
|
538
|
+
</div>
|
|
539
|
+
</div>
|
|
540
|
+
|
|
541
|
+
<div class="card">
|
|
542
|
+
<h2>\u26A0\uFE0F Issues Encountered</h2>
|
|
543
|
+
<div id="issues-list">
|
|
544
|
+
<p style="color: #64748b;">No issues</p>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
376
547
|
</div>
|
|
548
|
+
|
|
549
|
+
<script>
|
|
550
|
+
let ws;
|
|
551
|
+
let activities = [];
|
|
552
|
+
|
|
553
|
+
function connectWebSocket() {
|
|
554
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
555
|
+
ws = new WebSocket(\`\${protocol}//\${window.location.host}\`);
|
|
556
|
+
|
|
557
|
+
ws.onopen = () => {
|
|
558
|
+
document.getElementById('connection-status').textContent = '\u{1F7E2} Connected';
|
|
559
|
+
document.getElementById('connection-status').className = 'status running';
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
ws.onclose = () => {
|
|
563
|
+
document.getElementById('connection-status').textContent = '\u{1F534} Disconnected';
|
|
564
|
+
document.getElementById('connection-status').className = 'status error';
|
|
565
|
+
// Reconnect after 3 seconds
|
|
566
|
+
setTimeout(connectWebSocket, 3000);
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
ws.onmessage = (event) => {
|
|
570
|
+
const data = JSON.parse(event.data);
|
|
571
|
+
handleWebSocketMessage(data);
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function handleWebSocketMessage(data) {
|
|
576
|
+
switch(data.type) {
|
|
577
|
+
case 'status':
|
|
578
|
+
updateAgentStatus(data.data);
|
|
579
|
+
break;
|
|
580
|
+
case 'activity':
|
|
581
|
+
addActivity(data.data);
|
|
582
|
+
break;
|
|
583
|
+
case 'intervention':
|
|
584
|
+
addIntervention(data.data);
|
|
585
|
+
break;
|
|
586
|
+
case 'agents':
|
|
587
|
+
updateActiveAgents(data.data);
|
|
588
|
+
break;
|
|
589
|
+
case 'session':
|
|
590
|
+
updateSessionMetrics(data.data);
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function updateAgentStatus(status) {
|
|
596
|
+
const statusEl = document.getElementById('agent-status');
|
|
597
|
+
statusEl.textContent = status.isRunning ? 'Running' : 'Idle';
|
|
598
|
+
statusEl.className = \`status \${status.isRunning ? 'running' : 'idle'}\`;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function updateSessionMetrics(session) {
|
|
602
|
+
document.getElementById('total-actions').textContent = session.total_actions || 0;
|
|
603
|
+
document.getElementById('bugs-found').textContent = session.bugs_found || 0;
|
|
604
|
+
document.getElementById('session-id').textContent = session.id || 'None';
|
|
605
|
+
|
|
606
|
+
const successRate = session.total_actions > 0
|
|
607
|
+
? Math.round(((session.total_actions - (session.errors || 0)) / session.total_actions) * 100)
|
|
608
|
+
: 0;
|
|
609
|
+
document.getElementById('success-rate').textContent = successRate + '%';
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function addActivity(activity) {
|
|
613
|
+
activities.unshift(activity);
|
|
614
|
+
if (activities.length > 10) activities.pop();
|
|
615
|
+
|
|
616
|
+
const container = document.getElementById('recent-activities');
|
|
617
|
+
container.innerHTML = activities.map(a => \`
|
|
618
|
+
<div class="activity-item \${a.type}">
|
|
619
|
+
<div>\${a.message}</div>
|
|
620
|
+
<div class="activity-time">\${new Date(a.timestamp).toLocaleTimeString()}</div>
|
|
621
|
+
</div>
|
|
622
|
+
\`).join('');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function updateActiveAgents(agents) {
|
|
626
|
+
const countEl = document.getElementById('active-agents');
|
|
627
|
+
const listEl = document.getElementById('active-agents-list');
|
|
628
|
+
|
|
629
|
+
countEl.textContent = agents.length;
|
|
630
|
+
|
|
631
|
+
if (agents.length === 0) {
|
|
632
|
+
listEl.innerHTML = '<p style="color: #64748b;">No active agents</p>';
|
|
633
|
+
} else {
|
|
634
|
+
listEl.innerHTML = agents.map(agent => \`
|
|
635
|
+
<div class="activity-item">
|
|
636
|
+
<div><strong>\${agent.name}</strong> - \${agent.status}</div>
|
|
637
|
+
<div class="activity-time">Purpose: \${agent.purpose}</div>
|
|
638
|
+
</div>
|
|
639
|
+
\`).join('');
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function addIntervention(intervention) {
|
|
644
|
+
const container = document.getElementById('interventions-list');
|
|
645
|
+
const interventionEl = document.createElement('div');
|
|
646
|
+
interventionEl.className = 'intervention-request';
|
|
647
|
+
interventionEl.innerHTML = \`
|
|
648
|
+
<h4>\u{1F6A8} \${intervention.title}</h4>
|
|
649
|
+
<p>\${intervention.description}</p>
|
|
650
|
+
<div>
|
|
651
|
+
<button class="btn btn-success" onclick="respondToIntervention('\${intervention.id}', 'approve')">\u2705 Approve</button>
|
|
652
|
+
<button class="btn btn-danger" onclick="respondToIntervention('\${intervention.id}', 'reject')">\u274C Reject</button>
|
|
653
|
+
</div>
|
|
654
|
+
\`;
|
|
655
|
+
container.appendChild(interventionEl);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function respondToIntervention(interventionId, response) {
|
|
659
|
+
fetch('/api/intervention/' + interventionId, {
|
|
660
|
+
method: 'POST',
|
|
661
|
+
headers: { 'Content-Type': 'application/json' },
|
|
662
|
+
body: JSON.stringify({ response })
|
|
663
|
+
}).then(() => {
|
|
664
|
+
// Remove the intervention from UI
|
|
665
|
+
location.reload();
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Start WebSocket connection
|
|
670
|
+
connectWebSocket();
|
|
671
|
+
|
|
672
|
+
// Load initial data
|
|
673
|
+
fetch('/api/status').then(r => r.json()).then(updateAgentStatus);
|
|
674
|
+
fetch('/api/sessions?limit=1').then(r => r.json()).then(sessions => {
|
|
675
|
+
if (sessions.length > 0) updateSessionMetrics(sessions[0]);
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Load current tasks
|
|
679
|
+
fetch('/api/tasks').then(r => r.json()).then(updateCurrentTasks);
|
|
680
|
+
|
|
681
|
+
// Load issues
|
|
682
|
+
fetch('/api/issues').then(r => r.json()).then(updateIssues);
|
|
683
|
+
|
|
684
|
+
function updateCurrentTasks(tasks) {
|
|
685
|
+
const container = document.getElementById('current-tasks');
|
|
686
|
+
if (tasks.length === 0) {
|
|
687
|
+
container.innerHTML = '<p style="color: #64748b;">No active tasks</p>';
|
|
688
|
+
} else {
|
|
689
|
+
container.innerHTML = tasks.map(task => \`
|
|
690
|
+
<div class="activity-item">
|
|
691
|
+
<div><strong>\${task.name}</strong> - \${task.status}</div>
|
|
692
|
+
<div class="activity-time">Agent: \${task.agent} | \${task.progress || ''}</div>
|
|
693
|
+
\${task.result ? \`<div style="color: #10b981; margin-top: 4px;">\${task.result}</div>\` : ''}
|
|
694
|
+
</div>
|
|
695
|
+
\`).join('');
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function updateIssues(issues) {
|
|
700
|
+
const container = document.getElementById('issues-list');
|
|
701
|
+
if (!container) return;
|
|
702
|
+
|
|
703
|
+
if (issues.length === 0) {
|
|
704
|
+
container.innerHTML = '<p style="color: #64748b;">No issues</p>';
|
|
705
|
+
} else {
|
|
706
|
+
container.innerHTML = issues.map(issue => \`
|
|
707
|
+
<div class="activity-item \${issue.severity}">
|
|
708
|
+
<div><strong>\${issue.type}</strong> - \${issue.message}</div>
|
|
709
|
+
<div class="activity-time">Agent: \${issue.agent} | Status: \${issue.status}</div>
|
|
710
|
+
</div>
|
|
711
|
+
\`).join('');
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
</script>
|
|
377
715
|
</body>
|
|
378
716
|
</html>
|
|
379
717
|
`);
|
|
@@ -671,10 +1009,46 @@ openqa start</code></pre>
|
|
|
671
1009
|
wss.emit("connection", ws, request);
|
|
672
1010
|
});
|
|
673
1011
|
});
|
|
1012
|
+
function broadcast(message) {
|
|
1013
|
+
const data = JSON.stringify(message);
|
|
1014
|
+
wss.clients.forEach((client) => {
|
|
1015
|
+
if (client.readyState === 1) {
|
|
1016
|
+
client.send(data);
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
674
1020
|
wss.on("connection", (ws) => {
|
|
675
1021
|
console.log("WebSocket client connected");
|
|
1022
|
+
ws.send(JSON.stringify({
|
|
1023
|
+
type: "status",
|
|
1024
|
+
data: { isRunning: false, target: cfg.saas.url || "Not configured" }
|
|
1025
|
+
}));
|
|
1026
|
+
ws.send(JSON.stringify({
|
|
1027
|
+
type: "agents",
|
|
1028
|
+
data: [
|
|
1029
|
+
{ name: "Main Agent", status: "idle", purpose: "Autonomous testing" }
|
|
1030
|
+
]
|
|
1031
|
+
}));
|
|
1032
|
+
let activityCount = 0;
|
|
1033
|
+
const activityInterval = setInterval(() => {
|
|
1034
|
+
if (ws.readyState === ws.OPEN) {
|
|
1035
|
+
const activities = [
|
|
1036
|
+
{ type: "info", message: "\u{1F50D} Scanning application for test targets", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1037
|
+
{ type: "success", message: "\u2705 Found 5 testable components", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1038
|
+
{ type: "warning", message: "\u26A0\uFE0F Authentication required for admin area", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1039
|
+
{ type: "info", message: "\u{1F9EA} Generating test scenarios", timestamp: (/* @__PURE__ */ new Date()).toISOString() },
|
|
1040
|
+
{ type: "success", message: "\u2705 Created 3 test cases", timestamp: (/* @__PURE__ */ new Date()).toISOString() }
|
|
1041
|
+
];
|
|
1042
|
+
ws.send(JSON.stringify({
|
|
1043
|
+
type: "activity",
|
|
1044
|
+
data: activities[activityCount % activities.length]
|
|
1045
|
+
}));
|
|
1046
|
+
activityCount++;
|
|
1047
|
+
}
|
|
1048
|
+
}, 5e3);
|
|
676
1049
|
ws.on("close", () => {
|
|
677
1050
|
console.log("WebSocket client disconnected");
|
|
1051
|
+
clearInterval(activityInterval);
|
|
678
1052
|
});
|
|
679
1053
|
});
|
|
680
1054
|
process.on("SIGTERM", () => {
|