@halilertekin/claude-code-router-config 2.0.2 → 2.0.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.4
4
+ - UI üzerinden `.env` anahtarları ekleme/güncelleme eklendi.
5
+ - API tarafına `GET/POST /api/env` eklendi.
6
+
7
+ ## 2.0.3
8
+ - API anahtarları için `~/.env` otomatik yükleme eklendi (CLI + health monitor).
9
+ - Sağlayıcı API key çözümleme tek noktaya alındı.
10
+
3
11
  ## 2.0.2
4
12
  - UI sadeleştirildi, responsive hale getirildi ve Türkçe/Hollandaca desteği eklendi.
5
13
 
package/README.md CHANGED
@@ -1,10 +1,12 @@
1
1
  # Claude Code Router Config - Advanced Multi-Provider Setup
2
2
 
3
- 🚀 **v2.0.2** - Unified router + config package with z.ai (GLM 4.7) support, advanced CLI tools, analytics, smart routing, and configuration templates!
3
+ 🚀 **v2.0.4** - Unified router + config package with z.ai (GLM 4.7) support, advanced CLI tools, analytics, smart routing, and configuration templates!
4
4
 
5
5
  Use Claude Code as a single interface to access multiple AI providers with intelligent routing for optimal performance, cost, and quality.
6
6
 
7
- ## ✨ New in v2.0.2
7
+ ## ✨ New in v2.0.4
8
+ - UI üzerinden `.env` anahtarları ekleme/güncelleme (TR/NL).
9
+ - `~/.env` otomatik yükleme ile API anahtarlarının bulunması (CLI + health monitor).
8
10
  - **z.ai Support**: Native integration for GLM-4.7 via z.ai (PPInfra).
9
11
  - **Lightweight Mode**: New `ccc` function for zero-dependency routing.
10
12
  - **Direct GLM Alias**: Type `glm` to launch Claude Code with GLM-4.7 immediately.
package/cli/commands.js CHANGED
@@ -5,6 +5,7 @@ const os = require('os');
5
5
  const path = require('path');
6
6
  const { spawn } = require('child_process');
7
7
  const chalk = require('./chalk-safe');
8
+ const { resolveProviderKey } = require('../router/config');
8
9
  const configPath = path.join(os.homedir(), '.claude-code-router');
9
10
  const pidFile = path.join(configPath, 'router.pid');
10
11
  const serverScript = path.join(__dirname, '..', 'router', 'server.js');
@@ -137,7 +138,7 @@ async function testProvider(provider, model) {
137
138
  };
138
139
 
139
140
  // For now, just check if API key is set
140
- const apiKey = process.env[providerConfig.api_key.replace('$', '')];
141
+ const apiKey = resolveProviderKey(providerConfig);
141
142
  if (!apiKey) {
142
143
  throw new Error(`API key not set for ${provider}`);
143
144
  }
