Merge pull request 'Reintroduce status and power-related templates and routes' (#140) from refactor_stats into main
Reviewed-on: #140
This commit is contained in:
commit
6cc8faa0c3
|
@ -2333,7 +2333,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "peach-web"
|
name = "peach-web"
|
||||||
version = "0.6.19"
|
version = "0.6.21"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
"base64 0.13.0",
|
"base64 0.13.0",
|
||||||
|
@ -2347,8 +2347,10 @@ dependencies = [
|
||||||
"maud",
|
"maud",
|
||||||
"peach-lib",
|
"peach-lib",
|
||||||
"peach-network",
|
"peach-network",
|
||||||
|
"peach-stats",
|
||||||
"rouille",
|
"rouille",
|
||||||
"temporary",
|
"temporary",
|
||||||
|
"vnstat_parse",
|
||||||
"xdg",
|
"xdg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -3814,6 +3816,12 @@ version = "0.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vnstat_parse"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75334377a2918b45b5b8da023080375a4ec0aa04b0bc88f896ea93cf4b32feff"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "void"
|
name = "void"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "peach-web"
|
name = "peach-web"
|
||||||
version = "0.6.20"
|
version = "0.6.21"
|
||||||
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
|
authors = ["Andrew Reid <gnomad@cryptolab.net>", "Max Fowler <max@mfowler.info>"]
|
||||||
edition = "2018"
|
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."
|
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."
|
||||||
|
@ -44,11 +44,9 @@ lazy_static = "1.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
maud = "0.23"
|
maud = "0.23"
|
||||||
peach-lib = { path = "../peach-lib" }
|
peach-lib = { path = "../peach-lib" }
|
||||||
# these will be reintroduced when the full peachcloud mode is added
|
|
||||||
peach-network = { path = "../peach-network" }
|
peach-network = { path = "../peach-network" }
|
||||||
#peach-stats = { path = "../peach-stats" }
|
peach-stats = { path = "../peach-stats" }
|
||||||
rouille = { version = "3.5", default-features = false }
|
rouille = { version = "3.5", default-features = false }
|
||||||
temporary = "0.6"
|
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"
|
xdg = "2.2"
|
||||||
|
|
|
@ -166,6 +166,18 @@ pub fn mount_peachpub_routes(
|
||||||
routes::settings::admin::delete::handle_form(request)
|
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) => {
|
(GET) (/settings/scuttlebutt) => {
|
||||||
Response::html(routes::settings::scuttlebutt::menu::build_template(request))
|
Response::html(routes::settings::scuttlebutt::menu::build_template(request))
|
||||||
.reset_flash()
|
.reset_flash()
|
||||||
|
@ -248,10 +260,18 @@ pub fn mount_peachpub_routes(
|
||||||
routes::settings::theme::set_theme(theme)
|
routes::settings::theme::set_theme(theme)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
(GET) (/status) => {
|
||||||
|
Response::html(routes::status::device::build_template())
|
||||||
|
},
|
||||||
|
|
||||||
(GET) (/status/scuttlebutt) => {
|
(GET) (/status/scuttlebutt) => {
|
||||||
Response::html(routes::status::scuttlebutt::build_template()).add_cookie("back_url=/status/scuttlebutt")
|
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
|
// render the not_found template and set a 404 status code if none of
|
||||||
// the other blocks matches the request
|
// the other blocks matches the request
|
||||||
_ => Response::html(templates::not_found::build_template()).with_status_code(404)
|
_ => Response::html(templates::not_found::build_template()).with_status_code(404)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use maud::{html, PreEscaped};
|
use maud::{html, PreEscaped};
|
||||||
use peach_lib::sbot::SbotStatus;
|
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
|
/// Read the state of the go-sbot process and define status-related
|
||||||
/// elements accordingly.
|
/// 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.
|
/// Home template builder.
|
||||||
pub fn build_template() -> PreEscaped<String> {
|
pub fn build_template() -> PreEscaped<String> {
|
||||||
let (circle_color, center_circle_text, circle_border) = render_status_elements();
|
let (circle_color, center_circle_text, circle_border) = render_status_elements();
|
||||||
|
let status_url = render_status_url();
|
||||||
|
|
||||||
// render the home template html
|
// render the home template html
|
||||||
let home_template = html! {
|
let home_template = html! {
|
||||||
|
@ -63,7 +77,7 @@ pub fn build_template() -> PreEscaped<String> {
|
||||||
}
|
}
|
||||||
(PreEscaped("<!-- bottom-left -->"))
|
(PreEscaped("<!-- bottom-left -->"))
|
||||||
(PreEscaped("<!-- SYSTEM STATUS LINK AND ICON -->"))
|
(PreEscaped("<!-- SYSTEM STATUS LINK AND ICON -->"))
|
||||||
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) } {
|
div class={ "circle circle-small border-circle-small " (circle_border) } {
|
||||||
img class="icon-medium" src="/icons/heart-pulse.svg";
|
img class="icon-medium" src="/icons/heart-pulse.svg";
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,8 +11,10 @@ pub fn build_template() -> PreEscaped<String> {
|
||||||
div class="card center" {
|
div class="card center" {
|
||||||
(PreEscaped("<!-- BUTTONS -->"))
|
(PreEscaped("<!-- BUTTONS -->"))
|
||||||
div id="settingsButtons" {
|
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 {
|
@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="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" }
|
a id="scuttlebutt" class="button button-primary center" href="/settings/scuttlebutt" title="Scuttlebutt Settings" { "Scuttlebutt" }
|
||||||
|
|
|
@ -2,5 +2,6 @@ pub mod admin;
|
||||||
//pub mod dns;
|
//pub mod dns;
|
||||||
pub mod menu;
|
pub mod menu;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
pub mod power;
|
||||||
pub mod scuttlebutt;
|
pub mod scuttlebutt;
|
||||||
pub mod theme;
|
pub mod theme;
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
use maud::{html, PreEscaped};
|
||||||
|
use rouille::Request;
|
||||||
|
|
||||||
|
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<String> {
|
||||||
|
let (flash_name, flash_msg) = request.retrieve_flash();
|
||||||
|
|
||||||
|
let power_menu_template = html! {
|
||||||
|
(PreEscaped("<!-- POWER MENU -->"))
|
||||||
|
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="/settings" title="Cancel" { "Cancel" }
|
||||||
|
}
|
||||||
|
@if let (Some(name), Some(msg)) = (flash_name, flash_msg) {
|
||||||
|
(PreEscaped("<!-- FLASH MESSAGE -->"))
|
||||||
|
(templates::flash::build_template(name, msg))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let body = templates::nav::build_template(power_menu_template, "Power Menu", Some("/"));
|
||||||
|
|
||||||
|
let theme = theme::get_theme();
|
||||||
|
|
||||||
|
templates::base::build_template(body, theme)
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod menu;
|
||||||
|
pub mod reboot;
|
||||||
|
pub mod shutdown;
|
|
@ -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<Output> {
|
||||||
|
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)
|
||||||
|
}
|
|
@ -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<Output> {
|
||||||
|
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)
|
||||||
|
}
|
|
@ -1,238 +1,381 @@
|
||||||
use log::info;
|
use std::process::Command;
|
||||||
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::{
|
use maud::{html, Markup, PreEscaped};
|
||||||
config_manager::load_peach_config, dyndns_client, network_client, oled_client, sbot::SbotStatus,
|
use peach_lib::{config_manager, dyndns_client, oled_client};
|
||||||
};
|
|
||||||
use peach_stats::{
|
use peach_stats::{
|
||||||
stats,
|
stats,
|
||||||
stats::{CpuStatPercentages, DiskUsage, LoadAverage, MemStat},
|
stats::{CpuStatPercentages, MemStat},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::routes::authentication::Authenticated;
|
use crate::{templates, utils::theme};
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR /status
|
// ROUTE: /status
|
||||||
|
|
||||||
/// System statistics data.
|
/// Query systemd to determine the state of the networking service.
|
||||||
#[derive(Debug, Serialize)]
|
fn retrieve_networking_state() -> Option<String> {
|
||||||
pub struct StatusContext {
|
// call: `systemctl show networking.service --no-page`
|
||||||
pub back: Option<String>,
|
let networking_service_output = Command::new("systemctl")
|
||||||
pub cpu_stat_percent: Option<CpuStatPercentages>,
|
.arg("show")
|
||||||
pub disk_stats: Vec<DiskUsage>,
|
.arg("networking.service")
|
||||||
pub flash_name: Option<String>,
|
.arg("--no-page")
|
||||||
pub flash_msg: Option<String>,
|
.output()
|
||||||
pub load_average: Option<LoadAverage>,
|
.ok()?;
|
||||||
pub mem_stats: Option<MemStat>,
|
|
||||||
pub network_ping: String,
|
let service_info = std::str::from_utf8(&networking_service_output.stdout).ok()?;
|
||||||
pub oled_ping: String,
|
|
||||||
pub dyndns_enabled: bool,
|
// find the line starting with "ActiveState=" and return the value
|
||||||
pub dyndns_is_online: bool,
|
service_info
|
||||||
pub config_is_valid: bool,
|
.lines()
|
||||||
pub sbot_is_online: bool,
|
.find(|line| line.starts_with("ActiveState="))
|
||||||
pub title: Option<String>,
|
.and_then(|line| line.strip_prefix("ActiveState="))
|
||||||
pub uptime: Option<i32>,
|
.map(|state| state.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatusContext {
|
/// Query systemd to determine the state of the sbot service.
|
||||||
pub fn build() -> StatusContext {
|
fn retrieve_sbot_state() -> Option<String> {
|
||||||
// convert result to Option<CpuStatPercentages>, discard any error
|
// retrieve the name of the go-sbot service or set default
|
||||||
let cpu_stat_percent = stats::cpu_stats_percent().ok();
|
let go_sbot_service = config_manager::get_config_value("GO_SBOT_SERVICE")
|
||||||
let load_average = stats::load_average().ok();
|
.unwrap_or_else(|_| "go-sbot.service".to_string());
|
||||||
let mem_stats = stats::mem_stats().ok();
|
|
||||||
// TODO: add `wpa_supplicant_status` to peach_network to replace this ping call
|
let sbot_service_output = Command::new("systemctl")
|
||||||
// instead of: "is the network json-rpc server running?", we want to ask:
|
.arg("show")
|
||||||
// "is the wpa_supplicant systemd service functioning correctly?"
|
.arg(go_sbot_service)
|
||||||
let network_ping = match network_client::ping() {
|
.arg("--no-page")
|
||||||
Ok(_) => "ONLINE".to_string(),
|
.output()
|
||||||
Err(_) => "OFFLINE".to_string(),
|
.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<u64>, String) {
|
||||||
|
let uptime = stats::uptime().ok();
|
||||||
|
|
||||||
let oled_ping = match oled_client::ping() {
|
let oled_ping = match oled_client::ping() {
|
||||||
Ok(_) => "ONLINE".to_string(),
|
Ok(_) => "ONLINE".to_string(),
|
||||||
Err(_) => "OFFLINE".to_string(),
|
Err(_) => "OFFLINE".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let uptime = match stats::uptime() {
|
(uptime, oled_ping)
|
||||||
Ok(secs) => {
|
}
|
||||||
let uptime_mins = secs / 60;
|
|
||||||
uptime_mins.to_string()
|
fn retrieve_device_usage_data() -> (Option<CpuStatPercentages>, Option<MemStat>) {
|
||||||
|
// convert result to Option<CpuStatPercentages>, 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")
|
||||||
}
|
}
|
||||||
Err(_) => "Unavailable".to_string(),
|
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",
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
// parse the uptime string to a signed integer (for math)
|
html! {
|
||||||
let uptime_parsed = uptime.parse::<i32>().ok();
|
(PreEscaped("<!-- PEACH-NETWORK STATUS STACK -->"))
|
||||||
|
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()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// serialize disk usage data into Vec<DiskUsage>
|
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("<!-- PEACH-OLED STATUS STACK -->"))
|
||||||
|
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("<!-- DIAGNOSTICS AND LOGS STACK -->"))
|
||||||
|
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("<!-- DYNDNS STATUS STACK -->"))
|
||||||
|
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("<!-- CONFIG STATUS STACK -->"))
|
||||||
|
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("<!-- SBOT STATUS STACK -->"))
|
||||||
|
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<CpuStatPercentages>) -> 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<MemStat>) -> 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() {
|
let disk_usage_stats = match stats::disk_usage() {
|
||||||
Ok(disks) => disks,
|
Ok(disks) => disks,
|
||||||
Err(_) => Vec::new(),
|
Err(_) => Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut disk_stats = Vec::new();
|
|
||||||
// select only the partition we're interested in: /dev/mmcblk0p2 ("/")
|
// select only the partition we're interested in: /dev/mmcblk0p2 ("/")
|
||||||
for disk in disk_usage_stats {
|
let disk_usage = disk_usage_stats.iter().find(|disk| disk.mountpoint == "/");
|
||||||
if disk.mountpoint == "/" {
|
|
||||||
disk_stats.push(disk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dyndns_is_online & config_is_valid
|
html! {
|
||||||
let dyndns_enabled: bool;
|
@if let Some(disk) = disk_usage {
|
||||||
let dyndns_is_online: bool;
|
// calculate free disk space in megabytes
|
||||||
let config_is_valid: bool;
|
@let disk_free_mb = disk.one_k_blocks_free / 1024;
|
||||||
let load_peach_config_result = load_peach_config();
|
// calculate free disk space in gigabytes
|
||||||
match load_peach_config_result {
|
@let disk_free_gb = disk_free_mb / 1024;
|
||||||
Ok(peach_config) => {
|
// render disk free value as megabytes or gigabytes based on size
|
||||||
dyndns_enabled = peach_config.dyn_enabled;
|
@let disk_free_value = if disk_free_mb > 1024 {
|
||||||
config_is_valid = true;
|
format!("{} GB", disk_free_gb)
|
||||||
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 {
|
} else {
|
||||||
dyndns_is_online = false;
|
format!("{} MB", disk_free_mb)
|
||||||
}
|
|
||||||
}
|
|
||||||
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 {
|
div class="flex-grid" {
|
||||||
back: None,
|
span class="card-text" { "Disk" }
|
||||||
cpu_stat_percent,
|
span class="label-small push-right" { (disk.used_percentage) "% (" (disk_free_value) " free)" }
|
||||||
disk_stats,
|
}
|
||||||
flash_name: None,
|
meter value=(disk.used_percentage) min="0" max="100" title="Disk usage" {
|
||||||
flash_msg: None,
|
div class="meter-gauge" {
|
||||||
load_average,
|
span style={ "width: " (disk.used_percentage) "%;" } {
|
||||||
mem_stats,
|
"Disk Usage"
|
||||||
network_ping,
|
}
|
||||||
oled_ping,
|
}
|
||||||
dyndns_enabled,
|
}
|
||||||
dyndns_is_online,
|
} @else {
|
||||||
config_is_valid,
|
p class="card-text" { "Disk usage data unavailable" }
|
||||||
sbot_is_online,
|
|
||||||
title: None,
|
|
||||||
uptime: uptime_parsed,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
/// Display system uptime in hours and minutes.
|
||||||
pub fn device_status(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
fn render_uptime_capsule(uptime: Option<u64>) -> Markup {
|
||||||
// assign context through context_builder call
|
html! {
|
||||||
let mut context = StatusContext::build();
|
@if let Some(uptime_secs) = uptime {
|
||||||
context.back = Some("/".to_string());
|
@let uptime_mins = uptime_secs / 60;
|
||||||
context.title = Some("Device Status".to_string());
|
@if uptime_mins < 60 {
|
||||||
// check to see if there is a flash message to display
|
// display system uptime in minutes
|
||||||
if let Some(flash) = flash {
|
p class="capsule center-text" {
|
||||||
// add flash message contents to the context object
|
"Uptime: " (uptime_mins) " minutes"
|
||||||
context.flash_name = Some(flash.kind().to_string());
|
}
|
||||||
context.flash_msg = Some(flash.message().to_string());
|
} @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<String> {
|
||||||
|
let (uptime, oled_state) = retrieve_device_status_data();
|
||||||
|
let (cpu_usage, mem_usage) = retrieve_device_usage_data();
|
||||||
|
|
||||||
|
let device_status_template = html! {
|
||||||
|
(PreEscaped("<!-- DEVICE STATUS CARD -->"))
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// template_dir is set in Rocket.toml
|
|
||||||
Template::render("status/device", &context)
|
let body = templates::nav::build_template(device_status_template, "Device Status", Some("/"));
|
||||||
}
|
|
||||||
|
let theme = theme::get_theme();
|
||||||
// HELPERS AND ROUTES FOR /power/reboot
|
|
||||||
|
templates::base::build_template(body, theme)
|
||||||
/// Executes a system command to reboot the device immediately.
|
|
||||||
pub fn reboot() -> io::Result<Output> {
|
|
||||||
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<Redirect> {
|
|
||||||
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<Output> {
|
|
||||||
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<Redirect> {
|
|
||||||
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<String>,
|
|
||||||
pub flash_name: Option<String>,
|
|
||||||
pub flash_msg: Option<String>,
|
|
||||||
pub title: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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<FlashMessage>, _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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
//pub mod device;
|
pub mod device;
|
||||||
//pub mod network;
|
pub mod network;
|
||||||
pub mod scuttlebutt;
|
pub mod scuttlebutt;
|
||||||
|
|
|
@ -1,21 +1,285 @@
|
||||||
use rocket::{get, request::FlashMessage};
|
use maud::{html, Markup, PreEscaped};
|
||||||
use rocket_dyn_templates::Template;
|
use peach_network::network;
|
||||||
|
use vnstat_parse::Vnstat;
|
||||||
|
|
||||||
use crate::context::network::NetworkStatusContext;
|
use crate::{templates, utils::theme, AP_IFACE, WLAN_IFACE};
|
||||||
use crate::routes::authentication::Authenticated;
|
|
||||||
|
|
||||||
// HELPERS AND ROUTES FOR /status/network
|
enum NetworkState {
|
||||||
|
AccessPoint,
|
||||||
|
WiFiClient,
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/network")]
|
// ROUTE: /status/network
|
||||||
pub fn network_status(flash: Option<FlashMessage>, _auth: Authenticated) -> Template {
|
|
||||||
let mut context = NetworkStatusContext::build();
|
|
||||||
context.back = Some("/status".to_string());
|
|
||||||
context.title = Some("Network Status".to_string());
|
|
||||||
|
|
||||||
if let Some(flash) = flash {
|
/// Render the cog icon which is used as a link to the network settings page.
|
||||||
context.flash_name = Some(flash.kind().to_string());
|
fn render_network_config_icon() -> Markup {
|
||||||
context.flash_msg = Some(flash.message().to_string());
|
html! {
|
||||||
|
(PreEscaped("<!-- top-right config icon -->"))
|
||||||
|
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("<!-- network mode icon with label -->"))
|
||||||
|
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("<!-- network mode, ssid & ip with labels -->"))
|
||||||
|
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("<!-- NETWORK STATUS GRID -->"))
|
||||||
|
div class="two-grid" title="PeachCloud network mode and status" {
|
||||||
|
(render_network_config_icon())
|
||||||
|
(PreEscaped("<!-- left column -->"))
|
||||||
|
(render_network_mode_icon(state))
|
||||||
|
(PreEscaped("<!-- right column -->"))
|
||||||
|
(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<Vnstat>) -> 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<Vnstat>) -> 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<Vnstat>) -> 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<Vnstat>, 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<String> {
|
||||||
|
let (state, traffic, ip, ssid) = retrieve_network_data();
|
||||||
|
|
||||||
|
let network_status_template = html! {
|
||||||
|
(PreEscaped("<!-- NETWORK STATUS CARD -->"))
|
||||||
|
div class="card center" {
|
||||||
|
(PreEscaped("<!-- NETWORK INFO BOX -->"))
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ pub fn build_template(
|
||||||
let theme = theme::get_theme();
|
let theme = theme::get_theme();
|
||||||
|
|
||||||
// conditionally render the hermies icon and theme-switcher icon with correct link
|
// 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
|
// if we're using the dark theme, render light icons and "light" query param
|
||||||
"dark" => (
|
"dark" => (
|
||||||
"/icons/hermies_hex_light.svg",
|
"/icons/hermies_hex_light.svg",
|
||||||
|
@ -56,8 +56,7 @@ pub fn build_template(
|
||||||
a class="nav-item" href="/" {
|
a class="nav-item" href="/" {
|
||||||
img class="icon nav-icon-left" src="/icons/peach-icon.png" alt="PeachCloud" title="Home";
|
img class="icon nav-icon-left" src="/icons/peach-icon.png" alt="PeachCloud" title="Home";
|
||||||
}
|
}
|
||||||
// render the pre-defined theme-switcher icon
|
(theme_switcher)
|
||||||
(switcher)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue