Skip to main content

track/webui/
state.rs

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