@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.
@@ -166,6 +166,11 @@ var OpenQADatabase = class {
166
166
  await this.ensureInitialized();
167
167
  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());
168
168
  }
169
+ async clearAllConfig() {
170
+ await this.ensureInitialized();
171
+ this.db.data.config = {};
172
+ await this.db.write();
173
+ }
169
174
  async close() {
170
175
  }
171
176
  };
@@ -303,58 +308,410 @@ async function startWebServer() {
303
308
  res.json(cfg);
304
309
  });
305
310
  app.post("/api/config", async (req, res) => {
306
- const { key, value } = req.body;
307
- await config.set(key, value);
311
+ try {
312
+ const configData = req.body;
313
+ for (const [key, value] of Object.entries(configData)) {
314
+ await config.set(key, String(value));
315
+ }
316
+ res.json({ success: true });
317
+ } catch (error) {
318
+ res.status(500).json({ success: false, error: error.message });
319
+ }
320
+ });
321
+ app.post("/api/config/reset", async (req, res) => {
322
+ try {
323
+ await db.clearAllConfig();
324
+ res.json({ success: true });
325
+ } catch (error) {
326
+ res.status(500).json({ success: false, error: error.message });
327
+ }
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
+ });
308
337
  res.json({ success: true });
309
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
+ });
310
391
  app.get("/", (req, res) => {
311
392
  res.send(`
312
393
  <!DOCTYPE html>
313
394
  <html>
314
395
  <head>
315
- <title>OpenQA - Dashboard</title>
396
+ <title>OpenQA - Real-time Dashboard</title>
316
397
  <style>
317
- body { font-family: system-ui; max-width: 1200px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
398
+ body { font-family: system-ui; max-width: 1400px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
318
399
  h1 { color: #38bdf8; }
319
400
  .card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; margin: 20px 0; }
320
401
  .status { display: inline-block; padding: 4px 12px; border-radius: 4px; font-size: 14px; }
321
402
  .status.running { background: #10b981; color: white; }
322
403
  .status.idle { background: #f59e0b; color: white; }
404
+ .status.error { background: #ef4444; color: white; }
405
+ .status.paused { background: #64748b; color: white; }
323
406
  a { color: #38bdf8; text-decoration: none; }
324
407
  a:hover { text-decoration: underline; }
325
408
  nav { margin: 20px 0; }
326
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; }
327
453
  </style>
328
454
  </head>
329
455
  <body>
330
- <h1>\u{1F916} OpenQA Dashboard</h1>
456
+ <h1>\u{1F916} OpenQA Real-time Dashboard</h1>
331
457
  <nav>
332
458
  <a href="/">Dashboard</a>
333
459
  <a href="/kanban">Kanban</a>
334
460
  <a href="/config">Config</a>
461
+ <span style="color: #64748b;">|</span>
462
+ <span id="connection-status" class="status idle">\u{1F50C} Connecting...</span>
335
463
  </nav>
336
- <div class="card">
337
- <h2>Status</h2>
338
- <p>Agent: <span class="status idle">Idle</span></p>
339
- <p>Target: ${cfg.saas.url || "Not configured"}</p>
340
- <p>Auto-start: ${cfg.agent.autoStart ? "Enabled" : "Disabled"}</p>
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>
341
515
  </div>
342
- <div class="card">
343
- <h2>Quick Links</h2>
344
- <ul>
345
- <li><a href="/kanban">View Kanban Board</a></li>
346
- <li><a href="/config">Configure OpenQA</a></li>
347
- </ul>
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>
348
531
  </div>
349
- <div class="card">
350
- <h2>Getting Started</h2>
351
- <p>Configure your SaaS application target and start testing:</p>
352
- <ol>
353
- <li>Set SAAS_URL environment variable or use the <a href="/config">Config page</a></li>
354
- <li>Enable auto-start: <code>export AGENT_AUTO_START=true</code></li>
355
- <li>Restart OpenQA</li>
356
- </ol>
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>
357
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>
358
715
  </body>
359
716
  </html>
360
717
  `);
@@ -425,8 +782,39 @@ async function startWebServer() {
425
782
  .section h2 { margin-top: 0; color: #38bdf8; font-size: 18px; }
426
783
  .config-item { margin: 15px 0; }
427
784
  .config-item label { display: block; margin-bottom: 5px; color: #94a3b8; font-size: 14px; }
428
- .config-item .value { background: #334155; padding: 8px 12px; border-radius: 4px; font-family: monospace; font-size: 14px; }
785
+ .config-item input, .config-item select {
786
+ background: #334155;
787
+ border: 1px solid #475569;
788
+ color: #e2e8f0;
789
+ padding: 8px 12px;
790
+ border-radius: 4px;
791
+ font-family: monospace;
792
+ font-size: 14px;
793
+ width: 100%;
794
+ max-width: 400px;
795
+ }
796
+ .config-item input:focus, .config-item select:focus {
797
+ outline: none;
798
+ border-color: #38bdf8;
799
+ box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.1);
800
+ }
801
+ .btn {
802
+ background: #38bdf8;
803
+ color: white;
804
+ border: none;
805
+ padding: 10px 20px;
806
+ border-radius: 6px;
807
+ cursor: pointer;
808
+ font-size: 14px;
809
+ margin-right: 10px;
810
+ }
811
+ .btn:hover { background: #0ea5e9; }
812
+ .btn-secondary { background: #64748b; }
813
+ .btn-secondary:hover { background: #475569; }
814
+ .success { color: #10b981; margin-left: 10px; }
815
+ .error { color: #ef4444; margin-left: 10px; }
429
816
  code { background: #334155; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
817
+ .checkbox { margin-right: 8px; }
430
818
  </style>
431
819
  </head>
432
820
  <body>
@@ -439,47 +827,87 @@ async function startWebServer() {
439
827
 
440
828
  <div class="section">
441
829
  <h2>SaaS Target</h2>
442
- <div class="config-item">
443
- <label>URL</label>
444
- <div class="value">${cfg.saas.url || "Not configured"}</div>
445
- </div>
446
- <div class="config-item">
447
- <label>Auth Type</label>
448
- <div class="value">${cfg.saas.authType}</div>
449
- </div>
830
+ <form id="configForm">
831
+ <div class="config-item">
832
+ <label>URL</label>
833
+ <input type="url" id="saas_url" name="saas.url" value="${cfg.saas.url || ""}" placeholder="https://your-app.com">
834
+ </div>
835
+ <div class="config-item">
836
+ <label>Auth Type</label>
837
+ <select id="saas_authType" name="saas.authType">
838
+ <option value="none" ${cfg.saas.authType === "none" ? "selected" : ""}>None</option>
839
+ <option value="basic" ${cfg.saas.authType === "basic" ? "selected" : ""}>Basic Auth</option>
840
+ <option value="bearer" ${cfg.saas.authType === "bearer" ? "selected" : ""}>Bearer Token</option>
841
+ <option value="session" ${cfg.saas.authType === "session" ? "selected" : ""}>Session</option>
842
+ </select>
843
+ </div>
844
+ <div class="config-item">
845
+ <label>Username (for Basic Auth)</label>
846
+ <input type="text" id="saas_username" name="saas.username" value="${cfg.saas.username || ""}" placeholder="username">
847
+ </div>
848
+ <div class="config-item">
849
+ <label>Password (for Basic Auth)</label>
850
+ <input type="password" id="saas_password" name="saas.password" value="${cfg.saas.password || ""}" placeholder="password">
851
+ </div>
852
+ </form>
450
853
  </div>
451
854
 
452
855
  <div class="section">
453
856
  <h2>LLM Configuration</h2>
454
- <div class="config-item">
455
- <label>Provider</label>
456
- <div class="value">${cfg.llm.provider}</div>
457
- </div>
458
- <div class="config-item">
459
- <label>Model</label>
460
- <div class="value">${cfg.llm.model || "default"}</div>
461
- </div>
857
+ <form id="configForm">
858
+ <div class="config-item">
859
+ <label>Provider</label>
860
+ <select id="llm_provider" name="llm.provider">
861
+ <option value="openai" ${cfg.llm.provider === "openai" ? "selected" : ""}>OpenAI</option>
862
+ <option value="anthropic" ${cfg.llm.provider === "anthropic" ? "selected" : ""}>Anthropic</option>
863
+ <option value="ollama" ${cfg.llm.provider === "ollama" ? "selected" : ""}>Ollama</option>
864
+ </select>
865
+ </div>
866
+ <div class="config-item">
867
+ <label>Model</label>
868
+ <input type="text" id="llm_model" name="llm.model" value="${cfg.llm.model || ""}" placeholder="gpt-4, claude-3-sonnet, etc.">
869
+ </div>
870
+ <div class="config-item">
871
+ <label>API Key</label>
872
+ <input type="password" id="llm_apiKey" name="llm.apiKey" value="${cfg.llm.apiKey || ""}" placeholder="Your API key">
873
+ </div>
874
+ <div class="config-item">
875
+ <label>Base URL (for Ollama)</label>
876
+ <input type="url" id="llm_baseUrl" name="llm.baseUrl" value="${cfg.llm.baseUrl || ""}" placeholder="http://localhost:11434">
877
+ </div>
878
+ </form>
462
879
  </div>
463
880
 
464
881
  <div class="section">
465
882
  <h2>Agent Settings</h2>
466
- <div class="config-item">
467
- <label>Auto-start</label>
468
- <div class="value">${cfg.agent.autoStart ? "Enabled" : "Disabled"}</div>
469
- </div>
470
- <div class="config-item">
471
- <label>Interval</label>
472
- <div class="value">${cfg.agent.intervalMs}ms</div>
473
- </div>
474
- <div class="config-item">
475
- <label>Max Iterations</label>
476
- <div class="value">${cfg.agent.maxIterations}</div>
477
- </div>
883
+ <form id="configForm">
884
+ <div class="config-item">
885
+ <label>
886
+ <input type="checkbox" id="agent_autoStart" name="agent.autoStart" class="checkbox" ${cfg.agent.autoStart ? "checked" : ""}>
887
+ Auto-start
888
+ </label>
889
+ </div>
890
+ <div class="config-item">
891
+ <label>Interval (ms)</label>
892
+ <input type="number" id="agent_intervalMs" name="agent.intervalMs" value="${cfg.agent.intervalMs}" min="60000">
893
+ </div>
894
+ <div class="config-item">
895
+ <label>Max Iterations</label>
896
+ <input type="number" id="agent_maxIterations" name="agent.maxIterations" value="${cfg.agent.maxIterations}" min="1" max="100">
897
+ </div>
898
+ </form>
899
+ </div>
900
+
901
+ <div class="section">
902
+ <h2>Actions</h2>
903
+ <button type="button" class="btn" onclick="saveConfig()">Save Configuration</button>
904
+ <button type="button" class="btn btn-secondary" onclick="resetConfig()">Reset to Defaults</button>
905
+ <span id="message"></span>
478
906
  </div>
479
907
 
480
908
  <div class="section">
481
- <h2>How to Configure</h2>
482
- <p>Set environment variables before starting OpenQA:</p>
909
+ <h2>Environment Variables</h2>
910
+ <p>You can also set these environment variables before starting OpenQA:</p>
483
911
  <pre style="background: #334155; padding: 15px; border-radius: 6px; overflow-x: auto;"><code>export SAAS_URL="https://your-app.com"
484
912
  export AGENT_AUTO_START=true
485
913
  export LLM_PROVIDER=openai
@@ -487,6 +915,78 @@ export OPENAI_API_KEY="your-key"
487
915
 
488
916
  openqa start</code></pre>
489
917
  </div>
918
+
919
+ <script>
920
+ async function saveConfig() {
921
+ const form = document.getElementById('configForm');
922
+ const formData = new FormData(form);
923
+ const config = {};
924
+
925
+ for (let [key, value] of formData.entries()) {
926
+ if (value === '') continue;
927
+
928
+ // Handle nested keys like "saas.url"
929
+ const keys = key.split('.');
930
+ let obj = config;
931
+ for (let i = 0; i < keys.length - 1; i++) {
932
+ if (!obj[keys[i]]) obj[keys[i]] = {};
933
+ obj = obj[keys[i]];
934
+ }
935
+
936
+ // Convert checkbox values to boolean
937
+ if (key.includes('autoStart')) {
938
+ obj[keys[keys.length - 1]] = value === 'on';
939
+ } else if (key.includes('intervalMs') || key.includes('maxIterations')) {
940
+ obj[keys[keys.length - 1]] = parseInt(value);
941
+ } else {
942
+ obj[keys[keys.length - 1]] = value;
943
+ }
944
+ }
945
+
946
+ try {
947
+ const response = await fetch('/api/config', {
948
+ method: 'POST',
949
+ headers: { 'Content-Type': 'application/json' },
950
+ body: JSON.stringify(config)
951
+ });
952
+
953
+ const result = await response.json();
954
+ if (result.success) {
955
+ showMessage('Configuration saved successfully!', 'success');
956
+ setTimeout(() => location.reload(), 1500);
957
+ } else {
958
+ showMessage('Failed to save configuration', 'error');
959
+ }
960
+ } catch (error) {
961
+ showMessage('Error: ' + error.message, 'error');
962
+ }
963
+ }
964
+
965
+ async function resetConfig() {
966
+ if (confirm('Are you sure you want to reset all configuration to defaults?')) {
967
+ try {
968
+ const response = await fetch('/api/config/reset', { method: 'POST' });
969
+ const result = await response.json();
970
+ if (result.success) {
971
+ showMessage('Configuration reset to defaults', 'success');
972
+ setTimeout(() => location.reload(), 1500);
973
+ }
974
+ } catch (error) {
975
+ showMessage('Error: ' + error.message, 'error');
976
+ }
977
+ }
978
+ }
979
+
980
+ function showMessage(text, type) {
981
+ const messageEl = document.getElementById('message');
982
+ messageEl.textContent = text;
983
+ messageEl.className = type;
984
+ setTimeout(() => {
985
+ messageEl.textContent = '';
986
+ messageEl.className = '';
987
+ }, 3000);
988
+ }
989
+ </script>
490
990
  </body>
491
991
  </html>
492
992
  `);
@@ -509,10 +1009,46 @@ openqa start</code></pre>
509
1009
  wss.emit("connection", ws, request);
510
1010
  });
511
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
+ }
512
1020
  wss.on("connection", (ws) => {
513
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);
514
1049
  ws.on("close", () => {
515
1050
  console.log("WebSocket client disconnected");
1051
+ clearInterval(activityInterval);
516
1052
  });
517
1053
  });
518
1054
  process.on("SIGTERM", () => {