mirror of
https://git.quad4.io/RNS-Things/MeshChatX.git
synced 2026-05-01 14:27:09 +00:00
feat(rnstatus): update RNStatusHandler with new formatting functions and interface discovery integration
This commit is contained in:
@@ -3,6 +3,7 @@ import time
|
||||
from typing import Any
|
||||
|
||||
import RNS
|
||||
from RNS.Discovery import InterfaceDiscovery
|
||||
|
||||
|
||||
def size_str(num, suffix="B"):
|
||||
@@ -24,6 +25,63 @@ def size_str(num, suffix="B"):
|
||||
return f"{num:.2f}{last_unit}{suffix}"
|
||||
|
||||
|
||||
def fmt_per_second(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
x = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return str(value)
|
||||
ax = abs(x)
|
||||
if ax == 0:
|
||||
return "0"
|
||||
if ax >= 100:
|
||||
return f"{x:.1f}"
|
||||
if ax >= 1:
|
||||
return f"{x:.2f}"
|
||||
return f"{x:.3g}"
|
||||
|
||||
|
||||
def fmt_packet_count(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
x = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return str(value)
|
||||
return f"{int(round(x)):,}"
|
||||
|
||||
|
||||
def fmt_percentage(value: Any) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
x = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return str(value)
|
||||
ax = abs(x)
|
||||
if ax >= 100:
|
||||
return f"{x:.1f}"
|
||||
if ax >= 10:
|
||||
return f"{x:.2f}"
|
||||
return f"{x:.3g}"
|
||||
|
||||
|
||||
def stat_name_matches_discovered(stat_name: str, discovered_list: list) -> bool:
|
||||
if not stat_name or not discovered_list:
|
||||
return False
|
||||
for d in discovered_list:
|
||||
if not isinstance(d, dict):
|
||||
continue
|
||||
ro = d.get("reachable_on")
|
||||
if ro and str(ro) in stat_name:
|
||||
return True
|
||||
dn = d.get("name")
|
||||
if dn and str(dn) and str(dn) in stat_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class RNStatusHandler:
|
||||
def __init__(self, reticulum_instance):
|
||||
self.reticulum = reticulum_instance
|
||||
@@ -56,6 +114,12 @@ class RNStatusHandler:
|
||||
"link_count": link_count,
|
||||
}
|
||||
|
||||
discovered_list: list = []
|
||||
with contextlib.suppress(Exception):
|
||||
discovered_list = InterfaceDiscovery(
|
||||
discover_interfaces=False,
|
||||
).list_discovered_interfaces()
|
||||
|
||||
blackhole_enabled = False
|
||||
blackhole_sources = []
|
||||
blackhole_count = 0
|
||||
@@ -139,6 +203,7 @@ class RNStatusHandler:
|
||||
formatted_if: dict[str, Any] = {
|
||||
"name": name,
|
||||
"status": "Up" if ifstat.get("status") else "Down",
|
||||
"discovered": stat_name_matches_discovered(name, discovered_list),
|
||||
}
|
||||
|
||||
mode = ifstat.get("mode")
|
||||
@@ -165,9 +230,9 @@ class RNStatusHandler:
|
||||
formatted_if["tx_bytes"] = ifstat["txb"]
|
||||
formatted_if["tx_bytes_str"] = size_str(ifstat["txb"])
|
||||
if "rxs" in ifstat:
|
||||
formatted_if["rx_packets"] = ifstat["rxs"]
|
||||
formatted_if["rx_packets"] = fmt_packet_count(ifstat["rxs"])
|
||||
if "txs" in ifstat:
|
||||
formatted_if["tx_packets"] = ifstat["txs"]
|
||||
formatted_if["tx_packets"] = fmt_packet_count(ifstat["txs"])
|
||||
|
||||
if "clients" in ifstat and ifstat["clients"] is not None:
|
||||
formatted_if["clients"] = ifstat["clients"]
|
||||
@@ -194,29 +259,29 @@ class RNStatusHandler:
|
||||
|
||||
if "airtime_short" in ifstat and "airtime_long" in ifstat:
|
||||
formatted_if["airtime"] = {
|
||||
"short": ifstat["airtime_short"],
|
||||
"long": ifstat["airtime_long"],
|
||||
"short": fmt_percentage(ifstat["airtime_short"]),
|
||||
"long": fmt_percentage(ifstat["airtime_long"]),
|
||||
}
|
||||
|
||||
if "channel_load_short" in ifstat and "channel_load_long" in ifstat:
|
||||
formatted_if["channel_load"] = {
|
||||
"short": ifstat["channel_load_short"],
|
||||
"long": ifstat["channel_load_long"],
|
||||
"short": fmt_percentage(ifstat["channel_load_short"]),
|
||||
"long": fmt_percentage(ifstat["channel_load_long"]),
|
||||
}
|
||||
|
||||
if "peers" in ifstat and ifstat["peers"] is not None:
|
||||
formatted_if["peers"] = ifstat["peers"]
|
||||
|
||||
if "incoming_announce_frequency" in ifstat:
|
||||
formatted_if["incoming_announce_frequency"] = ifstat[
|
||||
"incoming_announce_frequency"
|
||||
]
|
||||
formatted_if["incoming_announce_frequency"] = fmt_per_second(
|
||||
ifstat["incoming_announce_frequency"]
|
||||
)
|
||||
if "outgoing_announce_frequency" in ifstat:
|
||||
formatted_if["outgoing_announce_frequency"] = ifstat[
|
||||
"outgoing_announce_frequency"
|
||||
]
|
||||
formatted_if["outgoing_announce_frequency"] = fmt_per_second(
|
||||
ifstat["outgoing_announce_frequency"]
|
||||
)
|
||||
if "held_announces" in ifstat:
|
||||
formatted_if["held_announces"] = ifstat["held_announces"]
|
||||
formatted_if["held_announces"] = fmt_packet_count(ifstat["held_announces"])
|
||||
|
||||
if "ifac_netname" in ifstat and ifstat["ifac_netname"] is not None:
|
||||
formatted_if["network_name"] = ifstat["ifac_netname"]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
>
|
||||
<div class="flex-1 overflow-y-auto w-full px-4 md:px-8 py-6">
|
||||
<div class="space-y-4 w-full max-w-6xl mx-auto">
|
||||
<div class="glass-card space-y-5">
|
||||
<div class="glass-card space-y-5 rounded-2xl border border-slate-200/70 p-5 dark:border-zinc-700/80">
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Network Diagnostics
|
||||
@@ -17,27 +17,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="primary-chip px-4 py-2 text-sm"
|
||||
class="primary-chip inline-flex items-center gap-2 px-4 py-2 text-sm"
|
||||
:disabled="isLoading"
|
||||
@click="refreshStatus"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
icon-name="refresh"
|
||||
class="w-4 h-4"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:class="{ 'animate-spin-reverse': isLoading }"
|
||||
/>
|
||||
Refresh
|
||||
</button>
|
||||
<label class="flex items-center gap-2 cursor-pointer secondary-chip px-4 py-2 text-sm">
|
||||
<label
|
||||
class="secondary-chip inline-flex cursor-pointer items-center gap-2 px-4 py-2 text-sm"
|
||||
>
|
||||
<input v-model="includeLinkStats" type="checkbox" class="rounded" />
|
||||
<span>Include Link Stats</span>
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-700 dark:text-gray-300">Sort by:</label>
|
||||
<select v-model="sorting" class="input-field text-sm">
|
||||
<div class="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<label class="shrink-0 text-sm text-gray-700 dark:text-gray-300">Sort by:</label>
|
||||
<select v-model="sorting" class="input-field min-w-[10rem] text-sm">
|
||||
<option value="">None</option>
|
||||
<option value="bitrate">Bitrate</option>
|
||||
<option value="rx">RX Bytes</option>
|
||||
@@ -48,27 +50,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div
|
||||
v-if="linkCount !== null"
|
||||
class="p-3 rounded-lg bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-300"
|
||||
class="rounded-xl border border-blue-200/80 bg-blue-50/90 p-4 text-blue-800 dark:border-blue-800/50 dark:bg-blue-950/30 dark:text-blue-200"
|
||||
>
|
||||
<div class="font-semibold">Active Links: {{ linkCount }}</div>
|
||||
<div class="text-sm font-semibold">
|
||||
Active Links: {{ formatInt(linkCount) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="blackholeEnabled !== null"
|
||||
class="p-3 rounded-lg bg-purple-50 dark:bg-purple-900/20 text-purple-700 dark:text-purple-300"
|
||||
class="rounded-xl border border-purple-200/80 bg-purple-50/90 p-4 text-purple-900 dark:border-purple-800/50 dark:bg-purple-950/30 dark:text-purple-100"
|
||||
>
|
||||
<div class="font-semibold flex justify-between items-center">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 font-semibold">
|
||||
<span>Blackhole: {{ blackholeEnabled ? "Publishing" : "Active" }}</span>
|
||||
<span class="text-sm opacity-80"> {{ blackholeCount }} Identities </span>
|
||||
<span class="text-sm font-normal opacity-90">
|
||||
{{ formatInt(blackholeCount) }} Identities
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="blackholeSources.length > 0" class="glass-card space-y-3">
|
||||
<div
|
||||
v-if="blackholeSources.length > 0"
|
||||
class="glass-card space-y-3 rounded-2xl border border-slate-200/70 p-5 dark:border-zinc-700/80"
|
||||
>
|
||||
<div class="font-semibold text-lg text-gray-900 dark:text-white">Blackhole Sources</div>
|
||||
<div class="grid gap-2">
|
||||
<div
|
||||
@@ -88,22 +97,42 @@
|
||||
No interfaces found. Click refresh to load status.
|
||||
</div>
|
||||
|
||||
<div v-for="iface in interfaces" :key="iface.name" class="glass-card space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-semibold text-lg text-gray-900 dark:text-white">{{ iface.name }}</div>
|
||||
<div
|
||||
v-for="iface in interfaces"
|
||||
:key="iface.name"
|
||||
class="glass-card overflow-hidden rounded-2xl border border-slate-200/70 dark:border-zinc-700/80"
|
||||
>
|
||||
<div
|
||||
class="flex flex-wrap items-start justify-between gap-3 border-b border-slate-100 bg-slate-50/60 px-4 py-4 dark:border-zinc-800/80 dark:bg-zinc-900/40 sm:px-5"
|
||||
>
|
||||
<div class="min-w-0 flex-1 space-y-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h3
|
||||
class="break-words text-base font-semibold leading-snug text-gray-900 dark:text-white sm:text-lg"
|
||||
>
|
||||
{{ iface.name }}
|
||||
</h3>
|
||||
<span
|
||||
v-if="iface.discovered"
|
||||
class="inline-flex shrink-0 items-center rounded-md bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-900 dark:bg-amber-900/45 dark:text-amber-100"
|
||||
>
|
||||
Discovered
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
:class="[
|
||||
iface.status === 'Up'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-200'
|
||||
: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-200',
|
||||
'rounded-full px-3 py-1 text-xs font-semibold',
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900/45 dark:text-green-100'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-900/45 dark:text-red-100',
|
||||
'shrink-0 rounded-full px-3 py-1 text-xs font-semibold',
|
||||
]"
|
||||
>
|
||||
{{ iface.status }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
|
||||
<div class="grid gap-x-6 gap-y-4 p-4 text-sm sm:p-5 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div v-if="iface.mode">
|
||||
<div class="text-gray-500 dark:text-gray-400">Mode</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.mode }}</div>
|
||||
@@ -122,19 +151,25 @@
|
||||
</div>
|
||||
<div v-if="iface.rx_packets !== undefined">
|
||||
<div class="text-gray-500 dark:text-gray-400">RX Packets</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.rx_packets }}</div>
|
||||
<div class="font-semibold tabular-nums text-gray-900 dark:text-white">
|
||||
{{ iface.rx_packets }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.tx_packets !== undefined">
|
||||
<div class="text-gray-500 dark:text-gray-400">TX Packets</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.tx_packets }}</div>
|
||||
<div class="font-semibold tabular-nums text-gray-900 dark:text-white">
|
||||
{{ iface.tx_packets }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.clients !== undefined">
|
||||
<div class="text-gray-500 dark:text-gray-400">Clients</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.clients }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ formatInt(iface.clients) }}</div>
|
||||
</div>
|
||||
<div v-if="iface.peers !== undefined">
|
||||
<div class="text-gray-500 dark:text-gray-400">Peers</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.peers }} reachable</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ formatInt(iface.peers) }} reachable
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.noise_floor">
|
||||
<div class="text-gray-500 dark:text-gray-400">Noise Floor</div>
|
||||
@@ -159,7 +194,7 @@
|
||||
<div v-if="iface.battery_percent !== undefined">
|
||||
<div class="text-gray-500 dark:text-gray-400">Battery</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ iface.battery_percent }}%<span v-if="iface.battery_state">
|
||||
{{ formatInt(iface.battery_percent) }}%<span v-if="iface.battery_state">
|
||||
({{ iface.battery_state }})</span
|
||||
>
|
||||
</div>
|
||||
@@ -170,13 +205,13 @@
|
||||
</div>
|
||||
<div v-if="iface.incoming_announce_frequency !== undefined">
|
||||
<div class="text-gray-500 dark:text-gray-400">Incoming Announces</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">
|
||||
<div class="font-semibold tabular-nums text-gray-900 dark:text-white">
|
||||
{{ iface.incoming_announce_frequency }}/s
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.outgoing_announce_frequency !== undefined">
|
||||
<div class="text-gray-500 dark:text-gray-400">Outgoing Announces</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">
|
||||
<div class="font-semibold tabular-nums text-gray-900 dark:text-white">
|
||||
{{ iface.outgoing_announce_frequency }}/s
|
||||
</div>
|
||||
</div>
|
||||
@@ -231,6 +266,16 @@ export default {
|
||||
this.refreshStatus();
|
||||
},
|
||||
methods: {
|
||||
formatInt(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return "";
|
||||
}
|
||||
const n = Number(value);
|
||||
if (Number.isNaN(n)) {
|
||||
return String(value);
|
||||
}
|
||||
return n.toLocaleString();
|
||||
},
|
||||
async refreshStatus() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
|
||||
@@ -19,6 +19,7 @@ describe("RNStatusPage.vue", () => {
|
||||
{
|
||||
name: "Interface 1",
|
||||
status: "Up",
|
||||
discovered: true,
|
||||
bitrate: "100 bps",
|
||||
rx_bytes_str: "10 B",
|
||||
tx_bytes_str: "5 B",
|
||||
@@ -62,6 +63,7 @@ describe("RNStatusPage.vue", () => {
|
||||
|
||||
expect(wrapper.text()).toContain("RNStatus - Network Status");
|
||||
expect(wrapper.text()).toContain("Interface 1");
|
||||
expect(wrapper.text()).toContain("Discovered");
|
||||
expect(wrapper.text()).toContain("Active Links: 5");
|
||||
expect(wrapper.text()).toContain("Blackhole: Publishing");
|
||||
expect(wrapper.text()).toContain("src1");
|
||||
|
||||
Reference in New Issue
Block a user