Skip to main content

intent_engine/dashboard/
cli_notifier.rs

1/// CLI Notifier - HTTP client for notifying Dashboard of CLI operations
2///
3/// This module provides a simple HTTP notification mechanism to inform the
4/// Dashboard when CLI commands modify the database. The Dashboard will then
5/// broadcast changes to connected WebSocket clients for real-time UI updates.
6use std::time::Duration;
7
8/// Default Dashboard port
9const DASHBOARD_PORT: u16 = 11391;
10
11/// Notification message types
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
13#[serde(tag = "type", rename_all = "snake_case")]
14pub enum NotificationMessage {
15    /// Task was created/updated/deleted
16    TaskChanged {
17        task_id: Option<i64>,
18        operation: String,
19        /// Project path that sent this notification
20        project_path: Option<String>,
21    },
22    /// Event was added
23    EventAdded {
24        task_id: i64,
25        event_id: i64,
26        /// Project path that sent this notification
27        project_path: Option<String>,
28    },
29    /// Workspace state changed (current_task_id updated)
30    WorkspaceChanged {
31        current_task_id: Option<i64>,
32        /// Project path that sent this notification
33        project_path: Option<String>,
34    },
35}
36
37/// CLI Notifier for sending notifications to Dashboard
38pub struct CliNotifier {
39    base_url: String,
40    client: reqwest::Client,
41}
42
43impl CliNotifier {
44    /// Create a new CLI notifier
45    pub fn new() -> Self {
46        let base_url = std::env::var("IE_DASHBOARD_BASE_URL")
47            .unwrap_or_else(|_| format!("http://127.0.0.1:{}", DASHBOARD_PORT));
48        Self::with_base_url(base_url)
49    }
50
51    /// Create a CLI notifier with custom base_url (for testing or custom config)
52    pub fn with_base_url(base_url: String) -> Self {
53        let client = reqwest::Client::builder()
54            .timeout(Duration::from_millis(100)) // Short timeout - don't block CLI
55            .build()
56            .unwrap_or_else(|_| reqwest::Client::new());
57
58        Self { base_url, client }
59    }
60
61    /// Create a CLI notifier with custom port (for testing)
62    pub fn with_port(port: u16) -> Self {
63        let base_url = format!("http://127.0.0.1:{}", port);
64        Self::with_base_url(base_url)
65    }
66
67    /// Send a notification to Dashboard (fire-and-forget, non-blocking)
68    pub async fn notify(&self, message: NotificationMessage) {
69        // Check: Environment variable to disable notifications
70        if std::env::var("IE_DISABLE_DASHBOARD_NOTIFICATIONS")
71            .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
72            .unwrap_or(false)
73        {
74            tracing::debug!(
75                "Dashboard notifications disabled via IE_DISABLE_DASHBOARD_NOTIFICATIONS"
76            );
77            return; // Skip all notification logic
78        }
79
80        let url = format!("{}/api/internal/cli-notify", self.base_url);
81
82        // Send notification - short timeout to avoid blocking CLI for too long
83        if let Err(e) = self.client.post(&url).json(&message).send().await {
84            tracing::debug!("Failed to notify Dashboard: {}", e);
85            // Silently ignore errors - Dashboard might not be running
86        }
87    }
88
89    /// Notify about task change
90    pub async fn notify_task_changed(
91        &self,
92        task_id: Option<i64>,
93        operation: &str,
94        project_path: Option<String>,
95    ) {
96        self.notify(NotificationMessage::TaskChanged {
97            task_id,
98            operation: operation.to_string(),
99            project_path,
100        })
101        .await;
102    }
103
104    /// Notify about event added
105    pub async fn notify_event_added(
106        &self,
107        task_id: i64,
108        event_id: i64,
109        project_path: Option<String>,
110    ) {
111        self.notify(NotificationMessage::EventAdded {
112            task_id,
113            event_id,
114            project_path,
115        })
116        .await;
117    }
118
119    /// Notify about workspace change
120    pub async fn notify_workspace_changed(
121        &self,
122        current_task_id: Option<i64>,
123        project_path: Option<String>,
124    ) {
125        self.notify(NotificationMessage::WorkspaceChanged {
126            current_task_id,
127            project_path,
128        })
129        .await;
130    }
131}
132
133impl Default for CliNotifier {
134    fn default() -> Self {
135        Self::new()
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn test_notifier_creation() {
145        let notifier = CliNotifier::new();
146        assert_eq!(notifier.base_url, "http://127.0.0.1:11391");
147    }
148
149    #[test]
150    fn test_notifier_with_custom_port() {
151        let notifier = CliNotifier::with_port(8080);
152        assert_eq!(notifier.base_url, "http://127.0.0.1:8080");
153    }
154
155    #[tokio::test]
156    async fn test_notify_non_blocking() {
157        // This should not panic even if Dashboard is not running
158        let notifier = CliNotifier::with_port(65000); // Port not in use
159        notifier
160            .notify_task_changed(Some(42), "created", Some("/test/path".to_string()))
161            .await;
162
163        // Should return immediately (fire-and-forget)
164        // No assertions needed - test passes if no panic
165    }
166}