Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/features/observers/ObserverComparison.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function ObserverComparison({ observerA, activityA, range, observerBId, u
const bPoints = dataB && aligned ? fillActivity(dataB.points, intervalMs, window, dataB) : [];
return { ...heardOption(aPoints, colors, window), grid: { left: 48, right: 14, top: 32, bottom: 22 },
legend: { data: ["A", "B"], top: 0, textStyle: { color: colors.textNormal } },
series: [["A", aPoints, colors.primary], ["B", bPoints, colors.secondary]].map(([name, points, color]) => ({
series: [["A", aPoints, colors.series[0]], ["B", bPoints, colors.series[2]]].map(([name, points, color]) => ({
name: name as string, type: "line" as const, symbol: "none", connectNulls: false,
data: (points as typeof aPoints).map(p => [p.t, p.observations]),
lineStyle: { width: 2, color: color as string }, itemStyle: { color: color as string },
Expand All @@ -57,9 +57,9 @@ export function ObserverComparison({ observerA, activityA, range, observerBId, u
}, [activityA, dataB, aligned, intervalMs, start, end, colors]);
const date = (value: number | null | undefined) => value == null ? "—" : new Date(value).toLocaleString(i18n.resolvedLanguage, { timeZone: "UTC", dateStyle: "short", timeStyle: "short" });
const groups = overlap.data && !overlap.isError ? [
{ label: t("observerCompare.onlyA"), count: overlap.data.onlyA, color: colors.primary, textClass: "text-primary" },
{ label: t("observerCompare.both"), count: overlap.data.both, color: colors.green, textClass: "text-green" },
{ label: t("observerCompare.onlyB"), count: overlap.data.onlyB, color: colors.secondary, textClass: "text-secondary" },
{ label: t("observerCompare.onlyA"), count: overlap.data.onlyA, color: colors.series[0]! },
{ label: t("observerCompare.both"), count: overlap.data.both, color: colors.series[1]! },
{ label: t("observerCompare.onlyB"), count: overlap.data.onlyB, color: colors.series[2]! },
] : [];
const refresh = () => {
const nextUntil = onRefresh();
Expand Down Expand Up @@ -92,7 +92,7 @@ export function ObserverComparison({ observerA, activityA, range, observerBId, u
{overlap.isError ? <p role="alert">{t("common.loadFailed")}</p> : !overlap.data ? <p role="status">{t("common.loading")}</p> : <>
<p className="mb-3 text-lg font-semibold">{t("observerCompare.total", { count: overlap.data.totalPackets })}</p>
{overlap.data.totalPackets === 0 ? <p>{t("observerCompare.empty")}</p> : <div aria-hidden className="mb-3 flex h-5 overflow-hidden rounded">{groups.map(g => <div key={g.label} style={{ width: `${g.count / overlap.data!.totalPackets * 100}%`, background: g.color }} />)}</div>}
<dl className="grid grid-cols-3 gap-2 text-sm">{groups.map(g => <div key={g.label}><dt className="text-text-muted">{g.label}</dt><dd className={`text-lg font-semibold ${g.textClass}`}>{g.count.toLocaleString(i18n.resolvedLanguage)}</dd></div>)}</dl>
<dl className="grid grid-cols-3 gap-2 text-sm">{groups.map(g => <div key={g.label}><dt className="text-text-muted">{g.label}</dt><dd className="text-lg font-semibold" style={{ color: g.color }}>{g.count.toLocaleString(i18n.resolvedLanguage)}</dd></div>)}</dl>
</>}
</Card>}
</>}
Expand Down
8 changes: 4 additions & 4 deletions src/features/stats/MeshTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,10 +126,10 @@ export function MeshTab({ range, onSelectObserver }: MeshTabProps) {
return (
<div className="mx-auto flex w-full min-w-0 max-w-[1200px] flex-col gap-3.5 p-4">
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<StatCard label={t("mesh.totalPackets")} sublabel={ovWindow} accent="var(--color-primary)" value={formatCount(kpis?.uniquePackets)} spark={sparks.packets} />
<StatCard label={t("mesh.observations")} sublabel={ovWindow} accent="var(--color-green)" value={formatCount(kpis?.observations)} spark={sparks.observations} />
<StatCard label={t("mesh.activeObservers")} sublabel={ovWindow} accent="var(--color-secondary)" value={kpis?.activeObservers ?? "—"} spark={sparks.observers} />
<StatCard label={t("mesh.activeIatas")} sublabel={ovWindow} accent="var(--color-warn)" value={kpis?.activeIatas ?? "—"} spark={sparks.iatas} />
<StatCard label={t("mesh.totalPackets")} sublabel={ovWindow} accent={colors.series[0]!} value={formatCount(kpis?.uniquePackets)} spark={sparks.packets} />
<StatCard label={t("mesh.observations")} sublabel={ovWindow} accent={colors.series[1]!} value={formatCount(kpis?.observations)} spark={sparks.observations} />
<StatCard label={t("mesh.activeObservers")} sublabel={ovWindow} accent={colors.series[2]!} value={kpis?.activeObservers ?? "—"} spark={sparks.observers} />
<StatCard label={t("mesh.activeIatas")} sublabel={ovWindow} accent={colors.series[3]!} value={kpis?.activeIatas ?? "—"} spark={sparks.iatas} />
</div>

<ChartCard
Expand Down
14 changes: 7 additions & 7 deletions src/features/stats/PathsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ export function PathsTab({ range }: { range: StatsRange }) {
const data = loading || query.isError ? undefined : query.data;
const hours = useMemo(() => pathHours(data), [data]);
const categories = useMemo(() => [
{ name: t("paths.categories.hashed"), value: data?.hashed ?? 0, color: c.primary },
{ name: t("paths.categories.hashed"), value: data?.hashed ?? 0, color: c.series[0]! },
{ name: t("paths.categories.empty"), value: data?.empty ?? 0, color: c.textDim },
{ name: t("paths.categories.trace"), value: data?.trace ?? 0, color: c.warn },
{ name: t("paths.categories.unclassified"), value: data?.unclassified ?? 0, color: c.secondary },
{ name: t("paths.categories.trace"), value: data?.trace ?? 0, color: c.series[3]! },
{ name: t("paths.categories.unclassified"), value: data?.unclassified ?? 0, color: c.series[2]! },
], [data, c, t]);
const charts = useMemo(() => {
const width = donutOption((data?.hashWidths ?? []).map((bin, i) => ({ name: t("paths.hashWidth", { count: bin.bytes }), value: bin.receptions, color: c.series[i] })), c, formatCount(data?.hashed ?? 0), t("paths.hashPathsCenter"));
Expand Down Expand Up @@ -49,10 +49,10 @@ export function PathsTab({ range }: { range: StatsRange }) {
<SectionInfo text={[t("paths.subtitle"), t("paths.measurement"), data && t("paths.window", { since: formatUtc(data.since), until: formatUtc(data.until) })]} />
{query.isError && <p role="alert" className="text-sm text-danger">{t("paths.error")}</p>}
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
<StatCard label={t("paths.receptions")} value={data ? formatCount(data.receptions) : "—"} accent={c.primary} sublabel={t(`stats.ranges.${range}`)} spark={sparks.receptions} />
<StatCard label={t("paths.withHashPaths")} value={data ? formatCount(data.hashed) : "—"} accent={c.green} spark={sparks.hashed} />
<StatCard label={t("paths.multiByteShare")} value={data?.hashed ? `${(100 * multi / data.hashed).toFixed(1)}%` : "—"} accent={c.secondary} spark={sparks.multiShare} />
<StatCard label={t("paths.mostEntries")} value={largest ?? "—"} accent={c.warn} spark={sparks.maxEntries} />
<StatCard label={t("paths.receptions")} value={data ? formatCount(data.receptions) : "—"} accent={c.series[0]!} sublabel={t(`stats.ranges.${range}`)} spark={sparks.receptions} />
<StatCard label={t("paths.withHashPaths")} value={data ? formatCount(data.hashed) : "—"} accent={c.series[1]!} spark={sparks.hashed} />
<StatCard label={t("paths.multiByteShare")} value={data?.hashed ? `${(100 * multi / data.hashed).toFixed(1)}%` : "—"} accent={c.series[2]!} spark={sparks.multiShare} />
<StatCard label={t("paths.mostEntries")} value={largest ?? "—"} accent={c.series[3]!} spark={sparks.maxEntries} />
</div>
<div className="grid min-w-0 grid-cols-1 gap-3.5 lg:grid-cols-2">
<ChartCard title={t("paths.widthTitle")} option={charts.width} height={260} isEmpty={!data?.hashed} {...state} />
Expand Down
8 changes: 4 additions & 4 deletions src/features/stats/ScopesTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ export function ScopesTab({ range }: { range: StatsRange }) {
</label>
{query.isError && <p role="alert" className="text-sm text-danger">{t("scopes.error")}</p>}
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
<StatCard label={t("scopes.active")} value={value(totals.active)} accent={colors.secondary} spark={sparks?.active} />
<StatCard label={t("scopes.scopedPackets")} value={packetsUnknown ? "—" : value(totals.packets)} accent={colors.primary} spark={sparks?.packets} />
<StatCard label={t("scopes.memberships")} value={value(totals.memberships)} accent={colors.green} spark={sparks?.observers} sublabel={sparks?.observers && t("scopes.observersLine")} />
<StatCard label={t("scopes.defaultNodes")} value={value(totals.nodes)} accent={colors.warn} spark={sparks?.nodes} sublabel={sparks?.nodes && t("scopes.nodesLine")} />
<StatCard label={t("scopes.active")} value={value(totals.active)} accent={colors.series[2]!} spark={sparks?.active} />
<StatCard label={t("scopes.scopedPackets")} value={packetsUnknown ? "—" : value(totals.packets)} accent={colors.series[0]!} spark={sparks?.packets} />
<StatCard label={t("scopes.memberships")} value={value(totals.memberships)} accent={colors.series[1]!} spark={sparks?.observers} sublabel={sparks?.observers && t("scopes.observersLine")} />
<StatCard label={t("scopes.defaultNodes")} value={value(totals.nodes)} accent={colors.series[3]!} spark={sparks?.nodes} sublabel={sparks?.nodes && t("scopes.nodesLine")} />
</div>
<div className="grid grid-cols-1 gap-3.5 lg:grid-cols-2">
<ChartCard title={t("scopes.packetsTitle")} option={packets} height={height} isLoading={loading} isError={query.isError} isEmpty={!totals.packets} />
Expand Down
8 changes: 4 additions & 4 deletions src/features/stats/SignalTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ export function SignalTab({ range }: { range: StatsRange }) {
<SectionInfo text={[t("signal.subtitle"), t("signal.measurement"), data && t("signal.window", { since: formatUtc(data.since), until: formatUtc(data.until) })]} />
{query.isError && <p role="alert" className="text-sm text-danger">{t("signal.error")}</p>}
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
<StatCard label={t("signal.receptions")} value={data ? formatCount(data.receptions) : "—"} accent={c.primary} sublabel={t(`stats.ranges.${range}`)} spark={sparks?.receptions} />
<StatCard label={t("signal.meanSnr")} value={average(data?.snr.average, "dB")} accent={c.secondary} spark={sparks?.snr} />
<StatCard label={t("signal.meanRssi")} value={average(data?.rssi.average, "dBm")} accent={c.green} spark={sparks?.rssi} />
<StatCard label={t("signal.hoursWithRecords")} value={data ? `${data.hourly.length}/${hours.length}` : "—"} accent={c.warn} presence={sparks?.reported} />
<StatCard label={t("signal.receptions")} value={data ? formatCount(data.receptions) : "—"} accent={c.series[0]!} sublabel={t(`stats.ranges.${range}`)} spark={sparks?.receptions} />
<StatCard label={t("signal.meanSnr")} value={average(data?.snr.average, "dB")} accent={c.series[2]!} spark={sparks?.snr} />
<StatCard label={t("signal.meanRssi")} value={average(data?.rssi.average, "dBm")} accent={c.series[1]!} spark={sparks?.rssi} />
<StatCard label={t("signal.hoursWithRecords")} value={data ? `${data.hourly.length}/${hours.length}` : "—"} accent={c.series[3]!} presence={sparks?.reported} />
</div>
<div className="grid min-w-0 grid-cols-1 gap-3.5 lg:grid-cols-2">
<ChartCard title={t("signal.histogramTitle", { metric: "SNR", unit: "dB" })} option={charts.snr} height={280} isEmpty={!data?.snr.samples} {...state} />
Expand Down
8 changes: 4 additions & 4 deletions src/features/stats/TrafficTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ export function TrafficTab({ range }: { range: StatsRange }) {
<SectionInfo text={[t("traffic.subtitle"), t("traffic.measurement")]} />
{query.isError && <p role="alert" className="text-sm text-danger">{t("traffic.error")}</p>}
<div className="grid grid-cols-2 gap-3 lg:grid-cols-4">
<StatCard label={t("traffic.receptions")} value={value(model.total)} accent={colors.primary} sublabel={t(`stats.ranges.${range}`)} spark={sparks?.totals} />
<StatCard label={t("traffic.reportingIatas")} value={value(model.areas.filter((area) => area.total > 0).length)} accent={colors.green} spark={sparks?.areas} />
<StatCard label={t("traffic.busiestHour")} value={unavailable || !model.peak ? "—" : formatCount(model.peak.total)} accent={colors.secondary} spark={sparks?.totals} markPeak />
<StatCard label={t("traffic.hoursWithRecords")} value={unavailable ? "—" : `${model.reportedHours}/${model.hours.length}`} accent={colors.warn} presence={sparks?.reported} />
<StatCard label={t("traffic.receptions")} value={value(model.total)} accent={colors.series[0]!} sublabel={t(`stats.ranges.${range}`)} spark={sparks?.totals} />
<StatCard label={t("traffic.reportingIatas")} value={value(model.areas.filter((area) => area.total > 0).length)} accent={colors.series[1]!} spark={sparks?.areas} />
<StatCard label={t("traffic.busiestHour")} value={unavailable || !model.peak ? "—" : formatCount(model.peak.total)} accent={colors.series[2]!} spark={sparks?.totals} markPeak />
<StatCard label={t("traffic.hoursWithRecords")} value={unavailable ? "—" : `${model.reportedHours}/${model.hours.length}`} accent={colors.series[3]!} presence={sparks?.reported} />
</div>
<ChartCard title={t("traffic.trendTitle")} right={<span className="text-[10px] text-text-muted">{t("traffic.hourlyRecords")}</span>} option={trend} height={260} isLoading={loading} isError={query.isError} isEmpty={!model.total} />
<ChartCard title={t("traffic.hourlyTitle", { range: t(`stats.ranges.${range}`) })} option={heatmap} height={Math.max(150, model.days.length * 15 + 72)} isLoading={loading} isError={query.isError} isEmpty={!model.total} />
Expand Down
20 changes: 10 additions & 10 deletions src/features/stats/chartOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@ export function observationsAreaOption(
smooth: true,
symbol: "none",
data: points.map((p) => [p.hour, p.observations]),
lineStyle: { color: c.primary, width: 2 },
itemStyle: { color: c.primary },
lineStyle: { color: c.series[0], width: 2 },
itemStyle: { color: c.series[0] },
areaStyle: {
color: {
type: "linear",
x: 0, y: 0, x2: 0, y2: 1,
colorStops: [
{ offset: 0, color: withAlpha(c.primary, 0.42) },
{ offset: 1, color: withAlpha(c.primary, 0.01) },
{ offset: 0, color: withAlpha(c.series[0]!, 0.42) },
{ offset: 1, color: withAlpha(c.series[0]!, 0.01) },
],
},
},
Expand All @@ -78,8 +78,8 @@ export function observationsAreaOption(
smooth: true,
symbol: "none",
data: points.map((p) => [p.hour, p.uniquePackets]),
lineStyle: { color: c.secondary, width: 1.3, type: "dashed" },
itemStyle: { color: c.secondary },
lineStyle: { color: c.series[2], width: 1.3, type: "dashed" },
itemStyle: { color: c.series[2] },
},
],
};
Expand Down Expand Up @@ -198,10 +198,10 @@ export function presetBarsOption(
},
},
series: [
{ name: labels.nodes, ...segment(rows.map((r) => r.nodes), c.primary) },
{ name: labels.nodes, ...segment(rows.map((r) => r.nodes), c.series[0]!) },
{
name: labels.observers,
...segment(rows.map((r) => r.observers), c.secondary),
...segment(rows.map((r) => r.observers), c.series[2]!),
// outer segment carries the row total so it sits at the end of the whole stack
label: {
show: true,
Expand Down Expand Up @@ -327,8 +327,8 @@ export function airtimeOption(points: TelemetryPoint[], c: ChartColors, bucketMs
xAxis: timeAxis(c),
yAxis: percentAxis(c),
series: [
{ name: "RX", type: "line", stack: "air", smooth: true, symbol: "none", connectNulls: true, data: airtimePctSeries(points, "airtimeRxSecs", bucketMs), lineStyle: { width: 1, color: c.green }, areaStyle: { color: withAlpha(c.green, 0.35) }, itemStyle: { color: c.green } },
{ name: "TX", type: "line", stack: "air", smooth: true, symbol: "none", connectNulls: true, data: airtimePctSeries(points, "airtimeTxSecs", bucketMs), lineStyle: { width: 1, color: c.primary }, areaStyle: { color: withAlpha(c.primary, 0.35) }, itemStyle: { color: c.primary } },
{ name: "RX", type: "line", stack: "air", smooth: true, symbol: "none", connectNulls: true, data: airtimePctSeries(points, "airtimeRxSecs", bucketMs), lineStyle: { width: 1, color: c.series[1] }, areaStyle: { color: withAlpha(c.series[1]!, 0.35) }, itemStyle: { color: c.series[1] } },
{ name: "TX", type: "line", stack: "air", smooth: true, symbol: "none", connectNulls: true, data: airtimePctSeries(points, "airtimeTxSecs", bucketMs), lineStyle: { width: 1, color: c.series[0] }, areaStyle: { color: withAlpha(c.series[0]!, 0.35) }, itemStyle: { color: c.series[0] } },
],
};
}
Expand Down
Loading
Loading