/** * Predictalytics Administration WebGUI JavaScript */ const STORAGE_KEY = 'predictalytics_admin_token'; let currentToken = null; let statusPollInterval = null; let terminalEventSource = null; let allLogs = []; let maxLogEntries = 600; // ─── INITIALISIERUNG & AUTHENTIFIZIERUNG ─── document.addEventListener('DOMContentLoaded', () => { setupNavigation(); // Prüfe, ob bereits ein Token in sessionStorage hinterlegt ist const savedToken = sessionStorage.getItem(STORAGE_KEY) || localStorage.getItem(STORAGE_KEY); if (savedToken) { verifyAndUnlock(savedToken); } else { showLogin(); } }); function showLogin(errorMessage = null) { document.getElementById('login-overlay').style.display = 'flex'; document.getElementById('app-container').style.display = 'none'; if (statusPollInterval) clearInterval(statusPollInterval); if (terminalEventSource) { terminalEventSource.close(); terminalEventSource = null; } const errBox = document.getElementById('login-error'); if (errorMessage) { errBox.textContent = errorMessage; errBox.style.display = 'block'; } else { errBox.style.display = 'none'; } const tokenInput = document.getElementById('admin-token-input'); tokenInput.value = ''; tokenInput.focus(); } async function handleLogin(e) { e.preventDefault(); const tokenInput = document.getElementById('admin-token-input'); const token = tokenInput.value.trim(); if (!token) return; const btn = document.getElementById('login-btn'); btn.disabled = true; btn.textContent = 'Prüfe...'; try { const response = await fetch('/api/admin/auth/verify', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Admin-Token': token }, body: JSON.stringify({ token }) }); if (response.ok) { currentToken = token; sessionStorage.setItem(STORAGE_KEY, token); unlockApp(); } else { const data = await response.json().catch(() => ({})); showLogin(data.message || 'Ungültiger Admin-Token. Zugriff verweigert.'); } } catch (err) { showLogin('Verbindungsfehler zum Admin-Server: ' + err.message); } finally { btn.disabled = false; btn.textContent = 'Entsperren & Anmelden'; } } async function verifyAndUnlock(token) { try { const response = await fetch('/api/admin/auth/verify', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Admin-Token': token }, body: JSON.stringify({ token }) }); if (response.ok) { currentToken = token; unlockApp(); } else { sessionStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_KEY); showLogin('Sitzung abgelaufen oder Token ungültig.'); } } catch (err) { showLogin('Server nicht erreichbar.'); } } function unlockApp() { document.getElementById('login-overlay').style.display = 'none'; document.getElementById('app-container').style.display = 'flex'; // Status sofort laden und Polling starten refreshStatus(); statusPollInterval = setInterval(refreshStatus, 3000); // Initiales Terminal-Streaming starten connectTerminalStream(); // Log-Dateien & Settings vorbereiten loadLogFiles(); loadSettings(); loadLicenseInfo(); } function handleLogout() { currentToken = null; sessionStorage.removeItem(STORAGE_KEY); localStorage.removeItem(STORAGE_KEY); showLogin('Erfolgreich abgemeldet.'); } // ─── API HELPER ─── async function api(endpoint, options = {}) { if (!options.headers) options.headers = {}; if (currentToken) { options.headers['X-Admin-Token'] = currentToken; } try { const res = await fetch(endpoint, options); if (res.status === 401) { handleLogout(); throw new Error('Unauthorized'); } return res; } catch (err) { console.error('API Error (' + endpoint + '):', err); throw err; } } // ─── STATUS & STEUERUNG ─── let lastStatus = {}; async function refreshStatus() { try { const res = await api('/api/admin/status'); if (!res.ok) return; const data = await res.json(); lastStatus = data; // Topbar Status-Pills const workersPill = document.getElementById('status-workers-pill'); const workersText = document.getElementById('status-workers-text'); const btnWorkers = document.getElementById('btn-toggle-workers'); const dashBtnWorkers = document.getElementById('dash-btn-workers'); const dashWorkersState = document.getElementById('dash-workers-state'); if (data.workersRunning) { workersPill.className = 'status-pill status-running'; workersText.textContent = 'Workers: Aktiv'; btnWorkers.textContent = '⏹ Stop Server'; dashBtnWorkers.textContent = 'Worker stoppen'; dashBtnWorkers.className = 'btn btn-sm btn-secondary'; dashWorkersState.textContent = 'Läuft'; dashWorkersState.style.color = '#10b981'; } else { workersPill.className = 'status-pill status-stopped'; workersText.textContent = 'Workers: Gestoppt'; btnWorkers.textContent = '▶ Start Server'; dashBtnWorkers.textContent = 'Worker starten'; dashBtnWorkers.className = 'btn btn-sm btn-primary'; dashWorkersState.textContent = 'Gestoppt'; dashWorkersState.style.color = '#94a3b8'; } // Webserver Status const webPill = document.getElementById('status-web-pill'); const webText = document.getElementById('status-web-text'); const btnWeb = document.getElementById('btn-toggle-webserver'); const dashBtnWeb = document.getElementById('dash-btn-web'); const dashWebState = document.getElementById('dash-web-state'); const dashWebUrl = document.getElementById('dash-web-url'); const dashLinkWeb = document.getElementById('dash-link-web'); if (data.webServerRunning) { webPill.className = 'status-pill status-running'; webText.textContent = 'Webserver :' + data.webServerPort; btnWeb.textContent = '⏹ Stop Webserver'; dashBtnWeb.textContent = 'Webserver stoppen'; dashBtnWeb.className = 'btn btn-sm btn-secondary'; dashWebState.textContent = 'Läuft :' + data.webServerPort; dashWebState.style.color = '#10b981'; dashWebUrl.textContent = data.webServerUrl; dashLinkWeb.href = data.webServerUrl; dashLinkWeb.style.display = 'inline-flex'; } else { webPill.className = 'status-pill status-stopped'; webText.textContent = 'Webserver: Gestoppt'; btnWeb.textContent = '▶ Start Webserver'; dashBtnWeb.textContent = 'Webserver starten'; dashBtnWeb.className = 'btn btn-sm btn-primary'; dashWebState.textContent = 'Gestoppt'; dashWebState.style.color = '#94a3b8'; dashWebUrl.textContent = 'Port: ' + data.webServerPort; dashLinkWeb.style.display = 'none'; } // DB Size const dbText = document.getElementById('status-dbsize-text'); const dashDbText = document.getElementById('dash-db-size'); const dbFormatted = data.dbSizeMb != null ? Number(data.dbSizeMb).toFixed(2) + ' MB' : '—'; dbText.textContent = dbFormatted; dashDbText.textContent = dbFormatted; // Busy Indicator const busyIndicator = document.getElementById('busy-indicator'); const busyText = document.getElementById('busy-operation-text'); if (data.isBusy) { busyIndicator.style.display = 'flex'; busyText.textContent = data.busyOperation || 'Wird ausgeführt...'; } else { busyIndicator.style.display = 'none'; } // Version & Uptime document.getElementById('header-version').textContent = 'v' + data.appVersion; document.getElementById('sys-memory').textContent = data.memoryMb + ' MB'; document.getElementById('dash-memory').textContent = data.memoryMb + ' MB'; document.getElementById('sys-uptime').textContent = data.uptimeText; document.getElementById('dash-uptime').textContent = 'Uptime: ' + data.uptimeText; } catch (e) { console.warn('Status poll failed:', e); } } async function toggleWorkers() { const isRunning = lastStatus.workersRunning; const action = isRunning ? 'stop' : 'start'; try { const res = await api('/api/admin/workers/' + action, { method: 'POST' }); const data = await res.json(); showToast(data.message || 'Worker-Status geändert.', res.ok ? 'success' : 'error'); refreshStatus(); } catch (e) { showToast('Aktion fehlgeschlagen: ' + e.message, 'error'); } } async function toggleWebserver() { const isRunning = lastStatus.webServerRunning; const action = isRunning ? 'stop' : 'start'; try { const res = await api('/api/admin/webserver/' + action, { method: 'POST' }); const data = await res.json(); showToast(data.message || 'Webserver-Status geändert.', res.ok ? 'success' : 'error'); refreshStatus(); } catch (e) { showToast('Aktion fehlgeschlagen: ' + e.message, 'error'); } } // ─── WARTUNGSAKTIONEN ─── async function triggerMarketSync() { if (lastStatus.workersRunning) { showToast('Markt-Sync kann nicht laufen, während die Worker aktiv sind. Bitte zuerst Worker stoppen.', 'warning'); return; } showToast('Manueller Markt-Sync gestartet...', 'success'); refreshStatus(); try { const res = await api('/api/admin/maintenance/sync-markets', { method: 'POST' }); const data = await res.json(); if (res.ok) { showToast(data.message || 'Markt-Sync erfolgreich abgeschlossen.', 'success'); } else { showToast(data.error || 'Markt-Sync fehlgeschlagen.', 'error'); } } catch (e) { showToast('Fehler: ' + e.message, 'error'); } finally { refreshStatus(); } } async function triggerDbUpdate() { if (lastStatus.workersRunning) { showToast('Datenbank-Update kann nicht laufen, während die Worker aktiv sind.', 'warning'); return; } showToast('Datenbank-Update gestartet...', 'success'); refreshStatus(); try { const res = await api('/api/admin/maintenance/update-database', { method: 'POST' }); const data = await res.json(); if (res.ok) { showToast(data.message || 'Datenbank erfolgreich aktualisiert.', 'success'); } else { showToast(data.error || 'Datenbank-Update fehlgeschlagen.', 'error'); } } catch (e) { showToast('Fehler: ' + e.message, 'error'); } finally { refreshStatus(); } } function openRecalculateModal() { if (lastStatus.workersRunning) { showToast('Neuberechnung kann nicht laufen, während die Worker aktiv sind. Bitte zuerst Worker stoppen.', 'warning'); return; } document.getElementById('confirm-modal').style.display = 'flex'; } function closeRecalculateModal() { document.getElementById('confirm-modal').style.display = 'none'; } async function executeRecalculateAll() { closeRecalculateModal(); showToast('Neuberechnung aller Trader eingeleitet...', 'warning'); refreshStatus(); try { const res = await api('/api/admin/maintenance/recalculate-traders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ confirm: true }) }); const data = await res.json(); if (res.ok) { showToast('Neuberechnung erfolgreich eingeleitet: ' + (data.summary ? data.summary.split('\n')[0] : 'Fertig'), 'success'); } else { showToast(data.error || 'Neuberechnung fehlgeschlagen.', 'error'); } } catch (e) { showToast('Fehler: ' + e.message, 'error'); } finally { refreshStatus(); } } // ─── LIVE-TERMINAL (SSE) ─── function connectTerminalStream() { if (terminalEventSource) { terminalEventSource.close(); } const url = '/api/admin/logs/stream?token=' + encodeURIComponent(currentToken); terminalEventSource = new EventSource(url); terminalEventSource.onmessage = (e) => { try { const entry = JSON.parse(e.data); if (entry && entry.Message) { appendLogLine(entry); } } catch (err) { // Ignoriere Ping / Non-JSON } }; terminalEventSource.onerror = (e) => { console.warn('Terminal EventSource error / disconnected.'); }; } function reconnectTerminalStream() { appendLogLine({ LevelText: 'INF', Message: 'Verbinde neu mit Live-Log-Stream...', Timestamp: new Date().toISOString() }); connectTerminalStream(); } function appendLogLine(entry) { allLogs.push(entry); if (allLogs.length > maxLogEntries) { allLogs.shift(); } renderLogLine(entry); } function renderLogLine(entry) { const filter = document.getElementById('term-level-filter').value; if (!matchesFilter(entry.LevelText, filter)) return; const termBody = document.getElementById('terminal-body'); const div = document.createElement('div'); const lvl = (entry.LevelText || 'INF').toLowerCase(); div.className = 'log-line log-' + lvl; const ts = entry.Timestamp ? new Date(entry.Timestamp).toLocaleTimeString('de-DE') : ''; let text = `[${ts}] [${entry.LevelText || 'INF'}] ${entry.Message}`; if (entry.Exception) { text += `\n ⚠ ${entry.Exception}`; } div.textContent = text; termBody.appendChild(div); // Auf 500 Zeilen begrenzen while (termBody.children.length > 500) { termBody.removeChild(termBody.firstChild); } const autoscroll = document.getElementById('term-autoscroll').checked; if (autoscroll) { termBody.scrollTop = termBody.scrollHeight; } } function filterTerminalLogs() { const termBody = document.getElementById('terminal-body'); termBody.innerHTML = ''; const filter = document.getElementById('term-level-filter').value; allLogs.forEach(entry => { if (matchesFilter(entry.LevelText, filter)) { renderLogLine(entry); } }); } function matchesFilter(levelText, filter) { if (filter === 'ALL') return true; if (filter === 'INF') return ['INF', 'WRN', 'ERR', 'FTL'].includes(levelText); if (filter === 'WRN') return ['WRN', 'ERR', 'FTL'].includes(levelText); if (filter === 'ERR') return ['ERR', 'FTL'].includes(levelText); return true; } function clearTerminal() { allLogs = []; document.getElementById('terminal-body').innerHTML = '
Terminal geleert.
'; } // ─── LOG-DATEIEN ─── async function loadLogFiles() { const tbody = document.getElementById('logfiles-table-body'); tbody.innerHTML = 'Lade Log-Dateien...'; try { const res = await api('/api/admin/logs/files'); if (!res.ok) throw new Error('Fehler beim Laden'); const files = await res.json(); if (!files || files.length === 0) { tbody.innerHTML = 'Keine Log-Dateien gefunden.'; return; } tbody.innerHTML = ''; files.forEach(f => { const tr = document.createElement('tr'); const dateStr = f.modifiedUtc ? new Date(f.modifiedUtc).toLocaleString('de-DE') : '—'; const dlUrl = `/api/admin/logs/files/download?file=${encodeURIComponent(f.relativePath)}&token=${encodeURIComponent(currentToken)}`; tr.innerHTML = ` ${escapeHtml(f.category)} ${escapeHtml(f.name)} ${f.sizeKb} KB ${dateStr} ⬇️ Herunterladen `; tbody.appendChild(tr); }); } catch (e) { tbody.innerHTML = 'Log-Dateien konnten nicht geladen werden.'; } } // ─── EINSTELLUNGEN ─── async function loadSettings() { try { const res = await api('/api/admin/settings'); if (!res.ok) return; const data = await res.json(); // Admin if (data.adminServer) { document.getElementById('cfg-admin-port').value = data.adminServer.port || 4001; document.getElementById('cfg-admin-host').value = data.adminServer.host || 'localhost'; document.getElementById('cfg-admin-token').value = data.adminServer.token || ''; } // Webserver if (data.webserver) { document.getElementById('cfg-web-port').value = data.webserver.port || 4000; document.getElementById('cfg-web-host').value = data.webserver.host || 'localhost'; document.getElementById('cfg-web-token').value = data.webserver.apiControlToken || ''; document.getElementById('cfg-web-dbdebug').checked = !!data.webserver.dbConnectionDebug; } // Datenbank if (data.database) { document.getElementById('cfg-db-server').value = data.database.server || ''; document.getElementById('cfg-db-name').value = data.database.database || ''; document.getElementById('cfg-db-user').value = data.database.user || ''; document.getElementById('cfg-db-password').value = data.database.password || ''; if (data.database.sslMode) { document.getElementById('cfg-db-ssl').value = data.database.sslMode; } } // Deployment Center if (data.deploymentCenter) { document.getElementById('cfg-dc-url').value = data.deploymentCenter.serverUrl || ''; document.getElementById('cfg-dc-token').value = data.deploymentCenter.token || ''; document.getElementById('cfg-dc-heartbeat').checked = !!data.deploymentCenter.heartbeatEnabled; document.getElementById('cfg-dc-source').value = data.deploymentCenter.source || ''; document.getElementById('cfg-dc-instance').value = data.deploymentCenter.instance || ''; document.getElementById('cfg-dc-interval').value = data.deploymentCenter.intervalSeconds || 60; document.getElementById('cfg-dc-errorreporting').checked = !!data.deploymentCenter.errorReportingEnabled; document.getElementById('cfg-dc-updatecheck').checked = !!data.deploymentCenter.updateCheckEnabled; if (data.deploymentCenter.updateChannel) { document.getElementById('cfg-dc-channel').value = data.deploymentCenter.updateChannel; } } // Egress if (data.egress) { document.getElementById('cfg-egress-channels').value = data.egress.channelsText || ''; } } catch (e) { console.warn('Failed to load settings:', e); } } async function saveSettings() { const payload = { adminServer: { port: parseInt(document.getElementById('cfg-admin-port').value, 10), host: document.getElementById('cfg-admin-host').value.trim(), token: document.getElementById('cfg-admin-token').value.trim() }, webserver: { port: parseInt(document.getElementById('cfg-web-port').value, 10), host: document.getElementById('cfg-web-host').value.trim(), apiControlToken: document.getElementById('cfg-web-token').value.trim(), dbConnectionDebug: document.getElementById('cfg-web-dbdebug').checked }, database: { server: document.getElementById('cfg-db-server').value.trim(), database: document.getElementById('cfg-db-name').value.trim(), user: document.getElementById('cfg-db-user').value.trim(), password: document.getElementById('cfg-db-password').value.trim(), sslMode: document.getElementById('cfg-db-ssl').value }, deploymentCenter: { token: document.getElementById('cfg-dc-token').value.trim(), heartbeatEnabled: document.getElementById('cfg-dc-heartbeat').checked, source: document.getElementById('cfg-dc-source').value.trim(), instance: document.getElementById('cfg-dc-instance').value.trim(), intervalSeconds: parseInt(document.getElementById('cfg-dc-interval').value, 10), errorReportingEnabled: document.getElementById('cfg-dc-errorreporting').checked, updateCheckEnabled: document.getElementById('cfg-dc-updatecheck').checked, updateChannel: document.getElementById('cfg-dc-channel').value }, egress: { channelsText: document.getElementById('cfg-egress-channels').value } }; try { const res = await api('/api/admin/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await res.json(); if (res.ok) { showToast(data.message || 'Einstellungen gespeichert.', 'success'); // Wenn der Admin-Token geändert wurde, Session aktualisieren if (payload.adminServer.token && payload.adminServer.token !== currentToken) { currentToken = payload.adminServer.token; sessionStorage.setItem(STORAGE_KEY, currentToken); } } else { showToast(data.error || 'Fehler beim Speichern der Einstellungen.', 'error'); } } catch (e) { showToast('Fehler: ' + e.message, 'error'); } } async function generateNewAdminToken() { try { const res = await api('/api/admin/settings/generate-token', { method: 'POST' }); if (res.ok) { const data = await res.json(); document.getElementById('cfg-admin-token').value = data.token; showToast('Neuer sicherer Token generiert. Zum Übernehmen auf "Speichern" klicken.', 'warning'); } } catch (e) { showToast('Token-Generierung fehlgeschlagen.', 'error'); } } // ─── LIZENZ & UPDATES ─── async function loadLicenseInfo() { try { const res = await api('/api/admin/license'); if (!res.ok) return; const data = await res.json(); document.getElementById('lic-product').textContent = data.productSlug; document.getElementById('lic-version').textContent = `v${data.version} (${data.gitCommit || 'head'})`; document.getElementById('lic-hwid').textContent = data.hardwareId || '—'; document.getElementById('lic-hwidsource').textContent = 'Quelle: ' + (data.hwidSource || '—'); document.getElementById('lic-status').textContent = data.status; document.getElementById('lic-status').style.color = data.isUsable ? '#10b981' : '#ef4444'; document.getElementById('lic-message').textContent = data.message || 'Gültig'; } catch (e) { console.warn('Failed to load license info:', e); } } async function checkForUpdates() { showToast('Prüfe auf Updates...', 'info'); try { const res = await api('/api/admin/updates/check', { method: 'POST' }); if (!res.ok) throw new Error('Prüfung fehlgeschlagen'); const data = await res.json(); const banner = document.getElementById('update-banner'); const title = document.getElementById('update-banner-title'); const body = document.getElementById('update-banner-body'); if (data.updateAvailable) { banner.style.display = 'block'; title.textContent = `⬆️ Neues Release v${data.latestVersion} verfügbar! (Installiert: v${data.currentVersion})`; let html = ''; if (data.isCritical) { html += '
⚠️ WICHTIG: Dieses Update ist als kritisch markiert.
'; } if (data.changelog) { html += `
${escapeHtml(data.changelog)}
`; } body.innerHTML = html; showToast(`Update verfügbar: v${data.latestVersion}`, 'warning'); } else { banner.style.display = 'none'; showToast(data.message || `Predictalytics v${data.currentVersion} ist aktuell.`, 'success'); } } catch (e) { showToast('Update-Prüfung fehlgeschlagen: ' + e.message, 'error'); } } // ─── NAVIGATION & TABS ─── function setupNavigation() { const links = document.querySelectorAll('.nav-link'); links.forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const tabName = link.getAttribute('data-tab'); switchTab(tabName); }); }); } function switchTab(tabName) { document.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active')); const targetLink = document.querySelector(`.nav-link[data-tab="${tabName}"]`); const targetPanel = document.getElementById('panel-' + tabName); if (targetLink) targetLink.classList.add('active'); if (targetPanel) targetPanel.classList.add('active'); // Wenn spezifische Tabs aktiviert werden, Daten nachladen if (tabName === 'logfiles') loadLogFiles(); if (tabName === 'settings') loadSettings(); if (tabName === 'license') loadLicenseInfo(); } // ─── TOAST BENACHRICHTIGUNGEN ─── function showToast(message, type = 'info') { const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateY(10px)'; toast.style.transition = 'all 0.3s ease'; setTimeout(() => toast.remove(), 300); }, 4000); } function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }