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
This commit is contained in:
MrAlders0n committed 2026-06-09 22:02:27 -04:00
1 parent ed9741ea63
commit be072bb42c
23 files changed
+1934 -291

No files matched your search

+52
View File
@@ -3,6 +3,16 @@ import type { CursorPage, PacketSummary, PacketDetail, IataCode, RegionSummary,
import type { ChannelSummary, ChannelMessage } from "../features/channels/types";
import type { ObserverSummary, Observer, AdvertObservation } from "../features/observers/types";
import type { NodeSummary, Node, NodeObservation, NodeNeighbor } from "../features/nodes/types";
import type {
StatsOverview,
ObservationPoint,
PayloadBreakdownItem,
TopNode,
TopObserver,
RadioPreset,
ScopeStats,
ObserverTelemetry,
} from "../features/stats/types";
// typed fetch wrapper with query params
@@ -239,4 +249,46 @@ export function getNodeNeighbors(nodeId: string): Promise<NodeNeighbor[]> {
return request(`/nodes/${nodeId}/neighbors`);
}
// stats endpoints. `iata` is a single code (undefined = all regions); the /stats/* endpoints filter
// by one IATA only, unlike the comma-separated `iatas` used elsewhere.
export function getStatsOverview(iata?: string): Promise<StatsOverview> {
return request("/stats/overview", { iata });
}
export function getStatsObservations(iata?: string, since?: number): Promise<ObservationPoint[]> {
return request("/stats/observations", { iata, since });
}
export function getPayloadBreakdown(iata?: string, since?: number): Promise<PayloadBreakdownItem[]> {
return request("/stats/payload-breakdown", { iata, since });
}
export function getTopNodes(iata?: string, limit = 10): Promise<TopNode[]> {
return request("/stats/top-nodes", { iata, limit });
}
export function getTopObservers(iata?: string, since?: number, limit = 10): Promise<TopObserver[]> {
return request("/stats/top-observers", { iata, since, limit });
}
export function getRadioPresets(iata?: string): Promise<RadioPreset[]> {
return request("/stats/radio-presets", { iata });
}
// renamed from getScopes to avoid colliding with the /scopes name list; this is the /stats/scopes
// aggregate (packet/observer/node counts), reported globally regardless of the active region.
export function getStatsScopes(): Promise<ScopeStats[]> {
return request("/stats/scopes");
}
export function getObserverTelemetry(
observerId: string,
range: string,
interval?: string,
afterId?: number,
): Promise<ObserverTelemetry> {
return request(`/observers/${observerId}/telemetry`, { range, interval, afterId });
}
export { ApiError };