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
2 changes: 1 addition & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ function AppInner() {
// master/detail layout and renders on any tab — same path NodeDetailPanel's onAnalyzePacket uses
Traces: <TraceList onAnalyze={hash => { if (hash) viewPacket(hash); }} onViewNode={viewNode} />,
Channels: <ChannelList wsManager={wsManager} onAnalyze={handleAnalyze} />,
Analytics: <StatsOverview wsManager={wsManager} />,
Analytics: <StatsOverview wsManager={wsManager} onViewNode={viewNode} />,
Map: <MapView wsManager={wsManager} selectedNodeId={selectedNodeId} onSelectNode={setSelectedNodeId} />,
};

Expand Down
6 changes: 3 additions & 3 deletions src/components/AppShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function CheckBox({ checked }: { checked: boolean }) {
);
}

// Compact header summary of the active selection, e.g. "ALL", "YVR, YYJ", "2 regions", "1 region · 3 IATA".
// Compact header summary of the active selection, e.g. "ALL", "YVR, YYJ", "2 regions", "1 region · 3 areas".
function regionSummaryLabel(selection: RegionSelection, t: TFunction): string {
if (isAllRegions(selection)) return t("region.allShort");
const parts: string[] = [];
Expand Down Expand Up @@ -135,7 +135,7 @@ function RegionSelector() {
className="flex items-center gap-1.5 bg-bg-raised border border-border rounded px-3 py-1 text-text-bright font-mono text-xs font-semibold hover:border-text-dim/30 transition-colors"
onClick={toggle}
>
<span className="text-text-muted font-normal text-[11px]">{t("region.label")}</span>
<span className="text-text-muted font-normal text-[11px] uppercase">{t("region.label")}</span>
{regionSummaryLabel(selection, t)}
<span className="text-text-dim text-[11px]">▾</span>
</button>
Expand Down Expand Up @@ -277,7 +277,7 @@ function RegionSelectorPanel() {
<>
<div className={`px-3 pt-2 pb-1 text-[10px] font-mono uppercase tracking-wide text-text-dim ${
hasRowsAbove ? "border-t border-border-subtle mt-1" : ""
}`}>IATA</div>
}`}>{t("region.areaGroup")}</div>
{iatas ? (
shownIatas.map((i) => {
const checked = selection.iatas.includes(i.iata);
Expand Down
2 changes: 1 addition & 1 deletion src/components/CopyLinkButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { VARIANT_CLASSES } from "./badge-utils";
// (e.g. the map camera) and use a null value to delete a key that's now at its default.
export function CopyLinkButton({
params,
label = "Copy Link",
label = "Copy link",
copiedLabel = "Copied",
ariaLabel,
preserveParams,
Expand Down
6 changes: 3 additions & 3 deletions src/features/map/MapSettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function BorderLegend() {
className="inline-block h-2.5 w-4 rounded-sm border"
style={{ borderColor: "var(--palette-secondary)", backgroundColor: "var(--palette-secondary)", opacity: 0.5 }}
/>
IATA region outline
Area outline
</div>
);
}
Expand Down Expand Up @@ -155,9 +155,9 @@ export function MapSettingsPanel({
/>
{neighborLines === "selected" && <NeighborLegend />}
</Section>
<Section title="IATA Borders">
<Section title="Area Borders">
<SegmentedControl
ariaLabel="IATA borders"
ariaLabel="Area borders"
options={BORDER_OPTIONS}
value={borders ? "on" : "off"}
onChange={(v) => onBordersChange(v === "on")}
Expand Down
2 changes: 1 addition & 1 deletion src/features/nodes/ForeignNodeBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Tooltip } from "../../components/Tooltip";
export function ForeignNodeBadge({ possiblyForeign }: { possiblyForeign?: boolean }) {
if (possiblyForeign !== true) return null;
return (
<Tooltip label="Reported position is outside this server's configured local borders.">
<Tooltip label="This node's position is outside this server's local area.">
<Badge variant="default">Possibly foreign</Badge>
</Tooltip>
);
Expand Down
8 changes: 4 additions & 4 deletions src/features/nodes/NodeDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ export function NodeDetailPanel({ nodeId, onClose, onViewObserver, onViewNode, o
<Section title="Capabilities">
<div className="flex flex-col gap-0.5 font-mono text-[13px]">
{node.minFirmwareVersion && <Field label="Min firmware" value={node.minFirmwareVersion} />}
<Field label="Multibyte paths" value={node.supportsMultibytePaths ? "yes" : "no"} />
<Field label="Multibyte traces" value={node.supportsMultibyteTraces ? "yes" : "no"} />
<Field label="Multi-byte paths" value={node.supportsMultibytePaths ? "yes" : "no"} />
<Field label="Multi-byte traces" value={node.supportsMultibyteTraces ? "yes" : "no"} />
{node.radio && <Field label="Radio" value={formatRadio(node.radio) ?? "—"} />}
{node.defaultScope && <Field label="Scope" value={node.defaultScope} />}
</div>
Expand Down Expand Up @@ -202,7 +202,7 @@ export function NodeDetailPanel({ nodeId, onClose, onViewObserver, onViewNode, o
)}
</Section>

<Section title="Observations">
<Section title="Recent packets">
{observations && observations.items.length > 0 ? (
<div className="flex flex-col gap-1.5">
{observations.items.map((obs) => (
Expand All @@ -214,7 +214,7 @@ export function NodeDetailPanel({ nodeId, onClose, onViewObserver, onViewNode, o
))}
</div>
) : (
<div className="font-mono text-[13px] text-text-dim">No recent observations</div>
<div className="font-mono text-[13px] text-text-dim">No recent packets</div>
)}
</Section>
</>
Expand Down
4 changes: 2 additions & 2 deletions src/features/nodes/NodeFilterBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ export function NodeFilterBar({
const controls = (fullWidth: boolean) => (
<>
<SelectDropdown label="Type" options={NODE_TYPE_OPTIONS} value={typeFilter} onChange={onTypeChange} fullWidth={fullWidth} />
<SelectDropdown label="Multibyte paths" options={MULTIBYTE_OPTIONS} allLabel="Any" value={pathsFilter} onChange={(v) => onPathsChange(v as MultibyteFilter)} fullWidth={fullWidth} />
<SelectDropdown label="Multibyte traces" options={MULTIBYTE_OPTIONS} allLabel="Any" value={tracesFilter} onChange={(v) => onTracesChange(v as MultibyteFilter)} fullWidth={fullWidth} />
<SelectDropdown label="Multi-byte paths" options={MULTIBYTE_OPTIONS} allLabel="Any" value={pathsFilter} onChange={(v) => onPathsChange(v as MultibyteFilter)} fullWidth={fullWidth} />
<SelectDropdown label="Multi-byte traces" options={MULTIBYTE_OPTIONS} allLabel="Any" value={tracesFilter} onChange={(v) => onTracesChange(v as MultibyteFilter)} fullWidth={fullWidth} />
{scopeOptions.length > 0 && (
<SelectDropdown label="Scope" options={scopeOptions.map((s) => ({ value: s, label: s }))} allLabel="Any" value={scopeFilter} onChange={onScopeChange} fullWidth={fullWidth} />
)}
Expand Down
2 changes: 1 addition & 1 deletion src/features/nodes/NodeTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ const COLUMNS: Column<NodeSummary>[] = [
cell: (node) => formatRadio(node.radio) ?? "—",
},
{
header: "IATAs",
header: "Areas",
cell: (node) =>
node.iatas && node.iatas.length > 0 ? (
<div className="flex flex-wrap gap-1">
Expand Down
12 changes: 7 additions & 5 deletions src/features/observers/ObserverComparison.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ export function ObserverComparison({ observerA, activityA, range, observerBId, u
const intervalMs = intervalToMs(activityParamsFor(range).interval)!;
const end = until == null ? 0 : Math.floor(until / intervalMs) * intervalMs;
const start = end - RANGE_MS[range];
// raw packets only outlive 24h/7d windows; at 30d the overlap would silently cover a few days
const overlapRange = range !== "30d";
const aligned = [activityA, dataB].every(data => data?.summary && data.windowStart === start && data.windowEnd === end && intervalToMs(data.interval) === intervalMs);
const overlap = useQuery({
queryKey: ["observer-dashboard-overlap", observerA.id, observerBId, start, end],
queryFn: ({ signal }) => getObserverComparison(undefined, { observerA: observerA.id, observerB: observerBId, since: start, until: end }, signal),
enabled: valid && aligned && !!b.data && !b.isError,
enabled: overlapRange && valid && aligned && !!b.data && !b.isError,
staleTime: 30_000, refetchInterval: false, refetchOnWindowFocus: false, retry: false,
});
const option = useMemo(() => {
Expand Down Expand Up @@ -65,13 +67,13 @@ export function ObserverComparison({ observerA, activityA, range, observerBId, u
void b.refetch();
if (nextUntil === until) {
void activityB.refetch();
if (aligned) void overlap.refetch();
if (aligned && overlapRange) void overlap.refetch();
}
}
};
const rows = aligned && b.data ? [
[t("observerPage.records"), activityA!.summary!.recordedPackets.toLocaleString(i18n.resolvedLanguage), dataB!.summary!.recordedPackets.toLocaleString(i18n.resolvedLanguage)],
[t("observerPage.lastHour"), `${activityA!.summary!.lastCompleteHour.toLocaleString(i18n.resolvedLanguage)} (${date(activityA!.summary!.lastCompleteHourStart)})`, `${dataB!.summary!.lastCompleteHour.toLocaleString(i18n.resolvedLanguage)} (${date(dataB!.summary!.lastCompleteHourStart)})`],
[t("observerPage.packetsLastHour"), `${activityA!.summary!.lastCompleteHour.toLocaleString(i18n.resolvedLanguage)} (${date(activityA!.summary!.lastCompleteHourStart)})`, `${dataB!.summary!.lastCompleteHour.toLocaleString(i18n.resolvedLanguage)} (${date(dataB!.summary!.lastCompleteHourStart)})`],
[t("observerPage.lastPacket"), date(activityA!.summary!.latestRecordedAt), date(dataB!.summary!.latestRecordedAt)],
[t("observerPage.battery"), observerA.batteryLevel == null ? "—" : formatBattery(observerA.batteryLevel), b.data.batteryLevel == null ? "—" : formatBattery(b.data.batteryLevel)],
[t("observerPage.uptime"), observerA.uptimeSeconds == null ? "—" : formatUptime(observerA.uptimeSeconds), b.data.uptimeSeconds == null ? "—" : formatUptime(b.data.uptimeSeconds)],
Expand All @@ -86,13 +88,13 @@ export function ObserverComparison({ observerA, activityA, range, observerBId, u
<p className="flex flex-wrap items-center gap-2 text-xs text-text-muted">{t("observerPage.window")}: {date(start)} – {date(end)}<InfoTip text={t("observerCompare.currentNote")} /></p>
<div className="overflow-x-auto"><table aria-label={t("observerCompare.metrics")} className="w-full text-left text-sm tabular-nums"><thead><tr><th className="p-2">{t("observerCompare.metric")}</th><th className="p-2">A</th><th className="p-2">B</th></tr></thead><tbody>{rows.map(([label, a, bValue]) => <tr className="border-t border-border" key={label}><th scope="row" className="p-2 font-normal text-text-muted">{label}</th><td className="p-2">{a}</td><td className="p-2">{bValue}</td></tr>)}</tbody></table></div>
<ChartCard title={t("observerCompare.activity")} option={option} height={240} />
<Card title={t("observerCompare.overlap")} right={<InfoTip text={[t("observerCompare.retention"), t("observerCompare.definition")]} />}>
{!overlapRange ? <p className="text-xs text-text-muted">{t("observerCompare.overlapShortRanges")}</p> : <Card title={t("observerCompare.overlap")} right={<InfoTip text={[t("observerCompare.retention"), t("observerCompare.definition")]} />}>
{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>
</>}
</Card>
</Card>}
</>}
</section>;
}
2 changes: 1 addition & 1 deletion src/features/observers/ObserverSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function ObserverSummary({ observer, activity, points, pending = false, a
{ key: "uptime", label: t("observerPage.uptime"), value: observer.uptimeSeconds != null ? formatUptime(observer.uptimeSeconds) : "—" },
{
key: "noise",
label: t("observerPage.lastNoise"),
label: t("observerPage.noise"),
value: noise != null && Number.isFinite(noise) ? `${noise.toLocaleString(i18n.resolvedLanguage, { maximumFractionDigits: 1 })} dBm` : "—",
},
];
Expand Down
2 changes: 1 addition & 1 deletion src/features/packets/ObservationTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function ObservationTable({ observations, selectedId, onSelect }: Props)
<thead>
<tr className="text-text-dim uppercase tracking-wider text-[9px]">
<th className="text-left font-medium py-1 px-1.5">Observer</th>
<th className="text-left font-medium py-1 px-1.5">IATA</th>
<th className="text-left font-medium py-1 px-1.5">Area</th>
<th className="text-left font-medium py-1 px-1.5">Heard</th>
<th className="text-left font-medium py-1 px-1.5">SNR</th>
<th className="text-left font-medium py-1 px-1.5">RSSI</th>
Expand Down
4 changes: 2 additions & 2 deletions src/features/packets/PacketAnalyzerDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ export function PacketAnalyzerDrawer({ detail, selectedObservationId, onClose, o
</Badge>
{detail.scope && <ScopeTag boxed>{detail.scope}</ScopeTag>}
<Tooltip
label={t("investigation.reportedBy", { count: observerCount })}
label={t("investigation.heardBy", { count: observerCount })}
className="ml-auto"
>
<span
className="font-mono text-[13px] text-primary font-semibold bg-primary/6 px-1.5 rounded-sm"
aria-label={t("investigation.reportedBy", { count: observerCount })}
aria-label={t("investigation.heardBy", { count: observerCount })}
>
×{observerCount}
</span>
Expand Down
2 changes: 1 addition & 1 deletion src/features/packets/PacketExpansion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export function PacketExpansion({ packet, onOpenAnalyzer, onViewPath, selectedOb
title={hasPath ? undefined : "No resolved path to map"}
className={ACTION_BUTTON_CLASS}
>
View path on map
Map all paths
</button>
</div>

Expand Down
4 changes: 2 additions & 2 deletions src/features/packets/PacketInvestigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ export function PacketInvestigation({ detail, selectedId, onSelect, onViewObserv
</button></h2>
<InfoTip text={[t(isTrace ? "investigation.traceNote" : "investigation.pathNote"), t("investigation.retention")]} />
</div>
<p className="font-mono text-[11px] text-text-muted">{t("investigation.reports", { count: detail.observations.length })} · {t("investigation.observers", { count: observerCount })}</p>
<p className="font-mono text-[11px] text-text-muted">{detail.observations.length === observerCount ? t("investigation.heardBy", { count: observerCount }) : `${t("investigation.heardTimes", { count: detail.observations.length })} ${t("investigation.byObservers", { count: observerCount })}`}</p>
{groups.length === 0 ? <p role="status" className="text-sm text-text-muted">{t("investigation.empty")}</p> : open && visible.map(group => <details key={group.key} open={group.reports.some(o => o.id === selectedId)} className="group/path">
<summary className="flex cursor-pointer list-none items-center gap-2 rounded px-1 py-1.5 font-mono text-xs hover:bg-text-normal/3 [&::-webkit-details-marker]:hidden">
<span aria-hidden className="text-[9px] text-text-dim transition-transform group-open/path:rotate-90">▶</span>
<span className="shrink-0 text-text-normal">{t(group.kind === "empty" ? "investigation.noEntries" : group.kind === "unavailable" ? "investigation.unavailablePath" : isTrace ? "investigation.tracePath" : "investigation.path", { number: groups.indexOf(group) + 1 })}</span>
{group.hashes.length > 0 && <code className="min-w-0 truncate text-primary">{group.hashes.join(" → ")}</code>}
<span className="ml-auto shrink-0 rounded-sm bg-text-normal/5 px-1.5 text-[11px] text-text-muted" aria-label={t("investigation.reports", { count: group.reports.length })}>×{group.reports.length}</span>
<span className="ml-auto shrink-0 rounded-sm bg-text-normal/5 px-1.5 text-[11px] text-text-muted" aria-label={t("investigation.observations", { count: group.reports.length })}>×{group.reports.length}</span>
</summary>
<ul className="mt-1 mb-2 flex flex-col gap-1.5">{group.reports.map(report => {
const key = isTrace ? "trace" : report.observerId;
Expand Down
2 changes: 1 addition & 1 deletion src/features/packets/PacketTableHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function PacketTableHeader() {
<span>Hops</span>
<span>Hash Size</span>
<span className="truncate" title="Summary and source / destination">Summary / Src → Dst</span>
<span>IATA</span>
<span>Area</span>
<span className="text-right">Age</span>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions src/features/routes/RouteDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export function RouteDetailPanel({ route: listed, iata, pathKey, onClose, onAnal
<div className="flex items-center gap-3 font-mono text-[13px]">
<Badge variant="default">{route.iata}</Badge>
<Field label="Hops" value={route.hopCount} />
<Field label="Obs" value={route.observationCount.toLocaleString()} />
<Field label="Heard" value={route.observationCount.toLocaleString()} />
</div>
</Section>

Expand Down Expand Up @@ -109,7 +109,7 @@ export function RouteDetailPanel({ route: listed, iata, pathKey, onClose, onAnal
<p className="break-words text-sm text-text-bright">{report.observerName ?? report.observerId.slice(0, 8)}</p>
<p className="text-xs text-text-normal"><Timestamp value={report.heardAt} ms /> · SNR <span className={level ? SIGNAL_LEVEL_CLASSES[level] : ""}>{formatSnr(report.snr)}</span>{report.snr != null && " dB"} · RSSI {report.rssi == null ? "—" : `${report.rssi} dBm`}</p>
<div className="flex flex-wrap gap-2">
{onAnalyzePacket && <button className={ACTION_BUTTON_CLASS} onClick={() => onAnalyzePacket(report.packetHash, report.id)}>{t("investigation.inspect")}</button>}
{onAnalyzePacket && <button className={ACTION_BUTTON_CLASS} onClick={() => onAnalyzePacket(report.packetHash, report.id)}>{t("routeEvidence.inspect")}</button>}
{onViewObserver && <button className={ACTION_BUTTON_CLASS} onClick={() => onViewObserver(report.observerId)}>{t("investigation.observer")}</button>}
</div>
</li>
Expand Down
6 changes: 3 additions & 3 deletions src/features/routes/RouteTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function CrossRouteCard({ route }: { route: CrossIATARoute }) {

const COLUMNS: Column<KnownRoute>[] = [
{
header: "IATA",
header: "Area",
sortValue: (r) => r.iata,
cell: (r) => <Badge variant="default">{r.iata}</Badge>,
},
Expand Down Expand Up @@ -302,7 +302,7 @@ export function RouteTable(actions: RouteActions) {
<div className="flex items-center gap-1.5">
<div className="flex-1 min-w-0 md:flex-none">
<MultiSelectDropdown
label="IATA"
label="Areas"
options={iataOptions}
selected={searchIatas}
onChange={setSearchIatas}
Expand Down Expand Up @@ -338,7 +338,7 @@ export function RouteTable(actions: RouteActions) {
) : crossRoutes && crossRoutes.length > 0 ? (
crossRoutes.map((r, i) => <CrossRouteCard key={i} route={r} />)
) : (
<div className="font-mono text-[13px] text-text-dim">No cross-IATA routes</div>
<div className="font-mono text-[13px] text-text-dim">No routes between these areas</div>
)}
</div>
) : (
Expand Down
2 changes: 1 addition & 1 deletion src/features/stats/ClockDriftTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function ClockDriftTab() {
},
{
id: "iatas",
header: "IATAs",
header: t("clockDrift.areas"),
cell: (e) => (
<div className="flex flex-wrap gap-1">
{(e.iatas ?? []).map((i) => (
Expand Down
Loading
Loading