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}