use std::collections::VecDeque;
use crate::commands::status::models::{AgentRow, ApprovalResponse, ApprovalsSummary, BudgetRow, RuntimeHealth};
use super::dialog::DialogAction;
pub const EVENT_LOG_CAPACITY: usize = 200;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Panel {
Agents,
EventLog,
Budget,
Approvals,
}
impl Panel {
pub fn next(self) -> Self {
match self {
Self::Agents => Self::EventLog,
Self::EventLog => Self::Approvals,
Self::Approvals => Self::Budget,
Self::Budget => Self::Agents,
}
}
pub fn prev(self) -> Self {
match self {
Self::Agents => Self::Budget,
Self::EventLog => Self::Agents,
Self::Approvals => Self::EventLog,
Self::Budget => Self::Approvals,
}
}
}
#[derive(Debug, Clone)]
pub struct EventEntry {
pub timestamp: String,
pub event_type: String,
pub agent_id: String,
pub message: String,
}
#[derive(Debug)]
pub struct DashboardState {
pub active_panel: Panel,
pub runtime: RuntimeHealth,
pub agents: Vec<AgentRow>,
pub approvals_summary: ApprovalsSummary,
pub pending_approvals: Vec<ApprovalResponse>,
pub budget: BudgetRow,
pub event_log: VecDeque<EventEntry>,
pub event_log_scroll: u16,
pub agent_selected: usize,
pub approval_selected: usize,
pub show_help: bool,
pub show_inspect: bool,
pub show_policy: bool,
pub policy_yaml: Option<String>,
pub confirm_dialog: Option<DialogAction>,
pub should_quit: bool,
}
impl Default for DashboardState {
fn default() -> Self {
Self::new()
}
}
impl DashboardState {
pub fn new() -> Self {
Self {
active_panel: Panel::Agents,
runtime: RuntimeHealth {
reachable: false,
status: "connecting…".to_string(),
uptime_secs: 0,
active_connections: 0,
pipeline_lag_ms: 0,
},
agents: Vec::new(),
approvals_summary: ApprovalsSummary {
pending_count: 0,
oldest_pending_age: None,
},
pending_approvals: Vec::new(),
budget: BudgetRow {
daily_spend_usd: "0.00".to_string(),
monthly_spend_usd: None,
daily_limit_usd: None,
monthly_limit_usd: None,
date: String::new(),
per_agent: vec![],
},
event_log: VecDeque::with_capacity(EVENT_LOG_CAPACITY),
event_log_scroll: 0,
agent_selected: 0,
approval_selected: 0,
show_help: false,
show_inspect: false,
show_policy: false,
policy_yaml: None,
confirm_dialog: None,
should_quit: false,
}
}
pub fn push_event(&mut self, entry: EventEntry) {
if self.event_log.len() >= EVENT_LOG_CAPACITY {
self.event_log.pop_front();
}
self.event_log.push_back(entry);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn panel_next_cycles_through_all() {
let start = Panel::Agents;
let second = start.next();
assert_eq!(second, Panel::EventLog);
let third = second.next();
assert_eq!(third, Panel::Approvals);
let fourth = third.next();
assert_eq!(fourth, Panel::Budget);
let back = fourth.next();
assert_eq!(back, Panel::Agents);
}
#[test]
fn panel_prev_cycles_backwards() {
let start = Panel::Agents;
let last = start.prev();
assert_eq!(last, Panel::Budget);
let third = last.prev();
assert_eq!(third, Panel::Approvals);
let second = third.prev();
assert_eq!(second, Panel::EventLog);
let first = second.prev();
assert_eq!(first, Panel::Agents);
}
#[test]
fn new_state_defaults() {
let state = DashboardState::new();
assert_eq!(state.active_panel, Panel::Agents);
assert!(!state.runtime.reachable);
assert!(state.agents.is_empty());
assert_eq!(state.approvals_summary.pending_count, 0);
assert!(state.pending_approvals.is_empty());
assert!(!state.show_help);
assert!(!state.show_inspect);
assert!(!state.show_policy);
assert!(state.policy_yaml.is_none());
assert!(state.confirm_dialog.is_none());
assert!(!state.should_quit);
}
#[test]
fn push_event_within_capacity() {
let mut state = DashboardState::new();
state.push_event(EventEntry {
timestamp: "2026-04-30T10:00:00Z".to_string(),
event_type: "violation".to_string(),
agent_id: "a1".to_string(),
message: "test".to_string(),
});
assert_eq!(state.event_log.len(), 1);
}
#[test]
fn push_event_evicts_oldest_at_capacity() {
let mut state = DashboardState::new();
for i in 0..EVENT_LOG_CAPACITY {
state.push_event(EventEntry {
timestamp: format!("t{i}"),
event_type: "test".to_string(),
agent_id: "a1".to_string(),
message: format!("msg {i}"),
});
}
assert_eq!(state.event_log.len(), EVENT_LOG_CAPACITY);
state.push_event(EventEntry {
timestamp: "overflow".to_string(),
event_type: "test".to_string(),
agent_id: "a1".to_string(),
message: "overflow".to_string(),
});
assert_eq!(state.event_log.len(), EVENT_LOG_CAPACITY);
assert_eq!(state.event_log.front().unwrap().timestamp, "t1");
assert_eq!(state.event_log.back().unwrap().timestamp, "overflow");
}
}