wsb 0.164.718

Workspace - A tool suite for file operations, version management, and development workflow automation
use crate::prelude::*;
use axum::{extract::State, http::StatusCode, Json};


use super::fleet::{collect_issue_repos, collect_repo_statuses, workspace_root};
use super::sessions::ErrorResponse;
use crate::web::state::WebState;

#[derive(Serialize)]
pub struct OverviewStats {
    pub session_count: i64,
    pub message_count: i64,
    pub project_count: i64,
    pub fleet_repo_count: i64,
    pub open_issue_count: i64,
    pub stats_updated_at: i64,
}

#[derive(Deserialize)]
pub struct RefreshRequest {
    #[serde(default)]
    pub keys: Vec<String>,
}

fn internal_error(msg: impl std::fmt::Display) -> (StatusCode, Json<ErrorResponse>) {
    (
        StatusCode::INTERNAL_SERVER_ERROR,
        Json(ErrorResponse {
            error: msg.to_string(),
        }),
    )
}

fn stats_from_map(map: &HashMap<String, i64>) -> OverviewStats {
    OverviewStats {
        session_count: map.get("session_count").copied().unwrap_or(0),
        message_count: map.get("message_count").copied().unwrap_or(0),
        project_count: map.get("project_count").copied().unwrap_or(0),
        fleet_repo_count: map.get("fleet_repo_count").copied().unwrap_or(0),
        open_issue_count: map.get("open_issue_count").copied().unwrap_or(0),
        stats_updated_at: map.get("stats_updated_at").copied().unwrap_or(0),
    }
}

pub async fn get_overview_stats(
    State(state): State<WebState>,
) -> Result<Json<OverviewStats>, (StatusCode, Json<ErrorResponse>)> {
    let db = state.db.clone();

    let map = tokio::task::spawn_blocking(move || {
        db.session_repo.get_overview_stats()
    }).await.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {}", e)))
    .map_err(|e| internal_error(&e))?;
    Ok(Json(stats_from_map(&map)))
}

pub async fn refresh_overview_stats(
    State(state): State<WebState>,
    Json(body): Json<RefreshRequest>,
) -> Result<Json<OverviewStats>, (StatusCode, Json<ErrorResponse>)> {
    let refresh_all = body.keys.is_empty();
    let keys = &body.keys;

    if refresh_all || keys.iter().any(|k| k == "fleet") {
        let count = match workspace_root() {
            Ok(root) => match collect_repo_statuses(&root) {
                Ok(repos) => repos.len() as i64,
                Err(e) => return Err(internal_error(e)),
            },
            Err(e) => return Err(e),
        };

        let db = state.db.clone();
        tokio::task::spawn_blocking(move || {
            db.session_repo.set_stat("fleet_repo_count", count)
        }).await.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {}", e)))
        .map_err(|e| internal_error(&e))?;
    }

    if refresh_all || keys.iter().any(|k| k == "issues") {
        let count = match compute_open_issue_count(&state).await {
            Ok(n) => n,
            Err(_) => 0,
        };

        let db = state.db.clone();
        tokio::task::spawn_blocking(move || {
            db.session_repo.set_stat("open_issue_count", count)
        }).await.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {}", e)))
        .map_err(|e| internal_error(&e))?;
    }

    if refresh_all || keys.iter().any(|k| k == "sessions") {
        let db = state.db.clone();

        let stats = tokio::task::spawn_blocking(move || {
            db.session_repo.get_stats(false)
        }).await.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {}", e)))
        .map_err(|e| internal_error(&e))?;

        let db = state.db.clone();
        tokio::task::spawn_blocking(move || {
            db.session_repo.set_stat("project_count", stats.project_count)
        }).await.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {}", e)))
        .map_err(|e| internal_error(&e))?;
    }

    let now = chrono::Utc::now().timestamp();
    let db = state.db.clone();
    tokio::task::spawn_blocking(move || {
        db.session_repo.set_stat("stats_updated_at", now)
    }).await.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {}", e)))
    .map_err(|e| internal_error(&e))?;

    let db = state.db.clone();
    let map = tokio::task::spawn_blocking(move || {
        db.session_repo.get_overview_stats()
    }).await.unwrap_or_else(|e| Err(anyhow::anyhow!("join error: {}", e)))
    .map_err(|e| internal_error(&e))?;
    Ok(Json(stats_from_map(&map)))
}

async fn compute_open_issue_count(state: &WebState) -> Result<i64, anyhow::Error> {
    let github = state.github.read().await;
    let client = github
        .as_ref()
        .ok_or_else(|| anyhow::anyhow!("GitHub token not configured"))?
        .clone();
    drop(github);

    let repo_root = std::env::current_dir()?;
    let repos = collect_issue_repos(&repo_root).map_err(|e| anyhow::anyhow!("{}", e))?;

    let mut total: i64 = 0;
    for repo in &repos {
        match client
            .list_issues(&repo.github.owner, &repo.github.repo, "open")
            .await
        {
            Ok(issues) => total += issues.len() as i64,
            Err(_) => {}
        }
    }
    Ok(total)
}