@@ -89,6 +89,12 @@ const animate = () => {
8989
9090const openDiagnostics = ( ) => {
9191 document . getElementById ( "diagnosticsModal" ) . classList . add ( "active" ) ;
92+ // Ensure bandwidth graph is correctly sized and drawn after modal opens
93+ setTimeout ( ( ) => {
94+ if ( typeof resizeBandwidthCanvas === "function" ) {
95+ resizeBandwidthCanvas ( ) ;
96+ }
97+ } , 50 ) ;
9298} ;
9399
94100const closeDiagnostics = ( ) => {
@@ -587,7 +593,6 @@ terminalInput.addEventListener("keypress", async (e) => {
587593 }
588594 }
589595
590-
591596 let scope = "GLOBAL" ;
592597 let target = null ;
593598
@@ -686,6 +691,21 @@ terminalInput.addEventListener("keypress", async (e) => {
686691 }
687692} ) ;
688693
694+ const formatBandwidth = ( bytes , short = false ) => {
695+ const kb = bytes / 1024 ;
696+ const mb = kb / 1024 ;
697+ const gb = mb / 1024 ;
698+ const space = short ? "" : " " ;
699+
700+ if ( gb >= 1 ) {
701+ return gb . toFixed ( short ? 1 : 2 ) + space + "GB" ;
702+ } else if ( mb >= 1 ) {
703+ return mb . toFixed ( short ? 1 : 2 ) + space + "MB" ;
704+ } else {
705+ return kb . toFixed ( short ? 0 : 1 ) + space + "KB" ;
706+ }
707+ } ;
708+
689709const evtSource = new EventSource ( "/events" ) ;
690710
691711evtSource . onmessage = ( event ) => {
@@ -796,14 +816,19 @@ evtSource.onmessage = (event) => {
796816 d . invalidPoW . toLocaleString ( ) ;
797817 document . getElementById ( "diag-invalid-sig" ) . innerText =
798818 d . invalidSig . toLocaleString ( ) ;
799- document . getElementById ( "diag-bandwidth-in" ) . innerText = formatBandwidth (
800- d . bytesReceived
801- ) ;
802- document . getElementById ( "diag-bandwidth-out" ) . innerText = formatBandwidth (
803- d . bytesRelayed
804- ) ;
805819 document . getElementById ( "diag-leave" ) . innerText =
806820 d . leaveMessages . toLocaleString ( ) ;
821+
822+ if ( typeof addBandwidthData === "function" ) {
823+ addBandwidthData ( d . bytesReceived , d . bytesRelayed ) ;
824+ drawBandwidthGraph ( ) ;
825+ document . getElementById ( "current-in" ) . innerText = formatBandwidth (
826+ d . bytesReceived
827+ ) ;
828+ document . getElementById ( "current-out" ) . innerText = formatBandwidth (
829+ d . bytesRelayed
830+ ) ;
831+ }
807832 }
808833} ;
809834
@@ -817,6 +842,135 @@ countEl.classList.add("loaded");
817842updateParticles ( initialCount ) ;
818843animate ( ) ;
819844
845+ const bandwidthHistory = { timestamps : [ ] , bytesIn : [ ] , bytesOut : [ ] } ;
846+ let selectedTimeRange = 300 ;
847+ const bandwidthCanvas = document . getElementById ( "bandwidthGraph" ) ;
848+ const bandwidthCtx = bandwidthCanvas ? bandwidthCanvas . getContext ( "2d" ) : null ;
849+ const bandwidthOverlay = document . getElementById ( "bandwidthOverlay" ) ;
850+
851+ function resizeBandwidthCanvas ( ) {
852+ if ( ! bandwidthCanvas ) return ;
853+ const rect = bandwidthCanvas . getBoundingClientRect ( ) ;
854+ bandwidthCanvas . width = rect . width ;
855+ bandwidthCanvas . height = rect . height ;
856+ drawBandwidthGraph ( ) ;
857+ }
858+
859+ window . addEventListener ( "resize" , resizeBandwidthCanvas ) ;
860+ setTimeout ( resizeBandwidthCanvas , 100 ) ;
861+
862+ const toggleBandwidthGraph = ( ) => {
863+ if ( ! bandwidthOverlay ) return ;
864+ bandwidthOverlay . classList . toggle ( "collapsed" ) ;
865+ const closeBtn = document . querySelector ( ".bandwidth-overlay .close-btn" ) ;
866+ if ( closeBtn ) {
867+ closeBtn . textContent = bandwidthOverlay . classList . contains ( "collapsed" )
868+ ? "+"
869+ : "−" ;
870+ }
871+ // Redraw after transition
872+ setTimeout ( resizeBandwidthCanvas , 350 ) ;
873+ } ;
874+
875+ window . toggleBandwidthGraph = toggleBandwidthGraph ;
876+
877+ const timePills = document . querySelectorAll ( ".time-pill" ) ;
878+ timePills . forEach ( ( pill ) => {
879+ pill . addEventListener ( "click" , ( e ) => {
880+ e . stopPropagation ( ) ;
881+ timePills . forEach ( ( p ) => p . classList . remove ( "active" ) ) ;
882+ pill . classList . add ( "active" ) ;
883+ const value = pill . dataset . value ;
884+ selectedTimeRange = value === "all" ? "all" : parseInt ( value ) ;
885+ drawBandwidthGraph ( ) ;
886+ } ) ;
887+ } ) ;
888+
889+ if ( timePills . length > 0 ) {
890+ timePills [ 0 ] . classList . add ( "active" ) ;
891+ }
892+
893+ const addBandwidthData = ( bytesIn , bytesOut ) => {
894+ bandwidthHistory . timestamps . push ( Date . now ( ) ) ;
895+ bandwidthHistory . bytesIn . push ( bytesIn ) ;
896+ bandwidthHistory . bytesOut . push ( bytesOut ) ;
897+
898+ if ( bandwidthHistory . timestamps . length > 360 ) {
899+ bandwidthHistory . timestamps . shift ( ) ;
900+ bandwidthHistory . bytesIn . shift ( ) ;
901+ bandwidthHistory . bytesOut . shift ( ) ;
902+ }
903+ } ;
904+
905+ const getFilteredData = ( ) => {
906+ if ( selectedTimeRange === "all" ) return bandwidthHistory ;
907+
908+ const cutoff = Date . now ( ) - selectedTimeRange * 1000 ;
909+ const startIndex = bandwidthHistory . timestamps . findIndex ( ( t ) => t >= cutoff ) ;
910+
911+ if ( startIndex === - 1 ) return bandwidthHistory ;
912+
913+ return {
914+ timestamps : bandwidthHistory . timestamps . slice ( startIndex ) ,
915+ bytesIn : bandwidthHistory . bytesIn . slice ( startIndex ) ,
916+ bytesOut : bandwidthHistory . bytesOut . slice ( startIndex ) ,
917+ } ;
918+ } ;
919+
920+ const drawBandwidthGraph = ( ) => {
921+ if ( ! bandwidthCanvas || ! bandwidthCtx ) return ;
922+ const w = bandwidthCanvas . width ;
923+ const h = bandwidthCanvas . height ;
924+
925+ if ( w === 0 || h === 0 ) return ;
926+
927+ const pad = { t : 10 , r : 10 , b : 20 , l : 50 } ;
928+
929+ bandwidthCtx . clearRect ( 0 , 0 , w , h ) ;
930+
931+ const data = getFilteredData ( ) ;
932+ if ( data . timestamps . length < 2 ) return ;
933+
934+ const max = Math . max ( ...data . bytesIn , ...data . bytesOut ) ;
935+ if ( max === 0 ) return ;
936+
937+ bandwidthCtx . fillStyle = "#9ca3af" ;
938+ bandwidthCtx . font =
939+ '10px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto' ;
940+ bandwidthCtx . textAlign = "right" ;
941+ [ max , max / 2 , 0 ] . forEach ( ( val , i ) => {
942+ bandwidthCtx . fillText (
943+ formatBandwidth ( val , true ) ,
944+ pad . l - 5 ,
945+ pad . t + ( ( h - pad . t - pad . b ) / 2 ) * i + 4
946+ ) ;
947+ } ) ;
948+
949+ const drawLine = ( points , color ) => {
950+ bandwidthCtx . strokeStyle = color ;
951+ bandwidthCtx . lineWidth = 2 ;
952+ bandwidthCtx . beginPath ( ) ;
953+ points . forEach ( ( val , i ) => {
954+ const x = pad . l + ( i / ( points . length - 1 ) ) * ( w - pad . l - pad . r ) ;
955+ const y = pad . t + ( h - pad . t - pad . b ) - ( val / max ) * ( h - pad . t - pad . b ) ;
956+ i === 0 ? bandwidthCtx . moveTo ( x , y ) : bandwidthCtx . lineTo ( x , y ) ;
957+ } ) ;
958+ bandwidthCtx . stroke ( ) ;
959+
960+ bandwidthCtx . lineTo (
961+ pad . l + ( w - pad . l - pad . r ) ,
962+ pad . t + ( h - pad . t - pad . b )
963+ ) ;
964+ bandwidthCtx . lineTo ( pad . l , pad . t + ( h - pad . t - pad . b ) ) ;
965+ bandwidthCtx . closePath ( ) ;
966+ bandwidthCtx . fillStyle = color + "33" ;
967+ bandwidthCtx . fill ( ) ;
968+ } ;
969+
970+ drawLine ( data . bytesIn , "#60a5fa" ) ;
971+ drawLine ( data . bytesOut , "#f97316" ) ;
972+ } ;
973+
820974const themes = [
821975 "hypermind.css" ,
822976 "hypermind-classic.css" ,
@@ -843,11 +997,11 @@ function showThemeNotification(themeName) {
843997 notification . classList . remove ( "hidden" ) ;
844998
845999 notification . offsetHeight ;
846-
1000+
8471001 notification . classList . add ( "show" ) ;
8481002
8491003 if ( notificationTimeout ) clearTimeout ( notificationTimeout ) ;
850-
1004+
8511005 notificationTimeout = setTimeout ( ( ) => {
8521006 notification . classList . remove ( "show" ) ;
8531007 setTimeout ( ( ) => {
0 commit comments