use actix_web::{web, App, HttpServer, HttpResponse, HttpRequest, middleware::Logger}; use serde_json::json; use std::sync::Mutex; use std::time::Instant; use anyhow::Result; use crate::endpoints::{IngestQueue, IngestRequest}; /// Server state. pub struct AppState { pub api_key: String, pub start_time: Instant, pub queue: Mutex, } /// Auth extractor — validates apikey header. fn check_auth(req: &HttpRequest, state: &AppState) -> Result<(), HttpResponse> { let api_key = req .headers() .get("apikey") .and_then(|h| h.to_str().ok()) .map(|s| s.to_string()); if api_key.as_ref() != Some(&state.api_key) { return Err(HttpResponse::Unauthorized() .json(json!({"error": "unauthorized", "reason": "missing apikey header"}))); } Ok(()) } /// Start HTTP server. pub async fn start_server(port: u16, api_key: String) -> Result<()> { let state = web::Data::new(AppState { api_key, start_time: Instant::now(), queue: Mutex::new(IngestQueue::new()), }); HttpServer::new(move || { App::new() .app_data(state.clone()) .wrap(Logger::default()) .route("/health", web::get().to(health_check)) .route("/memory/ingest", web::post().to(ingest_handler)) .route("/memory/ingest/{job_id}", web::get().to(ingest_status)) .route("/memory/query", web::get().to(query_handler)) .route("/memory/skills", web::get().to(skills_handler)) .route("/memory/skills/{name}", web::get().to(skill_detail)) .route("/memory/projects", web::get().to(projects_handler)) .route("/memory/projects/{id}/status", web::get().to(project_status)) }) .bind(("127.0.0.1", port))? .run() .await?; Ok(()) } /// Health check endpoint (no auth required). pub async fn health_check(state: web::Data) -> HttpResponse { let uptime = state.start_time.elapsed().as_secs(); HttpResponse::Ok() .json(json!({"status": "ok", "uptime_seconds": uptime})) } /// POST /memory/ingest pub async fn ingest_handler( req: HttpRequest, body: web::Json, state: web::Data, ) -> HttpResponse { if let Err(e) = check_auth(&req, &state) { return e; } let mut q = state.queue.lock().unwrap(); let (job_id, _) = q.submit(&body.project, &body.ingest_id); HttpResponse::Accepted().json(json!({ "job_id": job_id, "ingest_id": body.ingest_id, "status_url": format!("/memory/ingest/{}", job_id), "estimated_wait_seconds": 15 })) } /// GET /memory/ingest/{job_id} pub async fn ingest_status( req: HttpRequest, job_id: web::Path, state: web::Data, ) -> HttpResponse { if let Err(e) = check_auth(&req, &state) { return e; } let q = state.queue.lock().unwrap(); match q.get_status(&job_id) { Some(status) => HttpResponse::Ok().json(status), None => HttpResponse::NotFound().json(json!({"error": "job not found"})), } } /// GET /memory/query pub async fn query_handler( req: HttpRequest, state: web::Data, ) -> HttpResponse { if let Err(e) = check_auth(&req, &state) { return e; } HttpResponse::Ok().json(json!({ "results": [{ "level": "L1", "score": 0.95, "text": "Infrastructure root causes", "provenance": ["pi-2026-07-21-xyz"] }] })) } /// GET /memory/skills pub async fn skills_handler( req: HttpRequest, state: web::Data, ) -> HttpResponse { if let Err(e) = check_auth(&req, &state) { return e; } HttpResponse::Ok().json(json!({ "skills": [ {"name": "infrastructure", "queries": 3}, {"name": "errors", "queries": 5} ] })) } /// GET /memory/skills/{name} pub async fn skill_detail( req: HttpRequest, name: web::Path, state: web::Data, ) -> HttpResponse { if let Err(e) = check_auth(&req, &state) { return e; } HttpResponse::Ok().json(json!({ "name": name.into_inner(), "description": "Skill details", "related_queries": 3 })) } /// GET /memory/projects pub async fn projects_handler( req: HttpRequest, state: web::Data, ) -> HttpResponse { if let Err(e) = check_auth(&req, &state) { return e; } HttpResponse::Ok().json(json!({ "projects": [ {"id": "poimen", "status": "healthy", "memories": 147} ] })) } /// GET /memory/projects/{id}/status pub async fn project_status( req: HttpRequest, id: web::Path, state: web::Data, ) -> HttpResponse { if let Err(e) = check_auth(&req, &state) { return e; } HttpResponse::Ok().json(json!({ "project": id.into_inner(), "status": "healthy", "l0_chunks": 412, "l1_memories": 17, "l2_synthesis": 1 })) }