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