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, # Available apps: chat, mail, calendar, drive, tasks, docs, paper, sheet, slides,
# meet, research, sources, analytics, admin, monitoring, settings # meet, research, sources, analytics, admin, monitoring, settings
# Only listed apps will be visible in the UI and have their APIs enabled. # 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 # Default theme
# Available themes: dark, light, blue, purple, green, orange, sentient, cyberpunk, # Available themes: dark, light, blue, purple, green, orange, sentient, cyberpunk,

View file

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

View file

@ -36,6 +36,8 @@ git clone https://github.com/GeneralBots/botserver
cd botserver cd botserver
cargo run 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. 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::models::UserSession;
use crate::shared::state::AppState; use crate::shared::state::AppState;
use log::{error, trace}; 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) { pub fn register_import_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
let state_clone = Arc::clone(&state); let state_clone = Arc::clone(&state);
engine engine
.register_custom_syntax(["IMPORT", "$expr$"], false, move |context, inputs| { .register_custom_syntax(["IMPORT", "$expr$"], false, move |context, inputs| {
let file_path = context.eval_expression_tree(&inputs[0])?.to_string(); let file_path = context.eval_expression_tree(&inputs[0])?.to_string();
@ -205,7 +174,16 @@ fn execute_import(
match extension.as_str() { match extension.as_str() {
"csv" => import_csv(&full_path), "csv" => import_csv(&full_path),
"json" => import_json(&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), "tsv" => import_tsv(&full_path),
_ => Err(format!("Unsupported file format: .{}", extension).into()), _ => Err(format!("Unsupported file format: .{}", extension).into()),
} }
@ -227,7 +205,16 @@ fn execute_export(
match extension.as_str() { match extension.as_str() {
"csv" => export_csv(&full_path, data), "csv" => export_csv(&full_path, data),
"json" => export_json(&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), "tsv" => export_tsv(&full_path, data),
_ => Err(format!("Unsupported export format: .{}", extension).into()), _ => 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) Ok(result)
} }
#[cfg(feature = "sheet")]
fn import_excel(file_path: &str) -> Result<Dynamic, Box<dyn std::error::Error + Send + Sync>> { fn import_excel(file_path: &str) -> Result<Dynamic, Box<dyn std::error::Error + Send + Sync>> {
use calamine::{open_workbook, Reader, Xlsx}; use calamine::{open_workbook, Reader, Xlsx};
@ -474,6 +462,7 @@ fn export_json(
Ok(file_path.to_string()) Ok(file_path.to_string())
} }
#[cfg(feature = "sheet")]
fn export_excel( fn export_excel(
file_path: &str, file_path: &str,
data: Dynamic, data: Dynamic,
@ -534,7 +523,7 @@ fn parse_csv_line(line: &str) -> Vec<String> {
fn escape_csv_value(value: &str) -> String { fn escape_csv_value(value: &str) -> String {
if value.contains(',') || value.contains('"') || value.contains('\n') { if value.contains(',') || value.contains('"') || value.contains('\n') {
format!("\"{}\"", value.replace('"', "\"\"")) format!("{}", value.replace('"', ""))
} else { } else {
value.to_string() value.to_string()
} }

View file

@ -4,6 +4,7 @@
//! Supports multiple notification channels: email, webhook, in-app, SMS. //! Supports multiple notification channels: email, webhook, in-app, SMS.
use crate::billing::UsageMetric; use crate::billing::UsageMetric;
use crate::core::shared::state::BillingAlertNotification;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@ -57,10 +58,11 @@ impl AlertThresholds {
None None
} }
} }
} }
/// Alert severity levels /// Alert severity levels
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum AlertSeverity { pub enum AlertSeverity {
Warning, Warning,
@ -92,6 +94,7 @@ impl AlertSeverity {
Self::Exceeded => 3, Self::Exceeded => 3,
} }
} }
} }
impl std::fmt::Display for AlertSeverity { impl std::fmt::Display for AlertSeverity {
@ -133,13 +136,14 @@ impl UsageAlert {
percentage: f64, percentage: f64,
threshold: f64, threshold: f64,
) -> Self { ) -> Self {
let severity_clone = severity.clone();
let message = Self::generate_message(metric, severity, percentage, current_usage, limit); let message = Self::generate_message(metric, severity, percentage, current_usage, limit);
Self { Self {
id: Uuid::new_v4(), id: Uuid::new_v4(),
organization_id, organization_id,
metric, metric,
severity, severity: severity_clone,
current_usage, current_usage,
limit, limit,
percentage, percentage,
@ -151,6 +155,7 @@ impl UsageAlert {
notification_sent: false, notification_sent: false,
notification_channels: Vec::new(), notification_channels: Vec::new(),
} }
} }
fn generate_message( fn generate_message(
@ -161,7 +166,7 @@ impl UsageAlert {
limit: u64, limit: u64,
) -> String { ) -> String {
let metric_name = metric.display_name(); let metric_name = metric.display_name();
let severity_text = match severity { let severity_ = match severity {
AlertSeverity::Warning => "approaching limit", AlertSeverity::Warning => "approaching limit",
AlertSeverity::Critical => "near limit", AlertSeverity::Critical => "near limit",
AlertSeverity::Exceeded => "exceeded limit", AlertSeverity::Exceeded => "exceeded limit",
@ -171,11 +176,12 @@ impl UsageAlert {
"{} {} usage is {} ({:.1}% - {}/{})", "{} {} usage is {} ({:.1}% - {}/{})",
severity.emoji(), severity.emoji(),
metric_name, metric_name,
severity_text, severity_,
percentage, percentage,
Self::format_value(metric, current), Self::format_value(metric, current),
Self::format_value(metric, limit) Self::format_value(metric, limit)
) )
} }
fn format_value(metric: UsageMetric, value: u64) -> String { fn format_value(metric: UsageMetric, value: u64) -> String {
@ -198,10 +204,11 @@ impl UsageAlert {
self.notification_sent = true; self.notification_sent = true;
self.notification_channels = channels; self.notification_channels = channels;
} }
} }
/// Notification delivery 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")] #[serde(rename_all = "snake_case")]
pub enum NotificationChannel { pub enum NotificationChannel {
Email, Email,
@ -318,7 +325,7 @@ impl AlertManager {
}; };
// Check cooldown // 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; return None;
} }
@ -340,6 +347,7 @@ impl AlertManager {
self.send_notifications(org_id, &alert).await; self.send_notifications(org_id, &alert).await;
Some(alert) Some(alert)
} }
/// Check multiple metrics at once /// Check multiple metrics at once
@ -357,6 +365,7 @@ impl AlertManager {
} }
alerts alerts
} }
/// Get active alerts for an organization /// Get active alerts for an organization
@ -379,6 +388,7 @@ impl AlertManager {
} }
alerts alerts
} }
/// Acknowledge an alert /// Acknowledge an alert
@ -399,6 +409,7 @@ impl AlertManager {
alert.acknowledge(user_id); alert.acknowledge(user_id);
Ok(()) Ok(())
} }
/// Dismiss an alert /// Dismiss an alert
@ -421,6 +432,7 @@ impl AlertManager {
self.add_to_history(org_id, alert.clone()).await; self.add_to_history(org_id, alert.clone()).await;
Ok(alert) Ok(alert)
} }
/// Clear all alerts for an organization /// Clear all alerts for an organization
@ -453,6 +465,7 @@ impl AlertManager {
} }
counts counts
} }
// ======================================================================== // ========================================================================
@ -479,6 +492,7 @@ impl AlertManager {
&& alert.severity == severity && alert.severity == severity
&& alert.created_at > cooldown_threshold && alert.created_at > cooldown_threshold
}) })
} }
async fn store_alert(&self, org_id: Uuid, alert: UsageAlert) { async fn store_alert(&self, org_id: Uuid, alert: UsageAlert) {
@ -491,6 +505,7 @@ impl AlertManager {
}); });
org_alerts.push(alert); org_alerts.push(alert);
} }
async fn add_to_history(&self, org_id: Uuid, alert: UsageAlert) { 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 { if org_history.len() > self.max_history_per_org {
org_history.truncate(self.max_history_per_org); org_history.truncate(self.max_history_per_org);
} }
} }
async fn send_notifications(&self, org_id: Uuid, alert: &UsageAlert) { async fn send_notifications(&self, org_id: Uuid, alert: &UsageAlert) {
@ -513,7 +529,7 @@ impl AlertManager {
} }
// Check if this severity should be notified // Check if this severity should be notified
if !prefs.should_notify(alert.severity) { if !prefs.should_notify(alert.severity.clone()) {
return; return;
} }
@ -532,7 +548,9 @@ impl AlertManager {
} }
} }
} }
} }
} }
impl Default for AlertManager { impl Default for AlertManager {
@ -591,6 +609,7 @@ impl NotificationPreferences {
false false
} }
} }
} }
/// Quiet hours configuration /// Quiet hours configuration
@ -599,7 +618,7 @@ pub struct QuietHours {
pub start_hour: u8, pub start_hour: u8,
pub end_hour: u8, pub end_hour: u8,
pub timezone: String, pub timezone: String,
pub days: Vec<chrono::Weekday>, pub days: Vecchrono::Weekday
} }
impl QuietHours { impl QuietHours {
@ -614,7 +633,9 @@ impl QuietHours {
// Overnight quiet hours // Overnight quiet hours
hour >= self.start_hour || hour < self.end_hour hour >= self.start_hour || hour < self.end_hour
} }
} }
} }
/// Per-metric notification override /// Per-metric notification override
@ -702,7 +723,7 @@ impl AlertNotification {
Self { Self {
alert_id: alert.id, alert_id: alert.id,
organization_id: alert.organization_id, organization_id: alert.organization_id,
severity: alert.severity, severity: alert.severity.clone(),
title: format!( title: format!(
"{} Usage Alert: {}", "{} Usage Alert: {}",
alert.severity.emoji(), alert.severity.emoji(),
@ -725,12 +746,14 @@ impl AlertNotification {
// ============================================================================ // ============================================================================
/// Email notification handler /// Email notification handler
#[cfg(feature = "mail")]
pub struct EmailNotificationHandler { pub struct EmailNotificationHandler {
_smtp_host: String, _smtp_host: String,
_smtp_port: u16, _smtp_port: u16,
_from_address: String, _from_address: String,
} }
#[cfg(feature = "mail")]
impl EmailNotificationHandler { impl EmailNotificationHandler {
pub fn new(smtp_host: String, smtp_port: u16, from_address: String) -> Self { pub fn new(smtp_host: String, smtp_port: u16, from_address: String) -> Self {
Self { Self {
@ -741,6 +764,7 @@ impl EmailNotificationHandler {
} }
} }
#[cfg(feature = "mail")]
#[async_trait::async_trait] #[async_trait::async_trait]
impl NotificationHandler for EmailNotificationHandler { impl NotificationHandler for EmailNotificationHandler {
fn channel(&self) -> NotificationChannel { fn channel(&self) -> NotificationChannel {
@ -748,63 +772,15 @@ impl NotificationHandler for EmailNotificationHandler {
} }
async fn send(&self, notification: &AlertNotification) -> Result<(), NotificationError> { async fn send(&self, notification: &AlertNotification) -> Result<(), NotificationError> {
use lettre::{Message, SmtpTransport, Transport}; // Email functionality is only available when the mail feature is enabled
use lettre::transport::smtp::authentication::Credentials; // This stub implementation prevents compilation errors when mail feature is disabled
tracing::warn!(
tracing::info!( "Email notifications require the 'mail' feature to be enabled. Alert {} not sent.",
"Sending email notification for alert {} to {:?}", notification.alert_id
notification.alert_id,
notification.recipients
); );
// 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(()) Ok(())
} }
} }
/// Webhook notification handler /// Webhook notification handler
@ -834,7 +810,7 @@ impl NotificationHandler for WebhookNotificationHandler {
notification.alert_id 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 webhook_url = std::env::var("BILLING_WEBHOOK_URL").ok();
let url = match webhook_url { let url = match webhook_url {
@ -878,13 +854,15 @@ impl NotificationHandler for WebhookNotificationHandler {
tracing::debug!("Webhook notification sent successfully to {}", url); tracing::debug!("Webhook notification sent successfully to {}", url);
Ok(()) Ok(())
} }
} }
/// In-app notification handler /// In-app notification handler
pub struct InAppNotificationHandler { pub struct InAppNotificationHandler {
/// Broadcast channel for WebSocket notifications /// 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 { impl InAppNotificationHandler {
@ -894,12 +872,13 @@ impl InAppNotificationHandler {
/// Create with a broadcast channel for WebSocket notifications /// Create with a broadcast channel for WebSocket notifications
pub fn with_broadcast( pub fn with_broadcast(
broadcast: tokio::sync::broadcast::Sender<crate::core::shared::state::BillingAlertNotification>, broadcast: tokio::sync::broadcast::Sender<BillingAlertNotification>,
) -> Self { ) -> Self {
Self { Self {
broadcast: Some(broadcast), broadcast: Some(broadcast),
} }
} }
} }
impl Default for InAppNotificationHandler { impl Default for InAppNotificationHandler {
@ -967,199 +946,9 @@ impl NotificationHandler for InAppNotificationHandler {
); );
Ok(()) 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 { } else {
format!("{} bytes", bytes) format!("{} bytes", bytes)
} }
} }
/// Format number with thousands separators /// 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 // Alert Service Factory
// ============================================================================ // ============================================================================
@ -1505,6 +1076,7 @@ pub fn create_alert_manager(
} }
manager manager
} }
/// Create default notification handlers /// Create default notification handlers
@ -1515,10 +1087,4 @@ pub async fn register_default_handlers(manager: &AlertManager) {
manager manager
.register_handler(Arc::new(WebhookNotificationHandler::new())) .register_handler(Arc::new(WebhookNotificationHandler::new()))
.await; .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 chrono::{DateTime, Utc};
use diesel::prelude::*; use diesel::prelude::*;
use diesel::sql_types::{Nullable, Text, Timestamptz, Uuid as DieselUuid, Varchar}; use diesel::sql_types::{Nullable, Text, Timestamptz, Uuid as DieselUuid, Varchar};
#[cfg(feature = "mail")]
use lettre::{Message, SmtpTransport, Transport}; use lettre::{Message, SmtpTransport, Transport};
#[cfg(feature = "mail")]
use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::authentication::Credentials;
use log::{info, warn}; use log::warn;
#[cfg(feature = "mail")]
use log::info;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
@ -20,6 +24,7 @@ use uuid::Uuid;
// ============================================================================ // ============================================================================
/// Send invitation email via SMTP /// Send invitation email via SMTP
#[cfg(feature = "mail")]
async fn send_invitation_email( async fn send_invitation_email(
to_email: &str, to_email: &str,
role: &str, role: &str,
@ -77,6 +82,7 @@ The General Bots Team"#,
} }
/// Send invitation email by fetching details from database /// Send invitation email by fetching details from database
#[cfg(feature = "mail")]
async fn send_invitation_email_by_id(invitation_id: Uuid) -> Result<(), String> { 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_host = std::env::var("SMTP_HOST").unwrap_or_else(|_| "localhost".to_string());
let smtp_user = std::env::var("SMTP_USER").ok(); 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 log::{debug, info, trace, warn};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{LazyLock, Mutex, RwLock}; use std::sync::{LazyLock, Mutex, RwLock};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
#[cfg(feature = "monitoring")]
use sysinfo::{Pid, ProcessesToUpdate, System}; use sysinfo::{Pid, ProcessesToUpdate, System};
static THREAD_REGISTRY: LazyLock<RwLock<HashMap<String, ThreadInfo>>> = static THREAD_REGISTRY: LazyLock<RwLock<HashMap<String, ThreadInfo>>> =
@ -87,6 +81,7 @@ impl MemoryStats {
} }
} }
pub fn format_bytes(bytes: u64) -> String { pub fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024; const KB: u64 = 1024;
const MB: u64 = KB * 1024; const MB: u64 = KB * 1024;
@ -110,6 +105,7 @@ impl MemoryStats {
Self::format_bytes(self.virtual_bytes), Self::format_bytes(self.virtual_bytes),
); );
} }
} }
/// Get jemalloc memory statistics when the feature is enabled /// Get jemalloc memory statistics when the feature is enabled
@ -117,6 +113,7 @@ impl MemoryStats {
pub fn get_jemalloc_stats() -> Option<JemallocStats> { pub fn get_jemalloc_stats() -> Option<JemallocStats> {
use tikv_jemalloc_ctl::{epoch, stats}; use tikv_jemalloc_ctl::{epoch, stats};
// Advance the epoch to refresh statistics // Advance the epoch to refresh statistics
if epoch::advance().is_err() { if epoch::advance().is_err() {
return None; return None;
@ -135,6 +132,7 @@ pub fn get_jemalloc_stats() -> Option<JemallocStats> {
mapped, mapped,
retained, retained,
}) })
} }
#[cfg(not(feature = "jemalloc"))] #[cfg(not(feature = "jemalloc"))]
@ -169,6 +167,7 @@ impl JemallocStats {
); );
} }
/// Calculate fragmentation ratio (1.0 = no fragmentation) /// Calculate fragmentation ratio (1.0 = no fragmentation)
pub fn fragmentation_ratio(&self) -> f64 { pub fn fragmentation_ratio(&self) -> f64 {
if self.allocated > 0 { if self.allocated > 0 {
@ -177,6 +176,7 @@ impl JemallocStats {
1.0 1.0
} }
} }
} }
/// Log jemalloc stats if available /// Log jemalloc stats if available
@ -210,6 +210,7 @@ impl MemoryCheckpoint {
} }
} }
pub fn compare_and_log(&self) { pub fn compare_and_log(&self) {
let current = MemoryStats::current(); let current = MemoryStats::current();
let diff = current.rss_bytes as i64 - self.stats.rss_bytes as i64; let diff = current.rss_bytes as i64 - self.stats.rss_bytes as i64;
@ -230,6 +231,7 @@ impl MemoryCheckpoint {
debug!("[CHECKPOINT] {} unchanged", self.name); debug!("[CHECKPOINT] {} unchanged", self.name);
} }
} }
} }
pub struct ComponentMemoryTracker { pub struct ComponentMemoryTracker {
@ -245,6 +247,7 @@ impl ComponentMemoryTracker {
} }
} }
pub fn record(&self, component: &str) { pub fn record(&self, component: &str) {
let stats = MemoryStats::current(); let stats = MemoryStats::current();
if let Ok(mut components) = self.components.lock() { if let Ok(mut components) = self.components.lock() {
@ -295,6 +298,7 @@ impl ComponentMemoryTracker {
} }
} }
} }
} }
pub fn record_component(component: &str) { pub fn record_component(component: &str) {
@ -322,6 +326,7 @@ impl LeakDetector {
} }
} }
pub fn reset_baseline(&self) { pub fn reset_baseline(&self) {
let current = MemoryStats::current(); let current = MemoryStats::current();
if let Ok(mut baseline) = self.baseline.lock() { if let Ok(mut baseline) = self.baseline.lock() {
@ -378,11 +383,13 @@ impl LeakDetector {
None None
} }
} }
pub fn start_memory_monitor(interval_secs: u64, warn_threshold_mb: u64) { pub fn start_memory_monitor(interval_secs: u64, warn_threshold_mb: u64) {
let detector = LeakDetector::new(warn_threshold_mb, 5); let detector = LeakDetector::new(warn_threshold_mb, 5);
tokio::spawn(async move { tokio::spawn(async move {
register_thread("memory-monitor", "monitoring"); 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)> { pub fn get_process_memory() -> Option<(u64, u64)> {
let pid = Pid::from_u32(std::process::id()); let pid = Pid::from_u32(std::process::id());
let mut sys = System::new(); let mut sys = System::new();
sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true); sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true);
sys.process(pid).map(|p| (p.memory(), p.virtual_memory())) 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() { pub fn log_process_memory() {
@ -476,6 +493,7 @@ pub fn log_process_memory() {
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_memory_stats() { fn test_memory_stats() {
let stats = MemoryStats::current(); let stats = MemoryStats::current();
@ -503,4 +521,5 @@ mod tests {
log_thread_stats(); log_thread_stats();
unregister_thread("test-thread"); 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"))] #[cfg(all(test, feature = "llm"))]
use crate::core::shared::test_utils::MockLLMProvider; use crate::core::shared::test_utils::MockLLMProvider;
#[cfg(feature = "directory")] #[cfg(feature = "directory")]
use crate::directory::AuthService; use crate::core::directory::AuthService;
#[cfg(feature = "llm")] #[cfg(feature = "llm")]
use crate::llm::LLMProvider; use crate::llm::LLMProvider;
use crate::shared::models::BotResponse; 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::analytics::MetricsCollector;
use crate::core::shared::state::{AppState, Extensions}; use crate::core::shared::state::{AppState, Extensions};
#[cfg(feature = "directory")] #[cfg(feature = "directory")]
use crate::directory::client::ZitadelConfig; use crate::core::directory::client::ZitadelConfig;
#[cfg(feature = "directory")] #[cfg(feature = "directory")]
use crate::directory::AuthService; use crate::core::directory::AuthService;
#[cfg(feature = "llm")] #[cfg(feature = "llm")]
use crate::llm::LLMProvider; use crate::llm::LLMProvider;
use crate::shared::models::BotResponse; use crate::shared::models::BotResponse;

View file

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

View file

@ -29,11 +29,15 @@ pub mod tickets;
pub mod attendant; pub mod attendant;
pub mod analytics; pub mod analytics;
pub mod designer; pub mod designer;
#[cfg(feature = "docs")]
pub mod docs; pub mod docs;
pub mod learn; pub mod learn;
#[cfg(feature = "paper")]
pub mod paper; pub mod paper;
pub mod research; pub mod research;
#[cfg(feature = "sheet")]
pub mod sheet; pub mod sheet;
#[cfg(feature = "slides")]
pub mod slides; pub mod slides;
pub mod social; pub mod social;
pub mod sources; pub mod sources;
@ -203,7 +207,7 @@ use crate::core::bot_database::BotDatabaseManager;
use crate::core::config::AppConfig; use crate::core::config::AppConfig;
#[cfg(feature = "directory")] #[cfg(feature = "directory")]
use crate::directory::auth_handler; use crate::core::directory::auth_handler;
use package_manager::InstallMode; use package_manager::InstallMode;
use session::{create_session, get_session_history, get_sessions, start_session}; 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::analytics::configure_analytics_routes());
api_router = api_router.merge(crate::core::i18n::configure_i18n_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()); api_router = api_router.merge(crate::docs::configure_docs_routes());
}
#[cfg(feature = "paper")]
{
api_router = api_router.merge(crate::paper::configure_paper_routes()); api_router = api_router.merge(crate::paper::configure_paper_routes());
}
#[cfg(feature = "sheet")]
{
api_router = api_router.merge(crate::sheet::configure_sheet_routes()); 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::slides::configure_slides_routes());
}
api_router = api_router.merge(crate::video::configure_video_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::video::ui::configure_video_ui_routes());
api_router = api_router.merge(crate::research::configure_research_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::canvas::ui::configure_canvas_ui_routes());
api_router = api_router.merge(crate::social::configure_social_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::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()); 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::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_ui::configure_crm_routes());
api_router = api_router.merge(crate::contacts::crm::configure_crm_api_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()); 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); 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_url: base_url.to_string(),
issuer: base_url.to_string(), issuer: base_url.to_string(),
client_id: client_id.to_string(), client_id: client_id.to_string(),
@ -1072,7 +1094,7 @@ use crate::core::config::ConfigManager;
} }
} else { } else {
warn!("Failed to parse directory_config.json, using defaults"); 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_url: "http://localhost:8300".to_string(),
issuer: "http://localhost:8300".to_string(), issuer: "http://localhost:8300".to_string(),
client_id: String::new(), client_id: String::new(),
@ -1085,7 +1107,7 @@ use crate::core::config::ConfigManager;
} }
} else { } else {
warn!("directory_config.json not found, using default Zitadel config"); 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_url: "http://localhost:8300".to_string(),
issuer: "http://localhost:8300".to_string(), issuer: "http://localhost:8300".to_string(),
client_id: String::new(), client_id: String::new(),
@ -1099,7 +1121,7 @@ use crate::core::config::ConfigManager;
}; };
#[cfg(feature = "directory")] #[cfg(feature = "directory")]
let auth_service = Arc::new(tokio::sync::Mutex::new( 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")] #[cfg(feature = "directory")]
@ -1110,22 +1132,22 @@ use crate::core::config::ConfigManager;
Ok(pat_token) => { Ok(pat_token) => {
let pat_token = pat_token.trim().to_string(); let pat_token = pat_token.trim().to_string();
info!("Using admin PAT token for bootstrap authentication"); 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)))? .map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client with PAT: {}", e)))?
} }
Err(e) => { Err(e) => {
warn!("Failed to read admin PAT token: {}, falling back to OAuth2", 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)))? .map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client: {}", e)))?
} }
} }
} else { } else {
info!("Admin PAT not found, using OAuth2 client credentials for bootstrap"); 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)))? .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(_)) => { Ok(Some(_)) => {
info!("Bootstrap completed - admin credentials displayed in console"); info!("Bootstrap completed - admin credentials displayed in console");
} }

