use axum::extract::{Path, Query, State};
use axum::response::{Html, IntoResponse, Json};
use axum::http::StatusCode;
use inf_eval::{eval, Ctx, EvalError, Output};
use inf_parse::parse;
use inf_plan::plan;
use inf_source::BbgSource;
use inf_value::Value;
use serde_json::json;
use std::collections::BTreeMap;
use crate::Shared;
pub async fn dash() -> Html<&'static str> {
Html(include_str!("../static/bbg_dash.html"))
}
pub async fn get_report(
State(state): State<Shared>,
Path(name): Path<String>,
Query(q): Query<BTreeMap<String, String>>,
) -> impl IntoResponse {
let app = state.lock().expect("lock");
let limit = q.get("limit").and_then(|v| v.parse().ok()).unwrap_or(20usize);
let bbg_state = &app.cell.bbg.state;
let out = match name.as_str() {
"overview" => overview(bbg_state),
"particles" => particles(bbg_state, limit),
"axons" => axons(bbg_state, limit),
"neurons" => neurons(bbg_state, limit),
"signals" => signals(bbg_state, limit),
"balances" => balances(bbg_state, limit),
"other_dims" => other_dims(bbg_state),
"size" => size_bytes(bbg_state),
_ => return (StatusCode::NOT_FOUND, "unknown report").into_response(),
};
Json(out).into_response()
}
fn q(src: &BbgSource, script: &str) -> Result<Output, String> {
let prog = parse(script).map_err(|e| format!("parse: {} @ {}:{}", e.msg, e.line, e.col))?;
let ir = plan(&prog).map_err(|e| format!("plan: {}", e.msg))?;
eval(&ir, src, &Ctx::default()).map_err(|e: EvalError| format!("eval: {}", e.msg))
}
fn as_word(v: &Value) -> u64 {
match v {
Value::Word(w) => *w,
Value::Int(i) => (*i).max(0) as u64,
_ => 0,
}
}
fn as_hash_hex(v: &Value) -> String {
match v {
Value::Hash(h) => hex::encode(h),
other => format!("{other:?}"),
}
}
fn scalar(src: &BbgSource, script: &str) -> u64 {
match q(src, script) {
Ok(out) if !out.rows.is_empty() => as_word(&out.rows[0][0]),
_ => 0,
}
}
pub fn overview(state: &bbg::state::BbgState) -> serde_json::Value {
let src = BbgSource::new(state);
let count = |rel: &str, col: &str| scalar(&src, &format!("?[count({col})] := {rel}{{{col}}}"));
let particles = count("particles", "id");
let axons = count("axons", "from");
let neurons = count("neurons", "id");
let signals = count("signals", "step");
let balances = count("balances", "key");
let locations = count("locations", "id");
let coins = count("coins", "denom");
let cards = count("cards", "card");
let files = count("files", "particle");
let time_rows = q(&src, "?[height, root] := time{height, root}").map(|o| o.rows).unwrap_or_default();
let (height, root) = time_rows
.iter()
.max_by_key(|r| as_word(&r[0]))
.map(|r| (as_word(&r[0]), as_hash_hex(&r[1])))
.unwrap_or((0, String::new()));
json!({
"particles": particles, "axons": axons, "neurons": neurons,
"signals": signals, "balances": balances, "locations": locations,
"coins": coins, "cards": cards, "files": files,
"height": height, "root": root,
"commitments": state.commitments.len(),
"nullifiers": state.nullifiers.len(),
"intents": state.intents.len(),
})
}
pub fn particles(state: &bbg::state::BbgState, limit: usize) -> serde_json::Value {
let src = BbgSource::new(state);
let mut rows: Vec<(String, u64, u64, u64, u64, u64, u64)> = q(
&src,
"?[id, energy, pi_star, weight, s_yes, s_no, meta_score] := particles{id, energy, pi_star, weight, s_yes, s_no, meta_score}",
)
.map(|out| {
out.rows
.iter()
.map(|r| {
(
as_hash_hex(&r[0]),
as_word(&r[1]),
as_word(&r[2]),
as_word(&r[3]),
as_word(&r[4]),
as_word(&r[5]),
as_word(&r[6]),
)
})
.collect()
})
.unwrap_or_default();
rows.sort_by(|a, b| b.1.cmp(&a.1).then(b.3.cmp(&a.3)).then(a.0.cmp(&b.0)));
rows.truncate(limit);
json!(rows
.into_iter()
.map(|(id, energy, pi_star, weight, s_yes, s_no, meta_score)| json!({
"particle": id, "energy": energy, "pi_star": pi_star, "weight": weight,
"s_yes": s_yes, "s_no": s_no, "meta_score": meta_score,
}))
.collect::<Vec<_>>())
}
pub fn axons(state: &bbg::state::BbgState, limit: usize) -> serde_json::Value {
let src = BbgSource::new(state);
let top = |rel: &str, key_col: &str, other_col: &str| -> Vec<serde_json::Value> {
let script = format!("?[{key_col}, count({other_col})] := {rel}{{{key_col}, {other_col}}}");
let mut rows: Vec<(String, u64)> = q(&src, &script)
.map(|out| out.rows.iter().map(|r| (as_hash_hex(&r[0]), as_word(&r[1]))).collect())
.unwrap_or_default();
rows.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
rows.truncate(limit);
rows.into_iter().map(|(p, n)| json!({"particle": p, "degree": n})).collect()
};
json!({
"out_degree": top("axons_out", "from", "to"),
"in_degree": top("axons_in", "to", "from"),
})
}
pub fn neurons(state: &bbg::state::BbgState, limit: usize) -> serde_json::Value {
let src = BbgSource::new(state);
let mut rows: Vec<(String, u64, u64, u64)> = q(
&src,
"?[id, focus, karma, stake] := neurons{id, focus, karma, stake}",
)
.map(|out| {
out.rows
.iter()
.map(|r| (as_hash_hex(&r[0]), as_word(&r[1]), as_word(&r[2]), as_word(&r[3])))
.collect()
})
.unwrap_or_default();
rows.sort_by(|a, b| b.2.cmp(&a.2).then(a.0.cmp(&b.0)));
rows.truncate(limit);
json!(rows
.into_iter()
.map(|(id, focus, karma, stake)| json!({"neuron": id, "focus": focus, "karma": karma, "stake": stake}))
.collect::<Vec<_>>())
}
pub fn signals(state: &bbg::state::BbgState, limit: usize) -> serde_json::Value {
let src = BbgSource::new(state);
let mut rows: Vec<(u64, String, u64, u64, String)> = q(
&src,
"?[step, neuron, link_count, block_height, proof_hash] := signals{step, neuron, link_count, block_height, proof_hash}",
)
.map(|out| {
out.rows
.iter()
.map(|r| {
(
as_word(&r[0]),
as_hash_hex(&r[1]),
as_word(&r[2]),
as_word(&r[3]),
as_hash_hex(&r[4]),
)
})
.collect()
})
.unwrap_or_default();
rows.sort_by(|a, b| b.3.cmp(&a.3).then(b.0.cmp(&a.0)));
rows.truncate(limit);
json!(rows
.into_iter()
.map(|(step, neuron, link_count, block_height, proof_hash)| json!({
"step": step, "neuron": neuron, "link_count": link_count,
"block_height": block_height, "proof_hash": proof_hash,
}))
.collect::<Vec<_>>())
}
pub fn balances(state: &bbg::state::BbgState, limit: usize) -> serde_json::Value {
let src = BbgSource::new(state);
let mut rows: Vec<(String, u64)> = q(&src, "?[key, amount] := balances{key, amount}")
.map(|out| out.rows.iter().map(|r| (as_hash_hex(&r[0]), as_word(&r[1]))).collect())
.unwrap_or_default();
rows.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
rows.truncate(limit);
json!(rows.into_iter().map(|(key, amount)| json!({"key": key, "amount": amount})).collect::<Vec<_>>())
}
pub fn other_dims(state: &bbg::state::BbgState) -> serde_json::Value {
let src = BbgSource::new(state);
let rows = |rel: &str, cols: &[&str]| -> Vec<serde_json::Value> {
let binds = cols.join(", ");
q(&src, &format!("?[{binds}] := {rel}{{{binds}}}"))
.map(|out| {
out.rows
.iter()
.map(|r| {
json!(r.iter().map(as_hash_hex_or_word).collect::<Vec<_>>())
})
.collect()
})
.unwrap_or_default()
};
json!({
"locations": rows("locations", &["id", "lat", "lon"]),
"coins": rows("coins", &["denom", "total_supply"]),
"cards": rows("cards", &["card", "owner", "particle"]),
"files": rows("files", &["particle", "available", "chunk_count"]),
})
}
pub fn size_bytes(state: &bbg::state::BbgState) -> serde_json::Value {
use bbg::types::{CardRecord, CoinRecord, FileRecord, IntentRecord, LocationRecord, NeuronRecord, ParticleRecord, SignalRecord};
use std::mem::size_of;
const KEY: usize = 32; const GOLDILOCKS_ESTIMATE: usize = 8;
let particles = state.particles.len() * (KEY + size_of::<ParticleRecord>());
let axons_out: usize = state.axons_out.values().map(|v| KEY + v.len() * KEY).sum();
let axons_in: usize = state.axons_in.values().map(|v| KEY + v.len() * KEY).sum();
let neurons = state.neurons.len() * (KEY + size_of::<NeuronRecord>());
let locations = state.locations.len() * (KEY + size_of::<LocationRecord>());
let coins = state.coins.len() * (KEY + size_of::<CoinRecord>());
let cards = state.cards.len() * (KEY + size_of::<CardRecord>());
let files = state.files.len() * (KEY + size_of::<FileRecord>());
let time = state.time.len() * (8 + KEY); let signals = state.signals.len() * (8 + size_of::<SignalRecord>()); let commitments = state.commitments.len() * (KEY + GOLDILOCKS_ESTIMATE);
let nullifiers = state.nullifiers.len() * KEY;
let balances = state.balances.len() * (KEY + size_of::<u64>());
let intents = state.intents.len() * (KEY + size_of::<IntentRecord>());
let axon_edges = state.axon_edges.len() * (KEY + 2 * KEY);
let total = particles
+ axons_out
+ axons_in
+ neurons
+ locations
+ coins
+ cards
+ files
+ time
+ signals
+ commitments
+ nullifiers
+ balances
+ intents
+ axon_edges;
json!({
"particles": particles, "axons_out": axons_out, "axons_in": axons_in,
"neurons": neurons, "locations": locations, "coins": coins, "cards": cards,
"files": files, "time": time, "signals": signals, "commitments": commitments,
"nullifiers": nullifiers, "balances": balances, "intents": intents,
"axon_edges": axon_edges, "total": total,
"estimate": true,
})
}
fn as_hash_hex_or_word(v: &Value) -> serde_json::Value {
match v {
Value::Hash(h) => json!(hex::encode(h)),
Value::Word(w) => json!(w),
Value::Int(i) => json!(i),
Value::Bool(b) => json!(b),
other => json!(format!("{other:?}")),
}
}