@openqa/cli 1.1.0 → 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/agent/index.js +5 -0
- package/dist/agent/index.js.map +1 -1
- package/dist/cli/index.js +597 -58
- package/dist/cli/server.js +591 -55
- package/dist/database/index.js +5 -0
- package/dist/database/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -188,6 +188,11 @@ var init_database = __esm({
|
|
|
188
188
|
await this.ensureInitialized();
|
|
189
189
|
return this.db.data.kanban_tickets.filter((t) => t.column === column).sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
190
190
|
}
|
|
191
|
+
async clearAllConfig() {
|
|
192
|
+
await this.ensureInitialized();
|
|
193
|
+
this.db.data.config = {};
|
|
194
|
+
await this.db.write();
|
|
195
|
+
}
|
|
191
196
|
async close() {
|
|
192
197
|
}
|
|
193
198
|
};
|
|
@@ -340,58 +345,410 @@ async function startWebServer() {
|
|
|
340
345
|
res.json(cfg);
|
|
341
346
|
});
|
|
342
347
|
app.post("/api/config", async (req, res) => {
|
|
343
|
-
|
|
344
|
-
|
|
348
|
+
try {
|
|
349
|
+
const configData = req.body;
|
|
350
|
+
for (const [key, value] of Object.entries(configData)) {
|
|
351
|
+
await config.set(key, String(value));
|
|
352
|
+
}
|
|
353
|
+
res.json({ success: true });
|
|
354
|
+
} catch (error) {
|
|
355
|
+
res.status(500).json({ success: false, error: error.message });
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
app.post("/api/config/reset", async (req, res) => {
|
|
359
|
+
try {
|
|
360
|
+
await db.clearAllConfig();
|
|
361
|
+
res.json({ success: true });
|
|
362
|
+
} catch (error) {
|
|
363
|
+
res.status(500).json({ success: false, error: error.message });
|
|
364
|
+
}
|
|
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
|
+
});
|
|
345
374
|
res.json({ success: true });
|
|
346
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
|
+
});
|
|
347
428
|
app.get("/", (req, res) => {
|
|
348
429
|
res.send(`
|
|
349
430
|
<!DOCTYPE html>
|
|
350
431
|
<html>
|
|
351
432
|
<head>
|
|
352
|
-
<title>OpenQA - Dashboard</title>
|
|
433
|
+
<title>OpenQA - Real-time Dashboard</title>
|
|
353
434
|
<style>
|
|
354
|
-
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; }
|
|
355
436
|
h1 { color: #38bdf8; }
|
|
356
437
|
.card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; margin: 20px 0; }
|
|
357
438
|
.status { display: inline-block; padding: 4px 12px; border-radius: 4px; font-size: 14px; }
|
|
358
439
|
.status.running { background: #10b981; color: white; }
|
|
359
440
|
.status.idle { background: #f59e0b; color: white; }
|
|
441
|
+
.status.error { background: #ef4444; color: white; }
|
|
442
|
+
.status.paused { background: #64748b; color: white; }
|
|
360
443
|
a { color: #38bdf8; text-decoration: none; }
|
|
361
444
|
a:hover { text-decoration: underline; }
|
|
362
445
|
nav { margin: 20px 0; }
|
|
363
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; }
|
|
364
490
|
</style>
|
|
365
491
|
</head>
|
|
366
492
|
<body>
|
|
367
|
-
<h1>\u{1F916} OpenQA Dashboard</h1>
|
|
493
|
+
<h1>\u{1F916} OpenQA Real-time Dashboard</h1>
|
|
368
494
|
<nav>
|
|
369
495
|
<a href="/">Dashboard</a>
|
|
370
496
|
<a href="/kanban">Kanban</a>
|
|
371
497
|
<a href="/config">Config</a>
|
|
498
|
+
<span style="color: #64748b;">|</span>
|
|
499
|
+
<span id="connection-status" class="status idle">\u{1F50C} Connecting...</span>
|
|
372
500
|
</nav>
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
<
|
|
376
|
-
|
|
377
|
-
|
|
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>
|
|
378
552
|
</div>
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
<
|
|
382
|
-
<
|
|
383
|
-
<
|
|
384
|
-
|
|
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>
|
|
385
568
|
</div>
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
<
|
|
389
|
-
|
|
390
|
-
<
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
</
|
|
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>
|
|
394
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>
|
|
395
752
|
</body>
|
|
396
753
|
</html>
|
|
397
754
|
`);
|
|
@@ -462,8 +819,39 @@ async function startWebServer() {
|
|
|
462
819
|
.section h2 { margin-top: 0; color: #38bdf8; font-size: 18px; }
|
|
463
820
|
.config-item { margin: 15px 0; }
|
|
464
821
|
.config-item label { display: block; margin-bottom: 5px; color: #94a3b8; font-size: 14px; }
|
|
465
|
-
.config-item .
|
|
822
|
+
.config-item input, .config-item select {
|
|
823
|
+
background: #334155;
|
|
824
|
+
border: 1px solid #475569;
|
|
825
|
+
color: #e2e8f0;
|
|
826
|
+
padding: 8px 12px;
|
|
827
|
+
border-radius: 4px;
|
|
828
|
+
font-family: monospace;
|
|
829
|
+
font-size: 14px;
|
|
830
|
+
width: 100%;
|
|
831
|
+
max-width: 400px;
|
|
832
|
+
}
|
|
833
|
+
.config-item input:focus, .config-item select:focus {
|
|
834
|
+
outline: none;
|
|
835
|
+
border-color: #38bdf8;
|
|
836
|
+
box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.1);
|
|
837
|
+
}
|
|
838
|
+
.btn {
|
|
839
|
+
background: #38bdf8;
|
|
840
|
+
color: white;
|
|
841
|
+
border: none;
|
|
842
|
+
padding: 10px 20px;
|
|
843
|
+
border-radius: 6px;
|
|
844
|
+
cursor: pointer;
|
|
845
|
+
font-size: 14px;
|
|
846
|
+
margin-right: 10px;
|
|
847
|
+
}
|
|
848
|
+
.btn:hover { background: #0ea5e9; }
|
|
849
|
+
.btn-secondary { background: #64748b; }
|
|
850
|
+
.btn-secondary:hover { background: #475569; }
|
|
851
|
+
.success { color: #10b981; margin-left: 10px; }
|
|
852
|
+
.error { color: #ef4444; margin-left: 10px; }
|
|
466
853
|
code { background: #334155; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
|
854
|
+
.checkbox { margin-right: 8px; }
|
|
467
855
|
</style>
|
|
468
856
|
</head>
|
|
469
857
|
<body>
|
|
@@ -476,47 +864,87 @@ async function startWebServer() {
|
|
|
476
864
|
|
|
477
865
|
<div class="section">
|
|
478
866
|
<h2>SaaS Target</h2>
|
|
479
|
-
<
|
|
480
|
-
<
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
<
|
|
485
|
-
|
|
486
|
-
|
|
867
|
+
<form id="configForm">
|
|
868
|
+
<div class="config-item">
|
|
869
|
+
<label>URL</label>
|
|
870
|
+
<input type="url" id="saas_url" name="saas.url" value="${cfg.saas.url || ""}" placeholder="https://your-app.com">
|
|
871
|
+
</div>
|
|
872
|
+
<div class="config-item">
|
|
873
|
+
<label>Auth Type</label>
|
|
874
|
+
<select id="saas_authType" name="saas.authType">
|
|
875
|
+
<option value="none" ${cfg.saas.authType === "none" ? "selected" : ""}>None</option>
|
|
876
|
+
<option value="basic" ${cfg.saas.authType === "basic" ? "selected" : ""}>Basic Auth</option>
|
|
877
|
+
<option value="bearer" ${cfg.saas.authType === "bearer" ? "selected" : ""}>Bearer Token</option>
|
|
878
|
+
<option value="session" ${cfg.saas.authType === "session" ? "selected" : ""}>Session</option>
|
|
879
|
+
</select>
|
|
880
|
+
</div>
|
|
881
|
+
<div class="config-item">
|
|
882
|
+
<label>Username (for Basic Auth)</label>
|
|
883
|
+
<input type="text" id="saas_username" name="saas.username" value="${cfg.saas.username || ""}" placeholder="username">
|
|
884
|
+
</div>
|
|
885
|
+
<div class="config-item">
|
|
886
|
+
<label>Password (for Basic Auth)</label>
|
|
887
|
+
<input type="password" id="saas_password" name="saas.password" value="${cfg.saas.password || ""}" placeholder="password">
|
|
888
|
+
</div>
|
|
889
|
+
</form>
|
|
487
890
|
</div>
|
|
488
891
|
|
|
489
892
|
<div class="section">
|
|
490
893
|
<h2>LLM Configuration</h2>
|
|
491
|
-
<
|
|
492
|
-
<
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
894
|
+
<form id="configForm">
|
|
895
|
+
<div class="config-item">
|
|
896
|
+
<label>Provider</label>
|
|
897
|
+
<select id="llm_provider" name="llm.provider">
|
|
898
|
+
<option value="openai" ${cfg.llm.provider === "openai" ? "selected" : ""}>OpenAI</option>
|
|
899
|
+
<option value="anthropic" ${cfg.llm.provider === "anthropic" ? "selected" : ""}>Anthropic</option>
|
|
900
|
+
<option value="ollama" ${cfg.llm.provider === "ollama" ? "selected" : ""}>Ollama</option>
|
|
901
|
+
</select>
|
|
902
|
+
</div>
|
|
903
|
+
<div class="config-item">
|
|
904
|
+
<label>Model</label>
|
|
905
|
+
<input type="text" id="llm_model" name="llm.model" value="${cfg.llm.model || ""}" placeholder="gpt-4, claude-3-sonnet, etc.">
|
|
906
|
+
</div>
|
|
907
|
+
<div class="config-item">
|
|
908
|
+
<label>API Key</label>
|
|
909
|
+
<input type="password" id="llm_apiKey" name="llm.apiKey" value="${cfg.llm.apiKey || ""}" placeholder="Your API key">
|
|
910
|
+
</div>
|
|
911
|
+
<div class="config-item">
|
|
912
|
+
<label>Base URL (for Ollama)</label>
|
|
913
|
+
<input type="url" id="llm_baseUrl" name="llm.baseUrl" value="${cfg.llm.baseUrl || ""}" placeholder="http://localhost:11434">
|
|
914
|
+
</div>
|
|
915
|
+
</form>
|
|
499
916
|
</div>
|
|
500
917
|
|
|
501
918
|
<div class="section">
|
|
502
919
|
<h2>Agent Settings</h2>
|
|
503
|
-
<
|
|
504
|
-
<
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
920
|
+
<form id="configForm">
|
|
921
|
+
<div class="config-item">
|
|
922
|
+
<label>
|
|
923
|
+
<input type="checkbox" id="agent_autoStart" name="agent.autoStart" class="checkbox" ${cfg.agent.autoStart ? "checked" : ""}>
|
|
924
|
+
Auto-start
|
|
925
|
+
</label>
|
|
926
|
+
</div>
|
|
927
|
+
<div class="config-item">
|
|
928
|
+
<label>Interval (ms)</label>
|
|
929
|
+
<input type="number" id="agent_intervalMs" name="agent.intervalMs" value="${cfg.agent.intervalMs}" min="60000">
|
|
930
|
+
</div>
|
|
931
|
+
<div class="config-item">
|
|
932
|
+
<label>Max Iterations</label>
|
|
933
|
+
<input type="number" id="agent_maxIterations" name="agent.maxIterations" value="${cfg.agent.maxIterations}" min="1" max="100">
|
|
934
|
+
</div>
|
|
935
|
+
</form>
|
|
936
|
+
</div>
|
|
937
|
+
|
|
938
|
+
<div class="section">
|
|
939
|
+
<h2>Actions</h2>
|
|
940
|
+
<button type="button" class="btn" onclick="saveConfig()">Save Configuration</button>
|
|
941
|
+
<button type="button" class="btn btn-secondary" onclick="resetConfig()">Reset to Defaults</button>
|
|
942
|
+
<span id="message"></span>
|
|
515
943
|
</div>
|
|
516
944
|
|
|
517
945
|
<div class="section">
|
|
518
|
-
<h2>
|
|
519
|
-
<p>
|
|
946
|
+
<h2>Environment Variables</h2>
|
|
947
|
+
<p>You can also set these environment variables before starting OpenQA:</p>
|
|
520
948
|
<pre style="background: #334155; padding: 15px; border-radius: 6px; overflow-x: auto;"><code>export SAAS_URL="https://your-app.com"
|
|
521
949
|
export AGENT_AUTO_START=true
|
|
522
950
|
export LLM_PROVIDER=openai
|
|
@@ -524,6 +952,78 @@ export OPENAI_API_KEY="your-key"
|
|
|
524
952
|
|
|
525
953
|
openqa start</code></pre>
|
|
526
954
|
</div>
|
|
955
|
+
|
|
956
|
+
<script>
|
|
957
|
+
async function saveConfig() {
|
|
958
|
+
const form = document.getElementById('configForm');
|
|
959
|
+
const formData = new FormData(form);
|
|
960
|
+
const config = {};
|
|
961
|
+
|
|
962
|
+
for (let [key, value] of formData.entries()) {
|
|
963
|
+
if (value === '') continue;
|
|
964
|
+
|
|
965
|
+
// Handle nested keys like "saas.url"
|
|
966
|
+
const keys = key.split('.');
|
|
967
|
+
let obj = config;
|
|
968
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
969
|
+
if (!obj[keys[i]]) obj[keys[i]] = {};
|
|
970
|
+
obj = obj[keys[i]];
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Convert checkbox values to boolean
|
|
974
|
+
if (key.includes('autoStart')) {
|
|
975
|
+
obj[keys[keys.length - 1]] = value === 'on';
|
|
976
|
+
} else if (key.includes('intervalMs') || key.includes('maxIterations')) {
|
|
977
|
+
obj[keys[keys.length - 1]] = parseInt(value);
|
|
978
|
+
} else {
|
|
979
|
+
obj[keys[keys.length - 1]] = value;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
try {
|
|
984
|
+
const response = await fetch('/api/config', {
|
|
985
|
+
method: 'POST',
|
|
986
|
+
headers: { 'Content-Type': 'application/json' },
|
|
987
|
+
body: JSON.stringify(config)
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
const result = await response.json();
|
|
991
|
+
if (result.success) {
|
|
992
|
+
showMessage('Configuration saved successfully!', 'success');
|
|
993
|
+
setTimeout(() => location.reload(), 1500);
|
|
994
|
+
} else {
|
|
995
|
+
showMessage('Failed to save configuration', 'error');
|
|
996
|
+
}
|
|
997
|
+
} catch (error) {
|
|
998
|
+
showMessage('Error: ' + error.message, 'error');
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
async function resetConfig() {
|
|
1003
|
+
if (confirm('Are you sure you want to reset all configuration to defaults?')) {
|
|
1004
|
+
try {
|
|
1005
|
+
const response = await fetch('/api/config/reset', { method: 'POST' });
|
|
1006
|
+
const result = await response.json();
|
|
1007
|
+
if (result.success) {
|
|
1008
|
+
showMessage('Configuration reset to defaults', 'success');
|
|
1009
|
+
setTimeout(() => location.reload(), 1500);
|
|
1010
|
+
}
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
showMessage('Error: ' + error.message, 'error');
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function showMessage(text, type) {
|
|
1018
|
+
const messageEl = document.getElementById('message');
|
|
1019
|
+
messageEl.textContent = text;
|
|
1020
|
+
messageEl.className = type;
|
|
1021
|
+
setTimeout(() => {
|
|
1022
|
+
messageEl.textContent = '';
|
|
1023
|
+
messageEl.className = '';
|
|
1024
|
+
}, 3000);
|
|
1025
|
+
}
|
|
1026
|
+
</script>
|
|
527
1027
|
</body>
|
|
528
1028
|
</html>
|
|
529
1029
|
`);
|
|
@@ -546,10 +1046,46 @@ openqa start</code></pre>
|
|
|
546
1046
|
wss.emit("connection", ws, request);
|
|
547
1047
|
});
|
|
548
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
|
+
}
|
|
549
1057
|
wss.on("connection", (ws) => {
|
|
550
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);
|
|
551
1086
|
ws.on("close", () => {
|
|
552
1087
|
console.log("WebSocket client disconnected");
|
|
1088
|
+
clearInterval(activityInterval);
|
|
553
1089
|
});
|
|
554
1090
|
});
|
|
555
1091
|
process.on("SIGTERM", () => {
|
|
@@ -626,18 +1162,21 @@ program.command("stop").description("Stop the OpenQA agent").action(() => {
|
|
|
626
1162
|
const spinner = ora("Stopping OpenQA...").start();
|
|
627
1163
|
try {
|
|
628
1164
|
if (!existsSync(PID_FILE)) {
|
|
629
|
-
spinner.fail(chalk2.red("OpenQA
|
|
1165
|
+
spinner.fail(chalk2.red("No OpenQA daemon process found"));
|
|
1166
|
+
console.log(chalk2.yellow("Note: OpenQA might be running in foreground mode"));
|
|
1167
|
+
console.log(chalk2.cyan("Use Ctrl+C to stop foreground processes"));
|
|
630
1168
|
process.exit(1);
|
|
631
1169
|
}
|
|
632
1170
|
const pid = readFileSync2(PID_FILE, "utf-8").trim();
|
|
633
1171
|
try {
|
|
634
1172
|
process.kill(parseInt(pid), "SIGTERM");
|
|
635
1173
|
unlinkSync(PID_FILE);
|
|
636
|
-
spinner.succeed(chalk2.green("OpenQA stopped"));
|
|
1174
|
+
spinner.succeed(chalk2.green("OpenQA daemon stopped"));
|
|
637
1175
|
} catch (error) {
|
|
638
1176
|
spinner.fail(chalk2.red("Failed to stop OpenQA"));
|
|
639
|
-
console.error(chalk2.red("Process not found. Cleaning up PID file..."));
|
|
1177
|
+
console.error(chalk2.red("Process not found. Cleaning up stale PID file..."));
|
|
640
1178
|
unlinkSync(PID_FILE);
|
|
1179
|
+
console.log(chalk2.yellow("The daemon process was already stopped"));
|
|
641
1180
|
}
|
|
642
1181
|
} catch (error) {
|
|
643
1182
|
spinner.fail(chalk2.red("Failed to stop OpenQA"));
|