synheart_sensor_agent/transparency/
log.rs1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Arc;
11
12#[derive(Debug)]
14pub struct TransparencyLog {
15 keyboard_events: AtomicU64,
17 mouse_events: AtomicU64,
19 windows_completed: AtomicU64,
21 snapshots_exported: AtomicU64,
23 session_start: DateTime<Utc>,
25 persist_path: Option<PathBuf>,
27}
28
29impl TransparencyLog {
30 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 pub fn with_persistence(path: PathBuf) -> Self {
44 let mut log = Self::new();
45 log.persist_path = Some(path);
46
47 if let Err(e) = log.load() {
49 eprintln!("Note: Could not load previous transparency stats: {e}");
50 }
51
52 log
53 }
54
55 pub fn record_keyboard_event(&self) {
57 self.keyboard_events.fetch_add(1, Ordering::Relaxed);
58 }
59
60 pub fn record_keyboard_events(&self, count: u64) {
62 self.keyboard_events.fetch_add(count, Ordering::Relaxed);
63 }
64
65 pub fn record_mouse_event(&self) {
67 self.mouse_events.fetch_add(1, Ordering::Relaxed);
68 }
69
70 pub fn record_mouse_events(&self, count: u64) {
72 self.mouse_events.fetch_add(count, Ordering::Relaxed);
73 }
74
75 pub fn record_window_completed(&self) {
77 self.windows_completed.fetch_add(1, Ordering::Relaxed);
78 }
79
80 pub fn record_snapshot_exported(&self) {
82 self.snapshots_exported.fetch_add(1, Ordering::Relaxed);
83 }
84
85 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 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 pub fn save(&self) -> Result<(), std::io::Error> {
122 if let Some(ref path) = self.persist_path {
123 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 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 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#[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#[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
202pub type SharedTransparencyLog = Arc<TransparencyLog>;
204
205pub fn create_shared_log() -> SharedTransparencyLog {
207 Arc::new(TransparencyLog::new())
208}
209
210pub 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}