Backend - api/routes.rs models the Babel-style route shape; metric uses an untagged enum to round-trip both numeric hop counts and the literal "infinite" string the daemon emits for poisoned routes - routes_snapshot() runs the three GETs concurrently with try_join so the snapshot is internally consistent - poller spawns a second 5s loop emitting routes://updated; both loops are owned by the Poller and aborted together on stop_daemon Frontend - routes store mirrors the snapshot shape; tabbed view (radix-vue) with selected, fallback and queried lists - RouteTable component shared by selected/fallback; metric column is colour-coded (0 green, low neutral, high yellow, infinite red) - Queried subnets show a live `expires in 12s` countdown driven by a 1Hz tick ref instead of mutating the store
69 lines
2.0 KiB
Rust
69 lines
2.0 KiB
Rust
use crate::api::MyceliumClient;
|
|
use crate::error::AppResult;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// The daemon serializes the metric as either an unsigned integer for
|
|
/// reachable routes, or the literal string "infinite" for poisoned ones.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(untagged)]
|
|
pub enum Metric {
|
|
Value(u64),
|
|
Infinite(String),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct Route {
|
|
pub subnet: String,
|
|
pub next_hop: String,
|
|
pub metric: Metric,
|
|
pub seqno: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct QueriedSubnet {
|
|
pub subnet: String,
|
|
pub expiration: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct RoutesSnapshot {
|
|
pub selected: Vec<Route>,
|
|
pub fallback: Vec<Route>,
|
|
pub queried: Vec<QueriedSubnet>,
|
|
}
|
|
|
|
impl MyceliumClient {
|
|
pub async fn routes_selected(&self) -> AppResult<Vec<Route>> {
|
|
let r = self.http().get(self.url("/admin/routes/selected")).send().await?;
|
|
Self::parse(r).await
|
|
}
|
|
|
|
pub async fn routes_fallback(&self) -> AppResult<Vec<Route>> {
|
|
let r = self.http().get(self.url("/admin/routes/fallback")).send().await?;
|
|
Self::parse(r).await
|
|
}
|
|
|
|
pub async fn routes_queried(&self) -> AppResult<Vec<QueriedSubnet>> {
|
|
let r = self.http().get(self.url("/admin/routes/queried")).send().await?;
|
|
Self::parse(r).await
|
|
}
|
|
|
|
pub async fn routes_snapshot(&self) -> AppResult<RoutesSnapshot> {
|
|
// Issue the three calls concurrently so the snapshot reflects a
|
|
// near-coincident view of the routing table.
|
|
let (sel, fb, q) = tokio::try_join!(
|
|
self.routes_selected(),
|
|
self.routes_fallback(),
|
|
self.routes_queried(),
|
|
)?;
|
|
Ok(RoutesSnapshot {
|
|
selected: sel,
|
|
fallback: fb,
|
|
queried: q,
|
|
})
|
|
}
|
|
}
|