@halilertekin/claude-code-router-config 2.0.0 → 2.0.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/CHANGELOG.md +14 -0
- package/README.md +3 -2
- package/package.json +3 -1
- package/router/config.js +82 -0
- package/router/format.js +209 -0
- package/router/http.js +55 -0
- package/router/providers.js +53 -0
- package/router/route.js +93 -0
- package/router/server.js +520 -0
- package/router/stream.js +158 -0
- package/web-dashboard/public/css/dashboard.css +216 -435
- package/web-dashboard/public/index.html +178 -289
- package/web-dashboard/public/js/dashboard.js +311 -414
|
@@ -1,512 +1,409 @@
|
|
|
1
|
-
// Dashboard JavaScript
|
|
2
1
|
class Dashboard {
|
|
3
2
|
constructor() {
|
|
4
3
|
this.apiBase = '/api';
|
|
5
|
-
this.
|
|
6
|
-
this.
|
|
7
|
-
this.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
this.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
4
|
+
this.lang = this.detectLanguage();
|
|
5
|
+
this.providers = [];
|
|
6
|
+
this.translations = this.buildTranslations();
|
|
7
|
+
this.bindEvents();
|
|
8
|
+
this.setLanguage(this.lang);
|
|
9
|
+
this.refreshAll();
|
|
10
|
+
this.interval = setInterval(() => this.refreshAll(), 30000);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
buildTranslations() {
|
|
14
|
+
return {
|
|
15
|
+
tr: {
|
|
16
|
+
appTitle: 'Claude Code Router',
|
|
17
|
+
appSubtitle: 'Birleşik yönlendirici panosu',
|
|
18
|
+
refresh: 'Yenile',
|
|
19
|
+
connected: 'Bağlı',
|
|
20
|
+
disconnected: 'Bağlantı yok',
|
|
21
|
+
overview: 'Genel Bakış',
|
|
22
|
+
lastUpdated: 'Son güncelleme',
|
|
23
|
+
requests: 'İstekler',
|
|
24
|
+
tokens: 'Token',
|
|
25
|
+
cost: 'Maliyet',
|
|
26
|
+
avgLatency: 'Ort. Gecikme',
|
|
27
|
+
providers: 'Sağlayıcılar',
|
|
28
|
+
quickActions: 'Hızlı İşlemler',
|
|
29
|
+
export: 'Dışa aktar',
|
|
30
|
+
refreshHealth: 'Sağlığı yenile',
|
|
31
|
+
analytics: 'Analitik',
|
|
32
|
+
periodLabel: 'Dönem',
|
|
33
|
+
periodToday: 'Bugün',
|
|
34
|
+
periodWeek: 'Son 7 gün',
|
|
35
|
+
periodMonth: 'Son 30 gün',
|
|
36
|
+
totalRequests: 'Toplam İstek',
|
|
37
|
+
totalTokens: 'Toplam Token',
|
|
38
|
+
totalCost: 'Toplam Maliyet',
|
|
39
|
+
topProviders: 'En Çok Kullanılanlar',
|
|
40
|
+
health: 'Sağlık',
|
|
41
|
+
system: 'Sistem',
|
|
42
|
+
uptime: 'Çalışma Süresi',
|
|
43
|
+
memory: 'Bellek',
|
|
44
|
+
cpu: 'CPU',
|
|
45
|
+
node: 'Node Sürümü',
|
|
46
|
+
config: 'Yapılandırma',
|
|
47
|
+
configSummary: 'Özet',
|
|
48
|
+
providerCount: 'Sağlayıcı sayısı',
|
|
49
|
+
defaultRoute: 'Varsayılan rota',
|
|
50
|
+
logging: 'Loglama',
|
|
51
|
+
configJson: 'Konfigürasyon',
|
|
52
|
+
logOn: 'Açık',
|
|
53
|
+
logOff: 'Kapalı',
|
|
54
|
+
statusHealthy: 'Sağlıklı',
|
|
55
|
+
statusDegraded: 'Degrade',
|
|
56
|
+
statusDown: 'Kapalı',
|
|
57
|
+
statusUnknown: 'Bilinmiyor',
|
|
58
|
+
dataUnavailable: 'Veri yok'
|
|
59
|
+
},
|
|
60
|
+
nl: {
|
|
61
|
+
appTitle: 'Claude Code Router',
|
|
62
|
+
appSubtitle: 'Gecombineerde routerconsole',
|
|
63
|
+
refresh: 'Vernieuwen',
|
|
64
|
+
connected: 'Verbonden',
|
|
65
|
+
disconnected: 'Niet verbonden',
|
|
66
|
+
overview: 'Overzicht',
|
|
67
|
+
lastUpdated: 'Laatst bijgewerkt',
|
|
68
|
+
requests: 'Verzoeken',
|
|
69
|
+
tokens: 'Tokens',
|
|
70
|
+
cost: 'Kosten',
|
|
71
|
+
avgLatency: 'Gem. latentie',
|
|
72
|
+
providers: 'Providers',
|
|
73
|
+
quickActions: 'Snelle acties',
|
|
74
|
+
export: 'Exporteren',
|
|
75
|
+
refreshHealth: 'Status vernieuwen',
|
|
76
|
+
analytics: 'Analyse',
|
|
77
|
+
periodLabel: 'Periode',
|
|
78
|
+
periodToday: 'Vandaag',
|
|
79
|
+
periodWeek: 'Laatste 7 dagen',
|
|
80
|
+
periodMonth: 'Laatste 30 dagen',
|
|
81
|
+
totalRequests: 'Totaal verzoeken',
|
|
82
|
+
totalTokens: 'Totaal tokens',
|
|
83
|
+
totalCost: 'Totale kosten',
|
|
84
|
+
topProviders: 'Meest gebruikt',
|
|
85
|
+
health: 'Status',
|
|
86
|
+
system: 'Systeem',
|
|
87
|
+
uptime: 'Uptime',
|
|
88
|
+
memory: 'Geheugen',
|
|
89
|
+
cpu: 'CPU',
|
|
90
|
+
node: 'Node-versie',
|
|
91
|
+
config: 'Configuratie',
|
|
92
|
+
configSummary: 'Overzicht',
|
|
93
|
+
providerCount: 'Aantal providers',
|
|
94
|
+
defaultRoute: 'Standaard route',
|
|
95
|
+
logging: 'Logging',
|
|
96
|
+
configJson: 'Configuratie',
|
|
97
|
+
logOn: 'Aan',
|
|
98
|
+
logOff: 'Uit',
|
|
99
|
+
statusHealthy: 'Gezond',
|
|
100
|
+
statusDegraded: 'Gedegradeerd',
|
|
101
|
+
statusDown: 'Niet beschikbaar',
|
|
102
|
+
statusUnknown: 'Onbekend',
|
|
103
|
+
dataUnavailable: 'Geen gegevens'
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
detectLanguage() {
|
|
109
|
+
const stored = localStorage.getItem('ccr_lang');
|
|
110
|
+
if (stored) return stored;
|
|
111
|
+
const lang = navigator.language || 'tr';
|
|
112
|
+
if (lang.startsWith('nl')) return 'nl';
|
|
113
|
+
if (lang.startsWith('tr')) return 'tr';
|
|
114
|
+
return 'tr';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
t(key) {
|
|
118
|
+
return this.translations[this.lang]?.[key] || key;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setLanguage(lang) {
|
|
122
|
+
this.lang = lang;
|
|
123
|
+
localStorage.setItem('ccr_lang', lang);
|
|
124
|
+
document.documentElement.lang = lang;
|
|
125
|
+
|
|
126
|
+
document.querySelectorAll('[data-i18n]').forEach((el) => {
|
|
127
|
+
const key = el.getAttribute('data-i18n');
|
|
128
|
+
el.textContent = this.t(key);
|
|
23
129
|
});
|
|
24
130
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
periodSelect.addEventListener('change', () => {
|
|
29
|
-
this.loadAnalyticsData();
|
|
30
|
-
});
|
|
31
|
-
}
|
|
131
|
+
document.querySelectorAll('.lang-btn').forEach((btn) => {
|
|
132
|
+
btn.classList.toggle('active', btn.dataset.lang === lang);
|
|
133
|
+
});
|
|
32
134
|
|
|
33
|
-
|
|
34
|
-
const templateSelect = document.getElementById('template-select');
|
|
35
|
-
if (templateSelect) {
|
|
36
|
-
templateSelect.addEventListener('change', () => {
|
|
37
|
-
this.updateTemplatePreview();
|
|
38
|
-
});
|
|
39
|
-
}
|
|
135
|
+
this.updateLastUpdated();
|
|
40
136
|
}
|
|
41
137
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
item.classList.remove('active');
|
|
46
|
-
});
|
|
47
|
-
document.querySelector(`[data-tab="${tabName}"]`).classList.add('active');
|
|
138
|
+
bindEvents() {
|
|
139
|
+
const refreshBtn = document.getElementById('refresh-btn');
|
|
140
|
+
refreshBtn?.addEventListener('click', () => this.refreshAll());
|
|
48
141
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
content.classList.remove('active');
|
|
52
|
-
});
|
|
53
|
-
document.getElementById(tabName).classList.add('active');
|
|
142
|
+
const exportBtn = document.getElementById('export-btn');
|
|
143
|
+
exportBtn?.addEventListener('click', () => this.exportAnalytics());
|
|
54
144
|
|
|
55
|
-
|
|
145
|
+
const refreshHealth = document.getElementById('refresh-health');
|
|
146
|
+
refreshHealth?.addEventListener('click', () => this.loadHealth());
|
|
56
147
|
|
|
57
|
-
|
|
58
|
-
|
|
148
|
+
document.querySelectorAll('.lang-btn').forEach((btn) => {
|
|
149
|
+
btn.addEventListener('click', () => this.setLanguage(btn.dataset.lang));
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const periodSelect = document.getElementById('analytics-period');
|
|
153
|
+
periodSelect?.addEventListener('change', () => this.loadAnalytics());
|
|
59
154
|
}
|
|
60
155
|
|
|
61
|
-
async
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
break;
|
|
66
|
-
case 'analytics':
|
|
67
|
-
await this.loadAnalyticsData();
|
|
68
|
-
break;
|
|
69
|
-
case 'health':
|
|
70
|
-
await this.loadHealthData();
|
|
71
|
-
break;
|
|
72
|
-
case 'config':
|
|
73
|
-
await this.loadConfigData();
|
|
74
|
-
break;
|
|
156
|
+
async fetchJson(path) {
|
|
157
|
+
const response = await fetch(path);
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
throw new Error(`HTTP ${response.status}`);
|
|
75
160
|
}
|
|
161
|
+
return response.json();
|
|
76
162
|
}
|
|
77
163
|
|
|
78
|
-
async
|
|
164
|
+
async refreshAll() {
|
|
79
165
|
try {
|
|
166
|
+
this.setConnectionStatus(true);
|
|
80
167
|
await Promise.all([
|
|
81
|
-
this.
|
|
82
|
-
this.
|
|
168
|
+
this.loadOverview(),
|
|
169
|
+
this.loadAnalytics(),
|
|
170
|
+
this.loadHealth(),
|
|
171
|
+
this.loadConfig(),
|
|
172
|
+
this.loadStatus()
|
|
83
173
|
]);
|
|
174
|
+
this.updateLastUpdated();
|
|
84
175
|
} catch (error) {
|
|
85
|
-
this.
|
|
176
|
+
this.setConnectionStatus(false);
|
|
177
|
+
console.error('Failed to refresh dashboard', error);
|
|
86
178
|
}
|
|
87
179
|
}
|
|
88
180
|
|
|
89
|
-
async
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
]);
|
|
181
|
+
async loadOverview() {
|
|
182
|
+
const [todayResponse, providersResponse] = await Promise.all([
|
|
183
|
+
this.fetchJson(`${this.apiBase}/analytics/today`),
|
|
184
|
+
this.fetchJson(`${this.apiBase}/health/providers`)
|
|
185
|
+
]);
|
|
95
186
|
|
|
96
|
-
|
|
97
|
-
|
|
187
|
+
const today = todayResponse.data || {};
|
|
188
|
+
this.providers = providersResponse.data || [];
|
|
98
189
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
console.error('Failed to load overview data:', error);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
190
|
+
document.getElementById('today-requests').textContent = this.formatNumber(today.requests || 0);
|
|
191
|
+
document.getElementById('today-tokens').textContent = this.formatNumber(today.tokens || 0);
|
|
192
|
+
document.getElementById('today-cost').textContent = this.formatCurrency(today.cost || 0);
|
|
193
|
+
document.getElementById('today-latency').textContent = `${today.avgLatency || 0}ms`;
|
|
106
194
|
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
const period = document.getElementById('analytics-period')?.value || 'week';
|
|
110
|
-
const response = await fetch(`${this.apiBase}/analytics/summary`);
|
|
111
|
-
const data = await response.json();
|
|
112
|
-
|
|
113
|
-
this.updateAnalyticsDisplay(data.data);
|
|
114
|
-
} catch (error) {
|
|
115
|
-
console.error('Failed to load analytics data:', error);
|
|
116
|
-
}
|
|
195
|
+
this.renderProviderList('provider-status-list', this.providers);
|
|
117
196
|
}
|
|
118
197
|
|
|
119
|
-
async
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
fetch(`${this.apiBase}/health/system`)
|
|
124
|
-
]);
|
|
198
|
+
async loadAnalytics() {
|
|
199
|
+
const period = document.getElementById('analytics-period')?.value || 'week';
|
|
200
|
+
const summaryResponse = await this.fetchJson(`${this.apiBase}/analytics/summary?period=${period}`);
|
|
201
|
+
const summary = summaryResponse.data || {};
|
|
125
202
|
|
|
126
|
-
|
|
127
|
-
|
|
203
|
+
document.getElementById('summary-requests').textContent = this.formatNumber(summary.totalRequests || 0);
|
|
204
|
+
document.getElementById('summary-tokens').textContent = this.formatNumber(summary.totalTokens || 0);
|
|
205
|
+
document.getElementById('summary-cost').textContent = this.formatCurrency(summary.totalCost || 0);
|
|
206
|
+
document.getElementById('summary-latency').textContent = `${summary.avgLatency || 0}ms`;
|
|
128
207
|
|
|
129
|
-
|
|
130
|
-
this.updateSystemHealth(system.data);
|
|
131
|
-
} catch (error) {
|
|
132
|
-
console.error('Failed to load health data:', error);
|
|
133
|
-
}
|
|
208
|
+
this.renderTopProviders(summary.providers || {});
|
|
134
209
|
}
|
|
135
210
|
|
|
136
|
-
async
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
fetch(`${this.apiBase}/config/current`),
|
|
140
|
-
fetch(`${this.apiBase}/config/templates`)
|
|
141
|
-
]);
|
|
211
|
+
async loadHealth() {
|
|
212
|
+
const systemResponse = await this.fetchJson(`${this.apiBase}/health/system`);
|
|
213
|
+
const system = systemResponse.data || {};
|
|
142
214
|
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
this.updateConfigDisplay(config.data);
|
|
147
|
-
this.updateTemplateOptions(templates.data);
|
|
148
|
-
} catch (error) {
|
|
149
|
-
console.error('Failed to load config data:', error);
|
|
215
|
+
if (!this.providers.length) {
|
|
216
|
+
const providersResponse = await this.fetchJson(`${this.apiBase}/health/providers`);
|
|
217
|
+
this.providers = providersResponse.data || [];
|
|
150
218
|
}
|
|
151
|
-
}
|
|
152
219
|
|
|
153
|
-
|
|
154
|
-
if (!data) return;
|
|
220
|
+
this.renderProviderList('health-providers', this.providers);
|
|
155
221
|
|
|
156
|
-
document.getElementById('
|
|
157
|
-
document.getElementById('
|
|
158
|
-
document.getElementById('
|
|
159
|
-
document.getElementById('
|
|
222
|
+
document.getElementById('system-uptime').textContent = this.formatDuration(system.uptime || 0);
|
|
223
|
+
document.getElementById('system-memory').textContent = this.formatBytes(system.memory?.heapUsed || 0);
|
|
224
|
+
document.getElementById('system-cpu').textContent = system.cpu ? `${system.cpu.usage}%` : '-';
|
|
225
|
+
document.getElementById('system-node').textContent = system.nodeVersion || '-';
|
|
160
226
|
}
|
|
161
227
|
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
228
|
+
async loadConfig() {
|
|
229
|
+
const configResponse = await this.fetchJson(`${this.apiBase}/config`);
|
|
230
|
+
const config = configResponse.data || {};
|
|
165
231
|
|
|
166
|
-
|
|
232
|
+
document.getElementById('config-providers').textContent = (config.Providers || []).length;
|
|
233
|
+
document.getElementById('config-default').textContent = config.Router?.default || '-';
|
|
234
|
+
document.getElementById('config-logging').textContent = config.LOG ? this.t('logOn') : this.t('logOff');
|
|
167
235
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
<div style="font-weight: 600; margin-bottom: 0.5rem;">${provider.name}</div>
|
|
173
|
-
<div style="font-size: 0.875rem; color: var(--text-secondary);">${provider.status}</div>
|
|
174
|
-
`;
|
|
175
|
-
container.appendChild(statusDiv);
|
|
176
|
-
});
|
|
236
|
+
const configDisplay = document.getElementById('config-display');
|
|
237
|
+
if (configDisplay) {
|
|
238
|
+
configDisplay.textContent = JSON.stringify(config, null, 2);
|
|
239
|
+
}
|
|
177
240
|
}
|
|
178
241
|
|
|
179
|
-
|
|
180
|
-
|
|
242
|
+
async loadStatus() {
|
|
243
|
+
const statusResponse = await this.fetchJson(`${this.apiBase}/status`);
|
|
244
|
+
const status = statusResponse.data || {};
|
|
245
|
+
const version = status.version || 'v2';
|
|
246
|
+
const versionEl = document.getElementById('version');
|
|
247
|
+
if (versionEl) {
|
|
248
|
+
versionEl.textContent = `v${version}`.replace(/^vv/, 'v');
|
|
249
|
+
}
|
|
250
|
+
}
|
|
181
251
|
|
|
182
|
-
|
|
252
|
+
renderProviderList(targetId, providers) {
|
|
253
|
+
const container = document.getElementById(targetId);
|
|
183
254
|
if (!container) return;
|
|
184
255
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
<div class="metric">
|
|
190
|
-
<span class="label">Total Requests</span>
|
|
191
|
-
<span class="value">${this.formatNumber(data.totalRequests || 0)}</span>
|
|
192
|
-
</div>
|
|
193
|
-
<div class="metric">
|
|
194
|
-
<span class="label">Total Tokens</span>
|
|
195
|
-
<span class="value">${this.formatNumber(data.totalTokens || 0)}</span>
|
|
196
|
-
</div>
|
|
197
|
-
`;
|
|
198
|
-
|
|
199
|
-
if (showCosts) {
|
|
200
|
-
html += `
|
|
201
|
-
<div class="metric">
|
|
202
|
-
<span class="label">Total Cost</span>
|
|
203
|
-
<span class="value">$${(data.totalCost || 0).toFixed(4)}</span>
|
|
204
|
-
</div>
|
|
205
|
-
`;
|
|
256
|
+
container.innerHTML = '';
|
|
257
|
+
if (!providers.length) {
|
|
258
|
+
container.innerHTML = `<div class="muted">${this.t('dataUnavailable')}</div>`;
|
|
259
|
+
return;
|
|
206
260
|
}
|
|
207
261
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
</div>
|
|
213
|
-
`;
|
|
214
|
-
|
|
215
|
-
if (showDetailed && data.providers) {
|
|
216
|
-
html += '<h4 style="margin-top: 1rem; margin-bottom: 0.5rem;">Provider Breakdown</h4>';
|
|
217
|
-
Object.entries(data.providers).forEach(([provider, count]) => {
|
|
218
|
-
html += `
|
|
219
|
-
<div class="metric">
|
|
220
|
-
<span class="label">${provider}</span>
|
|
221
|
-
<span class="value">${this.formatNumber(count)}</span>
|
|
222
|
-
</div>
|
|
223
|
-
`;
|
|
224
|
-
});
|
|
225
|
-
}
|
|
262
|
+
providers.forEach((provider) => {
|
|
263
|
+
const statusKey = this.resolveStatus(provider.status || 'unknown');
|
|
264
|
+
const item = document.createElement('div');
|
|
265
|
+
item.className = 'list-item';
|
|
226
266
|
|
|
227
|
-
|
|
228
|
-
|
|
267
|
+
const left = document.createElement('div');
|
|
268
|
+
left.className = 'list-left';
|
|
229
269
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (!container) return;
|
|
270
|
+
const dot = document.createElement('span');
|
|
271
|
+
dot.className = `dot ${statusKey}`;
|
|
233
272
|
|
|
234
|
-
|
|
273
|
+
const name = document.createElement('span');
|
|
274
|
+
name.textContent = provider.name;
|
|
235
275
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const statusDiv = document.createElement('div');
|
|
239
|
-
statusDiv.className = `metric`;
|
|
240
|
-
statusDiv.innerHTML = `
|
|
241
|
-
<span class="label">${provider.name}</span>
|
|
242
|
-
<span class="status-badge ${statusClass}">${provider.status}</span>
|
|
243
|
-
`;
|
|
244
|
-
container.appendChild(statusDiv);
|
|
245
|
-
});
|
|
246
|
-
}
|
|
276
|
+
left.appendChild(dot);
|
|
277
|
+
left.appendChild(name);
|
|
247
278
|
|
|
248
|
-
|
|
249
|
-
|
|
279
|
+
const badge = document.createElement('span');
|
|
280
|
+
badge.textContent = this.statusLabel(statusKey);
|
|
250
281
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
282
|
+
item.appendChild(left);
|
|
283
|
+
item.appendChild(badge);
|
|
284
|
+
container.appendChild(item);
|
|
285
|
+
});
|
|
254
286
|
}
|
|
255
287
|
|
|
256
|
-
|
|
257
|
-
const container = document.getElementById('
|
|
288
|
+
renderTopProviders(providerStats) {
|
|
289
|
+
const container = document.getElementById('top-providers');
|
|
258
290
|
if (!container) return;
|
|
259
291
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
</pre>
|
|
264
|
-
`;
|
|
265
|
-
|
|
266
|
-
// Update provider config
|
|
267
|
-
const providerContainer = document.getElementById('provider-config');
|
|
268
|
-
if (providerContainer && config.Providers) {
|
|
269
|
-
providerContainer.innerHTML = '';
|
|
270
|
-
config.Providers.forEach(provider => {
|
|
271
|
-
providerContainer.innerHTML += `
|
|
272
|
-
<div class="metric">
|
|
273
|
-
<span class="label">${provider.name}</span>
|
|
274
|
-
<span class="value">${provider.models.length} models</span>
|
|
275
|
-
</div>
|
|
276
|
-
`;
|
|
277
|
-
});
|
|
278
|
-
}
|
|
292
|
+
const entries = Object.entries(providerStats)
|
|
293
|
+
.sort((a, b) => b[1] - a[1])
|
|
294
|
+
.slice(0, 5);
|
|
279
295
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
Object.entries(config.Router).forEach(([key, value]) => {
|
|
285
|
-
routerContainer.innerHTML += `
|
|
286
|
-
<div class="metric">
|
|
287
|
-
<span class="label">${key}</span>
|
|
288
|
-
<span class="value">${JSON.stringify(value)}</span>
|
|
289
|
-
</div>
|
|
290
|
-
`;
|
|
291
|
-
});
|
|
296
|
+
container.innerHTML = '';
|
|
297
|
+
if (!entries.length) {
|
|
298
|
+
container.innerHTML = `<div class="muted">${this.t('dataUnavailable')}</div>`;
|
|
299
|
+
return;
|
|
292
300
|
}
|
|
293
|
-
}
|
|
294
301
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
302
|
+
entries.forEach(([provider, count]) => {
|
|
303
|
+
const item = document.createElement('div');
|
|
304
|
+
item.className = 'list-item';
|
|
298
305
|
|
|
299
|
-
|
|
300
|
-
|
|
306
|
+
const left = document.createElement('div');
|
|
307
|
+
left.className = 'list-left';
|
|
301
308
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
select.removeChild(select.lastChild);
|
|
305
|
-
}
|
|
309
|
+
const dot = document.createElement('span');
|
|
310
|
+
dot.className = 'dot ok';
|
|
306
311
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
option.value = template.name;
|
|
310
|
-
option.textContent = template.description || template.name;
|
|
311
|
-
select.appendChild(option);
|
|
312
|
-
});
|
|
312
|
+
const name = document.createElement('span');
|
|
313
|
+
name.textContent = provider;
|
|
313
314
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
select.value = currentValue;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
315
|
+
left.appendChild(dot);
|
|
316
|
+
left.appendChild(name);
|
|
319
317
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
try {
|
|
323
|
-
await this.loadTabData(this.currentTab);
|
|
324
|
-
this.showSuccess('Data refreshed');
|
|
325
|
-
} catch (error) {
|
|
326
|
-
this.showError('Failed to refresh data');
|
|
327
|
-
}
|
|
328
|
-
}
|
|
318
|
+
const value = document.createElement('span');
|
|
319
|
+
value.textContent = this.formatNumber(count);
|
|
329
320
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
stopAutoRefresh() {
|
|
337
|
-
if (this.refreshInterval) {
|
|
338
|
-
clearInterval(this.refreshInterval);
|
|
339
|
-
this.refreshInterval = null;
|
|
340
|
-
}
|
|
321
|
+
item.appendChild(left);
|
|
322
|
+
item.appendChild(value);
|
|
323
|
+
container.appendChild(item);
|
|
324
|
+
});
|
|
341
325
|
}
|
|
342
326
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return (num / 1000).toFixed(1) + 'K';
|
|
349
|
-
}
|
|
350
|
-
return num.toString();
|
|
327
|
+
resolveStatus(status) {
|
|
328
|
+
if (['healthy', 'ok'].includes(status)) return 'ok';
|
|
329
|
+
if (['degraded', 'warn', 'warning'].includes(status)) return 'warn';
|
|
330
|
+
if (['down', 'unhealthy', 'error'].includes(status)) return 'down';
|
|
331
|
+
return 'unknown';
|
|
351
332
|
}
|
|
352
333
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
334
|
+
statusLabel(statusKey) {
|
|
335
|
+
switch (statusKey) {
|
|
336
|
+
case 'ok':
|
|
337
|
+
return this.t('statusHealthy');
|
|
338
|
+
case 'warn':
|
|
339
|
+
return this.t('statusDegraded');
|
|
340
|
+
case 'down':
|
|
341
|
+
return this.t('statusDown');
|
|
342
|
+
default:
|
|
343
|
+
return this.t('statusUnknown');
|
|
360
344
|
}
|
|
361
|
-
return bytes + ' B';
|
|
362
345
|
}
|
|
363
346
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return Math.floor(seconds) + 's';
|
|
347
|
+
setConnectionStatus(connected) {
|
|
348
|
+
const badge = document.getElementById('connection-status');
|
|
349
|
+
if (!badge) return;
|
|
350
|
+
badge.textContent = connected ? this.t('connected') : this.t('disconnected');
|
|
351
|
+
badge.classList.toggle('status-ok', connected);
|
|
352
|
+
badge.classList.toggle('status-down', !connected);
|
|
371
353
|
}
|
|
372
354
|
|
|
373
355
|
updateLastUpdated() {
|
|
374
356
|
const element = document.getElementById('last-updated');
|
|
375
357
|
if (element) {
|
|
376
|
-
|
|
358
|
+
const now = new Date();
|
|
359
|
+
element.textContent = `${this.t('lastUpdated')}: ${now.toLocaleTimeString(this.locale())}`;
|
|
377
360
|
}
|
|
378
361
|
}
|
|
379
362
|
|
|
380
|
-
|
|
381
|
-
document.getElementById('
|
|
382
|
-
|
|
363
|
+
exportAnalytics() {
|
|
364
|
+
const period = document.getElementById('analytics-period')?.value || 'week';
|
|
365
|
+
window.location.href = `${this.apiBase}/analytics/export?format=json&period=${period}`;
|
|
383
366
|
}
|
|
384
367
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
document.getElementById('connection-status').className = 'status-badge status-healthy';
|
|
368
|
+
locale() {
|
|
369
|
+
return this.lang === 'nl' ? 'nl-NL' : 'tr-TR';
|
|
388
370
|
}
|
|
389
371
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
document.getElementById('connection-status').className = 'status-badge status-unhealthy';
|
|
372
|
+
formatNumber(value) {
|
|
373
|
+
return new Intl.NumberFormat(this.locale()).format(value || 0);
|
|
393
374
|
}
|
|
394
|
-
}
|
|
395
375
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
376
|
+
formatCurrency(value) {
|
|
377
|
+
return new Intl.NumberFormat(this.locale(), {
|
|
378
|
+
style: 'currency',
|
|
379
|
+
currency: 'USD',
|
|
380
|
+
maximumFractionDigits: 2
|
|
381
|
+
}).format(value || 0);
|
|
402
382
|
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
async function testAllProviders() {
|
|
406
|
-
try {
|
|
407
|
-
const response = await fetch('/api/health/providers');
|
|
408
|
-
const data = await response.json();
|
|
409
|
-
alert(`Provider tests completed:\n${data.data.map(p => `${p.name}: ${p.status}`).join('\n')}`);
|
|
410
|
-
} catch (error) {
|
|
411
|
-
alert('Failed to test providers');
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
383
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
async function exportAnalytics() {
|
|
424
|
-
try {
|
|
425
|
-
const format = prompt('Export format (json/csv):', 'json');
|
|
426
|
-
if (format) {
|
|
427
|
-
window.location.href = `/api/analytics/export?format=${format}`;
|
|
384
|
+
formatBytes(bytes) {
|
|
385
|
+
if (!bytes) return '0 B';
|
|
386
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
387
|
+
let size = bytes;
|
|
388
|
+
let unitIndex = 0;
|
|
389
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
390
|
+
size /= 1024;
|
|
391
|
+
unitIndex += 1;
|
|
428
392
|
}
|
|
429
|
-
|
|
430
|
-
alert('Failed to export analytics');
|
|
393
|
+
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
|
431
394
|
}
|
|
432
|
-
}
|
|
433
395
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
async function runHealthChecks() {
|
|
445
|
-
alert('Running comprehensive health checks...');
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function applyTemplate() {
|
|
449
|
-
const select = document.getElementById('template-select');
|
|
450
|
-
if (select && select.value) {
|
|
451
|
-
alert(`Applying template: ${select.value}`);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
function backupConfig() {
|
|
456
|
-
alert('Configuration backup would be created here');
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function validateConfig() {
|
|
460
|
-
alert('Configuration validation would run here');
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function editConfig() {
|
|
464
|
-
alert('Configuration editor would open here');
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function reloadConfig() {
|
|
468
|
-
alert('Configuration would be reloaded here');
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function updateTemplatePreview() {
|
|
472
|
-
const select = document.getElementById('template-select');
|
|
473
|
-
if (select && select.value) {
|
|
474
|
-
console.log(`Template preview: ${select.value}`);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function closeModal() {
|
|
479
|
-
document.getElementById('modal').classList.add('hidden');
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function showModal(title, content, actionText = null, actionCallback = null) {
|
|
483
|
-
const modal = document.getElementById('modal');
|
|
484
|
-
const modalTitle = document.getElementById('modal-title');
|
|
485
|
-
const modalBody = document.getElementById('modal-body');
|
|
486
|
-
const modalAction = document.getElementById('modal-action');
|
|
487
|
-
|
|
488
|
-
modalTitle.textContent = title;
|
|
489
|
-
modalBody.innerHTML = content;
|
|
490
|
-
|
|
491
|
-
if (actionText && actionCallback) {
|
|
492
|
-
modalAction.textContent = actionText;
|
|
493
|
-
modalAction.style.display = 'block';
|
|
494
|
-
modalAction.onclick = actionCallback;
|
|
495
|
-
} else {
|
|
496
|
-
modalAction.style.display = 'none';
|
|
396
|
+
formatDuration(seconds) {
|
|
397
|
+
if (!seconds) return '0s';
|
|
398
|
+
const hours = Math.floor(seconds / 3600);
|
|
399
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
400
|
+
const secs = Math.floor(seconds % 60);
|
|
401
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
402
|
+
if (minutes > 0) return `${minutes}m ${secs}s`;
|
|
403
|
+
return `${secs}s`;
|
|
497
404
|
}
|
|
498
|
-
|
|
499
|
-
modal.classList.remove('hidden');
|
|
500
405
|
}
|
|
501
406
|
|
|
502
|
-
// Initialize dashboard when DOM is loaded
|
|
503
407
|
document.addEventListener('DOMContentLoaded', () => {
|
|
504
408
|
window.dashboard = new Dashboard();
|
|
505
409
|
});
|
|
506
|
-
|
|
507
|
-
// Cleanup on page unload
|
|
508
|
-
window.addEventListener('beforeunload', () => {
|
|
509
|
-
if (window.dashboard) {
|
|
510
|
-
window.dashboard.stopAutoRefresh();
|
|
511
|
-
}
|
|
512
|
-
});
|