feat: implement analytics and cost management dashboard pages

This commit is contained in:
2026-02-26 18:37:37 -05:00
parent 25986dd255
commit 519436eb4a
4 changed files with 309 additions and 582 deletions

View File

@@ -67,6 +67,8 @@ pub fn router(state: AppState) -> Router {
.route("/api/usage/time-series", get(handle_time_series))
.route("/api/usage/clients", get(handle_clients_usage))
.route("/api/usage/providers", get(handle_providers_usage))
.route("/api/usage/detailed", get(handle_detailed_usage))
.route("/api/analytics/breakdown", get(handle_analytics_breakdown))
.route("/api/models", get(handle_get_models))
.route("/api/models/{id}", put(handle_update_model))
.route("/api/clients", get(handle_get_clients).post(handle_create_client))
@@ -405,6 +407,83 @@ async fn handle_providers_usage(State(state): State<DashboardState>) -> Json<Api
}
}
async fn handle_detailed_usage(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
let pool = &state.app_state.db_pool;
let result = sqlx::query(
r#"
SELECT
strftime('%Y-%m-%d', timestamp) as date,
client_id,
provider,
model,
COUNT(*) as requests,
SUM(total_tokens) as tokens,
SUM(cost) as cost
FROM llm_requests
GROUP BY date, client_id, provider, model
ORDER BY date DESC
LIMIT 100
"#
)
.fetch_all(pool)
.await;
match result {
Ok(rows) => {
let usage: Vec<serde_json::Value> = rows.into_iter().map(|row| {
serde_json::json!({
"date": row.get::<String, _>("date"),
"client": row.get::<String, _>("client_id"),
"provider": row.get::<String, _>("provider"),
"model": row.get::<String, _>("model"),
"requests": row.get::<i64, _>("requests"),
"tokens": row.get::<i64, _>("tokens"),
"cost": row.get::<f64, _>("cost"),
})
}).collect();
Json(ApiResponse::success(serde_json::json!(usage)))
}
Err(e) => {
warn!("Failed to fetch detailed usage: {}", e);
Json(ApiResponse::error("Failed to fetch detailed usage".to_string()))
}
}
}
async fn handle_analytics_breakdown(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
let pool = &state.app_state.db_pool;
// Model breakdown
let models = sqlx::query(
"SELECT model as label, COUNT(*) as value FROM llm_requests GROUP BY model ORDER BY value DESC"
).fetch_all(pool);
// Client breakdown
let clients = sqlx::query(
"SELECT client_id as label, COUNT(*) as value FROM llm_requests GROUP BY client_id ORDER BY value DESC"
).fetch_all(pool);
match tokio::join!(models, clients) {
(Ok(m_rows), Ok(c_rows)) => {
let model_breakdown: Vec<serde_json::Value> = m_rows.into_iter().map(|r| {
serde_json::json!({ "label": r.get::<String, _>("label"), "value": r.get::<i64, _>("value") })
}).collect();
let client_breakdown: Vec<serde_json::Value> = c_rows.into_iter().map(|r| {
serde_json::json!({ "label": r.get::<String, _>("label"), "value": r.get::<i64, _>("value") })
}).collect();
Json(ApiResponse::success(serde_json::json!({
"models": model_breakdown,
"clients": client_breakdown
})))
}
_ => Json(ApiResponse::error("Failed to fetch analytics breakdown".to_string()))
}
}
// Client handlers
async fn handle_get_clients(State(state): State<DashboardState>) -> Json<ApiResponse<serde_json::Value>> {
let pool = &state.app_state.db_pool;