Skip to main content

synheart_sensor_agent/transparency/
log.rs

1//! Privacy-preserving transparency log.
2//!
3//! This module tracks and exposes statistics about data collection
4//! without storing any personal or identifying information.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Arc;
11
12/// Transparency statistics for the current session.
13#[derive(Debug)]
14pub struct TransparencyLog {
15    /// Number of keyboard events processed
16    keyboard_events: AtomicU64,
17    /// Number of mouse events processed
18    mouse_events: AtomicU64,
19    /// Number of shortcut events processed
20    shortcut_events: AtomicU64,
21    /// Session start time
22    session_start: DateTime<Utc>,
23    /// Path for persisting stats
24    persist_path: Option<PathBuf>,
25}
26
27impl TransparencyLog {
28    /// Create a new transparency log.
29    pub fn new() -> Self {
30        Self {
31            keyboard_events: AtomicU64::new(0),
32            mouse_events: AtomicU64::new(0),
33            shortcut_events: AtomicU64::new(0),
34            session_start: Utc::now(),
35            persist_path: None,
36        }
37    }
38
39    /// Create a transparency log with persistence.
40    pub fn with_persistence(path: PathBuf) -> Self {
41        let mut log = Self::new();
42        log.persist_path = Some(path);
43
44        // Try to load existing stats
45        if let Err(e) = log.load() {
46            eprintln!("Note: Could not load previous transparency stats: {e}");
47        }
48
49        log
50    }
51
52    /// Record a keyboard event.
53    pub fn record_keyboard_event(&self) {
54        self.keyboard_events.fetch_add(1, Ordering::Relaxed);
55    }
56
57    /// Record multiple keyboard events.
58    pub fn record_keyboard_events(&self, count: u64) {
59        self.keyboard_events.fetch_add(count, Ordering::Relaxed);
60    }
61
62    /// Record a mouse event.
63    pub fn record_mouse_event(&self) {
64        self.mouse_events.fetch_add(1, Ordering::Relaxed);
65    }
66
67    /// Record multiple mouse events.
68    pub fn record_mouse_events(&self, count: u64) {
69        self.mouse_events.fetch_add(count, Ordering::Relaxed);
70    }
71
72    /// Record a shortcut event.
73    pub fn record_shortcut_event(&self) {
74        self.shortcut_events.fetch_add(1, Ordering::Relaxed);
75    }
76
77    /// Get the current statistics.
78    pub fn stats(&self) -> TransparencyStats {
79        TransparencyStats {
80            keyboard_events: self.keyboard_events.load(Ordering::Relaxed),
81            mouse_events: self.mouse_events.load(Ordering::Relaxed),
82            shortcut_events: self.shortcut_events.load(Ordering::Relaxed),
83            session_start: self.session_start,
84            session_duration_secs: (Utc::now() - self.session_start).num_seconds() as u64,
85        }
86    }
87
88    /// Get a summary string for display.
89    pub fn summary(&self) -> String {
90        let stats = self.stats();
91        format!(
92            "Session Statistics:\n\
93             - Keyboard events processed: {}\n\
94             - Mouse events processed: {}\n\
95             - Shortcut events processed: {}\n\
96             - Session duration: {} seconds\n\
97             \n\
98             Privacy Guarantee:\n\
99             - No key content captured\n\
100             - No cursor coordinates captured\n\
101             - Only timing, categories, and magnitude data retained",
102            stats.keyboard_events,
103            stats.mouse_events,
104            stats.shortcut_events,
105            stats.session_duration_secs
106        )
107    }
108
109    /// Save stats to disk.
110    pub fn save(&self) -> Result<(), std::io::Error> {
111        if let Some(ref path) = self.persist_path {
112            // Ensure parent directory exists
113            if let Some(parent) = path.parent() {
114                std::fs::create_dir_all(parent)?;
115            }
116
117            let stats = self.stats();
118            let persisted = PersistedStats {
119                keyboard_events: stats.keyboard_events,
120                mouse_events: stats.mouse_events,
121                shortcut_events: stats.shortcut_events,
122                last_updated: Utc::now(),
123            };
124
125            let json = serde_json::to_string_pretty(&persisted).map_err(std::io::Error::other)?;
126
127            std::fs::write(path, json)?;
128        }
129        Ok(())
130    }
131
132    /// Load stats from disk.
133    fn load(&mut self) -> Result<(), std::io::Error> {
134        if let Some(ref path) = self.persist_path {
135            if path.exists() {
136                let content = std::fs::read_to_string(path)?;
137                let persisted: PersistedStats =
138                    serde_json::from_str(&content).map_err(std::io::Error::other)?;
139
140                self.keyboard_events
141                    .store(persisted.keyboard_events, Ordering::Relaxed);
142                self.mouse_events
143                    .store(persisted.mouse_events, Ordering::Relaxed);
144                self.shortcut_events
145                    .store(persisted.shortcut_events, Ordering::Relaxed);
146            }
147        }
148        Ok(())
149    }
150
151    /// Reset all counters.
152    pub fn reset(&self) {
153        self.keyboard_events.store(0, Ordering::Relaxed);
154        self.mouse_events.store(0, Ordering::Relaxed);
155        self.shortcut_events.store(0, Ordering::Relaxed);
156    }
157}
158
159impl Default for TransparencyLog {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165/// Snapshot of transparency statistics.
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct TransparencyStats {
168    /// Total keyboard events captured this session.
169    pub keyboard_events: u64,
170    /// Total mouse events captured this session.
171    pub mouse_events: u64,
172    /// Total shortcut events captured this session.
173    pub shortcut_events: u64,
174    /// When the current session started.
175    pub session_start: DateTime<Utc>,
176    /// Duration of the current session in seconds.
177    pub session_duration_secs: u64,
178}
179
180/// Stats format for persistence.
181#[derive(Debug, Serialize, Deserialize)]
182struct PersistedStats {
183    keyboard_events: u64,
184    mouse_events: u64,
185    #[serde(default)]
186    shortcut_events: u64,
187    last_updated: DateTime<Utc>,
188}
189
190/// Thread-safe shared transparency log.
191pub type SharedTransparencyLog = Arc<TransparencyLog>;
192
193/// Create a new shared transparency log.
194pub fn create_shared_log() -> SharedTransparencyLog {
195    Arc::new(TransparencyLog::new())
196}
197
198/// Create a new shared transparency log with persistence.
199pub fn create_shared_log_with_persistence(path: PathBuf) -> SharedTransparencyLog {
200    Arc::new(TransparencyLog::with_persistence(path))
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn test_transparency_log_counting() {
209        let log = TransparencyLog::new();
210
211        log.record_keyboard_event();
212        log.record_keyboard_event();
213        log.record_mouse_event();
214
215        let stats = log.stats();
216        assert_eq!(stats.keyboard_events, 2);
217        assert_eq!(stats.mouse_events, 1);
218    }
219
220    #[test]
221    fn test_transparency_log_reset() {
222        let log = TransparencyLog::new();
223
224        log.record_keyboard_events(100);
225        log.record_mouse_events(50);
226        log.reset();
227
228        let stats = log.stats();
229        assert_eq!(stats.keyboard_events, 0);
230        assert_eq!(stats.mouse_events, 0);
231    }
232
233    #[test]
234    fn test_summary_format() {
235        let log = TransparencyLog::new();
236        let summary = log.summary();
237
238        assert!(summary.contains("Keyboard events"));
239        assert!(summary.contains("Mouse events"));
240        assert!(summary.contains("Privacy Guarantee"));
241        assert!(summary.contains("No key content captured"));
242    }
243}