Skip to main content

node_launchpad/components/
status.rs

1// Copyright 2024 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9use super::footer::NodesToStart;
10use super::header::SelectedMenuItem;
11use super::popup::manage_nodes::GB;
12use super::utils::centered_rect_fixed;
13use super::{Component, Frame, footer::Footer, header::Header, popup::manage_nodes::GB_PER_NODE};
14use crate::action::OptionsActions;
15use crate::components::popup::manage_nodes::MAX_NODE_COUNT;
16use crate::components::popup::port_range::PORT_ALLOCATION;
17use crate::components::utils::open_logs;
18use crate::config::get_launchpad_nodes_data_dir_path;
19use crate::connection_mode::{ConnectionMode, NodeConnectionMode};
20use crate::error::ErrorPopup;
21use crate::node_mgmt::{
22    FIXED_INTERVAL, MaintainNodesArgs, NODES_ALL, NodeManagement, NodeManagementTask,
23    UpgradeNodesArgs,
24};
25use crate::node_mgmt::{PORT_MAX, PORT_MIN};
26use crate::style::{COOL_GREY, INDIGO, SIZZLING_RED, clear_area};
27use crate::system::{get_available_space_b, get_drive_name};
28use crate::tui::Event;
29use crate::upnp::UpnpSupport;
30use crate::{
31    action::{Action, StatusActions},
32    config::Config,
33    mode::{InputMode, Scene},
34    node_stats::NodeStats,
35    style::{EUCALYPTUS, GHOST_WHITE, LIGHT_PERIWINKLE, VERY_LIGHT_AZURE, VIVID_SKY_BLUE},
36};
37use ant_bootstrap::InitialPeersConfig;
38use ant_node_manager::add_services::config::PortRange;
39use ant_node_manager::config::get_node_registry_path;
40use ant_service_management::{
41    NodeRegistryManager, NodeServiceData, ServiceStatus, control::ServiceController,
42};
43use color_eyre::eyre::{Ok, OptionExt, Result};
44use crossterm::event::KeyEvent;
45use ratatui::text::Span;
46use ratatui::{prelude::*, widgets::*};
47use std::fmt;
48use std::{
49    path::PathBuf,
50    time::{Duration, Instant},
51    vec,
52};
53use throbber_widgets_tui::{self, Throbber, ThrobberState};
54use tokio::sync::mpsc::UnboundedSender;
55
56pub const NODE_STAT_UPDATE_INTERVAL: Duration = Duration::from_secs(5);
57pub const NODE_REGISTRY_UPDATE_INTERVAL: Duration = Duration::from_secs(180); // 3 minutes
58/// Faster registry polling when nodes are in transitional states (Starting/Stopping/Updating).
59/// refresh_node_registry(full_refresh=false) skips the peer connection check, so this is
60/// lightweight enough for frequent polling.
61const NODE_REGISTRY_TRANSITION_UPDATE_INTERVAL: Duration = Duration::from_secs(5);
62/// If nat detection fails for more than 3 times, we don't want to waste time running during every node start.
63const MAX_ERRORS_WHILE_RUNNING_NAT_DETECTION: usize = 3;
64
65// Table Widths
66const NODE_WIDTH: usize = 10;
67const VERSION_WIDTH: usize = 7;
68const ATTOS_WIDTH: usize = 5;
69const MEMORY_WIDTH: usize = 7;
70const MBPS_WIDTH: usize = 13;
71const RECORDS_WIDTH: usize = 4;
72const PEERS_WIDTH: usize = 5;
73const CONNS_WIDTH: usize = 5;
74const MODE_WIDTH: usize = 7;
75const STATUS_WIDTH: usize = 8;
76const FAILURE_WIDTH: usize = 64;
77const SPINNER_WIDTH: usize = 1;
78
79#[derive(Clone)]
80pub struct Status<'a> {
81    /// Whether the component is active right now, capturing keystrokes + drawing things.
82    active: bool,
83    action_sender: Option<UnboundedSender<Action>>,
84    config: Config,
85    // NAT
86    is_nat_status_determined: bool,
87    error_while_running_nat_detection: usize,
88    // Track if NAT detection is currently running
89    nat_detection_in_progress: bool,
90    // Device Stats Section
91    node_stats: NodeStats,
92    node_stats_last_update: Instant,
93    // Nodes
94    node_services: Vec<NodeServiceData>,
95    items: Option<StatefulTable<NodeItem<'a>>>,
96    /// To pass into node services.
97    network_id: Option<u8>,
98    // Node Management
99    node_management: NodeManagement,
100    // Amount of nodes
101    nodes_to_start: usize,
102    // Rewards address
103    rewards_address: String,
104    // Peers to pass into nodes for startup
105    init_peers_config: InitialPeersConfig,
106    // If path is provided, we don't fetch the binary from the network
107    antnode_path: Option<PathBuf>,
108    // Path where the node data is stored
109    data_dir_path: PathBuf,
110    // Connection mode
111    connection_mode: ConnectionMode,
112    // UPnP support
113    upnp_support: UpnpSupport,
114    // Port from
115    port_from: Option<u32>,
116    // Port to
117    port_to: Option<u32>,
118    storage_mountpoint: PathBuf,
119    available_disk_space_gb: usize,
120    error_popup: Option<ErrorPopup>,
121    // Node Registry Refresh
122    node_registry_last_update: Instant,
123}
124
125pub struct StatusConfig {
126    pub allocated_disk_space: usize,
127    pub antnode_path: Option<PathBuf>,
128    pub connection_mode: ConnectionMode,
129    pub upnp_support: UpnpSupport,
130    pub data_dir_path: PathBuf,
131    pub network_id: Option<u8>,
132    pub init_peers_config: InitialPeersConfig,
133    pub port_from: Option<u32>,
134    pub port_to: Option<u32>,
135    pub storage_mountpoint: PathBuf,
136    pub rewards_address: String,
137}
138
139impl Status<'_> {
140    pub async fn new(config: StatusConfig) -> Result<Self> {
141        let node_registry = NodeRegistryManager::load(&get_node_registry_path()?).await?;
142        let mut status = Self {
143            init_peers_config: config.init_peers_config,
144            action_sender: Default::default(),
145            config: Default::default(),
146            active: true,
147            is_nat_status_determined: false,
148            error_while_running_nat_detection: 0,
149            nat_detection_in_progress: false,
150            network_id: config.network_id,
151            node_stats: NodeStats::default(),
152            node_stats_last_update: Instant::now(),
153            node_services: Default::default(),
154            node_management: NodeManagement::new(node_registry.clone())?,
155            items: None,
156            nodes_to_start: config.allocated_disk_space,
157            rewards_address: config.rewards_address,
158            antnode_path: config.antnode_path,
159            data_dir_path: config.data_dir_path,
160            connection_mode: config.connection_mode,
161            upnp_support: config.upnp_support,
162            port_from: config.port_from,
163            port_to: config.port_to,
164            error_popup: None,
165            storage_mountpoint: config.storage_mountpoint.clone(),
166            available_disk_space_gb: (get_available_space_b(&config.storage_mountpoint)? / GB)
167                as usize,
168            node_registry_last_update: Instant::now(),
169        };
170
171        // Populate initial state from disk-cached registry data so the TUI can render
172        // immediately. A background refresh will be triggered once the action handler is
173        // registered (see register_action_handler), which will update nodes to their live
174        // status without blocking startup.
175        status.update_node_state(
176            node_registry.get_node_service_data().await,
177            node_registry.nat_status.read().await.is_some(),
178        )?;
179
180        Ok(status)
181    }
182
183    fn set_lock(&mut self, service_name: &str, locked: bool) {
184        if let Some(ref mut items) = self.items {
185            for item in &mut items.items {
186                if item.name == *service_name {
187                    item.locked = locked;
188                }
189            }
190        }
191    }
192
193    // FIXME: Can be used if NodeItem implements Copy. Dependencies cannot.
194    fn _lock_service(&mut self, service_name: &str) {
195        self.set_lock(service_name, true);
196    }
197
198    fn unlock_service(&mut self, service_name: &str) {
199        self.set_lock(service_name, false);
200    }
201
202    /// Updates the NodeStatus of a specific item in the items list based on its service name.
203    ///
204    /// # Arguments
205    ///
206    /// * `service_name` - The name of the service to update.
207    /// * `status` - The new status to assign to the item.
208    fn update_item(&mut self, service_name: String, status: NodeStatus) -> Result<()> {
209        if let Some(ref mut items) = self.items {
210            for item in &mut items.items {
211                if item.name == service_name {
212                    item.status = status;
213                }
214            }
215        }
216        Ok(())
217    }
218
219    fn update_node_items(&mut self, new_status: Option<NodeStatus>) -> Result<()> {
220        // Iterate over existing node services and update their corresponding NodeItem
221        if let Some(ref mut items) = self.items {
222            for node_item in self.node_services.iter() {
223                // Find the corresponding item by service name
224                if let Some(item) = items
225                    .items
226                    .iter_mut()
227                    .find(|i| i.name == node_item.service_name)
228                {
229                    if let Some(status) = new_status {
230                        item.status = status;
231                    } else if item.status == NodeStatus::Updating
232                        || item.status == NodeStatus::Starting
233                    {
234                        item.spinner_state.calc_next();
235                        // Only transition if the registry confirms the node is now running.
236                        // Other states (Stopped, Added) are likely stale from before the
237                        // operation was initiated, so we keep spinning until either the
238                        // registry shows Running or a completion action arrives.
239                        if node_item.status == ServiceStatus::Running {
240                            debug!(
241                                "Node {} transitioning from {:?} to Running (registry confirmed)",
242                                item.name, item.status
243                            );
244                            item.status = NodeStatus::Running;
245                            item.locked = false;
246                        }
247                    } else if item.status == NodeStatus::Stopping {
248                        item.spinner_state.calc_next();
249                        // Only transition if the registry confirms the node has stopped.
250                        if node_item.status == ServiceStatus::Stopped {
251                            debug!(
252                                "Node {} transitioning from Stopping to Stopped (registry confirmed)",
253                                item.name
254                            );
255                            item.status = NodeStatus::Stopped;
256                            item.locked = false;
257                        }
258                    } else {
259                        // Update status based on current node status
260                        item.status = match node_item.status {
261                            ServiceStatus::Running => {
262                                item.spinner_state.calc_next();
263                                NodeStatus::Running
264                            }
265                            ServiceStatus::Stopped => NodeStatus::Stopped,
266                            ServiceStatus::Added => NodeStatus::Added,
267                            ServiceStatus::Removed => NodeStatus::Removed,
268                        };
269                    }
270
271                    item.version = node_item.version.to_string();
272                    item.peers = match node_item.connected_peers {
273                        Some(ref peers) => peers.len(),
274                        None => 0,
275                    };
276
277                    // Update individual stats if available
278                    if let Some(stats) = self
279                        .node_stats
280                        .individual_stats
281                        .iter()
282                        .find(|s| s.service_name == node_item.service_name)
283                    {
284                        item.attos = stats.rewards_wallet_balance;
285                        item.memory = stats.memory_usage_mb;
286                        item.mbps = format!(
287                            "↓{:0>5.0} ↑{:0>5.0}",
288                            (stats.bandwidth_inbound_rate * 8) as f64 / 1_000_000.0,
289                            (stats.bandwidth_outbound_rate * 8) as f64 / 1_000_000.0,
290                        );
291                        item.records = stats.max_records;
292                        item.connections = stats.connections;
293                    }
294                } else {
295                    // If not found, create a new NodeItem and add it to items
296                    let new_item = NodeItem {
297                        name: node_item.service_name.clone(),
298                        version: node_item.version.to_string(),
299                        attos: 0,
300                        memory: 0,
301                        mbps: "-".to_string(),
302                        records: 0,
303                        peers: 0,
304                        connections: 0,
305                        mode: NodeConnectionMode::from(node_item),
306                        locked: false,
307                        status: NodeStatus::Added, // Set initial status as Added
308                        failure: node_item.get_critical_failure(),
309                        spinner: Throbber::default(),
310                        spinner_state: ThrobberState::default(),
311                    };
312                    items.items.push(new_item);
313                }
314            }
315        } else {
316            // If items is None, create a new list (fallback)
317            let node_items: Vec<NodeItem> = self
318                .node_services
319                .iter()
320                .filter_map(|node_item| {
321                    if node_item.status == ServiceStatus::Removed {
322                        return None;
323                    }
324                    // Update status based on current node status
325                    let status = match node_item.status {
326                        ServiceStatus::Running => NodeStatus::Running,
327                        ServiceStatus::Stopped => NodeStatus::Stopped,
328                        ServiceStatus::Added => NodeStatus::Added,
329                        ServiceStatus::Removed => NodeStatus::Removed,
330                    };
331
332                    // Create a new NodeItem for the first time
333                    Some(NodeItem {
334                        name: node_item.service_name.clone().to_string(),
335                        version: node_item.version.to_string(),
336                        attos: 0,
337                        memory: 0,
338                        mbps: "-".to_string(),
339                        records: 0,
340                        peers: 0,
341                        connections: 0,
342                        locked: false,
343                        mode: NodeConnectionMode::from(node_item),
344                        status,
345                        failure: node_item.get_critical_failure(),
346                        spinner: Throbber::default(),
347                        spinner_state: ThrobberState::default(),
348                    })
349                })
350                .collect();
351            self.items = Some(StatefulTable::with_items(node_items));
352        }
353        Ok(())
354    }
355
356    fn clear_node_items(&mut self) {
357        debug!("Cleaning items on Status page");
358        if let Some(items) = self.items.as_mut() {
359            items.items.clear();
360            debug!("Cleared the items on status page");
361        }
362    }
363
364    /// Tries to trigger the update of node stats if the last update was more than `NODE_STAT_UPDATE_INTERVAL` ago.
365    /// The result is sent via the StatusActions::NodesStatsObtained action.
366    fn try_update_node_stats(&mut self, force_update: bool) -> Result<()> {
367        if self.node_stats_last_update.elapsed() > NODE_STAT_UPDATE_INTERVAL || force_update {
368            self.node_stats_last_update = Instant::now();
369
370            NodeStats::fetch_all_node_stats(&self.node_services, self.get_actions_sender()?);
371        }
372        Ok(())
373    }
374
375    /// Tries to trigger a periodic refresh of the node registry if the last update was more than
376    /// `NODE_REGISTRY_UPDATE_INTERVAL` ago. This detects version changes from auto-upgraded
377    /// antnode processes. The result is sent via the StatusActions::RegistryRefreshed action.
378    ///
379    /// When nodes are in transitional states (Starting/Updating), uses a shorter polling interval
380    /// to detect status changes faster. The refresh uses `full_refresh=false` which skips the
381    /// peer connection check, making it lightweight enough for frequent polling.
382    fn try_update_node_registry(&mut self, force_update: bool) -> Result<()> {
383        let has_transitioning_nodes = self
384            .items
385            .as_ref()
386            .map(|items| {
387                items.items.iter().any(|i| {
388                    i.status == NodeStatus::Starting
389                        || i.status == NodeStatus::Stopping
390                        || i.status == NodeStatus::Updating
391                })
392            })
393            .unwrap_or(false);
394        let interval = if has_transitioning_nodes {
395            NODE_REGISTRY_TRANSITION_UPDATE_INTERVAL
396        } else {
397            NODE_REGISTRY_UPDATE_INTERVAL
398        };
399        if self.node_registry_last_update.elapsed() > interval || force_update {
400            self.node_registry_last_update = Instant::now();
401            let action_sender = self.get_actions_sender()?;
402
403            tokio::spawn(async move {
404                debug!("Starting periodic node registry refresh");
405
406                // Always reload the registry from disk to avoid using stale in-memory data
407                // This is crucial after operations like reset that modify the registry
408                let node_registry =
409                    match NodeRegistryManager::load(&get_node_registry_path().unwrap()).await {
410                        core::result::Result::Ok(registry) => registry,
411                        Err(err) => {
412                            error!("Failed to load node registry for periodic refresh: {err:?}");
413                            return;
414                        }
415                    };
416
417                if let Err(err) = ant_node_manager::refresh_node_registry(
418                    node_registry.clone(),
419                    &ServiceController {},
420                    false, // full_refresh
421                    true,  // is_local_network
422                    ant_node_manager::VerbosityLevel::Minimal,
423                )
424                .await
425                {
426                    error!("Failed to refresh node registry: {err:?}");
427                    return;
428                }
429
430                if let Err(err) = node_registry.save().await {
431                    error!("Failed to save node registry after periodic refresh: {err:?}");
432                    return;
433                }
434
435                debug!("Node registry refreshed and saved successfully");
436                let _ =
437                    action_sender.send(Action::StatusActions(StatusActions::RegistryRefreshed {
438                        all_nodes_data: node_registry.get_node_service_data().await,
439                        is_nat_status_determined: node_registry.nat_status.read().await.is_some(),
440                    }));
441            });
442        }
443        Ok(())
444    }
445
446    fn get_actions_sender(&self) -> Result<UnboundedSender<Action>> {
447        self.action_sender
448            .clone()
449            .ok_or_eyre("Action sender not registered")
450    }
451
452    fn update_node_state(
453        &mut self,
454        all_nodes_data: Vec<NodeServiceData>,
455        is_nat_status_determined: bool,
456    ) -> Result<()> {
457        self.is_nat_status_determined = is_nat_status_determined;
458
459        self.node_services = all_nodes_data
460            .into_iter()
461            .filter(|node| node.status != ServiceStatus::Removed)
462            .collect();
463
464        info!(
465            "Updated state from the data passed from NodeRegistryManager. Maintaining {:?} nodes.",
466            self.node_services.len()
467        );
468
469        Ok(())
470    }
471
472    /// Only run NAT detection if we haven't determined the status yet and we haven't failed more than 3 times.
473    fn should_we_run_nat_detection(&self) -> bool {
474        self.connection_mode == ConnectionMode::Automatic
475            && !self.is_nat_status_determined
476            && self.error_while_running_nat_detection < MAX_ERRORS_WHILE_RUNNING_NAT_DETECTION
477    }
478
479    fn _nodes_starting(&self) -> bool {
480        if let Some(items) = &self.items {
481            items
482                .items
483                .iter()
484                .any(|item| item.status == NodeStatus::Starting)
485        } else {
486            false
487        }
488    }
489
490    fn get_running_nodes(&self) -> Vec<String> {
491        self.node_services
492            .iter()
493            .filter_map(|node| {
494                if node.status == ServiceStatus::Running {
495                    Some(node.service_name.clone())
496                } else {
497                    None
498                }
499            })
500            .collect()
501    }
502
503    fn get_service_names_and_peer_ids(&self) -> (Vec<String>, Vec<String>) {
504        let mut service_names = Vec::new();
505        let mut peers_ids = Vec::new();
506
507        for node in &self.node_services {
508            // Only include nodes with a valid peer_id
509            if let Some(peer_id) = &node.peer_id {
510                service_names.push(node.service_name.clone());
511                peers_ids.push(peer_id.to_string().clone());
512            }
513        }
514
515        (service_names, peers_ids)
516    }
517}
518
519impl Component for Status<'_> {
520    fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> Result<()> {
521        self.action_sender = Some(tx);
522
523        // Update the stats to be shown as soon as the app is run
524        self.try_update_node_stats(true)?;
525
526        // Trigger a background registry refresh now that we have an action sender.
527        // Status::new() no longer blocks on refresh_node_registry() so the TUI can render
528        // immediately with disk-cached state. This background refresh will update nodes to
529        // their live status and send a RegistryRefreshed action when complete.
530        self.try_update_node_registry(true)?;
531
532        Ok(())
533    }
534
535    fn register_config_handler(&mut self, config: Config) -> Result<()> {
536        self.config = config;
537        Ok(())
538    }
539
540    fn handle_events(&mut self, event: Option<Event>) -> Result<Vec<Action>> {
541        let r = match event {
542            Some(Event::Key(key_event)) => self.handle_key_events(key_event)?,
543            _ => vec![],
544        };
545        Ok(r)
546    }
547
548    fn update(&mut self, action: Action) -> Result<Option<Action>> {
549        match action {
550            Action::Tick => {
551                self.try_update_node_stats(false)?;
552                self.try_update_node_registry(false)?;
553                let _ = self.update_node_items(None);
554            }
555            Action::SwitchScene(scene) => match scene {
556                Scene::Status
557                | Scene::StatusRewardsAddressPopUp
558                | Scene::RemoveNodePopUp
559                | Scene::UpgradeLaunchpadPopUp => {
560                    self.active = true;
561                    // make sure we're in navigation mode
562                    return Ok(Some(Action::SwitchInputMode(InputMode::Navigation)));
563                }
564                Scene::ManageNodesPopUp { .. } => self.active = true,
565                _ => self.active = false,
566            },
567            Action::StoreNodesToStart(count) => {
568                self.nodes_to_start = count;
569                if self.nodes_to_start == 0 {
570                    info!("Nodes to start set to 0. Sending command to stop all nodes.");
571                    return Ok(Some(Action::StatusActions(StatusActions::StopNodes)));
572                } else {
573                    info!("Nodes to start set to: {count}. Sending command to start nodes");
574                    return Ok(Some(Action::StatusActions(StatusActions::StartNodes)));
575                }
576            }
577            Action::StoreRewardsAddress(rewards_address) => {
578                debug!("Storing rewards address: {rewards_address:?}");
579                let has_changed = self.rewards_address != rewards_address;
580                let we_have_nodes = !self.node_services.is_empty();
581
582                self.rewards_address = rewards_address;
583
584                if we_have_nodes && has_changed {
585                    info!("Resetting antnode services because the Rewards Address was reset.");
586                    let action_sender = self.get_actions_sender()?;
587                    self.node_management
588                        .send_task(NodeManagementTask::ResetNodes {
589                            start_nodes_after_reset: false,
590                            action_sender,
591                        })?;
592                }
593            }
594            Action::StoreStorageDrive(ref drive_mountpoint, ref _drive_name) => {
595                info!("Resetting antnode services because the Storage Drive was changed.");
596                let action_sender = self.get_actions_sender()?;
597                self.node_management
598                    .send_task(NodeManagementTask::ResetNodes {
599                        start_nodes_after_reset: false,
600                        action_sender,
601                    })?;
602                self.data_dir_path =
603                    get_launchpad_nodes_data_dir_path(&drive_mountpoint.to_path_buf(), false)?;
604            }
605            Action::StoreConnectionMode(connection_mode) => {
606                self.connection_mode = connection_mode;
607                info!("Resetting antnode services because the Connection Mode range was changed.");
608                let action_sender = self.get_actions_sender()?;
609                self.node_management
610                    .send_task(NodeManagementTask::ResetNodes {
611                        start_nodes_after_reset: false,
612                        action_sender,
613                    })?;
614            }
615            Action::StorePortRange(port_from, port_range) => {
616                self.port_from = Some(port_from);
617                self.port_to = Some(port_range);
618                info!("Resetting antnode services because the Port Range was changed.");
619                let action_sender = self.get_actions_sender()?;
620                self.node_management
621                    .send_task(NodeManagementTask::ResetNodes {
622                        start_nodes_after_reset: false,
623                        action_sender,
624                    })?;
625            }
626            Action::SetUpnpSupport(ref upnp_support) => {
627                debug!("Setting UPnP support: {upnp_support:?}");
628                self.upnp_support = upnp_support.clone();
629            }
630            Action::StatusActions(status_action) => match status_action {
631                StatusActions::NodesStatsObtained(stats) => {
632                    self.node_stats = stats;
633                }
634                StatusActions::RegistryRefreshed {
635                    all_nodes_data,
636                    is_nat_status_determined,
637                } => {
638                    debug!("Processing periodic registry refresh results");
639                    self.update_node_state(all_nodes_data, is_nat_status_determined)?;
640                    let _ = self.update_node_items(None);
641                }
642                StatusActions::StartNodesCompleted {
643                    service_name,
644                    all_nodes_data,
645                    is_nat_status_determined,
646                } => {
647                    if service_name == *NODES_ALL {
648                        if let Some(items) = &self.items {
649                            let items_clone = items.clone();
650                            for item in &items_clone.items {
651                                self.unlock_service(item.name.as_str());
652                                self.update_item(item.name.clone(), NodeStatus::Running)?;
653                            }
654                        }
655                    } else {
656                        self.unlock_service(service_name.as_str());
657                        self.update_item(service_name, NodeStatus::Running)?;
658                    }
659                    self.update_node_state(all_nodes_data, is_nat_status_determined)?;
660                }
661                StatusActions::StopNodesCompleted {
662                    service_name,
663                    all_nodes_data,
664                    is_nat_status_determined,
665                } => {
666                    self.unlock_service(service_name.as_str());
667                    self.update_item(service_name, NodeStatus::Stopped)?;
668                    self.update_node_state(all_nodes_data, is_nat_status_determined)?;
669                }
670                StatusActions::UpdateNodesCompleted {
671                    all_nodes_data,
672                    is_nat_status_determined,
673                } => {
674                    if let Some(items) = &self.items {
675                        let items_clone = items.clone();
676                        for item in &items_clone.items {
677                            self.unlock_service(item.name.as_str());
678                        }
679                    }
680                    self.clear_node_items();
681                    self.update_node_state(all_nodes_data, is_nat_status_determined)?;
682
683                    let _ = self.update_node_items(None);
684                    debug!("Update nodes completed");
685                }
686                StatusActions::ResetNodesCompleted {
687                    trigger_start_node,
688                    all_nodes_data,
689                    is_nat_status_determined,
690                } => {
691                    if let Some(items) = &self.items {
692                        let items_clone = items.clone();
693                        for item in &items_clone.items {
694                            self.unlock_service(item.name.as_str());
695                        }
696                    }
697                    self.update_node_state(all_nodes_data, is_nat_status_determined)?;
698
699                    self.clear_node_items();
700
701                    if trigger_start_node {
702                        debug!("Reset nodes completed. Triggering start nodes.");
703                        return Ok(Some(Action::StatusActions(StatusActions::StartNodes)));
704                    }
705                    debug!("Reset nodes completed");
706                }
707                StatusActions::AddNodesCompleted {
708                    service_name,
709
710                    all_nodes_data,
711                    is_nat_status_determined,
712                } => {
713                    self.unlock_service(service_name.as_str());
714                    self.update_item(service_name.clone(), NodeStatus::Stopped)?;
715                    self.update_node_state(all_nodes_data, is_nat_status_determined)?;
716
717                    debug!("Adding {:?} completed", service_name.clone());
718                }
719                StatusActions::RemoveNodesCompleted {
720                    service_name,
721                    all_nodes_data,
722                    is_nat_status_determined,
723                } => {
724                    self.unlock_service(service_name.as_str());
725                    self.update_item(service_name, NodeStatus::Removed)?;
726                    self.update_node_state(all_nodes_data, is_nat_status_determined)?;
727
728                    let _ = self.update_node_items(None);
729                    debug!("Removing nodes completed");
730                }
731                StatusActions::SuccessfullyDetectedNatStatus => {
732                    debug!(
733                        "Successfully detected nat status, is_nat_status_determined set to true"
734                    );
735                    self.is_nat_status_determined = true;
736                    self.nat_detection_in_progress = false;
737                }
738                StatusActions::NatDetectionStarted => {
739                    debug!("NAT detection started");
740                    self.nat_detection_in_progress = true;
741                }
742                StatusActions::ErrorWhileRunningNatDetection => {
743                    self.error_while_running_nat_detection += 1;
744                    self.nat_detection_in_progress = false;
745                    debug!(
746                        "Error while running nat detection. Error count: {}",
747                        self.error_while_running_nat_detection
748                    );
749                }
750                StatusActions::ErrorLoadingNodeRegistry { raw_error }
751                | StatusActions::ErrorGettingNodeRegistryPath { raw_error } => {
752                    self.error_popup = Some(ErrorPopup::new(
753                        "Error".to_string(),
754                        "Error getting node registry path".to_string(),
755                        raw_error,
756                    ));
757                    if let Some(error_popup) = &mut self.error_popup {
758                        error_popup.show();
759                    }
760                    // Switch back to entry mode so we can handle key events
761                    return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
762                }
763                StatusActions::ErrorScalingUpNodes { raw_error } => {
764                    self.error_popup = Some(ErrorPopup::new(
765                        "Error".to_string(),
766                        "Error adding new nodes".to_string(),
767                        raw_error,
768                    ));
769                    if let Some(error_popup) = &mut self.error_popup {
770                        error_popup.show();
771                    }
772                    // Switch back to entry mode so we can handle key events
773                    return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
774                }
775                StatusActions::ErrorStoppingNodes {
776                    services,
777                    raw_error,
778                } => {
779                    for service_name in services {
780                        self.unlock_service(service_name.as_str());
781                    }
782                    self.error_popup = Some(ErrorPopup::new(
783                        "Error".to_string(),
784                        "Error stopping nodes".to_string(),
785                        raw_error,
786                    ));
787                    if let Some(error_popup) = &mut self.error_popup {
788                        error_popup.show();
789                    }
790                    // Switch back to entry mode so we can handle key events
791                    return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
792                }
793                StatusActions::ErrorUpdatingNodes { raw_error } => {
794                    if let Some(items) = &self.items {
795                        let items_clone = items.clone();
796                        for item in &items_clone.items {
797                            self.unlock_service(item.name.as_str());
798                        }
799                    }
800                    self.error_popup = Some(ErrorPopup::new(
801                        "Error".to_string(),
802                        "Error upgrading nodes".to_string(),
803                        raw_error,
804                    ));
805                    if let Some(error_popup) = &mut self.error_popup {
806                        error_popup.show();
807                    }
808                    // Switch back to entry mode so we can handle key events
809                    return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
810                }
811                StatusActions::ErrorResettingNodes { raw_error } => {
812                    self.error_popup = Some(ErrorPopup::new(
813                        "Error".to_string(),
814                        "Error resetting nodes".to_string(),
815                        raw_error,
816                    ));
817                    if let Some(error_popup) = &mut self.error_popup {
818                        error_popup.show();
819                    }
820                    // Switch back to entry mode so we can handle key events
821                    return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
822                }
823                StatusActions::ErrorAddingNodes { raw_error } => {
824                    self.error_popup = Some(ErrorPopup::new(
825                        "Error".to_string(),
826                        "Error adding node".to_string(),
827                        raw_error,
828                    ));
829                    if let Some(error_popup) = &mut self.error_popup {
830                        error_popup.show();
831                    }
832                    // Switch back to entry mode so we can handle key events
833                    return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
834                }
835                StatusActions::ErrorRemovingNodes {
836                    services,
837                    raw_error,
838                } => {
839                    for service_name in services {
840                        self.unlock_service(service_name.as_str());
841                    }
842                    self.error_popup = Some(ErrorPopup::new(
843                        "Error".to_string(),
844                        "Error removing node".to_string(),
845                        raw_error,
846                    ));
847                    if let Some(error_popup) = &mut self.error_popup {
848                        error_popup.show();
849                    }
850                    // Switch back to entry mode so we can handle key events
851                    return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
852                }
853                StatusActions::ErrorStartingNodes {
854                    services,
855                    raw_error,
856                } => {
857                    // Check if the services have already moved past the Starting state
858                    // (e.g., transitioned to Running via registry refresh, or the user has
859                    // already stopped the node). If so, this error is from a stale start
860                    // operation (typically the peer connection check timing out) and should
861                    // be suppressed.
862                    let all_moved_past_starting = services.iter().all(|service_name| {
863                        self.items
864                            .as_ref()
865                            .and_then(|items| items.items.iter().find(|i| i.name == *service_name))
866                            .map(|item| item.status != NodeStatus::Starting)
867                            .unwrap_or(false)
868                    });
869
870                    for service_name in &services {
871                        self.unlock_service(service_name.as_str());
872                    }
873
874                    if all_moved_past_starting {
875                        debug!(
876                            "Ignoring start error for {:?}: nodes already moved past Starting state",
877                            services
878                        );
879                    } else {
880                        self.error_popup = Some(ErrorPopup::new(
881                            "Error".to_string(),
882                            "Error starting node. Please try again.".to_string(),
883                            raw_error,
884                        ));
885                        if let Some(error_popup) = &mut self.error_popup {
886                            error_popup.show();
887                        }
888                        // Switch back to entry mode so we can handle key events
889                        return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
890                    }
891                }
892                StatusActions::TriggerManageNodes => {
893                    let mut amount_of_nodes = 0;
894                    if let Some(items) = &mut self.items {
895                        amount_of_nodes = items.items.len();
896                    }
897
898                    return Ok(Some(Action::SwitchScene(Scene::ManageNodesPopUp {
899                        amount_of_nodes,
900                    })));
901                }
902                StatusActions::TriggerRemoveNode => {
903                    if let Some(_node) = self.items.as_ref().and_then(|items| items.selected_item())
904                    {
905                        return Ok(Some(Action::SwitchScene(Scene::RemoveNodePopUp)));
906                    } else {
907                        debug!("No items to be removed");
908                        return Ok(None);
909                    }
910                }
911                StatusActions::PreviousTableItem => {
912                    if let Some(items) = &mut self.items {
913                        items.previous();
914                    }
915                }
916                StatusActions::NextTableItem => {
917                    if let Some(items) = &mut self.items {
918                        items.next();
919                    }
920                }
921                StatusActions::StartStopNode => {
922                    debug!("Start/Stop node");
923
924                    // Check if a node is selected
925                    if let Some(node) = self.items.as_ref().and_then(|items| items.selected_item())
926                    {
927                        let node_index = self
928                            .items
929                            .as_ref()
930                            .unwrap()
931                            .items
932                            .iter()
933                            .position(|item| item.name == node.name)
934                            .unwrap();
935                        let action_sender = self.get_actions_sender()?;
936                        let node = &mut self.items.as_mut().unwrap().items[node_index];
937
938                        if node.status == NodeStatus::Removed {
939                            debug!("Node is removed. Cannot be started.");
940                            return Ok(None);
941                        }
942
943                        if node.locked {
944                            debug!("Node still performing operation");
945                            return Ok(None);
946                        }
947                        node.locked = true; // Lock the node before starting or stopping
948
949                        let service_name = vec![node.name.clone()];
950
951                        match node.status {
952                            NodeStatus::Stopped | NodeStatus::Added => {
953                                debug!("Starting Node {:?}", node.name);
954                                self.node_management
955                                    .send_task(NodeManagementTask::StartNode {
956                                        services: service_name,
957                                        action_sender,
958                                    })?;
959                                node.status = NodeStatus::Starting;
960                            }
961                            NodeStatus::Running => {
962                                debug!("Stopping Node {:?}", node.name);
963                                self.node_management
964                                    .send_task(NodeManagementTask::StopNodes {
965                                        services: service_name,
966                                        action_sender,
967                                    })?;
968                                node.status = NodeStatus::Stopping;
969                            }
970                            _ => {
971                                debug!("Cannot Start/Stop node. Node status is {:?}", node.status);
972                            }
973                        }
974                    } else {
975                        debug!("Got action to Start/Stop node but no node was selected.");
976                        return Ok(None);
977                    }
978                }
979                StatusActions::StartNodes => {
980                    debug!("Got action to start nodes");
981
982                    if self.rewards_address.is_empty() {
983                        info!("Rewards address is not set. Ask for input.");
984                        return Ok(Some(Action::StatusActions(
985                            StatusActions::TriggerRewardsAddress,
986                        )));
987                    }
988
989                    if self.nodes_to_start == 0 {
990                        info!("Nodes to start not set. Ask for input.");
991                        return Ok(Some(Action::StatusActions(
992                            StatusActions::TriggerManageNodes,
993                        )));
994                    }
995
996                    // Set status and locking
997                    if let Some(ref mut items) = self.items {
998                        for item in &mut items.items {
999                            if item.status == NodeStatus::Added
1000                                || item.status == NodeStatus::Stopped
1001                            {
1002                                item.status = NodeStatus::Starting;
1003                                item.locked = true;
1004                            }
1005                        }
1006                    }
1007
1008                    let port_range = PortRange::Range(
1009                        self.port_from.unwrap_or(PORT_MIN) as u16,
1010                        self.port_to.unwrap_or(PORT_MAX) as u16,
1011                    );
1012
1013                    let action_sender = self.get_actions_sender()?;
1014
1015                    let maintain_nodes_args = MaintainNodesArgs {
1016                        action_sender: action_sender.clone(),
1017                        antnode_path: self.antnode_path.clone(),
1018                        connection_mode: self.connection_mode,
1019                        count: self.nodes_to_start as u16,
1020                        data_dir_path: Some(self.data_dir_path.clone()),
1021                        network_id: self.network_id,
1022                        owner: self.rewards_address.clone(),
1023                        init_peers_config: self.init_peers_config.clone(),
1024                        port_range: Some(port_range),
1025                        rewards_address: self.rewards_address.clone(),
1026                        run_nat_detection: self.should_we_run_nat_detection(),
1027                    };
1028
1029                    // Set NAT detection in progress flag if we're going to run detection
1030                    if maintain_nodes_args.run_nat_detection {
1031                        self.nat_detection_in_progress = true;
1032                    }
1033
1034                    debug!("Calling maintain_n_running_nodes");
1035
1036                    self.node_management
1037                        .send_task(NodeManagementTask::MaintainNodes {
1038                            args: maintain_nodes_args,
1039                        })?;
1040                }
1041                StatusActions::StopNodes => {
1042                    debug!("Got action to stop nodes");
1043
1044                    let running_nodes = self.get_running_nodes();
1045                    let action_sender = self.get_actions_sender()?;
1046                    info!("Stopping node service: {running_nodes:?}");
1047
1048                    self.node_management
1049                        .send_task(NodeManagementTask::StopNodes {
1050                            services: running_nodes,
1051                            action_sender,
1052                        })?;
1053                }
1054                StatusActions::AddNode => {
1055                    debug!("Got action to Add node");
1056
1057                    // Validations - Available space
1058                    if (GB_PER_NODE as usize) > self.available_disk_space_gb {
1059                        self.error_popup = Some(ErrorPopup::new(
1060                            "Cannot Add Node".to_string(),
1061                            format!("\nEach Node requires {GB_PER_NODE}GB of available space."),
1062                            format!(
1063                                "{} has only {}GB remaining.\n\nYou can free up some space or change to different drive in the options.",
1064                                get_drive_name(&self.storage_mountpoint)?,
1065                                self.available_disk_space_gb
1066                            ),
1067                        ));
1068                        if let Some(error_popup) = &mut self.error_popup {
1069                            error_popup.show();
1070                        }
1071                        // Switch back to entry mode so we can handle key events
1072                        return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
1073                    }
1074
1075                    // Validations - Amount of nodes
1076                    let amount_of_nodes = if let Some(ref items) = self.items {
1077                        items.items.len()
1078                    } else {
1079                        0
1080                    };
1081
1082                    if amount_of_nodes + 1 > MAX_NODE_COUNT {
1083                        self.error_popup = Some(ErrorPopup::new(
1084                            "Cannot Add Node".to_string(),
1085                            format!(
1086                                "There are not enough ports available in your\ncustom port range to start another node ({MAX_NODE_COUNT})."
1087                            ),
1088                            "\nVisit autonomi.com/support/port-error for help".to_string(),
1089                        ));
1090                        if let Some(error_popup) = &mut self.error_popup {
1091                            error_popup.show();
1092                        }
1093                        // Switch back to entry mode so we can handle key events
1094                        return Ok(Some(Action::SwitchInputMode(InputMode::Entry)));
1095                    }
1096
1097                    if self.rewards_address.is_empty() {
1098                        info!("Rewards address is not set. Ask for input.");
1099                        return Ok(Some(Action::StatusActions(
1100                            StatusActions::TriggerRewardsAddress,
1101                        )));
1102                    }
1103
1104                    if self.nodes_to_start == 0 {
1105                        info!("Nodes to start not set. Ask for input.");
1106                        return Ok(Some(Action::StatusActions(
1107                            StatusActions::TriggerManageNodes,
1108                        )));
1109                    }
1110
1111                    let port_range = PortRange::Range(
1112                        self.port_from.unwrap_or(PORT_MIN) as u16,
1113                        self.port_to.unwrap_or(PORT_MAX) as u16,
1114                    );
1115
1116                    let action_sender = self.get_actions_sender()?;
1117
1118                    let add_node_args = MaintainNodesArgs {
1119                        action_sender: action_sender.clone(),
1120                        antnode_path: self.antnode_path.clone(),
1121                        connection_mode: self.connection_mode,
1122                        count: 1,
1123                        data_dir_path: Some(self.data_dir_path.clone()),
1124                        network_id: self.network_id,
1125                        owner: self.rewards_address.clone(),
1126                        init_peers_config: self.init_peers_config.clone(),
1127                        port_range: Some(port_range),
1128                        rewards_address: self.rewards_address.clone(),
1129                        run_nat_detection: self.should_we_run_nat_detection(),
1130                    };
1131
1132                    // Set NAT detection in progress flag if we're going to run detection
1133                    if add_node_args.run_nat_detection {
1134                        self.nat_detection_in_progress = true;
1135                    }
1136
1137                    self.node_management
1138                        .send_task(NodeManagementTask::AddNode {
1139                            args: add_node_args,
1140                        })?;
1141                }
1142                StatusActions::RemoveNodes => {
1143                    debug!("Got action to remove node");
1144                    // Check if a node is selected
1145                    if self
1146                        .items
1147                        .as_ref()
1148                        .and_then(|items| items.selected_item())
1149                        .is_none()
1150                    {
1151                        debug!("Got action to Start/Stop node but no node was selected.");
1152                        return Ok(None);
1153                    }
1154
1155                    let node_index =
1156                        self.items
1157                            .as_ref()
1158                            .and_then(|items| {
1159                                items.items.iter().position(|item| {
1160                                    item.name == items.selected_item().unwrap().name
1161                                })
1162                            })
1163                            .unwrap();
1164
1165                    let action_sender = self.get_actions_sender()?;
1166
1167                    let node = &mut self.items.as_mut().unwrap().items[node_index];
1168
1169                    if node.locked {
1170                        debug!("Node still performing operation");
1171                        return Ok(None);
1172                    } else {
1173                        // Lock the node before starting or stopping
1174                        node.locked = true;
1175                    }
1176
1177                    let service_name = vec![node.name.clone()];
1178
1179                    // Send the task to remove the node
1180                    self.node_management
1181                        .send_task(NodeManagementTask::RemoveNodes {
1182                            services: service_name,
1183                            action_sender,
1184                        })?;
1185                }
1186                StatusActions::TriggerRewardsAddress => {
1187                    if self.rewards_address.is_empty() {
1188                        return Ok(Some(Action::SwitchScene(Scene::StatusRewardsAddressPopUp)));
1189                    } else {
1190                        return Ok(None);
1191                    }
1192                }
1193                StatusActions::TriggerNodeLogs => {
1194                    if let Some(node) = self.items.as_ref().and_then(|items| items.selected_item())
1195                    {
1196                        debug!("Got action to open node logs {:?}", node.name);
1197                        open_logs(Some(node.name.clone()))?;
1198                    } else {
1199                        debug!("Got action to open node logs but no node was selected.");
1200                    }
1201                }
1202            },
1203            Action::OptionsActions(OptionsActions::UpdateNodes) => {
1204                debug!("Got action to Update Nodes");
1205                let action_sender = self.get_actions_sender()?;
1206                info!("Got action to update nodes");
1207                let _ = self.update_node_items(Some(NodeStatus::Updating));
1208                let (service_names, peer_ids) = self.get_service_names_and_peer_ids();
1209
1210                let upgrade_nodes_args = UpgradeNodesArgs {
1211                    action_sender,
1212                    connection_timeout_s: 5,
1213                    do_not_start: true,
1214                    custom_bin_path: self.antnode_path.clone(),
1215                    force: false,
1216                    fixed_interval: Some(FIXED_INTERVAL),
1217                    peer_ids,
1218                    provided_env_variables: None,
1219                    service_names,
1220                    url: None,
1221                    version: None,
1222                };
1223                self.node_management
1224                    .send_task(NodeManagementTask::UpgradeNodes {
1225                        args: upgrade_nodes_args,
1226                    })?;
1227            }
1228            Action::OptionsActions(OptionsActions::ResetNodes) => {
1229                debug!("Got action to reset nodes");
1230                let action_sender = self.get_actions_sender()?;
1231                info!("Got action to reset nodes");
1232                self.node_management
1233                    .send_task(NodeManagementTask::ResetNodes {
1234                        start_nodes_after_reset: false,
1235                        action_sender,
1236                    })?;
1237            }
1238            Action::OptionsActions(OptionsActions::UpdateStorageDrive(mountpoint, _drive_name)) => {
1239                self.storage_mountpoint.clone_from(&mountpoint);
1240                self.available_disk_space_gb = (get_available_space_b(&mountpoint)? / GB) as usize;
1241            }
1242            _ => {}
1243        }
1244        Ok(None)
1245    }
1246
1247    fn draw(&mut self, f: &mut Frame<'_>, area: Rect) -> Result<()> {
1248        if !self.active {
1249            return Ok(());
1250        }
1251
1252        let layout = Layout::new(
1253            Direction::Vertical,
1254            [
1255                // Header
1256                Constraint::Length(1),
1257                // Device status
1258                Constraint::Max(6),
1259                // Node status
1260                Constraint::Min(3),
1261                // Footer
1262                Constraint::Length(3),
1263            ],
1264        )
1265        .split(area);
1266
1267        // ==== Header =====
1268
1269        let header = Header::new();
1270        f.render_stateful_widget(header, layout[0], &mut SelectedMenuItem::Status);
1271
1272        // ==== Device Status =====
1273
1274        // Device Status as a block with two tables so we can shrink the screen
1275        // and preserve as much as we can information
1276
1277        let combined_block = Block::default()
1278            .title(" Device Status ")
1279            .bold()
1280            .title_style(Style::default().fg(GHOST_WHITE))
1281            .borders(Borders::ALL)
1282            .padding(Padding::horizontal(1))
1283            .style(Style::default().fg(VERY_LIGHT_AZURE));
1284
1285        f.render_widget(combined_block.clone(), layout[1]);
1286
1287        let storage_allocated_row = Row::new(vec![
1288            Cell::new("Storage Allocated".to_string()).fg(GHOST_WHITE),
1289            Cell::new(format!("{} GB", (self.nodes_to_start as u64) * GB_PER_NODE)).fg(GHOST_WHITE),
1290        ]);
1291        let memory_use_val = if self.node_stats.total_memory_usage_mb as f64 / 1024_f64 > 1.0 {
1292            format!(
1293                "{:.2} GB",
1294                self.node_stats.total_memory_usage_mb as f64 / 1024_f64
1295            )
1296        } else {
1297            format!("{} MB", self.node_stats.total_memory_usage_mb)
1298        };
1299
1300        let memory_use_row = Row::new(vec![
1301            Cell::new("Memory Use".to_string()).fg(GHOST_WHITE),
1302            Cell::new(memory_use_val).fg(GHOST_WHITE),
1303        ]);
1304
1305        let connection_mode_string = match self.connection_mode {
1306            ConnectionMode::HomeNetwork => "Home Network".to_string(),
1307            ConnectionMode::UPnP => "UPnP".to_string(),
1308            ConnectionMode::CustomPorts => format!(
1309                "Custom Ports  {}-{}",
1310                self.port_from.unwrap_or(PORT_MIN),
1311                self.port_to.unwrap_or(PORT_MIN + PORT_ALLOCATION)
1312            ),
1313            ConnectionMode::Automatic => "Automatic".to_string(),
1314        };
1315
1316        let mut connection_mode_line = vec![Span::styled(
1317            connection_mode_string,
1318            Style::default().fg(GHOST_WHITE),
1319        )];
1320
1321        if matches!(
1322            self.connection_mode,
1323            ConnectionMode::Automatic | ConnectionMode::UPnP
1324        ) {
1325            connection_mode_line.push(Span::styled(" (", Style::default().fg(GHOST_WHITE)));
1326
1327            if self.connection_mode == ConnectionMode::Automatic {
1328                connection_mode_line.push(Span::styled("UPnP: ", Style::default().fg(GHOST_WHITE)));
1329            }
1330
1331            let span = match self.upnp_support {
1332                UpnpSupport::Supported => {
1333                    Span::styled("supported", Style::default().fg(EUCALYPTUS))
1334                }
1335                UpnpSupport::Unsupported => {
1336                    Span::styled("disabled / unsupported", Style::default().fg(SIZZLING_RED))
1337                }
1338                UpnpSupport::Loading => {
1339                    Span::styled("loading..", Style::default().fg(LIGHT_PERIWINKLE))
1340                }
1341                UpnpSupport::Unknown => {
1342                    Span::styled("unknown", Style::default().fg(LIGHT_PERIWINKLE))
1343                }
1344            };
1345
1346            connection_mode_line.push(span);
1347
1348            connection_mode_line.push(Span::styled(")", Style::default().fg(GHOST_WHITE)));
1349        }
1350
1351        let connection_mode_row = Row::new(vec![
1352            Cell::new("Connection".to_string()).fg(GHOST_WHITE),
1353            Cell::new(Line::from(connection_mode_line)),
1354        ]);
1355
1356        let stats_rows = vec![storage_allocated_row, memory_use_row, connection_mode_row];
1357        let stats_width = [Constraint::Length(5)];
1358        let column_constraints = [Constraint::Length(23), Constraint::Fill(1)];
1359        let stats_table = Table::new(stats_rows, stats_width).widths(column_constraints);
1360
1361        let wallet_not_set = if self.rewards_address.is_empty() {
1362            vec![
1363                Span::styled("Press ".to_string(), Style::default().fg(VIVID_SKY_BLUE)),
1364                Span::styled("[Ctrl+B] ".to_string(), Style::default().fg(GHOST_WHITE)),
1365                Span::styled(
1366                    "to add your ".to_string(),
1367                    Style::default().fg(VIVID_SKY_BLUE),
1368                ),
1369                Span::styled(
1370                    "Wallet Address".to_string(),
1371                    Style::default().fg(VIVID_SKY_BLUE).bold(),
1372                ),
1373            ]
1374        } else {
1375            vec![]
1376        };
1377
1378        let total_attos_earned_and_wallet_row = Row::new(vec![
1379            Cell::new("Attos Earned".to_string()).fg(VIVID_SKY_BLUE),
1380            Cell::new(format!(
1381                "{:?}",
1382                self.node_stats.total_rewards_wallet_balance
1383            ))
1384            .fg(VIVID_SKY_BLUE)
1385            .bold(),
1386            Cell::new(Line::from(wallet_not_set).alignment(Alignment::Right)),
1387        ]);
1388
1389        let attos_wallet_rows = vec![total_attos_earned_and_wallet_row];
1390        let attos_wallet_width = [Constraint::Length(5)];
1391        let column_constraints = [
1392            Constraint::Length(23),
1393            Constraint::Fill(1),
1394            Constraint::Length(if self.rewards_address.is_empty() {
1395                41 //TODO: make it dynamic with wallet_not_set
1396            } else {
1397                0
1398            }),
1399        ];
1400        let attos_wallet_table =
1401            Table::new(attos_wallet_rows, attos_wallet_width).widths(column_constraints);
1402
1403        let inner_area = combined_block.inner(layout[1]);
1404        let device_layout = Layout::new(
1405            Direction::Vertical,
1406            vec![Constraint::Length(5), Constraint::Length(1)],
1407        )
1408        .split(inner_area);
1409
1410        // Render both tables inside the combined block
1411        f.render_widget(stats_table, device_layout[0]);
1412        f.render_widget(attos_wallet_table, device_layout[1]);
1413
1414        // ==== Node Status =====
1415
1416        // No nodes. Empty Table.
1417        if let Some(ref items) = self.items {
1418            if items.items.is_empty() || self.rewards_address.is_empty() {
1419                let line1 = Line::from(vec![
1420                    Span::styled("Press ", Style::default().fg(LIGHT_PERIWINKLE)),
1421                    Span::styled("[+] ", Style::default().fg(GHOST_WHITE).bold()),
1422                    Span::styled("to Add and ", Style::default().fg(LIGHT_PERIWINKLE)),
1423                    Span::styled(
1424                        "Start your first node ",
1425                        Style::default().fg(GHOST_WHITE).bold(),
1426                    ),
1427                    Span::styled("on this device", Style::default().fg(LIGHT_PERIWINKLE)),
1428                ]);
1429
1430                let line2 = Line::from(vec![Span::styled(
1431                    format!(
1432                        "Each node will use {GB_PER_NODE}GB of storage and a small amount of memory, \
1433                        CPU, and Network bandwidth. Most computers can run many nodes at once, \
1434                        but we recommend you add them gradually"
1435                    ),
1436                    Style::default().fg(LIGHT_PERIWINKLE),
1437                )]);
1438
1439                f.render_widget(
1440                    Paragraph::new(vec![Line::raw(""), line1, Line::raw(""), line2])
1441                        .wrap(Wrap { trim: false })
1442                        .fg(LIGHT_PERIWINKLE)
1443                        .block(
1444                            Block::default()
1445                                .title(Line::from(vec![
1446                                    Span::styled(" Nodes", Style::default().fg(GHOST_WHITE).bold()),
1447                                    Span::styled(" (0) ", Style::default().fg(LIGHT_PERIWINKLE)),
1448                                ]))
1449                                .title_style(Style::default().fg(LIGHT_PERIWINKLE))
1450                                .borders(Borders::ALL)
1451                                .border_style(style::Style::default().fg(EUCALYPTUS))
1452                                .padding(Padding::horizontal(1)),
1453                        ),
1454                    layout[2],
1455                );
1456            } else {
1457                // Node/s block
1458                let block_nodes = Block::default()
1459                    .title(Line::from(vec![
1460                        Span::styled(" Nodes", Style::default().fg(GHOST_WHITE).bold()),
1461                        Span::styled(
1462                            format!(
1463                                " ({}) ",
1464                                if let Some(ref items) = self.items {
1465                                    items.items.len()
1466                                } else {
1467                                    0
1468                                }
1469                            ),
1470                            Style::default().fg(LIGHT_PERIWINKLE),
1471                        ),
1472                    ]))
1473                    .padding(Padding::new(1, 1, 0, 0))
1474                    .title_style(Style::default().fg(GHOST_WHITE))
1475                    .borders(Borders::ALL)
1476                    .border_style(Style::default().fg(EUCALYPTUS));
1477
1478                // Split the inner area of the combined block
1479                let inner_area = block_nodes.inner(layout[2]);
1480
1481                // Column Widths
1482                let node_widths = [
1483                    Constraint::Min(NODE_WIDTH as u16),
1484                    Constraint::Min(VERSION_WIDTH as u16),
1485                    Constraint::Min(ATTOS_WIDTH as u16),
1486                    Constraint::Min(MEMORY_WIDTH as u16),
1487                    Constraint::Min(MBPS_WIDTH as u16),
1488                    Constraint::Min(RECORDS_WIDTH as u16),
1489                    Constraint::Min(PEERS_WIDTH as u16),
1490                    Constraint::Min(CONNS_WIDTH as u16),
1491                    Constraint::Min(MODE_WIDTH as u16),
1492                    Constraint::Min(STATUS_WIDTH as u16),
1493                    Constraint::Fill(FAILURE_WIDTH as u16),
1494                    Constraint::Max(SPINNER_WIDTH as u16),
1495                ];
1496
1497                // Header
1498                let header_row = Row::new(vec![
1499                    Cell::new("Node").fg(COOL_GREY),
1500                    Cell::new("Version").fg(COOL_GREY),
1501                    Cell::new("Attos").fg(COOL_GREY),
1502                    Cell::new("Memory").fg(COOL_GREY),
1503                    Cell::new(
1504                        format!("{}{}", " ".repeat(MBPS_WIDTH - "Mbps".len()), "Mbps")
1505                            .fg(COOL_GREY),
1506                    ),
1507                    Cell::new("Recs").fg(COOL_GREY),
1508                    Cell::new("Peers").fg(COOL_GREY),
1509                    Cell::new("Conns").fg(COOL_GREY),
1510                    Cell::new("Mode").fg(COOL_GREY),
1511                    Cell::new("Status").fg(COOL_GREY),
1512                    Cell::new("Failure").fg(COOL_GREY),
1513                    Cell::new(" ").fg(COOL_GREY), // Spinner
1514                ])
1515                .style(Style::default().add_modifier(Modifier::BOLD));
1516
1517                if let Some(ref mut items_table) = self.items {
1518                    let mut items: Vec<Row> = Vec::new();
1519                    let item_count = items_table.items.len();
1520                    let selected_index = items_table.state.selected().unwrap_or(0);
1521                    for (i, node_item) in items_table.items.iter_mut().enumerate() {
1522                        let is_selected = items_table.state.selected() == Some(i);
1523                        items.push(node_item.render_as_row(i, layout[2], f, is_selected));
1524                    }
1525
1526                    let table = Table::new(items, node_widths)
1527                        .header(header_row)
1528                        .column_spacing(1)
1529                        .row_highlight_style(Style::default().bg(INDIGO))
1530                        .highlight_spacing(HighlightSpacing::Always);
1531
1532                    f.render_stateful_widget(table, inner_area, &mut items_table.state);
1533
1534                    // Only show scrollbar when content overflows the visible area.
1535                    // Subtract 1 from height to account for the header row.
1536                    let visible_rows = inner_area.height.saturating_sub(1) as usize;
1537                    if item_count > visible_rows {
1538                        let mut scrollbar_state = ScrollbarState::default()
1539                            .content_length(item_count)
1540                            .position(selected_index);
1541                        f.render_stateful_widget(
1542                            Scrollbar::default()
1543                                .orientation(ScrollbarOrientation::VerticalRight)
1544                                .begin_symbol(None)
1545                                .end_symbol(None),
1546                            inner_area,
1547                            &mut scrollbar_state,
1548                        );
1549                    }
1550                } else {
1551                    let table = Table::new(Vec::<Row>::new(), node_widths)
1552                        .header(header_row)
1553                        .column_spacing(1);
1554                    f.render_widget(table, inner_area);
1555                }
1556
1557                f.render_widget(block_nodes, layout[2]);
1558            }
1559        }
1560
1561        // ==== Footer =====
1562
1563        let selected = self
1564            .items
1565            .as_ref()
1566            .and_then(|items| items.selected_item())
1567            .is_some();
1568
1569        let footer = Footer::default();
1570        let footer_state = if let Some(ref items) = self.items {
1571            if !items.items.is_empty() || !self.rewards_address.is_empty() {
1572                if !self.get_running_nodes().is_empty() {
1573                    if selected {
1574                        &mut NodesToStart::RunningSelected
1575                    } else {
1576                        &mut NodesToStart::Running
1577                    }
1578                } else if selected {
1579                    &mut NodesToStart::NotRunningSelected
1580                } else {
1581                    &mut NodesToStart::NotRunning
1582                }
1583            } else {
1584                &mut NodesToStart::NotRunning
1585            }
1586        } else {
1587            &mut NodesToStart::NotRunning
1588        };
1589        f.render_stateful_widget(footer, layout[3], footer_state);
1590
1591        // ===== Popups =====
1592
1593        // Error Popup
1594        if let Some(error_popup) = &self.error_popup
1595            && error_popup.is_visible()
1596        {
1597            error_popup.draw_error(f, area);
1598
1599            return Ok(());
1600        }
1601
1602        if self.nat_detection_in_progress {
1603            let popup_text = vec![
1604                Line::raw("NAT Detection Running..."),
1605                Line::raw(""),
1606                Line::raw(""),
1607                Line::raw("Please wait, performing NAT detection"),
1608                Line::raw("This may take a couple minutes."),
1609            ];
1610
1611            let popup_area = centered_rect_fixed(50, 12, area);
1612            clear_area(f, popup_area);
1613
1614            let popup_border = Paragraph::new("").block(
1615                Block::default()
1616                    .borders(Borders::ALL)
1617                    .title(" NAT Detection ")
1618                    .bold()
1619                    .title_style(Style::new().fg(VIVID_SKY_BLUE))
1620                    .padding(Padding::uniform(2))
1621                    .border_style(Style::new().fg(GHOST_WHITE)),
1622            );
1623
1624            let centred_area = Layout::new(
1625                Direction::Vertical,
1626                vec![
1627                    // border
1628                    Constraint::Length(2),
1629                    // our text goes here
1630                    Constraint::Min(5),
1631                    // border
1632                    Constraint::Length(1),
1633                ],
1634            )
1635            .split(popup_area);
1636            let text = Paragraph::new(popup_text)
1637                .block(Block::default().padding(Padding::horizontal(2)))
1638                .wrap(Wrap { trim: false })
1639                .alignment(Alignment::Center)
1640                .fg(EUCALYPTUS);
1641            f.render_widget(text, centred_area[1]);
1642
1643            f.render_widget(popup_border, popup_area);
1644        }
1645
1646        Ok(())
1647    }
1648
1649    fn handle_key_events(&mut self, key: KeyEvent) -> Result<Vec<Action>> {
1650        debug!("Key received in Status: {:?}", key);
1651        if let Some(error_popup) = &mut self.error_popup
1652            && error_popup.is_visible()
1653        {
1654            if error_popup.handle_input(key) {
1655                // Popup was dismissed, switch back to navigation mode
1656                return Ok(vec![Action::SwitchInputMode(InputMode::Navigation)]);
1657            }
1658            // Popup is still visible, stay in entry mode
1659            return Ok(vec![]);
1660        }
1661        Ok(vec![])
1662    }
1663}
1664
1665#[allow(dead_code)]
1666#[derive(Default, Clone)]
1667struct StatefulTable<T> {
1668    state: TableState,
1669    items: Vec<T>,
1670    last_selected: Option<usize>,
1671}
1672
1673#[allow(dead_code)]
1674impl<T> StatefulTable<T> {
1675    fn with_items(items: Vec<T>) -> Self {
1676        StatefulTable {
1677            state: TableState::default(),
1678            items,
1679            last_selected: None,
1680        }
1681    }
1682
1683    fn next(&mut self) {
1684        let i = match self.state.selected() {
1685            Some(i) => {
1686                if !self.items.is_empty() {
1687                    if i >= self.items.len() - 1 { 0 } else { i + 1 }
1688                } else {
1689                    0
1690                }
1691            }
1692            None => self.last_selected.unwrap_or(0),
1693        };
1694        self.state.select(Some(i));
1695        self.last_selected = Some(i);
1696    }
1697
1698    fn previous(&mut self) {
1699        let i = match self.state.selected() {
1700            Some(i) => {
1701                if !self.items.is_empty() {
1702                    if i == 0 { self.items.len() - 1 } else { i - 1 }
1703                } else {
1704                    0
1705                }
1706            }
1707            None => self.last_selected.unwrap_or(0),
1708        };
1709        self.state.select(Some(i));
1710        self.last_selected = Some(i);
1711    }
1712
1713    fn selected_item(&self) -> Option<&T> {
1714        self.state
1715            .selected()
1716            .and_then(|index| self.items.get(index))
1717    }
1718}
1719
1720#[derive(Default, Debug, Copy, Clone, PartialEq)]
1721enum NodeStatus {
1722    #[default]
1723    Added,
1724    Running,
1725    Starting,
1726    Stopped,
1727    Stopping,
1728    Removed,
1729    Updating,
1730}
1731
1732impl fmt::Display for NodeStatus {
1733    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1734        match *self {
1735            NodeStatus::Added => write!(f, "Added"),
1736            NodeStatus::Running => write!(f, "Running"),
1737            NodeStatus::Starting => write!(f, "Starting"),
1738            NodeStatus::Stopped => write!(f, "Stopped"),
1739            NodeStatus::Stopping => write!(f, "Stopping"),
1740            NodeStatus::Removed => write!(f, "Removed"),
1741            NodeStatus::Updating => write!(f, "Updating"),
1742        }
1743    }
1744}
1745
1746#[derive(Default, Debug, Clone)]
1747pub struct NodeItem<'a> {
1748    name: String,
1749    version: String,
1750    attos: usize,
1751    memory: usize,
1752    mbps: String,
1753    records: usize,
1754    peers: usize,
1755    connections: usize,
1756    locked: bool, // Semaphore for being able to change status
1757    mode: NodeConnectionMode,
1758    status: NodeStatus,
1759    failure: Option<(chrono::DateTime<chrono::Utc>, String)>,
1760    spinner: Throbber<'a>,
1761    spinner_state: ThrobberState,
1762}
1763
1764impl NodeItem<'_> {
1765    fn render_as_row(
1766        &mut self,
1767        index: usize,
1768        area: Rect,
1769        f: &mut Frame<'_>,
1770        is_selected: bool,
1771    ) -> Row<'_> {
1772        let mut row_style = if is_selected {
1773            Style::default().fg(GHOST_WHITE).bg(INDIGO)
1774        } else {
1775            Style::default().fg(GHOST_WHITE)
1776        };
1777        let mut spinner_state = self.spinner_state.clone();
1778        match self.status {
1779            NodeStatus::Running => {
1780                self.spinner = self
1781                    .spinner
1782                    .clone()
1783                    .throbber_style(Style::default().fg(EUCALYPTUS).add_modifier(Modifier::BOLD))
1784                    .throbber_set(throbber_widgets_tui::BRAILLE_SIX_DOUBLE)
1785                    .use_type(throbber_widgets_tui::WhichUse::Spin);
1786                row_style = if is_selected {
1787                    Style::default().fg(EUCALYPTUS).bg(INDIGO)
1788                } else {
1789                    Style::default().fg(EUCALYPTUS)
1790                };
1791            }
1792            NodeStatus::Starting => {
1793                self.spinner = self
1794                    .spinner
1795                    .clone()
1796                    .throbber_style(Style::default().fg(EUCALYPTUS).add_modifier(Modifier::BOLD))
1797                    .throbber_set(throbber_widgets_tui::BOX_DRAWING)
1798                    .use_type(throbber_widgets_tui::WhichUse::Spin);
1799            }
1800            NodeStatus::Stopping => {
1801                self.spinner = self
1802                    .spinner
1803                    .clone()
1804                    .throbber_style(Style::default().fg(EUCALYPTUS).add_modifier(Modifier::BOLD))
1805                    .throbber_set(throbber_widgets_tui::BOX_DRAWING)
1806                    .use_type(throbber_widgets_tui::WhichUse::Spin);
1807            }
1808            NodeStatus::Stopped => {
1809                self.spinner = self
1810                    .spinner
1811                    .clone()
1812                    .throbber_style(
1813                        Style::default()
1814                            .fg(GHOST_WHITE)
1815                            .add_modifier(Modifier::BOLD),
1816                    )
1817                    .throbber_set(throbber_widgets_tui::BRAILLE_SIX_DOUBLE)
1818                    .use_type(throbber_widgets_tui::WhichUse::Full);
1819            }
1820            NodeStatus::Updating => {
1821                self.spinner = self
1822                    .spinner
1823                    .clone()
1824                    .throbber_style(
1825                        Style::default()
1826                            .fg(GHOST_WHITE)
1827                            .add_modifier(Modifier::BOLD),
1828                    )
1829                    .throbber_set(throbber_widgets_tui::VERTICAL_BLOCK)
1830                    .use_type(throbber_widgets_tui::WhichUse::Spin);
1831            }
1832            _ => {}
1833        };
1834
1835        let failure = self.failure.as_ref().map_or_else(
1836            || "-".to_string(),
1837            |(_dt, msg)| {
1838                if self.status == NodeStatus::Stopped {
1839                    msg.clone()
1840                } else {
1841                    "-".to_string()
1842                }
1843            },
1844        );
1845
1846        let row = vec![
1847            self.name.clone().to_string(),
1848            self.version.to_string(),
1849            format!(
1850                "{}{}",
1851                " ".repeat(ATTOS_WIDTH.saturating_sub(self.attos.to_string().len())),
1852                self.attos.to_string()
1853            ),
1854            format!(
1855                "{}{} MB",
1856                " ".repeat(MEMORY_WIDTH.saturating_sub(self.memory.to_string().len() + 4)),
1857                self.memory.to_string()
1858            ),
1859            format!(
1860                "{}{}",
1861                " ".repeat(MBPS_WIDTH.saturating_sub(self.mbps.to_string().len())),
1862                self.mbps.to_string()
1863            ),
1864            format!(
1865                "{}{}",
1866                " ".repeat(RECORDS_WIDTH.saturating_sub(self.records.to_string().len())),
1867                self.records.to_string()
1868            ),
1869            format!(
1870                "{}{}",
1871                " ".repeat(PEERS_WIDTH.saturating_sub(self.peers.to_string().len())),
1872                self.peers.to_string()
1873            ),
1874            format!(
1875                "{}{}",
1876                " ".repeat(CONNS_WIDTH.saturating_sub(self.connections.to_string().len())),
1877                self.connections.to_string()
1878            ),
1879            self.mode.to_string(),
1880            self.status.to_string(),
1881            failure,
1882        ];
1883        let throbber_area = Rect::new(area.width - 3, area.y + 2 + index as u16, 1, 1);
1884
1885        f.render_stateful_widget(self.spinner.clone(), throbber_area, &mut spinner_state);
1886
1887        Row::new(row).style(row_style)
1888    }
1889}