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,
|
||||
|
|
|
|||
124
Cargo.toml
124
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
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
//! Supports multiple notification channels: email, webhook, in-app, SMS.
|
||||
|
||||
use crate::billing::UsageMetric;
|
||||
use crate::core::shared::state::BillingAlertNotification;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -57,10 +58,11 @@ impl AlertThresholds {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Alert severity levels
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum AlertSeverity {
|
||||
Warning,
|
||||
|
|
@ -92,6 +94,7 @@ impl AlertSeverity {
|
|||
Self::Exceeded => 3,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AlertSeverity {
|
||||
|
|
@ -133,13 +136,14 @@ impl UsageAlert {
|
|||
percentage: f64,
|
||||
threshold: f64,
|
||||
) -> Self {
|
||||
let severity_clone = severity.clone();
|
||||
let message = Self::generate_message(metric, severity, percentage, current_usage, limit);
|
||||
|
||||
Self {
|
||||
id: Uuid::new_v4(),
|
||||
organization_id,
|
||||
metric,
|
||||
severity,
|
||||
severity: severity_clone,
|
||||
current_usage,
|
||||
limit,
|
||||
percentage,
|
||||
|
|
@ -151,6 +155,7 @@ impl UsageAlert {
|
|||
notification_sent: false,
|
||||
notification_channels: Vec::new(),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn generate_message(
|
||||
|
|
@ -161,7 +166,7 @@ impl UsageAlert {
|
|||
limit: u64,
|
||||
) -> String {
|
||||
let metric_name = metric.display_name();
|
||||
let severity_text = match severity {
|
||||
let severity_ = match severity {
|
||||
AlertSeverity::Warning => "approaching limit",
|
||||
AlertSeverity::Critical => "near limit",
|
||||
AlertSeverity::Exceeded => "exceeded limit",
|
||||
|
|
@ -171,11 +176,12 @@ impl UsageAlert {
|
|||
"{} {} usage is {} ({:.1}% - {}/{})",
|
||||
severity.emoji(),
|
||||
metric_name,
|
||||
severity_text,
|
||||
severity_,
|
||||
percentage,
|
||||
Self::format_value(metric, current),
|
||||
Self::format_value(metric, limit)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
fn format_value(metric: UsageMetric, value: u64) -> String {
|
||||
|
|
@ -198,10 +204,11 @@ impl UsageAlert {
|
|||
self.notification_sent = true;
|
||||
self.notification_channels = channels;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Notification delivery channels
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NotificationChannel {
|
||||
Email,
|
||||
|
|
@ -318,7 +325,7 @@ impl AlertManager {
|
|||
};
|
||||
|
||||
// Check cooldown
|
||||
if self.is_in_cooldown(org_id, metric, severity).await {
|
||||
if self.is_in_cooldown(org_id, metric, severity.clone()).await {
|
||||
return None;
|
||||
}
|
||||
|
||||
|
|
@ -340,6 +347,7 @@ impl AlertManager {
|
|||
self.send_notifications(org_id, &alert).await;
|
||||
|
||||
Some(alert)
|
||||
|
||||
}
|
||||
|
||||
/// Check multiple metrics at once
|
||||
|
|
@ -357,6 +365,7 @@ impl AlertManager {
|
|||
}
|
||||
|
||||
alerts
|
||||
|
||||
}
|
||||
|
||||
/// Get active alerts for an organization
|
||||
|
|
@ -379,6 +388,7 @@ impl AlertManager {
|
|||
}
|
||||
|
||||
alerts
|
||||
|
||||
}
|
||||
|
||||
/// Acknowledge an alert
|
||||
|
|
@ -399,6 +409,7 @@ impl AlertManager {
|
|||
alert.acknowledge(user_id);
|
||||
|
||||
Ok(())
|
||||
|
||||
}
|
||||
|
||||
/// Dismiss an alert
|
||||
|
|
@ -421,6 +432,7 @@ impl AlertManager {
|
|||
self.add_to_history(org_id, alert.clone()).await;
|
||||
|
||||
Ok(alert)
|
||||
|
||||
}
|
||||
|
||||
/// Clear all alerts for an organization
|
||||
|
|
@ -453,6 +465,7 @@ impl AlertManager {
|
|||
}
|
||||
|
||||
counts
|
||||
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
|
|
@ -479,6 +492,7 @@ impl AlertManager {
|
|||
&& alert.severity == severity
|
||||
&& alert.created_at > cooldown_threshold
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
async fn store_alert(&self, org_id: Uuid, alert: UsageAlert) {
|
||||
|
|
@ -491,6 +505,7 @@ impl AlertManager {
|
|||
});
|
||||
|
||||
org_alerts.push(alert);
|
||||
|
||||
}
|
||||
|
||||
async fn add_to_history(&self, org_id: Uuid, alert: UsageAlert) {
|
||||
|
|
@ -503,6 +518,7 @@ impl AlertManager {
|
|||
if org_history.len() > self.max_history_per_org {
|
||||
org_history.truncate(self.max_history_per_org);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async fn send_notifications(&self, org_id: Uuid, alert: &UsageAlert) {
|
||||
|
|
@ -513,7 +529,7 @@ impl AlertManager {
|
|||
}
|
||||
|
||||
// Check if this severity should be notified
|
||||
if !prefs.should_notify(alert.severity) {
|
||||
if !prefs.should_notify(alert.severity.clone()) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -532,7 +548,9 @@ impl AlertManager {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Default for AlertManager {
|
||||
|
|
@ -591,6 +609,7 @@ impl NotificationPreferences {
|
|||
false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Quiet hours configuration
|
||||
|
|
@ -599,7 +618,7 @@ pub struct QuietHours {
|
|||
pub start_hour: u8,
|
||||
pub end_hour: u8,
|
||||
pub timezone: String,
|
||||
pub days: Vec<chrono::Weekday>,
|
||||
pub days: Vecchrono::Weekday
|
||||
}
|
||||
|
||||
impl QuietHours {
|
||||
|
|
@ -614,7 +633,9 @@ impl QuietHours {
|
|||
// Overnight quiet hours
|
||||
hour >= self.start_hour || hour < self.end_hour
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Per-metric notification override
|
||||
|
|
@ -702,7 +723,7 @@ impl AlertNotification {
|
|||
Self {
|
||||
alert_id: alert.id,
|
||||
organization_id: alert.organization_id,
|
||||
severity: alert.severity,
|
||||
severity: alert.severity.clone(),
|
||||
title: format!(
|
||||
"{} Usage Alert: {}",
|
||||
alert.severity.emoji(),
|
||||
|
|
@ -725,12 +746,14 @@ impl AlertNotification {
|
|||
// ============================================================================
|
||||
|
||||
/// Email notification handler
|
||||
#[cfg(feature = "mail")]
|
||||
pub struct EmailNotificationHandler {
|
||||
_smtp_host: String,
|
||||
_smtp_port: u16,
|
||||
_from_address: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "mail")]
|
||||
impl EmailNotificationHandler {
|
||||
pub fn new(smtp_host: String, smtp_port: u16, from_address: String) -> Self {
|
||||
Self {
|
||||
|
|
@ -741,6 +764,7 @@ impl EmailNotificationHandler {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mail")]
|
||||
#[async_trait::async_trait]
|
||||
impl NotificationHandler for EmailNotificationHandler {
|
||||
fn channel(&self) -> NotificationChannel {
|
||||
|
|
@ -748,63 +772,15 @@ impl NotificationHandler for EmailNotificationHandler {
|
|||
}
|
||||
|
||||
async fn send(&self, notification: &AlertNotification) -> Result<(), NotificationError> {
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
|
||||
tracing::info!(
|
||||
"Sending email notification for alert {} to {:?}",
|
||||
notification.alert_id,
|
||||
notification.recipients
|
||||
// Email functionality is only available when the mail feature is enabled
|
||||
// This stub implementation prevents compilation errors when mail feature is disabled
|
||||
tracing::warn!(
|
||||
"Email notifications require the 'mail' feature to be enabled. Alert {} not sent.",
|
||||
notification.alert_id
|
||||
);
|
||||
|
||||
// Get SMTP config from environment
|
||||
let smtp_host = std::env::var("SMTP_HOST").unwrap_or_else(|_| "localhost".to_string());
|
||||
let smtp_user = std::env::var("SMTP_USER").ok();
|
||||
let smtp_pass = std::env::var("SMTP_PASS").ok();
|
||||
let from_email = std::env::var("SMTP_FROM").unwrap_or_else(|_| "alerts@generalbots.com".to_string());
|
||||
|
||||
let subject = format!("[{}] Billing Alert: {}",
|
||||
notification.severity.to_string().to_uppercase(),
|
||||
notification.title
|
||||
);
|
||||
|
||||
let body = format!(
|
||||
"Alert: {}\nSeverity: {}\nOrganization: {}\nTime: {}\n\nMessage: {}\n\nThreshold: {:?}\nCurrent Value: {:?}",
|
||||
notification.title,
|
||||
notification.severity,
|
||||
notification.organization_id,
|
||||
notification.created_at,
|
||||
notification.message,
|
||||
notification.limit,
|
||||
notification.current_usage
|
||||
);
|
||||
|
||||
for recipient in ¬ification.recipients {
|
||||
let email = Message::builder()
|
||||
.from(from_email.parse().map_err(|e| NotificationError::DeliveryFailed(format!("Invalid from address: {}", e)))?)
|
||||
.to(recipient.parse().map_err(|e| NotificationError::DeliveryFailed(format!("Invalid recipient {}: {}", recipient, e)))?)
|
||||
.subject(&subject)
|
||||
.body(body.clone())
|
||||
.map_err(|e| NotificationError::DeliveryFailed(format!("Failed to build email: {}", e)))?;
|
||||
|
||||
let mailer = if let (Some(user), Some(pass)) = (&smtp_user, &smtp_pass) {
|
||||
let creds = Credentials::new(user.clone(), pass.clone());
|
||||
SmtpTransport::relay(&smtp_host)
|
||||
.map_err(|e| NotificationError::DeliveryFailed(format!("SMTP relay error: {}", e)))?
|
||||
.credentials(creds)
|
||||
.build()
|
||||
} else {
|
||||
SmtpTransport::builder_dangerous(&smtp_host).build()
|
||||
};
|
||||
|
||||
mailer.send(&email)
|
||||
.map_err(|e| NotificationError::DeliveryFailed(format!("Failed to send to {}: {}", recipient, e)))?;
|
||||
|
||||
tracing::debug!("Email sent to {}", recipient);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Webhook notification handler
|
||||
|
|
@ -834,7 +810,7 @@ impl NotificationHandler for WebhookNotificationHandler {
|
|||
notification.alert_id
|
||||
);
|
||||
|
||||
// Get webhook URL from context or environment
|
||||
// Get webhook URL from con or environment
|
||||
let webhook_url = std::env::var("BILLING_WEBHOOK_URL").ok();
|
||||
|
||||
let url = match webhook_url {
|
||||
|
|
@ -878,13 +854,15 @@ impl NotificationHandler for WebhookNotificationHandler {
|
|||
|
||||
tracing::debug!("Webhook notification sent successfully to {}", url);
|
||||
Ok(())
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// In-app notification handler
|
||||
pub struct InAppNotificationHandler {
|
||||
/// Broadcast channel for WebSocket notifications
|
||||
broadcast: Option<tokio::sync::broadcast::Sender<crate::core::shared::state::BillingAlertNotification>>,
|
||||
broadcast: Option<tokio::sync::broadcast::Sender<BillingAlertNotification>>,
|
||||
}
|
||||
|
||||
impl InAppNotificationHandler {
|
||||
|
|
@ -894,12 +872,13 @@ impl InAppNotificationHandler {
|
|||
|
||||
/// Create with a broadcast channel for WebSocket notifications
|
||||
pub fn with_broadcast(
|
||||
broadcast: tokio::sync::broadcast::Sender<crate::core::shared::state::BillingAlertNotification>,
|
||||
broadcast: tokio::sync::broadcast::Sender<BillingAlertNotification>,
|
||||
) -> Self {
|
||||
Self {
|
||||
broadcast: Some(broadcast),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
impl Default for InAppNotificationHandler {
|
||||
|
|
@ -967,199 +946,9 @@ impl NotificationHandler for InAppNotificationHandler {
|
|||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Slack notification handler
|
||||
pub struct SlackNotificationHandler {}
|
||||
|
||||
impl SlackNotificationHandler {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn build_slack_message(&self, notification: &AlertNotification) -> serde_json::Value {
|
||||
let color = match notification.severity {
|
||||
AlertSeverity::Warning => "#FFA500",
|
||||
AlertSeverity::Critical => "#FF0000",
|
||||
AlertSeverity::Exceeded => "#8B0000",
|
||||
};
|
||||
|
||||
serde_json::json!({
|
||||
"attachments": [{
|
||||
"color": color,
|
||||
"title": notification.title,
|
||||
"text": notification.message,
|
||||
"fields": [
|
||||
{
|
||||
"title": "Metric",
|
||||
"value": notification.metric,
|
||||
"short": true
|
||||
},
|
||||
{
|
||||
"title": "Usage",
|
||||
"value": format!("{:.1}%", notification.percentage),
|
||||
"short": true
|
||||
}
|
||||
],
|
||||
"actions": [{
|
||||
"type": "button",
|
||||
"text": "View Usage",
|
||||
"url": notification.action_url
|
||||
}],
|
||||
"ts": notification.created_at.timestamp()
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SlackNotificationHandler {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NotificationHandler for SlackNotificationHandler {
|
||||
fn channel(&self) -> NotificationChannel {
|
||||
NotificationChannel::Slack
|
||||
}
|
||||
|
||||
async fn send(&self, notification: &AlertNotification) -> Result<(), NotificationError> {
|
||||
tracing::info!(
|
||||
"Sending Slack notification for alert {}",
|
||||
notification.alert_id
|
||||
);
|
||||
|
||||
// Get Slack webhook URL from context or environment
|
||||
let webhook_url = std::env::var("SLACK_WEBHOOK_URL").ok();
|
||||
|
||||
let url = match webhook_url {
|
||||
Some(url) => url,
|
||||
None => {
|
||||
tracing::warn!("No Slack webhook URL configured for alert {}", notification.alert_id);
|
||||
return Ok(()); // Silent skip if not configured
|
||||
}
|
||||
};
|
||||
|
||||
let message = self.build_slack_message(notification);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&message)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| NotificationError::DeliveryFailed(format!("Slack request failed: {}", e)))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(NotificationError::DeliveryFailed(
|
||||
format!("Slack webhook returned {}: {}", status, body)
|
||||
));
|
||||
}
|
||||
|
||||
tracing::debug!("Slack notification sent successfully");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Microsoft Teams notification handler
|
||||
pub struct TeamsNotificationHandler {}
|
||||
|
||||
impl TeamsNotificationHandler {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn build_teams_message(&self, notification: &AlertNotification) -> serde_json::Value {
|
||||
let theme_color = match notification.severity {
|
||||
AlertSeverity::Warning => "FFA500",
|
||||
AlertSeverity::Critical => "FF0000",
|
||||
AlertSeverity::Exceeded => "8B0000",
|
||||
};
|
||||
|
||||
serde_json::json!({
|
||||
"@type": "MessageCard",
|
||||
"@context": "http://schema.org/extensions",
|
||||
"themeColor": theme_color,
|
||||
"summary": notification.title,
|
||||
"sections": [{
|
||||
"activityTitle": notification.title,
|
||||
"facts": [
|
||||
{ "name": "Metric", "value": notification.metric },
|
||||
{ "name": "Current Usage", "value": format!("{:.1}%", notification.percentage) },
|
||||
{ "name": "Severity", "value": notification.severity.as_str() }
|
||||
],
|
||||
"text": notification.message
|
||||
}],
|
||||
"potentialAction": [{
|
||||
"@type": "OpenUri",
|
||||
"name": "View Usage",
|
||||
"targets": [{
|
||||
"os": "default",
|
||||
"uri": notification.action_url
|
||||
}]
|
||||
}]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TeamsNotificationHandler {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl NotificationHandler for TeamsNotificationHandler {
|
||||
fn channel(&self) -> NotificationChannel {
|
||||
NotificationChannel::MsTeams
|
||||
}
|
||||
|
||||
async fn send(&self, notification: &AlertNotification) -> Result<(), NotificationError> {
|
||||
tracing::info!(
|
||||
"Sending Teams notification for alert {}",
|
||||
notification.alert_id
|
||||
);
|
||||
|
||||
// Get Teams webhook URL from context or environment
|
||||
let webhook_url = std::env::var("TEAMS_WEBHOOK_URL").ok();
|
||||
|
||||
let url = match webhook_url {
|
||||
Some(url) => url,
|
||||
None => {
|
||||
tracing::warn!("No Teams webhook URL configured for alert {}", notification.alert_id);
|
||||
return Ok(()); // Silent skip if not configured
|
||||
}
|
||||
};
|
||||
|
||||
let message = self.build_teams_message(notification);
|
||||
|
||||
let client = reqwest::Client::new();
|
||||
let response = client
|
||||
.post(&url)
|
||||
.header("Content-Type", "application/json")
|
||||
.json(&message)
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| NotificationError::DeliveryFailed(format!("Teams request failed: {}", e)))?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
return Err(NotificationError::DeliveryFailed(
|
||||
format!("Teams webhook returned {}: {}", status, body)
|
||||
));
|
||||
}
|
||||
|
||||
tracing::debug!("Teams notification sent successfully");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -1232,6 +1021,7 @@ fn format_bytes(bytes: u64) -> String {
|
|||
} else {
|
||||
format!("{} bytes", bytes)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Format number with thousands separators
|
||||
|
|
@ -1266,225 +1056,6 @@ impl UsageMetric {
|
|||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Grace Period Support
|
||||
// ============================================================================
|
||||
|
||||
/// Grace period configuration for quota overages
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GracePeriodConfig {
|
||||
/// Whether grace period is enabled
|
||||
pub enabled: bool,
|
||||
/// Grace period duration in hours
|
||||
pub duration_hours: u32,
|
||||
/// Maximum overage percentage allowed during grace period
|
||||
pub max_overage_percent: f64,
|
||||
/// Metrics that support grace period
|
||||
pub applicable_metrics: Vec<UsageMetric>,
|
||||
}
|
||||
|
||||
impl Default for GracePeriodConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
duration_hours: 24,
|
||||
max_overage_percent: 10.0,
|
||||
applicable_metrics: vec![
|
||||
UsageMetric::Messages,
|
||||
UsageMetric::ApiCalls,
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Grace period status for an organization
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GracePeriodStatus {
|
||||
pub organization_id: Uuid,
|
||||
pub metric: UsageMetric,
|
||||
pub started_at: DateTime<Utc>,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub overage_at_start: u64,
|
||||
pub current_overage: u64,
|
||||
pub max_allowed_overage: u64,
|
||||
pub is_active: bool,
|
||||
}
|
||||
|
||||
impl GracePeriodStatus {
|
||||
pub fn new(
|
||||
organization_id: Uuid,
|
||||
metric: UsageMetric,
|
||||
config: &GracePeriodConfig,
|
||||
current_usage: u64,
|
||||
limit: u64,
|
||||
) -> Self {
|
||||
let now = Utc::now();
|
||||
let overage = current_usage.saturating_sub(limit);
|
||||
let max_allowed = (limit as f64 * config.max_overage_percent / 100.0) as u64;
|
||||
|
||||
Self {
|
||||
organization_id,
|
||||
metric,
|
||||
started_at: now,
|
||||
expires_at: now + chrono::Duration::hours(config.duration_hours as i64),
|
||||
overage_at_start: overage,
|
||||
current_overage: overage,
|
||||
max_allowed_overage: max_allowed,
|
||||
is_active: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_expired(&self) -> bool {
|
||||
Utc::now() > self.expires_at
|
||||
}
|
||||
|
||||
pub fn is_within_limits(&self) -> bool {
|
||||
self.current_overage <= self.max_allowed_overage
|
||||
}
|
||||
|
||||
pub fn remaining_time(&self) -> chrono::Duration {
|
||||
self.expires_at.signed_duration_since(Utc::now())
|
||||
}
|
||||
|
||||
pub fn update_overage(&mut self, current_usage: u64, limit: u64) {
|
||||
self.current_overage = current_usage.saturating_sub(limit);
|
||||
|
||||
if self.is_expired() || !self.is_within_limits() {
|
||||
self.is_active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Grace period manager
|
||||
pub struct GracePeriodManager {
|
||||
config: GracePeriodConfig,
|
||||
active_periods: Arc<RwLock<HashMap<(Uuid, UsageMetric), GracePeriodStatus>>>,
|
||||
}
|
||||
|
||||
impl GracePeriodManager {
|
||||
pub fn new(config: GracePeriodConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
active_periods: Arc::new(RwLock::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if grace period allows the operation
|
||||
pub async fn check_grace_period(
|
||||
&self,
|
||||
org_id: Uuid,
|
||||
metric: UsageMetric,
|
||||
current_usage: u64,
|
||||
limit: u64,
|
||||
) -> GracePeriodDecision {
|
||||
if !self.config.enabled || !self.config.applicable_metrics.contains(&metric) {
|
||||
return GracePeriodDecision::NotApplicable;
|
||||
}
|
||||
|
||||
let key = (org_id, metric);
|
||||
let mut periods = self.active_periods.write().await;
|
||||
|
||||
if let Some(status) = periods.get_mut(&key) {
|
||||
status.update_overage(current_usage, limit);
|
||||
|
||||
if status.is_active && status.is_within_limits() {
|
||||
return GracePeriodDecision::InGracePeriod {
|
||||
remaining: status.remaining_time(),
|
||||
overage_used: status.current_overage,
|
||||
overage_limit: status.max_allowed_overage,
|
||||
};
|
||||
} else {
|
||||
periods.remove(&key);
|
||||
return GracePeriodDecision::GracePeriodExpired;
|
||||
}
|
||||
}
|
||||
|
||||
// Start new grace period if within overage limits
|
||||
let potential_status = GracePeriodStatus::new(
|
||||
org_id,
|
||||
metric,
|
||||
&self.config,
|
||||
current_usage,
|
||||
limit,
|
||||
);
|
||||
|
||||
if potential_status.is_within_limits() {
|
||||
let remaining = potential_status.remaining_time();
|
||||
let overage_used = potential_status.current_overage;
|
||||
let overage_limit = potential_status.max_allowed_overage;
|
||||
|
||||
periods.insert(key, potential_status);
|
||||
|
||||
GracePeriodDecision::GracePeriodStarted {
|
||||
duration_hours: self.config.duration_hours,
|
||||
remaining,
|
||||
overage_used,
|
||||
overage_limit,
|
||||
}
|
||||
} else {
|
||||
GracePeriodDecision::OverageExceedsLimit {
|
||||
current_overage: current_usage.saturating_sub(limit),
|
||||
max_allowed: potential_status.max_allowed_overage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get active grace period status
|
||||
pub async fn get_status(
|
||||
&self,
|
||||
org_id: Uuid,
|
||||
metric: UsageMetric,
|
||||
) -> Option<GracePeriodStatus> {
|
||||
let periods = self.active_periods.read().await;
|
||||
periods.get(&(org_id, metric)).cloned()
|
||||
}
|
||||
|
||||
/// End grace period early (e.g., after upgrade)
|
||||
pub async fn end_grace_period(&self, org_id: Uuid, metric: UsageMetric) {
|
||||
let mut periods = self.active_periods.write().await;
|
||||
periods.remove(&(org_id, metric));
|
||||
}
|
||||
|
||||
/// Clean up expired grace periods
|
||||
pub async fn cleanup_expired(&self) {
|
||||
let mut periods = self.active_periods.write().await;
|
||||
periods.retain(|_, status| !status.is_expired());
|
||||
}
|
||||
}
|
||||
|
||||
/// Grace period decision
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GracePeriodDecision {
|
||||
NotApplicable,
|
||||
GracePeriodStarted {
|
||||
duration_hours: u32,
|
||||
remaining: chrono::Duration,
|
||||
overage_used: u64,
|
||||
overage_limit: u64,
|
||||
},
|
||||
InGracePeriod {
|
||||
remaining: chrono::Duration,
|
||||
overage_used: u64,
|
||||
overage_limit: u64,
|
||||
},
|
||||
GracePeriodExpired,
|
||||
OverageExceedsLimit {
|
||||
current_overage: u64,
|
||||
max_allowed: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl GracePeriodDecision {
|
||||
pub fn allows_operation(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::NotApplicable
|
||||
| Self::GracePeriodStarted { .. }
|
||||
| Self::InGracePeriod { .. }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Alert Service Factory
|
||||
// ============================================================================
|
||||
|
|
@ -1505,6 +1076,7 @@ pub fn create_alert_manager(
|
|||
}
|
||||
|
||||
manager
|
||||
|
||||
}
|
||||
|
||||
/// Create default notification handlers
|
||||
|
|
@ -1515,10 +1087,4 @@ pub async fn register_default_handlers(manager: &AlertManager) {
|
|||
manager
|
||||
.register_handler(Arc::new(WebhookNotificationHandler::new()))
|
||||
.await;
|
||||
manager
|
||||
.register_handler(Arc::new(SlackNotificationHandler::new()))
|
||||
.await;
|
||||
manager
|
||||
.register_handler(Arc::new(TeamsNotificationHandler::new()))
|
||||
.await;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,13 @@ use axum::{
|
|||
use chrono::{DateTime, Utc};
|
||||
use diesel::prelude::*;
|
||||
use diesel::sql_types::{Nullable, Text, Timestamptz, Uuid as DieselUuid, Varchar};
|
||||
#[cfg(feature = "mail")]
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
#[cfg(feature = "mail")]
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use log::{info, warn};
|
||||
use log::warn;
|
||||
#[cfg(feature = "mail")]
|
||||
use log::info;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -20,6 +24,7 @@ use uuid::Uuid;
|
|||
// ============================================================================
|
||||
|
||||
/// Send invitation email via SMTP
|
||||
#[cfg(feature = "mail")]
|
||||
async fn send_invitation_email(
|
||||
to_email: &str,
|
||||
role: &str,
|
||||
|
|
@ -77,6 +82,7 @@ The General Bots Team"#,
|
|||
}
|
||||
|
||||
/// Send invitation email by fetching details from database
|
||||
#[cfg(feature = "mail")]
|
||||
async fn send_invitation_email_by_id(invitation_id: Uuid) -> Result<(), String> {
|
||||
let smtp_host = std::env::var("SMTP_HOST").unwrap_or_else(|_| "localhost".to_string());
|
||||
let smtp_user = std::env::var("SMTP_USER").ok();
|
||||
|
|
|
|||
|
|
@ -1,14 +1,8 @@
|
|||
//! Memory and CPU monitoring with thread tracking
|
||||
//!
|
||||
//! This module provides tools to track memory/CPU usage per thread
|
||||
//! and identify potential leaks or CPU hogs in the botserver application.
|
||||
//!
|
||||
//! When compiled with the `jemalloc` feature, provides detailed allocation statistics.
|
||||
|
||||
use log::{debug, info, trace, warn};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{LazyLock, Mutex, RwLock};
|
||||
use std::time::{Duration, Instant};
|
||||
#[cfg(feature = "monitoring")]
|
||||
use sysinfo::{Pid, ProcessesToUpdate, System};
|
||||
|
||||
static THREAD_REGISTRY: LazyLock<RwLock<HashMap<String, ThreadInfo>>> =
|
||||
|
|
@ -87,6 +81,7 @@ impl MemoryStats {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn format_bytes(bytes: u64) -> String {
|
||||
const KB: u64 = 1024;
|
||||
const MB: u64 = KB * 1024;
|
||||
|
|
@ -110,6 +105,7 @@ impl MemoryStats {
|
|||
Self::format_bytes(self.virtual_bytes),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Get jemalloc memory statistics when the feature is enabled
|
||||
|
|
@ -117,6 +113,7 @@ impl MemoryStats {
|
|||
pub fn get_jemalloc_stats() -> Option<JemallocStats> {
|
||||
use tikv_jemalloc_ctl::{epoch, stats};
|
||||
|
||||
|
||||
// Advance the epoch to refresh statistics
|
||||
if epoch::advance().is_err() {
|
||||
return None;
|
||||
|
|
@ -135,6 +132,7 @@ pub fn get_jemalloc_stats() -> Option<JemallocStats> {
|
|||
mapped,
|
||||
retained,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "jemalloc"))]
|
||||
|
|
@ -169,6 +167,7 @@ impl JemallocStats {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
/// Calculate fragmentation ratio (1.0 = no fragmentation)
|
||||
pub fn fragmentation_ratio(&self) -> f64 {
|
||||
if self.allocated > 0 {
|
||||
|
|
@ -177,6 +176,7 @@ impl JemallocStats {
|
|||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Log jemalloc stats if available
|
||||
|
|
@ -210,6 +210,7 @@ impl MemoryCheckpoint {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn compare_and_log(&self) {
|
||||
let current = MemoryStats::current();
|
||||
let diff = current.rss_bytes as i64 - self.stats.rss_bytes as i64;
|
||||
|
|
@ -230,6 +231,7 @@ impl MemoryCheckpoint {
|
|||
debug!("[CHECKPOINT] {} unchanged", self.name);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub struct ComponentMemoryTracker {
|
||||
|
|
@ -245,6 +247,7 @@ impl ComponentMemoryTracker {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn record(&self, component: &str) {
|
||||
let stats = MemoryStats::current();
|
||||
if let Ok(mut components) = self.components.lock() {
|
||||
|
|
@ -295,6 +298,7 @@ impl ComponentMemoryTracker {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub fn record_component(component: &str) {
|
||||
|
|
@ -322,6 +326,7 @@ impl LeakDetector {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn reset_baseline(&self) {
|
||||
let current = MemoryStats::current();
|
||||
if let Ok(mut baseline) = self.baseline.lock() {
|
||||
|
|
@ -378,11 +383,13 @@ impl LeakDetector {
|
|||
|
||||
None
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pub fn start_memory_monitor(interval_secs: u64, warn_threshold_mb: u64) {
|
||||
let detector = LeakDetector::new(warn_threshold_mb, 5);
|
||||
|
||||
|
||||
tokio::spawn(async move {
|
||||
register_thread("memory-monitor", "monitoring");
|
||||
|
||||
|
|
@ -452,14 +459,24 @@ pub fn start_memory_monitor(interval_secs: u64, warn_threshold_mb: u64) {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
#[cfg(feature = "monitoring")]
|
||||
#[cfg(feature = "monitoring")]
|
||||
pub fn get_process_memory() -> Option<(u64, u64)> {
|
||||
let pid = Pid::from_u32(std::process::id());
|
||||
let mut sys = System::new();
|
||||
sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true);
|
||||
|
||||
|
||||
sys.process(pid).map(|p| (p.memory(), p.virtual_memory()))
|
||||
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "monitoring"))]
|
||||
pub fn get_process_memory() -> Option<(u64, u64)> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn log_process_memory() {
|
||||
|
|
@ -476,6 +493,7 @@ pub fn log_process_memory() {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_memory_stats() {
|
||||
let stats = MemoryStats::current();
|
||||
|
|
@ -503,4 +521,5 @@ mod tests {
|
|||
log_thread_stats();
|
||||
unregister_thread("test-thread");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::Result;
|
||||
#[cfg(feature = "sheet")]
|
||||
use calamine::Reader;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -63,6 +64,7 @@ impl UserDriveVectorDB {
|
|||
pub fn new(user_id: Uuid, bot_id: Uuid, db_path: PathBuf) -> Self {
|
||||
let collection_name = format!("drive_{}_{}", bot_id, user_id);
|
||||
|
||||
|
||||
Self {
|
||||
user_id,
|
||||
bot_id,
|
||||
|
|
@ -541,6 +543,7 @@ impl UserDriveVectorDB {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -554,6 +557,7 @@ impl FileContentExtractor {
|
|||
Ok(content)
|
||||
}
|
||||
|
||||
|
||||
t if t.starts_with("text/") => {
|
||||
let content = fs::read_to_string(file_path).await?;
|
||||
Ok(content)
|
||||
|
|
@ -573,8 +577,16 @@ impl FileContentExtractor {
|
|||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
| "application/vnd.ms-excel" => {
|
||||
log::info!("Spreadsheet extraction for {}", file_path.display());
|
||||
#[cfg(feature = "sheet")]
|
||||
{
|
||||
Self::extract_xlsx_text(file_path).await
|
||||
}
|
||||
#[cfg(not(feature = "sheet"))]
|
||||
{
|
||||
log::warn!("XLSX extraction requires 'sheet' feature");
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
"application/json" => {
|
||||
let content = fs::read_to_string(file_path).await?;
|
||||
|
|
@ -669,6 +681,7 @@ impl FileContentExtractor {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sheet")]
|
||||
async fn extract_xlsx_text(file_path: &Path) -> Result<String> {
|
||||
let path = file_path.to_path_buf();
|
||||
|
||||
|
|
@ -738,4 +751,5 @@ impl FileContentExtractor {
|
|||
| "text/x-java"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
42
src/main.rs
42
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,10 +452,22 @@ async fn run_axum_server(
|
|||
|
||||
api_router = api_router.merge(crate::analytics::configure_analytics_routes());
|
||||
api_router = api_router.merge(crate::core::i18n::configure_i18n_routes());
|
||||
#[cfg(feature = "docs")]
|
||||
{
|
||||
api_router = api_router.merge(crate::docs::configure_docs_routes());
|
||||
}
|
||||
#[cfg(feature = "paper")]
|
||||
{
|
||||
api_router = api_router.merge(crate::paper::configure_paper_routes());
|
||||
}
|
||||
#[cfg(feature = "sheet")]
|
||||
{
|
||||
api_router = api_router.merge(crate::sheet::configure_sheet_routes());
|
||||
}
|
||||
#[cfg(feature = "slides")]
|
||||
{
|
||||
api_router = api_router.merge(crate::slides::configure_slides_routes());
|
||||
}
|
||||
api_router = api_router.merge(crate::video::configure_video_routes());
|
||||
api_router = api_router.merge(crate::video::ui::configure_video_ui_routes());
|
||||
api_router = api_router.merge(crate::research::configure_research_routes());
|
||||
|
|
@ -486,9 +502,15 @@ async fn run_axum_server(
|
|||
api_router = api_router.merge(crate::canvas::ui::configure_canvas_ui_routes());
|
||||
api_router = api_router.merge(crate::social::configure_social_routes());
|
||||
api_router = api_router.merge(crate::social::ui::configure_social_ui_routes());
|
||||
api_router = api_router.merge(crate::email::ui::configure_email_ui_routes());
|
||||
api_router = api_router.merge(crate::learn::ui::configure_learn_ui_routes());
|
||||
#[cfg(feature = "email")]
|
||||
{
|
||||
api_router = api_router.merge(crate::email::ui::configure_email_ui_routes());
|
||||
}
|
||||
#[cfg(feature = "meet")]
|
||||
{
|
||||
api_router = api_router.merge(crate::meet::ui::configure_meet_ui_routes());
|
||||
}
|
||||
api_router = api_router.merge(crate::contacts::crm_ui::configure_crm_routes());
|
||||
api_router = api_router.merge(crate::contacts::crm::configure_crm_api_routes());
|
||||
api_router = api_router.merge(crate::billing::billing_ui::configure_billing_routes());
|
||||
|
|
@ -1060,7 +1082,7 @@ use crate::core::config::ConfigManager;
|
|||
|
||||
info!("Loaded Zitadel config from {}: url={}", config_path, base_url);
|
||||
|
||||
crate::directory::client::ZitadelConfig {
|
||||
crate::core::directory::client::ZitadelConfig {
|
||||
issuer_url: base_url.to_string(),
|
||||
issuer: base_url.to_string(),
|
||||
client_id: client_id.to_string(),
|
||||
|
|
@ -1072,7 +1094,7 @@ use crate::core::config::ConfigManager;
|
|||
}
|
||||
} else {
|
||||
warn!("Failed to parse directory_config.json, using defaults");
|
||||
crate::directory::client::ZitadelConfig {
|
||||
crate::core::directory::client::ZitadelConfig {
|
||||
issuer_url: "http://localhost:8300".to_string(),
|
||||
issuer: "http://localhost:8300".to_string(),
|
||||
client_id: String::new(),
|
||||
|
|
@ -1085,7 +1107,7 @@ use crate::core::config::ConfigManager;
|
|||
}
|
||||
} else {
|
||||
warn!("directory_config.json not found, using default Zitadel config");
|
||||
crate::directory::client::ZitadelConfig {
|
||||
crate::core::directory::client::ZitadelConfig {
|
||||
issuer_url: "http://localhost:8300".to_string(),
|
||||
issuer: "http://localhost:8300".to_string(),
|
||||
client_id: String::new(),
|
||||
|
|
@ -1099,7 +1121,7 @@ use crate::core::config::ConfigManager;
|
|||
};
|
||||
#[cfg(feature = "directory")]
|
||||
let auth_service = Arc::new(tokio::sync::Mutex::new(
|
||||
crate::directory::AuthService::new(zitadel_config.clone()).map_err(|e| std::io::Error::other(format!("Failed to create auth service: {}", e)))?,
|
||||
crate::core::directory::AuthService::new(zitadel_config.clone()).map_err(|e| std::io::Error::other(format!("Failed to create auth service: {}", e)))?,
|
||||
));
|
||||
|
||||
#[cfg(feature = "directory")]
|
||||
|
|
@ -1110,22 +1132,22 @@ use crate::core::config::ConfigManager;
|
|||
Ok(pat_token) => {
|
||||
let pat_token = pat_token.trim().to_string();
|
||||
info!("Using admin PAT token for bootstrap authentication");
|
||||
crate::directory::client::ZitadelClient::with_pat_token(zitadel_config, pat_token)
|
||||
crate::core::directory::client::ZitadelClient::with_pat_token(zitadel_config, pat_token)
|
||||
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client with PAT: {}", e)))?
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to read admin PAT token: {}, falling back to OAuth2", e);
|
||||
crate::directory::client::ZitadelClient::new(zitadel_config)
|
||||
crate::core::directory::client::ZitadelClient::new(zitadel_config)
|
||||
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client: {}", e)))?
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("Admin PAT not found, using OAuth2 client credentials for bootstrap");
|
||||
crate::directory::client::ZitadelClient::new(zitadel_config)
|
||||
crate::core::directory::client::ZitadelClient::new(zitadel_config)
|
||||
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client: {}", e)))?
|
||||
};
|
||||
|
||||
match crate::directory::bootstrap::check_and_bootstrap_admin(&bootstrap_client).await {
|
||||
match crate::core::directory::bootstrap::check_and_bootstrap_admin(&bootstrap_client).await {
|
||||
Ok(Some(_)) => {
|
||||
info!("Bootstrap completed - admin credentials displayed in console");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
use axum::{extract::State, response::Html, routing::get, Router};
|
||||
use chrono::Local;
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "monitoring")]
|
||||
use sysinfo::{Disks, Networks, System};
|
||||
|
||||
use crate::core::urls::ApiUrls;
|
||||
|
|
@ -10,7 +10,6 @@ use crate::shared::state::AppState;
|
|||
pub mod real_time;
|
||||
pub mod tracing;
|
||||
|
||||
|
||||
pub fn configure() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route(ApiUrls::MONITORING_DASHBOARD, get(dashboard))
|
||||
|
|
@ -35,8 +34,9 @@ pub fn configure() -> Router<Arc<AppState>> {
|
|||
.route("/api/ui/monitoring/messages", get(messages_panel))
|
||||
}
|
||||
|
||||
|
||||
async fn dashboard(State(state): State<Arc<AppState>>) -> Html<String> {
|
||||
#[cfg(feature = "monitoring")]
|
||||
let (cpu_usage, total_memory, used_memory, memory_percent, uptime_str) = {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
|
|
@ -52,6 +52,14 @@ async fn dashboard(State(state): State<Arc<AppState>>) -> Html<String> {
|
|||
let uptime = System::uptime();
|
||||
let uptime_str = format_uptime(uptime);
|
||||
|
||||
(cpu_usage, total_memory, used_memory, memory_percent, uptime_str)
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "monitoring"))]
|
||||
let (cpu_usage, total_memory, used_memory, memory_percent, uptime_str) = (
|
||||
0.0, 0, 0, 0.0, "N/A".to_string()
|
||||
);
|
||||
|
||||
let active_sessions = state
|
||||
.session_manager
|
||||
.try_lock()
|
||||
|
|
@ -97,30 +105,7 @@ async fn dashboard(State(state): State<Arc<AppState>>) -> Html<String> {
|
|||
<div class="metric-value">{uptime_str}</div>
|
||||
<div class="metric-subtitle">System running time</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="refresh-indicator" hx-get="/api/monitoring/dashboard" hx-trigger="every 10s" hx-swap="outerHTML" hx-target="closest .dashboard-grid, .refresh-indicator">
|
||||
<span class="refresh-dot"></span> Auto-refreshing
|
||||
</div>"##,
|
||||
cpu_status = if cpu_usage > 80.0 {
|
||||
"danger"
|
||||
} else if cpu_usage > 60.0 {
|
||||
"warning"
|
||||
} else {
|
||||
"success"
|
||||
},
|
||||
mem_status = if memory_percent > 80.0 {
|
||||
"danger"
|
||||
} else if memory_percent > 60.0 {
|
||||
"warning"
|
||||
} else {
|
||||
"success"
|
||||
},
|
||||
used_gb = used_memory as f64 / 1_073_741_824.0,
|
||||
total_gb = total_memory as f64 / 1_073_741_824.0,
|
||||
))
|
||||
}
|
||||
|
||||
</div><div class="refresh-indicator" hx-get="/api/monitoring/dashboard" hx-trigger="every 10s" hx-swap="outerHTML" hx-target="closest .dashboard-grid, .refresh-indicator"> <span class="refresh-dot"></span> Auto-refreshing </div>"##, cpu_status = if cpu_usage > 80.0 { "danger" } else if cpu_usage > 60.0 { "warning" } else { "success" }, mem_status = if memory_percent > 80.0 { "danger" } else if memory_percent > 60.0 { "warning" } else { "success" }, used_gb = used_memory as f64 / 1_073_741_824.0, total_gb = total_memory as f64 / 1_073_741_824.0, )) }
|
||||
|
||||
async fn services(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
let services = vec![
|
||||
|
|
@ -151,11 +136,7 @@ async fn services(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|||
<td>
|
||||
<button class="btn-sm" hx-post="/api/monitoring/services/{name_lower}/restart" hx-swap="none">Restart</button>
|
||||
</td>
|
||||
</tr>"##,
|
||||
name_lower = name.to_lowercase().replace(' ', "-"),
|
||||
));
|
||||
}
|
||||
|
||||
</tr>"##, name_lower = name.to_lowercase().replace(' ', "-"), )); }
|
||||
Html(format!(
|
||||
r##"<div class="services-view">
|
||||
<div class="section-header">
|
||||
|
|
@ -177,12 +158,11 @@ async fn services(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|||
{rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>"##
|
||||
))
|
||||
}
|
||||
|
||||
</div>"## )) }
|
||||
|
||||
async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
#[cfg(feature = "monitoring")]
|
||||
let (disk_rows, net_rows) = {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
|
|
@ -210,20 +190,7 @@ async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|||
</div>
|
||||
<span class="usage-text">{percent:.1}%</span>
|
||||
</td>
|
||||
</tr>"##,
|
||||
mount = disk.mount_point().display(),
|
||||
used_gb = used as f64 / 1_073_741_824.0,
|
||||
total_gb = total as f64 / 1_073_741_824.0,
|
||||
status = if percent > 90.0 {
|
||||
"danger"
|
||||
} else if percent > 70.0 {
|
||||
"warning"
|
||||
} else {
|
||||
"success"
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
</tr>"##, mount = disk.mount_point().display(), used_gb = used as f64 / 1_073_741_824.0, total_gb = total as f64 / 1_073_741_824.0, status = if percent > 90.0 { "danger" } else if percent > 70.0 { "warning" } else { "success" }, )); }
|
||||
let networks = Networks::new_with_refreshed_list();
|
||||
let mut net_rows = String::new();
|
||||
|
||||
|
|
@ -233,11 +200,15 @@ async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|||
<td>{name}</td>
|
||||
<td>{rx:.2} MB</td>
|
||||
<td>{tx:.2} MB</td>
|
||||
</tr>"##,
|
||||
rx = data.total_received() as f64 / 1_048_576.0,
|
||||
tx = data.total_transmitted() as f64 / 1_048_576.0,
|
||||
));
|
||||
}
|
||||
</tr>"##, rx = data.total_received() as f64 / 1_048_576.0, tx = data.total_transmitted() as f64 / 1_048_576.0, )); }
|
||||
(disk_rows, net_rows)
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "monitoring"))]
|
||||
let (disk_rows, net_rows) = (
|
||||
String::new(),
|
||||
String::new()
|
||||
);
|
||||
|
||||
Html(format!(
|
||||
r##"<div class="resources-view">
|
||||
|
|
@ -277,10 +248,7 @@ async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>"##
|
||||
))
|
||||
}
|
||||
|
||||
</div>"## )) }
|
||||
|
||||
async fn logs(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html(
|
||||
|
|
@ -308,11 +276,8 @@ async fn logs(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|||
<span class="log-message">Monitoring initialized</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>"##
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
</div>"## .to_string(), ) }
|
||||
|
||||
async fn llm_metrics(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html(
|
||||
|
|
@ -362,11 +327,7 @@ async fn llm_metrics(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>"##
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
</div>"## .to_string(), ) }
|
||||
|
||||
async fn health(State(state): State<Arc<AppState>>) -> Html<String> {
|
||||
let db_ok = state.conn.get().is_ok();
|
||||
|
|
@ -376,10 +337,8 @@ async fn health(State(state): State<Arc<AppState>>) -> Html<String> {
|
|||
r##"<div class="health-status {status}">
|
||||
<span class="status-icon"></span>
|
||||
<span class="status-text">{status}</span>
|
||||
</div>"##
|
||||
))
|
||||
}
|
||||
|
||||
</iv>"## )) }
|
||||
|
||||
fn format_uptime(seconds: u64) -> String {
|
||||
let days = seconds / 86400;
|
||||
|
|
@ -393,35 +352,30 @@ fn format_uptime(seconds: u64) -> String {
|
|||
} else {
|
||||
format!("{}m", minutes)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn check_postgres() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
fn check_redis() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
fn check_minio() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
fn check_llm() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
async fn timestamp(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
let now = Local::now();
|
||||
Html(format!("Last updated: {}", now.format("%H:%M:%S")))
|
||||
}
|
||||
|
||||
|
||||
async fn bots(State(state): State<Arc<AppState>>) -> Html<String> {
|
||||
let active_sessions = state
|
||||
.session_manager
|
||||
|
|
@ -435,10 +389,7 @@ async fn bots(State(state): State<Arc<AppState>>) -> Html<String> {
|
|||
<span class="bot-name">Active Sessions</span>
|
||||
<span class="bot-count">{active_sessions}</span>
|
||||
</div>
|
||||
</div>"##
|
||||
))
|
||||
}
|
||||
|
||||
</div>"## )) }
|
||||
|
||||
async fn services_status(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
let services = vec![
|
||||
|
|
@ -462,10 +413,12 @@ async fn services_status(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|||
}
|
||||
|
||||
Html(status_updates)
|
||||
|
||||
}
|
||||
|
||||
|
||||
async fn resources_bars(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
#[cfg(feature = "monitoring")]
|
||||
let (cpu_usage, memory_percent) = {
|
||||
let mut sys = System::new_all();
|
||||
sys.refresh_all();
|
||||
|
||||
|
|
@ -478,30 +431,24 @@ async fn resources_bars(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|||
0.0
|
||||
};
|
||||
|
||||
(cpu_usage, memory_percent)
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "monitoring"))]
|
||||
let (cpu_usage, memory_percent) = (0.0, 0.0);
|
||||
|
||||
Html(format!(
|
||||
r##"<g>
|
||||
<text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">CPU</text>
|
||||
<rect x="40" y="-8" width="100" height="10" rx="2" fill="#1e293b"/>
|
||||
<rect x="40" y="-8" width="{cpu_width}" height="10" rx="2" fill="#3b82f6"/>
|
||||
<text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{cpu_usage:.0}%</text>
|
||||
</g>
|
||||
<g transform="translate(0, 20)">
|
||||
<text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">MEM</text>
|
||||
<rect x="40" y="-8" width="100" height="10" rx="2" fill="#1e293b"/>
|
||||
<rect x="40" y="-8" width="{mem_width}" height="10" rx="2" fill="#10b981"/>
|
||||
<text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{memory_percent:.0}%</text>
|
||||
</g>"##,
|
||||
cpu_width = cpu_usage.min(100.0),
|
||||
mem_width = memory_percent.min(100.0),
|
||||
))
|
||||
}
|
||||
|
||||
</g> <g transform="translate(0, 20)"> <text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">MEM</text> <rect x="40" y="-8" width="100" height="10" rx="2" fill="#1e293b"/> <rect x="40" y="-8" width="{mem_width}" height="10" rx="2" fill="#10b981"/> <text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{memory_percent:.0}%</text> </g>"##, cpu_width = cpu_usage.min(100.0), mem_width = memory_percent.min(100.0), )) }
|
||||
|
||||
async fn activity_latest(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html("System monitoring active...".to_string())
|
||||
}
|
||||
|
||||
|
||||
async fn metric_sessions(State(state): State<Arc<AppState>>) -> Html<String> {
|
||||
let active_sessions = state
|
||||
.session_manager
|
||||
|
|
@ -510,29 +457,25 @@ async fn metric_sessions(State(state): State<Arc<AppState>>) -> Html<String> {
|
|||
.unwrap_or(0);
|
||||
|
||||
Html(format!("{}", active_sessions))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async fn metric_messages(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html("--".to_string())
|
||||
}
|
||||
|
||||
|
||||
async fn metric_response_time(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html("--".to_string())
|
||||
}
|
||||
|
||||
|
||||
async fn trend_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html("↑ 0%".to_string())
|
||||
}
|
||||
|
||||
|
||||
async fn rate_messages(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html("0/hr".to_string())
|
||||
}
|
||||
|
||||
|
||||
async fn sessions_panel(State(state): State<Arc<AppState>>) -> Html<String> {
|
||||
let active_sessions = state
|
||||
.session_manager
|
||||
|
|
@ -551,10 +494,7 @@ async fn sessions_panel(State(state): State<Arc<AppState>>) -> Html<String> {
|
|||
<p>No active sessions</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>"##
|
||||
))
|
||||
}
|
||||
|
||||
</div>"## )) }
|
||||
|
||||
async fn messages_panel(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html(
|
||||
|
|
@ -567,7 +507,5 @@ async fn messages_panel(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|||
<p>No recent messages</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>"##
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
</div>"## .to_string(), ) }
|
||||
|
|
|
|||
|
|
@ -833,6 +833,7 @@ fn validate_session_sync(session_id: &str) -> Result<AuthenticatedUser, AuthErro
|
|||
&session_id[..std::cmp::min(20, session_id.len())]);
|
||||
|
||||
// Try to get user data from session cache first
|
||||
#[cfg(feature = "directory")]
|
||||
if let Ok(cache_guard) = crate::directory::auth_routes::SESSION_CACHE.try_read() {
|
||||
if let Some(user_data) = cache_guard.get(session_id) {
|
||||
debug!("Found user in session cache: {}", user_data.email);
|
||||
|
|
|
|||
|
|
@ -47,8 +47,8 @@ async fn get_accounts_social(State(_state): State<Arc<AppState>>) -> Html<String
|
|||
<div class="account-item"><span class="account-icon">📘</span><span class="account-name">Facebook</span><span class="account-status disconnected">Not connected</span></div>
|
||||
<div class="account-item"><span class="account-icon">🐦</span><span class="account-name">Twitter/X</span><span class="account-status disconnected">Not connected</span></div>
|
||||
<div class="account-item"><span class="account-icon">💼</span><span class="account-name">LinkedIn</span><span class="account-status disconnected">Not connected</span></div>
|
||||
</div>"##.to_string())
|
||||
}
|
||||
|
||||
</div>"##.to_string()) }
|
||||
|
||||
async fn get_accounts_messaging(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html(r##"<div class="accounts-list">
|
||||
|
|
@ -56,16 +56,16 @@ async fn get_accounts_messaging(State(_state): State<Arc<AppState>>) -> Html<Str
|
|||
<div class="account-item"><span class="account-icon">📱</span><span class="account-name">WhatsApp</span><span class="account-status disconnected">Not connected</span></div>
|
||||
<div class="account-item"><span class="account-icon">✈️</span><span class="account-name">Telegram</span><span class="account-status disconnected">Not connected</span></div>
|
||||
<div class="account-item"><span class="account-icon">💼</span><span class="account-name">Teams</span><span class="account-status disconnected">Not connected</span></div>
|
||||
</div>"##.to_string())
|
||||
}
|
||||
|
||||
</div>"##.to_string()) }
|
||||
|
||||
async fn get_accounts_email(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html(r##"<div class="accounts-list">
|
||||
<div class="account-item"><span class="account-icon">📧</span><span class="account-name">Gmail</span><span class="account-status disconnected">Not connected</span></div>
|
||||
<div class="account-item"><span class="account-icon">📨</span><span class="account-name">Outlook</span><span class="account-status disconnected">Not connected</span></div>
|
||||
<div class="account-item"><span class="account-icon">⚙️</span><span class="account-name">SMTP</span><span class="account-status disconnected">Not configured</span></div>
|
||||
</div>"##.to_string())
|
||||
}
|
||||
|
||||
</div>"##.to_string()) }
|
||||
|
||||
async fn save_smtp_account(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
|
|
@ -132,10 +132,8 @@ async fn get_storage_info(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|||
<span class="storage-size">500 MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>"##
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
s
|
||||
</div>"## .to_string(), ) }
|
||||
|
||||
async fn get_storage_connections(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html(
|
||||
|
|
@ -144,10 +142,8 @@ async fn get_storage_connections(State(_state): State<Arc<AppState>>) -> Html<St
|
|||
<button class="btn-secondary" onclick="showAddConnectionModal()">
|
||||
+ Add Connection
|
||||
</button>
|
||||
</div>"##
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
</div>"## .to_string(), ) }
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
|
|
@ -177,11 +173,13 @@ async fn save_search_settings(
|
|||
settings.enable_ai_suggestions
|
||||
);
|
||||
|
||||
|
||||
Json(SearchSettingsResponse {
|
||||
success: true,
|
||||
message: Some("Search settings saved successfully".to_string()),
|
||||
error: None,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
@ -201,6 +199,7 @@ struct SmtpTestResponse {
|
|||
error: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "mail")]
|
||||
async fn test_smtp_connection(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Json(config): Json<SmtpTestRequest>,
|
||||
|
|
@ -208,6 +207,8 @@ async fn test_smtp_connection(
|
|||
use lettre::SmtpTransport;
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
|
||||
|
||||
|
||||
log::info!("Testing SMTP connection to {}:{}", config.host, config.port);
|
||||
|
||||
let mailer_result = if let (Some(user), Some(pass)) = (config.username, config.password) {
|
||||
|
|
@ -246,6 +247,19 @@ async fn test_smtp_connection(
|
|||
error: Some(format!("Failed to create SMTP transport: {}", e)),
|
||||
}),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mail"))]
|
||||
async fn test_smtp_connection(
|
||||
State(_state): State<Arc<AppState>>,
|
||||
Json(_config): Json<SmtpTestRequest>,
|
||||
) -> Json<SmtpTestResponse> {
|
||||
Json(SmtpTestResponse {
|
||||
success: false,
|
||||
message: None,
|
||||
error: Some("SMTP email feature is not enabled in this build".to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_2fa_status(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
|
|
@ -253,30 +267,24 @@ async fn get_2fa_status(State(_state): State<Arc<AppState>>) -> Html<String> {
|
|||
r##"<div class="status-indicator">
|
||||
<span class="status-dot inactive"></span>
|
||||
<span class="status-text">Two-factor authentication is not enabled</span>
|
||||
</div>"##
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
</div>"## .to_string(), ) }
|
||||
|
||||
async fn enable_2fa(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html(
|
||||
r##"<div class="status-indicator">
|
||||
<span class="status-dot active"></span>
|
||||
<span class="status-text">Two-factor authentication enabled</span>
|
||||
</div>"##
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
</div>"## .to_string(), ) }
|
||||
|
||||
async fn disable_2fa(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html(
|
||||
r##"<div class="status-indicator">
|
||||
<span class="status-dot inactive"></span>
|
||||
<span class="status-text">Two-factor authentication disabled</span>
|
||||
</div>"##
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
</div>"## .to_string(), ) }
|
||||
|
||||
async fn get_active_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html(
|
||||
|
|
@ -292,23 +300,16 @@ async fn get_active_sessions(State(_state): State<Arc<AppState>>) -> Html<String
|
|||
<span class="session-time">Active now</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sessions-empty">
|
||||
<p class="text-muted">No other active sessions</p>
|
||||
</div>"##
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
</div> <div class="sessions-empty"> <p class="text-muted">No other active sessions</p> </div>"## .to_string(), ) }
|
||||
|
||||
async fn revoke_all_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html(
|
||||
r##"<div class="success-message">
|
||||
<span class="success-icon">✓</span>
|
||||
<span>All other sessions have been revoked</span>
|
||||
</div>"##
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
</div>"## .to_string(), ) }
|
||||
|
||||
async fn get_trusted_devices(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||
Html(
|
||||
|
|
@ -321,10 +322,5 @@ async fn get_trusted_devices(State(_state): State<Arc<AppState>>) -> Html<String
|
|||
</div>
|
||||
</div>
|
||||
<span class="device-badge trusted">Trusted</span>
|
||||
</div>
|
||||
<div class="devices-empty">
|
||||
<p class="text-muted">No other trusted devices</p>
|
||||
</div>"##
|
||||
.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
</div> <div class="devices-empty"> <p class="text-muted">No other trusted devices</p> </div>"## .to_string(), ) }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue