Skip to main content

kasl/libs/
monitor.rs

1//! Core activity monitoring system for kasl.
2//!
3//! Implements the heart of kasl's activity tracking functionality, providing
4//! real-time monitoring of user input to automatically detect work sessions,
5//! breaks, and productivity patterns.
6//!
7//! ## Features
8//!
9//! - **Input Detection**: Low-level keyboard and mouse event capture
10//! - **State Machine**: Activity state tracking (Active/InPause)
11//! - **Timing Logic**: Configurable thresholds for activity and pause detection
12//! - **Database Integration**: Automatic workday and pause recording
13//! - **Configuration Management**: Flexible behavior customization
14//!
15//! ## Usage
16//!
17//! ```rust
18//! use kasl::libs::config::MonitorConfig;
19//! use kasl::libs::monitor::Monitor;
20//!
21//! let config = MonitorConfig {
22//!     pause_threshold: 120,
23//!     activity_threshold: 60,
24//!     poll_interval: 1000,
25//!     min_pause_duration: 30,
26//!     min_work_interval: 15,
27//!     ..Default::default()
28//! };
29//!
30//! let monitor = Monitor::new(config)?;
31//! monitor.run().await?;
32//! ```
33
34use crate::db::pauses::Pauses;
35use crate::db::workdays::Workdays;
36use crate::libs::config::MonitorConfig;
37use crate::libs::messages::Message;
38use crate::{msg_debug, msg_error, msg_info};
39use anyhow::Result;
40use chrono::{Local, NaiveDate};
41use rdev::{EventType, listen};
42use std::sync::{Arc, Mutex};
43use tokio::time::{self, Duration, Instant};
44use tracing::{Level, debug, instrument, span};
45
46/// Represents the current state of the user's activity.
47///
48/// Provides a clean, explicit way to manage the monitor's operational state.
49/// State changes are triggered by inactivity exceeding `pause_threshold` or
50/// new input activity detected.
51#[derive(Debug, Clone, Copy, PartialEq)]
52enum State {
53    /// The user is currently active and not on a pause.
54    ///
55    /// In this state, the monitor:
56    /// - Continues tracking input activity
57    /// - Updates workday end times with each activity
58    /// - Monitors for inactivity to detect pause start
59    /// - Ensures workday creation if sustained activity is detected
60    Active,
61
62    /// The user is currently on a pause due to inactivity.
63    ///
64    /// In this state, the monitor:
65    /// - Waits for activity to resume
66    /// - Does not update workday end times
67    /// - Prepares to record pause end time when activity resumes
68    /// - Resets workday start detection when returning to active
69    InPause,
70}
71
72/// The core activity monitor responsible for tracking user presence
73/// and managing workday and pause records.
74///
75/// Orchestrates all aspects of activity monitoring, from low-level input detection
76/// to high-level workday management.
77/// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
78/// ```
79///
80/// ## Thread Safety
81///
82/// The monitor uses thread-safe primitives to coordinate between:
83/// - **Input Thread**: Captures keyboard/mouse events via `rdev`
84/// - **Monitor Thread**: Runs the main monitoring loop
85/// - **Shared State**: Activity timestamps and workday tracking
86///
87/// ## Configuration Impact
88///
89/// All timing behavior is controlled by the [`MonitorConfig`]:
90/// - **Responsiveness**: Lower `poll_interval` = more responsive state changes
91/// - **Sensitivity**: Lower `pause_threshold` = more sensitive pause detection
92/// - **Workday Logic**: Higher `activity_threshold` = more deliberate workday starts
93/// - **Data Quality**: Higher `min_*` values = cleaner, less noisy data
94pub struct Monitor {
95    /// Configuration settings for the monitor, such as thresholds.
96    ///
97    /// This configuration controls all timing and behavior aspects of the
98    /// monitor. Changes to this configuration require restarting the monitor
99    /// to take effect, as the values are used throughout the monitoring loop.
100    pub config: MonitorConfig,
101
102    /// Database interface for managing pause records.
103    ///
104    /// This interface handles all pause-related database operations:
105    /// - Recording pause start times when inactivity is detected
106    /// - Recording pause end times when activity resumes
107    /// - Querying existing pause data for validation and reporting
108    pub pauses: Pauses,
109
110    /// Database interface for managing workday records.
111    ///
112    /// This interface handles workday lifecycle management:
113    /// - Creating new workday records when sustained activity is detected
114    /// - Updating workday end times as activity continues
115    /// - Querying workday status for duplicate prevention
116    pub workdays: Workdays,
117
118    /// Timestamp of the last detected user activity (keyboard, mouse).
119    ///
120    /// This timestamp is continuously updated by the input event listener
121    /// running in a separate thread. It's protected by a Mutex for thread-safe
122    /// access between the input thread and the monitoring loop.
123    ///
124    /// The timestamp is used to:
125    /// - Calculate inactivity duration for pause detection
126    /// - Determine if activity is "recent" for state transitions
127    /// - Provide timing information for workday management
128    pub last_activity: Arc<Mutex<Instant>>,
129
130    /// Optional timestamp marking the beginning of a period of sustained activity.
131    ///
132    /// This field implements a "sustained activity" detection mechanism to prevent
133    /// false workday starts from brief, accidental input events. The timestamp is:
134    /// - Set when activity begins after a period of inactivity
135    /// - Reset to None when a pause begins or workday is created
136    /// - Used to calculate activity duration for workday start logic
137    ///
138    /// ## Workday Start Logic
139    ///
140    /// A workday is created when:
141    /// 1. `activity_start` is set (continuous activity period began)
142    /// 2. Duration since `activity_start` exceeds `activity_threshold`
143    /// 3. No workday record exists for the current date
144    pub activity_start: Arc<Mutex<Option<Instant>>>,
145
146    /// The current operational state of the monitor (Active or InPause).
147    ///
148    /// This field tracks the monitor's current state and drives the main
149    /// monitoring loop logic. State transitions trigger various actions:
150    /// - Active β†’ InPause: Record pause start, reset activity tracking
151    /// - InPause β†’ Active: Record pause end, resume workday tracking
152    state: State,
153}
154
155impl Monitor {
156    /// Creates a new `Monitor` instance.
157    ///
158    /// This constructor initializes all monitor components and sets up the
159    /// background input event listener. It performs several critical setup
160    /// operations that are essential for proper monitoring functionality.
161    ///
162    /// ## Initialization Process
163    ///
164    /// 1. **Database Connections**: Establishes connections to workday and pause databases
165    /// 2. **Shared State**: Creates thread-safe containers for activity tracking
166    /// 3. **Input Listener**: Spawns background thread for keyboard/mouse monitoring
167    /// 4. **State Setup**: Initializes monitor in Active state
168    ///
169    /// ## Input Event Handling
170    ///
171    /// The constructor spawns a dedicated thread running `rdev::listen()` to capture:
172    /// - **Keyboard Events**: Key presses and releases
173    /// - **Mouse Events**: Button clicks, movements, and scroll wheel
174    /// - **Timestamp Updates**: Continuous activity timestamp maintenance
175    ///
176    /// ## Thread Architecture
177    ///
178    /// ```text
179    /// Main Thread          Input Thread
180    /// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
181    /// β”‚   Monitor   │────▢│  rdev::listen() β”‚
182    /// β”‚    Loop     β”‚     β”‚   (keyboard/    β”‚
183    /// β”‚             │◀────│     mouse)      β”‚
184    /// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
185    ///        β”‚                      β”‚
186    ///        └──── Shared State β”€β”€β”€β”€β”˜
187    ///        (last_activity, activity_start)
188    /// ```
189    ///
190    /// ## Error Handling
191    ///
192    /// The input listener includes error handling for:
193    /// - Input device access failures
194    /// - Platform-specific event capture issues
195    /// - Thread communication problems
196    ///
197    /// Errors in the input thread are logged but don't crash the main monitor,
198    /// allowing for graceful degradation when input monitoring isn't available.
199    ///
200    /// # Arguments
201    ///
202    /// * `config` - The [`MonitorConfig`] containing timing and behavior settings
203    ///
204    /// # Returns
205    ///
206    /// Returns `Ok(Monitor)` with a fully initialized monitor ready to start
207    /// the monitoring loop, or an error if database initialization fails.
208    ///
209    /// # Error Scenarios
210    ///
211    /// - **Database Connection**: Cannot connect to SQLite database files
212    /// - **Database Schema**: Database schema is incompatible or corrupted
213    /// - **File Permissions**: Cannot read/write database files
214    /// - **Resource Exhaustion**: System cannot create necessary threads or allocate memory
215    ///
216    /// # Examples
217    ///
218    /// ```rust,no_run
219    /// use kasl::libs::config::MonitorConfig;
220    /// use kasl::libs::monitor::Monitor;
221    ///
222    /// // Create monitor with default configuration
223    /// let config = MonitorConfig::default();
224    /// let monitor = Monitor::new(config)?;
225    ///
226    /// // Start monitoring
227    /// monitor.run().await?;
228    /// # Ok::<(), anyhow::Error>(())
229    /// ```
230    #[instrument(skip(config))]
231    pub fn new(config: MonitorConfig) -> Result<Self> {
232        let span = span!(Level::INFO, "monitor_init");
233        let _enter = span.enter();
234
235        debug!("Initializing monitor with config: {:?}", config);
236
237        // Initialize database connections
238        // These connections will be used throughout the monitor's lifetime
239        let pauses = Pauses::new()?;
240        let workdays = Workdays::new()?;
241
242        // Create shared state containers for cross-thread communication
243        let last_activity = Arc::new(Mutex::new(Instant::now()));
244        let activity_start = Arc::new(Mutex::new(None));
245
246        // Clone Arc references for the input event listener thread
247        // This allows the background thread to update shared state
248        let last_activity_clone = Arc::clone(&last_activity);
249        let activity_start_clone = Arc::clone(&activity_start);
250
251        // Spawn a new thread to listen for device events
252        // This ensures the main monitor loop is not blocked by event listening
253        std::thread::spawn(move || {
254            if let Err(e) = listen(move |event| match event.event_type {
255                // Monitor all types of user input for activity detection
256                EventType::KeyPress(_)
257                | EventType::KeyRelease(_)
258                | EventType::ButtonPress(_)
259                | EventType::ButtonRelease(_)
260                | EventType::MouseMove { .. }
261                | EventType::Wheel { .. } => {
262                    // Update activity tracking with current timestamp
263                    {
264                        let mut last_activity = last_activity_clone.lock().unwrap();
265                        *last_activity = Instant::now();
266                    }
267
268                    // Manage sustained activity tracking for workday detection
269                    {
270                        let mut activity_start = activity_start_clone.lock().unwrap();
271
272                        // If this is the first activity after inactivity, mark the start
273                        // This begins the sustained activity period for workday detection
274                        if activity_start.is_none() {
275                            *activity_start = Some(Instant::now());
276                        }
277                    }
278                }
279            }) {
280                // Log input listener errors but don't crash the application
281                // This allows the monitor to continue functioning even if input
282                // monitoring encounters platform-specific issues
283                msg_error!(Message::ErrorInRdevListener(format!("{:?}", e)));
284            }
285        });
286
287        Ok(Monitor {
288            config,
289            pauses,
290            workdays,
291            last_activity,
292            activity_start,
293            state: State::Active, // Initialize the monitor in the Active state
294        })
295    }
296
297    /// Runs the main monitoring loop.
298    ///
299    /// This asynchronous function implements the core monitoring logic that runs
300    /// continuously until the monitor is stopped. It orchestrates state management,
301    /// activity detection, and database operations in a coordinated fashion.
302    ///
303    /// ## Loop Architecture
304    ///
305    /// The monitoring loop operates on a fixed polling interval and performs
306    /// these operations each cycle:
307    ///
308    /// 1. **Activity Detection**: Check if recent input activity occurred
309    /// 2. **State Evaluation**: Determine appropriate state based on activity
310    /// 3. **State Transitions**: Handle transitions between Active and InPause
311    /// 4. **Workday Management**: Ensure workday records are properly maintained
312    /// 5. **Sleep**: Wait for the next polling interval
313    ///
314    /// ## State Machine Logic
315    ///
316    /// ```text
317    /// β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
318    /// β”‚                    Monitor Loop                             β”‚
319    /// β”‚                                                             β”‚
320    /// β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    Activity?    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
321    /// β”‚  β”‚   Active    │─────────No─────▢│  handle_inactivity  β”‚   β”‚
322    /// β”‚  β”‚             β”‚                 β”‚                     β”‚   β”‚
323    /// β”‚  β”‚             │◀────────────────│  (start pause)      β”‚   β”‚
324    /// β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
325    /// β”‚        β”‚                                                   β”‚
326    /// β”‚     Activity                                                β”‚
327    /// β”‚        β”‚                                                   β”‚
328    /// β”‚        β–Ό                                                   β”‚
329    /// β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                   β”‚
330    /// β”‚  β”‚ ensure_workday_     β”‚                                   β”‚
331    /// β”‚  β”‚ started             β”‚                                   β”‚
332    /// β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                   β”‚
333    /// β”‚                                                             β”‚
334    /// β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    Activity?    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
335    /// β”‚  β”‚  InPause    │─────────Yes────▢│handle_return_from_  β”‚   β”‚
336    /// β”‚  β”‚             β”‚                 β”‚pause                β”‚   β”‚
337    /// β”‚  β”‚             │◀────────────────│                     β”‚   β”‚
338    /// β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β”‚  (end pause)        β”‚   β”‚
339    /// β”‚                                  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
340    /// β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
341    /// ```
342    ///
343    /// ## Configuration-Driven Behavior
344    ///
345    /// The loop behavior is entirely controlled by the [`MonitorConfig`]:
346    /// - **`poll_interval`**: Controls loop frequency and CPU usage
347    /// - **`pause_threshold`**: Determines when inactivity becomes a pause
348    /// - **`activity_threshold`**: Controls workday start detection sensitivity
349    /// - **Special Case**: If `pause_threshold` is 0, monitoring is disabled
350    ///
351    /// ## Database Operations
352    ///
353    /// The loop performs these database operations as needed:
354    /// - **Workday Creation**: When sustained activity is detected
355    /// - **Pause Recording**: When inactivity exceeds threshold
356    /// - **Pause Completion**: When activity resumes after pause
357    /// - **Workday Updates**: Continuous end time updates during activity
358    ///
359    /// ## Error Handling
360    ///
361    /// Database errors during the monitoring loop are handled gracefully:
362    /// - Errors are logged with detailed information
363    /// - The loop continues running to avoid service interruption
364    /// - Critical errors are propagated to stop the monitor
365    ///
366    /// ## Performance Characteristics
367    ///
368    /// - **CPU Usage**: Directly proportional to polling frequency (1/poll_interval)
369    /// - **Memory Usage**: Constant, no accumulation of historical data
370    /// - **I/O Operations**: Minimal, only database writes for state changes
371    /// - **Network Usage**: None during monitoring
372    ///
373    /// # Returns
374    ///
375    /// Returns `Ok(())` when the monitoring loop is explicitly stopped,
376    /// or an error if a critical database operation fails that prevents
377    /// continued monitoring.
378    ///
379    /// # Error Scenarios
380    ///
381    /// - **Database Connection Loss**: SQLite database becomes unavailable
382    /// - **Disk Space Exhaustion**: Cannot write to database files
383    /// - **Permission Changes**: Database files become read-only
384    /// - **System Resource Exhaustion**: Cannot allocate memory for operations
385    ///
386    /// # Examples
387    ///
388    /// ```rust,no_run
389    /// use kasl::libs::config::MonitorConfig;
390    /// use kasl::libs::monitor::Monitor;
391    ///
392    /// // Start monitoring with custom configuration
393    /// let config = MonitorConfig {
394    ///     poll_interval: 1000,      // Check every second
395    ///     pause_threshold: 120,     // Pause after 2 minutes
396    ///     activity_threshold: 30,   // Workday starts after 30s
397    ///     ..Default::default()
398    /// };
399    ///
400    /// let mut monitor = Monitor::new(config)?;
401    /// monitor.run().await?; // Runs indefinitely
402    /// # Ok::<(), anyhow::Error>(())
403    /// ```
404    #[instrument(skip(self))]
405    pub async fn run(&mut self) -> Result<()> {
406        msg_info!(Message::MonitorStarted {
407            pause_threshold: self.config.pause_threshold,
408            poll_interval: self.config.poll_interval,
409            activity_threshold: self.config.activity_threshold,
410        });
411
412        // Special case: if pause threshold is 0, pauses are disabled
413        // This allows users to disable pause tracking while keeping workday tracking
414        if self.config.pause_threshold == 0 {
415            return Ok(());
416        }
417
418        // The main loop that periodically checks for activity and updates state
419        loop {
420            let activity_detected = self.detect_activity();
421            let today = Local::now().date_naive();
422
423            // State machine logic - handle state transitions based on current state and activity
424            // Use error handling to prevent database lock issues from crashing the watcher
425            match self.state {
426                // Currently active, but no recent activity detected
427                State::Active if !activity_detected => {
428                    if let Err(e) = self.handle_inactivity() {
429                        msg_error!(Message::DatabaseOperationFailed {
430                            operation: "handle_inactivity".to_string(),
431                            error: e.to_string()
432                        });
433                    }
434                }
435                // Currently in pause, but activity has resumed
436                State::InPause if activity_detected => {
437                    if let Err(e) = self.handle_return_from_pause() {
438                        msg_error!(Message::DatabaseOperationFailed {
439                            operation: "handle_return_from_pause".to_string(),
440                            error: e.to_string()
441                        });
442                    }
443                }
444                // Currently active with ongoing activity - ensure workday is tracked
445                State::Active if activity_detected => {
446                    if let Err(e) = self.ensure_workday_started(today) {
447                        msg_error!(Message::DatabaseOperationFailed {
448                            operation: "ensure_workday_started".to_string(),
449                            error: e.to_string()
450                        });
451                    }
452                }
453                // No action needed for: InPause with no activity
454                _ => {}
455            }
456
457            // Wait for the configured poll interval before the next check
458            // This controls the monitoring loop frequency and CPU usage
459            time::sleep(Duration::from_millis(self.config.poll_interval)).await;
460        }
461    }
462
463    /// Checks if any user activity has been detected within the last poll interval.
464    ///
465    /// This method implements the core activity detection logic that drives
466    /// the monitor's state machine. It determines whether the user is currently
467    /// active based on the recency of input events captured by the background
468    /// input listener thread.
469    ///
470    /// ## Detection Logic
471    ///
472    /// Activity is considered "detected" when the time elapsed since the last
473    /// input event is less than the polling interval. This approach ensures
474    /// that activity detection is synchronized with the monitoring loop's
475    /// polling frequency.
476    ///
477    /// ```text
478    /// Timeline: ───●───●─────────●──────────────────▢
479    ///          input input   poll               now
480    ///                          ↑
481    ///                   elapsed < poll_interval
482    ///                     = Activity Detected
483    /// ```
484    ///
485    /// ## Sensitivity Tuning
486    ///
487    /// The detection sensitivity can be adjusted through configuration:
488    /// - **Lower `poll_interval`**: More sensitive, detects brief activity
489    /// - **Higher `poll_interval`**: Less sensitive, requires sustained activity
490    ///
491    /// ## Thread Safety
492    ///
493    /// This method safely accesses the shared `last_activity` timestamp
494    /// that is continuously updated by the input listener thread. The mutex
495    /// protection ensures data consistency without blocking the input thread.
496    ///
497    /// ## Debug Logging
498    ///
499    /// When debug mode is enabled (`KASL_DEBUG=1`), this method logs detailed
500    /// timing information to help with troubleshooting and configuration tuning:
501    /// - Elapsed time since last activity
502    /// - Activity detection result
503    /// - Timing relationship to poll interval
504    ///
505    /// # Returns
506    ///
507    /// Returns `true` if user activity was detected within the last poll interval,
508    /// `false` if the user appears to be inactive.
509    ///
510    /// # Performance Considerations
511    ///
512    /// This method is called once per polling cycle and performs minimal work:
513    /// - Single mutex lock/unlock operation
514    /// - Simple timestamp arithmetic
515    /// - Optional debug logging
516    ///
517    /// The implementation is designed for frequent calling with minimal overhead.
518    ///
519    /// # Examples
520    ///
521    /// ```rust,no_run
522    /// use kasl::libs::monitor::Monitor;
523    /// use kasl::libs::config::MonitorConfig;
524    ///
525    /// let monitor = Monitor::new(MonitorConfig::default())?;
526    ///
527    /// // Check for activity
528    /// if monitor.detect_activity() {
529    ///     println!("User is active");
530    /// } else {
531    ///     println!("User appears inactive");
532    /// }
533    /// # Ok::<(), anyhow::Error>(())
534    /// ```
535    pub fn detect_activity(&self) -> bool {
536        let elapsed = self.last_activity.lock().unwrap().elapsed();
537
538        // Activity is considered detected if the time since last_activity
539        // is less than the poll_interval. This creates a sliding window
540        // of activity detection that aligns with the monitoring loop timing.
541        let is_active = elapsed < Duration::from_millis(self.config.poll_interval);
542
543        // Debug logging only visible with KASL_DEBUG=1
544        // This helps with configuration tuning and troubleshooting
545        msg_debug!(format!(
546            "Activity check: elapsed={:?}, active={}, threshold={:?}",
547            elapsed,
548            is_active,
549            Duration::from_millis(self.config.poll_interval)
550        ));
551
552        is_active
553    }
554
555    /// Handles the scenario when user inactivity is detected.
556    ///
557    /// This method implements the transition from Active to InPause state when
558    /// the user has been inactive for longer than the configured pause threshold.
559    /// It manages both the state transition and the database recording of the
560    /// pause event.
561    ///
562    /// ## Inactivity Assessment
563    ///
564    /// The method calculates total inactivity duration by examining the time
565    /// elapsed since the last recorded input activity. If this duration exceeds
566    /// the configured `pause_threshold`, a pause is initiated.
567    ///
568    /// ## Pause Recording Strategy
569    ///
570    /// When recording a pause, the system uses retroactive timing to ensure
571    /// accuracy:
572    /// ```text
573    /// Timeline: ──●─────────────────●──────▢
574    ///          last              threshold
575    ///         activity            exceeded
576    ///             ↑                   ↑
577    ///      pause actually         pause
578    ///         started            detected
579    /// ```
580    ///
581    /// The pause start time is calculated as:
582    /// `current_time - pause_threshold`
583    ///
584    /// This approach ensures that the recorded pause start time reflects when
585    /// the user actually became inactive, not when the system detected it.
586    ///
587    /// ## State Management
588    ///
589    /// When transitioning to pause state, the method performs several actions:
590    /// 1. **Database Recording**: Inserts pause start record with calculated timing
591    /// 2. **State Transition**: Changes monitor state from Active to InPause
592    /// 3. **Activity Reset**: Clears the sustained activity tracker
593    ///
594    /// ## Activity Tracking Reset
595    ///
596    /// The `activity_start` timestamp is reset to `None` during pause initiation.
597    /// This is crucial for preventing incorrect workday start detection when
598    /// activity resumes after a pause. It ensures that workday logic requires
599    /// sustained activity rather than brief resume activity.
600    ///
601    /// ## User Notification
602    ///
603    /// The method provides user feedback about pause detection through
604    /// informational messages. In foreground mode, users see real-time
605    /// pause notifications for immediate feedback.
606    ///
607    /// # Returns
608    ///
609    /// Returns `Ok(())` if the pause was successfully recorded and state
610    /// transition completed, or an error if database operations fail.
611    ///
612    /// # Error Scenarios
613    ///
614    /// - **Database Connection**: Cannot connect to pause database
615    /// - **Database Write**: Cannot insert pause start record
616    /// - **Timing Calculation**: System clock issues affecting timestamp calculation
617    ///
618    /// # Database Schema Impact
619    ///
620    /// This method creates records in the `pauses` table:
621    /// ```sql
622    /// INSERT INTO pauses (date, start_time) VALUES (?, ?);
623    /// ```
624    ///
625    /// # Examples
626    ///
627    /// ```rust,no_run
628    /// // This method is called automatically by the monitoring loop
629    /// // when inactivity exceeds the configured threshold
630    ///
631    /// // Example configuration for sensitive pause detection:
632    /// let config = MonitorConfig {
633    ///     pause_threshold: 30,  // Detect pauses after 30 seconds
634    ///     ..Default::default()
635    /// };
636    /// ```
637    fn handle_inactivity(&mut self) -> Result<()> {
638        let idle_time = self.last_activity.lock().unwrap().elapsed();
639
640        // Only initiate pause if inactivity exceeds the configured threshold
641        if idle_time >= Duration::from_secs(self.config.pause_threshold) {
642            let today = Local::now().date_naive();
643            // Do not record pauses before the workday has started β€” otherwise
644            // pre-work idle creates a pause that ends seconds before workdays.start.
645            if self.workdays.fetch(today)?.is_none() {
646                return Ok(());
647            }
648
649            msg_info!(Message::PauseStarted);
650
651            // Calculate the actual pause start time by subtracting the threshold
652            // This provides more accurate pause timing in reports
653            let pause_start_time = Local::now().naive_local() - chrono::Duration::seconds(self.config.pause_threshold as i64);
654
655            // Record the pause start in the database
656            self.pauses.insert_start_with_time(pause_start_time)?;
657
658            // Transition to pause state
659            self.state = State::InPause;
660
661            // Reset the activity_start timer when a pause begins
662            // This prevents incorrect workday start detection after pause ends
663            *self.activity_start.lock().unwrap() = None;
664        }
665
666        Ok(())
667    }
668
669    /// Handles the scenario when user activity resumes after a pause.
670    ///
671    /// This method manages the transition from InPause back to Active state
672    /// when user input activity is detected after a period of inactivity.
673    /// It completes the pause record in the database and prepares the monitor
674    /// for resumed activity tracking.
675    ///
676    /// ## Pause Completion
677    ///
678    /// When activity resumes, the method completes the pause record by:
679    /// 1. Recording the current time as the pause end time
680    /// 2. Calculating the total pause duration
681    /// 3. Updating the database with the complete pause record
682    ///
683    /// ## State Transition
684    ///
685    /// The transition from InPause to Active involves:
686    /// - **Database Update**: Recording pause end time
687    /// - **State Change**: Setting monitor state to Active
688    /// - **Activity Preparation**: Resuming normal activity tracking
689    ///
690    /// ## Activity Tracking Resumption
691    ///
692    /// After returning from pause, the monitor resumes normal operation:
693    /// - Input events are again tracked for workday management
694    /// - The `activity_start` timer may be set for new sustained activity detection
695    /// - Workday end times will be updated with continued activity
696    ///
697    /// ## Timing Accuracy
698    ///
699    /// The pause end time is recorded as the current timestamp when activity
700    /// is first detected after the pause period. This provides accurate
701    /// timing for pause duration calculations in reports.
702    ///
703    /// ## User Feedback
704    ///
705    /// The method provides immediate notification about pause completion,
706    /// which is especially useful in foreground monitoring mode for
707    /// real-time activity awareness.
708    ///
709    /// # Returns
710    ///
711    /// Returns `Ok(())` if the pause end was successfully recorded and the
712    /// state transition completed, or an error if database operations fail.
713    ///
714    /// # Error Scenarios
715    ///
716    /// - **Database Connection**: Cannot connect to pause database
717    /// - **Database Update**: Cannot record pause end time
718    /// - **Inconsistent State**: No active pause record to complete
719    ///
720    /// # Database Schema Impact
721    ///
722    /// This method updates records in the `pauses` table:
723    /// ```sql
724    /// UPDATE pauses SET end_time = ? WHERE end_time IS NULL AND date = ?;
725    /// ```
726    ///
727    /// # Pause Duration Calculation
728    ///
729    /// After this method completes, the pause duration can be calculated as:
730    /// ```
731    /// duration = end_time - start_time
732    /// ```
733    ///
734    /// This duration is used in productivity reports and statistics.
735    ///
736    /// # Examples
737    ///
738    /// ```rust,no_run
739    /// // This method is called automatically by the monitoring loop
740    /// // when activity resumes after a pause period
741    ///
742    /// // The resulting pause record will include:
743    /// // - start_time: When inactivity was first detected
744    /// // - end_time: When activity resumed (current time)
745    /// // - date: The date when the pause occurred
746    /// ```
747    fn handle_return_from_pause(&mut self) -> Result<()> {
748        msg_info!(Message::PauseEnded);
749
750        // Record the pause end time in the database
751        // This completes the pause record that was started in handle_inactivity()
752        self.pauses.insert_end()?;
753
754        // Transition back to active state
755        self.state = State::Active;
756
757        Ok(())
758    }
759
760    /// Ensures that a workday record has been started for the current day
761    /// if sustained activity is detected.
762    ///
763    /// This method implements intelligent workday start detection to automatically
764    /// create workday records when the user begins sustained work activity. It
765    /// prevents false workday starts from brief, accidental input while ensuring
766    /// that genuine work sessions are properly captured.
767    ///
768    /// ## Sustained Activity Logic
769    ///
770    /// The workday start detection uses a multi-stage process:
771    ///
772    /// 1. **Activity Initiation**: `activity_start` timestamp is set by input events
773    /// 2. **Duration Check**: Calculate time elapsed since activity started
774    /// 3. **Threshold Validation**: Compare duration against `activity_threshold`
775    /// 4. **Workday Creation**: Create workday record if threshold is exceeded
776    /// 5. **Tracker Reset**: Clear `activity_start` to prevent duplicate creation
777    ///
778    /// ```text
779    /// Timeline: ──●───●───●───●──────────●─────▢
780    ///          start              threshold   now
781    ///         activity             exceeded
782    ///             ↑                   ↑
783    ///      activity_start        workday
784    ///         is set             created
785    /// ```
786    ///
787    /// ## Duplicate Prevention
788    ///
789    /// The method includes multiple safeguards against duplicate workday creation:
790    /// - **Existence Check**: Verifies no workday record exists for the date
791    /// - **Activity Validation**: Requires valid `activity_start` timestamp
792    /// - **Threshold Enforcement**: Demands sustained activity duration
793    /// - **Reset Mechanism**: Clears tracker after successful creation
794    ///
795    /// ## Configuration Impact
796    ///
797    /// The `activity_threshold` setting controls workday start sensitivity:
798    /// - **Lower Values (10-30s)**: Quick workday detection, good for focused work
799    /// - **Higher Values (60-120s)**: Conservative detection, avoids false starts
800    /// - **Very High Values (300s+)**: Only detects deliberate work sessions
801    ///
802    /// ## Database Operations
803    ///
804    /// When creating a workday, the method:
805    /// 1. Queries for existing workday records on the target date
806    /// 2. Creates a new workday record with the current timestamp
807    /// 3. Handles any database errors gracefully
808    ///
809    /// ## Error Handling
810    ///
811    /// Database errors during workday creation are logged but don't crash
812    /// the monitor. This ensures continuous monitoring even if individual
813    /// database operations encounter issues.
814    ///
815    /// ## State Management
816    ///
817    /// After successful workday creation:
818    /// - `activity_start` is reset to `None`
819    /// - Monitor continues tracking for pause detection
820    /// - Future activity updates the workday end time
821    /// - No additional workday records are created for the date
822    ///
823    /// # Arguments
824    ///
825    /// * `today` - The current date for workday record creation
826    ///
827    /// # Returns
828    ///
829    /// Returns `Ok(())` if workday management completed successfully,
830    /// or an error if critical database operations fail.
831    ///
832    /// # Error Scenarios
833    ///
834    /// - **Database Connection**: Cannot connect to workdays database
835    /// - **Database Query**: Cannot check for existing workday records
836    /// - **Database Insert**: Cannot create new workday record
837    /// - **Date Validation**: Invalid date format or system clock issues
838    ///
839    /// # Database Schema Impact
840    ///
841    /// This method may create records in the `workdays` table:
842    /// ```sql
843    /// INSERT INTO workdays (date, start_time) VALUES (?, ?);
844    /// ```
845    ///
846    /// # Examples
847    ///
848    /// ```rust,no_run
849    /// // This method is called automatically during the monitoring loop
850    /// // when the user is active and no workday exists for the current date
851    ///
852    /// // Example: User starts work at 9:00 AM
853    /// // - First input at 9:00:00 sets activity_start
854    /// // - Continued input until 9:00:30
855    /// // - If activity_threshold = 30s, workday is created at 9:00:30
856    /// // - Workday start_time is recorded as current timestamp
857    /// ```
858    pub fn ensure_workday_started(&mut self, today: NaiveDate) -> Result<()> {
859        // Check if we have an activity start timestamp
860        let activity_start_time = {
861            let activity_start_guard = self.activity_start.lock().unwrap();
862            *activity_start_guard
863        };
864
865        // Only proceed if we have been tracking sustained activity
866        if let Some(start_time) = activity_start_time {
867            // Calculate how long the current activity period has lasted
868            let activity_duration = start_time.elapsed();
869
870            // Check if sustained activity exceeds the configured threshold
871            if activity_duration >= Duration::from_secs(self.config.activity_threshold) {
872                // Verify that no workday record already exists for today
873                if self.workdays.fetch(today)?.is_none() {
874                    // Create new workday record with current timestamp
875                    match self.workdays.insert_start(today) {
876                        Ok(()) => {
877                            msg_info!(Message::WorkdayStarting(today.to_string()));
878
879                            // Reset activity_start to prevent duplicate workday creation
880                            // This ensures we only create one workday per date
881                            *self.activity_start.lock().unwrap() = None;
882                        }
883                        Err(e) => {
884                            // Log workday creation errors but continue monitoring
885                            // This prevents single database errors from stopping the monitor
886                            msg_error!(Message::WorkdayCreateFailed);
887                            debug!("Workday creation error: {:?}", e);
888                        }
889                    }
890                }
891            }
892        }
893
894        Ok(())
895    }
896}