import { createStore, produce } from "solid-js/store"; import type { BackendEventPayload, FrontendEventPayload, MaglevdStatusPayload, StateSnapshot, TransitionRecord, } from "../types"; // FrontendState keys snapshots by maglevd name. A single store drives the // whole UI; reducers produce() into the right branch. export type FrontendState = { byName: Record; }; const [state, setState] = createStore({ byName: {} }); export { state }; export function replaceSnapshot(snap: StateSnapshot) { setState( produce((s) => { s.byName[snap.maglevd.name] = snap; }), ); } export function replaceAll(snaps: StateSnapshot[]) { const byName: Record = {}; for (const s of snaps) byName[s.maglevd.name] = s; setState({ byName }); } export function applyBackendTransition(maglevd: string, p: BackendEventPayload) { setState( produce((s) => { const snap = s.byName[maglevd]; if (!snap) return; const b = snap.backends.find((x) => x.name === p.backend); if (!b) return; b.state = p.transition.to; b.last_transition = p.transition; if (!b.transitions) b.transitions = []; b.transitions.push(p.transition); if (b.transitions.length > 20) { b.transitions = b.transitions.slice(b.transitions.length - 20); } }), ); } export function applyFrontendTransition(maglevd: string, _p: FrontendEventPayload) { // Frontend roll-up state is computed per render in the current cut, so // there is nothing to update in the store. Kept as a named reducer so // the SSE dispatcher has one entry per event type and future frontend // state fields have a single place to land. void maglevd; } export function applyMaglevdStatus(maglevd: string, p: MaglevdStatusPayload) { setState( produce((s) => { const snap = s.byName[maglevd]; if (!snap) return; snap.maglevd.connected = p.connected; snap.maglevd.last_error = p.last_error; }), ); } // applyBackendEffectiveWeight updates the effective_weight of every pool // row that references the backend with the given address. Driven by the // vpp-lb-sync-as-* log events so the UI reflects VPP LB changes without // waiting for the 30s refresh tick. export function applyBackendEffectiveWeight(maglevd: string, address: string, weight: number) { setState( produce((s) => { const snap = s.byName[maglevd]; if (!snap) return; const b = snap.backends.find((x) => x.address === address); if (!b) return; for (const fe of snap.frontends) { for (const pool of fe.pools) { for (const pb of pool.backends) { if (pb.name === b.name) { pb.effective_weight = weight; } } } } }), ); } // Helpers used by views. export function lastTransitionAge(t?: TransitionRecord): string { if (!t || !t.at_unix_ns || t.at_unix_ns <= 0) return ""; const ms = Date.now() - t.at_unix_ns / 1e6; const s = Math.floor(ms / 1000); if (s < 60) return `${s}s ago`; const m = Math.floor(s / 60); if (m < 60) return `${m}m ago`; const h = Math.floor(m / 60); if (h < 48) return `${h}h ago`; return `${Math.floor(h / 24)}d ago`; }