From 2636ea1e115345f8035ca2c145b883cd7819b074 Mon Sep 17 00:00:00 2001 From: bot Date: Sun, 5 Apr 2026 11:29:59 +0300 Subject: [PATCH] sligh changed to dashbaord --- README.md | 17 ++-- burst.png | Bin 22330 -> 0 bytes coordinator/dashboard.py | 123 +++++++++++++++++++++----- coordinator/static/dashboard.css | 10 +++ coordinator/static/dashboard.js | 126 +++++++++++++++++++++++++++ coordinator/templates/dashboard.html | 32 +++++++ coordinator/udp_ingest.py | 3 +- firmware/node/config.h | 12 ++- firmware/node/node.ino | 83 ++++++++++++++++-- heatmap.png | Bin 0 -> 88628 bytes 10 files changed, 364 insertions(+), 42 deletions(-) delete mode 100644 burst.png create mode 100644 heatmap.png diff --git a/README.md b/README.md index 7e595fd..489e92c 100644 --- a/README.md +++ b/README.md @@ -21,18 +21,19 @@ This is a learning/research project covering distributed systems, event-driven a ## Hardware -- **Nodes:** ESP32-S3 N16R8 (3 active, scattered across apartment) +- **Nodes:** ESP32-S3 N16R8 (4 active, scattered across apartment) - **Coordinator:** Orange Pi at `192.168.1.133`, runs 24/7 as systemd services - **Dev machine:** This PC at `192.168.1.101` — coding and flashing only - **Flashing:** Arduino CLI on this PC ### Active nodes -| node_id | MAC | Port | -|----------|-------------------|--------------| -| F68D6E30 | 44:1b:f6:8d:6e:30 | /dev/ttyACM0 | -| A1D6F190 | e0:72:a1:d6:f1:90 | /dev/ttyACM1 | -| A1D658D4 | e0:72:a1:d6:58:d4 | /dev/ttyACM2 | +| node_id | MAC | Location / Port | +|----------|-------------------|------------------------| +| F68D6E30 | 44:1b:f6:8d:6e:30 | desk /dev/ttyACM0 | +| A1D658D4 | e0:72:a1:d6:58:d4 | desk /dev/ttyACM1 | +| A1D700C4 | e0:72:a1:d7:00:c4 | desk /dev/ttyACM2 | +| A1D6F190 | e0:72:a1:d6:f1:90 | deployed (another room)| --- @@ -324,13 +325,13 @@ The `oui.txt` file is the IEEE public OUI database (39,171 entries as of downloa - [x] Arduino CLI installed, ESP32 core configured - [x] Node firmware: beacon scan + probe sniffing -- [x] Three ESP32-S3 nodes flashed and running +- [x] Four ESP32-S3 nodes flashed and running - [x] UDP ingestor (`udp_ingest.py`) — beacon + probe routing with field validation - [x] Web dashboard: Feed, Networks, Cross-node, Clients, Node detail views - [x] OUI vendor lookup in Clients view - [x] Dashboard split into HTML / CSS / JS (no monolithic file) - [x] Deployed to Orange Pi as systemd services (runs 24/7) -- [x] All three nodes sending to Orange Pi, confirmed live +- [x] All four nodes sending to Orange Pi, confirmed live - [x] Node heartbeat every 10s — uptime, free heap, AP signal, drives online/offline status - [x] RSSI history chart per network (canvas, per-node coloured lines, 1h/2h/6h/24h range) - [x] Presence tab — Present Now, New Arrivals, Regulars diff --git a/burst.png b/burst.png deleted file mode 100644 index 96cad3347f25e1c1c4ad760fa1e4798a9858baae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22330 zcmdSAXH-+`_b!TJLq#^AAV^anNS7`ppn}u{q)V6Hd+#yfY3?koa|rO_ug^8o^k%;tPjb0S60b*-!^k4RsYg zDyl08%Kq1DS16yHaz;IrFA=}zFa4B#?5+KrJw3P#oZaoIRDGPe9!YRL_445oe8OjhpEW3CrquIJD74T#f|ev~U&{=QP*)4?y$+Q**C(Zkc-Uf9>x$KKw<*U8gw zle$@vipnTkL*=PKP$qHuYLK~c#@4oRW1z{XOSO=h&!)G=W)zd@&Qq>0zeFySKY3-s zbpvyO>6ux2)80(QdxN*g)ftSinT642W5hBrhb3{rnYFtNc@i11RJPRDgOm?kLWTAn zqkKJr!Zhbu`|v450|ZXF?5asDH>r@wt*h=l z3->#wC%CMo<{67X4^l6&aNI5L#vyQvt);f5`#lzNIzx{ADFXwK3PsV$Mciot!320z zN)nu_EM}V&VP%}?SL5HEOpFfmNE9$@emPV=WcNMt#qajt_IL@5z0=%jc&FJ5uD0$a z4NtX16_w{*h182aN2#{RP8NP~NAI%rh>+;U=DEpH(sn{VsiK-s=)tpFip@IpIE*>k z;(I>mx2h_yfV64Crow~x(fA0mg`Hon5XL1cE-^OPB304=mA(UPx34c>UQqXQi+le* zN;6a$Q7Kd11roG0daxMUKT|#4`ZxoqWR8vyolb=x1LPN|&MEEVmub11~IlYu)#hI6l#rEcvBj0>_Bt{7CUPc6kq2u}J z_Vy5WgOTAvjvH<HE*GIVgp$|*(VXl8!t>MBU!PR37tL$=4VgsPe9c0#R&lavuM{o7Bs zjOHSPTd)NkF3BJHG{{H3To&8EUClcIkyAB}=^ChB3nQUW8DceA3oDXx+D|Og_~r%a z(CP%0b)OP3Kcjqm9G$#)XZI>V#s;8DRN;Dzs?N(@W2lCjF9Q6t_qGK(!V~56#h_^o z_8()DxGx--uA=%}(-#c6Nr$AF{oUHG?x3460za@+waj-7RR&!geBm|F~k`)l5h+Icjjo)&b< zS@rFczrEA-3mx(qS~Lff57^mRGEz?znJFuAD})sYCP`wY5$sujeZIAYu)#qETEqC# z<7i6-E(tJ1>c*H*Dq(2Q;urM~q?e!F@XFTLwI)=pUd2!zy24oXgW7r%ZR_{W0Dxnz zW=ZFz#DuuXWbkla=g(nA{1-s$UKa7^eUy&+{d}$tB-By7{||%9E!YxV75lN|y`-AY z0Gs9zc9<{TA+*x?V}f~XTujPI`GeyIKt=ym(3@08@o)7Uy|0ZynLRo4HtS?d-)~(r z*yWY8f#o$tO?9oxnC~;C9L^;-+?4+9LTA%*@~WL5l%={mGWY14%NN+$TSCe%gP=;9IMIns)=+&<$RCL|>Wn{j>|ZDLh%K48;s%+4k!ot~psDX; zAFemHQ3HWiwsAA*t~U2YWPg-p;V?uY(=L7U!F%zX4!7-Ohdd)7VZ`t zrr@D3Q*AAhq<((L?nS?^^)o`!UMSF-z6i{Z^fJ^$Jv zyMWlmax*M~5e)0Sg=Ishzsf`K=mfe#fsez&d4NyXI>9<05A&XrW{2L14BtkDJR1ow zYJ8ZQ@NyV&>+-j;Kan1Osj%JL)!eJdTP8P=9Ve+!B@;WNohZR{J(j8l!}8 z8Vr`U!`dwFt)sIo=}CD{)}3f}bws7Imr8`vm?~zhrxny@Dw;qP z7cq&G2PkddCH36*K{FAa{p50mp-UZ7KlG%;-sXIEOJFa{W?u!i-DwR@39kPHgT^QG67 zIbu%4Eh~jH)N{9;o{X0njwy8;?OLB3+I}?>BdxZ*5_>}Yn5x$6>H;EN#7phxeB@?= zjyR+y{1nOWl$KN7hA!5i3$Xrpe5HR=5JFXlZuWZ&QrhB;N22iSXf>V}`vCdE;T6J~ zbkmst;RF{;YPolhr6Q`vwGnVAzACL#?9pFZP>oxA?hjW<@jZH)*C>!Ve9(q?s|wNU zKRI)5#$_vAn`UYxB(C^W0OJ7_te}gk(af%iiIcWX5W}N7eeq$Cc*nN2@-M!WXL6>l zPOXOOdjVHaUr?HKOuc(+a6f^kci8U<|Itm(f6*1_9&ITT`(M;FMOfivxA+WQ8E7Ht zLY!E4Q=IhVqS64L;Kl4e#`hXPEIVTMP$CyT6i_WAM{l;r)Xd7kM!KtW41pIdnRNcAHS;@Av9;P0;6>Ws2#sGjhN4z~yY~PCMFM z{3CJ4*pGEpepk`5CUQ<9PTny{JknLX-}$q zl`Q0JGtv!@Ea7hJ675f{0<*}>K?fY+88N4`IstrNT6I9)6DgcJX$0uCfx9tyWss`;NE*e#|SG5-tO9nGfjvZ!OssClAuc6G1XDI+t z0XkIW_H(NkKwc+pyggwB(CF;AHIrb8?&n>gY|NRGEL_<;duVI{V7(4-{WHoMWI`S0~9Jq8GndPpmk-qa2gpx<|Z z+tsHtLB|}8;iZI~YR-JudOTESnKf_}@BKAcsZrX{#wFYKON`i>6CbVA-=5WRWKfx( zhO{HW+~pT}KAfktUl#LJkQGq_#nWWe;m@_~C%F-GAo1q~`NM;%hH6Cs_DlK&CB=$H z>;s7=bvilNnJE!IfXJ)S(MjGCTK96&7%hWCQ$syOO0giwciA&k)2I*nw)~m=dpCSV zD9;PIO=}m2BBZab8og}6;qaeA_#5-{Rl%OnL|ik_q7tN3aQ0|f2rjsIoPe_r87!@; zasQqe1%Gxvd*hgTGQBTd+r+2M2eBtEuU>R3t32mNzH-67R=0KypCS8$Leo`*J75%I z3DE_M^C~a7~&m=zC4SUGNPezIQa?G=IVe z*hx6aJAx$~3?+jCnxj5iB>Hl$HW3IbDHki^zbs)!`<+DtuKnBn)_TXOs8`AGHSWFt z4fk2@twy#;(d3jpigzaD$<+6pN5#exeXDjW0&@K z(@UocK&y9KsB~8@3-nckNA&MT#=rRP#i48G4Cr;~Qw&qr)W_rGQ{Kr5o2jdRZB$K( z$T#0TlzeR!m_506M*fW>-(6kw1qaISOQA^b07wP*^YO3CLA7Un1-s#3^6n9XZMAM9 zCoo+yI2Q{WI#EAr2$L6oQDYzErBZJuPb3AKa?m@I`ZLXq=Y#?GwOO@CCk8|1keR-x zCA1|=L2RTtAS$9?t`YCIvho{L(VuP04!t$xdY?dEB$DTe`t$zcWq^e*2Q>pw%ogS=ov+JImR z7R_CBP*?xh(J}CGgdLq;RwfqnDL&mKID;W8lPlb!aY<@=D4@Qr$>9tx&a%$_Xp_0fF&DWljbDJVTmg^10IrFFoFnY4ZqTJ7di6hXVOz4CVVq>r4C6ZylfH?%BnBOsHJDz7#|zO=HknIC={EF*Gq{ZIwfW^<=Xh z2SO(j#*Ub>)_&%3kBqQ0TQq^X$ZR!4RpCehI%IhwA(-EB#?9<}&3UuBS@sSKBd>3I z59AGpel<+Ane?(M?xuVfcUNY{e3Pi*;-BkcRv)nSp-1aCH9FOk|NO8Hz3Dm~{@t^s zX-r@KN#bG8Tek|Ko``~@a8kg&hA;RXRm))#O1?W&R5drJ@O1rZFHN@F^n*erT|xF< zM~;G%i(5WkGpf@5)d3v@h(CiZtD9-Z7+ka3=+Zod(OubqH#tldSNdx~xnFO9Q?bjzhzOGw2J<1%aOcc%}>JFHb+akt?N)dJwlqrv6m{nx~znKiYb+WJv-BtmSw4bY4Ft zca#UfpvT~LxPF_So8_Qv8{~*q3j+#i>z}7IwZ`M& zYj^KKKYsNxR<6V}cJC{&t9*{(&jYfwtEy2)(1mZ*)9ng1w5Z84eneG^#AtnDq!Mk+p9J8{OhWf%)p)e?V!nEZd~6nS4pvkQS;EAFKbEmy zp97b7TA1k<=Pu^sE~Q=CGJ6b_16;%ojsPem*##vlh(4Txnl(EQ*?V)JG=+kt!8gD8 zo?Pk0Qx3-)7O)G_Q>KOa9rZf~dUk8v*NvatKi#u6_Th4QUg=M3C19Q&~FVEV~HN?9O5vVJNIM|t8%eVN5;D$aba~^|isim)V8%V|Q5{#-V%>@juVE*4R$i*h4Gc~`8lmLd!YJ3x zH76QcqeB75i(+!C54ETo_9~;x6FzXTbCg|>3k=SSVz4Q?to%-SXf(p1Ro})mg`M8o z3O~m0UO=rhvpwW}j;g#`aGd%;&fdljRLKsUJ@IfKuxizY-ewZ3tCt&)f2DS7BD7hsA`W+kv@uln|Y1mWC(Gnj0PkrE1w3kA~v=+)-YII zzvtd*Df1_GNJ|2af z>oZ`}0pGkly52BT`D`<@{8?xy?LOUPp`?MfHwr19S1h~*ihvzo6QDw06ZY>VQTylL5e=9 z3VSLR?^&X(x+c{$XS*)>Ks)}<^|Uw)UMVk!oHkh_gEPOesQA;{QNjU8W|G44MZ&~xGTb~5je#YgVg6J@^!vS@q76>VmXX9 zdEdWvj73UXZ!|2iEi%7`zS~vYoH{sw6)$Hq<#txE4ZAu;S&DABo0a6>FWeQ9U#=J{ z6wDt!)M7NfQwA;|ut)~A{uYEp7?HeI*deb9@b*K_)ec{LK>Ehnh@sC^p}5(lYI z((()Qb;WnYEy(lw40)40ccl(qI;PwRIRyk>&=K}Y@CSkNFHCnlY)=(EYWAHJD@ag9 zYKlsI_TPtXzCc4@PJI~fnC*a1;!={2AL@!F3~D7Wo&Wi9pGK1k-^_7w?#z^ez3c1rM2qUlp5HDFZyE)A&%m6)}0Ou_u$|V>wv6-l7`$$$TL0F^7luv zGE^J|niM1PVjn@6o)+m7+ z2U~M<4wPE0)ksrTQfBGOVd;N-c;X9kg?+{0M+k%aGv+t=<)UCkdBSvN6vNVP67Aa zP>Lfzl6)*48u5kp?Fv~^+WyGJf@xJ6h(d=nSSAYc3WD`|4{MIpqg>%)#5ROy-(ZOL zSOSl5Rd4A&crA!-3xoByyW^lW`saFRb6K|b^{!7UYQ}^SRNoW2<{T(g8FXo zr2l!zsn4vOKlJIF1>p=n-R(hfg~vki?XI~#%k(UqoY3*ypbU&h8IOHlSI5BjOhr9j z!_b7w4I3X<9-8bQ74wQqYgV5w@w2RKKw9H&o1DP~`$9H+l|od0tE!!rN(emwF`1V= zn;TA9*is5m57b)DZQq5asm~1A$Zr3Pa27zP5nhI@g=q9;@V28SxM|yqd4-kqnWlp# zO!|wbf=4zZuF=pWzcswUQ&Xu<;Y0HBb}Cbqnsq4LIgi2v^QCVV51lWYFfgzR5Uap? zO^dH@(%pk2Z`jA$k>=`1n~o7%->H~QN$YHfW9Uc$ za`SOk?#YTb&5aunynBRs(^1R2ou9)99svPU#obZSj+3?~#>XoK1kZSHUbUNm8;X0Ju}@Z5HIdUi)k9s2@$U7cF~maE`6gIg zP*BU16|o@Nd{%ODicXwpFOA$6-hs={@DMUshNee(5uA~l5&Q2ryAQ@;;U^oB5m{5D zd>m2i?5GK#=omyM@JD=Q^F59)*0zqB8mJs+@G1KSeUM3*-_VuR`4qC6EQ>m zCCJ&;pJJnrzQ=kG%Dxo171b3&xJ$B%{G}z@Hga_S$k|cVl|Mkea(*1%&BMmOfx#|QdLHC=l@Z6&6?F1&QZ3VQa z>R4oGWE*2PNjxLCukB&_zHfP%`0U!Jz|sInhw#we5I*1rUj##s;@19MufoBD%yd`Q zbBW6tm7|jIdC%s!Te4juR?TEdeUCe(B5APP$1E$WkS+@~_lf1(J=@D@A%8e)^WL#+ zcq&wPsp1YIJShcwA5i&#m@g!|J*CL9%DIJ^8E*|TIVy^;jw_iAA6b7VvE4Z`!Qyb9 zs^R9-a5I3c*|uRjGQe{1MQ1-0e*NpZSX zXydZ#k3J=jbmoYouWL-|+59|`DomC&Mi1s~?Xkc5GYwxbm%>CVn0w1E5Bf2JO%K`c zu|xly{m|&X8hUJjtDUdW!M*nNqr862*k2H#6p=UTF=Gk?|_G44DTyugX!jU*^^3ZdLNhappxq=1CEC~}#e zo_?oO^b~XR)~$)$K7;KTnw?gwGl9*pUW7#rGb-Y@qEWqH%-;TfCm;U&{^7_~#RrGa zqjtu~jl-LvE>>jZ2jnp8!UgcFgQO=@En;dLjg$4cphhe9Yp&fr z_$SRm#XB!;nQLE0X?<*s%7VJkU94;!sjvhJwgDhh;SSoRnUe8+2$1r&Fd$f$jgfcr zQM|c%m`w^JkGo}4B{I`8{BG^t_Za~zGR(Kt$sJ#(?7QHeSjo;~P|Ipjs>+Y3c~QmL z;!}h>1OBq0*1~%l*ha{KiP=?=#*ir2>CUWO=d0mNAp&Ice%PN*9UV%QK`de11p(~IM1b^w z)5Qi>9SR+>kFlh!7pe7$Vv{+3RdjP0C2DRWpavcY>PBaMe>(bHrZjEf`=hM2o3yl$ z=Q7`t2b0AZ-AZNGyd$?EuSjpu?XR_Tw}0oQz>O4c$TlRuM=t(zbl<}dCnCE@ydznLUr|FH}`4bU7cUSq&PTkeEjQPAbBDge9Rd- z$Tk(~KGw9Y65qSt4*OPxu?M_s+z^(ToT1wAVd^XEE+Vs$cV>Q7XRkKY4ZmI73H7Lm zxg9uh;}ee}u=94DnZ2FYj_Vv~o*U1DNN}4J29}_1czfcC_>!jDeBBbMcFQb|Y9WJTOu&Hjon+XGtNBvjc=W=cTEC~*LVs#>YD3dk zRS6Pnoa^_J02QA4{;3eN%V5Ecdz*Em4r|EDC!?qvX@HJl{3suL{!qhOKO&D{*C_mW z9`wo@kGK2|6nL%m)WIXcFEF5+S!tWaHx3Cb7gfA(NeOOwIVHXR_gH-lzWN!A)HEL? zfxoY;=AEVyAquz-+R&rOnRV68=c^}izLy+v+e$iJ*hbG&p+P29^qY6rQ-3o0%`tO@6^>*5N#7n9ws061Pi&% zDWp5}Ral8F|5yg)oo)&;jkjky*3vCIc9OMjSgalT=_Z7i9^s>&myb%ieag;RzXnIT zGEbVpalsfNMN{i+#Tp3Q|1@kcSuMWzX=mv7?9IcfJ*bYW2SH!Sp?$$Vj-1XGJ7r(vO)71UDB6 z;+&kZ8fZ}z@JGYJ8|5ccJuyR8UScyY+x@D7cx7kfP!xlR>-fP}J=tEVzxwL@b$njj z&BM*d2pbzVHECjM^=f_vhnDpA1tJnZG2k-KwjW)*9tYTVc4YX5nxn3_`1MX1pjZz6 z4fLDYzoX>6bDMj+iY^Rz#r#v3)s$z#n!9wu*Y?lpynQ>J*v-b~3N;dpM~`?0k*>yi zee2@+s7x#?z9@U9ux*$w;JK}3b&>`J7q~2rSA@D9Dhv!ecFV=zIflHN{FE?JmVi2a zsagbvWht6FD@D43bA)Av{q{yKd2Sn6E!sEBp2s1oJ62Xc#vxRW$H&J_VnlQn^Uynp zAOXw5zHZ47c%9-2nOMHCw67PkuZdZ2-965$=@s))g9vNGsfI2VHG1s)DE&1cr6DdI zke?bWb=CPtd2s%0_?W9F8+(@sqv+sIw`ES5I*wsY*T_IL?LmFFnK2-MDTv5;UJeiB+1prj56?dm{ z>D9h``NF^vk-x$wP2)kfB_lb{9P2g=WiosmIslk@Irf-L(a6;chkM4&JY@K##G@a| z=fa}}0_*tmeKg(p`EpN|=&+ce#4kYAm}S$`jbpM-C(fg38qg?SUXfecCAM(7YUY{b z$Z?M8v1(8s5_i-9WzGjhlEcYe3Cz_Umb(-Feh>)YN5u9tXZ=R=QG!fIz;45b`r(T` z?=!z%t80E})}%ZA@EkiOVhqxd4sUp5b_BY7%EZ_bBSl-h3HMzv14o?|EGby9G7BS@ zP6DR;Z1otJdC$$H)dFCg~wxO<$kD>LCLrI+BpSty`hb5mvT!bXiIc5 zR7Pz9*Crm_4x+x*H0`;{a|Cpk+J|H9K&3h-80XXoPwOuYwfQ{&I!=jrpHr0Awlmrb zq|3lWf4?Q~>u>jv?R|}HY5JzB-4pFU*U!V+u7*`bN_}6>WLLp8R4grMhQ`nPv$WL( zgcN-Isy-1!E(s_)D6(h=fZZ23^anCjE44Z8SG(#J8Ir&)9TUD9nf{HeihC; zo?kzmw0+xIm*PY+^E9iWk(R&MV(mf0rL+A;(YHzQWKSEtNf3;fTi{onbLG5L9{E}; z2JkGilMAQ_sTd;q1$E&!r^72#(p`_HP%#OLX9>+oNiU2qYS`xVy^KMZ8`KP&oec5A zcJv56#>M!clJ~0bCvIJM6Sw88Xz1vP+6$B`;OMPjzr|3Y$2$}zP4?+EQy4W4F6?Z4 zzh^hGRc*E&j`Z-u>?l$t$DWRchfbai*_E%Rb!@IuuU3wb}qTeSC9o4;PFC%5EJPnGp=;Z6U+k zaC`h=G#HZZe0jv=^XB2&E`zi9Y^;a5V|2i(G*9l@iym)GyeSpbyvcwG_ACg8&f2Rl zaVZUMEFBKFG}I|yTGVg~f=O~96qt(u>y(E#xAh*s3;WU96Zf`!w)Z2?bqeK7tV54a`aL6<23&7#OpA8%^Y5IZ&uVyhc)QdT zcQtUKn`-!z>Tq+l`={O0n`v>GZig*=XU}I&`f55wo6otA$6YtWAPsHqrj4Qaom+*) zio<3*fl6l?3Z(Yv#Z@zpq{!e|eIGnguCJbfxG~4U5i~ES z(V`l7+;FKoLw%;gJvwZzkvypafmpU{7GN>+$iVLD^F76hy#vPTXvf23Tauj4^WCMc zcKp7MA~p*GW5;WSSyMzGK&|Y{*l0A(PV;WVZ8k_}1|! z=VJEFHD+@*ijgT)MItNTExs)_9&f*;NVvA%hWPyDifI+}w6G+a2bbx(S58TY4o^Seay%Kl>mE-{+MuqT6XiD4Du!SrxKLT0^91<)P9k{=^L zE2NW$zO_eZ`hTAwrwa0#OozgoQUBC0h{&PELGHcTG7tD&=&K{zhB$GOG)@3>e; zcX3>)B~LvO+9qS+fG)I*93Q+OGJwZuyF$&3?B+7f+~(wx=5-3XK*+m`IXNGzPjT>SLYGI67 zpSfA5mBJ_^@<`h;d~zqYok-Sg@Q4Ag5RZl(u)bkwPzj3&YG^q{1J&%(kciU$$Sz)3 z53(K@p0SS>l7oQd^uBtvU6eiQ@o1)D$zwLH*oDJY5@#~UX1p8O%O5KYYJnNSZ_G1? zo!wfKld<3G*bnVICSwJ}euW~lF_E;QhO!&jS(C9Y?Qus%<@L@ zj8{BcqzC%kUpuT?UH>~t=cMG0Ba!B>m4>mFsG*rj0GKwuI+^9G+W8h#BPZFx9kw;tNEc8Ro!D}=LJv95-5^vXWX+OYqAfRKgbd0d?e_det>)pnt=qmvK5Jj*wVVxIq z?gO|hJ3v&IyR|2mf0ih@OwoN^#hoz`p9`>_pzNypLCg7$Q&a8w)4p?m$v0Qxq>}T0 zSR!fLCYZ2eRelO2f1=l04*|nx({f}a@4k*#( z0Qa{+6qJv&DxVH^Y3TnvJZ@Zjn4Kb+KA{_9N@1RR=Kku~4)p1cD4beU=cx;9Ashb3 z8U#*3M=V4!508?3H*C;q9SJbR7;s!#c%q#6f$9CQrK@xp4y;|rs*HsoE@C%TxRQY{-)_(uSot}?r`ZFd zsCHw0lA4cbo~MbO*2O+B+1+vfKuJu7w4W~veXaZnlNIgfc`wjgugdf5q(s?tnh~sD zW9WXm2!W6~>uaWX?#SQ5bs_alBl25$+BBx;9UXsHeK!s34k5S`lji%gX!fL$U#sw84t=XS36r=u)n#^+96n(g-05a6Pu}$)60pEwWi@S@hH$r=9 z9vU&>YsXF+@qL~pF;d9uT#`S<*kAjeCk)KrV=f!7ecXOI5S;NjO$yu^7Z1OlQTQ-y zE;b7S|0EDR*MH8(?_5}JH+p^I1U3&l?ZGxZ+?T4_Ip#b!pJQaS8$Ie?SSXX&Et5xY zOOBRH6)*hCI(r{R^rh2uw~uF^$Xzsjey9;6-JWYyS=w}XWupGjgkOL^`E8x=0zr+f zF)lkNhd6(vkcTVUgKIP??6%?;$7GtcMKC@&Z1O>^fEvlC0qe70H2N`rMr%6#(M={C zwdDB#MALSi=s03HoGz>j@Qo7apcdT>kd%d@duZt93D)>BOd~oJl+$_=s_*bgVQS<) zluu{l&dFP6&~!!`Ahf40d^{7UXslosVb`aN!GtU}QaUZdgA5e?Dm5SUY@e>|FksiT z5x#S4;wgCpY-*=VftIYC-23`VVw~wzd|d5uM}EO2jk4iC!V2EyTNk~VQHOloe1s>I z5Hx(cE;Xlsk@GvLmt;q@bXsDQk#Jt2*0J3*(hipXNY0}3;Bh;%NJN&@(^9GY(|!73 zrf+PL_S!12xE1w8UAb`!^?x&{tluoc=Iw|4iY$u)IDJz!ma4elaSu66Zcmo!t7-Yy zR3fCklHGriK_H#HNY~>?f$;vZ)hnUMPwB%M7OMQ_p ze%Q{_GRYHpsBjD~{Vm=8d<|{sjOHD}&e+aBG|jJ1e6pkJC~}RK&fnr5f6qyNRbwe| z7b$kpo$+rDsS#EDn`bKc6W#c}ik#e@6WbHddTruIIk|QD2%jZ+p#0zX(TUH>Gw}lS z0eH&ktyw|w@I1H8+|g@z(w4u_$}SVWvX3WSb^RkCUU^b{kO#h3OA0*M56)m6Gd5Wj zjbH%<7y8+~uu@B82j8K;V_kst<$SXc&z-=y(O}!frB;{USteGq*0Ct`uu>mOJjcaC zR=>>56*hHg0cCRU*lFnp$DcNs-g150*Qh`?u>22ky3)XdsQgQ=+!@-t#N=si6YVj! z4y5qLvP)`x@Y+}8a$~EAC8?#BnbYOo(qmJx7_qdPfq}nRb|Wo7P(yUH%h2g^#NNl^ zM?>%A0QWb(9m-bK^f%DPx;3zAQp74Q5c85%aSaPmc z%iRdkY4m>v@-i1h>x&)~DF3O#wxpOzkN=KvHOxU8k7_Ww$n|ESa%mBwswm$^(UV$HJI9SBzj>S-VQA{<0Y>tN{S^O!~-6U%JoPJGY=)vn1`FYViDJ!6%wwgw3Vq(HhgkneBa`IQX2JkJf$U3^oVD2!fMmW^f(r@118ktToCK~dQ9e3&wloVkb$IZSJk_Gp4)Hy181BV zwY6EPSH$-pB6qrtPg*Ah2{Acn(nuJDc&B)cx67=hBNn2R3Tk z`pI=Yq=XyB$gE%STC{vt=)g_6xsLtl;l6MZwv3YA`OQjO37x9f5|^3 zn^cd*;yL~iga7B75u={}RssIC0?AQUwy(c#+y~98aZg=&|Jb=i&K=qYXzQlrOPZY& zR$kwT$NJBop^1UjKnvZnLA{V(%?w9NgPo@P)AF+Sc7@g1@sq;<91sDGYf zlF6sscshig;C?fNMF;Qtr${wFS-pkI+;9{9C6xn-+fu50< zegBB1ow(DD_eR_<5s*4|e6uz-JtA^y(HMc1EqT0h6e-(snkx( z?21|eg0+@4%w4i8u`=BUD=@r&eqUeGkgKuII>wxZ^=?WN$>aE?lM}25_g#fz5<~0J z!3ikwMp5R7h`m^>27r& z?m?n_dG0G1kjQ&FrcJ#Kk-_-S2Dffr@1rYGj;N`8p$OzNHsbr;X=Ns7o;VGW(28(c zeYYv*Prppu8~Dza^%ehTj=^#D%|!RImQO_bO?b#!$7~W6^wZCkhleLq6FVBis1Rj` zqSg;lT^k#UKcW@|75`#jco0>rW&g+D)$JyiH1AWnRk*!d&*1M~GjScwUf1Ps0+7@x zKY|A#)w*W4u7*@A`bU>O0J)1cy z;+KhPuzGjc^g1@V`5+U$^JEihQ8UTG$Fh_uWtT9~lps;o`~Bl1eoROjuoS~;1_q6s zixDX*t*J}G@#XyWdy>RFl+n3;5_(6^w)RHpOONj4lRVy(SA7`dQY{BN%QnW+%03Ex zkf$`$)qMwz(bN>NlH*q`J}z`2i#4yA`&3Lg8lz!*rpv)km7*1m%B>6S5W8mYC#>GI zHot)m&E9M$7i0n9A9CXi%(hn^T`qz!dBu8KI?~loM~0{h1h9!+J-nzP+C#!LUG!~H z{kImCQqmmqlPQ8Xf7Zw}Nwgs%o+3YYlJJVc`_Y1Kf7l>@3BDP)=tCiyat{qoO%m_G zFFKI2Wy?2{nR5#pj4q~S%6H#hC|+C`d*|bVyU)UzW&SZM$hx^(`BRABlj*(?6*gblTN4hwbpGX0Anw{m#b*Q zs<%s{v#Or<=`oJ@?qE5{4ox%(AO8R1sPE{`>&Z>+N$ZNe$uu9I{ zrzh}n$_ggyV-m(+s^&ODT{D$&gfj{Xu)Eza{j^_?LZfg@?5y#DzE`&ee5RFhr?#rB zC=8gR^qTI%?DU%E_-0fPn;xC+Ym0kjnuN{A7=5qrcxErLojX~(h8(T1h2x+1#x!k% zN-fSvNdMrtW@TRp0c^Ibwo_}W%Toe2q};*%Ta+&{>(qL@ zV?^F#&Uin!HT&J|n22AToj2q4wM5xLnXYg71kB_Q4lg7+cqV3iiZ{B5{N3ITwm#18 z?Mb{9uTE($?~%A{9@j~#HzO6rJD6myKf0XGq+NtpFut)kI7rVTbBD9HMg9h=@~x0Q z|Mj#kq#ALjUb(={z6f7Z0A~_{Ck^%Z?Kd$(W<1Y++gCs0TxCg8fw%{Ll88?XgSg3U zjT@m&!gX*(cPcLfLals1PdQWqStMxLJ%OM97DxB*p{4i|Ztc#PQ5wU093PiE7x+mb z*}w5edN-@Up3NL)$b)AAfZ&~o(ND+gZN9{1Z--$p=mRRzsD|ukhb`@!koOBJke4CPB6dnPCl{>_HX^k; ze_keSf)jzxo-Un5cXlSw;hpC*;*asZlPJKhVmL6|XJ1&RqkL4*zJH8Z z+HPZGyP*7cPQDy#RYgy8$L)m0_(TyeXh(s|EWdJM+$$l4i|3 z{NDS)zgm3PD-10Da>NizUW6JFmYuNWvldzR-AS%)Mp9QcNU$^s3@mCi=fce&VhIu?BYYYEY@wXH7&FCf;-wb3J*WbhC zwq@Sf_cegkMwP1B(oKpe9kd!@eXky01!b$*Qks9UYl4bNnMjdFz=uC>WGrWC%G*0P zKp@`14VguL#lPI&*2(>x8(#tjEWGBMnd8$&+3?WhpJAO+j9Du>A9f-4PF^*O&e~9d z^DAkLR9u z;n3HekY`=p`TA9)rLfWjr|WjN)6wNWtxohVGwO^IiPHGw-%eFwSiA7U*sB0wyMC}U zzMltirT(agED>z<;US}Mj2O`fY(w~H)>t(vtpRtAU8SO#U0iMO-xi+vu3`%E`r5d$ z{t6uFh%QkV08=@C8Meeq5@f0KpN!#PxEH09r+mHTmVYZacJuNc6=}JQj8~hD?EF@8 zkN+AdG;s>=kJvr>!auyQlC=L2B8#334%qS|rPyFl<`5*7Z2ot}p|hra*KoDN?d8e! z02PB=w}xY*x^-b0806WHh44!67`L$Fl2QapA=A&vy_x^pl>$(4L1UOH=v6NxaPYB? zU9#9+J`JG-=;2=x*LV z6zythd3|c;iAUZo%;A1xRB9#28M=JDXn}524hacaq$Wp74)i9E*S&@npg(!2(Krulr+VSd#pvgDBkVbK%k>)w%JY@CUUArH#5^{A$E&K$ghtwA30! zTTK?ftf~;0x!DM;`!Q5{WB(CWUn(H`UT44@?C=8jTBEoUKv_z92i_YfQk-Nqc3!^> zD-C;B|LLeqg+6nSs2@c9a{5-I_tzZJYw>AU$hTU$i>`4@gVHITO_@}YPJO9Qzwrn> z!H`0OEMpH!^e?tXDxRY~8SrMVgL}PJYc4El&1DUdLSM5&UG(Y7E<+w+6x((g@A_kw z(uS8NMDGgy$DZ&1(DkZv4-NG697uB|L6=HZ>qy|~AX~x0zLWQy=NZni}&wH7dS7xY(E6K1^ML%$M6N~libVMz?*9VG-YnX z3jIpAZu^x{-P)H_UncVY-YZU5bvw+8L2jC-i(31YrxLBzNJfoNtB=wzAB~!ndG{_4 zVaxbIpUi3xX3@y_Fs6;lx&c3x!DY9u8b14c-&IncvQ?+)mEiV$BfUHAD_OpBCOu%T z2H){LXJ}+w-R<`dv$K&vg_p#%LbZC6`=GR(g7$%kSkr5>g>ax}b$p<^kOhW$pugm3 zM^+B@H&htL~04ws(oCLn4C}qP-)zXYupl5cq5$BJ$W=@QClB#i@4Ai5%$()7?Zacn# z1SwpLT&uOyUVvCnlM%G7&)I_%5zvTJB(%|7eboK!AEr)~TRl83QAo~u+nn2>$F4q6 zkLTG8s9|0z{F~SjKl$6+bLE7(=JTc$u^*7D){fD%=xpwL_-L&6v$&o~ntCA)Zvpig z6ALgKWW;`!b5q1hRLb#`t0CW(j-6KT-@7;3X_0?^dEBEG17fht7sTUfdPiyLm5yvp zYc2$0c^;dbY(LTvKwz8#I$i~M%(PFkkfx?w1R;l-aMIX>_~G2<{C?ij4_DpINDL~O zaJ&ou88P&!rz#L&hl3Ud{lvPB^}s^})72VA@R9=Q_7NQOyt?T!CJl=BEoA#%o%p&F zBrRZFfxe)qGSvSpWb%e_9_qJ#SU$$i%82=!;2eqtQuI0^jm!s>j7tOGO~8y0&srqLf|s}s6mX&vH~yT=*!4r zk6^PuYv$5#CcmvJo_AnH`M;i_GoE?3UIpc>pW6CC7*^XvOw4;p^MEd_Y+TgAQ>cWU z*JI*%2xGQ+rO`@d zzRa2W6J1KMicCGhO{1N1%X<^T_82goUs&b@ren|$5r~?ZvpYEI(E9!buie>c#Lxov z3ep3NTk^zi+0f6UiTL;jbPbK&T4O}cfxc4B5rgVfGY!?x5WTsla%#K}E;rEbttzn)Q znkZPM`Xm4vikuuhQ%l3h0f=R!8zAeq(kmRCztjz>oB9_K&5f24%}2#B&q-p{A*NQpx?g=}j?8X9 z&CCj5vWX5?b4!e|hj8&b$!ZdRaqATVT!hoR><|z|<$e!yIXN*gu4>4L$XJ)Ft8wGu zm*|4p;?Ww|_Uw{mI57EOPBal1(x|Y%L(sa^v|I48>!~&cb&!CK(O$7%9MzS2ux(Ulv|^XZud ztt@C;d>t}5gpb{4{S;tPr&V{PwrbK};@MCYvSGDGsrUpWN;sW>#JBN_IL-2>FDAlO zxnB!qW{UXd<9lnLi#xotrQ{9OuH6RVbR=7DmDl+7$c93!Lff{X0>q)kd!g~-bPrVy zBOCnPV^C(ACrfL#9{Dj2dI{3XQcWRjDlyLT3cCysl?gN((0JN#s*C^GkaEX{g`GY9 z?j>J` za$n19rK+pQ@k%?{KvgdNX>W0Mcv>M=Dyn2Qci;6h#B=r`c`c+94 zgQ+wDkYVuSZ%E?d_?eXMOj;9k0 z-&2uhkjb-*Dh6Ob!3+(mX<4s^i)307G^dN^UEidosCSvSZ`~eXvE0oadcJzEw zx*R%?{K-O79+_gEBoCOqsoeigGpxF4;PV6a^a>6K6Gm7SpjVjn@(D>1n#zowPMnAqL6ZJqCXFegZN?M#jVRV0y;-)U)l*^+(3>39lUEzMEL zGq+4)Hq}$^oc|V+3dVPaB{=l1T=;f}>~R692DO>HdId550=B!&8OGJObdP8w=tz$n zn_;X;!W&!Si3R}OwDI+FLSe}}asW*-fK0tc|}gln;d_cJm+ zfCEE(AAvKHuSE7gAo(m_EL>MK(RLEqwCCru`E4C)Mf{bGlOydK_u4C!_<2Rs-g%{| zK?^Fi3)}d|F5DrfAZM>9yWm!Egh)7_q_CnO7uuB3pHv+Hf~`=43kI!is*S$u`=xAt zZv2}+*`ZLXBRO55KUw6R!wdE{3t0YwP7QJt{4!OIte5BJ;E``O*7G+@oCIf#q@SQf zbkE5QhX<;l;N|^v&ob|%gThV6{E{}P=b%gykao)xA<}#B&7J}Ya3uc)zUA~vE7(9} zEaz6R z+RaHu-8n!9(JJPZ<;n#Ju?ZZg(HkPdtiu+zys%YD(vnttLWiIdlDG3a;`=xBT2UnJ_G4{Zi}d8i|W5tRN0Km`s`m zW5rv{Gj+lWcV8p7%b<>?4{fUbvsMR@Qs--gwCc9|UURB3DJb(fzSS$!z&ha?$7_tZ zZ+(3LGLVx}L~roAGjn}Sj%z~=vOiQv=fO3Qoc9RXV>W!Gfu{9049ibxE%?1xpAb>a zra#No#44smq6LuMJAkr6$WuN$ba)9$j62S%3s}vxPvJp~M4T7xhIH%(mEJ+zj+4Bu zHEJ#pU_Bw1x2b%kf*}p@MLIW_#q7eYf{`JgKIT?36F75uvpQ=wYeFt)$tTq?L8z0> zmt_?U-u8q{P?gNA<`?^{X->Xtv9xmbsN!t+ESKB4E=lzJp>r7Uv(=gO4!%wnh1A+~ z<>2|YDa208>EHvbFP&68200o!px3`?Bj#n$2uc9w$`5hNR=0c>ByCNUM`JeLxKBYhzl?ZR^{w>wfbuI*(C7$LyI{Z5pMy$-s-@v z_1LAeM{lqIttSF9+LpKO0~zruyY9Nh=3V~Ej~ELPH}4D%ne~mhFA&F%SeA!bZQ^&N z?dbPW$6?%kyGfSO_5G*OR?+;0`zAq-y9comh~_yn_L;|^kK^$9)Ptr0xRyzhq|aB` z+EY`QvHF>4v{-x`2nH=KsJAT&{>c$LV2Ov^4c^b=MoNs{-#_%VPxcr|o;+0-2X5s{ z#qB%;p20NCWsFyTk|J1_2+#s_A2K2#IxRZ-<-1ytiAGcEW$|#@Q=bHF0tIR^$lmRd zWTsxFre1Q^kOdKf#MCYEW;H!cZNPa2aU?D3740=+gxJKmHHIQ`TCeXhU$%gMVZBkC zPcgLg9X!XTv@}52DIYE;@z?olyJBMM-Hfcy*%aUTZpy{75>>Q0|Ci&1iBZo~yXxA3OaC1OXpVWM%oUY?J7av=ep*ktP${<*{^o(VfBpYe75$GY?f-$3 tuple[str, str]: + return DEAUTH_REASONS.get(code, ("Unknown / vendor-specific", "normal")) + # ─── OUI lookup ────────────────────────────────────────────────────────────── def _load_oui(path: str) -> dict[str, str]: @@ -63,6 +103,7 @@ def oui_lookup(mac: str) -> tuple[str, bool]: def get_conn() -> sqlite3.Connection: conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row + conn.execute("PRAGMA journal_mode=WAL") return conn @@ -147,6 +188,7 @@ def build_networks() -> list[dict]: WHERE bssid IS NOT NULL GROUP BY bssid ORDER BY times_seen DESC + LIMIT 300 """) return rows @@ -169,13 +211,18 @@ def build_clients() -> list[dict]: WHERE src_mac IS NOT NULL GROUP BY src_mac ORDER BY times_seen DESC + LIMIT 300 """) - # Fetch all probed SSIDs in one query, group in Python — avoids N+1 - ssid_rows = query(""" + # Fetch SSIDs only for the MACs we actually returned + if not rows: + return [] + mac_list = [r["src_mac"] for r in rows] + placeholders = ",".join("?" * len(mac_list)) + ssid_rows = query(f""" SELECT DISTINCT src_mac, ssid FROM probe_events - WHERE src_mac IS NOT NULL AND ssid IS NOT NULL AND ssid != '' + WHERE src_mac IN ({placeholders}) AND ssid IS NOT NULL AND ssid != '' ORDER BY src_mac, ssid - """) + """, tuple(mac_list)) ssids_by_mac: dict = {} for s in ssid_rows: ssids_by_mac.setdefault(s["src_mac"], []).append(s["ssid"]) @@ -199,18 +246,24 @@ def build_cross_node() -> dict: rows = query(""" SELECT - bssid, - MAX(ssid) AS ssid, - MAX(channel) AS channel, - MAX(encryption) AS encryption, - node_id, - MAX(rssi) AS best_rssi, - ROUND(AVG(rssi), 1) AS avg_rssi, - COUNT(*) AS times_seen - FROM beacon_events - WHERE bssid IS NOT NULL - GROUP BY bssid, node_id - ORDER BY bssid, node_id + b.bssid, + MAX(b.ssid) AS ssid, + MAX(b.channel) AS channel, + MAX(b.encryption) AS encryption, + b.node_id, + MAX(b.rssi) AS best_rssi, + ROUND(AVG(b.rssi), 1) AS avg_rssi, + COUNT(*) AS times_seen + FROM beacon_events b + WHERE b.bssid IN ( + SELECT bssid FROM beacon_events + WHERE bssid IS NOT NULL + GROUP BY bssid + ORDER BY COUNT(DISTINCT node_id) DESC, COUNT(*) DESC + LIMIT 300 + ) + GROUP BY b.bssid, b.node_id + ORDER BY b.bssid, b.node_id """) networks: dict = {} @@ -455,12 +508,41 @@ def build_alerts() -> dict: d["vendor"] = vendor d["randomized"] = randomized + # Reason code breakdown — all time + reason_rows = query(""" + SELECT reason, + COUNT(*) AS total_frames, + COUNT(DISTINCT bssid) AS unique_bssids, + COUNT(DISTINCT dst) AS unique_targets, + MIN(received_at) AS first_seen, + MAX(received_at) AS last_seen + FROM deauth_events + GROUP BY reason + ORDER BY total_frames DESC + """) + for r in reason_rows: + desc, classification = deauth_reason_info(r["reason"]) + r["description"] = desc + r["classification"] = classification + + # Activity heatmap — frame count per day × hour + heatmap_rows = query(""" + SELECT date(received_at) AS day, + CAST(strftime('%H', received_at) AS INTEGER) AS hour, + COUNT(*) AS frames + FROM deauth_events + GROUP BY day, hour + ORDER BY day, hour + """) + return { - "recent": recent, - "bursts": bursts, - "summary": summary[0] if summary else {}, + "recent": recent, + "bursts": bursts, + "summary": summary[0] if summary else {}, "top_targets": top_targets, "top_targeted_devices": top_targeted_devices, + "reason_stats": reason_rows, + "heatmap": heatmap_rows, } @@ -792,7 +874,6 @@ async def api_heartbeats(): async def api_node_detail(node_id: str): detail = build_node_detail(node_id) if detail is None: - from fastapi import HTTPException raise HTTPException(status_code=404, detail="Node not found") return detail diff --git a/coordinator/static/dashboard.css b/coordinator/static/dashboard.css index d0cf57e..0672b7a 100644 --- a/coordinator/static/dashboard.css +++ b/coordinator/static/dashboard.css @@ -470,6 +470,16 @@ thead th.sort-active::after { content: ' ' attr(data-arrow); } margin-bottom: 6px; } +/* ── Deauth heatmap ── */ +#deauth-heatmap { + display: block; + background: var(--bg-panel); + border: 1px solid var(--border); +} +.reason-normal { color: var(--text-dim); } +.reason-suspicious { color: var(--orange); } +.reason-attack { color: #e74c3c; font-weight: 500; } + /* ── Alerts ── */ .alerts-summary { display: flex; diff --git a/coordinator/static/dashboard.js b/coordinator/static/dashboard.js index 814ee9b..21be0a3 100644 --- a/coordinator/static/dashboard.js +++ b/coordinator/static/dashboard.js @@ -438,6 +438,96 @@ function renderPresenceView() { } } +// ── Deauth heatmap ──────────────────────────────────────────────────────────── +function renderDeauthHeatmap(rows) { + const canvas = document.getElementById('deauth-heatmap'); + const empty = document.getElementById('heatmap-empty'); + const subtitle = document.getElementById('heatmap-subtitle'); + + if (!rows.length) { + canvas.style.display = 'none'; + empty.style.display = 'block'; + subtitle.textContent = ''; + return; + } + canvas.style.display = 'block'; + empty.style.display = 'none'; + + // Build lookup + const lookup = {}; + let maxFrames = 0; + rows.forEach(r => { + if (!lookup[r.day]) lookup[r.day] = {}; + lookup[r.day][r.hour] = r.frames; + if (r.frames > maxFrames) maxFrames = r.frames; + }); + const days = Object.keys(lookup).sort(); + subtitle.textContent = `${days.length} days`; + + const dpr = window.devicePixelRatio || 1; + const padL = 100, padT = 36, padR = 16, padB = 12; + const cellH = 42; + + // Fill container width exactly + const containerW = canvas.parentElement.clientWidth; + const cellW = Math.floor((containerW - padL - padR) / 24); + + const W = padL + 24 * cellW + padR; + const H = padT + days.length * cellH + padB; + + // Set physical pixel size, CSS display size + canvas.width = W * dpr; + canvas.height = H * dpr; + canvas.style.width = W + 'px'; + canvas.style.height = H + 'px'; + + const ctx = canvas.getContext('2d'); + ctx.scale(dpr, dpr); + ctx.clearRect(0, 0, W, H); + + // Hour labels + ctx.font = '11px Roboto, sans-serif'; + ctx.fillStyle = '#5a7a60'; + ctx.textAlign = 'center'; + for (let h = 0; h < 24; h++) { + ctx.fillText(String(h).padStart(2, '0'), padL + h * cellW + cellW / 2, padT - 10); + } + + // Day rows + days.forEach((day, di) => { + const y = padT + di * cellH; + + // Day label + ctx.font = '11px Roboto, sans-serif'; + ctx.fillStyle = '#5a7a60'; + ctx.textAlign = 'right'; + ctx.fillText(day.slice(5), padL - 8, y + cellH / 2 + 4); + + for (let h = 0; h < 24; h++) { + const frames = (lookup[day] && lookup[day][h]) || 0; + const x = padL + h * cellW; + + if (frames === 0) { + ctx.fillStyle = '#111a14'; + } else { + const t = Math.min(frames / maxFrames, 1); + const r = Math.round(80 + t * 151); + const g = Math.round(20 + t * 10); + const b = Math.round(20 + t * 10); + ctx.fillStyle = `rgb(${r},${g},${b})`; + } + ctx.fillRect(x + 1, y + 1, cellW - 2, cellH - 2); + + if (frames > 0) { + ctx.font = '11px Roboto, sans-serif'; + ctx.fillStyle = frames > maxFrames * 0.4 ? '#fff' : '#e74c3c'; + ctx.textAlign = 'center'; + ctx.fillText(frames > 999 ? '999+' : frames, x + cellW / 2, y + cellH / 2 + 4); + } + } + }); +} + // ── Alerts view ─────────────────────────────────────────────────────────────── async function fetchAlerts() { alertsData = await fetch('/api/alerts').then(r => r.json()); @@ -471,6 +561,33 @@ function renderAlertsView() { unique sources `; + // Reason code breakdown + const reasonsTbody = document.getElementById('reasons-tbody'); + const reasonsEmpty = document.getElementById('reasons-empty'); + const reasonsCount = document.getElementById('reasons-count'); + const reasons = a.reason_stats || []; + reasonsCount.textContent = `${reasons.length} codes`; + if (!reasons.length) { + reasonsTbody.innerHTML = ''; + reasonsEmpty.style.display = 'block'; + } else { + reasonsEmpty.style.display = 'none'; + reasonsTbody.innerHTML = reasons.map(r => { + const cls = `reason-${r.classification}`; + const label = r.classification === 'attack' ? 'ATTACK' + : r.classification === 'suspicious' ? 'SUSPICIOUS' : 'normal'; + return ` + ${r.reason} + ${r.description} + ${label} + ${r.total_frames} + ${r.unique_bssids} + ${r.unique_targets} + ${shortTime(r.last_seen)} + `; + }).join(''); + } + // Most targeted networks const targetsTbody = document.getElementById('targets-tbody'); const targetsEmpty = document.getElementById('targets-empty'); @@ -863,6 +980,7 @@ function setView(view) { document.getElementById('view-detail').classList.toggle('active', view === 'detail'); document.getElementById('view-presence').classList.toggle('active', view === 'presence'); document.getElementById('view-alerts').classList.toggle('active', view === 'alerts'); + document.getElementById('view-analysis').classList.toggle('active', view === 'analysis'); document.getElementById('view-search').classList.toggle('active', view === 'search'); document.getElementById('tab-feed').classList.toggle('active', view === 'feed' || view === 'detail'); document.getElementById('tab-networks').classList.toggle('active', view === 'networks' || view === 'network-chart'); @@ -870,6 +988,7 @@ function setView(view) { document.getElementById('tab-clients').classList.toggle('active', view === 'clients'); document.getElementById('tab-presence').classList.toggle('active', view === 'presence'); document.getElementById('tab-alerts').classList.toggle('active', view === 'alerts'); + document.getElementById('tab-analysis').classList.toggle('active', view === 'analysis'); document.getElementById('tab-search').classList.toggle('active', view === 'search'); } @@ -891,6 +1010,12 @@ async function showPresence() { setView('presence'); await fetchPresence(); } async function showAlerts() { setView('alerts'); await fetchAlerts(); } +async function showAnalysis() { + setView('analysis'); + const data = await fetch('/api/alerts').then(r => r.json()); + requestAnimationFrame(() => renderDeauthHeatmap(data.heatmap || [])); +} + function showSearch() { setView('search'); document.getElementById('search-input').focus(); } async function showDetail(node_id) { @@ -946,6 +1071,7 @@ function startSSE() { else if (currentView === 'crossnode') await fetchCrossNode(); else if (currentView === 'presence') await fetchPresence(); else if (currentView === 'alerts') await fetchAlerts(); + else if (currentView === 'analysis') await showAnalysis(); const nodes = await fetch('/api/nodes').then(r => r.json()); nodeData = Object.fromEntries(nodes.map(n => [n.node_id, n])); diff --git a/coordinator/templates/dashboard.html b/coordinator/templates/dashboard.html index e4b33b6..59a3156 100644 --- a/coordinator/templates/dashboard.html +++ b/coordinator/templates/dashboard.html @@ -29,6 +29,7 @@ + @@ -350,6 +351,23 @@ + +
+ Reason Code Breakdown + + all time · normal / suspicious / attack classification +
+
+ + + + + + +
CodeDescriptionClassFramesUnique BSSIDsUnique TargetsLast Seen
+ +
+
Detected Bursts @@ -385,6 +403,20 @@
+ +
+ + +
+ Deauth Activity Heatmap + + frame count per day × hour — pattern reveals manual vs automated activity +
+ + + +
+