1use gloo_net::http::Request;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7const API_BASE_URL: &str = "";
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct StatsData {
16 #[serde(default)]
17 pub version: u32,
18 #[serde(default)]
19 pub last_computed_date: Option<String>,
20 #[serde(default)]
21 pub total_sessions: u64,
22 #[serde(default)]
23 pub total_messages: u64,
24 #[serde(default)]
25 pub daily_activity: Vec<DailyActivityEntry>,
26 #[serde(default)]
27 pub model_usage: HashMap<String, ModelUsage>,
28 #[serde(default)]
30 pub daily_tokens_30d: Vec<u64>,
31 #[serde(default)]
32 pub forecast_tokens_30d: Vec<u64>,
33 #[serde(default)]
34 pub forecast_confidence: f64,
35 #[serde(default)]
36 pub forecast_cost_30d: f64,
37 #[serde(default)]
38 pub projects_by_cost: Vec<ProjectCost>,
39 #[serde(default)]
40 pub most_used_model: Option<MostUsedModel>,
41 #[serde(default)]
42 pub this_month_cost: f64,
43 #[serde(default)]
44 pub avg_session_cost: f64,
45 #[serde(default)]
46 pub cache_hit_ratio: f64,
47 #[serde(default)]
48 pub mcp_servers_count: usize,
49}
50
51#[derive(Debug, Clone, Default, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct ProjectCost {
54 pub project: String,
55 #[serde(default)]
56 pub cost: f64,
57 #[serde(default)]
58 pub percentage: f64,
59}
60
61#[derive(Debug, Clone, Default, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct MostUsedModel {
64 pub name: String,
65 #[serde(default)]
66 pub count: u64,
67}
68
69#[derive(Debug, Clone, Default, Serialize, Deserialize)]
70#[serde(rename_all = "camelCase")]
71pub struct DailyActivityEntry {
72 pub date: String,
73 #[serde(default)]
74 pub message_count: u64,
75 #[serde(default)]
76 pub session_count: u64,
77 #[serde(default)]
78 pub tool_call_count: u64,
79}
80
81#[derive(Debug, Clone, Default, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct ModelUsage {
84 #[serde(default)]
85 pub input_tokens: u64,
86 #[serde(default)]
87 pub output_tokens: u64,
88 #[serde(default)]
89 pub cache_read_input_tokens: u64,
90 #[serde(default)]
91 pub cache_creation_input_tokens: u64,
92 #[serde(default)]
93 pub cost_usd: f64,
94}
95
96impl ModelUsage {
97 pub fn total_tokens(&self) -> u64 {
98 self.input_tokens + self.output_tokens
99 }
100}
101
102impl StatsData {
103 pub fn total_tokens(&self) -> u64 {
105 self.model_usage.values().map(|m| m.total_tokens()).sum()
106 }
107
108 pub fn total_cost(&self) -> f64 {
110 self.model_usage.values().map(|m| m.cost_usd).sum()
111 }
112
113 pub fn avg_session_cost(&self) -> f64 {
115 if self.total_sessions == 0 {
116 return 0.0;
117 }
118 self.total_cost() / self.total_sessions as f64
119 }
120
121 pub fn this_month_sessions(&self) -> u64 {
123 let len = self.daily_activity.len();
125 let start = if len > 30 { len - 30 } else { 0 };
126
127 self.daily_activity[start..]
128 .iter()
129 .map(|entry| entry.session_count)
130 .sum()
131 }
132
133 pub fn this_week_tokens(&self) -> u64 {
135 let len = self.daily_activity.len();
137 let start = if len > 7 { len - 7 } else { 0 };
138
139 self.daily_activity[start..]
140 .iter()
141 .map(|entry| {
142 entry.message_count * 500
144 })
145 .sum()
146 }
147
148 pub fn daily_tokens_30d(&self) -> Vec<u64> {
150 let mut result = Vec::new();
151 let len = self.daily_activity.len();
152 let start = if len > 30 { len - 30 } else { 0 };
153
154 for entry in &self.daily_activity[start..] {
155 result.push(entry.message_count * 500);
157 }
158
159 while result.len() < 30 {
161 result.insert(0, 0);
162 }
163
164 result
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
170pub struct SessionData {
171 pub id: String,
172 pub date: Option<String>,
173 pub project: String,
174 pub model: String,
175 pub messages: u64,
176 pub tokens: u64,
177 pub input_tokens: u64,
178 pub output_tokens: u64,
179 pub cache_creation_tokens: u64,
180 pub cache_read_tokens: u64,
181 pub cost: f64,
182 pub status: String,
183 pub first_timestamp: Option<String>,
184 pub duration_seconds: Option<u64>,
185 pub preview: Option<String>,
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct RecentSessionsResponse {
191 pub sessions: Vec<SessionData>,
192 pub total: u64,
193}
194
195pub async fn fetch_stats() -> Result<StatsData, String> {
197 let url = format!("{}/api/stats", API_BASE_URL);
198 let response = Request::get(&url)
199 .send()
200 .await
201 .map_err(|e| format!("Network error: {}", e))?;
202
203 if !response.ok() {
204 return Err(format!("HTTP error: {}", response.status()));
205 }
206
207 let stats = response
208 .json::<StatsData>()
209 .await
210 .map_err(|e| format!("Parse error: {}", e))?;
211
212 Ok(stats)
213}
214
215pub async fn fetch_recent_sessions(limit: u32) -> Result<RecentSessionsResponse, String> {
217 let url = format!("{}/api/sessions/recent?limit={}", API_BASE_URL, limit);
218 let response = Request::get(&url)
219 .send()
220 .await
221 .map_err(|e| format!("Network error: {}", e))?;
222
223 if !response.ok() {
224 return Err(format!("HTTP error: {}", response.status()));
225 }
226
227 let sessions = response
228 .json::<RecentSessionsResponse>()
229 .await
230 .map_err(|e| format!("Parse error: {}", e))?;
231
232 Ok(sessions)
233}
234
235pub fn format_number(n: u64) -> String {
237 if n >= 1_000_000_000 {
238 format!("{:.1}B", n as f64 / 1_000_000_000.0)
239 } else if n >= 1_000_000 {
240 format!("{:.1}M", n as f64 / 1_000_000.0)
241 } else if n >= 1_000 {
242 format!("{:.1}K", n as f64 / 1_000.0)
243 } else {
244 n.to_string()
245 }
246}
247
248pub fn format_cost(cost: f64) -> String {
250 format!("${:.2}", cost)
251}