hammerwork_web/api/
system.rs

1//! System information and administration API endpoints.
2//!
3//! This module provides comprehensive system administration and information endpoints
4//! for monitoring the web dashboard service, runtime metrics, and maintenance operations.
5//!
6//! # API Endpoints
7//!
8//! - `GET /api/system/info` - Complete system information including build and runtime details
9//! - `GET /api/system/config` - Current server configuration
10//! - `GET /api/system/metrics` - Metrics and monitoring information
11//! - `POST /api/system/maintenance` - Perform maintenance operations
12//! - `GET /api/version` - Basic version information
13//!
14//! # Examples
15//!
16//! ## System Information
17//!
18//! ```rust
19//! use hammerwork_web::api::system::{SystemInfo, BuildInfo, RuntimeInfo, DatabaseInfo};
20//! use chrono::Utc;
21//!
22//! let build_info = BuildInfo {
23//!     version: "1.3.0".to_string(),
24//!     git_commit: Some("a1b2c3d".to_string()),
25//!     build_date: Some("2024-01-15".to_string()),
26//!     rust_version: "1.70.0".to_string(),
27//!     target_triple: "x86_64-unknown-linux-gnu".to_string(),
28//! };
29//!
30//! let runtime_info = RuntimeInfo {
31//!     process_id: 12345,
32//!     memory_usage_bytes: Some(134217728), // 128MB
33//!     cpu_usage_percent: Some(5.2),
34//!     thread_count: Some(8),
35//!     gc_collections: None, // Not applicable for Rust
36//! };
37//!
38//! let database_info = DatabaseInfo {
39//!     database_type: "PostgreSQL".to_string(),
40//!     connection_url: "***masked***".to_string(),
41//!     pool_size: 10,
42//!     active_connections: Some(3),
43//!     connection_health: true,
44//!     last_migration: Some("20240101_initial".to_string()),
45//! };
46//!
47//! let system_info = SystemInfo {
48//!     version: "1.3.0".to_string(),
49//!     build_info,
50//!     runtime_info,
51//!     database_info,
52//!     features: vec!["postgres".to_string(), "auth".to_string()],
53//!     uptime_seconds: 86400,
54//!     started_at: Utc::now(),
55//! };
56//!
57//! assert_eq!(system_info.version, "1.3.0");
58//! assert!(system_info.features.contains(&"postgres".to_string()));
59//! assert_eq!(system_info.runtime_info.process_id, 12345);
60//! ```
61//!
62//! ## Maintenance Operations
63//!
64//! ```rust
65//! use hammerwork_web::api::system::MaintenanceRequest;
66//! use serde_json::json;
67//!
68//! let cleanup_request = MaintenanceRequest {
69//!     operation: "cleanup".to_string(),
70//!     target: Some("old_jobs".to_string()),
71//!     dry_run: Some(true),
72//! };
73//!
74//! let vacuum_request = MaintenanceRequest {
75//!     operation: "vacuum".to_string(),
76//!     target: Some("hammerwork_jobs".to_string()),
77//!     dry_run: Some(false),
78//! };
79//!
80//! assert_eq!(cleanup_request.operation, "cleanup");
81//! assert_eq!(cleanup_request.dry_run, Some(true));
82//! assert_eq!(vacuum_request.operation, "vacuum");
83//! ```
84//!
85//! ## Server Configuration
86//!
87//! ```rust
88//! use hammerwork_web::api::system::ServerConfig;
89//!
90//! let config = ServerConfig {
91//!     bind_address: "0.0.0.0".to_string(),
92//!     port: 8080,
93//!     authentication_enabled: true,
94//!     cors_enabled: false,
95//!     websocket_max_connections: 100,
96//!     static_assets_path: "/var/www/dashboard".to_string(),
97//! };
98//!
99//! assert_eq!(config.bind_address, "0.0.0.0");
100//! assert_eq!(config.port, 8080);
101//! assert!(config.authentication_enabled);
102//! assert!(!config.cors_enabled);
103//! ```
104//!
105//! ## Metrics Information
106//!
107//! ```rust
108//! use hammerwork_web::api::system::MetricsInfo;
109//! use chrono::Utc;
110//!
111//! let metrics_info = MetricsInfo {
112//!     prometheus_enabled: true,
113//!     metrics_endpoint: "/metrics".to_string(),
114//!     custom_metrics_count: 15,
115//!     last_scrape: Some(Utc::now()),
116//! };
117//!
118//! assert!(metrics_info.prometheus_enabled);
119//! assert_eq!(metrics_info.metrics_endpoint, "/metrics");
120//! assert_eq!(metrics_info.custom_metrics_count, 15);
121//! ```
122
123use super::ApiResponse;
124use hammerwork::queue::DatabaseQueue;
125use serde::{Deserialize, Serialize};
126use std::sync::Arc;
127use warp::{Filter, Reply};
128
129/// System information
130#[derive(Debug, Serialize)]
131pub struct SystemInfo {
132    pub version: String,
133    pub build_info: BuildInfo,
134    pub runtime_info: RuntimeInfo,
135    pub database_info: DatabaseInfo,
136    pub features: Vec<String>,
137    pub uptime_seconds: u64,
138    pub started_at: chrono::DateTime<chrono::Utc>,
139}
140
141/// Build information
142#[derive(Debug, Serialize)]
143pub struct BuildInfo {
144    pub version: String,
145    pub git_commit: Option<String>,
146    pub build_date: Option<String>,
147    pub rust_version: String,
148    pub target_triple: String,
149}
150
151/// Runtime information
152#[derive(Debug, Serialize)]
153pub struct RuntimeInfo {
154    pub process_id: u32,
155    pub memory_usage_bytes: Option<u64>,
156    pub cpu_usage_percent: Option<f64>,
157    pub thread_count: Option<usize>,
158    pub gc_collections: Option<u64>,
159}
160
161/// Database information
162#[derive(Debug, Serialize)]
163pub struct DatabaseInfo {
164    pub database_type: String,
165    pub connection_url: String, // Masked for security
166    pub pool_size: u32,
167    pub active_connections: Option<u32>,
168    pub connection_health: bool,
169    pub last_migration: Option<String>,
170}
171
172/// Server configuration
173#[derive(Debug, Serialize)]
174pub struct ServerConfig {
175    pub bind_address: String,
176    pub port: u16,
177    pub authentication_enabled: bool,
178    pub cors_enabled: bool,
179    pub websocket_max_connections: usize,
180    pub static_assets_path: String,
181}
182
183/// Metrics endpoint information
184#[derive(Debug, Serialize)]
185pub struct MetricsInfo {
186    pub prometheus_enabled: bool,
187    pub metrics_endpoint: String,
188    pub custom_metrics_count: u32,
189    pub last_scrape: Option<chrono::DateTime<chrono::Utc>>,
190}
191
192/// Configuration update request
193#[derive(Debug, Deserialize)]
194pub struct ConfigUpdateRequest {
195    pub setting: String,
196    pub value: serde_json::Value,
197}
198
199/// Maintenance operation request
200#[derive(Debug, Deserialize)]
201pub struct MaintenanceRequest {
202    pub operation: String,      // "vacuum", "reindex", "cleanup", "optimize"
203    pub target: Option<String>, // table name or queue name
204    pub dry_run: Option<bool>,
205}
206
207/// Create system routes
208pub fn routes<T>(
209    queue: Arc<T>,
210) -> impl Filter<Extract = impl Reply, Error = warp::Rejection> + Clone
211where
212    T: DatabaseQueue + Send + Sync + 'static,
213{
214    let queue_filter = warp::any().map(move || queue.clone());
215
216    let info = warp::path("system")
217        .and(warp::path("info"))
218        .and(warp::path::end())
219        .and(warp::get())
220        .and(queue_filter.clone())
221        .and_then(system_info_handler);
222
223    let config = warp::path("system")
224        .and(warp::path("config"))
225        .and(warp::path::end())
226        .and(warp::get())
227        .and_then(system_config_handler);
228
229    let metrics_info = warp::path("system")
230        .and(warp::path("metrics"))
231        .and(warp::path::end())
232        .and(warp::get())
233        .and_then(metrics_info_handler);
234
235    let maintenance = warp::path("system")
236        .and(warp::path("maintenance"))
237        .and(warp::path::end())
238        .and(warp::post())
239        .and(queue_filter.clone())
240        .and(warp::body::json())
241        .and_then(maintenance_handler);
242
243    let version = warp::path("version")
244        .and(warp::path::end())
245        .and(warp::get())
246        .and_then(version_handler);
247
248    info.or(config).or(metrics_info).or(maintenance).or(version)
249}
250
251/// Handler for system information
252async fn system_info_handler<T>(queue: Arc<T>) -> Result<impl Reply, warp::Rejection>
253where
254    T: DatabaseQueue + Send + Sync,
255{
256    // Test database connection
257    let database_healthy = queue.get_all_queue_stats().await.is_ok();
258
259    let build_info = BuildInfo {
260        version: env!("CARGO_PKG_VERSION").to_string(),
261        git_commit: option_env!("GIT_COMMIT").map(|s| s.to_string()),
262        build_date: option_env!("BUILD_DATE").map(|s| s.to_string()),
263        rust_version: get_rust_version(),
264        target_triple: get_target_triple(),
265    };
266
267    let runtime_info = RuntimeInfo {
268        process_id: std::process::id(),
269        memory_usage_bytes: get_memory_usage(),
270        cpu_usage_percent: None, // Would need process monitoring
271        thread_count: None,      // Would need thread monitoring
272        gc_collections: None,    // Not applicable for Rust
273    };
274
275    let database_info = DatabaseInfo {
276        database_type: "PostgreSQL/MySQL".to_string(), // TODO: Detect actual type
277        connection_url: "***masked***".to_string(),
278        pool_size: 5,             // TODO: Get from actual config
279        active_connections: None, // TODO: Get from pool
280        connection_health: database_healthy,
281        last_migration: None, // TODO: Get from migration table
282    };
283
284    let features = vec![
285        #[cfg(feature = "postgres")]
286        "postgres".to_string(),
287        #[cfg(feature = "mysql")]
288        "mysql".to_string(),
289        #[cfg(feature = "auth")]
290        "auth".to_string(),
291    ];
292
293    let system_info = SystemInfo {
294        version: env!("CARGO_PKG_VERSION").to_string(),
295        build_info,
296        runtime_info,
297        database_info,
298        features,
299        uptime_seconds: 0,              // TODO: Track actual uptime
300        started_at: chrono::Utc::now(), // TODO: Track actual start time
301    };
302
303    Ok(warp::reply::json(&ApiResponse::success(system_info)))
304}
305
306/// Handler for system configuration
307async fn system_config_handler() -> Result<impl Reply, warp::Rejection> {
308    let config = ServerConfig {
309        bind_address: "127.0.0.1".to_string(), // TODO: Get from actual config
310        port: 8080,                            // TODO: Get from actual config
311        authentication_enabled: false,         // TODO: Get from actual config
312        cors_enabled: false,                   // TODO: Get from actual config
313        websocket_max_connections: 100,        // TODO: Get from actual config
314        static_assets_path: "./assets".to_string(), // TODO: Get from actual config
315    };
316
317    Ok(warp::reply::json(&ApiResponse::success(config)))
318}
319
320/// Handler for metrics information
321async fn metrics_info_handler() -> Result<impl Reply, warp::Rejection> {
322    let metrics_info = MetricsInfo {
323        prometheus_enabled: true, // TODO: Detect if metrics feature is enabled
324        metrics_endpoint: "/metrics".to_string(),
325        custom_metrics_count: 0, // TODO: Count actual custom metrics
326        last_scrape: None,       // TODO: Track last scrape time
327    };
328
329    Ok(warp::reply::json(&ApiResponse::success(metrics_info)))
330}
331
332/// Handler for maintenance operations
333async fn maintenance_handler<T>(
334    queue: Arc<T>,
335    request: MaintenanceRequest,
336) -> Result<impl Reply, warp::Rejection>
337where
338    T: DatabaseQueue + Send + Sync,
339{
340    let dry_run = request.dry_run.unwrap_or(false);
341
342    match request.operation.as_str() {
343        "cleanup" => {
344            if dry_run {
345                let response = ApiResponse::success(serde_json::json!({
346                    "operation": "cleanup",
347                    "dry_run": true,
348                    "message": "Dry run: Would clean up old completed and dead jobs",
349                    "estimated_deletions": 0
350                }));
351                Ok(warp::reply::json(&response))
352            } else {
353                // Perform actual cleanup
354                let older_than = chrono::Utc::now() - chrono::Duration::days(7); // Remove jobs older than 7 days
355                match queue.purge_dead_jobs(older_than).await {
356                    Ok(count) => {
357                        let response = ApiResponse::success(serde_json::json!({
358                            "operation": "cleanup",
359                            "dry_run": false,
360                            "message": format!("Cleaned up {} dead jobs", count),
361                            "deletions": count
362                        }));
363                        Ok(warp::reply::json(&response))
364                    }
365                    Err(e) => {
366                        let response = ApiResponse::<()>::error(format!("Cleanup failed: {}", e));
367                        Ok(warp::reply::json(&response))
368                    }
369                }
370            }
371        }
372        "vacuum" => {
373            // Database vacuum operation (PostgreSQL specific)
374            let response =
375                ApiResponse::<()>::error("Vacuum operation not yet implemented".to_string());
376            Ok(warp::reply::json(&response))
377        }
378        "reindex" => {
379            // Database reindex operation
380            let response =
381                ApiResponse::<()>::error("Reindex operation not yet implemented".to_string());
382            Ok(warp::reply::json(&response))
383        }
384        "optimize" => {
385            // General optimization operation
386            let response =
387                ApiResponse::<()>::error("Optimize operation not yet implemented".to_string());
388            Ok(warp::reply::json(&response))
389        }
390        _ => {
391            let response = ApiResponse::<()>::error(format!(
392                "Unknown maintenance operation: {}",
393                request.operation
394            ));
395            Ok(warp::reply::json(&response))
396        }
397    }
398}
399
400/// Handler for version information
401async fn version_handler() -> Result<impl Reply, warp::Rejection> {
402    let version_info = serde_json::json!({
403        "version": env!("CARGO_PKG_VERSION"),
404        "name": env!("CARGO_PKG_NAME"),
405        "description": env!("CARGO_PKG_DESCRIPTION"),
406        "authors": env!("CARGO_PKG_AUTHORS").split(':').collect::<Vec<_>>(),
407        "repository": env!("CARGO_PKG_REPOSITORY"),
408        "license": env!("CARGO_PKG_LICENSE"),
409        "rust_version": get_rust_version(),
410        "build_target": get_target_triple(),
411    });
412
413    Ok(warp::reply::json(&ApiResponse::success(version_info)))
414}
415
416/// Get Rust compiler version
417fn get_rust_version() -> String {
418    option_env!("RUSTC_VERSION")
419        .unwrap_or("unknown")
420        .to_string()
421}
422
423/// Get target triple
424fn get_target_triple() -> String {
425    std::env::consts::ARCH.to_string() + "-" + std::env::consts::OS
426}
427
428/// Get memory usage (platform-specific)
429fn get_memory_usage() -> Option<u64> {
430    #[cfg(target_os = "linux")]
431    {
432        use std::fs;
433        if let Ok(contents) = fs::read_to_string("/proc/self/status") {
434            for line in contents.lines() {
435                if line.starts_with("VmRSS:") {
436                    if let Some(kb) = line
437                        .split_whitespace()
438                        .nth(1)
439                        .and_then(|s| s.parse::<u64>().ok())
440                    {
441                        return Some(kb * 1024); // Convert KB to bytes
442                    }
443                }
444            }
445        }
446    }
447
448    #[cfg(target_os = "macos")]
449    {
450        // macOS memory usage would require system calls
451        // For now, return None
452    }
453
454    #[cfg(target_os = "windows")]
455    {
456        // Windows memory usage would require Windows API
457        // For now, return None
458    }
459
460    None
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    #[test]
468    fn test_maintenance_request_deserialization() {
469        let json = r#"{
470            "operation": "cleanup",
471            "target": "completed_jobs",
472            "dry_run": true
473        }"#;
474
475        let request: MaintenanceRequest = serde_json::from_str(json).unwrap();
476        assert_eq!(request.operation, "cleanup");
477        assert_eq!(request.target, Some("completed_jobs".to_string()));
478        assert_eq!(request.dry_run, Some(true));
479    }
480
481    #[test]
482    fn test_build_info_creation() {
483        let build_info = BuildInfo {
484            version: "1.0.0".to_string(),
485            git_commit: Some("abc123".to_string()),
486            build_date: Some("2024-01-01".to_string()),
487            rust_version: "1.70.0".to_string(),
488            target_triple: "x86_64-unknown-linux-gnu".to_string(),
489        };
490
491        let json = serde_json::to_string(&build_info).unwrap();
492        assert!(json.contains("1.0.0"));
493        assert!(json.contains("abc123"));
494    }
495
496    #[test]
497    fn test_get_rust_version() {
498        let version = get_rust_version();
499        assert!(!version.is_empty());
500    }
501
502    #[test]
503    fn test_get_target_triple() {
504        let target = get_target_triple();
505        assert!(!target.is_empty());
506        assert!(target.contains("-"));
507    }
508}