use crate::config::{AgentConfig, HeartbeatMetadata}; use crate::metrics::Metric; use serde_json::json; use std::time::Duration; use thiserror::Error; #[derive(Debug, Error)] pub enum ApiError { #[error("HTTP request failed: {0}")] RequestFailed(String), #[error("HTTP status error: {0}")] StatusError(u16), #[error("JSON parsing error: {0}")] JsonError(#[from] std::io::Error), #[error("Task join error: {0}")] JoinError(String), } pub type Result = std::result::Result; /// API client for communicating with the Towerops server #[derive(Clone)] pub struct ApiClient { base_url: String, token: String, } impl ApiClient { /// Create a new API client pub fn new(base_url: String, token: String) -> Result { Ok(Self { base_url, token }) } /// Fetch configuration from the API pub async fn fetch_config(&self) -> Result { let url = format!("{}/api/v1/agent/config", self.base_url); let token = self.token.clone(); let config = tokio::task::spawn_blocking(move || { let response = ureq::get(&url) .set("Authorization", &format!("Bearer {}", token)) .timeout(Duration::from_secs(30)) .call() .map_err(|e| ApiError::RequestFailed(e.to_string()))?; let status = response.status(); if status != 200 { return Err(ApiError::StatusError(status)); } let config: AgentConfig = response .into_json() .map_err(|e| ApiError::RequestFailed(e.to_string()))?; Ok(config) }) .await .map_err(|e| ApiError::JoinError(e.to_string()))??; Ok(config) } /// Submit metrics to the API pub async fn submit_metrics(&self, metrics: Vec) -> Result<()> { if metrics.is_empty() { return Ok(()); } let url = format!("{}/api/v1/agent/metrics", self.base_url); let token = self.token.clone(); tokio::task::spawn_blocking(move || { let response = ureq::post(&url) .set("Authorization", &format!("Bearer {}", token)) .timeout(Duration::from_secs(30)) .send_json(json!({ "metrics": metrics })) .map_err(|e| ApiError::RequestFailed(e.to_string()))?; let status = response.status(); if status != 200 { return Err(ApiError::StatusError(status)); } Ok(()) }) .await .map_err(|e| ApiError::JoinError(e.to_string()))??; Ok(()) } /// Send heartbeat to the API pub async fn heartbeat(&self, metadata: HeartbeatMetadata) -> Result<()> { let url = format!("{}/api/v1/agent/heartbeat", self.base_url); let token = self.token.clone(); tokio::task::spawn_blocking(move || { let response = ureq::post(&url) .set("Authorization", &format!("Bearer {}", token)) .timeout(Duration::from_secs(30)) .send_json(&metadata) .map_err(|e| ApiError::RequestFailed(e.to_string()))?; let status = response.status(); if status != 200 { return Err(ApiError::StatusError(status)); } Ok(()) }) .await .map_err(|e| ApiError::JoinError(e.to_string()))??; Ok(()) } }