@openqa/cli 2.1.1 → 2.1.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/README.md +26 -0
- package/dist/agent/index-v2.js +244 -37
- package/dist/agent/index-v2.js.map +1 -1
- package/dist/agent/index.js +24 -2
- package/dist/agent/index.js.map +1 -1
- package/dist/cli/daemon.js +960 -565
- package/dist/cli/env.html.js +717 -529
- package/dist/cli/index.js +709 -521
- package/dist/cli/server.js +709 -521
- package/package.json +1 -1
package/dist/cli/server.js
CHANGED
|
@@ -5588,672 +5588,860 @@ function getEnvHTML() {
|
|
|
5588
5588
|
<head>
|
|
5589
5589
|
<meta charset="UTF-8">
|
|
5590
5590
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5591
|
-
<title>
|
|
5591
|
+
<title>OpenQA \u2014 Environment</title>
|
|
5592
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
5593
|
+
<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">
|
|
5592
5594
|
<style>
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
+
:root {
|
|
5596
|
+
--bg: #080b10;
|
|
5597
|
+
--surface: #0d1117;
|
|
5598
|
+
--panel: #111720;
|
|
5599
|
+
--border: rgba(255,255,255,0.06);
|
|
5600
|
+
--border-hi: rgba(255,255,255,0.12);
|
|
5601
|
+
--accent: #f97316;
|
|
5602
|
+
--accent-lo: rgba(249,115,22,0.08);
|
|
5603
|
+
--accent-md: rgba(249,115,22,0.18);
|
|
5604
|
+
--green: #22c55e;
|
|
5605
|
+
--green-lo: rgba(34,197,94,0.08);
|
|
5606
|
+
--red: #ef4444;
|
|
5607
|
+
--red-lo: rgba(239,68,68,0.08);
|
|
5608
|
+
--amber: #f59e0b;
|
|
5609
|
+
--amber-lo: rgba(245,158,11,0.08);
|
|
5610
|
+
--blue: #38bdf8;
|
|
5611
|
+
--blue-lo: rgba(56,189,248,0.08);
|
|
5612
|
+
--text-1: #f1f5f9;
|
|
5613
|
+
--text-2: #8b98a8;
|
|
5614
|
+
--text-3: #4b5563;
|
|
5615
|
+
--mono: 'DM Mono', monospace;
|
|
5616
|
+
--sans: 'Syne', sans-serif;
|
|
5617
|
+
--radius: 10px;
|
|
5618
|
+
--radius-lg: 16px;
|
|
5619
|
+
}
|
|
5620
|
+
|
|
5621
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
5622
|
+
|
|
5595
5623
|
body {
|
|
5596
|
-
font-family:
|
|
5597
|
-
background:
|
|
5624
|
+
font-family: var(--sans);
|
|
5625
|
+
background: var(--bg);
|
|
5626
|
+
color: var(--text-1);
|
|
5598
5627
|
min-height: 100vh;
|
|
5599
|
-
|
|
5628
|
+
overflow-x: hidden;
|
|
5600
5629
|
}
|
|
5601
5630
|
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5631
|
+
/* \u2500\u2500 Layout \u2500\u2500 */
|
|
5632
|
+
.shell {
|
|
5633
|
+
display: grid;
|
|
5634
|
+
grid-template-columns: 220px 1fr;
|
|
5635
|
+
min-height: 100vh;
|
|
5605
5636
|
}
|
|
5606
5637
|
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
border-radius: 12px;
|
|
5612
|
-
margin-bottom: 20px;
|
|
5613
|
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
5638
|
+
/* \u2500\u2500 Sidebar \u2500\u2500 */
|
|
5639
|
+
aside {
|
|
5640
|
+
background: var(--surface);
|
|
5641
|
+
border-right: 1px solid var(--border);
|
|
5614
5642
|
display: flex;
|
|
5615
|
-
|
|
5616
|
-
|
|
5643
|
+
flex-direction: column;
|
|
5644
|
+
padding: 28px 0;
|
|
5645
|
+
position: sticky;
|
|
5646
|
+
top: 0;
|
|
5647
|
+
height: 100vh;
|
|
5617
5648
|
}
|
|
5618
5649
|
|
|
5619
|
-
.
|
|
5620
|
-
font-size: 24px;
|
|
5621
|
-
color: #1a202c;
|
|
5650
|
+
.logo {
|
|
5622
5651
|
display: flex;
|
|
5623
5652
|
align-items: center;
|
|
5624
5653
|
gap: 10px;
|
|
5654
|
+
padding: 0 24px 32px;
|
|
5655
|
+
border-bottom: 1px solid var(--border);
|
|
5656
|
+
margin-bottom: 12px;
|
|
5625
5657
|
}
|
|
5626
5658
|
|
|
5627
|
-
.
|
|
5628
|
-
|
|
5629
|
-
|
|
5659
|
+
.logo-mark {
|
|
5660
|
+
width: 34px; height: 34px;
|
|
5661
|
+
background: var(--accent);
|
|
5662
|
+
border-radius: 8px;
|
|
5663
|
+
display: grid;
|
|
5664
|
+
place-items: center;
|
|
5665
|
+
font-size: 17px;
|
|
5666
|
+
font-weight: 800;
|
|
5667
|
+
color: #fff;
|
|
5630
5668
|
}
|
|
5631
5669
|
|
|
5632
|
-
.
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5670
|
+
.logo-name { font-weight: 800; font-size: 18px; letter-spacing: -0.5px; }
|
|
5671
|
+
.logo-version { font-family: var(--mono); font-size: 10px; color: var(--text-3); }
|
|
5672
|
+
|
|
5673
|
+
.nav-section { padding: 8px 12px; flex: 1; overflow-y: auto; }
|
|
5674
|
+
|
|
5675
|
+
.nav-label {
|
|
5676
|
+
font-family: var(--mono);
|
|
5677
|
+
font-size: 10px;
|
|
5678
|
+
color: var(--text-3);
|
|
5679
|
+
letter-spacing: 1.5px;
|
|
5680
|
+
text-transform: uppercase;
|
|
5681
|
+
padding: 0 12px;
|
|
5682
|
+
margin: 16px 0 6px;
|
|
5683
|
+
}
|
|
5684
|
+
|
|
5685
|
+
.nav-item {
|
|
5686
|
+
display: flex;
|
|
5687
|
+
align-items: center;
|
|
5688
|
+
gap: 10px;
|
|
5689
|
+
padding: 9px 12px;
|
|
5690
|
+
border-radius: var(--radius);
|
|
5691
|
+
color: var(--text-2);
|
|
5692
|
+
text-decoration: none;
|
|
5636
5693
|
font-size: 14px;
|
|
5637
5694
|
font-weight: 600;
|
|
5695
|
+
transition: all 0.15s ease;
|
|
5638
5696
|
cursor: pointer;
|
|
5639
|
-
transition: all 0.2s;
|
|
5640
|
-
text-decoration: none;
|
|
5641
|
-
display: inline-flex;
|
|
5642
|
-
align-items: center;
|
|
5643
|
-
gap: 8px;
|
|
5644
5697
|
}
|
|
5698
|
+
.nav-item:hover { color: var(--text-1); background: var(--panel); }
|
|
5699
|
+
.nav-item.active { color: var(--accent); background: var(--accent-lo); }
|
|
5700
|
+
.nav-item .icon { font-size: 15px; width: 20px; text-align: center; }
|
|
5645
5701
|
|
|
5646
|
-
.
|
|
5647
|
-
|
|
5648
|
-
|
|
5702
|
+
.sidebar-footer {
|
|
5703
|
+
padding: 16px 24px;
|
|
5704
|
+
border-top: 1px solid var(--border);
|
|
5649
5705
|
}
|
|
5650
5706
|
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
transform: translateY(-1px);
|
|
5654
|
-
}
|
|
5707
|
+
/* \u2500\u2500 Main \u2500\u2500 */
|
|
5708
|
+
main { display: flex; flex-direction: column; min-height: 100vh; overflow-y: auto; }
|
|
5655
5709
|
|
|
5656
|
-
.
|
|
5657
|
-
|
|
5658
|
-
|
|
5710
|
+
.topbar {
|
|
5711
|
+
display: flex;
|
|
5712
|
+
align-items: center;
|
|
5713
|
+
justify-content: space-between;
|
|
5714
|
+
padding: 20px 32px;
|
|
5715
|
+
border-bottom: 1px solid var(--border);
|
|
5716
|
+
background: var(--surface);
|
|
5717
|
+
position: sticky;
|
|
5718
|
+
top: 0;
|
|
5719
|
+
z-index: 10;
|
|
5659
5720
|
}
|
|
5660
5721
|
|
|
5661
|
-
.
|
|
5662
|
-
|
|
5663
|
-
}
|
|
5722
|
+
.page-title { font-size: 15px; font-weight: 700; letter-spacing: -0.2px; }
|
|
5723
|
+
.page-sub { font-family: var(--mono); font-size: 11px; color: var(--text-3); margin-top: 2px; }
|
|
5664
5724
|
|
|
5665
|
-
.
|
|
5666
|
-
background: #48bb78;
|
|
5667
|
-
color: white;
|
|
5668
|
-
}
|
|
5725
|
+
.topbar-actions { display: flex; align-items: center; gap: 10px; }
|
|
5669
5726
|
|
|
5670
|
-
.btn
|
|
5671
|
-
|
|
5727
|
+
.btn {
|
|
5728
|
+
font-family: var(--sans);
|
|
5729
|
+
font-weight: 700;
|
|
5730
|
+
font-size: 12px;
|
|
5731
|
+
padding: 8px 16px;
|
|
5732
|
+
border-radius: 8px;
|
|
5733
|
+
border: none;
|
|
5734
|
+
cursor: pointer;
|
|
5735
|
+
transition: all 0.15s ease;
|
|
5736
|
+
display: inline-flex;
|
|
5737
|
+
align-items: center;
|
|
5738
|
+
gap: 6px;
|
|
5739
|
+
text-decoration: none;
|
|
5672
5740
|
}
|
|
5741
|
+
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
5673
5742
|
|
|
5674
|
-
.btn
|
|
5675
|
-
|
|
5676
|
-
|
|
5743
|
+
.btn-ghost {
|
|
5744
|
+
background: var(--panel);
|
|
5745
|
+
color: var(--text-2);
|
|
5746
|
+
border: 1px solid var(--border);
|
|
5677
5747
|
}
|
|
5748
|
+
.btn-ghost:hover { border-color: var(--border-hi); color: var(--text-1); }
|
|
5678
5749
|
|
|
5679
|
-
.
|
|
5680
|
-
background:
|
|
5681
|
-
|
|
5682
|
-
padding: 30px;
|
|
5683
|
-
border-radius: 12px;
|
|
5684
|
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
5750
|
+
.btn-primary {
|
|
5751
|
+
background: var(--accent);
|
|
5752
|
+
color: #fff;
|
|
5685
5753
|
}
|
|
5754
|
+
.btn-primary:hover:not(:disabled) { background: #ea580c; box-shadow: 0 0 20px rgba(249,115,22,0.35); }
|
|
5686
5755
|
|
|
5687
|
-
|
|
5756
|
+
/* \u2500\u2500 Content \u2500\u2500 */
|
|
5757
|
+
.content { padding: 28px 32px; display: flex; flex-direction: column; gap: 24px; }
|
|
5758
|
+
|
|
5759
|
+
/* \u2500\u2500 Tabs (category selector) \u2500\u2500 */
|
|
5760
|
+
.tab-bar {
|
|
5688
5761
|
display: flex;
|
|
5689
|
-
gap:
|
|
5690
|
-
|
|
5691
|
-
border
|
|
5692
|
-
|
|
5762
|
+
gap: 4px;
|
|
5763
|
+
background: var(--surface);
|
|
5764
|
+
border: 1px solid var(--border);
|
|
5765
|
+
border-radius: 10px;
|
|
5766
|
+
padding: 4px;
|
|
5767
|
+
flex-wrap: wrap;
|
|
5693
5768
|
}
|
|
5694
5769
|
|
|
5695
|
-
.tab {
|
|
5696
|
-
padding:
|
|
5770
|
+
.tab-btn {
|
|
5771
|
+
padding: 7px 14px;
|
|
5772
|
+
background: transparent;
|
|
5697
5773
|
border: none;
|
|
5698
|
-
|
|
5699
|
-
|
|
5774
|
+
border-radius: 7px;
|
|
5775
|
+
color: var(--text-3);
|
|
5776
|
+
font-family: var(--sans);
|
|
5777
|
+
font-size: 12px;
|
|
5700
5778
|
font-weight: 600;
|
|
5701
|
-
color: #718096;
|
|
5702
5779
|
cursor: pointer;
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
color: #667eea;
|
|
5709
|
-
border-bottom-color: #667eea;
|
|
5780
|
+
transition: all 0.15s ease;
|
|
5781
|
+
white-space: nowrap;
|
|
5782
|
+
display: flex;
|
|
5783
|
+
align-items: center;
|
|
5784
|
+
gap: 5px;
|
|
5710
5785
|
}
|
|
5711
|
-
|
|
5712
|
-
.tab
|
|
5713
|
-
|
|
5786
|
+
.tab-btn:hover { color: var(--text-2); }
|
|
5787
|
+
.tab-btn.active {
|
|
5788
|
+
background: var(--panel);
|
|
5789
|
+
color: var(--text-1);
|
|
5790
|
+
border: 1px solid var(--border-hi);
|
|
5714
5791
|
}
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5792
|
+
.tab-btn .tab-dot {
|
|
5793
|
+
width: 6px; height: 6px;
|
|
5794
|
+
border-radius: 50%;
|
|
5795
|
+
background: var(--text-3);
|
|
5718
5796
|
}
|
|
5797
|
+
.tab-btn.has-required .tab-dot { background: var(--amber); }
|
|
5798
|
+
.tab-btn.active .tab-dot { background: var(--accent); }
|
|
5719
5799
|
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
}
|
|
5800
|
+
/* \u2500\u2500 Section \u2500\u2500 */
|
|
5801
|
+
.section { display: none; flex-direction: column; gap: 16px; }
|
|
5802
|
+
.section.active { display: flex; }
|
|
5723
5803
|
|
|
5724
|
-
.
|
|
5804
|
+
.section-header {
|
|
5725
5805
|
display: flex;
|
|
5726
|
-
justify-content: space-between;
|
|
5727
5806
|
align-items: center;
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
.category-title {
|
|
5732
|
-
font-size: 18px;
|
|
5733
|
-
font-weight: 600;
|
|
5734
|
-
color: #2d3748;
|
|
5735
|
-
}
|
|
5736
|
-
|
|
5737
|
-
.env-grid {
|
|
5738
|
-
display: grid;
|
|
5739
|
-
gap: 20px;
|
|
5807
|
+
gap: 12px;
|
|
5808
|
+
margin-bottom: 4px;
|
|
5740
5809
|
}
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5810
|
+
.section-icon {
|
|
5811
|
+
width: 36px; height: 36px;
|
|
5812
|
+
background: var(--accent-lo);
|
|
5813
|
+
border: 1px solid var(--accent-md);
|
|
5744
5814
|
border-radius: 8px;
|
|
5745
|
-
|
|
5746
|
-
|
|
5815
|
+
display: grid;
|
|
5816
|
+
place-items: center;
|
|
5817
|
+
font-size: 16px;
|
|
5747
5818
|
}
|
|
5819
|
+
.section-title { font-size: 15px; font-weight: 700; }
|
|
5820
|
+
.section-desc { font-family: var(--mono); font-size: 11px; color: var(--text-3); margin-top: 2px; }
|
|
5748
5821
|
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5822
|
+
/* \u2500\u2500 Env card \u2500\u2500 */
|
|
5823
|
+
.env-card {
|
|
5824
|
+
background: var(--panel);
|
|
5825
|
+
border: 1px solid var(--border);
|
|
5826
|
+
border-radius: var(--radius-lg);
|
|
5827
|
+
padding: 20px 24px;
|
|
5828
|
+
transition: border-color 0.15s;
|
|
5752
5829
|
}
|
|
5830
|
+
.env-card:hover { border-color: var(--border-hi); }
|
|
5831
|
+
.env-card.has-value { border-color: rgba(249,115,22,0.15); }
|
|
5753
5832
|
|
|
5754
|
-
.env-
|
|
5833
|
+
.env-card-head {
|
|
5755
5834
|
display: flex;
|
|
5756
5835
|
justify-content: space-between;
|
|
5757
5836
|
align-items: flex-start;
|
|
5758
|
-
margin-bottom:
|
|
5837
|
+
margin-bottom: 6px;
|
|
5759
5838
|
}
|
|
5760
5839
|
|
|
5761
|
-
.env-
|
|
5762
|
-
font-
|
|
5763
|
-
|
|
5764
|
-
font-
|
|
5840
|
+
.env-key {
|
|
5841
|
+
font-family: var(--mono);
|
|
5842
|
+
font-size: 13px;
|
|
5843
|
+
font-weight: 500;
|
|
5844
|
+
color: var(--text-1);
|
|
5765
5845
|
display: flex;
|
|
5766
5846
|
align-items: center;
|
|
5767
5847
|
gap: 8px;
|
|
5768
5848
|
}
|
|
5769
5849
|
|
|
5770
|
-
.required
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
font-
|
|
5774
|
-
|
|
5850
|
+
.badge-required {
|
|
5851
|
+
font-family: var(--sans);
|
|
5852
|
+
font-size: 9px;
|
|
5853
|
+
font-weight: 700;
|
|
5854
|
+
letter-spacing: 0.08em;
|
|
5855
|
+
text-transform: uppercase;
|
|
5856
|
+
background: rgba(239,68,68,0.15);
|
|
5857
|
+
color: var(--red);
|
|
5858
|
+
border: 1px solid rgba(239,68,68,0.25);
|
|
5775
5859
|
border-radius: 4px;
|
|
5860
|
+
padding: 2px 6px;
|
|
5861
|
+
}
|
|
5862
|
+
.badge-sensitive {
|
|
5863
|
+
font-size: 9px;
|
|
5776
5864
|
font-weight: 700;
|
|
5865
|
+
font-family: var(--sans);
|
|
5866
|
+
letter-spacing: 0.08em;
|
|
5867
|
+
text-transform: uppercase;
|
|
5868
|
+
background: var(--amber-lo);
|
|
5869
|
+
color: var(--amber);
|
|
5870
|
+
border: 1px solid rgba(245,158,11,0.2);
|
|
5871
|
+
border-radius: 4px;
|
|
5872
|
+
padding: 2px 6px;
|
|
5777
5873
|
}
|
|
5778
5874
|
|
|
5779
|
-
.env-
|
|
5780
|
-
font-
|
|
5781
|
-
|
|
5782
|
-
|
|
5875
|
+
.env-desc {
|
|
5876
|
+
font-family: var(--mono);
|
|
5877
|
+
font-size: 11px;
|
|
5878
|
+
color: var(--text-3);
|
|
5879
|
+
margin-bottom: 14px;
|
|
5880
|
+
line-height: 1.5;
|
|
5783
5881
|
}
|
|
5784
5882
|
|
|
5785
|
-
.env-input-
|
|
5883
|
+
.env-input-row {
|
|
5786
5884
|
display: flex;
|
|
5787
|
-
gap:
|
|
5885
|
+
gap: 8px;
|
|
5788
5886
|
align-items: center;
|
|
5789
5887
|
}
|
|
5790
5888
|
|
|
5791
|
-
.env-input {
|
|
5889
|
+
.env-input, .env-select {
|
|
5792
5890
|
flex: 1;
|
|
5793
|
-
|
|
5794
|
-
border: 1px solid
|
|
5795
|
-
border-radius:
|
|
5796
|
-
|
|
5797
|
-
font-family:
|
|
5798
|
-
|
|
5799
|
-
|
|
5800
|
-
|
|
5801
|
-
.env-input:focus {
|
|
5891
|
+
background: var(--surface);
|
|
5892
|
+
border: 1px solid var(--border-hi);
|
|
5893
|
+
border-radius: 8px;
|
|
5894
|
+
padding: 10px 14px;
|
|
5895
|
+
font-family: var(--mono);
|
|
5896
|
+
font-size: 13px;
|
|
5897
|
+
color: var(--text-1);
|
|
5802
5898
|
outline: none;
|
|
5803
|
-
border-color
|
|
5804
|
-
|
|
5899
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
5900
|
+
appearance: none;
|
|
5805
5901
|
}
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5902
|
+
.env-input:focus, .env-select:focus {
|
|
5903
|
+
border-color: var(--accent);
|
|
5904
|
+
box-shadow: 0 0 0 3px rgba(249,115,22,0.12);
|
|
5809
5905
|
}
|
|
5906
|
+
.env-input.changed { border-color: rgba(249,115,22,0.5); }
|
|
5907
|
+
.env-input.invalid { border-color: var(--red); }
|
|
5810
5908
|
|
|
5811
|
-
.env-
|
|
5812
|
-
display: flex;
|
|
5813
|
-
gap: 5px;
|
|
5814
|
-
}
|
|
5909
|
+
.env-select option { background: var(--panel); }
|
|
5815
5910
|
|
|
5816
|
-
.
|
|
5817
|
-
|
|
5818
|
-
border:
|
|
5819
|
-
|
|
5820
|
-
|
|
5911
|
+
.env-action-btn {
|
|
5912
|
+
width: 36px; height: 36px;
|
|
5913
|
+
border-radius: 8px;
|
|
5914
|
+
border: 1px solid var(--border-hi);
|
|
5915
|
+
background: var(--surface);
|
|
5916
|
+
color: var(--text-2);
|
|
5821
5917
|
cursor: pointer;
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
|
|
5825
|
-
|
|
5826
|
-
|
|
5827
|
-
background: #cbd5e0;
|
|
5828
|
-
}
|
|
5829
|
-
|
|
5830
|
-
.icon-btn.test {
|
|
5831
|
-
background: #bee3f8;
|
|
5832
|
-
color: #2c5282;
|
|
5833
|
-
}
|
|
5834
|
-
|
|
5835
|
-
.icon-btn.test:hover {
|
|
5836
|
-
background: #90cdf4;
|
|
5837
|
-
}
|
|
5838
|
-
|
|
5839
|
-
.icon-btn.generate {
|
|
5840
|
-
background: #c6f6d5;
|
|
5841
|
-
color: #22543d;
|
|
5842
|
-
}
|
|
5843
|
-
|
|
5844
|
-
.icon-btn.generate:hover {
|
|
5845
|
-
background: #9ae6b4;
|
|
5918
|
+
display: grid;
|
|
5919
|
+
place-items: center;
|
|
5920
|
+
font-size: 14px;
|
|
5921
|
+
transition: all 0.15s;
|
|
5922
|
+
flex-shrink: 0;
|
|
5846
5923
|
}
|
|
5924
|
+
.env-action-btn:hover { background: var(--panel); color: var(--text-1); border-color: var(--border-hi); }
|
|
5925
|
+
.env-action-btn.test-btn:hover { background: var(--blue-lo); color: var(--blue); border-color: rgba(56,189,248,0.25); }
|
|
5926
|
+
.env-action-btn.gen-btn:hover { background: var(--green-lo); color: var(--green); border-color: rgba(34,197,94,0.25); }
|
|
5847
5927
|
|
|
5848
|
-
.
|
|
5849
|
-
|
|
5850
|
-
font-size:
|
|
5851
|
-
margin-top:
|
|
5928
|
+
.env-feedback {
|
|
5929
|
+
font-family: var(--mono);
|
|
5930
|
+
font-size: 11px;
|
|
5931
|
+
margin-top: 8px;
|
|
5932
|
+
min-height: 16px;
|
|
5852
5933
|
}
|
|
5934
|
+
.env-feedback.error { color: var(--red); }
|
|
5935
|
+
.env-feedback.success { color: var(--green); }
|
|
5853
5936
|
|
|
5854
|
-
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
|
|
5937
|
+
/* \u2500\u2500 Toast \u2500\u2500 */
|
|
5938
|
+
.toast-zone {
|
|
5939
|
+
position: fixed;
|
|
5940
|
+
bottom: 24px;
|
|
5941
|
+
right: 24px;
|
|
5942
|
+
display: flex;
|
|
5943
|
+
flex-direction: column;
|
|
5944
|
+
gap: 8px;
|
|
5945
|
+
z-index: 100;
|
|
5858
5946
|
}
|
|
5859
5947
|
|
|
5860
|
-
.
|
|
5861
|
-
padding:
|
|
5862
|
-
border-radius:
|
|
5863
|
-
|
|
5948
|
+
.toast {
|
|
5949
|
+
padding: 12px 18px;
|
|
5950
|
+
border-radius: 10px;
|
|
5951
|
+
font-size: 13px;
|
|
5952
|
+
font-weight: 600;
|
|
5864
5953
|
display: flex;
|
|
5865
5954
|
align-items: center;
|
|
5866
5955
|
gap: 10px;
|
|
5956
|
+
animation: slideIn 0.2s ease;
|
|
5957
|
+
max-width: 380px;
|
|
5867
5958
|
}
|
|
5959
|
+
.toast.success { background: var(--panel); border: 1px solid rgba(34,197,94,0.3); color: var(--green); }
|
|
5960
|
+
.toast.error { background: var(--panel); border: 1px solid rgba(239,68,68,0.3); color: var(--red); }
|
|
5961
|
+
.toast.warning { background: var(--panel); border: 1px solid rgba(245,158,11,0.3); color: var(--amber); }
|
|
5962
|
+
.toast.info { background: var(--panel); border: 1px solid rgba(56,189,248,0.3); color: var(--blue); }
|
|
5868
5963
|
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
color: #92400e;
|
|
5873
|
-
}
|
|
5874
|
-
|
|
5875
|
-
.alert-info {
|
|
5876
|
-
background: #eff6ff;
|
|
5877
|
-
border-left: 4px solid #3b82f6;
|
|
5878
|
-
color: #1e40af;
|
|
5964
|
+
@keyframes slideIn {
|
|
5965
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
5966
|
+
to { opacity: 1; transform: translateY(0); }
|
|
5879
5967
|
}
|
|
5880
5968
|
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5884
|
-
|
|
5969
|
+
/* \u2500\u2500 Modal (test result) \u2500\u2500 */
|
|
5970
|
+
.modal-backdrop {
|
|
5971
|
+
display: none;
|
|
5972
|
+
position: fixed; inset: 0;
|
|
5973
|
+
background: rgba(0,0,0,0.6);
|
|
5974
|
+
z-index: 200;
|
|
5975
|
+
align-items: center;
|
|
5976
|
+
justify-content: center;
|
|
5885
5977
|
}
|
|
5978
|
+
.modal-backdrop.open { display: flex; }
|
|
5886
5979
|
|
|
5887
|
-
.
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5980
|
+
.modal {
|
|
5981
|
+
background: var(--surface);
|
|
5982
|
+
border: 1px solid var(--border-hi);
|
|
5983
|
+
border-radius: var(--radius-lg);
|
|
5984
|
+
padding: 28px;
|
|
5985
|
+
width: 420px;
|
|
5986
|
+
max-width: 90vw;
|
|
5987
|
+
box-shadow: 0 24px 64px rgba(0,0,0,0.5);
|
|
5988
|
+
}
|
|
5989
|
+
.modal-title { font-size: 15px; font-weight: 700; margin-bottom: 16px; }
|
|
5990
|
+
.modal-body { margin-bottom: 20px; }
|
|
5991
|
+
.modal-result {
|
|
5992
|
+
padding: 14px;
|
|
5993
|
+
border-radius: 8px;
|
|
5994
|
+
font-family: var(--mono);
|
|
5995
|
+
font-size: 12px;
|
|
5891
5996
|
}
|
|
5997
|
+
.modal-result.ok { background: var(--green-lo); border: 1px solid rgba(34,197,94,0.2); color: var(--green); }
|
|
5998
|
+
.modal-result.fail { background: var(--red-lo); border: 1px solid rgba(239,68,68,0.2); color: var(--red); }
|
|
5999
|
+
.modal-footer { display: flex; justify-content: flex-end; }
|
|
5892
6000
|
|
|
6001
|
+
/* \u2500\u2500 Spinner \u2500\u2500 */
|
|
5893
6002
|
.spinner {
|
|
5894
|
-
|
|
5895
|
-
border
|
|
6003
|
+
width: 36px; height: 36px;
|
|
6004
|
+
border: 3px solid var(--border);
|
|
6005
|
+
border-top-color: var(--accent);
|
|
5896
6006
|
border-radius: 50%;
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
animation: spin 1s linear infinite;
|
|
5900
|
-
margin: 0 auto 20px;
|
|
6007
|
+
animation: spin 0.8s linear infinite;
|
|
6008
|
+
margin: 0 auto 16px;
|
|
5901
6009
|
}
|
|
6010
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
5902
6011
|
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
6012
|
+
.loading-state {
|
|
6013
|
+
text-align: center;
|
|
6014
|
+
padding: 60px 0;
|
|
6015
|
+
color: var(--text-3);
|
|
6016
|
+
font-family: var(--mono);
|
|
6017
|
+
font-size: 12px;
|
|
5906
6018
|
}
|
|
5907
6019
|
|
|
5908
|
-
|
|
6020
|
+
/* \u2500\u2500 Restart banner \u2500\u2500 */
|
|
6021
|
+
.restart-banner {
|
|
5909
6022
|
display: none;
|
|
5910
|
-
|
|
5911
|
-
|
|
5912
|
-
|
|
5913
|
-
|
|
5914
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
6023
|
+
background: var(--amber-lo);
|
|
6024
|
+
border: 1px solid rgba(245,158,11,0.25);
|
|
6025
|
+
border-radius: 10px;
|
|
6026
|
+
padding: 12px 18px;
|
|
6027
|
+
font-size: 13px;
|
|
6028
|
+
color: var(--amber);
|
|
6029
|
+
font-weight: 600;
|
|
5917
6030
|
align-items: center;
|
|
5918
|
-
|
|
6031
|
+
gap: 10px;
|
|
5919
6032
|
}
|
|
6033
|
+
.restart-banner.show { display: flex; }
|
|
6034
|
+
</style>
|
|
6035
|
+
</head>
|
|
6036
|
+
<body>
|
|
6037
|
+
<div class="shell">
|
|
5920
6038
|
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
6039
|
+
<!-- Sidebar -->
|
|
6040
|
+
<aside>
|
|
6041
|
+
<div class="logo">
|
|
6042
|
+
<div class="logo-mark">Q</div>
|
|
6043
|
+
<div>
|
|
6044
|
+
<div class="logo-name">OpenQA</div>
|
|
6045
|
+
<div class="logo-version">v1.3.4</div>
|
|
6046
|
+
</div>
|
|
6047
|
+
</div>
|
|
5924
6048
|
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
6049
|
+
<div class="nav-section">
|
|
6050
|
+
<div class="nav-label">Overview</div>
|
|
6051
|
+
<a class="nav-item" href="/">
|
|
6052
|
+
<span class="icon">\u{1F4CA}</span> Dashboard
|
|
6053
|
+
</a>
|
|
6054
|
+
<a class="nav-item" href="/sessions">
|
|
6055
|
+
<span class="icon">\u{1F9EA}</span> Sessions
|
|
6056
|
+
</a>
|
|
6057
|
+
<a class="nav-item" href="/issues">
|
|
6058
|
+
<span class="icon">\u{1F41B}</span> Issues
|
|
6059
|
+
</a>
|
|
5933
6060
|
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
6061
|
+
<div class="nav-label">Testing</div>
|
|
6062
|
+
<a class="nav-item" href="/tests">
|
|
6063
|
+
<span class="icon">\u26A1</span> Tests
|
|
6064
|
+
</a>
|
|
6065
|
+
<a class="nav-item" href="/coverage">
|
|
6066
|
+
<span class="icon">\u{1F4C8}</span> Coverage
|
|
6067
|
+
</a>
|
|
6068
|
+
<a class="nav-item" href="/kanban">
|
|
6069
|
+
<span class="icon">\u{1F4CB}</span> Kanban
|
|
6070
|
+
</a>
|
|
5940
6071
|
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
6072
|
+
<div class="nav-label">System</div>
|
|
6073
|
+
<a class="nav-item" href="/config">
|
|
6074
|
+
<span class="icon">\u2699\uFE0F</span> Config
|
|
6075
|
+
</a>
|
|
6076
|
+
<a class="nav-item active" href="/config/env">
|
|
6077
|
+
<span class="icon">\u{1F527}</span> Environment
|
|
6078
|
+
</a>
|
|
6079
|
+
<a class="nav-item" href="/logs">
|
|
6080
|
+
<span class="icon">\u{1F4DC}</span> Logs
|
|
6081
|
+
</a>
|
|
6082
|
+
</div>
|
|
5945
6083
|
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
gap: 10px;
|
|
5949
|
-
justify-content: flex-end;
|
|
5950
|
-
}
|
|
5951
|
-
</style>
|
|
5952
|
-
</head>
|
|
5953
|
-
<body>
|
|
5954
|
-
<div class="container">
|
|
5955
|
-
<div class="header">
|
|
5956
|
-
<h1>
|
|
5957
|
-
<span>\u2699\uFE0F</span>
|
|
6084
|
+
<div class="sidebar-footer">
|
|
6085
|
+
<div style="font-family:var(--mono);font-size:11px;color:var(--text-3);">
|
|
5958
6086
|
Environment Variables
|
|
5959
|
-
</
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
6087
|
+
</div>
|
|
6088
|
+
</div>
|
|
6089
|
+
</aside>
|
|
6090
|
+
|
|
6091
|
+
<!-- Main -->
|
|
6092
|
+
<main>
|
|
6093
|
+
<div class="topbar">
|
|
6094
|
+
<div>
|
|
6095
|
+
<div class="page-title">Environment Variables</div>
|
|
6096
|
+
<div class="page-sub">Configure runtime variables for OpenQA</div>
|
|
6097
|
+
</div>
|
|
6098
|
+
<div class="topbar-actions">
|
|
6099
|
+
<a class="btn btn-ghost" href="/config">\u2190 Back to Config</a>
|
|
6100
|
+
<button id="saveBtn" class="btn btn-primary" disabled>
|
|
6101
|
+
\u{1F4BE} Save Changes
|
|
6102
|
+
</button>
|
|
5963
6103
|
</div>
|
|
5964
6104
|
</div>
|
|
5965
6105
|
|
|
5966
6106
|
<div class="content">
|
|
5967
|
-
|
|
6107
|
+
|
|
6108
|
+
<!-- Restart banner -->
|
|
6109
|
+
<div class="restart-banner" id="restartBanner">
|
|
6110
|
+
\u26A0\uFE0F Some changes require a server restart to take effect.
|
|
6111
|
+
</div>
|
|
6112
|
+
|
|
6113
|
+
<!-- Loading -->
|
|
6114
|
+
<div class="loading-state" id="loadingState">
|
|
5968
6115
|
<div class="spinner"></div>
|
|
5969
|
-
|
|
6116
|
+
Loading environment variables\u2026
|
|
5970
6117
|
</div>
|
|
5971
6118
|
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
<button class="tab" data-category="web">\u{1F310} Web Server</button>
|
|
5981
|
-
<button class="tab" data-category="agent">\u{1F916} Agent</button>
|
|
5982
|
-
<button class="tab" data-category="database">\u{1F4BE} Database</button>
|
|
5983
|
-
<button class="tab" data-category="notifications">\u{1F514} Notifications</button>
|
|
5984
|
-
</div>
|
|
6119
|
+
<!-- Main content (hidden while loading) -->
|
|
6120
|
+
<div id="mainContent" style="display:none;flex-direction:column;gap:24px;">
|
|
6121
|
+
|
|
6122
|
+
<!-- Tab bar -->
|
|
6123
|
+
<div class="tab-bar" id="tabBar"></div>
|
|
6124
|
+
|
|
6125
|
+
<!-- Sections -->
|
|
6126
|
+
<div id="sections"></div>
|
|
5985
6127
|
|
|
5986
|
-
<div id="categories"></div>
|
|
5987
6128
|
</div>
|
|
5988
6129
|
</div>
|
|
5989
|
-
</
|
|
6130
|
+
</main>
|
|
6131
|
+
</div>
|
|
5990
6132
|
|
|
5991
|
-
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
<div class="modal-
|
|
5997
|
-
|
|
5998
|
-
|
|
6133
|
+
<!-- Test result modal -->
|
|
6134
|
+
<div class="modal-backdrop" id="testModal">
|
|
6135
|
+
<div class="modal">
|
|
6136
|
+
<div class="modal-title">Connection Test</div>
|
|
6137
|
+
<div class="modal-body">
|
|
6138
|
+
<div class="modal-result" id="testResultBox">\u2026</div>
|
|
6139
|
+
</div>
|
|
6140
|
+
<div class="modal-footer">
|
|
6141
|
+
<button class="btn btn-ghost" onclick="closeModal()">Close</button>
|
|
5999
6142
|
</div>
|
|
6000
6143
|
</div>
|
|
6144
|
+
</div>
|
|
6001
6145
|
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
let changedVariables = {};
|
|
6005
|
-
let restartRequired = false;
|
|
6146
|
+
<!-- Toast zone -->
|
|
6147
|
+
<div class="toast-zone" id="toastZone"></div>
|
|
6006
6148
|
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6149
|
+
<script>
|
|
6150
|
+
/* \u2500\u2500 State \u2500\u2500 */
|
|
6151
|
+
let envVars = [];
|
|
6152
|
+
let changed = {};
|
|
6153
|
+
let hasRequiredMissing = false;
|
|
6154
|
+
|
|
6155
|
+
const TABS = [
|
|
6156
|
+
{ id: 'llm', label: '\u{1F916} LLM', desc: 'Language model provider & API keys' },
|
|
6157
|
+
{ id: 'security', label: '\u{1F512} Security', desc: 'Authentication & JWT configuration' },
|
|
6158
|
+
{ id: 'target', label: '\u{1F3AF} Target App', desc: 'Application under test settings' },
|
|
6159
|
+
{ id: 'github', label: '\u{1F419} GitHub', desc: 'Repository & CI/CD integration' },
|
|
6160
|
+
{ id: 'web', label: '\u{1F310} Web Server', desc: 'HTTP host, port & CORS settings' },
|
|
6161
|
+
{ id: 'agent', label: '\u{1F916} Agent', desc: 'Autonomous agent behaviour' },
|
|
6162
|
+
{ id: 'database', label: '\u{1F4BE} Database', desc: 'Persistence & storage' },
|
|
6163
|
+
{ id: 'notifications', label: '\u{1F514} Notifications', desc: 'Slack & Discord webhooks' },
|
|
6164
|
+
];
|
|
6023
6165
|
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
</div>
|
|
6040
|
-
<div class="env-grid">
|
|
6041
|
-
\${vars.map(v => renderEnvItem(v)).join('')}
|
|
6042
|
-
</div>
|
|
6043
|
-
\`;
|
|
6044
|
-
|
|
6045
|
-
container.appendChild(section);
|
|
6046
|
-
});
|
|
6047
|
-
}
|
|
6166
|
+
/* \u2500\u2500 Init \u2500\u2500 */
|
|
6167
|
+
async function init() {
|
|
6168
|
+
try {
|
|
6169
|
+
const res = await fetch('/api/env');
|
|
6170
|
+
if (!res.ok) { toast('error', 'Failed to load environment variables (status ' + res.status + ')'); return; }
|
|
6171
|
+
const data = await res.json();
|
|
6172
|
+
envVars = data.variables || [];
|
|
6173
|
+
renderAll();
|
|
6174
|
+
document.getElementById('loadingState').style.display = 'none';
|
|
6175
|
+
const mc = document.getElementById('mainContent');
|
|
6176
|
+
mc.style.display = 'flex';
|
|
6177
|
+
} catch (e) {
|
|
6178
|
+
toast('error', 'Network error \u2014 ' + e.message);
|
|
6179
|
+
}
|
|
6180
|
+
}
|
|
6048
6181
|
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
type="\${inputType}"
|
|
6079
|
-
class="env-input"
|
|
6080
|
-
data-key="\${envVar.key}"
|
|
6081
|
-
value="\${value}"
|
|
6082
|
-
placeholder="\${envVar.placeholder || ''}"
|
|
6083
|
-
onchange="handleChange(this)"
|
|
6084
|
-
/>\`
|
|
6085
|
-
}
|
|
6086
|
-
<div class="env-actions">
|
|
6087
|
-
\${envVar.testable ? \`<button class="icon-btn test" onclick="testVariable('\${envVar.key}')" title="Test">\u{1F9EA}</button>\` : ''}
|
|
6088
|
-
\${envVar.key === 'OPENQA_JWT_SECRET' ? \`<button class="icon-btn generate" onclick="generateSecret('\${envVar.key}')" title="Generate">\u{1F511}</button>\` : ''}
|
|
6089
|
-
</div>
|
|
6090
|
-
</div>
|
|
6091
|
-
<div class="error-message" id="error-\${envVar.key}"></div>
|
|
6092
|
-
<div class="success-message" id="success-\${envVar.key}"></div>
|
|
6182
|
+
/* \u2500\u2500 Render \u2500\u2500 */
|
|
6183
|
+
function renderAll() {
|
|
6184
|
+
renderTabBar();
|
|
6185
|
+
renderSections();
|
|
6186
|
+
activateTab(TABS[0].id);
|
|
6187
|
+
}
|
|
6188
|
+
|
|
6189
|
+
function renderTabBar() {
|
|
6190
|
+
const bar = document.getElementById('tabBar');
|
|
6191
|
+
bar.innerHTML = TABS.map(t => {
|
|
6192
|
+
const vars = envVars.filter(v => v.category === t.id);
|
|
6193
|
+
const hasRequired = vars.some(v => v.required);
|
|
6194
|
+
return \`<button class="tab-btn\${hasRequired ? ' has-required' : ''}" data-tab="\${t.id}" onclick="activateTab('\${t.id}')">
|
|
6195
|
+
<span class="tab-dot"></span>
|
|
6196
|
+
\${t.label}
|
|
6197
|
+
</button>\`;
|
|
6198
|
+
}).join('');
|
|
6199
|
+
}
|
|
6200
|
+
|
|
6201
|
+
function renderSections() {
|
|
6202
|
+
const container = document.getElementById('sections');
|
|
6203
|
+
container.innerHTML = TABS.map(t => {
|
|
6204
|
+
const vars = envVars.filter(v => v.category === t.id);
|
|
6205
|
+
return \`<div class="section" id="section-\${t.id}">
|
|
6206
|
+
<div class="section-header">
|
|
6207
|
+
<div class="section-icon">\${t.label.split(' ')[0]}</div>
|
|
6208
|
+
<div>
|
|
6209
|
+
<div class="section-title">\${t.label.slice(t.label.indexOf(' ')+1)}</div>
|
|
6210
|
+
<div class="section-desc">\${t.desc}</div>
|
|
6093
6211
|
</div>
|
|
6094
|
-
|
|
6095
|
-
|
|
6212
|
+
</div>
|
|
6213
|
+
\${vars.map(renderCard).join('')}
|
|
6214
|
+
\${vars.length === 0 ? '<div style="color:var(--text-3);font-family:var(--mono);font-size:12px;padding:20px 0">No variables in this category.</div>' : ''}
|
|
6215
|
+
</div>\`;
|
|
6216
|
+
}).join('');
|
|
6217
|
+
}
|
|
6096
6218
|
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6108
|
-
|
|
6219
|
+
function renderCard(v) {
|
|
6220
|
+
const displayVal = v.displayValue || '';
|
|
6221
|
+
const isSensitive = v.sensitive;
|
|
6222
|
+
const inputType = (v.type === 'password' && !changed[v.key]) ? 'password' : 'text';
|
|
6223
|
+
|
|
6224
|
+
let inputHTML = '';
|
|
6225
|
+
if (v.type === 'select' || v.type === 'boolean') {
|
|
6226
|
+
const opts = v.type === 'boolean'
|
|
6227
|
+
? [{ val: 'true', lbl: 'true' }, { val: 'false', lbl: 'false' }]
|
|
6228
|
+
: (v.options || []).map(o => ({ val: o, lbl: o }));
|
|
6229
|
+
inputHTML = \`<select class="env-select" data-key="\${v.key}" onchange="handleChange(this)">
|
|
6230
|
+
<option value="">\u2014 Select \u2014</option>
|
|
6231
|
+
\${opts.map(o => \`<option value="\${o.val}" \${displayVal === o.val ? 'selected' : ''}>\${o.lbl}</option>\`).join('')}
|
|
6232
|
+
</select>\`;
|
|
6233
|
+
} else {
|
|
6234
|
+
inputHTML = \`<input
|
|
6235
|
+
type="\${inputType}"
|
|
6236
|
+
class="env-input"
|
|
6237
|
+
data-key="\${v.key}"
|
|
6238
|
+
value="\${escHtml(displayVal)}"
|
|
6239
|
+
placeholder="\${escHtml(v.placeholder || '')}"
|
|
6240
|
+
oninput="handleChange(this)"
|
|
6241
|
+
autocomplete="off"
|
|
6242
|
+
/>\`;
|
|
6243
|
+
}
|
|
6109
6244
|
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6245
|
+
const testBtn = v.testable
|
|
6246
|
+
? \`<button class="env-action-btn test-btn" onclick="testVar('\${v.key}')" title="Test connection">\u{1F9EA}</button>\`
|
|
6247
|
+
: '';
|
|
6248
|
+
|
|
6249
|
+
const genBtn = v.key === 'OPENQA_JWT_SECRET'
|
|
6250
|
+
? \`<button class="env-action-btn gen-btn" onclick="generateSecret('\${v.key}')" title="Generate secret">\u{1F511}</button>\`
|
|
6251
|
+
: '';
|
|
6252
|
+
|
|
6253
|
+
const toggleBtn = (v.type === 'password' || isSensitive)
|
|
6254
|
+
? \`<button class="env-action-btn" onclick="toggleVis('\${v.key}')" title="Toggle visibility" id="vis-\${v.key}">\u{1F441}</button>\`
|
|
6255
|
+
: '';
|
|
6256
|
+
|
|
6257
|
+
return \`<div class="env-card\${displayVal ? ' has-value' : ''}" id="card-\${v.key}">
|
|
6258
|
+
<div class="env-card-head">
|
|
6259
|
+
<div class="env-key">
|
|
6260
|
+
\${v.key}
|
|
6261
|
+
\${v.required ? '<span class="badge-required">Required</span>' : ''}
|
|
6262
|
+
\${isSensitive ? '<span class="badge-sensitive">Sensitive</span>' : ''}
|
|
6263
|
+
</div>
|
|
6264
|
+
</div>
|
|
6265
|
+
<div class="env-desc">\${v.description}</div>
|
|
6266
|
+
<div class="env-input-row">
|
|
6267
|
+
\${inputHTML}
|
|
6268
|
+
\${toggleBtn}
|
|
6269
|
+
\${testBtn}
|
|
6270
|
+
\${genBtn}
|
|
6271
|
+
</div>
|
|
6272
|
+
<div class="env-feedback" id="fb-\${v.key}"></div>
|
|
6273
|
+
</div>\`;
|
|
6274
|
+
}
|
|
6275
|
+
|
|
6276
|
+
/* \u2500\u2500 Tab switching \u2500\u2500 */
|
|
6277
|
+
function activateTab(id) {
|
|
6278
|
+
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === id));
|
|
6279
|
+
document.querySelectorAll('.section').forEach(s => s.classList.toggle('active', s.id === 'section-' + id));
|
|
6280
|
+
}
|
|
6281
|
+
|
|
6282
|
+
/* \u2500\u2500 Input handling \u2500\u2500 */
|
|
6283
|
+
function handleChange(el) {
|
|
6284
|
+
const key = el.dataset.key;
|
|
6285
|
+
const val = el.value;
|
|
6286
|
+
changed[key] = val;
|
|
6287
|
+
el.classList.add('changed');
|
|
6288
|
+
el.classList.remove('invalid');
|
|
6289
|
+
clearFeedback(key);
|
|
6290
|
+
document.getElementById('saveBtn').disabled = false;
|
|
6291
|
+
}
|
|
6292
|
+
|
|
6293
|
+
/* \u2500\u2500 Toggle password visibility \u2500\u2500 */
|
|
6294
|
+
function toggleVis(key) {
|
|
6295
|
+
const inp = document.querySelector('[data-key="' + key + '"]');
|
|
6296
|
+
if (!inp || inp.tagName !== 'INPUT') return;
|
|
6297
|
+
inp.type = inp.type === 'password' ? 'text' : 'password';
|
|
6298
|
+
}
|
|
6299
|
+
|
|
6300
|
+
/* \u2500\u2500 Save \u2500\u2500 */
|
|
6301
|
+
async function saveChanges() {
|
|
6302
|
+
if (!Object.keys(changed).length) return;
|
|
6303
|
+
|
|
6304
|
+
const btn = document.getElementById('saveBtn');
|
|
6305
|
+
btn.disabled = true;
|
|
6306
|
+
btn.textContent = '\u23F3 Saving\u2026';
|
|
6307
|
+
|
|
6308
|
+
try {
|
|
6309
|
+
const res = await fetch('/api/env/bulk', {
|
|
6310
|
+
method: 'POST',
|
|
6311
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6312
|
+
body: JSON.stringify({ variables: changed }),
|
|
6313
|
+
credentials: 'include',
|
|
6314
|
+
});
|
|
6315
|
+
|
|
6316
|
+
const body = await res.json().catch(() => ({}));
|
|
6317
|
+
|
|
6318
|
+
if (!res.ok) {
|
|
6319
|
+
const errStr = body.errors
|
|
6320
|
+
? Object.entries(body.errors).map(([k, v]) => k + ': ' + v).join('; ')
|
|
6321
|
+
: body.error || 'Failed to save';
|
|
6322
|
+
// Show per-field errors
|
|
6323
|
+
if (body.errors) {
|
|
6324
|
+
for (const [k, msg] of Object.entries(body.errors)) {
|
|
6325
|
+
setFeedback(k, 'error', msg);
|
|
6326
|
+
const inp = document.querySelector('[data-key="' + k + '"]');
|
|
6327
|
+
if (inp) inp.classList.add('invalid');
|
|
6126
6328
|
}
|
|
6127
|
-
|
|
6128
|
-
const result = await response.json();
|
|
6129
|
-
restartRequired = result.restartRequired;
|
|
6130
|
-
|
|
6131
|
-
showAlert('success', \`\u2705 Saved \${result.updated} variable(s) successfully!\` +
|
|
6132
|
-
(restartRequired ? ' \u26A0\uFE0F Restart required for changes to take effect.' : ''));
|
|
6133
|
-
|
|
6134
|
-
changedVariables = {};
|
|
6135
|
-
saveBtn.textContent = '\u{1F4BE} Save Changes';
|
|
6136
|
-
|
|
6137
|
-
// Reload to show updated values
|
|
6138
|
-
setTimeout(() => location.reload(), 2000);
|
|
6139
|
-
} catch (error) {
|
|
6140
|
-
showAlert('error', 'Failed to save: ' + error.message);
|
|
6141
|
-
saveBtn.disabled = false;
|
|
6142
|
-
saveBtn.textContent = '\u{1F4BE} Save Changes';
|
|
6143
6329
|
}
|
|
6330
|
+
toast('error', errStr);
|
|
6331
|
+
btn.disabled = false;
|
|
6332
|
+
btn.innerHTML = '\u{1F4BE} Save Changes';
|
|
6333
|
+
return;
|
|
6144
6334
|
}
|
|
6145
6335
|
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
const value = input.value;
|
|
6150
|
-
|
|
6151
|
-
if (!value) {
|
|
6152
|
-
showAlert('warning', 'Please enter a value first');
|
|
6153
|
-
return;
|
|
6154
|
-
}
|
|
6155
|
-
|
|
6156
|
-
try {
|
|
6157
|
-
const response = await fetch(\`/api/env/test/\${key}\`, {
|
|
6158
|
-
method: 'POST',
|
|
6159
|
-
headers: { 'Content-Type': 'application/json' },
|
|
6160
|
-
body: JSON.stringify({ value }),
|
|
6161
|
-
});
|
|
6162
|
-
|
|
6163
|
-
const result = await response.json();
|
|
6164
|
-
showTestResult(result);
|
|
6165
|
-
} catch (error) {
|
|
6166
|
-
showTestResult({ success: false, message: 'Test failed: ' + error.message });
|
|
6167
|
-
}
|
|
6336
|
+
toast('success', '\u2705 Saved ' + body.updated + ' variable(s)');
|
|
6337
|
+
if (body.restartRequired) {
|
|
6338
|
+
document.getElementById('restartBanner').classList.add('show');
|
|
6168
6339
|
}
|
|
6169
6340
|
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
const input = document.querySelector(\`[data-key="\${key}"]\`);
|
|
6181
|
-
input.value = result.value;
|
|
6182
|
-
handleChange(input);
|
|
6183
|
-
|
|
6184
|
-
document.getElementById(\`success-\${key}\`).textContent = '\u2705 Secret generated!';
|
|
6185
|
-
} catch (error) {
|
|
6186
|
-
document.getElementById(\`error-\${key}\`).textContent = 'Failed to generate: ' + error.message;
|
|
6187
|
-
}
|
|
6188
|
-
}
|
|
6341
|
+
changed = {};
|
|
6342
|
+
btn.innerHTML = '\u{1F4BE} Save Changes';
|
|
6343
|
+
// Reload to reflect masked values
|
|
6344
|
+
setTimeout(() => location.reload(), 1200);
|
|
6345
|
+
} catch (e) {
|
|
6346
|
+
toast('error', 'Network error \u2014 ' + e.message);
|
|
6347
|
+
btn.disabled = false;
|
|
6348
|
+
btn.innerHTML = '\u{1F4BE} Save Changes';
|
|
6349
|
+
}
|
|
6350
|
+
}
|
|
6189
6351
|
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
|
|
6195
|
-
resultDiv.innerHTML = \`
|
|
6196
|
-
<div class="alert \${result.success ? 'alert-success' : 'alert-warning'}">
|
|
6197
|
-
\${result.success ? '\u2705' : '\u274C'} \${result.message}
|
|
6198
|
-
</div>
|
|
6199
|
-
\`;
|
|
6200
|
-
|
|
6201
|
-
modal.classList.add('show');
|
|
6202
|
-
}
|
|
6352
|
+
/* \u2500\u2500 Test variable \u2500\u2500 */
|
|
6353
|
+
async function testVar(key) {
|
|
6354
|
+
const inp = document.querySelector('[data-key="' + key + '"]');
|
|
6355
|
+
const val = inp ? inp.value : '';
|
|
6356
|
+
if (!val) { toast('warning', 'Enter a value first'); return; }
|
|
6203
6357
|
|
|
6204
|
-
|
|
6205
|
-
|
|
6206
|
-
|
|
6358
|
+
setFeedback(key, '', '');
|
|
6359
|
+
const btn = document.querySelector('[onclick="testVar(\\''+key+'\\')"]');
|
|
6360
|
+
if (btn) { btn.textContent = '\u23F3'; btn.disabled = true; }
|
|
6207
6361
|
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
alerts.innerHTML = \`
|
|
6215
|
-
<div class="alert \${alertClass}">
|
|
6216
|
-
\${message}
|
|
6217
|
-
</div>
|
|
6218
|
-
\`;
|
|
6219
|
-
|
|
6220
|
-
setTimeout(() => alerts.innerHTML = '', 5000);
|
|
6221
|
-
}
|
|
6222
|
-
|
|
6223
|
-
// Get category title
|
|
6224
|
-
function getCategoryTitle(category) {
|
|
6225
|
-
const titles = {
|
|
6226
|
-
llm: '\u{1F916} LLM Configuration',
|
|
6227
|
-
security: '\u{1F512} Security Settings',
|
|
6228
|
-
target: '\u{1F3AF} Target Application',
|
|
6229
|
-
github: '\u{1F419} GitHub Integration',
|
|
6230
|
-
web: '\u{1F310} Web Server',
|
|
6231
|
-
agent: '\u{1F916} Agent Configuration',
|
|
6232
|
-
database: '\u{1F4BE} Database',
|
|
6233
|
-
notifications: '\u{1F514} Notifications',
|
|
6234
|
-
};
|
|
6235
|
-
return titles[category] || category;
|
|
6236
|
-
}
|
|
6237
|
-
|
|
6238
|
-
// Tab switching
|
|
6239
|
-
document.addEventListener('click', (e) => {
|
|
6240
|
-
if (e.target.classList.contains('tab')) {
|
|
6241
|
-
const category = e.target.dataset.category;
|
|
6242
|
-
|
|
6243
|
-
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
6244
|
-
e.target.classList.add('active');
|
|
6245
|
-
|
|
6246
|
-
document.querySelectorAll('.category-section').forEach(s => s.classList.remove('active'));
|
|
6247
|
-
document.querySelector(\`[data-category="\${category}"]\`).classList.add('active');
|
|
6248
|
-
}
|
|
6362
|
+
try {
|
|
6363
|
+
const res = await fetch('/api/env/test/' + key, {
|
|
6364
|
+
method: 'POST',
|
|
6365
|
+
headers: { 'Content-Type': 'application/json' },
|
|
6366
|
+
body: JSON.stringify({ value: val }),
|
|
6367
|
+
credentials: 'include',
|
|
6249
6368
|
});
|
|
6369
|
+
const result = await res.json();
|
|
6370
|
+
openModal(result.success, result.message);
|
|
6371
|
+
setFeedback(key, result.success ? 'success' : 'error', result.success ? '\u2713 Connected' : '\u2717 ' + result.message);
|
|
6372
|
+
} catch (e) {
|
|
6373
|
+
openModal(false, 'Network error: ' + e.message);
|
|
6374
|
+
} finally {
|
|
6375
|
+
if (btn) { btn.textContent = '\u{1F9EA}'; btn.disabled = false; }
|
|
6376
|
+
}
|
|
6377
|
+
}
|
|
6250
6378
|
|
|
6251
|
-
|
|
6252
|
-
|
|
6379
|
+
/* \u2500\u2500 Generate secret \u2500\u2500 */
|
|
6380
|
+
async function generateSecret(key) {
|
|
6381
|
+
try {
|
|
6382
|
+
const res = await fetch('/api/env/generate/' + key, {
|
|
6383
|
+
method: 'POST', credentials: 'include'
|
|
6384
|
+
});
|
|
6385
|
+
if (!res.ok) throw new Error('Failed to generate');
|
|
6386
|
+
const { value } = await res.json();
|
|
6387
|
+
const inp = document.querySelector('[data-key="' + key + '"]');
|
|
6388
|
+
if (inp) {
|
|
6389
|
+
inp.type = 'text';
|
|
6390
|
+
inp.value = value;
|
|
6391
|
+
handleChange(inp);
|
|
6392
|
+
}
|
|
6393
|
+
setFeedback(key, 'success', '\u2713 Secret generated \u2014 save to persist');
|
|
6394
|
+
} catch (e) {
|
|
6395
|
+
setFeedback(key, 'error', e.message);
|
|
6396
|
+
}
|
|
6397
|
+
}
|
|
6253
6398
|
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6399
|
+
/* \u2500\u2500 Modal \u2500\u2500 */
|
|
6400
|
+
function openModal(ok, msg) {
|
|
6401
|
+
const box = document.getElementById('testResultBox');
|
|
6402
|
+
box.className = 'modal-result ' + (ok ? 'ok' : 'fail');
|
|
6403
|
+
box.textContent = (ok ? '\u2713 ' : '\u2717 ') + msg;
|
|
6404
|
+
document.getElementById('testModal').classList.add('open');
|
|
6405
|
+
}
|
|
6406
|
+
function closeModal() {
|
|
6407
|
+
document.getElementById('testModal').classList.remove('open');
|
|
6408
|
+
}
|
|
6409
|
+
|
|
6410
|
+
/* \u2500\u2500 Toast \u2500\u2500 */
|
|
6411
|
+
function toast(type, msg) {
|
|
6412
|
+
const zone = document.getElementById('toastZone');
|
|
6413
|
+
const el = document.createElement('div');
|
|
6414
|
+
el.className = 'toast ' + type;
|
|
6415
|
+
el.textContent = msg;
|
|
6416
|
+
zone.appendChild(el);
|
|
6417
|
+
setTimeout(() => el.remove(), 4500);
|
|
6418
|
+
}
|
|
6419
|
+
|
|
6420
|
+
/* \u2500\u2500 Feedback \u2500\u2500 */
|
|
6421
|
+
function setFeedback(key, type, msg) {
|
|
6422
|
+
const el = document.getElementById('fb-' + key);
|
|
6423
|
+
if (!el) return;
|
|
6424
|
+
el.className = 'env-feedback' + (type ? ' ' + type : '');
|
|
6425
|
+
el.textContent = msg;
|
|
6426
|
+
}
|
|
6427
|
+
function clearFeedback(key) { setFeedback(key, '', ''); }
|
|
6428
|
+
|
|
6429
|
+
/* \u2500\u2500 Helpers \u2500\u2500 */
|
|
6430
|
+
function escHtml(s) {
|
|
6431
|
+
return String(s).replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c]));
|
|
6432
|
+
}
|
|
6433
|
+
|
|
6434
|
+
/* \u2500\u2500 Wire save button \u2500\u2500 */
|
|
6435
|
+
document.getElementById('saveBtn').addEventListener('click', saveChanges);
|
|
6436
|
+
|
|
6437
|
+
/* \u2500\u2500 Close modal on backdrop click \u2500\u2500 */
|
|
6438
|
+
document.getElementById('testModal').addEventListener('click', function(e) {
|
|
6439
|
+
if (e.target === this) closeModal();
|
|
6440
|
+
});
|
|
6441
|
+
|
|
6442
|
+
/* \u2500\u2500 Boot \u2500\u2500 */
|
|
6443
|
+
init();
|
|
6444
|
+
</script>
|
|
6257
6445
|
</body>
|
|
6258
6446
|
</html>`;
|
|
6259
6447
|
}
|