claude-dashboard 0.2.2

A terminal TUI dashboard that renders Claude Code usage reports with token stats, project breakdowns, and language distribution.
Documentation
use super::stats::DailyActivity;
use std::collections::HashMap;

// --- Per-model token detail (breakdown + tool usage) ---

#[derive(Debug, Clone, Default)]
pub struct ModelTokenDetail {
    pub total_tokens: u64,
    pub input_tokens: u64,
    pub output_tokens: u64,
    pub cache_read_input_tokens: u64,
    pub cache_creation_input_tokens: u64,
    pub web_search_requests: u64,
    pub web_fetch_requests: u64,
    pub tool_call_count: u64,
    pub tool_calls: HashMap<String, u64>,
}

impl ModelTokenDetail {
    /// 缓存命中率:cache_read / (input + cache_read)
    pub fn cache_hit_rate(&self) -> f64 {
        let denom = self.input_tokens + self.cache_read_input_tokens;
        if denom > 0 {
            self.cache_read_input_tokens as f64 / denom as f64
        } else {
            0.0
        }
    }

    #[allow(dead_code)]
    pub fn total_tool_requests(&self) -> u64 {
        self.tool_call_count + self.web_search_requests + self.web_fetch_requests
    }
}

// --- Session summary (aggregated from JSONL) ---

#[derive(Debug, Clone)]
pub struct SessionSummary {
    pub project_name: String,
    pub model: String,
    pub total_tokens: u64,
    pub input_tokens: u64,
    pub output_tokens: u64,
    pub cache_read_input_tokens: u64,
    pub cache_creation_input_tokens: u64,
    pub web_search_requests: u64,
    pub web_fetch_requests: u64,
    pub tool_call_count: u64,
    pub tool_calls: HashMap<String, u64>,
}

// --- Aggregate data for a view ---

#[derive(Debug, Clone, Default)]
pub struct ViewData {
    pub total_sessions: u64,
    pub total_messages: u64,
    pub total_tokens: u64,
    pub model_tokens: HashMap<String, u64>,
    pub model_token_detail: HashMap<String, ModelTokenDetail>,
    pub project_tokens: HashMap<String, u64>,
    pub daily_activity: Vec<DailyActivity>,
    pub total_web_search_requests: u64,
    pub total_web_fetch_requests: u64,
    pub total_tool_calls: u64,
    pub tool_calls: HashMap<String, u64>,
    pub longest_session_secs: u64,
    pub avg_session_secs: u64,
    pub peak_period: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_model_token_detail_cache_hit_rate() {
        let mut detail = ModelTokenDetail::default();
        detail.input_tokens = 800;
        detail.cache_read_input_tokens = 200;
        let rate = detail.cache_hit_rate();
        assert!((rate - 0.2).abs() < 1e-9);
    }

    #[test]
    fn test_model_token_detail_cache_hit_rate_zero() {
        let detail = ModelTokenDetail::default();
        assert_eq!(detail.cache_hit_rate(), 0.0);
    }

    #[test]
    fn test_model_token_detail_cache_hit_rate_full() {
        let mut detail = ModelTokenDetail::default();
        detail.input_tokens = 0;
        detail.cache_read_input_tokens = 1000;
        assert_eq!(detail.cache_hit_rate(), 1.0);
    }

    #[test]
    fn test_model_token_detail_total_tool_requests() {
        let mut detail = ModelTokenDetail::default();
        detail.tool_call_count = 50;
        detail.web_search_requests = 10;
        detail.web_fetch_requests = 5;
        assert_eq!(detail.total_tool_requests(), 65);
    }

    #[test]
    fn test_model_token_detail_total_tool_requests_zero() {
        let detail = ModelTokenDetail::default();
        assert_eq!(detail.total_tool_requests(), 0);
    }

    #[test]
    fn test_view_data_default() {
        let vd = ViewData::default();
        assert_eq!(vd.total_sessions, 0);
        assert_eq!(vd.total_messages, 0);
        assert_eq!(vd.total_tokens, 0);
        assert!(vd.model_tokens.is_empty());
        assert!(vd.model_token_detail.is_empty());
        assert!(vd.project_tokens.is_empty());
        assert!(vd.daily_activity.is_empty());
        assert_eq!(vd.peak_period, "");
    }
}