@openqa/cli 1.3.4 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +203 -6
- package/dist/agent/brain/diff-analyzer.js +140 -0
- package/dist/agent/brain/diff-analyzer.js.map +1 -0
- package/dist/agent/brain/llm-cache.js +47 -0
- package/dist/agent/brain/llm-cache.js.map +1 -0
- package/dist/agent/brain/llm-resilience.js +252 -0
- package/dist/agent/brain/llm-resilience.js.map +1 -0
- package/dist/agent/config/index.js +588 -0
- package/dist/agent/config/index.js.map +1 -0
- package/dist/agent/coverage/index.js +74 -0
- package/dist/agent/coverage/index.js.map +1 -0
- package/dist/agent/export/index.js +158 -0
- package/dist/agent/export/index.js.map +1 -0
- package/dist/agent/index-v2.js +2795 -0
- package/dist/agent/index-v2.js.map +1 -0
- package/dist/agent/index.js +369 -105
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/logger.js +41 -0
- package/dist/agent/logger.js.map +1 -0
- package/dist/agent/metrics.js +39 -0
- package/dist/agent/metrics.js.map +1 -0
- package/dist/agent/notifications/index.js +106 -0
- package/dist/agent/notifications/index.js.map +1 -0
- package/dist/agent/openapi/spec.js +338 -0
- package/dist/agent/openapi/spec.js.map +1 -0
- package/dist/agent/tools/project-runner.js +481 -0
- package/dist/agent/tools/project-runner.js.map +1 -0
- package/dist/cli/config.html.js +454 -0
- package/dist/cli/daemon.js +8810 -0
- package/dist/cli/dashboard.html.js +1622 -0
- package/dist/cli/env-config.js +391 -0
- package/dist/cli/env-routes.js +820 -0
- package/dist/cli/env.html.js +679 -0
- package/dist/cli/index.js +5980 -1896
- package/dist/cli/kanban.html.js +577 -0
- package/dist/cli/routes.js +895 -0
- package/dist/cli/routes.js.map +1 -0
- package/dist/cli/server.js +5855 -1860
- package/dist/database/index.js +485 -60
- package/dist/database/index.js.map +1 -1
- package/dist/database/sqlite.js +281 -0
- package/dist/database/sqlite.js.map +1 -0
- package/install.sh +19 -10
- package/package.json +19 -5
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
// cli/env.html.ts
|
|
2
|
+
function getEnvHTML() {
|
|
3
|
+
return `<!DOCTYPE html>
|
|
4
|
+
<html lang="en">
|
|
5
|
+
<head>
|
|
6
|
+
<meta charset="UTF-8">
|
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
8
|
+
<title>Environment Variables - OpenQA</title>
|
|
9
|
+
<style>
|
|
10
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
11
|
+
|
|
12
|
+
body {
|
|
13
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
14
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
15
|
+
min-height: 100vh;
|
|
16
|
+
padding: 20px;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.container {
|
|
20
|
+
max-width: 1200px;
|
|
21
|
+
margin: 0 auto;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.header {
|
|
25
|
+
background: rgba(255, 255, 255, 0.95);
|
|
26
|
+
backdrop-filter: blur(10px);
|
|
27
|
+
padding: 20px 30px;
|
|
28
|
+
border-radius: 12px;
|
|
29
|
+
margin-bottom: 20px;
|
|
30
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
31
|
+
display: flex;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
align-items: center;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.header h1 {
|
|
37
|
+
font-size: 24px;
|
|
38
|
+
color: #1a202c;
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: center;
|
|
41
|
+
gap: 10px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.header-actions {
|
|
45
|
+
display: flex;
|
|
46
|
+
gap: 10px;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.btn {
|
|
50
|
+
padding: 10px 20px;
|
|
51
|
+
border: none;
|
|
52
|
+
border-radius: 8px;
|
|
53
|
+
font-size: 14px;
|
|
54
|
+
font-weight: 600;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
transition: all 0.2s;
|
|
57
|
+
text-decoration: none;
|
|
58
|
+
display: inline-flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: 8px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.btn-primary {
|
|
64
|
+
background: #667eea;
|
|
65
|
+
color: white;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.btn-primary:hover {
|
|
69
|
+
background: #5568d3;
|
|
70
|
+
transform: translateY(-1px);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.btn-secondary {
|
|
74
|
+
background: #e2e8f0;
|
|
75
|
+
color: #4a5568;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.btn-secondary:hover {
|
|
79
|
+
background: #cbd5e0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.btn-success {
|
|
83
|
+
background: #48bb78;
|
|
84
|
+
color: white;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.btn-success:hover {
|
|
88
|
+
background: #38a169;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.btn:disabled {
|
|
92
|
+
opacity: 0.5;
|
|
93
|
+
cursor: not-allowed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.content {
|
|
97
|
+
background: rgba(255, 255, 255, 0.95);
|
|
98
|
+
backdrop-filter: blur(10px);
|
|
99
|
+
padding: 30px;
|
|
100
|
+
border-radius: 12px;
|
|
101
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.tabs {
|
|
105
|
+
display: flex;
|
|
106
|
+
gap: 10px;
|
|
107
|
+
margin-bottom: 30px;
|
|
108
|
+
border-bottom: 2px solid #e2e8f0;
|
|
109
|
+
padding-bottom: 10px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.tab {
|
|
113
|
+
padding: 10px 20px;
|
|
114
|
+
border: none;
|
|
115
|
+
background: none;
|
|
116
|
+
font-size: 14px;
|
|
117
|
+
font-weight: 600;
|
|
118
|
+
color: #718096;
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
border-bottom: 3px solid transparent;
|
|
121
|
+
transition: all 0.2s;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.tab.active {
|
|
125
|
+
color: #667eea;
|
|
126
|
+
border-bottom-color: #667eea;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.tab:hover {
|
|
130
|
+
color: #667eea;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.category-section {
|
|
134
|
+
display: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.category-section.active {
|
|
138
|
+
display: block;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.category-header {
|
|
142
|
+
display: flex;
|
|
143
|
+
justify-content: space-between;
|
|
144
|
+
align-items: center;
|
|
145
|
+
margin-bottom: 20px;
|
|
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;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.env-item {
|
|
160
|
+
border: 1px solid #e2e8f0;
|
|
161
|
+
border-radius: 8px;
|
|
162
|
+
padding: 20px;
|
|
163
|
+
transition: all 0.2s;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.env-item:hover {
|
|
167
|
+
border-color: #cbd5e0;
|
|
168
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.env-item-header {
|
|
172
|
+
display: flex;
|
|
173
|
+
justify-content: space-between;
|
|
174
|
+
align-items: flex-start;
|
|
175
|
+
margin-bottom: 10px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.env-label {
|
|
179
|
+
font-weight: 600;
|
|
180
|
+
color: #2d3748;
|
|
181
|
+
font-size: 14px;
|
|
182
|
+
display: flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
gap: 8px;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.required-badge {
|
|
188
|
+
background: #fc8181;
|
|
189
|
+
color: white;
|
|
190
|
+
font-size: 10px;
|
|
191
|
+
padding: 2px 6px;
|
|
192
|
+
border-radius: 4px;
|
|
193
|
+
font-weight: 700;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.env-description {
|
|
197
|
+
font-size: 13px;
|
|
198
|
+
color: #718096;
|
|
199
|
+
margin-bottom: 10px;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.env-input-group {
|
|
203
|
+
display: flex;
|
|
204
|
+
gap: 10px;
|
|
205
|
+
align-items: center;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.env-input {
|
|
209
|
+
flex: 1;
|
|
210
|
+
padding: 10px 12px;
|
|
211
|
+
border: 1px solid #e2e8f0;
|
|
212
|
+
border-radius: 6px;
|
|
213
|
+
font-size: 14px;
|
|
214
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
215
|
+
transition: all 0.2s;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.env-input:focus {
|
|
219
|
+
outline: none;
|
|
220
|
+
border-color: #667eea;
|
|
221
|
+
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.env-input.error {
|
|
225
|
+
border-color: #fc8181;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.env-actions {
|
|
229
|
+
display: flex;
|
|
230
|
+
gap: 5px;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.icon-btn {
|
|
234
|
+
padding: 8px;
|
|
235
|
+
border: none;
|
|
236
|
+
background: #e2e8f0;
|
|
237
|
+
border-radius: 6px;
|
|
238
|
+
cursor: pointer;
|
|
239
|
+
transition: all 0.2s;
|
|
240
|
+
font-size: 16px;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.icon-btn:hover {
|
|
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;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.error-message {
|
|
266
|
+
color: #e53e3e;
|
|
267
|
+
font-size: 12px;
|
|
268
|
+
margin-top: 5px;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.success-message {
|
|
272
|
+
color: #38a169;
|
|
273
|
+
font-size: 12px;
|
|
274
|
+
margin-top: 5px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.alert {
|
|
278
|
+
padding: 15px 20px;
|
|
279
|
+
border-radius: 8px;
|
|
280
|
+
margin-bottom: 20px;
|
|
281
|
+
display: flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
gap: 10px;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.alert-warning {
|
|
287
|
+
background: #fef5e7;
|
|
288
|
+
border-left: 4px solid #f59e0b;
|
|
289
|
+
color: #92400e;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.alert-info {
|
|
293
|
+
background: #eff6ff;
|
|
294
|
+
border-left: 4px solid #3b82f6;
|
|
295
|
+
color: #1e40af;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.alert-success {
|
|
299
|
+
background: #f0fdf4;
|
|
300
|
+
border-left: 4px solid #10b981;
|
|
301
|
+
color: #065f46;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.loading {
|
|
305
|
+
text-align: center;
|
|
306
|
+
padding: 40px;
|
|
307
|
+
color: #718096;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.spinner {
|
|
311
|
+
border: 3px solid #e2e8f0;
|
|
312
|
+
border-top: 3px solid #667eea;
|
|
313
|
+
border-radius: 50%;
|
|
314
|
+
width: 40px;
|
|
315
|
+
height: 40px;
|
|
316
|
+
animation: spin 1s linear infinite;
|
|
317
|
+
margin: 0 auto 20px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
@keyframes spin {
|
|
321
|
+
0% { transform: rotate(0deg); }
|
|
322
|
+
100% { transform: rotate(360deg); }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.modal {
|
|
326
|
+
display: none;
|
|
327
|
+
position: fixed;
|
|
328
|
+
top: 0;
|
|
329
|
+
left: 0;
|
|
330
|
+
right: 0;
|
|
331
|
+
bottom: 0;
|
|
332
|
+
background: rgba(0, 0, 0, 0.5);
|
|
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;
|
|
353
|
+
font-weight: 600;
|
|
354
|
+
margin-bottom: 15px;
|
|
355
|
+
color: #2d3748;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.modal-body {
|
|
359
|
+
margin-bottom: 20px;
|
|
360
|
+
color: #4a5568;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.modal-footer {
|
|
364
|
+
display: flex;
|
|
365
|
+
gap: 10px;
|
|
366
|
+
justify-content: flex-end;
|
|
367
|
+
}
|
|
368
|
+
</style>
|
|
369
|
+
</head>
|
|
370
|
+
<body>
|
|
371
|
+
<div class="container">
|
|
372
|
+
<div class="header">
|
|
373
|
+
<h1>
|
|
374
|
+
<span>\u2699\uFE0F</span>
|
|
375
|
+
Environment Variables
|
|
376
|
+
</h1>
|
|
377
|
+
<div class="header-actions">
|
|
378
|
+
<a href="/config" class="btn btn-secondary">\u2190 Back to Config</a>
|
|
379
|
+
<button id="saveBtn" class="btn btn-success" disabled>\u{1F4BE} Save Changes</button>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<div class="content">
|
|
384
|
+
<div id="loading" class="loading">
|
|
385
|
+
<div class="spinner"></div>
|
|
386
|
+
<div>Loading environment variables...</div>
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
<div id="main" style="display: none;">
|
|
390
|
+
<div id="alerts"></div>
|
|
391
|
+
|
|
392
|
+
<div class="tabs">
|
|
393
|
+
<button class="tab active" data-category="llm">\u{1F916} LLM</button>
|
|
394
|
+
<button class="tab" data-category="security">\u{1F512} Security</button>
|
|
395
|
+
<button class="tab" data-category="target">\u{1F3AF} Target App</button>
|
|
396
|
+
<button class="tab" data-category="github">\u{1F419} GitHub</button>
|
|
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>
|
|
402
|
+
|
|
403
|
+
<div id="categories"></div>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<!-- Test Result Modal -->
|
|
409
|
+
<div id="testModal" class="modal">
|
|
410
|
+
<div class="modal-content">
|
|
411
|
+
<div class="modal-header">Test Result</div>
|
|
412
|
+
<div class="modal-body" id="testResult"></div>
|
|
413
|
+
<div class="modal-footer">
|
|
414
|
+
<button class="btn btn-secondary" onclick="closeTestModal()">Close</button>
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
<script>
|
|
420
|
+
let envVariables = [];
|
|
421
|
+
let changedVariables = {};
|
|
422
|
+
let restartRequired = false;
|
|
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
|
+
}
|
|
440
|
+
|
|
441
|
+
// Render categories
|
|
442
|
+
function renderCategories() {
|
|
443
|
+
const container = document.getElementById('categories');
|
|
444
|
+
const categories = [...new Set(envVariables.map(v => v.category))];
|
|
445
|
+
|
|
446
|
+
categories.forEach((category, index) => {
|
|
447
|
+
const section = document.createElement('div');
|
|
448
|
+
section.className = 'category-section' + (index === 0 ? ' active' : '');
|
|
449
|
+
section.dataset.category = category;
|
|
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
|
+
}
|
|
562
|
+
|
|
563
|
+
// Test variable
|
|
564
|
+
async function testVariable(key) {
|
|
565
|
+
const input = document.querySelector(\`[data-key="\${key}"]\`);
|
|
566
|
+
const value = input.value;
|
|
567
|
+
|
|
568
|
+
if (!value) {
|
|
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
|
+
}
|
|
586
|
+
|
|
587
|
+
// Generate secret
|
|
588
|
+
async function generateSecret(key) {
|
|
589
|
+
try {
|
|
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
|
+
}
|
|
606
|
+
|
|
607
|
+
// Show test result
|
|
608
|
+
function showTestResult(result) {
|
|
609
|
+
const modal = document.getElementById('testModal');
|
|
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
|
+
}
|
|
620
|
+
|
|
621
|
+
function closeTestModal() {
|
|
622
|
+
document.getElementById('testModal').classList.remove('show');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Show alert
|
|
626
|
+
function showAlert(type, message) {
|
|
627
|
+
const alerts = document.getElementById('alerts');
|
|
628
|
+
const alertClass = type === 'error' ? 'alert-warning' :
|
|
629
|
+
type === 'success' ? 'alert-success' : 'alert-info';
|
|
630
|
+
|
|
631
|
+
alerts.innerHTML = \`
|
|
632
|
+
<div class="alert \${alertClass}">
|
|
633
|
+
\${message}
|
|
634
|
+
</div>
|
|
635
|
+
\`;
|
|
636
|
+
|
|
637
|
+
setTimeout(() => alerts.innerHTML = '', 5000);
|
|
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');
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// Save button
|
|
669
|
+
document.getElementById('saveBtn').addEventListener('click', saveChanges);
|
|
670
|
+
|
|
671
|
+
// Load on page load
|
|
672
|
+
loadEnvVariables();
|
|
673
|
+
</script>
|
|
674
|
+
</body>
|
|
675
|
+
</html>`;
|
|
676
|
+
}
|
|
677
|
+
export {
|
|
678
|
+
getEnvHTML
|
|
679
|
+
};
|