use timeweb_rs::authenticated;
use crate::tui;
pub fn spawn_stats_fetch(
tx: tokio::sync::mpsc::UnboundedSender<tui::event::AppEvent>,
token: String,
req: tui::app::StatsRequest
) {
tokio::spawn(async move {
let config = authenticated(token);
match fetch_resource_stats(&config, &req).await {
Ok(stats) => {
if stats.cpu.is_empty()
&& stats.ram.is_empty()
&& stats.net_in.is_empty()
&& stats.net_out.is_empty()
{
let _ = tx.send(tui::event::AppEvent::StatsError(format!(
"stats {}: no data points returned",
req.name
)));
}
let _ = tx.send(tui::event::AppEvent::Stats(Box::new(stats)));
}
Err(e) => {
let _ = tx.send(tui::event::AppEvent::StatsError(format!(
"stats {}: {e}",
req.name
)));
}
}
});
}
const fn tail_skip(len: usize) -> usize {
const LIVE_STATS_SAMPLES: usize = 60;
len.saturating_sub(LIVE_STATS_SAMPLES)
}
async fn fetch_resource_stats(
config: &timeweb_rs::apis::configuration::Configuration,
req: &tui::app::StatsRequest
) -> Result<tui::app::ResourceStats, String> {
use tui::app::ResourceTab;
match req.tab {
ResourceTab::Servers => fetch_server_stats(config, req).await,
ResourceTab::Apps => fetch_app_stats(config, req).await,
_ => Err("live statistics are not available for this resource".to_string())
}
}
fn stats_timestamp(at: chrono::DateTime<chrono::Utc>) -> String {
at.format("%Y-%m-%dT%H:%M:%S").to_string()
}
async fn fetch_server_stats(
config: &timeweb_rs::apis::configuration::Configuration,
req: &tui::app::StatsRequest
) -> Result<tui::app::ResourceStats, String> {
let id: i32 = req
.id
.parse()
.map_err(|_| format!("invalid server id {}", req.id))?;
let now = chrono::Utc::now();
let time_from = stats_timestamp(now - chrono::Duration::hours(24));
let keys = "system.cpu.util;network.request;network.response";
let resp = timeweb_rs::apis::servers_api::get_server_statistics_new(
config, id, &time_from, "24", keys
)
.await
.map_err(|e| e.to_string())?;
let mut stats = tui::app::ResourceStats {
id: req.id.clone(),
subject: req.name.clone(),
..Default::default()
};
for series in resp.statistics.into_iter().flatten() {
let Some(name) = series.name.as_deref() else {
continue;
};
let mut values: Vec<f64> = series
.list
.unwrap_or_default()
.into_iter()
.map(|p| p.value)
.collect();
let values = values.split_off(tail_skip(values.len()));
match name {
"system.cpu.util" => stats.cpu = values,
"network.request" => stats.net_in = values,
"network.response" => stats.net_out = values,
_ => {}
}
}
Ok(stats)
}
async fn fetch_app_stats(
config: &timeweb_rs::apis::configuration::Configuration,
req: &tui::app::StatsRequest
) -> Result<tui::app::ResourceStats, String> {
let now = chrono::Utc::now();
let from = stats_timestamp(now - chrono::Duration::hours(24));
let to = stats_timestamp(now);
let resp = timeweb_rs::apis::apps_api::get_app_statistics(config, &req.id, &from, &to)
.await
.map_err(|e| e.to_string())?;
let cpu: Vec<f64> = resp
.cpu
.iter()
.skip(tail_skip(resp.cpu.len()))
.map(|c| c.load)
.collect();
let ram: Vec<f64> = resp
.ram
.iter()
.skip(tail_skip(resp.ram.len()))
.map(|r| {
if r.total > 0.0 {
(r.used / r.total) * 100.0
} else {
0.0
}
})
.collect();
let net = &resp.network_traffic;
let net_in: Vec<f64> = net
.iter()
.skip(tail_skip(net.len()))
.map(|n| n.incoming)
.collect();
let net_out: Vec<f64> = net
.iter()
.skip(tail_skip(net.len()))
.map(|n| n.outgoing)
.collect();
Ok(tui::app::ResourceStats {
id: req.id.clone(),
subject: req.name.clone(),
cpu,
ram,
net_in,
net_out
})
}