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.
@ -152,4 +154,4 @@ According to our dual licensing model, this program can be used either under the
**General Bots Code Name:** [Guaribas](https://en.wikipedia.org/wiki/Guaribas) **General Bots Code Name:** [Guaribas](https://en.wikipedia.org/wiki/Guaribas)
> "No one should have to do work that can be done by a machine." - Roberto Mangabeira Unger > "No one should have to do work that can be done by a machine." - Roberto Mangabeira Unger

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()
} }

File diff suppressed because it is too large Load diff

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,11 +24,12 @@ 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,
custom_message: Option<&str>, custom_message: Option<&str>,
invitation_id: Uuid, invitation_id: Uuid,
) -> Result<(), String> { ) -> 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();
@ -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,506 +1,525 @@
//! 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>>> =
LazyLock::new(|| RwLock::new(HashMap::new())); LazyLock::new(|| RwLock::new(HashMap::new()));
static COMPONENT_TRACKER: LazyLock<ComponentMemoryTracker> = static COMPONENT_TRACKER: LazyLock<ComponentMemoryTracker> =
LazyLock::new(|| ComponentMemoryTracker::new(60)); LazyLock::new(|| ComponentMemoryTracker::new(60));
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ThreadInfo { pub struct ThreadInfo {
pub name: String, pub name: String,
pub started_at: Instant, pub started_at: Instant,
pub last_activity: Instant, pub last_activity: Instant,
pub activity_count: u64, pub activity_count: u64,
pub component: String, pub component: String,
} }
pub fn register_thread(name: &str, component: &str) { pub fn register_thread(name: &str, component: &str) {
let info = ThreadInfo { let info = ThreadInfo {
name: name.to_string(), name: name.to_string(),
started_at: Instant::now(), started_at: Instant::now(),
last_activity: Instant::now(), last_activity: Instant::now(),
activity_count: 0, activity_count: 0,
component: component.to_string(), component: component.to_string(),
}; };
if let Ok(mut registry) = THREAD_REGISTRY.write() { if let Ok(mut registry) = THREAD_REGISTRY.write() {
registry.insert(name.to_string(), info); registry.insert(name.to_string(), info);
} }
trace!("[THREAD] Registered: {} (component: {})", name, component); trace!("[THREAD] Registered: {} (component: {})", name, component);
} }
pub fn record_thread_activity(name: &str) { pub fn record_thread_activity(name: &str) {
if let Ok(mut registry) = THREAD_REGISTRY.write() { if let Ok(mut registry) = THREAD_REGISTRY.write() {
if let Some(info) = registry.get_mut(name) { if let Some(info) = registry.get_mut(name) {
info.last_activity = Instant::now(); info.last_activity = Instant::now();
info.activity_count += 1; info.activity_count += 1;
} }
} }
} }
pub fn unregister_thread(name: &str) { pub fn unregister_thread(name: &str) {
if let Ok(mut registry) = THREAD_REGISTRY.write() { if let Ok(mut registry) = THREAD_REGISTRY.write() {
registry.remove(name); registry.remove(name);
} }
info!("[THREAD] Unregistered: {}", name); info!("[THREAD] Unregistered: {}", name);
} }
pub fn log_thread_stats() { pub fn log_thread_stats() {
if let Ok(registry) = THREAD_REGISTRY.read() { if let Ok(registry) = THREAD_REGISTRY.read() {
info!("[THREADS] Active thread count: {}", registry.len()); info!("[THREADS] Active thread count: {}", registry.len());
for (name, info) in registry.iter() { for (name, info) in registry.iter() {
let uptime = info.started_at.elapsed().as_secs(); let uptime = info.started_at.elapsed().as_secs();
let idle = info.last_activity.elapsed().as_secs(); let idle = info.last_activity.elapsed().as_secs();
info!( info!(
"[THREAD] {} | component={} | uptime={}s | idle={}s | activities={}", "[THREAD] {} | component={} | uptime={}s | idle={}s | activities={}",
name, info.component, uptime, idle, info.activity_count name, info.component, uptime, idle, info.activity_count
); );
} }
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MemoryStats { pub struct MemoryStats {
pub rss_bytes: u64, pub rss_bytes: u64,
pub virtual_bytes: u64, pub virtual_bytes: u64,
pub timestamp: Instant, pub timestamp: Instant,
} }
impl MemoryStats { impl MemoryStats {
pub fn current() -> Self { pub fn current() -> Self {
let (rss, virt) = get_process_memory().unwrap_or((0, 0)); let (rss, virt) = get_process_memory().unwrap_or((0, 0));
Self { Self {
rss_bytes: rss, rss_bytes: rss,
virtual_bytes: virt, virtual_bytes: virt,
timestamp: Instant::now(), timestamp: Instant::now(),
} }
} }
pub fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if bytes >= GB { pub fn format_bytes(bytes: u64) -> String {
format!("{:.2} GB", bytes as f64 / GB as f64) const KB: u64 = 1024;
} else if bytes >= MB { const MB: u64 = KB * 1024;
format!("{:.2} MB", bytes as f64 / MB as f64) const GB: u64 = MB * 1024;
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
pub fn log(&self) { if bytes >= GB {
info!( format!("{:.2} GB", bytes as f64 / GB as f64)
"[MEMORY] RSS={}, Virtual={}", } else if bytes >= MB {
Self::format_bytes(self.rss_bytes), format!("{:.2} MB", bytes as f64 / MB as f64)
Self::format_bytes(self.virtual_bytes), } else if bytes >= KB {
); format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
} }
} }
pub fn log(&self) {
info!(
"[MEMORY] RSS={}, Virtual={}",
Self::format_bytes(self.rss_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
#[cfg(feature = "jemalloc")] #[cfg(feature = "jemalloc")]
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
if epoch::advance().is_err() {
return None;
}
let allocated = stats::allocated::read().ok()? as u64; // Advance the epoch to refresh statistics
let active = stats::active::read().ok()? as u64; if epoch::advance().is_err() {
let resident = stats::resident::read().ok()? as u64; return None;
let mapped = stats::mapped::read().ok()? as u64; }
let retained = stats::retained::read().ok()? as u64;
let allocated = stats::allocated::read().ok()? as u64;
let active = stats::active::read().ok()? as u64;
let resident = stats::resident::read().ok()? as u64;
let mapped = stats::mapped::read().ok()? as u64;
let retained = stats::retained::read().ok()? as u64;
Some(JemallocStats {
allocated,
active,
resident,
mapped,
retained,
})
Some(JemallocStats {
allocated,
active,
resident,
mapped,
retained,
})
} }
#[cfg(not(feature = "jemalloc"))] #[cfg(not(feature = "jemalloc"))]
pub fn get_jemalloc_stats() -> Option<JemallocStats> { pub fn get_jemalloc_stats() -> Option<JemallocStats> {
None None
} }
/// Jemalloc memory statistics /// Jemalloc memory statistics
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct JemallocStats { pub struct JemallocStats {
/// Total bytes allocated by the application /// Total bytes allocated by the application
pub allocated: u64, pub allocated: u64,
/// Total bytes in active pages allocated by the application /// Total bytes in active pages allocated by the application
pub active: u64, pub active: u64,
/// Total bytes in physically resident pages /// Total bytes in physically resident pages
pub resident: u64, pub resident: u64,
/// Total bytes in active extents mapped by the allocator /// Total bytes in active extents mapped by the allocator
pub mapped: u64, pub mapped: u64,
/// Total bytes retained (not returned to OS) /// Total bytes retained (not returned to OS)
pub retained: u64, pub retained: u64,
} }
impl JemallocStats { impl JemallocStats {
pub fn log(&self) { pub fn log(&self) {
info!( info!(
"[JEMALLOC] allocated={} active={} resident={} mapped={} retained={}", "[JEMALLOC] allocated={} active={} resident={} mapped={} retained={}",
MemoryStats::format_bytes(self.allocated), MemoryStats::format_bytes(self.allocated),
MemoryStats::format_bytes(self.active), MemoryStats::format_bytes(self.active),
MemoryStats::format_bytes(self.resident), MemoryStats::format_bytes(self.resident),
MemoryStats::format_bytes(self.mapped), MemoryStats::format_bytes(self.mapped),
MemoryStats::format_bytes(self.retained), MemoryStats::format_bytes(self.retained),
); );
} }
/// Calculate fragmentation ratio (1.0 = no fragmentation)
pub fn fragmentation_ratio(&self) -> f64 { /// Calculate fragmentation ratio (1.0 = no fragmentation)
if self.allocated > 0 { pub fn fragmentation_ratio(&self) -> f64 {
self.active as f64 / self.allocated as f64 if self.allocated > 0 {
} else { self.active as f64 / self.allocated as f64
1.0 } else {
} 1.0
} }
} }
}
/// Log jemalloc stats if available /// Log jemalloc stats if available
pub fn log_jemalloc_stats() { pub fn log_jemalloc_stats() {
if let Some(stats) = get_jemalloc_stats() { if let Some(stats) = get_jemalloc_stats() {
stats.log(); stats.log();
let frag = stats.fragmentation_ratio(); let frag = stats.fragmentation_ratio();
if frag > 1.5 { if frag > 1.5 {
warn!("[JEMALLOC] High fragmentation detected: {:.2}x", frag); warn!("[JEMALLOC] High fragmentation detected: {:.2}x", frag);
} }
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct MemoryCheckpoint { pub struct MemoryCheckpoint {
pub name: String, pub name: String,
pub stats: MemoryStats, pub stats: MemoryStats,
} }
impl MemoryCheckpoint { impl MemoryCheckpoint {
pub fn new(name: &str) -> Self { pub fn new(name: &str) -> Self {
let stats = MemoryStats::current(); let stats = MemoryStats::current();
info!( info!(
"[CHECKPOINT] {} started at RSS={}", "[CHECKPOINT] {} started at RSS={}",
name, name,
MemoryStats::format_bytes(stats.rss_bytes) MemoryStats::format_bytes(stats.rss_bytes)
);
Self {
name: name.to_string(),
stats,
}
}
pub fn compare_and_log(&self) {
let current = MemoryStats::current();
let diff = current.rss_bytes as i64 - self.stats.rss_bytes as i64;
if diff > 0 {
warn!(
"[CHECKPOINT] {} INCREASED by {}",
self.name,
MemoryStats::format_bytes(diff as u64),
); );
Self { } else if diff < 0 {
name: name.to_string(), info!(
stats, "[CHECKPOINT] {} decreased by {}",
} self.name,
MemoryStats::format_bytes((-diff) as u64),
);
} else {
debug!("[CHECKPOINT] {} unchanged", self.name);
} }
}
pub fn compare_and_log(&self) {
let current = MemoryStats::current();
let diff = current.rss_bytes as i64 - self.stats.rss_bytes as i64;
if diff > 0 {
warn!(
"[CHECKPOINT] {} INCREASED by {}",
self.name,
MemoryStats::format_bytes(diff as u64),
);
} else if diff < 0 {
info!(
"[CHECKPOINT] {} decreased by {}",
self.name,
MemoryStats::format_bytes((-diff) as u64),
);
} else {
debug!("[CHECKPOINT] {} unchanged", self.name);
}
}
} }
pub struct ComponentMemoryTracker { pub struct ComponentMemoryTracker {
components: Mutex<HashMap<String, Vec<MemoryStats>>>, components: Mutex<HashMap<String, Vec<MemoryStats>>>,
max_history: usize, max_history: usize,
} }
impl ComponentMemoryTracker { impl ComponentMemoryTracker {
pub fn new(max_history: usize) -> Self { pub fn new(max_history: usize) -> Self {
Self { Self {
components: Mutex::new(HashMap::new()), components: Mutex::new(HashMap::new()),
max_history, max_history,
}
}
pub fn record(&self, component: &str) {
let stats = MemoryStats::current();
if let Ok(mut components) = self.components.lock() {
let history = components.entry(component.to_string()).or_default();
history.push(stats);
if history.len() > self.max_history {
history.remove(0);
} }
} }
}
pub fn record(&self, component: &str) { pub fn get_growth_rate(&self, component: &str) -> Option<f64> {
let stats = MemoryStats::current(); if let Ok(components) = self.components.lock() {
if let Ok(mut components) = self.components.lock() { if let Some(history) = components.get(component) {
let history = components.entry(component.to_string()).or_default(); if history.len() >= 2 {
history.push(stats); let first = &history[0];
let last = &history[history.len() - 1];
if history.len() > self.max_history { let duration = last.timestamp.duration_since(first.timestamp).as_secs_f64();
history.remove(0); if duration > 0.0 {
} let byte_diff = last.rss_bytes as f64 - first.rss_bytes as f64;
} return Some(byte_diff / duration);
}
pub fn get_growth_rate(&self, component: &str) -> Option<f64> {
if let Ok(components) = self.components.lock() {
if let Some(history) = components.get(component) {
if history.len() >= 2 {
let first = &history[0];
let last = &history[history.len() - 1];
let duration = last.timestamp.duration_since(first.timestamp).as_secs_f64();
if duration > 0.0 {
let byte_diff = last.rss_bytes as f64 - first.rss_bytes as f64;
return Some(byte_diff / duration);
}
}
}
}
None
}
pub fn log_all(&self) {
if let Ok(components) = self.components.lock() {
for (name, history) in components.iter() {
if let Some(last) = history.last() {
let growth = self.get_growth_rate(name);
let growth_str = growth
.map(|g| {
let sign = if g >= 0.0 { "+" } else { "-" };
format!("{}{}/s", sign, MemoryStats::format_bytes(g.abs() as u64))
})
.unwrap_or_else(|| "N/A".to_string());
info!(
"[COMPONENT] {} | RSS={} | Growth={}",
name,
MemoryStats::format_bytes(last.rss_bytes),
growth_str
);
} }
} }
} }
} }
None
}
pub fn log_all(&self) {
if let Ok(components) = self.components.lock() {
for (name, history) in components.iter() {
if let Some(last) = history.last() {
let growth = self.get_growth_rate(name);
let growth_str = growth
.map(|g| {
let sign = if g >= 0.0 { "+" } else { "-" };
format!("{}{}/s", sign, MemoryStats::format_bytes(g.abs() as u64))
})
.unwrap_or_else(|| "N/A".to_string());
info!(
"[COMPONENT] {} | RSS={} | Growth={}",
name,
MemoryStats::format_bytes(last.rss_bytes),
growth_str
);
}
}
}
}
} }
pub fn record_component(component: &str) { pub fn record_component(component: &str) {
COMPONENT_TRACKER.record(component); COMPONENT_TRACKER.record(component);
} }
pub fn log_component_stats() { pub fn log_component_stats() {
COMPONENT_TRACKER.log_all(); COMPONENT_TRACKER.log_all();
} }
pub struct LeakDetector { pub struct LeakDetector {
baseline: Mutex<u64>, baseline: Mutex<u64>,
growth_threshold_bytes: u64, growth_threshold_bytes: u64,
consecutive_growth_count: Mutex<usize>, consecutive_growth_count: Mutex<usize>,
max_consecutive_growth: usize, max_consecutive_growth: usize,
} }
impl LeakDetector { impl LeakDetector {
pub fn new(growth_threshold_mb: u64, max_consecutive_growth: usize) -> Self { pub fn new(growth_threshold_mb: u64, max_consecutive_growth: usize) -> Self {
Self { Self {
baseline: Mutex::new(0), baseline: Mutex::new(0),
growth_threshold_bytes: growth_threshold_mb * 1024 * 1024, growth_threshold_bytes: growth_threshold_mb * 1024 * 1024,
consecutive_growth_count: Mutex::new(0), consecutive_growth_count: Mutex::new(0),
max_consecutive_growth, max_consecutive_growth,
} }
} }
pub fn reset_baseline(&self) {
let current = MemoryStats::current(); pub fn reset_baseline(&self) {
let current = MemoryStats::current();
if let Ok(mut baseline) = self.baseline.lock() {
*baseline = current.rss_bytes;
}
if let Ok(mut count) = self.consecutive_growth_count.lock() {
*count = 0;
}
}
pub fn check(&self) -> Option<String> {
let current = MemoryStats::current();
let baseline_val = match self.baseline.lock() {
Ok(b) => *b,
Err(_) => return None,
};
if baseline_val == 0 {
if let Ok(mut baseline) = self.baseline.lock() { if let Ok(mut baseline) = self.baseline.lock() {
*baseline = current.rss_bytes; *baseline = current.rss_bytes;
} }
if let Ok(mut count) = self.consecutive_growth_count.lock() { return None;
*count = 0;
}
} }
pub fn check(&self) -> Option<String> { let growth = current.rss_bytes.saturating_sub(baseline_val);
let current = MemoryStats::current();
let baseline_val = match self.baseline.lock() { if growth > self.growth_threshold_bytes {
Ok(b) => *b, let count = match self.consecutive_growth_count.lock() {
Ok(mut c) => {
*c += 1;
*c
}
Err(_) => return None, Err(_) => return None,
}; };
if baseline_val == 0 { if count >= self.max_consecutive_growth {
if let Ok(mut baseline) = self.baseline.lock() { return Some(format!(
*baseline = current.rss_bytes; "POTENTIAL MEMORY LEAK: grew by {} over {} checks. RSS={}, Baseline={}",
} MemoryStats::format_bytes(growth),
return None; count,
MemoryStats::format_bytes(current.rss_bytes),
MemoryStats::format_bytes(baseline_val),
));
} }
} else {
let growth = current.rss_bytes.saturating_sub(baseline_val); if let Ok(mut count) = self.consecutive_growth_count.lock() {
*count = 0;
if growth > self.growth_threshold_bytes { }
let count = match self.consecutive_growth_count.lock() { if let Ok(mut baseline) = self.baseline.lock() {
Ok(mut c) => { *baseline = current.rss_bytes;
*c += 1;
*c
}
Err(_) => return None,
};
if count >= self.max_consecutive_growth {
return Some(format!(
"POTENTIAL MEMORY LEAK: grew by {} over {} checks. RSS={}, Baseline={}",
MemoryStats::format_bytes(growth),
count,
MemoryStats::format_bytes(current.rss_bytes),
MemoryStats::format_bytes(baseline_val),
));
}
} else {
if let Ok(mut count) = self.consecutive_growth_count.lock() {
*count = 0;
}
if let Ok(mut baseline) = self.baseline.lock() {
*baseline = current.rss_bytes;
}
} }
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 {
register_thread("memory-monitor", "monitoring");
info!( tokio::spawn(async move {
"[MONITOR] Started (interval={}s, threshold={}MB)", register_thread("memory-monitor", "monitoring");
interval_secs, warn_threshold_mb
info!(
"[MONITOR] Started (interval={}s, threshold={}MB)",
interval_secs, warn_threshold_mb
);
let mut prev_rss: u64 = 0;
let mut tick_count: u64 = 0;
// First 2 minutes: check every 10 seconds for aggressive tracking
// After that: use normal interval
let startup_interval = Duration::from_secs(10);
let normal_interval = Duration::from_secs(interval_secs);
let startup_ticks = 12; // 2 minutes of 10-second intervals
let mut interval = tokio::time::interval(startup_interval);
loop {
interval.tick().await;
tick_count += 1;
record_thread_activity("memory-monitor");
let stats = MemoryStats::current();
let rss_diff = if prev_rss > 0 {
stats.rss_bytes as i64 - prev_rss as i64
} else {
0
};
let diff_str = if rss_diff > 0 {
format!("+{}", MemoryStats::format_bytes(rss_diff as u64))
} else if rss_diff < 0 {
format!("-{}", MemoryStats::format_bytes((-rss_diff) as u64))
} else {
"±0".to_string()
};
trace!(
"[MONITOR] tick={} RSS={} ({}) Virtual={}",
tick_count,
MemoryStats::format_bytes(stats.rss_bytes),
diff_str,
MemoryStats::format_bytes(stats.virtual_bytes),
); );
let mut prev_rss: u64 = 0; // Log jemalloc stats every 5 ticks if available
let mut tick_count: u64 = 0; if tick_count % 5 == 0 {
log_jemalloc_stats();
// First 2 minutes: check every 10 seconds for aggressive tracking
// After that: use normal interval
let startup_interval = Duration::from_secs(10);
let normal_interval = Duration::from_secs(interval_secs);
let startup_ticks = 12; // 2 minutes of 10-second intervals
let mut interval = tokio::time::interval(startup_interval);
loop {
interval.tick().await;
tick_count += 1;
record_thread_activity("memory-monitor");
let stats = MemoryStats::current();
let rss_diff = if prev_rss > 0 {
stats.rss_bytes as i64 - prev_rss as i64
} else {
0
};
let diff_str = if rss_diff > 0 {
format!("+{}", MemoryStats::format_bytes(rss_diff as u64))
} else if rss_diff < 0 {
format!("-{}", MemoryStats::format_bytes((-rss_diff) as u64))
} else {
"±0".to_string()
};
trace!(
"[MONITOR] tick={} RSS={} ({}) Virtual={}",
tick_count,
MemoryStats::format_bytes(stats.rss_bytes),
diff_str,
MemoryStats::format_bytes(stats.virtual_bytes),
);
// Log jemalloc stats every 5 ticks if available
if tick_count % 5 == 0 {
log_jemalloc_stats();
}
prev_rss = stats.rss_bytes;
record_component("global");
if let Some(warning) = detector.check() {
warn!("[MONITOR] {}", warning);
stats.log();
log_component_stats();
log_thread_stats();
}
// Switch to normal interval after startup period
if tick_count == startup_ticks {
trace!("[MONITOR] Switching to normal interval ({}s)", interval_secs);
interval = tokio::time::interval(normal_interval);
}
} }
});
prev_rss = stats.rss_bytes;
record_component("global");
if let Some(warning) = detector.check() {
warn!("[MONITOR] {}", warning);
stats.log();
log_component_stats();
log_thread_stats();
}
// Switch to normal interval after startup period
if tick_count == startup_ticks {
trace!("[MONITOR] Switching to normal interval ({}s)", interval_secs);
interval = tokio::time::interval(normal_interval);
}
}
});
} }
#[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() {
if let Some((rss, virt)) = get_process_memory() { if let Some((rss, virt)) = get_process_memory() {
trace!( trace!(
"[PROCESS] RSS={}, Virtual={}", "[PROCESS] RSS={}, Virtual={}",
MemoryStats::format_bytes(rss), MemoryStats::format_bytes(rss),
MemoryStats::format_bytes(virt) MemoryStats::format_bytes(virt)
); );
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn test_memory_stats() {
let stats = MemoryStats::current();
assert!(stats.rss_bytes > 0 || stats.virtual_bytes >= 0);
}
#[test] #[test]
fn test_format_bytes() { fn test_memory_stats() {
assert_eq!(MemoryStats::format_bytes(500), "500 B"); let stats = MemoryStats::current();
assert_eq!(MemoryStats::format_bytes(1024), "1.00 KB"); assert!(stats.rss_bytes > 0 || stats.virtual_bytes >= 0);
assert_eq!(MemoryStats::format_bytes(1024 * 1024), "1.00 MB"); }
assert_eq!(MemoryStats::format_bytes(1024 * 1024 * 1024), "1.00 GB");
} #[test]
fn test_format_bytes() {
#[test] assert_eq!(MemoryStats::format_bytes(500), "500 B");
fn test_checkpoint() { assert_eq!(MemoryStats::format_bytes(1024), "1.00 KB");
let checkpoint = MemoryCheckpoint::new("test"); assert_eq!(MemoryStats::format_bytes(1024 * 1024), "1.00 MB");
checkpoint.compare_and_log(); assert_eq!(MemoryStats::format_bytes(1024 * 1024 * 1024), "1.00 GB");
} }
#[test] #[test]
fn test_thread_registry() { fn test_checkpoint() {
register_thread("test-thread", "test-component"); let checkpoint = MemoryCheckpoint::new("test");
record_thread_activity("test-thread"); checkpoint.compare_and_log();
log_thread_stats(); }
unregister_thread("test-thread");
} #[test]
fn test_thread_registry() {
register_thread("test-thread", "test-component");
record_thread_activity("test-thread");
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"))] #[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;

File diff suppressed because it is too large Load diff

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,11 +452,23 @@ 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());
api_router = api_router.merge(crate::docs::configure_docs_routes()); #[cfg(feature = "docs")]
api_router = api_router.merge(crate::paper::configure_paper_routes()); {
api_router = api_router.merge(crate::sheet::configure_sheet_routes()); api_router = api_router.merge(crate::docs::configure_docs_routes());
api_router = api_router.merge(crate::slides::configure_slides_routes()); }
api_router = api_router.merge(crate::video::configure_video_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::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());
api_router = api_router.merge(crate::research::ui::configure_research_ui_routes()); api_router = api_router.merge(crate::research::ui::configure_research_ui_routes());
@ -484,12 +500,18 @@ async fn run_axum_server(
api_router = api_router.merge(crate::player::configure_player_routes()); api_router = api_router.merge(crate::player::configure_player_routes());
api_router = api_router.merge(crate::canvas::configure_canvas_routes()); api_router = api_router.merge(crate::canvas::configure_canvas_routes());
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::meet::ui::configure_meet_ui_routes()); {
api_router = api_router.merge(crate::contacts::crm_ui::configure_crm_routes()); 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::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());
api_router = api_router.merge(crate::billing::api::configure_billing_api_routes()); api_router = api_router.merge(crate::billing::api::configure_billing_api_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,35 +10,35 @@ 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))
.route(ApiUrls::MONITORING_SERVICES, get(services)) .route(ApiUrls::MONITORING_SERVICES, get(services))
.route(ApiUrls::MONITORING_RESOURCES, get(resources)) .route(ApiUrls::MONITORING_RESOURCES, get(resources))
.route(ApiUrls::MONITORING_LOGS, get(logs)) .route(ApiUrls::MONITORING_LOGS, get(logs))
.route(ApiUrls::MONITORING_LLM, get(llm_metrics)) .route(ApiUrls::MONITORING_LLM, get(llm_metrics))
.route(ApiUrls::MONITORING_HEALTH, get(health)) .route(ApiUrls::MONITORING_HEALTH, get(health))
// Additional endpoints expected by the frontend // Additional endpoints expected by the frontend
.route("/api/ui/monitoring/timestamp", get(timestamp)) .route("/api/ui/monitoring/timestamp", get(timestamp))
.route("/api/ui/monitoring/bots", get(bots)) .route("/api/ui/monitoring/bots", get(bots))
.route("/api/ui/monitoring/services/status", get(services_status)) .route("/api/ui/monitoring/services/status", get(services_status))
.route("/api/ui/monitoring/resources/bars", get(resources_bars)) .route("/api/ui/monitoring/resources/bars", get(resources_bars))
.route("/api/ui/monitoring/activity/latest", get(activity_latest)) .route("/api/ui/monitoring/activity/latest", get(activity_latest))
.route("/api/ui/monitoring/metric/sessions", get(metric_sessions)) .route("/api/ui/monitoring/metric/sessions", get(metric_sessions))
.route("/api/ui/monitoring/metric/messages", get(metric_messages)) .route("/api/ui/monitoring/metric/messages", get(metric_messages))
.route("/api/ui/monitoring/metric/response_time", get(metric_response_time)) .route("/api/ui/monitoring/metric/response_time", get(metric_response_time))
.route("/api/ui/monitoring/trend/sessions", get(trend_sessions)) .route("/api/ui/monitoring/trend/sessions", get(trend_sessions))
.route("/api/ui/monitoring/rate/messages", get(rate_messages)) .route("/api/ui/monitoring/rate/messages", get(rate_messages))
// Aliases for frontend compatibility // Aliases for frontend compatibility
.route("/api/ui/monitoring/sessions", get(sessions_panel)) .route("/api/ui/monitoring/sessions", get(sessions_panel))
.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> {
let mut sys = System::new_all(); #[cfg(feature = "monitoring")]
sys.refresh_all(); let (cpu_usage, total_memory, used_memory, memory_percent, uptime_str) = {
let mut sys = System::new_all();
sys.refresh_all();
let cpu_usage = sys.global_cpu_usage(); let cpu_usage = sys.global_cpu_usage();
let total_memory = sys.total_memory(); let total_memory = sys.total_memory();
@ -51,140 +51,120 @@ 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)
};
let active_sessions = state #[cfg(not(feature = "monitoring"))]
.session_manager let (cpu_usage, total_memory, used_memory, memory_percent, uptime_str) = (
.try_lock() 0.0, 0, 0, 0.0, "N/A".to_string()
.map(|sm| sm.active_count()) );
.unwrap_or(0);
Html(format!( let active_sessions = state
r##"<div class="dashboard-grid"> .session_manager
<div class="metric-card"> .try_lock()
<div class="metric-header"> .map(|sm| sm.active_count())
<span class="metric-title">CPU Usage</span> .unwrap_or(0);
<span class="metric-badge {cpu_status}">{cpu_usage:.1}%</span>
</div> Html(format!(
<div class="metric-value">{cpu_usage:.1}%</div> r##"<div class="dashboard-grid">
<div class="metric-bar"> <div class="metric-card">
<div class="metric-bar-fill" style="width: {cpu_usage}%"></div> <div class="metric-header">
</div> <span class="metric-title">CPU Usage</span>
<span class="metric-badge {cpu_status}">{cpu_usage:.1}%</span>
</div> </div>
<div class="metric-value">{cpu_usage:.1}%</div>
<div class="metric-card"> <div class="metric-bar">
<div class="metric-header"> <div class="metric-bar-fill" style="width: {cpu_usage}%"></div>
<span class="metric-title">Memory</span>
<span class="metric-badge {mem_status}">{memory_percent:.1}%</span>
</div>
<div class="metric-value">{used_gb:.1} GB / {total_gb:.1} GB</div>
<div class="metric-bar">
<div class="metric-bar-fill" style="width: {memory_percent}%"></div>
</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-title">Active Sessions</span>
</div>
<div class="metric-value">{active_sessions}</div>
<div class="metric-subtitle">Current conversations</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-title">Uptime</span>
</div>
<div class="metric-value">{uptime_str}</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"> <div class="metric-card">
<span class="refresh-dot"></span> Auto-refreshing <div class="metric-header">
</div>"##, <span class="metric-title">Memory</span>
cpu_status = if cpu_usage > 80.0 { <span class="metric-badge {mem_status}">{memory_percent:.1}%</span>
"danger" </div>
} else if cpu_usage > 60.0 { <div class="metric-value">{used_gb:.1} GB / {total_gb:.1} GB</div>
"warning" <div class="metric-bar">
} else { <div class="metric-bar-fill" style="width: {memory_percent}%"></div>
"success" </div>
}, </div>
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="metric-card">
<div class="metric-header">
<span class="metric-title">Active Sessions</span>
</div>
<div class="metric-value">{active_sessions}</div>
<div class="metric-subtitle">Current conversations</div>
</div>
<div class="metric-card">
<div class="metric-header">
<span class="metric-title">Uptime</span>
</div>
<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, )) }
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![
("PostgreSQL", check_postgres(), "Database"), ("PostgreSQL", check_postgres(), "Database"),
("Redis", check_redis(), "Cache"), ("Redis", check_redis(), "Cache"),
("MinIO", check_minio(), "Storage"), ("MinIO", check_minio(), "Storage"),
("LLM Server", check_llm(), "AI Backend"), ("LLM Server", check_llm(), "AI Backend"),
]; ];
let mut rows = String::new(); let mut rows = String::new();
for (name, status, desc) in services { for (name, status, desc) in services {
let (status_class, status_text) = if status { let (status_class, status_text) = if status {
("success", "Running") ("success", "Running")
} else { } else {
("danger", "Stopped") ("danger", "Stopped")
}; };
rows.push_str(&format!( rows.push_str(&format!(
r##"<tr> r##"<tr>
<td> <td>
<div class="service-name"> <div class="service-name">
<span class="status-dot {status_class}"></span> <span class="status-dot {status_class}"></span>
{name} {name}
</div>
</td>
<td>{desc}</td>
<td><span class="status-badge {status_class}">{status_text}</span></td>
<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(' ', "-"),
));
}
Html(format!(
r##"<div class="services-view">
<div class="section-header">
<h2>Services Status</h2>
<button class="btn-secondary" hx-get="/api/monitoring/services" hx-target="#monitoring-content" hx-swap="innerHTML">
Refresh
</button>
</div> </div>
<table class="data-table"> </td>
<thead> <td>{desc}</td>
<tr> <td><span class="status-badge {status_class}">{status_text}</span></td>
<th>Service</th> <td>
<th>Description</th> <button class="btn-sm" hx-post="/api/monitoring/services/{name_lower}/restart" hx-swap="none">Restart</button>
<th>Status</th> </td>
<th>Actions</th> </tr>"##, name_lower = name.to_lowercase().replace(' ', "-"), )); }
</tr> Html(format!(
</thead> r##"<div class="services-view">
<tbody> <div class="section-header">
{rows} <h2>Services Status</h2>
</tbody> <button class="btn-secondary" hx-get="/api/monitoring/services" hx-target="#monitoring-content" hx-swap="innerHTML">
</table> Refresh
</div>"## </button>
)) </div>
} <table class="data-table">
<thead>
<tr>
<th>Service</th>
<th>Description</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{rows}
</tbody>
</table>
</div>"## )) }
async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> { async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
let mut sys = System::new_all(); #[cfg(feature = "monitoring")]
sys.refresh_all(); let (disk_rows, net_rows) = {
let mut sys = System::new_all();
sys.refresh_all();
let disks = Disks::new_with_refreshed_list(); let disks = Disks::new_with_refreshed_list();
let mut disk_rows = String::new(); let mut disk_rows = String::new();
@ -201,273 +181,246 @@ async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
disk_rows.push_str(&format!( disk_rows.push_str(&format!(
r##"<tr> r##"<tr>
<td>{mount}</td> <td>{mount}</td>
<td>{used_gb:.1} GB</td> <td>{used_gb:.1} GB</td>
<td>{total_gb:.1} GB</td> <td>{total_gb:.1} GB</td>
<td> <td>
<div class="usage-bar"> <div class="usage-bar">
<div class="usage-fill {status}" style="width: {percent:.0}%"></div> <div class="usage-fill {status}" style="width: {percent:.0}%"></div>
</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();
for (name, data) in networks.list() { for (name, data) in networks.list() {
net_rows.push_str(&format!( net_rows.push_str(&format!(
r##"<tr> r##"<tr>
<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, };
));
}
Html(format!( #[cfg(not(feature = "monitoring"))]
r##"<div class="resources-view"> let (disk_rows, net_rows) = (
<div class="section-header"> String::new(),
<h2>System Resources</h2> String::new()
</div> );
<div class="resource-section"> Html(format!(
<h3>Disk Usage</h3> r##"<div class="resources-view">
<table class="data-table"> <div class="section-header">
<thead> <h2>System Resources</h2>
<tr> </div>
<th>Mount</th>
<th>Used</th>
<th>Total</th>
<th>Usage</th>
</tr>
</thead>
<tbody>
{disk_rows}
</tbody>
</table>
</div>
<div class="resource-section"> <div class="resource-section">
<h3>Network</h3> <h3>Disk Usage</h3>
<table class="data-table"> <table class="data-table">
<thead> <thead>
<tr> <tr>
<th>Interface</th> <th>Mount</th>
<th>Received</th> <th>Used</th>
<th>Transmitted</th> <th>Total</th>
</tr> <th>Usage</th>
</thead> </tr>
<tbody> </thead>
{net_rows} <tbody>
</tbody> {disk_rows}
</table> </tbody>
</div> </table>
</div>"## </div>
))
}
<div class="resource-section">
<h3>Network</h3>
<table class="data-table">
<thead>
<tr>
<th>Interface</th>
<th>Received</th>
<th>Transmitted</th>
</tr>
</thead>
<tbody>
{net_rows}
</tbody>
</table>
</div>
</div>"## )) }
async fn logs(State(_state): State<Arc<AppState>>) -> Html<String> { async fn logs(State(_state): State<Arc<AppState>>) -> Html<String> {
Html( Html(
r##"<div class="logs-view"> r##"<div class="logs-view">
<div class="section-header"> <div class="section-header">
<h2>System Logs</h2> <h2>System Logs</h2>
<div class="log-controls"> <div class="log-controls">
<select id="log-level" onchange="filterLogs(this.value)"> <select id="log-level" onchange="filterLogs(this.value)">
<option value="all">All Levels</option> <option value="all">All Levels</option>
<option value="error">Error</option> <option value="error">Error</option>
<option value="warn">Warning</option> <option value="warn">Warning</option>
<option value="info">Info</option> <option value="info">Info</option>
<option value="debug">Debug</option> <option value="debug">Debug</option>
</select> </select>
<button class="btn-secondary" onclick="clearLogs()">Clear</button> <button class="btn-secondary" onclick="clearLogs()">Clear</button>
</div> </div>
</div> </div>
<div class="log-container" id="log-container" <div class="log-container" id="log-container"
hx-get="/api/monitoring/logs/stream" hx-get="/api/monitoring/logs/stream"
hx-trigger="every 2s" hx-trigger="every 2s"
hx-swap="beforeend scroll:bottom"> hx-swap="beforeend scroll:bottom">
<div class="log-entry info"> <div class="log-entry info">
<span class="log-time">System ready</span> <span class="log-time">System ready</span>
<span class="log-level">INFO</span> <span class="log-level">INFO</span>
<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(
r##"<div class="llm-metrics-view"> r##"<div class="llm-metrics-view">
<div class="section-header"> <div class="section-header">
<h2>LLM Metrics</h2> <h2>LLM Metrics</h2>
</div> </div>
<div class="metrics-grid"> <div class="metrics-grid">
<div class="metric-card"> <div class="metric-card">
<div class="metric-title">Total Requests</div> <div class="metric-title">Total Requests</div>
<div class="metric-value" id="llm-total-requests" <div class="metric-value" id="llm-total-requests"
hx-get="/api/monitoring/llm/total" hx-get="/api/monitoring/llm/total"
hx-trigger="load, every 30s" hx-trigger="load, every 30s"
hx-swap="innerHTML"> hx-swap="innerHTML">
-- --
</div>
</div>
<div class="metric-card">
<div class="metric-title">Cache Hit Rate</div>
<div class="metric-value" id="llm-cache-rate"
hx-get="/api/monitoring/llm/cache-rate"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
--
</div>
</div>
<div class="metric-card">
<div class="metric-title">Avg Latency</div>
<div class="metric-value" id="llm-latency"
hx-get="/api/monitoring/llm/latency"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
--
</div>
</div>
<div class="metric-card">
<div class="metric-title">Total Tokens</div>
<div class="metric-value" id="llm-tokens"
hx-get="/api/monitoring/llm/tokens"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
--
</div>
</div> </div>
</div> </div>
</div>"##
.to_string(),
)
}
<div class="metric-card">
<div class="metric-title">Cache Hit Rate</div>
<div class="metric-value" id="llm-cache-rate"
hx-get="/api/monitoring/llm/cache-rate"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
--
</div>
</div>
<div class="metric-card">
<div class="metric-title">Avg Latency</div>
<div class="metric-value" id="llm-latency"
hx-get="/api/monitoring/llm/latency"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
--
</div>
</div>
<div class="metric-card">
<div class="metric-title">Total Tokens</div>
<div class="metric-value" id="llm-tokens"
hx-get="/api/monitoring/llm/tokens"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
--
</div>
</div>
</div>
</div>"## .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();
let status = if db_ok { "healthy" } else { "degraded" }; let status = if db_ok { "healthy" } else { "degraded" };
Html(format!( Html(format!(
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;
let hours = (seconds % 86400) / 3600; let hours = (seconds % 86400) / 3600;
let minutes = (seconds % 3600) / 60; let minutes = (seconds % 3600) / 60;
if days > 0 { if days > 0 {
format!("{}d {}h {}m", days, hours, minutes) format!("{}d {}h {}m", days, hours, minutes)
} else if hours > 0 { } else if hours > 0 {
format!("{}h {}m", hours, minutes) format!("{}h {}m", hours, minutes)
} 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
.try_lock() .try_lock()
.map(|sm| sm.active_count()) .map(|sm| sm.active_count())
.unwrap_or(0); .unwrap_or(0);
Html(format!(
r##"<div class="bots-list">
<div class="bot-item">
<span class="bot-name">Active Sessions</span>
<span class="bot-count">{active_sessions}</span>
</div>
</div>"##
))
}
Html(format!(
r##"<div class="bots-list">
<div class="bot-item">
<span class="bot-name">Active Sessions</span>
<span class="bot-count">{active_sessions}</span>
</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![
("postgresql", check_postgres()), ("postgresql", check_postgres()),
("redis", check_redis()), ("redis", check_redis()),
("minio", check_minio()), ("minio", check_minio()),
("llm", check_llm()), ("llm", check_llm()),
]; ];
let mut status_updates = String::new(); let mut status_updates = String::new();
for (name, running) in services { for (name, running) in services {
let status = if running { "running" } else { "stopped" }; let status = if running { "running" } else { "stopped" };
status_updates.push_str(&format!( status_updates.push_str(&format!(
r##"<script> r##"<script>
(function() {{ (function() {{
var el = document.querySelector('[data-service="{name}"]'); var el = document.querySelector('[data-service="{name}"]');
if (el) el.setAttribute('data-status', '{status}'); if (el) el.setAttribute('data-status', '{status}');
}})(); }})();
</script>"## </script>"##
)); ));
}
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> {
let mut sys = System::new_all(); #[cfg(feature = "monitoring")]
sys.refresh_all(); let (cpu_usage, memory_percent) = {
let mut sys = System::new_all();
sys.refresh_all();
let cpu_usage = sys.global_cpu_usage(); let cpu_usage = sys.global_cpu_usage();
let total_memory = sys.total_memory(); let total_memory = sys.total_memory();
@ -477,97 +430,82 @@ async fn resources_bars(State(_state): State<Arc<AppState>>) -> Html<String> {
} else { } else {
0.0 0.0
}; };
(cpu_usage, memory_percent)
};
Html(format!( #[cfg(not(feature = "monitoring"))]
r##"<g> let (cpu_usage, memory_percent) = (0.0, 0.0);
<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),
))
}
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), )) }
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
.try_lock() .try_lock()
.map(|sm| sm.active_count()) .map(|sm| sm.active_count())
.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
.try_lock() .try_lock()
.map(|sm| sm.active_count()) .map(|sm| sm.active_count())
.unwrap_or(0); .unwrap_or(0);
Html(format!( Html(format!(
r##"<div class="sessions-panel"> r##"<div class="sessions-panel">
<div class="panel-header"> <div class="panel-header">
<h3>Active Sessions</h3> <h3>Active Sessions</h3>
<span class="session-count">{active_sessions}</span> <span class="session-count">{active_sessions}</span>
</div>
<div class="session-list">
<div class="empty-state">
<p>No active sessions</p>
</div> </div>
<div class="session-list"> </div>
<div class="empty-state"> </div>"## )) }
<p>No active sessions</p>
</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(
r##"<div class="messages-panel"> r##"<div class="messages-panel">
<div class="panel-header"> <div class="panel-header">
<h3>Recent Messages</h3> <h3>Recent Messages</h3>
</div> </div>
<div class="message-list"> <div class="message-list">
<div class="empty-state"> <div class="empty-state">
<p>No recent messages</p> <p>No recent messages</p>
</div> </div>
</div> </div>
</div>"##
.to_string(), </div>"## .to_string(), ) }
)
}

View file

@ -832,9 +832,10 @@ fn validate_session_sync(session_id: &str) -> Result<AuthenticatedUser, AuthErro
session_id.len(), session_id.len(),
&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
if let Ok(cache_guard) = crate::directory::auth_routes::SESSION_CACHE.try_read() { #[cfg(feature = "directory")]
if let Some(user_data) = cache_guard.get(session_id) { 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); debug!("Found user in session cache: {}", user_data.email);
// Parse user_id from cached data // Parse user_id from cached data

View file

@ -6,10 +6,10 @@ pub mod rbac_ui;
pub mod security_admin; pub mod security_admin;
use axum::{ use axum::{
extract::State, extract::State,
response::{Html, Json}, response::{Html, Json},
routing::{get, post}, routing::{get, post},
Router, Router,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc; use std::sync::Arc;
@ -17,314 +17,310 @@ use std::sync::Arc;
use crate::shared::state::AppState; use crate::shared::state::AppState;
pub fn configure_settings_routes() -> Router<Arc<AppState>> { pub fn configure_settings_routes() -> Router<Arc<AppState>> {
Router::new() Router::new()
.route("/api/user/storage", get(get_storage_info)) .route("/api/user/storage", get(get_storage_info))
.route("/api/user/storage/connections", get(get_storage_connections)) .route("/api/user/storage/connections", get(get_storage_connections))
.route("/api/user/security/2fa/status", get(get_2fa_status)) .route("/api/user/security/2fa/status", get(get_2fa_status))
.route("/api/user/security/2fa/enable", post(enable_2fa)) .route("/api/user/security/2fa/enable", post(enable_2fa))
.route("/api/user/security/2fa/disable", post(disable_2fa)) .route("/api/user/security/2fa/disable", post(disable_2fa))
.route("/api/user/security/sessions", get(get_active_sessions)) .route("/api/user/security/sessions", get(get_active_sessions))
.route( .route(
"/api/user/security/sessions/revoke-all", "/api/user/security/sessions/revoke-all",
post(revoke_all_sessions), post(revoke_all_sessions),
) )
.route("/api/user/security/devices", get(get_trusted_devices)) .route("/api/user/security/devices", get(get_trusted_devices))
.route("/api/settings/search", post(save_search_settings)) .route("/api/settings/search", post(save_search_settings))
.route("/api/settings/smtp/test", post(test_smtp_connection)) .route("/api/settings/smtp/test", post(test_smtp_connection))
.route("/api/settings/accounts/social", get(get_accounts_social)) .route("/api/settings/accounts/social", get(get_accounts_social))
.route("/api/settings/accounts/messaging", get(get_accounts_messaging)) .route("/api/settings/accounts/messaging", get(get_accounts_messaging))
.route("/api/settings/accounts/email", get(get_accounts_email)) .route("/api/settings/accounts/email", get(get_accounts_email))
.route("/api/settings/accounts/smtp", post(save_smtp_account)) .route("/api/settings/accounts/smtp", post(save_smtp_account))
.route("/api/ops/health", get(get_ops_health)) .route("/api/ops/health", get(get_ops_health))
.route("/api/rbac/permissions", get(get_rbac_permissions)) .route("/api/rbac/permissions", get(get_rbac_permissions))
.merge(rbac::configure_rbac_routes()) .merge(rbac::configure_rbac_routes())
.merge(security_admin::configure_security_admin_routes()) .merge(security_admin::configure_security_admin_routes())
} }
async fn get_accounts_social(State(_state): State<Arc<AppState>>) -> Html<String> { async fn get_accounts_social(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">Instagram</span><span class="account-status disconnected">Not connected</span></div> <div class="account-item"><span class="account-icon">📷</span><span class="account-name">Instagram</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">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">
<div class="account-item"><span class="account-icon">💬</span><span class="account-name">Discord</span><span class="account-status disconnected">Not connected</span></div> <div class="account-item"><span class="account-icon">💬</span><span class="account-name">Discord</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">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>>,
Json(config): Json<serde_json::Value>, Json(config): Json<serde_json::Value>,
) -> Json<serde_json::Value> { ) -> Json<serde_json::Value> {
Json(serde_json::json!({ Json(serde_json::json!({
"success": true, "success": true,
"message": "SMTP configuration saved", "message": "SMTP configuration saved",
"config": config "config": config
})) }))
} }
async fn get_ops_health(State(_state): State<Arc<AppState>>) -> Json<serde_json::Value> { async fn get_ops_health(State(_state): State<Arc<AppState>>) -> Json<serde_json::Value> {
Json(serde_json::json!({ Json(serde_json::json!({
"status": "healthy", "status": "healthy",
"services": { "services": {
"api": {"status": "up", "latency_ms": 12}, "api": {"status": "up", "latency_ms": 12},
"database": {"status": "up", "latency_ms": 5}, "database": {"status": "up", "latency_ms": 5},
"cache": {"status": "up", "latency_ms": 1}, "cache": {"status": "up", "latency_ms": 1},
"storage": {"status": "up", "latency_ms": 8} "storage": {"status": "up", "latency_ms": 8}
}, },
"timestamp": chrono::Utc::now().to_rfc3339() "timestamp": chrono::Utc::now().to_rfc3339()
})) }))
} }
async fn get_rbac_permissions(State(_state): State<Arc<AppState>>) -> Json<serde_json::Value> { async fn get_rbac_permissions(State(_state): State<Arc<AppState>>) -> Json<serde_json::Value> {
Json(serde_json::json!({ Json(serde_json::json!({
"permissions": [ "permissions": [
{"id": "read:users", "name": "Read Users", "category": "Users"}, {"id": "read:users", "name": "Read Users", "category": "Users"},
{"id": "write:users", "name": "Write Users", "category": "Users"}, {"id": "write:users", "name": "Write Users", "category": "Users"},
{"id": "delete:users", "name": "Delete Users", "category": "Users"}, {"id": "delete:users", "name": "Delete Users", "category": "Users"},
{"id": "read:bots", "name": "Read Bots", "category": "Bots"}, {"id": "read:bots", "name": "Read Bots", "category": "Bots"},
{"id": "write:bots", "name": "Write Bots", "category": "Bots"}, {"id": "write:bots", "name": "Write Bots", "category": "Bots"},
{"id": "admin:billing", "name": "Manage Billing", "category": "Admin"}, {"id": "admin:billing", "name": "Manage Billing", "category": "Admin"},
{"id": "admin:settings", "name": "Manage Settings", "category": "Admin"} {"id": "admin:settings", "name": "Manage Settings", "category": "Admin"}
] ]
})) }))
} }
async fn get_storage_info(State(_state): State<Arc<AppState>>) -> Html<String> { async fn get_storage_info(State(_state): State<Arc<AppState>>) -> Html<String> {
Html( Html(
r##"<div class="storage-info"> r##"<div class="storage-info">
<div class="storage-bar"> <div class="storage-bar">
<div class="storage-used" style="width: 25%"></div> <div class="storage-used" style="width: 25%"></div>
</div> </div>
<div class="storage-details"> <div class="storage-details">
<span class="storage-used-text">2.5 GB used</span> <span class="storage-used-text">2.5 GB used</span>
<span class="storage-total-text">of 10 GB</span> <span class="storage-total-text">of 10 GB</span>
</div> </div>
<div class="storage-breakdown"> <div class="storage-breakdown">
<div class="storage-item"> <div class="storage-item">
<span class="storage-icon">📄</span> <span class="storage-icon">📄</span>
<span class="storage-label">Documents</span> <span class="storage-label">Documents</span>
<span class="storage-size">1.2 GB</span> <span class="storage-size">1.2 GB</span>
</div> </div>
<div class="storage-item"> <div class="storage-item">
<span class="storage-icon">🖼</span> <span class="storage-icon">🖼</span>
<span class="storage-label">Images</span> <span class="storage-label">Images</span>
<span class="storage-size">800 MB</span> <span class="storage-size">800 MB</span>
</div> </div>
<div class="storage-item"> <div class="storage-item">
<span class="storage-icon">📧</span> <span class="storage-icon">📧</span>
<span class="storage-label">Emails</span> <span class="storage-label">Emails</span>
<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(
r##"<div class="connections-empty"> r##"<div class="connections-empty">
<p class="text-muted">No external storage connections configured</p> <p class="text-muted">No external storage connections configured</p>
<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)]
struct SearchSettingsRequest { struct SearchSettingsRequest {
enable_fuzzy_search: Option<bool>, enable_fuzzy_search: Option<bool>,
search_result_limit: Option<i32>, search_result_limit: Option<i32>,
enable_ai_suggestions: Option<bool>, enable_ai_suggestions: Option<bool>,
index_attachments: Option<bool>, index_attachments: Option<bool>,
search_sources: Option<Vec<String>>, search_sources: Option<Vec<String>>,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct SearchSettingsResponse { struct SearchSettingsResponse {
success: bool, success: bool,
message: Option<String>, message: Option<String>,
error: Option<String>, error: Option<String>,
} }
async fn save_search_settings( async fn save_search_settings(
State(_state): State<Arc<AppState>>, State(_state): State<Arc<AppState>>,
Json(settings): Json<SearchSettingsRequest>, Json(settings): Json<SearchSettingsRequest>,
) -> Json<SearchSettingsResponse> { ) -> Json<SearchSettingsResponse> {
// In a real implementation, save to database // In a real implementation, save to database
log::info!("Saving search settings: fuzzy={:?}, limit={:?}, ai={:?}", log::info!("Saving search settings: fuzzy={:?}, limit={:?}, ai={:?}",
settings.enable_fuzzy_search, settings.enable_fuzzy_search,
settings.search_result_limit, settings.search_result_limit,
settings.enable_ai_suggestions settings.enable_ai_suggestions
); );
Json(SearchSettingsResponse {
success: true,
message: Some("Search settings saved successfully".to_string()),
error: None,
})
Json(SearchSettingsResponse {
success: true,
message: Some("Search settings saved successfully".to_string()),
error: None,
})
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)] #[allow(dead_code)]
struct SmtpTestRequest { struct SmtpTestRequest {
host: String, host: String,
port: i32, port: i32,
username: Option<String>, username: Option<String>,
password: Option<String>, password: Option<String>,
use_tls: Option<bool>, use_tls: Option<bool>,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct SmtpTestResponse { struct SmtpTestResponse {
success: bool, success: bool,
message: Option<String>, message: Option<String>,
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>,
) -> Json<SmtpTestResponse> { ) -> Json<SmtpTestResponse> {
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);
let mailer_result = if let (Some(user), Some(pass)) = (config.username, config.password) {
let creds = Credentials::new(user, pass);
SmtpTransport::relay(&config.host)
.map(|b| b.port(config.port as u16).credentials(creds).build())
} else {
Ok(SmtpTransport::builder_dangerous(&config.host)
.port(config.port as u16)
.build())
};
match mailer_result { log::info!("Testing SMTP connection to {}:{}", config.host, config.port);
Ok(mailer) => {
match mailer.test_connection() { let mailer_result = if let (Some(user), Some(pass)) = (config.username, config.password) {
Ok(true) => Json(SmtpTestResponse { let creds = Credentials::new(user, pass);
success: true, SmtpTransport::relay(&config.host)
message: Some("SMTP connection successful".to_string()), .map(|b| b.port(config.port as u16).credentials(creds).build())
error: None, } else {
}), Ok(SmtpTransport::builder_dangerous(&config.host)
Ok(false) => Json(SmtpTestResponse { .port(config.port as u16)
success: false, .build())
message: None, };
error: Some("SMTP connection test failed".to_string()),
}), match mailer_result {
Err(e) => Json(SmtpTestResponse { Ok(mailer) => {
success: false, match mailer.test_connection() {
message: None, Ok(true) => Json(SmtpTestResponse {
error: Some(format!("SMTP error: {}", e)), success: true,
}), message: Some("SMTP connection successful".to_string()),
} error: None,
}),
Ok(false) => Json(SmtpTestResponse {
success: false,
message: None,
error: Some("SMTP connection test failed".to_string()),
}),
Err(e) => Json(SmtpTestResponse {
success: false,
message: None,
error: Some(format!("SMTP error: {}", e)),
}),
} }
Err(e) => Json(SmtpTestResponse {
success: false,
message: None,
error: Some(format!("Failed to create SMTP transport: {}", e)),
}),
} }
Err(e) => Json(SmtpTestResponse {
success: false,
message: None,
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> {
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 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(
r##"<div class="session-item current"> r##"<div class="session-item current">
<div class="session-info"> <div class="session-info">
<div class="session-device"> <div class="session-device">
<span class="device-icon">💻</span> <span class="device-icon">💻</span>
<span class="device-name">Current Session</span> <span class="device-name">Current Session</span>
<span class="session-badge current">This device</span> <span class="session-badge current">This device</span>
</div>
<div class="session-details">
<span class="session-location">Current browser session</span>
<span class="session-time">Active now</span>
</div>
</div>
</div> </div>
<div class="sessions-empty"> <div class="session-details">
<p class="text-muted">No other active sessions</p> <span class="session-location">Current browser session</span>
</div>"## <span class="session-time">Active now</span>
.to_string(), </div>
) </div>
}
</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> { 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(
r##"<div class="device-item current"> r##"<div class="device-item current">
<div class="device-info"> <div class="device-info">
<span class="device-icon">💻</span> <span class="device-icon">💻</span>
<div class="device-details"> <div class="device-details">
<span class="device-name">Current Device</span> <span class="device-name">Current Device</span>
<span class="device-last-seen">Last active: Just now</span> <span class="device-last-seen">Last active: Just now</span>
</div>
</div>
<span class="device-badge trusted">Trusted</span>
</div> </div>
<div class="devices-empty"> </div>
<p class="text-muted">No other trusted devices</p> <span class="device-badge trusted">Trusted</span>
</div>"##
.to_string(), </div> <div class="devices-empty"> <p class="text-muted">No other trusted devices</p> </div>"## .to_string(), ) }
)
}