Skip to content

Improve code quality: add validation, documentation, and better error handling#16

Draft
Copilot wants to merge 5 commits intomasterfrom
copilot/suggest-code-improvements
Draft

Improve code quality: add validation, documentation, and better error handling#16
Copilot wants to merge 5 commits intomasterfrom
copilot/suggest-code-improvements

Conversation

Copy link
Copy Markdown

Copilot AI commented Oct 29, 2025

Refactored codebase to improve maintainability, robustness, and user experience while preserving all existing functionality.

Changes

Code Quality

  • Fixed dead code warning and Ordering type conflict
  • Extracted magic numbers into named constants (THRESHOLD_CHECK_INTERVAL_SECS, AFK_TIMEOUT_MINUTES, etc.)
  • Removed redundant continue statements in monitoring loops
  • Applied idiomatic patterns: map_or_else() instead of map().unwrap_or_else()

Documentation

  • Added doc comments to all public functions with usage examples
  • Documented edge cases in validation logic
  • Clarified behavior of category aggregation modes

Configuration Validation

impl AlertConfig {
    /// Validate that thresholds are in ascending order
    pub fn validate_thresholds(&self) -> bool {
        if self.thresholds_minutes.len() <= 1 {
            return true;  // Nothing to order
        }
        self.thresholds_minutes.windows(2).all(|w| w[0] < w[1])
    }
}

impl NotificationConfig {
    pub fn validate(&self) -> Result<()> {
        for (idx, alert) in self.alerts.iter().enumerate() {
            if alert.thresholds_minutes.is_empty() {
                log::warn!("Alert #{} for '{}' has no thresholds", idx + 1, alert.category);
            }
            if !alert.validate_thresholds() {
                return Err(anyhow!("Alert '{}' has non-ascending thresholds", alert.category));
            }
        }
        Ok(())
    }
}

Error Messages

  • Added context to client initialization failures: includes host:port and "Is the server running?" hint
  • Server connection errors now show full connection details
  • Alert numbering uses 1-based indexing in logs for clarity
  • Service warns when all monitoring features are disabled

Edge Cases

  • validate_thresholds() explicitly handles empty and single-element arrays
  • has_any_monitoring_enabled() checks for alerts with non-empty thresholds, not just presence of alerts
  • Improved error handling in get_server_classes() with fallback to defaults

Stats: 214 insertions, 29 deletions across 1 file

Original prompt

suggest improvements to this code


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Summary by CodeRabbit

  • New Features

    • Added threshold-based activity alerts with real-time monitoring.
    • Added hourly activity summaries and new-day greeting notifications.
    • Added server status monitoring to detect ActivityWatch availability changes.
    • Enhanced notifications with category icons and improved formatting.
    • Added AFK detection for accurate activity status.
  • Bug Fixes

    • Added configuration validation to prevent invalid threshold settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 29, 2025

Walkthrough

Comprehensive extension of monitoring and notification system. Adds configuration validation, implements full CategoryAlert logic with threshold tracking, introduces background threads for hourly summaries and new-day detection, and enhances notification delivery with platform-specific support and category formatting with emoji icons.

Changes

