Files
zeekayandHanzo Dev 1f30a4c21c fix(lux): the fleet is observed through lux-k8s, on /v1
luxd serves /v1 only — server/http/server.go pins baseURL="/v1" — so every
/ext call in this repo was a silent no-op. The fleet collector's _rpc()
returns None on non-200, so the lux.cloud dashboard rendered nodes_rpc_ok:0,
version:null and every node rpc_reachable:false while the fleet was in fact
running luxd/1.36.2 (mainnet), 1.36.24 (testnet), 1.36.25 (devnet).

Fixing the path alone would not have helped: the collector addressed each
validator by a hardcoded public LoadBalancer IP, and there are now zero
LoadBalancer Services in lux-mainnet/testnet/devnet. Those IPs time out. So
node addressing is no longer configured — it is derived from the cluster on
every collection, and the one way to reach a validator is the lux-k8s API
server's pod proxy, since luxd RPC is cluster-internal.

That collapses the old MODE A / MODE B split into a single path: read the CR
for the declared shape, the pods for phase/readiness/IP, then each pod's
JSON-RPC through the proxy. One credential authenticates both. Deleting the
endpoint ConfigMap and its 15 baked-in IPs removes the failure mode where
infrastructure moves and a dashboard quietly reports zeros.

Probes move to /v1/health/liveness, not /v1/health: a degraded-but-running
validator answers /v1/health with 503, so probing it would restart nodes that
are merely behind. Any fleet from charts/lux could never have gone Ready.

LuxNode.external_ip becomes .ip — with no public per-pod endpoint, the honest
value is the pod IP.

Avalanche endpoints keep /ext; that is their API, not ours.

Measured, with controls: pod-proxy POST /v1/info -> 200 on all 15 pods;
/ext/health -> 404; /zzz/health -> 404; nonexistent pod -> 404. A test asserts
every probe path is under /v1 and fails if /ext returns.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 19:18:04 -07:00

148 lines
4.3 KiB
TypeScript

// Read-only Lux fleet overview client.
//
// Fetches the public fleet overview (GET /v1/fleets/lux/overview) served by the
// read-only fleet API. The endpoint exposes only public chain data — no auth,
// no secrets — so the lux.cloud landing + fleet dashboard can render it freely.
import { apiBase, getApiUrl } from "@/lib/api"
import { getBrand } from "@/lib/brand"
/**
* Resolve the API base for the active brand.
*
* The web image is brand-agnostic, so the build-time NEXT_PUBLIC_API_URL bakes
* the wrong host for the browser. The brand config (resolved at runtime by
* hostname) is the canonical per-brand API URL — e.g. lux.cloud → api.cloud.lux.network.
* Fall back to getApiUrl() (build-time env) when no brand URL is present.
*/
export interface ChainHeight {
chain: string
bootstrapped: boolean | null
height: number | null
}
export interface LuxNode {
pod_name: string
pod_index: number
phase: string
ready: boolean
ip: string | null
rpc_reachable: boolean
node_id: string | null
version: string | null
git_commit: string | null
db_version: string | null
peers: number | null
healthy: boolean | null
chains: ChainHeight[]
}
export interface DexSummary {
d_chain_bootstrapped: boolean | null
venue: string
markets: number | null
note: string | null
}
export type FleetNetworkName = "mainnet" | "testnet" | "devnet"
export interface LuxNetworkFleet {
network: FleetNetworkName
namespace: string
network_id: number | null
chain_id: number | null
phase: string
image: string | null
version: string | null
plugin_set: string | null
vm_plugins: string[]
validators: number
nodes_total: number
nodes_ready: number
nodes_rpc_ok: number
total_peers: number
dex: DexSummary
nodes: LuxNode[]
error: string | null
}
export interface LuxFleetOverview {
cluster: string
networks: LuxNetworkFleet[]
totals: Record<string, number>
generated_at: number
}
/** Fetch the read-only fleet overview. Unauthenticated. */
export async function fetchFleetOverview(signal?: AbortSignal): Promise<LuxFleetOverview> {
const res = await fetch(`${apiBase()}/v1/fleets/lux/overview`, {
signal,
headers: { Accept: "application/json" },
cache: "no-store",
})
if (!res.ok) throw new Error(`fleet overview: HTTP ${res.status}`)
return res.json()
}
// ---------------------------------------------------------------------------
// Derived display helpers
// ---------------------------------------------------------------------------
export type NodeHealth = "healthy" | "syncing" | "down" | "unknown"
/** Classify a node for the status dot. */
export function nodeHealth(n: LuxNode): NodeHealth {
if (n.rpc_reachable) {
const c = n.chains.find((x) => x.chain === "C")
if (c?.bootstrapped === false) return "syncing"
return "healthy"
}
if (n.phase === "Running" && n.ready) return "unknown" // up but RPC not exposed
if (n.phase === "Pending" || n.phase === "ContainerCreating") return "syncing"
return "down"
}
/** Network-level rollup status from its nodes. */
export function fleetStatus(f: LuxNetworkFleet): "operational" | "degraded" | "down" {
if (f.nodes_total === 0) return "down"
if (f.nodes_ready === 0) return "down"
if (f.nodes_ready < f.validators || f.nodes_ready < f.nodes_total) return "degraded"
return "operational"
}
/** Best per-chain height across the network's RPC-reachable nodes. */
export function bestHeight(f: LuxNetworkFleet, chain: string): number | null {
let best: number | null = null
for (const n of f.nodes) {
const c = n.chains.find((x) => x.chain === chain)
if (c?.height != null) best = best == null ? c.height : Math.max(best, c.height)
}
return best
}
/** Whether any node reports the chain bootstrapped. */
export function chainBootstrapped(f: LuxNetworkFleet, chain: string): boolean | null {
let seen = false
for (const n of f.nodes) {
const c = n.chains.find((x) => x.chain === chain)
if (c?.bootstrapped === true) return true
if (c?.bootstrapped != null) seen = true
}
return seen ? false : null
}
export const NETWORK_LABEL: Record<FleetNetworkName, string> = {
mainnet: "Mainnet",
testnet: "Testnet",
devnet: "Devnet",
}
export const TRACKED_CHAINS = ["C", "P", "X", "D"] as const
export const CHAIN_LABEL: Record<string, string> = {
C: "C-Chain",
P: "P-Chain",
X: "X-Chain",
D: "D-Chain (DEX)",
}