//! REST HTTP handlers for the gateway: agents, projects, bot configuration, and pipeline. use crate::service::gateway::{self, GatewayState}; use poem::handler; use poem::http::StatusCode; use poem::web::Path as PoemPath; use poem::web::{Data, Json}; use poem::{Body, Response}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; use std::collections::BTreeMap; use std::sync::Arc; // ── Agent REST handlers ─────────────────────────────────────────────────────── /// `GET /gateway/mode` — returns `{"mode":"gateway"}`. #[handler] pub async fn gateway_mode_handler() -> Response { let body = json!({ "mode": "gateway" }); Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(Body::from(serde_json::to_vec(&body).unwrap_or_default())) } /// `POST /gateway/tokens` — generate a one-time join token. #[handler] pub async fn gateway_generate_token_handler(state: Data<&Arc>) -> Response { let token = gateway::generate_join_token(&state).await; let body = json!({ "token": token }); Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(Body::from(serde_json::to_vec(&body).unwrap_or_default())) } /// `GET /gateway/agents` — list all alive build agents registered in the CRDT. #[handler] pub async fn gateway_list_agents_handler(_state: Data<&Arc>) -> Response { let agents = gateway::list_agents(); let body = serde_json::to_vec(&agents).unwrap_or_default(); Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(Body::from(body)) } /// Request body for assigning an agent to a project. #[derive(Deserialize)] struct AssignAgentRequest { project: Option, } /// `POST /gateway/agents/:id/assign` — assign (or unassign) an agent to a project. #[handler] pub async fn gateway_assign_agent_handler( PoemPath(id): PoemPath, body: Json, state: Data<&Arc>, ) -> Response { match gateway::assign_agent(&state, &id, body.0.project).await { Ok(agent) => { let body = serde_json::to_vec(&agent).unwrap_or_default(); Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(Body::from(body)) } Err(gateway::Error::ProjectNotFound(msg)) => Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::from(msg)), Err(_) => Response::builder() .status(StatusCode::NOT_FOUND) .body(Body::from("agent not found")), } } // ── Gateway Web UI ──────────────────────────────────────────────────────────── /// `GET /api/gateway` — returns the list of registered projects and the active project. #[handler] pub async fn gateway_api_handler(state: Data<&Arc>) -> Response { let active = state.active_project.read().await.clone(); let projects: Vec = state .projects .read() .await .iter() .map(|(name, entry)| { json!({ "name": name, "url": entry.url, }) }) .collect(); let body = json!({ "active": active, "projects": projects }); Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(Body::from(serde_json::to_vec(&body).unwrap_or_default())) } /// Request body for `POST /api/gateway/switch`. #[derive(Deserialize)] struct SwitchRequest { project: String, } /// `POST /api/gateway/switch` — switch the active project. #[handler] pub async fn gateway_switch_handler( state: Data<&Arc>, body: Json, ) -> Response { match gateway::switch_project(&state, &body.project).await { Ok(_) => { let body_val = json!({ "ok": true }); Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(Body::from( serde_json::to_vec(&body_val).unwrap_or_default(), )) } Err(e) => { let body_val = json!({ "ok": false, "error": e.to_string() }); Response::builder() .status(StatusCode::BAD_REQUEST) .header("Content-Type", "application/json") .body(Body::from( serde_json::to_vec(&body_val).unwrap_or_default(), )) } } } // ── Project management API ──────────────────────────────────────────────────── /// Request body for adding a new project. #[derive(Deserialize)] struct AddProjectRequest { name: String, url: String, } /// `POST /api/gateway/projects` — add a new project. #[handler] pub async fn gateway_add_project_handler( state: Data<&Arc>, body: Json, ) -> Response { match gateway::add_project(&state, &body.name, &body.url).await { Ok(()) => { let name = body.0.name.trim().to_string(); let url = body.0.url.trim().to_string(); let body_val = json!({ "name": name, "url": url }); Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(Body::from( serde_json::to_vec(&body_val).unwrap_or_default(), )) } Err(gateway::Error::DuplicateToken(_)) => Response::builder() .status(StatusCode::CONFLICT) .body(Body::from(format!( "project '{}' already exists", body.0.name.trim() ))), Err(e) => Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::from(e.to_string())), } } /// `DELETE /api/gateway/projects/:name` — remove a project. #[handler] pub async fn gateway_remove_project_handler( PoemPath(name): PoemPath, state: Data<&Arc>, ) -> Response { match gateway::remove_project(&state, &name).await { Ok(()) => Response::builder() .status(StatusCode::NO_CONTENT) .body(Body::empty()), Err(gateway::Error::ProjectNotFound(msg)) => Response::builder() .status(StatusCode::NOT_FOUND) .body(Body::from(msg)), Err(e) => Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::from(e.to_string())), } } // ── Bot configuration API ───────────────────────────────────────────────────── /// Request/response body for the bot configuration API. #[derive(Deserialize, Serialize, Default)] pub(crate) struct BotConfigPayload { transport: String, homeserver: Option, username: Option, password: Option, slack_bot_token: Option, slack_signing_secret: Option, } /// `GET /api/gateway/bot-config` — return current bot.toml fields as JSON. #[handler] pub async fn gateway_bot_config_get_handler(state: Data<&Arc>) -> Response { let fields = gateway::io::read_bot_config_raw(&state.config_dir); let payload = BotConfigPayload { transport: fields.transport, homeserver: fields.homeserver, username: fields.username, password: fields.password, slack_bot_token: fields.slack_bot_token, slack_signing_secret: fields.slack_signing_secret, }; Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(Body::from(serde_json::to_vec(&payload).unwrap_or_default())) } /// `POST /api/gateway/bot-config` — write new bot.toml and restart the bot. #[handler] pub async fn gateway_bot_config_save_handler( state: Data<&Arc>, body: Json, ) -> Response { let content = gateway::config::serialize_bot_config( &body.transport, body.homeserver.as_deref(), body.username.as_deref(), body.password.as_deref(), body.slack_bot_token.as_deref(), body.slack_signing_secret.as_deref(), ); match gateway::save_bot_config_and_restart(&state, &content).await { Ok(()) => { let ok = json!({ "ok": true }); Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(Body::from(serde_json::to_vec(&ok).unwrap_or_default())) } Err(e) => { let err = json!({ "ok": false, "error": e.to_string() }); Response::builder() .status(StatusCode::INTERNAL_SERVER_ERROR) .header("Content-Type", "application/json") .body(Body::from(serde_json::to_vec(&err).unwrap_or_default())) } } } /// `GET /api/gateway/pipeline` — fetch pipeline status from all registered projects. #[handler] pub async fn gateway_all_pipeline_handler(state: Data<&Arc>) -> Response { let project_urls: BTreeMap = state .projects .read() .await .iter() .map(|(n, e)| (n.clone(), e.url.clone())) .collect(); let results = gateway::io::fetch_all_project_pipeline_statuses(&project_urls, &state.client).await; let active = state.active_project.read().await.clone(); let body = json!({ "active": active, "projects": results }); Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(Body::from(serde_json::to_vec(&body).unwrap_or_default())) } // ── Bot config page ─────────────────────────────────────────────────────────── /// Self-contained HTML page for bot configuration. const GATEWAY_BOT_CONFIG_HTML: &str = r#" Bot Configuration — Huskies Gateway
← Gateway

Bot Configuration


"#; /// Serve the bot configuration HTML page at `GET /bot-config`. #[handler] pub async fn gateway_bot_config_page_handler() -> Response { Response::builder() .status(StatusCode::OK) .header("Content-Type", "text/html; charset=utf-8") .body(Body::from(GATEWAY_BOT_CONFIG_HTML)) }