Cohort / File(s) Summary
Configuration & Validation
src/main.rs
Extended AlertConfig with display_label() and validate_thresholds() methods; extended NotificationConfig with validate() and has_any_monitoring_enabled() methods; updated load_config() to validate configuration post-load.
Constants & Configuration
src/main.rs
Added timing constants (THRESHOLD_CHECK_INTERVAL_SECS, CACHE_TTL_SECONDS, AFK_TIMEOUT_MINUTES, NEW_DAY_CHECK_INTERVAL_SECS, NOTIFICATION_TIMEOUT_MS); renamed std::cmp::Ordering alias from cmpOrdering to CmpOrdering.
CategoryAlert Implementation
src/main.rs
Replaced placeholder CategoryAlert with full implementation including thresholds_untriggered(), time_to_next_threshold(), update(), check(), and status() methods; added threshold monitoring loop via threshold_alerts() background thread.
Background Monitoring Threads
src/main.rs
Added start_hourly() for hourly summaries, start_new_day() for new-day greetings with adaptive polling interval, and start_server_monitor() for ActivityWatch server status change alerts; integrated shutdown channel handling across all threads.
Notification System
src/main.rs
Added notify() with output-only and macOS terminal-notifier support; implemented send_checkin(), send_detailed_checkin(), send_checkin_yesterday() with top-category summarization and Unicode escape decoding; added platform-specific try_terminal_notifier() variants.
Server & Activity Detection
src/main.rs
Added check_server_availability() for server availability checks; implemented get_active_status() for AFK-aware user activity detection with configurable timeout.
Category Formatting & Aggregation
src/main.rs
Added get_category_icon() and format_category_for_notification() for emoji-enhanced category display; implemented aggregate_categories_by_top_level() and aggregate_categories_by_all_levels(); added get_top_categories(), get_top_level_categories_for_notifications(), and get_all_level_categories_for_notifications() for filtered category outputs.
Data Retrieval & Utilities
src/main.rs
Reworked get_time() with TTL cache behavior; added query_activitywatch() for data retrieval; implemented get_server_classes() with graceful fallback; added to_hms() for duration formatting and decode_unicode_escapes() placeholder for Unicode handling.

Sequence Diagram

sequenceDiagram
    participant Main as main()
    participant Config as Config<br/>Validation
    participant Service as start_service()
    participant Threads as Background<br/>Threads
    participant Check as Threshold/<br/>Monitoring
    participant Notify as Notification<br/>System

    Main->>Config: load_config()
    Config->>Config: validate()
    Config-->>Main: NotificationConfig
    
    Main->>Service: start_service()
    
    Service->>Service: has_any_monitoring_enabled()?
    alt Disabled
        Service->>Service: log warning
    end
    
    Service->>Threads: spawn start_hourly()
    Service->>Threads: spawn start_new_day()
    Service->>Threads: spawn start_server_monitor()
    Service->>Check: spawn threshold_alerts()
    
    par Background Operations
        Threads->>Notify: send_checkin() / send_detailed_checkin()
        Notify->>Notify: get_top_categories()
        Notify-->>Threads: Result<()>
        
        Check->>Check: loop every THRESHOLD_CHECK_INTERVAL_SECS
        Check->>Check: evaluate thresholds
        alt Threshold Triggered
            Check->>Notify: notify()
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • CategoryAlert implementation: Full rewrite replacing placeholder; requires verification of threshold logic, time calculations, and notification trigger conditions.
  • Background thread orchestration: Three new concurrent threads (hourly, new-day, server-monitor) with shutdown channels; review for race conditions and proper resource cleanup.
  • Notification system integration: Multiple new notification methods with platform-specific code paths (macOS terminal-notifier); validate cross-platform compatibility.
  • Configuration validation flow: New validation paths in load_config() and start_service(); ensure all validation edge cases are covered.
  • Category formatting & aggregation: Multiple new aggregation and formatting functions; verify correctness of category grouping logic and emoji icon mapping.
  • Caching behavior in get_time(): TTL cache implementation requires careful review of cache invalidation logic and timing edge cases.

Possibly related PRs

  • feat: configurable alerts #14: Modifies the same configuration-driven alerting and aggregation code paths (AlertConfig/NotificationConfig validation, threshold alerting, and notification helper signatures).

Poem

