Files
gott/wwwroot/js/pages/api-keys-page.js
T
Meleki 608118a823 Key-Scanner: Auth-Header fix, 401-mit-Usage-Handling, breitere Suche, Live-Refresh
- AnthropicKeyChecker: Bearer-Auth + CLI-Header statt x-api-key (identisch mit AnthropicService)
- 401 mit Usage-Headern → 'usage_limited' statt 'invalid' (Key gültig, Limit erreicht)
- Haiku-Phase entfernt, Streaming-Logik entfernt → ein einfacher Request reicht
- KeyScannerService: SearchLanguages 1→13, PathExtensions 24 neue Pfad-Filter
- ApiKeysEndpoint: usage_limited als 'ok' ans Frontend
- Frontend: Adopt-Button auch bei quota_exceeded, Live-Refresh alle 3s während Scan
- CSS: Overflow-Ellipsis für lange URLs in Admin-Listen
2026-03-12 17:11:06 +01:00

429 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── 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 = '<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');
}
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 = '<i class="fa fa-refresh"></i>';
recheckBtn.addEventListener('click', async function() {
recheckBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
recheckBtn.disabled = true;
try {
await apiRecheckScannedKey(sk.scannedKeyId);
await loadScannedKeys();
} catch (err) { showToast(err.message, 'error'); }
finally { recheckBtn.innerHTML = '<i class="fa fa-refresh"></i>'; recheckBtn.disabled = false; }
});
actions.appendChild(recheckBtn);
// Übernehmen (bei valid oder quota_exceeded)
if (sk.status === 'valid' || sk.status === 'quota_exceeded') {
var adoptBtn = document.createElement('button');
adoptBtn.className = 'icon-btn';
adoptBtn.title = 'Als API-Key übernehmen';
adoptBtn.innerHTML = '<i class="fa fa-plus-circle" style="color:var(--accent)"></i>';
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 = '<i class="fa fa-trash"></i>';
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 = '<i class="fa fa-spinner fa-spin"></i> Läuft…';
// Polling starten wenn noch nicht aktiv
if (!pollTimer) {
pollTimer = setInterval(async function() {
var st = await apiScanStatus();
await loadScannedKeys();
if (!st.running) {
clearInterval(pollTimer);
pollTimer = null;
scanBtn.disabled = false;
scanBtn.innerHTML = '<i class="fa fa-search"></i> Jetzt scannen';
scanStatusEl.textContent = st.lastResult ? '✅ ' + st.lastResult : '';
}
}, 3000);
}
} else {
scanBtn.disabled = false;
scanBtn.innerHTML = '<i class="fa fa-search"></i> 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
}
});