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