Files
Predictalytics/src/Predictalytics.Hosting/Admin/wwwroot/index.html
T
Richard 1131d257cf 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
2026-09-07 21:09:03 +02:00

499 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Predictalytics Administration</title>
<link rel="stylesheet" href="css/admin.css">
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🛡️</text></svg>">
</head>
<body>
<!-- ─── TOKEN LOGIN GATEKEEPER ─── -->
<div id="login-overlay" class="login-modal" style="display: flex;">
<div class="login-card">
<div class="login-header">
<div class="login-shield">🛡️</div>
<h1>Predictalytics</h1>
<div class="login-badge">VERWALTUNG &amp; ADMINISTRATION</div>
<p class="login-hint">Dieses Interface ist passwortgeschützt. Bitte geben Sie den in den Einstellungen hinterlegten Admin-Token ein.</p>
</div>
<form id="login-form" onsubmit="handleLogin(event)">
<div class="form-group">
<label for="admin-token-input">Admin-Token</label>
<input type="password" id="admin-token-input" placeholder="Token eingeben..." required autocomplete="current-password" autofocus />
</div>
<div id="login-error" class="login-error" style="display: none;"></div>
<button type="submit" id="login-btn" class="btn btn-primary btn-block">Entsperren &amp; Anmelden</button>
</form>
</div>
</div>
<!-- ─── HAUPT-DASHBOARD (initial ausgeblendet) ─── -->
<div id="app-container" class="app-layout" style="display: none;">
<!-- TOPBAR -->
<header class="topbar">
<div class="topbar-left">
<div class="brand">
<span class="brand-title">Predictalytics</span>
<span class="badge badge-admin">ADMIN</span>
</div>
<div class="status-indicators">
<div id="status-workers-pill" class="status-pill status-stopped">
<span class="status-dot"></span>
<span id="status-workers-text">Workers: Gestoppt</span>
</div>
<div id="status-web-pill" class="status-pill status-stopped">
<span class="status-dot"></span>
<span id="status-web-text">Webserver: Gestoppt</span>
</div>
<div id="status-dbsize-pill" class="status-pill">
<span>DB: <strong id="status-dbsize-text">—</strong></span>
</div>
<div id="busy-indicator" class="busy-pill" style="display: none;">
<div class="spinner"></div>
<span id="busy-operation-text">Wird ausgeführt...</span>
</div>
</div>
</div>
<div class="topbar-right">
<button id="btn-toggle-workers" class="btn btn-sm btn-outline" onclick="toggleWorkers()">
▶ Start Server
</button>
<button id="btn-toggle-webserver" class="btn btn-sm btn-outline" onclick="toggleWebserver()">
▶ Start Webserver
</button>
<div class="topbar-divider"></div>
<span id="header-version" class="version-tag">v—</span>
<button class="btn btn-sm btn-secondary" onclick="handleLogout()" title="Sitzung beenden">
Abmelden
</button>
</div>
</header>
<!-- WORKSPACE (SIDEBAR + CONTENT) -->
<div class="workspace">
<!-- SIDEBAR NAVIGATION -->
<aside class="sidebar">
<nav class="nav-menu">
<a href="#dashboard" class="nav-link active" data-tab="dashboard">
<span class="nav-icon">📊</span>
<span>Dashboard</span>
</a>
<a href="#terminal" class="nav-link" data-tab="terminal">
<span class="nav-icon">💻</span>
<span>Terminal &amp; Logs</span>
</a>
<a href="#logfiles" class="nav-link" data-tab="logfiles">
<span class="nav-icon">📁</span>
<span>Log-Dateien</span>
</a>
<a href="#maintenance" class="nav-link" data-tab="maintenance">
<span class="nav-icon">🛠️</span>
<span>Wartung</span>
</a>
<a href="#settings" class="nav-link" data-tab="settings">
<span class="nav-icon">⚙️</span>
<span>Einstellungen</span>
</a>
<a href="#license" class="nav-link" data-tab="license">
<span class="nav-icon">📜</span>
<span>Lizenz &amp; Updates</span>
</a>
</nav>
<div class="sidebar-footer">
<div class="system-mini-info">
<div>RAM: <strong id="sys-memory">—</strong></div>
<div>Uptime: <strong id="sys-uptime">—</strong></div>
</div>
</div>
</aside>
<!-- CONTENT PANELS -->
<main class="content-area">
<!-- 1. DASHBOARD -->
<section id="panel-dashboard" class="tab-panel active">
<div class="panel-header">
<h2>System-Übersicht</h2>
<button class="btn btn-sm btn-secondary" onclick="refreshStatus()">Aktualisieren</button>
</div>
<div class="card-grid">
<div class="stat-card">
<div class="stat-title">Hintergrund-Worker (Data Crawler &amp; Scoring)</div>
<div class="stat-value" id="dash-workers-state">Gestoppt</div>
<p class="stat-desc">Polymarket, Limitless, Azuro und Analyse-Pipelines.</p>
<div class="stat-actions">
<button id="dash-btn-workers" class="btn btn-sm btn-primary" onclick="toggleWorkers()">Worker starten</button>
</div>
</div>
<div class="stat-card">
<div class="stat-title">Analyse-Webserver (Öffentliche WebUI)</div>
<div class="stat-value" id="dash-web-state">Gestoppt</div>
<p class="stat-desc" id="dash-web-url">Port: 5000</p>
<div class="stat-actions">
<button id="dash-btn-web" class="btn btn-sm btn-primary" onclick="toggleWebserver()">Webserver starten</button>
<a id="dash-link-web" href="#" target="_blank" class="btn btn-sm btn-secondary" style="display:none;">WebUI im Browser öffnen ↗</a>
</div>
</div>
<div class="stat-card">
<div class="stat-title">Datenbank-Speicher</div>
<div class="stat-value" id="dash-db-size">— MB</div>
<p class="stat-desc">MySQL / MariaDB Tabellen &amp; Indizes</p>
<div class="stat-actions">
<button class="btn btn-sm btn-secondary" onclick="triggerDbUpdate()">Schema aktualisieren</button>
</div>
</div>
<div class="stat-card">
<div class="stat-title">System-Ressourcen</div>
<div class="stat-value" id="dash-memory">— MB</div>
<p class="stat-desc" id="dash-uptime">Uptime: —</p>
<div class="stat-actions">
<button class="btn btn-sm btn-secondary" onclick="switchTab('terminal')">Terminal ansehen</button>
</div>
</div>
</div>
<!-- Schnellaktionen -->
<div class="section-box">
<h3>Häufige Verwaltungsaktionen</h3>
<div class="actions-row">
<button class="btn btn-secondary" onclick="triggerMarketSync()">🏛️ Märkte synchronisieren</button>
<button class="btn btn-secondary" onclick="triggerDbUpdate()">🗄️ Datenbank aktualisieren</button>
<button class="btn btn-secondary" onclick="checkForUpdates()">⬆️ Nach Updates suchen</button>
<button class="btn btn-warning" onclick="openRecalculateModal()">🔄 Alle Trader neu berechnen…</button>
</div>
</div>
</section>
<!-- 2. TERMINAL & LOGS -->
<section id="panel-terminal" class="tab-panel">
<div class="panel-header">
<h2>Live-Terminal</h2>
<div class="terminal-controls">
<label class="toggle-control">
<input type="checkbox" id="term-autoscroll" checked />
<span>Autoscroll</span>
</label>
<select id="term-level-filter" class="form-select-sm" onchange="filterTerminalLogs()">
<option value="ALL">Alle Stufen</option>
<option value="INF">Info &amp; höher</option>
<option value="WRN">Nur Warnungen &amp; Fehler</option>
<option value="ERR">Nur Fehler</option>
</select>
<button class="btn btn-sm btn-secondary" onclick="clearTerminal()">Terminal leeren</button>
<button class="btn btn-sm btn-secondary" onclick="reconnectTerminalStream()">Neu verbinden</button>
</div>
</div>
<div class="terminal-wrapper">
<div id="terminal-body" class="terminal-body">
<div class="log-line log-inf"><span>Terminal initialisiert. Verbinde mit Live-Stream...</span></div>
</div>
</div>
</section>
<!-- 3. LOG-DATEIEN -->
<section id="panel-logfiles" class="tab-panel">
<div class="panel-header">
<h2>Log-Dateien</h2>
<button class="btn btn-sm btn-secondary" onclick="loadLogFiles()">Liste aktualisieren</button>
</div>
<p class="section-hint">Übersicht aller auf der Maschine abgelegten Log-Dateien nach Kategorien (all, info, workers, platforms). Jede Datei kann direkt heruntergeladen werden.</p>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>Kategorie</th>
<th>Dateiname</th>
<th class="num">Größe</th>
<th>Geändert (UTC)</th>
<th class="actions-col">Aktion</th>
</tr>
</thead>
<tbody id="logfiles-table-body">
<tr><td colspan="5" class="loading-cell">Lade Log-Dateien...</td></tr>
</tbody>
</table>
</div>
</section>
<!-- 4. WARTUNG -->
<section id="panel-maintenance" class="tab-panel">
<div class="panel-header">
<h2>Wartungs- &amp; Entwicklungsaktionen</h2>
</div>
<div class="maintenance-grid">
<div class="maintenance-card">
<div class="card-icon">🏛️</div>
<div class="card-content">
<h3>Märkte synchronisieren</h3>
<p>Führt einen vollständigen Einmal-Sync aller Märkte und Events für Polymarket, Limitless und Azuro durch. Aktualisiert Titel, Auflösungszustände und Quoten.</p>
<div class="card-badge">Voraussetzung: Worker müssen gestoppt sein</div>
</div>
<div class="card-action">
<button class="btn btn-secondary" onclick="triggerMarketSync()">Markt-Sync starten</button>
</div>
</div>
<div class="maintenance-card">
<div class="card-icon">🗄️</div>
<div class="card-content">
<h3>Datenbank aktualisieren</h3>
<p>Wendet ausstehende Entity Framework Core Migrationen auf die konfigurierte Datenbank an und stellt das Schema sicher.</p>
<div class="card-badge">Voraussetzung: Worker müssen gestoppt sein</div>
</div>
<div class="card-action">
<button class="btn btn-secondary" onclick="triggerDbUpdate()">Datenbank aktualisieren</button>
</div>
</div>
<div class="maintenance-card danger-border">
<div class="card-icon">🔄</div>
<div class="card-content">
<h3>Alle Trader neu berechnen</h3>
<p>Löscht alle <strong>abgeleiteten</strong> Analysedaten (Positionen, Tages-Snapshots, Kategorie-Statistiken) und reiht sämtliche Trader zur vollständigen Neuberechnung mit der aktuellen Engine ein. Rohdaten (Trades und Märkte) bleiben unberührt.</p>
<div class="card-badge badge-warning">Achtung: Bereinigt Snapshots &amp; Zwischenwerte</div>
</div>
<div class="card-action">
<button class="btn btn-danger" onclick="openRecalculateModal()">Neuberechnung ausführen…</button>
</div>
</div>
</div>
</section>
<!-- 5. EINSTELLUNGEN -->
<section id="panel-settings" class="tab-panel">
<div class="panel-header">
<h2>Einstellungen</h2>
<button class="btn btn-primary" onclick="saveSettings()">Speichern &amp; Übernehmen</button>
</div>
<form id="settings-form" onsubmit="event.preventDefault(); saveSettings();">
<!-- VERWALTUNGS-WEBSERVER -->
<div class="settings-group">
<div class="group-header">Verwaltungs-Webserver (Dieses Admin-WebGUI)</div>
<div class="form-row">
<label>Admin-Port</label>
<input type="number" id="cfg-admin-port" min="1" max="65535" required />
<span class="field-hint">Standard: 5001. Bestimmt den Port, auf dem dieses Administrations-UI lauscht.</span>
</div>
<div class="form-row">
<label>Admin Bind-Adresse</label>
<input type="text" id="cfg-admin-host" required />
<span class="field-hint">'localhost' = nur lokal auf dieser Maschine; '0.0.0.0' = netzwerkweit erreichbar.</span>
</div>
<div class="form-row">
<label>Admin-Token</label>
<div class="input-with-action">
<input type="text" id="cfg-admin-token" required />
<button type="button" class="btn btn-sm btn-secondary" onclick="generateNewAdminToken()">Neu generieren</button>
</div>
<span class="field-hint">Schützt das gesamte Admin-WebGUI vor unbefugtem Zugriff. Bei Änderung muss neu angemeldet werden.</span>
</div>
</div>
<!-- ANALYSE-WEBSERVER -->
<div class="settings-group">
<div class="group-header">Analyse-Webserver (Öffentliche API &amp; Analyse-WebUI)</div>
<div class="form-row">
<label>Port</label>
<input type="number" id="cfg-web-port" min="1" max="65535" required />
</div>
<div class="form-row">
<label>Bind-Adresse</label>
<input type="text" id="cfg-web-host" required />
<span class="field-hint">'localhost' = lokal; '0.0.0.0' = von außen erreichbar.</span>
</div>
<div class="form-row">
<label>API-Token (Steuerbefehle)</label>
<input type="password" id="cfg-web-token" placeholder="••••••••" />
<span class="field-hint">Schützt schreibende API-Befehle (X-Predictalytics-Key).</span>
</div>
<div class="form-row">
<label>Datenbank-Debug</label>
<label class="checkbox-label">
<input type="checkbox" id="cfg-web-dbdebug" />
<span>Detaillierte Verbindungs- und SQL-Infos loggen</span>
</label>
</div>
</div>
<!-- DATENBANK -->
<div class="settings-group">
<div class="group-header">Datenbank (MySQL / MariaDB)</div>
<div class="form-row">
<label>Server</label>
<input type="text" id="cfg-db-server" required />
</div>
<div class="form-row">
<label>Datenbank</label>
<input type="text" id="cfg-db-name" required />
</div>
<div class="form-row">
<label>Benutzer</label>
<input type="text" id="cfg-db-user" required />
</div>
<div class="form-row">
<label>Passwort</label>
<input type="password" id="cfg-db-password" placeholder="••••••••" />
</div>
<div class="form-row">
<label>SSL-Modus</label>
<select id="cfg-db-ssl">
<option value="None">None (Unverschlüsselt)</option>
<option value="Preferred">Preferred (Empfohlen für entfernte Server)</option>
<option value="Required">Required (SSL erzwungen)</option>
</select>
</div>
</div>
<!-- DEPLOYMENT CENTER -->
<div class="settings-group">
<div class="group-header">Deployment Center</div>
<div class="form-row">
<label>Server-URL</label>
<input type="text" id="cfg-dc-url" readonly disabled class="input-disabled" />
</div>
<div class="form-row">
<label>API Token</label>
<input type="password" id="cfg-dc-token" placeholder="••••••••" />
</div>
<div class="form-row">
<label>Heartbeat aktiv</label>
<label class="checkbox-label">
<input type="checkbox" id="cfg-dc-heartbeat" />
<span>Dead-Man's-Switch Pings an Watchdog senden</span>
</label>
</div>
<div class="form-row">
<label>Monitor Source &amp; Instance</label>
<div class="input-dual">
<input type="text" id="cfg-dc-source" placeholder="Source (Predictalytics)" />
<input type="text" id="cfg-dc-instance" placeholder="Instance (default)" />
</div>
</div>
<div class="form-row">
<label>Intervall (Sekunden)</label>
<input type="number" id="cfg-dc-interval" min="15" max="3600" />
</div>
<div class="form-row">
<label>Fehler melden</label>
<label class="checkbox-label">
<input type="checkbox" id="cfg-dc-errorreporting" />
<span>Error/Fatal-Meldungen automatisch an das Deployment Center streamen</span>
</label>
</div>
<div class="form-row">
<label>Update-Prüfung</label>
<label class="checkbox-label">
<input type="checkbox" id="cfg-dc-updatecheck" />
<span>Beim Start auf neuere Versionen prüfen</span>
</label>
</div>
<div class="form-row">
<label>Update-Kanal</label>
<select id="cfg-dc-channel">
<option value="prod">prod (Produktion)</option>
<option value="beta">beta (Vorab-Versionen)</option>
<option value="dev">dev (Entwicklung)</option>
</select>
</div>
</div>
<!-- EGRESS -->
<div class="settings-group">
<div class="group-header">Egress (Proxies &amp; Quell-IPs)</div>
<div class="form-row">
<label>Egress-Kanäle</label>
<textarea id="cfg-egress-channels" rows="4" placeholder="prox-1|Proxy|http://user:pass@proxy:8080&#10;ip-1|SourceIp|192.168.1.100"></textarea>
<span class="field-hint">Format: id|type|value (Zeilengetrennt)</span>
</div>
</div>
<div class="form-submit-row">
<button type="submit" class="btn btn-primary btn-large">Einstellungen speichern &amp; übernehmen</button>
</div>
</form>
</section>
<!-- 6. LIZENZ & UPDATES -->
<section id="panel-license" class="tab-panel">
<div class="panel-header">
<h2>Lizenzstatus &amp; Deployment Center</h2>
<button class="btn btn-sm btn-secondary" onclick="checkForUpdates()">Auf Updates prüfen</button>
</div>
<div class="card-grid">
<div class="stat-card">
<div class="stat-title">Produkt &amp; Version</div>
<div class="stat-value" id="lic-product">—</div>
<p class="stat-desc" id="lic-version">Version: —</p>
</div>
<div class="stat-card">
<div class="stat-title">Hardware-Identifikation (HWID)</div>
<div class="stat-value font-mono" id="lic-hwid" style="font-size: 14px;">—</div>
<p class="stat-desc" id="lic-hwidsource">Quelle: —</p>
</div>
<div class="stat-card">
<div class="stat-title">Lizenzprüfung</div>
<div class="stat-value" id="lic-status">—</div>
<p class="stat-desc" id="lic-message">—</p>
</div>
</div>
<div id="update-banner" class="section-box" style="display: none; margin-top: 24px;">
<h3 id="update-banner-title">⬆️ Neues Update verfügbar</h3>
<div id="update-banner-body"></div>
</div>
</section>
</main>
</div>
</div>
<!-- ─── BESTÄTIGUNGS-MODAL FÜR NEUBERECHNUNG ─── -->
<div id="confirm-modal" class="modal-backdrop" style="display: none;">
<div class="modal-card">
<div class="modal-header">
<span class="modal-icon">⚠️</span>
<h3>Alle Trader neu berechnen</h3>
</div>
<div class="modal-body">
<p>Dies löscht alle <strong>abgeleiteten Analysedaten</strong> (Positionen, Tages-Snapshots, Kategorie-Statistiken) und markiert jeden Trader zur vollständigen Neuberechnung.</p>
<p>Rohdaten (Trades und Märkte) bleiben unberührt.</p>
<p>Danach muss der Server gestartet werden: Der Analytics-Worker baut jeden Trader mit der aktuellen Engine neu auf (kann je nach Datenbestand mehrere Stunden dauern).</p>
<p><strong>Möchten Sie den Vorgang wirklich starten?</strong></p>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" onclick="closeRecalculateModal()">Abbrechen</button>
<button class="btn btn-danger" onclick="executeRecalculateAll()">Ja, Daten zurücksetzen</button>
</div>
</div>
</div>
<!-- TOAST NOTIFICATION CONTAINER -->
<div id="toast-container" class="toast-container"></div>
<script src="js/admin.js"></script>
</body>
</html>