bridge-echo 0.2.0

HTTP bridge for Claude Code CLI
use crate::config::Config;
use crate::tracker::RequestTracker;
use tracing::{info, warn};

pub fn spawn(tracker: RequestTracker, config: &Config) {
    let token = match &config.discord_bot_token {
        Some(t) => t.clone(),
        None => {
            info!("Discord alerts disabled (BRIDGE_ECHO_DISCORD_BOT_TOKEN not set)");
            return;
        }
    };

    let channel_id = match &config.discord_alert_channel {
        Some(c) => c.clone(),
        None => {
            info!("Discord alerts disabled (BRIDGE_ECHO_DISCORD_ALERT_CHANNEL not set)");
            return;
        }
    };

    let thresholds = config.alert_thresholds_minutes.clone();
    if thresholds.is_empty() {
        info!("Discord alerts disabled (no thresholds configured)");
        return;
    }

    info!(
        "Discord alerts enabled — thresholds: {:?} min, channel: {channel_id}",
        thresholds
    );

    tokio::spawn(async move {
        alert_loop(tracker, &token, &channel_id, &thresholds).await;
    });
}

async fn alert_loop(tracker: RequestTracker, token: &str, channel_id: &str, thresholds: &[u64]) {
    let client = reqwest::Client::new();
    let url = format!("https://discord.com/api/v10/channels/{channel_id}/messages");

    loop {
        tokio::time::sleep(std::time::Duration::from_secs(30)).await;

        let requests = tracker.active_requests_for_alerting().await;

        for (id, channel, message_preview, elapsed_secs, alerts_sent) in requests {
            let elapsed_min = elapsed_secs / 60;

            for &threshold in thresholds {
                if elapsed_min >= threshold && !alerts_sent.contains(&threshold) {
                    let msg = format!(
                        "⚠️ **bridge-echo alert** — request #{id} on `{channel}` has been running for **{elapsed_min} min**\n> {message_preview}"
                    );

                    let res = client
                        .post(&url)
                        .header("Authorization", format!("Bot {token}"))
                        .json(&serde_json::json!({ "content": msg }))
                        .send()
                        .await;

                    match res {
                        Ok(r) if r.status().is_success() => {
                            info!("Alert sent for request #{id} at {threshold}min threshold");
                        }
                        Ok(r) => {
                            warn!(
                                "Discord alert failed for request #{id}: HTTP {}",
                                r.status()
                            );
                        }
                        Err(e) => {
                            warn!("Discord alert failed for request #{id}: {e}");
                        }
                    }

                    tracker.mark_alerted(id, threshold).await;
                }
            }
        }
    }
}