Skip to main content

offline_intelligence/api/
admin_api.rs

1//! Admin API endpoints
2//! 
3//! This module provides administrative functionality for system management.
4//! Currently a placeholder for future implementation.
5
6use axum::{
7    extract::State,
8    http::StatusCode,
9    response::IntoResponse,
10    Json,
11};
12use std::sync::{Arc, atomic::Ordering};
13use serde::{Deserialize, Serialize};
14
15use crate::shared_state::SharedState;
16
17/// System health response
18#[derive(Debug, Serialize)]
19pub struct HealthResponse {
20    pub status: String,
21    pub version: String,
22    pub uptime_seconds: u64,
23}
24
25/// Database statistics response
26#[derive(Debug, Serialize)]
27pub struct DbStatsResponse {
28    pub total_sessions: usize,
29    pub total_messages: usize,
30    pub total_summaries: usize,
31    pub database_size_bytes: u64,
32}
33
34/// Maintenance request
35#[derive(Debug, Deserialize)]
36pub struct MaintenanceRequest {
37    pub operation: String,
38    pub parameters: Option<serde_json::Value>,
39}
40
41
42// Store application start time as seconds since epoch
43static START_TIME: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
44
45fn init_start_time() {
46    let now: u64 = std::time::SystemTime::now()
47        .duration_since(std::time::UNIX_EPOCH)
48        .unwrap_or_else(|_| std::time::Duration::from_secs(0))
49        .as_secs();
50    START_TIME.store(now, Ordering::SeqCst);
51}
52
53fn get_uptime_seconds() -> u64 {
54    let start = START_TIME.load(Ordering::SeqCst);
55    if start == 0 {
56        // Initialize on first call
57        init_start_time();
58        return 0;
59    }
60    
61    let now = std::time::SystemTime::now()
62        .duration_since(std::time::UNIX_EPOCH)
63        .unwrap_or_else(|_| std::time::Duration::from_secs(0))
64        .as_secs();
65    
66    now.saturating_sub(start)
67}
68
69/// Health check endpoint
70pub async fn health(
71    State(_shared_state): State<Arc<SharedState>>,
72) -> Result<impl IntoResponse, StatusCode> {
73    // Initialize start time if not already set
74    if START_TIME.load(Ordering::SeqCst) == 0 {
75        init_start_time();
76    }
77    
78    Ok((
79        StatusCode::OK,
80        Json(HealthResponse {
81            status: "healthy".to_string(),
82            version: env!("CARGO_PKG_VERSION").to_string(),
83            uptime_seconds: get_uptime_seconds(),
84        }),
85    ))
86}
87
88/// Database statistics endpoint
89pub async fn db_stats(
90    State(shared_state): State<Arc<SharedState>>,
91) -> Result<impl IntoResponse, StatusCode> {
92    // Access the database to get actual statistics
93    let total_sessions = shared_state.conversations.counters.active_sessions.load(Ordering::Relaxed);
94    
95    let total_messages = shared_state.conversations.counters.processed_messages.load(Ordering::Relaxed);
96    
97    // Get cache statistics
98    let cache_hits = shared_state.conversations.counters.cache_hits.load(Ordering::Relaxed);
99    let cache_misses = shared_state.conversations.counters.cache_misses.load(Ordering::Relaxed);
100    let total_summaries = cache_hits + cache_misses; // Approximation
101    
102    // Get database file size from the database path
103    let database_size_bytes = match std::fs::metadata("data/conversations.db") {
104        Ok(metadata) => metadata.len(),
105        Err(_) => match std::fs::metadata("conversations.db") {
106            Ok(metadata) => metadata.len(),
107            Err(_) => 0,
108        }
109    };
110    
111    Ok((
112        StatusCode::OK,
113        Json(DbStatsResponse {
114            total_sessions,
115            total_messages,
116            total_summaries,
117            database_size_bytes,
118        }),
119    ))
120}
121
122/// Maintenance endpoint - performs system maintenance operations
123pub async fn maintenance(
124    State(shared_state): State<Arc<SharedState>>,
125    Json(payload): Json<MaintenanceRequest>,
126) -> Result<impl IntoResponse, StatusCode> {
127    match payload.operation.as_str() {
128        "cleanup_expired_sessions" => {
129            // Remove sessions from the in-memory DashMap that haven't been accessed
130            // in the last 30 minutes. Database records are kept intact.
131            const EXPIRY_SECS: u64 = 30 * 60;
132            let initial_count = shared_state.conversations.sessions.len();
133
134            shared_state.conversations.sessions.retain(|_, session| {
135                session.read()
136                    .map(|data| data.last_accessed.elapsed().as_secs() <= EXPIRY_SECS)
137                    .unwrap_or(true) // keep if lock poisoned
138            });
139
140            let removed = initial_count.saturating_sub(shared_state.conversations.sessions.len());
141            shared_state.counters.active_sessions.fetch_sub(
142                removed.min(shared_state.counters.active_sessions.load(std::sync::atomic::Ordering::Relaxed)),
143                std::sync::atomic::Ordering::Relaxed,
144            );
145
146            Ok((
147                StatusCode::OK,
148                Json(serde_json::json!({
149                    "message": "Expired sessions cleanup completed",
150                    "operation": "cleanup_expired_sessions",
151                    "initial_session_count": initial_count,
152                    "removed_count": removed,
153                    "status": "completed"
154                })),
155            ))
156        },
157        "optimize_database" => {
158            // Run PRAGMA optimize + WAL checkpoint to reclaim disk space and
159            // update query planner statistics.
160            match shared_state.database_pool.optimize() {
161                Ok(()) => Ok((
162                    StatusCode::OK,
163                    Json(serde_json::json!({
164                        "message": "Database optimization completed (PRAGMA optimize + WAL checkpoint)",
165                        "operation": "optimize_database",
166                        "status": "completed"
167                    })),
168                )),
169                Err(e) => Ok((
170                    StatusCode::INTERNAL_SERVER_ERROR,
171                    Json(serde_json::json!({
172                        "message": format!("Database optimization failed: {}", e),
173                        "operation": "optimize_database",
174                        "status": "error"
175                    })),
176                )),
177            }
178        },
179        "clear_inactive_sessions" => {
180            // More aggressive version: remove sessions idle for more than 5 minutes.
181            const INACTIVE_SECS: u64 = 5 * 60;
182            let initial_count = shared_state.conversations.sessions.len();
183
184            shared_state.conversations.sessions.retain(|_, session| {
185                session.read()
186                    .map(|data| data.last_accessed.elapsed().as_secs() <= INACTIVE_SECS)
187                    .unwrap_or(true)
188            });
189
190            let removed = initial_count.saturating_sub(shared_state.conversations.sessions.len());
191            shared_state.counters.active_sessions.fetch_sub(
192                removed.min(shared_state.counters.active_sessions.load(std::sync::atomic::Ordering::Relaxed)),
193                std::sync::atomic::Ordering::Relaxed,
194            );
195
196            Ok((
197                StatusCode::OK,
198                Json(serde_json::json!({
199                    "message": "Inactive sessions cleared",
200                    "operation": "clear_inactive_sessions",
201                    "initial_session_count": initial_count,
202                    "removed_count": removed,
203                    "status": "completed"
204                })),
205            ))
206        },
207        _ => {
208            Ok((
209                StatusCode::BAD_REQUEST,
210                Json(serde_json::json!({
211                    "message": format!("Unknown maintenance operation: {}", payload.operation),
212                    "supported_operations": ["cleanup_expired_sessions", "optimize_database", "clear_inactive_sessions"],
213                    "status": "error"
214                })),
215            ))
216        }
217    }
218}