// ── 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');
if (key.status === 'ok') parts.push('✅ Gültig');
else if (key.status === 'invalid') parts.push('❌ Ungültig');
else if (key.status === 'rate_limited') parts.push('⚠️ Rate Limited');
else if (key.status === 'quota_exceeded') parts.push('⚠️ Quota');
else if (key.status === 'error') parts.push('⚠️ Fehler');
if (key.plan) parts.push(key.plan);
if (key.usage7d != null) parts.push('7d: ' + key.usage7d + '%');
if (key.usage5h != null) parts.push('5h: ' + key.usage5h + '%');
if (key.checkedAt) parts.push(new Date(key.checkedAt).toLocaleString());
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 = '';
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 = '';
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 = '';
deactivateBtn.addEventListener('click', function() { deactivateKey(key); });
actions.appendChild(deactivateBtn);
} else {
var activateBtn = document.createElement('button');
activateBtn.className = 'icon-btn';
activateBtn.title = 'Aktivieren';
activateBtn.innerHTML = '';
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 = '';
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 = '';
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');
}
await loadKeys();
} 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();
// ── Scanner UI ──
var scanBtn = document.getElementById('scanTriggerBtn');
var scanStatusEl = document.getElementById('scanStatus');
var scannedList = document.getElementById('scannedKeysList');
var scannedKeys = [];
var pollTimer = null;
function renderScannedKeys() {
scannedList.innerHTML = '';
if (scannedKeys.length === 0) {
var empty = document.createElement('div');
empty.className = 'admin-user-meta';
empty.style.padding = '1rem 0';
empty.textContent = 'Keine gescannten Keys vorhanden.';
scannedList.appendChild(empty);
return;
}
scannedKeys.forEach(function(sk) {
var isAdopted = sk.adoptedApiKeyId != null;
var row = document.createElement('div');
row.className = 'admin-user-row';
if (isAdopted) row.style.opacity = '0.5';
var info = document.createElement('div');
info.className = 'admin-user-info';
info.style.minWidth = '0';
if (sk.sourceUrl) {
var nameSpan = document.createElement('a');
nameSpan.className = 'admin-user-name';
nameSpan.href = sk.sourceUrl;
nameSpan.target = '_blank';
nameSpan.rel = 'noopener';
nameSpan.style.cssText = 'word-break:break-all;font-size:.85em';
nameSpan.textContent = sk.sourceUrl.replace('https://github.com/', '');
info.appendChild(nameSpan);
}
var metaSpan = document.createElement('span');
metaSpan.className = 'admin-user-meta';
var statusIcon = sk.status === 'valid' ? '✅' : sk.status === 'invalid' ? '❌' : sk.status === 'unchecked' ? '⏳' : '⚠️';
var metaParts = isAdopted ? ['📥 Übernommen'] : [];
metaParts.push(statusIcon + ' ' + sk.status);
if (sk.plan && sk.plan !== 'Unknown') metaParts.push(sk.plan);
if (sk.usage7d != null) metaParts.push('7d: ' + sk.usage7d + '%');
if (sk.usage5h != null) metaParts.push('5h: ' + sk.usage5h + '%');
if (sk.checkedAt) metaParts.push(new Date(sk.checkedAt).toLocaleString());
metaSpan.textContent = metaParts.join(' · ');
info.appendChild(metaSpan);
var actions = document.createElement('div');
actions.className = 'admin-user-actions';
if (!isAdopted) {
// Recheck
var recheckBtn = document.createElement('button');
recheckBtn.className = 'icon-btn';
recheckBtn.title = 'Erneut prüfen';
recheckBtn.innerHTML = '';
recheckBtn.addEventListener('click', async function() {
recheckBtn.innerHTML = '';
recheckBtn.disabled = true;
try {
await apiRecheckScannedKey(sk.scannedKeyId);
await loadScannedKeys();
} catch (err) { showToast(err.message, 'error'); }
finally { recheckBtn.innerHTML = ''; recheckBtn.disabled = false; }
});
actions.appendChild(recheckBtn);
// Übernehmen (nur bei valid)
if (sk.status === 'valid') {
var adoptBtn = document.createElement('button');
adoptBtn.className = 'icon-btn';
adoptBtn.title = 'Als API-Key übernehmen';
adoptBtn.innerHTML = '';
adoptBtn.addEventListener('click', async function() {
try {
await apiAdoptScannedKey(sk.scannedKeyId);
showToast('Key übernommen');
await loadScannedKeys();
await loadKeys();
} catch (err) { showToast(err.message, 'error'); }
});
actions.appendChild(adoptBtn);
}
// Löschen
var delBtn = document.createElement('button');
delBtn.className = 'icon-btn';
delBtn.title = 'Ignorieren';
delBtn.innerHTML = '';
delBtn.addEventListener('click', async function() {
try {
await apiDeleteScannedKey(sk.scannedKeyId);
await loadScannedKeys();
} catch (err) { showToast(err.message, 'error'); }
});
actions.appendChild(delBtn);
}
row.appendChild(info);
row.appendChild(actions);
scannedList.appendChild(row);
});
}
async function loadScannedKeys() {
try {
scannedKeys = await apiGetScannedKeys();
renderScannedKeys();
} catch (err) { showToast(err.message, 'error'); }
}
async function updateScanStatus() {
try {
var s = await apiScanStatus();
if (s.running) {
scanStatusEl.textContent = '⏳ Scan läuft…';
scanBtn.disabled = true;
scanBtn.innerHTML = ' Läuft…';
// Polling starten wenn noch nicht aktiv
if (!pollTimer) {
pollTimer = setInterval(async function() {
var st = await apiScanStatus();
if (!st.running) {
clearInterval(pollTimer);
pollTimer = null;
scanBtn.disabled = false;
scanBtn.innerHTML = ' Jetzt scannen';
scanStatusEl.textContent = st.lastResult ? '✅ ' + st.lastResult : '';
await loadScannedKeys();
}
}, 3000);
}
} else {
scanBtn.disabled = false;
scanBtn.innerHTML = ' Jetzt scannen';
if (s.lastResult) {
var timeStr = s.lastRun ? new Date(s.lastRun).toLocaleString() + ' – ' : '';
scanStatusEl.textContent = timeStr + s.lastResult;
}
}
} catch (err) { /* ignore */ }
}
scanBtn.addEventListener('click', async function() {
try {
await apiTriggerScan();
showToast('Scan gestartet');
await updateScanStatus();
} catch (err) {
showToast(err.message, 'error');
}
});
await loadScannedKeys();
await updateScanStatus();
},
destroy() {
// Polling stoppen wenn Seite verlassen wird
}
});