perspt-dashboard 0.5.7

Real-time web dashboard for Perspt agent monitoring
Documentation
use askama::Template;
use axum::extract::{Query, State};
use axum::response::{Html, IntoResponse};

use crate::error::DashboardError;
use crate::state::AppState;
use crate::views::overview::{GlobalStats, OverviewViewModel, SessionSummary};

const PAGE_SIZE: usize = 20;

#[derive(serde::Deserialize)]
pub struct PaginationParams {
    pub page: Option<usize>,
}

#[derive(Template)]
#[template(path = "pages/overview.html")]
struct OverviewTemplate {
    title: String,
    sessions: Vec<SessionSummary>,
    stats: GlobalStats,
    current_page: usize,
    total_pages: usize,
}

pub async fn overview_handler(
    State(state): State<AppState>,
    Query(params): Query<PaginationParams>,
) -> Result<impl IntoResponse, DashboardError> {
    let current_page = params.page.unwrap_or(1).max(1);
    let offset = (current_page - 1) * PAGE_SIZE;

    let total_sessions = state.store.count_sessions().unwrap_or(0);
    let total_pages = (total_sessions + PAGE_SIZE - 1) / PAGE_SIZE.max(1);

    let sessions = state.store.list_sessions_paginated(PAGE_SIZE, offset)?;

    let mut nodes_by_session = Vec::new();
    let mut budgets = Vec::new();
    for s in &sessions {
        let nodes = state
            .store
            .get_latest_node_states(&s.session_id)
            .unwrap_or_default();
        nodes_by_session.push((s.session_id.clone(), nodes));

        let budget = state
            .store
            .get_budget_envelope(&s.session_id)
            .ok()
            .flatten();
        budgets.push((s.session_id.clone(), budget));
    }

    let llm_summary = state.store.get_global_llm_summary().unwrap_or((0, 0, 0, 0));

    let vm = OverviewViewModel::from_store(sessions, &nodes_by_session, &budgets, llm_summary);

    let tmpl = OverviewTemplate {
        title: "Dashboard".to_string(),
        sessions: vm.sessions,
        stats: vm.global_stats,
        current_page,
        total_pages,
    };
    Ok(Html(tmpl.render()?))
}