claude-dashboard 0.2.2

A terminal TUI dashboard that renders Claude Code usage reports with token stats, project breakdowns, and language distribution.
Documentation
mod cache;
mod client;
mod config;
mod prompt;

pub use cache::{cache_dir, delete_cache, read_cache, write_cache};
pub use client::call_haiku_api;
pub use config::{load_api_config, LlmApiConfig};
pub use prompt::build_prompt;

use std::fs;
use std::path::Path;
use std::time::UNIX_EPOCH;

// ── Report type ──────────────────────────────────────────

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum LlmReportType {
    All,
    Monthly,
    Yearly,
}

// ── Source file mtime ────────────────────────────────────

/// Compute the maximum modification timestamp (Unix seconds) across all
/// source data files that could affect report content.
pub fn compute_source_mtime(claude_dir: &Path) -> u64 {
    let mut max_mtime: u64 = 0;

    let mut check = |path: &Path| {
        if let Ok(meta) = fs::metadata(path) {
            if let Ok(modified) = meta.modified() {
                if let Ok(dur) = modified.duration_since(UNIX_EPOCH) {
                    max_mtime = max_mtime.max(dur.as_secs());
                }
            }
        }
    };

    check(&claude_dir.join("stats-cache.json"));
    check(&claude_dir.join("history.jsonl"));

    let projects_dir = claude_dir.join("projects");
    if let Ok(entries) = fs::read_dir(&projects_dir) {
        for entry in entries.flatten() {
            let proj_path = entry.path();
            if !proj_path.is_dir() {
                continue;
            }
            if let Ok(sessions) = fs::read_dir(&proj_path) {
                for s in sessions.flatten() {
                    let sp = s.path();
                    if sp.extension().map(|e| e == "jsonl").unwrap_or(false) {
                        check(&sp);
                    }
                }
            }
        }
    }

    max_mtime
}

// ── Tests ────────────────────────────────────────────────

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

    #[test]
    fn test_llm_report_type_equality() {
        assert_eq!(LlmReportType::All, LlmReportType::All);
        assert_eq!(LlmReportType::Monthly, LlmReportType::Monthly);
        assert_eq!(LlmReportType::Yearly, LlmReportType::Yearly);
        assert_ne!(LlmReportType::All, LlmReportType::Monthly);
        assert_ne!(LlmReportType::Monthly, LlmReportType::Yearly);
    }

    #[test]
    fn test_compute_source_mtime_no_files() {
        let mtime = compute_source_mtime(Path::new("/nonexistent/path"));
        assert_eq!(mtime, 0);
    }

    #[test]
    fn test_compute_source_mtime_real() {
        // Use the actual claude dir if available
        let home = dirs::home_dir().unwrap_or_default();
        let claude_dir = home.join(".claude");
        if claude_dir.exists() {
            let mtime = compute_source_mtime(&claude_dir);
            assert!(mtime > 0, "Real claude dir should have files with mtime > 0");
        }
    }
}