Files
beacon-web/src/features/stats/useLiveStats.ts
T
MrAlders0n be072bb42c feat(stats): add Mesh + Observer stats tab with ECharts
Integrate the stats feature onto main: live mesh KPIs, observations
time-series, payload/node-type donuts, top-nodes/observers leaderboards,
radio presets, scope stats, and per-observer telemetry (battery, airtime,
noise, queue, receive errors) with 1h/6h/24h bucketing.

- adapt to the multi-IATA region model (useRegion -> single iata for /stats/*)
- rename stats getScopes -> getStatsScopes (avoids /scopes name-list collision)
- donut legend kept right but scrollable + truncating so it never clips
- balanced mesh grid; observer empty-state for telemetry-less observers
- normalize telemetry t to ms (raw path emits seconds, bucketed emits ms)
- add echarts 5.5.1; lazy-load StatsOverview chunk
2026-06-09 22:02:27 -04:00

65 lines
2.5 KiB
TypeScript

import { useCallback, useEffect, useRef } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useRegion } from "../../hooks/useRegion";
import { useWsPacketHandler, useWsObserverStatusHandler } from "../../hooks/useWsHandlers";
import type { WsManager } from "../../api/ws-manager";
import type { WsPacketObservation, WsObserverStatus } from "../../types/ws";
import type { StatsOverview, StatsRange } from "./types";
// Live overview KPIs: every packetObservation bumps the cached overview counters (no refetch). High
// frequency, so increments are coalesced and flushed once per animation frame. The overview query also
// refetches periodically (useStatsOverview) so the live deltas self-correct against the server.
export function useLiveOverview(wsManager: WsManager) {
const region = useRegion();
const qc = useQueryClient();
const pending = useRef({ packets: 0, obs: 0 });
const raf = useRef<number | null>(null);
const flush = useCallback(() => {
raf.current = null;
const { packets, obs } = pending.current;
if (!packets && !obs) return;
pending.current = { packets: 0, obs: 0 };
qc.setQueryData<StatsOverview>(["stats-overview", region], (old) =>
old
? { ...old, totalPackets: old.totalPackets + packets, totalObservations: old.totalObservations + obs }
: old,
);
}, [qc, region]);
const onPacket = useCallback(
(data: WsPacketObservation["data"]) => {
pending.current.obs += 1;
if (data.packet?.isFirstObservation) pending.current.packets += 1;
if (raf.current == null) raf.current = requestAnimationFrame(flush);
},
[flush],
);
useWsPacketHandler(wsManager, onPacket);
useEffect(
() => () => {
if (raf.current != null) cancelAnimationFrame(raf.current);
},
[],
);
}
// When the selected observer reports a status update, refresh its header + telemetry so battery,
// uptime, and the newest points reflect the change. Status messages are infrequent, so a refetch is fine.
export function useLiveObserver(wsManager: WsManager, observerId: string | null, range: StatsRange) {
const qc = useQueryClient();
const onStatus = useCallback(
(data: WsObserverStatus["data"]) => {
if (!observerId || data.observerId !== observerId) return;
qc.invalidateQueries({ queryKey: ["observer", observerId] });
qc.invalidateQueries({ queryKey: ["observer-telemetry", observerId, range] });
},
[qc, observerId, range],
);
useWsObserverStatusHandler(wsManager, onStatus);
}