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