@halilertekin/claude-code-router-config 2.0.3 → 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 +4 -0
- package/README.md +3 -2
- package/package.json +1 -1
- package/router/config.js +75 -1
- package/router/server.js +60 -1
- package/web-dashboard/public/css/dashboard.css +29 -0
- package/web-dashboard/public/index.html +36 -0
- package/web-dashboard/public/js/dashboard.js +147 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
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
|
+
|
|
3
7
|
## 2.0.3
|
|
4
8
|
- API anahtarları için `~/.env` otomatik yükleme eklendi (CLI + health monitor).
|
|
5
9
|
- Sağlayıcı API key çözümleme tek noktaya alındı.
|
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Claude Code Router Config - Advanced Multi-Provider Setup
|
|
2
2
|
|
|
3
|
-
🚀 **v2.0.
|
|
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.
|
|
7
|
+
## ✨ New in v2.0.4
|
|
8
|
+
- UI üzerinden `.env` anahtarları ekleme/güncelleme (TR/NL).
|
|
8
9
|
- `~/.env` otomatik yükleme ile API anahtarlarının bulunması (CLI + health monitor).
|
|
9
10
|
- **z.ai Support**: Native integration for GLM-4.7 via z.ai (PPInfra).
|
|
10
11
|
- **Lightweight Mode**: New `ccc` function for zero-dependency routing.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@halilertekin/claude-code-router-config",
|
|
3
|
-
"version": "2.0.
|
|
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
|
@@ -31,11 +31,15 @@ function parseEnvLine(line) {
|
|
|
31
31
|
return { key, value };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function getEnvPath() {
|
|
35
|
+
return process.env.CCR_ENV_PATH || DEFAULT_ENV_PATH;
|
|
36
|
+
}
|
|
37
|
+
|
|
34
38
|
function loadDotenv() {
|
|
35
39
|
if (envLoaded) return;
|
|
36
40
|
envLoaded = true;
|
|
37
41
|
|
|
38
|
-
const envPath =
|
|
42
|
+
const envPath = getEnvPath();
|
|
39
43
|
if (!fs.existsSync(envPath)) return;
|
|
40
44
|
|
|
41
45
|
const content = fs.readFileSync(envPath, 'utf8');
|
|
@@ -62,6 +66,73 @@ function resolveEnv(value) {
|
|
|
62
66
|
});
|
|
63
67
|
}
|
|
64
68
|
|
|
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
|
+
|
|
65
136
|
function resolveConfigValue(value, key) {
|
|
66
137
|
if (Array.isArray(value)) {
|
|
67
138
|
return value.map((item) => resolveConfigValue(item, key));
|
|
@@ -125,6 +196,9 @@ function resolveProviderKey(provider) {
|
|
|
125
196
|
|
|
126
197
|
module.exports = {
|
|
127
198
|
loadConfig,
|
|
199
|
+
getEnvPath,
|
|
200
|
+
readEnvFile,
|
|
201
|
+
writeEnvValue,
|
|
128
202
|
getConfigPath,
|
|
129
203
|
getConfigDir,
|
|
130
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 {
|
|
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
|
}
|