jtl-wafi-dashboard.py aktualisiert

This commit is contained in:
2025-12-22 14:19:43 +01:00
parent 8d75b87f3b
commit 25a5ab4d3e

View File

@@ -1179,6 +1179,10 @@ def get_dashboard_html() -> str:
table { width: 100%; border-collapse: collapse; background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
th, td { padding: 14px 16px; text-align: left; }
th { background: rgba(0,0,0,0.2); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-secondary); }
th.sortable { cursor: pointer; user-select: none; transition: color 0.2s; }
th.sortable:hover { color: var(--accent); }
th.sortable .sort-icon { margin-left: 4px; opacity: 0.3; }
th.sortable.asc .sort-icon, th.sortable.desc .sort-icon { opacity: 1; color: var(--accent); }
td { border-top: 1px solid var(--border); font-size: 14px; }
tr:hover td { background: rgba(255,255,255,0.02); }
.status-badge { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; }
@@ -1270,15 +1274,15 @@ def get_dashboard_html() -> str:
</div>
<div class="section">
<div class="section-header"><h2 class="section-title">🖥️ Server</h2><span class="badge" id="agentCount">0</span></div>
<table><thead><tr><th>Status</th><th>Hostname</th><th>Shops</th><th>Load</th><th>Memory</th><th>Zuletzt</th><th>Aktionen</th></tr></thead><tbody id="agentsTable"></tbody></table>
<table><thead><tr><th class="sortable" onclick="sortAgents('status')">Status<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortAgents('hostname')">Hostname<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortAgents('shops')">Shops<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortAgents('load')">Load<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortAgents('memory')">Memory<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortAgents('last_seen')">Zuletzt<span class="sort-icon">⇅</span></th><th>Aktionen</th></tr></thead><tbody id="agentsTable"></tbody></table>
</div>
<div class="section">
<div class="section-header"><h2 class="section-title">🛡️ Shops hinter Link11</h2><span class="badge link11" id="link11Count">0</span></div>
<table><thead><tr><th>Status</th><th>Domain</th><th>Server</th><th>Modus</th><th>Req/min</th><th>Bans</th><th>Laufzeit</th><th>Aktionen</th></tr></thead><tbody id="shopsLink11Table"></tbody></table>
<table id="tableLink11"><thead><tr><th class="sortable" onclick="sortShops('link11','status')">Status<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('link11','domain')">Domain<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('link11','server')">Server<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('link11','modus')">Modus<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('link11','req')">Req/min<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('link11','bans')">Bans<span class="sort-icon">⇅</span></th><th class="sortable" onclick="sortShops('link11','runtime')">Laufzeit<span class="sort-icon">⇅</span></th><th>Aktionen</th></tr></thead><tbody id="shopsLink11Table"></tbody></table>
</div>
<div class="section">
<div class="section-header"><h2 class="section-title">⚡ Shops direkt verbunden</h2><span class="badge direct" id="directCount">0</span></div>
<table><thead><tr><th>Status</th><th>Domain</th><th>Server</th><th>Modus</th><th>Req/min</th><th>Bans</th><th>Laufzeit</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>
</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>
@@ -1292,12 +1296,19 @@ def get_dashboard_html() -> str:
<script>
let ws=null,agents={},shops={},currentLogsShop=null,currentSortBy='req_per_min',currentDetailShop=null;
const BOT_COLORS=['#4a9eff','#00d26a','#ff4757','#ffc107','#9b59b6','#1abc9c','#e74c3c','#3498db','#f39c12','#2ecc71'];
// 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 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);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=Object.values(agents);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 l11=Object.values(shops).filter(s=>s.link11),dir=Object.values(shops).filter(s=>!s.link11);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 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=>'<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>'+(s.mode?(s.bot_monitor_only?'🔍':(s.mode==='bot'?'🤖':'🌍'))+(s.mode==='geoip'&&s.geo_region?' ('+s.geo_region.toUpperCase()+')':'')+(s.bot_monitor_only?' Monitor':''):'-')+'</td><td>'+((s.stats?.req_per_min||0).toFixed(1))+'</td><td>'+(s.bot_monitor_only?'-':(s.stats?.active_bans||0))+'</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=>'<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">'+(s.stats?.active_bans||0)+' 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;}
@@ -1391,4 +1402,4 @@ def main():
if __name__ == "__main__":
main()
main()