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