Hitan2004 commited on
Commit
d3e88ae
Β·
verified Β·
1 Parent(s): a6d0ab6

Update frontend/index.html

Browse files
Files changed (1) hide show
  1. 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:all .3s}
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);transition:background .5s}
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);box-shadow:0 0 60px rgba(0,232,122,0.06) inset}
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;animation:fadeIn .3s}
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;position:relative}
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;animation:bannerIn .5s ease-out}
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
- .threat-table-wrap{max-height:380px;overflow-y:auto;margin-bottom:22px}
 
 
 
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. Backend: 127.0.0.1:7860</span></div></div>
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> β€” lightning fast<br>βœ“ Live feed while processing<br>βœ“ Auto-downloads CSV on finish<br>βœ“ Full report: 6 charts + risk gauge</div></div>
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 Β· batch size: 100</div></div>
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 β€” ALL ROWS</span><span id="reportRowCount" class="accent">0 rows</span></div>
455
- <div class="threat-table-wrap">
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 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
- // ════════════════════════════════════════════════
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');btn.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,count=parseFloat(row.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','ngrok-skip-browser-warning':'true'},body:JSON.stringify({rows}),signal:AbortSignal.timeout(20000)});
566
- if(!r.ok)return null;const d=await r.json();
 
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','ngrok-skip-browser-warning':'true'},body:JSON.stringify({rows:[row]}),signal:AbortSignal.timeout(5000)});
574
- if(!r.ok)return null;const d=await r.json();
 
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. Check the format.');return;}
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 of ${BATCH_SIZE}`;
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} Β· ${totalBatches} batches`);
603
  csvResults=[];csvIndex=0;csvConfSum=0;csvIntrusionCount=0;csvConfHistory=[];batchNum=0;
604
- Object.keys(csvCounts).forEach(k=>csvCounts[k]=0);Object.keys(csvSevCounts).forEach(k=>csvSevCounts[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';setText('liveStatus','SCANNING');
 
622
  await processBatches();
623
  }
624
 
625
  async function processBatches(){
626
- while(csvRunning && csvIndex<csvRows.length){
627
  const bStart=csvIndex,bEnd=Math.min(csvIndex+BATCH_SIZE,csvRows.length);
628
  const batch=csvRows.slice(bStart,bEnd);
629
  batchNum++;
630
- updateBatchChips(batchNum,totalBatches);
631
- setText('csvCurrentRow',`Batch ${batchNum}/${totalBatches} β€” rows ${(bStart+1).toLocaleString()}–${bEnd.toLocaleString()} Β· Sending ${batch.length} rows to model…`);
 
 
 
 
632
 
633
  let results=await predictBatch(batch);
634
  if(results){
635
  if(!csvUsingReal){csvUsingReal=true;setConnBadge('real');}
636
- } else {
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
- const rowNum=bStart+i+1;
646
- csvResults.push({rowNum,row,cls,conf,sev,isI});
647
- csvConfHistory.push(conf);
648
  csvCounts[cls]=(csvCounts[cls]||0)+1;
649
  csvSevCounts[sev]=(csvSevCounts[sev]||0)+1;
650
- csvConfSum+=conf;if(isI)csvIntrusionCount++;
 
651
  }
652
 
653
- // Show sample rows in feed (last 15 of this batch)
654
- const feedSlice=batch.slice(-15);
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)document.getElementById('csvProgressFill').classList.add('warning');
673
- updateCsvSidebar(rate);
 
 
 
674
  setText('csvAlertCount',csvIntrusionCount.toLocaleString()+' THREATS');
675
- await new Promise(r=>setTimeout(r,30)); // yield to browser
 
676
  }
677
- if(csvIndex>=csvRows.length) finishCsvAnalysis();
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
- while(tbody.children.length>500)tbody.removeChild(tbody.lastChild);
 
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;document.getElementById('csvStopBtn').disabled=true;
721
- document.getElementById('liveDot').className='dot red';setText('liveStatus','PAUSED');
 
 
722
  }
723
 
