Using heartbeat api instead

This commit is contained in:
Marco De Araujo 2025-12-22 10:51:58 -04:00
parent 4cedb17b60
commit d024280878
10 changed files with 319 additions and 40 deletions

6
Cargo.lock generated
View file

@ -1205,9 +1205,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.145" version = "1.0.146"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" checksum = "217ca874ae0207aac254aa02c957ded05585a90892cc8d87f9e5fa49669dadd8"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@ -1577,6 +1577,8 @@ dependencies = [
"clap", "clap",
"fluent-templates", "fluent-templates",
"reqwest", "reqwest",
"serde",
"serde_json",
"sys-locale", "sys-locale",
"unic-langid", "unic-langid",
] ]

View file

@ -10,3 +10,5 @@ anyhow = "1.0"
fluent-templates = "0.13.2" fluent-templates = "0.13.2"
unic-langid = "0.9.6" unic-langid = "0.9.6"
sys-locale = "0.3.2" sys-locale = "0.3.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.146"

124
formgen.txt Normal file
View file

@ -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<String>,
#[arg(short, long, env = "UPTIME_KUMA_API_KEY")]
api_key: Option<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 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"

4
src/heartbeat/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod parser;
pub mod model;
pub use parser::{format_duration, parse_response,status_to_string};
pub use model::HeartbeatResponse;

103
src/heartbeat/model.rs Normal file
View file

@ -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<u64>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MonitorHeartbeats {
pub monitor_id: u64,
pub heartbeats: Vec<HeartbeatEntry>,
}
#[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<String, Vec<HeartbeatEntry>>,
#[serde(rename = "uptimeList")]
pub uptime_list_raw: HashMap<String, f64>,
#[serde(skip)]
pub monitors: Vec<MonitorHeartbeats>,
#[serde(skip)]
pub uptime_data: Vec<UptimeData>,
}
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::<u64>()
.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::<u64>()
.with_context(|| format!("{}", t("invalid-monitor-id")))?;
let period_hours = parts[1]
.parse::<u32>()
.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())
}
}

28
src/heartbeat/parser.rs Normal file
View file

@ -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<HeartbeatResponse> {
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<u64>) -> String {
match duration_ms {
Some(ms) if ms < 1000 => format!("{}ms", ms),
Some(ms) => format!("{:.1}s", ms),
None => "N/A".to_string(),
}
}

14
src/i18n/loader.rs Normal file
View file

@ -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(())
}

5
src/i18n/mod.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod loader;
pub mod translate;
pub use loader::init_locales;
pub use translate::t;

13
src/i18n/translate.rs Normal file
View file

@ -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)
}

View file

@ -1,58 +1,42 @@
use std::str::FromStr; use anyhow::Result;
use anyhow::{Ok, Result};
use clap::Parser; use clap::Parser;
use fluent_templates::{Loader, static_loader};
use reqwest::blocking::Client; use reqwest::blocking::Client;
use unic_langid::LanguageIdentifier; use std::result::Result::Ok;
mod i18n;
static_loader! { use i18n::{init_locales, t};
static LOCALES = { mod heartbeat;
locales: "./src/locales",
fallback_language: "pt-BR",
customise: |bundle| bundle.set_use_isolating(false),
};
}
#[derive(Debug, Parser)] #[derive(Debug, Parser)]
#[command(author, version, about)] #[command(author, version, about)]
struct Args { struct Args {
#[arg(short, long, env = "UPTIME_KUMA_URL")] #[arg(short, long, env = "UPTIME_KUMA_URL")]
url: Option<String>, base_url: String,
#[arg(short, long, env = "UPTIME_KUMA_API_KEY")] #[arg(short, long, env = "STATUS_PAGE_SLUG")]
api_key: Option<String>, slug: String,
}
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)
} }
fn main() -> Result<()> { fn main() -> Result<()> {
init_locales()?;
let args = Args::parse(); 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 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() { if response.status().is_success() {
let metrics = response.text()?; let json_text = response.text()?;
println!("{}", t("success")); match heartbeat::parse_response(&json_text) {
println!("{}", t("metrics_preview")); Ok(data) => println!("moises: {}", data.uptime_data[0].get_perc_formated()),
println!("{}", &metrics[..200.min(metrics.len())]); Err(e) => println!("{}", e),
}
} else { } else {
println!("{}", response.status()); println!("{}", response.status());
println!("{}: {}", t("Response"), response.text()?); println!("{}: {}", t("Response"), response.text()?);