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