@openqa/cli 1.3.1 → 1.3.3

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 CHANGED
@@ -363,15 +363,50 @@ 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) => {
366
+ app.post("/api/intervention/:id", async (req, res) => {
367
367
  const { id } = req.params;
368
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 });
369
+ console.log(`Intervention ${id} ${response} by user`);
370
+ res.json({ success: true, message: `Intervention ${response}d` });
371
+ });
372
+ app.post("/api/test-connection", async (req, res) => {
373
+ try {
374
+ const cfg2 = config.getConfigSync();
375
+ if (cfg2.saas.url) {
376
+ const controller = new AbortController();
377
+ const timeoutId = setTimeout(() => controller.abort(), 1e4);
378
+ try {
379
+ const response = await fetch(cfg2.saas.url, {
380
+ method: "HEAD",
381
+ signal: controller.signal,
382
+ headers: cfg2.saas.authType === "basic" && cfg2.saas.username && cfg2.saas.password ? {
383
+ "Authorization": "Basic " + Buffer.from(`${cfg2.saas.username}:${cfg2.saas.password}`).toString("base64")
384
+ } : {}
385
+ });
386
+ clearTimeout(timeoutId);
387
+ if (response.ok) {
388
+ res.json({ success: true, message: "SaaS connection successful" });
389
+ } else {
390
+ res.json({ success: false, message: "SaaS connection failed" });
391
+ }
392
+ } catch (fetchError) {
393
+ clearTimeout(timeoutId);
394
+ throw fetchError;
395
+ }
396
+ } else {
397
+ res.json({ success: false, message: "No SaaS URL configured" });
398
+ }
399
+ } catch (error) {
400
+ res.json({ success: false, message: "Connection error: " + (error.message || String(error)) });
401
+ }
402
+ });
403
+ app.post("/api/start", async (req, res) => {
404
+ try {
405
+ console.log("Starting agent session...");
406
+ res.json({ success: true, message: "Session started" });
407
+ } catch (error) {
408
+ res.json({ success: false, message: "Failed to start session: " + (error.message || String(error)) });
409
+ }
375
410
  });