View file

@ -1,7 +1,7 @@
use axum::{extract::State, response::Html, routing::get, Router}; use axum::{extract::State, response::Html, routing::get, Router};
use chrono::Local; use chrono::Local;
use std::sync::Arc; use std::sync::Arc;
#[cfg(feature = "monitoring")]
use sysinfo::{Disks, Networks, System}; use sysinfo::{Disks, Networks, System};
use crate::core::urls::ApiUrls; use crate::core::urls::ApiUrls;
@ -10,7 +10,6 @@ use crate::shared::state::AppState;
pub mod real_time; pub mod real_time;
pub mod tracing; pub mod tracing;
pub fn configure() -> Router<Arc<AppState>> { pub fn configure() -> Router<Arc<AppState>> {
Router::new() Router::new()
.route(ApiUrls::MONITORING_DASHBOARD, get(dashboard)) .route(ApiUrls::MONITORING_DASHBOARD, get(dashboard))
@ -35,8 +34,9 @@ pub fn configure() -> Router<Arc<AppState>> {
.route("/api/ui/monitoring/messages", get(messages_panel)) .route("/api/ui/monitoring/messages", get(messages_panel))
} }
async fn dashboard(State(state): State<Arc<AppState>>) -> Html<String> { 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(); let mut sys = System::new_all();
sys.refresh_all(); sys.refresh_all();
@ -52,6 +52,14 @@ async fn dashboard(State(state): State<Arc<AppState>>) -> Html<String> {
let uptime = System::uptime(); let uptime = System::uptime();
let uptime_str = format_uptime(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 let active_sessions = state
.session_manager .session_manager
.try_lock() .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-value">{uptime_str}</div>
<div class="metric-subtitle">System running time</div> <div class="metric-subtitle">System running time</div>
</div> </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 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> { async fn services(State(_state): State<Arc<AppState>>) -> Html<String> {
let services = vec![ let services = vec![
@ -151,11 +136,7 @@ async fn services(State(_state): State<Arc<AppState>>) -> Html<String> {
<td> <td>
<button class="btn-sm" hx-post="/api/monitoring/services/{name_lower}/restart" hx-swap="none">Restart</button> <button class="btn-sm" hx-post="/api/monitoring/services/{name_lower}/restart" hx-swap="none">Restart</button>
</td> </td>
</tr>"##, </tr>"##, name_lower = name.to_lowercase().replace(' ', "-"), )); }
name_lower = name.to_lowercase().replace(' ', "-"),
));
}
Html(format!( Html(format!(
r##"<div class="services-view"> r##"<div class="services-view">
<div class="section-header"> <div class="section-header">
@ -177,12 +158,11 @@ async fn services(State(_state): State<Arc<AppState>>) -> Html<String> {
{rows} {rows}
</tbody> </tbody>
</table> </table>
</div>"## </div>"## )) }
))
}
async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> { async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
#[cfg(feature = "monitoring")]
let (disk_rows, net_rows) = {
let mut sys = System::new_all(); let mut sys = System::new_all();
sys.refresh_all(); sys.refresh_all();
@ -210,20 +190,7 @@ async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
</div> </div>
<span class="usage-text">{percent:.1}%</span> <span class="usage-text">{percent:.1}%</span>
</td> </td>
</tr>"##, </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" }, )); }
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 networks = Networks::new_with_refreshed_list();
let mut net_rows = String::new(); let mut net_rows = String::new();
@ -233,11 +200,15 @@ async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
<td>{name}</td> <td>{name}</td>
<td>{rx:.2} MB</td> <td>{rx:.2} MB</td>
<td>{tx:.2} MB</td> <td>{tx:.2} MB</td>
</tr>"##, </tr>"##, rx = data.total_received() as f64 / 1_048_576.0, tx = data.total_transmitted() as f64 / 1_048_576.0, )); }
rx = data.total_received() as f64 / 1_048_576.0, (disk_rows, net_rows)
tx = data.total_transmitted() as f64 / 1_048_576.0, };
));
} #[cfg(not(feature = "monitoring"))]
let (disk_rows, net_rows) = (
String::new(),
String::new()
);
Html(format!( Html(format!(
r##"<div class="resources-view"> r##"<div class="resources-view">
@ -277,10 +248,7 @@ async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
</tbody> </tbody>
</table> </table>
</div> </div>
</div>"## </div>"## )) }
))
}
async fn logs(State(_state): State<Arc<AppState>>) -> Html<String> { async fn logs(State(_state): State<Arc<AppState>>) -> Html<String> {
Html( Html(
@ -308,11 +276,8 @@ async fn logs(State(_state): State<Arc<AppState>>) -> Html<String> {
<span class="log-message">Monitoring initialized</span> <span class="log-message">Monitoring initialized</span>
</div> </div>
</div> </div>
</div>"##
.to_string(),
)
}
</div>"## .to_string(), ) }
async fn llm_metrics(State(_state): State<Arc<AppState>>) -> Html<String> { async fn llm_metrics(State(_state): State<Arc<AppState>>) -> Html<String> {
Html( Html(
@ -362,11 +327,7 @@ async fn llm_metrics(State(_state): State<Arc<AppState>>) -> Html<String> {
</div> </div>
</div> </div>
</div> </div>
</div>"## </div>"## .to_string(), ) }
.to_string(),
)
}
async fn health(State(state): State<Arc<AppState>>) -> Html<String> { async fn health(State(state): State<Arc<AppState>>) -> Html<String> {
let db_ok = state.conn.get().is_ok(); 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}"> r##"<div class="health-status {status}">
<span class="status-icon"></span> <span class="status-icon"></span>
<span class="status-text">{status}</span> <span class="status-text">{status}</span>
</div>"##
))
}
</iv>"## )) }
fn format_uptime(seconds: u64) -> String { fn format_uptime(seconds: u64) -> String {
let days = seconds / 86400; let days = seconds / 86400;
@ -393,35 +352,30 @@ fn format_uptime(seconds: u64) -> String {
} else { } else {
format!("{}m", minutes) format!("{}m", minutes)
} }
}
}
fn check_postgres() -> bool { fn check_postgres() -> bool {
true true
} }
fn check_redis() -> bool { fn check_redis() -> bool {
true true
} }
fn check_minio() -> bool { fn check_minio() -> bool {
true true
} }
fn check_llm() -> bool { fn check_llm() -> bool {
true true
} }
async fn timestamp(State(_state): State<Arc<AppState>>) -> Html<String> { async fn timestamp(State(_state): State<Arc<AppState>>) -> Html<String> {
let now = Local::now(); let now = Local::now();
Html(format!("Last updated: {}", now.format("%H:%M:%S"))) Html(format!("Last updated: {}", now.format("%H:%M:%S")))
} }
async fn bots(State(state): State<Arc<AppState>>) -> Html<String> { async fn bots(State(state): State<Arc<AppState>>) -> Html<String> {
let active_sessions = state let active_sessions = state
.session_manager .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-name">Active Sessions</span>
<span class="bot-count">{active_sessions}</span> <span class="bot-count">{active_sessions}</span>
</div> </div>
</div>"## </div>"## )) }
))
}
async fn services_status(State(_state): State<Arc<AppState>>) -> Html<String> { async fn services_status(State(_state): State<Arc<AppState>>) -> Html<String> {
let services = vec![ let services = vec![
@ -462,10 +413,12 @@ async fn services_status(State(_state): State<Arc<AppState>>) -> Html<String> {
} }
Html(status_updates) Html(status_updates)
} }
async fn resources_bars(State(_state): State<Arc<AppState>>) -> Html<String> { 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(); let mut sys = System::new_all();
sys.refresh_all(); sys.refresh_all();
@ -478,30 +431,24 @@ async fn resources_bars(State(_state): State<Arc<AppState>>) -> Html<String> {
0.0 0.0
}; };
(cpu_usage, memory_percent)
};
#[cfg(not(feature = "monitoring"))]
let (cpu_usage, memory_percent) = (0.0, 0.0);
Html(format!( Html(format!(
r##"<g> r##"<g>
<text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">CPU</text> <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="100" height="10" rx="2" fill="#1e293b"/>
<rect x="40" y="-8" width="{cpu_width}" height="10" rx="2" fill="#3b82f6"/> <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> <text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{cpu_usage:.0}%</text>
</g> </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 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> { async fn activity_latest(State(_state): State<Arc<AppState>>) -> Html<String> {
Html("System monitoring active...".to_string()) Html("System monitoring active...".to_string())
} }
async fn metric_sessions(State(state): State<Arc<AppState>>) -> Html<String> { async fn metric_sessions(State(state): State<Arc<AppState>>) -> Html<String> {
let active_sessions = state let active_sessions = state
.session_manager .session_manager
@ -510,29 +457,25 @@ async fn metric_sessions(State(state): State<Arc<AppState>>) -> Html<String> {
.unwrap_or(0); .unwrap_or(0);
Html(format!("{}", active_sessions)) Html(format!("{}", active_sessions))
}
}
async fn metric_messages(State(_state): State<Arc<AppState>>) -> Html<String> { async fn metric_messages(State(_state): State<Arc<AppState>>) -> Html<String> {
Html("--".to_string()) Html("--".to_string())
} }
async fn metric_response_time(State(_state): State<Arc<AppState>>) -> Html<String> { async fn metric_response_time(State(_state): State<Arc<AppState>>) -> Html<String> {
Html("--".to_string()) Html("--".to_string())
} }
async fn trend_sessions(State(_state): State<Arc<AppState>>) -> Html<String> { async fn trend_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
Html("↑ 0%".to_string()) Html("↑ 0%".to_string())
} }
async fn rate_messages(State(_state): State<Arc<AppState>>) -> Html<String> { async fn rate_messages(State(_state): State<Arc<AppState>>) -> Html<String> {
Html("0/hr".to_string()) Html("0/hr".to_string())
} }
async fn sessions_panel(State(state): State<Arc<AppState>>) -> Html<String> { async fn sessions_panel(State(state): State<Arc<AppState>>) -> Html<String> {
let active_sessions = state let active_sessions = state
.session_manager .session_manager
@ -551,10 +494,7 @@ async fn sessions_panel(State(state): State<Arc<AppState>>) -> Html<String> {
<p>No active sessions</p> <p>No active sessions</p>
</div> </div>
</div> </div>
</div>"## </div>"## )) }
))
}
async fn messages_panel(State(_state): State<Arc<AppState>>) -> Html<String> { async fn messages_panel(State(_state): State<Arc<AppState>>) -> Html<String> {
Html( Html(
@ -567,7 +507,5 @@ async fn messages_panel(State(_state): State<Arc<AppState>>) -> Html<String> {
<p>No recent messages</p> <p>No recent messages</p>
</div> </div>
</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())]); &session_id[..std::cmp::min(20, session_id.len())]);
// Try to get user data from session cache first // 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 Ok(cache_guard) = crate::directory::auth_routes::SESSION_CACHE.try_read() {
if let Some(user_data) = cache_guard.get(session_id) { if let Some(user_data) = cache_guard.get(session_id) {
debug!("Found user in session cache: {}", user_data.email); 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">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">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 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> { async fn get_accounts_messaging(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(r##"<div class="accounts-list"> 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">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">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 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> { async fn get_accounts_email(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(r##"<div class="accounts-list"> 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">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">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 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( async fn save_smtp_account(
State(_state): State<Arc<AppState>>, 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> <span class="storage-size">500 MB</span>
</div> </div>
</div> </div>
</div>"## s
.to_string(), </div>"## .to_string(), ) }
)
}
async fn get_storage_connections(State(_state): State<Arc<AppState>>) -> Html<String> { async fn get_storage_connections(State(_state): State<Arc<AppState>>) -> Html<String> {
Html( Html(
@ -144,10 +142,8 @@ async fn get_storage_connections(State(_state): State<Arc<AppState>>) -> Html<St
<button class="btn-secondary" onclick="showAddConnectionModal()"> <button class="btn-secondary" onclick="showAddConnectionModal()">
+ Add Connection + Add Connection
</button> </button>
</div>"##
.to_string(), </div>"## .to_string(), ) }
)
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)] #[allow(dead_code)]
@ -177,11 +173,13 @@ async fn save_search_settings(
settings.enable_ai_suggestions settings.enable_ai_suggestions
); );
Json(SearchSettingsResponse { Json(SearchSettingsResponse {
success: true, success: true,
message: Some("Search settings saved successfully".to_string()), message: Some("Search settings saved successfully".to_string()),
error: None, error: None,
}) })
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -201,6 +199,7 @@ struct SmtpTestResponse {
error: Option<String>, error: Option<String>,
} }
#[cfg(feature = "mail")]
async fn test_smtp_connection( async fn test_smtp_connection(
State(_state): State<Arc<AppState>>, State(_state): State<Arc<AppState>>,
Json(config): Json<SmtpTestRequest>, Json(config): Json<SmtpTestRequest>,
@ -208,6 +207,8 @@ async fn test_smtp_connection(
use lettre::SmtpTransport; use lettre::SmtpTransport;
use lettre::transport::smtp::authentication::Credentials; use lettre::transport::smtp::authentication::Credentials;
log::info!("Testing SMTP connection to {}:{}", config.host, config.port); log::info!("Testing SMTP connection to {}:{}", config.host, config.port);
let mailer_result = if let (Some(user), Some(pass)) = (config.username, config.password) { 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)), 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> { 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"> r##"<div class="status-indicator">
<span class="status-dot inactive"></span> <span class="status-dot inactive"></span>
<span class="status-text">Two-factor authentication is not enabled</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> { async fn enable_2fa(State(_state): State<Arc<AppState>>) -> Html<String> {
Html( Html(
r##"<div class="status-indicator"> r##"<div class="status-indicator">
<span class="status-dot active"></span> <span class="status-dot active"></span>
<span class="status-text">Two-factor authentication enabled</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> { async fn disable_2fa(State(_state): State<Arc<AppState>>) -> Html<String> {
Html( Html(
r##"<div class="status-indicator"> r##"<div class="status-indicator">
<span class="status-dot inactive"></span> <span class="status-dot inactive"></span>
<span class="status-text">Two-factor authentication disabled</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> { async fn get_active_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
Html( Html(
@ -292,23 +300,16 @@ async fn get_active_sessions(State(_state): State<Arc<AppState>>) -> Html<String
<span class="session-time">Active now</span> <span class="session-time">Active now</span>
</div> </div>
</div> </div>
</div>
<div class="sessions-empty"> </div> <div class="sessions-empty"> <p class="text-muted">No other active sessions</p> </div>"## .to_string(), ) }
<p class="text-muted">No other active sessions</p>
</div>"##
.to_string(),
)
}
async fn revoke_all_sessions(State(_state): State<Arc<AppState>>) -> Html<String> { async fn revoke_all_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
Html( Html(
r##"<div class="success-message"> r##"<div class="success-message">
<span class="success-icon"></span> <span class="success-icon"></span>
<span>All other sessions have been revoked</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> { async fn get_trusted_devices(State(_state): State<Arc<AppState>>) -> Html<String> {
Html( Html(
@ -321,10 +322,5 @@ async fn get_trusted_devices(State(_state): State<Arc<AppState>>) -> Html<String
</div> </div>
</div> </div>
<span class="device-badge trusted">Trusted</span> <span class="device-badge trusted">Trusted</span>
</div>
<div class="devices-empty"> </div> <div class="devices-empty"> <p class="text-muted">No other trusted devices</p> </div>"## .to_string(), ) }
<p class="text-muted">No other trusted devices</p>
</div>"##
.to_string(),
)
}