Spaces:
Configuration error
Configuration error
Update frontend/index.html
Browse files- frontend/index.html +278 -243
frontend/index.html
CHANGED
|
@@ -21,7 +21,6 @@ html{scroll-behavior:smooth}
|
|
| 21 |
body{background:var(--bg);color:var(--text);font-family:var(--sans);min-height:100vh;overflow-x:hidden}
|
| 22 |
body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:0;background:radial-gradient(ellipse 60% 40% at 10% 20%,rgba(0,232,122,0.04) 0%,transparent 60%),radial-gradient(ellipse 50% 50% at 90% 80%,rgba(0,200,232,0.03) 0%,transparent 60%),repeating-linear-gradient(0deg,transparent,transparent 40px,rgba(0,210,130,0.015) 40px,rgba(0,210,130,0.015) 41px),repeating-linear-gradient(90deg,transparent,transparent 40px,rgba(0,210,130,0.015) 40px,rgba(0,210,130,0.015) 41px)}
|
| 23 |
.app{position:relative;z-index:1;max-width:1500px;margin:0 auto;padding:0 24px 80px}
|
| 24 |
-
|
| 25 |
header{display:flex;align-items:center;justify-content:space-between;padding:20px 0;border-bottom:1px solid var(--border2);margin-bottom:0;position:relative}
|
| 26 |
header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height:1px;background:linear-gradient(90deg,var(--accent),transparent)}
|
| 27 |
.logo{display:flex;align-items:center;gap:14px}
|
|
@@ -36,7 +35,6 @@ header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height
|
|
| 36 |
.dot.green{background:var(--accent);box-shadow:0 0 8px var(--accent)}
|
| 37 |
@keyframes blink{0%,100%{opacity:1}50%{opacity:0.2}}
|
| 38 |
#sessionClock{font-family:var(--mono);font-size:11px;color:var(--muted2);background:var(--surface);padding:6px 12px;border-radius:6px;border:1px solid var(--border)}
|
| 39 |
-
|
| 40 |
.tab-nav{display:flex;border-bottom:1px solid var(--border2);margin-bottom:28px}
|
| 41 |
.tab-btn{padding:16px 32px;font-family:var(--mono);font-size:11px;font-weight:600;letter-spacing:2px;text-transform:uppercase;color:var(--muted);background:transparent;border:none;cursor:pointer;transition:color .2s;border-bottom:2px solid transparent;margin-bottom:-1px}
|
| 42 |
.tab-btn:hover{color:var(--muted2)}
|
|
@@ -45,11 +43,9 @@ header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height
|
|
| 45 |
.tab-pane{display:none}
|
| 46 |
.tab-pane.active{display:block;animation:fadeIn .25s ease}
|
| 47 |
@keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
| 48 |
-
|
| 49 |
.panel{background:var(--bg2);border:1px solid var(--border);border-radius:14px;overflow:hidden}
|
| 50 |
.panel-head{padding:12px 18px;font-family:var(--mono);font-size:10px;color:var(--muted);border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;letter-spacing:1.5px;text-transform:uppercase;background:rgba(0,0,0,0.2)}
|
| 51 |
.panel-head .accent{color:var(--accent)}.panel-head .red{color:var(--red)}.panel-head .cyan{color:var(--cyan)}.panel-head .amber{color:var(--amber)}
|
| 52 |
-
|
| 53 |
.config-strip{display:flex;gap:10px;align-items:center;background:var(--bg2);border:1px solid var(--border2);border-radius:12px;padding:14px 18px;margin-bottom:22px;flex-wrap:wrap}
|
| 54 |
.speed-wrap{display:flex;align-items:center;gap:8px;font-family:var(--mono);font-size:11px;color:var(--muted)}
|
| 55 |
.speed-wrap select{background:var(--surface);border:1px solid var(--border2);border-radius:7px;color:var(--cyan);font-family:var(--mono);font-size:11px;padding:7px 12px;outline:none;cursor:pointer}
|
|
@@ -66,10 +62,9 @@ header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height
|
|
| 66 |
#connBadge.real{background:rgba(0,232,122,0.1);color:var(--accent);border:1px solid rgba(0,232,122,0.2)}
|
| 67 |
#connBadge.local{background:rgba(255,170,0,0.1);color:var(--amber);border:1px solid rgba(255,170,0,0.2)}
|
| 68 |
#connBadge.idle{background:var(--surface);color:var(--muted);border:1px solid var(--border)}
|
| 69 |
-
|
| 70 |
.metrics{display:grid;grid-template-columns:repeat(6,1fr);gap:12px;margin-bottom:22px}
|
| 71 |
@media(max-width:1000px){.metrics{grid-template-columns:repeat(3,1fr)}}
|
| 72 |
-
.mc{background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:18px 16px;position:relative;overflow:hidden;transition:
|
| 73 |
.mc.flash{border-color:var(--accent);animation:mcflash .6s ease-out}
|
| 74 |
@keyframes mcflash{0%{box-shadow:0 0 16px rgba(0,232,122,0.5)}100%{box-shadow:none}}
|
| 75 |
.mc-label{font-family:var(--mono);font-size:9px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin-bottom:10px}
|
|
@@ -78,7 +73,6 @@ header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height
|
|
| 78 |
.mc-sub{font-size:10px;color:var(--muted);margin-top:6px;font-family:var(--mono)}
|
| 79 |
.mc-bar{position:absolute;bottom:0;left:0;height:2px;transition:width .6s ease}
|
| 80 |
.mc-bar.green{background:linear-gradient(90deg,var(--accent),transparent)}.mc-bar.red{background:linear-gradient(90deg,var(--red),transparent)}.mc-bar.blue{background:linear-gradient(90deg,var(--cyan),transparent)}.mc-bar.purple{background:linear-gradient(90deg,var(--purple),transparent)}
|
| 81 |
-
|
| 82 |
.main-grid{display:grid;grid-template-columns:1fr 360px;gap:18px;margin-bottom:18px}
|
| 83 |
@media(max-width:1100px){.main-grid{grid-template-columns:1fr}}
|
| 84 |
.feed-wrap{max-height:480px;overflow-y:auto}
|
|
@@ -109,21 +103,20 @@ header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height
|
|
| 109 |
.bottom-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
|
| 110 |
@media(max-width:800px){.bottom-grid{grid-template-columns:1fr}}
|
| 111 |
.heatmap-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:3px;padding:14px}
|
| 112 |
-
.hm-cell{aspect-ratio:1;border-radius:3px;background:var(--surface)
|
| 113 |
.summary-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;padding:14px}
|
| 114 |
.sum-item{background:var(--surface);border-radius:10px;padding:14px;border:1px solid var(--border)}
|
| 115 |
.sum-label{font-family:var(--mono);font-size:9px;color:var(--muted);letter-spacing:1.5px;text-transform:uppercase;margin-bottom:6px}
|
| 116 |
.sum-val{font-family:var(--mono);font-size:20px;font-weight:700}
|
| 117 |
-
|
| 118 |
/* CSV */
|
| 119 |
.upload-zone{border:2px dashed var(--border2);border-radius:20px;padding:70px 40px;text-align:center;cursor:pointer;transition:all .3s;background:var(--bg2);position:relative;overflow:hidden}
|
| 120 |
-
.upload-zone:hover,.upload-zone.drag-over{border-color:var(--accent);background:rgba(0,232,122,0.03)
|
| 121 |
.upload-zone input{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%}
|
| 122 |
.upload-icon{font-size:56px;margin-bottom:18px;display:block;opacity:.5}
|
| 123 |
.upload-title{font-family:var(--mono);font-size:18px;color:var(--accent);margin-bottom:10px;font-weight:700}
|
| 124 |
.upload-sub{font-size:13px;color:var(--muted2)}
|
| 125 |
.upload-hint{display:inline-block;margin-top:18px;padding:8px 20px;border-radius:8px;border:1px solid var(--border2);font-family:var(--mono);font-size:10px;color:var(--muted);letter-spacing:1px;background:var(--surface)}
|
| 126 |
-
.format-banner{display:flex;align-items:center;gap:12px;padding:12px 18px;border-radius:10px;font-family:var(--mono);font-size:11px;margin-bottom:16px;border:1px solid
|
| 127 |
.format-banner.ok{background:rgba(0,232,122,0.06);border-color:rgba(0,232,122,0.2);color:var(--accent)}
|
| 128 |
.file-card{background:var(--bg2);border:1px solid var(--border2);border-radius:14px;padding:18px 22px;margin-bottom:18px;display:flex;align-items:center;gap:18px}
|
| 129 |
.file-icon{font-size:32px;flex-shrink:0}
|
|
@@ -137,7 +130,7 @@ header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height
|
|
| 137 |
.progress-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}
|
| 138 |
.progress-title{font-family:var(--mono);font-size:11px;color:var(--muted);letter-spacing:2px;text-transform:uppercase}
|
| 139 |
.progress-stats{font-family:var(--mono);font-size:13px;color:var(--cyan);font-weight:600}
|
| 140 |
-
.progress-track{background:var(--surface);border-radius:6px;height:10px;overflow:hidden;margin-bottom:14px
|
| 141 |
.progress-fill{height:100%;background:linear-gradient(90deg,var(--accent),var(--cyan));border-radius:6px;transition:width .4s ease;width:0%}
|
| 142 |
.progress-fill.warning{background:linear-gradient(90deg,var(--amber),var(--red))}
|
| 143 |
.progress-row{display:flex;justify-content:space-between;font-family:var(--mono);font-size:10px;color:var(--muted);margin-bottom:12px;flex-wrap:wrap;gap:6px}
|
|
@@ -161,14 +154,12 @@ header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height
|
|
| 161 |
.sev-bar-track{flex:1;background:var(--surface);border-radius:3px;height:7px;overflow:hidden}
|
| 162 |
.sev-bar-fill{height:100%;border-radius:3px;transition:width .8s ease}
|
| 163 |
.sev-bar-cnt{font-family:var(--mono);font-size:10px;min-width:36px;text-align:right}
|
| 164 |
-
|
| 165 |
/* REPORT */
|
| 166 |
.processing-area{display:none}
|
| 167 |
.processing-area.visible{display:block}
|
| 168 |
.report-section{display:none}
|
| 169 |
.report-section.visible{display:block;animation:fadeIn .4s ease}
|
| 170 |
-
.completion-banner{background:linear-gradient(135deg,rgba(0,232,122,0.08),rgba(0,200,232,0.05));border:1px solid rgba(0,232,122,0.3);border-radius:14px;padding:20px 24px;display:flex;align-items:center;gap:18px;margin-bottom:22px
|
| 171 |
-
@keyframes bannerIn{from{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:none}}
|
| 172 |
.banner-title{font-family:var(--mono);font-size:15px;font-weight:700;color:var(--accent)}
|
| 173 |
.banner-sub{font-family:var(--mono);font-size:10px;color:var(--muted2);margin-top:4px;line-height:1.8}
|
| 174 |
.export-bar{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:24px}
|
|
@@ -205,7 +196,10 @@ header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height
|
|
| 205 |
.cluster-title{font-family:var(--mono);font-size:11px;font-weight:700;margin-bottom:6px}
|
| 206 |
.cluster-count{font-family:var(--mono);font-size:24px;font-weight:700;margin-bottom:4px}
|
| 207 |
.cluster-sub{font-family:var(--mono);font-size:9px;color:var(--muted);line-height:1.8}
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
| 209 |
.threat-table{width:100%;border-collapse:collapse;font-size:11px}
|
| 210 |
.threat-table th{padding:10px 16px;text-align:left;font-family:var(--mono);font-size:9px;color:var(--muted);background:var(--surface);position:sticky;top:0;letter-spacing:1.5px;z-index:2;border-bottom:1px solid var(--border2)}
|
| 211 |
.threat-table td{padding:9px 16px;border-top:1px solid var(--border);font-family:var(--mono);font-size:11px}
|
|
@@ -222,6 +216,10 @@ header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height
|
|
| 222 |
.risk-gauge-wrap{padding:16px;display:flex;flex-direction:column;align-items:center;gap:8px}
|
| 223 |
.risk-gauge-canvas{width:160px;height:90px;display:block}
|
| 224 |
.risk-label{font-family:var(--mono);font-size:11px;color:var(--muted2);text-align:center}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
::-webkit-scrollbar{width:4px;height:4px}
|
| 226 |
::-webkit-scrollbar-track{background:transparent}
|
| 227 |
::-webkit-scrollbar-thumb{background:var(--surface2);border-radius:2px}
|
|
@@ -296,7 +294,7 @@ header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height
|
|
| 296 |
</div>
|
| 297 |
<div class="panel">
|
| 298 |
<div class="panel-head"><span>// SYSTEM LOG</span></div>
|
| 299 |
-
<div class="term-wrap" id="termWrap"><div class="term-line"><span class="ts">[--:--:--]</span> <span class="info">SentinelNet initialized.
|
| 300 |
</div>
|
| 301 |
</div>
|
| 302 |
</div>
|
|
@@ -338,7 +336,7 @@ header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height
|
|
| 338 |
</div>
|
| 339 |
<div style="margin-top:22px;display:grid;grid-template-columns:repeat(3,1fr);gap:16px">
|
| 340 |
<div class="panel"><div class="panel-head"><span>// SUPPORTED FORMATS</span></div><div style="padding:16px;font-family:var(--mono);font-size:10px;color:var(--muted2);line-height:2.4">β NSL-KDD <span style="color:var(--accent)">with headers</span><br>β NSL-KDD <span style="color:var(--accent)">without headers</span><br>β KDDTrain+.txt / KDDTest+.txt<br>β 42 or 43 columns</div></div>
|
| 341 |
-
<div class="panel"><div class="panel-head"><span>// BATCH PROCESSING</span></div><div style="padding:16px;font-family:var(--mono);font-size:10px;color:var(--muted2);line-height:2.4">β <span style="color:var(--cyan)">100 rows/batch</span> β
|
| 342 |
<div class="panel"><div class="panel-head"><span>// THREAT CLASSES</span></div><div style="padding:16px"><div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><span class="cls-badge cls-normal">normal</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Clean traffic</span></div><div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><span class="cls-badge cls-DoS">DoS</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Denial of service</span></div><div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><span class="cls-badge cls-Probe">Probe</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Reconnaissance</span></div><div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><span class="cls-badge cls-R2L">R2L</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Remote to local</span></div><div style="display:flex;align-items:center;gap:8px"><span class="cls-badge cls-U2R">U2R</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Privilege escalation</span></div></div></div>
|
| 343 |
</div>
|
| 344 |
</div>
|
|
@@ -392,7 +390,7 @@ header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height
|
|
| 392 |
<div class="sev-bar-row"><div class="sev-bar-lbl">Medium</div><div class="sev-bar-track"><div class="sev-bar-fill" style="width:0%;background:var(--cyan)" id="sevbar-Medium"></div></div><div class="sev-bar-cnt" id="sevbc-Medium" style="color:var(--cyan)">0</div></div>
|
| 393 |
<div class="sev-bar-row"><div class="sev-bar-lbl">None</div><div class="sev-bar-track"><div class="sev-bar-fill" style="width:0%;background:var(--accent)" id="sevbar-None"></div></div><div class="sev-bar-cnt" id="sevbc-None" style="color:var(--accent)">0</div></div>
|
| 394 |
</div>
|
| 395 |
-
<div class="mini-card"><div class="mini-card-title">Processing Speed</div><div style="font-family:var(--mono);font-size:24px;font-weight:700;color:var(--accent)" id="csvProcRate">β</div><div style="font-family:var(--mono);font-size:9px;color:var(--muted);margin-top:4px">rows / second
|
| 396 |
</div>
|
| 397 |
</div>
|
| 398 |
|
|
@@ -450,14 +448,20 @@ header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height
|
|
| 450 |
<div class="panel-head"><span>// ATTACK PATTERN CLUSTERS</span><span class="accent" id="clusterCount">0 clusters</span></div>
|
| 451 |
<div class="cluster-grid" id="clusterGrid"></div>
|
| 452 |
</div>
|
|
|
|
| 453 |
<div class="panel">
|
| 454 |
-
<div class="panel-head"><span>// COMPLETE ANALYSIS
|
| 455 |
-
<div
|
| 456 |
<table class="threat-table">
|
| 457 |
<thead><tr><th>ROW</th><th>PROTOCOL</th><th>SERVICE</th><th>SRC BYTES</th><th>DST BYTES</th><th>CLASS</th><th>CONFIDENCE</th><th>SEVERITY</th><th>TRUE LABEL</th></tr></thead>
|
| 458 |
<tbody id="reportTableBody"></tbody>
|
| 459 |
</table>
|
| 460 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
</div>
|
| 462 |
</div>
|
| 463 |
</div>
|
|
@@ -465,56 +469,49 @@ header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height
|
|
| 465 |
</div>
|
| 466 |
|
| 467 |
<script>
|
| 468 |
-
//
|
| 469 |
-
// HARDCODED BACKEND β not exposed in UI
|
| 470 |
-
// Change this to your deployed URL when ready
|
| 471 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 472 |
const BACKEND_URL = '';
|
| 473 |
const BATCH_SIZE = 100;
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
// CONSTANTS
|
| 477 |
-
|
| 478 |
-
const
|
| 479 |
-
const
|
| 480 |
-
const
|
| 481 |
-
const
|
| 482 |
-
const
|
| 483 |
-
const
|
| 484 |
-
const
|
| 485 |
-
const
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 489 |
-
// LIVE MONITOR STATE
|
| 490 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 491 |
let monitorInterval=null,sessionInterval=null,sessionSeconds=0,isRunning=false,usingRealModel=false,packetId=0;
|
| 492 |
const counts={normal:0,DoS:0,Probe:0,R2L:0,U2R:0};
|
| 493 |
let totalPackets=0,totalIntrusions=0,confSum=0,peakClass=null;
|
| 494 |
let confBuckets={90:0,80:0,70:0,low:0};
|
| 495 |
let timelineBuckets=Array(60).fill(0),heatmapCells=Array(60).fill(null);
|
|
|
|
|
|
|
| 496 |
|
| 497 |
-
//
|
| 498 |
-
// CSV STATE
|
| 499 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 500 |
let csvRows=[],csvResults=[],csvIndex=0,csvRunning=false,csvStartTime=null;
|
| 501 |
let csvCounts={normal:0,DoS:0,Probe:0,R2L:0,U2R:0};
|
| 502 |
let csvSevCounts={Critical:0,High:0,Medium:0,None:0};
|
| 503 |
let csvConfSum=0,csvIntrusionCount=0,csvConfHistory=[],csvUsingReal=false,csvFormatInfo='';
|
| 504 |
let batchNum=0,totalBatches=0;
|
|
|
|
|
|
|
| 505 |
|
| 506 |
-
//
|
| 507 |
-
// TAB
|
| 508 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 509 |
function switchTab(name,btn){
|
| 510 |
document.querySelectorAll('.tab-pane').forEach(p=>p.classList.remove('active'));
|
| 511 |
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
| 512 |
-
document.getElementById('tab-'+name).classList.add('active');
|
|
|
|
| 513 |
}
|
| 514 |
|
| 515 |
-
//
|
| 516 |
-
// CSV PARSER β auto header detection
|
| 517 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 518 |
function parseCSV(text){
|
| 519 |
const lines=text.trim().split('\n').filter(l=>l.trim());
|
| 520 |
if(!lines.length)return{rows:[],hasHeader:false,cols:0};
|
|
@@ -534,9 +531,7 @@ function parseCSV(text){
|
|
| 534 |
return{rows,hasHeader:looksLikeHeader,cols:headers.length};
|
| 535 |
}
|
| 536 |
|
| 537 |
-
//
|
| 538 |
-
// LOCAL CLASSIFIER β uses real row features + label
|
| 539 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 540 |
function classifyLocal(row){
|
| 541 |
const rawLabel=(row.label||'').toString().toLowerCase().trim().replace(/\.$/,'');
|
| 542 |
if(rawLabel&&rawLabel!=='unknown'){
|
|
@@ -545,8 +540,9 @@ function classifyLocal(row){
|
|
| 545 |
}
|
| 546 |
const srcBytes=parseFloat(row.src_bytes)||0,flag=(row.flag||'').toUpperCase();
|
| 547 |
const serrorRate=parseFloat(row.serror_rate)||0,rerrorRate=parseFloat(row.rerror_rate)||0;
|
| 548 |
-
const srvCount=parseFloat(row.srv_count)||0
|
| 549 |
const loggedIn=parseFloat(row.logged_in)||0,numRoot=parseFloat(row.num_root)||0,rootShell=parseFloat(row.root_shell)||0;
|
|
|
|
| 550 |
let cls='normal',conf=0.75+Math.random()*0.15;
|
| 551 |
if(['S0','S1','S2','S3','REJ','RSTO','RSTR'].includes(flag)&&count>100){cls='DoS';conf=0.85+Math.random()*0.1;}
|
| 552 |
else if(srcBytes>50000&&(parseFloat(row.duration)||0)<5){cls='DoS';conf=0.80+Math.random()*0.12;}
|
|
@@ -557,28 +553,25 @@ function classifyLocal(row){
|
|
| 557 |
return{predicted_class:cls,severity:SEV_MAP[cls],confidence:+Math.min(0.99,conf).toFixed(4),is_intrusion:cls!=='normal'};
|
| 558 |
}
|
| 559 |
|
| 560 |
-
//
|
| 561 |
-
// API β sends batch to hardcoded backend
|
| 562 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 563 |
async function predictBatch(rows){
|
| 564 |
try{
|
| 565 |
-
const r=await fetch(BACKEND_URL+'/predict',{method:'POST',headers:{'Content-Type':'application/json'
|
| 566 |
-
if(!r.ok)return null;
|
|
|
|
| 567 |
return(d.status==='ok'&&Array.isArray(d.results))?d.results:null;
|
| 568 |
}catch{return null;}
|
| 569 |
}
|
| 570 |
-
|
| 571 |
async function predictSingle(row){
|
| 572 |
try{
|
| 573 |
-
const r=await fetch(BACKEND_URL+'/predict',{method:'POST',headers:{'Content-Type':'application/json'
|
| 574 |
-
if(!r.ok)return null;
|
|
|
|
| 575 |
return(d.status==='ok'&&d.results?.[0])?d.results[0]:null;
|
| 576 |
}catch{return null;}
|
| 577 |
}
|
| 578 |
|
| 579 |
-
//
|
| 580 |
-
// FILE UPLOAD
|
| 581 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 582 |
const uploadZone=document.getElementById('uploadZone');
|
| 583 |
uploadZone.addEventListener('dragover',e=>{e.preventDefault();uploadZone.classList.add('drag-over');});
|
| 584 |
uploadZone.addEventListener('dragleave',()=>uploadZone.classList.remove('drag-over'));
|
|
@@ -589,27 +582,26 @@ function processFileUpload(file){
|
|
| 589 |
const reader=new FileReader();
|
| 590 |
reader.onload=e=>{
|
| 591 |
const{rows,hasHeader,cols}=parseCSV(e.target.result);
|
| 592 |
-
if(!rows.length){alert('Could not parse CSV.
|
| 593 |
csvRows=rows;
|
| 594 |
csvFormatInfo=hasHeader?`With headers Β· ${cols} columns`:`Headerless β auto-mapped Β· ${cols} columns`;
|
| 595 |
totalBatches=Math.ceil(rows.length/BATCH_SIZE);
|
| 596 |
const banner=document.getElementById('formatBanner');
|
| 597 |
banner.style.display='flex';banner.className='format-banner ok';
|
| 598 |
-
banner.innerHTML=`β ${hasHeader?'Headers detected':'Headerless β NSL-KDD auto-mapped'} Β· ${cols} columns Β· ${rows.length.toLocaleString()} rows Β· ${totalBatches} batches
|
| 599 |
document.getElementById('csvUploadSection').style.display='none';
|
| 600 |
document.getElementById('csvProcessingArea').classList.add('visible');
|
| 601 |
setText('csvFileName',file.name);
|
| 602 |
-
setText('csvFileMeta',`${rows.length.toLocaleString()} rows Β· ${(file.size/1024).toFixed(1)} KB Β· ${csvFormatInfo}
|
| 603 |
csvResults=[];csvIndex=0;csvConfSum=0;csvIntrusionCount=0;csvConfHistory=[];batchNum=0;
|
| 604 |
-
Object.keys(csvCounts).forEach(k=>csvCounts[k]=0);
|
|
|
|
| 605 |
csvUsingReal=false;csvStartTime=null;
|
| 606 |
};
|
| 607 |
reader.readAsText(file);
|
| 608 |
}
|
| 609 |
|
| 610 |
-
//
|
| 611 |
-
// CSV ANALYSIS ENGINE β batch mode
|
| 612 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 613 |
async function startCsvAnalysis(){
|
| 614 |
if(csvRunning||csvIndex>=csvRows.length)return;
|
| 615 |
csvRunning=true;csvStartTime=csvStartTime||Date.now();
|
|
@@ -618,40 +610,45 @@ async function startCsvAnalysis(){
|
|
| 618 |
document.getElementById('csvProgressBlock').style.display='block';
|
| 619 |
document.getElementById('csvLiveGrid').style.display='grid';
|
| 620 |
document.getElementById('reportSection').classList.remove('visible');
|
| 621 |
-
document.getElementById('liveDot').className='dot amber';
|
|
|
|
| 622 |
await processBatches();
|
| 623 |
}
|
| 624 |
|
| 625 |
async function processBatches(){
|
| 626 |
-
while(csvRunning
|
| 627 |
const bStart=csvIndex,bEnd=Math.min(csvIndex+BATCH_SIZE,csvRows.length);
|
| 628 |
const batch=csvRows.slice(bStart,bEnd);
|
| 629 |
batchNum++;
|
| 630 |
-
|
| 631 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
|
| 633 |
let results=await predictBatch(batch);
|
| 634 |
if(results){
|
| 635 |
if(!csvUsingReal){csvUsingReal=true;setConnBadge('real');}
|
| 636 |
-
}
|
| 637 |
if(csvUsingReal||batchNum===1){csvUsingReal=false;setConnBadge('local');}
|
| 638 |
results=batch.map(r=>classifyLocal(r));
|
| 639 |
}
|
| 640 |
|
| 641 |
-
// Accumulate results
|
| 642 |
for(let i=0;i<batch.length;i++){
|
| 643 |
const row=batch[i],res=results[i];
|
| 644 |
const{predicted_class:cls,confidence:conf,severity:sev,is_intrusion:isI}=res;
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
csvConfHistory.push(conf);
|
| 648 |
csvCounts[cls]=(csvCounts[cls]||0)+1;
|
| 649 |
csvSevCounts[sev]=(csvSevCounts[sev]||0)+1;
|
| 650 |
-
csvConfSum+=conf;
|
|
|
|
| 651 |
}
|
| 652 |
|
| 653 |
-
// Show
|
| 654 |
-
const feedSlice=batch.slice(-
|
| 655 |
feedSlice.forEach((row,i)=>{
|
| 656 |
const ri=bEnd-feedSlice.length+i;
|
| 657 |
addCsvFeedRow(ri+1,row,results[batch.length-feedSlice.length+i].predicted_class,results[batch.length-feedSlice.length+i].confidence,results[batch.length-feedSlice.length+i].severity);
|
|
@@ -669,12 +666,16 @@ async function processBatches(){
|
|
| 669 |
setText('csvProgressEta',csvIndex<csvRows.length?`ETA: ${formatETA(remaining)}`:'Done!');
|
| 670 |
setText('csvSpeedStat',Math.round(rate)+' rows/s');
|
| 671 |
document.getElementById('csvProgressFill').style.width=pct+'%';
|
| 672 |
-
if(csvIntrusionCount/Math.max(csvIndex,1)>0.5)
|
| 673 |
-
|
|
|
|
|
|
|
|
|
|
| 674 |
setText('csvAlertCount',csvIntrusionCount.toLocaleString()+' THREATS');
|
| 675 |
-
|
|
|
|
| 676 |
}
|
| 677 |
-
if(csvIndex>=csvRows.length)
|
| 678 |
}
|
| 679 |
|
| 680 |
function setConnBadge(type){
|
|
@@ -701,7 +702,8 @@ function addCsvFeedRow(rowNum,row,cls,conf,sev){
|
|
| 701 |
if(cls!=='normal')tr.style.background='rgba(255,61,90,0.025)';
|
| 702 |
tr.innerHTML=`<td style="color:var(--muted)">${rowNum}</td><td style="color:var(--cyan)">${row.protocol_type||'β'}</td><td>${row.service||'β'}</td><td>${(row.src_bytes||0).toLocaleString()}</td><td><span class="cls-badge cls-${cls}">${cls}</span></td><td style="color:${conf>0.9?'var(--accent)':conf>0.8?'var(--cyan)':'var(--amber)'}">${(conf*100).toFixed(1)}%</td><td style="color:${SEV_COLOR[sev]}">β ${sev}</td>`;
|
| 703 |
tbody.insertBefore(tr,tbody.firstChild);
|
| 704 |
-
|
|
|
|
| 705 |
}
|
| 706 |
|
| 707 |
function updateCsvSidebar(rate){
|
|
@@ -717,22 +719,24 @@ function updateCsvSidebar(rate){
|
|
| 717 |
|
| 718 |
function stopCsvAnalysis(){
|
| 719 |
csvRunning=false;
|
| 720 |
-
document.getElementById('csvStartBtn').disabled=false;
|
| 721 |
-
document.getElementById('
|
|
|
|
|
|
|
| 722 |
}
|
| 723 |
|
| 724 |
function finishCsvAnalysis(){
|
| 725 |
csvRunning=false;
|
| 726 |
-
document.getElementById('csvStartBtn').disabled=true;
|
| 727 |
-
document.getElementById('
|
|
|
|
|
|
|
| 728 |
setText('csvProgressEta','Done!');
|
| 729 |
-
setText('csvCurrentRow',`β All ${csvRows.length.toLocaleString()} rows processed
|
| 730 |
-
setTimeout(()=>{exportAnnotatedCSV();buildReport();},
|
| 731 |
}
|
| 732 |
|
| 733 |
-
//
|
| 734 |
-
// REPORT BUILDER
|
| 735 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 736 |
function buildReport(){
|
| 737 |
document.getElementById('reportSection').classList.add('visible');
|
| 738 |
const elapsed=(Date.now()-csvStartTime)/1000;
|
|
@@ -740,12 +744,12 @@ function buildReport(){
|
|
| 740 |
const total=csvResults.length,threats=csvIntrusionCount;
|
| 741 |
const avgConf=(csvConfSum/total*100).toFixed(1)+'%';
|
| 742 |
const rate=(threats/total*100).toFixed(1)+'%';
|
| 743 |
-
// Risk score: weighted formula
|
| 744 |
const riskScore=Math.min(100,Math.round(((csvCounts.DoS||0)*0.4+(csvCounts.U2R||0)*0.35+(csvCounts.R2L||0)*0.15+(csvCounts.Probe||0)*0.1)/Math.max(total,1)*100*6));
|
| 745 |
|
| 746 |
-
setText('bannerSub',`${total.toLocaleString()} rows
|
| 747 |
setText('rmFile',fileName);setText('rmRows',total.toLocaleString());setText('rmDate',new Date().toLocaleString());
|
| 748 |
-
setText('rmModel',csvUsingReal?'Real Random Forest':'Local Simulation');
|
|
|
|
| 749 |
setText('reportSubtitle',`Generated ${new Date().toUTCString()}`);
|
| 750 |
setText('rs-total',total.toLocaleString());setText('rs-threats',threats.toLocaleString());
|
| 751 |
setText('rs-rate',rate);setText('rs-conf',avgConf);setText('rs-risk',riskScore+'/100');
|
|
@@ -754,13 +758,54 @@ function buildReport(){
|
|
| 754 |
const smx=Math.max(...sevs.map(s=>csvSevCounts[s]||0),1);
|
| 755 |
sevs.forEach(s=>{setWidth('rsevbar-'+s,(csvSevCounts[s]||0)/smx*100);setText('rsevbc-'+s,(csvSevCounts[s]||0).toLocaleString());});
|
| 756 |
|
| 757 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 758 |
setTimeout(()=>document.getElementById('reportSection').scrollIntoView({behavior:'smooth',block:'start'}),300);
|
| 759 |
}
|
| 760 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 761 |
function drawBarChart(){
|
| 762 |
const c=document.getElementById('reportBarCanvas'),ctx=c.getContext('2d');
|
| 763 |
-
const W=c.offsetWidth||300,H=160;c.width=W*devicePixelRatio;c.height=H*devicePixelRatio;
|
|
|
|
| 764 |
const classes=['normal','DoS','Probe','R2L','U2R'],colors=['#00e87a','#ff3d5a','#00c8e8','#ffaa00','#b06fff'];
|
| 765 |
const vals=classes.map(c=>csvCounts[c]||0),mx=Math.max(...vals,1);
|
| 766 |
const bw=(W-40)/classes.length,pad=bw*0.18;
|
|
@@ -768,32 +813,38 @@ function drawBarChart(){
|
|
| 768 |
const x=20+i*bw+pad,bW=bw-pad*2,bH=(vals[i]/mx)*(H-30),y=H-10-bH;
|
| 769 |
const g=ctx.createLinearGradient(0,y,0,H-10);g.addColorStop(0,colors[i]);g.addColorStop(1,colors[i]+'33');
|
| 770 |
ctx.fillStyle=g;ctx.beginPath();ctx.roundRect(x,y,bW,Math.max(bH,1),4);ctx.fill();
|
| 771 |
-
ctx.fillStyle='rgba(90,122,153,.9)';ctx.font='9px IBM Plex Mono';ctx.textAlign='center';
|
|
|
|
| 772 |
if(vals[i]>0){ctx.fillStyle=colors[i];ctx.fillText(vals[i].toLocaleString(),x+bW/2,y-4);}
|
| 773 |
});
|
| 774 |
}
|
| 775 |
|
| 776 |
function drawConfWave(){
|
| 777 |
const c=document.getElementById('reportConfCanvas'),ctx=c.getContext('2d');
|
| 778 |
-
const W=c.offsetWidth||300,H=160;c.width=W*devicePixelRatio;c.height=H*devicePixelRatio;
|
|
|
|
| 779 |
const data=csvConfHistory;if(data.length<2)return;
|
| 780 |
-
const
|
| 781 |
-
const sampled=data.filter((_,i)=>i%step===0);
|
| 782 |
-
const xStep=(W-20)/Math.max(sampled.length-1,1),mn=0.5,mx=1;
|
| 783 |
ctx.strokeStyle='rgba(0,200,120,.06)';ctx.lineWidth=1;
|
| 784 |
[.25,.5,.75,1].forEach(f=>{const y=10+(1-f)*(H-20);ctx.beginPath();ctx.moveTo(10,y);ctx.lineTo(W-10,y);ctx.stroke();});
|
| 785 |
-
ctx.beginPath();
|
| 786 |
-
ctx.lineTo(10+(
|
| 787 |
-
const g=ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,'rgba(0,200,232,.15)');g.addColorStop(1,'rgba(0,200,232,.01)');
|
| 788 |
-
ctx.
|
|
|
|
|
|
|
|
|
|
| 789 |
const avg=csvConfSum/csvResults.length,avgY=10+(1-(avg-mn)/(mx-mn))*(H-20);
|
| 790 |
-
ctx.beginPath();ctx.setLineDash([4,3]);ctx.strokeStyle='rgba(0,232,122,.6)';ctx.lineWidth=1;
|
| 791 |
-
ctx.
|
|
|
|
|
|
|
| 792 |
}
|
| 793 |
|
| 794 |
function drawIntensity(){
|
| 795 |
const c=document.getElementById('reportIntensityCanvas'),ctx=c.getContext('2d');
|
| 796 |
-
const W=c.offsetWidth||300,H=160;c.width=W*devicePixelRatio;c.height=H*devicePixelRatio;
|
|
|
|
| 797 |
const wSize=Math.max(5,Math.floor(csvResults.length/60));
|
| 798 |
const windows=[];
|
| 799 |
for(let i=0;i<csvResults.length;i+=wSize){const sl=csvResults.slice(i,i+wSize);windows.push(sl.filter(r=>r.isI).length/sl.length);}
|
|
@@ -803,9 +854,10 @@ function drawIntensity(){
|
|
| 803 |
[.25,.5,.75,1].forEach(f=>{const y=10+(1-f)*(H-20);ctx.beginPath();ctx.moveTo(10,y);ctx.lineTo(W-10,y);ctx.stroke();});
|
| 804 |
ctx.beginPath();windows.forEach((v,i)=>{const x=10+i*xStep,y=10+(1-v/mx)*(H-20);i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});
|
| 805 |
ctx.lineTo(10+(windows.length-1)*xStep,H-10);ctx.lineTo(10,H-10);ctx.closePath();
|
| 806 |
-
const g=ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,'rgba(255,61,90,.28)');g.addColorStop(1,'rgba(255,61,90,.02)');
|
| 807 |
-
ctx.
|
| 808 |
-
ctx.
|
|
|
|
| 809 |
}
|
| 810 |
|
| 811 |
function drawProto(){
|
|
@@ -814,13 +866,14 @@ function drawProto(){
|
|
| 814 |
csvResults.forEach(({row,isI})=>{const p=row.protocol_type||'unknown';pc[p]=(pc[p]||0)+1;if(isI)pt[p]=(pt[p]||0)+1;});
|
| 815 |
const sorted=Object.entries(pc).sort((a,b)=>b[1]-a[1]);
|
| 816 |
const mx=Math.max(...sorted.map(([,v])=>v),1);
|
|
|
|
| 817 |
sorted.forEach(([proto,cnt])=>{
|
| 818 |
const tc=pt[proto]||0,tp=cnt>0?Math.round(tc/cnt*100):0;
|
| 819 |
const div=document.createElement('div');div.className='proto-row';
|
| 820 |
div.innerHTML=`<div class="proto-lbl">${proto}</div><div class="proto-track"><div class="proto-fill" style="width:${cnt/mx*100}%;background:${tc>0?'var(--red)':'var(--accent)'}"></div></div><div class="proto-cnt">${cnt.toLocaleString()} <span style="color:${tc>0?'var(--red)':'var(--muted)'}">${tp}% threat</span></div>`;
|
| 821 |
-
|
| 822 |
});
|
| 823 |
-
|
| 824 |
}
|
| 825 |
|
| 826 |
function drawServices(){
|
|
@@ -829,23 +882,26 @@ function drawServices(){
|
|
| 829 |
csvResults.forEach(({row,isI})=>{const s=row.service||'unknown';sc[s]=(sc[s]||0)+1;if(isI)st[s]=(st[s]||0)+1;});
|
| 830 |
const sorted=Object.entries(sc).sort((a,b)=>b[1]-a[1]).slice(0,8);
|
| 831 |
const mx=Math.max(...sorted.map(([,v])=>v),1);
|
|
|
|
| 832 |
sorted.forEach(([svc,cnt])=>{
|
| 833 |
const hot=(st[svc]||0)>cnt*0.3;
|
| 834 |
const div=document.createElement('div');div.className='svc-row';
|
| 835 |
-
div.innerHTML=`<span class="svc-name" style="color:${hot?'var(--red)':'var(--cyan)'}">${svc}</span><span style="flex:1;margin:0 8px;background:var(--surface);border-radius:2px;height:4px;display:block;overflow:hidden"><span style="display:block;height:100%;width:${Math.round(cnt/mx*100)}%;background:${hot?'var(--red)':'var(--cyan)'};border-radius:2px
|
| 836 |
-
|
| 837 |
});
|
|
|
|
| 838 |
}
|
| 839 |
|
| 840 |
function drawGauge(score){
|
| 841 |
const c=document.getElementById('riskGaugeCanvas'),ctx=c.getContext('2d');
|
| 842 |
-
const W=160,H=90;c.width=W*devicePixelRatio;c.height=H*devicePixelRatio;
|
|
|
|
| 843 |
const cx=W/2,cy=H-8,r=66;
|
| 844 |
ctx.beginPath();ctx.arc(cx,cy,r,Math.PI,2*Math.PI);ctx.strokeStyle='rgba(255,255,255,.05)';ctx.lineWidth=14;ctx.lineCap='round';ctx.stroke();
|
| 845 |
[[0,.33,'#00e87a'],[.33,.66,'#ffaa00'],[.66,1,'#ff3d5a']].forEach(([from,to,col])=>{ctx.beginPath();ctx.arc(cx,cy,r,Math.PI+from*Math.PI,Math.PI+to*Math.PI);ctx.strokeStyle=col+'55';ctx.lineWidth=14;ctx.stroke();});
|
| 846 |
-
const
|
| 847 |
-
ctx.beginPath();ctx.arc(cx,cy,r,Math.PI,Math.PI+(score/100)*Math.PI);ctx.strokeStyle=
|
| 848 |
-
ctx.fillStyle=
|
| 849 |
ctx.fillStyle='rgba(90,122,153,.8)';ctx.font='9px IBM Plex Mono';ctx.fillText('/100',cx,cy-2);
|
| 850 |
const lbl=score<20?'LOW':score<40?'MODERATE':score<60?'ELEVATED':score<80?'HIGH':'CRITICAL';
|
| 851 |
setText('riskLabel',`${lbl} RISK Β· Score ${score}/100`);
|
|
@@ -853,42 +909,35 @@ function drawGauge(score){
|
|
| 853 |
|
| 854 |
function buildClusters(){
|
| 855 |
const grid=document.getElementById('clusterGrid');grid.innerHTML='';let cnt=0;
|
|
|
|
| 856 |
['DoS','Probe','R2L','U2R','normal'].forEach(cls=>{
|
| 857 |
const c=csvCounts[cls]||0;if(!c)return;cnt++;
|
| 858 |
const col=CLASS_COLOR[cls],res=csvResults.filter(r=>r.cls===cls);
|
| 859 |
const avgConf=res.reduce((s,r)=>s+r.conf,0)/c;
|
| 860 |
const div=document.createElement('div');div.className=`cluster-card ${cls}`;
|
| 861 |
div.innerHTML=`<div class="cluster-title" style="color:${col}">${cls} Traffic</div><div class="cluster-count" style="color:${col}">${c.toLocaleString()}</div><div class="cluster-sub">Severity: ${SEV_MAP[cls]}<br>Avg confidence: ${(avgConf*100).toFixed(1)}%<br>${(c/csvResults.length*100).toFixed(1)}% of dataset</div>`;
|
| 862 |
-
|
| 863 |
});
|
| 864 |
-
|
|
|
|
| 865 |
setText('clusterCount',cnt+' clusters');
|
| 866 |
}
|
| 867 |
|
| 868 |
-
|
| 869 |
-
const tbody=document.getElementById('reportTableBody');tbody.innerHTML='';
|
| 870 |
-
setText('reportRowCount',csvResults.length.toLocaleString()+' rows');
|
| 871 |
-
csvResults.forEach(({rowNum,row,cls,conf,sev,isI})=>{
|
| 872 |
-
const tr=document.createElement('tr');if(isI)tr.className='row-intrusion';
|
| 873 |
-
tr.innerHTML=`<td style="color:var(--muted)">${rowNum}</td><td style="color:var(--cyan)">${row.protocol_type||'β'}</td><td>${row.service||'β'}</td><td>${(row.src_bytes||0).toLocaleString()}</td><td>${(row.dst_bytes||0).toLocaleString()}</td><td><span class="cls-badge cls-${cls}">${cls}</span></td><td style="color:${conf>0.9?'var(--accent)':conf>0.8?'var(--cyan)':'var(--amber)'}">${(conf*100).toFixed(1)}%</td><td style="color:${SEV_COLOR[sev]}">β ${sev}</td><td style="color:var(--muted);font-size:10px">${row.label||'β'}</td>`;
|
| 874 |
-
tbody.appendChild(tr);
|
| 875 |
-
});
|
| 876 |
-
}
|
| 877 |
-
|
| 878 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 879 |
-
// EXPORTS
|
| 880 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 881 |
function exportAnnotatedCSV(){
|
| 882 |
if(!csvResults.length)return;
|
| 883 |
const headers=Object.keys(csvResults[0].row).concat(['predicted_class','severity','confidence','is_intrusion']);
|
| 884 |
const lines=[headers.join(',')];
|
| 885 |
-
csvResults.forEach(({row,cls,sev,conf,isI})=>{
|
|
|
|
|
|
|
|
|
|
| 886 |
downloadFile('sentinelnet_annotated.csv',lines.join('\n'),'text/csv');
|
| 887 |
}
|
| 888 |
|
| 889 |
function exportJSON(){
|
| 890 |
if(!csvResults.length){alert('No results.');return;}
|
| 891 |
-
const data={meta:{file:document.getElementById('csvFileName').textContent,date:new Date().toISOString(),total:csvResults.length,threats:csvIntrusionCount,threatRate:(csvIntrusionCount/csvResults.length*100).toFixed(1)+'%',model:csvUsingReal?'Real RF':'Local Sim',batchSize:BATCH_SIZE
|
| 892 |
downloadFile('sentinelnet_results.json',JSON.stringify(data,null,2),'application/json');
|
| 893 |
}
|
| 894 |
|
|
@@ -904,72 +953,26 @@ function exportPDFReport(){
|
|
| 904 |
const riskLbl=riskScore<20?'LOW':riskScore<40?'MODERATE':riskScore<60?'ELEVATED':riskScore<80?'HIGH':'CRITICAL';
|
| 905 |
const colors={normal:'#00e87a',DoS:'#ff3d5a',Probe:'#00c8e8',R2L:'#ffaa00',U2R:'#b06fff'};
|
| 906 |
const sevColors={None:'#00e87a',Medium:'#00c8e8',High:'#ffaa00',Critical:'#ff3d5a'};
|
| 907 |
-
|
| 908 |
const distRows=['normal','DoS','Probe','R2L','U2R'].map(c=>`<tr><td style="color:${colors[c]}">${c}</td><td>${(csvCounts[c]||0).toLocaleString()}</td><td>${total>0?((csvCounts[c]||0)/total*100).toFixed(1):'0'}%</td><td style="color:${sevColors[SEV_MAP[c]]}">${SEV_MAP[c]}</td></tr>`).join('');
|
| 909 |
-
|
| 910 |
const pc={};csvResults.forEach(({row})=>{const p=row.protocol_type||'unknown';pc[p]=(pc[p]||0)+1;});
|
| 911 |
const protoRows=Object.entries(pc).sort((a,b)=>b[1]-a[1]).map(([p,c])=>`<tr><td>${p}</td><td>${c.toLocaleString()}</td><td>${(c/total*100).toFixed(1)}%</td></tr>`).join('');
|
| 912 |
-
|
| 913 |
const topThreats=csvResults.filter(r=>r.isI).slice(0,100);
|
| 914 |
const threatRows=topThreats.map(({rowNum,row,cls,conf,sev})=>`<tr><td>${rowNum}</td><td>${row.protocol_type||'β'}</td><td>${row.service||'β'}</td><td>${(row.src_bytes||0).toLocaleString()}</td><td style="color:${colors[cls]};font-weight:bold">${cls}</td><td>${(conf*100).toFixed(1)}%</td><td style="color:${sevColors[sev]}">${sev}</td></tr>`).join('');
|
| 915 |
-
|
| 916 |
const html=`<!DOCTYPE html><html><head><meta charset="UTF-8"/><title>SentinelNet Report</title>
|
| 917 |
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600;700&display=swap" rel="stylesheet">
|
| 918 |
-
<style>
|
| 919 |
-
*{box-sizing:border-box;margin:0;padding:0}
|
| 920 |
-
body{font-family:'IBM Plex Mono',monospace;background:#04080d;color:#d8eeff;padding:40px;font-size:11px}
|
| 921 |
-
h1{color:#00e87a;font-size:22px;border-bottom:2px solid #00e87a;padding-bottom:12px;margin-bottom:8px}
|
| 922 |
-
h2{color:#00c8e8;font-size:13px;margin:28px 0 12px;letter-spacing:2px;text-transform:uppercase}
|
| 923 |
-
.meta{color:#4a6a88;font-size:10px;margin-bottom:32px;line-height:2.2}
|
| 924 |
-
.grid{display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin-bottom:32px}
|
| 925 |
-
.stat{background:#080e16;border:1px solid rgba(0,210,130,0.15);border-radius:10px;padding:16px;text-align:center}
|
| 926 |
-
.stat-val{font-size:24px;font-weight:700;margin-bottom:5px}
|
| 927 |
-
.stat-lbl{font-size:8px;color:#4a6a88;letter-spacing:2px;text-transform:uppercase}
|
| 928 |
-
table{width:100%;border-collapse:collapse;margin-bottom:28px}
|
| 929 |
-
th{background:#0d1520;padding:9px 12px;text-align:left;color:#4a6a88;font-size:8px;letter-spacing:1.5px;border-bottom:1px solid rgba(0,210,130,0.2)}
|
| 930 |
-
td{padding:7px 12px;border-top:1px solid rgba(0,210,130,0.06)}
|
| 931 |
-
.risk-box{background:#0d1520;border:1px solid rgba(0,210,130,0.2);border-radius:12px;padding:20px;text-align:center;margin-bottom:28px}
|
| 932 |
-
.risk-num{font-size:40px;font-weight:700;color:${riskScore<33?'#00e87a':riskScore<66?'#ffaa00':'#ff3d5a'}}
|
| 933 |
-
.footer{margin-top:40px;padding-top:14px;border-top:1px solid rgba(0,210,130,0.12);color:#4a6a88;font-size:9px;text-align:center}
|
| 934 |
-
@media print{body{background:#04080d!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}}
|
| 935 |
-
</style></head><body>
|
| 936 |
<h1>π‘ SentinelNet β Threat Analysis Report</h1>
|
| 937 |
-
<div class="meta">
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
</div>
|
| 945 |
-
<div class="grid">
|
| 946 |
-
<div class="stat"><div class="stat-val" style="color:#00c8e8">${total.toLocaleString()}</div><div class="stat-lbl">Total Rows</div></div>
|
| 947 |
-
<div class="stat"><div class="stat-val" style="color:#ff3d5a">${threats.toLocaleString()}</div><div class="stat-lbl">Threats Found</div></div>
|
| 948 |
-
<div class="stat"><div class="stat-val" style="color:#ffaa00">${rate}%</div><div class="stat-lbl">Threat Rate</div></div>
|
| 949 |
-
<div class="stat"><div class="stat-val" style="color:#00e87a">${avgConf}%</div><div class="stat-lbl">Avg Confidence</div></div>
|
| 950 |
-
<div class="stat"><div class="stat-val" style="color:${riskScore<33?'#00e87a':riskScore<66?'#ffaa00':'#ff3d5a'}">${riskScore}/100</div><div class="stat-lbl">Risk Score</div></div>
|
| 951 |
-
</div>
|
| 952 |
-
<div class="risk-box">
|
| 953 |
-
<div class="risk-num">${riskScore}</div>
|
| 954 |
-
<div style="font-size:13px;color:${riskScore<33?'#00e87a':riskScore<66?'#ffaa00':'#ff3d5a'};margin-top:4px">${riskLbl} RISK</div>
|
| 955 |
-
<div style="color:#4a6a88;font-size:10px;margin-top:8px">Weighted: DoSΓ0.4 Β· U2RΓ0.35 Β· R2LΓ0.15 Β· ProbeΓ0.1</div>
|
| 956 |
-
</div>
|
| 957 |
-
<h2>Attack Class Distribution</h2>
|
| 958 |
-
<table><thead><tr><th>CLASS</th><th>COUNT</th><th>PERCENTAGE</th><th>SEVERITY</th></tr></thead><tbody>${distRows}</tbody></table>
|
| 959 |
-
<h2>Protocol Breakdown</h2>
|
| 960 |
-
<table><thead><tr><th>PROTOCOL</th><th>COUNT</th><th>PERCENTAGE</th></tr></thead><tbody>${protoRows}</tbody></table>
|
| 961 |
-
<h2>Severity Summary</h2>
|
| 962 |
-
<table><thead><tr><th>SEVERITY</th><th>COUNT</th><th>PERCENTAGE</th></tr></thead><tbody>
|
| 963 |
-
${['Critical','High','Medium','None'].map(s=>`<tr><td style="color:${sevColors[s]}">${s}</td><td>${(csvSevCounts[s]||0).toLocaleString()}</td><td>${((csvSevCounts[s]||0)/total*100).toFixed(1)}%</td></tr>`).join('')}
|
| 964 |
-
</tbody></table>
|
| 965 |
-
<h2>Detected Threats β Top 100</h2>
|
| 966 |
-
<table><thead><tr><th>ROW</th><th>PROTOCOL</th><th>SERVICE</th><th>SRC BYTES</th><th>CLASS</th><th>CONFIDENCE</th><th>SEVERITY</th></tr></thead>
|
| 967 |
-
<tbody>${threatRows||'<tr><td colspan="7" style="color:#00e87a;text-align:center;padding:20px">No threats detected β clean dataset</td></tr>'}</tbody></table>
|
| 968 |
-
<div class="footer">Generated by SentinelNet v4.0 Β· ${dateStr} Β· NSL-KDD Intrusion Detection Β· Backend: ${BACKEND_URL}</div>
|
| 969 |
</body></html>`;
|
| 970 |
-
|
| 971 |
const win=window.open('','_blank','width=1100,height=900');
|
| 972 |
-
if(!win){alert('Pop-up blocked.
|
| 973 |
win.document.write(html);win.document.close();setTimeout(()=>win.print(),900);
|
| 974 |
}
|
| 975 |
|
|
@@ -979,9 +982,10 @@ function downloadFile(filename,content,type){
|
|
| 979 |
}
|
| 980 |
|
| 981 |
function resetCsvTab(){
|
| 982 |
-
csvRunning=false;
|
| 983 |
-
|
| 984 |
-
Object.keys(csvCounts).forEach(k=>csvCounts[k]=0);
|
|
|
|
| 985 |
csvStartTime=null;csvUsingReal=false;csvFormatInfo='';totalBatches=0;
|
| 986 |
document.getElementById('csvUploadSection').style.display='';
|
| 987 |
document.getElementById('csvProcessingArea').classList.remove('visible');
|
|
@@ -989,17 +993,18 @@ function resetCsvTab(){
|
|
| 989 |
document.getElementById('csvLiveGrid').style.display='none';
|
| 990 |
document.getElementById('reportSection').classList.remove('visible');
|
| 991 |
document.getElementById('csvFeedBody').innerHTML='';
|
|
|
|
| 992 |
document.getElementById('csvFileInput').value='';
|
| 993 |
document.getElementById('formatBanner').style.display='none';
|
| 994 |
document.getElementById('liveDot').className='dot';setText('liveStatus','IDLE');
|
| 995 |
-
document.getElementById('csvStartBtn').disabled=false;
|
|
|
|
| 996 |
document.getElementById('batchStatus').innerHTML='';
|
| 997 |
-
document.getElementById('csvProgressFill').style.width='0%';
|
|
|
|
| 998 |
}
|
| 999 |
|
| 1000 |
-
//
|
| 1001 |
-
// LIVE MONITOR
|
| 1002 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1003 |
function generatePacket(){
|
| 1004 |
const label=LABEL_POOL[Math.floor(Math.random()*LABEL_POOL.length)];
|
| 1005 |
const isAtk=label!=='normal';
|
|
@@ -1014,7 +1019,7 @@ async function tick(){
|
|
| 1014 |
const packet=generatePacket();
|
| 1015 |
let result;
|
| 1016 |
const api=await predictSingle(packet);
|
| 1017 |
-
if(api){result=api;if(!usingRealModel){usingRealModel=true;document.getElementById('connBadge').textContent='β REAL MODEL';document.getElementById('connBadge').className='real';document.getElementById('sum-model').textContent='REAL RF';termLog('info','Connected to model
|
| 1018 |
else{result=classifyLocal(packet);if(usingRealModel){usingRealModel=false;document.getElementById('connBadge').textContent='β LOCAL SIM';document.getElementById('connBadge').className='local';document.getElementById('sum-model').textContent='LOCAL';}}
|
| 1019 |
packetId++;totalPackets++;
|
| 1020 |
const cls=result.predicted_class,conf=result.confidence,sev=result.severity,isI=result.is_intrusion;
|
|
@@ -1027,20 +1032,27 @@ async function tick(){
|
|
| 1027 |
heatmapCells.shift();heatmapCells.push(cls);
|
| 1028 |
addFeedRow(packet,cls,sev,conf);
|
| 1029 |
if(cls==='U2R')termLog('crit',`U2R ALERT β Privilege escalation! Conf: ${(conf*100).toFixed(1)}%`);
|
| 1030 |
-
else if(cls==='DoS'&&Math.random()<0.15)termLog('warn',`DoS β ${packet.service} flood
|
| 1031 |
-
else if(cls==='
|
| 1032 |
-
|
| 1033 |
-
|
|
|
|
|
|
|
| 1034 |
}
|
| 1035 |
|
|
|
|
|
|
|
|
|
|
| 1036 |
function addFeedRow(packet,cls,sev,conf){
|
| 1037 |
const tbody=document.getElementById('feedBody');
|
| 1038 |
const now=new Date(),ts=`${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
| 1039 |
const tr=document.createElement('tr');tr.className='new-row';
|
| 1040 |
tr.innerHTML=`<td style="color:var(--muted)">${packetId}</td><td style="color:var(--muted)">${ts}</td><td style="color:var(--cyan)">${packet.protocol_type}</td><td>${packet.src_bytes.toLocaleString()}</td><td><span class="cls-badge cls-${cls}">${cls}</span></td><td style="color:${conf>0.9?'var(--accent)':conf>0.8?'var(--cyan)':'var(--amber)'}">${(conf*100).toFixed(1)}%</td><td style="color:${SEV_COLOR[sev]}">β ${sev}</td>`;
|
| 1041 |
tbody.insertBefore(tr,tbody.firstChild);
|
| 1042 |
-
|
| 1043 |
-
|
|
|
|
|
|
|
| 1044 |
document.getElementById('alertCount').textContent=totalIntrusions+' ALERTS';
|
| 1045 |
}
|
| 1046 |
|
|
@@ -1061,23 +1073,35 @@ function updateBars(){
|
|
| 1061 |
}
|
| 1062 |
function updateConfBars(){
|
| 1063 |
const mx=Math.max(...Object.values(confBuckets),1);
|
| 1064 |
-
setWidth('conf-90',confBuckets[90]/mx*100);setWidth('conf-80',confBuckets[80]/mx*100);
|
|
|
|
| 1065 |
setText('cbc-90',confBuckets[90]);setText('cbc-80',confBuckets[80]);setText('cbc-70',confBuckets[70]);setText('cbc-low',confBuckets.low);
|
| 1066 |
setText('avgConf','avg: '+(totalPackets>0?(confSum/totalPackets*100).toFixed(1)+'%':'β'));
|
| 1067 |
}
|
| 1068 |
function updateTimeline(){
|
| 1069 |
const canvas=document.getElementById('tlCanvas');if(!canvas)return;
|
| 1070 |
const ctx=canvas.getContext('2d');const W=canvas.offsetWidth;const H=80;
|
| 1071 |
-
|
|
|
|
|
|
|
| 1072 |
const data=timelineBuckets;const mx=Math.max(...data,1);const step=W/data.length;
|
| 1073 |
-
ctx.strokeStyle='rgba(0,200,120,.06)';ctx.lineWidth=1;
|
| 1074 |
-
|
| 1075 |
-
ctx.beginPath();ctx.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1076 |
}
|
| 1077 |
const HM={normal:'rgba(0,232,122,0.4)',DoS:'rgba(255,61,90,0.65)',Probe:'rgba(0,200,232,0.5)',R2L:'rgba(255,170,0,0.55)',U2R:'rgba(176,111,255,0.75)',null:'rgba(255,255,255,0.04)'};
|
| 1078 |
function updateHeatmap(){
|
| 1079 |
const grid=document.getElementById('heatmap');
|
| 1080 |
-
if(!grid.children.length){
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1081 |
heatmapCells.forEach((cls,i)=>{grid.children[i].style.background=HM[cls]||HM[null];});
|
| 1082 |
}
|
| 1083 |
function updateSummary(){
|
|
@@ -1091,51 +1115,62 @@ function termLog(type,msg){
|
|
| 1091 |
const div=document.createElement('div');div.className='term-line';
|
| 1092 |
div.innerHTML=`<span class="ts">[${ts}]</span> <span class="${type}">${msg}</span>`;
|
| 1093 |
wrap.appendChild(div);wrap.scrollTop=wrap.scrollHeight;
|
| 1094 |
-
while(wrap.children.length>
|
| 1095 |
}
|
| 1096 |
const CLS_TO_MC={DoS:'mc-intrusions',Probe:'mc-probe',R2L:'mc-intrusions',U2R:'mc-u2r',normal:'mc-normal'};
|
| 1097 |
function flashMetric(cls){const el=document.getElementById(CLS_TO_MC[cls]||'mc-total');if(!el)return;el.classList.remove('flash');void el.offsetWidth;el.classList.add('flash');}
|
| 1098 |
|
| 1099 |
function startMonitor(){
|
| 1100 |
-
if(isRunning)return;isRunning=true;
|
|
|
|
| 1101 |
document.getElementById('startBtn').disabled=true;document.getElementById('stopBtn').disabled=false;
|
| 1102 |
-
|
| 1103 |
document.getElementById('connBadge').textContent='β³ CONNECTING';document.getElementById('connBadge').className='idle';
|
| 1104 |
-
termLog('info','Monitor started
|
| 1105 |
-
termLog('info',`Speed: ${(1000/speed).toFixed(1)} pkt/s Β· Backend: ${BACKEND_URL}`);
|
| 1106 |
monitorInterval=setInterval(tick,speed);
|
| 1107 |
-
sessionInterval=setInterval(()=>{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1108 |
}
|
| 1109 |
function stopMonitor(){
|
| 1110 |
-
if(!isRunning)return;isRunning=false;
|
|
|
|
| 1111 |
document.getElementById('startBtn').disabled=false;document.getElementById('stopBtn').disabled=true;
|
| 1112 |
-
|
| 1113 |
document.getElementById('connBadge').textContent='β STOPPED';document.getElementById('connBadge').className='idle';
|
| 1114 |
-
termLog('warn',`Stopped. ${
|
| 1115 |
}
|
| 1116 |
function clearAll(){
|
| 1117 |
stopMonitor();Object.keys(counts).forEach(k=>counts[k]=0);
|
| 1118 |
-
totalPackets=0;totalIntrusions=0;confSum=0;packetId=0;
|
| 1119 |
-
|
|
|
|
|
|
|
| 1120 |
setText('sessionClock','Session: 00:00');setText('liveStatus','IDLE');
|
| 1121 |
-
document.getElementById('liveDot').className='dot';
|
| 1122 |
-
document.getElementById('
|
| 1123 |
-
document.getElementById('
|
| 1124 |
-
document.getElementById('
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1125 |
updateMetrics();updateBars();updateConfBars();updateHeatmap();
|
| 1126 |
-
['sum-rate','sum-conf','sum-peak'].forEach(id=>setText(id,'β'));
|
|
|
|
| 1127 |
}
|
| 1128 |
|
| 1129 |
-
//
|
| 1130 |
-
// UTILS
|
| 1131 |
-
// ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1132 |
function setText(id,v){const el=document.getElementById(id);if(el)el.textContent=v;}
|
| 1133 |
function setWidth(id,pct){const el=document.getElementById(id);if(el)el.style.width=pct+'%';}
|
| 1134 |
function pad(n){return String(n).padStart(2,'0');}
|
| 1135 |
function formatETA(secs){if(secs<60)return secs.toFixed(0)+'s';return`${Math.floor(secs/60)}m ${pad(Math.floor(secs%60))}s`;}
|
| 1136 |
|
| 1137 |
-
// Init
|
| 1138 |
-
updateHeatmap();updateTimeline();
|
| 1139 |
</script>
|
| 1140 |
</body>
|
| 1141 |
</html>
|
|
|
|
| 21 |
body{background:var(--bg);color:var(--text);font-family:var(--sans);min-height:100vh;overflow-x:hidden}
|
| 22 |
body::before{content:'';position:fixed;inset:0;pointer-events:none;z-index:0;background:radial-gradient(ellipse 60% 40% at 10% 20%,rgba(0,232,122,0.04) 0%,transparent 60%),radial-gradient(ellipse 50% 50% at 90% 80%,rgba(0,200,232,0.03) 0%,transparent 60%),repeating-linear-gradient(0deg,transparent,transparent 40px,rgba(0,210,130,0.015) 40px,rgba(0,210,130,0.015) 41px),repeating-linear-gradient(90deg,transparent,transparent 40px,rgba(0,210,130,0.015) 40px,rgba(0,210,130,0.015) 41px)}
|
| 23 |
.app{position:relative;z-index:1;max-width:1500px;margin:0 auto;padding:0 24px 80px}
|
|
|
|
| 24 |
header{display:flex;align-items:center;justify-content:space-between;padding:20px 0;border-bottom:1px solid var(--border2);margin-bottom:0;position:relative}
|
| 25 |
header::after{content:'';position:absolute;bottom:-1px;left:0;width:200px;height:1px;background:linear-gradient(90deg,var(--accent),transparent)}
|
| 26 |
.logo{display:flex;align-items:center;gap:14px}
|
|
|
|
| 35 |
.dot.green{background:var(--accent);box-shadow:0 0 8px var(--accent)}
|
| 36 |
@keyframes blink{0%,100%{opacity:1}50%{opacity:0.2}}
|
| 37 |
#sessionClock{font-family:var(--mono);font-size:11px;color:var(--muted2);background:var(--surface);padding:6px 12px;border-radius:6px;border:1px solid var(--border)}
|
|
|
|
| 38 |
.tab-nav{display:flex;border-bottom:1px solid var(--border2);margin-bottom:28px}
|
| 39 |
.tab-btn{padding:16px 32px;font-family:var(--mono);font-size:11px;font-weight:600;letter-spacing:2px;text-transform:uppercase;color:var(--muted);background:transparent;border:none;cursor:pointer;transition:color .2s;border-bottom:2px solid transparent;margin-bottom:-1px}
|
| 40 |
.tab-btn:hover{color:var(--muted2)}
|
|
|
|
| 43 |
.tab-pane{display:none}
|
| 44 |
.tab-pane.active{display:block;animation:fadeIn .25s ease}
|
| 45 |
@keyframes fadeIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
|
|
|
|
| 46 |
.panel{background:var(--bg2);border:1px solid var(--border);border-radius:14px;overflow:hidden}
|
| 47 |
.panel-head{padding:12px 18px;font-family:var(--mono);font-size:10px;color:var(--muted);border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;letter-spacing:1.5px;text-transform:uppercase;background:rgba(0,0,0,0.2)}
|
| 48 |
.panel-head .accent{color:var(--accent)}.panel-head .red{color:var(--red)}.panel-head .cyan{color:var(--cyan)}.panel-head .amber{color:var(--amber)}
|
|
|
|
| 49 |
.config-strip{display:flex;gap:10px;align-items:center;background:var(--bg2);border:1px solid var(--border2);border-radius:12px;padding:14px 18px;margin-bottom:22px;flex-wrap:wrap}
|
| 50 |
.speed-wrap{display:flex;align-items:center;gap:8px;font-family:var(--mono);font-size:11px;color:var(--muted)}
|
| 51 |
.speed-wrap select{background:var(--surface);border:1px solid var(--border2);border-radius:7px;color:var(--cyan);font-family:var(--mono);font-size:11px;padding:7px 12px;outline:none;cursor:pointer}
|
|
|
|
| 62 |
#connBadge.real{background:rgba(0,232,122,0.1);color:var(--accent);border:1px solid rgba(0,232,122,0.2)}
|
| 63 |
#connBadge.local{background:rgba(255,170,0,0.1);color:var(--amber);border:1px solid rgba(255,170,0,0.2)}
|
| 64 |
#connBadge.idle{background:var(--surface);color:var(--muted);border:1px solid var(--border)}
|
|
|
|
| 65 |
.metrics{display:grid;grid-template-columns:repeat(6,1fr);gap:12px;margin-bottom:22px}
|
| 66 |
@media(max-width:1000px){.metrics{grid-template-columns:repeat(3,1fr)}}
|
| 67 |
+
.mc{background:var(--bg2);border:1px solid var(--border);border-radius:12px;padding:18px 16px;position:relative;overflow:hidden;transition:border-color .3s}
|
| 68 |
.mc.flash{border-color:var(--accent);animation:mcflash .6s ease-out}
|
| 69 |
@keyframes mcflash{0%{box-shadow:0 0 16px rgba(0,232,122,0.5)}100%{box-shadow:none}}
|
| 70 |
.mc-label{font-family:var(--mono);font-size:9px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin-bottom:10px}
|
|
|
|
| 73 |
.mc-sub{font-size:10px;color:var(--muted);margin-top:6px;font-family:var(--mono)}
|
| 74 |
.mc-bar{position:absolute;bottom:0;left:0;height:2px;transition:width .6s ease}
|
| 75 |
.mc-bar.green{background:linear-gradient(90deg,var(--accent),transparent)}.mc-bar.red{background:linear-gradient(90deg,var(--red),transparent)}.mc-bar.blue{background:linear-gradient(90deg,var(--cyan),transparent)}.mc-bar.purple{background:linear-gradient(90deg,var(--purple),transparent)}
|
|
|
|
| 76 |
.main-grid{display:grid;grid-template-columns:1fr 360px;gap:18px;margin-bottom:18px}
|
| 77 |
@media(max-width:1100px){.main-grid{grid-template-columns:1fr}}
|
| 78 |
.feed-wrap{max-height:480px;overflow-y:auto}
|
|
|
|
| 103 |
.bottom-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
|
| 104 |
@media(max-width:800px){.bottom-grid{grid-template-columns:1fr}}
|
| 105 |
.heatmap-grid{display:grid;grid-template-columns:repeat(12,1fr);gap:3px;padding:14px}
|
| 106 |
+
.hm-cell{aspect-ratio:1;border-radius:3px;background:var(--surface)}
|
| 107 |
.summary-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;padding:14px}
|
| 108 |
.sum-item{background:var(--surface);border-radius:10px;padding:14px;border:1px solid var(--border)}
|
| 109 |
.sum-label{font-family:var(--mono);font-size:9px;color:var(--muted);letter-spacing:1.5px;text-transform:uppercase;margin-bottom:6px}
|
| 110 |
.sum-val{font-family:var(--mono);font-size:20px;font-weight:700}
|
|
|
|
| 111 |
/* CSV */
|
| 112 |
.upload-zone{border:2px dashed var(--border2);border-radius:20px;padding:70px 40px;text-align:center;cursor:pointer;transition:all .3s;background:var(--bg2);position:relative;overflow:hidden}
|
| 113 |
+
.upload-zone:hover,.upload-zone.drag-over{border-color:var(--accent);background:rgba(0,232,122,0.03)}
|
| 114 |
.upload-zone input{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%}
|
| 115 |
.upload-icon{font-size:56px;margin-bottom:18px;display:block;opacity:.5}
|
| 116 |
.upload-title{font-family:var(--mono);font-size:18px;color:var(--accent);margin-bottom:10px;font-weight:700}
|
| 117 |
.upload-sub{font-size:13px;color:var(--muted2)}
|
| 118 |
.upload-hint{display:inline-block;margin-top:18px;padding:8px 20px;border-radius:8px;border:1px solid var(--border2);font-family:var(--mono);font-size:10px;color:var(--muted);letter-spacing:1px;background:var(--surface)}
|
| 119 |
+
.format-banner{display:flex;align-items:center;gap:12px;padding:12px 18px;border-radius:10px;font-family:var(--mono);font-size:11px;margin-bottom:16px;border:1px solid}
|
| 120 |
.format-banner.ok{background:rgba(0,232,122,0.06);border-color:rgba(0,232,122,0.2);color:var(--accent)}
|
| 121 |
.file-card{background:var(--bg2);border:1px solid var(--border2);border-radius:14px;padding:18px 22px;margin-bottom:18px;display:flex;align-items:center;gap:18px}
|
| 122 |
.file-icon{font-size:32px;flex-shrink:0}
|
|
|
|
| 130 |
.progress-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}
|
| 131 |
.progress-title{font-family:var(--mono);font-size:11px;color:var(--muted);letter-spacing:2px;text-transform:uppercase}
|
| 132 |
.progress-stats{font-family:var(--mono);font-size:13px;color:var(--cyan);font-weight:600}
|
| 133 |
+
.progress-track{background:var(--surface);border-radius:6px;height:10px;overflow:hidden;margin-bottom:14px}
|
| 134 |
.progress-fill{height:100%;background:linear-gradient(90deg,var(--accent),var(--cyan));border-radius:6px;transition:width .4s ease;width:0%}
|
| 135 |
.progress-fill.warning{background:linear-gradient(90deg,var(--amber),var(--red))}
|
| 136 |
.progress-row{display:flex;justify-content:space-between;font-family:var(--mono);font-size:10px;color:var(--muted);margin-bottom:12px;flex-wrap:wrap;gap:6px}
|
|
|
|
| 154 |
.sev-bar-track{flex:1;background:var(--surface);border-radius:3px;height:7px;overflow:hidden}
|
| 155 |
.sev-bar-fill{height:100%;border-radius:3px;transition:width .8s ease}
|
| 156 |
.sev-bar-cnt{font-family:var(--mono);font-size:10px;min-width:36px;text-align:right}
|
|
|
|
| 157 |
/* REPORT */
|
| 158 |
.processing-area{display:none}
|
| 159 |
.processing-area.visible{display:block}
|
| 160 |
.report-section{display:none}
|
| 161 |
.report-section.visible{display:block;animation:fadeIn .4s ease}
|
| 162 |
+
.completion-banner{background:linear-gradient(135deg,rgba(0,232,122,0.08),rgba(0,200,232,0.05));border:1px solid rgba(0,232,122,0.3);border-radius:14px;padding:20px 24px;display:flex;align-items:center;gap:18px;margin-bottom:22px}
|
|
|
|
| 163 |
.banner-title{font-family:var(--mono);font-size:15px;font-weight:700;color:var(--accent)}
|
| 164 |
.banner-sub{font-family:var(--mono);font-size:10px;color:var(--muted2);margin-top:4px;line-height:1.8}
|
| 165 |
.export-bar{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:24px}
|
|
|
|
| 196 |
.cluster-title{font-family:var(--mono);font-size:11px;font-weight:700;margin-bottom:6px}
|
| 197 |
.cluster-count{font-family:var(--mono);font-size:24px;font-weight:700;margin-bottom:4px}
|
| 198 |
.cluster-sub{font-family:var(--mono);font-size:9px;color:var(--muted);line-height:1.8}
|
| 199 |
+
/* VIRTUAL TABLE */
|
| 200 |
+
.vt-wrap{position:relative;overflow:hidden}
|
| 201 |
+
.vt-scroll{overflow-y:auto;max-height:420px}
|
| 202 |
+
.vt-spacer{width:100%}
|
| 203 |
.threat-table{width:100%;border-collapse:collapse;font-size:11px}
|
| 204 |
.threat-table th{padding:10px 16px;text-align:left;font-family:var(--mono);font-size:9px;color:var(--muted);background:var(--surface);position:sticky;top:0;letter-spacing:1.5px;z-index:2;border-bottom:1px solid var(--border2)}
|
| 205 |
.threat-table td{padding:9px 16px;border-top:1px solid var(--border);font-family:var(--mono);font-size:11px}
|
|
|
|
| 216 |
.risk-gauge-wrap{padding:16px;display:flex;flex-direction:column;align-items:center;gap:8px}
|
| 217 |
.risk-gauge-canvas{width:160px;height:90px;display:block}
|
| 218 |
.risk-label{font-family:var(--mono);font-size:11px;color:var(--muted2);text-align:center}
|
| 219 |
+
/* pagination for report table */
|
| 220 |
+
.pager{display:flex;align-items:center;gap:8px;padding:12px 16px;font-family:var(--mono);font-size:11px;color:var(--muted);border-top:1px solid var(--border)}
|
| 221 |
+
.pager-btn{padding:5px 14px;border-radius:6px;background:var(--surface);border:1px solid var(--border2);color:var(--cyan);font-family:var(--mono);font-size:11px;cursor:pointer}
|
| 222 |
+
.pager-btn:disabled{opacity:.4;cursor:not-allowed}
|
| 223 |
::-webkit-scrollbar{width:4px;height:4px}
|
| 224 |
::-webkit-scrollbar-track{background:transparent}
|
| 225 |
::-webkit-scrollbar-thumb{background:var(--surface2);border-radius:2px}
|
|
|
|
| 294 |
</div>
|
| 295 |
<div class="panel">
|
| 296 |
<div class="panel-head"><span>// SYSTEM LOG</span></div>
|
| 297 |
+
<div class="term-wrap" id="termWrap"><div class="term-line"><span class="ts">[--:--:--]</span> <span class="info">SentinelNet initialized.</span></div></div>
|
| 298 |
</div>
|
| 299 |
</div>
|
| 300 |
</div>
|
|
|
|
| 336 |
</div>
|
| 337 |
<div style="margin-top:22px;display:grid;grid-template-columns:repeat(3,1fr);gap:16px">
|
| 338 |
<div class="panel"><div class="panel-head"><span>// SUPPORTED FORMATS</span></div><div style="padding:16px;font-family:var(--mono);font-size:10px;color:var(--muted2);line-height:2.4">β NSL-KDD <span style="color:var(--accent)">with headers</span><br>β NSL-KDD <span style="color:var(--accent)">without headers</span><br>β KDDTrain+.txt / KDDTest+.txt<br>β 42 or 43 columns</div></div>
|
| 339 |
+
<div class="panel"><div class="panel-head"><span>// BATCH PROCESSING</span></div><div style="padding:16px;font-family:var(--mono);font-size:10px;color:var(--muted2);line-height:2.4">β <span style="color:var(--cyan)">100 rows/batch</span> β fast<br>β Live feed while processing<br>β Auto-downloads CSV on finish<br>β Full report: 6 charts + risk gauge</div></div>
|
| 340 |
<div class="panel"><div class="panel-head"><span>// THREAT CLASSES</span></div><div style="padding:16px"><div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><span class="cls-badge cls-normal">normal</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Clean traffic</span></div><div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><span class="cls-badge cls-DoS">DoS</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Denial of service</span></div><div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><span class="cls-badge cls-Probe">Probe</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Reconnaissance</span></div><div style="display:flex;align-items:center;gap:8px;margin-bottom:8px"><span class="cls-badge cls-R2L">R2L</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Remote to local</span></div><div style="display:flex;align-items:center;gap:8px"><span class="cls-badge cls-U2R">U2R</span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">Privilege escalation</span></div></div></div>
|
| 341 |
</div>
|
| 342 |
</div>
|
|
|
|
| 390 |
<div class="sev-bar-row"><div class="sev-bar-lbl">Medium</div><div class="sev-bar-track"><div class="sev-bar-fill" style="width:0%;background:var(--cyan)" id="sevbar-Medium"></div></div><div class="sev-bar-cnt" id="sevbc-Medium" style="color:var(--cyan)">0</div></div>
|
| 391 |
<div class="sev-bar-row"><div class="sev-bar-lbl">None</div><div class="sev-bar-track"><div class="sev-bar-fill" style="width:0%;background:var(--accent)" id="sevbar-None"></div></div><div class="sev-bar-cnt" id="sevbc-None" style="color:var(--accent)">0</div></div>
|
| 392 |
</div>
|
| 393 |
+
<div class="mini-card"><div class="mini-card-title">Processing Speed</div><div style="font-family:var(--mono);font-size:24px;font-weight:700;color:var(--accent)" id="csvProcRate">β</div><div style="font-family:var(--mono);font-size:9px;color:var(--muted);margin-top:4px">rows / second</div></div>
|
| 394 |
</div>
|
| 395 |
</div>
|
| 396 |
|
|
|
|
| 448 |
<div class="panel-head"><span>// ATTACK PATTERN CLUSTERS</span><span class="accent" id="clusterCount">0 clusters</span></div>
|
| 449 |
<div class="cluster-grid" id="clusterGrid"></div>
|
| 450 |
</div>
|
| 451 |
+
<!-- PAGINATED TABLE β replaces the old all-rows table -->
|
| 452 |
<div class="panel">
|
| 453 |
+
<div class="panel-head"><span>// COMPLETE ANALYSIS</span><span id="reportRowCount" class="accent">0 rows</span></div>
|
| 454 |
+
<div style="overflow-x:auto">
|
| 455 |
<table class="threat-table">
|
| 456 |
<thead><tr><th>ROW</th><th>PROTOCOL</th><th>SERVICE</th><th>SRC BYTES</th><th>DST BYTES</th><th>CLASS</th><th>CONFIDENCE</th><th>SEVERITY</th><th>TRUE LABEL</th></tr></thead>
|
| 457 |
<tbody id="reportTableBody"></tbody>
|
| 458 |
</table>
|
| 459 |
</div>
|
| 460 |
+
<div class="pager">
|
| 461 |
+
<button class="pager-btn" id="pgPrev" onclick="changePage(-1)" disabled>β PREV</button>
|
| 462 |
+
<span id="pgInfo" style="flex:1;text-align:center">Page 1 of 1</span>
|
| 463 |
+
<button class="pager-btn" id="pgNext" onclick="changePage(1)" disabled>NEXT βΆ</button>
|
| 464 |
+
</div>
|
| 465 |
</div>
|
| 466 |
</div>
|
| 467 |
</div>
|
|
|
|
| 469 |
</div>
|
| 470 |
|
| 471 |
<script>
|
| 472 |
+
// ββ CONFIG ββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
| 473 |
const BACKEND_URL = '';
|
| 474 |
const BATCH_SIZE = 100;
|
| 475 |
+
const PAGE_SIZE = 100; // rows per page in report table β KEY PERF FIX
|
| 476 |
+
|
| 477 |
+
// ββ CONSTANTS βββββββββββββββββββββββββββββββββββ
|
| 478 |
+
const NSL_KDD_COLUMNS=['duration','protocol_type','service','flag','src_bytes','dst_bytes','land','wrong_fragment','urgent','hot','num_failed_logins','logged_in','num_compromised','root_shell','su_attempted','num_root','num_file_creations','num_shells','num_access_files','num_outbound_cmds','is_host_login','is_guest_login','count','srv_count','serror_rate','srv_serror_rate','rerror_rate','srv_rerror_rate','same_srv_rate','diff_srv_rate','srv_diff_host_rate','dst_host_count','dst_host_srv_count','dst_host_same_srv_rate','dst_host_diff_srv_rate','dst_host_same_src_port_rate','dst_host_srv_diff_host_rate','dst_host_serror_rate','dst_host_srv_serror_rate','dst_host_rerror_rate','dst_host_srv_rerror_rate','label','difficulty_level'];
|
| 479 |
+
const STRING_COLS=new Set(['protocol_type','service','flag','label']);
|
| 480 |
+
const ATTACK_MAP={normal:'normal',back:'DoS',land:'DoS',neptune:'DoS',pod:'DoS',smurf:'DoS',teardrop:'DoS',mailbomb:'DoS',apache2:'DoS',processtable:'DoS',udpstorm:'DoS',satan:'Probe',ipsweep:'Probe',nmap:'Probe',portsweep:'Probe',mscan:'Probe',saint:'Probe',guess_passwd:'R2L',ftp_write:'R2L',imap:'R2L',phf:'R2L',multihop:'R2L',warezmaster:'R2L',warezclient:'R2L',spy:'R2L',xlock:'R2L',xsnoop:'R2L',snmpguess:'R2L',snmpgetattack:'R2L',httptunnel:'R2L',sendmail:'R2L',named:'R2L',buffer_overflow:'U2R',loadmodule:'U2R',perl:'U2R',rootkit:'U2R',ps:'U2R',xterm:'U2R',sqlattack:'U2R'};
|
| 481 |
+
const SEV_MAP={normal:'None',DoS:'Critical',Probe:'Medium',R2L:'High',U2R:'Critical'};
|
| 482 |
+
const SEV_COLOR={None:'#00e87a',Medium:'#00c8e8',High:'#ffaa00',Critical:'#ff3d5a'};
|
| 483 |
+
const CLASS_COLOR={normal:'#00e87a',DoS:'#ff3d5a',Probe:'#00c8e8',R2L:'#ffaa00',U2R:'#b06fff'};
|
| 484 |
+
const PROTOCOLS=['tcp','udp','icmp'];
|
| 485 |
+
const SERVICES=['http','ftp','smtp','ssh','dns','telnet','pop3','imap4','finger','auth'];
|
| 486 |
+
const LABEL_POOL=[...Array(60).fill('normal'),...Array(12).fill('neptune'),...Array(6).fill('smurf'),...Array(4).fill('back'),...Array(5).fill('ipsweep'),...Array(4).fill('satan'),...Array(3).fill('portsweep'),...Array(2).fill('guess_passwd'),...Array(1).fill('buffer_overflow'),...Array(1).fill('rootkit')];
|
| 487 |
+
|
| 488 |
+
// ββ LIVE STATE ββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
| 489 |
let monitorInterval=null,sessionInterval=null,sessionSeconds=0,isRunning=false,usingRealModel=false,packetId=0;
|
| 490 |
const counts={normal:0,DoS:0,Probe:0,R2L:0,U2R:0};
|
| 491 |
let totalPackets=0,totalIntrusions=0,confSum=0,peakClass=null;
|
| 492 |
let confBuckets={90:0,80:0,70:0,low:0};
|
| 493 |
let timelineBuckets=Array(60).fill(0),heatmapCells=Array(60).fill(null);
|
| 494 |
+
// throttle timeline redraws
|
| 495 |
+
let tlDirty=false;
|
| 496 |
|
| 497 |
+
// ββ CSV STATE βββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 498 |
let csvRows=[],csvResults=[],csvIndex=0,csvRunning=false,csvStartTime=null;
|
| 499 |
let csvCounts={normal:0,DoS:0,Probe:0,R2L:0,U2R:0};
|
| 500 |
let csvSevCounts={Critical:0,High:0,Medium:0,None:0};
|
| 501 |
let csvConfSum=0,csvIntrusionCount=0,csvConfHistory=[],csvUsingReal=false,csvFormatInfo='';
|
| 502 |
let batchNum=0,totalBatches=0;
|
| 503 |
+
// pagination
|
| 504 |
+
let reportPage=0;
|
| 505 |
|
| 506 |
+
// ββ TAB ββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 507 |
function switchTab(name,btn){
|
| 508 |
document.querySelectorAll('.tab-pane').forEach(p=>p.classList.remove('active'));
|
| 509 |
document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
|
| 510 |
+
document.getElementById('tab-'+name).classList.add('active');
|
| 511 |
+
btn.classList.add('active');
|
| 512 |
}
|
| 513 |
|
| 514 |
+
// ββ CSV PARSER βββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 515 |
function parseCSV(text){
|
| 516 |
const lines=text.trim().split('\n').filter(l=>l.trim());
|
| 517 |
if(!lines.length)return{rows:[],hasHeader:false,cols:0};
|
|
|
|
| 531 |
return{rows,hasHeader:looksLikeHeader,cols:headers.length};
|
| 532 |
}
|
| 533 |
|
| 534 |
+
// ββ LOCAL CLASSIFIER βββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 535 |
function classifyLocal(row){
|
| 536 |
const rawLabel=(row.label||'').toString().toLowerCase().trim().replace(/\.$/,'');
|
| 537 |
if(rawLabel&&rawLabel!=='unknown'){
|
|
|
|
| 540 |
}
|
| 541 |
const srcBytes=parseFloat(row.src_bytes)||0,flag=(row.flag||'').toUpperCase();
|
| 542 |
const serrorRate=parseFloat(row.serror_rate)||0,rerrorRate=parseFloat(row.rerror_rate)||0;
|
| 543 |
+
const srvCount=parseFloat(row.srv_count)||0;
|
| 544 |
const loggedIn=parseFloat(row.logged_in)||0,numRoot=parseFloat(row.num_root)||0,rootShell=parseFloat(row.root_shell)||0;
|
| 545 |
+
const count=parseFloat(row.count)||0;
|
| 546 |
let cls='normal',conf=0.75+Math.random()*0.15;
|
| 547 |
if(['S0','S1','S2','S3','REJ','RSTO','RSTR'].includes(flag)&&count>100){cls='DoS';conf=0.85+Math.random()*0.1;}
|
| 548 |
else if(srcBytes>50000&&(parseFloat(row.duration)||0)<5){cls='DoS';conf=0.80+Math.random()*0.12;}
|
|
|
|
| 553 |
return{predicted_class:cls,severity:SEV_MAP[cls],confidence:+Math.min(0.99,conf).toFixed(4),is_intrusion:cls!=='normal'};
|
| 554 |
}
|
| 555 |
|
| 556 |
+
// ββ API ββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 557 |
async function predictBatch(rows){
|
| 558 |
try{
|
| 559 |
+
const r=await fetch(BACKEND_URL+'/predict',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({rows}),signal:AbortSignal.timeout(20000)});
|
| 560 |
+
if(!r.ok)return null;
|
| 561 |
+
const d=await r.json();
|
| 562 |
return(d.status==='ok'&&Array.isArray(d.results))?d.results:null;
|
| 563 |
}catch{return null;}
|
| 564 |
}
|
|
|
|
| 565 |
async function predictSingle(row){
|
| 566 |
try{
|
| 567 |
+
const r=await fetch(BACKEND_URL+'/predict',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({rows:[row]}),signal:AbortSignal.timeout(5000)});
|
| 568 |
+
if(!r.ok)return null;
|
| 569 |
+
const d=await r.json();
|
| 570 |
return(d.status==='ok'&&d.results?.[0])?d.results[0]:null;
|
| 571 |
}catch{return null;}
|
| 572 |
}
|
| 573 |
|
| 574 |
+
// ββ FILE UPLOAD ββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 575 |
const uploadZone=document.getElementById('uploadZone');
|
| 576 |
uploadZone.addEventListener('dragover',e=>{e.preventDefault();uploadZone.classList.add('drag-over');});
|
| 577 |
uploadZone.addEventListener('dragleave',()=>uploadZone.classList.remove('drag-over'));
|
|
|
|
| 582 |
const reader=new FileReader();
|
| 583 |
reader.onload=e=>{
|
| 584 |
const{rows,hasHeader,cols}=parseCSV(e.target.result);
|
| 585 |
+
if(!rows.length){alert('Could not parse CSV.');return;}
|
| 586 |
csvRows=rows;
|
| 587 |
csvFormatInfo=hasHeader?`With headers Β· ${cols} columns`:`Headerless β auto-mapped Β· ${cols} columns`;
|
| 588 |
totalBatches=Math.ceil(rows.length/BATCH_SIZE);
|
| 589 |
const banner=document.getElementById('formatBanner');
|
| 590 |
banner.style.display='flex';banner.className='format-banner ok';
|
| 591 |
+
banner.innerHTML=`β ${hasHeader?'Headers detected':'Headerless β NSL-KDD auto-mapped'} Β· ${cols} columns Β· ${rows.length.toLocaleString()} rows Β· ${totalBatches} batches`;
|
| 592 |
document.getElementById('csvUploadSection').style.display='none';
|
| 593 |
document.getElementById('csvProcessingArea').classList.add('visible');
|
| 594 |
setText('csvFileName',file.name);
|
| 595 |
+
setText('csvFileMeta',`${rows.length.toLocaleString()} rows Β· ${(file.size/1024).toFixed(1)} KB Β· ${csvFormatInfo}`);
|
| 596 |
csvResults=[];csvIndex=0;csvConfSum=0;csvIntrusionCount=0;csvConfHistory=[];batchNum=0;
|
| 597 |
+
Object.keys(csvCounts).forEach(k=>csvCounts[k]=0);
|
| 598 |
+
Object.keys(csvSevCounts).forEach(k=>csvSevCounts[k]=0);
|
| 599 |
csvUsingReal=false;csvStartTime=null;
|
| 600 |
};
|
| 601 |
reader.readAsText(file);
|
| 602 |
}
|
| 603 |
|
| 604 |
+
// ββ CSV ENGINE βββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 605 |
async function startCsvAnalysis(){
|
| 606 |
if(csvRunning||csvIndex>=csvRows.length)return;
|
| 607 |
csvRunning=true;csvStartTime=csvStartTime||Date.now();
|
|
|
|
| 610 |
document.getElementById('csvProgressBlock').style.display='block';
|
| 611 |
document.getElementById('csvLiveGrid').style.display='grid';
|
| 612 |
document.getElementById('reportSection').classList.remove('visible');
|
| 613 |
+
document.getElementById('liveDot').className='dot amber';
|
| 614 |
+
setText('liveStatus','SCANNING');
|
| 615 |
await processBatches();
|
| 616 |
}
|
| 617 |
|
| 618 |
async function processBatches(){
|
| 619 |
+
while(csvRunning&&csvIndex<csvRows.length){
|
| 620 |
const bStart=csvIndex,bEnd=Math.min(csvIndex+BATCH_SIZE,csvRows.length);
|
| 621 |
const batch=csvRows.slice(bStart,bEnd);
|
| 622 |
batchNum++;
|
| 623 |
+
|
| 624 |
+
// Only update chips every 5 batches β PERF FIX
|
| 625 |
+
if(batchNum===1||batchNum%5===0||batchNum===totalBatches)
|
| 626 |
+
updateBatchChips(batchNum,totalBatches);
|
| 627 |
+
|
| 628 |
+
setText('csvCurrentRow',`Batch ${batchNum}/${totalBatches} β rows ${(bStart+1).toLocaleString()}β${bEnd.toLocaleString()}`);
|
| 629 |
|
| 630 |
let results=await predictBatch(batch);
|
| 631 |
if(results){
|
| 632 |
if(!csvUsingReal){csvUsingReal=true;setConnBadge('real');}
|
| 633 |
+
}else{
|
| 634 |
if(csvUsingReal||batchNum===1){csvUsingReal=false;setConnBadge('local');}
|
| 635 |
results=batch.map(r=>classifyLocal(r));
|
| 636 |
}
|
| 637 |
|
|
|
|
| 638 |
for(let i=0;i<batch.length;i++){
|
| 639 |
const row=batch[i],res=results[i];
|
| 640 |
const{predicted_class:cls,confidence:conf,severity:sev,is_intrusion:isI}=res;
|
| 641 |
+
csvResults.push({rowNum:bStart+i+1,row,cls,conf,sev,isI});
|
| 642 |
+
// Only store sampled conf history to save memory β PERF FIX
|
| 643 |
+
if(csvResults.length%5===0)csvConfHistory.push(conf);
|
| 644 |
csvCounts[cls]=(csvCounts[cls]||0)+1;
|
| 645 |
csvSevCounts[sev]=(csvSevCounts[sev]||0)+1;
|
| 646 |
+
csvConfSum+=conf;
|
| 647 |
+
if(isI)csvIntrusionCount++;
|
| 648 |
}
|
| 649 |
|
| 650 |
+
// Show only last 5 rows in feed β PERF FIX (was 15)
|
| 651 |
+
const feedSlice=batch.slice(-5);
|
| 652 |
feedSlice.forEach((row,i)=>{
|
| 653 |
const ri=bEnd-feedSlice.length+i;
|
| 654 |
addCsvFeedRow(ri+1,row,results[batch.length-feedSlice.length+i].predicted_class,results[batch.length-feedSlice.length+i].confidence,results[batch.length-feedSlice.length+i].severity);
|
|
|
|
| 666 |
setText('csvProgressEta',csvIndex<csvRows.length?`ETA: ${formatETA(remaining)}`:'Done!');
|
| 667 |
setText('csvSpeedStat',Math.round(rate)+' rows/s');
|
| 668 |
document.getElementById('csvProgressFill').style.width=pct+'%';
|
| 669 |
+
if(csvIntrusionCount/Math.max(csvIndex,1)>0.5)
|
| 670 |
+
document.getElementById('csvProgressFill').classList.add('warning');
|
| 671 |
+
|
| 672 |
+
// Only update sidebar every 3 batches β PERF FIX
|
| 673 |
+
if(batchNum%3===0||batchNum===totalBatches)updateCsvSidebar(rate);
|
| 674 |
setText('csvAlertCount',csvIntrusionCount.toLocaleString()+' THREATS');
|
| 675 |
+
|
| 676 |
+
await new Promise(r=>setTimeout(r,60)); // more breathing room β PERF FIX
|
| 677 |
}
|
| 678 |
+
if(csvIndex>=csvRows.length)finishCsvAnalysis();
|
| 679 |
}
|
| 680 |
|
| 681 |
function setConnBadge(type){
|
|
|
|
| 702 |
if(cls!=='normal')tr.style.background='rgba(255,61,90,0.025)';
|
| 703 |
tr.innerHTML=`<td style="color:var(--muted)">${rowNum}</td><td style="color:var(--cyan)">${row.protocol_type||'β'}</td><td>${row.service||'β'}</td><td>${(row.src_bytes||0).toLocaleString()}</td><td><span class="cls-badge cls-${cls}">${cls}</span></td><td style="color:${conf>0.9?'var(--accent)':conf>0.8?'var(--cyan)':'var(--amber)'}">${(conf*100).toFixed(1)}%</td><td style="color:${SEV_COLOR[sev]}">β ${sev}</td>`;
|
| 704 |
tbody.insertBefore(tr,tbody.firstChild);
|
| 705 |
+
// Cap at 100 DOM rows β PERF FIX (was 500)
|
| 706 |
+
while(tbody.children.length>100)tbody.removeChild(tbody.lastChild);
|
| 707 |
}
|
| 708 |
|
| 709 |
function updateCsvSidebar(rate){
|
|
|
|
| 719 |
|
| 720 |
function stopCsvAnalysis(){
|
| 721 |
csvRunning=false;
|
| 722 |
+
document.getElementById('csvStartBtn').disabled=false;
|
| 723 |
+
document.getElementById('csvStopBtn').disabled=true;
|
| 724 |
+
document.getElementById('liveDot').className='dot red';
|
| 725 |
+
setText('liveStatus','PAUSED');
|
| 726 |
}
|
| 727 |
|
| 728 |
function finishCsvAnalysis(){
|
| 729 |
csvRunning=false;
|
| 730 |
+
document.getElementById('csvStartBtn').disabled=true;
|
| 731 |
+
document.getElementById('csvStopBtn').disabled=true;
|
| 732 |
+
document.getElementById('liveDot').className='dot green';
|
| 733 |
+
setText('liveStatus','DONE');
|
| 734 |
setText('csvProgressEta','Done!');
|
| 735 |
+
setText('csvCurrentRow',`β All ${csvRows.length.toLocaleString()} rows processed. Building reportβ¦`);
|
| 736 |
+
setTimeout(()=>{exportAnnotatedCSV();buildReport();},300);
|
| 737 |
}
|
| 738 |
|
| 739 |
+
// ββ REPORT BUILDER βββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 740 |
function buildReport(){
|
| 741 |
document.getElementById('reportSection').classList.add('visible');
|
| 742 |
const elapsed=(Date.now()-csvStartTime)/1000;
|
|
|
|
| 744 |
const total=csvResults.length,threats=csvIntrusionCount;
|
| 745 |
const avgConf=(csvConfSum/total*100).toFixed(1)+'%';
|
| 746 |
const rate=(threats/total*100).toFixed(1)+'%';
|
|
|
|
| 747 |
const riskScore=Math.min(100,Math.round(((csvCounts.DoS||0)*0.4+(csvCounts.U2R||0)*0.35+(csvCounts.R2L||0)*0.15+(csvCounts.Probe||0)*0.1)/Math.max(total,1)*100*6));
|
| 748 |
|
| 749 |
+
setText('bannerSub',`${total.toLocaleString()} rows Β· ${formatETA(elapsed)} Β· ${threats.toLocaleString()} threats detected`);
|
| 750 |
setText('rmFile',fileName);setText('rmRows',total.toLocaleString());setText('rmDate',new Date().toLocaleString());
|
| 751 |
+
setText('rmModel',csvUsingReal?'Real Random Forest':'Local Simulation');
|
| 752 |
+
setText('rmDuration',formatETA(elapsed));setText('rmFormat',csvFormatInfo);
|
| 753 |
setText('reportSubtitle',`Generated ${new Date().toUTCString()}`);
|
| 754 |
setText('rs-total',total.toLocaleString());setText('rs-threats',threats.toLocaleString());
|
| 755 |
setText('rs-rate',rate);setText('rs-conf',avgConf);setText('rs-risk',riskScore+'/100');
|
|
|
|
| 758 |
const smx=Math.max(...sevs.map(s=>csvSevCounts[s]||0),1);
|
| 759 |
sevs.forEach(s=>{setWidth('rsevbar-'+s,(csvSevCounts[s]||0)/smx*100);setText('rsevbc-'+s,(csvSevCounts[s]||0).toLocaleString());});
|
| 760 |
|
| 761 |
+
// Draw charts using requestAnimationFrame β PERF FIX
|
| 762 |
+
requestAnimationFrame(()=>{
|
| 763 |
+
drawBarChart();drawConfWave();drawIntensity();
|
| 764 |
+
drawProto();drawServices();drawGauge(riskScore);
|
| 765 |
+
buildClusters();
|
| 766 |
+
reportPage=0;renderReportPage(); // paginated table β PERF FIX
|
| 767 |
+
});
|
| 768 |
+
|
| 769 |
setTimeout(()=>document.getElementById('reportSection').scrollIntoView({behavior:'smooth',block:'start'}),300);
|
| 770 |
}
|
| 771 |
|
| 772 |
+
// ββ PAGINATED TABLE β renders only PAGE_SIZE rows at a time βββββββββββββββββ
|
| 773 |
+
function renderReportPage(){
|
| 774 |
+
const tbody=document.getElementById('reportTableBody');
|
| 775 |
+
const total=csvResults.length;
|
| 776 |
+
const totalPages=Math.ceil(total/PAGE_SIZE);
|
| 777 |
+
const start=reportPage*PAGE_SIZE;
|
| 778 |
+
const end=Math.min(start+PAGE_SIZE,total);
|
| 779 |
+
|
| 780 |
+
// Build fragment β only PAGE_SIZE rows, not all rows β KEY PERF FIX
|
| 781 |
+
const frag=document.createDocumentFragment();
|
| 782 |
+
for(let i=start;i<end;i++){
|
| 783 |
+
const{rowNum,row,cls,conf,sev,isI}=csvResults[i];
|
| 784 |
+
const tr=document.createElement('tr');
|
| 785 |
+
if(isI)tr.className='row-intrusion';
|
| 786 |
+
tr.innerHTML=`<td style="color:var(--muted)">${rowNum}</td><td style="color:var(--cyan)">${row.protocol_type||'β'}</td><td>${row.service||'β'}</td><td>${(row.src_bytes||0).toLocaleString()}</td><td>${(row.dst_bytes||0).toLocaleString()}</td><td><span class="cls-badge cls-${cls}">${cls}</span></td><td style="color:${conf>0.9?'var(--accent)':conf>0.8?'var(--cyan)':'var(--amber)'}">${(conf*100).toFixed(1)}%</td><td style="color:${SEV_COLOR[sev]}">β ${sev}</td><td style="color:var(--muted);font-size:10px">${row.label||'β'}</td>`;
|
| 787 |
+
frag.appendChild(tr);
|
| 788 |
+
}
|
| 789 |
+
tbody.innerHTML='';
|
| 790 |
+
tbody.appendChild(frag);
|
| 791 |
+
|
| 792 |
+
setText('reportRowCount',`${total.toLocaleString()} rows`);
|
| 793 |
+
setText('pgInfo',`Page ${reportPage+1} of ${totalPages} Β· rows ${start+1}β${end}`);
|
| 794 |
+
document.getElementById('pgPrev').disabled=reportPage===0;
|
| 795 |
+
document.getElementById('pgNext').disabled=reportPage>=totalPages-1;
|
| 796 |
+
}
|
| 797 |
+
|
| 798 |
+
function changePage(dir){
|
| 799 |
+
reportPage+=dir;
|
| 800 |
+
renderReportPage();
|
| 801 |
+
document.getElementById('reportSection').scrollIntoView({behavior:'smooth',block:'start'});
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
// ββ CHARTS βββββββββββββββββββββββββββββββββββββββ
|
| 805 |
function drawBarChart(){
|
| 806 |
const c=document.getElementById('reportBarCanvas'),ctx=c.getContext('2d');
|
| 807 |
+
const W=c.offsetWidth||300,H=160;c.width=W*devicePixelRatio;c.height=H*devicePixelRatio;
|
| 808 |
+
ctx.scale(devicePixelRatio,devicePixelRatio);ctx.clearRect(0,0,W,H);
|
| 809 |
const classes=['normal','DoS','Probe','R2L','U2R'],colors=['#00e87a','#ff3d5a','#00c8e8','#ffaa00','#b06fff'];
|
| 810 |
const vals=classes.map(c=>csvCounts[c]||0),mx=Math.max(...vals,1);
|
| 811 |
const bw=(W-40)/classes.length,pad=bw*0.18;
|
|
|
|
| 813 |
const x=20+i*bw+pad,bW=bw-pad*2,bH=(vals[i]/mx)*(H-30),y=H-10-bH;
|
| 814 |
const g=ctx.createLinearGradient(0,y,0,H-10);g.addColorStop(0,colors[i]);g.addColorStop(1,colors[i]+'33');
|
| 815 |
ctx.fillStyle=g;ctx.beginPath();ctx.roundRect(x,y,bW,Math.max(bH,1),4);ctx.fill();
|
| 816 |
+
ctx.fillStyle='rgba(90,122,153,.9)';ctx.font='9px IBM Plex Mono';ctx.textAlign='center';
|
| 817 |
+
ctx.fillText(cls,x+bW/2,H-1);
|
| 818 |
if(vals[i]>0){ctx.fillStyle=colors[i];ctx.fillText(vals[i].toLocaleString(),x+bW/2,y-4);}
|
| 819 |
});
|
| 820 |
}
|
| 821 |
|
| 822 |
function drawConfWave(){
|
| 823 |
const c=document.getElementById('reportConfCanvas'),ctx=c.getContext('2d');
|
| 824 |
+
const W=c.offsetWidth||300,H=160;c.width=W*devicePixelRatio;c.height=H*devicePixelRatio;
|
| 825 |
+
ctx.scale(devicePixelRatio,devicePixelRatio);ctx.clearRect(0,0,W,H);
|
| 826 |
const data=csvConfHistory;if(data.length<2)return;
|
| 827 |
+
const xStep=(W-20)/Math.max(data.length-1,1),mn=0.5,mx=1;
|
|
|
|
|
|
|
| 828 |
ctx.strokeStyle='rgba(0,200,120,.06)';ctx.lineWidth=1;
|
| 829 |
[.25,.5,.75,1].forEach(f=>{const y=10+(1-f)*(H-20);ctx.beginPath();ctx.moveTo(10,y);ctx.lineTo(W-10,y);ctx.stroke();});
|
| 830 |
+
ctx.beginPath();data.forEach((v,i)=>{const x=10+i*xStep,y=10+(1-(v-mn)/(mx-mn))*(H-20);i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});
|
| 831 |
+
ctx.lineTo(10+(data.length-1)*xStep,H-10);ctx.lineTo(10,H-10);ctx.closePath();
|
| 832 |
+
const g=ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,'rgba(0,200,232,.15)');g.addColorStop(1,'rgba(0,200,232,.01)');
|
| 833 |
+
ctx.fillStyle=g;ctx.fill();
|
| 834 |
+
ctx.beginPath();ctx.strokeStyle='#00c8e8';ctx.lineWidth=1.5;
|
| 835 |
+
data.forEach((v,i)=>{const x=10+i*xStep,y=10+(1-(v-mn)/(mx-mn))*(H-20);i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});
|
| 836 |
+
ctx.stroke();
|
| 837 |
const avg=csvConfSum/csvResults.length,avgY=10+(1-(avg-mn)/(mx-mn))*(H-20);
|
| 838 |
+
ctx.beginPath();ctx.setLineDash([4,3]);ctx.strokeStyle='rgba(0,232,122,.6)';ctx.lineWidth=1;
|
| 839 |
+
ctx.moveTo(10,avgY);ctx.lineTo(W-10,avgY);ctx.stroke();ctx.setLineDash([]);
|
| 840 |
+
ctx.fillStyle='rgba(0,232,122,.8)';ctx.font='9px IBM Plex Mono';ctx.textAlign='left';
|
| 841 |
+
ctx.fillText(`avg ${(avg*100).toFixed(1)}%`,14,avgY-5);
|
| 842 |
}
|
| 843 |
|
| 844 |
function drawIntensity(){
|
| 845 |
const c=document.getElementById('reportIntensityCanvas'),ctx=c.getContext('2d');
|
| 846 |
+
const W=c.offsetWidth||300,H=160;c.width=W*devicePixelRatio;c.height=H*devicePixelRatio;
|
| 847 |
+
ctx.scale(devicePixelRatio,devicePixelRatio);ctx.clearRect(0,0,W,H);
|
| 848 |
const wSize=Math.max(5,Math.floor(csvResults.length/60));
|
| 849 |
const windows=[];
|
| 850 |
for(let i=0;i<csvResults.length;i+=wSize){const sl=csvResults.slice(i,i+wSize);windows.push(sl.filter(r=>r.isI).length/sl.length);}
|
|
|
|
| 854 |
[.25,.5,.75,1].forEach(f=>{const y=10+(1-f)*(H-20);ctx.beginPath();ctx.moveTo(10,y);ctx.lineTo(W-10,y);ctx.stroke();});
|
| 855 |
ctx.beginPath();windows.forEach((v,i)=>{const x=10+i*xStep,y=10+(1-v/mx)*(H-20);i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});
|
| 856 |
ctx.lineTo(10+(windows.length-1)*xStep,H-10);ctx.lineTo(10,H-10);ctx.closePath();
|
| 857 |
+
const g=ctx.createLinearGradient(0,0,0,H);g.addColorStop(0,'rgba(255,61,90,.28)');g.addColorStop(1,'rgba(255,61,90,.02)');
|
| 858 |
+
ctx.fillStyle=g;ctx.fill();
|
| 859 |
+
ctx.beginPath();ctx.strokeStyle='#ff3d5a';ctx.lineWidth=2;
|
| 860 |
+
windows.forEach((v,i)=>{const x=10+i*xStep,y=10+(1-v/mx)*(H-20);i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});ctx.stroke();
|
| 861 |
}
|
| 862 |
|
| 863 |
function drawProto(){
|
|
|
|
| 866 |
csvResults.forEach(({row,isI})=>{const p=row.protocol_type||'unknown';pc[p]=(pc[p]||0)+1;if(isI)pt[p]=(pt[p]||0)+1;});
|
| 867 |
const sorted=Object.entries(pc).sort((a,b)=>b[1]-a[1]);
|
| 868 |
const mx=Math.max(...sorted.map(([,v])=>v),1);
|
| 869 |
+
const frag=document.createDocumentFragment();
|
| 870 |
sorted.forEach(([proto,cnt])=>{
|
| 871 |
const tc=pt[proto]||0,tp=cnt>0?Math.round(tc/cnt*100):0;
|
| 872 |
const div=document.createElement('div');div.className='proto-row';
|
| 873 |
div.innerHTML=`<div class="proto-lbl">${proto}</div><div class="proto-track"><div class="proto-fill" style="width:${cnt/mx*100}%;background:${tc>0?'var(--red)':'var(--accent)'}"></div></div><div class="proto-cnt">${cnt.toLocaleString()} <span style="color:${tc>0?'var(--red)':'var(--muted)'}">${tp}% threat</span></div>`;
|
| 874 |
+
frag.appendChild(div);
|
| 875 |
});
|
| 876 |
+
el.appendChild(frag);
|
| 877 |
}
|
| 878 |
|
| 879 |
function drawServices(){
|
|
|
|
| 882 |
csvResults.forEach(({row,isI})=>{const s=row.service||'unknown';sc[s]=(sc[s]||0)+1;if(isI)st[s]=(st[s]||0)+1;});
|
| 883 |
const sorted=Object.entries(sc).sort((a,b)=>b[1]-a[1]).slice(0,8);
|
| 884 |
const mx=Math.max(...sorted.map(([,v])=>v),1);
|
| 885 |
+
const frag=document.createDocumentFragment();
|
| 886 |
sorted.forEach(([svc,cnt])=>{
|
| 887 |
const hot=(st[svc]||0)>cnt*0.3;
|
| 888 |
const div=document.createElement('div');div.className='svc-row';
|
| 889 |
+
div.innerHTML=`<span class="svc-name" style="color:${hot?'var(--red)':'var(--cyan)'}">${svc}</span><span style="flex:1;margin:0 8px;background:var(--surface);border-radius:2px;height:4px;display:block;overflow:hidden"><span style="display:block;height:100%;width:${Math.round(cnt/mx*100)}%;background:${hot?'var(--red)':'var(--cyan)'};border-radius:2px"></span></span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">${cnt.toLocaleString()}</span>`;
|
| 890 |
+
frag.appendChild(div);
|
| 891 |
});
|
| 892 |
+
el.appendChild(frag);
|
| 893 |
}
|
| 894 |
|
| 895 |
function drawGauge(score){
|
| 896 |
const c=document.getElementById('riskGaugeCanvas'),ctx=c.getContext('2d');
|
| 897 |
+
const W=160,H=90;c.width=W*devicePixelRatio;c.height=H*devicePixelRatio;
|
| 898 |
+
ctx.scale(devicePixelRatio,devicePixelRatio);ctx.clearRect(0,0,W,H);
|
| 899 |
const cx=W/2,cy=H-8,r=66;
|
| 900 |
ctx.beginPath();ctx.arc(cx,cy,r,Math.PI,2*Math.PI);ctx.strokeStyle='rgba(255,255,255,.05)';ctx.lineWidth=14;ctx.lineCap='round';ctx.stroke();
|
| 901 |
[[0,.33,'#00e87a'],[.33,.66,'#ffaa00'],[.66,1,'#ff3d5a']].forEach(([from,to,col])=>{ctx.beginPath();ctx.arc(cx,cy,r,Math.PI+from*Math.PI,Math.PI+to*Math.PI);ctx.strokeStyle=col+'55';ctx.lineWidth=14;ctx.stroke();});
|
| 902 |
+
const sc=score<33?'#00e87a':score<66?'#ffaa00':'#ff3d5a';
|
| 903 |
+
ctx.beginPath();ctx.arc(cx,cy,r,Math.PI,Math.PI+(score/100)*Math.PI);ctx.strokeStyle=sc;ctx.lineWidth=14;ctx.lineCap='round';ctx.stroke();
|
| 904 |
+
ctx.fillStyle=sc;ctx.font='bold 22px IBM Plex Mono';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(score,cx,cy-16);
|
| 905 |
ctx.fillStyle='rgba(90,122,153,.8)';ctx.font='9px IBM Plex Mono';ctx.fillText('/100',cx,cy-2);
|
| 906 |
const lbl=score<20?'LOW':score<40?'MODERATE':score<60?'ELEVATED':score<80?'HIGH':'CRITICAL';
|
| 907 |
setText('riskLabel',`${lbl} RISK Β· Score ${score}/100`);
|
|
|
|
| 909 |
|
| 910 |
function buildClusters(){
|
| 911 |
const grid=document.getElementById('clusterGrid');grid.innerHTML='';let cnt=0;
|
| 912 |
+
const frag=document.createDocumentFragment();
|
| 913 |
['DoS','Probe','R2L','U2R','normal'].forEach(cls=>{
|
| 914 |
const c=csvCounts[cls]||0;if(!c)return;cnt++;
|
| 915 |
const col=CLASS_COLOR[cls],res=csvResults.filter(r=>r.cls===cls);
|
| 916 |
const avgConf=res.reduce((s,r)=>s+r.conf,0)/c;
|
| 917 |
const div=document.createElement('div');div.className=`cluster-card ${cls}`;
|
| 918 |
div.innerHTML=`<div class="cluster-title" style="color:${col}">${cls} Traffic</div><div class="cluster-count" style="color:${col}">${c.toLocaleString()}</div><div class="cluster-sub">Severity: ${SEV_MAP[cls]}<br>Avg confidence: ${(avgConf*100).toFixed(1)}%<br>${(c/csvResults.length*100).toFixed(1)}% of dataset</div>`;
|
| 919 |
+
frag.appendChild(div);
|
| 920 |
});
|
| 921 |
+
grid.appendChild(frag);
|
| 922 |
+
if(!cnt)grid.innerHTML='<div style="padding:20px;font-family:var(--mono);font-size:11px;color:var(--accent);grid-column:span 4">β No attack clusters β clean dataset</div>';
|
| 923 |
setText('clusterCount',cnt+' clusters');
|
| 924 |
}
|
| 925 |
|
| 926 |
+
// ββ EXPORTS ββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 927 |
function exportAnnotatedCSV(){
|
| 928 |
if(!csvResults.length)return;
|
| 929 |
const headers=Object.keys(csvResults[0].row).concat(['predicted_class','severity','confidence','is_intrusion']);
|
| 930 |
const lines=[headers.join(',')];
|
| 931 |
+
csvResults.forEach(({row,cls,sev,conf,isI})=>{
|
| 932 |
+
const vals=Object.values(row).map(v=>typeof v==='string'&&v.includes(',')?`"${v}"`:v);
|
| 933 |
+
vals.push(cls,sev,conf,isI?1:0);lines.push(vals.join(','));
|
| 934 |
+
});
|
| 935 |
downloadFile('sentinelnet_annotated.csv',lines.join('\n'),'text/csv');
|
| 936 |
}
|
| 937 |
|
| 938 |
function exportJSON(){
|
| 939 |
if(!csvResults.length){alert('No results.');return;}
|
| 940 |
+
const data={meta:{file:document.getElementById('csvFileName').textContent,date:new Date().toISOString(),total:csvResults.length,threats:csvIntrusionCount,threatRate:(csvIntrusionCount/csvResults.length*100).toFixed(1)+'%',model:csvUsingReal?'Real RF':'Local Sim',batchSize:BATCH_SIZE},distribution:csvCounts,severity:csvSevCounts,avgConf:(csvConfSum/csvResults.length*100).toFixed(2)+'%',results:csvResults.map(({rowNum,cls,conf,sev,isI})=>({rowNum,cls,conf,sev,isI}))};
|
| 941 |
downloadFile('sentinelnet_results.json',JSON.stringify(data,null,2),'application/json');
|
| 942 |
}
|
| 943 |
|
|
|
|
| 953 |
const riskLbl=riskScore<20?'LOW':riskScore<40?'MODERATE':riskScore<60?'ELEVATED':riskScore<80?'HIGH':'CRITICAL';
|
| 954 |
const colors={normal:'#00e87a',DoS:'#ff3d5a',Probe:'#00c8e8',R2L:'#ffaa00',U2R:'#b06fff'};
|
| 955 |
const sevColors={None:'#00e87a',Medium:'#00c8e8',High:'#ffaa00',Critical:'#ff3d5a'};
|
|
|
|
| 956 |
const distRows=['normal','DoS','Probe','R2L','U2R'].map(c=>`<tr><td style="color:${colors[c]}">${c}</td><td>${(csvCounts[c]||0).toLocaleString()}</td><td>${total>0?((csvCounts[c]||0)/total*100).toFixed(1):'0'}%</td><td style="color:${sevColors[SEV_MAP[c]]}">${SEV_MAP[c]}</td></tr>`).join('');
|
|
|
|
| 957 |
const pc={};csvResults.forEach(({row})=>{const p=row.protocol_type||'unknown';pc[p]=(pc[p]||0)+1;});
|
| 958 |
const protoRows=Object.entries(pc).sort((a,b)=>b[1]-a[1]).map(([p,c])=>`<tr><td>${p}</td><td>${c.toLocaleString()}</td><td>${(c/total*100).toFixed(1)}%</td></tr>`).join('');
|
|
|
|
| 959 |
const topThreats=csvResults.filter(r=>r.isI).slice(0,100);
|
| 960 |
const threatRows=topThreats.map(({rowNum,row,cls,conf,sev})=>`<tr><td>${rowNum}</td><td>${row.protocol_type||'β'}</td><td>${row.service||'β'}</td><td>${(row.src_bytes||0).toLocaleString()}</td><td style="color:${colors[cls]};font-weight:bold">${cls}</td><td>${(conf*100).toFixed(1)}%</td><td style="color:${sevColors[sev]}">${sev}</td></tr>`).join('');
|
|
|
|
| 961 |
const html=`<!DOCTYPE html><html><head><meta charset="UTF-8"/><title>SentinelNet Report</title>
|
| 962 |
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600;700&display=swap" rel="stylesheet">
|
| 963 |
+
<style>*{box-sizing:border-box;margin:0;padding:0}body{font-family:'IBM Plex Mono',monospace;background:#04080d;color:#d8eeff;padding:40px;font-size:11px}h1{color:#00e87a;font-size:22px;border-bottom:2px solid #00e87a;padding-bottom:12px;margin-bottom:8px}h2{color:#00c8e8;font-size:13px;margin:28px 0 12px;letter-spacing:2px;text-transform:uppercase}.meta{color:#4a6a88;font-size:10px;margin-bottom:32px;line-height:2.2}.grid{display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin-bottom:32px}.stat{background:#080e16;border:1px solid rgba(0,210,130,0.15);border-radius:10px;padding:16px;text-align:center}.stat-val{font-size:24px;font-weight:700;margin-bottom:5px}.stat-lbl{font-size:8px;color:#4a6a88;letter-spacing:2px;text-transform:uppercase}table{width:100%;border-collapse:collapse;margin-bottom:28px}th{background:#0d1520;padding:9px 12px;text-align:left;color:#4a6a88;font-size:8px;letter-spacing:1.5px;border-bottom:1px solid rgba(0,210,130,0.2)}td{padding:7px 12px;border-top:1px solid rgba(0,210,130,0.06)}.risk-box{background:#0d1520;border:1px solid rgba(0,210,130,0.2);border-radius:12px;padding:20px;text-align:center;margin-bottom:28px}.risk-num{font-size:40px;font-weight:700;color:${riskScore<33?'#00e87a':riskScore<66?'#ffaa00':'#ff3d5a'}}.footer{margin-top:40px;padding-top:14px;border-top:1px solid rgba(0,210,130,0.12);color:#4a6a88;font-size:9px;text-align:center}@media print{body{background:#04080d!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}}</style></head><body>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 964 |
<h1>π‘ SentinelNet β Threat Analysis Report</h1>
|
| 965 |
+
<div class="meta">File: <strong style="color:#d8eeff">${fileName}</strong> | Date: ${dateStr} | Model: ${modelSrc} | Duration: ${formatETA(elapsed)}</div>
|
| 966 |
+
<div class="grid"><div class="stat"><div class="stat-val" style="color:#00c8e8">${total.toLocaleString()}</div><div class="stat-lbl">Total Rows</div></div><div class="stat"><div class="stat-val" style="color:#ff3d5a">${threats.toLocaleString()}</div><div class="stat-lbl">Threats Found</div></div><div class="stat"><div class="stat-val" style="color:#ffaa00">${rate}%</div><div class="stat-lbl">Threat Rate</div></div><div class="stat"><div class="stat-val" style="color:#00e87a">${avgConf}%</div><div class="stat-lbl">Avg Confidence</div></div><div class="stat"><div class="stat-val" style="color:${riskScore<33?'#00e87a':riskScore<66?'#ffaa00':'#ff3d5a'}">${riskScore}/100</div><div class="stat-lbl">Risk Score</div></div></div>
|
| 967 |
+
<div class="risk-box"><div class="risk-num">${riskScore}</div><div style="font-size:13px;color:${riskScore<33?'#00e87a':riskScore<66?'#ffaa00':'#ff3d5a'};margin-top:4px">${riskLbl} RISK</div><div style="color:#4a6a88;font-size:10px;margin-top:8px">Weighted: DoSΓ0.4 Β· U2RΓ0.35 Β· R2LΓ0.15 Β· ProbeΓ0.1</div></div>
|
| 968 |
+
<h2>Attack Class Distribution</h2><table><thead><tr><th>CLASS</th><th>COUNT</th><th>PERCENTAGE</th><th>SEVERITY</th></tr></thead><tbody>${distRows}</tbody></table>
|
| 969 |
+
<h2>Protocol Breakdown</h2><table><thead><tr><th>PROTOCOL</th><th>COUNT</th><th>PERCENTAGE</th></tr></thead><tbody>${protoRows}</tbody></table>
|
| 970 |
+
<h2>Severity Summary</h2><table><thead><tr><th>SEVERITY</th><th>COUNT</th><th>PERCENTAGE</th></tr></thead><tbody>${['Critical','High','Medium','None'].map(s=>`<tr><td style="color:${sevColors[s]}">${s}</td><td>${(csvSevCounts[s]||0).toLocaleString()}</td><td>${((csvSevCounts[s]||0)/total*100).toFixed(1)}%</td></tr>`).join('')}</tbody></table>
|
| 971 |
+
<h2>Detected Threats β Top 100</h2><table><thead><tr><th>ROW</th><th>PROTOCOL</th><th>SERVICE</th><th>SRC BYTES</th><th>CLASS</th><th>CONFIDENCE</th><th>SEVERITY</th></tr></thead><tbody>${threatRows||'<tr><td colspan="7" style="color:#00e87a;text-align:center;padding:20px">No threats detected</td></tr>'}</tbody></table>
|
| 972 |
+
<div class="footer">Generated by SentinelNet Β· ${dateStr} Β· NSL-KDD Intrusion Detection</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
</body></html>`;
|
|
|
|
| 974 |
const win=window.open('','_blank','width=1100,height=900');
|
| 975 |
+
if(!win){alert('Pop-up blocked.');return;}
|
| 976 |
win.document.write(html);win.document.close();setTimeout(()=>win.print(),900);
|
| 977 |
}
|
| 978 |
|
|
|
|
| 982 |
}
|
| 983 |
|
| 984 |
function resetCsvTab(){
|
| 985 |
+
csvRunning=false;csvRows=[];csvResults=[];csvIndex=0;csvConfSum=0;csvIntrusionCount=0;
|
| 986 |
+
csvConfHistory=[];batchNum=0;reportPage=0;
|
| 987 |
+
Object.keys(csvCounts).forEach(k=>csvCounts[k]=0);
|
| 988 |
+
Object.keys(csvSevCounts).forEach(k=>csvSevCounts[k]=0);
|
| 989 |
csvStartTime=null;csvUsingReal=false;csvFormatInfo='';totalBatches=0;
|
| 990 |
document.getElementById('csvUploadSection').style.display='';
|
| 991 |
document.getElementById('csvProcessingArea').classList.remove('visible');
|
|
|
|
| 993 |
document.getElementById('csvLiveGrid').style.display='none';
|
| 994 |
document.getElementById('reportSection').classList.remove('visible');
|
| 995 |
document.getElementById('csvFeedBody').innerHTML='';
|
| 996 |
+
document.getElementById('reportTableBody').innerHTML='';
|
| 997 |
document.getElementById('csvFileInput').value='';
|
| 998 |
document.getElementById('formatBanner').style.display='none';
|
| 999 |
document.getElementById('liveDot').className='dot';setText('liveStatus','IDLE');
|
| 1000 |
+
document.getElementById('csvStartBtn').disabled=false;
|
| 1001 |
+
document.getElementById('csvStopBtn').disabled=true;
|
| 1002 |
document.getElementById('batchStatus').innerHTML='';
|
| 1003 |
+
document.getElementById('csvProgressFill').style.width='0%';
|
| 1004 |
+
document.getElementById('csvProgressFill').classList.remove('warning');
|
| 1005 |
}
|
| 1006 |
|
| 1007 |
+
// ββ LIVE MONITOR βββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 1008 |
function generatePacket(){
|
| 1009 |
const label=LABEL_POOL[Math.floor(Math.random()*LABEL_POOL.length)];
|
| 1010 |
const isAtk=label!=='normal';
|
|
|
|
| 1019 |
const packet=generatePacket();
|
| 1020 |
let result;
|
| 1021 |
const api=await predictSingle(packet);
|
| 1022 |
+
if(api){result=api;if(!usingRealModel){usingRealModel=true;document.getElementById('connBadge').textContent='β REAL MODEL';document.getElementById('connBadge').className='real';document.getElementById('sum-model').textContent='REAL RF';termLog('info','Connected to model');}}
|
| 1023 |
else{result=classifyLocal(packet);if(usingRealModel){usingRealModel=false;document.getElementById('connBadge').textContent='β LOCAL SIM';document.getElementById('connBadge').className='local';document.getElementById('sum-model').textContent='LOCAL';}}
|
| 1024 |
packetId++;totalPackets++;
|
| 1025 |
const cls=result.predicted_class,conf=result.confidence,sev=result.severity,isI=result.is_intrusion;
|
|
|
|
| 1032 |
heatmapCells.shift();heatmapCells.push(cls);
|
| 1033 |
addFeedRow(packet,cls,sev,conf);
|
| 1034 |
if(cls==='U2R')termLog('crit',`U2R ALERT β Privilege escalation! Conf: ${(conf*100).toFixed(1)}%`);
|
| 1035 |
+
else if(cls==='DoS'&&Math.random()<0.15)termLog('warn',`DoS β ${packet.service} flood`);
|
| 1036 |
+
else if(cls==='normal'&&totalPackets%50===0)termLog('ok',`${totalPackets} packets. Rate: ${(totalIntrusions/totalPackets*100).toFixed(1)}%`);
|
| 1037 |
+
updateMetrics();updateBars();updateConfBars();
|
| 1038 |
+
// Throttle timeline β only redraw every 5 ticks β PERF FIX
|
| 1039 |
+
tlDirty=true;
|
| 1040 |
+
updateHeatmap();updateSummary();flashMetric(cls);
|
| 1041 |
}
|
| 1042 |
|
| 1043 |
+
// Separate timer for timeline β PERF FIX
|
| 1044 |
+
setInterval(()=>{if(tlDirty){updateTimeline();tlDirty=false;}},500);
|
| 1045 |
+
|
| 1046 |
function addFeedRow(packet,cls,sev,conf){
|
| 1047 |
const tbody=document.getElementById('feedBody');
|
| 1048 |
const now=new Date(),ts=`${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
| 1049 |
const tr=document.createElement('tr');tr.className='new-row';
|
| 1050 |
tr.innerHTML=`<td style="color:var(--muted)">${packetId}</td><td style="color:var(--muted)">${ts}</td><td style="color:var(--cyan)">${packet.protocol_type}</td><td>${packet.src_bytes.toLocaleString()}</td><td><span class="cls-badge cls-${cls}">${cls}</span></td><td style="color:${conf>0.9?'var(--accent)':conf>0.8?'var(--cyan)':'var(--amber)'}">${(conf*100).toFixed(1)}%</td><td style="color:${SEV_COLOR[sev]}">β ${sev}</td>`;
|
| 1051 |
tbody.insertBefore(tr,tbody.firstChild);
|
| 1052 |
+
// Cap at 100 DOM rows β PERF FIX (was 200)
|
| 1053 |
+
while(tbody.children.length>100)tbody.removeChild(tbody.lastChild);
|
| 1054 |
+
document.getElementById('emptyState').style.display='none';
|
| 1055 |
+
document.getElementById('feedTable').style.display='';
|
| 1056 |
document.getElementById('alertCount').textContent=totalIntrusions+' ALERTS';
|
| 1057 |
}
|
| 1058 |
|
|
|
|
| 1073 |
}
|
| 1074 |
function updateConfBars(){
|
| 1075 |
const mx=Math.max(...Object.values(confBuckets),1);
|
| 1076 |
+
setWidth('conf-90',confBuckets[90]/mx*100);setWidth('conf-80',confBuckets[80]/mx*100);
|
| 1077 |
+
setWidth('conf-70',confBuckets[70]/mx*100);setWidth('conf-low',confBuckets.low/mx*100);
|
| 1078 |
setText('cbc-90',confBuckets[90]);setText('cbc-80',confBuckets[80]);setText('cbc-70',confBuckets[70]);setText('cbc-low',confBuckets.low);
|
| 1079 |
setText('avgConf','avg: '+(totalPackets>0?(confSum/totalPackets*100).toFixed(1)+'%':'β'));
|
| 1080 |
}
|
| 1081 |
function updateTimeline(){
|
| 1082 |
const canvas=document.getElementById('tlCanvas');if(!canvas)return;
|
| 1083 |
const ctx=canvas.getContext('2d');const W=canvas.offsetWidth;const H=80;
|
| 1084 |
+
if(!W)return;
|
| 1085 |
+
canvas.width=W*devicePixelRatio;canvas.height=H*devicePixelRatio;
|
| 1086 |
+
ctx.scale(devicePixelRatio,devicePixelRatio);ctx.clearRect(0,0,W,H);
|
| 1087 |
const data=timelineBuckets;const mx=Math.max(...data,1);const step=W/data.length;
|
| 1088 |
+
ctx.strokeStyle='rgba(0,200,120,.06)';ctx.lineWidth=1;
|
| 1089 |
+
[.25,.5,.75,1].forEach(f=>{ctx.beginPath();ctx.moveTo(0,H*f);ctx.lineTo(W,H*f);ctx.stroke();});
|
| 1090 |
+
ctx.beginPath();ctx.moveTo(0,H);
|
| 1091 |
+
data.forEach((v,i)=>{const x=i*step,y=H-(v/mx*(H-10));ctx.lineTo(x,y);});
|
| 1092 |
+
ctx.lineTo(W,H);ctx.closePath();ctx.fillStyle='rgba(255,61,90,.12)';ctx.fill();
|
| 1093 |
+
ctx.beginPath();ctx.strokeStyle='#ff3d5a';ctx.lineWidth=1.5;
|
| 1094 |
+
data.forEach((v,i)=>{const x=i*step,y=H-(v/mx*(H-10));i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});
|
| 1095 |
+
ctx.stroke();
|
| 1096 |
}
|
| 1097 |
const HM={normal:'rgba(0,232,122,0.4)',DoS:'rgba(255,61,90,0.65)',Probe:'rgba(0,200,232,0.5)',R2L:'rgba(255,170,0,0.55)',U2R:'rgba(176,111,255,0.75)',null:'rgba(255,255,255,0.04)'};
|
| 1098 |
function updateHeatmap(){
|
| 1099 |
const grid=document.getElementById('heatmap');
|
| 1100 |
+
if(!grid.children.length){
|
| 1101 |
+
const f=document.createDocumentFragment();
|
| 1102 |
+
for(let i=0;i<60;i++){const d=document.createElement('div');d.className='hm-cell';f.appendChild(d);}
|
| 1103 |
+
grid.appendChild(f);
|
| 1104 |
+
}
|
| 1105 |
heatmapCells.forEach((cls,i)=>{grid.children[i].style.background=HM[cls]||HM[null];});
|
| 1106 |
}
|
| 1107 |
function updateSummary(){
|
|
|
|
| 1115 |
const div=document.createElement('div');div.className='term-line';
|
| 1116 |
div.innerHTML=`<span class="ts">[${ts}]</span> <span class="${type}">${msg}</span>`;
|
| 1117 |
wrap.appendChild(div);wrap.scrollTop=wrap.scrollHeight;
|
| 1118 |
+
while(wrap.children.length>50)wrap.removeChild(wrap.firstChild);
|
| 1119 |
}
|
| 1120 |
const CLS_TO_MC={DoS:'mc-intrusions',Probe:'mc-probe',R2L:'mc-intrusions',U2R:'mc-u2r',normal:'mc-normal'};
|
| 1121 |
function flashMetric(cls){const el=document.getElementById(CLS_TO_MC[cls]||'mc-total');if(!el)return;el.classList.remove('flash');void el.offsetWidth;el.classList.add('flash');}
|
| 1122 |
|
| 1123 |
function startMonitor(){
|
| 1124 |
+
if(isRunning)return;isRunning=true;
|
| 1125 |
+
const speed=parseInt(document.getElementById('speedSel').value);
|
| 1126 |
document.getElementById('startBtn').disabled=true;document.getElementById('stopBtn').disabled=false;
|
| 1127 |
+
setText('liveStatus','LIVE');document.getElementById('liveDot').className='dot';
|
| 1128 |
document.getElementById('connBadge').textContent='β³ CONNECTING';document.getElementById('connBadge').className='idle';
|
| 1129 |
+
termLog('info','Monitor started');
|
|
|
|
| 1130 |
monitorInterval=setInterval(tick,speed);
|
| 1131 |
+
sessionInterval=setInterval(()=>{
|
| 1132 |
+
sessionSeconds++;
|
| 1133 |
+
const m=Math.floor(sessionSeconds/60),s=sessionSeconds%60;
|
| 1134 |
+
setText('sessionClock',`Session: ${pad(m)}:${pad(s)}`);
|
| 1135 |
+
timelineBuckets[sessionSeconds%60]=0;
|
| 1136 |
+
},1000);
|
| 1137 |
}
|
| 1138 |
function stopMonitor(){
|
| 1139 |
+
if(!isRunning)return;isRunning=false;
|
| 1140 |
+
clearInterval(monitorInterval);clearInterval(sessionInterval);
|
| 1141 |
document.getElementById('startBtn').disabled=false;document.getElementById('stopBtn').disabled=true;
|
| 1142 |
+
setText('liveStatus','PAUSED');document.getElementById('liveDot').className='dot red';
|
| 1143 |
document.getElementById('connBadge').textContent='β STOPPED';document.getElementById('connBadge').className='idle';
|
| 1144 |
+
termLog('warn',`Stopped. ${totalPackets} packets analyzed.`);
|
| 1145 |
}
|
| 1146 |
function clearAll(){
|
| 1147 |
stopMonitor();Object.keys(counts).forEach(k=>counts[k]=0);
|
| 1148 |
+
totalPackets=0;totalIntrusions=0;confSum=0;packetId=0;
|
| 1149 |
+
confBuckets={90:0,80:0,70:0,low:0};
|
| 1150 |
+
timelineBuckets=Array(60).fill(0);heatmapCells=Array(60).fill(null);
|
| 1151 |
+
peakClass=null;sessionSeconds=0;usingRealModel=false;
|
| 1152 |
setText('sessionClock','Session: 00:00');setText('liveStatus','IDLE');
|
| 1153 |
+
document.getElementById('liveDot').className='dot';
|
| 1154 |
+
document.getElementById('feedBody').innerHTML='';
|
| 1155 |
+
document.getElementById('feedTable').style.display='none';
|
| 1156 |
+
document.getElementById('emptyState').style.display='';
|
| 1157 |
+
document.getElementById('alertCount').textContent='0 ALERTS';
|
| 1158 |
+
document.getElementById('connBadge').className='idle';
|
| 1159 |
+
document.getElementById('connBadge').textContent='β IDLE';
|
| 1160 |
+
document.getElementById('termWrap').innerHTML=`<div class="term-line"><span class="ts">[--:--:--]</span> <span class="info">Reset.</span></div>`;
|
| 1161 |
updateMetrics();updateBars();updateConfBars();updateHeatmap();
|
| 1162 |
+
['sum-rate','sum-conf','sum-peak'].forEach(id=>setText(id,'β'));
|
| 1163 |
+
setText('sum-model','LOCAL');
|
| 1164 |
}
|
| 1165 |
|
| 1166 |
+
// ββ UTILS ββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 1167 |
function setText(id,v){const el=document.getElementById(id);if(el)el.textContent=v;}
|
| 1168 |
function setWidth(id,pct){const el=document.getElementById(id);if(el)el.style.width=pct+'%';}
|
| 1169 |
function pad(n){return String(n).padStart(2,'0');}
|
| 1170 |
function formatETA(secs){if(secs<60)return secs.toFixed(0)+'s';return`${Math.floor(secs/60)}m ${pad(Math.floor(secs%60))}s`;}
|
| 1171 |
|
| 1172 |
+
// Init β deferred to after page load β PERF FIX
|
| 1173 |
+
window.addEventListener('load',()=>{updateHeatmap();updateTimeline();});
|
| 1174 |
</script>
|
| 1175 |
</body>
|
| 1176 |
</html>
|