Serve analytics from hourly rollups and handle empty regions (#132)

This commit is contained in:
MrAlders0n authored and GitHub committed 2026-10-02 08:03:00 -04:00
1 parent f9ffb564c7
commit 39e921c277
38 files changed
+507 -391

No files matched your search

+47 -29
View File
@@ -5,7 +5,7 @@ import type { ChannelPage, 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,
StatsSeries,
SignalStats,
PathStats,
ObserverComparison,
@@ -71,11 +71,11 @@ async function request<T>(path: string, params?: Record<string, string | number
// endpoint functions
export function getObserverComparison(
iatas: string[] | undefined,
iatas: StatsRegion,
params: { observerA: string; observerB: string; since: number; until: number },
signal?: AbortSignal,
): Promise<ObserverComparison> {
return request("/stats/observer-comparison", { ...params, iatas: iatasParam(iatas) }, signal);
return request("/stats/observer-comparison", { ...params, ...statsRegionParams(iatas) }, signal);
}
// The region filter travels as the comma-separated `iatas` param; undefined/empty means all regions.
@@ -83,10 +83,22 @@ function iatasParam(iatas?: string[]): string | undefined {
return iatas && iatas.length > 0 ? iatas.join(",") : undefined;
}
// An empty list is a region with no IATAs. These endpoints would answer it with every IATA, so skip them.
const noIatas = (iatas?: string[]) => iatas?.length === 0;
const emptyPage = <T>(): CursorPage<T> => ({ items: [], nextCursor: null, hasMore: false });
// Stats endpoints also take a region slug, which the server answers with zeros when it has no IATAs.
export type StatsRegion = string[] | { region: string } | undefined;
function statsRegionParams(r: StatsRegion): { iatas?: string; region?: string } {
return Array.isArray(r) || r === undefined ? { iatas: iatasParam(r) } : { region: r.region };
}
export function getPackets(
iatas: string[] | undefined,
params?: { cursor?: number; limit?: number; payloadTypes?: number[]; routeTypes?: number[]; scopes?: string[] },
): Promise<CursorPage<PacketSummary>> {
if (noIatas(iatas)) return Promise.resolve(emptyPage());
return request("/packets", {
iatas: iatasParam(iatas),
cursor: params?.cursor,
@@ -127,6 +139,7 @@ export function getRegion(regionId: number): Promise<Region> {
// Preserve the server cursor rather than deriving it from the displayed channel order.
export function getChannels(params?: { iatas?: string[]; limit?: number; cursor?: number | string; keyKnown?: boolean }): Promise<ChannelPage> {
if (noIatas(params?.iatas)) return Promise.resolve(emptyPage());
const iatas = params?.iatas ?? [];
return request("/channels", {
iata: iatas.length === 1 ? iatas[0] : undefined,
@@ -145,6 +158,7 @@ export async function getChannelMessagesPage(
channelId: number,
params?: { iatas?: string[]; cursor?: number; limit?: number; scope?: string },
): Promise<CursorPage<ChannelMessage>> {
if (noIatas(params?.iatas)) return emptyPage();
const limit = params?.limit ?? DEFAULT_PAGE_SIZE;
const page = await request<{ items: ChannelMessage[]; nextCursor?: number | null; hasMore?: boolean }>(`/channels/${channelId}/messages`, {
iatas: iatasParam(params?.iatas),
@@ -163,6 +177,7 @@ export function getBrokers(): Promise<BrokerStatus[]> {
// Transport scope names (e.g. "#bc", "#west") for the scope filter dropdowns. With IATAs the server
// returns only that region's scopes (manual config + its MeshMapper catalogue); bare, every stored name.
export function getScopes(iatas?: string[]): Promise<string[]> {
if (noIatas(iatas)) return Promise.resolve([]);
return request("/scopes", { iatas: iatasParam(iatas) });
}
@@ -217,6 +232,7 @@ export function getTraces(
iatas: string[] | undefined,
params?: { scope?: string; type?: TraceType; since?: number; until?: number; cursor?: number; limit?: number },
): Promise<TraceTagSummary[]> {
if (noIatas(iatas)) return Promise.resolve([]);
return request("/traces", {
iatas: iatasParam(iatas),
scope: params?.scope,
@@ -268,6 +284,7 @@ export function getNodesPage(
neighbors?: boolean; // include each node's neighborIds (?neighbors=true)
},
): Promise<CursorPage<NodeSummary>> {
if (noIatas(iatas)) return Promise.resolve(emptyPage());
return request("/nodes", {
iatas: iatasParam(iatas),
cursor: params?.cursor,
@@ -286,6 +303,7 @@ export function getObserversPage(
iatas: string[] | undefined,
params?: { cursor?: number; limit?: number; type?: string; broker?: string; status?: string; name?: string },
): Promise<CursorPage<ObserverSummary>> {
if (noIatas(iatas)) return Promise.resolve(emptyPage());
return request("/observers", {
iatas: iatasParam(iatas),
cursor: params?.cursor,
@@ -317,60 +335,60 @@ export function getNodeNeighbors(nodeId: string): Promise<NodeNeighbor[]> {
// stats endpoints
export function getStatsOverview(iatas?: string[]): Promise<StatsOverview> {
return request("/stats/overview", { iatas: iatasParam(iatas) });
export function getStatsSeries(since: number, until: number, iatas?: StatsRegion, signal?: AbortSignal): Promise<StatsSeries> {
return request("/stats/series", { since, until, ...statsRegionParams(iatas) }, signal);
}
export function getStatsObservations(iatas?: string[], since?: number, signal?: AbortSignal): Promise<ObservationPoint[]> {
return request("/stats/observations", { iatas: iatasParam(iatas), since }, signal);
export function getStatsObservations(iatas?: StatsRegion, since?: number, signal?: AbortSignal): Promise<ObservationPoint[]> {
return request("/stats/observations", { ...statsRegionParams(iatas), since }, signal);
}
export function getSignalStats(since: number, until: number, iatas?: string[], signal?: AbortSignal): Promise<SignalStats> {
return request("/stats/signal", { since, until, iatas: iatasParam(iatas) }, signal);
export function getSignalStats(since: number, until: number, iatas?: StatsRegion, signal?: AbortSignal): Promise<SignalStats> {
return request("/stats/signal", { since, until, ...statsRegionParams(iatas) }, signal);
}
export function getPathStats(since: number, until: number, iatas?: string[], signal?: AbortSignal): Promise<PathStats> {
return request("/stats/paths", { since, until, iatas: iatasParam(iatas) }, signal);
export function getPathStats(since: number, until: number, iatas?: StatsRegion, signal?: AbortSignal): Promise<PathStats> {
return request("/stats/paths", { since, until, ...statsRegionParams(iatas) }, signal);
}
export function getPayloadBreakdown(iatas?: string[], since?: number): Promise<PayloadBreakdownItem[]> {
return request("/stats/payload-breakdown", { iatas: iatasParam(iatas), since });
export function getPayloadBreakdown(iatas?: StatsRegion, since?: number): Promise<PayloadBreakdownItem[]> {
return request("/stats/payload-breakdown", { ...statsRegionParams(iatas), since });
}
export function getTopNodes(iatas?: string[], limit = 10): Promise<TopNode[]> {
return request("/stats/top-nodes", { iatas: iatasParam(iatas), limit });
export function getTopNodes(iatas?: StatsRegion, since?: number, limit = 10): Promise<TopNode[]> {
return request("/stats/top-nodes", { ...statsRegionParams(iatas), since, limit });
}
export function getTopObservers(iatas?: string[], since?: number, limit = 10): Promise<TopObserver[]> {
return request("/stats/top-observers", { iatas: iatasParam(iatas), since, limit });
export function getTopObservers(iatas?: StatsRegion, since?: number, limit = 10): Promise<TopObserver[]> {
return request("/stats/top-observers", { ...statsRegionParams(iatas), since, limit });
}
export function getTopAdvertisers(iatas?: string[], since?: number, limit = 10): Promise<TopAdvertiser[]> {
return request("/stats/top-advertisers", { iatas: iatasParam(iatas), since, limit });
export function getTopAdvertisers(iatas?: StatsRegion, since?: number, limit = 10): Promise<TopAdvertiser[]> {
return request("/stats/top-advertisers", { ...statsRegionParams(iatas), since, limit });
}
export function getTopTalkers(iatas?: string[], since?: number, limit = 10): Promise<TopTalker[]> {
return request("/stats/top-talkers", { iatas: iatasParam(iatas), since, limit });
export function getTopTalkers(iatas?: StatsRegion, since?: number, limit = 10): Promise<TopTalker[]> {
return request("/stats/top-talkers", { ...statsRegionParams(iatas), since, limit });
}
export function getRadioPresets(iatas?: string[]): Promise<RadioPreset[]> {
return request("/stats/radio-presets", { iatas: iatasParam(iatas) });
export function getRadioPresets(iatas?: StatsRegion): Promise<RadioPreset[]> {
return request("/stats/radio-presets", { ...statsRegionParams(iatas) });
}
export function getStatsNodeTypes(iatas?: string[]): Promise<NodeTypeCount[]> {
return request("/stats/node-types", { iatas: iatasParam(iatas) });
export function getStatsNodeTypes(iatas?: StatsRegion): Promise<NodeTypeCount[]> {
return request("/stats/node-types", { ...statsRegionParams(iatas) });
}
// Repeaters/room servers whose clock has drifted past the server threshold, worst-first. Not
// time-windowed and top-N only (no cursor), so callers pass a generous limit and page client-side.
export function getClockDrift(iatas?: string[], limit = 100): Promise<ClockDriftEntry[]> {
return request("/stats/clock-drift", { iatas: iatasParam(iatas), limit });
export function getClockDrift(iatas?: StatsRegion, limit = 100): Promise<ClockDriftEntry[]> {
return request("/stats/clock-drift", { ...statsRegionParams(iatas), limit });
}
// renamed from getScopes to avoid colliding with the /scopes name list; this is the /stats/scopes
// aggregate (packet/observer/node counts), filtered by the selected IATAs.
export function getStatsScopes(iatas?: string[], signal?: AbortSignal): Promise<ScopeStats[]> {
return request("/stats/scopes", { iatas: iatasParam(iatas) }, signal);
export function getStatsScopes(iatas?: StatsRegion, since?: number, signal?: AbortSignal): Promise<ScopeStats[]> {
return request("/stats/scopes", { ...statsRegionParams(iatas), since }, signal);
}
export function getObserverTelemetry(
+2 -1
View File
@@ -257,7 +257,8 @@ export class WsManager {
}
private sendSubscribe(): void {
if (!this.filter) return;
// the server reads an empty IATA list as "all", so a region with no IATAs subscribes to nothing
if (!this.filter || this.filter.iatas?.length === 0) return;
const id = `sub-${this.nextId()}`;
this.lastSubscribeId = id;
this.send({