- POST /api/admin/api-keys/{keyId}/check Endpoint
- Sendet minimale Anfrage an Anthropic API, liest Rate-Limit-Headers
- Plan-Erkennung: Max (Overage > 0), Pro (kein Fallback-Header), Pro/Max (sonst), API Console
- Budget-Auslastung: 5h + 7d + Overage mit Status-Warnings
- Frontend: Heartbeat-Button zeigt Ergebnis inline unter dem Key-Eintrag
- apiCheckApiKey() API-Funktion
254 lines
8.3 KiB
JavaScript
254 lines
8.3 KiB
JavaScript
// ── API Keys Page (Admin) ──
|
|
|
|
registerPage('api-keys', {
|
|
async init() {
|
|
if (!currentUser || !currentUser.isAdmin) {
|
|
navigateTo('dashboard');
|
|
return;
|
|
}
|
|
|
|
var keys = [];
|
|
var editingKeyId = null;
|
|
|
|
var list = document.getElementById('apiKeysList');
|
|
var newBtn = document.getElementById('apiKeysNewBtn');
|
|
var form = document.getElementById('apiKeysForm');
|
|
var formTitle = document.getElementById('apiKeysFormTitle');
|
|
var providerSelect = document.getElementById('apiKeysProvider');
|
|
var nameInput = document.getElementById('apiKeysName');
|
|
var keyInput = document.getElementById('apiKeysKey');
|
|
var saveBtn = document.getElementById('apiKeysFormSaveBtn');
|
|
|
|
function renderKeys() {
|
|
list.innerHTML = '';
|
|
|
|
if (keys.length === 0) {
|
|
var empty = document.createElement('div');
|
|
empty.className = 'admin-user-meta';
|
|
empty.style.padding = '1rem 0';
|
|
empty.textContent = 'Keine API-Keys konfiguriert.';
|
|
list.appendChild(empty);
|
|
return;
|
|
}
|
|
|
|
keys.forEach(function(key) {
|
|
var wrapper = document.createElement('div');
|
|
|
|
var row = document.createElement('div');
|
|
row.className = 'admin-user-row';
|
|
|
|
var info = document.createElement('div');
|
|
info.className = 'admin-user-info';
|
|
|
|
var nameSpan = document.createElement('span');
|
|
nameSpan.className = 'admin-user-name';
|
|
nameSpan.textContent = key.name;
|
|
|
|
var metaSpan = document.createElement('span');
|
|
metaSpan.className = 'admin-user-meta';
|
|
var parts = [key.provider];
|
|
if (key.isActive) parts.push('✅ Aktiv');
|
|
metaSpan.textContent = parts.join(' · ');
|
|
|
|
info.appendChild(nameSpan);
|
|
info.appendChild(metaSpan);
|
|
|
|
var actions = document.createElement('div');
|
|
actions.className = 'admin-user-actions';
|
|
|
|
// Status-Zeile (für Check-Ergebnis)
|
|
var statusLine = document.createElement('div');
|
|
statusLine.className = 'admin-user-meta';
|
|
statusLine.style.cssText = 'padding:0 var(--box-padding) .5rem;display:none;font-size:.85em';
|
|
|
|
// Check (nur Anthropic)
|
|
if (key.provider === 'anthropic') {
|
|
var checkBtn = document.createElement('button');
|
|
checkBtn.className = 'icon-btn';
|
|
checkBtn.title = 'Key prüfen';
|
|
checkBtn.innerHTML = '<i class="fa fa-heartbeat"></i>';
|
|
checkBtn.addEventListener('click', function() { checkKey(key, checkBtn, statusLine); });
|
|
actions.appendChild(checkBtn);
|
|
}
|
|
|
|
// Bearbeiten
|
|
var editBtn = document.createElement('button');
|
|
editBtn.className = 'icon-btn';
|
|
editBtn.title = 'Bearbeiten';
|
|
editBtn.innerHTML = '<i class="fa fa-pencil"></i>';
|
|
editBtn.addEventListener('click', function() { startEdit(key); });
|
|
actions.appendChild(editBtn);
|
|
|
|
// Aktivieren/Deaktivieren
|
|
if (key.isActive) {
|
|
var deactivateBtn = document.createElement('button');
|
|
deactivateBtn.className = 'icon-btn';
|
|
deactivateBtn.title = 'Deaktivieren';
|
|
deactivateBtn.innerHTML = '<i class="fa fa-toggle-on" style="color:var(--accent)"></i>';
|
|
deactivateBtn.addEventListener('click', function() { deactivateKey(key); });
|
|
actions.appendChild(deactivateBtn);
|
|
} else {
|
|
var activateBtn = document.createElement('button');
|
|
activateBtn.className = 'icon-btn';
|
|
activateBtn.title = 'Aktivieren';
|
|
activateBtn.innerHTML = '<i class="fa fa-toggle-off"></i>';
|
|
activateBtn.addEventListener('click', function() { activateKey(key); });
|
|
actions.appendChild(activateBtn);
|
|
}
|
|
|
|
// Löschen
|
|
var deleteBtn = document.createElement('button');
|
|
deleteBtn.className = 'icon-btn';
|
|
deleteBtn.title = 'Löschen';
|
|
deleteBtn.innerHTML = '<i class="fa fa-trash"></i>';
|
|
deleteBtn.addEventListener('click', function() { deleteKey(key); });
|
|
actions.appendChild(deleteBtn);
|
|
|
|
row.appendChild(info);
|
|
row.appendChild(actions);
|
|
wrapper.appendChild(row);
|
|
wrapper.appendChild(statusLine);
|
|
list.appendChild(wrapper);
|
|
});
|
|
}
|
|
|
|
async function loadKeys() {
|
|
try {
|
|
keys = await apiGetApiKeys();
|
|
renderKeys();
|
|
} catch (err) {
|
|
showToast(err.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function checkKey(key, btn, statusLine) {
|
|
var origHtml = btn.innerHTML;
|
|
btn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
|
|
btn.disabled = true;
|
|
statusLine.style.display = 'block';
|
|
statusLine.textContent = 'Prüfe…';
|
|
statusLine.style.opacity = '0.5';
|
|
try {
|
|
var result = await apiCheckApiKey(key.keyId);
|
|
statusLine.style.opacity = '1';
|
|
if (result.status === 'ok') {
|
|
var parts = ['✅ Gültig', result.keyType];
|
|
if (result.usage5h != null) parts.push('5h: ' + result.usage5h + '%');
|
|
if (result.usage7d != null) {
|
|
var s7d = '7d: ' + result.usage7d + '%';
|
|
if (result.status7d === 'allowed_warning') s7d += ' ⚠️';
|
|
else if (result.status7d === 'exceeded') s7d += ' 🛑';
|
|
parts.push(s7d);
|
|
}
|
|
if (result.overage != null && result.overage > 0) parts.push('Overage: ' + result.overage + '%');
|
|
statusLine.textContent = parts.join(' · ');
|
|
} else {
|
|
statusLine.textContent = '❌ ' + (result.message || 'Unbekannter Fehler');
|
|
}
|
|
} catch (err) {
|
|
statusLine.style.opacity = '1';
|
|
statusLine.textContent = '❌ ' + err.message;
|
|
} finally {
|
|
btn.innerHTML = origHtml;
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
function startEdit(key) {
|
|
editingKeyId = key.keyId;
|
|
formTitle.textContent = 'API-Key bearbeiten';
|
|
providerSelect.value = key.provider;
|
|
providerSelect.disabled = true;
|
|
nameInput.value = key.name;
|
|
keyInput.value = key.apiKey;
|
|
saveBtn.textContent = 'Speichern';
|
|
form.hidden = false;
|
|
nameInput.focus();
|
|
}
|
|
|
|
async function activateKey(key) {
|
|
try {
|
|
await apiActivateApiKey(key.keyId);
|
|
showToast(key.name + ' aktiviert');
|
|
await loadKeys();
|
|
} catch (err) {
|
|
showToast(err.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function deactivateKey(key) {
|
|
try {
|
|
await apiDeactivateApiKey(key.keyId);
|
|
showToast(key.name + ' deaktiviert');
|
|
await loadKeys();
|
|
} catch (err) {
|
|
showToast(err.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function deleteKey(key) {
|
|
if (!confirm('API-Key "' + key.name + '" wirklich löschen?')) return;
|
|
try {
|
|
await apiDeleteApiKey(key.keyId);
|
|
showToast('API-Key gelöscht');
|
|
await loadKeys();
|
|
} catch (err) {
|
|
showToast(err.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Neuer Key
|
|
newBtn.addEventListener('click', function() {
|
|
editingKeyId = null;
|
|
formTitle.textContent = 'Neuer API-Key';
|
|
providerSelect.value = 'anthropic';
|
|
providerSelect.disabled = false;
|
|
nameInput.value = '';
|
|
keyInput.value = '';
|
|
saveBtn.textContent = 'Erstellen';
|
|
form.hidden = false;
|
|
nameInput.focus();
|
|
});
|
|
|
|
// Speichern (Erstellen oder Bearbeiten)
|
|
saveBtn.addEventListener('click', () => withButtonLoading(saveBtn, saveBtn.textContent, async () => {
|
|
var name = nameInput.value.trim();
|
|
|
|
if (!name) {
|
|
showToast('Name erforderlich', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (editingKeyId) {
|
|
// Bearbeiten
|
|
var data = { name: name };
|
|
var newKey = keyInput.value.trim();
|
|
if (newKey) data.apiKey = newKey;
|
|
await apiUpdateApiKey(editingKeyId, data);
|
|
showToast('API-Key aktualisiert');
|
|
} else {
|
|
// Erstellen
|
|
var provider = providerSelect.value;
|
|
var apiKey = keyInput.value.trim();
|
|
if (!apiKey) {
|
|
showToast('API-Key erforderlich', 'error');
|
|
return;
|
|
}
|
|
await apiCreateApiKey({ provider: provider, name: name, apiKey: apiKey });
|
|
showToast('API-Key erstellt');
|
|
}
|
|
form.hidden = true;
|
|
editingKeyId = null;
|
|
await loadKeys();
|
|
} catch (err) {
|
|
showToast(err.message, 'error');
|
|
}
|
|
}));
|
|
|
|
await loadKeys();
|
|
},
|
|
|
|
destroy() {}
|
|
});
|