From a1b16f8d3839c1d68015e2ee8e27fdfaf869330e Mon Sep 17 00:00:00 2001 From: glyph Date: Wed, 2 Nov 2022 15:15:06 +0000 Subject: [PATCH] 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;