2026-08-22 21:43:23 -07:00
|
|
|
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<IngestQueue>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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))
|
|
|
|
|
})
|
2026-08-23 00:01:30 -07:00
|
|
|
.bind(("0.0.0.0", port))?
|
2026-08-22 21:43:23 -07:00
|
|
|
.run()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Health check endpoint (no auth required).
|
|
|
|
|
pub async fn health_check(state: web::Data<AppState>) -> 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<IngestRequest>,
|
|
|
|
|
state: web::Data<AppState>,
|
|
|
|
|
) -> 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<String>,
|
|
|
|
|
state: web::Data<AppState>,
|
|
|
|
|
) -> 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<AppState>,
|
|
|
|
|
) -> 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<AppState>,
|
|
|
|
|
) -> 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<String>,
|
|
|
|
|
state: web::Data<AppState>,
|
|
|
|
|
) -> 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<AppState>,
|
|
|
|
|
) -> 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<String>,
|
|
|
|
|
state: web::Data<AppState>,
|
|
|
|
|
) -> 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
|
|
|
|
|
}))
|
|
|
|
|
}
|