// 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 generated_at: number } /** Fetch the read-only fleet overview. Unauthenticated. */ export async function fetchFleetOverview(signal?: AbortSignal): Promise { 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 = { mainnet: "Mainnet", testnet: "Testnet", devnet: "Devnet", } export const TRACKED_CHAINS = ["C", "P", "X", "D"] as const export const CHAIN_LABEL: Record = { C: "C-Chain", P: "P-Chain", X: "X-Chain", D: "D-Chain (DEX)", }