Skip to main content

oxios_markdown/
stats.rs

1//! Stats: today's completion report.
2//!
3//! Ported from files.md (`server/stats/stats.go`) by Artem Zakirullin.
4
5use crate::fs::{VirtualFs, display_name, is_checklist_item};
6use crate::types::{DIR_ARCHIVE, FileEntry, FsError};
7
8/// A completed item shown in today's report.
9#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
10pub struct CompletedItem {
11    /// The display name (capitalized, no extension).
12    pub display_name: String,
13    /// Whether this is a checklist item.
14    pub is_checklist: bool,
15}
16
17/// Today's completion report.
18#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
19pub struct TodayReport {
20    /// Items completed today.
21    pub completed_items: Vec<CompletedItem>,
22    /// Total number of archived files (all time).
23    pub total_done: usize,
24}
25
26/// Get list of files done today (ctime > midnight UTC).
27pub fn done_today(fs: &VirtualFs) -> Result<Vec<FileEntry>, FsError> {
28    let all = fs.files_and_dirs(DIR_ARCHIVE)?;
29    let midnight = beginning_of_day_utc();
30    Ok(all
31        .into_iter()
32        .filter(|f| !f.is_dir && f.ctime > midnight)
33        .collect())
34}
35
36/// Get today's completion report: files done today + total archived count.
37pub fn today_report(fs: &VirtualFs) -> Result<TodayReport, FsError> {
38    let today_files = done_today(fs)?;
39    let all_archived = fs.files_and_dirs(DIR_ARCHIVE)?;
40    let total_done = all_archived.iter().filter(|f| !f.is_dir).count();
41
42    let completed_items: Vec<CompletedItem> = today_files
43        .iter()
44        .map(|f| {
45            let is_checklist = is_checklist_item(&f.name);
46            CompletedItem {
47                display_name: display_name(&f.name),
48                is_checklist,
49            }
50        })
51        .collect();
52
53    Ok(TodayReport {
54        completed_items,
55        total_done,
56    })
57}
58
59/// Format today's report as a string (matching Go output format).
60pub fn format_today_report(report: &TodayReport) -> String {
61    let mut lines: Vec<String> = Vec::new();
62    for item in &report.completed_items {
63        let emoji = if item.is_checklist { "ā˜‘ļø" } else { "āœ…" };
64        lines.push(format!("{} <b>{}</b>", emoji, item.display_name));
65    }
66    lines.push(format!("\nšŸ“Š {} tasks done in total", report.total_done));
67    lines.join("\n")
68}
69
70/// Returns the Unix timestamp (milliseconds) of midnight UTC today.
71fn beginning_of_day_utc() -> i64 {
72    use chrono::Utc;
73    let now = Utc::now();
74    let midnight = now
75        .date_naive()
76        .and_hms_opt(0, 0, 0)
77        .expect("midnight is always a valid time");
78    let dt = midnight.and_utc();
79    dt.timestamp_millis()
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use tempfile::TempDir;
86
87    fn setup() -> (VirtualFs, TempDir) {
88        let dir = TempDir::new().unwrap();
89        let fs = VirtualFs::new(dir.path().to_path_buf()).unwrap();
90        (fs, dir)
91    }
92
93    #[test]
94    fn test_done_today_empty() {
95        let (fs, _t) = setup();
96        let result = done_today(&fs).unwrap();
97        assert!(result.is_empty());
98    }
99
100    #[test]
101    fn test_today_report_empty() {
102        let (fs, _t) = setup();
103        let report = today_report(&fs).unwrap();
104        assert!(report.completed_items.is_empty());
105        assert_eq!(report.total_done, 0);
106    }
107
108    #[test]
109    fn test_today_report_with_files() {
110        let (fs, _t) = setup();
111        fs.write(DIR_ARCHIVE, "MyTask.md", "content").unwrap();
112        let report = today_report(&fs).unwrap();
113        assert_eq!(report.completed_items.len(), 1);
114        assert_eq!(report.total_done, 1);
115        assert_eq!(report.completed_items[0].display_name, "MyTask");
116        assert!(!report.completed_items[0].is_checklist);
117    }
118
119    #[test]
120    fn test_format_today_report() {
121        let report = TodayReport {
122            completed_items: vec![CompletedItem {
123                display_name: "Rust".into(),
124                is_checklist: false,
125            }],
126            total_done: 5,
127        };
128        let formatted = format_today_report(&report);
129        assert!(formatted.contains("āœ…"));
130        assert!(formatted.contains("<b>Rust</b>"));
131        assert!(formatted.contains("šŸ“Š 5 tasks done in total"));
132    }
133}