use std::time::Duration;
use serde::Deserialize;
use crate::monitor::dashboard::{MemoryData, PalaceRow};
pub const DEFAULT_MEMORY_URL: &str = "http://127.0.0.1:7070";
const REQUEST_TIMEOUT: Duration = Duration::from_secs(3);
pub fn resolve_memory_url() -> String {
match crate::read_daemon_addr("trusty-memory") {
Ok(Some(addr)) => normalize_url(&addr),
_ => DEFAULT_MEMORY_URL.to_string(),
}
}
pub fn normalize_url(raw: &str) -> String {
if raw.starts_with("http://") || raw.starts_with("https://") {
raw.to_string()
} else {
format!("http://{raw}")
}
}
#[derive(Debug, Deserialize)]
struct StatusWire {
#[serde(default)]
version: String,
#[serde(default)]
palace_count: u64,
#[serde(default)]
total_drawers: u64,
#[serde(default)]
total_vectors: u64,
#[serde(default)]
total_kg_triples: u64,
}
#[derive(Debug, Default, Deserialize)]
struct PalaceWire {
#[serde(default)]
id: String,
#[serde(default)]
name: String,
#[serde(default, alias = "vectors", alias = "total_vectors")]
vector_count: u64,
}
#[derive(Debug, Clone)]
pub struct MemoryClient {
base: String,
http: reqwest::Client,
}
impl MemoryClient {
pub fn new(base: impl Into<String>) -> Self {
let http = reqwest::Client::builder()
.timeout(REQUEST_TIMEOUT)
.build()
.unwrap_or_default();
Self {
base: base.into(),
http,
}
}
pub fn base_url(&self) -> &str {
&self.base
}
pub fn set_base_url(&mut self, base: impl Into<String>) {
self.base = base.into();
}
pub async fn fetch_all(&self) -> anyhow::Result<MemoryData> {
let status: StatusWire = self
.http
.get(format!("{}/api/v1/status", self.base))
.send()
.await?
.error_for_status()?
.json()
.await?;
let palaces = match self.palaces().await {
Ok(rows) => rows,
Err(e) => {
tracing::warn!("palace list probe failed: {e}");
Vec::new()
}
};
Ok(MemoryData {
version: status.version,
palace_count: status.palace_count,
total_drawers: status.total_drawers,
total_vectors: status.total_vectors,
total_kg_triples: status.total_kg_triples,
palaces,
})
}
pub async fn is_healthy(&self) -> bool {
matches!(
self.http.get(format!("{}/health", self.base)).send().await,
Ok(r) if r.status().is_success()
)
}
async fn palaces(&self) -> anyhow::Result<Vec<PalaceRow>> {
let raw: serde_json::Value = self
.http
.get(format!("{}/api/v1/palaces", self.base))
.send()
.await?
.error_for_status()?
.json()
.await?;
Ok(parse_palaces(&raw))
}
}
pub fn parse_palaces(raw: &serde_json::Value) -> Vec<PalaceRow> {
let array = match raw {
serde_json::Value::Array(items) => items.clone(),
serde_json::Value::Object(obj) => match obj.get("palaces") {
Some(serde_json::Value::Array(items)) => items.clone(),
_ => Vec::new(),
},
_ => Vec::new(),
};
array
.into_iter()
.filter_map(|v| serde_json::from_value::<PalaceWire>(v).ok())
.map(|p| PalaceRow {
id: p.id,
name: p.name,
vector_count: p.vector_count,
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_memory_url_is_local() {
assert!(DEFAULT_MEMORY_URL.starts_with("http://127.0.0.1"));
}
#[test]
fn normalize_url_adds_scheme() {
assert_eq!(normalize_url("127.0.0.1:7070"), "http://127.0.0.1:7070");
assert_eq!(
normalize_url("http://127.0.0.1:7070"),
"http://127.0.0.1:7070"
);
}
#[test]
fn memory_client_stores_base_url() {
let client = MemoryClient::new("http://127.0.0.1:7070");
assert_eq!(client.base_url(), "http://127.0.0.1:7070");
}
#[test]
fn memory_client_repoints() {
let mut client = MemoryClient::new("http://127.0.0.1:7070");
client.set_base_url("http://127.0.0.1:8080");
assert_eq!(client.base_url(), "http://127.0.0.1:8080");
}
#[test]
fn resolve_memory_url_returns_http_url() {
let url = resolve_memory_url();
assert!(url.starts_with("http://") || url.starts_with("https://"));
}
#[test]
fn palace_list_accepts_array_and_object_shapes() {
let arr = serde_json::json!([
{"id": "p1", "name": "default", "vector_count": 8400},
{"id": "p2", "name": "work", "vectors": 0},
]);
let rows = parse_palaces(&arr);
assert_eq!(rows.len(), 2);
assert_eq!(rows[0].id, "p1");
assert_eq!(rows[0].vector_count, 8400);
assert_eq!(rows[1].name, "work");
let obj = serde_json::json!({
"palaces": [{"id": "p3", "name": "notes", "total_vectors": 12}],
});
let rows = parse_palaces(&obj);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0].vector_count, 12);
assert!(parse_palaces(&serde_json::json!("nonsense")).is_empty());
}
}