diff --git a/server/src/config.rs b/server/src/config.rs index c8df85df..39ef3d79 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -56,6 +56,13 @@ pub struct ProjectConfig { /// Default: `true`. #[serde(default = "default_matrix_status_consumer")] pub matrix_status_consumer: bool, + /// Whether the Slack bot subscribes to the status broadcaster and forwards + /// pipeline events to its configured channels. + /// Set to `false` to silence Slack status notifications without affecting + /// other consumers (web UI, Matrix, Discord, WhatsApp, agent context). + /// Default: `true`. + #[serde(default = "default_slack_status_consumer")] + pub slack_status_consumer: bool, /// IANA timezone name (e.g. `"Europe/London"`, `"America/New_York"`). /// When set, timer HH:MM inputs are interpreted in this timezone instead /// of the container/host local time. Falls back to `chrono::Local` when absent. @@ -144,6 +151,10 @@ fn default_matrix_status_consumer() -> bool { true } +fn default_slack_status_consumer() -> bool { + true +} + fn default_max_mesh_peers() -> usize { 3 } @@ -275,6 +286,7 @@ impl Default for ProjectConfig { rate_limit_notifications: default_rate_limit_notifications(), web_ui_status_consumer: default_web_ui_status_consumer(), matrix_status_consumer: default_matrix_status_consumer(), + slack_status_consumer: default_slack_status_consumer(), timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -358,6 +370,7 @@ impl ProjectConfig { rate_limit_notifications: legacy.rate_limit_notifications, web_ui_status_consumer: default_web_ui_status_consumer(), matrix_status_consumer: default_matrix_status_consumer(), + slack_status_consumer: default_slack_status_consumer(), timezone: legacy.timezone, rendezvous: None, trusted_keys: Vec::new(), @@ -392,6 +405,7 @@ impl ProjectConfig { rate_limit_notifications: legacy.rate_limit_notifications, web_ui_status_consumer: default_web_ui_status_consumer(), matrix_status_consumer: default_matrix_status_consumer(), + slack_status_consumer: default_slack_status_consumer(), timezone: legacy.timezone, rendezvous: None, trusted_keys: Vec::new(), @@ -414,6 +428,7 @@ impl ProjectConfig { rate_limit_notifications: legacy.rate_limit_notifications, web_ui_status_consumer: default_web_ui_status_consumer(), matrix_status_consumer: default_matrix_status_consumer(), + slack_status_consumer: default_slack_status_consumer(), timezone: legacy.timezone, rendezvous: None, trusted_keys: Vec::new(), diff --git a/server/src/main.rs b/server/src/main.rs index d62a7eb5..f79d53b1 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -763,6 +763,37 @@ async fn main() -> Result<(), std::io::Error> { watcher_rx_for_slack, root.clone(), ); + + // Subscribe to the status broadcaster if the slack_status_consumer toggle + // is enabled (default: true). Formats each StatusEvent via the common + // formatter and sends the resulting text to all configured Slack channels. + // The task exits automatically when the broadcaster is dropped on shutdown. + { + use crate::service::status::format::format_status_event; + + let status_enabled = config::ProjectConfig::load(root) + .map(|c| c.slack_status_consumer) + .unwrap_or(true); + + if status_enabled { + let mut sub = ctx.services.status.subscribe(); + let transport = Arc::clone(&ctx.transport) as Arc; + let channels: Vec = ctx.channel_ids.iter().cloned().collect(); + tokio::spawn(async move { + while let Some(event) = sub.recv().await { + let plain = format_status_event(&event); + for channel in &channels { + if let Err(e) = transport.send_message(channel, &plain, "").await { + crate::slog!( + "[slack] Failed to send status event to {channel}: {e}" + ); + } + } + } + crate::slog!("[slack] Status subscriber task exiting — broadcaster dropped"); + }); + } + } } else { drop(watcher_rx_for_slack); } diff --git a/server/src/worktree.rs b/server/src/worktree.rs index c2cbdfdd..6374c941 100644 --- a/server/src/worktree.rs +++ b/server/src/worktree.rs @@ -530,6 +530,7 @@ mod tests { rate_limit_notifications: true, web_ui_status_consumer: true, matrix_status_consumer: true, + slack_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -561,6 +562,7 @@ mod tests { rate_limit_notifications: true, web_ui_status_consumer: true, matrix_status_consumer: true, + slack_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -592,6 +594,7 @@ mod tests { rate_limit_notifications: true, web_ui_status_consumer: true, matrix_status_consumer: true, + slack_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -623,6 +626,7 @@ mod tests { rate_limit_notifications: true, web_ui_status_consumer: true, matrix_status_consumer: true, + slack_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -653,6 +657,7 @@ mod tests { rate_limit_notifications: true, web_ui_status_consumer: true, matrix_status_consumer: true, + slack_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -690,6 +695,7 @@ mod tests { rate_limit_notifications: true, web_ui_status_consumer: true, matrix_status_consumer: true, + slack_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -768,6 +774,7 @@ mod tests { rate_limit_notifications: true, web_ui_status_consumer: true, matrix_status_consumer: true, + slack_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -804,6 +811,7 @@ mod tests { rate_limit_notifications: true, web_ui_status_consumer: true, matrix_status_consumer: true, + slack_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -887,6 +895,7 @@ mod tests { rate_limit_notifications: true, web_ui_status_consumer: true, matrix_status_consumer: true, + slack_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -926,6 +935,7 @@ mod tests { rate_limit_notifications: true, web_ui_status_consumer: true, matrix_status_consumer: true, + slack_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -955,6 +965,7 @@ mod tests { rate_limit_notifications: true, web_ui_status_consumer: true, matrix_status_consumer: true, + slack_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(), @@ -990,6 +1001,7 @@ mod tests { rate_limit_notifications: true, web_ui_status_consumer: true, matrix_status_consumer: true, + slack_status_consumer: true, timezone: None, rendezvous: None, trusted_keys: Vec::new(),