724
  function finishCsvAnalysis(){
725
  csvRunning=false;
726
- document.getElementById('csvStartBtn').disabled=true;document.getElementById('csvStopBtn').disabled=true;
727
- document.getElementById('liveDot').className='dot green';setText('liveStatus','DONE');
 
 
728
  setText('csvProgressEta','Done!');
729
- setText('csvCurrentRow',`βœ“ All ${csvRows.length.toLocaleString()} rows processed in ${totalBatches} batches. Building report…`);
730
- setTimeout(()=>{exportAnnotatedCSV();buildReport();},500);
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 in ${totalBatches} batches Β· ${formatETA(elapsed)} Β· ${threats.toLocaleString()} threats Β· Annotated CSV auto-downloaded`);
747
  setText('rmFile',fileName);setText('rmRows',total.toLocaleString());setText('rmDate',new Date().toLocaleString());
748
- setText('rmModel',csvUsingReal?'Real Random Forest':'Local Simulation');setText('rmDuration',formatETA(elapsed));setText('rmFormat',csvFormatInfo);
 
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
- drawBarChart();drawConfWave();drawIntensity();drawProto();drawServices();drawGauge(riskScore);buildClusters();buildTable();
 
 
 
 
 
 
 
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;ctx.scale(devicePixelRatio,devicePixelRatio);ctx.clearRect(0,0,W,H);
 
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';ctx.fillText(cls,x+bW/2,H-1);
 
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;ctx.scale(devicePixelRatio,devicePixelRatio);ctx.clearRect(0,0,W,H);
 
779
  const data=csvConfHistory;if(data.length<2)return;
780
- const step=Math.max(1,Math.ceil(data.length/300));
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();sampled.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);});
786
- ctx.lineTo(10+(sampled.length-1)*xStep,H-10);ctx.lineTo(10,H-10);ctx.closePath();
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)');ctx.fillStyle=g;ctx.fill();
788
- ctx.beginPath();ctx.strokeStyle='#00c8e8';ctx.lineWidth=1.5;sampled.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);});ctx.stroke();
 
 
 
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;ctx.moveTo(10,avgY);ctx.lineTo(W-10,avgY);ctx.stroke();ctx.setLineDash([]);
791
- ctx.fillStyle='rgba(0,232,122,.8)';ctx.font='9px IBM Plex Mono';ctx.textAlign='left';ctx.fillText(`avg ${(avg*100).toFixed(1)}%`,14,avgY-5);
 
 
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;ctx.scale(devicePixelRatio,devicePixelRatio);ctx.clearRect(0,0,W,H);
 
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)');ctx.fillStyle=g;ctx.fill();
807
- ctx.beginPath();ctx.strokeStyle='#ff3d5a';ctx.lineWidth=2;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();
808
- ctx.fillStyle='rgba(255,61,90,.7)';ctx.font='9px IBM Plex Mono';ctx.textAlign='left';ctx.fillText('threat density over dataset',14,H-3);
 
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
- el.appendChild(div);
822
  });
823
- if(!sorted.length)el.innerHTML='<div style="color:var(--muted);font-family:var(--mono);font-size:11px">No data</div>';
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;transition:width .8s"></span></span><span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">${cnt.toLocaleString()}</span>`;
836
- el.appendChild(div);
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;ctx.scale(devicePixelRatio,devicePixelRatio);ctx.clearRect(0,0,W,H);
 
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 scoreColor=score<33?'#00e87a':score<66?'#ffaa00':'#ff3d5a';
847
- ctx.beginPath();ctx.arc(cx,cy,r,Math.PI,Math.PI+(score/100)*Math.PI);ctx.strokeStyle=scoreColor;ctx.lineWidth=14;ctx.lineCap='round';ctx.stroke();
848
- ctx.fillStyle=scoreColor;ctx.font='bold 22px IBM Plex Mono';ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(score,cx,cy-16);
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
- grid.appendChild(div);
863
  });
864
- 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 detected β€” clean dataset</div>';
 
865
  setText('clusterCount',cnt+' clusters');
866
  }
867
 
