@openqa/cli 2.1.1 → 2.1.2

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