@openqa/cli 2.1.0 → 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 +277 -92
- package/dist/agent/index-v2.js.map +1 -1
- package/dist/agent/index.js +109 -118
- package/dist/agent/index.js.map +1 -1
- package/dist/cli/daemon.js +1045 -635
- package/dist/cli/dashboard.html.js +53 -16
- package/dist/cli/env.html.js +717 -529
- package/dist/cli/index.js +762 -537
- package/dist/cli/server.js +762 -537
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2668,7 +2668,7 @@ function getDashboardHTML() {
|
|
|
2668
2668
|
.logo-mark {
|
|
2669
2669
|
width: 34px;
|
|
2670
2670
|
height: 34px;
|
|
2671
|
-
background:
|
|
2671
|
+
background: transparent;
|
|
2672
2672
|
border-radius: 8px;
|
|
2673
2673
|
display: grid;
|
|
2674
2674
|
place-items: center;
|
|
@@ -3290,7 +3290,9 @@ function getDashboardHTML() {
|
|
|
3290
3290
|
<!-- Sidebar -->
|
|
3291
3291
|
<aside>
|
|
3292
3292
|
<div class="logo">
|
|
3293
|
-
<div class="logo-mark"
|
|
3293
|
+
<div class="logo-mark">
|
|
3294
|
+
<img src="https://openqa.orkajs.com/_next/image?url=https%3A%2F%2Forkajs.com%2Floutre-orka-qa.png&w=256&q=75" alt="OpenQA Logo" style="width: 40px; height: 40px;">
|
|
3295
|
+
</div>
|
|
3294
3296
|
<div>
|
|
3295
3297
|
<div class="logo-name">OpenQA</div>
|
|
3296
3298
|
<div class="logo-version">v2.1.0 \xB7 OSS</div>
|
|
@@ -3300,42 +3302,69 @@ function getDashboardHTML() {
|
|
|
3300
3302
|
<div class="nav-section">
|
|
3301
3303
|
<div class="nav-label">Overview</div>
|
|
3302
3304
|
<a class="nav-item active" href="/">
|
|
3303
|
-
<span class="icon"
|
|
3305
|
+
<span class="icon">
|
|
3306
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-gauge-icon lucide-gauge"><path d="m12 14 4-4"/><path d="M3.34 19a10 10 0 1 1 17.32 0"/></svg>
|
|
3307
|
+
</span> Dashboard
|
|
3304
3308
|
</a>
|
|
3305
3309
|
<a class="nav-item" href="/kanban">
|
|
3306
|
-
<span class="icon"
|
|
3310
|
+
<span class="icon">
|
|
3311
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-dashed-kanban-icon lucide-square-dashed-kanban"><path d="M8 7v7"/><path d="M12 7v4"/><path d="M16 7v9"/><path d="M5 3a2 2 0 0 0-2 2"/><path d="M9 3h1"/><path d="M14 3h1"/><path d="M19 3a2 2 0 0 1 2 2"/><path d="M21 9v1"/><path d="M21 14v1"/><path d="M21 19a2 2 0 0 1-2 2"/><path d="M14 21h1"/><path d="M9 21h1"/><path d="M5 21a2 2 0 0 1-2-2"/><path d="M3 14v1"/><path d="M3 9v1"/></svg>
|
|
3312
|
+
</span> Kanban
|
|
3307
3313
|
<span class="badge" id="kanban-count">0</span>
|
|
3308
3314
|
</a>
|
|
3309
3315
|
|
|
3310
3316
|
<div class="nav-label">Agents</div>
|
|
3311
3317
|
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('agents-table')">
|
|
3312
|
-
<span class="icon"
|
|
3318
|
+
<span class="icon">
|
|
3319
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-activity-icon lucide-activity"><path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"/></svg>
|
|
3320
|
+
</span> Active Agents
|
|
3313
3321
|
</a>
|
|
3314
3322
|
<a class="nav-item" href="javascript:void(0)" onclick="switchAgentTab('specialists'); scrollToSection('agents-table')">
|
|
3315
|
-
<span class="icon"
|
|
3323
|
+
<span class="icon">
|
|
3324
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-hat-glasses-icon lucide-hat-glasses"><path d="M14 18a2 2 0 0 0-4 0"/><path d="m19 11-2.11-6.657a2 2 0 0 0-2.752-1.148l-1.276.61A2 2 0 0 1 12 4H8.5a2 2 0 0 0-1.925 1.456L5 11"/><path d="M2 11h20"/><circle cx="17" cy="18" r="3"/><circle cx="7" cy="18" r="3"/></svg>
|
|
3325
|
+
</span> Specialists
|
|
3316
3326
|
</a>
|
|
3317
3327
|
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('interventions-panel')">
|
|
3318
|
-
<span class="icon"
|
|
3328
|
+
<span class="icon">
|
|
3329
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-user-cog-icon lucide-user-cog"><path d="M10 15H6a4 4 0 0 0-4 4v2"/><path d="m14.305 16.53.923-.382"/><path d="m15.228 13.852-.923-.383"/><path d="m16.852 12.228-.383-.923"/><path d="m16.852 17.772-.383.924"/><path d="m19.148 12.228.383-.923"/><path d="m19.53 18.696-.382-.924"/><path d="m20.772 13.852.924-.383"/><path d="m20.772 16.148.924.383"/><circle cx="18" cy="15" r="3"/><circle cx="9" cy="7" r="4"/></svg>
|
|
3330
|
+
</span> Interventions
|
|
3319
3331
|
<span class="badge" id="intervention-count" style="background: var(--red);">0</span>
|
|
3320
3332
|
</a>
|
|
3321
3333
|
|
|
3322
3334
|
<div class="nav-label">Analysis</div>
|
|
3323
3335
|
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('issues-panel')">
|
|
3324
|
-
<span class="icon"
|
|
3336
|
+
<span class="icon">
|
|
3337
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug-play-icon lucide-bug-play"><path d="M10 19.655A6 6 0 0 1 6 14v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 3.97"/><path d="M14 15.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997a1 1 0 0 1-1.517-.86z"/>
|
|
3338
|
+
<path d="M14.12 3.88 16 2"/>
|
|
3339
|
+
<path d="M21 5a4 4 0 0 1-3.55 3.97"/>
|
|
3340
|
+
<path d="M3 21a4 4 0 0 1 3.81-4"/>
|
|
3341
|
+
<path d="M3 5a4 4 0 0 0 3.55 3.97"/>
|
|
3342
|
+
<path d="M6 13H2"/><path d="m8 2 1.88 1.88"/>
|
|
3343
|
+
<path d="M9 7.13V6a3 3 0 1 1 6 0v1.13"/>
|
|
3344
|
+
</svg>
|
|
3345
|
+
</span> Bug Reports
|
|
3325
3346
|
</a>
|
|
3326
3347
|
<a class="nav-item" href="javascript:void(0)" onclick="switchChartTab('performance'); scrollToSection('chart-performance')">
|
|
3327
|
-
<span class="icon"
|
|
3348
|
+
<span class="icon">
|
|
3349
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chart-spline-icon lucide-chart-spline"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M7 16c.5-2 1.5-7 4-7 2 0 2 3 4 3 2.5 0 4.5-5 5-7"/></svg>
|
|
3350
|
+
</span> Performance
|
|
3328
3351
|
</a>
|
|
3329
3352
|
<a class="nav-item" href="javascript:void(0)" onclick="scrollToSection('activity-list')">
|
|
3330
|
-
<span class="icon"
|
|
3353
|
+
<span class="icon">
|
|
3354
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scroll-text-icon lucide-scroll-text"><path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/></svg>
|
|
3355
|
+
</span> Logs
|
|
3331
3356
|
</a>
|
|
3332
3357
|
|
|
3333
3358
|
<div class="nav-label">System</div>
|
|
3334
3359
|
<a class="nav-item" href="/config">
|
|
3335
|
-
<span class="icon"
|
|
3360
|
+
<span class="icon">
|
|
3361
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-columns3-cog-icon lucide-columns-3-cog"><path d="M10.5 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v5.5"/><path d="m14.3 19.6 1-.4"/><path d="M15 3v7.5"/><path d="m15.2 16.9-.9-.3"/><path d="m16.6 21.7.3-.9"/><path d="m16.8 15.3-.4-1"/><path d="m19.1 15.2.3-.9"/><path d="m19.6 21.7-.4-1"/><path d="m20.7 16.8 1-.4"/><path d="m21.7 19.4-.9-.3"/><path d="M9 3v18"/><circle cx="18" cy="18" r="3"/></svg>
|
|
3362
|
+
</span> Config
|
|
3336
3363
|
</a>
|
|
3337
3364
|
<a class="nav-item" href="/config/env">
|
|
3338
|
-
<span class="icon"
|
|
3365
|
+
<span class="icon">
|
|
3366
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-columns3-cog-icon lucide-columns-3-cog"><path d="M10.5 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v5.5"/><path d="m14.3 19.6 1-.4"/><path d="M15 3v7.5"/><path d="m15.2 16.9-.9-.3"/><path d="m16.6 21.7.3-.9"/><path d="m16.8 15.3-.4-1"/><path d="m19.1 15.2.3-.9"/><path d="m19.6 21.7-.4-1"/><path d="m20.7 16.8 1-.4"/><path d="m21.7 19.4-.9-.3"/><path d="M9 3v18"/><circle cx="18" cy="18" r="3"/></svg>
|
|
3367
|
+
</span> Environment
|
|
3339
3368
|
</a>
|
|
3340
3369
|
</div>
|
|
3341
3370
|
|
|
@@ -3367,7 +3396,9 @@ function getDashboardHTML() {
|
|
|
3367
3396
|
<div class="metric-card">
|
|
3368
3397
|
<div class="metric-header">
|
|
3369
3398
|
<div class="metric-label">Active Agents</div>
|
|
3370
|
-
<div class="metric-icon"
|
|
3399
|
+
<div class="metric-icon">
|
|
3400
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-terminal-icon lucide-square-terminal"><path d="m7 11 2-2-2-2"/><path d="M11 13h4"/><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/></svg>
|
|
3401
|
+
</div>
|
|
3371
3402
|
</div>
|
|
3372
3403
|
<div class="metric-value" id="active-agents">0</div>
|
|
3373
3404
|
<div class="metric-change positive" id="agents-change">\u2191 0 from last hour</div>
|
|
@@ -3375,7 +3406,9 @@ function getDashboardHTML() {
|
|
|
3375
3406
|
<div class="metric-card">
|
|
3376
3407
|
<div class="metric-header">
|
|
3377
3408
|
<div class="metric-label">Total Actions</div>
|
|
3378
|
-
<div class="metric-icon"
|
|
3409
|
+
<div class="metric-icon">
|
|
3410
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-todo-icon lucide-list-todo"><path d="M13 5h8"/><path d="M13 12h8"/><path d="M13 19h8"/><path d="m3 17 2 2 4-4"/><rect x="3" y="4" width="6" height="6" rx="1"/></svg>
|
|
3411
|
+
</div>
|
|
3379
3412
|
</div>
|
|
3380
3413
|
<div class="metric-value" id="total-actions">0</div>
|
|
3381
3414
|
<div class="metric-change positive" id="actions-change">\u2191 0% this session</div>
|
|
@@ -3383,7 +3416,9 @@ function getDashboardHTML() {
|
|
|
3383
3416
|
<div class="metric-card">
|
|
3384
3417
|
<div class="metric-header">
|
|
3385
3418
|
<div class="metric-label">Bugs Found</div>
|
|
3386
|
-
<div class="metric-icon"
|
|
3419
|
+
<div class="metric-icon">
|
|
3420
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-todo-icon lucide-list-todo"><path d="M13 5h8"/><path d="M13 12h8"/><path d="M13 19h8"/><path d="m3 17 2 2 4-4"/><rect x="3" y="4" width="6" height="6" rx="1"/></svg>
|
|
3421
|
+
</div>
|
|
3387
3422
|
</div>
|
|
3388
3423
|
<div class="metric-value" id="bugs-found">0</div>
|
|
3389
3424
|
<div class="metric-change negative" id="bugs-change">\u2193 0 from yesterday</div>
|
|
@@ -3391,7 +3426,9 @@ function getDashboardHTML() {
|
|
|
3391
3426
|
<div class="metric-card">
|
|
3392
3427
|
<div class="metric-header">
|
|
3393
3428
|
<div class="metric-label">Success Rate</div>
|
|
3394
|
-
<div class="metric-icon"
|
|
3429
|
+
<div class="metric-icon">
|
|
3430
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-cloud-check-icon lucide-cloud-check"><path d="m17 15-5.5 5.5L9 18"/><path d="M5.516 16.07A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 3.501 7.327"/></svg>
|
|
3431
|
+
</div>
|
|
3395
3432
|
</div>
|
|
3396
3433
|
<div class="metric-value" id="success-rate">\u2014</div>
|
|
3397
3434
|
<div class="metric-change positive" id="rate-change">\u2191 0 pts improvement</div>
|
|
@@ -5655,672 +5692,860 @@ function getEnvHTML() {
|
|
|
5655
5692
|
<head>
|
|
5656
5693
|
<meta charset="UTF-8">
|
|
5657
5694
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5658
|
-
<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">
|
|
5659
5698
|
<style>
|
|
5660
|
-
|
|
5661
|
-
|
|
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
|
+
|
|
5662
5727
|
body {
|
|
5663
|
-
font-family:
|
|
5664
|
-
background:
|
|
5728
|
+
font-family: var(--sans);
|
|
5729
|
+
background: var(--bg);
|
|
5730
|
+
color: var(--text-1);
|
|
5665
5731
|
min-height: 100vh;
|
|
5666
|
-
|
|
5732
|
+
overflow-x: hidden;
|
|
5667
5733
|
}
|
|
5668
5734
|
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5735
|
+
/* \u2500\u2500 Layout \u2500\u2500 */
|
|
5736
|
+
.shell {
|
|
5737
|
+
display: grid;
|
|
5738
|
+
grid-template-columns: 220px 1fr;
|
|
5739
|
+
min-height: 100vh;
|
|
5672
5740
|
}
|
|
5673
5741
|
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
border-radius: 12px;
|
|
5679
|
-
margin-bottom: 20px;
|
|
5680
|
-
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);
|
|
5681
5746
|
display: flex;
|
|
5682
|
-
|
|
5683
|
-
|
|
5747
|
+
flex-direction: column;
|
|
5748
|
+
padding: 28px 0;
|
|
5749
|
+
position: sticky;
|
|
5750
|
+
top: 0;
|
|
5751
|
+
height: 100vh;
|
|
5684
5752
|
}
|
|
5685
5753
|
|
|
5686
|
-
.
|
|
5687
|
-
font-size: 24px;
|
|
5688
|
-
color: #1a202c;
|
|
5754
|
+
.logo {
|
|
5689
5755
|
display: flex;
|
|
5690
5756
|
align-items: center;
|
|
5691
5757
|
gap: 10px;
|
|
5758
|
+
padding: 0 24px 32px;
|
|
5759
|
+
border-bottom: 1px solid var(--border);
|
|
5760
|
+
margin-bottom: 12px;
|
|
5692
5761
|
}
|
|
5693
5762
|
|
|
5694
|
-
.
|
|
5695
|
-
|
|
5696
|
-
|
|
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;
|
|
5697
5772
|
}
|
|
5698
5773
|
|
|
5699
|
-
.
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
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;
|
|
5703
5797
|
font-size: 14px;
|
|
5704
5798
|
font-weight: 600;
|
|
5799
|
+
transition: all 0.15s ease;
|
|
5705
5800
|
cursor: pointer;
|
|
5706
|
-
transition: all 0.2s;
|
|
5707
|
-
text-decoration: none;
|
|
5708
|
-
display: inline-flex;
|
|
5709
|
-
align-items: center;
|
|
5710
|
-
gap: 8px;
|
|
5711
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; }
|
|
5712
5805
|
|
|
5713
|
-
.
|
|
5714
|
-
|
|
5715
|
-
|
|
5806
|
+
.sidebar-footer {
|
|
5807
|
+
padding: 16px 24px;
|
|
5808
|
+
border-top: 1px solid var(--border);
|
|
5716
5809
|
}
|
|
5717
5810
|
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
transform: translateY(-1px);
|
|
5721
|
-
}
|
|
5811
|
+
/* \u2500\u2500 Main \u2500\u2500 */
|
|
5812
|
+
main { display: flex; flex-direction: column; min-height: 100vh; overflow-y: auto; }
|
|
5722
5813
|
|
|
5723
|
-
.
|
|
5724
|
-
|
|
5725
|
-
|
|
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;
|
|
5726
5824
|
}
|
|
5727
5825
|
|
|
5728
|
-
.
|
|
5729
|
-
|
|
5730
|
-
}
|
|
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; }
|
|
5731
5828
|
|
|
5732
|
-
.
|
|
5733
|
-
background: #48bb78;
|
|
5734
|
-
color: white;
|
|
5735
|
-
}
|
|
5829
|
+
.topbar-actions { display: flex; align-items: center; gap: 10px; }
|
|
5736
5830
|
|
|
5737
|
-
.btn
|
|
5738
|
-
|
|
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;
|
|
5739
5844
|
}
|
|
5845
|
+
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
5740
5846
|
|
|
5741
|
-
.btn
|
|
5742
|
-
|
|
5743
|
-
|
|
5847
|
+
.btn-ghost {
|
|
5848
|
+
background: var(--panel);
|
|
5849
|
+
color: var(--text-2);
|
|
5850
|
+
border: 1px solid var(--border);
|
|
5744
5851
|
}
|
|
5852
|
+
.btn-ghost:hover { border-color: var(--border-hi); color: var(--text-1); }
|
|
5745
5853
|
|
|
5746
|
-
.
|
|
5747
|
-
background:
|
|
5748
|
-
|
|
5749
|
-
padding: 30px;
|
|
5750
|
-
border-radius: 12px;
|
|
5751
|
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
5854
|
+
.btn-primary {
|
|
5855
|
+
background: var(--accent);
|
|
5856
|
+
color: #fff;
|
|
5752
5857
|
}
|
|
5858
|
+
.btn-primary:hover:not(:disabled) { background: #ea580c; box-shadow: 0 0 20px rgba(249,115,22,0.35); }
|
|
5753
5859
|
|
|
5754
|
-
|
|
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 {
|
|
5755
5865
|
display: flex;
|
|
5756
|
-
gap:
|
|
5757
|
-
|
|
5758
|
-
border
|
|
5759
|
-
|
|
5866
|
+
gap: 4px;
|
|
5867
|
+
background: var(--surface);
|
|
5868
|
+
border: 1px solid var(--border);
|
|
5869
|
+
border-radius: 10px;
|
|
5870
|
+
padding: 4px;
|
|
5871
|
+
flex-wrap: wrap;
|
|
5760
5872
|
}
|
|
5761
5873
|
|
|
5762
|
-
.tab {
|
|
5763
|
-
padding:
|
|
5874
|
+
.tab-btn {
|
|
5875
|
+
padding: 7px 14px;
|
|
5876
|
+
background: transparent;
|
|
5764
5877
|
border: none;
|
|
5765
|
-
|
|
5766
|
-
|
|
5878
|
+
border-radius: 7px;
|
|
5879
|
+
color: var(--text-3);
|
|
5880
|
+
font-family: var(--sans);
|
|
5881
|
+
font-size: 12px;
|
|
5767
5882
|
font-weight: 600;
|
|
5768
|
-
color: #718096;
|
|
5769
5883
|
cursor: pointer;
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
color: #667eea;
|
|
5776
|
-
border-bottom-color: #667eea;
|
|
5884
|
+
transition: all 0.15s ease;
|
|
5885
|
+
white-space: nowrap;
|
|
5886
|
+
display: flex;
|
|
5887
|
+
align-items: center;
|
|
5888
|
+
gap: 5px;
|
|
5777
5889
|
}
|
|
5778
|
-
|
|
5779
|
-
.tab
|
|
5780
|
-
|
|
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);
|
|
5781
5895
|
}
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5896
|
+
.tab-btn .tab-dot {
|
|
5897
|
+
width: 6px; height: 6px;
|
|
5898
|
+
border-radius: 50%;
|
|
5899
|
+
background: var(--text-3);
|
|
5785
5900
|
}
|
|
5901
|
+
.tab-btn.has-required .tab-dot { background: var(--amber); }
|
|
5902
|
+
.tab-btn.active .tab-dot { background: var(--accent); }
|
|
5786
5903
|
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
}
|
|
5904
|
+
/* \u2500\u2500 Section \u2500\u2500 */
|
|
5905
|
+
.section { display: none; flex-direction: column; gap: 16px; }
|
|
5906
|
+
.section.active { display: flex; }
|
|
5790
5907
|
|
|
5791
|
-
.
|
|
5908
|
+
.section-header {
|
|
5792
5909
|
display: flex;
|
|
5793
|
-
justify-content: space-between;
|
|
5794
5910
|
align-items: center;
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
.category-title {
|
|
5799
|
-
font-size: 18px;
|
|
5800
|
-
font-weight: 600;
|
|
5801
|
-
color: #2d3748;
|
|
5802
|
-
}
|
|
5803
|
-
|
|
5804
|
-
.env-grid {
|
|
5805
|
-
display: grid;
|
|
5806
|
-
gap: 20px;
|
|
5911
|
+
gap: 12px;
|
|
5912
|
+
margin-bottom: 4px;
|
|
5807
5913
|
}
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5914
|
+
.section-icon {
|
|
5915
|
+
width: 36px; height: 36px;
|
|
5916
|
+
background: var(--accent-lo);
|
|
5917
|
+
border: 1px solid var(--accent-md);
|
|
5811
5918
|
border-radius: 8px;
|
|
5812
|
-
|
|
5813
|
-
|
|
5919
|
+
display: grid;
|
|
5920
|
+
place-items: center;
|
|
5921
|
+
font-size: 16px;
|
|
5814
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; }
|
|
5815
5925
|
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
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;
|
|
5819
5933
|
}
|
|
5934
|
+
.env-card:hover { border-color: var(--border-hi); }
|
|
5935
|
+
.env-card.has-value { border-color: rgba(249,115,22,0.15); }
|
|
5820
5936
|
|
|
5821
|
-
.env-
|
|
5937
|
+
.env-card-head {
|
|
5822
5938
|
display: flex;
|
|
5823
5939
|
justify-content: space-between;
|
|
5824
5940
|
align-items: flex-start;
|
|
5825
|
-
margin-bottom:
|
|
5941
|
+
margin-bottom: 6px;
|
|
5826
5942
|
}
|
|
5827
5943
|
|
|
5828
|
-
.env-
|
|
5829
|
-
font-
|
|
5830
|
-
|
|
5831
|
-
font-
|
|
5944
|
+
.env-key {
|
|
5945
|
+
font-family: var(--mono);
|
|
5946
|
+
font-size: 13px;
|
|
5947
|
+
font-weight: 500;
|
|
5948
|
+
color: var(--text-1);
|
|
5832
5949
|
display: flex;
|
|
5833
5950
|
align-items: center;
|
|
5834
5951
|
gap: 8px;
|
|
5835
5952
|
}
|
|
5836
5953
|
|
|
5837
|
-
.required
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
font-
|
|
5841
|
-
|
|
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);
|
|
5842
5963
|
border-radius: 4px;
|
|
5964
|
+
padding: 2px 6px;
|
|
5965
|
+
}
|
|
5966
|
+
.badge-sensitive {
|
|
5967
|
+
font-size: 9px;
|
|
5843
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;
|
|
5844
5977
|
}
|
|
5845
5978
|
|
|
5846
|
-
.env-
|
|
5847
|
-
font-
|
|
5848
|
-
|
|
5849
|
-
|
|
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;
|
|
5850
5985
|
}
|
|
5851
5986
|
|
|
5852
|
-
.env-input-
|
|
5987
|
+
.env-input-row {
|
|
5853
5988
|
display: flex;
|
|
5854
|
-
gap:
|
|
5989
|
+
gap: 8px;
|
|
5855
5990
|
align-items: center;
|
|
5856
5991
|
}
|
|
5857
5992
|
|
|
5858
|
-
.env-input {
|
|
5993
|
+
.env-input, .env-select {
|
|
5859
5994
|
flex: 1;
|
|
5860
|
-
|
|
5861
|
-
border: 1px solid
|
|
5862
|
-
border-radius:
|
|
5863
|
-
|
|
5864
|
-
font-family:
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
.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);
|
|
5869
6002
|
outline: none;
|
|
5870
|
-
border-color
|
|
5871
|
-
|
|
6003
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
6004
|
+
appearance: none;
|
|
5872
6005
|
}
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
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);
|
|
5876
6009
|
}
|
|
6010
|
+
.env-input.changed { border-color: rgba(249,115,22,0.5); }
|
|
6011
|
+
.env-input.invalid { border-color: var(--red); }
|
|
5877
6012
|
|
|
5878
|
-
.env-
|
|
5879
|
-
display: flex;
|
|
5880
|
-
gap: 5px;
|
|
5881
|
-
}
|
|
6013
|
+
.env-select option { background: var(--panel); }
|
|
5882
6014
|
|
|
5883
|
-
.
|
|
5884
|
-
|
|
5885
|
-
border:
|
|
5886
|
-
|
|
5887
|
-
|
|
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);
|
|
5888
6021
|
cursor: pointer;
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
background: #cbd5e0;
|
|
5895
|
-
}
|
|
5896
|
-
|
|
5897
|
-
.icon-btn.test {
|
|
5898
|
-
background: #bee3f8;
|
|
5899
|
-
color: #2c5282;
|
|
5900
|
-
}
|
|
5901
|
-
|
|
5902
|
-
.icon-btn.test:hover {
|
|
5903
|
-
background: #90cdf4;
|
|
5904
|
-
}
|
|
5905
|
-
|
|
5906
|
-
.icon-btn.generate {
|
|
5907
|
-
background: #c6f6d5;
|
|
5908
|
-
color: #22543d;
|
|
5909
|
-
}
|
|
5910
|
-
|
|
5911
|
-
.icon-btn.generate:hover {
|
|
5912
|
-
background: #9ae6b4;
|
|
6022
|
+
display: grid;
|
|
6023
|
+
place-items: center;
|
|
6024
|
+
font-size: 14px;
|
|
6025
|
+
transition: all 0.15s;
|
|
6026
|
+
flex-shrink: 0;
|
|
5913
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); }
|
|
5914
6031
|
|
|
5915
|
-
.
|
|
5916
|
-
|
|
5917
|
-
font-size:
|
|
5918
|
-
margin-top:
|
|
6032
|
+
.env-feedback {
|
|
6033
|
+
font-family: var(--mono);
|
|
6034
|
+
font-size: 11px;
|
|
6035
|
+
margin-top: 8px;
|
|
6036
|
+
min-height: 16px;
|
|
5919
6037
|
}
|
|
6038
|
+
.env-feedback.error { color: var(--red); }
|
|
6039
|
+
.env-feedback.success { color: var(--green); }
|
|
5920
6040
|
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
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;
|
|
5925
6050
|
}
|
|
5926
6051
|
|
|
5927
|
-
.
|
|
5928
|
-
padding:
|
|
5929
|
-
border-radius:
|
|
5930
|
-
|
|
6052
|
+
.toast {
|
|
6053
|
+
padding: 12px 18px;
|
|
6054
|
+
border-radius: 10px;
|
|
6055
|
+
font-size: 13px;
|
|
6056
|
+
font-weight: 600;
|
|
5931
6057
|
display: flex;
|
|
5932
6058
|
align-items: center;
|
|
5933
6059
|
gap: 10px;
|
|
6060
|
+
animation: slideIn 0.2s ease;
|
|
6061
|
+
max-width: 380px;
|
|
5934
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); }
|
|
5935
6067
|
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
color: #92400e;
|
|
5940
|
-
}
|
|
5941
|
-
|
|
5942
|
-
.alert-info {
|
|
5943
|
-
background: #eff6ff;
|
|
5944
|
-
border-left: 4px solid #3b82f6;
|
|
5945
|
-
color: #1e40af;
|
|
6068
|
+
@keyframes slideIn {
|
|
6069
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
6070
|
+
to { opacity: 1; transform: translateY(0); }
|
|
5946
6071
|
}
|
|
5947
6072
|
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
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;
|
|
5952
6081
|
}
|
|
6082
|
+
.modal-backdrop.open { display: flex; }
|
|
5953
6083
|
|
|
5954
|
-
.
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
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;
|
|
5958
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; }
|
|
5959
6104
|
|
|
6105
|
+
/* \u2500\u2500 Spinner \u2500\u2500 */
|
|
5960
6106
|
.spinner {
|
|
5961
|
-
|
|
5962
|
-
border
|
|
6107
|
+
width: 36px; height: 36px;
|
|
6108
|
+
border: 3px solid var(--border);
|
|
6109
|
+
border-top-color: var(--accent);
|
|
5963
6110
|
border-radius: 50%;
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
animation: spin 1s linear infinite;
|
|
5967
|
-
margin: 0 auto 20px;
|
|
6111
|
+
animation: spin 0.8s linear infinite;
|
|
6112
|
+
margin: 0 auto 16px;
|
|
5968
6113
|
}
|
|
6114
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
5969
6115
|
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
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;
|
|
5973
6122
|
}
|
|
5974
6123
|
|
|
5975
|
-
|
|
6124
|
+
/* \u2500\u2500 Restart banner \u2500\u2500 */
|
|
6125
|
+
.restart-banner {
|
|
5976
6126
|
display: none;
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
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;
|
|
5984
6134
|
align-items: center;
|
|
5985
|
-
|
|
6135
|
+
gap: 10px;
|
|
5986
6136
|
}
|
|
6137
|
+
.restart-banner.show { display: flex; }
|
|
6138
|
+
</style>
|
|
6139
|
+
</head>
|
|
6140
|
+
<body>
|
|
6141
|
+
<div class="shell">
|
|
5987
6142
|
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
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>
|
|
5991
6152
|
|
|
5992
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
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>
|
|
6000
6164
|
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
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>
|
|
6007
6175
|
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
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>
|
|
6012
6187
|
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
gap: 10px;
|
|
6016
|
-
justify-content: flex-end;
|
|
6017
|
-
}
|
|
6018
|
-
</style>
|
|
6019
|
-
</head>
|
|
6020
|
-
<body>
|
|
6021
|
-
<div class="container">
|
|
6022
|
-
<div class="header">
|
|
6023
|
-
<h1>
|
|
6024
|
-
<span>\u2699\uFE0F</span>
|
|
6188
|
+
<div class="sidebar-footer">
|
|
6189
|
+
<div style="font-family:var(--mono);font-size:11px;color:var(--text-3);">
|
|
6025
6190
|
Environment Variables
|
|
6026
|
-
</
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
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>
|
|
6030
6207
|
</div>
|
|
6031
6208
|
</div>
|
|
6032
6209
|
|
|
6033
6210
|
<div class="content">
|
|
6034
|
-
|
|
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">
|
|
6035
6219
|
<div class="spinner"></div>
|
|
6036
|
-
|
|
6220
|
+
Loading environment variables\u2026
|
|
6037
6221
|
</div>
|
|
6038
6222
|
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
<button class="tab" data-category="web">\u{1F310} Web Server</button>
|
|
6048
|
-
<button class="tab" data-category="agent">\u{1F916} Agent</button>
|
|
6049
|
-
<button class="tab" data-category="database">\u{1F4BE} Database</button>
|
|
6050
|
-
<button class="tab" data-category="notifications">\u{1F514} Notifications</button>
|
|
6051
|
-
</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>
|
|
6052
6231
|
|
|
6053
|
-
<div id="categories"></div>
|
|
6054
6232
|
</div>
|
|
6055
6233
|
</div>
|
|
6056
|
-
</
|
|
6234
|
+
</main>
|
|
6235
|
+
</div>
|
|
6057
6236
|
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
<div class="modal-
|
|
6064
|
-
|
|
6065
|
-
|
|
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>
|
|
6066
6246
|
</div>
|
|
6067
6247
|
</div>
|
|
6248
|
+
</div>
|
|
6068
6249
|
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
let changedVariables = {};
|
|
6072
|
-
let restartRequired = false;
|
|
6250
|
+
<!-- Toast zone -->
|
|
6251
|
+
<div class="toast-zone" id="toastZone"></div>
|
|
6073
6252
|
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
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
|
+
}
|
|
6090
6285
|
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
const section = document.createElement('div');
|
|
6098
|
-
section.className = 'category-section' + (index === 0 ? ' active' : '');
|
|
6099
|
-
section.dataset.category = category;
|
|
6100
|
-
|
|
6101
|
-
const vars = envVariables.filter(v => v.category === category);
|
|
6102
|
-
|
|
6103
|
-
section.innerHTML = \`
|
|
6104
|
-
<div class="category-header">
|
|
6105
|
-
<div class="category-title">\${getCategoryTitle(category)}</div>
|
|
6106
|
-
</div>
|
|
6107
|
-
<div class="env-grid">
|
|
6108
|
-
\${vars.map(v => renderEnvItem(v)).join('')}
|
|
6109
|
-
</div>
|
|
6110
|
-
\`;
|
|
6111
|
-
|
|
6112
|
-
container.appendChild(section);
|
|
6113
|
-
});
|
|
6114
|
-
}
|
|
6286
|
+
/* \u2500\u2500 Render \u2500\u2500 */
|
|
6287
|
+
function renderAll() {
|
|
6288
|
+
renderTabBar();
|
|
6289
|
+
renderSections();
|
|
6290
|
+
activateTab(TABS[0].id);
|
|
6291
|
+
}
|
|
6115
6292
|
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
|
|
6127
|
-
|
|
6128
|
-
|
|
6129
|
-
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
envVar.type === 'boolean' ?
|
|
6139
|
-
\`<select class="env-input" data-key="\${envVar.key}" onchange="handleChange(this)">
|
|
6140
|
-
<option value="">-- Select --</option>
|
|
6141
|
-
<option value="true" \${value === 'true' ? 'selected' : ''}>true</option>
|
|
6142
|
-
<option value="false" \${value === 'false' ? 'selected' : ''}>false</option>
|
|
6143
|
-
</select>\` :
|
|
6144
|
-
\`<input
|
|
6145
|
-
type="\${inputType}"
|
|
6146
|
-
class="env-input"
|
|
6147
|
-
data-key="\${envVar.key}"
|
|
6148
|
-
value="\${value}"
|
|
6149
|
-
placeholder="\${envVar.placeholder || ''}"
|
|
6150
|
-
onchange="handleChange(this)"
|
|
6151
|
-
/>\`
|
|
6152
|
-
}
|
|
6153
|
-
<div class="env-actions">
|
|
6154
|
-
\${envVar.testable ? \`<button class="icon-btn test" onclick="testVariable('\${envVar.key}')" title="Test">\u{1F9EA}</button>\` : ''}
|
|
6155
|
-
\${envVar.key === 'OPENQA_JWT_SECRET' ? \`<button class="icon-btn generate" onclick="generateSecret('\${envVar.key}')" title="Generate">\u{1F511}</button>\` : ''}
|
|
6156
|
-
</div>
|
|
6157
|
-
</div>
|
|
6158
|
-
<div class="error-message" id="error-\${envVar.key}"></div>
|
|
6159
|
-
<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>
|
|
6160
6315
|
</div>
|
|
6161
|
-
|
|
6162
|
-
|
|
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
|
+
}
|
|
6163
6322
|
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
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
|
+
}
|
|
6176
6348
|
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
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');
|
|
6193
6432
|
}
|
|
6194
|
-
|
|
6195
|
-
const result = await response.json();
|
|
6196
|
-
restartRequired = result.restartRequired;
|
|
6197
|
-
|
|
6198
|
-
showAlert('success', \`\u2705 Saved \${result.updated} variable(s) successfully!\` +
|
|
6199
|
-
(restartRequired ? ' \u26A0\uFE0F Restart required for changes to take effect.' : ''));
|
|
6200
|
-
|
|
6201
|
-
changedVariables = {};
|
|
6202
|
-
saveBtn.textContent = '\u{1F4BE} Save Changes';
|
|
6203
|
-
|
|
6204
|
-
// Reload to show updated values
|
|
6205
|
-
setTimeout(() => location.reload(), 2000);
|
|
6206
|
-
} catch (error) {
|
|
6207
|
-
showAlert('error', 'Failed to save: ' + error.message);
|
|
6208
|
-
saveBtn.disabled = false;
|
|
6209
|
-
saveBtn.textContent = '\u{1F4BE} Save Changes';
|
|
6210
6433
|
}
|
|
6434
|
+
toast('error', errStr);
|
|
6435
|
+
btn.disabled = false;
|
|
6436
|
+
btn.innerHTML = '\u{1F4BE} Save Changes';
|
|
6437
|
+
return;
|
|
6211
6438
|
}
|
|
6212
6439
|
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
const value = input.value;
|
|
6217
|
-
|
|
6218
|
-
if (!value) {
|
|
6219
|
-
showAlert('warning', 'Please enter a value first');
|
|
6220
|
-
return;
|
|
6221
|
-
}
|
|
6222
|
-
|
|
6223
|
-
try {
|
|
6224
|
-
const response = await fetch(\`/api/env/test/\${key}\`, {
|
|
6225
|
-
method: 'POST',
|
|
6226
|
-
headers: { 'Content-Type': 'application/json' },
|
|
6227
|
-
body: JSON.stringify({ value }),
|
|
6228
|
-
});
|
|
6229
|
-
|
|
6230
|
-
const result = await response.json();
|
|
6231
|
-
showTestResult(result);
|
|
6232
|
-
} catch (error) {
|
|
6233
|
-
showTestResult({ success: false, message: 'Test failed: ' + error.message });
|
|
6234
|
-
}
|
|
6440
|
+
toast('success', '\u2705 Saved ' + body.updated + ' variable(s)');
|
|
6441
|
+
if (body.restartRequired) {
|
|
6442
|
+
document.getElementById('restartBanner').classList.add('show');
|
|
6235
6443
|
}
|
|
6236
6444
|
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
const input = document.querySelector(\`[data-key="\${key}"]\`);
|
|
6248
|
-
input.value = result.value;
|
|
6249
|
-
handleChange(input);
|
|
6250
|
-
|
|
6251
|
-
document.getElementById(\`success-\${key}\`).textContent = '\u2705 Secret generated!';
|
|
6252
|
-
} catch (error) {
|
|
6253
|
-
document.getElementById(\`error-\${key}\`).textContent = 'Failed to generate: ' + error.message;
|
|
6254
|
-
}
|
|
6255
|
-
}
|
|
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
|
+
}
|
|
6256
6455
|
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
|
|
6262
|
-
resultDiv.innerHTML = \`
|
|
6263
|
-
<div class="alert \${result.success ? 'alert-success' : 'alert-warning'}">
|
|
6264
|
-
\${result.success ? '\u2705' : '\u274C'} \${result.message}
|
|
6265
|
-
</div>
|
|
6266
|
-
\`;
|
|
6267
|
-
|
|
6268
|
-
modal.classList.add('show');
|
|
6269
|
-
}
|
|
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; }
|
|
6270
6461
|
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6462
|
+
setFeedback(key, '', '');
|
|
6463
|
+
const btn = document.querySelector('[onclick="testVar(\\''+key+'\\')"]');
|
|
6464
|
+
if (btn) { btn.textContent = '\u23F3'; btn.disabled = true; }
|
|
6274
6465
|
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
alerts.innerHTML = \`
|
|
6282
|
-
<div class="alert \${alertClass}">
|
|
6283
|
-
\${message}
|
|
6284
|
-
</div>
|
|
6285
|
-
\`;
|
|
6286
|
-
|
|
6287
|
-
setTimeout(() => alerts.innerHTML = '', 5000);
|
|
6288
|
-
}
|
|
6289
|
-
|
|
6290
|
-
// Get category title
|
|
6291
|
-
function getCategoryTitle(category) {
|
|
6292
|
-
const titles = {
|
|
6293
|
-
llm: '\u{1F916} LLM Configuration',
|
|
6294
|
-
security: '\u{1F512} Security Settings',
|
|
6295
|
-
target: '\u{1F3AF} Target Application',
|
|
6296
|
-
github: '\u{1F419} GitHub Integration',
|
|
6297
|
-
web: '\u{1F310} Web Server',
|
|
6298
|
-
agent: '\u{1F916} Agent Configuration',
|
|
6299
|
-
database: '\u{1F4BE} Database',
|
|
6300
|
-
notifications: '\u{1F514} Notifications',
|
|
6301
|
-
};
|
|
6302
|
-
return titles[category] || category;
|
|
6303
|
-
}
|
|
6304
|
-
|
|
6305
|
-
// Tab switching
|
|
6306
|
-
document.addEventListener('click', (e) => {
|
|
6307
|
-
if (e.target.classList.contains('tab')) {
|
|
6308
|
-
const category = e.target.dataset.category;
|
|
6309
|
-
|
|
6310
|
-
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
6311
|
-
e.target.classList.add('active');
|
|
6312
|
-
|
|
6313
|
-
document.querySelectorAll('.category-section').forEach(s => s.classList.remove('active'));
|
|
6314
|
-
document.querySelector(\`[data-category="\${category}"]\`).classList.add('active');
|
|
6315
|
-
}
|
|
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',
|
|
6316
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
|
+
}
|
|
6482
|
+
|
|
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
|
+
}
|
|
6502
|
+
|
|
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
|
+
}
|
|
6317
6513
|
|
|
6318
|
-
|
|
6319
|
-
|
|
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
|
+
}
|
|
6320
6523
|
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
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>
|
|
6324
6549
|
</body>
|
|
6325
6550
|
</html>`;
|
|
6326
6551
|
}
|