376
411
  app.get("/api/tasks", async (req, res) => {
377
412
  const tasks = [
@@ -434,158 +469,482 @@ async function startWebServer() {
434
469
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
435
470
  <script src="https://cdn.jsdelivr.net/npm/@xyflow/react@11/dist/umd/index.js"></script>
436
471
  <style>
437
- body { font-family: system-ui; max-width: 1600px; margin: 20px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
438
- h1 { color: #f97316; margin-bottom: 30px; }
439
- .card { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 24px; margin: 20px 0; }
440
- .card-header { display: flex; justify-content: between; align-items: center; margin-bottom: 20px; }
441
- .card-title { font-size: 18px; font-weight: 600; color: #f97316; }
442
- .status { display: inline-block; padding: 6px 12px; border-radius: 20px; font-size: 12px; font-weight: 500; }
472
+ * { box-sizing: border-box; margin: 0; padding: 0; }
473
+
474
+ body {
475
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
476
+ background: #0a0a0a;
477
+ color: #ffffff;
478
+ min-height: 100vh;
479
+ overflow-x: hidden;
480
+ line-height: 1.6;
481
+ }
482
+
483
+ .dashboard-container {
484
+ max-width: 1920px;
485
+ margin: 0 auto;
486
+ padding: 20px;
487
+ min-height: 100vh;
488
+ }
489
+
490
+ .dashboard-header {
491
+ display: flex;
492
+ justify-content: space-between;
493
+ align-items: center;
494
+ margin-bottom: 32px;
495
+ padding: 24px 32px;
496
+ background: linear-gradient(135deg, #1a1a1a, #2a2a2a);
497
+ border-radius: 16px;
498
+ border: 1px solid #333333;
499
+ backdrop-filter: blur(10px);
500
+ }
501
+
502
+ .dashboard-title {
503
+ font-size: 32px;
504
+ font-weight: 700;
505
+ color: #f97316;
506
+ margin: 0;
507
+ letter-spacing: -0.5px;
508
+ }
509
+
510
+ .nav {
511
+ display: flex;
512
+ gap: 8px;
513
+ background: rgba(255, 255, 255, 0.05);
514
+ padding: 4px;
515
+ border-radius: 12px;
516
+ }
517
+
518
+ .nav-links a {
519
+ color: #9ca3af;
520
+ text-decoration: none;
521
+ font-weight: 500;
522
+ padding: 12px 24px;
523
+ border-radius: 8px;
524
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
525
+ display: flex;
526
+ align-items: center;
527
+ gap: 8px;
528
+ }
529
+
530
+ .nav-links a:hover, .nav-links a.active {
531
+ color: #ffffff;
532
+ background: #f97316;
533
+ transform: translateY(-1px);
534
+ }
535
+
536
+ .connection-status {
537
+ padding: 8px 16px;
538
+ border-radius: 20px;
539
+ font-size: 12px;
540
+ font-weight: 600;
541
+ text-transform: uppercase;
542
+ letter-spacing: 0.5px;
543
+ }
544
+
443
545
  .status.running { background: linear-gradient(135deg, #10b981, #059669); color: white; }
444
546
  .status.idle { background: linear-gradient(135deg, #f59e0b, #d97706); color: white; }
445
547
  .status.error { background: linear-gradient(135deg, #ef4444, #dc2626); color: white; }
446
548
  .status.paused { background: linear-gradient(135deg, #64748b, #475569); color: white; }
447
- .nav { display: flex; justify-content: space-between; align-items: center; margin: 20px 0; padding: 15px; background: #1e293b; border-radius: 12px; }
448
- .nav-links { display: flex; gap: 30px; }
449
- .nav-links a { color: #94a3b8; text-decoration: none; font-weight: 500; transition: color 0.2s; }
450
- .nav-links a:hover, .nav-links a.active { color: #f97316; }
451
- .grid { display: grid; gap: 20px; }
452
- .grid-2 { grid-template-columns: repeat(2, 1fr); }
453
- .grid-3 { grid-template-columns: repeat(3, 1fr); }
454
- .grid-4 { grid-template-columns: repeat(4, 1fr); }
455
- .metric-card {
456
- background: linear-gradient(135deg, #1e293b, #334155);
457
- border: 1px solid #334155;
458
- border-radius: 12px;
459
- padding: 20px;
460
- text-align: center;
549
+
550
+ /* Metrics Grid */
551
+ .metrics-grid {
552
+ display: grid;
553
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
554
+ gap: 24px;
555
+ margin-bottom: 32px;
556
+ }
557
+
558
+ .metric-card {
559
+ background: linear-gradient(145deg, #1a1a1a, #2d2d2d);
560
+ border: 1px solid #333333;
561
+ border-radius: 16px;
562
+ padding: 28px 24px;
461
563
  position: relative;
462
564
  overflow: hidden;
565
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
566
+ min-height: 160px;
567
+ display: flex;
568
+ flex-direction: column;
569
+ justify-content: space-between;
463
570
  }
464
- .metric-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, #f97316, #ea580c); }
465
- .metric-value { font-size: 32px; font-weight: bold; color: #f97316; margin: 10px 0; }
466
- .metric-label { color: #94a3b8; font-size: 14px; font-weight: 500; }
467
- .metric-change { font-size: 12px; margin-top: 5px; }
571
+
572
+ .metric-card::before {
573
+ content: '';
574
+ position: absolute;
575
+ top: 0;
576
+ left: 0;
577
+ right: 0;
578
+ height: 4px;
579
+ background: linear-gradient(90deg, #f97316, #ea580c, #f59e0b);
580
+ background-size: 200% 100%;
581
+ animation: shimmer 3s ease-in-out infinite;
582
+ }
583
+
584
+ @keyframes shimmer {
585
+ 0%, 100% { background-position: -200% 0; }
586
+ 50% { background-position: 200% 0; }
587
+ }
588
+
589
+ .metric-card:hover {
590
+ transform: translateY(-4px);
591
+ border-color: #f97316;
592
+ box-shadow: 0 20px 40px rgba(249, 115, 22, 0.15);
593
+ }
594
+
595
+ .metric-header {
596
+ display: flex;
597
+ justify-content: space-between;
598
+ align-items: flex-start;
599
+ margin-bottom: 16px;
600
+ }
601
+
602
+ .metric-label {
603
+ color: #9ca3af;
604
+ font-size: 14px;
605
+ font-weight: 500;
606
+ text-transform: uppercase;
607
+ letter-spacing: 0.5px;
608
+ }
609
+
610
+ .metric-icon {
611
+ width: 40px;
612
+ height: 40px;
613
+ background: rgba(249, 115, 22, 0.1);
614
+ border-radius: 12px;
615
+ display: flex;
616
+ align-items: center;
617
+ justify-content: center;
618
+ font-size: 20px;
619
+ }
620
+
621
+ .metric-value {
622
+ font-size: 42px;
623
+ font-weight: 700;
624
+ color: #f97316;
625
+ margin: 8px 0;
626
+ line-height: 1;
627
+ background: linear-gradient(135deg, #f97316, #fbbf24);
628
+ -webkit-background-clip: text;
629
+ -webkit-text-fill-color: transparent;
630
+ background-clip: text;
631
+ }
632
+
633
+ .metric-change {
634
+ font-size: 13px;
635
+ font-weight: 600;
636
+ display: flex;
637
+ align-items: center;
638
+ gap: 4px;
639
+ }
640
+
468
641
  .metric-change.positive { color: #10b981; }
469
642
  .metric-change.negative { color: #ef4444; }
470
- .chart-container { position: relative; height: 300px; margin: 20px 0; }
471
- .hierarchy-container { height: 400px; border: 1px solid #334155; border-radius: 8px; background: #0f172a; }
472
- .activity-item {
473
- background: #334155;
474
- padding: 15px;
475
- margin: 10px 0;
476
- border-radius: 8px;
643
+
644
+ /* Main Content Grid */
645
+ .main-grid {
646
+ display: grid;
647
+ grid-template-columns: 1fr 1fr;
648
+ gap: 24px;
649
+ margin-bottom: 32px;
650
+ }
651
+
652
+ .card {
653
+ background: linear-gradient(145deg, #1a1a1a, #2d2d2d);
654
+ border: 1px solid #333333;
655
+ border-radius: 16px;
656
+ padding: 28px;
657
+ position: relative;
658
+ overflow: hidden;
659
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
660
+ }
661
+
662
+ .card:hover {
663
+ border-color: #444444;
664
+ transform: translateY(-2px);
665
+ }
666
+
667
+ .card-header {
668
+ display: flex;
669
+ justify-content: space-between;
670
+ align-items: center;
671
+ margin-bottom: 24px;
672
+ }
673
+
674
+ .card-title {
675
+ font-size: 20px;
676
+ font-weight: 600;
677
+ color: #ffffff;
678
+ margin: 0;
679
+ }
680
+
681
+ .tabs {
682
+ display: flex;
683
+ gap: 4px;
684
+ background: rgba(255, 255, 255, 0.05);
685
+ padding: 4px;
686
+ border-radius: 12px;
687
+ }
688
+
689
+ .tab {
690
+ padding: 10px 20px;
691
+ background: transparent;
692
+ border: none;
693
+ border-radius: 8px;
694
+ color: #9ca3af;
695
+ font-weight: 500;
696
+ cursor: pointer;
697
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
698
+ }
699
+
700
+ .tab.active {
701
+ background: #f97316;
702
+ color: white;
703
+ }
704
+
705
+ .tab:hover:not(.active) {
706
+ color: #ffffff;
707
+ background: rgba(255, 255, 255, 0.1);
708
+ }
709
+
710
+ .chart-container {
711
+ position: relative;
712
+ height: 320px;
713
+ margin: 20px 0;
714
+ }
715
+
716
+ .hierarchy-container {
717
+ height: 420px;
718
+ border: 1px solid #333333;
719
+ border-radius: 12px;
720
+ background: #0a0a0a;
721
+ overflow: hidden;
722
+ }
723
+
724
+ /* Activity Feed */
725
+ .activity-feed {
726
+ max-height: 400px;
727
+ overflow-y: auto;
728
+ padding-right: 8px;
729
+ }
730
+
731
+ .activity-feed::-webkit-scrollbar {
732
+ width: 6px;
733
+ }
734
+
735
+ .activity-feed::-webkit-scrollbar-track {
736
+ background: rgba(255, 255, 255, 0.05);
737
+ border-radius: 3px;
738
+ }
739
+
740
+ .activity-feed::-webkit-scrollbar-thumb {
741
+ background: #f97316;
742
+ border-radius: 3px;
743
+ }
744
+
745
+ .activity-item {
746
+ background: rgba(255, 255, 255, 0.03);
747
+ border: 1px solid rgba(255, 255, 255, 0.08);
477
748
  border-left: 4px solid #f97316;
749
+ border-radius: 12px;
750
+ padding: 16px 20px;
751
+ margin-bottom: 12px;
478
752
  font-size: 14px;
479
- transition: all 0.2s;
753
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
754
+ position: relative;
755
+ overflow: hidden;
756
+ }
757
+
758
+ .activity-item::before {
759
+ content: '';
760
+ position: absolute;
761
+ top: 0;
762
+ left: 0;
763
+ right: 0;
764
+ height: 1px;
765
+ background: linear-gradient(90deg, transparent, rgba(249, 115, 22, 0.3), transparent);
766
+ opacity: 0;
767
+ transition: opacity 0.3s;
480
768
  }
481
- .activity-item:hover { transform: translateX(4px); background: #475569; }
769
+
770
+ .activity-item:hover {
771
+ background: rgba(255, 255, 255, 0.06);
772
+ border-left-color: #fbbf24;
773
+ transform: translateX(8px);
774
+ }
775
+
776
+ .activity-item:hover::before {
777
+ opacity: 1;
778
+ }
779
+
482
780
  .activity-item.error { border-left-color: #ef4444; }
483
781
  .activity-item.success { border-left-color: #10b981; }
484
782
  .activity-item.warning { border-left-color: #f59e0b; }
485
- .activity-time { color: #64748b; font-size: 12px; }
486
- .intervention-request {
487
- background: linear-gradient(135deg, #7c2d12, #92400e);
488
- border: 1px solid #dc2626;
489
- padding: 20px;
490
- border-radius: 12px;
491
- margin: 15px 0;
783
+
784
+ .activity-time {
785
+ color: #6b7280;
786
+ font-size: 12px;
787
+ margin-top: 8px;
788
+ display: flex;
789
+ align-items: center;
790
+ gap: 4px;
791
+ }
792
+
793
+ /* Buttons */
794
+ .btn {
795
+ background: linear-gradient(135deg, #f97316, #ea580c);
796
+ color: white;
797
+ border: none;
798
+ padding: 12px 24px;
799
+ border-radius: 10px;
800
+ cursor: pointer;
801
+ font-size: 14px;
802
+ font-weight: 600;
803
+ margin: 4px;
804
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
492
805
  position: relative;
806
+ overflow: hidden;
493
807
  }
494
- .intervention-request::before { content: '\u{1F6A8}'; position: absolute; top: 15px; right: 15px; font-size: 20px; }
495
- .intervention-request h4 { color: #fbbf24; margin: 0 0 10px 0; }
496
- .btn {
497
- background: linear-gradient(135deg, #f97316, #ea580c);
498
- color: white;
499
- border: none;
500
- padding: 10px 20px;
501
- border-radius: 8px;
502
- cursor: pointer;
503
- font-size: 14px;
504
- font-weight: 500;
505
- margin: 5px;
506
- transition: all 0.2s;
808
+
809
+ .btn::before {
810
+ content: '';
811
+ position: absolute;
812
+ top: 0;
813
+ left: -100%;
814
+ width: 100%;
815
+ height: 100%;
816
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
817
+ transition: left 0.5s;
818
+ }
819
+
820
+ .btn:hover {
821
+ transform: translateY(-2px);
822
+ box-shadow: 0 8px 24px rgba(249, 115, 22, 0.3);
507
823
  }
508
- .btn:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(249, 115, 22, 0.3); }
824
+
825
+ .btn:hover::before {
826
+ left: 100%;
827
+ }
828
+
509
829
  .btn-success { background: linear-gradient(135deg, #10b981, #059669); }
510
- .btn-success:hover { box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3); }
830
+ .btn-success:hover { box-shadow: 0 8px 24px rgba(16, 185, 129, 0.3); }
511
831
  .btn-danger { background: linear-gradient(135deg, #ef4444, #dc2626); }
512
- .btn-danger:hover { box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3); }
513
- .pulse { animation: pulse 2s infinite; }
514
- @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
515
- .loading { color: #f59e0b; }
516
- .tabs { display: flex; gap: 10px; margin-bottom: 20px; }
517
- .tab { padding: 10px 20px; background: #334155; border-radius: 8px; cursor: pointer; transition: all 0.2s; }
518
- .tab.active { background: #f97316; color: white; }
519
- .tab-content { display: none; }
520
- .tab-content.active { display: block; }
521
- .agent-node {
522
- background: #1e293b;
523
- border: 2px solid #f97316;
524
- border-radius: 8px;
525
- padding: 10px;
526
- margin: 10px;
527
- text-align: center;
832
+ .btn-danger:hover { box-shadow: 0 8px 24px rgba(239, 68, 68, 0.3); }
833
+
834
+ /* Performance bars */
835
+ .performance-bar {
836
+ height: 6px;
837
+ background: rgba(255, 255, 255, 0.1);
838
+ border-radius: 3px;
839
+ overflow: hidden;
840
+ margin: 8px 0;
528
841
  }
529
- .performance-bar {
530
- height: 8px;
531
- background: #334155;
532
- border-radius: 4px;
533
- overflow: hidden;
534
- margin: 10px 0;
842
+
843
+ .performance-fill {
844
+ height: 100%;
845
+ background: linear-gradient(90deg, #10b981, #f97316);
846
+ transition: width 1s cubic-bezier(0.4, 0, 0.2, 1);
847
+ border-radius: 3px;
535
848
  }
536
- .performance-fill {
537
- height: 100%;
538
- background: linear-gradient(90deg, #10b981, #f97316);
539
- transition: width 1s ease;
849
+
850
+ /* Responsive */
851
+ @media (max-width: 1200px) {
852
+ .main-grid {
853
+ grid-template-columns: 1fr;
854
+ }
540
855
  }
856
+
857
+ @media (max-width: 768px) {
858
+ .dashboard-container {
859
+ padding: 16px;
860
+ }
861
+
862
+ .metrics-grid {
863
+ grid-template-columns: 1fr;
864
+ gap: 16px;
865
+ }
866
+
867
+ .dashboard-header {
868
+ flex-direction: column;
869
+ gap: 16px;
870
+ padding: 20px;
871
+ }
872
+
873
+ .dashboard-title {
874
+ font-size: 24px;
875
+ }
876
+
877
+ .metric-value {
878
+ font-size: 32px;
879
+ }
880
+ }
881
+
882
+ /* Loading animation */
883
+ .pulse { animation: pulse 2s infinite; }
884
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
885
+ </style>
541
886
  </style>
542
887
  </head>
543
888
  <body>
544
- <div class="nav">
545
- <div class="nav-links">
546
- <a href="/" class="active">\u{1F4CA} Dashboard</a>
547
- <a href="/kanban">\u{1F4CB} Kanban</a>
548
- <a href="/config">\u2699\uFE0F Config</a>
549
- </div>
550
- <div>
551
- <span id="connection-status" class="status idle">\u{1F50C} Connecting...</span>
552
- </div>
553
- </div>
554
-
555
- <!-- Key Metrics -->
556
- <div class="grid-4">
557
- <div class="metric-card">
558
- <div class="metric-label">\u{1F916} Active Agents</div>
559
- <div class="metric-value" id="active-agents">0</div>
560
- <div class="metric-change positive">\u2191 2 from last hour</div>
561
- </div>
562
- <div class="metric-card">
563
- <div class="metric-label">\u{1F4CB} Total Actions</div>
564
- <div class="metric-value" id="total-actions">0</div>
565
- <div class="metric-change positive">\u2191 12% increase</div>
566
- </div>
567
- <div class="metric-card">
568
- <div class="metric-label">\u{1F41B} Bugs Found</div>
569
- <div class="metric-value" id="bugs-found">0</div>
570
- <div class="metric-change negative">\u2193 3 from yesterday</div>
571
- </div>
572
- <div class="metric-card">
573
- <div class="metric-label">\u26A1 Success Rate</div>
574
- <div class="metric-value" id="success-rate">0%</div>
575
- <div class="metric-change positive">\u2191 5% improvement</div>
889
+ <div class="dashboard-container">
890
+ <header class="dashboard-header">
891
+ <h1 class="dashboard-title">\u{1F916} OpenQA Professional Dashboard</h1>
892
+ <nav class="nav">
893
+ <div class="nav-links">
894
+ <a href="/" class="active">\u{1F4CA} Dashboard</a>
895
+ <a href="/kanban">\u{1F4CB} Kanban</a>
896
+ <a href="/config">\u2699\uFE0F Config</a>
897
+ </div>
898
+ <span id="connection-status" class="connection-status status idle">\u{1F50C} Connecting...</span>
899
+ </nav>
900
+ </header>
901
+
902
+ <!-- Key Metrics -->
903
+ <div class="metrics-grid">
904
+ <div class="metric-card">
905
+ <div class="metric-header">
906
+ <div class="metric-label">\u{1F916} Active Agents</div>
907
+ <div class="metric-icon">\u{1F916}</div>
908
+ </div>
909
+ <div class="metric-value" id="active-agents">0</div>
910
+ <div class="metric-change positive">\u2191 2 from last hour</div>
911
+ </div>
912
+ <div class="metric-card">
913
+ <div class="metric-header">
914
+ <div class="metric-label">\u{1F4CB} Total Actions</div>
915
+ <div class="metric-icon">\u{1F4CB}</div>
916
+ </div>
917
+ <div class="metric-value" id="total-actions">0</div>
918
+ <div class="metric-change positive">\u2191 12% increase</div>
919
+ </div>
920
+ <div class="metric-card">
921
+ <div class="metric-header">
922
+ <div class="metric-label">\u{1F41B} Bugs Found</div>
923
+ <div class="metric-icon">\u{1F41B}</div>
924
+ </div>
925
+ <div class="metric-value" id="bugs-found">0</div>
926
+ <div class="metric-change negative">\u2193 3 from yesterday</div>
927
+ </div>
928
+ <div class="metric-card">
929
+ <div class="metric-header">
930
+ <div class="metric-label">\u26A1 Success Rate</div>
931
+ <div class="metric-icon">\u26A1</div>
932
+ </div>
933
+ <div class="metric-value" id="success-rate">0%</div>
934
+ <div class="metric-change positive">\u2191 5% improvement</div>
935
+ </div>
576
936
  </div>
577
- </div>
578
937
 
579
938
  <!-- Charts and Hierarchy -->
580
- <div class="grid-2">
939
+ <div class="main-grid">
581
940
  <div class="card">
582
941
  <div class="card-header">
583
942
  <h2 class="card-title">\u{1F4C8} Performance Metrics</h2>
584
- </div>
585
- <div class="tabs">
586
- <div class="tab active" onclick="switchTab('performance')">Performance</div>
587
- <div class="tab" onclick="switchTab('activity')">Activity</div>
588
- <div class="tab" onclick="switchTab('errors')">Error Rate</div>
943
+ <div class="tabs">
944
+ <div class="tab active" onclick="switchTab('performance')">Performance</div>
945
+ <div class="tab" onclick="switchTab('activity')">Activity</div>
946
+ <div class="tab" onclick="switchTab('errors')">Error Rate</div>
947
+ </div>
589
948
  </div>
590
949
  <div class="chart-container">
591
950
  <canvas id="performanceChart"></canvas>
@@ -610,36 +969,36 @@ async function startWebServer() {
610
969
  <div class="card">
611
970
  <div class="card-header">
612
971
  <h2 class="card-title">\u{1F916} Agent Details</h2>
613
- </div>
614
- <div class="tabs">
615
- <div class="tab active" onclick="switchAgentTab('active')">Active Agents</div>
616
- <div class="tab" onclick="switchAgentTab('specialists')">Specialists</div>
617
- <div class="tab" onclick="switchAgentTab('performance')">Performance</div>
972
+ <div class="tabs">
973
+ <div class="tab active" onclick="switchAgentTab('active')">Active Agents</div>
974
+ <div class="tab" onclick="switchAgentTab('specialists')">Specialists</div>
975
+ <div class="tab" onclick="switchAgentTab('performance')">Performance</div>
976
+ </div>
618
977
  </div>
619
978
  <div id="active-agents-content" class="tab-content active">
620
979
  <div id="active-agents-list">
621
- <p style="color: #64748b;">Loading agents...</p>
980
+ <p style="color: #9ca3af;">Loading agents...</p>
622
981
  </div>
623
982
  </div>
624
983
  <div id="specialists-content" class="tab-content">
625
984
  <div id="specialists-list">
626
- <p style="color: #64748b;">No specialists active</p>
985
+ <p style="color: #9ca3af;">No specialists active</p>
627
986
  </div>
628
987
  </div>
629
988
  <div id="performance-content" class="tab-content">
630
989
  <div id="performance-metrics">
631
- <p style="color: #64748b;">Performance data loading...</p>
990
+ <p style="color: #9ca3af;">Performance data loading...</p>
632
991
  </div>
633
992
  </div>
634
993
  </div>
635
994
 
636
995
  <!-- Activity and Interventions -->
637
- <div class="grid-2">
996
+ <div class="main-grid">
638
997
  <div class="card">
639
998
  <div class="card-header">
640
999
  <h2 class="card-title">\u26A1 Recent Activity</h2>
641
1000
  </div>
642
- <div id="recent-activities" style="max-height: 400px; overflow-y: auto;">
1001
+ <div class="activity-feed" id="recent-activities">
643
1002
  <div class="activity-item">
644
1003
  <div>\u{1F504} Waiting for agent activity...</div>
645
1004
  <div class="activity-time">System ready</div>
@@ -651,20 +1010,20 @@ async function startWebServer() {
651
1010
  <div class="card-header">
652
1011
  <h2 class="card-title">\u{1F6A8} Human Interventions</h2>
653
1012
  </div>
654
- <div id="interventions-list" style="max-height: 400px; overflow-y: auto;">
655
- <p style="color: #64748b;">No interventions required</p>
1013
+ <div class="activity-feed" id="interventions-list">
1014
+ <p style="color: #9ca3af;">No interventions required</p>
656
1015
  </div>
657
1016
  </div>
658
1017
  </div>
659
1018
 
660
1019
  <!-- Tasks and Issues -->
661
- <div class="grid-2">
1020
+ <div class="main-grid">
662
1021
  <div class="card">
663
1022
  <div class="card-header">
664
1023
  <h2 class="card-title">\u{1F4DD} Current Tasks</h2>
665
1024
  </div>
666
- <div id="current-tasks" style="max-height: 400px; overflow-y: auto;">
667
- <p style="color: #64748b;">No active tasks</p>
1025
+ <div class="activity-feed" id="current-tasks">
1026
+ <p style="color: #9ca3af;">No active tasks</p>
668
1027
  </div>
669
1028
  </div>
670
1029
 
@@ -672,40 +1031,8 @@ async function startWebServer() {
672
1031
  <div class="card-header">
673
1032
  <h2 class="card-title">\u26A0\uFE0F Issues Encountered</h2>
674
1033
  </div>
675
- <div id="issues-list" style="max-height: 400px; overflow-y: auto;">
676
- <p style="color: #64748b;">No issues</p>
677
- </div>
678
- </div>
679
- </div>
680
-
681
- <div class="grid">
682
- <div class="card">
683
- <h2>\u{1F916} Active Agents</h2>
684
- <div id="active-agents-list">
685
- <p style="color: #64748b;">No active agents</p>
686
- </div>
687
- </div>
688
-
689
- <div class="card">
690
- <h2>\u{1F6A8} Human Interventions</h2>
691
- <div id="interventions-list">
692
- <p style="color: #64748b;">No interventions required</p>
693
- </div>
694
- </div>
695
- </div>
696
-
697
- <div class="grid">
698
- <div class="card">
699
- <h2>\u{1F4DD} Current Tasks</h2>
700
- <div id="current-tasks">
701
- <p style="color: #64748b;">No active tasks</p>
702
- </div>
703
- </div>
704
-
705
- <div class="card">
706
- <h2>\u26A0\uFE0F Issues Encountered</h2>
707
- <div id="issues-list">
708
- <p style="color: #64748b;">No issues</p>
1034
+ <div class="activity-feed" id="issues-list">
1035
+ <p style="color: #9ca3af;">No issues</p>
709
1036
  </div>
710
1037
  </div>
711
1038
  </div>
@@ -1334,228 +1661,720 @@ async function startWebServer() {
1334
1661
  `);
1335
1662
  });
1336
1663
  app.get("/config", (req, res) => {
1664
+ const cfg2 = config.getConfigSync();
1337
1665
  res.send(`
1338
- <!DOCTYPE html>
1339
- <html>
1340
- <head>
1341
- <title>OpenQA - Configuration</title>
1342
- <style>
1343
- body { font-family: system-ui; max-width: 800px; margin: 40px auto; padding: 20px; background: #0f172a; color: #e2e8f0; }
1344
- h1 { color: #38bdf8; }
1345
- nav { margin: 20px 0; }
1346
- nav a { color: #38bdf8; text-decoration: none; margin-right: 20px; }
1347
- nav a:hover { text-decoration: underline; }
1348
- .section { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 20px; margin: 20px 0; }
1349
- .section h2 { margin-top: 0; color: #38bdf8; font-size: 18px; }
1350
- .config-item { margin: 15px 0; }
1351
- .config-item label { display: block; margin-bottom: 5px; color: #94a3b8; font-size: 14px; }
1352
- .config-item input, .config-item select {
1353
- background: #334155;
1354
- border: 1px solid #475569;
1355
- color: #e2e8f0;
1356
- padding: 8px 12px;
1357
- border-radius: 4px;
1358
- font-family: monospace;
1359
- font-size: 14px;
1360
- width: 100%;
1361
- max-width: 400px;
1362
- }
1363
- .config-item input:focus, .config-item select:focus {
1364
- outline: none;
1365
- border-color: #38bdf8;
1366
- box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.1);
1367
- }
1368
- .btn {
1369
- background: #38bdf8;
1370
- color: white;
1371
- border: none;
1372
- padding: 10px 20px;
1373
- border-radius: 6px;
1374
- cursor: pointer;
1375
- font-size: 14px;
1376
- margin-right: 10px;
1377
- }
1378
- .btn:hover { background: #0ea5e9; }
1379
- .btn-secondary { background: #64748b; }
1380
- .btn-secondary:hover { background: #475569; }
1381
- .success { color: #10b981; margin-left: 10px; }
1382
- .error { color: #ef4444; margin-left: 10px; }
1383
- code { background: #334155; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
1384
- .checkbox { margin-right: 8px; }
1385
- </style>
1386
- </head>
1387
- <body>
1388
- <h1>\u2699\uFE0F Configuration</h1>
1389
- <nav>
1390
- <a href="/">Dashboard</a>
1391
- <a href="/kanban">Kanban</a>
1392
- <a href="/config">Config</a>
1393
- </nav>
1394
-
1395
- <div class="section">
1396
- <h2>SaaS Target</h2>
1397
- <form id="configForm">
1398
- <div class="config-item">
1399
- <label>URL</label>
1400
- <input type="url" id="saas_url" name="saas.url" value="${cfg.saas.url || ""}" placeholder="https://your-app.com">
1401
- </div>
1402
- <div class="config-item">
1403
- <label>Auth Type</label>
1404
- <select id="saas_authType" name="saas.authType">
1405
- <option value="none" ${cfg.saas.authType === "none" ? "selected" : ""}>None</option>
1406
- <option value="basic" ${cfg.saas.authType === "basic" ? "selected" : ""}>Basic Auth</option>
1407
- <option value="bearer" ${cfg.saas.authType === "bearer" ? "selected" : ""}>Bearer Token</option>
1408
- <option value="session" ${cfg.saas.authType === "session" ? "selected" : ""}>Session</option>
1409
- </select>
1666
+ <!DOCTYPE html>
1667
+ <html lang="en">
1668
+ <head>
1669
+ <meta charset="UTF-8">
1670
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1671
+ <title>OpenQA \u2014 Configuration</title>
1672
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1673
+ <link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@300;400;500&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet">
1674
+ <style>
1675
+ :root {
1676
+ --bg: #080b10;
1677
+ --surface: #0d1117;
1678
+ --panel: #111720;
1679
+ --border: rgba(255,255,255,0.06);
1680
+ --border-hi: rgba(255,255,255,0.12);
1681
+ --accent: #f97316;
1682
+ --accent-lo: rgba(249,115,22,0.08);
1683
+ --accent-md: rgba(249,115,22,0.18);
1684
+ --green: #22c55e;
1685
+ --green-lo: rgba(34,197,94,0.08);
1686
+ --red: #ef4444;
1687
+ --red-lo: rgba(239,68,68,0.08);
1688
+ --amber: #f59e0b;
1689
+ --blue: #38bdf8;
1690
+ --text-1: #f1f5f9;
1691
+ --text-2: #8b98a8;
1692
+ --text-3: #4b5563;
1693
+ --mono: 'DM Mono', monospace;
1694
+ --sans: 'Syne', sans-serif;
1695
+ --radius: 10px;
1696
+ --radius-lg: 16px;
1697
+ }
1698
+
1699
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
1700
+
1701
+ body {
1702
+ font-family: var(--sans);
1703
+ background: var(--bg);
1704
+ color: var(--text-1);
1705
+ min-height: 100vh;
1706
+ overflow-x: hidden;
1707
+ }
1708
+
1709
+ /* \u2500\u2500 Layout \u2500\u2500 */
1710
+ .shell {
1711
+ display: grid;
1712
+ grid-template-columns: 220px 1fr;
1713
+ grid-template-rows: 1fr;
1714
+ min-height: 100vh;
1715
+ }
1716
+
1717
+ /* \u2500\u2500 Sidebar \u2500\u2500 */
1718
+ aside {
1719
+ background: var(--surface);
1720
+ border-right: 1px solid var(--border);
1721
+ display: flex;
1722
+ flex-direction: column;
1723
+ padding: 28px 0;
1724
+ position: sticky;
1725
+ top: 0;
1726
+ height: 100vh;
1727
+ }
1728
+
1729
+ .logo {
1730
+ display: flex;
1731
+ align-items: center;
1732
+ gap: 10px;
1733
+ padding: 0 24px 32px;
1734
+ border-bottom: 1px solid var(--border);
1735
+ margin-bottom: 12px;
1736
+ }
1737
+
1738
+ .logo-mark {
1739
+ width: 34px;
1740
+ height: 34px;
1741
+ background: var(--accent);
1742
+ border-radius: 8px;
1743
+ display: grid;
1744
+ place-items: center;
1745
+ font-size: 16px;
1746
+ flex-shrink: 0;
1747
+ }
1748
+
1749
+ .logo-name {
1750
+ font-family: var(--sans);
1751
+ font-weight: 800;
1752
+ font-size: 18px;
1753
+ letter-spacing: -0.5px;
1754
+ color: var(--text-1);
1755
+ }
1756
+
1757
+ .logo-version {
1758
+ font-family: var(--mono);
1759
+ font-size: 10px;
1760
+ color: var(--text-3);
1761
+ letter-spacing: 0.5px;
1762
+ margin-top: 2px;
1763
+ }
1764
+
1765
+ .nav-section {
1766
+ padding: 8px 12px;
1767
+ flex: 1;
1768
+ }
1769
+
1770
+ .nav-label {
1771
+ font-family: var(--mono);
1772
+ font-size: 10px;
1773
+ color: var(--text-3);
1774
+ letter-spacing: 1.5px;
1775
+ text-transform: uppercase;
1776
+ padding: 0 12px;
1777
+ margin: 16px 0 6px;
1778
+ }
1779
+
1780
+ .nav-item {
1781
+ display: flex;
1782
+ align-items: center;
1783
+ gap: 10px;
1784
+ padding: 9px 12px;
1785
+ border-radius: var(--radius);
1786
+ color: var(--text-2);
1787
+ text-decoration: none;
1788
+ font-size: 14px;
1789
+ font-weight: 600;
1790
+ transition: all 0.15s ease;
1791
+ cursor: pointer;
1792
+ }
1793
+
1794
+ .nav-item:hover { color: var(--text-1); background: var(--panel); }
1795
+ .nav-item.active { color: var(--accent); background: var(--accent-lo); }
1796
+ .nav-item .icon { font-size: 15px; width: 20px; text-align: center; }
1797
+
1798
+ .sidebar-footer {
1799
+ padding: 16px 24px;
1800
+ border-top: 1px solid var(--border);
1801
+ }
1802
+
1803
+ .status-pill {
1804
+ display: flex;
1805
+ align-items: center;
1806
+ gap: 8px;
1807
+ font-family: var(--mono);
1808
+ font-size: 11px;
1809
+ color: var(--text-2);
1810
+ }
1811
+
1812
+ .dot {
1813
+ width: 7px;
1814
+ height: 7px;
1815
+ border-radius: 50%;
1816
+ background: var(--green);
1817
+ box-shadow: 0 0 8px var(--green);
1818
+ }
1819
+
1820
+ /* \u2500\u2500 Main \u2500\u2500 */
1821
+ main {
1822
+ display: flex;
1823
+ flex-direction: column;
1824
+ min-height: 100vh;
1825
+ overflow-y: auto;
1826
+ }
1827
+
1828
+ .topbar {
1829
+ display: flex;
1830
+ align-items: center;
1831
+ justify-content: space-between;
1832
+ padding: 20px 32px;
1833
+ border-bottom: 1px solid var(--border);
1834
+ background: var(--surface);
1835
+ position: sticky;
1836
+ top: 0;
1837
+ z-index: 10;
1838
+ }
1839
+
1840
+ .page-title {
1841
+ font-size: 15px;
1842
+ font-weight: 700;
1843
+ color: var(--text-1);
1844
+ letter-spacing: -0.2px;
1845
+ }
1846
+
1847
+ .page-breadcrumb {
1848
+ font-family: var(--mono);
1849
+ font-size: 11px;
1850
+ color: var(--text-3);
1851
+ margin-top: 2px;
1852
+ }
1853
+
1854
+ .topbar-actions {
1855
+ display: flex;
1856
+ align-items: center;
1857
+ gap: 12px;
1858
+ }
1859
+
1860
+ .btn-sm {
1861
+ font-family: var(--sans);
1862
+ font-weight: 700;
1863
+ font-size: 12px;
1864
+ padding: 8px 16px;
1865
+ border-radius: 8px;
1866
+ border: none;
1867
+ cursor: pointer;
1868
+ transition: all 0.15s ease;
1869
+ letter-spacing: 0.2px;
1870
+ }
1871
+
1872
+ .btn-ghost {
1873
+ background: var(--panel);
1874
+ color: var(--text-2);
1875
+ border: 1px solid var(--border);
1876
+ }
1877
+ .btn-ghost:hover { border-color: var(--border-hi); color: var(--text-1); }
1878
+
1879
+ .btn-primary {
1880
+ background: var(--accent);
1881
+ color: #fff;
1882
+ }
1883
+ .btn-primary:hover { background: #ea580c; box-shadow: 0 0 20px rgba(249,115,22,0.35); }
1884
+
1885
+ /* \u2500\u2500 Content \u2500\u2500 */
1886
+ .content {
1887
+ padding: 28px 32px;
1888
+ display: flex;
1889
+ flex-direction: column;
1890
+ gap: 24px;
1891
+ }
1892
+
1893
+ /* \u2500\u2500 Panel \u2500\u2500 */
1894
+ .panel {
1895
+ background: var(--panel);
1896
+ border: 1px solid var(--border);
1897
+ border-radius: var(--radius-lg);
1898
+ overflow: hidden;
1899
+ }
1900
+
1901
+ .panel-head {
1902
+ display: flex;
1903
+ align-items: center;
1904
+ justify-content: space-between;
1905
+ padding: 18px 24px;
1906
+ border-bottom: 1px solid var(--border);
1907
+ }
1908
+
1909
+ .panel-title {
1910
+ font-size: 13px;
1911
+ font-weight: 700;
1912
+ color: var(--text-1);
1913
+ letter-spacing: -0.1px;
1914
+ }
1915
+
1916
+ .panel-body {
1917
+ padding: 24px;
1918
+ }
1919
+
1920
+ /* \u2500\u2500 Form \u2500\u2500 */
1921
+ .form-grid {
1922
+ display: grid;
1923
+ gap: 20px;
1924
+ }
1925
+
1926
+ .form-section {
1927
+ display: flex;
1928
+ flex-direction: column;
1929
+ gap: 16px;
1930
+ }
1931
+
1932
+ .form-section-title {
1933
+ font-size: 12px;
1934
+ font-weight: 700;
1935
+ color: var(--text-2);
1936
+ text-transform: uppercase;
1937
+ letter-spacing: 1px;
1938
+ margin-bottom: 8px;
1939
+ padding-bottom: 8px;
1940
+ border-bottom: 1px solid var(--border);
1941
+ }
1942
+
1943
+ .form-row {
1944
+ display: grid;
1945
+ grid-template-columns: 1fr 1fr;
1946
+ gap: 16px;
1947
+ }
1948
+
1949
+ .form-field {
1950
+ display: flex;
1951
+ flex-direction: column;
1952
+ gap: 6px;
1953
+ }
1954
+
1955
+ .form-field.full {
1956
+ grid-column: 1 / -1;
1957
+ }
1958
+
1959
+ label {
1960
+ font-family: var(--mono);
1961
+ font-size: 11px;
1962
+ color: var(--text-3);
1963
+ text-transform: uppercase;
1964
+ letter-spacing: 0.5px;
1965
+ font-weight: 500;
1966
+ }
1967
+
1968
+ input, select {
1969
+ background: var(--surface);
1970
+ border: 1px solid var(--border);
1971
+ color: var(--text-1);
1972
+ padding: 10px 14px;
1973
+ border-radius: var(--radius);
1974
+ font-family: var(--mono);
1975
+ font-size: 12px;
1976
+ transition: all 0.15s ease;
1977
+ }
1978
+
1979
+ input:focus, select:focus {
1980
+ outline: none;
1981
+ border-color: var(--accent);
1982
+ box-shadow: 0 0 0 1px var(--accent);
1983
+ }
1984
+
1985
+ input[type="checkbox"] {
1986
+ width: 16px;
1987
+ height: 16px;
1988
+ margin: 0;
1989
+ cursor: pointer;
1990
+ }
1991
+
1992
+ .checkbox-label {
1993
+ display: flex;
1994
+ align-items: center;
1995
+ gap: 8px;
1996
+ cursor: pointer;
1997
+ font-size: 12px;
1998
+ color: var(--text-2);
1999
+ text-transform: none;
2000
+ letter-spacing: normal;
2001
+ }
2002
+
2003
+ /* \u2500\u2500 Actions \u2500\u2500 */
2004
+ .actions {
2005
+ display: flex;
2006
+ gap: 12px;
2007
+ padding: 20px 24px;
2008
+ background: var(--surface);
2009
+ border-top: 1px solid var(--border);
2010
+ }
2011
+
2012
+ .message {
2013
+ font-family: var(--mono);
2014
+ font-size: 11px;
2015
+ padding: 8px 12px;
2016
+ border-radius: var(--radius);
2017
+ margin-left: auto;
2018
+ }
2019
+
2020
+ .message.success {
2021
+ background: var(--green-lo);
2022
+ color: var(--green);
2023
+ border: 1px solid rgba(34,197,94,0.2);
2024
+ }
2025
+
2026
+ .message.error {
2027
+ background: var(--red-lo);
2028
+ color: var(--red);
2029
+ border: 1px solid rgba(239,68,68,0.2);
2030
+ }
2031
+
2032
+ /* \u2500\u2500 Code Block \u2500\u2500 */
2033
+ .code-block {
2034
+ background: var(--surface);
2035
+ border: 1px solid var(--border);
2036
+ border-radius: var(--radius);
2037
+ padding: 20px;
2038
+ font-family: var(--mono);
2039
+ font-size: 11px;
2040
+ color: var(--text-2);
2041
+ overflow-x: auto;
2042
+ }
2043
+
2044
+ .code-block pre {
2045
+ margin: 0;
2046
+ line-height: 1.6;
2047
+ }
2048
+
2049
+ /* \u2500\u2500 Responsive \u2500\u2500 */
2050
+ @media (max-width: 900px) {
2051
+ .shell { grid-template-columns: 1fr; }
2052
+ aside { display: none; }
2053
+ .form-row { grid-template-columns: 1fr; }
2054
+ }
2055
+ </style>
2056
+ </head>
2057
+ <body>
2058
+
2059
+ <div class="shell">
2060
+
2061
+ <!-- \u2500\u2500 Sidebar \u2500\u2500 -->
2062
+ <aside>
2063
+ <div class="logo">
2064
+ <div class="logo-mark">\u2699</div>
2065
+ <div>
2066
+ <div class="logo-name">OpenQA</div>
2067
+ <div class="logo-version">v2.1.0 \xB7 OSS</div>
2068
+ </div>
2069
+ </div>
2070
+
2071
+ <div class="nav-section">
2072
+ <div class="nav-label">Overview</div>
2073
+ <a class="nav-item" href="/">
2074
+ <span class="icon">\u25A6</span> Dashboard
2075
+ </a>
2076
+ <a class="nav-item" href="/kanban">
2077
+ <span class="icon">\u229E</span> Kanban
2078
+ </a>
2079
+
2080
+ <div class="nav-label">System</div>
2081
+ <a class="nav-item active" href="/config">
2082
+ <span class="icon">\u2699</span> Configuration
2083
+ </a>
2084
+ </div>
2085
+
2086
+ <div class="sidebar-footer">
2087
+ <div class="status-pill">
2088
+ <div class="dot"></div>
2089
+ <span>System Ready</span>
2090
+ </div>
2091
+ </div>
2092
+ </aside>
2093
+
2094
+ <!-- \u2500\u2500 Main \u2500\u2500 -->
2095
+ <main>
2096
+
2097
+ <!-- Topbar -->
2098
+ <div class="topbar">
2099
+ <div>
2100
+ <div class="page-title">Configuration</div>
2101
+ <div class="page-breadcrumb">openqa / system / settings</div>
2102
+ </div>
2103
+ <div class="topbar-actions">
2104
+ <button class="btn-sm btn-ghost">Export Config</button>
2105
+ <button class="btn-sm btn-ghost">Import Config</button>
2106
+ <button class="btn-sm btn-primary" onclick="saveAllConfig()">Save All</button>
2107
+ </div>
2108
+ </div>
2109
+
2110
+ <!-- Content -->
2111
+ <div class="content">
2112
+
2113
+ <!-- SaaS Configuration -->
2114
+ <div class="panel">
2115
+ <div class="panel-head">
2116
+ <span class="panel-title">\u{1F310} SaaS Target Configuration</span>
2117
+ </div>
2118
+ <div class="panel-body">
2119
+ <form class="form-grid" id="saas-form">
2120
+ <div class="form-section">
2121
+ <div class="form-section-title">Target Application</div>
2122
+ <div class="form-field full">
2123
+ <label>Application URL</label>
2124
+ <input type="url" id="saas_url" name="saas.url" value="${cfg2.saas.url || ""}" placeholder="https://your-app.com">
1410
2125
  </div>
1411
- <div class="config-item">
1412
- <label>Username (for Basic Auth)</label>
1413
- <input type="text" id="saas_username" name="saas.username" value="${cfg.saas.username || ""}" placeholder="username">
2126
+ <div class="form-row">
2127
+ <div class="form-field">
2128
+ <label>Authentication Type</label>
2129
+ <select id="saas_authType" name="saas.authType">
2130
+ <option value="none" ${cfg2.saas.authType === "none" ? "selected" : ""}>None</option>
2131
+ <option value="basic" ${cfg2.saas.authType === "basic" ? "selected" : ""}>Basic Auth</option>
2132
+ <option value="bearer" ${cfg2.saas.authType === "bearer" ? "selected" : ""}>Bearer Token</option>
2133
+ <option value="session" ${cfg2.saas.authType === "session" ? "selected" : ""}>Session</option>
2134
+ </select>
2135
+ </div>
2136
+ <div class="form-field">
2137
+ <label>Timeout (seconds)</label>
2138
+ <input type="number" id="saas_timeout" name="saas.timeout" value="30" min="5" max="300">
2139
+ </div>
1414
2140
  </div>
1415
- <div class="config-item">
1416
- <label>Password (for Basic Auth)</label>
1417
- <input type="password" id="saas_password" name="saas.password" value="${cfg.saas.password || ""}" placeholder="password">
2141
+ <div class="form-row">
2142
+ <div class="form-field">
2143
+ <label>Username</label>
2144
+ <input type="text" id="saas_username" name="saas.username" value="${cfg2.saas.username || ""}" placeholder="username">
2145
+ </div>
2146
+ <div class="form-field">
2147
+ <label>Password</label>
2148
+ <input type="password" id="saas_password" name="saas.password" value="${cfg2.saas.password || ""}" placeholder="password">
2149
+ </div>
1418
2150
  </div>
1419
- </form>
1420
- </div>
2151
+ </div>
2152
+ </form>
2153
+ </div>
2154
+ </div>
1421
2155
 
1422
- <div class="section">
1423
- <h2>LLM Configuration</h2>
1424
- <form id="configForm">
1425
- <div class="config-item">
1426
- <label>Provider</label>
1427
- <select id="llm_provider" name="llm.provider">
1428
- <option value="openai" ${cfg.llm.provider === "openai" ? "selected" : ""}>OpenAI</option>
1429
- <option value="anthropic" ${cfg.llm.provider === "anthropic" ? "selected" : ""}>Anthropic</option>
1430
- <option value="ollama" ${cfg.llm.provider === "ollama" ? "selected" : ""}>Ollama</option>
1431
- </select>
1432
- </div>
1433
- <div class="config-item">
1434
- <label>Model</label>
1435
- <input type="text" id="llm_model" name="llm.model" value="${cfg.llm.model || ""}" placeholder="gpt-4, claude-3-sonnet, etc.">
2156
+ <!-- LLM Configuration -->
2157
+ <div class="panel">
2158
+ <div class="panel-head">
2159
+ <span class="panel-title">\u{1F916} LLM Configuration</span>
2160
+ </div>
2161
+ <div class="panel-body">
2162
+ <form class="form-grid" id="llm-form">
2163
+ <div class="form-section">
2164
+ <div class="form-section-title">Language Model Provider</div>
2165
+ <div class="form-row">
2166
+ <div class="form-field">
2167
+ <label>Provider</label>
2168
+ <select id="llm_provider" name="llm.provider">
2169
+ <option value="openai" ${cfg2.llm.provider === "openai" ? "selected" : ""}>OpenAI</option>
2170
+ <option value="anthropic" ${cfg2.llm.provider === "anthropic" ? "selected" : ""}>Anthropic</option>
2171
+ <option value="ollama" ${cfg2.llm.provider === "ollama" ? "selected" : ""}>Ollama</option>
2172
+ </select>
2173
+ </div>
2174
+ <div class="form-field">
2175
+ <label>Model</label>
2176
+ <input type="text" id="llm_model" name="llm.model" value="${cfg2.llm.model || ""}" placeholder="gpt-4, claude-3-sonnet, etc.">
2177
+ </div>
1436
2178
  </div>
1437
- <div class="config-item">
2179
+ <div class="form-field full">
1438
2180
  <label>API Key</label>
1439
- <input type="password" id="llm_apiKey" name="llm.apiKey" value="${cfg.llm.apiKey || ""}" placeholder="Your API key">
2181
+ <input type="password" id="llm_apiKey" name="llm.apiKey" value="${cfg2.llm.apiKey || ""}" placeholder="Your API key">
1440
2182
  </div>
1441
- <div class="config-item">
2183
+ <div class="form-field full">
1442
2184
  <label>Base URL (for Ollama)</label>
1443
- <input type="url" id="llm_baseUrl" name="llm.baseUrl" value="${cfg.llm.baseUrl || ""}" placeholder="http://localhost:11434">
2185
+ <input type="url" id="llm_baseUrl" name="llm.baseUrl" value="${cfg2.llm.baseUrl || ""}" placeholder="http://localhost:11434">
1444
2186
  </div>
1445
- </form>
1446
- </div>
2187
+ </div>
2188
+ </form>
2189
+ </div>
2190
+ </div>
1447
2191
 
1448
- <div class="section">
1449
- <h2>Agent Settings</h2>
1450
- <form id="configForm">
1451
- <div class="config-item">
1452
- <label>
1453
- <input type="checkbox" id="agent_autoStart" name="agent.autoStart" class="checkbox" ${cfg.agent.autoStart ? "checked" : ""}>
1454
- Auto-start
2192
+ <!-- Agent Configuration -->
2193
+ <div class="panel">
2194
+ <div class="panel-head">
2195
+ <span class="panel-title">\u{1F3AF} Agent Settings</span>
2196
+ </div>
2197
+ <div class="panel-body">
2198
+ <form class="form-grid" id="agent-form">
2199
+ <div class="form-section">
2200
+ <div class="form-section-title">Agent Behavior</div>
2201
+ <div class="form-field">
2202
+ <label class="checkbox-label">
2203
+ <input type="checkbox" id="agent_autoStart" name="agent.autoStart" ${cfg2.agent.autoStart ? "checked" : ""}>
2204
+ Auto-start on launch
1455
2205
  </label>
1456
2206
  </div>
1457
- <div class="config-item">
1458
- <label>Interval (ms)</label>
1459
- <input type="number" id="agent_intervalMs" name="agent.intervalMs" value="${cfg.agent.intervalMs}" min="60000">
1460
- </div>
1461
- <div class="config-item">
1462
- <label>Max Iterations</label>
1463
- <input type="number" id="agent_maxIterations" name="agent.maxIterations" value="${cfg.agent.maxIterations}" min="1" max="100">
2207
+ <div class="form-row">
2208
+ <div class="form-field">
2209
+ <label>Check Interval (ms)</label>
2210
+ <input type="number" id="agent_intervalMs" name="agent.intervalMs" value="${cfg2.agent.intervalMs}" min="60000" step="60000">
2211
+ </div>
2212
+ <div class="form-field">
2213
+ <label>Max Iterations</label>
2214
+ <input type="number" id="agent_maxIterations" name="agent.maxIterations" value="${cfg2.agent.maxIterations}" min="1" max="100">
2215
+ </div>
1464
2216
  </div>
1465
- </form>
1466
- </div>
2217
+ </div>
2218
+ </form>
2219
+ </div>
2220
+ </div>
1467
2221
 
1468
- <div class="section">
1469
- <h2>Actions</h2>
1470
- <button type="button" class="btn" onclick="saveConfig()">Save Configuration</button>
1471
- <button type="button" class="btn btn-secondary" onclick="resetConfig()">Reset to Defaults</button>
1472
- <span id="message"></span>
1473
- </div>
2222
+ <!-- Environment Variables -->
2223
+ <div class="panel">
2224
+ <div class="panel-head">
2225
+ <span class="panel-title">\u{1F4DD} Environment Variables</span>
2226
+ </div>
2227
+ <div class="panel-body">
2228
+ <p style="color: var(--text-3); font-size: 12px; margin-bottom: 16px;">
2229
+ You can also set these environment variables before starting OpenQA:
2230
+ </p>
2231
+ <div class="code-block">
2232
+ <pre>export SAAS_URL="https://your-app.com"
2233
+ export SAAS_AUTH_TYPE="basic"
2234
+ export SAAS_USERNAME="admin"
2235
+ export SAAS_PASSWORD="secret"
2236
+
2237
+ export LLM_PROVIDER="openai"
2238
+ export OPENAI_API_KEY="your-openai-key"
2239
+ export LLM_MODEL="gpt-4"
1474
2240
 
1475
- <div class="section">
1476
- <h2>Environment Variables</h2>
1477
- <p>You can also set these environment variables before starting OpenQA:</p>
1478
- <pre style="background: #334155; padding: 15px; border-radius: 6px; overflow-x: auto;"><code>export SAAS_URL="https://your-app.com"
1479
2241
  export AGENT_AUTO_START=true
1480
- export LLM_PROVIDER=openai
1481
- export OPENAI_API_KEY="your-key"
2242
+ export AGENT_INTERVAL_MS=3600000
2243
+ export AGENT_MAX_ITERATIONS=20
1482
2244
 
1483
- openqa start</code></pre>
2245
+ openqa start</pre>
1484
2246
  </div>
2247
+ </div>
2248
+ </div>
1485
2249
 
1486
- <script>
1487
- async function saveConfig() {
1488
- const form = document.getElementById('configForm');
1489
- const formData = new FormData(form);
1490
- const config = {};
1491
-
1492
- for (let [key, value] of formData.entries()) {
1493
- if (value === '') continue;
1494
-
1495
- // Handle nested keys like "saas.url"
1496
- const keys = key.split('.');
1497
- let obj = config;
1498
- for (let i = 0; i < keys.length - 1; i++) {
1499
- if (!obj[keys[i]]) obj[keys[i]] = {};
1500
- obj = obj[keys[i]];
1501
- }
1502
-
1503
- // Convert checkbox values to boolean
1504
- if (key.includes('autoStart')) {
1505
- obj[keys[keys.length - 1]] = value === 'on';
1506
- } else if (key.includes('intervalMs') || key.includes('maxIterations')) {
1507
- obj[keys[keys.length - 1]] = parseInt(value);
1508
- } else {
1509
- obj[keys[keys.length - 1]] = value;
1510
- }
1511
- }
1512
-
1513
- try {
1514
- const response = await fetch('/api/config', {
1515
- method: 'POST',
1516
- headers: { 'Content-Type': 'application/json' },
1517
- body: JSON.stringify(config)
1518
- });
1519
-
1520
- const result = await response.json();
1521
- if (result.success) {
1522
- showMessage('Configuration saved successfully!', 'success');
1523
- setTimeout(() => location.reload(), 1500);
1524
- } else {
1525
- showMessage('Failed to save configuration', 'error');
1526
- }
1527
- } catch (error) {
1528
- showMessage('Error: ' + error.message, 'error');
1529
- }
1530
- }
1531
-
1532
- async function resetConfig() {
1533
- if (confirm('Are you sure you want to reset all configuration to defaults?')) {
1534
- try {
1535
- const response = await fetch('/api/config/reset', { method: 'POST' });
1536
- const result = await response.json();
1537
- if (result.success) {
1538
- showMessage('Configuration reset to defaults', 'success');
1539
- setTimeout(() => location.reload(), 1500);
1540
- }
1541
- } catch (error) {
1542
- showMessage('Error: ' + error.message, 'error');
1543
- }
1544
- }
1545
- }
1546
-
1547
- function showMessage(text, type) {
1548
- const messageEl = document.getElementById('message');
1549
- messageEl.textContent = text;
1550
- messageEl.className = type;
1551
- setTimeout(() => {
1552
- messageEl.textContent = '';
1553
- messageEl.className = '';
1554
- }, 3000);
1555
- }
1556
- </script>
1557
- </body>
1558
- </html>
2250
+ <!-- Actions -->
2251
+ <div class="actions">
2252
+ <button class="btn-sm btn-ghost" onclick="testConnection()">Test Connection</button>
2253
+ <button class="btn-sm btn-ghost" onclick="exportConfig()">Export Config</button>
2254
+ <button class="btn-sm btn-ghost" onclick="resetConfig()">Reset to Defaults</button>
2255
+ <div id="message"></div>
2256
+ </div>
2257
+
2258
+ </div><!-- /content -->
2259
+ </main>
2260
+ </div><!-- /shell -->
2261
+
2262
+ <script>
2263
+ async function saveAllConfig() {
2264
+ const forms = ['saas-form', 'llm-form', 'agent-form'];
2265
+ const config = {};
2266
+
2267
+ for (const formId of forms) {
2268
+ const form = document.getElementById(formId);
2269
+ const formData = new FormData(form);
2270
+
2271
+ for (let [key, value] of formData.entries()) {
2272
+ if (value === '') continue;
2273
+
2274
+ // Handle nested keys like "saas.url"
2275
+ const keys = key.split('.');
2276
+ let obj = config;
2277
+ for (let i = 0; i < keys.length - 1; i++) {
2278
+ if (!obj[keys[i]]) obj[keys[i]] = {};
2279
+ obj = obj[keys[i]];
2280
+ }
2281
+
2282
+ // Convert checkbox values to boolean
2283
+ if (key.includes('autoStart')) {
2284
+ obj[keys[keys.length - 1]] = value === 'on';
2285
+ } else if (key.includes('intervalMs') || key.includes('maxIterations') || key.includes('timeout')) {
2286
+ obj[keys[keys.length - 1]] = parseInt(value);
2287
+ } else {
2288
+ obj[keys[keys.length - 1]] = value;
2289
+ }
2290
+ }
2291
+ }
2292
+
2293
+ try {
2294
+ const response = await fetch('/api/config', {
2295
+ method: 'POST',
2296
+ headers: { 'Content-Type': 'application/json' },
2297
+ body: JSON.stringify(config)
2298
+ });
2299
+
2300
+ if (response.ok) {
2301
+ showMessage('Configuration saved successfully!', 'success');
2302
+ } else {
2303
+ showMessage('Failed to save configuration', 'error');
2304
+ }
2305
+ } catch (error) {
2306
+ showMessage('Error: ' + error.message, 'error');
2307
+ }
2308
+ }
2309
+
2310
+ async function testConnection() {
2311
+ showMessage('Testing connection...', 'success');
2312
+
2313
+ try {
2314
+ const response = await fetch('/api/test-connection', { method: 'POST' });
2315
+
2316
+ if (response.ok) {
2317
+ showMessage('Connection successful!', 'success');
2318
+ } else {
2319
+ showMessage('Connection failed', 'error');
2320
+ }
2321
+ } catch (error) {
2322
+ showMessage('Connection error: ' + error.message, 'error');
2323
+ }
2324
+ }
2325
+
2326
+ async function exportConfig() {
2327
+ try {
2328
+ const response = await fetch('/api/config');
2329
+ const config = await response.json();
2330
+
2331
+ const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
2332
+ const url = URL.createObjectURL(blob);
2333
+ const a = document.createElement('a');
2334
+ a.href = url;
2335
+ a.download = 'openqa-config.json';
2336
+ a.click();
2337
+ URL.revokeObjectURL(url);
2338
+
2339
+ showMessage('Configuration exported', 'success');
2340
+ } catch (error) {
2341
+ showMessage('Export failed: ' + error.message, 'error');
2342
+ }
2343
+ }
2344
+
2345
+ async function resetConfig() {
2346
+ if (confirm('Are you sure you want to reset all configuration to defaults? This cannot be undone.')) {
2347
+ try {
2348
+ const response = await fetch('/api/config/reset', { method: 'POST' });
2349
+
2350
+ if (response.ok) {
2351
+ location.reload();
2352
+ } else {
2353
+ showMessage('Failed to reset configuration', 'error');
2354
+ }
2355
+ } catch (error) {
2356
+ showMessage('Error: ' + error.message, 'error');
2357
+ }
2358
+ }
2359
+ }
2360
+
2361
+ function showMessage(msg, type) {
2362
+ const el = document.getElementById('message');
2363
+ el.textContent = msg;
2364
+ el.className = 'message ' + type;
2365
+ setTimeout(() => { el.textContent = ''; el.className = ''; }, 5000);
2366
+ }
2367
+
2368
+ // Auto-save on field change
2369
+ document.querySelectorAll('input, select').forEach(field => {
2370
+ field.addEventListener('change', () => {
2371
+ showMessage('Changes made - click "Save All" to apply', 'success');
2372
+ });
2373
+ });
2374
+ </script>
2375
+
2376
+ </body>
2377
+ </html>
1559
2378
  `);
1560
2379
  });
1561
2380
  const server = app.listen(cfg.web.port, cfg.web.host, () => {