868
- function buildTable(){
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})=>{const vals=Object.values(row).map(v=>typeof v==='string'&&v.includes(',')?`"${v}"`:v);vals.push(cls,sev,conf,isI?1:0);lines.push(vals.join(','));});
 
 
 
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,backend:BACKEND_URL},distribution:csvCounts,severity:csvSevCounts,avgConf:(csvConfSum/csvResults.length*100).toFixed(2)+'%',results:csvResults.map(({rowNum,cls,conf,sev,isI})=>({rowNum,cls,conf,sev,isI}))};
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
- File: <strong style="color:#d8eeff">${fileName}</strong> &nbsp;|&nbsp;
939
- Date: ${dateStr} &nbsp;|&nbsp;
940
- Model: ${modelSrc} &nbsp;|&nbsp;
941
- Duration: ${formatETA(elapsed)} &nbsp;|&nbsp;
942
- Batches: ${totalBatches} Γ— ${BATCH_SIZE} rows &nbsp;|&nbsp;
943
- Format: ${csvFormatInfo}
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 &nbsp;Β·&nbsp; ${dateStr} &nbsp;Β·&nbsp; NSL-KDD Intrusion Detection &nbsp;Β·&nbsp; 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. Please allow pop-ups and try again.');return;}
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
- csvRows=[];csvResults=[];csvIndex=0;csvConfSum=0;csvIntrusionCount=0;csvConfHistory=[];batchNum=0;
984
- Object.keys(csvCounts).forEach(k=>csvCounts[k]=0);Object.keys(csvSevCounts).forEach(k=>csvSevCounts[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;document.getElementById('csvStopBtn').disabled=true;
 
996
  document.getElementById('batchStatus').innerHTML='';
997
- document.getElementById('csvProgressFill').style.width='0%';document.getElementById('csvProgressFill').classList.remove('warning');
 
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 at '+BACKEND_URL);}}
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, ${packet.src_bytes.toLocaleString()} bytes`);
1031
- else if(cls==='R2L'&&Math.random()<0.2)termLog('warn',`R2L attempt on ${packet.service}`);
1032
- else if(cls==='normal'&&totalPackets%50===0)termLog('ok',`${totalPackets} packets. Intrusion rate: ${(totalIntrusions/totalPackets*100).toFixed(1)}%`);
1033
- updateMetrics();updateBars();updateConfBars();updateTimeline();updateHeatmap();updateSummary();flashMetric(cls);
 
 
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
- while(tbody.children.length>200)tbody.removeChild(tbody.lastChild);
1043
- document.getElementById('emptyState').style.display='none';document.getElementById('feedTable').style.display='';
 
 
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);setWidth('conf-70',confBuckets[70]/mx*100);setWidth('conf-low',confBuckets.low/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
- canvas.width=W*devicePixelRatio;canvas.height=H*devicePixelRatio;ctx.scale(devicePixelRatio,devicePixelRatio);ctx.clearRect(0,0,W,H);
 
 
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;[.25,.5,.75,1].forEach(f=>{ctx.beginPath();ctx.moveTo(0,H*f);ctx.lineTo(W,H*f);ctx.stroke();});
1074
- ctx.beginPath();ctx.moveTo(0,H);data.forEach((v,i)=>{const x=i*step,y=H-(v/mx*(H-10));ctx.lineTo(x,y);});ctx.lineTo(W,H);ctx.closePath();ctx.fillStyle='rgba(255,61,90,.12)';ctx.fill();
1075
- ctx.beginPath();ctx.strokeStyle='#ff3d5a';ctx.lineWidth=1.5;data.forEach((v,i)=>{const x=i*step,y=H-(v/mx*(H-10));i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);});ctx.stroke();
 
 
 
 
 
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){for(let i=0;i<60;i++){const d=document.createElement('div');d.className='hm-cell';grid.appendChild(d);}}
 
 
 
 
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>80)wrap.removeChild(wrap.firstChild);
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;const speed=parseInt(document.getElementById('speedSel').value);
 
1101
  document.getElementById('startBtn').disabled=true;document.getElementById('stopBtn').disabled=false;
1102
- document.getElementById('liveStatus').textContent='LIVE';document.getElementById('liveDot').className='dot';
1103
  document.getElementById('connBadge').textContent='⟳ CONNECTING';document.getElementById('connBadge').className='idle';
1104
- termLog('info','Monitor started β€” synthetic NSL-KDD traffic');
1105
- termLog('info',`Speed: ${(1000/speed).toFixed(1)} pkt/s Β· Backend: ${BACKEND_URL}`);
1106
  monitorInterval=setInterval(tick,speed);
1107
- sessionInterval=setInterval(()=>{sessionSeconds++;const m=Math.floor(sessionSeconds/60),s=sessionSeconds%60;setText('sessionClock',`Session: ${pad(m)}:${pad(s)}`);timelineBuckets[sessionSeconds%60]=0;updateTimeline();},1000);
 
 
 
 
 
1108
  }
1109
  function stopMonitor(){
1110
- if(!isRunning)return;isRunning=false;clearInterval(monitorInterval);clearInterval(sessionInterval);
 
1111
  document.getElementById('startBtn').disabled=false;document.getElementById('stopBtn').disabled=true;
1112
- document.getElementById('liveStatus').textContent='PAUSED';document.getElementById('liveDot').className='dot red';
1113
  document.getElementById('connBadge').textContent='β–  STOPPED';document.getElementById('connBadge').className='idle';
1114
- termLog('warn',`Stopped. ${pad(Math.floor(sessionSeconds/60))}:${pad(sessionSeconds%60)} Β· ${totalPackets} packets`);
1115
  }
1116
  function clearAll(){
1117
  stopMonitor();Object.keys(counts).forEach(k=>counts[k]=0);
1118
- totalPackets=0;totalIntrusions=0;confSum=0;packetId=0;confBuckets={90:0,80:0,70:0,low:0};
1119
- timelineBuckets=Array(60).fill(0);heatmapCells=Array(60).fill(null);peakClass=null;sessionSeconds=0;usingRealModel=false;
 
 
1120
  setText('sessionClock','Session: 00:00');setText('liveStatus','IDLE');
1121
- document.getElementById('liveDot').className='dot';document.getElementById('feedBody').innerHTML='';
1122
- document.getElementById('feedTable').style.display='none';document.getElementById('emptyState').style.display='';
1123
- document.getElementById('alertCount').textContent='0 ALERTS';document.getElementById('connBadge').className='idle';document.getElementById('connBadge').textContent='β€” IDLE';
1124
- document.getElementById('termWrap').innerHTML=`<div class="term-line"><span class="ts">[--:--:--]</span> <span class="info">Reset. Backend: ${BACKEND_URL}</span></div>`;
 
 
 
 
1125
  updateMetrics();updateBars();updateConfBars();updateHeatmap();
1126
- ['sum-rate','sum-conf','sum-peak'].forEach(id=>setText(id,'β€”'));setText('sum-model','LOCAL');
 
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> &nbsp;|&nbsp; Date: ${dateStr} &nbsp;|&nbsp; Model: ${modelSrc} &nbsp;|&nbsp; 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>