jtl-wafi-dashboard.py aktualisiert
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
JTL-WAFi Dashboard v3.0.0 - WebSocket Real-Time Dashboard
|
JTL-WAFi Dashboard v3.1.0 - WebSocket Real-Time Dashboard
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
- Bot Rate-Limiting: Bots nach Rate-Limit bannen
|
- Bot Rate-Limiting: Bots nach Rate-Limit bannen
|
||||||
@@ -11,8 +11,11 @@ Features:
|
|||||||
- PHP-FPM Restart: OPcache automatisch leeren
|
- PHP-FPM Restart: OPcache automatisch leeren
|
||||||
- Alle Agent/Shop-Daten im Memory
|
- Alle Agent/Shop-Daten im Memory
|
||||||
- DB nur für: Passwort, Tokens, Sessions
|
- DB nur für: Passwort, Tokens, Sessions
|
||||||
|
- IP Ban/Whitelist: IPs manuell bannen oder whitelisten
|
||||||
|
- Live Stats: Top IPs, Top Requests, Suspicious IPs
|
||||||
|
- Auto-Ban: Automatische IP-Bans bei verdächtigem Verhalten
|
||||||
|
|
||||||
v3.0.0: Auto-Update, FPM-Restart, Country-Detection Bugfix
|
v3.1.0: IP Ban/Whitelist, Enhanced Live Logs, Human/Bot Stats Split
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -36,7 +39,7 @@ import uvicorn
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# VERSION & CONFIG
|
# VERSION & CONFIG
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
VERSION = "3.0.0"
|
VERSION = "3.1.0"
|
||||||
|
|
||||||
DATA_DIR = "/var/lib/jtl-wafi"
|
DATA_DIR = "/var/lib/jtl-wafi"
|
||||||
SSL_DIR = "/var/lib/jtl-wafi/ssl"
|
SSL_DIR = "/var/lib/jtl-wafi/ssl"
|
||||||
@@ -1442,7 +1445,27 @@ def get_dashboard_html() -> str:
|
|||||||
.btn-icon:hover { border-color: var(--accent); color: var(--accent); }
|
.btn-icon:hover { border-color: var(--accent); color: var(--accent); }
|
||||||
.actions { display: flex; gap: 8px; align-items: center; }
|
.actions { display: flex; gap: 8px; align-items: center; }
|
||||||
.logs-panel { position: fixed; bottom: 0; left: 0; right: 0; background: var(--bg-secondary); border-top: 1px solid var(--border); height: 300px; transform: translateY(100%); transition: transform 0.3s ease; z-index: 200; }
|
.logs-panel { position: fixed; bottom: 0; left: 0; right: 0; background: var(--bg-secondary); border-top: 1px solid var(--border); height: 300px; transform: translateY(100%); transition: transform 0.3s ease; z-index: 200; }
|
||||||
|
.logs-panel.enhanced { height: 400px; }
|
||||||
.logs-panel.open { transform: translateY(0); }
|
.logs-panel.open { transform: translateY(0); }
|
||||||
|
.logs-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; height: calc(100% - 50px); padding: 12px; }
|
||||||
|
.logs-column { background: var(--card-bg); border-radius: 8px; padding: 8px; overflow-y: auto; }
|
||||||
|
.logs-column-header { font-size: 12px; font-weight: 600; color: var(--accent); margin-bottom: 8px; padding-bottom: 4px; border-bottom: 1px solid var(--border); }
|
||||||
|
.logs-feed .logs-content { height: calc(100% - 30px); overflow-y: auto; }
|
||||||
|
.top-ips-list, .suspicious-ips-list, .top-requests-list, .top-blocked-list { font-size: 11px; }
|
||||||
|
.ip-item, .request-item { display: flex; justify-content: space-between; align-items: center; padding: 4px 6px; border-radius: 4px; margin-bottom: 4px; background: var(--bg); }
|
||||||
|
.ip-item:hover { background: var(--border); }
|
||||||
|
.ip-item .ip-info { display: flex; flex-direction: column; gap: 2px; flex: 1; }
|
||||||
|
.ip-item .ip-addr { font-family: monospace; font-weight: 500; }
|
||||||
|
.ip-item .ip-meta { font-size: 10px; color: var(--text-secondary); }
|
||||||
|
.ip-item .ip-count { font-weight: 600; color: var(--accent); margin-right: 8px; }
|
||||||
|
.ip-item .ip-actions { display: flex; gap: 4px; }
|
||||||
|
.ip-item .ip-actions button { padding: 2px 6px; font-size: 10px; border-radius: 3px; border: none; cursor: pointer; }
|
||||||
|
.ip-item .btn-ban { background: var(--danger); color: white; }
|
||||||
|
.ip-item .btn-whitelist { background: var(--success); color: white; }
|
||||||
|
.suspicious-item { background: rgba(255,71,87,0.1); border-left: 3px solid var(--danger); }
|
||||||
|
.suspicious-item .ip-count { color: var(--danger); }
|
||||||
|
.request-item .request-path { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: monospace; }
|
||||||
|
.request-item .request-count { font-weight: 600; color: var(--accent); }
|
||||||
.logs-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; border-bottom: 1px solid var(--border); }
|
.logs-header { display: flex; justify-content: space-between; align-items: center; padding: 12px 20px; border-bottom: 1px solid var(--border); }
|
||||||
.logs-content { height: calc(100% - 48px); overflow-y: auto; padding: 12px 20px; font-family: monospace; font-size: 12px; line-height: 1.6; }
|
.logs-content { height: calc(100% - 48px); overflow-y: auto; padding: 12px 20px; font-family: monospace; font-size: 12px; line-height: 1.6; }
|
||||||
.log-entry { color: var(--text-secondary); }
|
.log-entry { color: var(--text-secondary); }
|
||||||
@@ -1482,12 +1505,12 @@ def get_dashboard_html() -> str:
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<div class="logo">🌍 JTL-<span>WAFi</span> <small style="font-size:11px;opacity:0.5">v3.0</small></div>
|
<div class="logo">🌍 JTL-<span>WAFi</span> <small style="font-size:11px;opacity:0.5">v3.1</small></div>
|
||||||
<div class="header-right">
|
<div class="header-right">
|
||||||
<div class="clock" id="clock">--:--:--</div>
|
<div class="clock" id="clock">--:--:--</div>
|
||||||
<div class="connection-status"><div class="status-dot" id="wsStatus"></div><span id="wsStatusText">Verbinde...</span></div>
|
<div class="connection-status"><div class="status-dot" id="wsStatus"></div><span id="wsStatusText">Verbinde...</span></div>
|
||||||
<div style="display:flex;align-items:center;gap:8px"><span style="font-size:12px;color:var(--text-secondary)">Update:</span><select id="updateInterval" onchange="setUpdateInterval(this.value)" style="padding:4px 8px;border-radius:4px;border:1px solid var(--border);background:var(--card-bg);color:var(--text);font-size:12px"><option value="2000">2s</option><option value="5000">5s</option><option value="10000" selected>10s</option><option value="15000">15s</option></select></div><div style="display:flex;align-items:center;gap:6px;padding:4px 8px;border-radius:4px;border:1px solid var(--border);background:var(--card-bg)"><label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;color:var(--text-secondary)" title="OPcache nach Aktivierung/Deaktivierung leeren"><input type="checkbox" id="autoFpmRestart" checked style="margin:0"><span>FPM-Restart</span></label></div>
|
<div style="display:flex;align-items:center;gap:8px"><span style="font-size:12px;color:var(--text-secondary)">Update:</span><select id="updateInterval" onchange="setUpdateInterval(this.value)" style="padding:4px 8px;border-radius:4px;border:1px solid var(--border);background:var(--card-bg);color:var(--text);font-size:12px"><option value="2000">2s</option><option value="5000">5s</option><option value="10000" selected>10s</option><option value="15000">15s</option></select></div><div style="display:flex;align-items:center;gap:6px;padding:4px 8px;border-radius:4px;border:1px solid var(--border);background:var(--card-bg)"><label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:12px;color:var(--text-secondary)" title="OPcache nach Aktivierung/Deaktivierung leeren"><input type="checkbox" id="autoFpmRestart" checked style="margin:0"><span>FPM-Restart</span></label></div>
|
||||||
<div class="update-dropdown" style="position:relative"><button class="btn-header" onclick="toggleUpdateDropdown()" id="updateDropdownBtn">🔄 v3.0</button><div id="updateDropdownMenu" style="display:none;position:absolute;right:0;top:100%;margin-top:4px;background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:8px 0;min-width:200px;z-index:1000;box-shadow:0 4px 12px rgba(0,0,0,0.3)"><button onclick="updateDashboard()" style="display:block;width:100%;text-align:left;padding:8px 16px;border:none;background:none;color:var(--text);cursor:pointer;font-size:13px" onmouseover="this.style.background='var(--bg)'" onmouseout="this.style.background='none'">📊 Dashboard updaten</button><button onclick="updateAgents()" style="display:block;width:100%;text-align:left;padding:8px 16px;border:none;background:none;color:var(--text);cursor:pointer;font-size:13px" onmouseover="this.style.background='var(--bg)'" onmouseout="this.style.background='none'">🖥️ Alle Agents updaten (<span id="agentCountDropdown">0</span>)</button></div></div>
|
<div class="update-dropdown" style="position:relative"><button class="btn-header" onclick="toggleUpdateDropdown()" id="updateDropdownBtn">🔄 v3.1</button><div id="updateDropdownMenu" style="display:none;position:absolute;right:0;top:100%;margin-top:4px;background:var(--card-bg);border:1px solid var(--border);border-radius:8px;padding:8px 0;min-width:200px;z-index:1000;box-shadow:0 4px 12px rgba(0,0,0,0.3)"><button onclick="updateDashboard()" style="display:block;width:100%;text-align:left;padding:8px 16px;border:none;background:none;color:var(--text);cursor:pointer;font-size:13px" onmouseover="this.style.background='var(--bg)'" onmouseout="this.style.background='none'">📊 Dashboard updaten</button><button onclick="updateAgents()" style="display:block;width:100%;text-align:left;padding:8px 16px;border:none;background:none;color:var(--text);cursor:pointer;font-size:13px" onmouseover="this.style.background='var(--bg)'" onmouseout="this.style.background='none'">🖥️ Alle Agents updaten (<span id="agentCountDropdown">0</span>)</button></div></div>
|
||||||
<div style="display:flex;gap:8px"><button class="btn-header" onclick="openPasswordModal()">🔑</button><a href="/logout" class="btn-header">Abmelden</a></div>
|
<div style="display:flex;gap:8px"><button class="btn-header" onclick="openPasswordModal()">🔑</button><a href="/logout" class="btn-header">Abmelden</a></div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -1497,8 +1520,8 @@ def get_dashboard_html() -> str:
|
|||||||
<div class="stat-card"><div class="stat-label">Shops Aktiv</div><div class="stat-value" id="statShops">0</div></div>
|
<div class="stat-card"><div class="stat-label">Shops Aktiv</div><div class="stat-value" id="statShops">0</div></div>
|
||||||
<div class="stat-card"><div class="stat-label">🛡️ Link11</div><div class="stat-value link11" id="statLink11">0</div></div>
|
<div class="stat-card"><div class="stat-label">🛡️ Link11</div><div class="stat-value link11" id="statLink11">0</div></div>
|
||||||
<div class="stat-card"><div class="stat-label">⚡ Direkt</div><div class="stat-value direct" id="statDirect">0</div></div>
|
<div class="stat-card"><div class="stat-label">⚡ Direkt</div><div class="stat-value direct" id="statDirect">0</div></div>
|
||||||
<div class="stat-card"><div class="stat-label">Requests/min</div><div class="stat-value" id="statReqMin">0</div></div>
|
<div class="stat-card"><div class="stat-label">Human/min</div><div class="stat-value success" id="statHumanRpm">0</div></div>
|
||||||
<div class="stat-card"><div class="stat-label">Aktive Bans</div><div class="stat-value warning" id="statBans">0</div></div>
|
<div class="stat-card"><div class="stat-label">Bot/min</div><div class="stat-value warning" id="statBotRpm">0</div></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="top-shops-card">
|
<div class="top-shops-card">
|
||||||
<div class="top-shops-header" onclick="openAllShopsModal()">
|
<div class="top-shops-header" onclick="openAllShopsModal()">
|
||||||
@@ -1525,13 +1548,46 @@ def get_dashboard_html() -> str:
|
|||||||
<table id="tableDirect"><thead><tr><th class="sortable" onclick="sortShops('direct','status')">Status<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('direct','domain')">Domain<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('direct','server')">Server<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('direct','modus')">Modus<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('direct','req')">Req/min<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('direct','bans')">Bans<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('direct','runtime')">Laufzeit<span class="sort-icon">⇅</span></th><th>Aktionen</th></tr></thead><tbody id="shopsDirectTable"></tbody></table>
|
<table id="tableDirect"><thead><tr><th class="sortable" onclick="sortShops('direct','status')">Status<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('direct','domain')">Domain<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('direct','server')">Server<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('direct','modus')">Modus<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('direct','req')">Req/min<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('direct','bans')">Bans<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('direct','runtime')">Laufzeit<span class="sort-icon">⇅</span></th><th>Aktionen</th></tr></thead><tbody id="shopsDirectTable"></tbody></table>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<div class="logs-panel" id="logsPanel"><div class="logs-header"><span>📜 Live Logs: <span id="logsShop">-</span></span><button class="btn btn-secondary" onclick="closeLogs()">✕</button></div><div class="logs-content" id="logsContent"></div></div>
|
<div class="logs-panel enhanced" id="logsPanel">
|
||||||
|
<div class="logs-header">
|
||||||
|
<span>📜 Live Logs: <span id="logsShop">-</span></span>
|
||||||
|
<div style="display:flex;gap:8px;align-items:center">
|
||||||
|
<label style="font-size:12px;color:var(--text-secondary)">Zeitfenster:</label>
|
||||||
|
<select id="statsWindowSelect" onchange="changeStatsWindow(this.value)" style="padding:4px 8px;border-radius:4px;border:1px solid var(--border);background:var(--card-bg);color:var(--text);font-size:12px">
|
||||||
|
<option value="5m" selected>5 min</option>
|
||||||
|
<option value="10m">10 min</option>
|
||||||
|
<option value="15m">15 min</option>
|
||||||
|
<option value="30m">30 min</option>
|
||||||
|
<option value="60m">60 min</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-secondary" onclick="closeLogs()">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="logs-grid">
|
||||||
|
<div class="logs-column logs-feed">
|
||||||
|
<div class="logs-column-header">📝 Live Feed</div>
|
||||||
|
<div class="logs-content" id="logsContent"></div>
|
||||||
|
</div>
|
||||||
|
<div class="logs-column logs-ips">
|
||||||
|
<div class="logs-column-header">🔝 Top IPs</div>
|
||||||
|
<div class="top-ips-list" id="topIpsList"></div>
|
||||||
|
<div class="logs-column-header" style="margin-top:12px">⚠️ Suspicious IPs</div>
|
||||||
|
<div class="suspicious-ips-list" id="suspiciousIpsList"></div>
|
||||||
|
</div>
|
||||||
|
<div class="logs-column logs-requests">
|
||||||
|
<div class="logs-column-header">📊 Top Requests</div>
|
||||||
|
<div class="top-requests-list" id="topRequestsList"></div>
|
||||||
|
<div class="logs-column-header" style="margin-top:12px">🚫 Top Blocked</div>
|
||||||
|
<div class="top-blocked-list" id="topBlockedList"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal-overlay" id="activateModal"><div class="modal"><h3 class="modal-title">🚀 Shop aktivieren (v2.5)</h3><form id="activateForm"><input type="hidden" name="domain" id="activateDomain"><div class="form-group"><label>Domain</label><input type="text" id="activateDomainDisplay" readonly></div><div style="border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:16px" id="monitorOnlyGroup"><div class="form-group" style="margin:0"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;justify-content:flex-start"><input type="checkbox" name="monitor_only" id="monitorOnlyCheck" onchange="toggleMonitorOnly()"> 🔍 Nur überwachen (kein Blocking)</label></div><small style="color:var(--text-secondary);display:block;margin-top:8px">Alle Anfragen werden protokolliert, aber nicht blockiert</small></div><div id="blockingOptions"><div style="border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:16px"><div class="form-group" style="margin-bottom:12px"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;justify-content:flex-start"><input type="checkbox" name="bot_mode" id="botModeCheck" checked onchange="toggleBotOptions()"> 🤖 Bot Rate-Limiting</label></div><div id="botOptionsGroup"><div class="form-group"><label>Bot Rate-Limit (Req/min pro Bot)</label><input type="number" name="bot_rate_limit" value="30" min="1"></div><div class="form-group"><label>Bot Ban-Dauer (Sekunden)</label><input type="number" name="bot_ban_duration" value="300" min="60"><small>300=5min</small></div></div></div><div style="border:1px solid var(--border);border-radius:8px;padding:16px"><div class="form-group" style="margin-bottom:12px"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;justify-content:flex-start"><input type="checkbox" name="country_mode" id="countryModeCheck" onchange="toggleCountryOptions()"> 🌍 Country Rate-Limiting</label></div><div id="countryOptionsGroup" style="display:none"><div class="form-group"><label>Country Rate-Limit (Req/min pro Land)</label><input type="number" name="country_rate_limit" value="100" min="1"></div><div class="form-group"><label>Country Ban-Dauer (Sekunden)</label><input type="number" name="country_ban_duration" value="600" min="60"><small>600=10min</small></div><div class="form-group"><label>Unlimitierte Länder</label><input type="text" name="unlimited_countries" value="de,at,ch" placeholder="de,at,ch,us,gb"><small>Kommagetrennte ISO-Codes (z.B. de,at,ch). Diese Länder werden nicht limitiert.</small></div><div class="form-group"><label>Presets:</label><button type="button" class="btn btn-secondary" onclick="setCountries('dach')">🇩🇪🇦🇹🇨🇭 DACH</button> <button type="button" class="btn btn-secondary" onclick="setCountries('eu_plus')">🇪🇺 EU+</button></div></div></div></div><div class="modal-actions"><button type="button" class="btn btn-secondary" onclick="closeModal('activateModal')">Abbrechen</button><button type="submit" class="btn btn-primary">Aktivieren</button></div></form></div></div>
|
<div class="modal-overlay" id="activateModal"><div class="modal"><h3 class="modal-title">🚀 Shop aktivieren (v2.5)</h3><form id="activateForm"><input type="hidden" name="domain" id="activateDomain"><div class="form-group"><label>Domain</label><input type="text" id="activateDomainDisplay" readonly></div><div style="border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:16px" id="monitorOnlyGroup"><div class="form-group" style="margin:0"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;justify-content:flex-start"><input type="checkbox" name="monitor_only" id="monitorOnlyCheck" onchange="toggleMonitorOnly()"> 🔍 Nur überwachen (kein Blocking)</label></div><small style="color:var(--text-secondary);display:block;margin-top:8px">Alle Anfragen werden protokolliert, aber nicht blockiert</small></div><div id="blockingOptions"><div style="border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:16px"><div class="form-group" style="margin-bottom:12px"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;justify-content:flex-start"><input type="checkbox" name="bot_mode" id="botModeCheck" checked onchange="toggleBotOptions()"> 🤖 Bot Rate-Limiting</label></div><div id="botOptionsGroup"><div class="form-group"><label>Bot Rate-Limit (Req/min pro Bot)</label><input type="number" name="bot_rate_limit" value="30" min="1"></div><div class="form-group"><label>Bot Ban-Dauer (Sekunden)</label><input type="number" name="bot_ban_duration" value="300" min="60"><small>300=5min</small></div></div></div><div style="border:1px solid var(--border);border-radius:8px;padding:16px"><div class="form-group" style="margin-bottom:12px"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;justify-content:flex-start"><input type="checkbox" name="country_mode" id="countryModeCheck" onchange="toggleCountryOptions()"> 🌍 Country Rate-Limiting</label></div><div id="countryOptionsGroup" style="display:none"><div class="form-group"><label>Country Rate-Limit (Req/min pro Land)</label><input type="number" name="country_rate_limit" value="100" min="1"></div><div class="form-group"><label>Country Ban-Dauer (Sekunden)</label><input type="number" name="country_ban_duration" value="600" min="60"><small>600=10min</small></div><div class="form-group"><label>Unlimitierte Länder</label><input type="text" name="unlimited_countries" value="de,at,ch" placeholder="de,at,ch,us,gb"><small>Kommagetrennte ISO-Codes (z.B. de,at,ch). Diese Länder werden nicht limitiert.</small></div><div class="form-group"><label>Presets:</label><button type="button" class="btn btn-secondary" onclick="setCountries('dach')">🇩🇪🇦🇹🇨🇭 DACH</button> <button type="button" class="btn btn-secondary" onclick="setCountries('eu_plus')">🇪🇺 EU+</button></div></div></div></div><div class="modal-actions"><button type="button" class="btn btn-secondary" onclick="closeModal('activateModal')">Abbrechen</button><button type="submit" class="btn btn-primary">Aktivieren</button></div></form></div></div>
|
||||||
<div class="modal-overlay" id="bulkActivateModal"><div class="modal"><h3 class="modal-title">⚡ Massenaktivierung (v2.5)</h3><form id="bulkActivateForm"><div style="border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:16px" id="bulkMonitorOnlyGroup"><div class="form-group" style="margin:0"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;justify-content:flex-start"><input type="checkbox" name="monitor_only" id="bulkMonitorOnlyCheck" onchange="toggleBulkMonitorOnly()"> 🔍 Nur überwachen (kein Blocking)</label></div></div><div id="bulkBlockingOptions"><div style="border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:16px"><div class="form-group" style="margin-bottom:12px"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;justify-content:flex-start"><input type="checkbox" name="bot_mode" id="bulkBotModeCheck" checked onchange="toggleBulkBotOptions()"> 🤖 Bot Rate-Limiting</label></div><div id="bulkBotOptionsGroup"><div class="form-group"><label>Bot Rate-Limit (Req/min)</label><input type="number" name="bot_rate_limit" value="30" min="1"></div><div class="form-group"><label>Bot Ban-Dauer (Sek)</label><input type="number" name="bot_ban_duration" value="300" min="60"><small>300=5min</small></div></div></div><div style="border:1px solid var(--border);border-radius:8px;padding:16px"><div class="form-group" style="margin-bottom:12px"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;justify-content:flex-start"><input type="checkbox" name="country_mode" id="bulkCountryModeCheck" onchange="toggleBulkCountryOptions()"> 🌍 Country Rate-Limiting</label></div><div id="bulkCountryOptionsGroup" style="display:none"><div class="form-group"><label>Country Rate-Limit (Req/min)</label><input type="number" name="country_rate_limit" value="100" min="1"></div><div class="form-group"><label>Country Ban-Dauer (Sek)</label><input type="number" name="country_ban_duration" value="600" min="60"><small>600=10min</small></div><div class="form-group"><label>Unlimitierte Länder</label><input type="text" name="unlimited_countries" value="de,at,ch" placeholder="de,at,ch"><small>Kommagetrennte ISO-Codes</small></div><div class="form-group"><label>Presets:</label><button type="button" class="btn btn-secondary" onclick="setBulkCountries('dach')">🇩🇪🇦🇹🇨🇭 DACH</button> <button type="button" class="btn btn-secondary" onclick="setBulkCountries('eu_plus')">🇪🇺 EU+</button></div></div></div></div><div class="form-group" style="margin-top:16px"><label>Filter</label><select name="filter_type"><option value="all">Alle inaktiven</option><option value="direct">Nur Direkte ⚡</option><option value="link11">Nur Link11 🛡️</option></select></div><div class="modal-actions"><button type="button" class="btn btn-secondary" onclick="closeModal('bulkActivateModal')">Abbrechen</button><button type="submit" class="btn btn-success">▶️ Aktivieren</button></div></form></div></div>
|
<div class="modal-overlay" id="bulkActivateModal"><div class="modal"><h3 class="modal-title">⚡ Massenaktivierung (v2.5)</h3><form id="bulkActivateForm"><div style="border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:16px" id="bulkMonitorOnlyGroup"><div class="form-group" style="margin:0"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;justify-content:flex-start"><input type="checkbox" name="monitor_only" id="bulkMonitorOnlyCheck" onchange="toggleBulkMonitorOnly()"> 🔍 Nur überwachen (kein Blocking)</label></div></div><div id="bulkBlockingOptions"><div style="border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:16px"><div class="form-group" style="margin-bottom:12px"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;justify-content:flex-start"><input type="checkbox" name="bot_mode" id="bulkBotModeCheck" checked onchange="toggleBulkBotOptions()"> 🤖 Bot Rate-Limiting</label></div><div id="bulkBotOptionsGroup"><div class="form-group"><label>Bot Rate-Limit (Req/min)</label><input type="number" name="bot_rate_limit" value="30" min="1"></div><div class="form-group"><label>Bot Ban-Dauer (Sek)</label><input type="number" name="bot_ban_duration" value="300" min="60"><small>300=5min</small></div></div></div><div style="border:1px solid var(--border);border-radius:8px;padding:16px"><div class="form-group" style="margin-bottom:12px"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;justify-content:flex-start"><input type="checkbox" name="country_mode" id="bulkCountryModeCheck" onchange="toggleBulkCountryOptions()"> 🌍 Country Rate-Limiting</label></div><div id="bulkCountryOptionsGroup" style="display:none"><div class="form-group"><label>Country Rate-Limit (Req/min)</label><input type="number" name="country_rate_limit" value="100" min="1"></div><div class="form-group"><label>Country Ban-Dauer (Sek)</label><input type="number" name="country_ban_duration" value="600" min="60"><small>600=10min</small></div><div class="form-group"><label>Unlimitierte Länder</label><input type="text" name="unlimited_countries" value="de,at,ch" placeholder="de,at,ch"><small>Kommagetrennte ISO-Codes</small></div><div class="form-group"><label>Presets:</label><button type="button" class="btn btn-secondary" onclick="setBulkCountries('dach')">🇩🇪🇦🇹🇨🇭 DACH</button> <button type="button" class="btn btn-secondary" onclick="setBulkCountries('eu_plus')">🇪🇺 EU+</button></div></div></div></div><div class="form-group" style="margin-top:16px"><label>Filter</label><select name="filter_type"><option value="all">Alle inaktiven</option><option value="direct">Nur Direkte ⚡</option><option value="link11">Nur Link11 🛡️</option></select></div><div class="modal-actions"><button type="button" class="btn btn-secondary" onclick="closeModal('bulkActivateModal')">Abbrechen</button><button type="submit" class="btn btn-success">▶️ Aktivieren</button></div></form></div></div>
|
||||||
<div class="modal-overlay" id="bulkDeactivateModal"><div class="modal"><h3 class="modal-title">⏹️ Massendeaktivierung</h3><form id="bulkDeactivateForm"><div class="form-group"><label>Filter</label><select name="filter_type"><option value="all">Alle aktiven</option><option value="direct">Nur Direkte ⚡</option><option value="link11">Nur Link11 🛡️</option></select></div><div class="modal-actions"><button type="button" class="btn btn-secondary" onclick="closeModal('bulkDeactivateModal')">Abbrechen</button><button type="submit" class="btn btn-danger">⏹️ Deaktivieren</button></div></form></div></div>
|
<div class="modal-overlay" id="bulkDeactivateModal"><div class="modal"><h3 class="modal-title">⏹️ Massendeaktivierung</h3><form id="bulkDeactivateForm"><div class="form-group"><label>Filter</label><select name="filter_type"><option value="all">Alle aktiven</option><option value="direct">Nur Direkte ⚡</option><option value="link11">Nur Link11 🛡️</option></select></div><div class="modal-actions"><button type="button" class="btn btn-secondary" onclick="closeModal('bulkDeactivateModal')">Abbrechen</button><button type="submit" class="btn btn-danger">⏹️ Deaktivieren</button></div></form></div></div>
|
||||||
<div class="modal-overlay" id="passwordModal"><div class="modal"><h3 class="modal-title">🔑 Passwort ändern</h3><form id="passwordForm"><div class="form-group"><label>Aktuelles Passwort</label><input type="password" name="current" required></div><div class="form-group"><label>Neues Passwort</label><input type="password" name="new_pw" required minlength="8"></div><div class="form-group"><label>Bestätigen</label><input type="password" name="confirm" required></div><div class="modal-actions"><button type="button" class="btn btn-secondary" onclick="closeModal('passwordModal')">Abbrechen</button><button type="submit" class="btn btn-primary">Speichern</button></div></form></div></div>
|
<div class="modal-overlay" id="passwordModal"><div class="modal"><h3 class="modal-title">🔑 Passwort ändern</h3><form id="passwordForm"><div class="form-group"><label>Aktuelles Passwort</label><input type="password" name="current" required></div><div class="form-group"><label>Neues Passwort</label><input type="password" name="new_pw" required minlength="8"></div><div class="form-group"><label>Bestätigen</label><input type="password" name="confirm" required></div><div class="modal-actions"><button type="button" class="btn btn-secondary" onclick="closeModal('passwordModal')">Abbrechen</button><button type="submit" class="btn btn-primary">Speichern</button></div></form></div></div>
|
||||||
<div class="modal-overlay" id="allShopsModal"><div class="modal xlarge"><h3 class="modal-title"><span>📊 Alle Shops</span><button class="btn btn-secondary" onclick="closeModal('allShopsModal')">✕</button></h3><div style="margin-bottom:16px"><button class="btn" id="sortByReq" onclick="sortAllShops('req_per_min')">Nach Requests</button> <button class="btn btn-secondary" id="sortByBans" onclick="sortAllShops('active_bans')">Nach Bans</button></div><table><thead><tr><th>#</th><th>Domain</th><th>Server</th><th>Status</th><th>Req/min</th><th>Bans</th><th>Typ</th></tr></thead><tbody id="allShopsTable"></tbody></table></div></div>
|
<div class="modal-overlay" id="allShopsModal"><div class="modal xlarge"><h3 class="modal-title"><span>📊 Alle Shops</span><button class="btn btn-secondary" onclick="closeModal('allShopsModal')">✕</button></h3><div style="margin-bottom:16px"><button class="btn" id="sortByReq" onclick="sortAllShops('req_per_min')">Nach Requests</button> <button class="btn btn-secondary" id="sortByBans" onclick="sortAllShops('active_bans')">Nach Bans</button></div><table><thead><tr><th>#</th><th>Domain</th><th>Server</th><th>Status</th><th>Req/min</th><th>Bans</th><th>Typ</th></tr></thead><tbody id="allShopsTable"></tbody></table></div></div>
|
||||||
<div class="modal-overlay" id="detailModal"><div class="modal large"><h3 class="modal-title"><a href="#" id="detailDomainLink" target="_blank" style="color:var(--accent);text-decoration:none" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'"><span id="detailDomain">-</span> 🔗</a><button class="btn btn-secondary" onclick="closeModal('detailModal')">✕</button></h3><div style="color:var(--text-secondary);margin:-8px 0 16px 0;font-size:13px">Server: <span id="detailServer">-</span></div><div class="detail-grid"><div class="detail-card"><div class="detail-card-label">Status</div><div class="detail-card-value" id="detailStatus">-</div></div><div class="detail-card"><div class="detail-card-label">Modus</div><div class="detail-card-value" id="detailMode">-</div></div><div class="detail-card"><div class="detail-card-label">Unlimitiert</div><div class="detail-card-value" id="detailRegion">-</div></div><div class="detail-card"><div class="detail-card-label">Rate-Limit</div><div class="detail-card-value" id="detailRateLimit">-</div></div><div class="detail-card"><div class="detail-card-label">Ban-Dauer</div><div class="detail-card-value" id="detailBanDuration">-</div></div><div class="detail-card"><div class="detail-card-label">Laufzeit</div><div class="detail-card-value" id="detailRuntime">-</div></div></div><div class="detail-grid"><div class="detail-card"><div class="detail-card-label">Req/min</div><div class="detail-card-value" id="detailReqMin">0</div></div><div class="detail-card"><div class="detail-card-label">Aktive Bans</div><div class="detail-card-value" id="detailActiveBans">0</div></div><div class="detail-card"><div class="detail-card-label">Total Bans</div><div class="detail-card-value" id="detailTotalBans">0</div></div></div><div class="detail-section" id="detailActions"><div class="detail-section-title">⚙️ Steuerung</div><div style="display:flex;flex-wrap:wrap;gap:8px;padding:12px;background:var(--card-bg);border-radius:8px;align-items:center"><button class="btn btn-danger" id="detailBtnDeactivate" onclick="detailDeactivate()">⏹️ Deaktivieren</button><div style="border-left:1px solid var(--border);margin:0 8px;height:24px"></div><span style="color:var(--text-secondary);font-size:13px">Wechseln zu:</span><button class="btn" onclick="detailSwitchMode('bot-monitor')">🔍 Monitor</button><button class="btn" onclick="toggleRateLimitForm()">🤖 Bot-Limit ▾</button><button class="btn" onclick="detailSwitchMode('country-dach')">🇩🇪🇦🇹🇨🇭 DACH</button><button class="btn" onclick="detailSwitchMode('country-eu')">🇪🇺 EU+</button></div><div id="rateLimitFormArea" style="display:none;margin-top:12px;padding:12px;background:var(--card-bg);border-radius:8px;border:1px solid var(--border)"><div style="display:flex;gap:16px;align-items:flex-end;flex-wrap:wrap"><div><label style="display:block;font-size:12px;color:var(--text-secondary);margin-bottom:4px">Rate-Limit (Req/min)</label><input type="number" id="detailRateLimitInput" value="30" min="1" max="1000" style="width:100px;padding:8px;border-radius:4px;border:1px solid var(--border);background:var(--bg);color:var(--text)"></div><div><label style="display:block;font-size:12px;color:var(--text-secondary);margin-bottom:4px">Ban-Dauer</label><select id="detailBanDurationInput" style="padding:8px;border-radius:4px;border:1px solid var(--border);background:var(--bg);color:var(--text)"><option value="60">1 Minute</option><option value="300" selected>5 Minuten</option><option value="600">10 Minuten</option><option value="1800">30 Minuten</option><option value="3600">1 Stunde</option><option value="86400">24 Stunden</option></select></div><button class="btn btn-primary" onclick="applyRateLimit()">✓ Anwenden</button><button class="btn btn-secondary" onclick="toggleRateLimitForm()">Abbrechen</button></div></div></div><div class="detail-section"><div class="detail-section-title">📈 Bot-Aktivität über Zeit</div><div class="chart-container"><canvas id="requestChart"></canvas></div><div class="chart-legend" id="chartLegend"></div></div><div class="detail-section"><div class="detail-section-title">🌍 Country-Aktivität über Zeit</div><div class="chart-container"><canvas id="countryChart"></canvas></div><div class="chart-legend" id="countryChartLegend"></div></div><div class="detail-section"><div class="detail-section-title">🤖 Top Bots</div><div class="bot-list" id="detailTopBots"></div></div><div class="detail-section"><div class="detail-section-title">🌍 Top Countries</div><div class="bot-list" id="detailTopCountries"></div></div><div class="detail-section"><div class="detail-section-title">🚫 Aktuell gebannt</div><div class="bot-list" id="detailBannedBots"></div></div></div></div>
|
<div class="modal-overlay" id="detailModal"><div class="modal large"><h3 class="modal-title"><a href="#" id="detailDomainLink" target="_blank" style="color:var(--accent);text-decoration:none" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'"><span id="detailDomain">-</span> 🔗</a><button class="btn btn-secondary" onclick="closeModal('detailModal')">✕</button></h3><div style="color:var(--text-secondary);margin:-8px 0 16px 0;font-size:13px">Server: <span id="detailServer">-</span></div><div class="detail-grid"><div class="detail-card"><div class="detail-card-label">Status</div><div class="detail-card-value" id="detailStatus">-</div></div><div class="detail-card"><div class="detail-card-label">Modus</div><div class="detail-card-value" id="detailMode">-</div></div><div class="detail-card"><div class="detail-card-label">Unlimitiert</div><div class="detail-card-value" id="detailRegion">-</div></div><div class="detail-card"><div class="detail-card-label">Rate-Limit</div><div class="detail-card-value" id="detailRateLimit">-</div></div><div class="detail-card"><div class="detail-card-label">Ban-Dauer</div><div class="detail-card-value" id="detailBanDuration">-</div></div><div class="detail-card"><div class="detail-card-label">Laufzeit</div><div class="detail-card-value" id="detailRuntime">-</div></div></div><div class="detail-grid"><div class="detail-card"><div class="detail-card-label">Human/min</div><div class="detail-card-value success" id="detailHumanRpm">0</div></div><div class="detail-card"><div class="detail-card-label">Bot/min</div><div class="detail-card-value warning" id="detailBotRpm">0</div></div><div class="detail-card"><div class="detail-card-label">Aktive Bans</div><div class="detail-card-value" id="detailActiveBans">0</div></div><div class="detail-card"><div class="detail-card-label">Total Bans</div><div class="detail-card-value" id="detailTotalBans">0</div></div></div><div class="detail-section" id="detailActions"><div class="detail-section-title">⚙️ Steuerung</div><div style="display:flex;flex-wrap:wrap;gap:8px;padding:12px;background:var(--card-bg);border-radius:8px;align-items:center"><button class="btn btn-danger" id="detailBtnDeactivate" onclick="detailDeactivate()">⏹️ Deaktivieren</button><div style="border-left:1px solid var(--border);margin:0 8px;height:24px"></div><span style="color:var(--text-secondary);font-size:13px">Wechseln zu:</span><button class="btn" onclick="detailSwitchMode('bot-monitor')">🔍 Monitor</button><button class="btn" onclick="toggleRateLimitForm()">🤖 Bot-Limit ▾</button><button class="btn" onclick="detailSwitchMode('country-dach')">🇩🇪🇦🇹🇨🇭 DACH</button><button class="btn" onclick="detailSwitchMode('country-eu')">🇪🇺 EU+</button></div><div id="rateLimitFormArea" style="display:none;margin-top:12px;padding:12px;background:var(--card-bg);border-radius:8px;border:1px solid var(--border)"><div style="display:flex;gap:16px;align-items:flex-end;flex-wrap:wrap"><div><label style="display:block;font-size:12px;color:var(--text-secondary);margin-bottom:4px">Rate-Limit (Req/min)</label><input type="number" id="detailRateLimitInput" value="30" min="1" max="1000" style="width:100px;padding:8px;border-radius:4px;border:1px solid var(--border);background:var(--bg);color:var(--text)"></div><div><label style="display:block;font-size:12px;color:var(--text-secondary);margin-bottom:4px">Ban-Dauer</label><select id="detailBanDurationInput" style="padding:8px;border-radius:4px;border:1px solid var(--border);background:var(--bg);color:var(--text)"><option value="60">1 Minute</option><option value="300" selected>5 Minuten</option><option value="600">10 Minuten</option><option value="1800">30 Minuten</option><option value="3600">1 Stunde</option><option value="86400">24 Stunden</option></select></div><button class="btn btn-primary" onclick="applyRateLimit()">✓ Anwenden</button><button class="btn btn-secondary" onclick="toggleRateLimitForm()">Abbrechen</button></div></div></div><div class="detail-section"><div class="detail-section-title">📈 Bot-Aktivität über Zeit</div><div class="chart-container"><canvas id="requestChart"></canvas></div><div class="chart-legend" id="chartLegend"></div></div><div class="detail-section"><div class="detail-section-title">🌍 Country-Aktivität über Zeit</div><div class="chart-container"><canvas id="countryChart"></canvas></div><div class="chart-legend" id="countryChartLegend"></div></div><div class="detail-section"><div class="detail-section-title">🤖 Top Bots</div><div class="bot-list" id="detailTopBots"></div></div><div class="detail-section"><div class="detail-section-title">🌍 Top Countries</div><div class="bot-list" id="detailTopCountries"></div></div><div class="detail-section"><div class="detail-section-title">🚫 Aktuell gebannt</div><div class="bot-list" id="detailBannedBots"></div></div></div></div>
|
||||||
<div class="toast-container" id="toastContainer"></div>
|
<div class="toast-container" id="toastContainer"></div>
|
||||||
<script>
|
<script>
|
||||||
let ws=null,agents={},shops={},currentLogsShop=null,currentSortBy='req_per_min',currentDetailShop=null;
|
let ws=null,agents={},shops={},currentLogsShop=null,currentSortBy='req_per_min',currentDetailShop=null;
|
||||||
@@ -1549,13 +1605,13 @@ def get_dashboard_html() -> str:
|
|||||||
function updateClock(){document.getElementById('clock').textContent=new Date().toLocaleTimeString('de-DE');}
|
function updateClock(){document.getElementById('clock').textContent=new Date().toLocaleTimeString('de-DE');}
|
||||||
setInterval(updateClock,1000);updateClock();
|
setInterval(updateClock,1000);updateClock();
|
||||||
function connect(){const p=location.protocol==='https:'?'wss:':'ws:';ws=new WebSocket(p+'//'+location.host+'/ws/dashboard');ws.onopen=()=>{document.getElementById('wsStatus').classList.add('connected');document.getElementById('wsStatusText').textContent='Verbunden';};ws.onclose=()=>{document.getElementById('wsStatus').classList.remove('connected');document.getElementById('wsStatusText').textContent='Getrennt';setTimeout(connect,3000);};ws.onmessage=e=>handleMessage(JSON.parse(e.data));}
|
function connect(){const p=location.protocol==='https:'?'wss:':'ws:';ws=new WebSocket(p+'//'+location.host+'/ws/dashboard');ws.onopen=()=>{document.getElementById('wsStatus').classList.add('connected');document.getElementById('wsStatusText').textContent='Verbunden';};ws.onclose=()=>{document.getElementById('wsStatus').classList.remove('connected');document.getElementById('wsStatusText').textContent='Getrennt';setTimeout(connect,3000);};ws.onmessage=e=>handleMessage(JSON.parse(e.data));}
|
||||||
function handleMessage(msg){switch(msg.type){case'initial_state':case'refresh':agents={};msg.data.agents.forEach(a=>agents[a.id]=a);shops={};msg.data.shops.forEach(s=>shops[s.domain]=s);updateStats(msg.data.stats);renderAgents();renderShops();renderTopShops();break;case'agent.online':case'agent.update':case'agent.pending':const a=msg.data;if(!agents[a.agent_id])agents[a.agent_id]={};Object.assign(agents[a.agent_id],{id:a.agent_id,hostname:a.hostname,status:a.status||'online',approved:a.approved,shops_total:a.shops_summary?.total||0,shops_active:a.shops_summary?.active||0,load_1m:a.system?.load_1m,memory_percent:a.system?.memory_percent,last_seen:new Date().toISOString()});scheduleRender();break;case'agent.offline':if(agents[msg.data.agent_id]){agents[msg.data.agent_id].status='offline';scheduleRender();}break;case'agent.approved':if(agents[msg.data.agent_id]){agents[msg.data.agent_id].status='online';agents[msg.data.agent_id].approved=true;scheduleRender();toast('Agent freigegeben','success');}break;case'shop.full_update':msg.data.shops.forEach(s=>{shops[s.domain]={...s,agent_id:msg.data.agent_id,agent_hostname:msg.data.hostname};});scheduleRender();break;case'shop.stats':if(shops[msg.data.domain]){shops[msg.data.domain].stats=msg.data.stats;scheduleRender();}break;case'shop_history':updateBotChart(msg.data);updateCountryChart(msg.data);break;case'top_shops':case'all_shops_sorted':renderAllShopsTable(msg.data.shops,msg.data.sort_by);break;case'log.entry':if(msg.data.shop===currentLogsShop)addLogEntry(msg.data.line);break;case'bot.banned':toast('🚫 '+msg.data.bot_name+' gebannt','warning');break;case'command.result':toast(msg.data.message,msg.data.status==='success'?'success':'error');break;}}
|
function handleMessage(msg){switch(msg.type){case'initial_state':case'refresh':agents={};msg.data.agents.forEach(a=>agents[a.id]=a);shops={};msg.data.shops.forEach(s=>shops[s.domain]=s);updateStats(msg.data.stats);renderAgents();renderShops();renderTopShops();break;case'agent.online':case'agent.update':case'agent.pending':const a=msg.data;if(!agents[a.agent_id])agents[a.agent_id]={};Object.assign(agents[a.agent_id],{id:a.agent_id,hostname:a.hostname,status:a.status||'online',approved:a.approved,shops_total:a.shops_summary?.total||0,shops_active:a.shops_summary?.active||0,load_1m:a.system?.load_1m,memory_percent:a.system?.memory_percent,last_seen:new Date().toISOString()});scheduleRender();break;case'agent.offline':if(agents[msg.data.agent_id]){agents[msg.data.agent_id].status='offline';scheduleRender();}break;case'agent.approved':if(agents[msg.data.agent_id]){agents[msg.data.agent_id].status='online';agents[msg.data.agent_id].approved=true;scheduleRender();toast('Agent freigegeben','success');}break;case'shop.full_update':msg.data.shops.forEach(s=>{shops[s.domain]={...s,agent_id:msg.data.agent_id,agent_hostname:msg.data.hostname};});scheduleRender();break;case'shop.stats':if(shops[msg.data.domain]){shops[msg.data.domain].stats=msg.data.stats;scheduleRender();}break;case'shop_history':updateBotChart(msg.data);updateCountryChart(msg.data);break;case'top_shops':case'all_shops_sorted':renderAllShopsTable(msg.data.shops,msg.data.sort_by);break;case'log.entry':if(msg.data.shop===currentLogsShop)addLogEntry(msg.data.line);break;case'bot.banned':toast('🚫 '+msg.data.bot_name+' gebannt','warning');break;case'command.result':toast(msg.data.message,msg.data.status==='success'?'success':'error');break;case'livestats.result':renderLiveStats(msg.data);break;}}
|
||||||
function renderAgents(){const t=document.getElementById('agentsTable'),l=getSortedAgents();document.getElementById('agentCount').textContent=l.length+' Agents';t.innerHTML=l.map(a=>'<tr><td><span class="status-badge status-'+(a.status||'offline')+'">'+(a.status==='online'?'🟢':a.status==='pending'?'🟡':'🔴')+' '+(a.status==='pending'?'Warte':a.status==='online'?'Online':'Offline')+'</span></td><td><strong>'+a.hostname+'</strong></td><td>'+(a.shops_active||0)+'/'+(a.shops_total||0)+'</td><td>'+(a.load_1m!=null?a.load_1m.toFixed(2):'-')+'</td><td>'+(a.memory_percent!=null?a.memory_percent.toFixed(1)+'%':'-')+'</td><td>'+(a.last_seen?formatTime(a.last_seen):'-')+'</td><td>'+(a.status==='pending'?'<button class="btn btn-primary" onclick="approveAgent(\\''+a.id+'\\')">✓</button>':'')+'</td></tr>').join('');}
|
function renderAgents(){const t=document.getElementById('agentsTable'),l=getSortedAgents();document.getElementById('agentCount').textContent=l.length+' Agents';t.innerHTML=l.map(a=>'<tr><td><span class="status-badge status-'+(a.status||'offline')+'">'+(a.status==='online'?'🟢':a.status==='pending'?'🟡':'🔴')+' '+(a.status==='pending'?'Warte':a.status==='online'?'Online':'Offline')+'</span></td><td><strong>'+a.hostname+'</strong></td><td>'+(a.shops_active||0)+'/'+(a.shops_total||0)+'</td><td>'+(a.load_1m!=null?a.load_1m.toFixed(2):'-')+'</td><td>'+(a.memory_percent!=null?a.memory_percent.toFixed(1)+'%':'-')+'</td><td>'+(a.last_seen?formatTime(a.last_seen):'-')+'</td><td>'+(a.status==='pending'?'<button class="btn btn-primary" onclick="approveAgent(\\''+a.id+'\\')">✓</button>':'')+'</td></tr>').join('');}
|
||||||
function renderShops(){const all=Object.values(shops);let l11=all.filter(s=>s.link11),dir=all.filter(s=>!s.link11);l11=getSortedShops(l11,'link11');dir=getSortedShops(dir,'direct');document.getElementById('link11Count').textContent=l11.length+' Shops';document.getElementById('directCount').textContent=dir.length+' Shops';document.getElementById('shopsLink11Table').innerHTML=renderShopRows(l11);document.getElementById('shopsDirectTable').innerHTML=renderShopRows(dir);}
|
function renderShops(){const all=Object.values(shops);let l11=all.filter(s=>s.link11),dir=all.filter(s=>!s.link11);l11=getSortedShops(l11,'link11');dir=getSortedShops(dir,'direct');document.getElementById('link11Count').textContent=l11.length+' Shops';document.getElementById('directCount').textContent=dir.length+' Shops';document.getElementById('shopsLink11Table').innerHTML=renderShopRows(l11);document.getElementById('shopsDirectTable').innerHTML=renderShopRows(dir);}
|
||||||
function renderShopRows(l){return l.map(s=>{const modeStr=s.monitor_only?'🔍 Monitor':((s.bot_mode?'🤖':'')+(s.country_mode?' 🌍':''))||'-';const bansStr=s.monitor_only?'-':((s.stats?.active_bot_bans||0)+(s.stats?.active_country_bans||0));return '<tr><td><span class="status-badge status-'+(s.status||'inactive')+'">'+(s.status==='active'?'✅':'⭕')+'</span></td><td><span class="domain-link" onclick="openDetailModal(\\''+s.domain+'\\')">'+s.domain+'</span></td><td>'+(s.agent_hostname||'-')+'</td><td>'+modeStr+'</td><td>'+((s.stats?.req_per_min||0).toFixed(1))+'</td><td>'+bansStr+'</td><td>'+formatRuntime(s.runtime_minutes)+'</td><td class="actions"><a href="https://'+s.domain+'" target="_blank" class="btn-icon">🔗</a>'+(s.status==='active'?'<button class="btn-icon" onclick="openLogs(\\''+s.domain+'\\')">📜</button><button class="btn btn-danger" onclick="deactivateShop(\\''+s.domain+'\\')">Stop</button>':'<button class="btn btn-primary" onclick="openActivateModal(\\''+s.domain+'\\')">Start</button>')+'</td></tr>';}).join('');}
|
function renderShopRows(l){return l.map(s=>{const modeStr=s.monitor_only?'🔍 Monitor':((s.bot_mode?'🤖':'')+(s.country_mode?' 🌍':''))||'-';const bansStr=s.monitor_only?'-':((s.stats?.active_bot_bans||0)+(s.stats?.active_country_bans||0));return '<tr><td><span class="status-badge status-'+(s.status||'inactive')+'">'+(s.status==='active'?'✅':'⭕')+'</span></td><td><span class="domain-link" onclick="openDetailModal(\\''+s.domain+'\\')">'+s.domain+'</span></td><td>'+(s.agent_hostname||'-')+'</td><td>'+modeStr+'</td><td>'+((s.stats?.req_per_min||0).toFixed(1))+'</td><td>'+bansStr+'</td><td>'+formatRuntime(s.runtime_minutes)+'</td><td class="actions"><a href="https://'+s.domain+'" target="_blank" class="btn-icon">🔗</a>'+(s.status==='active'?'<button class="btn-icon" onclick="openLogs(\\''+s.domain+'\\')">📜</button><button class="btn btn-danger" onclick="deactivateShop(\\''+s.domain+'\\')">Stop</button>':'<button class="btn btn-primary" onclick="openActivateModal(\\''+s.domain+'\\')">Start</button>')+'</td></tr>';}).join('');}
|
||||||
function renderTopShops(){const l=Object.values(shops).filter(s=>s.status==='active').sort((a,b)=>(b.stats?.req_per_min||0)-(a.stats?.req_per_min||0)).slice(0,10);document.getElementById('topShopsList').innerHTML=l.map(s=>{const bans=(s.stats?.active_bot_bans||0)+(s.stats?.active_country_bans||0);return '<div class="top-shop-item" onclick="openDetailModal(\\''+s.domain+'\\')"><div class="top-shop-domain">'+s.domain+'</div><div class="top-shop-stats"><span class="top-shop-req">'+((s.stats?.req_per_min||0).toFixed(1))+' req/m</span><span class="top-shop-bans">'+bans+' bans</span></div></div>';}).join('')||'<div style="color:var(--text-secondary)">Keine aktiven Shops</div>';}
|
function renderTopShops(){const l=Object.values(shops).filter(s=>s.status==='active').sort((a,b)=>(b.stats?.req_per_min||0)-(a.stats?.req_per_min||0)).slice(0,10);document.getElementById('topShopsList').innerHTML=l.map(s=>{const bans=(s.stats?.active_bot_bans||0)+(s.stats?.active_country_bans||0);return '<div class="top-shop-item" onclick="openDetailModal(\\''+s.domain+'\\')"><div class="top-shop-domain">'+s.domain+'</div><div class="top-shop-stats"><span class="top-shop-req">'+((s.stats?.req_per_min||0).toFixed(1))+' req/m</span><span class="top-shop-bans">'+bans+' bans</span></div></div>';}).join('')||'<div style="color:var(--text-secondary)">Keine aktiven Shops</div>';}
|
||||||
function updateStats(s){document.getElementById('statAgents').textContent=s.agents_online||0;document.getElementById('statShops').textContent=s.shops_active||0;document.getElementById('statLink11').textContent=s.shops_link11||0;document.getElementById('statDirect').textContent=s.shops_direct||0;document.getElementById('statReqMin').textContent=(s.req_per_min||0).toFixed(1);document.getElementById('statBans').textContent=s.active_bans||0;}
|
function updateStats(s){document.getElementById('statAgents').textContent=s.agents_online||0;document.getElementById('statShops').textContent=s.shops_active||0;document.getElementById('statLink11').textContent=s.shops_link11||0;document.getElementById('statDirect').textContent=s.shops_direct||0;document.getElementById('statHumanRpm').textContent=(s.human_rpm||0).toFixed(1);document.getElementById('statBotRpm').textContent=(s.bot_rpm||0).toFixed(1);}
|
||||||
function refreshStats(){const l=Object.values(shops),a=l.filter(s=>s.status==='active');document.getElementById('statShops').textContent=a.length;document.getElementById('statLink11').textContent=l.filter(s=>s.link11).length;document.getElementById('statDirect').textContent=l.filter(s=>!s.link11).length;document.getElementById('statReqMin').textContent=a.reduce((sum,s)=>sum+(s.stats?.req_per_min||0),0).toFixed(1);document.getElementById('statBans').textContent=a.reduce((sum,s)=>sum+((s.stats?.active_bot_bans||0)+(s.stats?.active_country_bans||0)),0);}
|
function refreshStats(){const l=Object.values(shops),a=l.filter(s=>s.status==='active');document.getElementById('statShops').textContent=a.length;document.getElementById('statLink11').textContent=l.filter(s=>s.link11).length;document.getElementById('statDirect').textContent=l.filter(s=>!s.link11).length;document.getElementById('statHumanRpm').textContent=a.reduce((sum,s)=>sum+(s.stats?.human_rpm||0),0).toFixed(1);document.getElementById('statBotRpm').textContent=a.reduce((sum,s)=>sum+(s.stats?.bot_rpm||0),0).toFixed(1);}
|
||||||
async function approveAgent(id){await fetch('/api/agents/'+id+'/approve',{method:'POST'});}
|
async function approveAgent(id){await fetch('/api/agents/'+id+'/approve',{method:'POST'});}
|
||||||
function openActivateModal(d){document.getElementById('activateDomain').value=d;document.getElementById('activateDomainDisplay').value=d;document.getElementById('monitorOnlyCheck').checked=false;document.getElementById('botModeCheck').checked=true;document.getElementById('countryModeCheck').checked=false;toggleMonitorOnly();toggleBotOptions();toggleCountryOptions();document.getElementById('activateModal').classList.add('open');}
|
function openActivateModal(d){document.getElementById('activateDomain').value=d;document.getElementById('activateDomainDisplay').value=d;document.getElementById('monitorOnlyCheck').checked=false;document.getElementById('botModeCheck').checked=true;document.getElementById('countryModeCheck').checked=false;toggleMonitorOnly();toggleBotOptions();toggleCountryOptions();document.getElementById('activateModal').classList.add('open');}
|
||||||
function closeModal(id){document.getElementById(id).classList.remove('open');}
|
function closeModal(id){document.getElementById(id).classList.remove('open');}
|
||||||
@@ -1578,11 +1634,20 @@ def get_dashboard_html() -> str:
|
|||||||
function openAllShopsModal(){document.getElementById('allShopsModal').classList.add('open');sortAllShops('req_per_min');}
|
function openAllShopsModal(){document.getElementById('allShopsModal').classList.add('open');sortAllShops('req_per_min');}
|
||||||
function sortAllShops(by){currentSortBy=by;document.getElementById('sortByReq').className='btn'+(by==='req_per_min'?' btn-primary':' btn-secondary');document.getElementById('sortByBans').className='btn'+(by==='active_bans'?' btn-primary':' btn-secondary');if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'get_all_shops_sorted',data:{sort_by:by}}));}
|
function sortAllShops(by){currentSortBy=by;document.getElementById('sortByReq').className='btn'+(by==='req_per_min'?' btn-primary':' btn-secondary');document.getElementById('sortByBans').className='btn'+(by==='active_bans'?' btn-primary':' btn-secondary');if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'get_all_shops_sorted',data:{sort_by:by}}));}
|
||||||
function renderAllShopsTable(l,by){document.getElementById('allShopsTable').innerHTML=l.map((s,i)=>{const bans=(s.active_bot_bans||0)+(s.active_country_bans||0);return '<tr onclick="openDetailModal(\\''+s.domain+'\\')" style="cursor:pointer"><td>'+(i+1)+'</td><td><strong>'+s.domain+'</strong></td><td>'+(s.agent_hostname||'-')+'</td><td><span class="status-badge status-'+(s.status||'inactive')+'">'+(s.status==='active'?'✅':'⭕')+'</span></td><td style="'+(by==='req_per_min'?'color:var(--accent);font-weight:600':'')+'">'+(s.req_per_min||0).toFixed(1)+'</td><td style="'+(by==='active_bans'?'color:var(--warning);font-weight:600':'')+'">'+bans+'</td><td>'+(s.link11?'🛡️':'⚡')+'</td></tr>';}).join('');}
|
function renderAllShopsTable(l,by){document.getElementById('allShopsTable').innerHTML=l.map((s,i)=>{const bans=(s.active_bot_bans||0)+(s.active_country_bans||0);return '<tr onclick="openDetailModal(\\''+s.domain+'\\')" style="cursor:pointer"><td>'+(i+1)+'</td><td><strong>'+s.domain+'</strong></td><td>'+(s.agent_hostname||'-')+'</td><td><span class="status-badge status-'+(s.status||'inactive')+'">'+(s.status==='active'?'✅':'⭕')+'</span></td><td style="'+(by==='req_per_min'?'color:var(--accent);font-weight:600':'')+'">'+(s.req_per_min||0).toFixed(1)+'</td><td style="'+(by==='active_bans'?'color:var(--warning);font-weight:600':'')+'">'+bans+'</td><td>'+(s.link11?'🛡️':'⚡')+'</td></tr>';}).join('');}
|
||||||
function openDetailModal(d){currentDetailShop=d;const s=shops[d];if(!s)return;document.getElementById('detailDomain').textContent=d;document.getElementById('detailDomainLink').href='https://'+d;document.getElementById('detailServer').textContent=s.agent_hostname||'-';document.getElementById('detailStatus').textContent=s.status==='active'?'✅ Aktiv':'⭕ Inaktiv';const modeStr=s.monitor_only?'🔍 Monitor':((s.bot_mode?'🤖 Bot':'')+(s.country_mode?' 🌍 Country':''))||'-';document.getElementById('detailMode').textContent=modeStr;const unlimitedStr=s.unlimited_countries&&s.unlimited_countries.length>0?s.unlimited_countries.map(c=>c.toUpperCase()).join(', '):(s.country_mode?'(keine)':'-');document.getElementById('detailRegion').textContent=unlimitedStr;const rlStr=s.monitor_only?'- (Monitor)':(s.bot_mode&&s.bot_rate_limit?'Bot:'+s.bot_rate_limit+'/min':'')+(s.country_mode&&s.country_rate_limit?(s.bot_mode?' | ':'')+'Country:'+s.country_rate_limit+'/min':'')||'-';document.getElementById('detailRateLimit').textContent=rlStr;const bdStr=s.monitor_only?'- (Monitor)':(s.bot_mode&&s.bot_ban_duration?'Bot:'+(s.bot_ban_duration>=60?Math.round(s.bot_ban_duration/60)+'m':s.bot_ban_duration+'s'):'')+(s.country_mode&&s.country_ban_duration?(s.bot_mode?' | ':'')+'Country:'+(s.country_ban_duration>=60?Math.round(s.country_ban_duration/60)+'m':s.country_ban_duration+'s'):'')||'-';document.getElementById('detailBanDuration').textContent=bdStr;document.getElementById('detailRuntime').textContent=formatRuntime(s.runtime_minutes);const st=s.stats||{};document.getElementById('detailReqMin').textContent=(st.req_per_min||0).toFixed(1);const activeBans=s.monitor_only?'-':((st.active_bot_bans||0)+(st.active_country_bans||0));document.getElementById('detailActiveBans').textContent=activeBans;const totalBans=s.monitor_only?'-':((st.bot_bans||0)+(st.country_bans||0));document.getElementById('detailTotalBans').textContent=totalBans;document.getElementById('detailTopBots').innerHTML=Object.entries(st.top_bots||{}).sort((a,b)=>b[1]-a[1]).map(([n,c])=>'<div class="bot-item"><span>'+n+'</span><span>'+c+'</span></div>').join('')||'<div style="color:var(--text-secondary);padding:8px">Keine Daten</div>';document.getElementById('detailTopCountries').innerHTML=Object.entries(st.top_countries||{}).sort((a,b)=>b[1]-a[1]).map(([n,c])=>'<div class="bot-item"><span>'+n.toUpperCase()+'</span><span>'+c+'</span></div>').join('')||'<div style="color:var(--text-secondary);padding:8px">Keine Daten</div>';const bannedList=(st.banned_bots||[]).concat(st.banned_countries||[]);document.getElementById('detailBannedBots').innerHTML=s.monitor_only?'<div style="color:var(--text-secondary);padding:8px">Monitor-Only (keine Bans)</div>':bannedList.map(n=>'<div class="bot-item"><span>🚫 '+n+'</span></div>').join('')||'<div style="color:var(--text-secondary);padding:8px">Keine Bans</div>';document.getElementById('detailBtnDeactivate').style.display=s.status==='active'?'inline-block':'none';document.getElementById('rateLimitFormArea').style.display='none';document.getElementById('detailRateLimitInput').value=s.bot_rate_limit||30;const bd=s.bot_ban_duration||300;document.getElementById('detailBanDurationInput').value=[60,300,600,1800,3600,86400].includes(bd)?bd:300;document.getElementById('chartLegend').innerHTML='';document.getElementById('countryChartLegend').innerHTML='';const cv=document.getElementById('requestChart');cv.getContext('2d').clearRect(0,0,cv.width,cv.height);const ccv=document.getElementById('countryChart');ccv.getContext('2d').clearRect(0,0,ccv.width,ccv.height);if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'get_shop_history',data:{domain:d}}));document.getElementById('detailModal').classList.add('open');}
|
function openDetailModal(d){currentDetailShop=d;const s=shops[d];if(!s)return;document.getElementById('detailDomain').textContent=d;document.getElementById('detailDomainLink').href='https://'+d;document.getElementById('detailServer').textContent=s.agent_hostname||'-';document.getElementById('detailStatus').textContent=s.status==='active'?'✅ Aktiv':'⭕ Inaktiv';const modeStr=s.monitor_only?'🔍 Monitor':((s.bot_mode?'🤖 Bot':'')+(s.country_mode?' 🌍 Country':''))||'-';document.getElementById('detailMode').textContent=modeStr;const unlimitedStr=s.unlimited_countries&&s.unlimited_countries.length>0?s.unlimited_countries.map(c=>c.toUpperCase()).join(', '):(s.country_mode?'(keine)':'-');document.getElementById('detailRegion').textContent=unlimitedStr;const rlStr=s.monitor_only?'- (Monitor)':(s.bot_mode&&s.bot_rate_limit?'Bot:'+s.bot_rate_limit+'/min':'')+(s.country_mode&&s.country_rate_limit?(s.bot_mode?' | ':'')+'Country:'+s.country_rate_limit+'/min':'')||'-';document.getElementById('detailRateLimit').textContent=rlStr;const bdStr=s.monitor_only?'- (Monitor)':(s.bot_mode&&s.bot_ban_duration?'Bot:'+(s.bot_ban_duration>=60?Math.round(s.bot_ban_duration/60)+'m':s.bot_ban_duration+'s'):'')+(s.country_mode&&s.country_ban_duration?(s.bot_mode?' | ':'')+'Country:'+(s.country_ban_duration>=60?Math.round(s.country_ban_duration/60)+'m':s.country_ban_duration+'s'):'')||'-';document.getElementById('detailBanDuration').textContent=bdStr;document.getElementById('detailRuntime').textContent=formatRuntime(s.runtime_minutes);const st=s.stats||{};document.getElementById('detailHumanRpm').textContent=(st.human_rpm||0).toFixed(1);document.getElementById('detailBotRpm').textContent=(st.bot_rpm||0).toFixed(1);const activeBans=s.monitor_only?'-':((st.active_bot_bans||0)+(st.active_country_bans||0));document.getElementById('detailActiveBans').textContent=activeBans;const totalBans=s.monitor_only?'-':((st.bot_bans||0)+(st.country_bans||0));document.getElementById('detailTotalBans').textContent=totalBans;document.getElementById('detailTopBots').innerHTML=Object.entries(st.top_bots||{}).sort((a,b)=>b[1]-a[1]).map(([n,c])=>'<div class="bot-item"><span>'+n+'</span><span>'+c+'</span></div>').join('')||'<div style="color:var(--text-secondary);padding:8px">Keine Daten</div>';document.getElementById('detailTopCountries').innerHTML=Object.entries(st.top_countries||{}).sort((a,b)=>b[1]-a[1]).map(([n,c])=>'<div class="bot-item"><span>'+n.toUpperCase()+'</span><span>'+c+'</span></div>').join('')||'<div style="color:var(--text-secondary);padding:8px">Keine Daten</div>';const bannedList=(st.banned_bots||[]).concat(st.banned_countries||[]);document.getElementById('detailBannedBots').innerHTML=s.monitor_only?'<div style="color:var(--text-secondary);padding:8px">Monitor-Only (keine Bans)</div>':bannedList.map(n=>'<div class="bot-item"><span>🚫 '+n+'</span></div>').join('')||'<div style="color:var(--text-secondary);padding:8px">Keine Bans</div>';document.getElementById('detailBtnDeactivate').style.display=s.status==='active'?'inline-block':'none';document.getElementById('rateLimitFormArea').style.display='none';document.getElementById('detailRateLimitInput').value=s.bot_rate_limit||30;const bd=s.bot_ban_duration||300;document.getElementById('detailBanDurationInput').value=[60,300,600,1800,3600,86400].includes(bd)?bd:300;document.getElementById('chartLegend').innerHTML='';document.getElementById('countryChartLegend').innerHTML='';const cv=document.getElementById('requestChart');cv.getContext('2d').clearRect(0,0,cv.width,cv.height);const ccv=document.getElementById('countryChart');ccv.getContext('2d').clearRect(0,0,ccv.width,ccv.height);if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'get_shop_history',data:{domain:d}}));document.getElementById('detailModal').classList.add('open');}
|
||||||
function updateBotChart(data){const cv=document.getElementById('requestChart'),ctx=cv.getContext('2d'),ct=cv.parentElement,w=ct.clientWidth-32,h=230;cv.width=w;cv.height=h;ctx.clearRect(0,0,w,h);const bh=data.bot_history||{},bn=Object.keys(bh).slice(0,10);if(bn.length===0){ctx.fillStyle='#a0a0b0';ctx.font='14px sans-serif';ctx.fillText('Noch keine Bot-Daten',w/2-60,h/2);return;}let ts=new Set();bn.forEach(b=>bh[b].forEach(p=>ts.add(p.timestamp)));ts=[...ts].sort();if(ts.length<2){ctx.fillStyle='#a0a0b0';ctx.font='14px sans-serif';ctx.fillText('Warte auf Daten...',w/2-50,h/2);return;}let mx=1;bn.forEach(b=>bh[b].forEach(p=>{if(p.count>mx)mx=p.count;}));const pd={t:20,r:20,b:40,l:50},cW=w-pd.l-pd.r,cH=h-pd.t-pd.b;ctx.strokeStyle='rgba(255,255,255,0.1)';ctx.lineWidth=1;for(let i=0;i<=4;i++){const y=pd.t+(cH/4)*i;ctx.beginPath();ctx.moveTo(pd.l,y);ctx.lineTo(w-pd.r,y);ctx.stroke();ctx.fillStyle='#a0a0b0';ctx.font='10px sans-serif';ctx.fillText(Math.round(mx-(mx/4)*i),5,y+4);}const step=Math.max(1,Math.floor(ts.length/6));ctx.fillStyle='#a0a0b0';ctx.font='10px sans-serif';for(let i=0;i<ts.length;i+=step){const t=ts[i].split(' ')[1]?.substring(0,5)||ts[i],x=pd.l+(cW/(ts.length-1))*i;ctx.fillText(t,x-15,h-10);}const lg=document.getElementById('chartLegend');lg.innerHTML='';bn.forEach((bot,idx)=>{const c=BOT_COLORS[idx%BOT_COLORS.length],pts=bh[bot];ctx.strokeStyle=c;ctx.lineWidth=2;ctx.beginPath();pts.forEach((p,i)=>{const ti=ts.indexOf(p.timestamp),x=pd.l+(cW/(ts.length-1))*ti,y=pd.t+cH-(p.count/mx)*cH;i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});ctx.stroke();lg.innerHTML+='<div class="legend-item"><div class="legend-color" style="background:'+c+'"></div><span>'+bot+'</span></div>';});}
|
function updateBotChart(data){const cv=document.getElementById('requestChart'),ctx=cv.getContext('2d'),ct=cv.parentElement,w=ct.clientWidth-32,h=230;cv.width=w;cv.height=h;ctx.clearRect(0,0,w,h);const bh=data.bot_history||{},bn=Object.keys(bh).slice(0,10);if(bn.length===0){ctx.fillStyle='#a0a0b0';ctx.font='14px sans-serif';ctx.fillText('Noch keine Bot-Daten',w/2-60,h/2);return;}let ts=new Set();bn.forEach(b=>bh[b].forEach(p=>ts.add(p.timestamp)));ts=[...ts].sort();if(ts.length<2){ctx.fillStyle='#a0a0b0';ctx.font='14px sans-serif';ctx.fillText('Warte auf Daten...',w/2-50,h/2);return;}let mx=1;bn.forEach(b=>bh[b].forEach(p=>{if(p.count>mx)mx=p.count;}));const pd={t:20,r:20,b:40,l:50},cW=w-pd.l-pd.r,cH=h-pd.t-pd.b;ctx.strokeStyle='rgba(255,255,255,0.1)';ctx.lineWidth=1;for(let i=0;i<=4;i++){const y=pd.t+(cH/4)*i;ctx.beginPath();ctx.moveTo(pd.l,y);ctx.lineTo(w-pd.r,y);ctx.stroke();ctx.fillStyle='#a0a0b0';ctx.font='10px sans-serif';ctx.fillText(Math.round(mx-(mx/4)*i),5,y+4);}const step=Math.max(1,Math.floor(ts.length/6));ctx.fillStyle='#a0a0b0';ctx.font='10px sans-serif';for(let i=0;i<ts.length;i+=step){const t=ts[i].split(' ')[1]?.substring(0,5)||ts[i],x=pd.l+(cW/(ts.length-1))*i;ctx.fillText(t,x-15,h-10);}const lg=document.getElementById('chartLegend');lg.innerHTML='';bn.forEach((bot,idx)=>{const c=BOT_COLORS[idx%BOT_COLORS.length],pts=bh[bot];ctx.strokeStyle=c;ctx.lineWidth=2;ctx.beginPath();pts.forEach((p,i)=>{const ti=ts.indexOf(p.timestamp),x=pd.l+(cW/(ts.length-1))*ti,y=pd.t+cH-(p.count/mx)*cH;i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});ctx.stroke();lg.innerHTML+='<div class="legend-item"><div class="legend-color" style="background:'+c+'"></div><span>'+bot+'</span></div>';});}
|
||||||
function updateCountryChart(data){const cv=document.getElementById('countryChart'),ctx=cv.getContext('2d'),ct=cv.parentElement,w=ct.clientWidth-32,h=230;cv.width=w;cv.height=h;ctx.clearRect(0,0,w,h);const ch=data.country_history||{},cn=Object.keys(ch).slice(0,10);if(cn.length===0){ctx.fillStyle='#a0a0b0';ctx.font='14px sans-serif';ctx.fillText('Noch keine Country-Daten',w/2-70,h/2);return;}let ts=new Set();cn.forEach(c=>ch[c].forEach(p=>ts.add(p.timestamp)));ts=[...ts].sort();if(ts.length<2){ctx.fillStyle='#a0a0b0';ctx.font='14px sans-serif';ctx.fillText('Warte auf Daten...',w/2-50,h/2);return;}let mx=1;cn.forEach(c=>ch[c].forEach(p=>{if(p.count>mx)mx=p.count;}));const pd={t:20,r:20,b:40,l:50},cW=w-pd.l-pd.r,cH=h-pd.t-pd.b;ctx.strokeStyle='rgba(255,255,255,0.1)';ctx.lineWidth=1;for(let i=0;i<=4;i++){const y=pd.t+(cH/4)*i;ctx.beginPath();ctx.moveTo(pd.l,y);ctx.lineTo(w-pd.r,y);ctx.stroke();ctx.fillStyle='#a0a0b0';ctx.font='10px sans-serif';ctx.fillText(Math.round(mx-(mx/4)*i),5,y+4);}const step=Math.max(1,Math.floor(ts.length/6));ctx.fillStyle='#a0a0b0';ctx.font='10px sans-serif';for(let i=0;i<ts.length;i+=step){const t=ts[i].split(' ')[1]?.substring(0,5)||ts[i],x=pd.l+(cW/(ts.length-1))*i;ctx.fillText(t,x-15,h-10);}const lg=document.getElementById('countryChartLegend');lg.innerHTML='';cn.forEach((country,idx)=>{const clr=BOT_COLORS[idx%BOT_COLORS.length],pts=ch[country];ctx.strokeStyle=clr;ctx.lineWidth=2;ctx.beginPath();pts.forEach((p,i)=>{const ti=ts.indexOf(p.timestamp),x=pd.l+(cW/(ts.length-1))*ti,y=pd.t+cH-(p.count/mx)*cH;i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});ctx.stroke();lg.innerHTML+='<div class="legend-item"><div class="legend-color" style="background:'+clr+'"></div><span>'+country.toUpperCase()+'</span></div>';});}
|
function updateCountryChart(data){const cv=document.getElementById('countryChart'),ctx=cv.getContext('2d'),ct=cv.parentElement,w=ct.clientWidth-32,h=230;cv.width=w;cv.height=h;ctx.clearRect(0,0,w,h);const ch=data.country_history||{},cn=Object.keys(ch).slice(0,10);if(cn.length===0){ctx.fillStyle='#a0a0b0';ctx.font='14px sans-serif';ctx.fillText('Noch keine Country-Daten',w/2-70,h/2);return;}let ts=new Set();cn.forEach(c=>ch[c].forEach(p=>ts.add(p.timestamp)));ts=[...ts].sort();if(ts.length<2){ctx.fillStyle='#a0a0b0';ctx.font='14px sans-serif';ctx.fillText('Warte auf Daten...',w/2-50,h/2);return;}let mx=1;cn.forEach(c=>ch[c].forEach(p=>{if(p.count>mx)mx=p.count;}));const pd={t:20,r:20,b:40,l:50},cW=w-pd.l-pd.r,cH=h-pd.t-pd.b;ctx.strokeStyle='rgba(255,255,255,0.1)';ctx.lineWidth=1;for(let i=0;i<=4;i++){const y=pd.t+(cH/4)*i;ctx.beginPath();ctx.moveTo(pd.l,y);ctx.lineTo(w-pd.r,y);ctx.stroke();ctx.fillStyle='#a0a0b0';ctx.font='10px sans-serif';ctx.fillText(Math.round(mx-(mx/4)*i),5,y+4);}const step=Math.max(1,Math.floor(ts.length/6));ctx.fillStyle='#a0a0b0';ctx.font='10px sans-serif';for(let i=0;i<ts.length;i+=step){const t=ts[i].split(' ')[1]?.substring(0,5)||ts[i],x=pd.l+(cW/(ts.length-1))*i;ctx.fillText(t,x-15,h-10);}const lg=document.getElementById('countryChartLegend');lg.innerHTML='';cn.forEach((country,idx)=>{const clr=BOT_COLORS[idx%BOT_COLORS.length],pts=ch[country];ctx.strokeStyle=clr;ctx.lineWidth=2;ctx.beginPath();pts.forEach((p,i)=>{const ti=ts.indexOf(p.timestamp),x=pd.l+(cW/(ts.length-1))*ti,y=pd.t+cH-(p.count/mx)*cH;i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});ctx.stroke();lg.innerHTML+='<div class="legend-item"><div class="legend-color" style="background:'+clr+'"></div><span>'+country.toUpperCase()+'</span></div>';});}
|
||||||
function openLogs(d){currentLogsShop=d;document.getElementById('logsShop').textContent=d;document.getElementById('logsContent').innerHTML='<div style="color:#666">Warte auf Logs...</div>';document.getElementById('logsPanel').classList.add('open');if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'log.subscribe',data:{shop:d}}));}
|
let currentStatsWindow='5m',liveStatsInterval=null;
|
||||||
function closeLogs(){if(currentLogsShop&&ws&&ws.readyState===1)ws.send(JSON.stringify({type:'log.unsubscribe',data:{shop:currentLogsShop}}));currentLogsShop=null;document.getElementById('logsPanel').classList.remove('open');}
|
function openLogs(d){currentLogsShop=d;document.getElementById('logsShop').textContent=d;document.getElementById('logsContent').innerHTML='<div style="color:#666">Warte auf Logs...</div>';document.getElementById('topIpsList').innerHTML='<div style="color:#666;padding:8px">Lade...</div>';document.getElementById('suspiciousIpsList').innerHTML='<div style="color:#666;padding:8px">Lade...</div>';document.getElementById('topRequestsList').innerHTML='<div style="color:#666;padding:8px">Lade...</div>';document.getElementById('topBlockedList').innerHTML='<div style="color:#666;padding:8px">Lade...</div>';document.getElementById('logsPanel').classList.add('open');if(ws&&ws.readyState===1){ws.send(JSON.stringify({type:'log.subscribe',data:{shop:d}}));requestLiveStats();}if(liveStatsInterval)clearInterval(liveStatsInterval);liveStatsInterval=setInterval(requestLiveStats,5000);}
|
||||||
|
function closeLogs(){if(currentLogsShop&&ws&&ws.readyState===1)ws.send(JSON.stringify({type:'log.unsubscribe',data:{shop:currentLogsShop}}));currentLogsShop=null;document.getElementById('logsPanel').classList.remove('open');if(liveStatsInterval){clearInterval(liveStatsInterval);liveStatsInterval=null;}}
|
||||||
|
function changeStatsWindow(w){currentStatsWindow=w;requestLiveStats();}
|
||||||
|
function requestLiveStats(){if(!currentLogsShop||!ws||ws.readyState!==1)return;ws.send(JSON.stringify({type:'command.livestats',data:{shop:currentLogsShop,window:currentStatsWindow}}));}
|
||||||
|
function renderLiveStats(data){if(!data.success||!data.stats)return;const st=data.stats;document.getElementById('topIpsList').innerHTML=renderTopIps(st.top_ips||[]);document.getElementById('suspiciousIpsList').innerHTML=renderSuspiciousIps(st.suspicious_ips||[]);document.getElementById('topRequestsList').innerHTML=renderRequests(st.top_requests||[]);document.getElementById('topBlockedList').innerHTML=renderRequests(st.top_blocked||[],'blocked');}
|
||||||
|
function renderTopIps(ips){if(!ips.length)return '<div style="color:#666;padding:8px">Keine Daten</div>';return ips.slice(0,10).map(ip=>'<div class="ip-item"><div class="ip-info"><span class="ip-addr">'+ip.ip+'</span><span class="ip-meta">'+ip.country+' | '+(ip.org||ip.asn||'-')+'</span></div><span class="ip-count">'+ip.count+'x</span><div class="ip-actions"><button class="btn-ban" onclick="quickBan(\\''+ip.ip+'\\')">🚫</button><button class="btn-whitelist" onclick="quickWhitelist(\\''+ip.ip+'\\')">✓</button></div></div>').join('');}
|
||||||
|
function renderSuspiciousIps(ips){if(!ips.length)return '<div style="color:#666;padding:8px">Keine verdächtigen IPs</div>';return ips.slice(0,5).map(ip=>'<div class="ip-item suspicious-item"><div class="ip-info"><span class="ip-addr">'+ip.ip+'</span><span class="ip-meta">'+ip.country+' | '+ip.reason+(ip.errors?' | '+ip.errors+' Errors':'')+'</span></div><span class="ip-count">'+ip.count+'x</span><div class="ip-actions"><button class="btn-ban" onclick="quickBan(\\''+ip.ip+'\\')">🚫</button></div></div>').join('');}
|
||||||
|
function renderRequests(reqs,type){if(!reqs.length)return '<div style="color:#666;padding:8px">Keine Daten</div>';return reqs.slice(0,10).map(r=>'<div class="request-item'+(type==='blocked'?' blocked':'')+'"><span class="request-path" title="'+r.path+'">'+r.path+'</span><span class="request-count">'+r.count+'x</span></div>').join('');}
|
||||||
|
function quickBan(ip){if(!currentLogsShop)return;const duration=prompt('Ban-Dauer auswählen:\\n\\n900 = 15 Minuten\\n3600 = 1 Stunde\\n21600 = 6 Stunden\\n86400 = 24 Stunden\\n604800 = 7 Tage\\n-1 = Permanent','3600');if(duration===null)return;ws.send(JSON.stringify({type:'command.ban',data:{shop:currentLogsShop,ip:ip,duration:parseInt(duration),reason:'Manual ban from dashboard'}}));toast('IP '+ip+' wird gebannt...','success');}
|
||||||
|
function quickWhitelist(ip){if(!currentLogsShop)return;const desc=prompt('Beschreibung (optional):','');if(desc===null)return;ws.send(JSON.stringify({type:'command.whitelist',data:{shop:currentLogsShop,ip:ip,description:desc}}));toast('IP '+ip+' wird gewhitelisted...','success');}
|
||||||
function addLogEntry(line){const c=document.getElementById('logsContent');if(c.querySelector('div[style*="color:#666"]'))c.innerHTML='';const e=document.createElement('div');e.className='log-entry'+(line.includes('BANNED')?' banned':'');e.textContent=line;c.insertBefore(e,c.firstChild);while(c.children.length>100)c.removeChild(c.lastChild);}
|
function addLogEntry(line){const c=document.getElementById('logsContent');if(c.querySelector('div[style*="color:#666"]'))c.innerHTML='';const e=document.createElement('div');e.className='log-entry'+(line.includes('BANNED')?' banned':'');e.textContent=line;c.insertBefore(e,c.firstChild);while(c.children.length>100)c.removeChild(c.lastChild);}
|
||||||
function formatTime(iso){return new Date(iso).toLocaleTimeString('de-DE');}
|
function formatTime(iso){return new Date(iso).toLocaleTimeString('de-DE');}
|
||||||
function formatRuntime(m){if(!m||m<=0)return'-';if(m<60)return Math.round(m)+'m';const h=m/60;if(h<24)return Math.round(h)+'h';return Math.round(h/24)+'d';}
|
function formatRuntime(m){if(!m||m<=0)return'-';if(m<60)return Math.round(m)+'m';const h=m/60;if(h<24)return Math.round(h)+'h';return Math.round(h/24)+'d';}
|
||||||
|
|||||||
Reference in New Issue
Block a user