use gloo_net::http::Request;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
const API_BASE_URL: &str = "";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StatsData {
#[serde(default)]
pub version: u32,
#[serde(default)]
pub last_computed_date: Option<String>,
#[serde(default)]
pub total_sessions: u64,
#[serde(default)]
pub total_messages: u64,
#[serde(default)]
pub daily_activity: Vec<DailyActivityEntry>,
#[serde(default)]
pub model_usage: HashMap<String, ModelUsage>,
#[serde(default)]
pub daily_tokens_30d: Vec<u64>,
#[serde(default)]
pub forecast_tokens_30d: Vec<u64>,
#[serde(default)]
pub forecast_confidence: f64,
#[serde(default)]
pub forecast_cost_30d: f64,
#[serde(default)]
pub projects_by_cost: Vec<ProjectCost>,
#[serde(default)]
pub most_used_model: Option<MostUsedModel>,
#[serde(default)]
pub this_month_cost: f64,
#[serde(default)]
pub avg_session_cost: f64,
#[serde(default)]
pub cache_hit_ratio: f64,
#[serde(default)]
pub mcp_servers_count: usize,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProjectCost {
pub project: String,
#[serde(default)]
pub cost: f64,
#[serde(default)]
pub percentage: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MostUsedModel {
pub name: String,
#[serde(default)]
pub count: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QuotaData {
#[serde(default)]
pub current_cost: f64,
pub budget_limit: Option<f64>,
#[serde(default)]
pub usage_pct: f64,
#[serde(default)]
pub projected_monthly_cost: f64,
pub projected_overage: Option<f64>,
#[serde(default)]
pub alert_level: String, pub error: Option<String>, }
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DailyActivityEntry {
pub date: String,
#[serde(default)]
pub message_count: u64,
#[serde(default)]
pub session_count: u64,
#[serde(default)]
pub tool_call_count: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelUsage {
#[serde(default)]
pub input_tokens: u64,
#[serde(default)]
pub output_tokens: u64,
#[serde(default)]
pub cache_read_input_tokens: u64,
#[serde(default)]
pub cache_creation_input_tokens: u64,
#[serde(default)]
pub cost_usd: f64,
}
impl ModelUsage {
pub fn total_tokens(&self) -> u64 {
self.input_tokens + self.output_tokens
}
}
impl StatsData {
pub fn total_tokens(&self) -> u64 {
self.model_usage.values().map(|m| m.total_tokens()).sum()
}
pub fn total_cost(&self) -> f64 {
self.model_usage.values().map(|m| m.cost_usd).sum()
}
pub fn avg_session_cost(&self) -> f64 {
if self.total_sessions == 0 {
return 0.0;
}
self.total_cost() / self.total_sessions as f64
}
pub fn this_month_sessions(&self) -> u64 {
let start = self.daily_activity.len().saturating_sub(30);
self.daily_activity[start..]
.iter()
.map(|entry| entry.session_count)
.sum()
}
pub fn this_week_tokens(&self) -> u64 {
let start = self.daily_activity.len().saturating_sub(7);
self.daily_activity[start..]
.iter()
.map(|entry| {
entry.message_count * 500
})
.sum()
}
pub fn daily_tokens_30d(&self) -> Vec<u64> {
let mut result = Vec::new();
let start = self.daily_activity.len().saturating_sub(30);
for entry in &self.daily_activity[start..] {
result.push(entry.message_count * 500);
}
while result.len() < 30 {
result.insert(0, 0);
}
result
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SessionData {
pub id: String,
pub date: Option<String>,
pub project: String,
pub model: String,
pub messages: u64,
pub tokens: u64,
pub input_tokens: u64,
pub output_tokens: u64,
pub cache_creation_tokens: u64,
pub cache_read_tokens: u64,
pub cost: f64,
pub status: String,
pub first_timestamp: Option<String>,
pub duration_seconds: Option<u64>,
pub preview: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecentSessionsResponse {
pub sessions: Vec<SessionData>,
pub total: u64,
}
pub async fn fetch_stats() -> Result<StatsData, String> {
let url = format!("{}/api/stats", API_BASE_URL);
let response = Request::get(&url)
.send()
.await
.map_err(|e| format!("Network error: {}", e))?;
if !response.ok() {
return Err(format!("HTTP error: {}", response.status()));
}
let stats = response
.json::<StatsData>()
.await
.map_err(|e| format!("Parse error: {}", e))?;
Ok(stats)
}
pub async fn fetch_recent_sessions(limit: u32) -> Result<RecentSessionsResponse, String> {
let url = format!("{}/api/sessions/recent?limit={}", API_BASE_URL, limit);
let response = Request::get(&url)
.send()
.await
.map_err(|e| format!("Network error: {}", e))?;
if !response.ok() {
return Err(format!("HTTP error: {}", response.status()));
}
let sessions = response
.json::<RecentSessionsResponse>()
.await
.map_err(|e| format!("Parse error: {}", e))?;
Ok(sessions)
}
pub async fn fetch_quota() -> Result<QuotaData, String> {
let url = format!("{}/api/quota", API_BASE_URL);
let response = Request::get(&url)
.send()
.await
.map_err(|e| format!("Network error: {}", e))?;
if !response.ok() {
return Err(format!("HTTP error: {}", response.status()));
}
let quota = response
.json::<QuotaData>()
.await
.map_err(|e| format!("Parse error: {}", e))?;
Ok(quota)
}
pub fn format_number(n: u64) -> String {
if n >= 1_000_000_000 {
format!("{:.1}B", n as f64 / 1_000_000_000.0)
} else if n >= 1_000_000 {
format!("{:.1}M", n as f64 / 1_000_000.0)
} else if n >= 1_000 {
format!("{:.1}K", n as f64 / 1_000.0)
} else {
n.to_string()
}
}
pub fn format_cost(cost: f64) -> String {
format!("${:.2}", cost)
}