offline_intelligence/api/
admin_api.rs1use 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#[derive(Debug, Serialize)]
19pub struct HealthResponse {
20 pub status: String,
21 pub version: String,
22 pub uptime_seconds: u64,
23}
24
25#[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#[derive(Debug, Deserialize)]
36pub struct MaintenanceRequest {
37 pub operation: String,
38 pub parameters: Option<serde_json::Value>,
39}
40
41
42static 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 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
69pub async fn health(
71 State(_shared_state): State<Arc<SharedState>>,
72) -> Result<impl IntoResponse, StatusCode> {
73 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
88pub async fn db_stats(
90 State(shared_state): State<Arc<SharedState>>,
91) -> Result<impl IntoResponse, StatusCode> {
92 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 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; 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
122pub 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 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) });
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 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 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}