diff --git a/server/src/dashboard.html b/server/src/dashboard.html index 41585f3..2ceccc1 100644 --- a/server/src/dashboard.html +++ b/server/src/dashboard.html @@ -895,23 +895,17 @@ // --- WebSocket Activity Feed --- const MAX_ACTIVITY_HISTORY = 100; - function saveActivityHistory(time, htmlData) { - try { - let history = JSON.parse(localStorage.getItem('activityHistory') || '[]'); - history.push({ time, data: htmlData }); - if (history.length > MAX_ACTIVITY_HISTORY) history = history.slice(-MAX_ACTIVITY_HISTORY); - localStorage.setItem('activityHistory', JSON.stringify(history)); - } catch(e) {} - } - - function loadActivityHistory() { + async function loadActivityHistory() { try { + const response = await fetch('/api/activity'); + const history = await response.json(); const feed = document.getElementById('activity-feed'); - let history = JSON.parse(localStorage.getItem('activityHistory') || '[]'); + feed.innerHTML = ''; history.forEach(item => { const div = document.createElement('div'); div.className = 'feed-entry'; - div.innerHTML = `[${item.time}] ${item.data}`; + const timeStr = new Date(item.time).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', second:'2-digit'}); + div.innerHTML = `[${timeStr}] ${item.message || item.data}`; feed.appendChild(div); }); if (history.length > 0) { @@ -931,12 +925,10 @@ if (data.type === 'activity') { const div = document.createElement('div'); div.className = 'feed-entry'; - const time = new Date().toLocaleTimeString(); - div.innerHTML = `[${time}] ${data.data}`; + const timeStr = new Date(data.data.time).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', second:'2-digit'}); + div.innerHTML = `[${timeStr}] ${data.data.message || data.data.data || data.data}`; feed.appendChild(div); - saveActivityHistory(time, data.data); - // Auto-scroll logic const isScrolledToBottom = feed.scrollHeight - feed.clientHeight <= feed.scrollTop + 20; if (isScrolledToBottom) { diff --git a/server/src/main.rs b/server/src/main.rs index dc7adfd..cb55dcf 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -225,6 +225,23 @@ async fn run_server(state: Arc) -> Result<(), Box = app_state_clone + .clients + .read() + .unwrap_or_else(|e| e.into_inner()) + .values() + .cloned() + .collect(); + for client_tx in senders { + let _ = client_tx.try_send(msg.clone()); + } + } + }); + let app = Router::new() .route( "/api/version", @@ -350,6 +367,18 @@ async fn run_server(state: Arc) -> Result<(), Box>() + }); + axum::Json(serde_json::json!(activities)) + } + }), + ) .route( "/api/stats", get({ @@ -491,30 +520,7 @@ async fn handle_socket(socket: WebSocket, state: Arc, client_type: Str .and_then(|p| p.get("name")) .and_then(|n| n.as_str()) .unwrap_or("unknown_tool"); - let activity_msg = format!("Agent executed tool: {}", name); - - let event = serde_json::json!({ - "type": "activity", - "data": activity_msg - }); - - let senders: Vec<_> = state_clone - .clients - .read() - .unwrap_or_else(|e| e.into_inner()) - .iter() - .filter_map(|(id, tx)| { - if id != &session_id_clone { - Some(tx.clone()) - } else { - None - } - }) - .collect(); - - for client_tx in senders { - let _ = client_tx.try_send(event.to_string()); - } + handler.state.broadcast_activity(&format!("Agent executed tool: {}", name)); } } // End if proxy @@ -840,6 +846,7 @@ fn main() -> Result<(), Box> { tech_debts: Store::new("tech_debts", db.clone()), gates: Store::new("gates", db.clone()), context_workspaces: Store::new("context_workspaces", db.clone()), + recent_activities: Store::new("recent_activities", db.clone()), activity_tx: tokio::sync::broadcast::channel(100).0, }); diff --git a/server/src/state.rs b/server/src/state.rs index 19624ec..6630084 100644 --- a/server/src/state.rs +++ b/server/src/state.rs @@ -27,6 +27,7 @@ pub struct MemoryState { pub tech_debts: Store>, pub gates: Store>, pub context_workspaces: Store>, + pub recent_activities: Store>, pub activity_tx: tokio::sync::broadcast::Sender, } @@ -37,9 +38,26 @@ impl MemoryState { } pub fn broadcast_activity(&self, message: &str) { + let time = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as u64; + + let item = serde_json::json!({ + "time": time, + "message": message + }); + + self.recent_activities.modify(|activities| { + activities.push_back(item.clone()); + if activities.len() > 100 { + activities.pop_front(); + } + }); + let payload = serde_json::json!({ "type": "activity", - "data": message + "data": item }) .to_string(); let _ = self.activity_tx.send(payload); diff --git a/update_main.patch b/update_main.patch new file mode 100644 index 0000000..93a0bef --- /dev/null +++ b/update_main.patch @@ -0,0 +1,25 @@ +--- server/src/main.rs ++++ server/src/main.rs +@@ -225,6 +225,20 @@ + next_id: AtomicUsize::new(1), + }); + ++ let app_state_clone = Arc::clone(&app_state); ++ let mut rx = state.activity_tx.subscribe(); ++ tokio::spawn(async move { ++ while let Ok(msg) = rx.recv().await { ++ let senders: Vec<_> = app_state_clone ++ .clients ++ .read() ++ .unwrap_or_else(|e| e.into_inner()) ++ .values() ++ .cloned() ++ .collect(); ++ for client_tx in senders { ++ let _ = client_tx.try_send(msg.clone()); ++ } ++ } ++ }); ++ + let app = Router::new() + .route(