@@ -108,6 +108,24 @@ pub struct NetworkInterfaces {
108108 pub scan_in_progress : bool ,
109109}
110110
111+ /// Resource storing heartbeat (connection checking) state
112+ #[ derive( Resource ) ]
113+ pub struct HeartbeatState {
114+ /// Whether connection checking is enabled
115+ pub enabled : bool ,
116+ /// Whether we're waiting for initial state from server
117+ pub loading : bool ,
118+ }
119+
120+ impl Default for HeartbeatState {
121+ fn default ( ) -> Self {
122+ Self {
123+ enabled : false , // Default to off (no network traffic)
124+ loading : true , // Loading initial state
125+ }
126+ }
127+ }
128+
111129/// Request to update subnet (used by trigger_scan_on_interface)
112130#[ derive( Serialize ) ]
113131#[ allow( dead_code) ]
@@ -126,9 +144,11 @@ impl Plugin for NetworkPlugin {
126144 . init_resource :: < PendingMessages > ( )
127145 . init_resource :: < NetworkInterfaces > ( )
128146 . init_resource :: < PendingInterfaceData > ( )
147+ . init_resource :: < HeartbeatState > ( )
148+ . init_resource :: < PendingHeartbeatData > ( )
129149 . add_message :: < ReconnectEvent > ( )
130- . add_systems ( Startup , ( connect_websocket, fetch_initial_devices, fetch_network_interfaces) )
131- . add_systems ( Update , ( process_messages, process_interface_data, handle_reconnect) ) ;
150+ . add_systems ( Startup , ( connect_websocket, fetch_initial_devices, fetch_network_interfaces, fetch_heartbeat_state ) )
151+ . add_systems ( Update , ( process_messages, process_interface_data, process_heartbeat_data , handle_reconnect) ) ;
132152 }
133153}
134154
@@ -278,6 +298,10 @@ fn refetch_interfaces(daemon_config: &DaemonConfig, pending: &PendingInterfaceDa
278298#[ derive( Resource , Default ) ]
279299pub struct PendingInterfaceData ( pub Arc < Mutex < Option < Vec < NetworkInterfaceInfo > > > > ) ;
280300
301+ /// Pending heartbeat data from async fetch
302+ #[ derive( Resource , Default ) ]
303+ pub struct PendingHeartbeatData ( pub Arc < Mutex < Option < bool > > > ) ;
304+
281305/// Shared message queue between WebSocket callback and Bevy
282306#[ derive( Resource , Default , Clone ) ]
283307pub struct PendingMessages ( pub Arc < Mutex < Vec < WsMessage > > > ) ;
@@ -340,6 +364,7 @@ pub struct DiscoveryJson {
340364 #[ allow( dead_code) ]
341365 pub port : u16 ,
342366 pub switch_port : Option < u8 > ,
367+ pub last_seen : Option < String > ,
343368}
344369
345370#[ derive( Debug , Clone , Deserialize ) ]
@@ -368,6 +393,7 @@ impl From<DeviceJson> for DeviceData {
368393 version : json. firmware . version ,
369394 position : json. pose . map ( |p| [ p[ 0 ] , p[ 1 ] , p[ 2 ] ] ) ,
370395 model_path : json. model_path ,
396+ last_seen : json. discovery . last_seen ,
371397 }
372398 }
373399}
@@ -551,6 +577,86 @@ fn process_interface_data(
551577 }
552578}
553579
580+ /// Fetch initial heartbeat state from backend
581+ fn fetch_heartbeat_state ( pending : Res < PendingHeartbeatData > , daemon_config : Res < DaemonConfig > ) {
582+ #[ cfg( target_arch = "wasm32" ) ]
583+ {
584+ use wasm_bindgen_futures:: spawn_local;
585+
586+ let pending_clone = pending. 0 . clone ( ) ;
587+ let base_url = daemon_config. http_url . clone ( ) ;
588+
589+ spawn_local ( async move {
590+ let url = format ! ( "{}/api/heartbeat" , base_url) ;
591+
592+ tracing:: info!( "Fetching heartbeat state from: {}" , url) ;
593+
594+ match gloo_net:: http:: Request :: get ( & url) . send ( ) . await {
595+ Ok ( response) => {
596+ if let Ok ( text) = response. text ( ) . await {
597+ tracing:: debug!( "Heartbeat response: {}" , text) ;
598+ if let Ok ( json) = serde_json:: from_str :: < serde_json:: Value > ( & text) {
599+ if let Some ( enabled) = json. get ( "heartbeat_enabled" ) . and_then ( |v| v. as_bool ( ) ) {
600+ if let Ok ( mut data) = pending_clone. lock ( ) {
601+ * data = Some ( enabled) ;
602+ }
603+ }
604+ }
605+ }
606+ }
607+ Err ( e) => {
608+ tracing:: error!( "Failed to fetch heartbeat state: {:?}" , e) ;
609+ }
610+ }
611+ } ) ;
612+ }
613+ }
614+
615+ /// Process pending heartbeat data
616+ fn process_heartbeat_data (
617+ pending : Res < PendingHeartbeatData > ,
618+ mut heartbeat_state : ResMut < HeartbeatState > ,
619+ ) {
620+ if let Ok ( mut data) = pending. 0 . lock ( ) {
621+ if let Some ( enabled) = data. take ( ) {
622+ heartbeat_state. enabled = enabled;
623+ heartbeat_state. loading = false ;
624+ }
625+ }
626+ }
627+
628+ /// Toggle heartbeat checking (called from UI)
629+ pub fn toggle_heartbeat ( enabled : bool , base_url : & str ) {
630+ #[ cfg( target_arch = "wasm32" ) ]
631+ {
632+ use wasm_bindgen_futures:: spawn_local;
633+
634+ let base_url = base_url. to_string ( ) ;
635+
636+ spawn_local ( async move {
637+ let url = format ! ( "{}/api/heartbeat" , base_url) ;
638+ let body = serde_json:: json!( { "enabled" : enabled } ) ;
639+
640+ tracing:: info!( "Setting heartbeat to: {}" , enabled) ;
641+
642+ match gloo_net:: http:: Request :: post ( & url)
643+ . header ( "Content-Type" , "application/json" )
644+ . body ( body. to_string ( ) )
645+ . unwrap ( )
646+ . send ( )
647+ . await
648+ {
649+ Ok ( _) => {
650+ tracing:: info!( "Heartbeat set to: {}" , enabled) ;
651+ }
652+ Err ( e) => {
653+ tracing:: error!( "Failed to set heartbeat: {:?}" , e) ;
654+ }
655+ }
656+ } ) ;
657+ }
658+ }
659+
554660/// Trigger a scan on the selected interface (called from UI)
555661pub fn trigger_scan_on_interface ( subnet : & str , prefix_len : u8 , base_url : & str ) {
556662 #[ cfg( target_arch = "wasm32" ) ]
0 commit comments