mirror of
https://github.com/MeshCore-Beacon/beacon-web.git
synced 2026-09-25 15:03:36 +00:00
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
65 lines
2.5 KiB
TypeScript
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);
|
|
}
|