Auto-commit: 20260118_195334
This commit is contained in:
parent
033bb504b9
commit
5126c648ff
15 changed files with 2625 additions and 3064 deletions
6
.cargo/config.toml
Normal file
6
.cargo/config.toml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
[build]
|
||||
rustc-wrapper = "sccache"
|
||||
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
linker = "clang"
|
||||
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
|
||||
2
.product
2
.product
|
|
@ -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,
|
||||
|
|
|
|||
126
Cargo.toml
126
Cargo.toml
|
|
@ -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
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
> "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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
62
src/main.rs
62
src/main.rs
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -51,140 +51,120 @@ async fn dashboard(State(state): State<Arc<AppState>>) -> Html<String> {
|
|||
|
||||
let uptime = System::uptime();
|
||||
let uptime_str = format_uptime(uptime);
|
||||
|
||||
(cpu_usage, total_memory, used_memory, memory_percent, uptime_str)
|
||||
};
|
||||
|
||||
let active_sessions = state
|
||||
.session_manager
|
||||
.try_lock()
|
||||
.map(|sm| sm.active_count())
|
||||
.unwrap_or(0);
|
||||
#[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()
|
||||
);
|
||||
|
||||
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>
|
||||
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();
|
||||
|
|
@ -477,97 +430,82 @@ async fn resources_bars(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
(cpu_usage, memory_percent)
|
||||
};
|
||||
|
||||
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),
|
||||
))
|
||||
}
|
||||
#[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(), ) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(), ) }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue