anycoder-ebfbd3c7 / index.html
bughunter230's picture
Upload folder using huggingface_hub
5d60d2c verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Evil Twin Attack Detector (ML) — Browser Demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description"
content="Evil Twin attack detection demo using a lightweight ML model in the browser (TensorFlow.js). No backend required." />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs@4.20.0/dist/tf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js"></script>
<style>
:root {
--bg: #0b1020;
--bg-2: #0f1630;
--card: #121a35;
--card-2: #0f1a3a;
--text: #dfe7ff;
--muted: #8ea0c8;
--primary: #5b8cff;
--primary-2: #7ea2ff;
--accent: #8cffd9;
--danger: #ff6b6b;
--warn: #f7b267;
--good: #57e39a;
--shadow: 0 8px 30px rgba(0, 0, 0, .35);
--radius: 14px;
--radius-sm: 10px;
--radius-xs: 8px;
--grid-gap: 16px;
--ring: 0 0 0 2px rgba(91, 140, 255, .2), 0 0 0 6px rgba(91, 140, 255, .15);
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
}
body {
margin: 0;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
color: var(--text);
background:
radial-gradient(1200px 600px at 20% -10%, #1c2447 0%, rgba(28, 36, 71, 0) 60%),
radial-gradient(900px 800px at 100% 0%, #12204a 0%, rgba(18, 32, 74, 0) 60%),
linear-gradient(180deg, var(--bg) 0%, #060912 100%);
background-attachment: fixed;
}
header {
position: sticky;
top: 0;
z-index: 5;
background: linear-gradient(180deg, rgba(10, 15, 30, 0.95), rgba(10, 15, 30, 0.6));
backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.nav {
max-width: 1200px;
margin: 0 auto;
padding: 14px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
font-weight: 700;
letter-spacing: .3px;
}
.brand .logo {
width: 36px;
height: 36px;
border-radius: 10px;
background: conic-gradient(from 180deg at 50% 50%, #6ea8ff, #8cffd9, #6ea8ff);
box-shadow: inset 0 0 20px rgba(255, 255, 255, .25), 0 8px 25px rgba(140, 255, 217, .15);
position: relative;
overflow: hidden;
}
.brand .logo::after {
content: '';
position: absolute;
inset: 2px;
background: radial-gradient(120px 120px at 30% 20%, rgba(255, 255, 255, .45), rgba(255, 255, 255, 0));
border-radius: 8px;
}
.brand .title {
display: flex;
flex-direction: column;
line-height: 1.15;
}
.brand .title b {
font-size: 16px;
}
.brand .title small {
color: var(--muted);
font-weight: 500;
}
.hdr-actions {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.link {
color: var(--primary);
text-decoration: none;
padding: 8px 10px;
border-radius: 8px;
background: rgba(91, 140, 255, .08);
border: 1px solid rgba(91, 140, 255, .25);
}
.link:hover {
background: rgba(91, 140, 255, .15);
}
.btn {
background: linear-gradient(180deg, var(--primary) 0%, var(--primary-2) 100%);
color: #071022;
border: none;
padding: 10px 14px;
border-radius: 10px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 8px 20px rgba(91, 140, 255, .35);
}
.btn.secondary {
background: linear-gradient(180deg, #1a244a, #121a3a);
color: var(--text);
border: 1px solid rgba(255, 255, 255, .08);
box-shadow: none;
}
.btn.warn {
background: linear-gradient(180deg, #ffb86c, #ff9a52);
color: #2c1200;
}
.btn:disabled {
opacity: .6;
cursor: not-allowed;
}
main {
max-width: 1200px;
margin: 18px auto 60px;
padding: 0 20px;
display: grid;
grid-template-columns: 1.1fr 1.6fr;
gap: 22px;
}
@media (max-width: 1000px) {
main {
grid-template-columns: 1fr;
}
}
.card {
background: linear-gradient(180deg, rgba(20, 30, 60, .9), rgba(16, 22, 45, .85));
border: 1px solid rgba(255, 255, 255, .06);
border-radius: var(--radius);
box-shadow: var(--shadow);
overflow: clip;
}
.card h2 {
margin: 0;
padding: 16px 18px;
font-size: 16px;
border-bottom: 1px solid rgba(255, 255, 255, .06);
background: linear-gradient(180deg, rgba(255, 255, 255, .04), rgba(255, 255, 255, 0));
}
.card .body {
padding: 16px;
}
.grid {
display: grid;
gap: var(--grid-gap);
grid-template-columns: repeat(12, 1fr);
}
.col-12 {
grid-column: span 12;
}
.col-6 {
grid-column: span 6;
}
.col-4 {
grid-column: span 4;
}
.col-8 {
grid-column: span 8;
}
@media (max-width: 800px) {
.col-6,
.col-4,
.col-8 {
grid-column: span 12;
}
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
}
.field label {
font-size: 12px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: .7px;
}
input,
select {
appearance: none;
outline: none;
background: #0d1530;
border: 1px solid rgba(255, 255, 255, .08);
color: var(--text);
padding: 10px 12px;
border-radius: var(--radius-sm);
transition: border .2s ease, box-shadow .2s ease, background .2s ease;
}
input:focus,
select:focus {
border-color: rgba(91, 140, 255, .7);
box-shadow: var(--ring);
background: #0f1a3f;
}
.actions {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.muted {
color: var(--muted);
font-size: 13px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
border: 1px solid rgba(255, 255, 255, .1);
background: rgba(255, 255, 255, .04);
}
.badge.good {
background: rgba(87, 227, 154, .12);
color: var(--good);
border-color: rgba(87, 227, 154, .35);
}
.badge.warn {
background: rgba(247, 178, 103, .12);
color: var(--warn);
border-color: rgba(247, 178, 103, .35);
}
.badge.danger {
background: rgba(255, 107, 107, .14);
color: var(--danger);
border-color: rgba(255, 107, 107, .35);
}
.sep {
height: 1px;
background: rgba(255, 255, 255, .06);
margin: 12px 0;
}
.kpi {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border: 1px solid rgba(255, 255, 255, .08);
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, .03);
}
.kpi .label {
color: var(--muted);
font-size: 12px;
}
.kpi .value {
font-weight: 800;
font-size: 18px;
}
.progress {
height: 10px;
width: 100%;
background: rgba(255, 255, 255, .08);
border-radius: 999px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, .06);
}
.progress>div {
height: 100%;
background: linear-gradient(90deg, #8cffd9, #5b8cff);
width: 0%;
transition: width .2s ease;
}
.list {
display: flex;
flex-direction: column;
gap: 10px;
max-height: 300px;
overflow: auto;
}
.list .row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, .06);
background: rgba(255, 255, 255, .03);
}
.mini {
font-size: 12px;
color: var(--muted);
}
.score {
font-weight: 800;
font-variant-numeric: tabular-nums;
}
.pill {
padding: 4px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 800;
border: 1px solid rgba(255, 255, 255, .08);
}
.pill.good {
background: rgba(87, 227, 154, .12);
color: var(--good);
border-color: rgba(87, 227, 154, .25);
}
.pill.bad {
background: rgba(255, 107, 107, .12);
color: var(--danger);
border-color: rgba(255, 107, 107, .25);
}
.chart-wrap {
height: 220px;
border-radius: var(--radius-sm);
background: #0e1430;
border: 1px solid rgba(255, 255, 255, .06);
padding: 10px;
}
.note {
font-size: 12px;
color: var(--muted);
padding: 10px 12px;
border-radius: var(--radius-sm);
background: rgba(255, 255, 255, .03);
border: 1px solid rgba(255, 255, 255, .06);
}
.toast {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 50;
padding: 12px 14px;
background: #121b3a;
border: 1px solid rgba(255, 255, 255, .1);
border-left: 4px solid var(--primary);
border-radius: 10px;
color: var(--text);
box-shadow: var(--shadow);
display: none;
max-width: 420px;
}
.toast.show {
display: block;
animation: slideIn .25s ease;
}
@keyframes slideIn {
from {
transform: translateY(10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
@media (max-width: 700px) {
.grid-2 {
grid-template-columns: 1fr;
}
}
.small {
font-size: 12px;
}
</style>
</head>
<body>
<header>
<div class="nav">
<div class="brand">
<div class="logo" aria-hidden="true"></div>
<div class="title">
<b>Evil Twin Attack Detector</b>
<small>Client-side ML demo (TensorFlow.js)</small>
</div>
</div>
<div class="hdr-actions">
<a class="link" href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" rel="noreferrer">Built
with anycoder</a>
<button class="btn secondary" id="btnRetrain">Retrain</button>
</div>
</div>
</header>
<main>
<section class="card">
<h2>1) Add Wi‑Fi sample & predict</h2>
<div class="body grid">
<div class="col-12">
<div class="grid-2">
<div class="field">
<label for="ssid">SSID</label>
<input id="ssid" placeholder="e.g., CoffeeShop_WiFi" value="CoffeeShop_WiFi" />
</div>
<div class="field">
<label for="bssid">BSSID (MAC)</label>
<input id="bssid" placeholder="02:00:00:AA:BB:CC" value="02:00:00:AA:BB:CC" />
</div>
<div class="field">
<label for="rssi">Signal (RSSI dBm)</label>
<input id="rssi" type="number" step="1" value="-55" />
</div>
<div class="field">
<label for="channel">Channel</label>
<input id="channel" type="number" step="1" value="6" />
</div>
<div class="field">
<label for="frequency">Frequency (MHz)</label>
<input id="frequency" type="number" step="1" value="2437" />
</div>
<div class="field">
<label for="encryption">Encryption</label>
<select id="encryption">
<option value="WPA2-PSK">WPA2-PSK</option>
<option value="WPA3-PSK">WPA3-PSK</option>
<option value="WEP">WEP</option>
<option value="Open">Open</option>
</select>
</div>
<div class="field">
<label for="hidden">Hidden</label>
<select id="hidden">
<option value="false">No</option>
<option value="true">Yes</option>
</select>
</div>
<div class="field">
<label for="enc-mismatch">Encryption mismatch</label>
<select id="enc-mismatch">
<option value="false">No</option>
<option value="true">Yes</option>
</select>
</div>
<div class="field">
<label for="expected-bssids">Expected BSSIDs (comma)</label>
<input id="expected-bssids" placeholder="02:00:00:AA:BB:CC, 06:00:00:11:22:33" value="02:00:00:AA:BB:CC, 06:00:00:11:22:33" />
</div>
<div class="field">
<label for="known-ch">Known channels (comma)</label>
<input id="known-ch" placeholder="6, 11, 1" value="6, 11, 1" />
</div>
</div>
</div>
<div class="col-12">
<div class="actions">
<button class="btn" id="btnPredict">Predict</button>
<button class="btn secondary" id="btnRandom">Randomize sample</button>
<span class="muted">Feature vector is automatically derived from these inputs.</span>
</div>
</div>
<div class="col-12 sep"></div>
<div class="col-12">
<div id="predCard" class="kpi" style="display:none">
<div style="display:flex; flex-direction:column; gap:6px;">
<div class="label">Prediction</div>
<div id="predLabel" class="value"></div>
<div class="mini" id="predExplain"></div>
</div>
<div style="display:flex; flex-direction:column; gap:6px; align-items:flex-end;">
<div class="label">Evil Twin probability</div>
<div class="score" id="predProb"></div>
<div class="progress" style="width:220px;">
<div id="predBar" style="width:0%"></div>
</div>
</div>
</div>
</div>
<div class="col-12">
<div class="note">
Tip: Toggle “Encryption mismatch” to simulate when an AP claims a different security type than expected. The
model uses signal strength, channel, and known channels/BSSIDs to inform its inference.
</div>
</div>
</div>
</section>
<section class="card">
<h2>2) Real-time threat monitor</h2>
<div class="body">
<div class="grid">
<div class="col-12">
<div class="actions">
<button class="btn" id="btnMonitor">Start monitoring</button>
<button class="btn warn" id="btnClearDetections">Clear detections</button>
<span class="muted">Simulated environment: random APs appear; malicious ones are generated by a hidden adversary policy.</span>
</div>
</div>
<div class="col-6">
<div class="kpi">
<div>
<div class="label">Total scanned</div>
<div class="value" id="kpiTotal">0</div>
</div>
<div>
<div class="label">Detections (evil twin)</div>
<div class="value" id="kpiDetections" style="color:var(--danger)">0</div>
</div>
</div>
</div>
<div class="col-6">
<div class="kpi">
<div>
<div class="label">Detection rate</div>
<div class="value" id="kpiRate">0%</div>
</div>
<div>
<div class="label">Last risk</div>
<div class="value" id="kpiLast"></div>
</div>
</div>
</div>
<div class="col-12 chart-wrap">
<canvas id="monitorChart"></canvas>
</div>
<div class="col-12 list" id="detList"></div>
<div class="col-12">
<div class="note">
Detection logic uses the trained model score with a calibrated threshold of 0.50. In production, tune the
threshold with a validation set to balance false positives/negatives.
</div>
</div>
</div>
</div>
</section>
<section class="card">
<h2>3) Train the model (on synthetic data)</h2>
<div class="body grid">
<div class="col-12 actions">
<button class="btn" id="btnTrain">Generate data & Train</button>
<span class="muted">The dataset is generated in-browser and based on plausible Evil Twin behaviors.</span>
</div>
<div class="col-12 kpi" id="trainKPI" style="display:none">
<div style="display:flex; gap:16px; flex-wrap:wrap;">
<div>
<div class="label">Training samples</div>
<div class="value" id="kpiTrainN">0</div>
</div>
<div>
<div class="label">Validation samples</div>
<div class="value" id="kpiValN">0</div>
</div>
<div>
<div class="label">Epochs</div>
<div class="value" id="kpiEpochs">0/10</div>
</div>
<div>
<div class="label">Final loss</div>
<div class="value" id="kpiLoss"></div>
</div>
<div>
<div class="label">Val accuracy</div>
<div class="value" id="kpiAcc"></div>
</div>
</div>
</div>
<div class="col-12">
<div class="chart-wrap" style="height: 260px;">
<canvas id="lossChart"></canvas>
</div>
</div>
<div class="col-12">
<div class="chart-wrap" style="height: 260px;">
<canvas id="cmChart"></canvas>
</div>
</div>
<div class="col-12">
<div class="note">
This demo runs fully in your browser using TensorFlow.js. No network scans are performed; it simulates Wi‑Fi
features for training and prediction. For a production deployment, feed real measurements (e.g., BSSID,
signal, channel) from your platform’s Wi‑Fi stack, and monitor over time for anomalies.
</div>
</div>
</div>
</section>
</main>
<div class="toast" id="toast"></div>
<script>
// --- Utilities ---
const $ = (sel) => document.querySelector(sel);
const fmtPct = (x) => (x*100).toFixed(1) + '%';
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const randn = (() => {
// Box-Muller
let spare = null;
return () => {
if (spare !== null) { const n = spare; spare = null; return n; }
let u = 0, v = 0, s = 0;
do { u = Math.random()*2-1; v = Math.random()*2-1; s = u*u+v*v; } while (s === 0 || s >= 1);
const mul = Math.sqrt(-2.0 * Math.log(s) / s);
spare = v * mul;
return u * mul;
};
})();
const logistic = (x) => 1 / (1 + Math.exp(-x));
const showToast = (msg, color) => {
const t = $('#toast');
t.textContent = msg;
t.style.borderLeftColor = color || 'var(--primary)';
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2500);
};
// --- Model (simple logistic regression with 3 features) ---
// Features: [signalNorm, chDeltaNorm, mismatchScore]
const normalizeSignal = (rssi) => (clamp((rssi + 100) / 50, 0, 1));
const normalizeChDelta = (delta) => (clamp(Math.abs(delta) / 5, 0, 1));
function mismatchScore({encryption, encMismatch, hidden}){
let s = 0;
if (encMismatch) s += 0.35;
if (hidden) s += 0.15;
if (encryption === 'WEP') s += 0.20;
if (encryption === 'Open') s += 0.20;
if (encryption === 'WPA3-PSK') s -= 0.05; // slightly less suspicious in isolation
return clamp(s, 0, 1);
}
function extractFeatures({rssi, channel, knownChannels, encryption, encMismatch, hidden}){
// nearest known channel
let nearest = Infinity;
for (const kc of knownChannels) nearest = Math.min(nearest, Math.abs(kc - channel));
const chDelta = isFinite(nearest) ? nearest : 10;
const signalNorm = normalizeSignal(rssi);
const chDeltaNorm = normalizeChDelta(chDelta);
const mismatch = mismatchScore({encryption, encMismatch, hidden});
return [signalNorm, chDeltaNorm, mismatch];
}
// Hidden adversary policy for monitor
function adversaryIsEvilTwinLabel(features){
const [sNorm, chDeltaNorm, mismatch] = features;
const y = -1.4 + 2.0*mismatch + 1.3*(1 - sNorm) + 1.0*chDeltaNorm;
const p = 1/(1+Math.exp(-y));
return Math.random() < p;
}
// Weights learned by gradient descent on synthetic data
let W = tf.tensor1d([0,0,0]); // [w1, w2, w3]
let b = tf.scalar(0);
let trained = false;
let training = false;
function predictProba(featuresArr){
if (!trained) return 0.0;
const t = tf.tensor2d([featuresArr]);
const z = tf.sum(tf.mul(W, t.reshape([3])), 0).add(b);
return logistic(z.dataSync()[0]);
}
function predictLabel(featuresArr){
const p = predictProba(featuresArr);
return p >= 0.5 ? 1 : 0;
}
// --- Data generation (synthetic, but plausible) ---
function genSamples(n, knownChannels, advPolicy){
const X = [];
const y = [];
for (let i=0; i<n; i++){
const isEvil = Math.random() < 0.45; // 45% malicious in training
let rssi, channel, encryption, encMismatch=false, hidden=false;
if (isEvil){
rssi = clamp(-30 + randn()*6, -100, -25);
channel = Math.random() < 0.55 ? (knownChannels[0] ?? 6) : (1 + Math.floor(Math.random()*14));
encryption = Math.random() < 0.5 ? 'WEP' : (Math.random() < 0.5 ? 'Open' : 'WPA2-PSK');
encMismatch = Math.random() < 0.55;
hidden = Math.random() < 0.35;
} else {
rssi = clamp(-65 + randn()*8, -100, -25);
channel = (Math.random() < 0.75) ? (knownChannels[Math.floor(Math.random()*knownChannels.length)] ?? 6) : (1 + Math.floor(Math.random()*14));
encryption = Math.random() < 0.65 ? 'WPA2-PSK' : (Math.random() < 0.5 ? 'WPA3-PSK' : 'Open');
encMismatch = Math.random() < 0.08;
hidden = Math.random() < 0.12;
}
const features = extractFeatures({rssi, channel, knownChannels, encryption, encMismatch, hidden});
const label = advPolicy ? (adversaryIsEvilTwinLabel(features) ? 1 : 0) : isEvil ? 1 : 0;
X.push(features);
y.push(label);
}
return {X, y};
}
// --- Training loop ---
async function trainModel({epochs=10, lr=0.35, trainN=6000, valN=2000, knownChannels=[1,6,11]}){
if (trained || training) return;
training = true;
showToast('Generating synthetic dataset...', 'var(--primary)');
$('#trainKPI').style.display = 'flex';
const train = genSamples(trainN, knownChannels, true);
const val = genSamples(valN, knownChannels, true);
$('#kpiTrainN').textContent = trainN.toLocaleString();
$('#kpiValN').textContent = valN.toLocaleString();
$('#kpiEpochs').textContent = `0/${epochs}`;
$('#kpiLoss').textContent = '—';
$('#kpiAcc').textContent = '—';
const Xtr = tf.tensor2d(train.X);
const ytr = tf.tensor2d(train.y.map(v => [v]), [trainN, 1]);
const Xval = tf.tensor2d(val.X);
const yval = tf.tensor2d(val.y.map(v => [v]), [valN, 1]);
// Initialize weights with small noise
W.dispose(); b.dispose();
W = tf.tensor1d([ (Math.random()-0.5)*0.01, (Math.random()-0.5)*0.01, (Math.random()-0.5)*0.01 ]);
b = tf.scalar((Math.random()-0.5)*0.01);
const lossSeries = [];
const accSeries = [];
for (let e=1; e<=epochs; e++){
// Forward + backward
const lossT = tf.tidy(() => {
const z = Xtr.matMul(W.reshape([3,1])).add(b);
const preds = z.sigmoid();
const loss = tf.losses.logLoss(ytr, preds).mean();
return loss;
});
const lossVal = (await lossT.data())[0];
lossT.dispose();
// Accuracy
const acc = await accuracyOn(Xval, yval);
lossSeries.push(lossVal);
accSeries.push(acc);
$('#kpiEpochs').textContent = `${e}/${epochs}`;
$('#kpiLoss').textContent = lossVal.toFixed(3);
$('#kpiAcc').textContent = fmtPct(acc);
// Update weights
const opt = tf.train.sgd(lr);
await opt.minimize(() => {
const z = Xtr.matMul(W.reshape([3,1])).add(b);
const preds = z.sigmoid();
const loss = tf.losses.logLoss(ytr, preds).mean();
return loss;
});
// Update charts
updateLossChart(lossSeries, accSeries);
await tf.nextFrame();
}
// Final validation metrics + confusion matrix
const predsVal = tf.tidy(() => {
const z = Xval.matMul(W.reshape([3,1])).add(b);
return z.sigmoid();
});
const yhat = (await predsVal.data()).map(v => v >= 0.5 ? 1 : 0);
predsVal.dispose();
const cm = confusionMatrix(val.y, yhat, 2);
renderConfusionMatrix(cm);
Xtr.dispose(); ytr.dispose(); Xval.dispose(); yval.dispose();
trained = true;
training = false;
showToast('Model trained. Try predicting samples or start the monitor.', 'var(--good)');
}
async function accuracyOn(X, y){
const preds = tf.tidy(() => {
const z = X.matMul(W.reshape([3,1])).add(b);
return z.sigmoid();
});
const arr = await preds.data();
preds.dispose();
const n = y.shape[0];
let correct = 0;
for (let i=0; i<n; i++){
const yi = y.get(i,0);
const pi = arr[i] >= 0.5 ? 1 : 0;
if (yi === pi) correct++;
}
return correct / n;
}
function confusionMatrix(yTrue, yPred, classes=2){
const cm = Array.from({length: classes}, () => Array(classes).fill(0));
for (let i=0; i<yTrue.length; i++){
cm[yTrue[i]][yPred[i]] += 1;
}
return cm;
}
// --- Charts ---
let lossChart, cmChart, monitorChart;
function initCharts(){
const lossCtx = $('#lossChart').getContext('2d');
lossChart = new Chart(lossCtx, {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'Loss', data: [], borderColor: '#ff9a52', backgroundColor: 'rgba(255,154,82,.2)', tension: .2, yAxisID: 'y' },
{ label: 'Val Acc', data: [], borderColor: '#8cffd9', backgroundColor: 'rgba(140,255,217,.2)', tension: .2, yAxisID: 'y1' }
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { type: 'linear', position: 'left', min: 0, grid:{ color:'rgba(255,255,255,.06)' }, ticks:{ color:'#9ab' } },
y1: { type: 'linear', position: 'right', min: 0, max: 1, grid:{ drawOnChartArea:false }, ticks:{ color:'#9ab' } },
x: { grid:{ color:'rgba(255,255,255,.06)' }, ticks:{ color:'#9ab' } }
},
plugins: {
legend: { labels: { color:'#dfe7ff' } }
}
}
});
const cmCtx = $('#cmChart').getContext('2d');
cmChart = new Chart(cmCtx, {
type: 'bar',
data: {
labels: ['True Neg', 'False Pos', 'False Neg', 'True Pos'],
datasets: [{ label:'Validation set', data: [0,0,0,0], backgroundColor: ['#57e39a','#f7b267','#f7b267','#57e39a'] }]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { beginAtZero: true, grid:{ color:'rgba(255,255,255,.06)' }, ticks:{ color:'#9ab' } },
x: { grid:{ color:'rgba(255,255,255,.06)' }, ticks:{ color:'#9ab' } }
},
plugins: { legend: { labels: { color:'#dfe7ff' } } }
}
});
const monCtx = $('#monitorChart').getContext('2d');
monitorChart = new Chart(monCtx, {
type: 'line',
data: {
labels: [],
datasets: [
{ label:'Risk score', data: [], borderColor:'#5b8cff', backgroundColor:'rgba(91,140,255,.2)', tension:.25 },
{ label:'Threshold', data: [], borderColor:'#ff6b6b', borderDash:[6,4], pointRadius:0 }
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { min: 0, max: 1, grid:{ color:'rgba(255,255,255,.06)' }, ticks:{ color:'#9ab' } },
x: { grid:{ color:'rgba(255,255,255,.06)' }, ticks:{ color:'#9ab', autoSkip: true } }
},
plugins: { legend: { labels:{ color:'#dfe7ff' } } }
}
});
}
function updateLossChart(lossArr, accArr){
if (!lossChart) return;
lossChart.data.labels = lossArr.map((_,i)=>`Epoch ${i+1}`);
lossChart.data.datasets[0].data = lossArr;
lossChart.data.datasets[1].data = accArr;
lossChart.update();
}
function renderConfusionMatrix(cm){
if (!cmChart) return;
const tn = cm[0][0], fp = cm[0][1], fn = cm[1][0], tp = cm[1][1];
cmChart.data.datasets[0].data = [tn, fp, fn, tp];
cmChart.update();
}
function pushMonitorPoint(score){
if (!monitorChart) return;
const labels = monitorChart.data.labels;
labels.push(labels.length+1);
monitorChart.data.datasets[0].data.push(score);
monitorChart.data.datasets[1].push(0.5);
if (labels.length > 50){
labels.shift();
monitorChart.data.datasets.forEach(ds => ds.data.shift());
}
monitorChart.update();
}
// --- UI: sample parsing & prediction ---
function parseListNumberInput(el){
return el.value
.split(',')
.map(s => Number(s.trim()))
.filter(v => Number.isFinite(v));
}
function getSampleFromUI(){
const ssid = $('#ssid').value.trim() || 'Unknown';
const bssid = $('#bssid').value.trim() || '00:00:00:00:00:00';
const rssi = Number($('#rssi').value);
const channel = Number($('#channel').value);
const frequency = Number($('#frequency').value) || (2412 + (channel-1)*5);
const encryption = $('#encryption').value;
const hidden = $('#hidden').value === 'true';
const encMismatch = $('#enc-mismatch').value === 'true';
const expectedBSSIDs = $('#expected-bssids').value.split(',').map(s => s.trim()).filter(Boolean);
const knownChannels = parseListNumberInput($('#known-ch')) || [1,6,11];
return {
ssid, bssid, rssi, channel, frequency,
encryption, hidden, encMismatch, expectedBSSIDs, knownChannels
};
}
function featuresFromUI(){
const s = getSampleFromUI();
return extractFeatures({
rssi: s.rssi,
channel: s.channel,
knownChannels: s.knownChannels,
encryption: s.encryption,
encMismatch: s.encMismatch,
hidden: s.hidden
});
}
function explain(features, sample){
const [sNorm, chDeltaNorm, mismatch] = features.map(v => Number(v.toFixed(2)));
const nearest = sample.knownChannels.reduce((best, kc) => Math.min(best, Math.abs(kc - sample.channel)), 99);
const exp = [];
if (mismatch > 0.5) exp.push(`Security mismatch or weak encryption (${mismatch.toFixed(2)})`);
if (sNorm < 0.5) exp.push(`Unusually strong signal (${sNorm.toFixed(2)})`);
if (chDeltaNorm > 0.4) exp.push(`Channel far from known (Δ=${nearest})`);
if (sample.hidden) exp.push(`Hidden SSID`);
return exp.length ? exp.join('; ') : 'No strong anomalies detected';
}
function showPrediction(features){
const sample = getSampleFromUI();
const prob = predictProba(features);
const label