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