rssume 0.2.1

RSS middleware with AI-powered translation and summarization
use super::api::AppState;
use axum::response::Html;
use axum::{Extension, Router, routing::get};
use std::sync::Arc;
use tera::{Context, Tera};

pub fn router(state: Arc<AppState>) -> Router {
    Router::new()
        .route("/panel", get(dashboard))
        .route("/panel/feed/{name}", get(feed_detail))
        .route("/panel/settings", get(settings))
        .route("/panel/monitor", get(monitor_page))
        .route("/panel/feed/{name}/logs", get(feed_logs_page))
        .layer(Extension(state))
}

pub(super) fn tera_instance() -> Result<Tera, crate::error::AppError> {
    let mut tera = Tera::default();
    tera.add_raw_template("base.html", include_str!("../../templates/base.html"))?;
    tera.add_raw_template(
        "dashboard.html",
        include_str!("../../templates/dashboard.html"),
    )?;
    tera.add_raw_template("feed.html", include_str!("../../templates/feed.html"))?;
    tera.add_raw_template(
        "settings.html",
        include_str!("../../templates/settings.html"),
    )?;
    tera.add_raw_template("monitor.html", include_str!("../../templates/monitor.html"))?;
    tera.add_raw_template("logs.html", include_str!("../../templates/logs.html"))?;
    tera.add_raw_template(
        "partials/stats_bar.html",
        include_str!("../../templates/partials/stats_bar.html"),
    )?;
    tera.add_raw_template(
        "partials/monitor_status.html",
        include_str!("../../templates/partials/monitor_status.html"),
    )?;
    Ok(tera)
}

async fn dashboard(
    Extension(s): Extension<Arc<AppState>>,
) -> Result<Html<String>, crate::error::AppError> {
    let stats = crate::storage::all_feed_stats()?;
    let cfg = s.config.read().await;
    let mon = s.monitor.read().await;
    let tera = tera_instance()?;
    let mut ctx = Context::new();
    ctx.insert("title", "rssume Dashboard");
    ctx.insert("feeds", &cfg.feeds);
    ctx.insert("feed_statuses", &mon.feeds);
    ctx.insert("stats", &stats);
    ctx.insert("total_prompt_tokens", &mon.token_usage.total_prompt_tokens);
    ctx.insert(
        "total_completion_tokens",
        &mon.token_usage.total_completion_tokens,
    );
    Ok(Html(tera.render("dashboard.html", &ctx).map_err(|e| {
        crate::error::AppError::Storage(format!("render: {}", e))
    })?))
}

async fn feed_detail(
    Extension(s): Extension<Arc<AppState>>,
    axum::extract::Path(name): axum::extract::Path<String>,
) -> Result<Html<String>, crate::error::AppError> {
    let data = crate::storage::FeedData::load(&name)?;
    let mon = s.monitor.read().await;
    let tera = tera_instance()?;
    let mut ctx = Context::new();
    ctx.insert("title", &format!("rssume - {}", name));
    ctx.insert("feed_name", &name);
    ctx.insert("articles", &data.articles);
    ctx.insert("runtime_status", &mon.feeds.get(&name).map(|s| &s.status));
    Ok(Html(tera.render("feed.html", &ctx).map_err(|e| {
        crate::error::AppError::Storage(format!("render: {}", e))
    })?))
}

async fn monitor_page(
    Extension(s): Extension<Arc<AppState>>,
) -> Result<Html<String>, crate::error::AppError> {
    let cfg = s.config.read().await;
    let mon = s.monitor.read().await;
    let tera = tera_instance()?;
    let active = mon.active_translations();
    let recent_count: usize = mon
        .translation_logs
        .values()
        .map(|l| {
            l.iter()
                .filter(|l| {
                    matches!(
                        l.status,
                        crate::monitor::LogStatus::Completed | crate::monitor::LogStatus::Failed(_)
                    )
                })
                .count()
        })
        .sum();
    let feeds: Vec<serde_json::Value> = cfg
        .feeds
        .iter()
        .map(|f| {
            let rt = mon.feeds.get(&f.name);
            let d = crate::storage::FeedData::load(&f.name).ok();
            serde_json::json!({
                "name": f.name,
                "status": rt.map(|r| format!("{:?}", r.status)).unwrap_or_else(|| "Idle".into()),
                "articles": d.as_ref().map(|d| d.article_count()).unwrap_or(0),
                "last_fetch_at": rt.and_then(|r| r.last_fetch_at.as_ref()),
                "translating_current": match rt.map(|r| &r.status) {
                    Some(crate::monitor::FeedStatus::Translating { current, .. }) => current,
                    _ => &0u32,
                },
                "translating_total": match rt.map(|r| &r.status) {
                    Some(crate::monitor::FeedStatus::Translating { total, .. }) => total,
                    _ => &0u32,
                },
                "translating_title": match rt.map(|r| &r.status) {
                    Some(crate::monitor::FeedStatus::Translating { current_title, .. }) => current_title,
                    _ => "",
                },
            })
        })
        .collect();
    let active_translations: Vec<serde_json::Value> = active
        .iter()
        .map(|(f, l)| {
            serde_json::json!({
                "feed_name": f,
                "article_title": l.article_title,
                "stage": format!("{:?}", l.stage),
                "streamed_text": match &l.status {
                    crate::monitor::LogStatus::Streaming { tokens } => tokens.clone(),
                    _ => String::new(),
                },
            })
        })
        .collect();
    let mut ctx = Context::new();
    ctx.insert("title", "rssume Monitor");
    ctx.insert("feeds", &feeds);
    ctx.insert("active", &active_translations);
    ctx.insert("recent_count", &recent_count);
    Ok(Html(tera.render("monitor.html", &ctx).map_err(|e| {
        crate::error::AppError::Storage(format!("render: {}", e))
    })?))
}

async fn feed_logs_page(
    Extension(s): Extension<Arc<AppState>>,
    axum::extract::Path(name): axum::extract::Path<String>,
) -> Result<Html<String>, crate::error::AppError> {
    let mon = s.monitor.read().await;
    let tera = tera_instance()?;
    let mut ctx = Context::new();
    ctx.insert("title", &format!("rssume - {} logs", name));
    ctx.insert("feed_name", &name);
    ctx.insert("logs", &mon.get_logs(&name));
    Ok(Html(tera.render("logs.html", &ctx).map_err(|e| {
        crate::error::AppError::Storage(format!("render: {}", e))
    })?))
}

async fn settings() -> Result<Html<String>, crate::error::AppError> {
    let config = crate::config::Config::load().unwrap_or_else(|_| crate::config::Config {
        server: crate::config::ServerConfig {
            host: "127.0.0.1".into(),
            port: 3000,
        },
        language: crate::config::LanguageConfig {
            target: "zh_CN".into(),
        },
        llm: crate::config::LlmConfig {
            translation: crate::config::LlmProviderConfig {
                provider: "".into(),
                model: "".into(),
                api_key: "".into(),
                base_url: "".into(),
                prompt_append: None,
            },
            summary: crate::config::LlmProviderConfig {
                provider: "".into(),
                model: "".into(),
                api_key: "".into(),
                base_url: "".into(),
                prompt_append: None,
            },
        },
        feeds: vec![],
        logging: Default::default(),
    });
    let tera = tera_instance()?;
    let mut ctx = Context::new();
    ctx.insert("title", "rssume Settings");
    ctx.insert("config", &config);
    Ok(Html(tera.render("settings.html", &ctx).map_err(|e| {
        crate::error::AppError::Storage(format!("render: {}", e))
    })?))
}