1use std::sync::Arc;
2
3use axum::Json;
4use axum::extract::{Path, Query, State};
5use axum::http::StatusCode;
6use axum::response::{Html, IntoResponse, Response};
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use mnemo_core::error::Error as CoreError;
11use mnemo_core::query::MnemoEngine;
12use mnemo_core::storage::MemoryFilter;
13
14type AppState = Arc<MnemoEngine>;
15
16pub struct AdminError(CoreError);
21
22impl IntoResponse for AdminError {
23 fn into_response(self) -> Response {
24 let (status, msg) = match &self.0 {
25 CoreError::Validation(m) => (StatusCode::BAD_REQUEST, m.clone()),
26 CoreError::PermissionDenied(m) => (StatusCode::FORBIDDEN, m.clone()),
27 CoreError::NotFound(m) => (StatusCode::NOT_FOUND, m.clone()),
28 other => (StatusCode::INTERNAL_SERVER_ERROR, other.to_string()),
29 };
30 (status, Json(serde_json::json!({"error": msg}))).into_response()
31 }
32}
33
34impl From<CoreError> for AdminError {
35 fn from(e: CoreError) -> Self {
36 AdminError(e)
37 }
38}
39
40#[derive(Debug, Serialize)]
45pub struct StatsResponse {
46 pub memory_count: usize,
47 pub event_count: usize,
48 pub agent_ids: Vec<String>,
49}
50
51#[derive(Debug, Serialize)]
52pub struct MemorySummary {
53 pub id: String,
54 pub agent_id: String,
55 pub content_preview: String,
56 pub memory_type: String,
57 pub scope: String,
58 pub importance: f32,
59 pub quarantined: bool,
60 pub quarantine_reason: Option<String>,
61 pub tags: Vec<String>,
62 pub created_at: String,
63 pub updated_at: String,
64}
65
66#[derive(Debug, Serialize)]
67pub struct EventSummary {
68 pub id: String,
69 pub agent_id: String,
70 pub event_type: String,
71 pub thread_id: Option<String>,
72 pub timestamp: String,
73 pub model: Option<String>,
74 pub tokens_input: Option<i64>,
75 pub tokens_output: Option<i64>,
76}
77
78#[derive(Debug, Serialize)]
79pub struct PaginatedMemories {
80 pub memories: Vec<MemorySummary>,
81 pub total: usize,
82 pub limit: usize,
83 pub offset: usize,
84}
85
86#[derive(Debug, Serialize)]
87pub struct PaginatedEvents {
88 pub events: Vec<EventSummary>,
89 pub total: usize,
90 pub limit: usize,
91 pub offset: usize,
92}
93
94#[derive(Debug, Serialize)]
95pub struct QuarantineResponse {
96 pub id: String,
97 pub quarantined: bool,
98 pub message: String,
99}
100
101#[derive(Debug, Deserialize)]
106pub struct MemoryQueryParams {
107 pub limit: Option<usize>,
108 pub offset: Option<usize>,
109 pub agent_id: Option<String>,
110}
111
112#[derive(Debug, Deserialize)]
113pub struct EventQueryParams {
114 pub limit: Option<usize>,
115 pub offset: Option<usize>,
116}
117
118pub async fn dashboard_handler() -> Html<&'static str> {
124 Html(include_str!("dashboard.html"))
125}
126
127pub async fn stats_handler(
129 State(engine): State<AppState>,
130) -> Result<Json<StatsResponse>, AdminError> {
131 let filter = MemoryFilter::default();
135 let memories = engine.storage.list_memories(&filter, 10_000, 0).await?;
136
137 let memory_count = memories.len();
138 let mut agent_ids: Vec<String> = memories
139 .iter()
140 .map(|m| m.agent_id.clone())
141 .collect::<std::collections::BTreeSet<_>>()
142 .into_iter()
143 .collect();
144 agent_ids.sort();
145
146 let mut event_count: usize = 0;
148 for aid in &agent_ids {
149 let events = engine.storage.list_events(aid, 10_000, 0).await?;
150 event_count += events.len();
151 }
152
153 Ok(Json(StatsResponse {
154 memory_count,
155 event_count,
156 agent_ids,
157 }))
158}
159
160pub async fn agents_handler(
162 State(engine): State<AppState>,
163) -> Result<Json<Vec<String>>, AdminError> {
164 let filter = MemoryFilter::default();
165 let memories = engine.storage.list_memories(&filter, 10_000, 0).await?;
166
167 let mut agent_ids: Vec<String> = memories
168 .iter()
169 .map(|m| m.agent_id.clone())
170 .collect::<std::collections::BTreeSet<_>>()
171 .into_iter()
172 .collect();
173 agent_ids.sort();
174
175 Ok(Json(agent_ids))
176}
177
178pub async fn memories_handler(
180 State(engine): State<AppState>,
181 Query(params): Query<MemoryQueryParams>,
182) -> Result<Json<PaginatedMemories>, AdminError> {
183 let limit = params.limit.unwrap_or(50).min(500);
184 let offset = params.offset.unwrap_or(0);
185
186 let filter = MemoryFilter {
187 agent_id: params.agent_id,
188 ..Default::default()
189 };
190
191 let memories = engine
193 .storage
194 .list_memories(&filter, limit + 1, offset)
195 .await?;
196
197 let has_more = memories.len() > limit;
198 let page: Vec<_> = memories.into_iter().take(limit).collect();
199
200 let summaries: Vec<MemorySummary> = page
201 .iter()
202 .map(|m| {
203 let preview = if m.content.len() > 100 {
204 let end = m
205 .content
206 .char_indices()
207 .take_while(|(i, _)| *i <= 100)
208 .last()
209 .map(|(i, c)| i + c.len_utf8())
210 .unwrap_or(m.content.len());
211 format!("{}...", &m.content[..end])
212 } else {
213 m.content.clone()
214 };
215 MemorySummary {
216 id: m.id.to_string(),
217 agent_id: m.agent_id.clone(),
218 content_preview: preview,
219 memory_type: m.memory_type.to_string(),
220 scope: m.scope.to_string(),
221 importance: m.importance,
222 quarantined: m.quarantined,
223 quarantine_reason: m.quarantine_reason.clone(),
224 tags: m.tags.clone(),
225 created_at: m.created_at.clone(),
226 updated_at: m.updated_at.clone(),
227 }
228 })
229 .collect();
230
231 let total = if has_more {
233 offset + limit + 1
234 } else {
235 offset + page.len()
236 };
237
238 Ok(Json(PaginatedMemories {
239 memories: summaries,
240 total,
241 limit,
242 offset,
243 }))
244}
245
246pub async fn events_handler(
248 State(engine): State<AppState>,
249 Query(params): Query<EventQueryParams>,
250) -> Result<Json<PaginatedEvents>, AdminError> {
251 let limit = params.limit.unwrap_or(50).min(500);
252 let offset = params.offset.unwrap_or(0);
253
254 let filter = MemoryFilter::default();
257 let memories = engine.storage.list_memories(&filter, 10_000, 0).await?;
258
259 let agent_ids: Vec<String> = memories
260 .iter()
261 .map(|m| m.agent_id.clone())
262 .collect::<std::collections::BTreeSet<_>>()
263 .into_iter()
264 .collect();
265
266 let mut all_events = Vec::new();
267 for aid in &agent_ids {
268 let events = engine.storage.list_events(aid, 10_000, 0).await?;
269 all_events.extend(events);
270 }
271
272 all_events.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
274
275 let total = all_events.len();
276 let page: Vec<_> = all_events.into_iter().skip(offset).take(limit).collect();
277
278 let summaries: Vec<EventSummary> = page
279 .iter()
280 .map(|e| EventSummary {
281 id: e.id.to_string(),
282 agent_id: e.agent_id.clone(),
283 event_type: e.event_type.to_string(),
284 thread_id: e.thread_id.clone(),
285 timestamp: e.timestamp.clone(),
286 model: e.model.clone(),
287 tokens_input: e.tokens_input,
288 tokens_output: e.tokens_output,
289 })
290 .collect();
291
292 Ok(Json(PaginatedEvents {
293 events: summaries,
294 total,
295 limit,
296 offset,
297 }))
298}
299
300pub async fn quarantine_handler(
302 State(engine): State<AppState>,
303 Path(id): Path<Uuid>,
304) -> Result<Json<QuarantineResponse>, AdminError> {
305 let record = engine
306 .storage
307 .get_memory(id)
308 .await?
309 .ok_or_else(|| CoreError::NotFound(format!("memory {id} not found")))?;
310
311 let mut updated = record;
312 updated.quarantined = true;
313 updated.quarantine_reason = Some("Quarantined by admin".to_string());
314 engine.storage.update_memory(&updated).await?;
315
316 Ok(Json(QuarantineResponse {
317 id: id.to_string(),
318 quarantined: true,
319 message: "Memory quarantined successfully".to_string(),
320 }))
321}
322
323pub async fn unquarantine_handler(
325 State(engine): State<AppState>,
326 Path(id): Path<Uuid>,
327) -> Result<Json<QuarantineResponse>, AdminError> {
328 let record = engine
329 .storage
330 .get_memory(id)
331 .await?
332 .ok_or_else(|| CoreError::NotFound(format!("memory {id} not found")))?;
333
334 let mut updated = record;
335 updated.quarantined = false;
336 updated.quarantine_reason = None;
337 engine.storage.update_memory(&updated).await?;
338
339 Ok(Json(QuarantineResponse {
340 id: id.to_string(),
341 quarantined: false,
342 message: "Memory released from quarantine".to_string(),
343 }))
344}
345
346pub async fn health_handler() -> Json<serde_json::Value> {
348 Json(serde_json::json!({"status": "ok", "service": "mnemo-admin"}))
349}