crates/monetize-server/src/services/estate.rs
//! **`EstateAdmin.Draw`** — the estate, drawn, over the wire.
//!
//! The renderer is [`crate::estate_svg`]; this is the door onto it, and the
//! whole of its job is to put the two halves in front of it honestly:
//!
//! * the DECLARED half is [`AppState::config`] — `[machines.<id>]`, the clouds,
//! and which cloud a purchase spends at. It is the file this server started
//! from, not a re-read: a picture drawn from a config the running process
//! never loaded would show machines it has never heard of.
//! * the MEASURED half is [`Measured::from_product`], which asks the product's
//! plugin what it already knows. Nothing here measures anything, and nothing
//! here turns a refusal into a number — that rule lives in the renderer and
//! in the plugin, and this file must not be the place it is quietly undone.
//!
//! # Two refusals, on purpose
//!
//! **A product this server does not serve is `NOT_FOUND`.** `estate_svg::from_config`
//! would cheerfully draw an empty board for a misspelt product, and an empty
//! board is already what an undeclared estate looks like — so a typo would be
//! indistinguishable from the truth. [`product_or_not_found`] names the products
//! that ARE linked in, which is the answer the operator needs.
//!
//! **A product that declares no machines is not asked anything.** With an empty
//! roll call there is nothing on the board for a reading to land on, so
//! [`Measured::default`] is the complete and honest answer and the plugin's box
//! is not disturbed. This is not an optimisation: it is what keeps a picture of
//! an estate that is not written down from hanging on a box that is not going to
//! be drawn.
//!
//! # What is deliberately NOT filled in
//!
//! [`Measured::tenants`] stays `None`, so the board's subtitle carries no tenant
//! count. Counting them means `Product::list_tenants` — a second live call to
//! the same box, for a number that already has a screen of its own
//! (`Products` → the product's tenants). A picture of the IRON should not hang
//! on the tenant list.
use std::sync::Arc;
use monetize_proto as pb;
use monetize_proto::estate_admin_server::EstateAdmin;
use tonic::{Request, Response, Status};
use crate::estate_svg::{estate_svg, Measured};
use crate::services::{blocking, product_or_not_found};
use crate::state::AppState;
pub struct EstateAdminService(pub Arc<AppState>);
#[tonic::async_trait]
impl EstateAdmin for EstateAdminService {
async fn draw(
&self,
request: Request<pb::DrawEstateRequest>,
) -> Result<Response<pb::DrawEstateResponse>, Status> {
let r = request.into_inner();
let (product, estate) = (r.product, r.estate);
// On the blocking pool like every other handler that touches a
// provider: `Product::servable` and `Product::twins` are sync calls to
// another box.
let (svg, machines) = blocking(&self.0, move |state| {
Ok(if estate.is_empty() {
draw_counted(state, &product)
} else {
draw_site(state, &product, &estate)
})
})
.await??;
Ok(Response::new(pb::DrawEstateResponse {
svg,
machines: Some(machines),
}))
}
}
/// **A site's board from its live fleet reports** ([`crate::estate_svg::from_fleet`]);
/// the product's `[machines]` board when no machine of the site has reported yet.
pub fn draw_site(state: &AppState, product: &str, estate: &str) -> Result<(String, u32), Status> {
product_or_not_found(state, product)?;
let Some(g) = state.fleet.guardian() else {
return draw_counted(state, product);
};
let boxes: Vec<crate::estate_svg::Reported> = g
.store
.roles_of(estate)
.into_iter()
.map(|role| {
let rec = g.store.box_record(estate, &role).unwrap_or_default();
crate::estate_svg::Reported {
serves_at: rec
.report
.health
.get("serves_at")
.cloned()
.unwrap_or_default(),
heard_unix_ms: rec.heard_unix_ms,
role,
}
})
.collect();
let e = crate::estate_svg::from_fleet(product, &boxes, state.now(), g.cfg.silent_after_ms);
if e.machines.is_empty() {
return draw_counted(state, product);
}
let n = e.machines.len() as u32;
Ok((estate_svg(&e), n))
}
/// The whole of the drawing, on the blocking pool. Separate from the trait
/// method so a test can hold the picture to account without a wire.
pub fn draw(state: &AppState, product: &str) -> Result<String, Status> {
draw_counted(state, product).map(|(svg, _)| svg)
}
/// [`draw`], and how many machines the board carries — counted off the SAME
/// roll call the picture was drawn from, so the number a console prints
/// beside the board ("this estate names no machines yet") cannot disagree
/// with the board.
pub fn draw_counted(state: &AppState, product: &str) -> Result<(String, u32), Status> {
let plugin = product_or_not_found(state, product)?;
let config = state
.config
.read()
.unwrap_or_else(|e| e.into_inner())
.clone();
let roll = crate::estate_svg::roll_call(&config, product);
// See the module's header: an empty roll call is asked nothing.
let live = if roll.is_empty() {
Measured::default()
} else {
Measured::from_product(plugin, &roll)
};
let estate = crate::estate_svg::from_config(&config, product, &live);
Ok((estate_svg(&estate), roll.len() as u32))
}