dashboard-v2 PR 3: fleet aggregator via DashboardStatus RPC
Build with clawstor cache / Cargo build (clawstor-cached) (pull_request) Successful in 15s

Pivot from per-node dashboard to single-pane-of-glass. The
aggregator (typically the operator's laptop) holds a fleet-CA
leaf cert + the peer list; each dashboard request fans out to
every peer over the existing QUIC/mTLS cluster port and issues
the new DashboardStatus RPC. Peers don't need to run any HTTP
server of their own.

Backend:
* New RPC method DashboardStatus = 0x1c
* Server handler reads BlobStore / TagStore / RefStore /
  SnapshotStore / RefTracking counts + on-disk bytes + rustc
  release. Cheap: 5 filesystem walks per request.
* Client wrapper call_dashboard_status
* serve_v2 rewritten as aggregator: V2State holds a QuicClient +
  peer list from `[[cluster.peers]]`. Endpoints:
    GET /api/v2/fleet             fan-out to every peer, parallel
    GET /api/v2/node/:name/status one peer, on-demand
  Failed peers surface as { online: false, error: "..." } cards
  instead of dropping.

serve.rs graceful degrade: v2 aggregator routes only mount when
[cluster.tls] is set. Static SPA still serves at /v2/* even
without an aggregator config so operators see the SPA's built-in
"config missing" error.

Deployment model (this session):
* Aggregator runs on quantum (Mac) with a signed leaf.
* Fleet daemons run cluster-only — no HTTP dashboard anywhere
  on tank/architect/morpheus. The clawstor-dashboard.service
  systemd units on the fleet are being retired.

Frontend rework to consume /api/v2/fleet ships in the next PR.
This commit is contained in:
Omar Sobh
2026-07-14 16:19:27 -07:00
parent f33468b7c2
commit a2114b918d
4 changed files with 323 additions and 507 deletions
+13
View File
@@ -169,6 +169,19 @@ pub async fn call_peer_status(conn: &Connection) -> Result<PeerStatusReply> {
serde_json::from_slice(&reply).context("decoding PeerStatusReply JSON")
}
/// Convenience wrapper for [`Method::DashboardStatus`]. Feeds the
/// dashboard-v2 aggregator. Empty payload → JSON reply with counts
/// + on-disk bytes.
pub async fn call_dashboard_status(conn: &Connection) -> Result<DashboardStatusReply> {
let reply = rpc_call(conn, Method::DashboardStatus, &[]).await?;
if reply.len() == 1 {
if let Some(code) = decode_error(reply[0]) {
bail!("peer replied with error: {}", code.describe());
}
}
serde_json::from_slice(&reply).context("decoding DashboardStatusReply JSON")
}
/// Recognise a single-byte reply as one of our error codes. Returns
/// `None` for any other single-byte value (which is a valid reply,
/// just an unusually short one).