@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/env.html.js
CHANGED
|
@@ -5,672 +5,860 @@ function getEnvHTML() {
|
|
|
5
5
|
<head>
|
|
6
6
|
<meta charset="UTF-8">
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
-
<title>
|
|
8
|
+
<title>OpenQA \u2014 Environment</title>
|
|
9
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
10
|
+
<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">
|
|
9
11
|
<style>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #080b10;
|
|
14
|
+
--surface: #0d1117;
|
|
15
|
+
--panel: #111720;
|
|
16
|
+
--border: rgba(255,255,255,0.06);
|
|
17
|
+
--border-hi: rgba(255,255,255,0.12);
|
|
18
|
+
--accent: #f97316;
|
|
19
|
+
--accent-lo: rgba(249,115,22,0.08);
|
|
20
|
+
--accent-md: rgba(249,115,22,0.18);
|
|
21
|
+
--green: #22c55e;
|
|
22
|
+
--green-lo: rgba(34,197,94,0.08);
|
|
23
|
+
--red: #ef4444;
|
|
24
|
+
--red-lo: rgba(239,68,68,0.08);
|
|
25
|
+
--amber: #f59e0b;
|
|
26
|
+
--amber-lo: rgba(245,158,11,0.08);
|
|
27
|
+
--blue: #38bdf8;
|
|
28
|
+
--blue-lo: rgba(56,189,248,0.08);
|
|
29
|
+
--text-1: #f1f5f9;
|
|
30
|
+
--text-2: #8b98a8;
|
|
31
|
+
--text-3: #4b5563;
|
|
32
|
+
--mono: 'DM Mono', monospace;
|
|
33
|
+
--sans: 'Syne', sans-serif;
|
|
34
|
+
--radius: 10px;
|
|
35
|
+
--radius-lg: 16px;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
39
|
+
|
|
12
40
|
body {
|
|
13
|
-
font-family:
|
|
14
|
-
background:
|
|
41
|
+
font-family: var(--sans);
|
|
42
|
+
background: var(--bg);
|
|
43
|
+
color: var(--text-1);
|
|
15
44
|
min-height: 100vh;
|
|
16
|
-
|
|
45
|
+
overflow-x: hidden;
|
|
17
46
|
}
|
|
18
47
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
48
|
+
/* \u2500\u2500 Layout \u2500\u2500 */
|
|
49
|
+
.shell {
|
|
50
|
+
display: grid;
|
|
51
|
+
grid-template-columns: 220px 1fr;
|
|
52
|
+
min-height: 100vh;
|
|
22
53
|
}
|
|
23
54
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
border-radius: 12px;
|
|
29
|
-
margin-bottom: 20px;
|
|
30
|
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
55
|
+
/* \u2500\u2500 Sidebar \u2500\u2500 */
|
|
56
|
+
aside {
|
|
57
|
+
background: var(--surface);
|
|
58
|
+
border-right: 1px solid var(--border);
|
|
31
59
|
display: flex;
|
|
32
|
-
|
|
33
|
-
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
padding: 28px 0;
|
|
62
|
+
position: sticky;
|
|
63
|
+
top: 0;
|
|
64
|
+
height: 100vh;
|
|
34
65
|
}
|
|
35
66
|
|
|
36
|
-
.
|
|
37
|
-
font-size: 24px;
|
|
38
|
-
color: #1a202c;
|
|
67
|
+
.logo {
|
|
39
68
|
display: flex;
|
|
40
69
|
align-items: center;
|
|
41
70
|
gap: 10px;
|
|
71
|
+
padding: 0 24px 32px;
|
|
72
|
+
border-bottom: 1px solid var(--border);
|
|
73
|
+
margin-bottom: 12px;
|
|
42
74
|
}
|
|
43
75
|
|
|
44
|
-
.
|
|
45
|
-
|
|
46
|
-
|
|
76
|
+
.logo-mark {
|
|
77
|
+
width: 34px; height: 34px;
|
|
78
|
+
background: var(--accent);
|
|
79
|
+
border-radius: 8px;
|
|
80
|
+
display: grid;
|
|
81
|
+
place-items: center;
|
|
82
|
+
font-size: 17px;
|
|
83
|
+
font-weight: 800;
|
|
84
|
+
color: #fff;
|
|
47
85
|
}
|
|
48
86
|
|
|
49
|
-
.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
87
|
+
.logo-name { font-weight: 800; font-size: 18px; letter-spacing: -0.5px; }
|
|
88
|
+
.logo-version { font-family: var(--mono); font-size: 10px; color: var(--text-3); }
|
|
89
|
+
|
|
90
|
+
.nav-section { padding: 8px 12px; flex: 1; overflow-y: auto; }
|
|
91
|
+
|
|
92
|
+
.nav-label {
|
|
93
|
+
font-family: var(--mono);
|
|
94
|
+
font-size: 10px;
|
|
95
|
+
color: var(--text-3);
|
|
96
|
+
letter-spacing: 1.5px;
|
|
97
|
+
text-transform: uppercase;
|
|
98
|
+
padding: 0 12px;
|
|
99
|
+
margin: 16px 0 6px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.nav-item {
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
gap: 10px;
|
|
106
|
+
padding: 9px 12px;
|
|
107
|
+
border-radius: var(--radius);
|
|
108
|
+
color: var(--text-2);
|
|
109
|
+
text-decoration: none;
|
|
53
110
|
font-size: 14px;
|
|
54
111
|
font-weight: 600;
|
|
112
|
+
transition: all 0.15s ease;
|
|
55
113
|
cursor: pointer;
|
|
56
|
-
transition: all 0.2s;
|
|
57
|
-
text-decoration: none;
|
|
58
|
-
display: inline-flex;
|
|
59
|
-
align-items: center;
|
|
60
|
-
gap: 8px;
|
|
61
114
|
}
|
|
115
|
+
.nav-item:hover { color: var(--text-1); background: var(--panel); }
|
|
116
|
+
.nav-item.active { color: var(--accent); background: var(--accent-lo); }
|
|
117
|
+
.nav-item .icon { font-size: 15px; width: 20px; text-align: center; }
|
|
62
118
|
|
|
63
|
-
.
|
|
64
|
-
|
|
65
|
-
|
|
119
|
+
.sidebar-footer {
|
|
120
|
+
padding: 16px 24px;
|
|
121
|
+
border-top: 1px solid var(--border);
|
|
66
122
|
}
|
|
67
123
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
transform: translateY(-1px);
|
|
71
|
-
}
|
|
124
|
+
/* \u2500\u2500 Main \u2500\u2500 */
|
|
125
|
+
main { display: flex; flex-direction: column; min-height: 100vh; overflow-y: auto; }
|
|
72
126
|
|
|
73
|
-
.
|
|
74
|
-
|
|
75
|
-
|
|
127
|
+
.topbar {
|
|
128
|
+
display: flex;
|
|
129
|
+
align-items: center;
|
|
130
|
+
justify-content: space-between;
|
|
131
|
+
padding: 20px 32px;
|
|
132
|
+
border-bottom: 1px solid var(--border);
|
|
133
|
+
background: var(--surface);
|
|
134
|
+
position: sticky;
|
|
135
|
+
top: 0;
|
|
136
|
+
z-index: 10;
|
|
76
137
|
}
|
|
77
138
|
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
}
|
|
139
|
+
.page-title { font-size: 15px; font-weight: 700; letter-spacing: -0.2px; }
|
|
140
|
+
.page-sub { font-family: var(--mono); font-size: 11px; color: var(--text-3); margin-top: 2px; }
|
|
81
141
|
|
|
82
|
-
.
|
|
83
|
-
background: #48bb78;
|
|
84
|
-
color: white;
|
|
85
|
-
}
|
|
142
|
+
.topbar-actions { display: flex; align-items: center; gap: 10px; }
|
|
86
143
|
|
|
87
|
-
.btn
|
|
88
|
-
|
|
144
|
+
.btn {
|
|
145
|
+
font-family: var(--sans);
|
|
146
|
+
font-weight: 700;
|
|
147
|
+
font-size: 12px;
|
|
148
|
+
padding: 8px 16px;
|
|
149
|
+
border-radius: 8px;
|
|
150
|
+
border: none;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
transition: all 0.15s ease;
|
|
153
|
+
display: inline-flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
gap: 6px;
|
|
156
|
+
text-decoration: none;
|
|
89
157
|
}
|
|
158
|
+
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
90
159
|
|
|
91
|
-
.btn
|
|
92
|
-
|
|
93
|
-
|
|
160
|
+
.btn-ghost {
|
|
161
|
+
background: var(--panel);
|
|
162
|
+
color: var(--text-2);
|
|
163
|
+
border: 1px solid var(--border);
|
|
94
164
|
}
|
|
165
|
+
.btn-ghost:hover { border-color: var(--border-hi); color: var(--text-1); }
|
|
95
166
|
|
|
96
|
-
.
|
|
97
|
-
background:
|
|
98
|
-
|
|
99
|
-
padding: 30px;
|
|
100
|
-
border-radius: 12px;
|
|
101
|
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
167
|
+
.btn-primary {
|
|
168
|
+
background: var(--accent);
|
|
169
|
+
color: #fff;
|
|
102
170
|
}
|
|
171
|
+
.btn-primary:hover:not(:disabled) { background: #ea580c; box-shadow: 0 0 20px rgba(249,115,22,0.35); }
|
|
172
|
+
|
|
173
|
+
/* \u2500\u2500 Content \u2500\u2500 */
|
|
174
|
+
.content { padding: 28px 32px; display: flex; flex-direction: column; gap: 24px; }
|
|
103
175
|
|
|
104
|
-
|
|
176
|
+
/* \u2500\u2500 Tabs (category selector) \u2500\u2500 */
|
|
177
|
+
.tab-bar {
|
|
105
178
|
display: flex;
|
|
106
|
-
gap:
|
|
107
|
-
|
|
108
|
-
border
|
|
109
|
-
|
|
179
|
+
gap: 4px;
|
|
180
|
+
background: var(--surface);
|
|
181
|
+
border: 1px solid var(--border);
|
|
182
|
+
border-radius: 10px;
|
|
183
|
+
padding: 4px;
|
|
184
|
+
flex-wrap: wrap;
|
|
110
185
|
}
|
|
111
186
|
|
|
112
|
-
.tab {
|
|
113
|
-
padding:
|
|
187
|
+
.tab-btn {
|
|
188
|
+
padding: 7px 14px;
|
|
189
|
+
background: transparent;
|
|
114
190
|
border: none;
|
|
115
|
-
|
|
116
|
-
|
|
191
|
+
border-radius: 7px;
|
|
192
|
+
color: var(--text-3);
|
|
193
|
+
font-family: var(--sans);
|
|
194
|
+
font-size: 12px;
|
|
117
195
|
font-weight: 600;
|
|
118
|
-
color: #718096;
|
|
119
196
|
cursor: pointer;
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
color: #667eea;
|
|
126
|
-
border-bottom-color: #667eea;
|
|
197
|
+
transition: all 0.15s ease;
|
|
198
|
+
white-space: nowrap;
|
|
199
|
+
display: flex;
|
|
200
|
+
align-items: center;
|
|
201
|
+
gap: 5px;
|
|
127
202
|
}
|
|
128
|
-
|
|
129
|
-
.tab
|
|
130
|
-
|
|
203
|
+
.tab-btn:hover { color: var(--text-2); }
|
|
204
|
+
.tab-btn.active {
|
|
205
|
+
background: var(--panel);
|
|
206
|
+
color: var(--text-1);
|
|
207
|
+
border: 1px solid var(--border-hi);
|
|
131
208
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
209
|
+
.tab-btn .tab-dot {
|
|
210
|
+
width: 6px; height: 6px;
|
|
211
|
+
border-radius: 50%;
|
|
212
|
+
background: var(--text-3);
|
|
135
213
|
}
|
|
214
|
+
.tab-btn.has-required .tab-dot { background: var(--amber); }
|
|
215
|
+
.tab-btn.active .tab-dot { background: var(--accent); }
|
|
136
216
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
217
|
+
/* \u2500\u2500 Section \u2500\u2500 */
|
|
218
|
+
.section { display: none; flex-direction: column; gap: 16px; }
|
|
219
|
+
.section.active { display: flex; }
|
|
140
220
|
|
|
141
|
-
.
|
|
221
|
+
.section-header {
|
|
142
222
|
display: flex;
|
|
143
|
-
justify-content: space-between;
|
|
144
223
|
align-items: center;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
.category-title {
|
|
149
|
-
font-size: 18px;
|
|
150
|
-
font-weight: 600;
|
|
151
|
-
color: #2d3748;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
.env-grid {
|
|
155
|
-
display: grid;
|
|
156
|
-
gap: 20px;
|
|
224
|
+
gap: 12px;
|
|
225
|
+
margin-bottom: 4px;
|
|
157
226
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
227
|
+
.section-icon {
|
|
228
|
+
width: 36px; height: 36px;
|
|
229
|
+
background: var(--accent-lo);
|
|
230
|
+
border: 1px solid var(--accent-md);
|
|
161
231
|
border-radius: 8px;
|
|
162
|
-
|
|
163
|
-
|
|
232
|
+
display: grid;
|
|
233
|
+
place-items: center;
|
|
234
|
+
font-size: 16px;
|
|
164
235
|
}
|
|
236
|
+
.section-title { font-size: 15px; font-weight: 700; }
|
|
237
|
+
.section-desc { font-family: var(--mono); font-size: 11px; color: var(--text-3); margin-top: 2px; }
|
|
165
238
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
239
|
+
/* \u2500\u2500 Env card \u2500\u2500 */
|
|
240
|
+
.env-card {
|
|
241
|
+
background: var(--panel);
|
|
242
|
+
border: 1px solid var(--border);
|
|
243
|
+
border-radius: var(--radius-lg);
|
|
244
|
+
padding: 20px 24px;
|
|
245
|
+
transition: border-color 0.15s;
|
|
169
246
|
}
|
|
247
|
+
.env-card:hover { border-color: var(--border-hi); }
|
|
248
|
+
.env-card.has-value { border-color: rgba(249,115,22,0.15); }
|
|
170
249
|
|
|
171
|
-
.env-
|
|
250
|
+
.env-card-head {
|
|
172
251
|
display: flex;
|
|
173
252
|
justify-content: space-between;
|
|
174
253
|
align-items: flex-start;
|
|
175
|
-
margin-bottom:
|
|
254
|
+
margin-bottom: 6px;
|
|
176
255
|
}
|
|
177
256
|
|
|
178
|
-
.env-
|
|
179
|
-
font-
|
|
180
|
-
|
|
181
|
-
font-
|
|
257
|
+
.env-key {
|
|
258
|
+
font-family: var(--mono);
|
|
259
|
+
font-size: 13px;
|
|
260
|
+
font-weight: 500;
|
|
261
|
+
color: var(--text-1);
|
|
182
262
|
display: flex;
|
|
183
263
|
align-items: center;
|
|
184
264
|
gap: 8px;
|
|
185
265
|
}
|
|
186
266
|
|
|
187
|
-
.required
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
font-
|
|
191
|
-
|
|
267
|
+
.badge-required {
|
|
268
|
+
font-family: var(--sans);
|
|
269
|
+
font-size: 9px;
|
|
270
|
+
font-weight: 700;
|
|
271
|
+
letter-spacing: 0.08em;
|
|
272
|
+
text-transform: uppercase;
|
|
273
|
+
background: rgba(239,68,68,0.15);
|
|
274
|
+
color: var(--red);
|
|
275
|
+
border: 1px solid rgba(239,68,68,0.25);
|
|
192
276
|
border-radius: 4px;
|
|
277
|
+
padding: 2px 6px;
|
|
278
|
+
}
|
|
279
|
+
.badge-sensitive {
|
|
280
|
+
font-size: 9px;
|
|
193
281
|
font-weight: 700;
|
|
282
|
+
font-family: var(--sans);
|
|
283
|
+
letter-spacing: 0.08em;
|
|
284
|
+
text-transform: uppercase;
|
|
285
|
+
background: var(--amber-lo);
|
|
286
|
+
color: var(--amber);
|
|
287
|
+
border: 1px solid rgba(245,158,11,0.2);
|
|
288
|
+
border-radius: 4px;
|
|
289
|
+
padding: 2px 6px;
|
|
194
290
|
}
|
|
195
291
|
|
|
196
|
-
.env-
|
|
197
|
-
font-
|
|
198
|
-
|
|
199
|
-
|
|
292
|
+
.env-desc {
|
|
293
|
+
font-family: var(--mono);
|
|
294
|
+
font-size: 11px;
|
|
295
|
+
color: var(--text-3);
|
|
296
|
+
margin-bottom: 14px;
|
|
297
|
+
line-height: 1.5;
|
|
200
298
|
}
|
|
201
299
|
|
|
202
|
-
.env-input-
|
|
300
|
+
.env-input-row {
|
|
203
301
|
display: flex;
|
|
204
|
-
gap:
|
|
302
|
+
gap: 8px;
|
|
205
303
|
align-items: center;
|
|
206
304
|
}
|
|
207
305
|
|
|
208
|
-
.env-input {
|
|
306
|
+
.env-input, .env-select {
|
|
209
307
|
flex: 1;
|
|
210
|
-
|
|
211
|
-
border: 1px solid
|
|
212
|
-
border-radius:
|
|
213
|
-
|
|
214
|
-
font-family:
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
.env-input:focus {
|
|
308
|
+
background: var(--surface);
|
|
309
|
+
border: 1px solid var(--border-hi);
|
|
310
|
+
border-radius: 8px;
|
|
311
|
+
padding: 10px 14px;
|
|
312
|
+
font-family: var(--mono);
|
|
313
|
+
font-size: 13px;
|
|
314
|
+
color: var(--text-1);
|
|
219
315
|
outline: none;
|
|
220
|
-
border-color
|
|
221
|
-
|
|
316
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
317
|
+
appearance: none;
|
|
222
318
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
319
|
+
.env-input:focus, .env-select:focus {
|
|
320
|
+
border-color: var(--accent);
|
|
321
|
+
box-shadow: 0 0 0 3px rgba(249,115,22,0.12);
|
|
226
322
|
}
|
|
323
|
+
.env-input.changed { border-color: rgba(249,115,22,0.5); }
|
|
324
|
+
.env-input.invalid { border-color: var(--red); }
|
|
227
325
|
|
|
228
|
-
.env-
|
|
229
|
-
display: flex;
|
|
230
|
-
gap: 5px;
|
|
231
|
-
}
|
|
326
|
+
.env-select option { background: var(--panel); }
|
|
232
327
|
|
|
233
|
-
.
|
|
234
|
-
|
|
235
|
-
border:
|
|
236
|
-
|
|
237
|
-
|
|
328
|
+
.env-action-btn {
|
|
329
|
+
width: 36px; height: 36px;
|
|
330
|
+
border-radius: 8px;
|
|
331
|
+
border: 1px solid var(--border-hi);
|
|
332
|
+
background: var(--surface);
|
|
333
|
+
color: var(--text-2);
|
|
238
334
|
cursor: pointer;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
background: #cbd5e0;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
.icon-btn.test {
|
|
248
|
-
background: #bee3f8;
|
|
249
|
-
color: #2c5282;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
.icon-btn.test:hover {
|
|
253
|
-
background: #90cdf4;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
.icon-btn.generate {
|
|
257
|
-
background: #c6f6d5;
|
|
258
|
-
color: #22543d;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
.icon-btn.generate:hover {
|
|
262
|
-
background: #9ae6b4;
|
|
335
|
+
display: grid;
|
|
336
|
+
place-items: center;
|
|
337
|
+
font-size: 14px;
|
|
338
|
+
transition: all 0.15s;
|
|
339
|
+
flex-shrink: 0;
|
|
263
340
|
}
|
|
341
|
+
.env-action-btn:hover { background: var(--panel); color: var(--text-1); border-color: var(--border-hi); }
|
|
342
|
+
.env-action-btn.test-btn:hover { background: var(--blue-lo); color: var(--blue); border-color: rgba(56,189,248,0.25); }
|
|
343
|
+
.env-action-btn.gen-btn:hover { background: var(--green-lo); color: var(--green); border-color: rgba(34,197,94,0.25); }
|
|
264
344
|
|
|
265
|
-
.
|
|
266
|
-
|
|
267
|
-
font-size:
|
|
268
|
-
margin-top:
|
|
345
|
+
.env-feedback {
|
|
346
|
+
font-family: var(--mono);
|
|
347
|
+
font-size: 11px;
|
|
348
|
+
margin-top: 8px;
|
|
349
|
+
min-height: 16px;
|
|
269
350
|
}
|
|
351
|
+
.env-feedback.error { color: var(--red); }
|
|
352
|
+
.env-feedback.success { color: var(--green); }
|
|
270
353
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
354
|
+
/* \u2500\u2500 Toast \u2500\u2500 */
|
|
355
|
+
.toast-zone {
|
|
356
|
+
position: fixed;
|
|
357
|
+
bottom: 24px;
|
|
358
|
+
right: 24px;
|
|
359
|
+
display: flex;
|
|
360
|
+
flex-direction: column;
|
|
361
|
+
gap: 8px;
|
|
362
|
+
z-index: 100;
|
|
275
363
|
}
|
|
276
364
|
|
|
277
|
-
.
|
|
278
|
-
padding:
|
|
279
|
-
border-radius:
|
|
280
|
-
|
|
365
|
+
.toast {
|
|
366
|
+
padding: 12px 18px;
|
|
367
|
+
border-radius: 10px;
|
|
368
|
+
font-size: 13px;
|
|
369
|
+
font-weight: 600;
|
|
281
370
|
display: flex;
|
|
282
371
|
align-items: center;
|
|
283
372
|
gap: 10px;
|
|
373
|
+
animation: slideIn 0.2s ease;
|
|
374
|
+
max-width: 380px;
|
|
284
375
|
}
|
|
376
|
+
.toast.success { background: var(--panel); border: 1px solid rgba(34,197,94,0.3); color: var(--green); }
|
|
377
|
+
.toast.error { background: var(--panel); border: 1px solid rgba(239,68,68,0.3); color: var(--red); }
|
|
378
|
+
.toast.warning { background: var(--panel); border: 1px solid rgba(245,158,11,0.3); color: var(--amber); }
|
|
379
|
+
.toast.info { background: var(--panel); border: 1px solid rgba(56,189,248,0.3); color: var(--blue); }
|
|
285
380
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
color: #92400e;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
.alert-info {
|
|
293
|
-
background: #eff6ff;
|
|
294
|
-
border-left: 4px solid #3b82f6;
|
|
295
|
-
color: #1e40af;
|
|
381
|
+
@keyframes slideIn {
|
|
382
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
383
|
+
to { opacity: 1; transform: translateY(0); }
|
|
296
384
|
}
|
|
297
385
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
386
|
+
/* \u2500\u2500 Modal (test result) \u2500\u2500 */
|
|
387
|
+
.modal-backdrop {
|
|
388
|
+
display: none;
|
|
389
|
+
position: fixed; inset: 0;
|
|
390
|
+
background: rgba(0,0,0,0.6);
|
|
391
|
+
z-index: 200;
|
|
392
|
+
align-items: center;
|
|
393
|
+
justify-content: center;
|
|
302
394
|
}
|
|
395
|
+
.modal-backdrop.open { display: flex; }
|
|
303
396
|
|
|
304
|
-
.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
397
|
+
.modal {
|
|
398
|
+
background: var(--surface);
|
|
399
|
+
border: 1px solid var(--border-hi);
|
|
400
|
+
border-radius: var(--radius-lg);
|
|
401
|
+
padding: 28px;
|
|
402
|
+
width: 420px;
|
|
403
|
+
max-width: 90vw;
|
|
404
|
+
box-shadow: 0 24px 64px rgba(0,0,0,0.5);
|
|
405
|
+
}
|
|
406
|
+
.modal-title { font-size: 15px; font-weight: 700; margin-bottom: 16px; }
|
|
407
|
+
.modal-body { margin-bottom: 20px; }
|
|
408
|
+
.modal-result {
|
|
409
|
+
padding: 14px;
|
|
410
|
+
border-radius: 8px;
|
|
411
|
+
font-family: var(--mono);
|
|
412
|
+
font-size: 12px;
|
|
308
413
|
}
|
|
414
|
+
.modal-result.ok { background: var(--green-lo); border: 1px solid rgba(34,197,94,0.2); color: var(--green); }
|
|
415
|
+
.modal-result.fail { background: var(--red-lo); border: 1px solid rgba(239,68,68,0.2); color: var(--red); }
|
|
416
|
+
.modal-footer { display: flex; justify-content: flex-end; }
|
|
309
417
|
|
|
418
|
+
/* \u2500\u2500 Spinner \u2500\u2500 */
|
|
310
419
|
.spinner {
|
|
311
|
-
|
|
312
|
-
border
|
|
420
|
+
width: 36px; height: 36px;
|
|
421
|
+
border: 3px solid var(--border);
|
|
422
|
+
border-top-color: var(--accent);
|
|
313
423
|
border-radius: 50%;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
animation: spin 1s linear infinite;
|
|
317
|
-
margin: 0 auto 20px;
|
|
424
|
+
animation: spin 0.8s linear infinite;
|
|
425
|
+
margin: 0 auto 16px;
|
|
318
426
|
}
|
|
427
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
319
428
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
429
|
+
.loading-state {
|
|
430
|
+
text-align: center;
|
|
431
|
+
padding: 60px 0;
|
|
432
|
+
color: var(--text-3);
|
|
433
|
+
font-family: var(--mono);
|
|
434
|
+
font-size: 12px;
|
|
323
435
|
}
|
|
324
436
|
|
|
325
|
-
|
|
437
|
+
/* \u2500\u2500 Restart banner \u2500\u2500 */
|
|
438
|
+
.restart-banner {
|
|
326
439
|
display: none;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
z-index: 1000;
|
|
334
|
-
align-items: center;
|
|
335
|
-
justify-content: center;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
.modal.show {
|
|
339
|
-
display: flex;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
.modal-content {
|
|
343
|
-
background: white;
|
|
344
|
-
padding: 30px;
|
|
345
|
-
border-radius: 12px;
|
|
346
|
-
max-width: 500px;
|
|
347
|
-
width: 90%;
|
|
348
|
-
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
.modal-header {
|
|
352
|
-
font-size: 20px;
|
|
440
|
+
background: var(--amber-lo);
|
|
441
|
+
border: 1px solid rgba(245,158,11,0.25);
|
|
442
|
+
border-radius: 10px;
|
|
443
|
+
padding: 12px 18px;
|
|
444
|
+
font-size: 13px;
|
|
445
|
+
color: var(--amber);
|
|
353
446
|
font-weight: 600;
|
|
354
|
-
|
|
355
|
-
color: #2d3748;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
.modal-body {
|
|
359
|
-
margin-bottom: 20px;
|
|
360
|
-
color: #4a5568;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
.modal-footer {
|
|
364
|
-
display: flex;
|
|
447
|
+
align-items: center;
|
|
365
448
|
gap: 10px;
|
|
366
|
-
justify-content: flex-end;
|
|
367
449
|
}
|
|
450
|
+
.restart-banner.show { display: flex; }
|
|
368
451
|
</style>
|
|
369
452
|
</head>
|
|
370
453
|
<body>
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
454
|
+
<div class="shell">
|
|
455
|
+
|
|
456
|
+
<!-- Sidebar -->
|
|
457
|
+
<aside>
|
|
458
|
+
<div class="logo">
|
|
459
|
+
<div class="logo-mark">Q</div>
|
|
460
|
+
<div>
|
|
461
|
+
<div class="logo-name">OpenQA</div>
|
|
462
|
+
<div class="logo-version">v1.3.4</div>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
<div class="nav-section">
|
|
467
|
+
<div class="nav-label">Overview</div>
|
|
468
|
+
<a class="nav-item" href="/">
|
|
469
|
+
<span class="icon">\u{1F4CA}</span> Dashboard
|
|
470
|
+
</a>
|
|
471
|
+
<a class="nav-item" href="/sessions">
|
|
472
|
+
<span class="icon">\u{1F9EA}</span> Sessions
|
|
473
|
+
</a>
|
|
474
|
+
<a class="nav-item" href="/issues">
|
|
475
|
+
<span class="icon">\u{1F41B}</span> Issues
|
|
476
|
+
</a>
|
|
477
|
+
|
|
478
|
+
<div class="nav-label">Testing</div>
|
|
479
|
+
<a class="nav-item" href="/tests">
|
|
480
|
+
<span class="icon">\u26A1</span> Tests
|
|
481
|
+
</a>
|
|
482
|
+
<a class="nav-item" href="/coverage">
|
|
483
|
+
<span class="icon">\u{1F4C8}</span> Coverage
|
|
484
|
+
</a>
|
|
485
|
+
<a class="nav-item" href="/kanban">
|
|
486
|
+
<span class="icon">\u{1F4CB}</span> Kanban
|
|
487
|
+
</a>
|
|
488
|
+
|
|
489
|
+
<div class="nav-label">System</div>
|
|
490
|
+
<a class="nav-item" href="/config">
|
|
491
|
+
<span class="icon">\u2699\uFE0F</span> Config
|
|
492
|
+
</a>
|
|
493
|
+
<a class="nav-item active" href="/config/env">
|
|
494
|
+
<span class="icon">\u{1F527}</span> Environment
|
|
495
|
+
</a>
|
|
496
|
+
<a class="nav-item" href="/logs">
|
|
497
|
+
<span class="icon">\u{1F4DC}</span> Logs
|
|
498
|
+
</a>
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
<div class="sidebar-footer">
|
|
502
|
+
<div style="font-family:var(--mono);font-size:11px;color:var(--text-3);">
|
|
375
503
|
Environment Variables
|
|
376
|
-
</
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
</aside>
|
|
507
|
+
|
|
508
|
+
<!-- Main -->
|
|
509
|
+
<main>
|
|
510
|
+
<div class="topbar">
|
|
511
|
+
<div>
|
|
512
|
+
<div class="page-title">Environment Variables</div>
|
|
513
|
+
<div class="page-sub">Configure runtime variables for OpenQA</div>
|
|
514
|
+
</div>
|
|
515
|
+
<div class="topbar-actions">
|
|
516
|
+
<a class="btn btn-ghost" href="/config">\u2190 Back to Config</a>
|
|
517
|
+
<button id="saveBtn" class="btn btn-primary" disabled>
|
|
518
|
+
\u{1F4BE} Save Changes
|
|
519
|
+
</button>
|
|
380
520
|
</div>
|
|
381
521
|
</div>
|
|
382
522
|
|
|
383
523
|
<div class="content">
|
|
384
|
-
|
|
524
|
+
|
|
525
|
+
<!-- Restart banner -->
|
|
526
|
+
<div class="restart-banner" id="restartBanner">
|
|
527
|
+
\u26A0\uFE0F Some changes require a server restart to take effect.
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
<!-- Loading -->
|
|
531
|
+
<div class="loading-state" id="loadingState">
|
|
385
532
|
<div class="spinner"></div>
|
|
386
|
-
|
|
533
|
+
Loading environment variables\u2026
|
|
387
534
|
</div>
|
|
388
535
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
<button class="tab" data-category="web">\u{1F310} Web Server</button>
|
|
398
|
-
<button class="tab" data-category="agent">\u{1F916} Agent</button>
|
|
399
|
-
<button class="tab" data-category="database">\u{1F4BE} Database</button>
|
|
400
|
-
<button class="tab" data-category="notifications">\u{1F514} Notifications</button>
|
|
401
|
-
</div>
|
|
536
|
+
<!-- Main content (hidden while loading) -->
|
|
537
|
+
<div id="mainContent" style="display:none;flex-direction:column;gap:24px;">
|
|
538
|
+
|
|
539
|
+
<!-- Tab bar -->
|
|
540
|
+
<div class="tab-bar" id="tabBar"></div>
|
|
541
|
+
|
|
542
|
+
<!-- Sections -->
|
|
543
|
+
<div id="sections"></div>
|
|
402
544
|
|
|
403
|
-
<div id="categories"></div>
|
|
404
545
|
</div>
|
|
405
546
|
</div>
|
|
547
|
+
</main>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<!-- Test result modal -->
|
|
551
|
+
<div class="modal-backdrop" id="testModal">
|
|
552
|
+
<div class="modal">
|
|
553
|
+
<div class="modal-title">Connection Test</div>
|
|
554
|
+
<div class="modal-body">
|
|
555
|
+
<div class="modal-result" id="testResultBox">\u2026</div>
|
|
556
|
+
</div>
|
|
557
|
+
<div class="modal-footer">
|
|
558
|
+
<button class="btn btn-ghost" onclick="closeModal()">Close</button>
|
|
559
|
+
</div>
|
|
406
560
|
</div>
|
|
561
|
+
</div>
|
|
562
|
+
|
|
563
|
+
<!-- Toast zone -->
|
|
564
|
+
<div class="toast-zone" id="toastZone"></div>
|
|
565
|
+
|
|
566
|
+
<script>
|
|
567
|
+
/* \u2500\u2500 State \u2500\u2500 */
|
|
568
|
+
let envVars = [];
|
|
569
|
+
let changed = {};
|
|
570
|
+
let hasRequiredMissing = false;
|
|
571
|
+
|
|
572
|
+
const TABS = [
|
|
573
|
+
{ id: 'llm', label: '\u{1F916} LLM', desc: 'Language model provider & API keys' },
|
|
574
|
+
{ id: 'security', label: '\u{1F512} Security', desc: 'Authentication & JWT configuration' },
|
|
575
|
+
{ id: 'target', label: '\u{1F3AF} Target App', desc: 'Application under test settings' },
|
|
576
|
+
{ id: 'github', label: '\u{1F419} GitHub', desc: 'Repository & CI/CD integration' },
|
|
577
|
+
{ id: 'web', label: '\u{1F310} Web Server', desc: 'HTTP host, port & CORS settings' },
|
|
578
|
+
{ id: 'agent', label: '\u{1F916} Agent', desc: 'Autonomous agent behaviour' },
|
|
579
|
+
{ id: 'database', label: '\u{1F4BE} Database', desc: 'Persistence & storage' },
|
|
580
|
+
{ id: 'notifications', label: '\u{1F514} Notifications', desc: 'Slack & Discord webhooks' },
|
|
581
|
+
];
|
|
582
|
+
|
|
583
|
+
/* \u2500\u2500 Init \u2500\u2500 */
|
|
584
|
+
async function init() {
|
|
585
|
+
try {
|
|
586
|
+
const res = await fetch('/api/env');
|
|
587
|
+
if (!res.ok) { toast('error', 'Failed to load environment variables (status ' + res.status + ')'); return; }
|
|
588
|
+
const data = await res.json();
|
|
589
|
+
envVars = data.variables || [];
|
|
590
|
+
renderAll();
|
|
591
|
+
document.getElementById('loadingState').style.display = 'none';
|
|
592
|
+
const mc = document.getElementById('mainContent');
|
|
593
|
+
mc.style.display = 'flex';
|
|
594
|
+
} catch (e) {
|
|
595
|
+
toast('error', 'Network error \u2014 ' + e.message);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
407
598
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
599
|
+
/* \u2500\u2500 Render \u2500\u2500 */
|
|
600
|
+
function renderAll() {
|
|
601
|
+
renderTabBar();
|
|
602
|
+
renderSections();
|
|
603
|
+
activateTab(TABS[0].id);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function renderTabBar() {
|
|
607
|
+
const bar = document.getElementById('tabBar');
|
|
608
|
+
bar.innerHTML = TABS.map(t => {
|
|
609
|
+
const vars = envVars.filter(v => v.category === t.id);
|
|
610
|
+
const hasRequired = vars.some(v => v.required);
|
|
611
|
+
return \`<button class="tab-btn\${hasRequired ? ' has-required' : ''}" data-tab="\${t.id}" onclick="activateTab('\${t.id}')">
|
|
612
|
+
<span class="tab-dot"></span>
|
|
613
|
+
\${t.label}
|
|
614
|
+
</button>\`;
|
|
615
|
+
}).join('');
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function renderSections() {
|
|
619
|
+
const container = document.getElementById('sections');
|
|
620
|
+
container.innerHTML = TABS.map(t => {
|
|
621
|
+
const vars = envVars.filter(v => v.category === t.id);
|
|
622
|
+
return \`<div class="section" id="section-\${t.id}">
|
|
623
|
+
<div class="section-header">
|
|
624
|
+
<div class="section-icon">\${t.label.split(' ')[0]}</div>
|
|
625
|
+
<div>
|
|
626
|
+
<div class="section-title">\${t.label.slice(t.label.indexOf(' ')+1)}</div>
|
|
627
|
+
<div class="section-desc">\${t.desc}</div>
|
|
628
|
+
</div>
|
|
629
|
+
</div>
|
|
630
|
+
\${vars.map(renderCard).join('')}
|
|
631
|
+
\${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>' : ''}
|
|
632
|
+
</div>\`;
|
|
633
|
+
}).join('');
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function renderCard(v) {
|
|
637
|
+
const displayVal = v.displayValue || '';
|
|
638
|
+
const isSensitive = v.sensitive;
|
|
639
|
+
const inputType = (v.type === 'password' && !changed[v.key]) ? 'password' : 'text';
|
|
640
|
+
|
|
641
|
+
let inputHTML = '';
|
|
642
|
+
if (v.type === 'select' || v.type === 'boolean') {
|
|
643
|
+
const opts = v.type === 'boolean'
|
|
644
|
+
? [{ val: 'true', lbl: 'true' }, { val: 'false', lbl: 'false' }]
|
|
645
|
+
: (v.options || []).map(o => ({ val: o, lbl: o }));
|
|
646
|
+
inputHTML = \`<select class="env-select" data-key="\${v.key}" onchange="handleChange(this)">
|
|
647
|
+
<option value="">\u2014 Select \u2014</option>
|
|
648
|
+
\${opts.map(o => \`<option value="\${o.val}" \${displayVal === o.val ? 'selected' : ''}>\${o.lbl}</option>\`).join('')}
|
|
649
|
+
</select>\`;
|
|
650
|
+
} else {
|
|
651
|
+
inputHTML = \`<input
|
|
652
|
+
type="\${inputType}"
|
|
653
|
+
class="env-input"
|
|
654
|
+
data-key="\${v.key}"
|
|
655
|
+
value="\${escHtml(displayVal)}"
|
|
656
|
+
placeholder="\${escHtml(v.placeholder || '')}"
|
|
657
|
+
oninput="handleChange(this)"
|
|
658
|
+
autocomplete="off"
|
|
659
|
+
/>\`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const testBtn = v.testable
|
|
663
|
+
? \`<button class="env-action-btn test-btn" onclick="testVar('\${v.key}')" title="Test connection">\u{1F9EA}</button>\`
|
|
664
|
+
: '';
|
|
665
|
+
|
|
666
|
+
const genBtn = v.key === 'OPENQA_JWT_SECRET'
|
|
667
|
+
? \`<button class="env-action-btn gen-btn" onclick="generateSecret('\${v.key}')" title="Generate secret">\u{1F511}</button>\`
|
|
668
|
+
: '';
|
|
669
|
+
|
|
670
|
+
const toggleBtn = (v.type === 'password' || isSensitive)
|
|
671
|
+
? \`<button class="env-action-btn" onclick="toggleVis('\${v.key}')" title="Toggle visibility" id="vis-\${v.key}">\u{1F441}</button>\`
|
|
672
|
+
: '';
|
|
673
|
+
|
|
674
|
+
return \`<div class="env-card\${displayVal ? ' has-value' : ''}" id="card-\${v.key}">
|
|
675
|
+
<div class="env-card-head">
|
|
676
|
+
<div class="env-key">
|
|
677
|
+
\${v.key}
|
|
678
|
+
\${v.required ? '<span class="badge-required">Required</span>' : ''}
|
|
679
|
+
\${isSensitive ? '<span class="badge-sensitive">Sensitive</span>' : ''}
|
|
415
680
|
</div>
|
|
416
681
|
</div>
|
|
417
|
-
|
|
682
|
+
<div class="env-desc">\${v.description}</div>
|
|
683
|
+
<div class="env-input-row">
|
|
684
|
+
\${inputHTML}
|
|
685
|
+
\${toggleBtn}
|
|
686
|
+
\${testBtn}
|
|
687
|
+
\${genBtn}
|
|
688
|
+
</div>
|
|
689
|
+
<div class="env-feedback" id="fb-\${v.key}"></div>
|
|
690
|
+
</div>\`;
|
|
691
|
+
}
|
|
418
692
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
// Load environment variables
|
|
425
|
-
async function loadEnvVariables() {
|
|
426
|
-
try {
|
|
427
|
-
const response = await fetch('/api/env');
|
|
428
|
-
if (!response.ok) throw new Error('Failed to load variables');
|
|
429
|
-
|
|
430
|
-
const data = await response.json();
|
|
431
|
-
envVariables = data.variables;
|
|
432
|
-
|
|
433
|
-
renderCategories();
|
|
434
|
-
document.getElementById('loading').style.display = 'none';
|
|
435
|
-
document.getElementById('main').style.display = 'block';
|
|
436
|
-
} catch (error) {
|
|
437
|
-
showAlert('error', 'Failed to load environment variables: ' + error.message);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
693
|
+
/* \u2500\u2500 Tab switching \u2500\u2500 */
|
|
694
|
+
function activateTab(id) {
|
|
695
|
+
document.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === id));
|
|
696
|
+
document.querySelectorAll('.section').forEach(s => s.classList.toggle('active', s.id === 'section-' + id));
|
|
697
|
+
}
|
|
440
698
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const vars = envVariables.filter(v => v.category === category);
|
|
452
|
-
|
|
453
|
-
section.innerHTML = \`
|
|
454
|
-
<div class="category-header">
|
|
455
|
-
<div class="category-title">\${getCategoryTitle(category)}</div>
|
|
456
|
-
</div>
|
|
457
|
-
<div class="env-grid">
|
|
458
|
-
\${vars.map(v => renderEnvItem(v)).join('')}
|
|
459
|
-
</div>
|
|
460
|
-
\`;
|
|
461
|
-
|
|
462
|
-
container.appendChild(section);
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Render single env item
|
|
467
|
-
function renderEnvItem(envVar) {
|
|
468
|
-
const inputType = envVar.type === 'password' ? 'password' : 'text';
|
|
469
|
-
const value = envVar.displayValue || '';
|
|
470
|
-
|
|
471
|
-
return \`
|
|
472
|
-
<div class="env-item" data-key="\${envVar.key}">
|
|
473
|
-
<div class="env-item-header">
|
|
474
|
-
<div class="env-label">
|
|
475
|
-
\${envVar.key}
|
|
476
|
-
\${envVar.required ? '<span class="required-badge">REQUIRED</span>' : ''}
|
|
477
|
-
</div>
|
|
478
|
-
</div>
|
|
479
|
-
<div class="env-description">\${envVar.description}</div>
|
|
480
|
-
<div class="env-input-group">
|
|
481
|
-
\${envVar.type === 'select' ?
|
|
482
|
-
\`<select class="env-input" data-key="\${envVar.key}" onchange="handleChange(this)">
|
|
483
|
-
<option value="">-- Select --</option>
|
|
484
|
-
\${envVar.options.map(opt =>
|
|
485
|
-
\`<option value="\${opt}" \${value === opt ? 'selected' : ''}>\${opt}</option>\`
|
|
486
|
-
).join('')}
|
|
487
|
-
</select>\` :
|
|
488
|
-
envVar.type === 'boolean' ?
|
|
489
|
-
\`<select class="env-input" data-key="\${envVar.key}" onchange="handleChange(this)">
|
|
490
|
-
<option value="">-- Select --</option>
|
|
491
|
-
<option value="true" \${value === 'true' ? 'selected' : ''}>true</option>
|
|
492
|
-
<option value="false" \${value === 'false' ? 'selected' : ''}>false</option>
|
|
493
|
-
</select>\` :
|
|
494
|
-
\`<input
|
|
495
|
-
type="\${inputType}"
|
|
496
|
-
class="env-input"
|
|
497
|
-
data-key="\${envVar.key}"
|
|
498
|
-
value="\${value}"
|
|
499
|
-
placeholder="\${envVar.placeholder || ''}"
|
|
500
|
-
onchange="handleChange(this)"
|
|
501
|
-
/>\`
|
|
502
|
-
}
|
|
503
|
-
<div class="env-actions">
|
|
504
|
-
\${envVar.testable ? \`<button class="icon-btn test" onclick="testVariable('\${envVar.key}')" title="Test">\u{1F9EA}</button>\` : ''}
|
|
505
|
-
\${envVar.key === 'OPENQA_JWT_SECRET' ? \`<button class="icon-btn generate" onclick="generateSecret('\${envVar.key}')" title="Generate">\u{1F511}</button>\` : ''}
|
|
506
|
-
</div>
|
|
507
|
-
</div>
|
|
508
|
-
<div class="error-message" id="error-\${envVar.key}"></div>
|
|
509
|
-
<div class="success-message" id="success-\${envVar.key}"></div>
|
|
510
|
-
</div>
|
|
511
|
-
\`;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Handle input change
|
|
515
|
-
function handleChange(input) {
|
|
516
|
-
const key = input.dataset.key;
|
|
517
|
-
const value = input.value;
|
|
518
|
-
|
|
519
|
-
changedVariables[key] = value;
|
|
520
|
-
document.getElementById('saveBtn').disabled = false;
|
|
521
|
-
|
|
522
|
-
// Clear messages
|
|
523
|
-
document.getElementById(\`error-\${key}\`).textContent = '';
|
|
524
|
-
document.getElementById(\`success-\${key}\`).textContent = '';
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// Save changes
|
|
528
|
-
async function saveChanges() {
|
|
529
|
-
const saveBtn = document.getElementById('saveBtn');
|
|
530
|
-
saveBtn.disabled = true;
|
|
531
|
-
saveBtn.textContent = '\u{1F4BE} Saving...';
|
|
532
|
-
|
|
533
|
-
try {
|
|
534
|
-
const response = await fetch('/api/env/bulk', {
|
|
535
|
-
method: 'POST',
|
|
536
|
-
headers: { 'Content-Type': 'application/json' },
|
|
537
|
-
body: JSON.stringify({ variables: changedVariables }),
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
if (!response.ok) {
|
|
541
|
-
const error = await response.json();
|
|
542
|
-
throw new Error(error.error || 'Failed to save');
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const result = await response.json();
|
|
546
|
-
restartRequired = result.restartRequired;
|
|
547
|
-
|
|
548
|
-
showAlert('success', \`\u2705 Saved \${result.updated} variable(s) successfully!\` +
|
|
549
|
-
(restartRequired ? ' \u26A0\uFE0F Restart required for changes to take effect.' : ''));
|
|
550
|
-
|
|
551
|
-
changedVariables = {};
|
|
552
|
-
saveBtn.textContent = '\u{1F4BE} Save Changes';
|
|
553
|
-
|
|
554
|
-
// Reload to show updated values
|
|
555
|
-
setTimeout(() => location.reload(), 2000);
|
|
556
|
-
} catch (error) {
|
|
557
|
-
showAlert('error', 'Failed to save: ' + error.message);
|
|
558
|
-
saveBtn.disabled = false;
|
|
559
|
-
saveBtn.textContent = '\u{1F4BE} Save Changes';
|
|
560
|
-
}
|
|
561
|
-
}
|
|
699
|
+
/* \u2500\u2500 Input handling \u2500\u2500 */
|
|
700
|
+
function handleChange(el) {
|
|
701
|
+
const key = el.dataset.key;
|
|
702
|
+
const val = el.value;
|
|
703
|
+
changed[key] = val;
|
|
704
|
+
el.classList.add('changed');
|
|
705
|
+
el.classList.remove('invalid');
|
|
706
|
+
clearFeedback(key);
|
|
707
|
+
document.getElementById('saveBtn').disabled = false;
|
|
708
|
+
}
|
|
562
709
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
showAlert('warning', 'Please enter a value first');
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
try {
|
|
574
|
-
const response = await fetch(\`/api/env/test/\${key}\`, {
|
|
575
|
-
method: 'POST',
|
|
576
|
-
headers: { 'Content-Type': 'application/json' },
|
|
577
|
-
body: JSON.stringify({ value }),
|
|
578
|
-
});
|
|
579
|
-
|
|
580
|
-
const result = await response.json();
|
|
581
|
-
showTestResult(result);
|
|
582
|
-
} catch (error) {
|
|
583
|
-
showTestResult({ success: false, message: 'Test failed: ' + error.message });
|
|
584
|
-
}
|
|
585
|
-
}
|
|
710
|
+
/* \u2500\u2500 Toggle password visibility \u2500\u2500 */
|
|
711
|
+
function toggleVis(key) {
|
|
712
|
+
const inp = document.querySelector('[data-key="' + key + '"]');
|
|
713
|
+
if (!inp || inp.tagName !== 'INPUT') return;
|
|
714
|
+
inp.type = inp.type === 'password' ? 'text' : 'password';
|
|
715
|
+
}
|
|
586
716
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
const response = await fetch(\`/api/env/generate/\${key}\`, {
|
|
591
|
-
method: 'POST',
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
if (!response.ok) throw new Error('Failed to generate');
|
|
595
|
-
|
|
596
|
-
const result = await response.json();
|
|
597
|
-
const input = document.querySelector(\`[data-key="\${key}"]\`);
|
|
598
|
-
input.value = result.value;
|
|
599
|
-
handleChange(input);
|
|
600
|
-
|
|
601
|
-
document.getElementById(\`success-\${key}\`).textContent = '\u2705 Secret generated!';
|
|
602
|
-
} catch (error) {
|
|
603
|
-
document.getElementById(\`error-\${key}\`).textContent = 'Failed to generate: ' + error.message;
|
|
604
|
-
}
|
|
605
|
-
}
|
|
717
|
+
/* \u2500\u2500 Save \u2500\u2500 */
|
|
718
|
+
async function saveChanges() {
|
|
719
|
+
if (!Object.keys(changed).length) return;
|
|
606
720
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
const resultDiv = document.getElementById('testResult');
|
|
611
|
-
|
|
612
|
-
resultDiv.innerHTML = \`
|
|
613
|
-
<div class="alert \${result.success ? 'alert-success' : 'alert-warning'}">
|
|
614
|
-
\${result.success ? '\u2705' : '\u274C'} \${result.message}
|
|
615
|
-
</div>
|
|
616
|
-
\`;
|
|
617
|
-
|
|
618
|
-
modal.classList.add('show');
|
|
619
|
-
}
|
|
721
|
+
const btn = document.getElementById('saveBtn');
|
|
722
|
+
btn.disabled = true;
|
|
723
|
+
btn.textContent = '\u23F3 Saving\u2026';
|
|
620
724
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
725
|
+
try {
|
|
726
|
+
const res = await fetch('/api/env/bulk', {
|
|
727
|
+
method: 'POST',
|
|
728
|
+
headers: { 'Content-Type': 'application/json' },
|
|
729
|
+
body: JSON.stringify({ variables: changed }),
|
|
730
|
+
credentials: 'include',
|
|
731
|
+
});
|
|
624
732
|
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// Get category title
|
|
641
|
-
function getCategoryTitle(category) {
|
|
642
|
-
const titles = {
|
|
643
|
-
llm: '\u{1F916} LLM Configuration',
|
|
644
|
-
security: '\u{1F512} Security Settings',
|
|
645
|
-
target: '\u{1F3AF} Target Application',
|
|
646
|
-
github: '\u{1F419} GitHub Integration',
|
|
647
|
-
web: '\u{1F310} Web Server',
|
|
648
|
-
agent: '\u{1F916} Agent Configuration',
|
|
649
|
-
database: '\u{1F4BE} Database',
|
|
650
|
-
notifications: '\u{1F514} Notifications',
|
|
651
|
-
};
|
|
652
|
-
return titles[category] || category;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
// Tab switching
|
|
656
|
-
document.addEventListener('click', (e) => {
|
|
657
|
-
if (e.target.classList.contains('tab')) {
|
|
658
|
-
const category = e.target.dataset.category;
|
|
659
|
-
|
|
660
|
-
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
661
|
-
e.target.classList.add('active');
|
|
662
|
-
|
|
663
|
-
document.querySelectorAll('.category-section').forEach(s => s.classList.remove('active'));
|
|
664
|
-
document.querySelector(\`[data-category="\${category}"]\`).classList.add('active');
|
|
733
|
+
const body = await res.json().catch(() => ({}));
|
|
734
|
+
|
|
735
|
+
if (!res.ok) {
|
|
736
|
+
const errStr = body.errors
|
|
737
|
+
? Object.entries(body.errors).map(([k, v]) => k + ': ' + v).join('; ')
|
|
738
|
+
: body.error || 'Failed to save';
|
|
739
|
+
// Show per-field errors
|
|
740
|
+
if (body.errors) {
|
|
741
|
+
for (const [k, msg] of Object.entries(body.errors)) {
|
|
742
|
+
setFeedback(k, 'error', msg);
|
|
743
|
+
const inp = document.querySelector('[data-key="' + k + '"]');
|
|
744
|
+
if (inp) inp.classList.add('invalid');
|
|
745
|
+
}
|
|
665
746
|
}
|
|
747
|
+
toast('error', errStr);
|
|
748
|
+
btn.disabled = false;
|
|
749
|
+
btn.innerHTML = '\u{1F4BE} Save Changes';
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
toast('success', '\u2705 Saved ' + body.updated + ' variable(s)');
|
|
754
|
+
if (body.restartRequired) {
|
|
755
|
+
document.getElementById('restartBanner').classList.add('show');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
changed = {};
|
|
759
|
+
btn.innerHTML = '\u{1F4BE} Save Changes';
|
|
760
|
+
// Reload to reflect masked values
|
|
761
|
+
setTimeout(() => location.reload(), 1200);
|
|
762
|
+
} catch (e) {
|
|
763
|
+
toast('error', 'Network error \u2014 ' + e.message);
|
|
764
|
+
btn.disabled = false;
|
|
765
|
+
btn.innerHTML = '\u{1F4BE} Save Changes';
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
/* \u2500\u2500 Test variable \u2500\u2500 */
|
|
770
|
+
async function testVar(key) {
|
|
771
|
+
const inp = document.querySelector('[data-key="' + key + '"]');
|
|
772
|
+
const val = inp ? inp.value : '';
|
|
773
|
+
if (!val) { toast('warning', 'Enter a value first'); return; }
|
|
774
|
+
|
|
775
|
+
setFeedback(key, '', '');
|
|
776
|
+
const btn = document.querySelector('[onclick="testVar(\\''+key+'\\')"]');
|
|
777
|
+
if (btn) { btn.textContent = '\u23F3'; btn.disabled = true; }
|
|
778
|
+
|
|
779
|
+
try {
|
|
780
|
+
const res = await fetch('/api/env/test/' + key, {
|
|
781
|
+
method: 'POST',
|
|
782
|
+
headers: { 'Content-Type': 'application/json' },
|
|
783
|
+
body: JSON.stringify({ value: val }),
|
|
784
|
+
credentials: 'include',
|
|
666
785
|
});
|
|
786
|
+
const result = await res.json();
|
|
787
|
+
openModal(result.success, result.message);
|
|
788
|
+
setFeedback(key, result.success ? 'success' : 'error', result.success ? '\u2713 Connected' : '\u2717 ' + result.message);
|
|
789
|
+
} catch (e) {
|
|
790
|
+
openModal(false, 'Network error: ' + e.message);
|
|
791
|
+
} finally {
|
|
792
|
+
if (btn) { btn.textContent = '\u{1F9EA}'; btn.disabled = false; }
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/* \u2500\u2500 Generate secret \u2500\u2500 */
|
|
797
|
+
async function generateSecret(key) {
|
|
798
|
+
try {
|
|
799
|
+
const res = await fetch('/api/env/generate/' + key, {
|
|
800
|
+
method: 'POST', credentials: 'include'
|
|
801
|
+
});
|
|
802
|
+
if (!res.ok) throw new Error('Failed to generate');
|
|
803
|
+
const { value } = await res.json();
|
|
804
|
+
const inp = document.querySelector('[data-key="' + key + '"]');
|
|
805
|
+
if (inp) {
|
|
806
|
+
inp.type = 'text';
|
|
807
|
+
inp.value = value;
|
|
808
|
+
handleChange(inp);
|
|
809
|
+
}
|
|
810
|
+
setFeedback(key, 'success', '\u2713 Secret generated \u2014 save to persist');
|
|
811
|
+
} catch (e) {
|
|
812
|
+
setFeedback(key, 'error', e.message);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/* \u2500\u2500 Modal \u2500\u2500 */
|
|
817
|
+
function openModal(ok, msg) {
|
|
818
|
+
const box = document.getElementById('testResultBox');
|
|
819
|
+
box.className = 'modal-result ' + (ok ? 'ok' : 'fail');
|
|
820
|
+
box.textContent = (ok ? '\u2713 ' : '\u2717 ') + msg;
|
|
821
|
+
document.getElementById('testModal').classList.add('open');
|
|
822
|
+
}
|
|
823
|
+
function closeModal() {
|
|
824
|
+
document.getElementById('testModal').classList.remove('open');
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/* \u2500\u2500 Toast \u2500\u2500 */
|
|
828
|
+
function toast(type, msg) {
|
|
829
|
+
const zone = document.getElementById('toastZone');
|
|
830
|
+
const el = document.createElement('div');
|
|
831
|
+
el.className = 'toast ' + type;
|
|
832
|
+
el.textContent = msg;
|
|
833
|
+
zone.appendChild(el);
|
|
834
|
+
setTimeout(() => el.remove(), 4500);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/* \u2500\u2500 Feedback \u2500\u2500 */
|
|
838
|
+
function setFeedback(key, type, msg) {
|
|
839
|
+
const el = document.getElementById('fb-' + key);
|
|
840
|
+
if (!el) return;
|
|
841
|
+
el.className = 'env-feedback' + (type ? ' ' + type : '');
|
|
842
|
+
el.textContent = msg;
|
|
843
|
+
}
|
|
844
|
+
function clearFeedback(key) { setFeedback(key, '', ''); }
|
|
845
|
+
|
|
846
|
+
/* \u2500\u2500 Helpers \u2500\u2500 */
|
|
847
|
+
function escHtml(s) {
|
|
848
|
+
return String(s).replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c]));
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/* \u2500\u2500 Wire save button \u2500\u2500 */
|
|
852
|
+
document.getElementById('saveBtn').addEventListener('click', saveChanges);
|
|
667
853
|
|
|
668
|
-
|
|
669
|
-
|
|
854
|
+
/* \u2500\u2500 Close modal on backdrop click \u2500\u2500 */
|
|
855
|
+
document.getElementById('testModal').addEventListener('click', function(e) {
|
|
856
|
+
if (e.target === this) closeModal();
|
|
857
|
+
});
|
|
670
858
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
859
|
+
/* \u2500\u2500 Boot \u2500\u2500 */
|
|
860
|
+
init();
|
|
861
|
+
</script>
|
|
674
862
|
</body>
|
|
675
863
|
</html>`;
|
|
676
864
|
}
|