use crate::domain::analytics::*;
use crate::ports::analytics_ports::{AnalysisEngine, AnalyticsStore};
use serde::Serialize;
use tauri::Manager;
pub struct AppState {
db_path: String,
}
impl AppState {
fn open_store(
&self,
) -> anyhow::Result<crate::adapters::analytics::sqlite_store::SqliteAnalyticsStore> {
let store =
crate::adapters::analytics::sqlite_store::SqliteAnalyticsStore::open(&self.db_path)?;
store.initialize_schema()?;
Ok(store)
}
}
fn get_state(app_handle: &tauri::AppHandle) -> AppState {
let db_path = dirs::home_dir()
.map(|h| {
h.join(".claudy")
.join("analytics")
.join("analytics.db")
.to_string_lossy()
.to_string()
})
.unwrap_or_else(|| {
app_handle
.path()
.app_data_dir()
.map(|p: std::path::PathBuf| p.join("analytics.db").to_string_lossy().to_string())
.unwrap_or_default()
});
AppState { db_path }
}
#[derive(Serialize)]
pub struct SessionSummary {
pub session_uuid: String,
pub project_name: String,
pub model: Option<String>,
pub started_at: Option<String>,
pub ended_at: Option<String>,
pub total_turns: i32,
pub total_cost_usd: f64,
pub total_duration_ms: i64,
pub first_message: Option<String>,
}
#[tauri::command]
pub fn get_sessions(
app_handle: tauri::AppHandle,
limit: Option<u32>,
days: Option<u32>,
project: Option<String>,
) -> Result<Vec<SessionSummary>, String> {
let state = get_state(&app_handle);
let store = state.open_store().map_err(|e| e.to_string())?;
let projects = store.list_projects().map_err(|e| e.to_string())?;
let project_map: std::collections::HashMap<i64, String> = projects
.into_iter()
.map(|p| (p.id, p.display_name))
.collect();
let project_id = project
.map(|p| store.get_project_by_encoded_dir(&p))
.transpose()
.map_err(|e| e.to_string())?
.flatten()
.map(|p| p.id);
let capped = limit.unwrap_or(50).min(1000);
let sessions = store
.get_sessions(capped, days, project_id)
.map_err(|e| e.to_string())?;
Ok(sessions
.into_iter()
.map(|s| SessionSummary {
session_uuid: s.session_uuid,
project_name: project_map.get(&s.project_id).cloned().unwrap_or_default(),
model: s.model,
started_at: s.started_at,
ended_at: s.ended_at,
total_turns: s.total_turns,
total_cost_usd: s.total_cost_usd,
total_duration_ms: s.total_duration_ms,
first_message: s.first_message,
})
.collect())
}
#[tauri::command]
pub fn get_token_trends(
app_handle: tauri::AppHandle,
days: Option<u32>,
project: Option<String>,
) -> Result<Vec<TokenTrendPoint>, String> {
let state = get_state(&app_handle);
let store = state.open_store().map_err(|e| e.to_string())?;
let project_id = project
.map(|p| store.get_project_by_encoded_dir(&p))
.transpose()
.map_err(|e| e.to_string())?
.flatten()
.map(|p| p.id);
let engine =
crate::adapters::analytics::analysis::SqliteAnalysisEngine::new(std::sync::Arc::new(store));
engine
.compute_token_trends(days.unwrap_or(30), project_id)
.map_err(|e: anyhow::Error| e.to_string())
}
#[tauri::command]
pub fn get_tool_distribution(
app_handle: tauri::AppHandle,
days: Option<u32>,
project: Option<String>,
) -> Result<Vec<ToolDistribution>, String> {
let state = get_state(&app_handle);
let store = state.open_store().map_err(|e| e.to_string())?;
let project_id = project
.map(|p| store.get_project_by_encoded_dir(&p))
.transpose()
.map_err(|e| e.to_string())?
.flatten()
.map(|p| p.id);
let engine =
crate::adapters::analytics::analysis::SqliteAnalysisEngine::new(std::sync::Arc::new(store));
engine
.compute_tool_distribution(days, project_id)
.map_err(|e: anyhow::Error| e.to_string())
}
#[tauri::command]
pub fn get_cost_metrics(
app_handle: tauri::AppHandle,
days: Option<u32>,
project: Option<String>,
) -> Result<CostMetrics, String> {
let state = get_state(&app_handle);
let store = state.open_store().map_err(|e| e.to_string())?;
let project_id = project
.map(|p| store.get_project_by_encoded_dir(&p))
.transpose()
.map_err(|e| e.to_string())?
.flatten()
.map(|p| p.id);
let engine =
crate::adapters::analytics::analysis::SqliteAnalysisEngine::new(std::sync::Arc::new(store));
engine
.compute_cost_metrics(days.unwrap_or(30), project_id)
.map_err(|e: anyhow::Error| e.to_string())
}
#[tauri::command]
pub fn get_dashboard_stats(
app_handle: tauri::AppHandle,
days: Option<u32>,
project: Option<String>,
) -> Result<DashboardStats, String> {
let state = get_state(&app_handle);
let store = state.open_store().map_err(|e| e.to_string())?;
let project_id = project
.map(|p| store.get_project_by_encoded_dir(&p))
.transpose()
.map_err(|e| e.to_string())?
.flatten()
.map(|p| p.id);
let engine =
crate::adapters::analytics::analysis::SqliteAnalysisEngine::new(std::sync::Arc::new(store));
engine
.compute_dashboard_stats(days.unwrap_or(30), project_id)
.map_err(|e: anyhow::Error| e.to_string())
}
#[tauri::command]
pub fn get_recommendations(app_handle: tauri::AppHandle) -> Result<Vec<Recommendation>, String> {
let state = get_state(&app_handle);
let store = state.open_store().map_err(|e| e.to_string())?;
store.get_recommendations().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn trigger_ingestion(
app_handle: tauri::AppHandle,
full: Option<bool>,
) -> Result<IngestionResult, String> {
let state = get_state(&app_handle);
if let Ok(store) = state.open_store() {
let cache_path = dirs::home_dir()
.map(|h| h.join(".claudy").join("cache").join("models_dev.json"))
.unwrap_or_default();
let _ = crate::adapters::analytics::pricing::sync::run_pricing_sync(&store, &cache_path);
}
let result = crate::adapters::analytics::ingestion::run_ingestion(
&state.db_path,
full.unwrap_or(false),
None,
)
.map_err(|e| e.to_string())?;
if let Ok(store) = state.open_store() {
let _ = crate::adapters::analytics::recommendations::generate(&store);
}
Ok(result)
}
#[tauri::command]
pub fn get_model_comparison(app_handle: tauri::AppHandle) -> Result<Vec<ModelPerformance>, String> {
let _state = get_state(&app_handle);
Ok(vec![])
}
#[tauri::command]
pub fn get_projects(app_handle: tauri::AppHandle) -> Result<Vec<ProjectRecord>, String> {
let state = get_state(&app_handle);
let store = state.open_store().map_err(|e| e.to_string())?;
store.list_projects().map_err(|e| e.to_string())
}
fn config_path() -> std::path::PathBuf {
dirs::home_dir()
.map(|h| h.join(".claudy").join("config.yaml"))
.unwrap_or_else(|| std::path::PathBuf::from("config.yaml"))
}
#[tauri::command]
pub fn get_config() -> Result<serde_json::Value, String> {
let path = config_path();
let cfg = crate::config::registry::AppRegistry::open(&path).map_err(|e| e.to_string())?;
serde_json::to_value(&cfg).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn update_config(partial: serde_json::Value) -> Result<serde_json::Value, String> {
let path = config_path();
let mut cfg = crate::config::registry::AppRegistry::open(&path).map_err(|e| e.to_string())?;
if let Some(obj) = partial.as_object() {
if let Some(v) = obj.get("compaction") {
let policy: crate::config::registry::ContextWindowPolicy =
serde_json::from_value(v.clone()).map_err(|e| e.to_string())?;
cfg.compaction = policy;
}
if let Some(v) = obj.get("channel") {
let settings: crate::config::registry::BridgeSettings =
serde_json::from_value(v.clone()).map_err(|e| e.to_string())?;
cfg.channel = settings;
}
if let Some(v) = obj.get("provider_overrides") {
let overrides: std::collections::HashMap<String, crate::config::registry::ModelPreset> =
serde_json::from_value(v.clone()).map_err(|e| e.to_string())?;
cfg.provider_overrides = overrides;
}
if let Some(v) = obj.get("openrouter_aliases") {
let aliases: std::collections::HashMap<String, String> =
serde_json::from_value(v.clone()).map_err(|e| e.to_string())?;
cfg.openrouter_aliases = aliases;
}
if let Some(v) = obj.get("custom_providers") {
let providers: std::collections::HashMap<
String,
crate::config::registry::UserEndpoint,
> = serde_json::from_value(v.clone()).map_err(|e| e.to_string())?;
cfg.custom_providers = providers;
}
if let Some(v) = obj.get("model_settings") {
let settings: std::collections::HashMap<
String,
crate::config::registry::PerModelOverrides,
> = serde_json::from_value(v.clone()).map_err(|e| e.to_string())?;
cfg.model_settings = settings;
}
if let Some(v) = obj.get("agents") {
let agents: std::collections::HashMap<String, crate::domain::agent::AgentConfig> =
serde_json::from_value(v.clone()).map_err(|e| e.to_string())?;
cfg.agents = agents;
}
}
cfg.write_to(&path).map_err(|e| e.to_string())?;
serde_json::to_value(&cfg).map_err(|e| e.to_string())
}