Auto-commit: 20260118_195334

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-18 19:53:34 -03:00
parent 033bb504b9
commit 5126c648ff
15 changed files with 2625 additions and 3064 deletions

6
.cargo/config.toml Normal file
View file

@ -0,0 +1,6 @@
[build]
rustc-wrapper = "sccache"
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]

View file

@ -12,7 +12,7 @@ name=General Bots
# Available apps: chat, mail, calendar, drive, tasks, docs, paper, sheet, slides,
# meet, research, sources, analytics, admin, monitoring, settings
# Only listed apps will be visible in the UI and have their APIs enabled.
apps=chat,mail,calendar,drive,tasks,docs,paper,sheet,slides,meet,research,sources,analytics,admin,monitoring,settings
apps=chat,drive,tasks,sources,settings
# Default theme
# Available themes: dark, light, blue, purple, green, orange, sentient, cyberpunk,

View file

@ -2,27 +2,28 @@
name = "botserver"
version = "6.1.0"
edition = "2021"
# ... [authors, description, license, repository sections remain the same]
resolver = "2" # Better feature resolution
[dependencies.botlib]
path = "../botlib"
features = ["database", "i18n"]
# Remove features here - control them in botlib's Cargo.toml
# features = ["database", "i18n"] # BAD - causes full recompile
[features]
# ===== DEFAULT FEATURE SET =====
default = ["chat", "drive", "tasks", "automation"]
# ===== SINGLE DEFAULT FEATURE SET =====
default = ["chat", "drive", "tasks", "automation", "cache"]
# ===== COMMUNICATION APPS =====
chat = []
chat = ["botlib/chat"] # Delegate to botlib
people = []
mail = ["email", "imap", "lettre", "mailparse", "native-tls"]
mail = ["botlib/mail"] # Delegate optional deps to botlib
meet = ["dep:livekit"]
social = []
whatsapp = []
telegram = []
instagram = []
msteams = []
# CONSIDER: Do you REALLY need this mega-feature?
communications = ["chat", "people", "mail", "meet", "social", "whatsapp", "telegram", "instagram", "msteams", "cache"]
# ===== PRODUCTIVITY APPS =====
@ -34,11 +35,11 @@ workspace = []
productivity = ["calendar", "tasks", "project", "goals", "workspace", "cache"]
# ===== DOCUMENT APPS =====
paper = ["docx-rs", "ooxmlsdk", "dep:pdf-extract"]
paper = ["docs", "dep:pdf-extract"] # Reuse docs
docs = ["docx-rs", "ooxmlsdk"]
sheet = ["umya-spreadsheet", "calamine", "rust_xlsxwriter", "spreadsheet-ods"]
sheet = ["calamine", "spreadsheet-ods"] # Reduced - pick one Excel lib
slides = ["ooxmlsdk"]
drive = ["dep:aws-config", "dep:aws-sdk-s3", "dep:pdf-extract", "dep:zip", "dep:downloader", "dep:flate2", "dep:tar"]
drive = ["dep:aws-config", "dep:aws-sdk-s3", "dep:pdf-extract", "dep:flate2"]
documents = ["paper", "docs", "sheet", "slides", "drive"]
# ===== MEDIA APPS =====
@ -87,88 +88,77 @@ jemalloc = ["dep:tikv-jemallocator", "dep:tikv-jemalloc-ctl"]
console = ["dep:crossterm", "dep:ratatui", "monitoring"]
# ===== BUNDLE FEATURES =====
# REDUCED VERSION - Enable only what you actually use
full = [
# Communication
"chat", "people", "mail", "meet", "social", "whatsapp", "telegram", "instagram", "msteams",
"chat", "people", "mail",
# Productivity
"calendar", "tasks", "project", "goals", "workspace",
"tasks", "calendar",
# Documents
"paper", "docs", "sheet", "slides", "drive",
# Media
"video", "player", "canvas",
# Learning
"learn", "research", "sources",
# Analytics
"analytics", "dashboards", "monitoring",
# Development
"designer", "editor", "automation",
# Admin
"attendant", "security", "settings",
"drive", "docs",
# Core tech
"llm", "vectordb", "nvidia", "cache", "compliance", "timeseries", "weba", "directory",
"progress-bars", "grpc", "jemalloc", "console"
"llm", "cache", "compliance"
]
minimal = ["chat"]
lightweight = ["chat", "drive", "tasks", "people"]
[dependencies]
# === CORE RUNTIME (Always Required) ===
# === CORE RUNTIME (Minimal) ===
aes-gcm = "0.10"
anyhow = "1.0"
argon2 = "0.5"
async-lock = "2.8.0"
async-stream = "0.3"
async-trait = "0.1"
axum = { version = "0.7.5", features = ["ws", "multipart", "macros"] }
axum-server = { version = "0.7", features = ["tls-rustls"] }
axum = { version = "0.7.5", default-features = false, features = [] } # NO defaults!
base64 = "0.22"
bytes = "1.8"
chrono = { version = "0.4", features = ["serde"] }
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
color-eyre = "0.6.5"
diesel = { version = "2.1", features = ["postgres", "uuid", "chrono", "serde_json", "r2d2", "numeric", "128-column-tables"] }
bigdecimal = { version = "0.4", features = ["serde"] }
diesel = { version = "2.1", default-features = false, features = ["postgres", "r2d2"] } # MINIMAL!
bigdecimal = { version = "0.4", default-features = false }
diesel_migrations = "2.1.0"
dirs = "5.0"
dotenvy = "0.15"
env_logger = "0.11"
futures = "0.3"
futures-util = "0.3"
tokio-util = { version = "0.7", features = ["io", "compat"] }
futures-util = { version = "0.3", default-features = false }
tokio-util = { version = "0.7", default-features = false, features = ["codec"] }
hex = "0.4"
hmac = "0.12.1"
hyper = { version = "1.4", features = ["full"] }
hyper-rustls = { version = "0.27", features = ["http2"] }
hyper = { version = "1.4", default-features = false, features = ["client", "server", "http1", "http2"] }
hyper-rustls = { version = "0.27", default-features = false, features = ["http2"] }
log = "0.4"
num-format = "0.4"
once_cell = "1.18.0"
rand = "0.9.2"
regex = "1.11"
reqwest = { version = "0.12", features = ["json", "stream", "multipart", "rustls-tls", "rustls-tls-native-roots"] }
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } # Reduced
serde = { version = "1.0", default-features = false, features = ["derive", "std"] }
serde_json = "1.0"
toml = "0.8"
sha2 = "0.10.9"
sha1 = "0.10.6"
tokio = { version = "1.41", features = ["full"] }
tokio = { version = "1.41", default-features = false, features = ["rt", "sync", "time", "macros", "net"] }
tokio-stream = "0.1"
tower = "0.4"
tower-http = { version = "0.5", features = ["cors", "fs", "trace"] }
tower-http = { version = "0.5", default-features = false, features = ["cors", "fs"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt"] }
tracing-subscriber = { version = "0.3", default-features = false }
urlencoding = "2.1"
uuid = { version = "1.11", features = ["serde", "v4", "v5"] }
uuid = { version = "1.11", default-features = false, features = ["v4"] }
# === TLS/SECURITY DEPENDENCIES ===
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
tokio-rustls = "0.26"
rcgen = { version = "0.14", features = ["pem"] }
rcgen = { version = "0.14", default-features = false }
x509-parser = "0.15"
rustls-native-certs = "0.8"
webpki-roots = "0.25"
ring = "0.17"
ciborium = "0.2"
time = { version = "0.3", features = ["formatting", "parsing"] }
time = { version = "0.3", default-features = false, features = ["formatting"] }
jsonwebtoken = "9.3"
tower-cookies = "0.10"
@ -176,7 +166,7 @@ tower-cookies = "0.10"
# Email Integration (mail feature)
imap = { version = "3.0.0-alpha.15", optional = true }
lettre = { version = "0.11", features = ["smtp-transport", "builder", "tokio1", "tokio1-native-tls"], optional = true }
lettre = { version = "0.11", default-features = false, optional = true }
mailparse = { version = "0.15", optional = true }
native-tls = { version = "0.2", optional = true }
@ -186,29 +176,30 @@ livekit = { version = "0.7", optional = true }
# Vector Database (vectordb feature)
qdrant-client = { version = "1.12", optional = true }
# Document Processing (paper, docs, sheet, slides features)
# Document Processing - PICK ONE PER FORMAT!
docx-rs = { version = "0.4", optional = true }
ooxmlsdk = { version = "0.3", features = ["docx", "pptx", "parts", "office2021"], optional = true }
umya-spreadsheet = { version = "2.3", optional = true }
ooxmlsdk = { version = "0.3", default-features = false, optional = true }
# umya-spreadsheet = { version = "2.3", optional = true } # REMOVE - pick one
calamine = { version = "0.26", optional = true }
rust_xlsxwriter = { version = "0.79", optional = true }
# rust_xlsxwriter = { version = "0.79", optional = true } # REMOVE - pick one
spreadsheet-ods = { version = "1.0", optional = true }
# File Storage & Drive (drive feature)
aws-config = { version = "1.8.8", features = ["behavior-version-latest"], optional = true }
aws-sdk-s3 = { version = "1.109.0", features = ["behavior-version-latest"], optional = true }
aws-config = { version = "1.8.8", default-features = false, optional = true }
aws-sdk-s3 = { version = "1.109.0", default-features = false, optional = true }
pdf-extract = { version = "0.10.0", optional = true }
quick-xml = { version = "0.37", features = ["serialize"] }
zip = { version = "2.2", optional = true }
downloader = { version = "0.2", optional = true }
quick-xml = { version = "0.37", default-features = false }
# zip = { version = "2.2", optional = true } # Only if needed
# downloader = { version = "0.2", optional = true } # Use reqwest instead
flate2 = { version = "1.0", optional = true }
tar = { version = "0.4", optional = true }
# tar = { version = "0.4", optional = true } # Only if needed
# Task Management (tasks feature)
cron = { version = "0.15.0", optional = true }
# Automation & Scripting (automation feature)
rhai = { git = "https://github.com/therealprof/rhai.git", branch = "features/use-web-time", features = ["sync"], optional = true }
# REPLACE git with version
rhai = { version = "1.23", features = ["sync"], optional = true }
# Compliance & Reporting (compliance feature)
csv = { version = "1.3", optional = true }
@ -225,13 +216,13 @@ qrcode = { version = "0.14", default-features = false }
thiserror = "2.0"
# Caching/Sessions (cache feature)
redis = { version = "0.27", features = ["tokio-comp"], optional = true }
redis = { version = "0.27", default-features = false, features = ["tokio-comp"], optional = true }
# System Monitoring (monitoring feature)
sysinfo = { version = "0.37.2", optional = true }
# Networking/gRPC (grpc feature)
tonic = { version = "0.14.2", features = ["transport"], optional = true }
tonic = { version = "0.14.2", default-features = false, features = ["transport", "tls"], optional = true }
# UI Enhancement (progress-bars feature)
indicatif = { version = "0.18.0", optional = true }
@ -239,7 +230,7 @@ smartstring = "1.0.1"
# Memory allocator (jemalloc feature)
tikv-jemallocator = { version = "0.6", optional = true }
tikv-jemalloc-ctl = { version = "0.6", features = ["stats"], optional = true }
tikv-jemalloc-ctl = { version = "0.6", default-features = false, optional = true }
scopeguard = "1.2.0"
# Vault secrets management
@ -249,7 +240,7 @@ vaultrs = "0.7"
icalendar = "0.17"
# Layered configuration
figment = { version = "0.10", features = ["toml", "env", "json"] }
figment = { version = "0.10", default-features = false, features = ["toml"] }
# Rate limiting
governor = "0.10"
@ -261,15 +252,26 @@ rss = "2.0"
scraper = "0.25"
walkdir = "2.5.0"
# Embedded static files (UI fallback when no external folder)
# Embedded static files
rust-embed = "8.5"
mime_guess = "2.0"
hyper-util = { version = "0.1.19", features = ["client-legacy", "tokio"] }
hyper-util = { version = "0.1.19", default-features = false, features = ["client-legacy"] }
http-body-util = "0.1.3"
[dev-dependencies]
mockito = "1.7.0"
tempfile = "3"
[profile.dev]
opt-level = 1 # Slightly optimized debug builds
split-debuginfo = "unpacked"
incremental = true
[profile.release]
opt-level = 3
lto = "thin"
codegen-units = 1
panic = "abort"
[lints]
workspace = true

View file

@ -36,6 +36,8 @@ git clone https://github.com/GeneralBots/botserver
cd botserver
cargo run
```
cargo install sccache
sudo apt-get install mold # or build from source
On first run, botserver automatically sets up PostgreSQL, S3 storage, Redis cache, and downloads AI models.

View file

@ -1,33 +1,3 @@
/*****************************************************************************\
| ® |
| |
| |
| |
| |
| |
| General Bots Copyright (c) pragmatismo.com.br. All rights reserved. |
| Licensed under the AGPL-3.0. |
| |
| According to our dual licensing model, this program can be used either |
| under the terms of the GNU Affero General Public License, version 3, |
| or under a proprietary license. |
| |
| The texts of the GNU Affero General Public License with an additional |
| permission and of our proprietary license can be found at and |
| in the LICENSE file you have received along with this program. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY, without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| "General Bots" is a registered trademark of pragmatismo.com.br. |
| The licensing of the program under the AGPLv3 does not imply a |
| trademark license. Therefore any rights, title and interest in |
| our trademarks remain entirely with us. |
| |
\*****************************************************************************/
use crate::shared::models::UserSession;
use crate::shared::state::AppState;
use log::{error, trace};
@ -46,7 +16,6 @@ pub fn register_import_export(state: Arc<AppState>, user: UserSession, engine: &
pub fn register_import_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state);
engine
.register_custom_syntax(["IMPORT", "$expr$"], false, move |context, inputs| {
let file_path = context.eval_expression_tree(&inputs[0])?.to_string();
@ -205,7 +174,16 @@ fn execute_import(
match extension.as_str() {
"csv" => import_csv(&full_path),
"json" => import_json(&full_path),
"xlsx" | "xls" => import_excel(&full_path),
"xlsx" | "xls" => {
#[cfg(feature = "sheet")]
{
import_excel(&full_path)
}
#[cfg(not(feature = "sheet"))]
{
Err(format!("Excel import requires 'sheet' feature. File: {}", file_path).into())
}
}
"tsv" => import_tsv(&full_path),
_ => Err(format!("Unsupported file format: .{}", extension).into()),
}
@ -227,7 +205,16 @@ fn execute_export(
match extension.as_str() {
"csv" => export_csv(&full_path, data),
"json" => export_json(&full_path, data),
"xlsx" => export_excel(&full_path, data),
"xlsx" => {
#[cfg(feature = "sheet")]
{
export_excel(&full_path, data)
}
#[cfg(not(feature = "sheet"))]
{
Err(format!("Excel export requires 'sheet' feature. File: {}", file_path).into())
}
}
"tsv" => export_tsv(&full_path, data),
_ => Err(format!("Unsupported export format: .{}", extension).into()),
}
@ -361,6 +348,7 @@ fn import_json(file_path: &str) -> Result<Dynamic, Box<dyn std::error::Error + S
Ok(result)
}
#[cfg(feature = "sheet")]
fn import_excel(file_path: &str) -> Result<Dynamic, Box<dyn std::error::Error + Send + Sync>> {
use calamine::{open_workbook, Reader, Xlsx};
@ -474,6 +462,7 @@ fn export_json(
Ok(file_path.to_string())
}
#[cfg(feature = "sheet")]
fn export_excel(
file_path: &str,
data: Dynamic,
@ -534,7 +523,7 @@ fn parse_csv_line(line: &str) -> Vec<String> {
fn escape_csv_value(value: &str) -> String {
if value.contains(',') || value.contains('"') || value.contains('\n') {
format!("\"{}\"", value.replace('"', "\"\""))
format!("{}", value.replace('"', ""))
} else {
value.to_string()
}

View file

@ -4,6 +4,7 @@
//! Supports multiple notification channels: email, webhook, in-app, SMS.
use crate::billing::UsageMetric;
use crate::core::shared::state::BillingAlertNotification;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@ -57,10 +58,11 @@ impl AlertThresholds {
None
}
}
}
/// Alert severity levels
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AlertSeverity {
Warning,
@ -92,6 +94,7 @@ impl AlertSeverity {
Self::Exceeded => 3,
}
}
}
impl std::fmt::Display for AlertSeverity {
@ -133,13 +136,14 @@ impl UsageAlert {
percentage: f64,
threshold: f64,
) -> Self {
let severity_clone = severity.clone();
let message = Self::generate_message(metric, severity, percentage, current_usage, limit);
Self {
id: Uuid::new_v4(),
organization_id,
metric,
severity,
severity: severity_clone,
current_usage,
limit,
percentage,
@ -151,6 +155,7 @@ impl UsageAlert {
notification_sent: false,
notification_channels: Vec::new(),
}
}
fn generate_message(
@ -161,7 +166,7 @@ impl UsageAlert {
limit: u64,
) -> String {
let metric_name = metric.display_name();
let severity_text = match severity {
let severity_ = match severity {
AlertSeverity::Warning => "approaching limit",
AlertSeverity::Critical => "near limit",
AlertSeverity::Exceeded => "exceeded limit",
@ -171,11 +176,12 @@ impl UsageAlert {
"{} {} usage is {} ({:.1}% - {}/{})",
severity.emoji(),
metric_name,
severity_text,
severity_,
percentage,
Self::format_value(metric, current),
Self::format_value(metric, limit)
)
}
fn format_value(metric: UsageMetric, value: u64) -> String {
@ -198,10 +204,11 @@ impl UsageAlert {
self.notification_sent = true;
self.notification_channels = channels;
}
}
/// Notification delivery channels
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NotificationChannel {
Email,
@ -318,7 +325,7 @@ impl AlertManager {
};
// Check cooldown
if self.is_in_cooldown(org_id, metric, severity).await {
if self.is_in_cooldown(org_id, metric, severity.clone()).await {
return None;
}
@ -340,6 +347,7 @@ impl AlertManager {
self.send_notifications(org_id, &alert).await;
Some(alert)
}
/// Check multiple metrics at once
@ -357,6 +365,7 @@ impl AlertManager {
}
alerts
}
/// Get active alerts for an organization
@ -379,6 +388,7 @@ impl AlertManager {
}
alerts
}
/// Acknowledge an alert
@ -399,6 +409,7 @@ impl AlertManager {
alert.acknowledge(user_id);
Ok(())
}
/// Dismiss an alert
@ -421,6 +432,7 @@ impl AlertManager {
self.add_to_history(org_id, alert.clone()).await;
Ok(alert)
}
/// Clear all alerts for an organization
@ -453,6 +465,7 @@ impl AlertManager {
}
counts
}
// ========================================================================
@ -479,6 +492,7 @@ impl AlertManager {
&& alert.severity == severity
&& alert.created_at > cooldown_threshold
})
}
async fn store_alert(&self, org_id: Uuid, alert: UsageAlert) {
@ -491,6 +505,7 @@ impl AlertManager {
});
org_alerts.push(alert);
}
async fn add_to_history(&self, org_id: Uuid, alert: UsageAlert) {
@ -503,6 +518,7 @@ impl AlertManager {
if org_history.len() > self.max_history_per_org {
org_history.truncate(self.max_history_per_org);
}
}
async fn send_notifications(&self, org_id: Uuid, alert: &UsageAlert) {
@ -513,7 +529,7 @@ impl AlertManager {
}
// Check if this severity should be notified
if !prefs.should_notify(alert.severity) {
if !prefs.should_notify(alert.severity.clone()) {
return;
}
@ -532,7 +548,9 @@ impl AlertManager {
}
}
}
}
}
impl Default for AlertManager {
@ -591,6 +609,7 @@ impl NotificationPreferences {
false
}
}
}
/// Quiet hours configuration
@ -599,7 +618,7 @@ pub struct QuietHours {
pub start_hour: u8,
pub end_hour: u8,
pub timezone: String,
pub days: Vec<chrono::Weekday>,
pub days: Vecchrono::Weekday
}
impl QuietHours {
@ -614,7 +633,9 @@ impl QuietHours {
// Overnight quiet hours
hour >= self.start_hour || hour < self.end_hour
}
}
}
/// Per-metric notification override
@ -702,7 +723,7 @@ impl AlertNotification {
Self {
alert_id: alert.id,
organization_id: alert.organization_id,
severity: alert.severity,
severity: alert.severity.clone(),
title: format!(
"{} Usage Alert: {}",
alert.severity.emoji(),
@ -725,12 +746,14 @@ impl AlertNotification {
// ============================================================================
/// Email notification handler
#[cfg(feature = "mail")]
pub struct EmailNotificationHandler {
_smtp_host: String,
_smtp_port: u16,
_from_address: String,
}
#[cfg(feature = "mail")]
impl EmailNotificationHandler {
pub fn new(smtp_host: String, smtp_port: u16, from_address: String) -> Self {
Self {
@ -741,6 +764,7 @@ impl EmailNotificationHandler {
}
}
#[cfg(feature = "mail")]
#[async_trait::async_trait]
impl NotificationHandler for EmailNotificationHandler {
fn channel(&self) -> NotificationChannel {
@ -748,63 +772,15 @@ impl NotificationHandler for EmailNotificationHandler {
}
async fn send(&self, notification: &AlertNotification) -> Result<(), NotificationError> {
use lettre::{Message, SmtpTransport, Transport};
use lettre::transport::smtp::authentication::Credentials;
tracing::info!(
"Sending email notification for alert {} to {:?}",
notification.alert_id,
notification.recipients
// Email functionality is only available when the mail feature is enabled
// This stub implementation prevents compilation errors when mail feature is disabled
tracing::warn!(
"Email notifications require the 'mail' feature to be enabled. Alert {} not sent.",
notification.alert_id
);
// Get SMTP config from environment
let smtp_host = std::env::var("SMTP_HOST").unwrap_or_else(|_| "localhost".to_string());
let smtp_user = std::env::var("SMTP_USER").ok();
let smtp_pass = std::env::var("SMTP_PASS").ok();
let from_email = std::env::var("SMTP_FROM").unwrap_or_else(|_| "alerts@generalbots.com".to_string());
let subject = format!("[{}] Billing Alert: {}",
notification.severity.to_string().to_uppercase(),
notification.title
);
let body = format!(
"Alert: {}\nSeverity: {}\nOrganization: {}\nTime: {}\n\nMessage: {}\n\nThreshold: {:?}\nCurrent Value: {:?}",
notification.title,
notification.severity,
notification.organization_id,
notification.created_at,
notification.message,
notification.limit,
notification.current_usage
);
for recipient in &notification.recipients {
let email = Message::builder()
.from(from_email.parse().map_err(|e| NotificationError::DeliveryFailed(format!("Invalid from address: {}", e)))?)
.to(recipient.parse().map_err(|e| NotificationError::DeliveryFailed(format!("Invalid recipient {}: {}", recipient, e)))?)
.subject(&subject)
.body(body.clone())
.map_err(|e| NotificationError::DeliveryFailed(format!("Failed to build email: {}", e)))?;
let mailer = if let (Some(user), Some(pass)) = (&smtp_user, &smtp_pass) {
let creds = Credentials::new(user.clone(), pass.clone());
SmtpTransport::relay(&smtp_host)
.map_err(|e| NotificationError::DeliveryFailed(format!("SMTP relay error: {}", e)))?
.credentials(creds)
.build()
} else {
SmtpTransport::builder_dangerous(&smtp_host).build()
};
mailer.send(&email)
.map_err(|e| NotificationError::DeliveryFailed(format!("Failed to send to {}: {}", recipient, e)))?;
tracing::debug!("Email sent to {}", recipient);
}
Ok(())
}
}
/// Webhook notification handler
@ -834,7 +810,7 @@ impl NotificationHandler for WebhookNotificationHandler {
notification.alert_id
);
// Get webhook URL from context or environment
// Get webhook URL from con or environment
let webhook_url = std::env::var("BILLING_WEBHOOK_URL").ok();
let url = match webhook_url {
@ -878,13 +854,15 @@ impl NotificationHandler for WebhookNotificationHandler {
tracing::debug!("Webhook notification sent successfully to {}", url);
Ok(())
}
}
/// In-app notification handler
pub struct InAppNotificationHandler {
/// Broadcast channel for WebSocket notifications
broadcast: Option<tokio::sync::broadcast::Sender<crate::core::shared::state::BillingAlertNotification>>,
broadcast: Option<tokio::sync::broadcast::Sender<BillingAlertNotification>>,
}
impl InAppNotificationHandler {
@ -894,12 +872,13 @@ impl InAppNotificationHandler {
/// Create with a broadcast channel for WebSocket notifications
pub fn with_broadcast(
broadcast: tokio::sync::broadcast::Sender<crate::core::shared::state::BillingAlertNotification>,
broadcast: tokio::sync::broadcast::Sender<BillingAlertNotification>,
) -> Self {
Self {
broadcast: Some(broadcast),
}
}
}
impl Default for InAppNotificationHandler {
@ -967,199 +946,9 @@ impl NotificationHandler for InAppNotificationHandler {
);
Ok(())
}
}
/// Slack notification handler
pub struct SlackNotificationHandler {}
impl SlackNotificationHandler {
pub fn new() -> Self {
Self {}
}
fn build_slack_message(&self, notification: &AlertNotification) -> serde_json::Value {
let color = match notification.severity {
AlertSeverity::Warning => "#FFA500",
AlertSeverity::Critical => "#FF0000",
AlertSeverity::Exceeded => "#8B0000",
};
serde_json::json!({
"attachments": [{
"color": color,
"title": notification.title,
"text": notification.message,
"fields": [
{
"title": "Metric",
"value": notification.metric,
"short": true
},
{
"title": "Usage",
"value": format!("{:.1}%", notification.percentage),
"short": true
}
],
"actions": [{
"type": "button",
"text": "View Usage",
"url": notification.action_url
}],
"ts": notification.created_at.timestamp()
}]
})
}
}
impl Default for SlackNotificationHandler {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl NotificationHandler for SlackNotificationHandler {
fn channel(&self) -> NotificationChannel {
NotificationChannel::Slack
}
async fn send(&self, notification: &AlertNotification) -> Result<(), NotificationError> {
tracing::info!(
"Sending Slack notification for alert {}",
notification.alert_id
);
// Get Slack webhook URL from context or environment
let webhook_url = std::env::var("SLACK_WEBHOOK_URL").ok();
let url = match webhook_url {
Some(url) => url,
None => {
tracing::warn!("No Slack webhook URL configured for alert {}", notification.alert_id);
return Ok(()); // Silent skip if not configured
}
};
let message = self.build_slack_message(notification);
let client = reqwest::Client::new();
let response = client
.post(&url)
.header("Content-Type", "application/json")
.json(&message)
.timeout(std::time::Duration::from_secs(30))
.send()
.await
.map_err(|e| NotificationError::DeliveryFailed(format!("Slack request failed: {}", e)))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(NotificationError::DeliveryFailed(
format!("Slack webhook returned {}: {}", status, body)
));
}
tracing::debug!("Slack notification sent successfully");
Ok(())
}
}
/// Microsoft Teams notification handler
pub struct TeamsNotificationHandler {}
impl TeamsNotificationHandler {
pub fn new() -> Self {
Self {}
}
fn build_teams_message(&self, notification: &AlertNotification) -> serde_json::Value {
let theme_color = match notification.severity {
AlertSeverity::Warning => "FFA500",
AlertSeverity::Critical => "FF0000",
AlertSeverity::Exceeded => "8B0000",
};
serde_json::json!({
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"themeColor": theme_color,
"summary": notification.title,
"sections": [{
"activityTitle": notification.title,
"facts": [
{ "name": "Metric", "value": notification.metric },
{ "name": "Current Usage", "value": format!("{:.1}%", notification.percentage) },
{ "name": "Severity", "value": notification.severity.as_str() }
],
"text": notification.message
}],
"potentialAction": [{
"@type": "OpenUri",
"name": "View Usage",
"targets": [{
"os": "default",
"uri": notification.action_url
}]
}]
})
}
}
impl Default for TeamsNotificationHandler {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl NotificationHandler for TeamsNotificationHandler {
fn channel(&self) -> NotificationChannel {
NotificationChannel::MsTeams
}
async fn send(&self, notification: &AlertNotification) -> Result<(), NotificationError> {
tracing::info!(
"Sending Teams notification for alert {}",
notification.alert_id
);
// Get Teams webhook URL from context or environment
let webhook_url = std::env::var("TEAMS_WEBHOOK_URL").ok();
let url = match webhook_url {
Some(url) => url,
None => {
tracing::warn!("No Teams webhook URL configured for alert {}", notification.alert_id);
return Ok(()); // Silent skip if not configured
}
};
let message = self.build_teams_message(notification);
let client = reqwest::Client::new();
let response = client
.post(&url)
.header("Content-Type", "application/json")
.json(&message)
.timeout(std::time::Duration::from_secs(30))
.send()
.await
.map_err(|e| NotificationError::DeliveryFailed(format!("Teams request failed: {}", e)))?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(NotificationError::DeliveryFailed(
format!("Teams webhook returned {}: {}", status, body)
));
}
tracing::debug!("Teams notification sent successfully");
Ok(())
}
}
// ============================================================================
@ -1232,6 +1021,7 @@ fn format_bytes(bytes: u64) -> String {
} else {
format!("{} bytes", bytes)
}
}
/// Format number with thousands separators
@ -1266,225 +1056,6 @@ impl UsageMetric {
}
}
// ============================================================================
// Grace Period Support
// ============================================================================
/// Grace period configuration for quota overages
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GracePeriodConfig {
/// Whether grace period is enabled
pub enabled: bool,
/// Grace period duration in hours
pub duration_hours: u32,
/// Maximum overage percentage allowed during grace period
pub max_overage_percent: f64,
/// Metrics that support grace period
pub applicable_metrics: Vec<UsageMetric>,
}
impl Default for GracePeriodConfig {
fn default() -> Self {
Self {
enabled: true,
duration_hours: 24,
max_overage_percent: 10.0,
applicable_metrics: vec![
UsageMetric::Messages,
UsageMetric::ApiCalls,
],
}
}
}
/// Grace period status for an organization
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GracePeriodStatus {
pub organization_id: Uuid,
pub metric: UsageMetric,
pub started_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub overage_at_start: u64,
pub current_overage: u64,
pub max_allowed_overage: u64,
pub is_active: bool,
}
impl GracePeriodStatus {
pub fn new(
organization_id: Uuid,
metric: UsageMetric,
config: &GracePeriodConfig,
current_usage: u64,
limit: u64,
) -> Self {
let now = Utc::now();
let overage = current_usage.saturating_sub(limit);
let max_allowed = (limit as f64 * config.max_overage_percent / 100.0) as u64;
Self {
organization_id,
metric,
started_at: now,
expires_at: now + chrono::Duration::hours(config.duration_hours as i64),
overage_at_start: overage,
current_overage: overage,
max_allowed_overage: max_allowed,
is_active: true,
}
}
pub fn is_expired(&self) -> bool {
Utc::now() > self.expires_at
}
pub fn is_within_limits(&self) -> bool {
self.current_overage <= self.max_allowed_overage
}
pub fn remaining_time(&self) -> chrono::Duration {
self.expires_at.signed_duration_since(Utc::now())
}
pub fn update_overage(&mut self, current_usage: u64, limit: u64) {
self.current_overage = current_usage.saturating_sub(limit);
if self.is_expired() || !self.is_within_limits() {
self.is_active = false;
}
}
}
/// Grace period manager
pub struct GracePeriodManager {
config: GracePeriodConfig,
active_periods: Arc<RwLock<HashMap<(Uuid, UsageMetric), GracePeriodStatus>>>,
}
impl GracePeriodManager {
pub fn new(config: GracePeriodConfig) -> Self {
Self {
config,
active_periods: Arc::new(RwLock::new(HashMap::new())),
}
}
/// Check if grace period allows the operation
pub async fn check_grace_period(
&self,
org_id: Uuid,
metric: UsageMetric,
current_usage: u64,
limit: u64,
) -> GracePeriodDecision {
if !self.config.enabled || !self.config.applicable_metrics.contains(&metric) {
return GracePeriodDecision::NotApplicable;
}
let key = (org_id, metric);
let mut periods = self.active_periods.write().await;
if let Some(status) = periods.get_mut(&key) {
status.update_overage(current_usage, limit);
if status.is_active && status.is_within_limits() {
return GracePeriodDecision::InGracePeriod {
remaining: status.remaining_time(),
overage_used: status.current_overage,
overage_limit: status.max_allowed_overage,
};
} else {
periods.remove(&key);
return GracePeriodDecision::GracePeriodExpired;
}
}
// Start new grace period if within overage limits
let potential_status = GracePeriodStatus::new(
org_id,
metric,
&self.config,
current_usage,
limit,
);
if potential_status.is_within_limits() {
let remaining = potential_status.remaining_time();
let overage_used = potential_status.current_overage;
let overage_limit = potential_status.max_allowed_overage;
periods.insert(key, potential_status);
GracePeriodDecision::GracePeriodStarted {
duration_hours: self.config.duration_hours,
remaining,
overage_used,
overage_limit,
}
} else {
GracePeriodDecision::OverageExceedsLimit {
current_overage: current_usage.saturating_sub(limit),
max_allowed: potential_status.max_allowed_overage,
}
}
}
/// Get active grace period status
pub async fn get_status(
&self,
org_id: Uuid,
metric: UsageMetric,
) -> Option<GracePeriodStatus> {
let periods = self.active_periods.read().await;
periods.get(&(org_id, metric)).cloned()
}
/// End grace period early (e.g., after upgrade)
pub async fn end_grace_period(&self, org_id: Uuid, metric: UsageMetric) {
let mut periods = self.active_periods.write().await;
periods.remove(&(org_id, metric));
}
/// Clean up expired grace periods
pub async fn cleanup_expired(&self) {
let mut periods = self.active_periods.write().await;
periods.retain(|_, status| !status.is_expired());
}
}
/// Grace period decision
#[derive(Debug, Clone)]
pub enum GracePeriodDecision {
NotApplicable,
GracePeriodStarted {
duration_hours: u32,
remaining: chrono::Duration,
overage_used: u64,
overage_limit: u64,
},
InGracePeriod {
remaining: chrono::Duration,
overage_used: u64,
overage_limit: u64,
},
GracePeriodExpired,
OverageExceedsLimit {
current_overage: u64,
max_allowed: u64,
},
}
impl GracePeriodDecision {
pub fn allows_operation(&self) -> bool {
matches!(
self,
Self::NotApplicable
| Self::GracePeriodStarted { .. }
| Self::InGracePeriod { .. }
)
}
}
// ============================================================================
// Alert Service Factory
// ============================================================================
@ -1505,6 +1076,7 @@ pub fn create_alert_manager(
}
manager
}
/// Create default notification handlers
@ -1515,10 +1087,4 @@ pub async fn register_default_handlers(manager: &AlertManager) {
manager
.register_handler(Arc::new(WebhookNotificationHandler::new()))
.await;
manager
.register_handler(Arc::new(SlackNotificationHandler::new()))
.await;
manager
.register_handler(Arc::new(TeamsNotificationHandler::new()))
.await;
}

View file

@ -8,9 +8,13 @@ use axum::{
use chrono::{DateTime, Utc};
use diesel::prelude::*;
use diesel::sql_types::{Nullable, Text, Timestamptz, Uuid as DieselUuid, Varchar};
#[cfg(feature = "mail")]
use lettre::{Message, SmtpTransport, Transport};
#[cfg(feature = "mail")]
use lettre::transport::smtp::authentication::Credentials;
use log::{info, warn};
use log::warn;
#[cfg(feature = "mail")]
use log::info;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
@ -20,6 +24,7 @@ use uuid::Uuid;
// ============================================================================
/// Send invitation email via SMTP
#[cfg(feature = "mail")]
async fn send_invitation_email(
to_email: &str,
role: &str,
@ -77,6 +82,7 @@ The General Bots Team"#,
}
/// Send invitation email by fetching details from database
#[cfg(feature = "mail")]
async fn send_invitation_email_by_id(invitation_id: Uuid) -> Result<(), String> {
let smtp_host = std::env::var("SMTP_HOST").unwrap_or_else(|_| "localhost".to_string());
let smtp_user = std::env::var("SMTP_USER").ok();

View file

@ -1,14 +1,8 @@
//! Memory and CPU monitoring with thread tracking
//!
//! This module provides tools to track memory/CPU usage per thread
//! and identify potential leaks or CPU hogs in the botserver application.
//!
//! When compiled with the `jemalloc` feature, provides detailed allocation statistics.
use log::{debug, info, trace, warn};
use std::collections::HashMap;
use std::sync::{LazyLock, Mutex, RwLock};
use std::time::{Duration, Instant};
#[cfg(feature = "monitoring")]
use sysinfo::{Pid, ProcessesToUpdate, System};
static THREAD_REGISTRY: LazyLock<RwLock<HashMap<String, ThreadInfo>>> =
@ -87,6 +81,7 @@ impl MemoryStats {
}
}
pub fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
@ -110,6 +105,7 @@ impl MemoryStats {
Self::format_bytes(self.virtual_bytes),
);
}
}
/// Get jemalloc memory statistics when the feature is enabled
@ -117,6 +113,7 @@ impl MemoryStats {
pub fn get_jemalloc_stats() -> Option<JemallocStats> {
use tikv_jemalloc_ctl::{epoch, stats};
// Advance the epoch to refresh statistics
if epoch::advance().is_err() {
return None;
@ -135,6 +132,7 @@ pub fn get_jemalloc_stats() -> Option<JemallocStats> {
mapped,
retained,
})
}
#[cfg(not(feature = "jemalloc"))]
@ -169,6 +167,7 @@ impl JemallocStats {
);
}
/// Calculate fragmentation ratio (1.0 = no fragmentation)
pub fn fragmentation_ratio(&self) -> f64 {
if self.allocated > 0 {
@ -177,6 +176,7 @@ impl JemallocStats {
1.0
}
}
}
/// Log jemalloc stats if available
@ -210,6 +210,7 @@ impl MemoryCheckpoint {
}
}
pub fn compare_and_log(&self) {
let current = MemoryStats::current();
let diff = current.rss_bytes as i64 - self.stats.rss_bytes as i64;
@ -230,6 +231,7 @@ impl MemoryCheckpoint {
debug!("[CHECKPOINT] {} unchanged", self.name);
}
}
}
pub struct ComponentMemoryTracker {
@ -245,6 +247,7 @@ impl ComponentMemoryTracker {
}
}
pub fn record(&self, component: &str) {
let stats = MemoryStats::current();
if let Ok(mut components) = self.components.lock() {
@ -295,6 +298,7 @@ impl ComponentMemoryTracker {
}
}
}
}
pub fn record_component(component: &str) {
@ -322,6 +326,7 @@ impl LeakDetector {
}
}
pub fn reset_baseline(&self) {
let current = MemoryStats::current();
if let Ok(mut baseline) = self.baseline.lock() {
@ -378,11 +383,13 @@ impl LeakDetector {
None
}
}
pub fn start_memory_monitor(interval_secs: u64, warn_threshold_mb: u64) {
let detector = LeakDetector::new(warn_threshold_mb, 5);
tokio::spawn(async move {
register_thread("memory-monitor", "monitoring");
@ -452,14 +459,24 @@ pub fn start_memory_monitor(interval_secs: u64, warn_threshold_mb: u64) {
}
}
});
}
#[cfg(feature = "monitoring")]
#[cfg(feature = "monitoring")]
pub fn get_process_memory() -> Option<(u64, u64)> {
let pid = Pid::from_u32(std::process::id());
let mut sys = System::new();
sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true);
sys.process(pid).map(|p| (p.memory(), p.virtual_memory()))
}
#[cfg(not(feature = "monitoring"))]
pub fn get_process_memory() -> Option<(u64, u64)> {
None
}
pub fn log_process_memory() {
@ -476,6 +493,7 @@ pub fn log_process_memory() {
mod tests {
use super::*;
#[test]
fn test_memory_stats() {
let stats = MemoryStats::current();
@ -503,4 +521,5 @@ mod tests {
log_thread_stats();
unregister_thread("test-thread");
}
}

View file

@ -15,7 +15,7 @@ use crate::core::shared::test_utils::create_mock_auth_service;
#[cfg(all(test, feature = "llm"))]
use crate::core::shared::test_utils::MockLLMProvider;
#[cfg(feature = "directory")]
use crate::directory::AuthService;
use crate::core::directory::AuthService;
#[cfg(feature = "llm")]
use crate::llm::LLMProvider;
use crate::shared::models::BotResponse;

View file

@ -5,9 +5,9 @@ use crate::core::session::SessionManager;
use crate::core::shared::analytics::MetricsCollector;
use crate::core::shared::state::{AppState, Extensions};
#[cfg(feature = "directory")]
use crate::directory::client::ZitadelConfig;
use crate::core::directory::client::ZitadelConfig;
#[cfg(feature = "directory")]
use crate::directory::AuthService;
use crate::core::directory::AuthService;
#[cfg(feature = "llm")]
use crate::llm::LLMProvider;
use crate::shared::models::BotResponse;

View file

@ -1,4 +1,5 @@
use anyhow::Result;
#[cfg(feature = "sheet")]
use calamine::Reader;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
@ -63,6 +64,7 @@ impl UserDriveVectorDB {
pub fn new(user_id: Uuid, bot_id: Uuid, db_path: PathBuf) -> Self {
let collection_name = format!("drive_{}_{}", bot_id, user_id);
Self {
user_id,
bot_id,
@ -541,6 +543,7 @@ impl UserDriveVectorDB {
}
Ok(())
}
}
#[derive(Debug)]
@ -554,6 +557,7 @@ impl FileContentExtractor {
Ok(content)
}
t if t.starts_with("text/") => {
let content = fs::read_to_string(file_path).await?;
Ok(content)
@ -573,8 +577,16 @@ impl FileContentExtractor {
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
| "application/vnd.ms-excel" => {
log::info!("Spreadsheet extraction for {}", file_path.display());
#[cfg(feature = "sheet")]
{
Self::extract_xlsx_text(file_path).await
}
#[cfg(not(feature = "sheet"))]
{
log::warn!("XLSX extraction requires 'sheet' feature");
Ok(String::new())
}
}
"application/json" => {
let content = fs::read_to_string(file_path).await?;
@ -669,6 +681,7 @@ impl FileContentExtractor {
}
}
#[cfg(feature = "sheet")]
async fn extract_xlsx_text(file_path: &Path) -> Result<String> {
let path = file_path.to_path_buf();
@ -738,4 +751,5 @@ impl FileContentExtractor {
| "text/x-java"
)
}
}

View file

@ -29,11 +29,15 @@ pub mod tickets;
pub mod attendant;
pub mod analytics;
pub mod designer;
#[cfg(feature = "docs")]
pub mod docs;
pub mod learn;
#[cfg(feature = "paper")]
pub mod paper;
pub mod research;
#[cfg(feature = "sheet")]
pub mod sheet;
#[cfg(feature = "slides")]
pub mod slides;
pub mod social;
pub mod sources;
@ -203,7 +207,7 @@ use crate::core::bot_database::BotDatabaseManager;
use crate::core::config::AppConfig;
#[cfg(feature = "directory")]
use crate::directory::auth_handler;
use crate::core::directory::auth_handler;
use package_manager::InstallMode;
use session::{create_session, get_session_history, get_sessions, start_session};
@ -448,10 +452,22 @@ async fn run_axum_server(
api_router = api_router.merge(crate::analytics::configure_analytics_routes());
api_router = api_router.merge(crate::core::i18n::configure_i18n_routes());
#[cfg(feature = "docs")]
{
api_router = api_router.merge(crate::docs::configure_docs_routes());
}
#[cfg(feature = "paper")]
{
api_router = api_router.merge(crate::paper::configure_paper_routes());
}
#[cfg(feature = "sheet")]
{
api_router = api_router.merge(crate::sheet::configure_sheet_routes());
}
#[cfg(feature = "slides")]
{
api_router = api_router.merge(crate::slides::configure_slides_routes());
}
api_router = api_router.merge(crate::video::configure_video_routes());
api_router = api_router.merge(crate::video::ui::configure_video_ui_routes());
api_router = api_router.merge(crate::research::configure_research_routes());
@ -486,9 +502,15 @@ async fn run_axum_server(
api_router = api_router.merge(crate::canvas::ui::configure_canvas_ui_routes());
api_router = api_router.merge(crate::social::configure_social_routes());
api_router = api_router.merge(crate::social::ui::configure_social_ui_routes());
api_router = api_router.merge(crate::email::ui::configure_email_ui_routes());
api_router = api_router.merge(crate::learn::ui::configure_learn_ui_routes());
#[cfg(feature = "email")]
{
api_router = api_router.merge(crate::email::ui::configure_email_ui_routes());
}
#[cfg(feature = "meet")]
{
api_router = api_router.merge(crate::meet::ui::configure_meet_ui_routes());
}
api_router = api_router.merge(crate::contacts::crm_ui::configure_crm_routes());
api_router = api_router.merge(crate::contacts::crm::configure_crm_api_routes());
api_router = api_router.merge(crate::billing::billing_ui::configure_billing_routes());
@ -1060,7 +1082,7 @@ use crate::core::config::ConfigManager;
info!("Loaded Zitadel config from {}: url={}", config_path, base_url);
crate::directory::client::ZitadelConfig {
crate::core::directory::client::ZitadelConfig {
issuer_url: base_url.to_string(),
issuer: base_url.to_string(),
client_id: client_id.to_string(),
@ -1072,7 +1094,7 @@ use crate::core::config::ConfigManager;
}
} else {
warn!("Failed to parse directory_config.json, using defaults");
crate::directory::client::ZitadelConfig {
crate::core::directory::client::ZitadelConfig {
issuer_url: "http://localhost:8300".to_string(),
issuer: "http://localhost:8300".to_string(),
client_id: String::new(),
@ -1085,7 +1107,7 @@ use crate::core::config::ConfigManager;
}
} else {
warn!("directory_config.json not found, using default Zitadel config");
crate::directory::client::ZitadelConfig {
crate::core::directory::client::ZitadelConfig {
issuer_url: "http://localhost:8300".to_string(),
issuer: "http://localhost:8300".to_string(),
client_id: String::new(),
@ -1099,7 +1121,7 @@ use crate::core::config::ConfigManager;
};
#[cfg(feature = "directory")]
let auth_service = Arc::new(tokio::sync::Mutex::new(
crate::directory::AuthService::new(zitadel_config.clone()).map_err(|e| std::io::Error::other(format!("Failed to create auth service: {}", e)))?,
crate::core::directory::AuthService::new(zitadel_config.clone()).map_err(|e| std::io::Error::other(format!("Failed to create auth service: {}", e)))?,
));
#[cfg(feature = "directory")]
@ -1110,22 +1132,22 @@ use crate::core::config::ConfigManager;
Ok(pat_token) => {
let pat_token = pat_token.trim().to_string();
info!("Using admin PAT token for bootstrap authentication");
crate::directory::client::ZitadelClient::with_pat_token(zitadel_config, pat_token)
crate::core::directory::client::ZitadelClient::with_pat_token(zitadel_config, pat_token)
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client with PAT: {}", e)))?
}
Err(e) => {
warn!("Failed to read admin PAT token: {}, falling back to OAuth2", e);
crate::directory::client::ZitadelClient::new(zitadel_config)
crate::core::directory::client::ZitadelClient::new(zitadel_config)
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client: {}", e)))?
}
}
} else {
info!("Admin PAT not found, using OAuth2 client credentials for bootstrap");
crate::directory::client::ZitadelClient::new(zitadel_config)
crate::core::directory::client::ZitadelClient::new(zitadel_config)
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client: {}", e)))?
};
match crate::directory::bootstrap::check_and_bootstrap_admin(&bootstrap_client).await {
match crate::core::directory::bootstrap::check_and_bootstrap_admin(&bootstrap_client).await {
Ok(Some(_)) => {
info!("Bootstrap completed - admin credentials displayed in console");
}

View file

@ -1,7 +1,7 @@
use axum::{extract::State, response::Html, routing::get, Router};
use chrono::Local;
use std::sync::Arc;
#[cfg(feature = "monitoring")]
use sysinfo::{Disks, Networks, System};
use crate::core::urls::ApiUrls;
@ -10,7 +10,6 @@ use crate::shared::state::AppState;
pub mod real_time;
pub mod tracing;
pub fn configure() -> Router<Arc<AppState>> {
Router::new()
.route(ApiUrls::MONITORING_DASHBOARD, get(dashboard))
@ -35,8 +34,9 @@ pub fn configure() -> Router<Arc<AppState>> {
.route("/api/ui/monitoring/messages", get(messages_panel))
}
async fn dashboard(State(state): State<Arc<AppState>>) -> Html<String> {
#[cfg(feature = "monitoring")]
let (cpu_usage, total_memory, used_memory, memory_percent, uptime_str) = {
let mut sys = System::new_all();
sys.refresh_all();
@ -52,6 +52,14 @@ async fn dashboard(State(state): State<Arc<AppState>>) -> Html<String> {
let uptime = System::uptime();
let uptime_str = format_uptime(uptime);
(cpu_usage, total_memory, used_memory, memory_percent, uptime_str)
};
#[cfg(not(feature = "monitoring"))]
let (cpu_usage, total_memory, used_memory, memory_percent, uptime_str) = (
0.0, 0, 0, 0.0, "N/A".to_string()
);
let active_sessions = state
.session_manager
.try_lock()
@ -97,30 +105,7 @@ async fn dashboard(State(state): State<Arc<AppState>>) -> Html<String> {
<div class="metric-value">{uptime_str}</div>
<div class="metric-subtitle">System running time</div>
</div>
</div>
<div class="refresh-indicator" hx-get="/api/monitoring/dashboard" hx-trigger="every 10s" hx-swap="outerHTML" hx-target="closest .dashboard-grid, .refresh-indicator">
<span class="refresh-dot"></span> Auto-refreshing
</div>"##,
cpu_status = if cpu_usage > 80.0 {
"danger"
} else if cpu_usage > 60.0 {
"warning"
} else {
"success"
},
mem_status = if memory_percent > 80.0 {
"danger"
} else if memory_percent > 60.0 {
"warning"
} else {
"success"
},
used_gb = used_memory as f64 / 1_073_741_824.0,
total_gb = total_memory as f64 / 1_073_741_824.0,
))
}
</div><div class="refresh-indicator" hx-get="/api/monitoring/dashboard" hx-trigger="every 10s" hx-swap="outerHTML" hx-target="closest .dashboard-grid, .refresh-indicator"> <span class="refresh-dot"></span> Auto-refreshing </div>"##, cpu_status = if cpu_usage > 80.0 { "danger" } else if cpu_usage > 60.0 { "warning" } else { "success" }, mem_status = if memory_percent > 80.0 { "danger" } else if memory_percent > 60.0 { "warning" } else { "success" }, used_gb = used_memory as f64 / 1_073_741_824.0, total_gb = total_memory as f64 / 1_073_741_824.0, )) }
async fn services(State(_state): State<Arc<AppState>>) -> Html<String> {
let services = vec![
@ -151,11 +136,7 @@ async fn services(State(_state): State<Arc<AppState>>) -> Html<String> {
<td>
<button class="btn-sm" hx-post="/api/monitoring/services/{name_lower}/restart" hx-swap="none">Restart</button>
</td>
</tr>"##,
name_lower = name.to_lowercase().replace(' ', "-"),
));
}
</tr>"##, name_lower = name.to_lowercase().replace(' ', "-"), )); }
Html(format!(
r##"<div class="services-view">
<div class="section-header">
@ -177,12 +158,11 @@ async fn services(State(_state): State<Arc<AppState>>) -> Html<String> {
{rows}
</tbody>
</table>
</div>"##
))
}
</div>"## )) }
async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
#[cfg(feature = "monitoring")]
let (disk_rows, net_rows) = {
let mut sys = System::new_all();
sys.refresh_all();
@ -210,20 +190,7 @@ async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
</div>
<span class="usage-text">{percent:.1}%</span>
</td>
</tr>"##,
mount = disk.mount_point().display(),
used_gb = used as f64 / 1_073_741_824.0,
total_gb = total as f64 / 1_073_741_824.0,
status = if percent > 90.0 {
"danger"
} else if percent > 70.0 {
"warning"
} else {
"success"
},
));
}
</tr>"##, mount = disk.mount_point().display(), used_gb = used as f64 / 1_073_741_824.0, total_gb = total as f64 / 1_073_741_824.0, status = if percent > 90.0 { "danger" } else if percent > 70.0 { "warning" } else { "success" }, )); }
let networks = Networks::new_with_refreshed_list();
let mut net_rows = String::new();
@ -233,11 +200,15 @@ async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
<td>{name}</td>
<td>{rx:.2} MB</td>
<td>{tx:.2} MB</td>
</tr>"##,
rx = data.total_received() as f64 / 1_048_576.0,
tx = data.total_transmitted() as f64 / 1_048_576.0,
));
}
</tr>"##, rx = data.total_received() as f64 / 1_048_576.0, tx = data.total_transmitted() as f64 / 1_048_576.0, )); }
(disk_rows, net_rows)
};
#[cfg(not(feature = "monitoring"))]
let (disk_rows, net_rows) = (
String::new(),
String::new()
);
Html(format!(
r##"<div class="resources-view">
@ -277,10 +248,7 @@ async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
</tbody>
</table>
</div>
</div>"##
))
}
</div>"## )) }
async fn logs(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(
@ -308,11 +276,8 @@ async fn logs(State(_state): State<Arc<AppState>>) -> Html<String> {
<span class="log-message">Monitoring initialized</span>
</div>
</div>
</div>"##
.to_string(),
)
}
</div>"## .to_string(), ) }
async fn llm_metrics(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(
@ -362,11 +327,7 @@ async fn llm_metrics(State(_state): State<Arc<AppState>>) -> Html<String> {
</div>
</div>
</div>
</div>"##
.to_string(),
)
}
</div>"## .to_string(), ) }
async fn health(State(state): State<Arc<AppState>>) -> Html<String> {
let db_ok = state.conn.get().is_ok();
@ -376,10 +337,8 @@ async fn health(State(state): State<Arc<AppState>>) -> Html<String> {
r##"<div class="health-status {status}">
<span class="status-icon"></span>
<span class="status-text">{status}</span>
</div>"##
))
}
</iv>"## )) }
fn format_uptime(seconds: u64) -> String {
let days = seconds / 86400;
@ -393,35 +352,30 @@ fn format_uptime(seconds: u64) -> String {
} else {
format!("{}m", minutes)
}
}
}
fn check_postgres() -> bool {
true
}
fn check_redis() -> bool {
true
}
fn check_minio() -> bool {
true
}
fn check_llm() -> bool {
true
}
async fn timestamp(State(_state): State<Arc<AppState>>) -> Html<String> {
let now = Local::now();
Html(format!("Last updated: {}", now.format("%H:%M:%S")))
}
async fn bots(State(state): State<Arc<AppState>>) -> Html<String> {
let active_sessions = state
.session_manager
@ -435,10 +389,7 @@ async fn bots(State(state): State<Arc<AppState>>) -> Html<String> {
<span class="bot-name">Active Sessions</span>
<span class="bot-count">{active_sessions}</span>
</div>
</div>"##
))
}
</div>"## )) }
async fn services_status(State(_state): State<Arc<AppState>>) -> Html<String> {
let services = vec![
@ -462,10 +413,12 @@ async fn services_status(State(_state): State<Arc<AppState>>) -> Html<String> {
}
Html(status_updates)
}
async fn resources_bars(State(_state): State<Arc<AppState>>) -> Html<String> {
#[cfg(feature = "monitoring")]
let (cpu_usage, memory_percent) = {
let mut sys = System::new_all();
sys.refresh_all();
@ -478,30 +431,24 @@ async fn resources_bars(State(_state): State<Arc<AppState>>) -> Html<String> {
0.0
};
(cpu_usage, memory_percent)
};
#[cfg(not(feature = "monitoring"))]
let (cpu_usage, memory_percent) = (0.0, 0.0);
Html(format!(
r##"<g>
<text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">CPU</text>
<rect x="40" y="-8" width="100" height="10" rx="2" fill="#1e293b"/>
<rect x="40" y="-8" width="{cpu_width}" height="10" rx="2" fill="#3b82f6"/>
<text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{cpu_usage:.0}%</text>
</g>
<g transform="translate(0, 20)">
<text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">MEM</text>
<rect x="40" y="-8" width="100" height="10" rx="2" fill="#1e293b"/>
<rect x="40" y="-8" width="{mem_width}" height="10" rx="2" fill="#10b981"/>
<text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{memory_percent:.0}%</text>
</g>"##,
cpu_width = cpu_usage.min(100.0),
mem_width = memory_percent.min(100.0),
))
}
</g> <g transform="translate(0, 20)"> <text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">MEM</text> <rect x="40" y="-8" width="100" height="10" rx="2" fill="#1e293b"/> <rect x="40" y="-8" width="{mem_width}" height="10" rx="2" fill="#10b981"/> <text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{memory_percent:.0}%</text> </g>"##, cpu_width = cpu_usage.min(100.0), mem_width = memory_percent.min(100.0), )) }
async fn activity_latest(State(_state): State<Arc<AppState>>) -> Html<String> {
Html("System monitoring active...".to_string())
}
async fn metric_sessions(State(state): State<Arc<AppState>>) -> Html<String> {
let active_sessions = state
.session_manager
@ -510,29 +457,25 @@ async fn metric_sessions(State(state): State<Arc<AppState>>) -> Html<String> {
.unwrap_or(0);
Html(format!("{}", active_sessions))
}
}
async fn metric_messages(State(_state): State<Arc<AppState>>) -> Html<String> {
Html("--".to_string())
}
async fn metric_response_time(State(_state): State<Arc<AppState>>) -> Html<String> {
Html("--".to_string())
}
async fn trend_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
Html("↑ 0%".to_string())
}
async fn rate_messages(State(_state): State<Arc<AppState>>) -> Html<String> {
Html("0/hr".to_string())
}
async fn sessions_panel(State(state): State<Arc<AppState>>) -> Html<String> {
let active_sessions = state
.session_manager
@ -551,10 +494,7 @@ async fn sessions_panel(State(state): State<Arc<AppState>>) -> Html<String> {
<p>No active sessions</p>
</div>
</div>
</div>"##
))
}
</div>"## )) }
async fn messages_panel(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(
@ -567,7 +507,5 @@ async fn messages_panel(State(_state): State<Arc<AppState>>) -> Html<String> {
<p>No recent messages</p>
</div>
</div>
</div>"##
.to_string(),
)
}
</div>"## .to_string(), ) }

View file

@ -833,6 +833,7 @@ fn validate_session_sync(session_id: &str) -> Result<AuthenticatedUser, AuthErro
&session_id[..std::cmp::min(20, session_id.len())]);
// Try to get user data from session cache first
#[cfg(feature = "directory")]
if let Ok(cache_guard) = crate::directory::auth_routes::SESSION_CACHE.try_read() {
if let Some(user_data) = cache_guard.get(session_id) {
debug!("Found user in session cache: {}", user_data.email);

View file

@ -47,8 +47,8 @@ async fn get_accounts_social(State(_state): State<Arc<AppState>>) -> Html<String
<div class="account-item"><span class="account-icon">📘</span><span class="account-name">Facebook</span><span class="account-status disconnected">Not connected</span></div>
<div class="account-item"><span class="account-icon">🐦</span><span class="account-name">Twitter/X</span><span class="account-status disconnected">Not connected</span></div>
<div class="account-item"><span class="account-icon">💼</span><span class="account-name">LinkedIn</span><span class="account-status disconnected">Not connected</span></div>
</div>"##.to_string())
}
</div>"##.to_string()) }
async fn get_accounts_messaging(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(r##"<div class="accounts-list">
@ -56,16 +56,16 @@ async fn get_accounts_messaging(State(_state): State<Arc<AppState>>) -> Html<Str
<div class="account-item"><span class="account-icon">📱</span><span class="account-name">WhatsApp</span><span class="account-status disconnected">Not connected</span></div>
<div class="account-item"><span class="account-icon"></span><span class="account-name">Telegram</span><span class="account-status disconnected">Not connected</span></div>
<div class="account-item"><span class="account-icon">💼</span><span class="account-name">Teams</span><span class="account-status disconnected">Not connected</span></div>
</div>"##.to_string())
}
</div>"##.to_string()) }
async fn get_accounts_email(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(r##"<div class="accounts-list">
<div class="account-item"><span class="account-icon">📧</span><span class="account-name">Gmail</span><span class="account-status disconnected">Not connected</span></div>
<div class="account-item"><span class="account-icon">📨</span><span class="account-name">Outlook</span><span class="account-status disconnected">Not connected</span></div>
<div class="account-item"><span class="account-icon"></span><span class="account-name">SMTP</span><span class="account-status disconnected">Not configured</span></div>
</div>"##.to_string())
}
</div>"##.to_string()) }
async fn save_smtp_account(
State(_state): State<Arc<AppState>>,
@ -132,10 +132,8 @@ async fn get_storage_info(State(_state): State<Arc<AppState>>) -> Html<String> {
<span class="storage-size">500 MB</span>
</div>
</div>
</div>"##
.to_string(),
)
}
s
</div>"## .to_string(), ) }
async fn get_storage_connections(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(
@ -144,10 +142,8 @@ async fn get_storage_connections(State(_state): State<Arc<AppState>>) -> Html<St
<button class="btn-secondary" onclick="showAddConnectionModal()">
+ Add Connection
</button>
</div>"##
.to_string(),
)
}
</div>"## .to_string(), ) }
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
@ -177,11 +173,13 @@ async fn save_search_settings(
settings.enable_ai_suggestions
);
Json(SearchSettingsResponse {
success: true,
message: Some("Search settings saved successfully".to_string()),
error: None,
})
}
#[derive(Debug, Deserialize)]
@ -201,6 +199,7 @@ struct SmtpTestResponse {
error: Option<String>,
}
#[cfg(feature = "mail")]
async fn test_smtp_connection(
State(_state): State<Arc<AppState>>,
Json(config): Json<SmtpTestRequest>,
@ -208,6 +207,8 @@ async fn test_smtp_connection(
use lettre::SmtpTransport;
use lettre::transport::smtp::authentication::Credentials;
log::info!("Testing SMTP connection to {}:{}", config.host, config.port);
let mailer_result = if let (Some(user), Some(pass)) = (config.username, config.password) {
@ -246,6 +247,19 @@ async fn test_smtp_connection(
error: Some(format!("Failed to create SMTP transport: {}", e)),
}),
}
}
#[cfg(not(feature = "mail"))]
async fn test_smtp_connection(
State(_state): State<Arc<AppState>>,
Json(_config): Json<SmtpTestRequest>,
) -> Json<SmtpTestResponse> {
Json(SmtpTestResponse {
success: false,
message: None,
error: Some("SMTP email feature is not enabled in this build".to_string()),
})
}
async fn get_2fa_status(State(_state): State<Arc<AppState>>) -> Html<String> {
@ -253,30 +267,24 @@ async fn get_2fa_status(State(_state): State<Arc<AppState>>) -> Html<String> {
r##"<div class="status-indicator">
<span class="status-dot inactive"></span>
<span class="status-text">Two-factor authentication is not enabled</span>
</div>"##
.to_string(),
)
}
</div>"## .to_string(), ) }
async fn enable_2fa(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(
r##"<div class="status-indicator">
<span class="status-dot active"></span>
<span class="status-text">Two-factor authentication enabled</span>
</div>"##
.to_string(),
)
}
</div>"## .to_string(), ) }
async fn disable_2fa(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(
r##"<div class="status-indicator">
<span class="status-dot inactive"></span>
<span class="status-text">Two-factor authentication disabled</span>
</div>"##
.to_string(),
)
}
</div>"## .to_string(), ) }
async fn get_active_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(
@ -292,23 +300,16 @@ async fn get_active_sessions(State(_state): State<Arc<AppState>>) -> Html<String
<span class="session-time">Active now</span>
</div>
</div>
</div>
<div class="sessions-empty">
<p class="text-muted">No other active sessions</p>
</div>"##
.to_string(),
)
}
</div> <div class="sessions-empty"> <p class="text-muted">No other active sessions</p> </div>"## .to_string(), ) }
async fn revoke_all_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(
r##"<div class="success-message">
<span class="success-icon"></span>
<span>All other sessions have been revoked</span>
</div>"##
.to_string(),
)
}
</div>"## .to_string(), ) }
async fn get_trusted_devices(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(
@ -321,10 +322,5 @@ async fn get_trusted_devices(State(_state): State<Arc<AppState>>) -> Html<String
</div>
</div>
<span class="device-badge trusted">Trusted</span>
</div>
<div class="devices-empty">
<p class="text-muted">No other trusted devices</p>
</div>"##
.to_string(),
)
}
</div> <div class="devices-empty"> <p class="text-muted">No other trusted devices</p> </div>"## .to_string(), ) }