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