From 0fab57d94f2cbeca8e2cb4cf44bedaefe5538a5f Mon Sep 17 00:00:00 2001 From: glyph Date: Tue, 25 Oct 2022 15:14:01 +0100 Subject: [PATCH 01/13] uncomment vnstat_parse dependency --- peach-web/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/peach-web/Cargo.toml b/peach-web/Cargo.toml index 63be68f..0507d6e 100644 --- a/peach-web/Cargo.toml +++ b/peach-web/Cargo.toml @@ -49,6 +49,5 @@ peach-network = { path = "../peach-network" } #peach-stats = { path = "../peach-stats" } rouille = { version = "3.5", default-features = false } temporary = "0.6" -# TODO: uncomment this when data usage feature is in place -#vnstat_parse = "0.1.0" +vnstat_parse = "0.1.0" xdg = "2.2" From 4e7fbd5fdf9ead55742a6b9818ab7ffe8091208c Mon Sep 17 00:00:00 2001 From: glyph Date: Tue, 25 Oct 2022 15:14:52 +0100 Subject: [PATCH 02/13] add the refactored template for network status --- peach-web/src/routes/status/mod.rs | 2 +- peach-web/src/routes/status/network.rs | 292 +++++++++++++++++++++++-- 2 files changed, 279 insertions(+), 15 deletions(-) diff --git a/peach-web/src/routes/status/mod.rs b/peach-web/src/routes/status/mod.rs index 90d533e..d68980b 100644 --- a/peach-web/src/routes/status/mod.rs +++ b/peach-web/src/routes/status/mod.rs @@ -1,3 +1,3 @@ //pub mod device; -//pub mod network; +pub mod network; pub mod scuttlebutt; diff --git a/peach-web/src/routes/status/network.rs b/peach-web/src/routes/status/network.rs index c75c0dc..d3ca1bc 100644 --- a/peach-web/src/routes/status/network.rs +++ b/peach-web/src/routes/status/network.rs @@ -1,21 +1,285 @@ -use rocket::{get, request::FlashMessage}; -use rocket_dyn_templates::Template; +use maud::{html, Markup, PreEscaped}; +use peach_network::network; +use vnstat_parse::Vnstat; -use crate::context::network::NetworkStatusContext; -use crate::routes::authentication::Authenticated; +use crate::{templates, utils::theme, AP_IFACE, WLAN_IFACE}; -// HELPERS AND ROUTES FOR /status/network +enum NetworkState { + AccessPoint, + WiFiClient, +} -#[get("/network")] -pub fn network_status(flash: Option, _auth: Authenticated) -> Template { - let mut context = NetworkStatusContext::build(); - context.back = Some("/status".to_string()); - context.title = Some("Network Status".to_string()); +// ROUTE: /status/network - if let Some(flash) = flash { - context.flash_name = Some(flash.kind().to_string()); - context.flash_msg = Some(flash.message().to_string()); +/// Render the cog icon which is used as a link to the network settings page. +fn render_network_config_icon() -> Markup { + html! { + (PreEscaped("")) + a class="link two-grid-top-right" href="/settings/network" title="Configure network settings" { + img id="configureNetworking" class="icon-small" src="/icons/cog.svg" alt="Configure"; + } + } +} + +/// Render the network mode icon, either a WiFi signal or router, based +/// on the state of the AP and WiFi interfaces. +/// +/// A router icon is shown if the AP is online (interface is "up"). +/// +/// A WiFi signal icon is shown if the AP interface is down. The colour of +/// the icon is black if the WLAN interface is up and gray if it's down. +fn render_network_mode_icon(state: &NetworkState) -> Markup { + // TODO: make this DRYer + let (icon_class, icon_src, icon_alt, label_title, label_value) = match state { + NetworkState::AccessPoint => ( + "center icon icon-active", + "/icons/router.svg", + "WiFi router", + "Access Point Online", + "ONLINE", + ), + NetworkState::WiFiClient => match network::state(WLAN_IFACE) { + Ok(Some(state)) if state == "up" => ( + "center icon icon-active", + "/icons/wifi.svg", + "WiFi signal", + "WiFi Client Online", + "ONLINE", + ), + _ => ( + "center icon icon-inactive", + "/icons/wifi.svg", + "WiFi signal", + "WiFi Client Offline", + "OFFLINE", + ), + }, }; - Template::render("status/network", &context) + html! { + (PreEscaped("")) + div class="grid-column-1" { + img id="netModeIcon" class=(icon_class) src=(icon_src) alt=(icon_alt); + label id="netModeLabel" for="netModeIcon" class="center label-small font-gray" title=(label_title) { (label_value) } + } + } +} + +/// Render the network data associated with the deployed access point or +/// connected WiFi client depending on active mode. +/// +/// Data includes the network mode (access point or WiFi client), SSID and IP +/// address. +fn render_network_data(state: &NetworkState, ssid: String, ip: String) -> Markup { + let (mode_value, mode_title, ssid_value, ip_title) = match state { + NetworkState::AccessPoint => ( + "Access Point", + "Access Point SSID", + // TODO: remove hardcoding of this value (query interface instead) + "peach", + "Access Point IP Address", + ), + NetworkState::WiFiClient => ( + "WiFi Client", + "WiFi SSID", + ssid.as_str(), + "WiFi Client IP Address", + ), + }; + + html! { + (PreEscaped("")) + div class="grid-column-2" { + label class="label-small font-gray" for="netMode" title="Network Mode" { "MODE" } + p id="netMode" class="card-text" title="Network Mode" { (mode_value) } + label class="label-small font-gray" for="netSsid" title=(mode_title) { "SSID" } + p id="netSsid" class="card-text" title="SSID" { (ssid_value) } + label class="label-small font-gray" for="netIp" title=(ip_title) { "IP" } + p id="netIp" class="card-text" title="IP" { (ip) } + } + } +} + +/// Render the network status grid comprised of the network config icon, +/// network mode icon and network data text. +fn render_network_status_grid(state: &NetworkState, ssid: String, ip: String) -> Markup { + html! { + (PreEscaped("")) + div class="two-grid" title="PeachCloud network mode and status" { + (render_network_config_icon()) + (PreEscaped("")) + (render_network_mode_icon(state)) + (PreEscaped("")) + (render_network_data(state, ssid, ip)) + } + } +} + +/// Render the signal strength stack comprised of a signal icon, RSSI value +/// and label. +/// +/// This stack is displayed when the network mode is set to WiFi +/// client (ie. the value reported is the strength of the connection of the +/// local WiFi interface to a remote access point). +fn render_signal_strength_stack() -> Markup { + let wlan_rssi = match network::rssi(WLAN_IFACE) { + Ok(Some(rssi)) => rssi, + _ => 0.to_string(), + }; + + html! { + div class="stack" { + img id="netSignal" class="icon icon-medium" alt="Signal" title="WiFi Signal (%)" src="/icons/low-signal.svg"; + div class="flex-grid" style="padding-top: 0.5rem;" { + label class="label-medium" for="netSignal" style="padding-right: 3px;" title="Signal strength of WiFi connection (%)" { (wlan_rssi) } + } + label class="label-small font-gray" { "SIGNAL" } + } + } +} + +/// Render the connected devices stack comprised of a devices icon, value +/// of connected devices and label. +/// +/// This stack is displayed when the network mode is set to access point +/// (ie. the value reported is the number of remote devices connected to the +/// local access point). +fn render_connected_devices_stack() -> Markup { + html! { + div class="stack" { + img id="devices" class="icon icon-medium" title="Connected devices" src="/icons/devices.svg" alt="Digital devices"; + div class="flex-grid" style="padding-top: 0.5rem;" { + label class="label-medium" for="devices" style="padding-right: 3px;" title="Number of connected devices"; + } + label class="label-small font-gray" { "DEVICES" } + } + } +} + +/// Render the data download stack comprised of a download icon, traffic value +/// and label. +/// +/// A zero value is displayed if no interface traffic is available for the +/// WLAN interface. +fn render_data_download_stack(iface_traffic: &Option) -> Markup { + html! { + div class="stack" { + img id="dataDownload" class="icon icon-medium" title="Download" src="/icons/down-arrow.svg" alt="Download"; + div class="flex-grid" style="padding-top: 0.5rem;" { + @if let Some(traffic) = iface_traffic { + label class="label-medium" for="dataDownload" style="padding-right: 3px;" title={ "Data download total in " (traffic.all_time_rx_unit) } { (traffic.all_time_rx) } + label class="label-small font-near-black" { (traffic.all_time_rx_unit) } + } @else { + label class="label-medium" for="dataDownload" style="padding-right: 3px;" title="Data download total" { "0" } + label class="label-small font-near-black"; + } + } + label class="label-small font-gray" { "DOWNLOAD" } + } + } +} + +/// Render the data upload stack comprised of an upload icon, traffic value +/// and label. +/// +/// A zero value is displayed if no interface traffic is available for the +/// WLAN interface. +fn render_data_upload_stack(iface_traffic: Option) -> Markup { + html! { + div class="stack" { + img id="dataUpload" class="icon icon-medium" title="Upload" src="/icons/up-arrow.svg" alt="Upload"; + div class="flex-grid" style="padding-top: 0.5rem;" { + @if let Some(traffic) = iface_traffic { + label class="label-medium" for="dataUpload" style="padding-right: 3px;" title={ "Data upload total in " (traffic.all_time_tx_unit) } { (traffic.all_time_tx) } + label class="label-small font-near-black" { (traffic.all_time_tx_unit) } + } @else { + label class="label-medium" for="dataUpload" style="padding-right: 3px;" title="Data upload total" { "0" } + label class="label-small font-near-black"; + } + } + label class="label-small font-gray" { "UPLOAD" } + } + } +} + +/// Render the device / signal and traffic grid. +/// +/// The connected devices stack is displayed if the network mode is set to +/// access point and the signal strength stack is displayed if the network +/// mode is set to WiFi client. +fn render_device_and_traffic_grid(state: NetworkState, iface_traffic: Option) -> Markup { + html! { + div class="three-grid card-container" { + @match state { + NetworkState::AccessPoint => (render_connected_devices_stack()), + NetworkState::WiFiClient => (render_signal_strength_stack()), + } + (render_data_download_stack(&iface_traffic)) + (render_data_upload_stack(iface_traffic)) + } + } +} + +/// Network state data retrieval. +/// +/// This data is injected into the template rendering functions. +fn retrieve_network_data() -> (NetworkState, Option, String, String) { + // if the access point interface is "up", + // retrieve the traffic stats, ip and ssidfor the ap interface. + // otherwise retrieve the stats and ip for the wlan interface. + let (state, traffic, ip, ssid) = match network::state(AP_IFACE) { + Ok(Some(state)) if state == "up" => { + let ap_traffic = Vnstat::get(AP_IFACE).ok(); + + let ap_ip = match network::ip(AP_IFACE) { + Ok(Some(ip)) => ip, + _ => String::from("x.x.x.x"), + }; + + let ap_ssid = String::from("peach"); + + (NetworkState::AccessPoint, ap_traffic, ap_ip, ap_ssid) + } + _ => { + let wlan_traffic = Vnstat::get(WLAN_IFACE).ok(); + + let wlan_ip = match network::ip(WLAN_IFACE) { + Ok(Some(ip)) => ip, + _ => String::from("x.x.x.x"), + }; + + let wlan_ssid = match network::ssid(WLAN_IFACE) { + Ok(Some(ssid)) => ssid, + _ => String::from("Not connected"), + }; + + (NetworkState::WiFiClient, wlan_traffic, wlan_ip, wlan_ssid) + } + }; + + (state, traffic, ip, ssid) +} + +/// Network status template builder. +pub fn build_template() -> PreEscaped { + let (state, traffic, ip, ssid) = retrieve_network_data(); + + let network_status_template = html! { + (PreEscaped("")) + div class="card center" { + (PreEscaped("")) + div class="capsule capsule-container success-border" { + (render_network_status_grid(&state, ssid, ip)) + hr style="color: var(--light-gray);"; + (render_device_and_traffic_grid(state, traffic)) + } + } + }; + + let body = + templates::nav::build_template(network_status_template, "Network Status", Some("/status")); + + let theme = theme::get_theme(); + + templates::base::build_template(body, theme) } From d9167a2cd6acdcba5f691ef6374d0522ad205943 Mon Sep 17 00:00:00 2001 From: glyph Date: Tue, 25 Oct 2022 15:15:13 +0100 Subject: [PATCH 03/13] mount the network status route --- peach-web/src/private_router.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/peach-web/src/private_router.rs b/peach-web/src/private_router.rs index 248e375..ec1f120 100644 --- a/peach-web/src/private_router.rs +++ b/peach-web/src/private_router.rs @@ -252,6 +252,10 @@ pub fn mount_peachpub_routes( Response::html(routes::status::scuttlebutt::build_template()).add_cookie("back_url=/status/scuttlebutt") }, + (GET) (/status/network) => { + Response::html(routes::status::network::build_template()) + }, + // render the not_found template and set a 404 status code if none of // the other blocks matches the request _ => Response::html(templates::not_found::build_template()).with_status_code(404) From 3bf095e148bf25b65f770ecac71485b105593e54 Mon Sep 17 00:00:00 2001 From: glyph Date: Tue, 25 Oct 2022 15:16:17 +0100 Subject: [PATCH 04/13] bump the version number and update the lockfile --- Cargo.lock | 9 ++++++++- peach-web/Cargo.toml | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99e1f25..c1de23b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2333,7 +2333,7 @@ dependencies = [ [[package]] name = "peach-web" -version = "0.6.19" +version = "0.6.20" dependencies = [ "async-std", "base64 0.13.0", @@ -2349,6 +2349,7 @@ dependencies = [ "peach-network", "rouille", "temporary", + "vnstat_parse", "xdg", ] @@ -3814,6 +3815,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "vnstat_parse" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75334377a2918b45b5b8da023080375a4ec0aa04b0bc88f896ea93cf4b32feff" + [[package]] name = "void" version = "1.0.2" diff --git a/peach-web/Cargo.toml b/peach-web/Cargo.toml index 0507d6e..88effd8 100644 --- a/peach-web/Cargo.toml +++ b/peach-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "peach-web" -version = "0.6.20" +version = "0.6.21" authors = ["Andrew Reid ", "Max Fowler "] edition = "2018" description = "peach-web is a web application which provides a web interface for monitoring and interacting with the PeachCloud device. This allows administration of the single-board computer (ie. Raspberry Pi) running PeachCloud, as well as the ssb-server and related plugins." From a1b16f8d3839c1d68015e2ee8e27fdfaf869330e Mon Sep 17 00:00:00 2001 From: glyph Date: Wed, 2 Nov 2022 15:15:06 +0000 Subject: [PATCH 05/13] add refactored device status template and import module --- peach-web/src/routes/status/device.rs | 529 ++++++++++++++++++++++++++ peach-web/src/routes/status/mod.rs | 2 +- 2 files changed, 530 insertions(+), 1 deletion(-) diff --git a/peach-web/src/routes/status/device.rs b/peach-web/src/routes/status/device.rs index ec66837..5e57b0d 100644 --- a/peach-web/src/routes/status/device.rs +++ b/peach-web/src/routes/status/device.rs @@ -1,3 +1,531 @@ +use std::process::Command; + +use maud::{html, Markup, PreEscaped}; +use peach_lib::{config_manager, dyndns_client, oled_client}; +use peach_stats::{ + stats, + stats::{CpuStatPercentages, MemStat}, +}; + +use crate::{templates, utils::theme}; + +// ROUTE: /status + +/// Query systemd to determine the state of the networking service. +fn retrieve_networking_state() -> Option { + // call: `systemctl show networking.service --no-page` + let networking_service_output = Command::new("systemctl") + .arg("show") + .arg("networking.service") + .arg("--no-page") + .output() + .ok()?; + + let service_info = std::str::from_utf8(&networking_service_output.stdout).ok()?; + + // find the line starting with "ActiveState=" and return the value + service_info + .lines() + .find(|line| line.starts_with("ActiveState=")) + .map(|line| line.strip_prefix("ActiveState=")) + .flatten() + .map(|state| state.to_string()) +} + +/// Query systemd to determine the state of the sbot service. +fn retrieve_sbot_state() -> Option { + // retrieve the name of the go-sbot service or set default + let go_sbot_service = config_manager::get_config_value("GO_SBOT_SERVICE") + .unwrap_or("go-sbot.service".to_string()); + + let sbot_service_output = Command::new("systemctl") + .arg("show") + .arg(go_sbot_service) + .arg("--no-page") + .output() + .ok()?; + + let service_info = std::str::from_utf8(&sbot_service_output.stdout).ok()?; + + // find the line starting with "ActiveState=" and return the value + service_info + .lines() + .find(|line| line.starts_with("ActiveState=")) + .map(|line| line.strip_prefix("ActiveState=")) + .flatten() + .map(|state| state.to_string()) +} + +fn retrieve_device_status_data() -> (Option, String) { + let uptime = stats::uptime().ok(); + + let oled_ping = match oled_client::ping() { + Ok(_) => "ONLINE".to_string(), + Err(_) => "OFFLINE".to_string(), + }; + + (uptime, oled_ping) +} + +fn retrieve_device_usage_data<'a>() -> (Option, Option) { + // convert result to Option, discard any error + let cpu_stat_percent = stats::cpu_stats_percent().ok(); + let mem_stats = stats::mem_stats().ok(); + + (cpu_stat_percent, mem_stats) +} + +fn render_network_capsule() -> Markup { + let (state, stack_class, img_class) = match retrieve_networking_state() { + Some(state) if state.as_str() == "active" => { + ("active", "stack capsule border-success", "icon icon-medium") + } + Some(state) if state.as_str() == "inactive" => ( + "inactive", + "stack capsule border-warning", + "icon icon-inactive icon-medium", + ), + Some(state) if state.as_str() == "failed" => ( + "failed", + "stack capsule border-danger", + "icon icon-inactive icon-medium", + ), + _ => ( + "error", + "stack capsule border-danger", + "icon icon-inactive icon-medium", + ), + }; + + html! { + (PreEscaped("")) + div class=(stack_class) { + img id="networkIcon" class=(img_class) alt="Network" title="Networking service status" src="icons/wifi.svg"; + div class="stack" style="padding-top: 0.5rem;" { + label class="label-small font-near-black" { "Networking" } + label class="label-small font-near-black" { (state.to_uppercase()) } + } + } + } +} + +fn render_oled_capsule(state: String) -> Markup { + let (stack_class, img_class) = match state.as_str() { + "ONLINE" => ("stack capsule border-success", "icon icon-medium"), + _ => ( + "stack capsule border-warning", + "icon icon-inactive icon-medium", + ), + }; + + html! { + (PreEscaped("")) + div class=(stack_class) { + img id="oledIcon" class=(img_class) alt="Display" title="OLED display microservice status" src="icons/lcd.svg"; + div class="stack" style="padding-top: 0.5rem;" { + label class="label-small font-near-black" { "Display" } + label class="label-small font-near-black" { (state) } + } + } + } +} + +fn render_diagnostics_capsule() -> Markup { + // TODO: write a diagnostics module (maybe in peach-lib) + + let diagnostics_state = "CLEAR"; + + html! { + (PreEscaped("")) + div class="stack capsule border-success" { + img id="statsIcon" class="icon icon-medium" alt="Line chart" title="System diagnostics and logs" src="icons/chart.svg"; + div class="stack" style="padding-top: 0.5rem;" { + label class="label-small font-near-black" { "Diagnostics" } + label class="label-small font-near-black" { (diagnostics_state) }; + } + } + } +} + +fn render_dyndns_capsule() -> Markup { + let (state, stack_class, img_class) = match dyndns_client::is_dns_updater_online() { + Ok(true) => ("ONLINE", "stack capsule border-success", "icon icon-medium"), + Ok(false) => ( + "OFFLINE", + "stack capsule border-warning", + "icon icon-inactive icon-medium", + ), + Err(_) => ( + "ERROR", + "stack capsule border-danger", + "icon icon-inactive icon-medium", + ), + }; + + html! { + (PreEscaped("")) + div class=(stack_class) { + img id="dnsIcon" class=(img_class) alt="Dyndns" title="Dyndns status" src="icons/dns.png"; + div class="stack" style="padding-top: 0.5rem;" { + label class="label-small font-near-black" { "Dyn DNS" } + label class="label-small font-near-black" { (state) } + } + } + } +} + +fn render_config_capsule() -> Markup { + let (state, stack_class, img_class) = + match config_manager::load_peach_config_from_disc().is_ok() { + true => ("LOADED", "stack capsule border-success", "icon icon-medium"), + false => ( + "INVALID", + "stack capsule border-warning", + "icon icon-inactive icon-medium", + ), + }; + + html! { + (PreEscaped("")) + div class=(stack_class) { + img id="configIcon" class=(img_class) alt="Config" title="Config status" src="icons/clipboard.png"; + div class="stack" style="padding-top: 0.5rem;" { + label class="label-small font-near-black" { "Config" } + label class="label-small font-near-black" { (state) } + } + } + } +} + +fn render_sbot_capsule() -> Markup { + let (state, stack_class, img_class) = match retrieve_sbot_state() { + Some(state) if state.as_str() == "active" => { + ("active", "stack capsule border-success", "icon icon-medium") + } + Some(state) if state.as_str() == "inactive" => ( + "inactive", + "stack capsule border-warning", + "icon icon-inactive icon-medium", + ), + Some(state) if state.as_str() == "failed" => ( + "failed", + "stack capsule border-danger", + "icon icon-inactive icon-medium", + ), + _ => ( + "error", + "stack capsule border-danger", + "icon icon-inactive icon-medium", + ), + }; + + html! { + (PreEscaped("")) + div class=(stack_class) { + img id="sbotIcon" class=(img_class) alt="Sbot" title="Sbot status" src="icons/hermies.svg"; + div class="stack" style="padding-top: 0.5rem;" { + label class="label-small font-near-black" { "Sbot" } + label class="label-small font-near-black" { (state.to_uppercase()) } + } + } + } +} + +fn render_cpu_usage_meter(cpu_usage_percent: Option) -> Markup { + html! { + @if let Some(cpu_usage) = cpu_usage_percent { + @let cpu_usage_total = (cpu_usage.nice + cpu_usage.system + cpu_usage.user).round(); + div class="flex-grid" { + span class="card-text" { "CPU" } + span class="label-small push-right" { (cpu_usage_total) "%" } + } + meter value=(cpu_usage_total) min="0" max="100" title="CPU usage" { + div class="meter-gauge" { + span style={ "width: " (cpu_usage_total) "%;" } { + "CPU Usage" + } + } + } + } @else { + p class="card-text" { "CPU usage data unavailable" } + } + } +} + +fn render_mem_usage_meter(mem_stats: Option) -> Markup { + html! { + @if let Some(mem) = mem_stats { + // convert kilobyte values to megabyte values + @let mem_free_mb = mem.free / 1024; + @let mem_total_mb = mem.total / 1024; + @let mem_used_mb = mem.used / 1024; + // calculate memory usage as a percentage + @let mem_used_percent = mem_used_mb * 100 / mem_total_mb; + // render disk free value as megabytes or gigabytes based on size + @let mem_free_value = if mem_free_mb > 1024 { + format!("{} GB", (mem_free_mb / 1024)) + } else { + format!("{} MB", mem_free_mb) + }; + + div class="flex-grid" { + span class="card-text" { "Memory" } + span class="label-small push-right" { (mem_used_percent) "% (" (mem_free_value) " free)" } + } + meter value=(mem_used_mb) min="0" max=(mem_total_mb) title="Memory usage" { + div class="meter-gauge" { + span style={ "width: " (mem_used_percent) "%;" } { "Memory Usage" } + } + } + } @else { + p class="card-text" { "Memory usage data unavailable" } + } + } +} + +fn render_disk_usage_meter() -> Markup { + let disk_usage_stats = match stats::disk_usage() { + Ok(disks) => disks, + Err(_) => Vec::new(), + }; + + // select only the partition we're interested in: /dev/mmcblk0p2 ("/") + let disk_usage = disk_usage_stats.iter().find(|disk| disk.mountpoint == "/"); + + html! { + @if let Some(disk) = disk_usage { + // calculate free disk space in megabytes + @let disk_free_mb = disk.one_k_blocks_free / 1024; + // calculate free disk space in gigabytes + @let disk_free_gb = disk_free_mb / 1024; + // render disk free value as megabytes or gigabytes based on size + @let disk_free_value = if disk_free_mb > 1024 { + format!("{} GB", disk_free_gb) + } else { + format!("{} MB", disk_free_mb) + }; + + div class="flex-grid" { + span class="card-text" { "Disk" } + span class="label-small push-right" { (disk.used_percentage) "% (" (disk_free_value) " free)" } + } + meter value=(disk.used_percentage) min="0" max="100" title="Disk usage" { + div class="meter-gauge" { + span style={ "width: " (disk.used_percentage) "%;" } { + "Disk Usage" + } + } + } + } @else { + p class="card-text" { "Disk usage data unavailable" } + } + } +} + +/// Display system uptime in hours and minutes. +fn render_uptime_capsule(uptime: Option) -> Markup { + html! { + @if let Some(uptime_secs) = uptime { + @let uptime_mins = uptime_secs / 60; + @if uptime_mins < 60 { + // display system uptime in minutes + p class="capsule center-text" { + "Uptime: " (uptime_mins) " minutes" + } + } @else { + // display system uptime in hours and minutes + @let hours = uptime_mins / 60; + @let mins = uptime_mins % 60; + p class="capsule center-text" { + "Uptime: " (hours) " hours, " (mins) " minutes" + } + } + } @else { + p class="card-text" { "Uptime data unavailable" } + } + } +} + +/// Device status template builder. +pub fn build_template() -> PreEscaped { + let (uptime, oled_state) = retrieve_device_status_data(); + let (cpu_usage, mem_usage) = retrieve_device_usage_data(); + + let device_status_template = html! { + (PreEscaped("")) + div class="card center" { + div class="card-container" { + // display status capsules for network, oled and diagnostics + div class="three-grid" { + (render_network_capsule()) + (render_oled_capsule(oled_state)) + (render_diagnostics_capsule()) + } + // display status capsules for dyndns, config and sbot + div class="three-grid" style="padding-top: 1rem; padding-bottom: 1rem;" { + (render_dyndns_capsule()) + (render_config_capsule()) + (render_sbot_capsule()) + } + (render_cpu_usage_meter(cpu_usage)) + (render_mem_usage_meter(mem_usage)) + (render_disk_usage_meter()) + (render_uptime_capsule(uptime)) + } + } + }; + + let body = templates::nav::build_template(device_status_template, "Device Status", Some("/")); + + let theme = theme::get_theme(); + + templates::base::build_template(body, theme) +} + +/* + +{%- extends "nav" -%} +{%- block card -%} + {# ASSIGN VARIABLES #} + {# ---------------- #} + {%- if mem_stats -%} + {% set mem_usage_percent = mem_stats.used / mem_stats.total * 100 | round -%} + {% set mem_used = mem_stats.used / 1024 | round -%} + {% set mem_free = mem_stats.free / 1024 | round -%} + {% set mem_total = mem_stats.total / 1024 | round -%} + {% endif -%} + {% if cpu_stat_percent -%} + {% set cpu_usage_percent = cpu_stat_percent.nice + cpu_stat_percent.system + cpu_stat_percent.user | round -%} + {%- endif -%} + {%- if disk_stats -%} + {%- for disk in disk_stats -%} + {%- set_global disk_usage_percent = disk.used_percentage -%} + {# Calculate free disk space in megabytes #} + {%- set_global disk_free = disk.one_k_blocks_free / 1024 | round -%} + {%- endfor -%} + {%- endif -%} + +
+
+ {# Display microservice status for network, oled & stats #} +
+ +
+ Network +
+ + +
+
+ +
+ Display +
+ + +
+
+ +
+ Stats +
+ + +
+
+
+
+ +
+ Dyndns +
+ + +
+
+ +
+ Config +
+ + +
+
+ +
+ Sbot +
+ + +
+
+
+ {# Display CPU usage meter #} + {%- if cpu_stat_percent -%} +
+ CPU + {{ cpu_usage_percent }}% +
+ +
+ CPU Usage +
+
+ {%- else -%} +

CPU usage data unavailable

+ {% endif -%} + {# Display memory usage meter #} + {%- if mem_stats %} +
+ Memory + {{ mem_usage_percent }}% ({{ mem_free }} MB free) +
+ +
+ Memory Usage +
+
+ {%- else -%} +

Memory usage data unavailable

+ {% endif -%} + {# Display disk usage meter #} + {%- if disk_stats %} +
+ Disk + {{ disk_usage_percent }}% ({% if disk_free > 1024 %}{{ disk_free / 1024 | round }} GB{% else %}{{ disk_free }} MB{% endif %} free) +
+ +
+ Disk Usage +
+
+ {%- else -%} +

Disk usage data unavailable

+ {%- endif %} + {# Display system uptime in minutes #} + {%- if uptime and uptime < 60 %} +

Uptime: {{ uptime }} minutes

+ {# Display system uptime in hours & minutes #} + {%- elif uptime and uptime > 60 -%} +

Uptime: {{ uptime / 60 | round(method="floor") }} hours, {{ uptime % 60 }} minutes

+ {%- else -%} +

Uptime data unavailable

+ {%- endif %} + + + {%- if flash_msg and flash_name == "success" -%} + +
{{ flash_msg }}.
+ {%- elif flash_msg and flash_name == "error" -%} + +
{{ flash_msg }}.
+ {%- endif %} +
+
+{%- endblock card %} + +// CONTEXT + use log::info; use rocket::{ get, @@ -236,3 +764,4 @@ pub fn power_menu(flash: Option, _auth: Authenticated) -> Template }; Template::render("power", &context) } +*/ diff --git a/peach-web/src/routes/status/mod.rs b/peach-web/src/routes/status/mod.rs index d68980b..a2e0f54 100644 --- a/peach-web/src/routes/status/mod.rs +++ b/peach-web/src/routes/status/mod.rs @@ -1,3 +1,3 @@ -//pub mod device; +pub mod device; pub mod network; pub mod scuttlebutt; From 2eca7792084a1eff33b425797cae31fab7c79e72 Mon Sep 17 00:00:00 2001 From: glyph Date: Wed, 2 Nov 2022 15:15:42 +0000 Subject: [PATCH 06/13] add peach-stats dependency --- peach-web/Cargo.toml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/peach-web/Cargo.toml b/peach-web/Cargo.toml index 88effd8..1b13b00 100644 --- a/peach-web/Cargo.toml +++ b/peach-web/Cargo.toml @@ -44,9 +44,8 @@ lazy_static = "1.4" log = "0.4" maud = "0.23" peach-lib = { path = "../peach-lib" } -# these will be reintroduced when the full peachcloud mode is added peach-network = { path = "../peach-network" } -#peach-stats = { path = "../peach-stats" } +peach-stats = { path = "../peach-stats" } rouille = { version = "3.5", default-features = false } temporary = "0.6" vnstat_parse = "0.1.0" From cfe270a9957447c5d1d7e23cdcfb62bacdb81fcb Mon Sep 17 00:00:00 2001 From: glyph Date: Wed, 2 Nov 2022 15:16:00 +0000 Subject: [PATCH 07/13] mount device status route --- peach-web/src/private_router.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/peach-web/src/private_router.rs b/peach-web/src/private_router.rs index ec1f120..031a178 100644 --- a/peach-web/src/private_router.rs +++ b/peach-web/src/private_router.rs @@ -248,6 +248,10 @@ pub fn mount_peachpub_routes( routes::settings::theme::set_theme(theme) }, + (GET) (/status) => { + Response::html(routes::status::device::build_template()) + }, + (GET) (/status/scuttlebutt) => { Response::html(routes::status::scuttlebutt::build_template()).add_cookie("back_url=/status/scuttlebutt") }, From 8c3a92aa8883a91cf81d96df6d5d4af5c98b12db Mon Sep 17 00:00:00 2001 From: glyph Date: Wed, 2 Nov 2022 15:16:21 +0000 Subject: [PATCH 08/13] update lockfile --- Cargo.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index c1de23b..5fb2432 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2333,7 +2333,7 @@ dependencies = [ [[package]] name = "peach-web" -version = "0.6.20" +version = "0.6.21" dependencies = [ "async-std", "base64 0.13.0", @@ -2347,6 +2347,7 @@ dependencies = [ "maud", "peach-lib", "peach-network", + "peach-stats", "rouille", "temporary", "vnstat_parse", From 7d5d6bcc1fa03793febb45c03e7d5e9122e03773 Mon Sep 17 00:00:00 2001 From: glyph Date: Thu, 3 Nov 2022 12:02:01 +0000 Subject: [PATCH 09/13] add power menu template builder and mount route --- peach-web/src/private_router.rs | 4 ++++ peach-web/src/routes/mod.rs | 1 + peach-web/src/routes/power/menu.rs | 24 ++++++++++++++++++++++++ peach-web/src/routes/power/mod.rs | 1 + 4 files changed, 30 insertions(+) create mode 100644 peach-web/src/routes/power/menu.rs create mode 100644 peach-web/src/routes/power/mod.rs diff --git a/peach-web/src/private_router.rs b/peach-web/src/private_router.rs index 031a178..b858e31 100644 --- a/peach-web/src/private_router.rs +++ b/peach-web/src/private_router.rs @@ -49,6 +49,10 @@ pub fn mount_peachpub_routes( Response::html(routes::guide::build_template()) }, + (GET) (/power) => { + Response::html(routes::power::menu::build_template()) + }, + (POST) (/scuttlebutt/block) => { routes::scuttlebutt::block::handle_form(request) }, diff --git a/peach-web/src/routes/mod.rs b/peach-web/src/routes/mod.rs index fdc9575..43f5527 100644 --- a/peach-web/src/routes/mod.rs +++ b/peach-web/src/routes/mod.rs @@ -3,6 +3,7 @@ pub mod authentication; //pub mod index; pub mod guide; pub mod home; +pub mod power; pub mod scuttlebutt; pub mod settings; pub mod status; diff --git a/peach-web/src/routes/power/menu.rs b/peach-web/src/routes/power/menu.rs new file mode 100644 index 0000000..8d88d9d --- /dev/null +++ b/peach-web/src/routes/power/menu.rs @@ -0,0 +1,24 @@ +use maud::{html, PreEscaped}; + +use crate::{templates, utils::theme}; + +pub fn build_template() -> PreEscaped { + let power_menu_template = html! { + (PreEscaped("")) + div class="card center" { + div class="card-container" { + div id="buttons" { + a id="rebootBtn" class="button button-primary center" href="/reboot" title="Reboot Device" { "Reboot" } + a id="shutdownBtn" class="button button-warning center" href="/shutdown" title="Shutdown Device" { "Shutdown" } + a id="cancelBtn" class="button button-secondary center" href="/" title="Cancel" { "Cancel" } + } + } + } + }; + + let body = templates::nav::build_template(power_menu_template, "Power Menu", Some("/")); + + let theme = theme::get_theme(); + + templates::base::build_template(body, theme) +} diff --git a/peach-web/src/routes/power/mod.rs b/peach-web/src/routes/power/mod.rs new file mode 100644 index 0000000..b9a0e3e --- /dev/null +++ b/peach-web/src/routes/power/mod.rs @@ -0,0 +1 @@ +pub mod menu; From 8cbb295c3aeb2db84471d00a5d43afe0e4bdaadf Mon Sep 17 00:00:00 2001 From: glyph Date: Mon, 28 Nov 2022 09:10:42 +0200 Subject: [PATCH 10/13] add power menu to settings menu and mount routes --- peach-web/src/private_router.rs | 16 ++++++--- peach-web/src/routes/mod.rs | 1 - peach-web/src/routes/power/mod.rs | 1 - peach-web/src/routes/settings/menu.rs | 4 ++- peach-web/src/routes/settings/mod.rs | 1 + .../src/routes/{ => settings}/power/menu.rs | 19 ++++++++-- peach-web/src/routes/settings/power/mod.rs | 3 ++ peach-web/src/routes/settings/power/reboot.rs | 36 +++++++++++++++++++ .../src/routes/settings/power/shutdown.rs | 35 ++++++++++++++++++ 9 files changed, 106 insertions(+), 10 deletions(-) delete mode 100644 peach-web/src/routes/power/mod.rs rename peach-web/src/routes/{ => settings}/power/menu.rs (56%) create mode 100644 peach-web/src/routes/settings/power/mod.rs create mode 100644 peach-web/src/routes/settings/power/reboot.rs create mode 100644 peach-web/src/routes/settings/power/shutdown.rs diff --git a/peach-web/src/private_router.rs b/peach-web/src/private_router.rs index b858e31..944cccd 100644 --- a/peach-web/src/private_router.rs +++ b/peach-web/src/private_router.rs @@ -49,10 +49,6 @@ pub fn mount_peachpub_routes( Response::html(routes::guide::build_template()) }, - (GET) (/power) => { - Response::html(routes::power::menu::build_template()) - }, - (POST) (/scuttlebutt/block) => { routes::scuttlebutt::block::handle_form(request) }, @@ -170,6 +166,18 @@ pub fn mount_peachpub_routes( routes::settings::admin::delete::handle_form(request) }, + (GET) (/settings/power) => { + Response::html(routes::settings::power::menu::build_template(request)) + }, + + (GET) (/settings/power/reboot) => { + routes::settings::power::reboot::handle_reboot() + }, + + (GET) (/settings/power/shutdown) => { + routes::settings::power::shutdown::handle_shutdown() + }, + (GET) (/settings/scuttlebutt) => { Response::html(routes::settings::scuttlebutt::menu::build_template(request)) .reset_flash() diff --git a/peach-web/src/routes/mod.rs b/peach-web/src/routes/mod.rs index 43f5527..fdc9575 100644 --- a/peach-web/src/routes/mod.rs +++ b/peach-web/src/routes/mod.rs @@ -3,7 +3,6 @@ pub mod authentication; //pub mod index; pub mod guide; pub mod home; -pub mod power; pub mod scuttlebutt; pub mod settings; pub mod status; diff --git a/peach-web/src/routes/power/mod.rs b/peach-web/src/routes/power/mod.rs deleted file mode 100644 index b9a0e3e..0000000 --- a/peach-web/src/routes/power/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod menu; diff --git a/peach-web/src/routes/settings/menu.rs b/peach-web/src/routes/settings/menu.rs index ad122b5..919601f 100644 --- a/peach-web/src/routes/settings/menu.rs +++ b/peach-web/src/routes/settings/menu.rs @@ -11,8 +11,10 @@ pub fn build_template() -> PreEscaped { div class="card center" { (PreEscaped("")) div id="settingsButtons" { - // render the network settings button if we're not in standalone mode + // render the network settings and power menu buttons if we're + // not in standalone mode @if !SERVER_CONFIG.standalone_mode { + a id="power" class="button button-primary center" href="/settings/power" title="Power Menu" { "Power" } a id="network" class="button button-primary center" href="/settings/network" title="Network Settings" { "Network" } } a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" } diff --git a/peach-web/src/routes/settings/mod.rs b/peach-web/src/routes/settings/mod.rs index b864d03..7f4c31c 100644 --- a/peach-web/src/routes/settings/mod.rs +++ b/peach-web/src/routes/settings/mod.rs @@ -2,5 +2,6 @@ pub mod admin; //pub mod dns; pub mod menu; pub mod network; +pub mod power; pub mod scuttlebutt; pub mod theme; diff --git a/peach-web/src/routes/power/menu.rs b/peach-web/src/routes/settings/power/menu.rs similarity index 56% rename from peach-web/src/routes/power/menu.rs rename to peach-web/src/routes/settings/power/menu.rs index 8d88d9d..42dc28b 100644 --- a/peach-web/src/routes/power/menu.rs +++ b/peach-web/src/routes/settings/power/menu.rs @@ -1,8 +1,17 @@ use maud::{html, PreEscaped}; +use rouille::Request; -use crate::{templates, utils::theme}; +use crate::{ + templates, + utils::{flash::FlashRequest, theme}, +}; + +/// Power menu template builder. +/// +/// Presents options for rebooting or shutting down the device. +pub fn build_template(request: &Request) -> PreEscaped { + let (flash_name, flash_msg) = request.retrieve_flash(); -pub fn build_template() -> PreEscaped { let power_menu_template = html! { (PreEscaped("")) div class="card center" { @@ -10,7 +19,11 @@ pub fn build_template() -> PreEscaped { div id="buttons" { a id="rebootBtn" class="button button-primary center" href="/reboot" title="Reboot Device" { "Reboot" } a id="shutdownBtn" class="button button-warning center" href="/shutdown" title="Shutdown Device" { "Shutdown" } - a id="cancelBtn" class="button button-secondary center" href="/" title="Cancel" { "Cancel" } + a id="cancelBtn" class="button button-secondary center" href="/settings" title="Cancel" { "Cancel" } + } + @if let (Some(name), Some(msg)) = (flash_name, flash_msg) { + (PreEscaped("")) + (templates::flash::build_template(name, msg)) } } } diff --git a/peach-web/src/routes/settings/power/mod.rs b/peach-web/src/routes/settings/power/mod.rs new file mode 100644 index 0000000..380e31c --- /dev/null +++ b/peach-web/src/routes/settings/power/mod.rs @@ -0,0 +1,3 @@ +pub mod menu; +pub mod reboot; +pub mod shutdown; diff --git a/peach-web/src/routes/settings/power/reboot.rs b/peach-web/src/routes/settings/power/reboot.rs new file mode 100644 index 0000000..5c1ba7b --- /dev/null +++ b/peach-web/src/routes/settings/power/reboot.rs @@ -0,0 +1,36 @@ +use log::info; +use rouille::Response; + +use std::{ + io::Result, + process::{Command, Output}, +}; + +use crate::utils::flash::FlashResponse; + +/// Executes a system command to reboot the device immediately. +fn reboot() -> Result { + info!("Rebooting the device"); + // ideally, we'd like to reboot after 5 seconds to allow time for JSON + // response but this is not possible with the `shutdown` command alone. + // TODO: send "rebooting..." message to `peach-oled` for display + Command::new("sudo") + .arg("shutdown") + .arg("-r") + .arg("now") + .output() +} + +pub fn handle_reboot() -> Response { + let (name, msg) = match reboot() { + Ok(_) => ("success".to_string(), "Rebooting the device".to_string()), + Err(err) => ( + "error".to_string(), + format!("Failed to reboot the device: {}", err), + ), + }; + + let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg)); + + Response::redirect_303("/power").add_flash(flash_name, flash_msg) +} diff --git a/peach-web/src/routes/settings/power/shutdown.rs b/peach-web/src/routes/settings/power/shutdown.rs new file mode 100644 index 0000000..ae1dfa3 --- /dev/null +++ b/peach-web/src/routes/settings/power/shutdown.rs @@ -0,0 +1,35 @@ +use log::info; +use rouille::Response; + +use std::{ + io::Result, + process::{Command, Output}, +}; + +use crate::utils::flash::FlashResponse; + +/// Executes a system command to shutdown the device immediately. +fn shutdown() -> Result { + info!("Shutting down the device"); + // ideally, we'd like to shutdown after 5 seconds to allow time for JSON + // response but this is not possible with the `shutdown` command alone. + // TODO: send "shutting down..." message to `peach-oled` for display + Command::new("sudo").arg("shutdown").arg("now").output() +} + +pub fn handle_shutdown() -> Response { + let (name, msg) = match shutdown() { + Ok(_) => ( + "success".to_string(), + "Shutting down the device".to_string(), + ), + Err(err) => ( + "error".to_string(), + format!("Failed to shutdown the device: {}", err), + ), + }; + + let (flash_name, flash_msg) = (format!("flash_name={}", name), format!("flash_msg={}", msg)); + + Response::redirect_303("/power").add_flash(flash_name, flash_msg) +} From b8ff9443777116152233f5304ff4e6fa086315a4 Mon Sep 17 00:00:00 2001 From: glyph Date: Mon, 28 Nov 2022 09:11:21 +0200 Subject: [PATCH 11/13] conditionally render status url based on run-mode --- peach-web/src/routes/home.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/peach-web/src/routes/home.rs b/peach-web/src/routes/home.rs index 4b085be..b70cd97 100644 --- a/peach-web/src/routes/home.rs +++ b/peach-web/src/routes/home.rs @@ -1,7 +1,7 @@ use maud::{html, PreEscaped}; use peach_lib::sbot::SbotStatus; -use crate::{templates, utils::theme}; +use crate::{templates, utils::theme, SERVER_CONFIG}; /// Read the state of the go-sbot process and define status-related /// elements accordingly. @@ -24,9 +24,23 @@ fn render_status_elements<'a>() -> (&'a str, &'a str, &'a str) { } } +/// Render the URL for the status element (icon / link). +/// +/// If the application is running in standalone mode then the element links +/// directly to the Scuttlebutt status page. If not, it links to the device +/// status page. +fn render_status_url<'a>() -> &'a str { + if SERVER_CONFIG.standalone_mode { + "/status/scuttlebutt" + } else { + "/status" + } +} + /// Home template builder. pub fn build_template() -> PreEscaped { let (circle_color, center_circle_text, circle_border) = render_status_elements(); + let status_url = render_status_url(); // render the home template html let home_template = html! { @@ -63,7 +77,7 @@ pub fn build_template() -> PreEscaped { } (PreEscaped("")) (PreEscaped("")) - a class="bottom-left" href="/status/scuttlebutt" title="Status" { + a class="bottom-left" href=(status_url) title="Status" { div class={ "circle circle-small border-circle-small " (circle_border) } { img class="icon-medium" src="/icons/heart-pulse.svg"; } From ebc7b9d41740f2efbb2bd5ef278636c9432d5dde Mon Sep 17 00:00:00 2001 From: glyph Date: Mon, 28 Nov 2022 09:12:01 +0200 Subject: [PATCH 12/13] remove old context code and refine status parsing --- peach-web/src/routes/status/device.rs | 394 +------------------------- 1 file changed, 4 insertions(+), 390 deletions(-) diff --git a/peach-web/src/routes/status/device.rs b/peach-web/src/routes/status/device.rs index 5e57b0d..ee750b3 100644 --- a/peach-web/src/routes/status/device.rs +++ b/peach-web/src/routes/status/device.rs @@ -27,8 +27,7 @@ fn retrieve_networking_state() -> Option { service_info .lines() .find(|line| line.starts_with("ActiveState=")) - .map(|line| line.strip_prefix("ActiveState=")) - .flatten() + .and_then(|line| line.strip_prefix("ActiveState=")) .map(|state| state.to_string()) } @@ -36,7 +35,7 @@ fn retrieve_networking_state() -> Option { fn retrieve_sbot_state() -> Option { // retrieve the name of the go-sbot service or set default let go_sbot_service = config_manager::get_config_value("GO_SBOT_SERVICE") - .unwrap_or("go-sbot.service".to_string()); + .unwrap_or_else(|_| "go-sbot.service".to_string()); let sbot_service_output = Command::new("systemctl") .arg("show") @@ -51,8 +50,7 @@ fn retrieve_sbot_state() -> Option { service_info .lines() .find(|line| line.starts_with("ActiveState=")) - .map(|line| line.strip_prefix("ActiveState=")) - .flatten() + .and_then(|line| line.strip_prefix("ActiveState=")) .map(|state| state.to_string()) } @@ -67,7 +65,7 @@ fn retrieve_device_status_data() -> (Option, String) { (uptime, oled_ping) } -fn retrieve_device_usage_data<'a>() -> (Option, Option) { +fn retrieve_device_usage_data() -> (Option, Option) { // convert result to Option, discard any error let cpu_stat_percent = stats::cpu_stats_percent().ok(); let mem_stats = stats::mem_stats().ok(); @@ -381,387 +379,3 @@ pub fn build_template() -> PreEscaped { templates::base::build_template(body, theme) } - -/* - -{%- extends "nav" -%} -{%- block card -%} - {# ASSIGN VARIABLES #} - {# ---------------- #} - {%- if mem_stats -%} - {% set mem_usage_percent = mem_stats.used / mem_stats.total * 100 | round -%} - {% set mem_used = mem_stats.used / 1024 | round -%} - {% set mem_free = mem_stats.free / 1024 | round -%} - {% set mem_total = mem_stats.total / 1024 | round -%} - {% endif -%} - {% if cpu_stat_percent -%} - {% set cpu_usage_percent = cpu_stat_percent.nice + cpu_stat_percent.system + cpu_stat_percent.user | round -%} - {%- endif -%} - {%- if disk_stats -%} - {%- for disk in disk_stats -%} - {%- set_global disk_usage_percent = disk.used_percentage -%} - {# Calculate free disk space in megabytes #} - {%- set_global disk_free = disk.one_k_blocks_free / 1024 | round -%} - {%- endfor -%} - {%- endif -%} - -
-
- {# Display microservice status for network, oled & stats #} -
- -
- Network -
- - -
-
- -
- Display -
- - -
-
- -
- Stats -
- - -
-
-
-
- -
- Dyndns -
- - -
-
- -
- Config -
- - -
-
- -
- Sbot -
- - -
-
-
- {# Display CPU usage meter #} - {%- if cpu_stat_percent -%} -
- CPU - {{ cpu_usage_percent }}% -
- -
- CPU Usage -
-
- {%- else -%} -

CPU usage data unavailable

- {% endif -%} - {# Display memory usage meter #} - {%- if mem_stats %} -
- Memory - {{ mem_usage_percent }}% ({{ mem_free }} MB free) -
- -
- Memory Usage -
-
- {%- else -%} -

Memory usage data unavailable

- {% endif -%} - {# Display disk usage meter #} - {%- if disk_stats %} -
- Disk - {{ disk_usage_percent }}% ({% if disk_free > 1024 %}{{ disk_free / 1024 | round }} GB{% else %}{{ disk_free }} MB{% endif %} free) -
- -
- Disk Usage -
-
- {%- else -%} -

Disk usage data unavailable

- {%- endif %} - {# Display system uptime in minutes #} - {%- if uptime and uptime < 60 %} -

Uptime: {{ uptime }} minutes

- {# Display system uptime in hours & minutes #} - {%- elif uptime and uptime > 60 -%} -

Uptime: {{ uptime / 60 | round(method="floor") }} hours, {{ uptime % 60 }} minutes

- {%- else -%} -

Uptime data unavailable

- {%- endif %} - - - {%- if flash_msg and flash_name == "success" -%} - -
{{ flash_msg }}.
- {%- elif flash_msg and flash_name == "error" -%} - -
{{ flash_msg }}.
- {%- endif %} -
-
-{%- endblock card %} - -// CONTEXT - -use log::info; -use rocket::{ - get, - request::FlashMessage, - response::{Flash, Redirect}, -}; -use rocket_dyn_templates::Template; -use serde::Serialize; -use std::{ - io, - process::{Command, Output}, -}; - -use peach_lib::{ - config_manager::load_peach_config, dyndns_client, network_client, oled_client, sbot::SbotStatus, -}; -use peach_stats::{ - stats, - stats::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat}, -}; - -use crate::routes::authentication::Authenticated; - -// HELPERS AND ROUTES FOR /status - -/// System statistics data. -#[derive(Debug, Serialize)] -pub struct StatusContext { - pub back: Option, - pub cpu_stat_percent: Option, - pub disk_stats: Vec, - pub flash_name: Option, - pub flash_msg: Option, - pub load_average: Option, - pub mem_stats: Option, - pub network_ping: String, - pub oled_ping: String, - pub dyndns_enabled: bool, - pub dyndns_is_online: bool, - pub config_is_valid: bool, - pub sbot_is_online: bool, - pub title: Option, - pub uptime: Option, -} - -impl StatusContext { - pub fn build() -> StatusContext { - // convert result to Option, discard any error - let cpu_stat_percent = stats::cpu_stats_percent().ok(); - let load_average = stats::load_average().ok(); - let mem_stats = stats::mem_stats().ok(); - // TODO: add `wpa_supplicant_status` to peach_network to replace this ping call - // instead of: "is the network json-rpc server running?", we want to ask: - // "is the wpa_supplicant systemd service functioning correctly?" - let network_ping = match network_client::ping() { - Ok(_) => "ONLINE".to_string(), - Err(_) => "OFFLINE".to_string(), - }; - let oled_ping = match oled_client::ping() { - Ok(_) => "ONLINE".to_string(), - Err(_) => "OFFLINE".to_string(), - }; - - let uptime = match stats::uptime() { - Ok(secs) => { - let uptime_mins = secs / 60; - uptime_mins.to_string() - } - Err(_) => "Unavailable".to_string(), - }; - - // parse the uptime string to a signed integer (for math) - let uptime_parsed = uptime.parse::().ok(); - - // serialize disk usage data into Vec - let disk_usage_stats = match stats::disk_usage() { - Ok(disks) => disks, - Err(_) => Vec::new(), - }; - - let mut disk_stats = Vec::new(); - // select only the partition we're interested in: /dev/mmcblk0p2 ("/") - for disk in disk_usage_stats { - if disk.mountpoint == "/" { - disk_stats.push(disk); - } - } - - // dyndns_is_online & config_is_valid - let dyndns_enabled: bool; - let dyndns_is_online: bool; - let config_is_valid: bool; - let load_peach_config_result = load_peach_config(); - match load_peach_config_result { - Ok(peach_config) => { - dyndns_enabled = peach_config.dyn_enabled; - config_is_valid = true; - if dyndns_enabled { - let is_dyndns_online_result = dyndns_client::is_dns_updater_online(); - match is_dyndns_online_result { - Ok(is_online) => { - dyndns_is_online = is_online; - } - Err(_err) => { - dyndns_is_online = false; - } - } - } else { - dyndns_is_online = false; - } - } - Err(_err) => { - dyndns_enabled = false; - dyndns_is_online = false; - config_is_valid = false; - } - } - - // test if go-sbot is running - let sbot_status = SbotStatus::read(); - let sbot_is_online: bool = match sbot_status { - // return true if state is active - Ok(status) => matches!(status.state == Some("active".to_string()), true), - _ => false, - }; - - StatusContext { - back: None, - cpu_stat_percent, - disk_stats, - flash_name: None, - flash_msg: None, - load_average, - mem_stats, - network_ping, - oled_ping, - dyndns_enabled, - dyndns_is_online, - config_is_valid, - sbot_is_online, - title: None, - uptime: uptime_parsed, - } - } -} - -#[get("/")] -pub fn device_status(flash: Option, _auth: Authenticated) -> Template { - // assign context through context_builder call - let mut context = StatusContext::build(); - context.back = Some("/".to_string()); - context.title = Some("Device Status".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.kind().to_string()); - context.flash_msg = Some(flash.message().to_string()); - }; - // template_dir is set in Rocket.toml - Template::render("status/device", &context) -} - -// HELPERS AND ROUTES FOR /power/reboot - -/// Executes a system command to reboot the device immediately. -pub fn reboot() -> io::Result { - info!("Rebooting the device"); - // ideally, we'd like to reboot after 5 seconds to allow time for JSON - // response but this is not possible with the `shutdown` command alone. - // TODO: send "rebooting..." message to `peach-oled` for display - Command::new("sudo") - .arg("shutdown") - .arg("-r") - .arg("now") - .output() -} - -#[get("/power/reboot")] -pub fn reboot_cmd(_auth: Authenticated) -> Flash { - match reboot() { - Ok(_) => Flash::success(Redirect::to("/power"), "Rebooting the device"), - Err(_) => Flash::error(Redirect::to("/power"), "Failed to reboot the device"), - } -} - -// HELPERS AND ROUTES FOR /power/shutdown - -/// Executes a system command to shutdown the device immediately. -pub fn shutdown() -> io::Result { - info!("Shutting down the device"); - // ideally, we'd like to reboot after 5 seconds to allow time for JSON - // response but this is not possible with the `shutdown` command alone. - // TODO: send "shutting down..." message to `peach-oled` for display - Command::new("sudo").arg("shutdown").arg("now").output() -} - -#[get("/power/shutdown")] -pub fn shutdown_cmd(_auth: Authenticated) -> Flash { - match shutdown() { - Ok(_) => Flash::success(Redirect::to("/power"), "Shutting down the device"), - Err(_) => Flash::error(Redirect::to("/power"), "Failed to shutdown the device"), - } -} - -// HELPERS AND ROUTES FOR /power - -#[derive(Debug, Serialize)] -pub struct PowerContext { - pub back: Option, - pub flash_name: Option, - pub flash_msg: Option, - pub title: Option, -} - -impl PowerContext { - pub fn build() -> PowerContext { - PowerContext { - back: None, - flash_name: None, - flash_msg: None, - title: None, - } - } -} - -#[get("/power")] -pub fn power_menu(flash: Option, _auth: Authenticated) -> Template { - let mut context = PowerContext::build(); - context.back = Some("/".to_string()); - context.title = Some("Power Menu".to_string()); - // check to see if there is a flash message to display - if let Some(flash) = flash { - // add flash message contents to the context object - context.flash_name = Some(flash.kind().to_string()); - context.flash_msg = Some(flash.message().to_string()); - }; - Template::render("power", &context) -} -*/ From 6028e07bde9b0fcf1e152fc4b68f08c1d90fcbc5 Mon Sep 17 00:00:00 2001 From: glyph Date: Mon, 28 Nov 2022 09:12:25 +0200 Subject: [PATCH 13/13] single variable name change for clarity --- peach-web/src/templates/nav.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/peach-web/src/templates/nav.rs b/peach-web/src/templates/nav.rs index 0919fcd..a2e19f6 100644 --- a/peach-web/src/templates/nav.rs +++ b/peach-web/src/templates/nav.rs @@ -14,7 +14,7 @@ pub fn build_template( let theme = theme::get_theme(); // conditionally render the hermies icon and theme-switcher icon with correct link - let (hermies, switcher) = match theme.as_str() { + let (hermies, theme_switcher) = match theme.as_str() { // if we're using the dark theme, render light icons and "light" query param "dark" => ( "/icons/hermies_hex_light.svg", @@ -56,8 +56,7 @@ pub fn build_template( a class="nav-item" href="/" { img class="icon nav-icon-left" src="/icons/peach-icon.png" alt="PeachCloud" title="Home"; } - // render the pre-defined theme-switcher icon - (switcher) + (theme_switcher) } } }