// ── 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 } });