@@ -214,7 +215,7 @@ async function showDetailedStatus(options = {}) {
214
215
  // Provider status
215
216
  console.log(chalk.yellow('\nProviders:'));
216
217
  for (const provider of config.Providers) {
217
- const apiKey = process.env[provider.api_key.replace('$', '')];
218
+ const apiKey = resolveProviderKey(provider);
218
219
  const status = apiKey ? '🟢 Active' : '🔴 Missing API Key';
219
220
  console.log(` ${provider.name}: ${status}`);
220
221
  }
@@ -3,6 +3,7 @@ const { spawn } = require('child_process');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
+ const { resolveProviderKey } = require('../router/config');
6
7
 
7
8
  class HealthMonitor {
8
9
  constructor(options = {}) {
@@ -81,7 +82,7 @@ class HealthMonitor {
81
82
 
82
83
  try {
83
84
  // Check if API key is configured
84
- const apiKey = process.env[provider.api_key.replace('$', '')];
85
+ const apiKey = resolveProviderKey(provider);
85
86
  if (!apiKey) {
86
87
  throw new Error('API key not configured');
87
88
  }
@@ -172,11 +173,18 @@ class HealthMonitor {
172
173
  };
173
174
 
174
175
  // Use curl for testing (more reliable than node HTTP for different APIs)
176
+ const apiKey = resolveProviderKey(provider);
177
+ if (!apiKey) {
178
+ clearTimeout(timeout);
179
+ resolve({ success: false, error: 'API key not configured' });
180
+ return;
181
+ }
182
+
175
183
  const curl = spawn('curl', [
176
184
  '-s', '-w', '%{http_code}',
177
185
  '-o', '/dev/null',
178
186
  '-m', Math.floor(this.timeout / 1000),
179
- '-H', `Authorization: Bearer ${process.env[provider.api_key.replace('$', '')]}`,
187
+ '-H', `Authorization: Bearer ${apiKey}`,
180
188
  '-H', 'Content-Type: application/json',
181
189
  '-d', JSON.stringify(testRequest),
182
190
  provider.api_base_url
@@ -469,4 +477,4 @@ const healthMonitor = new HealthMonitor();
469
477
  module.exports = {
470
478
  HealthMonitor,
471
479
  healthMonitor
472
- };
480
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halilertekin/claude-code-router-config",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "Multi-provider configuration for Claude Code Router with intent-based routing, advanced CLI tools, analytics, and smart routing. Setup OpenAI, Anthropic, Gemini, Qwen, GLM, OpenRouter, and GitHub Copilot with intelligent routing.",
5
5
  "main": "install.js",
6
6
  "bin": {
package/router/config.js CHANGED
@@ -4,6 +4,53 @@ const path = require('path');
4
4
 
5
5
  const DEFAULT_CONFIG_DIR = path.join(os.homedir(), '.claude-code-router');
6
6
  const DEFAULT_CONFIG_PATH = path.join(DEFAULT_CONFIG_DIR, 'config.json');
7
+ const DEFAULT_ENV_PATH = path.join(os.homedir(), '.env');
8
+ let envLoaded = false;
9
+
10
+ function parseEnvLine(line) {
11
+ const trimmed = line.trim();
12
+ if (!trimmed || trimmed.startsWith('#')) return null;
13
+
14
+ const withoutExport = trimmed.startsWith('export ')
15
+ ? trimmed.slice('export '.length).trim()
16
+ : trimmed;
17
+
18
+ const separatorIndex = withoutExport.indexOf('=');
19
+ if (separatorIndex === -1) return null;
20
+
21
+ const key = withoutExport.slice(0, separatorIndex).trim();
22
+ if (!key) return null;
23
+
24
+ let value = withoutExport.slice(separatorIndex + 1).trim();
25
+ const hasQuotes = (value.startsWith('"') && value.endsWith('"'))
26
+ || (value.startsWith('\'') && value.endsWith('\''));
27
+ if (hasQuotes) {
28
+ value = value.slice(1, -1);
29
+ }
30
+
31
+ return { key, value };
32
+ }
33
+
34
+ function getEnvPath() {
35
+ return process.env.CCR_ENV_PATH || DEFAULT_ENV_PATH;
36
+ }
37
+
38
+ function loadDotenv() {
39
+ if (envLoaded) return;
40
+ envLoaded = true;
41
+
42
+ const envPath = getEnvPath();
43
+ if (!fs.existsSync(envPath)) return;
44
+
45
+ const content = fs.readFileSync(envPath, 'utf8');
46
+ content.split(/\r?\n/).forEach((line) => {
47
+ const parsed = parseEnvLine(line);
48
+ if (!parsed) return;
49
+ if (process.env[parsed.key] === undefined) {
50
+ process.env[parsed.key] = parsed.value;
51
+ }
52
+ });
53
+ }
7
54
 
8
55
  function resolveEnv(value) {
9
56
  if (typeof value !== 'string') return value;
@@ -19,15 +66,86 @@ function resolveEnv(value) {
19
66
  });
20
67
  }
21
68
 
22
- function resolveConfigValue(value) {
69
+ function readEnvFile() {
70
+ const envPath = getEnvPath();
71
+ if (!fs.existsSync(envPath)) {
72
+ return { path: envPath, entries: {} };
73
+ }
74
+
75
+ const content = fs.readFileSync(envPath, 'utf8');
76
+ const entries = {};
77
+ content.split(/\r?\n/).forEach((line) => {
78
+ const parsed = parseEnvLine(line);
79
+ if (!parsed) return;
80
+ entries[parsed.key] = parsed.value;
81
+ });
82
+
83
+ return { path: envPath, entries };
84
+ }
85
+
86
+ function formatEnvValue(value) {
87
+ const safe = /^[A-Za-z0-9_./:@-]+$/.test(value);
88
+ if (safe) return value;
89
+ const escaped = value
90
+ .replace(/\\/g, '\\\\')
91
+ .replace(/"/g, '\\"')
92
+ .replace(/\n/g, '\\n');
93
+ return `"${escaped}"`;
94
+ }
95
+
96
+ function writeEnvValue(key, value) {
97
+ const envPath = getEnvPath();
98
+ const normalizedKey = String(key || '').trim();
99
+ if (!/^[A-Za-z0-9_]+$/.test(normalizedKey)) {
100
+ throw new Error('Invalid environment key');
101
+ }
102
+
103
+ const normalizedValue = value === undefined || value === null ? '' : String(value);
104
+ const formattedValue = formatEnvValue(normalizedValue);
105
+
106
+ let lines = [];
107
+ let updated = false;
108
+
109
+ if (fs.existsSync(envPath)) {
110
+ const content = fs.readFileSync(envPath, 'utf8');
111
+ lines = content.split(/\r?\n/);
112
+ }
113
+
114
+ lines = lines.map((line) => {
115
+ const parsed = parseEnvLine(line);
116
+ if (!parsed || parsed.key !== normalizedKey) return line;
117
+ const prefix = line.trim().startsWith('export ') ? 'export ' : '';
118
+ updated = true;
119
+ return `${prefix}${normalizedKey}=${formattedValue}`;
120
+ });
121
+
122
+ if (!updated) {
123
+ lines.push(`${normalizedKey}=${formattedValue}`);
124
+ }
125
+
126
+ const output = lines.filter((line, index, arr) => {
127
+ if (index === arr.length - 1) return true;
128
+ return line !== '' || arr[index + 1] !== '';
129
+ });
130
+
131
+ fs.writeFileSync(envPath, `${output.join('\n')}\n`, 'utf8');
132
+
133
+ return { path: envPath, updated: true };
134
+ }
135
+
136
+ function resolveConfigValue(value, key) {
23
137
  if (Array.isArray(value)) {
24
- return value.map(resolveConfigValue);
138
+ return value.map((item) => resolveConfigValue(item, key));
25
139
  }
26
140
  if (value && typeof value === 'object') {
27
141
  return Object.fromEntries(
28
- Object.entries(value).map(([key, val]) => [key, resolveConfigValue(val)])
142
+ Object.entries(value).map(([childKey, val]) => [
143
+ childKey,
144
+ resolveConfigValue(val, childKey)
145
+ ])
29
146
  );
30
147
  }
148
+ if (key === 'api_key') return value;
31
149
  return resolveEnv(value);
32
150
  }
33
151
 
@@ -47,13 +165,14 @@ function applyDefaults(config) {
47
165
  }
48
166
 
49
167
  function loadConfig() {
168
+ loadDotenv();
50
169
  const configPath = process.env.CCR_CONFIG_PATH || DEFAULT_CONFIG_PATH;
51
170
  if (!fs.existsSync(configPath)) {
52
171
  throw new Error(`Config file not found at ${configPath}`);
53
172
  }
54
173
 
55
174
  const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
56
- const resolved = resolveConfigValue(raw);
175
+ const resolved = resolveConfigValue(raw, null);
57
176
  return applyDefaults(resolved);
58
177
  }
59
178
 
@@ -66,6 +185,7 @@ function getConfigDir() {
66
185
  }
67
186
 
68
187
  function resolveProviderKey(provider) {
188
+ loadDotenv();
69
189
  if (!provider?.api_key) return null;
70
190
  if (typeof provider.api_key === 'string' && provider.api_key.startsWith('$')) {
71
191
  const envKey = provider.api_key.slice(1);
@@ -76,6 +196,9 @@ function resolveProviderKey(provider) {
76
196
 
77
197
  module.exports = {
78
198
  loadConfig,
199
+ getEnvPath,
200
+ readEnvFile,
201
+ writeEnvValue,
79
202
  getConfigPath,
80
203
  getConfigDir,
81
204
  resolveProviderKey
package/router/server.js CHANGED
@@ -6,7 +6,13 @@ const fs = require('fs');
6
6
  const os = require('os');
7
7
  const path = require('path');
8
8
 
9
- const { loadConfig, getConfigPath, getConfigDir } = require('./config');
9
+ const {
10
+ loadConfig,
11
+ getConfigPath,
12
+ getConfigDir,
13
+ readEnvFile,
14
+ writeEnvValue
15
+ } = require('./config');
10
16
  const { resolveRoute, estimateTokens } = require('./route');
11
17
  const {
12
18
  anthropicToOpenAI,
@@ -299,6 +305,59 @@ function setupApi(app) {
299
305
  }
300
306
  });
301
307
 
308
+ app.get('/api/env', (req, res) => {
309
+ try {
310
+ const config = loadConfig();
311
+ const envFile = readEnvFile();
312
+ const keys = new Set();
313
+
314
+ (config.Providers || []).forEach((provider) => {
315
+ if (typeof provider.api_key === 'string' && provider.api_key.startsWith('$')) {
316
+ keys.add(provider.api_key.slice(1));
317
+ }
318
+ });
319
+
320
+ if ((config.Providers || []).some((provider) => provider.name?.toLowerCase() === 'openrouter')) {
321
+ keys.add('OPENROUTER_REFERRER');
322
+ keys.add('OPENROUTER_APP_NAME');
323
+ }
324
+
325
+ const data = Array.from(keys)
326
+ .sort()
327
+ .map((key) => {
328
+ const value = process.env[key] || envFile.entries[key] || '';
329
+ return { name: key, present: Boolean(value) };
330
+ });
331
+
332
+ res.json({ success: true, data: { envPath: envFile.path, keys: data } });
333
+ } catch (error) {
334
+ res.status(500).json({ success: false, error: error.message });
335
+ }
336
+ });
337
+
338
+ app.post('/api/env', (req, res) => {
339
+ try {
340
+ const key = String(req.body?.key || '').trim();
341
+ const value = req.body?.value;
342
+
343
+ if (!key) {
344
+ res.status(400).json({ success: false, error: 'Missing env key' });
345
+ return;
346
+ }
347
+
348
+ if (value === undefined || value === null || String(value).trim() === '') {
349
+ res.status(400).json({ success: false, error: 'Missing env value' });
350
+ return;
351
+ }
352
+
353
+ const result = writeEnvValue(key, value);
354
+ process.env[key] = String(value);
355
+ res.json({ success: true, data: { key, path: result.path } });
356
+ } catch (error) {
357
+ res.status(500).json({ success: false, error: error.message });
358
+ }
359
+ });
360
+
302
361
  app.post('/api/config', (req, res) => {
303
362
  try {
304
363
  const configPath = getConfigPath();
@@ -193,11 +193,40 @@ body::before {
193
193
  gap: 10px;
194
194
  }
195
195
 
196
+ .form {
197
+ display: grid;
198
+ gap: 12px;
199
+ }
200
+
201
+ .form-label {
202
+ font-size: 13px;
203
+ text-transform: uppercase;
204
+ letter-spacing: 0.08em;
205
+ color: var(--muted);
206
+ }
207
+
208
+ .field-row {
209
+ display: flex;
210
+ gap: 10px;
211
+ flex-wrap: wrap;
212
+ }
213
+
196
214
  .select {
197
215
  border: 1px solid var(--border);
198
216
  border-radius: 10px;
199
217
  padding: 8px 12px;
200
218
  background: #fff;
219
+ min-width: 160px;
220
+ }
221
+
222
+ .input {
223
+ border: 1px solid var(--border);
224
+ border-radius: 10px;
225
+ padding: 10px 12px;
226
+ background: #fff;
227
+ flex: 1 1 180px;
228
+ min-width: 160px;
229
+ font-family: inherit;
201
230
  }
202
231
 
203
232
  .grid {
@@ -185,6 +185,42 @@
185
185
  </div>
186
186
  </div>
187
187
  </section>
188
+
189
+ <section class="panel">
190
+ <div class="panel-header">
191
+ <h2 data-i18n="env">Ortam Değişkenleri</h2>
192
+ <span class="muted" data-i18n="envHint">Anahtarları hızlıca güncelle</span>
193
+ </div>
194
+ <div class="grid two">
195
+ <div class="card">
196
+ <div class="card-header">
197
+ <h3 data-i18n="envStatus">Durum</h3>
198
+ </div>
199
+ <div class="card-body">
200
+ <div id="env-list" class="list"></div>
201
+ <div id="env-path" class="muted"></div>
202
+ </div>
203
+ </div>
204
+ <div class="card">
205
+ <div class="card-header">
206
+ <h3 data-i18n="envUpdate">Güncelle</h3>
207
+ </div>
208
+ <div class="card-body">
209
+ <div class="form">
210
+ <label class="form-label" data-i18n="envKeyLabel">Anahtar</label>
211
+ <div class="field-row">
212
+ <select id="env-key-select" class="select"></select>
213
+ <input id="env-key-custom" class="input" data-i18n-placeholder="envKeyPlaceholder" placeholder="CUSTOM_KEY">
214
+ </div>
215
+ <label class="form-label" data-i18n="envValueLabel">Değer</label>
216
+ <input id="env-value" class="input" type="password" placeholder="********">
217
+ <button class="btn" id="env-save" data-i18n="envSave">Kaydet</button>
218
+ <span id="env-result" class="muted"></span>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </section>
188
224
  </main>
189
225
 
190
226
  <footer class="footer">
@@ -3,6 +3,7 @@ class Dashboard {
3
3
  this.apiBase = '/api';
4
4
  this.lang = this.detectLanguage();
5
5
  this.providers = [];
6
+ this.envKeys = [];
6
7
  this.translations = this.buildTranslations();
7
8
  this.bindEvents();
8
9
  this.setLanguage(this.lang);
@@ -49,6 +50,20 @@ class Dashboard {
49
50
  defaultRoute: 'Varsayılan rota',
50
51
  logging: 'Loglama',
51
52
  configJson: 'Konfigürasyon',
53
+ env: 'Ortam Değişkenleri',
54
+ envHint: 'Anahtarları hızlıca güncelle',
55
+ envStatus: 'Durum',
56
+ envUpdate: 'Güncelle',
57
+ envKeyLabel: 'Anahtar',
58
+ envKeyPlaceholder: 'CUSTOM_KEY',
59
+ envValueLabel: 'Değer',
60
+ envSave: 'Kaydet',
61
+ envSaved: 'Kaydedildi',
62
+ envSaveError: 'Kaydedilemedi',
63
+ envSet: 'Tanımlı',
64
+ envMissing: 'Eksik',
65
+ envPath: 'Dosya',
66
+ envSelect: 'Anahtar seç'
52
67
  logOn: 'Açık',
53
68
  logOff: 'Kapalı',
54
69
  statusHealthy: 'Sağlıklı',
@@ -94,6 +109,20 @@ class Dashboard {
94
109
  defaultRoute: 'Standaard route',
95
110
  logging: 'Logging',
96
111
  configJson: 'Configuratie',
112
+ env: 'Omgevingsvariabelen',
113
+ envHint: 'Snel sleutels bijwerken',
114
+ envStatus: 'Status',
115
+ envUpdate: 'Bijwerken',
116
+ envKeyLabel: 'Sleutel',
117
+ envKeyPlaceholder: 'CUSTOM_KEY',
118
+ envValueLabel: 'Waarde',
119
+ envSave: 'Opslaan',
120
+ envSaved: 'Opgeslagen',
121
+ envSaveError: 'Opslaan mislukt',
122
+ envSet: 'Ingesteld',
123
+ envMissing: 'Ontbreekt',
124
+ envPath: 'Bestand',
125
+ envSelect: 'Sleutel kiezen'
97
126
  logOn: 'Aan',
98
127
  logOff: 'Uit',
99
128
  statusHealthy: 'Gezond',
@@ -128,6 +157,11 @@ class Dashboard {
128
157
  el.textContent = this.t(key);
129
158
  });
130
159
 
160
+ document.querySelectorAll('[data-i18n-placeholder]').forEach((el) => {
161
+ const key = el.getAttribute('data-i18n-placeholder');
162
+ el.setAttribute('placeholder', this.t(key));
163
+ });
164
+
131
165
  document.querySelectorAll('.lang-btn').forEach((btn) => {
132
166
  btn.classList.toggle('active', btn.dataset.lang === lang);
133
167
  });
@@ -145,6 +179,9 @@ class Dashboard {
145
179
  const refreshHealth = document.getElementById('refresh-health');
146
180
  refreshHealth?.addEventListener('click', () => this.loadHealth());
147
181
 
182
+ const envSave = document.getElementById('env-save');
183
+ envSave?.addEventListener('click', () => this.saveEnv());
184
+
148
185
  document.querySelectorAll('.lang-btn').forEach((btn) => {
149
186
  btn.addEventListener('click', () => this.setLanguage(btn.dataset.lang));
150
187
  });
@@ -161,6 +198,18 @@ class Dashboard {
161
198
  return response.json();
162
199
  }
163
200
 
201
+ async postJson(path, payload) {
202
+ const response = await fetch(path, {
203
+ method: 'POST',
204
+ headers: { 'content-type': 'application/json' },
205
+ body: JSON.stringify(payload)
206
+ });
207
+ if (!response.ok) {
208
+ throw new Error(`HTTP ${response.status}`);
209
+ }
210
+ return response.json();
211
+ }
212
+
164
213
  async refreshAll() {
165
214
  try {
166
215
  this.setConnectionStatus(true);
@@ -169,7 +218,8 @@ class Dashboard {
169
218
  this.loadAnalytics(),
170
219
  this.loadHealth(),
171
220
  this.loadConfig(),
172
- this.loadStatus()
221
+ this.loadStatus(),
222
+ this.loadEnv()
173
223
  ]);
174
224
  this.updateLastUpdated();
175
225
  } catch (error) {
@@ -249,6 +299,19 @@ class Dashboard {
249
299
  }
250
300
  }
251
301
 
302
+ async loadEnv() {
303
+ const envResponse = await this.fetchJson(`${this.apiBase}/env`);
304
+ const payload = envResponse.data || {};
305
+ this.envKeys = payload.keys || [];
306
+ this.renderEnvList(this.envKeys);
307
+ this.populateEnvSelect(this.envKeys);
308
+
309
+ const envPath = document.getElementById('env-path');
310
+ if (envPath && payload.envPath) {
311
+ envPath.textContent = `${this.t('envPath')}: ${payload.envPath}`;
312
+ }
313
+ }
314
+
252
315
  renderProviderList(targetId, providers) {
253
316
  const container = document.getElementById(targetId);
254
317
  if (!container) return;
@@ -324,6 +387,61 @@ class Dashboard {
324
387
  });
325
388
  }
326
389
 
390
+ renderEnvList(keys) {
391
+ const container = document.getElementById('env-list');
392
+ if (!container) return;
393
+
394
+ container.innerHTML = '';
395
+ if (!keys.length) {
396
+ container.innerHTML = `<div class="muted">${this.t('dataUnavailable')}</div>`;
397
+ return;
398
+ }
399
+
400
+ keys.forEach((item) => {
401
+ const entry = document.createElement('div');
402
+ entry.className = 'list-item';
403
+
404
+ const left = document.createElement('div');
405
+ left.className = 'list-left';
406
+
407
+ const dot = document.createElement('span');
408
+ dot.className = `dot ${item.present ? 'ok' : 'warn'}`;
409
+
410
+ const name = document.createElement('span');
411
+ name.textContent = item.name;
412
+
413
+ left.appendChild(dot);
414
+ left.appendChild(name);
415
+
416
+ const badge = document.createElement('span');
417
+ badge.textContent = item.present ? this.t('envSet') : this.t('envMissing');
418
+
419
+ entry.appendChild(left);
420
+ entry.appendChild(badge);
421
+ container.appendChild(entry);
422
+ });
423
+ }
424
+
425
+ populateEnvSelect(keys) {
426
+ const select = document.getElementById('env-key-select');
427
+ if (!select) return;
428
+ select.innerHTML = '';
429
+
430
+ const placeholder = document.createElement('option');
431
+ placeholder.value = '';
432
+ placeholder.textContent = this.t('envSelect');
433
+ placeholder.disabled = true;
434
+ placeholder.selected = true;
435
+ select.appendChild(placeholder);
436
+
437
+ keys.forEach((item) => {
438
+ const option = document.createElement('option');
439
+ option.value = item.name;
440
+ option.textContent = item.name;
441
+ select.appendChild(option);
442
+ });
443
+ }
444
+
327
445
  resolveStatus(status) {
328
446
  if (['healthy', 'ok'].includes(status)) return 'ok';
329
447
  if (['degraded', 'warn', 'warning'].includes(status)) return 'warn';
@@ -365,6 +483,34 @@ class Dashboard {
365
483
  window.location.href = `${this.apiBase}/analytics/export?format=json&period=${period}`;
366
484
  }
367
485
 
486
+ async saveEnv() {
487
+ const select = document.getElementById('env-key-select');
488
+ const custom = document.getElementById('env-key-custom');
489
+ const valueInput = document.getElementById('env-value');
490
+ const result = document.getElementById('env-result');
491
+
492
+ const keyCandidate = (custom?.value || select?.value || '').trim();
493
+ const value = valueInput?.value?.trim() || '';
494
+
495
+ if (!keyCandidate || !value) {
496
+ if (result) result.textContent = this.t('envSaveError');
497
+ return;
498
+ }
499
+
500
+ if (result) result.textContent = '';
501
+
502
+ try {
503
+ await this.postJson(`${this.apiBase}/env`, { key: keyCandidate, value });
504
+ if (result) result.textContent = this.t('envSaved');
505
+ if (valueInput) valueInput.value = '';
506
+ if (custom) custom.value = '';
507
+ await this.loadEnv();
508
+ } catch (error) {
509
+ if (result) result.textContent = this.t('envSaveError');
510
+ console.error('Failed to save env', error);
511
+ }
512
+ }
513
+
368
514
  locale() {
369
515
  return this.lang === 'nl' ? 'nl-NL' : 'tr-TR';
370
516
  }