use std::fs;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
const CACHE_VERSION: u32 = 2;
#[derive(Serialize, Deserialize)]
struct CachedReport {
source_mtime: u64,
generated_at: String,
content: String,
#[serde(default)]
version: u32,
}
pub fn cache_dir() -> PathBuf {
dirs::home_dir()
.unwrap_or_default()
.join(".claude-dashboard")
}
fn cache_path(key: &str) -> PathBuf {
cache_dir().join(format!("llm-{}.json", key))
}
pub fn read_cache(key: &str) -> Option<(u64, String)> {
let path = cache_path(key);
let content = fs::read_to_string(&path).ok()?;
let report: CachedReport = serde_json::from_str(&content).ok()?;
if report.version != CACHE_VERSION {
return None; }
Some((report.source_mtime, report.content))
}
pub fn write_cache(key: &str, source_mtime: u64, content: &str) -> Result<(), String> {
let dir = cache_dir();
fs::create_dir_all(&dir).map_err(|e| format!("Cannot create cache dir: {}", e))?;
let report = CachedReport {
source_mtime,
generated_at: chrono::Local::now()
.format("%Y-%m-%dT%H:%M:%S")
.to_string(),
content: content.to_string(),
version: CACHE_VERSION,
};
let json = serde_json::to_string_pretty(&report)
.map_err(|e| format!("Cannot serialize cache: {}", e))?;
fs::write(cache_path(key), json)
.map_err(|e| format!("Cannot write cache: {}", e))?;
Ok(())
}
#[allow(dead_code)]
pub fn delete_cache(key: &str) -> Result<(), String> {
let path = cache_path(key);
if path.exists() {
fs::remove_file(&path).map_err(|e| format!("Cannot delete cache: {}", e))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_key_formats() {
assert!(cache_path("all").to_string_lossy().contains("llm-all"));
assert!(cache_path("monthly_2026_05").to_string_lossy().contains("monthly_2026_05"));
assert!(cache_path("yearly_2026").to_string_lossy().contains("yearly_2026"));
}
#[test]
fn test_cache_write_read_delete() {
let key = "__test_cache_roundtrip";
let _ = delete_cache(key);
assert!(read_cache(key).is_none());
write_cache(key, 12345, "hello world").unwrap();
let (mtime, content) = read_cache(key).unwrap();
assert_eq!(mtime, 12345);
assert_eq!(content, "hello world");
delete_cache(key).unwrap();
assert!(read_cache(key).is_none());
}
#[test]
fn test_cache_overwrite() {
let key = "__test_cache_overwrite";
let _ = delete_cache(key);
write_cache(key, 100, "first").unwrap();
write_cache(key, 200, "second").unwrap();
let (mtime, content) = read_cache(key).unwrap();
assert_eq!(mtime, 200);
assert_eq!(content, "second");
delete_cache(key).unwrap();
}
#[test]
fn test_cache_version_check() {
let key = "__test_version_check";
let _ = delete_cache(key);
let dir = cache_dir();
let _ = std::fs::create_dir_all(&dir);
let old_report = serde_json::json!({
"source_mtime": 99999,
"generated_at": "2026-01-01T00:00:00",
"content": "old content",
"version": 0 });
let path = cache_path(key);
let _ = std::fs::write(&path, serde_json::to_string_pretty(&old_report).unwrap());
assert!(read_cache(key).is_none());
let _ = delete_cache(key);
}
}