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=")) .and_then(|line| line.strip_prefix("ActiveState=")) .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_else(|_| "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=")) .and_then(|line| line.strip_prefix("ActiveState=")) .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() -> (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) }