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