/** * 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 = '
${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;
}