diff --git a/.story_kit/work/1_backlog/292_story_show_server_logs_in_web_ui.md b/.story_kit/work/1_backlog/292_story_show_server_logs_in_web_ui.md deleted file mode 100644 index c8328bd..0000000 --- a/.story_kit/work/1_backlog/292_story_show_server_logs_in_web_ui.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: "Show server logs in web UI" ---- - -# Story 292: Show server logs in web UI - -## User Story - -As a project owner using the web UI, I want to see live server logs in the interface, so that I can debug agent behavior and pipeline issues without needing terminal access. - -## Acceptance Criteria - -- [ ] Web UI has a server logs panel accessible from the main interface -- [ ] Logs stream in real-time via WebSocket or SSE -- [ ] Logs can be filtered by keyword (same as get_server_logs MCP tool's filter param) -- [ ] Log entries show timestamp and severity level -- [ ] Panel doesn't interfere with the existing pipeline board and work item views - -## Out of Scope - -- TBD diff --git a/Cargo.lock b/Cargo.lock index ca777ea..2a86839 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4026,7 +4026,7 @@ dependencies = [ "tempfile", "tokio", "tokio-tungstenite 0.29.0", - "toml 1.0.6+spec-1.1.0", + "toml 1.0.7+spec-1.1.0", "uuid", "wait-timeout", "walkdir", @@ -4367,22 +4367,22 @@ dependencies = [ "serde_spanned", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", - "winnow", + "winnow 0.7.14", ] [[package]] name = "toml" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +checksum = "dd28d57d8a6f6e458bc0b8784f8fdcc4b99a437936056fa122cb234f18656a96" dependencies = [ "indexmap", "serde_core", "serde_spanned", - "toml_datetime 1.0.0+spec-1.1.0", + "toml_datetime 1.0.1+spec-1.1.0", "toml_parser", "toml_writer", - "winnow", + "winnow 1.0.0", ] [[package]] @@ -4396,9 +4396,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.0.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" dependencies = [ "serde_core", ] @@ -4412,23 +4412,23 @@ dependencies = [ "indexmap", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", - "winnow", + "winnow 0.7.14", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.0.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.0.7+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" [[package]] name = "tower" @@ -5444,6 +5444,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" + [[package]] name = "winreg" version = "0.10.1" diff --git a/server/src/matrix/bot.rs b/server/src/matrix/bot.rs index 5d8a435..7e7cd11 100644 --- a/server/src/matrix/bot.rs +++ b/server/src/matrix/bot.rs @@ -103,11 +103,7 @@ pub fn load_history(project_root: &std::path::Path) -> HashMap() - .ok() - .map(|room_id| (room_id, v)) - }) + .filter_map(|(k, v)| k.parse::().ok().map(|room_id| (room_id, v))) .collect() } @@ -237,10 +233,7 @@ fn read_stage_items( project_root: &std::path::Path, stage_dir: &str, ) -> Vec<(String, Option)> { - let dir = project_root - .join(".story_kit") - .join("work") - .join(stage_dir); + let dir = project_root.join(".story_kit").join("work").join(stage_dir); if !dir.exists() { return Vec::new(); } @@ -252,13 +245,11 @@ fn read_stage_items( continue; } if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) { - let name = std::fs::read_to_string(&path) - .ok() - .and_then(|contents| { - crate::io::story_metadata::parse_front_matter(&contents) - .ok() - .and_then(|m| m.name) - }); + let name = std::fs::read_to_string(&path).ok().and_then(|contents| { + crate::io::story_metadata::parse_front_matter(&contents) + .ok() + .and_then(|m| m.name) + }); items.push((stem.to_string(), name)); } } @@ -408,7 +399,10 @@ pub async fn run_bot( .ok_or_else(|| "No user ID after login".to_string())? .to_owned(); - slog!("[matrix-bot] Logged in as {bot_user_id} (device: {})", login_response.device_id); + slog!( + "[matrix-bot] Logged in as {bot_user_id} (device: {})", + login_response.device_id + ); // Bootstrap cross-signing keys for E2EE verification support. // Pass the bot's password for UIA (User-Interactive Authentication) — @@ -540,7 +534,9 @@ pub async fn run_bot( agents, }; - slog!("[matrix-bot] Cryptographic identity verification is always ON — commands from unencrypted rooms or unverified devices are rejected"); + slog!( + "[matrix-bot] Cryptographic identity verification is always ON — commands from unencrypted rooms or unverified devices are rejected" + ); // Register event handlers and inject shared context. client.add_event_handler_context(ctx); @@ -679,9 +675,7 @@ pub fn parse_ambient_command( // Strip a leading @mention (handles "@localpart" and "@localpart:server"). let rest = if let Some(after_at) = lower.strip_prefix('@') { // Skip everything up to the first whitespace (the full mention token). - let word_end = after_at - .find(char::is_whitespace) - .unwrap_or(after_at.len()); + let word_end = after_at.find(char::is_whitespace).unwrap_or(after_at.len()); after_at[word_end..].trim() } else if let Some(after) = lower.strip_prefix(display_lower.as_str()) { after.trim() @@ -734,10 +728,7 @@ async fn is_reply_to_bot( /// is the correct trust model: a user is accepted when they have cross-signing /// configured, regardless of whether the bot has run an explicit verification /// ceremony with a specific device. -async fn check_sender_verified( - client: &Client, - sender: &OwnedUserId, -) -> Result { +async fn check_sender_verified(client: &Client, sender: &OwnedUserId) -> Result { let identity = client .encryption() .get_user_identity(sender) @@ -803,8 +794,9 @@ async fn on_to_device_verification_request( } break; } - VerificationRequestState::Done - | VerificationRequestState::Cancelled(_) => break, + VerificationRequestState::Done | VerificationRequestState::Cancelled(_) => { + break; + } _ => {} } } @@ -1022,11 +1014,9 @@ async fn on_room_message( slog!("[matrix-bot] Message from {sender}: {user_message}"); // Check for bot-level commands (e.g. "help") before invoking the LLM. - if let Some(response) = super::commands::try_handle_command( - &ctx.bot_name, - ctx.bot_user_id.as_str(), - &user_message, - ) { + if let Some(response) = + super::commands::try_handle_command(&ctx.bot_name, ctx.bot_user_id.as_str(), &user_message) + { slog!("[matrix-bot] Handled bot command from {sender}"); let html = markdown_to_html(&response); if let Ok(resp) = room @@ -1083,9 +1073,7 @@ async fn handle_message( // flattening history into a text prefix. let resume_session_id: Option = { let guard = ctx.history.lock().await; - guard - .get(&room_id) - .and_then(|conv| conv.session_id.clone()) + guard.get(&room_id).and_then(|conv| conv.session_id.clone()) }; // The prompt is just the current message with sender attribution. @@ -1260,7 +1248,11 @@ async fn handle_message( let conv = guard.entry(room_id).or_default(); // Store the session ID so the next turn uses --resume. - slog!("[matrix-bot] storing session_id: {:?} (was: {:?})", new_session_id, conv.session_id); + slog!( + "[matrix-bot] storing session_id: {:?} (was: {:?})", + new_session_id, + conv.session_id + ); if new_session_id.is_some() { conv.session_id = new_session_id; } @@ -2017,7 +2009,10 @@ mod tests { #[test] fn startup_announcement_uses_configured_display_name_not_hardcoded() { assert_eq!(format_startup_announcement("HAL"), "HAL is online."); - assert_eq!(format_startup_announcement("Assistant"), "Assistant is online."); + assert_eq!( + format_startup_announcement("Assistant"), + "Assistant is online." + ); } // -- extract_command (status trigger) ------------------------------------ @@ -2072,7 +2067,7 @@ mod tests { let pool = AgentPool::new_test(3001); let out = build_pipeline_status(dir.path(), &pool); - assert!(out.contains("Upcoming"), "missing Upcoming: {out}"); + assert!(out.contains("Backlog"), "missing Backlog: {out}"); assert!(out.contains("In Progress"), "missing In Progress: {out}"); assert!(out.contains("QA"), "missing QA: {out}"); assert!(out.contains("Merge"), "missing Merge: {out}"); @@ -2104,7 +2099,10 @@ mod tests { let pool = AgentPool::new_test(3001); let out = build_pipeline_status(dir.path(), &pool); - assert!(out.contains("Free Agents"), "missing Free Agents section: {out}"); + assert!( + out.contains("Free Agents"), + "missing Free Agents section: {out}" + ); } #[test] @@ -2114,8 +2112,11 @@ mod tests { let out = build_pipeline_status(dir.path(), &pool); // Stages and headers should use markdown bold (**text**). - assert!(out.contains("**Pipeline Status**"), "missing bold title: {out}"); - assert!(out.contains("**Upcoming**"), "stage should use bold: {out}"); + assert!( + out.contains("**Pipeline Status**"), + "missing bold title: {out}" + ); + assert!(out.contains("**Backlog**"), "stage should use bold: {out}"); } #[test] @@ -2157,13 +2158,19 @@ mod tests { #[test] fn ambient_command_on_with_at_mention() { let uid = make_user_id("@timmy:homeserver.local"); - assert_eq!(parse_ambient_command("@timmy ambient on", &uid, "Timmy"), Some(true)); + assert_eq!( + parse_ambient_command("@timmy ambient on", &uid, "Timmy"), + Some(true) + ); } #[test] fn ambient_command_off_with_at_mention() { let uid = make_user_id("@timmy:homeserver.local"); - assert_eq!(parse_ambient_command("@timmy ambient off", &uid, "Timmy"), Some(false)); + assert_eq!( + parse_ambient_command("@timmy ambient off", &uid, "Timmy"), + Some(false) + ); } #[test] @@ -2178,39 +2185,60 @@ mod tests { #[test] fn ambient_command_on_with_display_name() { let uid = make_user_id("@timmy:homeserver.local"); - assert_eq!(parse_ambient_command("timmy ambient on", &uid, "Timmy"), Some(true)); + assert_eq!( + parse_ambient_command("timmy ambient on", &uid, "Timmy"), + Some(true) + ); } #[test] fn ambient_command_off_with_display_name() { let uid = make_user_id("@timmy:homeserver.local"); - assert_eq!(parse_ambient_command("timmy ambient off", &uid, "Timmy"), Some(false)); + assert_eq!( + parse_ambient_command("timmy ambient off", &uid, "Timmy"), + Some(false) + ); } #[test] fn ambient_command_on_bare() { // "ambient on" without any bot mention is also recognised. let uid = make_user_id("@timmy:homeserver.local"); - assert_eq!(parse_ambient_command("ambient on", &uid, "Timmy"), Some(true)); + assert_eq!( + parse_ambient_command("ambient on", &uid, "Timmy"), + Some(true) + ); } #[test] fn ambient_command_off_bare() { let uid = make_user_id("@timmy:homeserver.local"); - assert_eq!(parse_ambient_command("ambient off", &uid, "Timmy"), Some(false)); + assert_eq!( + parse_ambient_command("ambient off", &uid, "Timmy"), + Some(false) + ); } #[test] fn ambient_command_case_insensitive() { let uid = make_user_id("@timmy:homeserver.local"); - assert_eq!(parse_ambient_command("@Timmy AMBIENT ON", &uid, "Timmy"), Some(true)); - assert_eq!(parse_ambient_command("TIMMY AMBIENT OFF", &uid, "Timmy"), Some(false)); + assert_eq!( + parse_ambient_command("@Timmy AMBIENT ON", &uid, "Timmy"), + Some(true) + ); + assert_eq!( + parse_ambient_command("TIMMY AMBIENT OFF", &uid, "Timmy"), + Some(false) + ); } #[test] fn ambient_command_unrelated_message_returns_none() { let uid = make_user_id("@timmy:homeserver.local"); - assert_eq!(parse_ambient_command("@timmy what is the status?", &uid, "Timmy"), None); + assert_eq!( + parse_ambient_command("@timmy what is the status?", &uid, "Timmy"), + None + ); assert_eq!(parse_ambient_command("hello there", &uid, "Timmy"), None); assert_eq!(parse_ambient_command("ambient", &uid, "Timmy"), None); } @@ -2237,11 +2265,17 @@ mod tests { let guard = ambient_rooms.lock().await; assert!(guard.contains(&room_a), "room_a should be in ambient mode"); - assert!(!guard.contains(&room_b), "room_b should NOT be in ambient mode"); + assert!( + !guard.contains(&room_b), + "room_b should NOT be in ambient mode" + ); drop(guard); // Disable ambient mode for room_a. ambient_rooms.lock().await.remove(&room_a); - assert!(!ambient_rooms.lock().await.contains(&room_a), "room_a ambient mode should be off"); + assert!( + !ambient_rooms.lock().await.contains(&room_a), + "room_a ambient mode should be off" + ); } }