diff --git a/PROMPT.md b/PROMPT.md index c6c6886d7..dcee9dc5d 100644 --- a/PROMPT.md +++ b/PROMPT.md @@ -231,7 +231,7 @@ When configuring CI/CD pipelines (e.g., Forgejo Actions): - name: Setup Workspace run: | # 1. Clone only the root workspace configuration - git clone --depth 1 https://alm.pragmatismo.com.br/GeneralBots/gb.git workspace + git clone --depth 1 workspace # 2. Setup only the necessary dependencies (botlib) cd workspace diff --git a/scripts/utils/check-space.sh b/scripts/utils/check-space.sh index c19f9ab52..78f82fa00 100644 --- a/scripts/utils/check-space.sh +++ b/scripts/utils/check-space.sh @@ -8,7 +8,7 @@ done #!/bin/bash # Directory to analyze -TARGET_DIR="/opt/gbo/tenants/pragmatismo" +TARGET_DIR="/opt/gbo/tenants/system" echo "Calculating sizes for directories in $TARGET_DIR..." echo "" @@ -26,4 +26,4 @@ du -h --max-depth=1 "$TARGET_DIR" | sort -hr | awk -F'\t' '{printf "%-50s %s\n", echo "" echo "Total size:" -du -sh "$TARGET_DIR" \ No newline at end of file +du -sh "$TARGET_DIR" diff --git a/src/core/package_manager/facade.rs b/src/core/package_manager/facade.rs index 9dfbd6622..ea5b7d3ee 100644 --- a/src/core/package_manager/facade.rs +++ b/src/core/package_manager/facade.rs @@ -436,7 +436,8 @@ impl PackageManager { "VAULT_ADDR=http://127.0.0.1:8200 /opt/gbo/bin/vault operator unseal {}", key_str ); - let unseal_output = safe_lxc(&["exec", container_name, "--", "bash", "-c", &unseal_cmd]); + let unseal_output = + safe_lxc(&["exec", container_name, "--", "bash", "-c", &unseal_cmd]); if let Some(output) = unseal_output { if !output.status.success() { @@ -594,7 +595,7 @@ Store credentials in Vault: API: http://{}:8086 Store credentials in Vault: - botserver vault put gbo/observability url=http://{}:8086 token= org=pragmatismo bucket=metrics", + botserver vault put gbo/observability url=http://{}:8086 token= org=system bucket=metrics", ip, ip ) } @@ -926,7 +927,11 @@ Store credentials in Vault: let has_subdir = if list_output.status.success() { let contents = String::from_utf8_lossy(&list_output.stdout); // If first entry contains '/', there's a subdirectory structure - contents.lines().next().map(|l| l.contains('/')).unwrap_or(false) + contents + .lines() + .next() + .map(|l| l.contains('/')) + .unwrap_or(false) } else { false }; @@ -1081,11 +1086,9 @@ Store credentials in Vault: .map_err(|e| anyhow::anyhow!("Failed to set env: {}", e))?; } - let output = cmd - .execute() - .with_context(|| { - format!("Failed to execute command for component '{}'", component) - })?; + let output = cmd.execute().with_context(|| { + format!("Failed to execute command for component '{}'", component) + })?; if !output.status.success() { error!( "Command had non-zero exit: {}", @@ -1269,7 +1272,8 @@ Store credentials in Vault: "proxy", &listen_arg, &connect_arg, - ]).ok_or_else(|| anyhow::anyhow!("Failed to execute lxc port forward command"))?; + ]) + .ok_or_else(|| anyhow::anyhow!("Failed to execute lxc port forward command"))?; if !output.status.success() { warn!("Failed to setup port forwarding for port {}", port); } diff --git a/src/core/package_manager/installer.rs b/src/core/package_manager/installer.rs index 8e18e3e3b..018175d74 100644 --- a/src/core/package_manager/installer.rs +++ b/src/core/package_manager/installer.rs @@ -860,7 +860,7 @@ impl PackageManager { "mkdir -p {{CONF_PATH}}/influxdb".to_string(), ], post_install_cmds_linux: vec![ - "{{BIN_PATH}}/influx setup --org pragmatismo --bucket metrics --username admin --password {{GENERATED_PASSWORD}} --force".to_string(), + "{{BIN_PATH}}/influx setup --org system --bucket metrics --username admin --password {{GENERATED_PASSWORD}} --force".to_string(), ], pre_install_cmds_macos: vec![ "mkdir -p {{DATA_PATH}}/influxdb".to_string(), @@ -1082,7 +1082,8 @@ EOF"#.to_string(), trace!( "Starting component {} with command: {}", - component.name, rendered_cmd + component.name, + rendered_cmd ); trace!( "Working directory: {}, logs_path: {}", @@ -1108,7 +1109,8 @@ EOF"#.to_string(), trace!( "About to spawn shell command for {}: {}", - component.name, rendered_cmd + component.name, + rendered_cmd ); trace!("[START] Working dir: {}", bin_path.display()); let child = SafeCommand::new("sh") @@ -1118,11 +1120,7 @@ EOF"#.to_string(), .and_then(|cmd| cmd.spawn_with_envs(&evaluated_envs)) .map_err(|e| anyhow::anyhow!("Failed to spawn process: {}", e)); - trace!( - "Spawn result for {}: {:?}", - component.name, - child.is_ok() - ); + trace!("Spawn result for {}: {:?}", component.name, child.is_ok()); std::thread::sleep(std::time::Duration::from_secs(2)); trace!( @@ -1132,11 +1130,7 @@ EOF"#.to_string(), let check_proc = safe_pgrep(&["-f", &component.name]); if let Some(output) = check_proc { let pids = String::from_utf8_lossy(&output.stdout); - trace!( - "pgrep '{}' result: '{}'", - component.name, - pids.trim() - ); + trace!("pgrep '{}' result: '{}'", component.name, pids.trim()); } match child { @@ -1199,11 +1193,14 @@ EOF"#.to_string(), client_key.display(), vault_addr )) - .map(|o| o.status.success()) - .unwrap_or(false); + .map(|o| o.status.success()) + .unwrap_or(false); if !vault_check { - trace!("Vault not reachable at {}, skipping credential fetch", vault_addr); + trace!( + "Vault not reachable at {}, skipping credential fetch", + vault_addr + ); return credentials; } @@ -1211,10 +1208,18 @@ EOF"#.to_string(), let vault_bin_str = vault_bin.to_string_lossy(); // Get CA cert path for Vault TLS - let ca_cert_path = std::env::var("VAULT_CACERT") - .unwrap_or_else(|_| base_path.join("conf/system/certificates/ca/ca.crt").to_string_lossy().to_string()); + let ca_cert_path = std::env::var("VAULT_CACERT").unwrap_or_else(|_| { + base_path + .join("conf/system/certificates/ca/ca.crt") + .to_string_lossy() + .to_string() + }); - trace!("Fetching drive credentials from Vault at {} using {}", vault_addr, vault_bin_str); + trace!( + "Fetching drive credentials from Vault at {} using {}", + vault_addr, + vault_bin_str + ); let drive_cmd = format!( "VAULT_ADDR={} VAULT_TOKEN={} VAULT_CACERT={} {} kv get -format=json secret/gbo/drive", vault_addr, vault_token, ca_cert_path, vault_bin_str @@ -1227,13 +1232,19 @@ EOF"#.to_string(), match serde_json::from_str::(&json_str) { Ok(json) => { if let Some(data) = json.get("data").and_then(|d| d.get("data")) { - if let Some(accesskey) = data.get("accesskey").and_then(|v| v.as_str()) { + if let Some(accesskey) = + data.get("accesskey").and_then(|v| v.as_str()) + { trace!("Found DRIVE_ACCESSKEY from Vault"); - credentials.insert("DRIVE_ACCESSKEY".to_string(), accesskey.to_string()); + credentials.insert( + "DRIVE_ACCESSKEY".to_string(), + accesskey.to_string(), + ); } if let Some(secret) = data.get("secret").and_then(|v| v.as_str()) { trace!("Found DRIVE_SECRET from Vault"); - credentials.insert("DRIVE_SECRET".to_string(), secret.to_string()); + credentials + .insert("DRIVE_SECRET".to_string(), secret.to_string()); } } else { warn!("Vault response missing data.data field"); @@ -1259,7 +1270,8 @@ EOF"#.to_string(), if let Ok(json) = serde_json::from_str::(&json_str) { if let Some(data) = json.get("data").and_then(|d| d.get("data")) { if let Some(password) = data.get("password").and_then(|v| v.as_str()) { - credentials.insert("CACHE_PASSWORD".to_string(), password.to_string()); + credentials + .insert("CACHE_PASSWORD".to_string(), password.to_string()); } } } diff --git a/src/core/product.rs b/src/core/product.rs index 026f3d266..ef2cfe7b3 100644 --- a/src/core/product.rs +++ b/src/core/product.rs @@ -12,9 +12,8 @@ use std::sync::RwLock; use tracing::{info, warn}; /// Global product configuration instance -pub static PRODUCT_CONFIG: Lazy> = Lazy::new(|| { - RwLock::new(ProductConfig::load().unwrap_or_default()) -}); +pub static PRODUCT_CONFIG: Lazy> = + Lazy::new(|| RwLock::new(ProductConfig::load().unwrap_or_default())); /// Product configuration structure #[derive(Debug, Clone)] @@ -52,9 +51,22 @@ impl Default for ProductConfig { let mut apps = HashSet::new(); // All apps enabled by default for app in &[ - "chat", "mail", "calendar", "drive", "tasks", "docs", "paper", - "sheet", "slides", "meet", "research", "sources", "analytics", - "admin", "monitoring", "settings", + "chat", + "mail", + "calendar", + "drive", + "tasks", + "docs", + "paper", + "sheet", + "slides", + "meet", + "research", + "sources", + "analytics", + "admin", + "monitoring", + "settings", ] { apps.insert(app.to_string()); } @@ -67,7 +79,7 @@ impl Default for ProductConfig { favicon: None, primary_color: None, support_email: None, - docs_url: Some("https://docs.pragmatismo.com.br".to_string()), + docs_url: None, copyright: None, } } @@ -76,11 +88,7 @@ impl Default for ProductConfig { impl ProductConfig { /// Load configuration from .product file pub fn load() -> Result { - let paths = [ - ".product", - "./botserver/.product", - "../.product", - ]; + let paths = [".product", "./botserver/.product", "../.product"]; let mut content = None; for path in &paths { @@ -215,7 +223,9 @@ impl ProductConfig { /// Get copyright text with year substitution pub fn get_copyright(&self) -> String { let year = chrono::Utc::now().format("%Y").to_string(); - let template = self.copyright.as_deref() + let template = self + .copyright + .as_deref() .unwrap_or("© {year} {name}. All rights reserved."); template @@ -231,7 +241,8 @@ impl ProductConfig { /// Reload configuration from file pub fn reload() -> Result<(), ProductConfigError> { let new_config = Self::load()?; - let mut config = PRODUCT_CONFIG.write() + let mut config = PRODUCT_CONFIG + .write() .map_err(|_| ProductConfigError::LockError)?; *config = new_config; info!("Product configuration reloaded"); @@ -295,7 +306,7 @@ pub fn replace_branding(text: &str) -> String { pub fn get_product_config_json() -> serde_json::Value { // Get compiled features from our new module let compiled = crate::core::features::COMPILED_FEATURES; - + // Get current config let config = PRODUCT_CONFIG.read().ok(); @@ -327,7 +338,7 @@ pub fn get_product_config_json() -> serde_json::Value { "compiled_features": compiled, "version": env!("CARGO_PKG_VERSION"), "theme": "sentient", - }) + }), } } @@ -337,7 +348,6 @@ pub fn get_workspace_manifest() -> serde_json::Value { serde_json::to_value(manifest).unwrap_or_else(|_| serde_json::json!({})) } - /// Middleware to check if an app is enabled before allowing API access pub async fn app_gate_middleware( req: axum::http::Request, @@ -390,18 +400,15 @@ pub async fn app_gate_middleware( // Some core apps like settings might not be in feature flags explicitly or always enabled. // For simplicity, if it's not in compiled features but is a known core route, we might allow it, // but here we enforce strict feature containment. - // Exception: 'settings' and 'auth' are often core. + // Exception: 'settings' and 'auth' are often core. if app != "settings" && app != "auth" && !crate::core::features::is_feature_compiled(app) { - let error_response = serde_json::json!({ + let error_response = serde_json::json!({ "error": "not_implemented", "message": format!("The '{}' feature is not compiled in this build", app), "code": 501 }); - return ( - StatusCode::NOT_IMPLEMENTED, - axum::Json(error_response) - ).into_response(); + return (StatusCode::NOT_IMPLEMENTED, axum::Json(error_response)).into_response(); } if !is_app_enabled(app) { @@ -411,10 +418,7 @@ pub async fn app_gate_middleware( "code": 403 }); - return ( - StatusCode::FORBIDDEN, - axum::Json(error_response) - ).into_response(); + return (StatusCode::FORBIDDEN, axum::Json(error_response)).into_response(); } } @@ -424,9 +428,22 @@ pub async fn app_gate_middleware( /// Get list of disabled apps for logging/debugging pub fn get_disabled_apps() -> Vec { let all_apps = vec![ - "chat", "mail", "calendar", "drive", "tasks", "docs", "paper", - "sheet", "slides", "meet", "research", "sources", "analytics", - "admin", "monitoring", "settings", + "chat", + "mail", + "calendar", + "drive", + "tasks", + "docs", + "paper", + "sheet", + "slides", + "meet", + "research", + "sources", + "analytics", + "admin", + "monitoring", + "settings", ]; all_apps diff --git a/src/core/secrets/mod.rs b/src/core/secrets/mod.rs index 90655cf45..94380caa2 100644 --- a/src/core/secrets/mod.rs +++ b/src/core/secrets/mod.rs @@ -255,9 +255,7 @@ impl SecretsManager { s.get("url") .cloned() .unwrap_or_else(|| "http://localhost:8086".into()), - s.get("org") - .cloned() - .unwrap_or_else(|| "pragmatismo".into()), + s.get("org").cloned().unwrap_or_else(|| "system".into()), s.get("bucket").cloned().unwrap_or_else(|| "metrics".into()), s.get("token").cloned().unwrap_or_default(), )) diff --git a/src/timeseries/mod.rs b/src/timeseries/mod.rs index 2f0afb94f..2d719e183 100644 --- a/src/timeseries/mod.rs +++ b/src/timeseries/mod.rs @@ -1,22 +1,3 @@ - - - - - - - - - - - - - - - - - - - use crate::shared::utils::create_tls_client; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -25,10 +6,8 @@ use std::sync::Arc; use tokio::sync::mpsc; use tokio::sync::RwLock; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TimeSeriesConfig { - pub url: String, pub token: String, @@ -49,7 +28,7 @@ impl Default for TimeSeriesConfig { Self { url: "http://localhost:8086".to_string(), token: String::new(), - org: "pragmatismo".to_string(), + org: "system".to_string(), bucket: "metrics".to_string(), batch_size: 1000, flush_interval_ms: 1000, @@ -58,10 +37,8 @@ impl Default for TimeSeriesConfig { } } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MetricPoint { - pub measurement: String, pub tags: HashMap, @@ -71,7 +48,6 @@ pub struct MetricPoint { pub timestamp: Option>, } - #[derive(Debug, Clone, Serialize, Deserialize)] pub enum FieldValue { Float(f64), @@ -82,7 +58,6 @@ pub enum FieldValue { } impl MetricPoint { - pub fn new(measurement: impl Into) -> Self { Self { measurement: measurement.into(), @@ -92,55 +67,46 @@ impl MetricPoint { } } - pub fn tag(mut self, key: impl Into, value: impl Into) -> Self { self.tags.insert(key.into(), value.into()); self } - pub fn field_f64(mut self, key: impl Into, value: f64) -> Self { self.fields.insert(key.into(), FieldValue::Float(value)); self } - pub fn field_i64(mut self, key: impl Into, value: i64) -> Self { self.fields.insert(key.into(), FieldValue::Integer(value)); self } - pub fn field_u64(mut self, key: impl Into, value: u64) -> Self { self.fields .insert(key.into(), FieldValue::UnsignedInteger(value)); self } - pub fn field_str(mut self, key: impl Into, value: impl Into) -> Self { self.fields .insert(key.into(), FieldValue::String(value.into())); self } - pub fn field_bool(mut self, key: impl Into, value: bool) -> Self { self.fields.insert(key.into(), FieldValue::Boolean(value)); self } - pub fn at(mut self, timestamp: DateTime) -> Self { self.timestamp = Some(timestamp); self } - pub fn to_line_protocol(&self) -> String { let mut line = self.measurement.clone(); - let mut sorted_tags: Vec<_> = self.tags.iter().collect(); sorted_tags.sort_by_key(|(k, _)| *k); for (key, value) in sorted_tags { @@ -150,7 +116,6 @@ impl MetricPoint { line.push_str(&escape_tag_value(value)); } - line.push(' '); let mut sorted_fields: Vec<_> = self.fields.iter().collect(); sorted_fields.sort_by_key(|(k, _)| *k); @@ -171,7 +136,6 @@ impl MetricPoint { .collect(); line.push_str(&fields_str.join(",")); - if let Some(ts) = self.timestamp { line.push(' '); line.push_str(&ts.timestamp_nanos_opt().unwrap_or(0).to_string()); @@ -181,40 +145,34 @@ impl MetricPoint { } } - fn escape_tag_key(s: &str) -> String { s.replace(',', "\\,") .replace('=', "\\=") .replace(' ', "\\ ") } - fn escape_tag_value(s: &str) -> String { s.replace(',', "\\,") .replace('=', "\\=") .replace(' ', "\\ ") } - fn escape_field_key(s: &str) -> String { s.replace(',', "\\,") .replace('=', "\\=") .replace(' ', "\\ ") } - fn escape_string_value(s: &str) -> String { s.replace('\\', "\\\\").replace('"', "\\\"") } - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct QueryResult { pub columns: Vec, pub rows: Vec>, } - pub struct TimeSeriesClient { config: TimeSeriesConfig, http_client: reqwest::Client, @@ -223,7 +181,6 @@ pub struct TimeSeriesClient { } impl TimeSeriesClient { - pub async fn new(config: TimeSeriesConfig) -> Result { let http_client = create_tls_client(Some(30)); @@ -237,23 +194,15 @@ impl TimeSeriesClient { write_sender, }; - let buffer_clone = write_buffer.clone(); let config_clone = config.clone(); tokio::spawn(async move { - Self::background_writer( - write_receiver, - buffer_clone, - http_client, - config_clone, - ) - .await; + Self::background_writer(write_receiver, buffer_clone, http_client, config_clone).await; }); Ok(client) } - async fn background_writer( mut receiver: mpsc::Receiver, buffer: Arc>>, @@ -291,7 +240,6 @@ impl TimeSeriesClient { } } - async fn flush_points( http_client: &reqwest::Client, config: &TimeSeriesConfig, @@ -334,7 +282,6 @@ impl TimeSeriesClient { Ok(()) } - pub async fn write_point(&self, point: MetricPoint) -> Result<(), TimeSeriesError> { self.write_sender .send(point) @@ -342,7 +289,6 @@ impl TimeSeriesClient { .map_err(|e| TimeSeriesError::WriteError(e.to_string())) } - pub async fn write_points(&self, points: Vec) -> Result<(), TimeSeriesError> { for point in points { self.write_point(point).await?; @@ -350,7 +296,6 @@ impl TimeSeriesClient { Ok(()) } - pub async fn query(&self, flux_query: &str) -> Result { let url = format!("{}/api/v2/query?org={}", self.config.url, self.config.org); @@ -382,7 +327,6 @@ impl TimeSeriesClient { Self::parse_csv_result(&csv_data) } - fn parse_csv_result(csv_data: &str) -> Result { let mut result = QueryResult { columns: Vec::new(), @@ -391,7 +335,6 @@ impl TimeSeriesClient { let mut lines = csv_data.lines().peekable(); - while let Some(line) = lines.peek() { if line.starts_with('#') || line.is_empty() { lines.next(); @@ -400,12 +343,13 @@ impl TimeSeriesClient { } } - if let Some(header_line) = lines.next() { - result.columns = header_line.split(',').map(|s| s.trim().to_string()).collect(); + result.columns = header_line + .split(',') + .map(|s| s.trim().to_string()) + .collect(); } - for line in lines { if line.is_empty() || line.starts_with('#') { continue; @@ -436,7 +380,6 @@ impl TimeSeriesClient { Ok(result) } - pub async fn query_range( &self, measurement: &str, @@ -459,7 +402,6 @@ impl TimeSeriesClient { self.query(&flux).await } - pub async fn query_last(&self, measurement: &str) -> Result { let flux = format!( r#"from(bucket: "{}") @@ -472,7 +414,6 @@ impl TimeSeriesClient { self.query(&flux).await } - pub async fn query_stats( &self, measurement: &str, @@ -498,7 +439,6 @@ impl TimeSeriesClient { self.query(&flux).await } - pub async fn health_check(&self) -> Result { let url = format!("{}/health", self.config.url); @@ -513,11 +453,9 @@ impl TimeSeriesClient { } } - pub struct Metrics; impl Metrics { - pub fn message(bot_id: &str, channel: &str, direction: &str) -> MetricPoint { MetricPoint::new("messages") .tag("bot_id", bot_id) @@ -527,7 +465,6 @@ impl Metrics { .at(Utc::now()) } - pub fn response_time(bot_id: &str, duration_ms: f64) -> MetricPoint { MetricPoint::new("response_time") .tag("bot_id", bot_id) @@ -535,7 +472,6 @@ impl Metrics { .at(Utc::now()) } - pub fn llm_tokens( bot_id: &str, model: &str, @@ -551,7 +487,6 @@ impl Metrics { .at(Utc::now()) } - pub fn active_sessions(bot_id: &str, count: i64) -> MetricPoint { MetricPoint::new("active_sessions") .tag("bot_id", bot_id) @@ -559,7 +494,6 @@ impl Metrics { .at(Utc::now()) } - pub fn error(bot_id: &str, error_type: &str, message: &str) -> MetricPoint { MetricPoint::new("errors") .tag("bot_id", bot_id) @@ -569,7 +503,6 @@ impl Metrics { .at(Utc::now()) } - pub fn storage_usage(bot_id: &str, bytes_used: u64, file_count: u64) -> MetricPoint { MetricPoint::new("storage_usage") .tag("bot_id", bot_id) @@ -578,8 +511,12 @@ impl Metrics { .at(Utc::now()) } - - pub fn api_request(endpoint: &str, method: &str, status_code: i64, duration_ms: f64) -> MetricPoint { + pub fn api_request( + endpoint: &str, + method: &str, + status_code: i64, + duration_ms: f64, + ) -> MetricPoint { MetricPoint::new("api_requests") .tag("endpoint", endpoint) .tag("method", method) @@ -589,7 +526,6 @@ impl Metrics { .at(Utc::now()) } - pub fn system(cpu_percent: f64, memory_percent: f64, disk_percent: f64) -> MetricPoint { MetricPoint::new("system_metrics") .field_f64("cpu_percent", cpu_percent) @@ -599,7 +535,6 @@ impl Metrics { } } - #[derive(Debug, Clone)] pub enum TimeSeriesError { ConnectionError(String),