🐰 Thresholds now tick, notifications fly,
New day arrives, emoji skies delight,
Hourly summaries hop through time,
AFK logic makes the alerts sublime,
Background threads spin—a grand design! 🎪

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Improve code quality: add validation, documentation, and better error handling" accurately reflects legitimate changes present in the pull request. The changeset does introduce validation methods (validate_thresholds on AlertConfig and validate on NotificationConfig), improves error handling with context-rich error messages and fallback logic, and the PR objectives confirm documentation improvements via added doc comments. However, the title provides only a partial view of the scope—it omits substantial new functionality additions including the comprehensive CategoryAlert implementation, new background threads (start_hourly, start_new_day, start_server_monitor), AFK-aware logic, notification helpers, and category formatting enhancements. The title is accurate but narrow relative to the 214-line changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch copilot/suggest-code-improvements

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI and others added 4 commits October 29, 2025 12:27
Co-authored-by: 0xbrayo <62670517+0xbrayo@users.noreply.github.com>
Co-authored-by: 0xbrayo <62670517+0xbrayo@users.noreply.github.com>
Co-authored-by: 0xbrayo <62670517+0xbrayo@users.noreply.github.com>
Co-authored-by: 0xbrayo <62670517+0xbrayo@users.noreply.github.com>
Copilot AI changed the title [WIP] Suggest improvements to existing code Improve code quality: add validation, documentation, and better error handling Oct 29, 2025
Copilot AI requested a review from 0xbrayo October 29, 2025 12:44
@0xbrayo
Copy link
Copy Markdown
Member

0xbrayo commented Oct 29, 2025

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 29, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/main.rs (2)

522-546: DST-unsafe local time construction can panic

Using Local.from_local_datetime(...).single().unwrap() can panic on DST transitions (ambiguous/nonexistent local times). Use earliest()/latest() fallback to be robust.

-            let now = Local::now();
-            let day_end = now.date_naive().and_hms_opt(0, 0, 0).unwrap();
-            let mut day_end = Local.from_local_datetime(&day_end).single().unwrap();
-            if day_end < now {
-                day_end += Duration::days(1);
-            }
-            let time_to_next_day = day_end - now + TIME_OFFSET;
+            let now = Local::now();
+            let next_midnight_naive = (now + Duration::days(1))
+                .date_naive()
+                .and_hms_opt(0, 0, 0)
+                .expect("midnight must be constructible");
+            let next_midnight = Local
+                .from_local_datetime(&next_midnight_naive)
+                .earliest()
+                .or_else(|| Local.from_local_datetime(&next_midnight_naive).latest())
+                .expect("resolve ambiguous/nonexistent local time");
+            let time_to_next_day = next_midnight - now + TIME_OFFSET;

932-939: DST-unsafe next-hour calculation

Same .single().unwrap() issue as above; use earliest()/latest() resolution.

-            let next_hour = now + Duration::hours(1);
-            let next_hour = next_hour
-                .date_naive()
-                .and_hms_opt(next_hour.hour(), 0, 0)
-                .unwrap();
-            let next_hour = Local.from_local_datetime(&next_hour).single().unwrap();
+            let nh = now + Duration::hours(1);
+            let nh_naive = nh.date_naive().and_hms_opt(nh.hour(), 0, 0).unwrap();
+            let next_hour = Local
+                .from_local_datetime(&nh_naive)
+                .earliest()
+                .or_else(|| Local.from_local_datetime(&nh_naive).latest())
+                .unwrap();
🧹 Nitpick comments (10)
src/main.rs (10)

38-43: Configurable intervals and type safety

Consider making these intervals/timeouts configurable (config/CLI/env) and representing them as chrono::Duration (or a single newtype) to avoid mixing u64/i32/chrono::Duration/std::time::Duration across call sites.


76-94: Threshold validation ergonomics

Current bool return loses context. Prefer Result<()> with the first offending index/pair to improve error messages, or return Option<(idx_left, idx_right)>. Also decide if equal thresholds should be rejected (strictly increasing) or deduped/sorted.

Confirm the intended rule: strictly increasing vs non-decreasing?


140-176: Doc/behavior mismatch in has_any_monitoring_enabled

Docs say “valid thresholds,” but code checks only non-empty. Recommend checking both non-empty and valid:

-                .any(|alert| !alert.thresholds_minutes.is_empty())
+                .any(|a| !a.thresholds_minutes.is_empty() && a.validate_thresholds())

Also consider including 1‑based index in the error message for consistency with the warning.


