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