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(