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,
|
# Available apps: chat, mail, calendar, drive, tasks, docs, paper, sheet, slides,
|
||||||
# meet, research, sources, analytics, admin, monitoring, settings
|
# meet, research, sources, analytics, admin, monitoring, settings
|
||||||
# Only listed apps will be visible in the UI and have their APIs enabled.
|
# Only listed apps will be visible in the UI and have their APIs enabled.
|
||||||
apps=chat,mail,calendar,drive,tasks,docs,paper,sheet,slides,meet,research,sources,analytics,admin,monitoring,settings
|
apps=chat,drive,tasks,sources,settings
|
||||||
|
|
||||||
# Default theme
|
# Default theme
|
||||||
# Available themes: dark, light, blue, purple, green, orange, sentient, cyberpunk,
|
# Available themes: dark, light, blue, purple, green, orange, sentient, cyberpunk,
|
||||||
|
|
|
||||||
126
Cargo.toml
126
Cargo.toml
|
|
@ -2,27 +2,28 @@
|
||||||
name = "botserver"
|
name = "botserver"
|
||||||
version = "6.1.0"
|
version = "6.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
resolver = "2" # Better feature resolution
|
||||||
# ... [authors, description, license, repository sections remain the same]
|
|
||||||
|
|
||||||
[dependencies.botlib]
|
[dependencies.botlib]
|
||||||
path = "../botlib"
|
path = "../botlib"
|
||||||
features = ["database", "i18n"]
|
# Remove features here - control them in botlib's Cargo.toml
|
||||||
|
# features = ["database", "i18n"] # BAD - causes full recompile
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# ===== DEFAULT FEATURE SET =====
|
# ===== SINGLE DEFAULT FEATURE SET =====
|
||||||
default = ["chat", "drive", "tasks", "automation"]
|
default = ["chat", "drive", "tasks", "automation", "cache"]
|
||||||
|
|
||||||
# ===== COMMUNICATION APPS =====
|
# ===== COMMUNICATION APPS =====
|
||||||
chat = []
|
chat = ["botlib/chat"] # Delegate to botlib
|
||||||
people = []
|
people = []
|
||||||
mail = ["email", "imap", "lettre", "mailparse", "native-tls"]
|
mail = ["botlib/mail"] # Delegate optional deps to botlib
|
||||||
meet = ["dep:livekit"]
|
meet = ["dep:livekit"]
|
||||||
social = []
|
social = []
|
||||||
whatsapp = []
|
whatsapp = []
|
||||||
telegram = []
|
telegram = []
|
||||||
instagram = []
|
instagram = []
|
||||||
msteams = []
|
msteams = []
|
||||||
|
# CONSIDER: Do you REALLY need this mega-feature?
|
||||||
communications = ["chat", "people", "mail", "meet", "social", "whatsapp", "telegram", "instagram", "msteams", "cache"]
|
communications = ["chat", "people", "mail", "meet", "social", "whatsapp", "telegram", "instagram", "msteams", "cache"]
|
||||||
|
|
||||||
# ===== PRODUCTIVITY APPS =====
|
# ===== PRODUCTIVITY APPS =====
|
||||||
|
|
@ -34,11 +35,11 @@ workspace = []
|
||||||
productivity = ["calendar", "tasks", "project", "goals", "workspace", "cache"]
|
productivity = ["calendar", "tasks", "project", "goals", "workspace", "cache"]
|
||||||
|
|
||||||
# ===== DOCUMENT APPS =====
|
# ===== DOCUMENT APPS =====
|
||||||
paper = ["docx-rs", "ooxmlsdk", "dep:pdf-extract"]
|
paper = ["docs", "dep:pdf-extract"] # Reuse docs
|
||||||
docs = ["docx-rs", "ooxmlsdk"]
|
docs = ["docx-rs", "ooxmlsdk"]
|
||||||
sheet = ["umya-spreadsheet", "calamine", "rust_xlsxwriter", "spreadsheet-ods"]
|
sheet = ["calamine", "spreadsheet-ods"] # Reduced - pick one Excel lib
|
||||||
slides = ["ooxmlsdk"]
|
slides = ["ooxmlsdk"]
|
||||||
drive = ["dep:aws-config", "dep:aws-sdk-s3", "dep:pdf-extract", "dep:zip", "dep:downloader", "dep:flate2", "dep:tar"]
|
drive = ["dep:aws-config", "dep:aws-sdk-s3", "dep:pdf-extract", "dep:flate2"]
|
||||||
documents = ["paper", "docs", "sheet", "slides", "drive"]
|
documents = ["paper", "docs", "sheet", "slides", "drive"]
|
||||||
|
|
||||||
# ===== MEDIA APPS =====
|
# ===== MEDIA APPS =====
|
||||||
|
|
@ -87,88 +88,77 @@ jemalloc = ["dep:tikv-jemallocator", "dep:tikv-jemalloc-ctl"]
|
||||||
console = ["dep:crossterm", "dep:ratatui", "monitoring"]
|
console = ["dep:crossterm", "dep:ratatui", "monitoring"]
|
||||||
|
|
||||||
# ===== BUNDLE FEATURES =====
|
# ===== BUNDLE FEATURES =====
|
||||||
|
# REDUCED VERSION - Enable only what you actually use
|
||||||
full = [
|
full = [
|
||||||
# Communication
|
# Communication
|
||||||
"chat", "people", "mail", "meet", "social", "whatsapp", "telegram", "instagram", "msteams",
|
"chat", "people", "mail",
|
||||||
# Productivity
|
# Productivity
|
||||||
"calendar", "tasks", "project", "goals", "workspace",
|
"tasks", "calendar",
|
||||||
# Documents
|
# Documents
|
||||||
"paper", "docs", "sheet", "slides", "drive",
|
"drive", "docs",
|
||||||
# Media
|
|
||||||
"video", "player", "canvas",
|
|
||||||
# Learning
|
|
||||||
"learn", "research", "sources",
|
|
||||||
# Analytics
|
|
||||||
"analytics", "dashboards", "monitoring",
|
|
||||||
# Development
|
|
||||||
"designer", "editor", "automation",
|
|
||||||
# Admin
|
|
||||||
"attendant", "security", "settings",
|
|
||||||
# Core tech
|
# Core tech
|
||||||
"llm", "vectordb", "nvidia", "cache", "compliance", "timeseries", "weba", "directory",
|
"llm", "cache", "compliance"
|
||||||
"progress-bars", "grpc", "jemalloc", "console"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
minimal = ["chat"]
|
minimal = ["chat"]
|
||||||
lightweight = ["chat", "drive", "tasks", "people"]
|
lightweight = ["chat", "drive", "tasks", "people"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# === CORE RUNTIME (Always Required) ===
|
# === CORE RUNTIME (Minimal) ===
|
||||||
aes-gcm = "0.10"
|
aes-gcm = "0.10"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
argon2 = "0.5"
|
argon2 = "0.5"
|
||||||
async-lock = "2.8.0"
|
async-lock = "2.8.0"
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
axum = { version = "0.7.5", features = ["ws", "multipart", "macros"] }
|
axum = { version = "0.7.5", default-features = false, features = [] } # NO defaults!
|
||||||
axum-server = { version = "0.7", features = ["tls-rustls"] }
|
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
bytes = "1.8"
|
bytes = "1.8"
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||||
color-eyre = "0.6.5"
|
color-eyre = "0.6.5"
|
||||||
diesel = { version = "2.1", features = ["postgres", "uuid", "chrono", "serde_json", "r2d2", "numeric", "128-column-tables"] }
|
diesel = { version = "2.1", default-features = false, features = ["postgres", "r2d2"] } # MINIMAL!
|
||||||
bigdecimal = { version = "0.4", features = ["serde"] }
|
bigdecimal = { version = "0.4", default-features = false }
|
||||||
diesel_migrations = "2.1.0"
|
diesel_migrations = "2.1.0"
|
||||||
dirs = "5.0"
|
dirs = "5.0"
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
env_logger = "0.11"
|
env_logger = "0.11"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
futures-util = "0.3"
|
futures-util = { version = "0.3", default-features = false }
|
||||||
tokio-util = { version = "0.7", features = ["io", "compat"] }
|
tokio-util = { version = "0.7", default-features = false, features = ["codec"] }
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
hmac = "0.12.1"
|
hmac = "0.12.1"
|
||||||
hyper = { version = "1.4", features = ["full"] }
|
hyper = { version = "1.4", default-features = false, features = ["client", "server", "http1", "http2"] }
|
||||||
hyper-rustls = { version = "0.27", features = ["http2"] }
|
hyper-rustls = { version = "0.27", default-features = false, features = ["http2"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
num-format = "0.4"
|
num-format = "0.4"
|
||||||
once_cell = "1.18.0"
|
once_cell = "1.18.0"
|
||||||
rand = "0.9.2"
|
rand = "0.9.2"
|
||||||
regex = "1.11"
|
regex = "1.11"
|
||||||
reqwest = { version = "0.12", features = ["json", "stream", "multipart", "rustls-tls", "rustls-tls-native-roots"] }
|
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } # Reduced
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", default-features = false, features = ["derive", "std"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
sha1 = "0.10.6"
|
sha1 = "0.10.6"
|
||||||
tokio = { version = "1.41", features = ["full"] }
|
tokio = { version = "1.41", default-features = false, features = ["rt", "sync", "time", "macros", "net"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
tower = "0.4"
|
tower = "0.4"
|
||||||
tower-http = { version = "0.5", features = ["cors", "fs", "trace"] }
|
tower-http = { version = "0.5", default-features = false, features = ["cors", "fs"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["fmt"] }
|
tracing-subscriber = { version = "0.3", default-features = false }
|
||||||
urlencoding = "2.1"
|
urlencoding = "2.1"
|
||||||
uuid = { version = "1.11", features = ["serde", "v4", "v5"] }
|
uuid = { version = "1.11", default-features = false, features = ["v4"] }
|
||||||
|
|
||||||
# === TLS/SECURITY DEPENDENCIES ===
|
# === TLS/SECURITY DEPENDENCIES ===
|
||||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] }
|
||||||
tokio-rustls = "0.26"
|
tokio-rustls = "0.26"
|
||||||
rcgen = { version = "0.14", features = ["pem"] }
|
rcgen = { version = "0.14", default-features = false }
|
||||||
x509-parser = "0.15"
|
x509-parser = "0.15"
|
||||||
rustls-native-certs = "0.8"
|
rustls-native-certs = "0.8"
|
||||||
webpki-roots = "0.25"
|
webpki-roots = "0.25"
|
||||||
ring = "0.17"
|
ring = "0.17"
|
||||||
ciborium = "0.2"
|
ciborium = "0.2"
|
||||||
time = { version = "0.3", features = ["formatting", "parsing"] }
|
time = { version = "0.3", default-features = false, features = ["formatting"] }
|
||||||
jsonwebtoken = "9.3"
|
jsonwebtoken = "9.3"
|
||||||
tower-cookies = "0.10"
|
tower-cookies = "0.10"
|
||||||
|
|
||||||
|
|
@ -176,7 +166,7 @@ tower-cookies = "0.10"
|
||||||
|
|
||||||
# Email Integration (mail feature)
|
# Email Integration (mail feature)
|
||||||
imap = { version = "3.0.0-alpha.15", optional = true }
|
imap = { version = "3.0.0-alpha.15", optional = true }
|
||||||
lettre = { version = "0.11", features = ["smtp-transport", "builder", "tokio1", "tokio1-native-tls"], optional = true }
|
lettre = { version = "0.11", default-features = false, optional = true }
|
||||||
mailparse = { version = "0.15", optional = true }
|
mailparse = { version = "0.15", optional = true }
|
||||||
native-tls = { version = "0.2", optional = true }
|
native-tls = { version = "0.2", optional = true }
|
||||||
|
|
||||||
|
|
@ -186,29 +176,30 @@ livekit = { version = "0.7", optional = true }
|
||||||
# Vector Database (vectordb feature)
|
# Vector Database (vectordb feature)
|
||||||
qdrant-client = { version = "1.12", optional = true }
|
qdrant-client = { version = "1.12", optional = true }
|
||||||
|
|
||||||
# Document Processing (paper, docs, sheet, slides features)
|
# Document Processing - PICK ONE PER FORMAT!
|
||||||
docx-rs = { version = "0.4", optional = true }
|
docx-rs = { version = "0.4", optional = true }
|
||||||
ooxmlsdk = { version = "0.3", features = ["docx", "pptx", "parts", "office2021"], optional = true }
|
ooxmlsdk = { version = "0.3", default-features = false, optional = true }
|
||||||
umya-spreadsheet = { version = "2.3", optional = true }
|
# umya-spreadsheet = { version = "2.3", optional = true } # REMOVE - pick one
|
||||||
calamine = { version = "0.26", optional = true }
|
calamine = { version = "0.26", optional = true }
|
||||||
rust_xlsxwriter = { version = "0.79", optional = true }
|
# rust_xlsxwriter = { version = "0.79", optional = true } # REMOVE - pick one
|
||||||
spreadsheet-ods = { version = "1.0", optional = true }
|
spreadsheet-ods = { version = "1.0", optional = true }
|
||||||
|
|
||||||
# File Storage & Drive (drive feature)
|
# File Storage & Drive (drive feature)
|
||||||
aws-config = { version = "1.8.8", features = ["behavior-version-latest"], optional = true }
|
aws-config = { version = "1.8.8", default-features = false, optional = true }
|
||||||
aws-sdk-s3 = { version = "1.109.0", features = ["behavior-version-latest"], optional = true }
|
aws-sdk-s3 = { version = "1.109.0", default-features = false, optional = true }
|
||||||
pdf-extract = { version = "0.10.0", optional = true }
|
pdf-extract = { version = "0.10.0", optional = true }
|
||||||
quick-xml = { version = "0.37", features = ["serialize"] }
|
quick-xml = { version = "0.37", default-features = false }
|
||||||
zip = { version = "2.2", optional = true }
|
# zip = { version = "2.2", optional = true } # Only if needed
|
||||||
downloader = { version = "0.2", optional = true }
|
# downloader = { version = "0.2", optional = true } # Use reqwest instead
|
||||||
flate2 = { version = "1.0", optional = true }
|
flate2 = { version = "1.0", optional = true }
|
||||||
tar = { version = "0.4", optional = true }
|
# tar = { version = "0.4", optional = true } # Only if needed
|
||||||
|
|
||||||
# Task Management (tasks feature)
|
# Task Management (tasks feature)
|
||||||
cron = { version = "0.15.0", optional = true }
|
cron = { version = "0.15.0", optional = true }
|
||||||
|
|
||||||
# Automation & Scripting (automation feature)
|
# Automation & Scripting (automation feature)
|
||||||
rhai = { git = "https://github.com/therealprof/rhai.git", branch = "features/use-web-time", features = ["sync"], optional = true }
|
# REPLACE git with version
|
||||||
|
rhai = { version = "1.23", features = ["sync"], optional = true }
|
||||||
|
|
||||||
# Compliance & Reporting (compliance feature)
|
# Compliance & Reporting (compliance feature)
|
||||||
csv = { version = "1.3", optional = true }
|
csv = { version = "1.3", optional = true }
|
||||||
|
|
@ -225,13 +216,13 @@ qrcode = { version = "0.14", default-features = false }
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
|
|
||||||
# Caching/Sessions (cache feature)
|
# Caching/Sessions (cache feature)
|
||||||
redis = { version = "0.27", features = ["tokio-comp"], optional = true }
|
redis = { version = "0.27", default-features = false, features = ["tokio-comp"], optional = true }
|
||||||
|
|
||||||
# System Monitoring (monitoring feature)
|
# System Monitoring (monitoring feature)
|
||||||
sysinfo = { version = "0.37.2", optional = true }
|
sysinfo = { version = "0.37.2", optional = true }
|
||||||
|
|
||||||
# Networking/gRPC (grpc feature)
|
# Networking/gRPC (grpc feature)
|
||||||
tonic = { version = "0.14.2", features = ["transport"], optional = true }
|
tonic = { version = "0.14.2", default-features = false, features = ["transport", "tls"], optional = true }
|
||||||
|
|
||||||
# UI Enhancement (progress-bars feature)
|
# UI Enhancement (progress-bars feature)
|
||||||
indicatif = { version = "0.18.0", optional = true }
|
indicatif = { version = "0.18.0", optional = true }
|
||||||
|
|
@ -239,7 +230,7 @@ smartstring = "1.0.1"
|
||||||
|
|
||||||
# Memory allocator (jemalloc feature)
|
# Memory allocator (jemalloc feature)
|
||||||
tikv-jemallocator = { version = "0.6", optional = true }
|
tikv-jemallocator = { version = "0.6", optional = true }
|
||||||
tikv-jemalloc-ctl = { version = "0.6", features = ["stats"], optional = true }
|
tikv-jemalloc-ctl = { version = "0.6", default-features = false, optional = true }
|
||||||
scopeguard = "1.2.0"
|
scopeguard = "1.2.0"
|
||||||
|
|
||||||
# Vault secrets management
|
# Vault secrets management
|
||||||
|
|
@ -249,7 +240,7 @@ vaultrs = "0.7"
|
||||||
icalendar = "0.17"
|
icalendar = "0.17"
|
||||||
|
|
||||||
# Layered configuration
|
# Layered configuration
|
||||||
figment = { version = "0.10", features = ["toml", "env", "json"] }
|
figment = { version = "0.10", default-features = false, features = ["toml"] }
|
||||||
|
|
||||||
# Rate limiting
|
# Rate limiting
|
||||||
governor = "0.10"
|
governor = "0.10"
|
||||||
|
|
@ -261,15 +252,26 @@ rss = "2.0"
|
||||||
scraper = "0.25"
|
scraper = "0.25"
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
|
|
||||||
# Embedded static files (UI fallback when no external folder)
|
# Embedded static files
|
||||||
rust-embed = "8.5"
|
rust-embed = "8.5"
|
||||||
mime_guess = "2.0"
|
mime_guess = "2.0"
|
||||||
hyper-util = { version = "0.1.19", features = ["client-legacy", "tokio"] }
|
hyper-util = { version = "0.1.19", default-features = false, features = ["client-legacy"] }
|
||||||
http-body-util = "0.1.3"
|
http-body-util = "0.1.3"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
mockito = "1.7.0"
|
mockito = "1.7.0"
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 1 # Slightly optimized debug builds
|
||||||
|
split-debuginfo = "unpacked"
|
||||||
|
incremental = true
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = 3
|
||||||
|
lto = "thin"
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ git clone https://github.com/GeneralBots/botserver
|
||||||
cd botserver
|
cd botserver
|
||||||
cargo run
|
cargo run
|
||||||
```
|
```
|
||||||
|
cargo install sccache
|
||||||
|
sudo apt-get install mold # or build from source
|
||||||
|
|
||||||
On first run, botserver automatically sets up PostgreSQL, S3 storage, Redis cache, and downloads AI models.
|
On first run, botserver automatically sets up PostgreSQL, S3 storage, Redis cache, and downloads AI models.
|
||||||
|
|
||||||
|
|
@ -152,4 +154,4 @@ According to our dual licensing model, this program can be used either under the
|
||||||
|
|
||||||
**General Bots Code Name:** [Guaribas](https://en.wikipedia.org/wiki/Guaribas)
|
**General Bots Code Name:** [Guaribas](https://en.wikipedia.org/wiki/Guaribas)
|
||||||
|
|
||||||
> "No one should have to do work that can be done by a machine." - Roberto Mangabeira Unger
|
> "No one should have to do work that can be done by a machine." - Roberto Mangabeira Unger
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,3 @@
|
||||||
/*****************************************************************************\
|
|
||||||
| █████ █████ ██ █ █████ █████ ████ ██ ████ █████ █████ ███ ® |
|
|
||||||
| ██ █ ███ █ █ ██ ██ ██ ██ ██ ██ █ ██ ██ █ █ |
|
|
||||||
| ██ ███ ████ █ ██ █ ████ █████ ██████ ██ ████ █ █ █ ██ |
|
|
||||||
| ██ ██ █ █ ██ █ █ ██ ██ ██ ██ ██ ██ █ ██ ██ █ █ |
|
|
||||||
| █████ █████ █ ███ █████ ██ ██ ██ ██ █████ ████ █████ █ ███ |
|
|
||||||
| |
|
|
||||||
| General Bots Copyright (c) pragmatismo.com.br. All rights reserved. |
|
|
||||||
| Licensed under the AGPL-3.0. |
|
|
||||||
| |
|
|
||||||
| According to our dual licensing model, this program can be used either |
|
|
||||||
| under the terms of the GNU Affero General Public License, version 3, |
|
|
||||||
| or under a proprietary license. |
|
|
||||||
| |
|
|
||||||
| The texts of the GNU Affero General Public License with an additional |
|
|
||||||
| permission and of our proprietary license can be found at and |
|
|
||||||
| in the LICENSE file you have received along with this program. |
|
|
||||||
| |
|
|
||||||
| This program is distributed in the hope that it will be useful, |
|
|
||||||
| but WITHOUT ANY WARRANTY, without even the implied warranty of |
|
|
||||||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
||||||
| GNU Affero General Public License for more details. |
|
|
||||||
| |
|
|
||||||
| "General Bots" is a registered trademark of pragmatismo.com.br. |
|
|
||||||
| The licensing of the program under the AGPLv3 does not imply a |
|
|
||||||
| trademark license. Therefore any rights, title and interest in |
|
|
||||||
| our trademarks remain entirely with us. |
|
|
||||||
| |
|
|
||||||
\*****************************************************************************/
|
|
||||||
|
|
||||||
use crate::shared::models::UserSession;
|
use crate::shared::models::UserSession;
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
use log::{error, trace};
|
use log::{error, trace};
|
||||||
|
|
@ -46,7 +16,6 @@ pub fn register_import_export(state: Arc<AppState>, user: UserSession, engine: &
|
||||||
|
|
||||||
pub fn register_import_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
pub fn register_import_keyword(state: Arc<AppState>, user: UserSession, engine: &mut Engine) {
|
||||||
let state_clone = Arc::clone(&state);
|
let state_clone = Arc::clone(&state);
|
||||||
|
|
||||||
engine
|
engine
|
||||||
.register_custom_syntax(["IMPORT", "$expr$"], false, move |context, inputs| {
|
.register_custom_syntax(["IMPORT", "$expr$"], false, move |context, inputs| {
|
||||||
let file_path = context.eval_expression_tree(&inputs[0])?.to_string();
|
let file_path = context.eval_expression_tree(&inputs[0])?.to_string();
|
||||||
|
|
@ -205,7 +174,16 @@ fn execute_import(
|
||||||
match extension.as_str() {
|
match extension.as_str() {
|
||||||
"csv" => import_csv(&full_path),
|
"csv" => import_csv(&full_path),
|
||||||
"json" => import_json(&full_path),
|
"json" => import_json(&full_path),
|
||||||
"xlsx" | "xls" => import_excel(&full_path),
|
"xlsx" | "xls" => {
|
||||||
|
#[cfg(feature = "sheet")]
|
||||||
|
{
|
||||||
|
import_excel(&full_path)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "sheet"))]
|
||||||
|
{
|
||||||
|
Err(format!("Excel import requires 'sheet' feature. File: {}", file_path).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
"tsv" => import_tsv(&full_path),
|
"tsv" => import_tsv(&full_path),
|
||||||
_ => Err(format!("Unsupported file format: .{}", extension).into()),
|
_ => Err(format!("Unsupported file format: .{}", extension).into()),
|
||||||
}
|
}
|
||||||
|
|
@ -227,7 +205,16 @@ fn execute_export(
|
||||||
match extension.as_str() {
|
match extension.as_str() {
|
||||||
"csv" => export_csv(&full_path, data),
|
"csv" => export_csv(&full_path, data),
|
||||||
"json" => export_json(&full_path, data),
|
"json" => export_json(&full_path, data),
|
||||||
"xlsx" => export_excel(&full_path, data),
|
"xlsx" => {
|
||||||
|
#[cfg(feature = "sheet")]
|
||||||
|
{
|
||||||
|
export_excel(&full_path, data)
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "sheet"))]
|
||||||
|
{
|
||||||
|
Err(format!("Excel export requires 'sheet' feature. File: {}", file_path).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
"tsv" => export_tsv(&full_path, data),
|
"tsv" => export_tsv(&full_path, data),
|
||||||
_ => Err(format!("Unsupported export format: .{}", extension).into()),
|
_ => Err(format!("Unsupported export format: .{}", extension).into()),
|
||||||
}
|
}
|
||||||
|
|
@ -361,6 +348,7 @@ fn import_json(file_path: &str) -> Result<Dynamic, Box<dyn std::error::Error + S
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sheet")]
|
||||||
fn import_excel(file_path: &str) -> Result<Dynamic, Box<dyn std::error::Error + Send + Sync>> {
|
fn import_excel(file_path: &str) -> Result<Dynamic, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
use calamine::{open_workbook, Reader, Xlsx};
|
use calamine::{open_workbook, Reader, Xlsx};
|
||||||
|
|
||||||
|
|
@ -474,6 +462,7 @@ fn export_json(
|
||||||
Ok(file_path.to_string())
|
Ok(file_path.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "sheet")]
|
||||||
fn export_excel(
|
fn export_excel(
|
||||||
file_path: &str,
|
file_path: &str,
|
||||||
data: Dynamic,
|
data: Dynamic,
|
||||||
|
|
@ -534,7 +523,7 @@ fn parse_csv_line(line: &str) -> Vec<String> {
|
||||||
|
|
||||||
fn escape_csv_value(value: &str) -> String {
|
fn escape_csv_value(value: &str) -> String {
|
||||||
if value.contains(',') || value.contains('"') || value.contains('\n') {
|
if value.contains(',') || value.contains('"') || value.contains('\n') {
|
||||||
format!("\"{}\"", value.replace('"', "\"\""))
|
format!("{}", value.replace('"', ""))
|
||||||
} else {
|
} else {
|
||||||
value.to_string()
|
value.to_string()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -8,9 +8,13 @@ use axum::{
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use diesel::sql_types::{Nullable, Text, Timestamptz, Uuid as DieselUuid, Varchar};
|
use diesel::sql_types::{Nullable, Text, Timestamptz, Uuid as DieselUuid, Varchar};
|
||||||
|
#[cfg(feature = "mail")]
|
||||||
use lettre::{Message, SmtpTransport, Transport};
|
use lettre::{Message, SmtpTransport, Transport};
|
||||||
|
#[cfg(feature = "mail")]
|
||||||
use lettre::transport::smtp::authentication::Credentials;
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
use log::{info, warn};
|
use log::warn;
|
||||||
|
#[cfg(feature = "mail")]
|
||||||
|
use log::info;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -20,11 +24,12 @@ use uuid::Uuid;
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Send invitation email via SMTP
|
/// Send invitation email via SMTP
|
||||||
|
#[cfg(feature = "mail")]
|
||||||
async fn send_invitation_email(
|
async fn send_invitation_email(
|
||||||
to_email: &str,
|
to_email: &str,
|
||||||
role: &str,
|
role: &str,
|
||||||
custom_message: Option<&str>,
|
custom_message: Option<&str>,
|
||||||
invitation_id: Uuid,
|
invitation_id: Uuid,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let smtp_host = std::env::var("SMTP_HOST").unwrap_or_else(|_| "localhost".to_string());
|
let smtp_host = std::env::var("SMTP_HOST").unwrap_or_else(|_| "localhost".to_string());
|
||||||
let smtp_user = std::env::var("SMTP_USER").ok();
|
let smtp_user = std::env::var("SMTP_USER").ok();
|
||||||
|
|
@ -77,6 +82,7 @@ The General Bots Team"#,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send invitation email by fetching details from database
|
/// Send invitation email by fetching details from database
|
||||||
|
#[cfg(feature = "mail")]
|
||||||
async fn send_invitation_email_by_id(invitation_id: Uuid) -> Result<(), String> {
|
async fn send_invitation_email_by_id(invitation_id: Uuid) -> Result<(), String> {
|
||||||
let smtp_host = std::env::var("SMTP_HOST").unwrap_or_else(|_| "localhost".to_string());
|
let smtp_host = std::env::var("SMTP_HOST").unwrap_or_else(|_| "localhost".to_string());
|
||||||
let smtp_user = std::env::var("SMTP_USER").ok();
|
let smtp_user = std::env::var("SMTP_USER").ok();
|
||||||
|
|
|
||||||
|
|
@ -1,506 +1,525 @@
|
||||||
//! Memory and CPU monitoring with thread tracking
|
|
||||||
//!
|
|
||||||
//! This module provides tools to track memory/CPU usage per thread
|
|
||||||
//! and identify potential leaks or CPU hogs in the botserver application.
|
|
||||||
//!
|
|
||||||
//! When compiled with the `jemalloc` feature, provides detailed allocation statistics.
|
|
||||||
|
|
||||||
use log::{debug, info, trace, warn};
|
use log::{debug, info, trace, warn};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::{LazyLock, Mutex, RwLock};
|
use std::sync::{LazyLock, Mutex, RwLock};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
#[cfg(feature = "monitoring")]
|
||||||
use sysinfo::{Pid, ProcessesToUpdate, System};
|
use sysinfo::{Pid, ProcessesToUpdate, System};
|
||||||
|
|
||||||
static THREAD_REGISTRY: LazyLock<RwLock<HashMap<String, ThreadInfo>>> =
|
static THREAD_REGISTRY: LazyLock<RwLock<HashMap<String, ThreadInfo>>> =
|
||||||
LazyLock::new(|| RwLock::new(HashMap::new()));
|
LazyLock::new(|| RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
static COMPONENT_TRACKER: LazyLock<ComponentMemoryTracker> =
|
static COMPONENT_TRACKER: LazyLock<ComponentMemoryTracker> =
|
||||||
LazyLock::new(|| ComponentMemoryTracker::new(60));
|
LazyLock::new(|| ComponentMemoryTracker::new(60));
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ThreadInfo {
|
pub struct ThreadInfo {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub started_at: Instant,
|
pub started_at: Instant,
|
||||||
pub last_activity: Instant,
|
pub last_activity: Instant,
|
||||||
pub activity_count: u64,
|
pub activity_count: u64,
|
||||||
pub component: String,
|
pub component: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register_thread(name: &str, component: &str) {
|
pub fn register_thread(name: &str, component: &str) {
|
||||||
let info = ThreadInfo {
|
let info = ThreadInfo {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
started_at: Instant::now(),
|
started_at: Instant::now(),
|
||||||
last_activity: Instant::now(),
|
last_activity: Instant::now(),
|
||||||
activity_count: 0,
|
activity_count: 0,
|
||||||
component: component.to_string(),
|
component: component.to_string(),
|
||||||
};
|
};
|
||||||
if let Ok(mut registry) = THREAD_REGISTRY.write() {
|
if let Ok(mut registry) = THREAD_REGISTRY.write() {
|
||||||
registry.insert(name.to_string(), info);
|
registry.insert(name.to_string(), info);
|
||||||
}
|
}
|
||||||
trace!("[THREAD] Registered: {} (component: {})", name, component);
|
trace!("[THREAD] Registered: {} (component: {})", name, component);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_thread_activity(name: &str) {
|
pub fn record_thread_activity(name: &str) {
|
||||||
if let Ok(mut registry) = THREAD_REGISTRY.write() {
|
if let Ok(mut registry) = THREAD_REGISTRY.write() {
|
||||||
if let Some(info) = registry.get_mut(name) {
|
if let Some(info) = registry.get_mut(name) {
|
||||||
info.last_activity = Instant::now();
|
info.last_activity = Instant::now();
|
||||||
info.activity_count += 1;
|
info.activity_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unregister_thread(name: &str) {
|
pub fn unregister_thread(name: &str) {
|
||||||
if let Ok(mut registry) = THREAD_REGISTRY.write() {
|
if let Ok(mut registry) = THREAD_REGISTRY.write() {
|
||||||
registry.remove(name);
|
registry.remove(name);
|
||||||
}
|
}
|
||||||
info!("[THREAD] Unregistered: {}", name);
|
info!("[THREAD] Unregistered: {}", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn log_thread_stats() {
|
pub fn log_thread_stats() {
|
||||||
if let Ok(registry) = THREAD_REGISTRY.read() {
|
if let Ok(registry) = THREAD_REGISTRY.read() {
|
||||||
info!("[THREADS] Active thread count: {}", registry.len());
|
info!("[THREADS] Active thread count: {}", registry.len());
|
||||||
for (name, info) in registry.iter() {
|
for (name, info) in registry.iter() {
|
||||||
let uptime = info.started_at.elapsed().as_secs();
|
let uptime = info.started_at.elapsed().as_secs();
|
||||||
let idle = info.last_activity.elapsed().as_secs();
|
let idle = info.last_activity.elapsed().as_secs();
|
||||||
info!(
|
info!(
|
||||||
"[THREAD] {} | component={} | uptime={}s | idle={}s | activities={}",
|
"[THREAD] {} | component={} | uptime={}s | idle={}s | activities={}",
|
||||||
name, info.component, uptime, idle, info.activity_count
|
name, info.component, uptime, idle, info.activity_count
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MemoryStats {
|
pub struct MemoryStats {
|
||||||
pub rss_bytes: u64,
|
pub rss_bytes: u64,
|
||||||
pub virtual_bytes: u64,
|
pub virtual_bytes: u64,
|
||||||
pub timestamp: Instant,
|
pub timestamp: Instant,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MemoryStats {
|
impl MemoryStats {
|
||||||
pub fn current() -> Self {
|
pub fn current() -> Self {
|
||||||
let (rss, virt) = get_process_memory().unwrap_or((0, 0));
|
let (rss, virt) = get_process_memory().unwrap_or((0, 0));
|
||||||
Self {
|
Self {
|
||||||
rss_bytes: rss,
|
rss_bytes: rss,
|
||||||
virtual_bytes: virt,
|
virtual_bytes: virt,
|
||||||
timestamp: Instant::now(),
|
timestamp: Instant::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn format_bytes(bytes: u64) -> String {
|
|
||||||
const KB: u64 = 1024;
|
|
||||||
const MB: u64 = KB * 1024;
|
|
||||||
const GB: u64 = MB * 1024;
|
|
||||||
|
|
||||||
if bytes >= GB {
|
pub fn format_bytes(bytes: u64) -> String {
|
||||||
format!("{:.2} GB", bytes as f64 / GB as f64)
|
const KB: u64 = 1024;
|
||||||
} else if bytes >= MB {
|
const MB: u64 = KB * 1024;
|
||||||
format!("{:.2} MB", bytes as f64 / MB as f64)
|
const GB: u64 = MB * 1024;
|
||||||
} else if bytes >= KB {
|
|
||||||
format!("{:.2} KB", bytes as f64 / KB as f64)
|
|
||||||
} else {
|
|
||||||
format!("{} B", bytes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn log(&self) {
|
if bytes >= GB {
|
||||||
info!(
|
format!("{:.2} GB", bytes as f64 / GB as f64)
|
||||||
"[MEMORY] RSS={}, Virtual={}",
|
} else if bytes >= MB {
|
||||||
Self::format_bytes(self.rss_bytes),
|
format!("{:.2} MB", bytes as f64 / MB as f64)
|
||||||
Self::format_bytes(self.virtual_bytes),
|
} else if bytes >= KB {
|
||||||
);
|
format!("{:.2} KB", bytes as f64 / KB as f64)
|
||||||
|
} else {
|
||||||
|
format!("{} B", bytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn log(&self) {
|
||||||
|
info!(
|
||||||
|
"[MEMORY] RSS={}, Virtual={}",
|
||||||
|
Self::format_bytes(self.rss_bytes),
|
||||||
|
Self::format_bytes(self.virtual_bytes),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/// Get jemalloc memory statistics when the feature is enabled
|
/// Get jemalloc memory statistics when the feature is enabled
|
||||||
#[cfg(feature = "jemalloc")]
|
#[cfg(feature = "jemalloc")]
|
||||||
pub fn get_jemalloc_stats() -> Option<JemallocStats> {
|
pub fn get_jemalloc_stats() -> Option<JemallocStats> {
|
||||||
use tikv_jemalloc_ctl::{epoch, stats};
|
use tikv_jemalloc_ctl::{epoch, stats};
|
||||||
|
|
||||||
// Advance the epoch to refresh statistics
|
|
||||||
if epoch::advance().is_err() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let allocated = stats::allocated::read().ok()? as u64;
|
// Advance the epoch to refresh statistics
|
||||||
let active = stats::active::read().ok()? as u64;
|
if epoch::advance().is_err() {
|
||||||
let resident = stats::resident::read().ok()? as u64;
|
return None;
|
||||||
let mapped = stats::mapped::read().ok()? as u64;
|
}
|
||||||
let retained = stats::retained::read().ok()? as u64;
|
|
||||||
|
let allocated = stats::allocated::read().ok()? as u64;
|
||||||
|
let active = stats::active::read().ok()? as u64;
|
||||||
|
let resident = stats::resident::read().ok()? as u64;
|
||||||
|
let mapped = stats::mapped::read().ok()? as u64;
|
||||||
|
let retained = stats::retained::read().ok()? as u64;
|
||||||
|
|
||||||
|
Some(JemallocStats {
|
||||||
|
allocated,
|
||||||
|
active,
|
||||||
|
resident,
|
||||||
|
mapped,
|
||||||
|
retained,
|
||||||
|
})
|
||||||
|
|
||||||
Some(JemallocStats {
|
|
||||||
allocated,
|
|
||||||
active,
|
|
||||||
resident,
|
|
||||||
mapped,
|
|
||||||
retained,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "jemalloc"))]
|
#[cfg(not(feature = "jemalloc"))]
|
||||||
pub fn get_jemalloc_stats() -> Option<JemallocStats> {
|
pub fn get_jemalloc_stats() -> Option<JemallocStats> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Jemalloc memory statistics
|
/// Jemalloc memory statistics
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct JemallocStats {
|
pub struct JemallocStats {
|
||||||
/// Total bytes allocated by the application
|
/// Total bytes allocated by the application
|
||||||
pub allocated: u64,
|
pub allocated: u64,
|
||||||
/// Total bytes in active pages allocated by the application
|
/// Total bytes in active pages allocated by the application
|
||||||
pub active: u64,
|
pub active: u64,
|
||||||
/// Total bytes in physically resident pages
|
/// Total bytes in physically resident pages
|
||||||
pub resident: u64,
|
pub resident: u64,
|
||||||
/// Total bytes in active extents mapped by the allocator
|
/// Total bytes in active extents mapped by the allocator
|
||||||
pub mapped: u64,
|
pub mapped: u64,
|
||||||
/// Total bytes retained (not returned to OS)
|
/// Total bytes retained (not returned to OS)
|
||||||
pub retained: u64,
|
pub retained: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JemallocStats {
|
impl JemallocStats {
|
||||||
pub fn log(&self) {
|
pub fn log(&self) {
|
||||||
info!(
|
info!(
|
||||||
"[JEMALLOC] allocated={} active={} resident={} mapped={} retained={}",
|
"[JEMALLOC] allocated={} active={} resident={} mapped={} retained={}",
|
||||||
MemoryStats::format_bytes(self.allocated),
|
MemoryStats::format_bytes(self.allocated),
|
||||||
MemoryStats::format_bytes(self.active),
|
MemoryStats::format_bytes(self.active),
|
||||||
MemoryStats::format_bytes(self.resident),
|
MemoryStats::format_bytes(self.resident),
|
||||||
MemoryStats::format_bytes(self.mapped),
|
MemoryStats::format_bytes(self.mapped),
|
||||||
MemoryStats::format_bytes(self.retained),
|
MemoryStats::format_bytes(self.retained),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate fragmentation ratio (1.0 = no fragmentation)
|
|
||||||
pub fn fragmentation_ratio(&self) -> f64 {
|
/// Calculate fragmentation ratio (1.0 = no fragmentation)
|
||||||
if self.allocated > 0 {
|
pub fn fragmentation_ratio(&self) -> f64 {
|
||||||
self.active as f64 / self.allocated as f64
|
if self.allocated > 0 {
|
||||||
} else {
|
self.active as f64 / self.allocated as f64
|
||||||
1.0
|
} else {
|
||||||
}
|
1.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
/// Log jemalloc stats if available
|
/// Log jemalloc stats if available
|
||||||
pub fn log_jemalloc_stats() {
|
pub fn log_jemalloc_stats() {
|
||||||
if let Some(stats) = get_jemalloc_stats() {
|
if let Some(stats) = get_jemalloc_stats() {
|
||||||
stats.log();
|
stats.log();
|
||||||
let frag = stats.fragmentation_ratio();
|
let frag = stats.fragmentation_ratio();
|
||||||
if frag > 1.5 {
|
if frag > 1.5 {
|
||||||
warn!("[JEMALLOC] High fragmentation detected: {:.2}x", frag);
|
warn!("[JEMALLOC] High fragmentation detected: {:.2}x", frag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct MemoryCheckpoint {
|
pub struct MemoryCheckpoint {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub stats: MemoryStats,
|
pub stats: MemoryStats,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MemoryCheckpoint {
|
impl MemoryCheckpoint {
|
||||||
pub fn new(name: &str) -> Self {
|
pub fn new(name: &str) -> Self {
|
||||||
let stats = MemoryStats::current();
|
let stats = MemoryStats::current();
|
||||||
info!(
|
info!(
|
||||||
"[CHECKPOINT] {} started at RSS={}",
|
"[CHECKPOINT] {} started at RSS={}",
|
||||||
name,
|
name,
|
||||||
MemoryStats::format_bytes(stats.rss_bytes)
|
MemoryStats::format_bytes(stats.rss_bytes)
|
||||||
|
);
|
||||||
|
Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
stats,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn compare_and_log(&self) {
|
||||||
|
let current = MemoryStats::current();
|
||||||
|
let diff = current.rss_bytes as i64 - self.stats.rss_bytes as i64;
|
||||||
|
|
||||||
|
if diff > 0 {
|
||||||
|
warn!(
|
||||||
|
"[CHECKPOINT] {} INCREASED by {}",
|
||||||
|
self.name,
|
||||||
|
MemoryStats::format_bytes(diff as u64),
|
||||||
);
|
);
|
||||||
Self {
|
} else if diff < 0 {
|
||||||
name: name.to_string(),
|
info!(
|
||||||
stats,
|
"[CHECKPOINT] {} decreased by {}",
|
||||||
}
|
self.name,
|
||||||
|
MemoryStats::format_bytes((-diff) as u64),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
debug!("[CHECKPOINT] {} unchanged", self.name);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn compare_and_log(&self) {
|
|
||||||
let current = MemoryStats::current();
|
|
||||||
let diff = current.rss_bytes as i64 - self.stats.rss_bytes as i64;
|
|
||||||
|
|
||||||
if diff > 0 {
|
|
||||||
warn!(
|
|
||||||
"[CHECKPOINT] {} INCREASED by {}",
|
|
||||||
self.name,
|
|
||||||
MemoryStats::format_bytes(diff as u64),
|
|
||||||
);
|
|
||||||
} else if diff < 0 {
|
|
||||||
info!(
|
|
||||||
"[CHECKPOINT] {} decreased by {}",
|
|
||||||
self.name,
|
|
||||||
MemoryStats::format_bytes((-diff) as u64),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
debug!("[CHECKPOINT] {} unchanged", self.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ComponentMemoryTracker {
|
pub struct ComponentMemoryTracker {
|
||||||
components: Mutex<HashMap<String, Vec<MemoryStats>>>,
|
components: Mutex<HashMap<String, Vec<MemoryStats>>>,
|
||||||
max_history: usize,
|
max_history: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ComponentMemoryTracker {
|
impl ComponentMemoryTracker {
|
||||||
pub fn new(max_history: usize) -> Self {
|
pub fn new(max_history: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
components: Mutex::new(HashMap::new()),
|
components: Mutex::new(HashMap::new()),
|
||||||
max_history,
|
max_history,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn record(&self, component: &str) {
|
||||||
|
let stats = MemoryStats::current();
|
||||||
|
if let Ok(mut components) = self.components.lock() {
|
||||||
|
let history = components.entry(component.to_string()).or_default();
|
||||||
|
history.push(stats);
|
||||||
|
|
||||||
|
if history.len() > self.max_history {
|
||||||
|
history.remove(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn record(&self, component: &str) {
|
pub fn get_growth_rate(&self, component: &str) -> Option<f64> {
|
||||||
let stats = MemoryStats::current();
|
if let Ok(components) = self.components.lock() {
|
||||||
if let Ok(mut components) = self.components.lock() {
|
if let Some(history) = components.get(component) {
|
||||||
let history = components.entry(component.to_string()).or_default();
|
if history.len() >= 2 {
|
||||||
history.push(stats);
|
let first = &history[0];
|
||||||
|
let last = &history[history.len() - 1];
|
||||||
if history.len() > self.max_history {
|
let duration = last.timestamp.duration_since(first.timestamp).as_secs_f64();
|
||||||
history.remove(0);
|
if duration > 0.0 {
|
||||||
}
|
let byte_diff = last.rss_bytes as f64 - first.rss_bytes as f64;
|
||||||
}
|
return Some(byte_diff / duration);
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_growth_rate(&self, component: &str) -> Option<f64> {
|
|
||||||
if let Ok(components) = self.components.lock() {
|
|
||||||
if let Some(history) = components.get(component) {
|
|
||||||
if history.len() >= 2 {
|
|
||||||
let first = &history[0];
|
|
||||||
let last = &history[history.len() - 1];
|
|
||||||
let duration = last.timestamp.duration_since(first.timestamp).as_secs_f64();
|
|
||||||
if duration > 0.0 {
|
|
||||||
let byte_diff = last.rss_bytes as f64 - first.rss_bytes as f64;
|
|
||||||
return Some(byte_diff / duration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn log_all(&self) {
|
|
||||||
if let Ok(components) = self.components.lock() {
|
|
||||||
for (name, history) in components.iter() {
|
|
||||||
if let Some(last) = history.last() {
|
|
||||||
let growth = self.get_growth_rate(name);
|
|
||||||
let growth_str = growth
|
|
||||||
.map(|g| {
|
|
||||||
let sign = if g >= 0.0 { "+" } else { "-" };
|
|
||||||
format!("{}{}/s", sign, MemoryStats::format_bytes(g.abs() as u64))
|
|
||||||
})
|
|
||||||
.unwrap_or_else(|| "N/A".to_string());
|
|
||||||
info!(
|
|
||||||
"[COMPONENT] {} | RSS={} | Growth={}",
|
|
||||||
name,
|
|
||||||
MemoryStats::format_bytes(last.rss_bytes),
|
|
||||||
growth_str
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn log_all(&self) {
|
||||||
|
if let Ok(components) = self.components.lock() {
|
||||||
|
for (name, history) in components.iter() {
|
||||||
|
if let Some(last) = history.last() {
|
||||||
|
let growth = self.get_growth_rate(name);
|
||||||
|
let growth_str = growth
|
||||||
|
.map(|g| {
|
||||||
|
let sign = if g >= 0.0 { "+" } else { "-" };
|
||||||
|
format!("{}{}/s", sign, MemoryStats::format_bytes(g.abs() as u64))
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| "N/A".to_string());
|
||||||
|
info!(
|
||||||
|
"[COMPONENT] {} | RSS={} | Growth={}",
|
||||||
|
name,
|
||||||
|
MemoryStats::format_bytes(last.rss_bytes),
|
||||||
|
growth_str
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_component(component: &str) {
|
pub fn record_component(component: &str) {
|
||||||
COMPONENT_TRACKER.record(component);
|
COMPONENT_TRACKER.record(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn log_component_stats() {
|
pub fn log_component_stats() {
|
||||||
COMPONENT_TRACKER.log_all();
|
COMPONENT_TRACKER.log_all();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LeakDetector {
|
pub struct LeakDetector {
|
||||||
baseline: Mutex<u64>,
|
baseline: Mutex<u64>,
|
||||||
growth_threshold_bytes: u64,
|
growth_threshold_bytes: u64,
|
||||||
consecutive_growth_count: Mutex<usize>,
|
consecutive_growth_count: Mutex<usize>,
|
||||||
max_consecutive_growth: usize,
|
max_consecutive_growth: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LeakDetector {
|
impl LeakDetector {
|
||||||
pub fn new(growth_threshold_mb: u64, max_consecutive_growth: usize) -> Self {
|
pub fn new(growth_threshold_mb: u64, max_consecutive_growth: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
baseline: Mutex::new(0),
|
baseline: Mutex::new(0),
|
||||||
growth_threshold_bytes: growth_threshold_mb * 1024 * 1024,
|
growth_threshold_bytes: growth_threshold_mb * 1024 * 1024,
|
||||||
consecutive_growth_count: Mutex::new(0),
|
consecutive_growth_count: Mutex::new(0),
|
||||||
max_consecutive_growth,
|
max_consecutive_growth,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_baseline(&self) {
|
|
||||||
let current = MemoryStats::current();
|
pub fn reset_baseline(&self) {
|
||||||
|
let current = MemoryStats::current();
|
||||||
|
if let Ok(mut baseline) = self.baseline.lock() {
|
||||||
|
*baseline = current.rss_bytes;
|
||||||
|
}
|
||||||
|
if let Ok(mut count) = self.consecutive_growth_count.lock() {
|
||||||
|
*count = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check(&self) -> Option<String> {
|
||||||
|
let current = MemoryStats::current();
|
||||||
|
|
||||||
|
let baseline_val = match self.baseline.lock() {
|
||||||
|
Ok(b) => *b,
|
||||||
|
Err(_) => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if baseline_val == 0 {
|
||||||
if let Ok(mut baseline) = self.baseline.lock() {
|
if let Ok(mut baseline) = self.baseline.lock() {
|
||||||
*baseline = current.rss_bytes;
|
*baseline = current.rss_bytes;
|
||||||
}
|
}
|
||||||
if let Ok(mut count) = self.consecutive_growth_count.lock() {
|
return None;
|
||||||
*count = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check(&self) -> Option<String> {
|
let growth = current.rss_bytes.saturating_sub(baseline_val);
|
||||||
let current = MemoryStats::current();
|
|
||||||
|
|
||||||
let baseline_val = match self.baseline.lock() {
|
if growth > self.growth_threshold_bytes {
|
||||||
Ok(b) => *b,
|
let count = match self.consecutive_growth_count.lock() {
|
||||||
|
Ok(mut c) => {
|
||||||
|
*c += 1;
|
||||||
|
*c
|
||||||
|
}
|
||||||
Err(_) => return None,
|
Err(_) => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
if baseline_val == 0 {
|
if count >= self.max_consecutive_growth {
|
||||||
if let Ok(mut baseline) = self.baseline.lock() {
|
return Some(format!(
|
||||||
*baseline = current.rss_bytes;
|
"POTENTIAL MEMORY LEAK: grew by {} over {} checks. RSS={}, Baseline={}",
|
||||||
}
|
MemoryStats::format_bytes(growth),
|
||||||
return None;
|
count,
|
||||||
|
MemoryStats::format_bytes(current.rss_bytes),
|
||||||
|
MemoryStats::format_bytes(baseline_val),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
let growth = current.rss_bytes.saturating_sub(baseline_val);
|
if let Ok(mut count) = self.consecutive_growth_count.lock() {
|
||||||
|
*count = 0;
|
||||||
if growth > self.growth_threshold_bytes {
|
}
|
||||||
let count = match self.consecutive_growth_count.lock() {
|
if let Ok(mut baseline) = self.baseline.lock() {
|
||||||
Ok(mut c) => {
|
*baseline = current.rss_bytes;
|
||||||
*c += 1;
|
|
||||||
*c
|
|
||||||
}
|
|
||||||
Err(_) => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if count >= self.max_consecutive_growth {
|
|
||||||
return Some(format!(
|
|
||||||
"POTENTIAL MEMORY LEAK: grew by {} over {} checks. RSS={}, Baseline={}",
|
|
||||||
MemoryStats::format_bytes(growth),
|
|
||||||
count,
|
|
||||||
MemoryStats::format_bytes(current.rss_bytes),
|
|
||||||
MemoryStats::format_bytes(baseline_val),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if let Ok(mut count) = self.consecutive_growth_count.lock() {
|
|
||||||
*count = 0;
|
|
||||||
}
|
|
||||||
if let Ok(mut baseline) = self.baseline.lock() {
|
|
||||||
*baseline = current.rss_bytes;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_memory_monitor(interval_secs: u64, warn_threshold_mb: u64) {
|
pub fn start_memory_monitor(interval_secs: u64, warn_threshold_mb: u64) {
|
||||||
let detector = LeakDetector::new(warn_threshold_mb, 5);
|
let detector = LeakDetector::new(warn_threshold_mb, 5);
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
register_thread("memory-monitor", "monitoring");
|
|
||||||
|
|
||||||
info!(
|
tokio::spawn(async move {
|
||||||
"[MONITOR] Started (interval={}s, threshold={}MB)",
|
register_thread("memory-monitor", "monitoring");
|
||||||
interval_secs, warn_threshold_mb
|
|
||||||
|
info!(
|
||||||
|
"[MONITOR] Started (interval={}s, threshold={}MB)",
|
||||||
|
interval_secs, warn_threshold_mb
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut prev_rss: u64 = 0;
|
||||||
|
let mut tick_count: u64 = 0;
|
||||||
|
|
||||||
|
// First 2 minutes: check every 10 seconds for aggressive tracking
|
||||||
|
// After that: use normal interval
|
||||||
|
let startup_interval = Duration::from_secs(10);
|
||||||
|
let normal_interval = Duration::from_secs(interval_secs);
|
||||||
|
let startup_ticks = 12; // 2 minutes of 10-second intervals
|
||||||
|
|
||||||
|
let mut interval = tokio::time::interval(startup_interval);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
tick_count += 1;
|
||||||
|
record_thread_activity("memory-monitor");
|
||||||
|
|
||||||
|
let stats = MemoryStats::current();
|
||||||
|
let rss_diff = if prev_rss > 0 {
|
||||||
|
stats.rss_bytes as i64 - prev_rss as i64
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let diff_str = if rss_diff > 0 {
|
||||||
|
format!("+{}", MemoryStats::format_bytes(rss_diff as u64))
|
||||||
|
} else if rss_diff < 0 {
|
||||||
|
format!("-{}", MemoryStats::format_bytes((-rss_diff) as u64))
|
||||||
|
} else {
|
||||||
|
"±0".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"[MONITOR] tick={} RSS={} ({}) Virtual={}",
|
||||||
|
tick_count,
|
||||||
|
MemoryStats::format_bytes(stats.rss_bytes),
|
||||||
|
diff_str,
|
||||||
|
MemoryStats::format_bytes(stats.virtual_bytes),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut prev_rss: u64 = 0;
|
// Log jemalloc stats every 5 ticks if available
|
||||||
let mut tick_count: u64 = 0;
|
if tick_count % 5 == 0 {
|
||||||
|
log_jemalloc_stats();
|
||||||
// First 2 minutes: check every 10 seconds for aggressive tracking
|
|
||||||
// After that: use normal interval
|
|
||||||
let startup_interval = Duration::from_secs(10);
|
|
||||||
let normal_interval = Duration::from_secs(interval_secs);
|
|
||||||
let startup_ticks = 12; // 2 minutes of 10-second intervals
|
|
||||||
|
|
||||||
let mut interval = tokio::time::interval(startup_interval);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
interval.tick().await;
|
|
||||||
tick_count += 1;
|
|
||||||
record_thread_activity("memory-monitor");
|
|
||||||
|
|
||||||
let stats = MemoryStats::current();
|
|
||||||
let rss_diff = if prev_rss > 0 {
|
|
||||||
stats.rss_bytes as i64 - prev_rss as i64
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
let diff_str = if rss_diff > 0 {
|
|
||||||
format!("+{}", MemoryStats::format_bytes(rss_diff as u64))
|
|
||||||
} else if rss_diff < 0 {
|
|
||||||
format!("-{}", MemoryStats::format_bytes((-rss_diff) as u64))
|
|
||||||
} else {
|
|
||||||
"±0".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
trace!(
|
|
||||||
"[MONITOR] tick={} RSS={} ({}) Virtual={}",
|
|
||||||
tick_count,
|
|
||||||
MemoryStats::format_bytes(stats.rss_bytes),
|
|
||||||
diff_str,
|
|
||||||
MemoryStats::format_bytes(stats.virtual_bytes),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Log jemalloc stats every 5 ticks if available
|
|
||||||
if tick_count % 5 == 0 {
|
|
||||||
log_jemalloc_stats();
|
|
||||||
}
|
|
||||||
|
|
||||||
prev_rss = stats.rss_bytes;
|
|
||||||
record_component("global");
|
|
||||||
|
|
||||||
if let Some(warning) = detector.check() {
|
|
||||||
warn!("[MONITOR] {}", warning);
|
|
||||||
stats.log();
|
|
||||||
log_component_stats();
|
|
||||||
log_thread_stats();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch to normal interval after startup period
|
|
||||||
if tick_count == startup_ticks {
|
|
||||||
trace!("[MONITOR] Switching to normal interval ({}s)", interval_secs);
|
|
||||||
interval = tokio::time::interval(normal_interval);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
prev_rss = stats.rss_bytes;
|
||||||
|
record_component("global");
|
||||||
|
|
||||||
|
if let Some(warning) = detector.check() {
|
||||||
|
warn!("[MONITOR] {}", warning);
|
||||||
|
stats.log();
|
||||||
|
log_component_stats();
|
||||||
|
log_thread_stats();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to normal interval after startup period
|
||||||
|
if tick_count == startup_ticks {
|
||||||
|
trace!("[MONITOR] Switching to normal interval ({}s)", interval_secs);
|
||||||
|
interval = tokio::time::interval(normal_interval);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "monitoring")]
|
||||||
|
#[cfg(feature = "monitoring")]
|
||||||
pub fn get_process_memory() -> Option<(u64, u64)> {
|
pub fn get_process_memory() -> Option<(u64, u64)> {
|
||||||
let pid = Pid::from_u32(std::process::id());
|
let pid = Pid::from_u32(std::process::id());
|
||||||
let mut sys = System::new();
|
let mut sys = System::new();
|
||||||
sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true);
|
sys.refresh_processes(ProcessesToUpdate::Some(&[pid]), true);
|
||||||
|
|
||||||
sys.process(pid).map(|p| (p.memory(), p.virtual_memory()))
|
|
||||||
|
sys.process(pid).map(|p| (p.memory(), p.virtual_memory()))
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "monitoring"))]
|
||||||
|
pub fn get_process_memory() -> Option<(u64, u64)> {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn log_process_memory() {
|
pub fn log_process_memory() {
|
||||||
if let Some((rss, virt)) = get_process_memory() {
|
if let Some((rss, virt)) = get_process_memory() {
|
||||||
trace!(
|
trace!(
|
||||||
"[PROCESS] RSS={}, Virtual={}",
|
"[PROCESS] RSS={}, Virtual={}",
|
||||||
MemoryStats::format_bytes(rss),
|
MemoryStats::format_bytes(rss),
|
||||||
MemoryStats::format_bytes(virt)
|
MemoryStats::format_bytes(virt)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_memory_stats() {
|
|
||||||
let stats = MemoryStats::current();
|
|
||||||
assert!(stats.rss_bytes > 0 || stats.virtual_bytes >= 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_format_bytes() {
|
fn test_memory_stats() {
|
||||||
assert_eq!(MemoryStats::format_bytes(500), "500 B");
|
let stats = MemoryStats::current();
|
||||||
assert_eq!(MemoryStats::format_bytes(1024), "1.00 KB");
|
assert!(stats.rss_bytes > 0 || stats.virtual_bytes >= 0);
|
||||||
assert_eq!(MemoryStats::format_bytes(1024 * 1024), "1.00 MB");
|
}
|
||||||
assert_eq!(MemoryStats::format_bytes(1024 * 1024 * 1024), "1.00 GB");
|
|
||||||
}
|
#[test]
|
||||||
|
fn test_format_bytes() {
|
||||||
#[test]
|
assert_eq!(MemoryStats::format_bytes(500), "500 B");
|
||||||
fn test_checkpoint() {
|
assert_eq!(MemoryStats::format_bytes(1024), "1.00 KB");
|
||||||
let checkpoint = MemoryCheckpoint::new("test");
|
assert_eq!(MemoryStats::format_bytes(1024 * 1024), "1.00 MB");
|
||||||
checkpoint.compare_and_log();
|
assert_eq!(MemoryStats::format_bytes(1024 * 1024 * 1024), "1.00 GB");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_thread_registry() {
|
fn test_checkpoint() {
|
||||||
register_thread("test-thread", "test-component");
|
let checkpoint = MemoryCheckpoint::new("test");
|
||||||
record_thread_activity("test-thread");
|
checkpoint.compare_and_log();
|
||||||
log_thread_stats();
|
}
|
||||||
unregister_thread("test-thread");
|
|
||||||
}
|
#[test]
|
||||||
|
fn test_thread_registry() {
|
||||||
|
register_thread("test-thread", "test-component");
|
||||||
|
record_thread_activity("test-thread");
|
||||||
|
log_thread_stats();
|
||||||
|
unregister_thread("test-thread");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use crate::core::shared::test_utils::create_mock_auth_service;
|
||||||
#[cfg(all(test, feature = "llm"))]
|
#[cfg(all(test, feature = "llm"))]
|
||||||
use crate::core::shared::test_utils::MockLLMProvider;
|
use crate::core::shared::test_utils::MockLLMProvider;
|
||||||
#[cfg(feature = "directory")]
|
#[cfg(feature = "directory")]
|
||||||
use crate::directory::AuthService;
|
use crate::core::directory::AuthService;
|
||||||
#[cfg(feature = "llm")]
|
#[cfg(feature = "llm")]
|
||||||
use crate::llm::LLMProvider;
|
use crate::llm::LLMProvider;
|
||||||
use crate::shared::models::BotResponse;
|
use crate::shared::models::BotResponse;
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ use crate::core::session::SessionManager;
|
||||||
use crate::core::shared::analytics::MetricsCollector;
|
use crate::core::shared::analytics::MetricsCollector;
|
||||||
use crate::core::shared::state::{AppState, Extensions};
|
use crate::core::shared::state::{AppState, Extensions};
|
||||||
#[cfg(feature = "directory")]
|
#[cfg(feature = "directory")]
|
||||||
use crate::directory::client::ZitadelConfig;
|
use crate::core::directory::client::ZitadelConfig;
|
||||||
#[cfg(feature = "directory")]
|
#[cfg(feature = "directory")]
|
||||||
use crate::directory::AuthService;
|
use crate::core::directory::AuthService;
|
||||||
#[cfg(feature = "llm")]
|
#[cfg(feature = "llm")]
|
||||||
use crate::llm::LLMProvider;
|
use crate::llm::LLMProvider;
|
||||||
use crate::shared::models::BotResponse;
|
use crate::shared::models::BotResponse;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
62
src/main.rs
62
src/main.rs
|
|
@ -29,11 +29,15 @@ pub mod tickets;
|
||||||
pub mod attendant;
|
pub mod attendant;
|
||||||
pub mod analytics;
|
pub mod analytics;
|
||||||
pub mod designer;
|
pub mod designer;
|
||||||
|
#[cfg(feature = "docs")]
|
||||||
pub mod docs;
|
pub mod docs;
|
||||||
pub mod learn;
|
pub mod learn;
|
||||||
|
#[cfg(feature = "paper")]
|
||||||
pub mod paper;
|
pub mod paper;
|
||||||
pub mod research;
|
pub mod research;
|
||||||
|
#[cfg(feature = "sheet")]
|
||||||
pub mod sheet;
|
pub mod sheet;
|
||||||
|
#[cfg(feature = "slides")]
|
||||||
pub mod slides;
|
pub mod slides;
|
||||||
pub mod social;
|
pub mod social;
|
||||||
pub mod sources;
|
pub mod sources;
|
||||||
|
|
@ -203,7 +207,7 @@ use crate::core::bot_database::BotDatabaseManager;
|
||||||
use crate::core::config::AppConfig;
|
use crate::core::config::AppConfig;
|
||||||
|
|
||||||
#[cfg(feature = "directory")]
|
#[cfg(feature = "directory")]
|
||||||
use crate::directory::auth_handler;
|
use crate::core::directory::auth_handler;
|
||||||
|
|
||||||
use package_manager::InstallMode;
|
use package_manager::InstallMode;
|
||||||
use session::{create_session, get_session_history, get_sessions, start_session};
|
use session::{create_session, get_session_history, get_sessions, start_session};
|
||||||
|
|
@ -448,11 +452,23 @@ async fn run_axum_server(
|
||||||
|
|
||||||
api_router = api_router.merge(crate::analytics::configure_analytics_routes());
|
api_router = api_router.merge(crate::analytics::configure_analytics_routes());
|
||||||
api_router = api_router.merge(crate::core::i18n::configure_i18n_routes());
|
api_router = api_router.merge(crate::core::i18n::configure_i18n_routes());
|
||||||
api_router = api_router.merge(crate::docs::configure_docs_routes());
|
#[cfg(feature = "docs")]
|
||||||
api_router = api_router.merge(crate::paper::configure_paper_routes());
|
{
|
||||||
api_router = api_router.merge(crate::sheet::configure_sheet_routes());
|
api_router = api_router.merge(crate::docs::configure_docs_routes());
|
||||||
api_router = api_router.merge(crate::slides::configure_slides_routes());
|
}
|
||||||
api_router = api_router.merge(crate::video::configure_video_routes());
|
#[cfg(feature = "paper")]
|
||||||
|
{
|
||||||
|
api_router = api_router.merge(crate::paper::configure_paper_routes());
|
||||||
|
}
|
||||||
|
#[cfg(feature = "sheet")]
|
||||||
|
{
|
||||||
|
api_router = api_router.merge(crate::sheet::configure_sheet_routes());
|
||||||
|
}
|
||||||
|
#[cfg(feature = "slides")]
|
||||||
|
{
|
||||||
|
api_router = api_router.merge(crate::slides::configure_slides_routes());
|
||||||
|
}
|
||||||
|
api_router = api_router.merge(crate::video::configure_video_routes());
|
||||||
api_router = api_router.merge(crate::video::ui::configure_video_ui_routes());
|
api_router = api_router.merge(crate::video::ui::configure_video_ui_routes());
|
||||||
api_router = api_router.merge(crate::research::configure_research_routes());
|
api_router = api_router.merge(crate::research::configure_research_routes());
|
||||||
api_router = api_router.merge(crate::research::ui::configure_research_ui_routes());
|
api_router = api_router.merge(crate::research::ui::configure_research_ui_routes());
|
||||||
|
|
@ -484,12 +500,18 @@ async fn run_axum_server(
|
||||||
api_router = api_router.merge(crate::player::configure_player_routes());
|
api_router = api_router.merge(crate::player::configure_player_routes());
|
||||||
api_router = api_router.merge(crate::canvas::configure_canvas_routes());
|
api_router = api_router.merge(crate::canvas::configure_canvas_routes());
|
||||||
api_router = api_router.merge(crate::canvas::ui::configure_canvas_ui_routes());
|
api_router = api_router.merge(crate::canvas::ui::configure_canvas_ui_routes());
|
||||||
api_router = api_router.merge(crate::social::configure_social_routes());
|
api_router = api_router.merge(crate::social::configure_social_routes());
|
||||||
api_router = api_router.merge(crate::social::ui::configure_social_ui_routes());
|
api_router = api_router.merge(crate::social::ui::configure_social_ui_routes());
|
||||||
api_router = api_router.merge(crate::email::ui::configure_email_ui_routes());
|
api_router = api_router.merge(crate::learn::ui::configure_learn_ui_routes());
|
||||||
api_router = api_router.merge(crate::learn::ui::configure_learn_ui_routes());
|
#[cfg(feature = "email")]
|
||||||
api_router = api_router.merge(crate::meet::ui::configure_meet_ui_routes());
|
{
|
||||||
api_router = api_router.merge(crate::contacts::crm_ui::configure_crm_routes());
|
api_router = api_router.merge(crate::email::ui::configure_email_ui_routes());
|
||||||
|
}
|
||||||
|
#[cfg(feature = "meet")]
|
||||||
|
{
|
||||||
|
api_router = api_router.merge(crate::meet::ui::configure_meet_ui_routes());
|
||||||
|
}
|
||||||
|
api_router = api_router.merge(crate::contacts::crm_ui::configure_crm_routes());
|
||||||
api_router = api_router.merge(crate::contacts::crm::configure_crm_api_routes());
|
api_router = api_router.merge(crate::contacts::crm::configure_crm_api_routes());
|
||||||
api_router = api_router.merge(crate::billing::billing_ui::configure_billing_routes());
|
api_router = api_router.merge(crate::billing::billing_ui::configure_billing_routes());
|
||||||
api_router = api_router.merge(crate::billing::api::configure_billing_api_routes());
|
api_router = api_router.merge(crate::billing::api::configure_billing_api_routes());
|
||||||
|
|
@ -1060,7 +1082,7 @@ use crate::core::config::ConfigManager;
|
||||||
|
|
||||||
info!("Loaded Zitadel config from {}: url={}", config_path, base_url);
|
info!("Loaded Zitadel config from {}: url={}", config_path, base_url);
|
||||||
|
|
||||||
crate::directory::client::ZitadelConfig {
|
crate::core::directory::client::ZitadelConfig {
|
||||||
issuer_url: base_url.to_string(),
|
issuer_url: base_url.to_string(),
|
||||||
issuer: base_url.to_string(),
|
issuer: base_url.to_string(),
|
||||||
client_id: client_id.to_string(),
|
client_id: client_id.to_string(),
|
||||||
|
|
@ -1072,7 +1094,7 @@ use crate::core::config::ConfigManager;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("Failed to parse directory_config.json, using defaults");
|
warn!("Failed to parse directory_config.json, using defaults");
|
||||||
crate::directory::client::ZitadelConfig {
|
crate::core::directory::client::ZitadelConfig {
|
||||||
issuer_url: "http://localhost:8300".to_string(),
|
issuer_url: "http://localhost:8300".to_string(),
|
||||||
issuer: "http://localhost:8300".to_string(),
|
issuer: "http://localhost:8300".to_string(),
|
||||||
client_id: String::new(),
|
client_id: String::new(),
|
||||||
|
|
@ -1085,7 +1107,7 @@ use crate::core::config::ConfigManager;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
warn!("directory_config.json not found, using default Zitadel config");
|
warn!("directory_config.json not found, using default Zitadel config");
|
||||||
crate::directory::client::ZitadelConfig {
|
crate::core::directory::client::ZitadelConfig {
|
||||||
issuer_url: "http://localhost:8300".to_string(),
|
issuer_url: "http://localhost:8300".to_string(),
|
||||||
issuer: "http://localhost:8300".to_string(),
|
issuer: "http://localhost:8300".to_string(),
|
||||||
client_id: String::new(),
|
client_id: String::new(),
|
||||||
|
|
@ -1099,7 +1121,7 @@ use crate::core::config::ConfigManager;
|
||||||
};
|
};
|
||||||
#[cfg(feature = "directory")]
|
#[cfg(feature = "directory")]
|
||||||
let auth_service = Arc::new(tokio::sync::Mutex::new(
|
let auth_service = Arc::new(tokio::sync::Mutex::new(
|
||||||
crate::directory::AuthService::new(zitadel_config.clone()).map_err(|e| std::io::Error::other(format!("Failed to create auth service: {}", e)))?,
|
crate::core::directory::AuthService::new(zitadel_config.clone()).map_err(|e| std::io::Error::other(format!("Failed to create auth service: {}", e)))?,
|
||||||
));
|
));
|
||||||
|
|
||||||
#[cfg(feature = "directory")]
|
#[cfg(feature = "directory")]
|
||||||
|
|
@ -1110,22 +1132,22 @@ use crate::core::config::ConfigManager;
|
||||||
Ok(pat_token) => {
|
Ok(pat_token) => {
|
||||||
let pat_token = pat_token.trim().to_string();
|
let pat_token = pat_token.trim().to_string();
|
||||||
info!("Using admin PAT token for bootstrap authentication");
|
info!("Using admin PAT token for bootstrap authentication");
|
||||||
crate::directory::client::ZitadelClient::with_pat_token(zitadel_config, pat_token)
|
crate::core::directory::client::ZitadelClient::with_pat_token(zitadel_config, pat_token)
|
||||||
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client with PAT: {}", e)))?
|
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client with PAT: {}", e)))?
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Failed to read admin PAT token: {}, falling back to OAuth2", e);
|
warn!("Failed to read admin PAT token: {}, falling back to OAuth2", e);
|
||||||
crate::directory::client::ZitadelClient::new(zitadel_config)
|
crate::core::directory::client::ZitadelClient::new(zitadel_config)
|
||||||
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client: {}", e)))?
|
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client: {}", e)))?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
info!("Admin PAT not found, using OAuth2 client credentials for bootstrap");
|
info!("Admin PAT not found, using OAuth2 client credentials for bootstrap");
|
||||||
crate::directory::client::ZitadelClient::new(zitadel_config)
|
crate::core::directory::client::ZitadelClient::new(zitadel_config)
|
||||||
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client: {}", e)))?
|
.map_err(|e| std::io::Error::other(format!("Failed to create bootstrap client: {}", e)))?
|
||||||
};
|
};
|
||||||
|
|
||||||
match crate::directory::bootstrap::check_and_bootstrap_admin(&bootstrap_client).await {
|
match crate::core::directory::bootstrap::check_and_bootstrap_admin(&bootstrap_client).await {
|
||||||
Ok(Some(_)) => {
|
Ok(Some(_)) => {
|
||||||
info!("Bootstrap completed - admin credentials displayed in console");
|
info!("Bootstrap completed - admin credentials displayed in console");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
use axum::{extract::State, response::Html, routing::get, Router};
|
use axum::{extract::State, response::Html, routing::get, Router};
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
#[cfg(feature = "monitoring")]
|
||||||
use sysinfo::{Disks, Networks, System};
|
use sysinfo::{Disks, Networks, System};
|
||||||
|
|
||||||
use crate::core::urls::ApiUrls;
|
use crate::core::urls::ApiUrls;
|
||||||
|
|
@ -10,35 +10,35 @@ use crate::shared::state::AppState;
|
||||||
pub mod real_time;
|
pub mod real_time;
|
||||||
pub mod tracing;
|
pub mod tracing;
|
||||||
|
|
||||||
|
|
||||||
pub fn configure() -> Router<Arc<AppState>> {
|
pub fn configure() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route(ApiUrls::MONITORING_DASHBOARD, get(dashboard))
|
.route(ApiUrls::MONITORING_DASHBOARD, get(dashboard))
|
||||||
.route(ApiUrls::MONITORING_SERVICES, get(services))
|
.route(ApiUrls::MONITORING_SERVICES, get(services))
|
||||||
.route(ApiUrls::MONITORING_RESOURCES, get(resources))
|
.route(ApiUrls::MONITORING_RESOURCES, get(resources))
|
||||||
.route(ApiUrls::MONITORING_LOGS, get(logs))
|
.route(ApiUrls::MONITORING_LOGS, get(logs))
|
||||||
.route(ApiUrls::MONITORING_LLM, get(llm_metrics))
|
.route(ApiUrls::MONITORING_LLM, get(llm_metrics))
|
||||||
.route(ApiUrls::MONITORING_HEALTH, get(health))
|
.route(ApiUrls::MONITORING_HEALTH, get(health))
|
||||||
// Additional endpoints expected by the frontend
|
// Additional endpoints expected by the frontend
|
||||||
.route("/api/ui/monitoring/timestamp", get(timestamp))
|
.route("/api/ui/monitoring/timestamp", get(timestamp))
|
||||||
.route("/api/ui/monitoring/bots", get(bots))
|
.route("/api/ui/monitoring/bots", get(bots))
|
||||||
.route("/api/ui/monitoring/services/status", get(services_status))
|
.route("/api/ui/monitoring/services/status", get(services_status))
|
||||||
.route("/api/ui/monitoring/resources/bars", get(resources_bars))
|
.route("/api/ui/monitoring/resources/bars", get(resources_bars))
|
||||||
.route("/api/ui/monitoring/activity/latest", get(activity_latest))
|
.route("/api/ui/monitoring/activity/latest", get(activity_latest))
|
||||||
.route("/api/ui/monitoring/metric/sessions", get(metric_sessions))
|
.route("/api/ui/monitoring/metric/sessions", get(metric_sessions))
|
||||||
.route("/api/ui/monitoring/metric/messages", get(metric_messages))
|
.route("/api/ui/monitoring/metric/messages", get(metric_messages))
|
||||||
.route("/api/ui/monitoring/metric/response_time", get(metric_response_time))
|
.route("/api/ui/monitoring/metric/response_time", get(metric_response_time))
|
||||||
.route("/api/ui/monitoring/trend/sessions", get(trend_sessions))
|
.route("/api/ui/monitoring/trend/sessions", get(trend_sessions))
|
||||||
.route("/api/ui/monitoring/rate/messages", get(rate_messages))
|
.route("/api/ui/monitoring/rate/messages", get(rate_messages))
|
||||||
// Aliases for frontend compatibility
|
// Aliases for frontend compatibility
|
||||||
.route("/api/ui/monitoring/sessions", get(sessions_panel))
|
.route("/api/ui/monitoring/sessions", get(sessions_panel))
|
||||||
.route("/api/ui/monitoring/messages", get(messages_panel))
|
.route("/api/ui/monitoring/messages", get(messages_panel))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn dashboard(State(state): State<Arc<AppState>>) -> Html<String> {
|
async fn dashboard(State(state): State<Arc<AppState>>) -> Html<String> {
|
||||||
let mut sys = System::new_all();
|
#[cfg(feature = "monitoring")]
|
||||||
sys.refresh_all();
|
let (cpu_usage, total_memory, used_memory, memory_percent, uptime_str) = {
|
||||||
|
let mut sys = System::new_all();
|
||||||
|
sys.refresh_all();
|
||||||
|
|
||||||
let cpu_usage = sys.global_cpu_usage();
|
let cpu_usage = sys.global_cpu_usage();
|
||||||
let total_memory = sys.total_memory();
|
let total_memory = sys.total_memory();
|
||||||
|
|
@ -51,140 +51,120 @@ async fn dashboard(State(state): State<Arc<AppState>>) -> Html<String> {
|
||||||
|
|
||||||
let uptime = System::uptime();
|
let uptime = System::uptime();
|
||||||
let uptime_str = format_uptime(uptime);
|
let uptime_str = format_uptime(uptime);
|
||||||
|
|
||||||
|
(cpu_usage, total_memory, used_memory, memory_percent, uptime_str)
|
||||||
|
};
|
||||||
|
|
||||||
let active_sessions = state
|
#[cfg(not(feature = "monitoring"))]
|
||||||
.session_manager
|
let (cpu_usage, total_memory, used_memory, memory_percent, uptime_str) = (
|
||||||
.try_lock()
|
0.0, 0, 0, 0.0, "N/A".to_string()
|
||||||
.map(|sm| sm.active_count())
|
);
|
||||||
.unwrap_or(0);
|
|
||||||
|
|
||||||
Html(format!(
|
let active_sessions = state
|
||||||
r##"<div class="dashboard-grid">
|
.session_manager
|
||||||
<div class="metric-card">
|
.try_lock()
|
||||||
<div class="metric-header">
|
.map(|sm| sm.active_count())
|
||||||
<span class="metric-title">CPU Usage</span>
|
.unwrap_or(0);
|
||||||
<span class="metric-badge {cpu_status}">{cpu_usage:.1}%</span>
|
|
||||||
</div>
|
Html(format!(
|
||||||
<div class="metric-value">{cpu_usage:.1}%</div>
|
r##"<div class="dashboard-grid">
|
||||||
<div class="metric-bar">
|
<div class="metric-card">
|
||||||
<div class="metric-bar-fill" style="width: {cpu_usage}%"></div>
|
<div class="metric-header">
|
||||||
</div>
|
<span class="metric-title">CPU Usage</span>
|
||||||
|
<span class="metric-badge {cpu_status}">{cpu_usage:.1}%</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="metric-value">{cpu_usage:.1}%</div>
|
||||||
<div class="metric-card">
|
<div class="metric-bar">
|
||||||
<div class="metric-header">
|
<div class="metric-bar-fill" style="width: {cpu_usage}%"></div>
|
||||||
<span class="metric-title">Memory</span>
|
|
||||||
<span class="metric-badge {mem_status}">{memory_percent:.1}%</span>
|
|
||||||
</div>
|
|
||||||
<div class="metric-value">{used_gb:.1} GB / {total_gb:.1} GB</div>
|
|
||||||
<div class="metric-bar">
|
|
||||||
<div class="metric-bar-fill" style="width: {memory_percent}%"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="metric-card">
|
|
||||||
<div class="metric-header">
|
|
||||||
<span class="metric-title">Active Sessions</span>
|
|
||||||
</div>
|
|
||||||
<div class="metric-value">{active_sessions}</div>
|
|
||||||
<div class="metric-subtitle">Current conversations</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="metric-card">
|
|
||||||
<div class="metric-header">
|
|
||||||
<span class="metric-title">Uptime</span>
|
|
||||||
</div>
|
|
||||||
<div class="metric-value">{uptime_str}</div>
|
|
||||||
<div class="metric-subtitle">System running time</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="refresh-indicator" hx-get="/api/monitoring/dashboard" hx-trigger="every 10s" hx-swap="outerHTML" hx-target="closest .dashboard-grid, .refresh-indicator">
|
<div class="metric-card">
|
||||||
<span class="refresh-dot"></span> Auto-refreshing
|
<div class="metric-header">
|
||||||
</div>"##,
|
<span class="metric-title">Memory</span>
|
||||||
cpu_status = if cpu_usage > 80.0 {
|
<span class="metric-badge {mem_status}">{memory_percent:.1}%</span>
|
||||||
"danger"
|
</div>
|
||||||
} else if cpu_usage > 60.0 {
|
<div class="metric-value">{used_gb:.1} GB / {total_gb:.1} GB</div>
|
||||||
"warning"
|
<div class="metric-bar">
|
||||||
} else {
|
<div class="metric-bar-fill" style="width: {memory_percent}%"></div>
|
||||||
"success"
|
</div>
|
||||||
},
|
</div>
|
||||||
mem_status = if memory_percent > 80.0 {
|
|
||||||
"danger"
|
|
||||||
} else if memory_percent > 60.0 {
|
|
||||||
"warning"
|
|
||||||
} else {
|
|
||||||
"success"
|
|
||||||
},
|
|
||||||
used_gb = used_memory as f64 / 1_073_741_824.0,
|
|
||||||
total_gb = total_memory as f64 / 1_073_741_824.0,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-header">
|
||||||
|
<span class="metric-title">Active Sessions</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-value">{active_sessions}</div>
|
||||||
|
<div class="metric-subtitle">Current conversations</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-header">
|
||||||
|
<span class="metric-title">Uptime</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-value">{uptime_str}</div>
|
||||||
|
<div class="metric-subtitle">System running time</div>
|
||||||
|
</div>
|
||||||
|
</div><div class="refresh-indicator" hx-get="/api/monitoring/dashboard" hx-trigger="every 10s" hx-swap="outerHTML" hx-target="closest .dashboard-grid, .refresh-indicator"> <span class="refresh-dot"></span> Auto-refreshing </div>"##, cpu_status = if cpu_usage > 80.0 { "danger" } else if cpu_usage > 60.0 { "warning" } else { "success" }, mem_status = if memory_percent > 80.0 { "danger" } else if memory_percent > 60.0 { "warning" } else { "success" }, used_gb = used_memory as f64 / 1_073_741_824.0, total_gb = total_memory as f64 / 1_073_741_824.0, )) }
|
||||||
|
|
||||||
async fn services(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn services(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
let services = vec![
|
let services = vec![
|
||||||
("PostgreSQL", check_postgres(), "Database"),
|
("PostgreSQL", check_postgres(), "Database"),
|
||||||
("Redis", check_redis(), "Cache"),
|
("Redis", check_redis(), "Cache"),
|
||||||
("MinIO", check_minio(), "Storage"),
|
("MinIO", check_minio(), "Storage"),
|
||||||
("LLM Server", check_llm(), "AI Backend"),
|
("LLM Server", check_llm(), "AI Backend"),
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut rows = String::new();
|
let mut rows = String::new();
|
||||||
for (name, status, desc) in services {
|
for (name, status, desc) in services {
|
||||||
let (status_class, status_text) = if status {
|
let (status_class, status_text) = if status {
|
||||||
("success", "Running")
|
("success", "Running")
|
||||||
} else {
|
} else {
|
||||||
("danger", "Stopped")
|
("danger", "Stopped")
|
||||||
};
|
};
|
||||||
|
|
||||||
rows.push_str(&format!(
|
rows.push_str(&format!(
|
||||||
r##"<tr>
|
r##"<tr>
|
||||||
<td>
|
<td>
|
||||||
<div class="service-name">
|
<div class="service-name">
|
||||||
<span class="status-dot {status_class}"></span>
|
<span class="status-dot {status_class}"></span>
|
||||||
{name}
|
{name}
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>{desc}</td>
|
|
||||||
<td><span class="status-badge {status_class}">{status_text}</span></td>
|
|
||||||
<td>
|
|
||||||
<button class="btn-sm" hx-post="/api/monitoring/services/{name_lower}/restart" hx-swap="none">Restart</button>
|
|
||||||
</td>
|
|
||||||
</tr>"##,
|
|
||||||
name_lower = name.to_lowercase().replace(' ', "-"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Html(format!(
|
|
||||||
r##"<div class="services-view">
|
|
||||||
<div class="section-header">
|
|
||||||
<h2>Services Status</h2>
|
|
||||||
<button class="btn-secondary" hx-get="/api/monitoring/services" hx-target="#monitoring-content" hx-swap="innerHTML">
|
|
||||||
Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<table class="data-table">
|
</td>
|
||||||
<thead>
|
<td>{desc}</td>
|
||||||
<tr>
|
<td><span class="status-badge {status_class}">{status_text}</span></td>
|
||||||
<th>Service</th>
|
<td>
|
||||||
<th>Description</th>
|
<button class="btn-sm" hx-post="/api/monitoring/services/{name_lower}/restart" hx-swap="none">Restart</button>
|
||||||
<th>Status</th>
|
</td>
|
||||||
<th>Actions</th>
|
</tr>"##, name_lower = name.to_lowercase().replace(' ', "-"), )); }
|
||||||
</tr>
|
Html(format!(
|
||||||
</thead>
|
r##"<div class="services-view">
|
||||||
<tbody>
|
<div class="section-header">
|
||||||
{rows}
|
<h2>Services Status</h2>
|
||||||
</tbody>
|
<button class="btn-secondary" hx-get="/api/monitoring/services" hx-target="#monitoring-content" hx-swap="innerHTML">
|
||||||
</table>
|
Refresh
|
||||||
</div>"##
|
</button>
|
||||||
))
|
</div>
|
||||||
}
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Service</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>"## )) }
|
||||||
|
|
||||||
async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
let mut sys = System::new_all();
|
#[cfg(feature = "monitoring")]
|
||||||
sys.refresh_all();
|
let (disk_rows, net_rows) = {
|
||||||
|
let mut sys = System::new_all();
|
||||||
|
sys.refresh_all();
|
||||||
|
|
||||||
let disks = Disks::new_with_refreshed_list();
|
let disks = Disks::new_with_refreshed_list();
|
||||||
let mut disk_rows = String::new();
|
let mut disk_rows = String::new();
|
||||||
|
|
@ -201,273 +181,246 @@ async fn resources(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
|
|
||||||
disk_rows.push_str(&format!(
|
disk_rows.push_str(&format!(
|
||||||
r##"<tr>
|
r##"<tr>
|
||||||
<td>{mount}</td>
|
<td>{mount}</td>
|
||||||
<td>{used_gb:.1} GB</td>
|
<td>{used_gb:.1} GB</td>
|
||||||
<td>{total_gb:.1} GB</td>
|
<td>{total_gb:.1} GB</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="usage-bar">
|
<div class="usage-bar">
|
||||||
<div class="usage-fill {status}" style="width: {percent:.0}%"></div>
|
<div class="usage-fill {status}" style="width: {percent:.0}%"></div>
|
||||||
</div>
|
</div>
|
||||||
<span class="usage-text">{percent:.1}%</span>
|
<span class="usage-text">{percent:.1}%</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>"##,
|
</tr>"##, mount = disk.mount_point().display(), used_gb = used as f64 / 1_073_741_824.0, total_gb = total as f64 / 1_073_741_824.0, status = if percent > 90.0 { "danger" } else if percent > 70.0 { "warning" } else { "success" }, )); }
|
||||||
mount = disk.mount_point().display(),
|
|
||||||
used_gb = used as f64 / 1_073_741_824.0,
|
|
||||||
total_gb = total as f64 / 1_073_741_824.0,
|
|
||||||
status = if percent > 90.0 {
|
|
||||||
"danger"
|
|
||||||
} else if percent > 70.0 {
|
|
||||||
"warning"
|
|
||||||
} else {
|
|
||||||
"success"
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let networks = Networks::new_with_refreshed_list();
|
let networks = Networks::new_with_refreshed_list();
|
||||||
let mut net_rows = String::new();
|
let mut net_rows = String::new();
|
||||||
|
|
||||||
for (name, data) in networks.list() {
|
for (name, data) in networks.list() {
|
||||||
net_rows.push_str(&format!(
|
net_rows.push_str(&format!(
|
||||||
r##"<tr>
|
r##"<tr>
|
||||||
<td>{name}</td>
|
<td>{name}</td>
|
||||||
<td>{rx:.2} MB</td>
|
<td>{rx:.2} MB</td>
|
||||||
<td>{tx:.2} MB</td>
|
<td>{tx:.2} MB</td>
|
||||||
</tr>"##,
|
</tr>"##, rx = data.total_received() as f64 / 1_048_576.0, tx = data.total_transmitted() as f64 / 1_048_576.0, )); }
|
||||||
rx = data.total_received() as f64 / 1_048_576.0,
|
(disk_rows, net_rows)
|
||||||
tx = data.total_transmitted() as f64 / 1_048_576.0,
|
};
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Html(format!(
|
#[cfg(not(feature = "monitoring"))]
|
||||||
r##"<div class="resources-view">
|
let (disk_rows, net_rows) = (
|
||||||
<div class="section-header">
|
String::new(),
|
||||||
<h2>System Resources</h2>
|
String::new()
|
||||||
</div>
|
);
|
||||||
|
|
||||||
<div class="resource-section">
|
Html(format!(
|
||||||
<h3>Disk Usage</h3>
|
r##"<div class="resources-view">
|
||||||
<table class="data-table">
|
<div class="section-header">
|
||||||
<thead>
|
<h2>System Resources</h2>
|
||||||
<tr>
|
</div>
|
||||||
<th>Mount</th>
|
|
||||||
<th>Used</th>
|
|
||||||
<th>Total</th>
|
|
||||||
<th>Usage</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{disk_rows}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="resource-section">
|
<div class="resource-section">
|
||||||
<h3>Network</h3>
|
<h3>Disk Usage</h3>
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Interface</th>
|
<th>Mount</th>
|
||||||
<th>Received</th>
|
<th>Used</th>
|
||||||
<th>Transmitted</th>
|
<th>Total</th>
|
||||||
</tr>
|
<th>Usage</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody>
|
</thead>
|
||||||
{net_rows}
|
<tbody>
|
||||||
</tbody>
|
{disk_rows}
|
||||||
</table>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
</div>"##
|
</div>
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
<div class="resource-section">
|
||||||
|
<h3>Network</h3>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Interface</th>
|
||||||
|
<th>Received</th>
|
||||||
|
<th>Transmitted</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{net_rows}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>"## )) }
|
||||||
|
|
||||||
async fn logs(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn logs(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html(
|
Html(
|
||||||
r##"<div class="logs-view">
|
r##"<div class="logs-view">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>System Logs</h2>
|
<h2>System Logs</h2>
|
||||||
<div class="log-controls">
|
<div class="log-controls">
|
||||||
<select id="log-level" onchange="filterLogs(this.value)">
|
<select id="log-level" onchange="filterLogs(this.value)">
|
||||||
<option value="all">All Levels</option>
|
<option value="all">All Levels</option>
|
||||||
<option value="error">Error</option>
|
<option value="error">Error</option>
|
||||||
<option value="warn">Warning</option>
|
<option value="warn">Warning</option>
|
||||||
<option value="info">Info</option>
|
<option value="info">Info</option>
|
||||||
<option value="debug">Debug</option>
|
<option value="debug">Debug</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn-secondary" onclick="clearLogs()">Clear</button>
|
<button class="btn-secondary" onclick="clearLogs()">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="log-container" id="log-container"
|
<div class="log-container" id="log-container"
|
||||||
hx-get="/api/monitoring/logs/stream"
|
hx-get="/api/monitoring/logs/stream"
|
||||||
hx-trigger="every 2s"
|
hx-trigger="every 2s"
|
||||||
hx-swap="beforeend scroll:bottom">
|
hx-swap="beforeend scroll:bottom">
|
||||||
<div class="log-entry info">
|
<div class="log-entry info">
|
||||||
<span class="log-time">System ready</span>
|
<span class="log-time">System ready</span>
|
||||||
<span class="log-level">INFO</span>
|
<span class="log-level">INFO</span>
|
||||||
<span class="log-message">Monitoring initialized</span>
|
<span class="log-message">Monitoring initialized</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>"##
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
</div>"## .to_string(), ) }
|
||||||
|
|
||||||
async fn llm_metrics(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn llm_metrics(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html(
|
Html(
|
||||||
r##"<div class="llm-metrics-view">
|
r##"<div class="llm-metrics-view">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>LLM Metrics</h2>
|
<h2>LLM Metrics</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="metrics-grid">
|
<div class="metrics-grid">
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<div class="metric-title">Total Requests</div>
|
<div class="metric-title">Total Requests</div>
|
||||||
<div class="metric-value" id="llm-total-requests"
|
<div class="metric-value" id="llm-total-requests"
|
||||||
hx-get="/api/monitoring/llm/total"
|
hx-get="/api/monitoring/llm/total"
|
||||||
hx-trigger="load, every 30s"
|
hx-trigger="load, every 30s"
|
||||||
hx-swap="innerHTML">
|
hx-swap="innerHTML">
|
||||||
--
|
--
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="metric-card">
|
|
||||||
<div class="metric-title">Cache Hit Rate</div>
|
|
||||||
<div class="metric-value" id="llm-cache-rate"
|
|
||||||
hx-get="/api/monitoring/llm/cache-rate"
|
|
||||||
hx-trigger="load, every 30s"
|
|
||||||
hx-swap="innerHTML">
|
|
||||||
--
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="metric-card">
|
|
||||||
<div class="metric-title">Avg Latency</div>
|
|
||||||
<div class="metric-value" id="llm-latency"
|
|
||||||
hx-get="/api/monitoring/llm/latency"
|
|
||||||
hx-trigger="load, every 30s"
|
|
||||||
hx-swap="innerHTML">
|
|
||||||
--
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="metric-card">
|
|
||||||
<div class="metric-title">Total Tokens</div>
|
|
||||||
<div class="metric-value" id="llm-tokens"
|
|
||||||
hx-get="/api/monitoring/llm/tokens"
|
|
||||||
hx-trigger="load, every 30s"
|
|
||||||
hx-swap="innerHTML">
|
|
||||||
--
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>"##
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-title">Cache Hit Rate</div>
|
||||||
|
<div class="metric-value" id="llm-cache-rate"
|
||||||
|
hx-get="/api/monitoring/llm/cache-rate"
|
||||||
|
hx-trigger="load, every 30s"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
--
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-title">Avg Latency</div>
|
||||||
|
<div class="metric-value" id="llm-latency"
|
||||||
|
hx-get="/api/monitoring/llm/latency"
|
||||||
|
hx-trigger="load, every 30s"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
--
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="metric-card">
|
||||||
|
<div class="metric-title">Total Tokens</div>
|
||||||
|
<div class="metric-value" id="llm-tokens"
|
||||||
|
hx-get="/api/monitoring/llm/tokens"
|
||||||
|
hx-trigger="load, every 30s"
|
||||||
|
hx-swap="innerHTML">
|
||||||
|
--
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>"## .to_string(), ) }
|
||||||
|
|
||||||
async fn health(State(state): State<Arc<AppState>>) -> Html<String> {
|
async fn health(State(state): State<Arc<AppState>>) -> Html<String> {
|
||||||
let db_ok = state.conn.get().is_ok();
|
let db_ok = state.conn.get().is_ok();
|
||||||
let status = if db_ok { "healthy" } else { "degraded" };
|
let status = if db_ok { "healthy" } else { "degraded" };
|
||||||
|
|
||||||
Html(format!(
|
Html(format!(
|
||||||
r##"<div class="health-status {status}">
|
r##"<div class="health-status {status}">
|
||||||
<span class="status-icon"></span>
|
<span class="status-icon"></span>
|
||||||
<span class="status-text">{status}</span>
|
<span class="status-text">{status}</span>
|
||||||
</div>"##
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
</iv>"## )) }
|
||||||
|
|
||||||
fn format_uptime(seconds: u64) -> String {
|
fn format_uptime(seconds: u64) -> String {
|
||||||
let days = seconds / 86400;
|
let days = seconds / 86400;
|
||||||
let hours = (seconds % 86400) / 3600;
|
let hours = (seconds % 86400) / 3600;
|
||||||
let minutes = (seconds % 3600) / 60;
|
let minutes = (seconds % 3600) / 60;
|
||||||
|
|
||||||
if days > 0 {
|
if days > 0 {
|
||||||
format!("{}d {}h {}m", days, hours, minutes)
|
format!("{}d {}h {}m", days, hours, minutes)
|
||||||
} else if hours > 0 {
|
} else if hours > 0 {
|
||||||
format!("{}h {}m", hours, minutes)
|
format!("{}h {}m", hours, minutes)
|
||||||
} else {
|
} else {
|
||||||
format!("{}m", minutes)
|
format!("{}m", minutes)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
fn check_postgres() -> bool {
|
fn check_postgres() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn check_redis() -> bool {
|
fn check_redis() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn check_minio() -> bool {
|
fn check_minio() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn check_llm() -> bool {
|
fn check_llm() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn timestamp(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn timestamp(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
Html(format!("Last updated: {}", now.format("%H:%M:%S")))
|
Html(format!("Last updated: {}", now.format("%H:%M:%S")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn bots(State(state): State<Arc<AppState>>) -> Html<String> {
|
async fn bots(State(state): State<Arc<AppState>>) -> Html<String> {
|
||||||
let active_sessions = state
|
let active_sessions = state
|
||||||
.session_manager
|
.session_manager
|
||||||
.try_lock()
|
.try_lock()
|
||||||
.map(|sm| sm.active_count())
|
.map(|sm| sm.active_count())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
Html(format!(
|
|
||||||
r##"<div class="bots-list">
|
|
||||||
<div class="bot-item">
|
|
||||||
<span class="bot-name">Active Sessions</span>
|
|
||||||
<span class="bot-count">{active_sessions}</span>
|
|
||||||
</div>
|
|
||||||
</div>"##
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Html(format!(
|
||||||
|
r##"<div class="bots-list">
|
||||||
|
<div class="bot-item">
|
||||||
|
<span class="bot-name">Active Sessions</span>
|
||||||
|
<span class="bot-count">{active_sessions}</span>
|
||||||
|
</div>
|
||||||
|
</div>"## )) }
|
||||||
|
|
||||||
async fn services_status(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn services_status(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
let services = vec![
|
let services = vec![
|
||||||
("postgresql", check_postgres()),
|
("postgresql", check_postgres()),
|
||||||
("redis", check_redis()),
|
("redis", check_redis()),
|
||||||
("minio", check_minio()),
|
("minio", check_minio()),
|
||||||
("llm", check_llm()),
|
("llm", check_llm()),
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut status_updates = String::new();
|
let mut status_updates = String::new();
|
||||||
for (name, running) in services {
|
for (name, running) in services {
|
||||||
let status = if running { "running" } else { "stopped" };
|
let status = if running { "running" } else { "stopped" };
|
||||||
status_updates.push_str(&format!(
|
status_updates.push_str(&format!(
|
||||||
r##"<script>
|
r##"<script>
|
||||||
(function() {{
|
(function() {{
|
||||||
var el = document.querySelector('[data-service="{name}"]');
|
var el = document.querySelector('[data-service="{name}"]');
|
||||||
if (el) el.setAttribute('data-status', '{status}');
|
if (el) el.setAttribute('data-status', '{status}');
|
||||||
}})();
|
}})();
|
||||||
</script>"##
|
</script>"##
|
||||||
));
|
));
|
||||||
}
|
|
||||||
|
|
||||||
Html(status_updates)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Html(status_updates)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
async fn resources_bars(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn resources_bars(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
let mut sys = System::new_all();
|
#[cfg(feature = "monitoring")]
|
||||||
sys.refresh_all();
|
let (cpu_usage, memory_percent) = {
|
||||||
|
let mut sys = System::new_all();
|
||||||
|
sys.refresh_all();
|
||||||
|
|
||||||
let cpu_usage = sys.global_cpu_usage();
|
let cpu_usage = sys.global_cpu_usage();
|
||||||
let total_memory = sys.total_memory();
|
let total_memory = sys.total_memory();
|
||||||
|
|
@ -477,97 +430,82 @@ async fn resources_bars(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
|
(cpu_usage, memory_percent)
|
||||||
|
};
|
||||||
|
|
||||||
Html(format!(
|
#[cfg(not(feature = "monitoring"))]
|
||||||
r##"<g>
|
let (cpu_usage, memory_percent) = (0.0, 0.0);
|
||||||
<text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">CPU</text>
|
|
||||||
<rect x="40" y="-8" width="100" height="10" rx="2" fill="#1e293b"/>
|
|
||||||
<rect x="40" y="-8" width="{cpu_width}" height="10" rx="2" fill="#3b82f6"/>
|
|
||||||
<text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{cpu_usage:.0}%</text>
|
|
||||||
</g>
|
|
||||||
<g transform="translate(0, 20)">
|
|
||||||
<text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">MEM</text>
|
|
||||||
<rect x="40" y="-8" width="100" height="10" rx="2" fill="#1e293b"/>
|
|
||||||
<rect x="40" y="-8" width="{mem_width}" height="10" rx="2" fill="#10b981"/>
|
|
||||||
<text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{memory_percent:.0}%</text>
|
|
||||||
</g>"##,
|
|
||||||
cpu_width = cpu_usage.min(100.0),
|
|
||||||
mem_width = memory_percent.min(100.0),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
Html(format!(
|
||||||
|
r##"<g>
|
||||||
|
<text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">CPU</text>
|
||||||
|
<rect x="40" y="-8" width="100" height="10" rx="2" fill="#1e293b"/>
|
||||||
|
<rect x="40" y="-8" width="{cpu_width}" height="10" rx="2" fill="#3b82f6"/>
|
||||||
|
<text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{cpu_usage:.0}%</text>
|
||||||
|
</g> <g transform="translate(0, 20)"> <text x="0" y="0" fill="#94a3b8" font-family="system-ui" font-size="10">MEM</text> <rect x="40" y="-8" width="100" height="10" rx="2" fill="#1e293b"/> <rect x="40" y="-8" width="{mem_width}" height="10" rx="2" fill="#10b981"/> <text x="150" y="0" fill="#f8fafc" font-family="system-ui" font-size="10">{memory_percent:.0}%</text> </g>"##, cpu_width = cpu_usage.min(100.0), mem_width = memory_percent.min(100.0), )) }
|
||||||
|
|
||||||
async fn activity_latest(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn activity_latest(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html("System monitoring active...".to_string())
|
Html("System monitoring active...".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn metric_sessions(State(state): State<Arc<AppState>>) -> Html<String> {
|
async fn metric_sessions(State(state): State<Arc<AppState>>) -> Html<String> {
|
||||||
let active_sessions = state
|
let active_sessions = state
|
||||||
.session_manager
|
.session_manager
|
||||||
.try_lock()
|
.try_lock()
|
||||||
.map(|sm| sm.active_count())
|
.map(|sm| sm.active_count())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
Html(format!("{}", active_sessions))
|
||||||
|
|
||||||
Html(format!("{}", active_sessions))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn metric_messages(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn metric_messages(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html("--".to_string())
|
Html("--".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn metric_response_time(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn metric_response_time(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html("--".to_string())
|
Html("--".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn trend_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn trend_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html("↑ 0%".to_string())
|
Html("↑ 0%".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn rate_messages(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn rate_messages(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html("0/hr".to_string())
|
Html("0/hr".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn sessions_panel(State(state): State<Arc<AppState>>) -> Html<String> {
|
async fn sessions_panel(State(state): State<Arc<AppState>>) -> Html<String> {
|
||||||
let active_sessions = state
|
let active_sessions = state
|
||||||
.session_manager
|
.session_manager
|
||||||
.try_lock()
|
.try_lock()
|
||||||
.map(|sm| sm.active_count())
|
.map(|sm| sm.active_count())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
Html(format!(
|
Html(format!(
|
||||||
r##"<div class="sessions-panel">
|
r##"<div class="sessions-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h3>Active Sessions</h3>
|
<h3>Active Sessions</h3>
|
||||||
<span class="session-count">{active_sessions}</span>
|
<span class="session-count">{active_sessions}</span>
|
||||||
|
</div>
|
||||||
|
<div class="session-list">
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>No active sessions</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="session-list">
|
</div>
|
||||||
<div class="empty-state">
|
</div>"## )) }
|
||||||
<p>No active sessions</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>"##
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async fn messages_panel(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn messages_panel(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html(
|
Html(
|
||||||
r##"<div class="messages-panel">
|
r##"<div class="messages-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h3>Recent Messages</h3>
|
<h3>Recent Messages</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="message-list">
|
<div class="message-list">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>No recent messages</p>
|
<p>No recent messages</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>"##
|
|
||||||
.to_string(),
|
</div>"## .to_string(), ) }
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -832,9 +832,10 @@ fn validate_session_sync(session_id: &str) -> Result<AuthenticatedUser, AuthErro
|
||||||
session_id.len(),
|
session_id.len(),
|
||||||
&session_id[..std::cmp::min(20, session_id.len())]);
|
&session_id[..std::cmp::min(20, session_id.len())]);
|
||||||
|
|
||||||
// Try to get user data from session cache first
|
// Try to get user data from session cache first
|
||||||
if let Ok(cache_guard) = crate::directory::auth_routes::SESSION_CACHE.try_read() {
|
#[cfg(feature = "directory")]
|
||||||
if let Some(user_data) = cache_guard.get(session_id) {
|
if let Ok(cache_guard) = crate::directory::auth_routes::SESSION_CACHE.try_read() {
|
||||||
|
if let Some(user_data) = cache_guard.get(session_id) {
|
||||||
debug!("Found user in session cache: {}", user_data.email);
|
debug!("Found user in session cache: {}", user_data.email);
|
||||||
|
|
||||||
// Parse user_id from cached data
|
// Parse user_id from cached data
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ pub mod rbac_ui;
|
||||||
pub mod security_admin;
|
pub mod security_admin;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::State,
|
extract::State,
|
||||||
response::{Html, Json},
|
response::{Html, Json},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -17,314 +17,310 @@ use std::sync::Arc;
|
||||||
use crate::shared::state::AppState;
|
use crate::shared::state::AppState;
|
||||||
|
|
||||||
pub fn configure_settings_routes() -> Router<Arc<AppState>> {
|
pub fn configure_settings_routes() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/api/user/storage", get(get_storage_info))
|
.route("/api/user/storage", get(get_storage_info))
|
||||||
.route("/api/user/storage/connections", get(get_storage_connections))
|
.route("/api/user/storage/connections", get(get_storage_connections))
|
||||||
.route("/api/user/security/2fa/status", get(get_2fa_status))
|
.route("/api/user/security/2fa/status", get(get_2fa_status))
|
||||||
.route("/api/user/security/2fa/enable", post(enable_2fa))
|
.route("/api/user/security/2fa/enable", post(enable_2fa))
|
||||||
.route("/api/user/security/2fa/disable", post(disable_2fa))
|
.route("/api/user/security/2fa/disable", post(disable_2fa))
|
||||||
.route("/api/user/security/sessions", get(get_active_sessions))
|
.route("/api/user/security/sessions", get(get_active_sessions))
|
||||||
.route(
|
.route(
|
||||||
"/api/user/security/sessions/revoke-all",
|
"/api/user/security/sessions/revoke-all",
|
||||||
post(revoke_all_sessions),
|
post(revoke_all_sessions),
|
||||||
)
|
)
|
||||||
.route("/api/user/security/devices", get(get_trusted_devices))
|
.route("/api/user/security/devices", get(get_trusted_devices))
|
||||||
.route("/api/settings/search", post(save_search_settings))
|
.route("/api/settings/search", post(save_search_settings))
|
||||||
.route("/api/settings/smtp/test", post(test_smtp_connection))
|
.route("/api/settings/smtp/test", post(test_smtp_connection))
|
||||||
.route("/api/settings/accounts/social", get(get_accounts_social))
|
.route("/api/settings/accounts/social", get(get_accounts_social))
|
||||||
.route("/api/settings/accounts/messaging", get(get_accounts_messaging))
|
.route("/api/settings/accounts/messaging", get(get_accounts_messaging))
|
||||||
.route("/api/settings/accounts/email", get(get_accounts_email))
|
.route("/api/settings/accounts/email", get(get_accounts_email))
|
||||||
.route("/api/settings/accounts/smtp", post(save_smtp_account))
|
.route("/api/settings/accounts/smtp", post(save_smtp_account))
|
||||||
.route("/api/ops/health", get(get_ops_health))
|
.route("/api/ops/health", get(get_ops_health))
|
||||||
.route("/api/rbac/permissions", get(get_rbac_permissions))
|
.route("/api/rbac/permissions", get(get_rbac_permissions))
|
||||||
.merge(rbac::configure_rbac_routes())
|
.merge(rbac::configure_rbac_routes())
|
||||||
.merge(security_admin::configure_security_admin_routes())
|
.merge(security_admin::configure_security_admin_routes())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_accounts_social(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn get_accounts_social(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html(r##"<div class="accounts-list">
|
Html(r##"<div class="accounts-list">
|
||||||
<div class="account-item"><span class="account-icon">📷</span><span class="account-name">Instagram</span><span class="account-status disconnected">Not connected</span></div>
|
<div class="account-item"><span class="account-icon">📷</span><span class="account-name">Instagram</span><span class="account-status disconnected">Not connected</span></div>
|
||||||
<div class="account-item"><span class="account-icon">📘</span><span class="account-name">Facebook</span><span class="account-status disconnected">Not connected</span></div>
|
<div class="account-item"><span class="account-icon">📘</span><span class="account-name">Facebook</span><span class="account-status disconnected">Not connected</span></div>
|
||||||
<div class="account-item"><span class="account-icon">🐦</span><span class="account-name">Twitter/X</span><span class="account-status disconnected">Not connected</span></div>
|
<div class="account-item"><span class="account-icon">🐦</span><span class="account-name">Twitter/X</span><span class="account-status disconnected">Not connected</span></div>
|
||||||
<div class="account-item"><span class="account-icon">💼</span><span class="account-name">LinkedIn</span><span class="account-status disconnected">Not connected</span></div>
|
<div class="account-item"><span class="account-icon">💼</span><span class="account-name">LinkedIn</span><span class="account-status disconnected">Not connected</span></div>
|
||||||
</div>"##.to_string())
|
|
||||||
}
|
</div>"##.to_string()) }
|
||||||
|
|
||||||
async fn get_accounts_messaging(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn get_accounts_messaging(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html(r##"<div class="accounts-list">
|
Html(r##"<div class="accounts-list">
|
||||||
<div class="account-item"><span class="account-icon">💬</span><span class="account-name">Discord</span><span class="account-status disconnected">Not connected</span></div>
|
<div class="account-item"><span class="account-icon">💬</span><span class="account-name">Discord</span><span class="account-status disconnected">Not connected</span></div>
|
||||||
<div class="account-item"><span class="account-icon">📱</span><span class="account-name">WhatsApp</span><span class="account-status disconnected">Not connected</span></div>
|
<div class="account-item"><span class="account-icon">📱</span><span class="account-name">WhatsApp</span><span class="account-status disconnected">Not connected</span></div>
|
||||||
<div class="account-item"><span class="account-icon">✈️</span><span class="account-name">Telegram</span><span class="account-status disconnected">Not connected</span></div>
|
<div class="account-item"><span class="account-icon">✈️</span><span class="account-name">Telegram</span><span class="account-status disconnected">Not connected</span></div>
|
||||||
<div class="account-item"><span class="account-icon">💼</span><span class="account-name">Teams</span><span class="account-status disconnected">Not connected</span></div>
|
<div class="account-item"><span class="account-icon">💼</span><span class="account-name">Teams</span><span class="account-status disconnected">Not connected</span></div>
|
||||||
</div>"##.to_string())
|
|
||||||
}
|
</div>"##.to_string()) }
|
||||||
|
|
||||||
async fn get_accounts_email(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn get_accounts_email(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html(r##"<div class="accounts-list">
|
Html(r##"<div class="accounts-list">
|
||||||
<div class="account-item"><span class="account-icon">📧</span><span class="account-name">Gmail</span><span class="account-status disconnected">Not connected</span></div>
|
<div class="account-item"><span class="account-icon">📧</span><span class="account-name">Gmail</span><span class="account-status disconnected">Not connected</span></div>
|
||||||
<div class="account-item"><span class="account-icon">📨</span><span class="account-name">Outlook</span><span class="account-status disconnected">Not connected</span></div>
|
<div class="account-item"><span class="account-icon">📨</span><span class="account-name">Outlook</span><span class="account-status disconnected">Not connected</span></div>
|
||||||
<div class="account-item"><span class="account-icon">⚙️</span><span class="account-name">SMTP</span><span class="account-status disconnected">Not configured</span></div>
|
<div class="account-item"><span class="account-icon">⚙️</span><span class="account-name">SMTP</span><span class="account-status disconnected">Not configured</span></div>
|
||||||
</div>"##.to_string())
|
|
||||||
}
|
</div>"##.to_string()) }
|
||||||
|
|
||||||
async fn save_smtp_account(
|
async fn save_smtp_account(
|
||||||
State(_state): State<Arc<AppState>>,
|
State(_state): State<Arc<AppState>>,
|
||||||
Json(config): Json<serde_json::Value>,
|
Json(config): Json<serde_json::Value>,
|
||||||
) -> Json<serde_json::Value> {
|
) -> Json<serde_json::Value> {
|
||||||
Json(serde_json::json!({
|
Json(serde_json::json!({
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "SMTP configuration saved",
|
"message": "SMTP configuration saved",
|
||||||
"config": config
|
"config": config
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_ops_health(State(_state): State<Arc<AppState>>) -> Json<serde_json::Value> {
|
async fn get_ops_health(State(_state): State<Arc<AppState>>) -> Json<serde_json::Value> {
|
||||||
Json(serde_json::json!({
|
Json(serde_json::json!({
|
||||||
"status": "healthy",
|
"status": "healthy",
|
||||||
"services": {
|
"services": {
|
||||||
"api": {"status": "up", "latency_ms": 12},
|
"api": {"status": "up", "latency_ms": 12},
|
||||||
"database": {"status": "up", "latency_ms": 5},
|
"database": {"status": "up", "latency_ms": 5},
|
||||||
"cache": {"status": "up", "latency_ms": 1},
|
"cache": {"status": "up", "latency_ms": 1},
|
||||||
"storage": {"status": "up", "latency_ms": 8}
|
"storage": {"status": "up", "latency_ms": 8}
|
||||||
},
|
},
|
||||||
"timestamp": chrono::Utc::now().to_rfc3339()
|
"timestamp": chrono::Utc::now().to_rfc3339()
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_rbac_permissions(State(_state): State<Arc<AppState>>) -> Json<serde_json::Value> {
|
async fn get_rbac_permissions(State(_state): State<Arc<AppState>>) -> Json<serde_json::Value> {
|
||||||
Json(serde_json::json!({
|
Json(serde_json::json!({
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{"id": "read:users", "name": "Read Users", "category": "Users"},
|
{"id": "read:users", "name": "Read Users", "category": "Users"},
|
||||||
{"id": "write:users", "name": "Write Users", "category": "Users"},
|
{"id": "write:users", "name": "Write Users", "category": "Users"},
|
||||||
{"id": "delete:users", "name": "Delete Users", "category": "Users"},
|
{"id": "delete:users", "name": "Delete Users", "category": "Users"},
|
||||||
{"id": "read:bots", "name": "Read Bots", "category": "Bots"},
|
{"id": "read:bots", "name": "Read Bots", "category": "Bots"},
|
||||||
{"id": "write:bots", "name": "Write Bots", "category": "Bots"},
|
{"id": "write:bots", "name": "Write Bots", "category": "Bots"},
|
||||||
{"id": "admin:billing", "name": "Manage Billing", "category": "Admin"},
|
{"id": "admin:billing", "name": "Manage Billing", "category": "Admin"},
|
||||||
{"id": "admin:settings", "name": "Manage Settings", "category": "Admin"}
|
{"id": "admin:settings", "name": "Manage Settings", "category": "Admin"}
|
||||||
]
|
]
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_storage_info(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn get_storage_info(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html(
|
Html(
|
||||||
r##"<div class="storage-info">
|
r##"<div class="storage-info">
|
||||||
<div class="storage-bar">
|
<div class="storage-bar">
|
||||||
<div class="storage-used" style="width: 25%"></div>
|
<div class="storage-used" style="width: 25%"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="storage-details">
|
<div class="storage-details">
|
||||||
<span class="storage-used-text">2.5 GB used</span>
|
<span class="storage-used-text">2.5 GB used</span>
|
||||||
<span class="storage-total-text">of 10 GB</span>
|
<span class="storage-total-text">of 10 GB</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="storage-breakdown">
|
<div class="storage-breakdown">
|
||||||
<div class="storage-item">
|
<div class="storage-item">
|
||||||
<span class="storage-icon">📄</span>
|
<span class="storage-icon">📄</span>
|
||||||
<span class="storage-label">Documents</span>
|
<span class="storage-label">Documents</span>
|
||||||
<span class="storage-size">1.2 GB</span>
|
<span class="storage-size">1.2 GB</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="storage-item">
|
<div class="storage-item">
|
||||||
<span class="storage-icon">🖼️</span>
|
<span class="storage-icon">🖼️</span>
|
||||||
<span class="storage-label">Images</span>
|
<span class="storage-label">Images</span>
|
||||||
<span class="storage-size">800 MB</span>
|
<span class="storage-size">800 MB</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="storage-item">
|
<div class="storage-item">
|
||||||
<span class="storage-icon">📧</span>
|
<span class="storage-icon">📧</span>
|
||||||
<span class="storage-label">Emails</span>
|
<span class="storage-label">Emails</span>
|
||||||
<span class="storage-size">500 MB</span>
|
<span class="storage-size">500 MB</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>"##
|
s
|
||||||
.to_string(),
|
</div>"## .to_string(), ) }
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_storage_connections(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn get_storage_connections(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html(
|
Html(
|
||||||
r##"<div class="connections-empty">
|
r##"<div class="connections-empty">
|
||||||
<p class="text-muted">No external storage connections configured</p>
|
<p class="text-muted">No external storage connections configured</p>
|
||||||
<button class="btn-secondary" onclick="showAddConnectionModal()">
|
<button class="btn-secondary" onclick="showAddConnectionModal()">
|
||||||
+ Add Connection
|
+ Add Connection
|
||||||
</button>
|
</button>
|
||||||
</div>"##
|
|
||||||
.to_string(),
|
</div>"## .to_string(), ) }
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct SearchSettingsRequest {
|
struct SearchSettingsRequest {
|
||||||
enable_fuzzy_search: Option<bool>,
|
enable_fuzzy_search: Option<bool>,
|
||||||
search_result_limit: Option<i32>,
|
search_result_limit: Option<i32>,
|
||||||
enable_ai_suggestions: Option<bool>,
|
enable_ai_suggestions: Option<bool>,
|
||||||
index_attachments: Option<bool>,
|
index_attachments: Option<bool>,
|
||||||
search_sources: Option<Vec<String>>,
|
search_sources: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct SearchSettingsResponse {
|
struct SearchSettingsResponse {
|
||||||
success: bool,
|
success: bool,
|
||||||
message: Option<String>,
|
message: Option<String>,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_search_settings(
|
async fn save_search_settings(
|
||||||
State(_state): State<Arc<AppState>>,
|
State(_state): State<Arc<AppState>>,
|
||||||
Json(settings): Json<SearchSettingsRequest>,
|
Json(settings): Json<SearchSettingsRequest>,
|
||||||
) -> Json<SearchSettingsResponse> {
|
) -> Json<SearchSettingsResponse> {
|
||||||
// In a real implementation, save to database
|
// In a real implementation, save to database
|
||||||
log::info!("Saving search settings: fuzzy={:?}, limit={:?}, ai={:?}",
|
log::info!("Saving search settings: fuzzy={:?}, limit={:?}, ai={:?}",
|
||||||
settings.enable_fuzzy_search,
|
settings.enable_fuzzy_search,
|
||||||
settings.search_result_limit,
|
settings.search_result_limit,
|
||||||
settings.enable_ai_suggestions
|
settings.enable_ai_suggestions
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
Json(SearchSettingsResponse {
|
||||||
|
success: true,
|
||||||
|
message: Some("Search settings saved successfully".to_string()),
|
||||||
|
error: None,
|
||||||
|
})
|
||||||
|
|
||||||
Json(SearchSettingsResponse {
|
|
||||||
success: true,
|
|
||||||
message: Some("Search settings saved successfully".to_string()),
|
|
||||||
error: None,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
struct SmtpTestRequest {
|
struct SmtpTestRequest {
|
||||||
host: String,
|
host: String,
|
||||||
port: i32,
|
port: i32,
|
||||||
username: Option<String>,
|
username: Option<String>,
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
use_tls: Option<bool>,
|
use_tls: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct SmtpTestResponse {
|
struct SmtpTestResponse {
|
||||||
success: bool,
|
success: bool,
|
||||||
message: Option<String>,
|
message: Option<String>,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "mail")]
|
||||||
async fn test_smtp_connection(
|
async fn test_smtp_connection(
|
||||||
State(_state): State<Arc<AppState>>,
|
State(_state): State<Arc<AppState>>,
|
||||||
Json(config): Json<SmtpTestRequest>,
|
Json(config): Json<SmtpTestRequest>,
|
||||||
) -> Json<SmtpTestResponse> {
|
) -> Json<SmtpTestResponse> {
|
||||||
use lettre::SmtpTransport;
|
use lettre::SmtpTransport;
|
||||||
use lettre::transport::smtp::authentication::Credentials;
|
use lettre::transport::smtp::authentication::Credentials;
|
||||||
|
|
||||||
log::info!("Testing SMTP connection to {}:{}", config.host, config.port);
|
|
||||||
|
|
||||||
let mailer_result = if let (Some(user), Some(pass)) = (config.username, config.password) {
|
|
||||||
let creds = Credentials::new(user, pass);
|
|
||||||
SmtpTransport::relay(&config.host)
|
|
||||||
.map(|b| b.port(config.port as u16).credentials(creds).build())
|
|
||||||
} else {
|
|
||||||
Ok(SmtpTransport::builder_dangerous(&config.host)
|
|
||||||
.port(config.port as u16)
|
|
||||||
.build())
|
|
||||||
};
|
|
||||||
|
|
||||||
match mailer_result {
|
log::info!("Testing SMTP connection to {}:{}", config.host, config.port);
|
||||||
Ok(mailer) => {
|
|
||||||
match mailer.test_connection() {
|
let mailer_result = if let (Some(user), Some(pass)) = (config.username, config.password) {
|
||||||
Ok(true) => Json(SmtpTestResponse {
|
let creds = Credentials::new(user, pass);
|
||||||
success: true,
|
SmtpTransport::relay(&config.host)
|
||||||
message: Some("SMTP connection successful".to_string()),
|
.map(|b| b.port(config.port as u16).credentials(creds).build())
|
||||||
error: None,
|
} else {
|
||||||
}),
|
Ok(SmtpTransport::builder_dangerous(&config.host)
|
||||||
Ok(false) => Json(SmtpTestResponse {
|
.port(config.port as u16)
|
||||||
success: false,
|
.build())
|
||||||
message: None,
|
};
|
||||||
error: Some("SMTP connection test failed".to_string()),
|
|
||||||
}),
|
match mailer_result {
|
||||||
Err(e) => Json(SmtpTestResponse {
|
Ok(mailer) => {
|
||||||
success: false,
|
match mailer.test_connection() {
|
||||||
message: None,
|
Ok(true) => Json(SmtpTestResponse {
|
||||||
error: Some(format!("SMTP error: {}", e)),
|
success: true,
|
||||||
}),
|
message: Some("SMTP connection successful".to_string()),
|
||||||
}
|
error: None,
|
||||||
|
}),
|
||||||
|
Ok(false) => Json(SmtpTestResponse {
|
||||||
|
success: false,
|
||||||
|
message: None,
|
||||||
|
error: Some("SMTP connection test failed".to_string()),
|
||||||
|
}),
|
||||||
|
Err(e) => Json(SmtpTestResponse {
|
||||||
|
success: false,
|
||||||
|
message: None,
|
||||||
|
error: Some(format!("SMTP error: {}", e)),
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
Err(e) => Json(SmtpTestResponse {
|
|
||||||
success: false,
|
|
||||||
message: None,
|
|
||||||
error: Some(format!("Failed to create SMTP transport: {}", e)),
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
Err(e) => Json(SmtpTestResponse {
|
||||||
|
success: false,
|
||||||
|
message: None,
|
||||||
|
error: Some(format!("Failed to create SMTP transport: {}", e)),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "mail"))]
|
||||||
|
async fn test_smtp_connection(
|
||||||
|
State(_state): State<Arc<AppState>>,
|
||||||
|
Json(_config): Json<SmtpTestRequest>,
|
||||||
|
) -> Json<SmtpTestResponse> {
|
||||||
|
Json(SmtpTestResponse {
|
||||||
|
success: false,
|
||||||
|
message: None,
|
||||||
|
error: Some("SMTP email feature is not enabled in this build".to_string()),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_2fa_status(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn get_2fa_status(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html(
|
Html(
|
||||||
r##"<div class="status-indicator">
|
r##"<div class="status-indicator">
|
||||||
<span class="status-dot inactive"></span>
|
<span class="status-dot inactive"></span>
|
||||||
<span class="status-text">Two-factor authentication is not enabled</span>
|
<span class="status-text">Two-factor authentication is not enabled</span>
|
||||||
</div>"##
|
|
||||||
.to_string(),
|
</div>"## .to_string(), ) }
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn enable_2fa(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn enable_2fa(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html(
|
Html(
|
||||||
r##"<div class="status-indicator">
|
r##"<div class="status-indicator">
|
||||||
<span class="status-dot active"></span>
|
<span class="status-dot active"></span>
|
||||||
<span class="status-text">Two-factor authentication enabled</span>
|
<span class="status-text">Two-factor authentication enabled</span>
|
||||||
</div>"##
|
|
||||||
.to_string(),
|
</div>"## .to_string(), ) }
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn disable_2fa(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn disable_2fa(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html(
|
Html(
|
||||||
r##"<div class="status-indicator">
|
r##"<div class="status-indicator">
|
||||||
<span class="status-dot inactive"></span>
|
<span class="status-dot inactive"></span>
|
||||||
<span class="status-text">Two-factor authentication disabled</span>
|
<span class="status-text">Two-factor authentication disabled</span>
|
||||||
</div>"##
|
|
||||||
.to_string(),
|
</div>"## .to_string(), ) }
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_active_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn get_active_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html(
|
Html(
|
||||||
r##"<div class="session-item current">
|
r##"<div class="session-item current">
|
||||||
<div class="session-info">
|
<div class="session-info">
|
||||||
<div class="session-device">
|
<div class="session-device">
|
||||||
<span class="device-icon">💻</span>
|
<span class="device-icon">💻</span>
|
||||||
<span class="device-name">Current Session</span>
|
<span class="device-name">Current Session</span>
|
||||||
<span class="session-badge current">This device</span>
|
<span class="session-badge current">This device</span>
|
||||||
</div>
|
|
||||||
<div class="session-details">
|
|
||||||
<span class="session-location">Current browser session</span>
|
|
||||||
<span class="session-time">Active now</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="sessions-empty">
|
<div class="session-details">
|
||||||
<p class="text-muted">No other active sessions</p>
|
<span class="session-location">Current browser session</span>
|
||||||
</div>"##
|
<span class="session-time">Active now</span>
|
||||||
.to_string(),
|
</div>
|
||||||
)
|
</div>
|
||||||
}
|
|
||||||
|
</div> <div class="sessions-empty"> <p class="text-muted">No other active sessions</p> </div>"## .to_string(), ) }
|
||||||
|
|
||||||
async fn revoke_all_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn revoke_all_sessions(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html(
|
Html(
|
||||||
r##"<div class="success-message">
|
r##"<div class="success-message">
|
||||||
<span class="success-icon">✓</span>
|
<span class="success-icon">✓</span>
|
||||||
<span>All other sessions have been revoked</span>
|
<span>All other sessions have been revoked</span>
|
||||||
</div>"##
|
|
||||||
.to_string(),
|
</div>"## .to_string(), ) }
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_trusted_devices(State(_state): State<Arc<AppState>>) -> Html<String> {
|
async fn get_trusted_devices(State(_state): State<Arc<AppState>>) -> Html<String> {
|
||||||
Html(
|
Html(
|
||||||
r##"<div class="device-item current">
|
r##"<div class="device-item current">
|
||||||
<div class="device-info">
|
<div class="device-info">
|
||||||
<span class="device-icon">💻</span>
|
<span class="device-icon">💻</span>
|
||||||
<div class="device-details">
|
<div class="device-details">
|
||||||
<span class="device-name">Current Device</span>
|
<span class="device-name">Current Device</span>
|
||||||
<span class="device-last-seen">Last active: Just now</span>
|
<span class="device-last-seen">Last active: Just now</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span class="device-badge trusted">Trusted</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="devices-empty">
|
</div>
|
||||||
<p class="text-muted">No other trusted devices</p>
|
<span class="device-badge trusted">Trusted</span>
|
||||||
</div>"##
|
|
||||||
.to_string(),
|
</div> <div class="devices-empty"> <p class="text-muted">No other trusted devices</p> </div>"## .to_string(), ) }
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue