Skip to main content

track/webui/
state.rs

1//! Application state shared across handlers.
2
3use crate::db::{Database, SectionRevs};
4use crate::utils::Result;
5use std::sync::Arc;
6use std::time::Duration;
7use tokio::sync::{broadcast, Mutex};
8
9/// Event types broadcast via SSE
10#[derive(Clone, Debug, serde::Serialize)]
11#[serde(tag = "type", rename_all = "snake_case")]
12pub enum SseEvent {
13    /// Task header (name, alias) was updated
14    Header,
15    /// Description was updated
16    Description,
17    /// Ticket was updated
18    Ticket,
19    /// Links were updated
20    Links,
21    /// TODOs were updated
22    Todos,
23    /// Scraps were updated
24    Scraps,
25    /// Worktrees were updated
26    Worktrees,
27    /// Repositories were updated
28    Repos,
29}
30
31/// State snapshot for change detection using revision numbers
32#[derive(Clone, Debug, PartialEq)]
33struct ChangeState {
34    current_task_id: Option<i64>,
35    revs: SectionRevs,
36}
37
38/// Shared argument state
39#[derive(Clone)]
40pub struct AppState {
41    /// Database connection wrapped for async access
42    pub db: Arc<Mutex<Database>>,
43    /// Broadcast channel for SSE events
44    pub sse_tx: broadcast::Sender<SseEvent>,
45    /// Last known state for change detection
46    last_state: Arc<Mutex<Option<ChangeState>>>,
47}
48
49impl AppState {
50    /// Create application state backed by an existing database (for tests).
51    pub fn from_database(db: Database) -> Self {
52        let (sse_tx, _) = broadcast::channel(100);
53
54        Self {
55            db: Arc::new(Mutex::new(db)),
56            sse_tx,
57            last_state: Arc::new(Mutex::new(None)),
58        }
59    }
60
61    /// Create new application state with database connection
62    pub fn new() -> Result<Self> {
63        let db = Database::new()?;
64        let (sse_tx, _) = broadcast::channel(100);
65
66        Ok(Self {
67            db: Arc::new(Mutex::new(db)),
68            sse_tx,
69            last_state: Arc::new(Mutex::new(None)),
70        })
71    }
72
73    /// Broadcast an SSE event to all connected clients
74    pub fn broadcast(&self, event: SseEvent) {
75        // Ignore send errors (no receivers connected)
76        let _ = self.sse_tx.send(event);
77    }
78
79    /// Get current change state (task ID and all revision numbers)
80    async fn get_change_state(&self) -> Result<ChangeState> {
81        let db = self.db.lock().await;
82        let current_task_id = db.get_current_task_id()?;
83        let revs = db.get_all_revs()?;
84
85        Ok(ChangeState {
86            current_task_id,
87            revs,
88        })
89    }
90
91    /// Broadcast all section events (used on task switch)
92    fn broadcast_all(&self) {
93        self.broadcast(SseEvent::Header);
94        self.broadcast(SseEvent::Description);
95        self.broadcast(SseEvent::Ticket);
96        self.broadcast(SseEvent::Links);
97        self.broadcast(SseEvent::Todos);
98        self.broadcast(SseEvent::Scraps);
99        self.broadcast(SseEvent::Repos);
100        self.broadcast(SseEvent::Worktrees);
101    }
102
103    /// Start background task to detect database changes
104    pub async fn start_change_detection(&self) {
105        let mut interval = tokio::time::interval(Duration::from_secs(1));
106
107        // Initialize with current state
108        {
109            let mut last = self.last_state.lock().await;
110            if last.is_none() {
111                if let Ok(initial_state) = self.get_change_state().await {
112                    *last = Some(initial_state);
113                }
114            }
115        }
116
117        loop {
118            interval.tick().await;
119
120            // Get current state
121            let current = match self.get_change_state().await {
122                Ok(state) => state,
123                Err(e) => {
124                    eprintln!("Error getting change state: {}", e);
125                    continue;
126                }
127            };
128
129            // Compare with last state and broadcast specific events
130            let mut last = self.last_state.lock().await;
131
132            if let Some(ref prev) = *last {
133                // Check if current task changed (task switch or new task)
134                if current.current_task_id != prev.current_task_id {
135                    // Task switched - reload all sections
136                    self.broadcast_all();
137                } else {
138                    // Same task - check for specific rev changes
139                    if current.revs.task != prev.revs.task {
140                        // Task metadata changed (description, ticket, or alias)
141                        self.broadcast(SseEvent::Header);
142                        self.broadcast(SseEvent::Description);
143                        self.broadcast(SseEvent::Ticket);
144                    }
145
146                    if current.revs.links != prev.revs.links {
147                        self.broadcast(SseEvent::Links);
148                    }
149
150                    // TODOs are affected by both todos and worktrees revisions
151                    if current.revs.todos != prev.revs.todos
152                        || current.revs.worktrees != prev.revs.worktrees
153                    {
154                        self.broadcast(SseEvent::Todos);
155                    }
156
157                    if current.revs.repos != prev.revs.repos {
158                        self.broadcast(SseEvent::Repos);
159                    }
160
161                    if current.revs.scraps != prev.revs.scraps {
162                        self.broadcast(SseEvent::Scraps);
163                    }
164                }
165            }
166
167            // Update last state
168            *last = Some(current);
169        }
170    }
171}