From d6f187d5890576be6fe8df313e817d29f706a9ff Mon Sep 17 00:00:00 2001 From: khajamoddin Date: Wed, 18 Feb 2026 07:44:40 +0530 Subject: [PATCH] feat: enable database configuration through cloud console --- crates/sentinel-core/src/config.rs | 27 ++- crates/sentinel-core/src/http.rs | 40 ++++ crates/sentinel-core/src/lib.rs | 1 + crates/sentinel-core/src/storage.rs | 6 +- public/console/index.html | 323 ++++++++++++++++++++++++++-- 5 files changed, 370 insertions(+), 27 deletions(-) diff --git a/crates/sentinel-core/src/config.rs b/crates/sentinel-core/src/config.rs index 120cfff..e60e544 100644 --- a/crates/sentinel-core/src/config.rs +++ b/crates/sentinel-core/src/config.rs @@ -3,6 +3,19 @@ use std::collections::HashMap; use std::path::PathBuf; use std::time::Duration; +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +pub struct DatabaseConfig { + pub database_url: Option, + pub enable_local_db: bool, + #[serde(default = "default_max_connections")] + pub max_connections: u32, + #[serde(default = "default_idle_timeout")] + pub idle_timeout_secs: u64, +} + +fn default_max_connections() -> u32 { 10 } +fn default_idle_timeout() -> u64 { 300 } + #[derive(Debug, Deserialize, Serialize, Clone)] #[allow(clippy::struct_excessive_bools)] pub struct OrchestratorConfig { @@ -181,8 +194,7 @@ pub struct AgentConfig { pub local_tsdb_max_disk_mb: u64, // Persistence Layer - pub database_url: Option, - pub enable_local_db: bool, + pub database: DatabaseConfig, // Control Plane pub orchestrator: Option, @@ -297,8 +309,7 @@ impl Default for AgentConfig { local_tsdb_retention_hours: 24, local_tsdb_max_disk_mb: 512, - database_url: None, - enable_local_db: false, + database: DatabaseConfig::default(), orchestrator: None, @@ -331,12 +342,12 @@ impl AgentConfig { match self.mode { RunMode::Dev => { if overrides.log_level.is_none() { self.log_level = LogLevel::Debug; } - if overrides.enable_local_db.is_none() { self.enable_local_db = true; } + if overrides.enable_local_db.is_none() { self.database.enable_local_db = true; } }, RunMode::Demo => { // In Demo mode, we simulate GPU data if not explicitly enabled if overrides.enable_gpu.is_none() { self.enable_gpu = false; } - if overrides.enable_local_db.is_none() { self.enable_local_db = true; } + if overrides.enable_local_db.is_none() { self.database.enable_local_db = true; } }, RunMode::Prod => { if overrides.log_level.is_none() { self.log_level = LogLevel::Info; } @@ -367,8 +378,8 @@ impl AgentConfig { if let Some(v) = overrides.local_tsdb_path { self.local_tsdb_path = v; } if let Some(v) = overrides.local_tsdb_retention_hours { self.local_tsdb_retention_hours = v; } if let Some(v) = overrides.local_tsdb_max_disk_mb { self.local_tsdb_max_disk_mb = v; } - if let Some(v) = overrides.database_url { self.database_url = Some(v); } - if let Some(v) = overrides.enable_local_db { self.enable_local_db = v; } + if let Some(v) = overrides.database_url { self.database.database_url = Some(v); } + if let Some(v) = overrides.enable_local_db { self.database.enable_local_db = v; } if let Some(v) = overrides.log_level { self.log_level = v; } if let Some(v) = overrides.orchestrator { self.orchestrator = Some(v); } if let Some(v) = overrides.efficiency_profile_path { self.efficiency_profile_path = Some(v); } diff --git a/crates/sentinel-core/src/http.rs b/crates/sentinel-core/src/http.rs index 94e64d4..72bae71 100644 --- a/crates/sentinel-core/src/http.rs +++ b/crates/sentinel-core/src/http.rs @@ -29,6 +29,7 @@ pub struct HttpState { pub orchestrator_token: Option, pub security: crate::config::SecurityConfig, pub storage: std::sync::Arc, + pub database_config: Arc>, } pub fn build_router(state: HttpState) -> Router { @@ -49,6 +50,10 @@ pub fn build_router(state: HttpState) -> Router { let console_dir = std::env::var("SENTINEL_CONSOLE_DIR").unwrap_or_else(|_| "./public/console".to_string()); router = router.nest_service("/", tower_http::services::ServeDir::new(console_dir)); + // Database Configuration Endpoints + router = router + .route("/api/config/database", get(get_database_config_handler).post(update_database_config_handler)); + if let Some(orch_state) = &state.orchestrator { if state.orchestrator_allow_public || state.listen_is_loopback { router = router.nest_service( @@ -238,3 +243,38 @@ async fn tsdb_export_handler( } } } + +async fn get_database_config_handler( + State(state): State, + user: crate::auth::AuthenticatedUser, +) -> impl IntoResponse { + if let Err(e) = crate::auth::require_permission(&user, crate::auth::Permission::ManageOrchestrator) { + return e; + } + let config = state.database_config.read(); + Json((*config).clone()).into_response() +} + +async fn update_database_config_handler( + State(state): State, + user: crate::auth::AuthenticatedUser, + Json(new_config): Json, +) -> impl IntoResponse { + if let Err(e) = crate::auth::require_permission(&user, crate::auth::Permission::ManageOrchestrator) { + return e; + } + + { + let mut config = state.database_config.write(); + *config = new_config; + } + + info!( + target: "audit", + action = "database_config_updated", + user = ?user.user_id, + role = ?user.role + ); + + (StatusCode::OK, "Database configuration updated (Effect on restart for connection parameters)").into_response() +} diff --git a/crates/sentinel-core/src/lib.rs b/crates/sentinel-core/src/lib.rs index 93adae6..89f1e43 100644 --- a/crates/sentinel-core/src/lib.rs +++ b/crates/sentinel-core/src/lib.rs @@ -664,6 +664,7 @@ impl Agent { orchestrator_token: config.orchestrator.as_ref().and_then(|o| o.token.clone()), security: config.security.clone(), storage: storage.clone(), + database_config: Arc::new(parking_lot::RwLock::new(config.database.clone())), }; let router = build_router(http_state); let http_task = serve(&config.listen_address, router) diff --git a/crates/sentinel-core/src/storage.rs b/crates/sentinel-core/src/storage.rs index 0718f29..58e1645 100644 --- a/crates/sentinel-core/src/storage.rs +++ b/crates/sentinel-core/src/storage.rs @@ -19,10 +19,10 @@ pub struct Storage { impl Storage { pub async fn new(config: &AgentConfig) -> Result { // 1. Try PostgreSQL / TimescaleDB if URL provided - if let Some(url) = &config.database_url { + if let Some(url) = &config.database.database_url { tracing::info!("Storage: Connecting to PostgreSQL/Timescale at configured URL..."); let pool = sqlx::postgres::PgPoolOptions::new() - .max_connections(20) + .max_connections(config.database.max_connections) .connect(url).await .context("Failed to connect to PostgreSQL")?; @@ -51,7 +51,7 @@ impl Storage { } // 2. Default: SQLite (Embedded) - if config.enable_local_db { + if config.database.enable_local_db { let path = format!("sqlite://{}/sentinel.db?mode=rwc", config.local_tsdb_path); tracing::info!("Storage: Initializing embedded SQLite DB at {}", path); diff --git a/public/console/index.html b/public/console/index.html index 7a28567..4e23cb6 100644 --- a/public/console/index.html +++ b/public/console/index.html @@ -359,6 +359,145 @@ border-radius: 50%; animation: pulse-cyan 2s infinite; } + + /* Settings Page Styles */ + .settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + gap: 24px; + } + + .settings-section { + margin-bottom: 32px; + } + + .settings-section h3 { + font-family: 'Outfit', sans-serif; + font-size: 18px; + margin-bottom: 16px; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 10px; + } + + .setting-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 0; + border-bottom: 1px solid var(--border); + } + + .setting-row:last-child { + border-bottom: none; + } + + .setting-info { + display: flex; + flex-direction: column; + gap: 4px; + } + + .setting-label { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); + } + + .setting-desc { + font-size: 12px; + color: var(--text-secondary); + } + + /* Toggle Switch */ + .switch { + position: relative; + display: inline-block; + width: 44px; + height: 24px; + } + + .switch input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #333; + transition: .4s; + border-radius: 24px; + } + + .slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: white; + transition: .4s; + border-radius: 50%; + } + + input:checked+.slider { + background-color: var(--accent-cyan); + } + + input:checked+.slider:before { + transform: translateX(20px); + } + + /* Input Styles */ + .settings-input { + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-primary); + padding: 8px 12px; + font-size: 14px; + width: 100%; + outline: none; + transition: border-color 0.2s; + } + + .settings-input:focus { + border-color: var(--accent-cyan); + } + + .action-btn { + background: linear-gradient(90deg, var(--accent-cyan), var(--accent-purple)); + color: #000; + border: none; + padding: 10px 20px; + border-radius: 8px; + font-size: 14px; + font-weight: 700; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + transition: opacity 0.2s; + } + + .action-btn:hover { + opacity: 0.9; + } + + #settings-status { + font-size: 13px; + color: var(--success); + font-weight: 600; + display: none; + } ESNODE Sentinel | Autonomic AI Infrastructure Efficiency Metrics + + +