generalbots/src/console/status_panel.rs
Rodrigo Rodriguez (Pragmatismo) 061c14b4a2 Fix tasks UI, WebSocket progress, memory monitoring, and app generator
Tasks UI fixes:
- Fix task list to query auto_tasks table instead of tasks table
- Fix task detail endpoint to use UUID binding for auto_tasks query
- Add proper filter handling: complete, active, awaiting, paused, blocked
- Add TaskStats fields: awaiting, paused, blocked, time_saved
- Add /api/tasks/time-saved endpoint
- Add count-all to stats HTML response

App generator improvements:
- Add AgentActivity struct for detailed terminal-style progress
- Add emit_activity method for rich progress events
- Add detailed logging for LLM calls with timing
- Track files_written, tables_synced, bytes_generated

Memory and performance:
- Add memory_monitor module for tracking RSS and thread activity
- Skip 0-byte files in drive monitor and document processor
- Change DRIVE_MONITOR checking logs from info to trace
- Remove unused profile_section macro

WebSocket progress:
- Ensure TaskProgressEvent includes activity field
- Add with_activity builder method
2025-12-30 22:42:32 -03:00

293 lines
11 KiB
Rust

use crate::config::ConfigManager;
#[cfg(feature = "nvidia")]
use crate::nvidia;
#[cfg(feature = "nvidia")]
use crate::nvidia::get_system_metrics;
use crate::shared::models::schema::bots::dsl::*;
use crate::shared::state::AppState;
use diesel::prelude::*;
use std::sync::Arc;
use sysinfo::System;
/// Cache for component status to avoid spawning pgrep processes too frequently
struct ComponentStatusCache {
statuses: Vec<(String, bool, String)>, // (name, is_running, port)
last_check: std::time::Instant,
}
impl ComponentStatusCache {
fn new() -> Self {
Self {
statuses: Vec::new(),
last_check: std::time::Instant::now() - std::time::Duration::from_secs(60),
}
}
fn needs_refresh(&self) -> bool {
// Only check component status every 10 seconds
self.last_check.elapsed() > std::time::Duration::from_secs(10)
}
fn refresh(&mut self) {
let components = vec![
("Tables", "postgres", "5432"),
("Cache", "valkey-server", "6379"),
("Drive", "minio", "9000"),
("LLM", "llama-server", "8081"),
];
self.statuses.clear();
for (comp_name, process, port) in components {
let is_running = Self::check_component_running(process);
self.statuses
.push((comp_name.to_string(), is_running, port.to_string()));
}
self.last_check = std::time::Instant::now();
}
fn check_component_running(process_name: &str) -> bool {
std::process::Command::new("pgrep")
.arg("-f")
.arg(process_name)
.output()
.map(|output| !output.stdout.is_empty())
.unwrap_or(false)
}
fn get_statuses(&self) -> &[(String, bool, String)] {
&self.statuses
}
}
/// Cache for bot list to avoid database queries too frequently
struct BotListCache {
bot_list: Vec<(String, uuid::Uuid)>,
last_check: std::time::Instant,
}
impl BotListCache {
fn new() -> Self {
Self {
bot_list: Vec::new(),
last_check: std::time::Instant::now() - std::time::Duration::from_secs(60),
}
}
fn needs_refresh(&self) -> bool {
// Only query database every 5 seconds
self.last_check.elapsed() > std::time::Duration::from_secs(5)
}
fn refresh(&mut self, app_state: &Arc<AppState>) {
if let Ok(mut conn) = app_state.conn.get() {
if let Ok(list) = bots
.filter(is_active.eq(true))
.select((name, id))
.load::<(String, uuid::Uuid)>(&mut *conn)
{
self.bot_list = list;
}
}
self.last_check = std::time::Instant::now();
}
fn get_bots(&self) -> &[(String, uuid::Uuid)] {
&self.bot_list
}
}
pub struct StatusPanel {
app_state: Arc<AppState>,
last_update: std::time::Instant,
last_system_refresh: std::time::Instant,
cached_content: String,
system: System,
component_cache: ComponentStatusCache,
bot_cache: BotListCache,
}
impl std::fmt::Debug for StatusPanel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StatusPanel")
.field("app_state", &"Arc<AppState>")
.field("last_update", &self.last_update)
.field("cached_content_len", &self.cached_content.len())
.field("system", &"System")
.finish()
}
}
impl StatusPanel {
pub fn new(app_state: Arc<AppState>) -> Self {
// Only initialize with CPU and memory info, not all system info
let mut system = System::new();
system.refresh_cpu_all();
system.refresh_memory();
Self {
app_state,
last_update: std::time::Instant::now(),
last_system_refresh: std::time::Instant::now(),
cached_content: String::new(),
system,
component_cache: ComponentStatusCache::new(),
bot_cache: BotListCache::new(),
}
}
pub fn update(&mut self) -> Result<(), std::io::Error> {
// Only refresh system metrics every 2 seconds instead of every call
// This is the main CPU hog - refresh_all() is very expensive
if self.last_system_refresh.elapsed() > std::time::Duration::from_secs(2) {
// Only refresh CPU and memory, not ALL system info
self.system.refresh_cpu_all();
self.system.refresh_memory();
self.last_system_refresh = std::time::Instant::now();
}
// Refresh component status cache if needed (every 10 seconds)
if self.component_cache.needs_refresh() {
self.component_cache.refresh();
}
// Refresh bot list cache if needed (every 5 seconds)
if self.bot_cache.needs_refresh() {
self.bot_cache.refresh(&self.app_state);
}
self.cached_content = self.render(None);
self.last_update = std::time::Instant::now();
Ok(())
}
pub fn render(&mut self, selected_bot: Option<String>) -> String {
let mut lines = vec![
"╔═══════════════════════════════════════╗".to_string(),
"║ SYSTEM METRICS ║".to_string(),
"╚═══════════════════════════════════════╝".to_string(),
String::new(),
];
// Use cached CPU usage - don't refresh here
let cpu_usage = self.system.global_cpu_usage();
let cpu_bar = Self::create_progress_bar(cpu_usage, 20);
lines.push(format!(" CPU: {:5.1}% {}", cpu_usage, cpu_bar));
#[cfg(feature = "nvidia")]
{
let system_metrics = get_system_metrics().unwrap_or_default();
if let Some(gpu_usage) = system_metrics.gpu_usage {
let gpu_bar = Self::create_progress_bar(gpu_usage, 20);
lines.push(format!(" GPU: {:5.1}% {}", gpu_usage, gpu_bar));
} else {
lines.push(" GPU: Not available".to_string());
}
}
#[cfg(not(feature = "nvidia"))]
{
lines.push(" GPU: Feature not enabled".to_string());
}
let total_mem = self.system.total_memory() as f32 / 1024.0 / 1024.0 / 1024.0;
let used_mem = self.system.used_memory() as f32 / 1024.0 / 1024.0 / 1024.0;
let mem_percentage = if total_mem > 0.0 {
(used_mem / total_mem) * 100.0
} else {
0.0
};
let mem_bar = Self::create_progress_bar(mem_percentage, 20);
lines.push(format!(
" MEM: {:5.1}% {} ({:.1}/{:.1} GB)",
mem_percentage, mem_bar, used_mem, total_mem
));
lines.push("".to_string());
lines.push("╔═══════════════════════════════════════╗".to_string());
lines.push("║ COMPONENTS STATUS ║".to_string());
lines.push("╚═══════════════════════════════════════╝".to_string());
lines.push("".to_string());
// Use cached component statuses instead of spawning pgrep every time
for (comp_name, is_running, port) in self.component_cache.get_statuses() {
let status = if *is_running {
format!(" ONLINE [Port: {}]", port)
} else {
" OFFLINE".to_string()
};
lines.push(format!(" {:<10} {}", comp_name, status));
}
lines.push("".to_string());
lines.push("╔═══════════════════════════════════════╗".to_string());
lines.push("║ ACTIVE BOTS ║".to_string());
lines.push("╚═══════════════════════════════════════╝".to_string());
lines.push("".to_string());
// Use cached bot list instead of querying database every time
let bot_list = self.bot_cache.get_bots();
if bot_list.is_empty() {
lines.push(" No active bots".to_string());
} else {
for (bot_name, bot_id) in bot_list {
let marker = if let Some(ref selected) = selected_bot {
if selected == bot_name {
""
} else {
" "
}
} else {
" "
};
lines.push(format!(" {} {}", marker, bot_name));
if let Some(ref selected) = selected_bot {
if selected == bot_name {
lines.push("".to_string());
lines.push(" ┌─ Bot Configuration ─────────┐".to_string());
let config_manager = ConfigManager::new(self.app_state.conn.clone());
let llm_model = config_manager
.get_config(bot_id, "llm-model", None)
.unwrap_or_else(|_| "N/A".to_string());
lines.push(format!(" Model: {}", llm_model));
let ctx_size = config_manager
.get_config(bot_id, "llm-server-ctx-size", None)
.unwrap_or_else(|_| "N/A".to_string());
lines.push(format!(" Context: {}", ctx_size));
let temp = config_manager
.get_config(bot_id, "llm-temperature", None)
.unwrap_or_else(|_| "N/A".to_string());
lines.push(format!(" Temp: {}", temp));
lines.push(" └─────────────────────────────┘".to_string());
}
}
}
}
lines.push("".to_string());
lines.push("╔═══════════════════════════════════════╗".to_string());
lines.push("║ SESSIONS ║".to_string());
lines.push("╚═══════════════════════════════════════╝".to_string());
let session_count = self
.app_state
.response_channels
.try_lock()
.map(|channels| channels.len())
.unwrap_or(0);
lines.push(format!(" Active Sessions: {}", session_count));
lines.join("\n")
}
fn create_progress_bar(percentage: f32, width: usize) -> String {
let filled = (percentage / 100.0 * width as f32).round() as usize;
let empty = width.saturating_sub(filled);
let filled_chars = "".repeat(filled);
let empty_chars = "".repeat(empty);
format!("[{}{}]", filled_chars, empty_chars)
}
pub fn check_component_running(process_name: &str) -> bool {
ComponentStatusCache::check_component_running(process_name)
}
}