jtl-wafi-dashboard.py aktualisiert

This commit is contained in:
2025-12-30 11:57:13 +01:00
parent 76636e182c
commit b6a82f8097

View File

@@ -1359,6 +1359,7 @@ def get_dashboard_html() -> str:
<div class="header-right">
<div class="clock" id="clock">--:--:--</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;gap:8px"><button class="btn-header" onclick="openPasswordModal()">🔑</button><a href="/logout" class="btn-header">Abmelden</a></div>
</div>
</header>
@@ -1397,8 +1398,8 @@ def get_dashboard_html() -> str:
</div>
</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="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 class="form-group" id="monitorOnlyGroup"><label style="display:flex;align-items:center;gap:8px;cursor:pointer"><input type="checkbox" name="monitor_only" id="monitorOnlyCheck" onchange="toggleMonitorOnly()"> 🔍 Nur überwachen (kein Blocking)</label><small style="color:var(--text-secondary)">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"><label style="display:flex;align-items:center;gap:8px;cursor:pointer"><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"><label style="display:flex;align-items:center;gap:8px;cursor:pointer"><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 class="form-group" id="bulkMonitorOnlyGroup"><label style="display:flex;align-items:center;gap:8px;cursor:pointer"><input type="checkbox" name="monitor_only" id="bulkMonitorOnlyCheck" onchange="toggleBulkMonitorOnly()"> 🔍 Nur überwachen (kein Blocking)</label></div><div id="bulkBlockingOptions"><div style="border:1px solid var(--border);border-radius:8px;padding:16px;margin-bottom:16px"><div class="form-group"><label style="display:flex;align-items:center;gap:8px;cursor:pointer"><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"><label style="display:flex;align-items:center;gap:8px;cursor:pointer"><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="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="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="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>
@@ -1406,18 +1407,21 @@ def get_dashboard_html() -> str:
<div class="toast-container" id="toastContainer"></div>
<script>
let ws=null,agents={},shops={},currentLogsShop=null,currentSortBy='req_per_min',currentDetailShop=null;
let updateInterval=10000,pendingRender=false,renderTimer=null;
const BOT_COLORS=['#4a9eff','#00d26a','#ff4757','#ffc107','#9b59b6','#1abc9c','#e74c3c','#3498db','#f39c12','#2ecc71'];
function setUpdateInterval(ms){updateInterval=parseInt(ms);if(renderTimer)clearInterval(renderTimer);renderTimer=setInterval(()=>{if(pendingRender){renderAgents();renderShops();renderTopShops();refreshStats();pendingRender=false;}},updateInterval);}
function scheduleRender(){pendingRender=true;}
// Sort state für jede Tabelle
let sortState={agents:{col:null,asc:false},link11:{col:null,asc:false},direct:{col:null,asc:false}};
function updateSortIcons(tableId,col,asc){const table=document.getElementById(tableId);if(!table)return;table.querySelectorAll('th.sortable').forEach(th=>{th.classList.remove('asc','desc');const icon=th.querySelector('.sort-icon');if(icon)icon.textContent='';});if(col){const ths=table.querySelectorAll('th.sortable');ths.forEach(th=>{if(th.textContent.replace('','').trim().toLowerCase().includes(col.substring(0,3))){th.classList.add(asc?'asc':'desc');const icon=th.querySelector('.sort-icon');if(icon)icon.textContent=asc?'':'';}});}}
function sortAgents(col){const st=sortState.agents;if(st.col===col){st.asc=!st.asc;}else{st.col=col;st.asc=false;}renderAgents();updateSortIcons('agentsTable',col,st.asc);}
function sortShops(type,col){const st=sortState[type];if(st.col===col){st.asc=!st.asc;}else{st.col=col;st.asc=false;}renderShops();const tableId=type==='link11'?'tableLink11':'tableDirect';updateSortIcons(tableId,col,st.asc);}
function getSortedAgents(){const list=Object.values(agents);const st=sortState.agents;if(!st.col)return list;const dir=st.asc?1:-1;return list.sort((a,b)=>{let va,vb;switch(st.col){case'status':const order={online:3,pending:2,offline:1};va=order[a.status]||0;vb=order[b.status]||0;break;case'hostname':va=(a.hostname||'').toLowerCase();vb=(b.hostname||'').toLowerCase();return dir*(va<vb?-1:va>vb?1:0);case'shops':va=(a.shops_active||0);vb=(b.shops_active||0);break;case'load':va=(a.load_1m||0);vb=(b.load_1m||0);break;case'memory':va=(a.memory_percent||0);vb=(b.memory_percent||0);break;case'last_seen':va=a.last_seen||'';vb=b.last_seen||'';return dir*(va<vb?-1:va>vb?1:0);default:return 0;}return dir*(va-vb);});}
function getSortedShops(list,type){const st=sortState[type];if(!st.col)return list;const dir=st.asc?1:-1;return list.sort((a,b)=>{let va,vb;switch(st.col){case'status':va=a.status==='active'?1:0;vb=b.status==='active'?1:0;break;case'domain':va=(a.domain||'').toLowerCase();vb=(b.domain||'').toLowerCase();return dir*(va<vb?-1:va>vb?1:0);case'server':va=(a.agent_hostname||'').toLowerCase();vb=(b.agent_hostname||'').toLowerCase();return dir*(va<vb?-1:va>vb?1:0);case'modus':va=(a.mode||'').toLowerCase();vb=(b.mode||'').toLowerCase();return dir*(va<vb?-1:va>vb?1:0);case'req':va=(a.stats?.req_per_min||0);vb=(b.stats?.req_per_min||0);break;case'bans':va=(a.stats?.active_bans||0);vb=(b.stats?.active_bans||0);break;case'runtime':va=(a.runtime_minutes||0);vb=(b.runtime_minutes||0);break;default:return 0;}return dir*(va-vb);});}
function getSortedShops(list,type){const st=sortState[type];if(!st.col)return list;const dir=st.asc?1:-1;return list.sort((a,b)=>{let va,vb;switch(st.col){case'status':va=a.status==='active'?1:0;vb=b.status==='active'?1:0;break;case'domain':va=(a.domain||'').toLowerCase();vb=(b.domain||'').toLowerCase();return dir*(va<vb?-1:va>vb?1:0);case'server':va=(a.agent_hostname||'').toLowerCase();vb=(b.agent_hostname||'').toLowerCase();return dir*(va<vb?-1:va>vb?1:0);case'modus':va=(a.mode||'').toLowerCase();vb=(b.mode||'').toLowerCase();return dir*(va<vb?-1:va>vb?1:0);case'req':va=(a.stats?.req_per_min||0);vb=(b.stats?.req_per_min||0);break;case'bans':va=((a.stats?.active_bot_bans||0)+(a.stats?.active_country_bans||0));vb=((b.stats?.active_bot_bans||0)+(b.stats?.active_country_bans||0));break;case'runtime':va=(a.runtime_minutes||0);vb=(b.runtime_minutes||0);break;default:return 0;}return dir*(va-vb);});}
function updateClock(){document.getElementById('clock').textContent=new Date().toLocaleTimeString('de-DE');}
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 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()});renderAgents();break;case'agent.offline':if(agents[msg.data.agent_id]){agents[msg.data.agent_id].status='offline';renderAgents();}break;case'agent.approved':if(agents[msg.data.agent_id]){agents[msg.data.agent_id].status='online';agents[msg.data.agent_id].approved=true;renderAgents();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};});renderShops();renderTopShops();refreshStats();break;case'shop.stats':if(shops[msg.data.domain]){shops[msg.data.domain].stats=msg.data.stats;renderShops();renderTopShops();refreshStats();}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;}}
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 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('');}
@@ -1460,6 +1464,7 @@ def get_dashboard_html() -> str:
async function applyRateLimit(){if(!currentDetailShop)return;const rateLimit=document.getElementById('detailRateLimitInput').value;const banDuration=document.getElementById('detailBanDurationInput').value;if(!confirm('Rate-Limit aktivieren: '+rateLimit+' Req/min?'))return;const s=shops[currentDetailShop];toast('Wechsle zu Rate-Limit...','info');if(s&&s.status==='active'){const dfd=new FormData();dfd.append('domain',currentDetailShop);await fetch('/api/shops/deactivate',{method:'POST',body:dfd});await new Promise(r=>setTimeout(r,500));}const fd=new FormData();fd.append('domain',currentDetailShop);fd.append('bot_mode','true');fd.append('bot_rate_limit',rateLimit);fd.append('bot_ban_duration',banDuration);fd.append('country_mode','false');fd.append('monitor_only','false');await fetch('/api/shops/activate',{method:'POST',body:fd});closeModal('detailModal');}
async function detailSwitchMode(mode){if(!currentDetailShop)return;const s=shops[currentDetailShop];const modeNames={'bot-monitor':'🔍 Monitor','country-dach':'🇩🇪 DACH','country-eu':'🇪🇺 EU+'};if(!confirm('Modus wechseln zu '+modeNames[mode]+'?'))return;toast('Wechsle Modus...','info');if(s&&s.status==='active'){const dfd=new FormData();dfd.append('domain',currentDetailShop);await fetch('/api/shops/deactivate',{method:'POST',body:dfd});await new Promise(r=>setTimeout(r,500));}const fd=new FormData();fd.append('domain',currentDetailShop);if(mode==='bot-monitor'){fd.append('monitor_only','true');}else if(mode==='country-dach'){fd.append('bot_mode','true');fd.append('bot_rate_limit','30');fd.append('bot_ban_duration','300');fd.append('country_mode','true');fd.append('country_rate_limit','100');fd.append('country_ban_duration','600');fd.append('unlimited_countries','de,at,ch');}else if(mode==='country-eu'){fd.append('bot_mode','true');fd.append('bot_rate_limit','30');fd.append('bot_ban_duration','300');fd.append('country_mode','true');fd.append('country_rate_limit','100');fd.append('country_ban_duration','600');fd.append('unlimited_countries','de,at,ch,be,cy,ee,es,fi,fr,gb,gr,hr,ie,it,lt,lu,lv,mt,nl,pt,si,sk');}await fetch('/api/shops/activate',{method:'POST',body:fd});closeModal('detailModal');}
connect();
setUpdateInterval(10000);
</script>
</body>
</html>'''