Eigenständiges token-geschütztes Verwaltungs-WebGUI (Admin WebGUI) hinzugefügt
- Unabhängige Kestrel-Instanz auf Port 5001 (vollständig getrennt vom Analyse-Webserver) - Admin API Endpunkte ohne Swagger-Exposition - Token-Gatekeeper mit sofortigem Login-Overlay bei unautorisiertem Zugriff - Live-Terminal via SSE-Log-Streaming und Log-Download-Übersicht - Steuerung von Workern, Analyse-Webserver, Wartungsaktionen und Einstellungen - Nahtlose Integration in Avalonia Desktop-Shell und Linux Headless-Modus - Unit-Tests für Admin-Filter, Optionen und Log-Puffer
This commit is contained in:
@@ -0,0 +1,734 @@
|
||||
/**
|
||||
* 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 = '<div class="log-line log-inf"><span>Terminal geleert.</span></div>';
|
||||
}
|
||||
|
||||
// ─── LOG-DATEIEN ───
|
||||
|
||||
async function loadLogFiles() {
|
||||
const tbody = document.getElementById('logfiles-table-body');
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="loading-cell">Lade Log-Dateien...</td></tr>';
|
||||
|
||||
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 = '<tr><td colspan="5" class="loading-cell">Keine Log-Dateien gefunden.</td></tr>';
|
||||
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 = `
|
||||
<td><span class="badge" style="background:#1e293b;padding:2px 8px;border-radius:4px;">${escapeHtml(f.category)}</span></td>
|
||||
<td class="font-mono"><strong>${escapeHtml(f.name)}</strong></td>
|
||||
<td class="num">${f.sizeKb} KB</td>
|
||||
<td>${dateStr}</td>
|
||||
<td class="actions-col">
|
||||
<a href="${dlUrl}" class="btn btn-sm btn-secondary" download="${escapeHtml(f.name)}">⬇️ Herunterladen</a>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
} catch (e) {
|
||||
tbody.innerHTML = '<tr><td colspan="5" class="loading-cell" style="color:#ef4444;">Log-Dateien konnten nicht geladen werden.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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 || 5001;
|
||||
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 || 5000;
|
||||
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 += '<div class="card-badge badge-warning" style="margin-bottom:8px;">⚠️ WICHTIG: Dieses Update ist als kritisch markiert.</div>';
|
||||
}
|
||||
if (data.changelog) {
|
||||
html += `<pre class="terminal-body" style="height:auto;max-height:200px;margin-top:8px;">${escapeHtml(data.changelog)}</pre>`;
|
||||
}
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user