324-327: DRY: hostname init helper

Same hostname fallback logic appears 3x. Extract a small helper (e.g., fn init_hostname() -> String) and reuse.

Also applies to: 352-355, 381-384


547-566: Threshold notifications may be delayed; cap refresh interval

update() defers querying until predicted threshold time. If the prediction is based on stale time_spent, alerts can be late. Cap the defer with the loop tick to bound latency.

-        if now.with_timezone(&Utc) > (self.last_check + time_to_threshold) {
+        let min_poll = Duration::seconds(THRESHOLD_CHECK_INTERVAL_SECS as i64);
+        if now.with_timezone(&Utc) >= self.last_check + time_to_threshold.min(min_poll) {

660-664: Status is tracked but never logged

If you intended to log on change, add a debug line.

-            if Some(&status) != alert.last_status.as_ref() {
-                alert.last_status = Some(status);
-            }
+            if Some(&status) != alert.last_status.as_ref() {
+                log::debug!("Threshold status: {}", status);
+                alert.last_status = Some(status);
+            }

687-720: TIME_CACHE can grow unbounded

Keys include date+mode and are never evicted. Consider an LRU/TTL eviction (e.g., moka) or manual pruning to recent N entries to prevent unbounded memory growth during long runs or varied dates.


1032-1032: Avoid hard-coded “5 minutes” in log

Tie the message to NEW_DAY_CHECK_INTERVAL_SECS to keep logs consistent with config.

-            log::debug!("New day thread sleeping for 5 minutes until next check");
+            log::debug!(
+                "New day thread sleeping for {} minutes until next check",
+                NEW_DAY_CHECK_INTERVAL_SECS / 60
+            );

1209-1229: Avoid spawning “which”

You can try spawning terminal-notifier directly and interpret NotFound, or use which::which crate. Reduces extra process and shell dependency.


1265-1272: Implement Unicode unescape (optional)

If inputs can contain “\uXXXX” sequences, consider implementing actual unescaping (e.g., via serde_json round-trip or a small parser) to improve output fidelity.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8cfeb26 and 7987c99.

📒 Files selected for processing (1)
  • src/main.rs (35 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: Lint and Format Check
  • GitHub Check: Build (Linux x64)
  • GitHub Check: Build (Linux ARM64)
  • GitHub Check: Build (Windows)
  • GitHub Check: Build (macOS)
  • GitHub Check: Build (macOS)
  • GitHub Check: Build (Windows)
  • GitHub Check: Lint and Format Check
  • GitHub Check: Build (Linux ARM64)
  • GitHub Check: Build (Linux x64)
🔇 Additional comments (8)
src/main.rs (8)

18-18: Good alias to avoid name clash with atomic Ordering

Avoids confusion between cmp::Ordering and atomic::Ordering.


268-270: Config validated before use

Nice guardrail; fail-fast on invalid configs.


304-312: Better error context on client init/connect

Clear, actionable messages (host:port + hint).

Also applies to: 315-323


399-403: Good UX when idling

Warning when all monitoring is disabled is helpful.


1278-1284: Graceful fallback for classes

Nice resilience: default_classes used on missing/unparseable settings and when client not initialized.

Also applies to: 1294-1315


1451-1451: Robust float sort

partial_cmp with Equal fallback avoids panic on NaN; good call.


1198-1199: No changes required

The Notification::timeout method accepts a generic type parameter T: Into<Timeout>, which means it works with both i32 values and explicit Timeout enum variants. Your current code passing NOTIFICATION_TIMEOUT_MS directly is already correct and requires no modification.


1129-1149: The review comment is based on an incorrect assumption about the API behavior

The web search confirms that the aw-server API returns events in descending order (newest-first) when using limit=1. Specifically, the datastore's get(limit=1) returns the most recent event first, not the oldest. Therefore, calling client.get_events(..., Some(1)) and accessing events[0] correctly retrieves the latest event—the code is already correct as written and does not need the suggested change to use last().

Likely an incorrect or invalid review comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants