Auto-commit: 20260118_195334

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

6
.cargo/config.toml Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,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 std::collections::HashMap;
use std::sync::{LazyLock, Mutex, RwLock};
use std::time::{Duration, Instant};
#[cfg(feature = "monitoring")]
use sysinfo::{Pid, ProcessesToUpdate, System};
static THREAD_REGISTRY: LazyLock<RwLock<HashMap<String, ThreadInfo>>> =
LazyLock::new(|| RwLock::new(HashMap::new()));
LazyLock::new(|| RwLock::new(HashMap::new()));
static COMPONENT_TRACKER: LazyLock<ComponentMemoryTracker> =
LazyLock::new(|| ComponentMemoryTracker::new(60));
LazyLock::new(|| ComponentMemoryTracker::new(60));
#[derive(Debug, Clone)]
pub struct ThreadInfo {
pub name: String,
pub started_at: Instant,
pub last_activity: Instant,
pub activity_count: u64,
pub component: String,
pub name: String,
pub started_at: Instant,
pub last_activity: Instant,
pub activity_count: u64,
pub component: String,
}
pub fn register_thread(name: &str, component: &str) {
let info = ThreadInfo {
name: name.to_string(),
started_at: Instant::now(),
last_activity: Instant::now(),
activity_count: 0,
component: component.to_string(),
};
if let Ok(mut registry) = THREAD_REGISTRY.write() {
registry.insert(name.to_string(), info);
}
trace!("[THREAD] Registered: {} (component: {})", name, component);
let info = ThreadInfo {
name: name.to_string(),
started_at: Instant::now(),
last_activity: Instant::now(),
activity_count: 0,
component: component.to_string(),
};
if let Ok(mut registry) = THREAD_REGISTRY.write() {
registry.insert(name.to_string(), info);
}
trace!("[THREAD] Registered: {} (component: {})", name, component);
}
pub fn record_thread_activity(name: &str) {
if let Ok(mut registry) = THREAD_REGISTRY.write() {
if let Some(info) = registry.get_mut(name) {
info.last_activity = Instant::now();
info.activity_count += 1;
}
}
if let Ok(mut registry) = THREAD_REGISTRY.write() {
if let Some(info) = registry.get_mut(name) {
info.last_activity = Instant::now();
info.activity_count += 1;
}
}
}
pub fn unregister_thread(name: &str) {
if let Ok(mut registry) = THREAD_REGISTRY.write() {
registry.remove(name);
}
info!("[THREAD] Unregistered: {}", name);
if let Ok(mut registry) = THREAD_REGISTRY.write() {
registry.remove(name);
}
info!("[THREAD] Unregistered: {}", name);
}
pub fn log_thread_stats() {
if let Ok(registry) = THREAD_REGISTRY.read() {
info!("[THREADS] Active thread count: {}", registry.len());
for (name, info) in registry.iter() {
let uptime = info.started_at.elapsed().as_secs();
let idle = info.last_activity.elapsed().as_secs();
info!(
"[THREAD] {} | component={} | uptime={}s | idle={}s | activities={}",
name, info.component, uptime, idle, info.activity_count
);
}
}
if let Ok(registry) = THREAD_REGISTRY.read() {
info!("[THREADS] Active thread count: {}", registry.len());
for (name, info) in registry.iter() {
let uptime = info.started_at.elapsed().as_secs();
let idle = info.last_activity.elapsed().as_secs();
info!(
"[THREAD] {} | component={} | uptime={}s | idle={}s | activities={}",
name, info.component, uptime, idle, info.activity_count
);
}
}
}
#[derive(Debug, Clone)]
pub struct MemoryStats {
pub rss_bytes: u64,
pub virtual_bytes: u64,
pub timestamp: Instant,
pub rss_bytes: u64,
pub virtual_bytes: u64,
pub timestamp: Instant,
}
impl MemoryStats {
pub fn current() -> Self {
let (rss, virt) = get_process_memory().unwrap_or((0, 0));
Self {
rss_bytes: rss,
virtual_bytes: virt,
timestamp: Instant::now(),
}
}
pub fn current() -> Self {
let (rss, virt) = get_process_memory().unwrap_or((0, 0));
Self {
rss_bytes: rss,
virtual_bytes: virt,
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 {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.2} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
pub fn format_bytes(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
pub fn log(&self) {
info!(
"[MEMORY] RSS={}, Virtual={}",
Self::format_bytes(self.rss_bytes),
Self::format_bytes(self.virtual_bytes),
);
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} 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
#[cfg(feature = "jemalloc")]
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;
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;
// Advance the epoch to refresh statistics
if epoch::advance().is_err() {
return None;
}
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"))]
pub fn get_jemalloc_stats() -> Option<JemallocStats> {
None
None
}
/// Jemalloc memory statistics
#[derive(Debug, Clone)]
pub struct JemallocStats {
/// Total bytes allocated by the application
pub allocated: u64,
/// Total bytes in active pages allocated by the application
pub active: u64,
/// Total bytes in physically resident pages
pub resident: u64,
/// Total bytes in active extents mapped by the allocator
pub mapped: u64,
/// Total bytes retained (not returned to OS)
pub retained: u64,
/// Total bytes allocated by the application
pub allocated: u64,
/// Total bytes in active pages allocated by the application
pub active: u64,
/// Total bytes in physically resident pages
pub resident: u64,
/// Total bytes in active extents mapped by the allocator
pub mapped: u64,
/// Total bytes retained (not returned to OS)
pub retained: u64,
}
impl JemallocStats {
pub fn log(&self) {
info!(
"[JEMALLOC] allocated={} active={} resident={} mapped={} retained={}",
MemoryStats::format_bytes(self.allocated),
MemoryStats::format_bytes(self.active),
MemoryStats::format_bytes(self.resident),
MemoryStats::format_bytes(self.mapped),
MemoryStats::format_bytes(self.retained),
);
}
pub fn log(&self) {
info!(
"[JEMALLOC] allocated={} active={} resident={} mapped={} retained={}",
MemoryStats::format_bytes(self.allocated),
MemoryStats::format_bytes(self.active),
MemoryStats::format_bytes(self.resident),
MemoryStats::format_bytes(self.mapped),
MemoryStats::format_bytes(self.retained),
);
}
/// Calculate fragmentation ratio (1.0 = no fragmentation)
pub fn fragmentation_ratio(&self) -> f64 {
if self.allocated > 0 {
self.active as f64 / self.allocated as f64
} else {
1.0
}
/// Calculate fragmentation ratio (1.0 = no fragmentation)
pub fn fragmentation_ratio(&self) -> f64 {
if self.allocated > 0 {
self.active as f64 / self.allocated as f64
} else {
1.0
}
}
}
/// Log jemalloc stats if available
pub fn log_jemalloc_stats() {
if let Some(stats) = get_jemalloc_stats() {
stats.log();
let frag = stats.fragmentation_ratio();
if frag > 1.5 {
warn!("[JEMALLOC] High fragmentation detected: {:.2}x", frag);
}
}
if let Some(stats) = get_jemalloc_stats() {
stats.log();
let frag = stats.fragmentation_ratio();
if frag > 1.5 {
warn!("[JEMALLOC] High fragmentation detected: {:.2}x", frag);
}
}
}
#[derive(Debug)]
pub struct MemoryCheckpoint {
pub name: String,
pub stats: MemoryStats,
pub name: String,
pub stats: MemoryStats,
}
impl MemoryCheckpoint {
pub fn new(name: &str) -> Self {
let stats = MemoryStats::current();
info!(
"[CHECKPOINT] {} started at RSS={}",
name,
MemoryStats::format_bytes(stats.rss_bytes)
pub fn new(name: &str) -> Self {
let stats = MemoryStats::current();
info!(
"[CHECKPOINT] {} started at RSS={}",
name,
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 {
name: name.to_string(),
stats,
}
} else if diff < 0 {
info!(
"[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 {
components: Mutex<HashMap<String, Vec<MemoryStats>>>,
max_history: usize,
components: Mutex<HashMap<String, Vec<MemoryStats>>>,
max_history: usize,
}
impl ComponentMemoryTracker {
pub fn new(max_history: usize) -> Self {
Self {
components: Mutex::new(HashMap::new()),
max_history,
pub fn new(max_history: usize) -> Self {
Self {
components: Mutex::new(HashMap::new()),
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) {
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 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
);
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
);
}
}
}
}
}
pub fn record_component(component: &str) {
COMPONENT_TRACKER.record(component);
COMPONENT_TRACKER.record(component);
}
pub fn log_component_stats() {
COMPONENT_TRACKER.log_all();
COMPONENT_TRACKER.log_all();
}
pub struct LeakDetector {
baseline: Mutex<u64>,
growth_threshold_bytes: u64,
consecutive_growth_count: Mutex<usize>,
max_consecutive_growth: usize,
baseline: Mutex<u64>,
growth_threshold_bytes: u64,
consecutive_growth_count: Mutex<usize>,
max_consecutive_growth: usize,
}
impl LeakDetector {
pub fn new(growth_threshold_mb: u64, max_consecutive_growth: usize) -> Self {
Self {
baseline: Mutex::new(0),
growth_threshold_bytes: growth_threshold_mb * 1024 * 1024,
consecutive_growth_count: Mutex::new(0),
max_consecutive_growth,
}
}
pub fn new(growth_threshold_mb: u64, max_consecutive_growth: usize) -> Self {
Self {
baseline: Mutex::new(0),
growth_threshold_bytes: growth_threshold_mb * 1024 * 1024,
consecutive_growth_count: Mutex::new(0),
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() {
*baseline = current.rss_bytes;
}
if let Ok(mut count) = self.consecutive_growth_count.lock() {
*count = 0;
}
return None;
}
pub fn check(&self) -> Option<String> {
let current = MemoryStats::current();
let growth = current.rss_bytes.saturating_sub(baseline_val);
let baseline_val = match self.baseline.lock() {
Ok(b) => *b,
if growth > self.growth_threshold_bytes {
let count = match self.consecutive_growth_count.lock() {
Ok(mut c) => {
*c += 1;
*c
}
Err(_) => return None,
};
if baseline_val == 0 {
if let Ok(mut baseline) = self.baseline.lock() {
*baseline = current.rss_bytes;
}
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),
));
}
let growth = current.rss_bytes.saturating_sub(baseline_val);
if growth > self.growth_threshold_bytes {
let count = match self.consecutive_growth_count.lock() {
Ok(mut c) => {
*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;
}
} 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) {
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!(
"[MONITOR] Started (interval={}s, threshold={}MB)",
interval_secs, warn_threshold_mb
tokio::spawn(async move {
register_thread("memory-monitor", "monitoring");
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;
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),
);
// 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);
}
// 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);
}
}
});
}
#[cfg(feature = "monitoring")]
#[cfg(feature = "monitoring")]
pub fn get_process_memory() -> Option<(u64, u64)> {
let pid = Pid::from_u32(std::process::id());
let mut sys = System::new();
sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true);
let pid = Pid::from_u32(std::process::id());
let mut sys = System::new();
sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true);
sys.process(pid).map(|p| (p.memory(), p.virtual_memory()))
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() {
if let Some((rss, virt)) = get_process_memory() {
trace!(
"[PROCESS] RSS={}, Virtual={}",
MemoryStats::format_bytes(rss),
MemoryStats::format_bytes(virt)
);
}
if let Some((rss, virt)) = get_process_memory() {
trace!(
"[PROCESS] RSS={}, Virtual={}",
MemoryStats::format_bytes(rss),
MemoryStats::format_bytes(virt)
);
}
}
#[cfg(test)]
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]
fn test_format_bytes() {
assert_eq!(MemoryStats::format_bytes(500), "500 B");
assert_eq!(MemoryStats::format_bytes(1024), "1.00 KB");
assert_eq!(MemoryStats::format_bytes(1024 * 1024), "1.00 MB");
assert_eq!(MemoryStats::format_bytes(1024 * 1024 * 1024), "1.00 GB");
}
#[test]
fn test_checkpoint() {
let checkpoint = MemoryCheckpoint::new("test");
checkpoint.compare_and_log();
}
#[test]
fn test_thread_registry() {
register_thread("test-thread", "test-component");
record_thread_activity("test-thread");
log_thread_stats();
unregister_thread("test-thread");
}
#[test]
fn test_memory_stats() {
let stats = MemoryStats::current();
assert!(stats.rss_bytes > 0 || stats.virtual_bytes >= 0);
}
#[test]
fn test_format_bytes() {
assert_eq!(MemoryStats::format_bytes(500), "500 B");
assert_eq!(MemoryStats::format_bytes(1024), "1.00 KB");
assert_eq!(MemoryStats::format_bytes(1024 * 1024), "1.00 MB");
assert_eq!(MemoryStats::format_bytes(1024 * 1024 * 1024), "1.00 GB");
}
#[test]
fn test_checkpoint() {
let checkpoint = MemoryCheckpoint::new("test");
checkpoint.compare_and_log();
}
#[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"))]
use crate::core::shared::test_utils::MockLLMProvider;
#[cfg(feature = "directory")]
use crate::directory::AuthService;
use crate::core::directory::AuthService;
#[cfg(feature = "llm")]
use crate::llm::LLMProvider;
use crate::shared::models::BotResponse;

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,7 +1,7 @@
use axum::{extract::State, response::Html, routing::get, Router};
use chrono::Local;
use std::sync::Arc;
#[cfg(feature = "monitoring")]
use sysinfo::{Disks, Networks, System};
use crate::core::urls::ApiUrls;
@ -10,35 +10,35 @@ use crate::shared::state::AppState;
pub mod real_time;
pub mod tracing;
pub fn configure() -> Router<Arc<AppState>> {
Router::new()
.route(ApiUrls::MONITORING_DASHBOARD, get(dashboard))
.route(ApiUrls::MONITORING_SERVICES, get(services))
.route(ApiUrls::MONITORING_RESOURCES, get(resources))
.route(ApiUrls::MONITORING_LOGS, get(logs))
.route(ApiUrls::MONITORING_LLM, get(llm_metrics))
.route(ApiUrls::MONITORING_HEALTH, get(health))
// Additional endpoints expected by the frontend
.route("/api/ui/monitoring/timestamp", get(timestamp))
.route("/api/ui/monitoring/bots", get(bots))
.route("/api/ui/monitoring/services/status", get(services_status))
.route("/api/ui/monitoring/resources/bars", get(resources_bars))
.route("/api/ui/monitoring/activity/latest", get(activity_latest))
.route("/api/ui/monitoring/metric/sessions", get(metric_sessions))
.route("/api/ui/monitoring/metric/messages", get(metric_messages))
.route("/api/ui/monitoring/metric/response_time", get(metric_response_time))
.route("/api/ui/monitoring/trend/sessions", get(trend_sessions))
.route("/api/ui/monitoring/rate/messages", get(rate_messages))
// Aliases for frontend compatibility
.route("/api/ui/monitoring/sessions", get(sessions_panel))
.route("/api/ui/monitoring/messages", get(messages_panel))
Router::new()
.route(ApiUrls::MONITORING_DASHBOARD, get(dashboard))
.route(ApiUrls::MONITORING_SERVICES, get(services))
.route(ApiUrls::MONITORING_RESOURCES, get(resources))
.route(ApiUrls::MONITORING_LOGS, get(logs))
.route(ApiUrls::MONITORING_LLM, get(llm_metrics))
.route(ApiUrls::MONITORING_HEALTH, get(health))
// Additional endpoints expected by the frontend
.route("/api/ui/monitoring/timestamp", get(timestamp))
.route("/api/ui/monitoring/bots", get(bots))
.route("/api/ui/monitoring/services/status", get(services_status))
.route("/api/ui/monitoring/resources/bars", get(resources_bars))
.route("/api/ui/monitoring/activity/latest", get(activity_latest))
.route("/api/ui/monitoring/metric/sessions", get(metric_sessions))
.route("/api/ui/monitoring/metric/messages", get(metric_messages))
.route("/api/ui/monitoring/metric/response_time", get(metric_response_time))
.route("/api/ui/monitoring/trend/sessions", get(trend_sessions))
.route("/api/ui/monitoring/rate/messages", get(rate_messages))
// Aliases for frontend compatibility
.route("/api/ui/monitoring/sessions", get(sessions_panel))
.route("/api/ui/monitoring/messages", get(messages_panel))
}
async fn dashboard(State(state): State<Arc<AppState>>) -> Html<String> {
let mut sys = System::new_all();
sys.refresh_all();
#[cfg(feature = "monitoring")]
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 total_memory = sys.total_memory();
@ -52,139 +52,119 @@ async fn dashboard(State(state): State<Arc<AppState>>) -> Html<String> {
let uptime = System::uptime();
let uptime_str = format_uptime(uptime);
let active_sessions = state
.session_manager
.try_lock()
.map(|sm| sm.active_count())
.unwrap_or(0);
(cpu_usage, total_memory, used_memory, memory_percent, uptime_str)
};
Html(format!(
r##"<div class="dashboard-grid">
<div class="metric-card">
<div class="metric-header">
<span class="metric-title">CPU Usage</span>
<span class="metric-badge {cpu_status}">{cpu_usage:.1}%</span>
</div>
<div class="metric-value">{cpu_usage:.1}%</div>
<div class="metric-bar">
<div class="metric-bar-fill" style="width: {cpu_usage}%"></div>
</div>
#[cfg(not(feature = "monitoring"))]
let (cpu_usage, total_memory, used_memory, memory_percent, uptime_str) = (
0.0, 0, 0, 0.0, "N/A".to_string()
);
let active_sessions = state
.session_manager
.try_lock()
.map(|sm| sm.active_count())
.unwrap_or(0);
Html(format!(
r##"<div class="dashboard-grid">
<div class="metric-card">
<div class="metric-header">
<span class="metric-title">CPU Usage</span>
<span class="metric-badge {cpu_status}">{cpu_usage:.1}%</span>
</div>
<div class="metric-card">
<div class="metric-header">
<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 class="metric-value">{cpu_usage:.1}%</div>
<div class="metric-bar">
<div class="metric-bar-fill" style="width: {cpu_usage}%"></div>
</div>
</div>
<div class="refresh-indicator" hx-get="/api/monitoring/dashboard" hx-trigger="every 10s" hx-swap="outerHTML" hx-target="closest .dashboard-grid, .refresh-indicator">
<span class="refresh-dot"></span> Auto-refreshing
</div>"##,
cpu_status = if cpu_usage > 80.0 {
"danger"
} else if cpu_usage > 60.0 {
"warning"
} else {
"success"
},
mem_status = if memory_percent > 80.0 {
"danger"
} else if memory_percent > 60.0 {
"warning"
} else {
"success"
},
used_gb = used_memory as f64 / 1_073_741_824.0,
total_gb = total_memory as f64 / 1_073_741_824.0,
))
}
<div class="metric-card">
<div class="metric-header">
<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 class="refresh-indicator" hx-get="/api/monitoring/dashboard" hx-trigger="every 10s" hx-swap="outerHTML" hx-target="closest .dashboard-grid, .refresh-indicator"> <span class="refresh-dot"></span> Auto-refreshing </div>"##, cpu_status = if cpu_usage > 80.0 { "danger" } else if cpu_usage > 60.0 { "warning" } else { "success" }, mem_status = if memory_percent > 80.0 { "danger" } else if memory_percent > 60.0 { "warning" } else { "success" }, used_gb = used_memory as f64 / 1_073_741_824.0, total_gb = total_memory as f64 / 1_073_741_824.0, )) }
async fn services(State(_state): State<Arc<AppState>>) -> Html<String> {
let services = vec![
("PostgreSQL", check_postgres(), "Database"),
("Redis", check_redis(), "Cache"),
("MinIO", check_minio(), "Storage"),
("LLM Server", check_llm(), "AI Backend"),
];
let services = vec![
("PostgreSQL", check_postgres(), "Database"),
("Redis", check_redis(), "Cache"),
("MinIO", check_minio(), "Storage"),
("LLM Server", check_llm(), "AI Backend"),
];
let mut rows = String::new();
for (name, status, desc) in services {
let (status_class, status_text) = if status {
("success", "Running")
} else {
("danger", "Stopped")
};
let mut rows = String::new();
for (name, status, desc) in services {
let (status_class, status_text) = if status {
("success", "Running")
} else {
("danger", "Stopped")
};
rows.push_str(&format!(
r##"<tr>
<td>
<div class="service-name">
<span class="status-dot {status_class}"></span>
{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>
rows.push_str(&format!(
r##"<tr>
<td>
<div class="service-name">
<span class="status-dot {status_class}"></span>
{name}
</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>"##
))
}
</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>
<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> {
let mut sys = System::new_all();
sys.refresh_all();
#[cfg(feature = "monitoring")]
let (disk_rows, net_rows) = {
let mut sys = System::new_all();
sys.refresh_all();
let disks = Disks::new_with_refreshed_list();
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!(
r##"<tr>
<td>{mount}</td>
<td>{used_gb:.1} GB</td>
<td>{total_gb:.1} GB</td>
<td>
<div class="usage-bar">
<div class="usage-fill {status}" style="width: {percent:.0}%"></div>
</div>
<span class="usage-text">{percent:.1}%</span>
</td>
</tr>"##,
mount = disk.mount_point().display(),
used_gb = used as f64 / 1_073_741_824.0,
total_gb = total as f64 / 1_073_741_824.0,
status = if percent > 90.0 {
"danger"
} else if percent > 70.0 {
"warning"
} else {
"success"
},
));
}
<td>{mount}</td>
<td>{used_gb:.1} GB</td>
<td>{total_gb:.1} GB</td>
<td>
<div class="usage-bar">
<div class="usage-fill {status}" style="width: {percent:.0}%"></div>
</div>
<span class="usage-text">{percent:.1}%</span>
</td>
</tr>"##, mount = disk.mount_point().display(), used_gb = used as f64 / 1_073_741_824.0, total_gb = total as f64 / 1_073_741_824.0, status = if percent > 90.0 { "danger" } else if percent > 70.0 { "warning" } else { "success" }, )); }
let networks = Networks::new_with_refreshed_list();
let mut net_rows = String::new();
for (name, data) in networks.list() {
net_rows.push_str(&format!(
r##"<tr>
<td>{name}</td>
<td>{rx:.2} MB</td>
<td>{tx:.2} MB</td>
</tr>"##,
rx = data.total_received() as f64 / 1_048_576.0,
tx = data.total_transmitted() as f64 / 1_048_576.0,
));
}
<td>{name}</td>
<td>{rx:.2} MB</td>
<td>{tx:.2} MB</td>
</tr>"##, rx = data.total_received() as f64 / 1_048_576.0, tx = data.total_transmitted() as f64 / 1_048_576.0, )); }
(disk_rows, net_rows)
};
Html(format!(
r##"<div class="resources-view">
<div class="section-header">
<h2>System Resources</h2>
</div>
#[cfg(not(feature = "monitoring"))]
let (disk_rows, net_rows) = (
String::new(),
String::new()
);
<div class="resource-section">
<h3>Disk Usage</h3>
<table class="data-table">
<thead>
<tr>
<th>Mount</th>
<th>Used</th>
<th>Total</th>
<th>Usage</th>
</tr>
</thead>
<tbody>
{disk_rows}
</tbody>
</table>
</div>
Html(format!(
r##"<div class="resources-view">
<div class="section-header">
<h2>System Resources</h2>
</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>"##
))
}
<div class="resource-section">
<h3>Disk Usage</h3>
<table class="data-table">
<thead>
<tr>
<th>Mount</th>
<th>Used</th>
<th>Total</th>
<th>Usage</th>
</tr>
</thead>
<tbody>
{disk_rows}
</tbody>
</table>
</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> {
Html(
r##"<div class="logs-view">
<div class="section-header">
<h2>System Logs</h2>
<div class="log-controls">
<select id="log-level" onchange="filterLogs(this.value)">
<option value="all">All Levels</option>
<option value="error">Error</option>
<option value="warn">Warning</option>
<option value="info">Info</option>
<option value="debug">Debug</option>
</select>
<button class="btn-secondary" onclick="clearLogs()">Clear</button>
</div>
</div>
<div class="log-container" id="log-container"
hx-get="/api/monitoring/logs/stream"
hx-trigger="every 2s"
hx-swap="beforeend scroll:bottom">
<div class="log-entry info">
<span class="log-time">System ready</span>
<span class="log-level">INFO</span>
<span class="log-message">Monitoring initialized</span>
</div>
</div>
</div>"##
.to_string(),
)
}
Html(
r##"<div class="logs-view">
<div class="section-header">
<h2>System Logs</h2>
<div class="log-controls">
<select id="log-level" onchange="filterLogs(this.value)">
<option value="all">All Levels</option>
<option value="error">Error</option>
<option value="warn">Warning</option>
<option value="info">Info</option>
<option value="debug">Debug</option>
</select>
<button class="btn-secondary" onclick="clearLogs()">Clear</button>
</div>
</div>
<div class="log-container" id="log-container"
hx-get="/api/monitoring/logs/stream"
hx-trigger="every 2s"
hx-swap="beforeend scroll:bottom">
<div class="log-entry info">
<span class="log-time">System ready</span>
<span class="log-level">INFO</span>
<span class="log-message">Monitoring initialized</span>
</div>
</div>
</div>"## .to_string(), ) }
async fn llm_metrics(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(
r##"<div class="llm-metrics-view">
<div class="section-header">
<h2>LLM Metrics</h2>
</div>
Html(
r##"<div class="llm-metrics-view">
<div class="section-header">
<h2>LLM Metrics</h2>
</div>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-title">Total Requests</div>
<div class="metric-value" id="llm-total-requests"
hx-get="/api/monitoring/llm/total"
hx-trigger="load, every 30s"
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 class="metrics-grid">
<div class="metric-card">
<div class="metric-title">Total Requests</div>
<div class="metric-value" id="llm-total-requests"
hx-get="/api/monitoring/llm/total"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
--
</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> {
let db_ok = state.conn.get().is_ok();
let status = if db_ok { "healthy" } else { "degraded" };
let db_ok = state.conn.get().is_ok();
let status = if db_ok { "healthy" } else { "degraded" };
Html(format!(
r##"<div class="health-status {status}">
<span class="status-icon"></span>
<span class="status-text">{status}</span>
</div>"##
))
}
Html(format!(
r##"<div class="health-status {status}">
<span class="status-icon"></span>
<span class="status-text">{status}</span>
</iv>"## )) }
fn format_uptime(seconds: u64) -> String {
let days = seconds / 86400;
let hours = (seconds % 86400) / 3600;
let minutes = (seconds % 3600) / 60;
let days = seconds / 86400;
let hours = (seconds % 86400) / 3600;
let minutes = (seconds % 3600) / 60;
if days > 0 {
format!("{}d {}h {}m", days, hours, minutes)
} else if hours > 0 {
format!("{}h {}m", hours, minutes)
} else {
format!("{}m", minutes)
}
if days > 0 {
format!("{}d {}h {}m", days, hours, minutes)
} else if hours > 0 {
format!("{}h {}m", hours, minutes)
} else {
format!("{}m", minutes)
}
}
fn check_postgres() -> bool {
true
true
}
fn check_redis() -> bool {
true
true
}
fn check_minio() -> bool {
true
true
}
fn check_llm() -> bool {
true
true
}
async fn timestamp(State(_state): State<Arc<AppState>>) -> Html<String> {
let now = Local::now();
Html(format!("Last updated: {}", now.format("%H:%M:%S")))
let now = Local::now();
Html(format!("Last updated: {}", now.format("%H:%M:%S")))
}
async fn bots(State(state): State<Arc<AppState>>) -> Html<String> {
let active_sessions = state
.session_manager
.try_lock()
.map(|sm| sm.active_count())
.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>"##
))
}
let active_sessions = state
.session_manager
.try_lock()
.map(|sm| sm.active_count())
.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>"## )) }
async fn services_status(State(_state): State<Arc<AppState>>) -> Html<String> {
let services = vec![
("postgresql", check_postgres()),
("redis", check_redis()),
("minio", check_minio()),
("llm", check_llm()),
];
let services = vec![
("postgresql", check_postgres()),
("redis", check_redis()),
("minio", check_minio()),
("llm", check_llm()),
];
let mut status_updates = String::new();
for (name, running) in services {
let status = if running { "running" } else { "stopped" };
status_updates.push_str(&format!(
r##"<script>
(function() {{
var el = document.querySelector('[data-service="{name}"]');
if (el) el.setAttribute('data-status', '{status}');
}})();
</script>"##
));
}
Html(status_updates)
let mut status_updates = String::new();
for (name, running) in services {
let status = if running { "running" } else { "stopped" };
status_updates.push_str(&format!(
r##"<script>
(function() {{
var el = document.querySelector('[data-service="{name}"]');
if (el) el.setAttribute('data-status', '{status}');
}})();
</script>"##
));
}
Html(status_updates)
}
async fn resources_bars(State(_state): State<Arc<AppState>>) -> Html<String> {
let mut sys = System::new_all();
sys.refresh_all();
#[cfg(feature = "monitoring")]
let (cpu_usage, memory_percent) = {
let mut sys = System::new_all();
sys.refresh_all();
let cpu_usage = sys.global_cpu_usage();
let total_memory = sys.total_memory();
@ -478,96 +431,81 @@ async fn resources_bars(State(_state): State<Arc<AppState>>) -> Html<String> {
0.0
};
Html(format!(
r##"<g>
<text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">CPU</text>
<rect x="40" y="-8" width="100" height="10" rx="2" fill="#1e293b"/>
<rect x="40" y="-8" width="{cpu_width}" height="10" rx="2" fill="#3b82f6"/>
<text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{cpu_usage:.0}%</text>
</g>
<g transform="translate(0, 20)">
<text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">MEM</text>
<rect x="40" y="-8" width="100" height="10" rx="2" fill="#1e293b"/>
<rect x="40" y="-8" width="{mem_width}" height="10" rx="2" fill="#10b981"/>
<text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{memory_percent:.0}%</text>
</g>"##,
cpu_width = cpu_usage.min(100.0),
mem_width = memory_percent.min(100.0),
))
}
(cpu_usage, memory_percent)
};
#[cfg(not(feature = "monitoring"))]
let (cpu_usage, memory_percent) = (0.0, 0.0);
Html(format!(
r##"<g>
<text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">CPU</text>
<rect x="40" y="-8" width="100" height="10" rx="2" fill="#1e293b"/>
<rect x="40" y="-8" width="{cpu_width}" height="10" rx="2" fill="#3b82f6"/>
<text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{cpu_usage:.0}%</text>
</g> <g transform="translate(0, 20)"> <text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">MEM</text> <rect x="40" y="-8" width="100" height="10" rx="2" fill="#1e293b"/> <rect x="40" y="-8" width="{mem_width}" height="10" rx="2" fill="#10b981"/> <text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{memory_percent:.0}%</text> </g>"##, cpu_width = cpu_usage.min(100.0), mem_width = memory_percent.min(100.0), )) }
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> {
let active_sessions = state
.session_manager
.try_lock()
.map(|sm| sm.active_count())
.unwrap_or(0);
let active_sessions = state
.session_manager
.try_lock()
.map(|sm| sm.active_count())
.unwrap_or(0);
Html(format!("{}", active_sessions))
Html(format!("{}", active_sessions))
}
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> {
Html("--".to_string())
Html("--".to_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> {
Html("0/hr".to_string())
Html("0/hr".to_string())
}
async fn sessions_panel(State(state): State<Arc<AppState>>) -> Html<String> {
let active_sessions = state
.session_manager
.try_lock()
.map(|sm| sm.active_count())
.unwrap_or(0);
let active_sessions = state
.session_manager
.try_lock()
.map(|sm| sm.active_count())
.unwrap_or(0);
Html(format!(
r##"<div class="sessions-panel">
<div class="panel-header">
<h3>Active Sessions</h3>
<span class="session-count">{active_sessions}</span>
Html(format!(
r##"<div class="sessions-panel">
<div class="panel-header">
<h3>Active Sessions</h3>
<span class="session-count">{active_sessions}</span>
</div>
<div class="session-list">
<div class="empty-state">
<p>No active sessions</p>
</div>
<div class="session-list">
<div class="empty-state">
<p>No active sessions</p>
</div>
</div>
</div>"##
))
}
</div>
</div>"## )) }
async fn messages_panel(State(_state): State<Arc<AppState>>) -> Html<String> {
Html(
r##"<div class="messages-panel">
<div class="panel-header">
<h3>Recent Messages</h3>
</div>
<div class="message-list">
<div class="empty-state">
<p>No recent messages</p>
</div>
</div>
</div>"##
.to_string(),
)
}
Html(
r##"<div class="messages-panel">
<div class="panel-header">
<h3>Recent Messages</h3>
</div>
<div class="message-list">
<div class="empty-state">
<p>No recent messages</p>
</div>
</div>
</div>"## .to_string(), ) }

View file

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

View file

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