From d024280878f47b29c29c67560fcd61762f21feb0 Mon Sep 17 00:00:00 2001 From: Marco De Araujo Date: Mon, 22 Dec 2025 10:51:58 -0400 Subject: [PATCH] Using heartbeat api instead --- Cargo.lock | 6 +- Cargo.toml | 2 + formgen.txt | 124 ++++++++++++++++++++++++++++++++++++++++ src/heartbeat/mod.rs | 4 ++ src/heartbeat/model.rs | 103 +++++++++++++++++++++++++++++++++ src/heartbeat/parser.rs | 28 +++++++++ src/i18n/loader.rs | 14 +++++ src/i18n/mod.rs | 5 ++ src/i18n/translate.rs | 13 +++++ src/main.rs | 60 +++++++------------ 10 files changed, 319 insertions(+), 40 deletions(-) create mode 100644 formgen.txt create mode 100644 src/heartbeat/mod.rs create mode 100644 src/heartbeat/model.rs create mode 100644 src/heartbeat/parser.rs create mode 100644 src/i18n/loader.rs create mode 100644 src/i18n/mod.rs create mode 100644 src/i18n/translate.rs diff --git a/Cargo.lock b/Cargo.lock index c45276c..10934c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1205,9 +1205,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.146" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "217ca874ae0207aac254aa02c957ded05585a90892cc8d87f9e5fa49669dadd8" dependencies = [ "itoa", "memchr", @@ -1577,6 +1577,8 @@ dependencies = [ "clap", "fluent-templates", "reqwest", + "serde", + "serde_json", "sys-locale", "unic-langid", ] diff --git a/Cargo.toml b/Cargo.toml index 2e6c508..775aaa1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,5 @@ anyhow = "1.0" fluent-templates = "0.13.2" unic-langid = "0.9.6" sys-locale = "0.3.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.146" diff --git a/formgen.txt b/formgen.txt new file mode 100644 index 0000000..0f5a3ac --- /dev/null +++ b/formgen.txt @@ -0,0 +1,124 @@ +. +├── Cargo.toml +├── formgen.txt +├── .gitignore +└── src + ├── i18n + │   ├── loader.rs + │   ├── mod.rs + │   └── translate.rs + ├── locales + │   ├── en-US + │   │   └── main.ftl + │   └── pt-BR + │   └── main.ftl + └── main.rs + +6 directories, 9 files + + +=== CONTEÚDO DOS ARQUIVOS === + + +=== ./src/main.rs === + +use anyhow::{Ok, Result}; +use clap::Parser; +use reqwest::blocking::Client; +mod i18n; +use i18n::{init_locales, t}; + +#[derive(Debug, Parser)] +#[command(author, version, about)] +struct Args { + #[arg(short, long, env = "UPTIME_KUMA_URL")] + url: Option, + + #[arg(short, long, env = "UPTIME_KUMA_API_KEY")] + api_key: Option, +} + +fn main() -> Result<()> { + init_locales()?; + + let args = Args::parse(); + let url = args + .url + .as_deref() + .ok_or_else(|| anyhow::anyhow!("{}", t("missing_url")))?; + let api_key = args + .api_key + .as_deref() + .ok_or_else(|| anyhow::anyhow!("{}", t("missing_api_key")))?; + + let client = Client::new(); + + let response = client.get(url).basic_auth("", Some(api_key)).send()?; + + if response.status().is_success() { + let metrics = response.text()?; + println!("{}", t("success")); + println!("{}", t("metrics_preview")); + println!("{}", &metrics[..200.min(metrics.len())]); + } else { + println!("{}", response.status()); + println!("{}: {}", t("Response"), response.text()?); + } + Ok(()) +} + +=== ./src/i18n/translate.rs === + +use crate::i18n::loader::LOCALES; +use fluent_templates::Loader; +use std::str::FromStr; +use unic_langid::LanguageIdentifier; + +fn get_sys_locale() -> LanguageIdentifier { + let sys_lang = sys_locale::get_locale().unwrap_or_else(|| String::from("pt-BR")); + LanguageIdentifier::from_str(&sys_lang).expect("Invalid language") +} + +pub fn t(key: &str) -> String { + LOCALES.lookup(&get_sys_locale(), key) +} + +=== ./src/i18n/loader.rs === + +use anyhow::Ok; +use fluent_templates::static_loader; + +static_loader! { + pub static LOCALES = { + locales: "./src/locales", + fallback_language: "pt-BR", + customise: |bundle| bundle.set_use_isolating(false), + }; +} + +pub fn init_locales() -> anyhow::Result<()> { + Ok(()) +} + +=== ./src/i18n/mod.rs === + +pub mod loader; +pub mod translate; + +pub use loader::init_locales; +pub use translate::t; + +=== ./Cargo.toml === + +[package] +name = "uptime-kuma-dashboard" +version = "0.1.0" +edition = "2024" + +[dependencies] +clap = { version = "4.5", features = ["derive", "env"] } +reqwest = { version = "0.12", features = ["blocking", "json"] } +anyhow = "1.0" +fluent-templates = "0.13.2" +unic-langid = "0.9.6" +sys-locale = "0.3.2" diff --git a/src/heartbeat/mod.rs b/src/heartbeat/mod.rs new file mode 100644 index 0000000..80f15d7 --- /dev/null +++ b/src/heartbeat/mod.rs @@ -0,0 +1,4 @@ +pub mod parser; +pub mod model; +pub use parser::{format_duration, parse_response,status_to_string}; +pub use model::HeartbeatResponse; diff --git a/src/heartbeat/model.rs b/src/heartbeat/model.rs new file mode 100644 index 0000000..4b3a8e4 --- /dev/null +++ b/src/heartbeat/model.rs @@ -0,0 +1,103 @@ +use std::collections::HashMap; + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; + +use crate::i18n::t; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct HeartbeatEntry { + pub status: u8, + pub time: String, + pub msg: String, + #[serde(default)] + pub ping: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct MonitorHeartbeats { + pub monitor_id: u64, + pub heartbeats: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct UptimeData { + pub monitor_id: u64, + pub period_hours: u32, + pub uptime_percentage: f64, +} + +impl UptimeData { + pub fn get_perc_formated(&self) -> String { + format!("{:.2}%", self.uptime_percentage * 100.00) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct HeartbeatResponse { + #[serde(rename = "heartbeatList")] + pub heartbeat_list_raw: HashMap>, + + #[serde(rename = "uptimeList")] + pub uptime_list_raw: HashMap, + + #[serde(skip)] + pub monitors: Vec, + + #[serde(skip)] + pub uptime_data: Vec, +} + +impl HeartbeatResponse { + pub fn process(&mut self) -> Result<()> { + for (monitor_id_str, heartbeats) in &self.heartbeat_list_raw { + let monitor_id = monitor_id_str + .parse::() + .with_context(|| format!("{}", t("invalid-monitor-id")))?; + + self.monitors.push(MonitorHeartbeats { + monitor_id, + heartbeats: heartbeats.clone(), + }); + } + + for (key, percentage) in &self.uptime_list_raw { + let parts: Vec<&str> = key.split('_').collect(); + if parts.len() != 2 { + return Err(anyhow::anyhow!("Formato invalido (colocar o t) {}", key)); + } + + let monitor_id = parts[0] + .parse::() + .with_context(|| format!("{}", t("invalid-monitor-id")))?; + + let period_hours = parts[1] + .parse::() + .with_context(|| format!("{}", t("invalid-period-hours")))?; + + self.uptime_data.push(UptimeData { + monitor_id, + period_hours, + uptime_percentage: *percentage, + }); + } + + self.monitors.sort_by_key(|m| m.monitor_id); + self.uptime_data + .sort_by_key(|u| (u.monitor_id, u.period_hours)); + Ok(()) + } + + pub fn get_uptime(&self, monitor_id: u64, period_hours: u32) -> Option<&UptimeData> { + self.uptime_data + .iter() + .find(|u| u.monitor_id == monitor_id && u.period_hours == period_hours) + } + + pub fn get_latest_heartbeat(&self, monitor_id: u64) -> Option<&HeartbeatEntry> { + self.monitors + .iter() + .find(|m| m.monitor_id == monitor_id) + .and_then(|m| m.heartbeats.last()) + } +} diff --git a/src/heartbeat/parser.rs b/src/heartbeat/parser.rs new file mode 100644 index 0000000..6153c50 --- /dev/null +++ b/src/heartbeat/parser.rs @@ -0,0 +1,28 @@ +use anyhow::{Context, Ok, Result}; +use std::collections::HashMap; +use crate::i18n::t; +use super::HeartbeatResponse; + +pub fn parse_response(json_text: &str) -> Result { + let mut response: HeartbeatResponse = serde_json::from_str(json_text) + .with_context(|| t("invalid-json-heartbeat"))?; + + response.process()?; + Ok(response) +} + +pub fn status_to_string(status: u8) -> String { + match status { + 1 => t("up"), + 2 => t("down"), + _ => t("unknown"), + } +} + +pub fn format_duration(duration_ms:Option) -> String { + match duration_ms { + Some(ms) if ms < 1000 => format!("{}ms", ms), + Some(ms) => format!("{:.1}s", ms), + None => "N/A".to_string(), + } +} diff --git a/src/i18n/loader.rs b/src/i18n/loader.rs new file mode 100644 index 0000000..7c46b17 --- /dev/null +++ b/src/i18n/loader.rs @@ -0,0 +1,14 @@ +use anyhow::Ok; +use fluent_templates::static_loader; + +static_loader! { + pub static LOCALES = { + locales: "./src/locales", + fallback_language: "pt-BR", + customise: |bundle| bundle.set_use_isolating(false), + }; +} + +pub fn init_locales() -> anyhow::Result<()> { + Ok(()) +} diff --git a/src/i18n/mod.rs b/src/i18n/mod.rs new file mode 100644 index 0000000..b12fbd8 --- /dev/null +++ b/src/i18n/mod.rs @@ -0,0 +1,5 @@ +pub mod loader; +pub mod translate; + +pub use loader::init_locales; +pub use translate::t; diff --git a/src/i18n/translate.rs b/src/i18n/translate.rs new file mode 100644 index 0000000..aed7b7c --- /dev/null +++ b/src/i18n/translate.rs @@ -0,0 +1,13 @@ +use crate::i18n::loader::LOCALES; +use fluent_templates::Loader; +use std::str::FromStr; +use unic_langid::LanguageIdentifier; + +fn get_sys_locale() -> LanguageIdentifier { + let sys_lang = sys_locale::get_locale().unwrap_or_else(|| String::from("pt-BR")); + LanguageIdentifier::from_str(&sys_lang).expect("Invalid language") +} + +pub fn t(key: &str) -> String { + LOCALES.lookup(&get_sys_locale(), key) +} diff --git a/src/main.rs b/src/main.rs index d4feaaf..7135534 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,58 +1,42 @@ -use std::str::FromStr; - -use anyhow::{Ok, Result}; +use anyhow::Result; use clap::Parser; -use fluent_templates::{Loader, static_loader}; use reqwest::blocking::Client; -use unic_langid::LanguageIdentifier; - -static_loader! { - static LOCALES = { - locales: "./src/locales", - fallback_language: "pt-BR", - customise: |bundle| bundle.set_use_isolating(false), - }; -} +use std::result::Result::Ok; +mod i18n; +use i18n::{init_locales, t}; +mod heartbeat; #[derive(Debug, Parser)] #[command(author, version, about)] struct Args { #[arg(short, long, env = "UPTIME_KUMA_URL")] - url: Option, + base_url: String, - #[arg(short, long, env = "UPTIME_KUMA_API_KEY")] - api_key: Option, -} - -fn get_sys_locale() -> LanguageIdentifier { - let sys_lang = sys_locale::get_locale().unwrap_or_else(|| String::from("pt-BR")); - LanguageIdentifier::from_str(&sys_lang).expect("Invalid language") -} - -fn t(key: &str) -> String { - LOCALES.lookup(&get_sys_locale(), key) + #[arg(short, long, env = "STATUS_PAGE_SLUG")] + slug: String, } fn main() -> Result<()> { + init_locales()?; let args = Args::parse(); - let url = args - .url - .as_deref() - .ok_or_else(|| anyhow::anyhow!("{}", t("missing_url")))?; - let api_key = args - .api_key - .as_deref() - .ok_or_else(|| anyhow::anyhow!("{}", t("missing_api_key")))?; + let heartbeat_url = format!( + "{}/api/status-page/heartbeat/{}", + args.base_url.trim_end_matches("/"), + args.slug + ); let client = Client::new(); - let response = client.get(url).basic_auth("", Some(api_key)).send()?; + println!("{}", heartbeat_url); + + let response = client.get(heartbeat_url).send()?; if response.status().is_success() { - let metrics = response.text()?; - println!("{}", t("success")); - println!("{}", t("metrics_preview")); - println!("{}", &metrics[..200.min(metrics.len())]); + let json_text = response.text()?; + match heartbeat::parse_response(&json_text) { + Ok(data) => println!("moises: {}", data.uptime_data[0].get_perc_formated()), + Err(e) => println!("{}", e), + } } else { println!("{}", response.status()); println!("{}: {}", t("Response"), response.text()?);